Når vi fortsætter den igangværende række artikler om brugerdefinerede implementationer af Windows-konsolværktøjer, kan vi ikke undgå at berøre TFTP (Trivial File Transfer Protocol) — et enkelt protokol til filoverførsel.
Som sidst, lad os kort gennemgå teorien, se på koden, der implementerer funktionalitet svarende til det ønskede, og analysere det. Mere information — under overskriften.
Jeg vil ikke kopiere den referenceinformation, som traditionelt findes i slutningen af artiklen; jeg vil blot sige, at TFTP i sin essens er en forenklet variation af FTP-protokollen, hvor tilpasningen af adgangskontrol er fjernet, og der i bund og grund ikke er andet end kommandoer til at modtage og overføre filer. Men for at gøre vores implementering lidt mere elegant og tilpasset nutidens programmeringsprincipper, er syntaksen blevet lidt ændret — det ændrer ikke principperne for funktionalitet, men grænsefladen bliver, IMHO, lidt mere logisk og kombinerer de positive aspekter af både FTP og TFTP.
Specifikt anmoder klienten ved opstart om IP-adressen til serveren og porten, hvor den brugerdefinerede TFTP er åben (på grund af inkompatibilitet med den standardprotokol, anså jeg det for passende at lade brugeren vælge porten), hvorefter forbindelsen oprettes, og klienten kan sende en af kommandoerne — get eller put, for at modtage eller sende en fil til serveren. Alle filer sendes i binær tilstand — for at forenkle logikken.
Til implementeringen af protokollen har jeg traditionelt brugt 4 klasser:
- TFTPClient
- TFTPServer
- TFTPClientTester
- TFTPServerTester
Da testklasserne kun eksisterer til debug af de primære, vil jeg ikke gennemgå dem, men koden vil være tilgængelig i et repository, og linket til det kan findes i slutningen af artiklen. Jeg vil nu tage fat på de primære klasser.
TFTPClient
Formålet med denne klasse er at oprette forbindelse til en fjernserver via dens IP-adresse og portnummer, læse fra inputstrømmen (i dette tilfælde — tastaturet) en kommando, analysere den, sende den til serveren, og afhængig af om filoverførsel eller modtagelse er nødvendig, overføre den eller modtage den.
Koden, der starter klienten, så den kan oprette forbindelse til serveren og vente på en kommando fra inputstrømmen, ser således ud. En række globale variabler, der bruges her, beskrives uden for artiklen, i den fulde tekst af programmet. På grund af deres trivielle karakter nævner jeg dem ikke for at undgå at overbelaste artiklen.
public void run(String ip, int port)
{
this.ip = ip;
this.port = port;
try {
inicialization();
Scanner keyboard = new Scanner(System.in);
while (isRunning) {
getAndParseInput(keyboard);
sendCommand();
selector();
}
}
catch (Exception e) {
System.out.println(e.getMessage());
}
}Lad os gennemgå de metoder, der kaldes i denne kodeblok:
Her sker filoverførslen — ved hjælp af scanneren repræsenterer vi indholdet af filen som en byte-array, som vi sekventielt skriver til socketen, hvorefter vi lukker den og åbner den igen (ikke den mest oplagte løsning, men det sikrer lettelse af ressourcer), hvorefter vi viser en besked på skærmen om den vellykkede overførsel.
private void put(String sourcePath, String destPath)
{
File src = new File(sourcePath);
try {
InputStream scanner = new FileInputStream(src);
byte[] bytes = scanner.readAllBytes();
for (byte b : bytes)
sout.write(b);
sout.close();
inicialization();
System.out.println("nDonen");
}
catch (Exception e) {
System.out.println(e.getMessage());
}
}Denne kodefragment beskriver, hvordan data modtages fra serveren. Alt er igen trivielt, men det interessante er kun den første kodeblok. For at kunne forstå, hvor mange bytes der skal læses fra socketen, skal man vide, hvor meget den overførte fil vejer. Filstørrelsen på serveren repræsenteres som et langt heltal, så her accepteres 4 bytes, som derefter konverteres til ét tal. Det er ikke en særlig Java-agtig tilgang, det er mere ligesom i C, men det opfylder sit formål.
Dernæst er alt trivielt — vi modtager et kendt antal bytes fra socketen og skriver dem til en fil, hvorefter vi viser en besked om succes.
private void get(String sourcePath, String destPath){
long sizeOfFile = 0;
try {
byte[] sizeBytes = new byte[Long.SIZE];
for (int i =0; i < Long.SIZE/Byte.SIZE; i++)
{
sizeBytes[i] = (byte)sin.read();
sizeOfFile*=256;
sizeOfFile+=sizeBytes[i];
}
FileOutputStream writer = new FileOutputStream(new File(destPath));
for (int i =0; i < sizeOfFile; i++)
{
writer.write(sin.read());
}
writer.close();
System.out.println("nDONEn");
}
catch (Exception e){
System.out.println(e.getMessage());
}
}Hvis der er indtastet en kommando i klientvinduet, som er forskellig fra get eller put, vil funktionen showErrorMessage blive kaldt for at vise, at input er ugyldigt. Dette er så trivielt, at jeg undlader det. Mere interessant er funktionen til at modtage og opdele inputstrengen. Her overfører vi en scanner, som vi forventer at få en streng fra, adskilt med to mellemrum, og som indeholder en kommando, kildeadresse og destinationsadresse.
private void getAndParseInput(Scanner scanner)
{
try {
input = scanner.nextLine().split(" ");
typeOfCommand = input[0];
sourcePath = input[1];
destPath = input[2];
}
catch (Exception e) {
System.out.println("Ugyldigt input");
}
}Afsendelse af kommando — overførsel af den indtastede kommando fra scanneren til soklen og tvungen afsendelse af den.
private void sendCommand()
{
try {
for (String str : input) {
for (char ch : str.toCharArray()) {
sout.write(ch);
}
sout.write(' ');
}
sout.write('n');
}
catch (Exception e) {
System.out.print(e.getMessage());
}
}Vælgeren — en funktion, der bestemmer programmets handlinger afhængigt af den indtastede streng. Her er det ikke særlig pænt, og der bruges en ikke særlig god tilgang ved at tvinge udgang uden for kodeblokken, men hovedårsagen hertil er fraværet af visse elementer i Java, som delegater i C#, funktionspegere fra C++ eller i det mindste det skrækkelige goto, som ville tillade en mere elegant implementering. Hvis du ved, hvordan man kan gøre koden lidt mere elegant, ser jeg frem til kritik i kommentarerne. Jeg mener, der er behov for et ordforråd af typen String-delegate, men der er ingen delegeret...
private void selector()
{
do{
if (typeOfCommand.equals("get")){
get(sourcePath, destPath);
break;
}
if (typeOfCommand.equals("put")){
put(sourcePath, destPath);
break;
}
showErrorMessage();
}
while (false);
}
}TFTPServer
Serverens funktionalitet adskiller sig fra klientens funktionalitet ved stort set kun det, at kommandoerne kommer til den fra en sokkel i stedet for fra et tastatur. Nogle metoder stemmer overens, derfor vil jeg ikke medtage dem, men kun nævne forskellene.
Til at starte her bruges metoden run, som modtager en port og behandler inputdata fra soklen i et evighedsløkke.
public void run(int port) {
this.port = port;
incialization();
while (true) {
getAndParseInput();
selector();
}
}Metoden put, som er en wrapper til metoden writeToFileFromSocket, åbner en skrive-strøm til en fil og skriver alle bytes fra input-socketten til filen; når skrivningen er afsluttet, viser den en meddelelse om succesfuld overførsel.
private void put(String source, String dest){
writeToFileFromSocket();
System.out.print("nDonen");
};
private void writeToFileFromSocket()
{
try {
FileOutputStream writer = new FileOutputStream(new File(destPath));
byte[] bytes = sin.readAllBytes();
for (byte b : bytes) {
writer.write(b);
}
writer.close();
}
catch (Exception e){
System.out.println(e.getMessage());
}
}Metoden get muliggør modtagelse af en fil fra serveren. Som nævnt i afsnittet om klientsiden af programmet, for at kunne overføre filen succesfuldt, skal dens størrelse, som lagres som en lang integer, kendes. Derfor opdeler jeg den i et array af 4 bytes, sender dem byte for byte til socketten, og derefter, når jeg har modtaget og samlet dem til et tal på klienten, sender jeg alle bytes, der udgør filen, læst fra inputstrømmen fra filen.
private void get(String source, String dest){
File sending = new File(source);
try {
FileInputStream readFromFile = new FileInputStream(sending);
byte[] arr = readFromFile.readAllBytes();
byte[] bytes = ByteBuffer.allocate(Long.SIZE / Byte.SIZE).putLong(sending.length()).array();
for (int i = 0; i<Long.SIZE / Byte.SIZE; i++)
sout.write(bytes[i]);
sout.flush();
for (byte b : arr)
sout.write(b);
}
catch (Exception e){
System.out.println(e.getMessage());
}
}; Metoden getAndParseInput svarer til den tilsvarende i klienten, med den eneste forskel, at den læser data fra socketten i stedet for fra tastaturet. Koden i arkivet er som selector.
I dette tilfælde er initialiseringen flyttet til en separat kodeblok, da ressourcerne i denne implementering frigives og genanvendes efter overførslen for at sikre beskyttelse mod hukommelseslækage.
private void initialization()
{
try {
serverSocket = new ServerSocket(port);
socket = serverSocket.accept();
sin = socket.getInputStream();
sout = socket.getOutputStream();
}
catch (Exception e) {
System.out.print(e.getMessage());
}
}Sammenfattende:
Vi har lige skrevet vores variation over et simpelt datatransmissionsprotokol og forstået, hvordan den skal fungere. I princippet er der ikke noget nyt her, men der var ikke lignende artikler på Habré, og i forbindelse med skrivingen af en serie artikler om cmd-værktøjer kunne dette emne ikke udelades.
Links:
Kilde: habr.com
