Skrivprogramvara med funktionerna i Windows klient-serververktyg, del 02

Vi fortsätter den pågående serien av artiklar som ägnas åt anpassade implementeringar av Windows-konsolverktyg och vi kan inte låta bli att beröra TFTP (Trivial File Transfer Protocol) - ett enkelt filöverföringsprotokoll.

Låt oss som förra gången kort gå igenom teorin, se koden som implementerar funktionalitet liknande den som krävs och analysera den. Fler detaljer - under snittet

Jag kommer inte att kopiera och klistra in referensinformation, vars länkar traditionellt kan hittas i slutet av artikeln, jag kommer bara att säga att TFTP i sin kärna är en förenklad variant av FTP-protokollet, där inställningen för åtkomstkontroll har har tagits bort, och det finns faktiskt ingenting här förutom kommandon för att ta emot och överföra en fil . Men för att göra vår implementering lite mer elegant och anpassad till de nuvarande principerna för att skriva kod har syntaxen ändrats något - detta ändrar inte principerna för driften, men gränssnittet, IMHO, blir lite mer logiskt och kombinerar de positiva aspekterna av FTP och TFTP.

I synnerhet, när den startas, begär klienten serverns IP-adress och porten på vilken anpassad TFTP är öppen (på grund av inkompatibilitet med standardprotokollet ansåg jag det lämpligt att lämna användaren möjligheten att välja en port), varefter en anslutning uppstår, som ett resultat av vilket klienten kan skicka ett av kommandona - get eller put, för att ta emot eller skicka en fil till servern. Alla filer skickas i binärt läge för att förenkla logiken.

För att implementera protokollet använde jag traditionellt 4 klasser:

  • TFTPClient
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

På grund av det faktum att testklasser endast existerar för att felsöka de viktigaste, kommer jag inte att analysera dem, men koden kommer att finnas i förvaret; en länk till den finns i slutet av artikeln. Nu ska jag titta på huvudklasserna.

TFTPClient

Uppgiften för den här klassen är att ansluta till en fjärrserver med dess ip och portnummer, läsa ett kommando från indataströmmen (i det här fallet tangentbordet), analysera det, överföra det till servern och, beroende på om du behöver skicka eller ta emot en fil, överföra den eller hämta.

Koden för att starta klienten för att ansluta till servern och vänta på ett kommando från ingångsströmmen ser ut så här. Ett antal globala variabler som används här beskrivs utanför artikeln, i programmets fullständiga text. På grund av deras trivialitet citerar jag dem inte för att inte överbelasta artikeln.

 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());
        }
    }

Låt oss gå igenom metoderna som kallas i detta kodblock:

Här skickas filen - med hjälp av en skanner presenterar vi filens innehåll som en array av byte, som vi skriver en efter en till sockeln, varefter vi stänger den och öppnar den igen (inte den mest uppenbara lösningen, men det garanterar frigörandet av resurser), varefter vi visar ett meddelande om framgångsrik överföring.

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());
        }
    }

Detta kodfragment beskriver hämtning av data från servern. Allt är återigen trivialt, bara det första kodblocket är av intresse. För att förstå exakt hur många byte som behöver läsas från sockeln måste du veta hur mycket den överförda filen väger. Filstorleken på servern representeras som ett långt heltal, så 4 byte accepteras här, som sedan konverteras till ett nummer. Detta är inte en väldigt Java-metod, det är ganska likt för SI, men det löser sitt problem.

Då är allt trivialt - vi får ett känt antal byte från socket och skriver dem till en fil, varefter vi visar ett framgångsmeddelande.

   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());
       }
    }

Om ett annat kommando än get eller put skrevs in i klientfönstret kommer funktionen showErrorMessage att anropas, vilket indikerar att inmatningen var felaktig. På grund av trivialitet kommer jag inte att citera det. Något mer intressant är funktionen att ta emot och dela inmatningssträngen. Vi skickar skannern in i den, från vilken vi förväntar oss att få en rad separerad av två mellanslag och som innehåller kommandot, källadressen och destinationsadressen.

    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("Bad input");
        }
    }

Skicka ett kommando – överför det inmatade kommandot från skannern till uttaget och tvingar det att skickas

    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());
        }
    }

En väljare är en funktion som bestämmer programmets åtgärder beroende på den angivna strängen. Allt här är inte särskilt vackert och tricket som används är inte det bästa med forcerad exit utanför kodblocket, men huvudorsaken till detta är frånvaron i Java av vissa saker, som delegater i C#, funktionspekare från C++ eller på åtminstone det fruktansvärda och hemska goto, som låter dig implementera detta vackert. Om du vet hur man gör koden lite mer elegant tar jag gärna emot kritik i kommentarerna. Det förefaller mig som om en ordbok för strängdelegat behövs här, men det finns ingen delegat...

    private void selector()
    {
        do{
            if (typeOfCommand.equals("get")){
                get(sourcePath, destPath);
                break;
            }
            if (typeOfCommand.equals("put")){
                put(sourcePath, destPath);
                break;
            }
            showErrorMessage();
        }
        while (false);
    }
}

TFTPServer

Serverns funktionalitet skiljer sig i stort från klientens funktionalitet, bara genom att kommandon inte kommer till den från tangentbordet utan från uttaget. Vissa av metoderna är i allmänhet desamma, så jag kommer inte att citera dem, jag kommer bara att beröra skillnaderna.

För att starta används runmetoden som tar emot en port som ingång och bearbetar indata från sockeln i en evig loop.

    public void run(int port) {
            this.port = port;
            incialization();
            while (true) {
                getAndParseInput();
                selector();
            }
    }

Put-metoden, som omsluter metoden writeToFileFromSocket som öppnar en skrivström till en fil och skriver alla indatabytes från sockeln, visar ett meddelande som indikerar att överföringen har slutförts framgångsrikt när skrivningen är klar.

    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());
        }
    }

Get-metoden hämtar serverfilen. Som redan nämnts i avsnittet om klientsidan av programmet, för att framgångsrikt överföra en fil måste du veta dess storlek, lagrad i ett långt heltal, så jag delar upp den i en array med 4 byte, överför dem byte-för-byte till sockeln, och sedan, efter att ha tagit emot och satt ihop dem på klienten till ett nummer tillbaka, överför jag alla byte som utgör filen, läst från indataströmmen från 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());
        }
    };

GetAndParseInput-metoden är densamma som i klienten, den enda skillnaden är att den läser data från sockeln snarare än från tangentbordet. Koden finns i förvaret, precis som selector.
I detta fall placeras initieringen i ett separat kodblock, eftersom inom denna implementering, efter att överföringen är slutförd, släpps resurser och återupptas igen - igen för att ge skydd mot minnesläckor.

    private void  incialization()
    {
        try {
            serverSocket = new ServerSocket(port);
            socket = serverSocket.accept();
            sin = socket.getInputStream();
            sout = socket.getOutputStream();
        }
        catch (Exception e) {
            System.out.print(e.getMessage());
        }
    }

För att sammanfatta:

Vi har precis skrivit en egen variant på ett enkelt dataöverföringsprotokoll och kommit på hur det ska fungera. I princip upptäckte jag inte Amerika här och skrev inte så mycket nytt, men det fanns inga liknande artiklar om Habré, och som en del av att skriva en serie artiklar om cmd-verktyg var det omöjligt att inte beröra det.

Länkar:

Källkodsförråd
Kort om TFTP
Samma sak, fast på ryska

Källa: will.com

Lägg en kommentar