Psací software s funkcemi obslužných programů klient-server Windows, část 02

Pokračujeme v pokračující sérii článků věnovaných vlastním implementacím obslužných programů konzoly Windows a nemůžeme se nedotknout TFTP (Trivial File Transfer Protocol) – jednoduchého protokolu pro přenos souborů.

Jako minule si krátce projdeme teorii, podívejme se na kód, který implementuje funkcionalitu podobnou té požadované, a analyzujme ji. Další podrobnosti - pod střihem

Nebudu kopírovat a vkládat referenční informace, odkazy na které lze tradičně nalézt na konci článku, pouze řeknu, že v jádru je TFTP zjednodušenou variantou protokolu FTP, ve kterém má nastavení řízení přístupu byl odstraněn a ve skutečnosti zde není nic kromě příkazů pro příjem a přenos souboru . Aby však naše implementace byla o něco elegantnější a přizpůsobená současným principům psaní kódu, byla mírně pozměněna syntaxe – tím se nemění principy fungování, ale rozhraní se IMHO stává o něco logičtějším a kombinuje pozitivní aspekty FTP a TFTP.

Klient při spuštění požaduje zejména IP adresu serveru a port, na kterém je otevřen vlastní TFTP (kvůli nekompatibilitě se standardním protokolem jsem považoval za vhodné ponechat uživateli možnost volby portu), načež dojde k připojení, v důsledku čehož může klient odeslat jeden z příkazů - get nebo put, přijmout nebo odeslat soubor na server. Všechny soubory jsou odesílány v binárním režimu, aby se zjednodušila logika.

K implementaci protokolu jsem tradičně používal 4 třídy:

  • TFTPClient
  • TFTP Server
  • TFTPClientTester
  • TFTPServerTester

Vzhledem k tomu, že testovací třídy existují pouze pro ladění těch hlavních, nebudu je rozebírat, ale kód bude v úložišti, odkaz na něj najdete na konci článku. Nyní se podívám na hlavní třídy.

TFTPClient

Úkolem této třídy je připojit se ke vzdálenému serveru pomocí jeho IP a čísla portu, přečíst příkaz ze vstupního proudu (v tomto případě klávesnice), analyzovat jej, přenést na server a v závislosti na tom, zda potřebujete odeslat nebo přijmout soubor, přenést jej nebo získat.

Kód pro spuštění klienta pro připojení k serveru a čekání na příkaz ze vstupního streamu vypadá takto. Řada globálních proměnných, které jsou zde použity, je popsána mimo článek, v plném znění programu. Kvůli jejich triviálnosti je necituji, abych článek nezahltil.

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

Pojďme si projít metody volané v tomto bloku kódu:

Zde je soubor odeslán - pomocí skeneru prezentujeme obsah souboru jako pole bajtů, které zapisujeme jeden po druhém do soketu, načež jej zavřeme a znovu otevřeme (není to nejzřetelnější řešení, ale garantuje uvolnění zdrojů), načež zobrazíme zprávu o úspěšném převodu.

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

Tento fragment kódu popisuje načítání dat ze serveru. Vše je opět triviální, zajímavý je pouze první blok kódu. Abyste přesně pochopili, kolik bajtů je třeba přečíst ze soketu, musíte vědět, jakou váhu přenesený soubor váží. Velikost souboru na serveru je reprezentována jako dlouhé celé číslo, takže jsou zde akceptovány 4 bajty, které jsou následně převedeny na jedno číslo. Toto není příliš Java přístup, je to spíše podobné pro SI, ale řeší jeho problém.

Pak je vše triviální - obdržíme známý počet bajtů ze socketu a zapíšeme je do souboru, načež zobrazíme zprávu o úspěchu.

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

Pokud byl do okna klienta zadán jiný příkaz než get nebo put, bude zavolána funkce showErrorMessage, která značí, že zadání bylo nesprávné. Kvůli triviálnosti to nebudu citovat. O něco zajímavější je funkce příjmu a dělení vstupního řetězce. Vložíme do něj skener, od kterého očekáváme přijetí řádku odděleného dvěma mezerami a obsahujícího příkaz, zdrojovou adresu a cílovou adresu.

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

Odeslání příkazu – přenese zadaný příkaz ze skeneru do zásuvky a vynutí jeho odeslání

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

Selektor je funkce, která určuje akce programu v závislosti na zadaném řetězci. Všechno zde není příliš krásné a použitý trik není nejlepší s vynuceným ukončením mimo blok kódu, ale hlavním důvodem je absence některých věcí v Javě, jako jsou delegáti v C#, ukazatele funkcí z C++ nebo at přinejmenším hrozné a hrozné goto, které vám umožní toto krásně implementovat. Pokud víte, jak udělat kód trochu elegantnějším, uvítám kritiku v komentářích. Zdá se mi, že je zde potřeba String-delegate slovník, ale není zde žádný delegát...

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

TFTP Server

Funkčnost serveru se od funkčnosti klienta značně liší pouze tím, že příkazy k němu nepřicházejí z klávesnice, ale ze zásuvky. Některé metody jsou obecně stejné, proto je nebudu citovat, pouze se dotknu rozdílů.

Ke spuštění se používá metoda run, která přijímá jako vstup port a zpracovává vstupní data ze zásuvky ve věčné smyčce.

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

Metoda put, která zabalí metodu writeToFileFromSocket, která otevře datový proud pro zápis do souboru a zapíše všechny vstupní bajty ze soketu, zobrazí po dokončení zápisu zprávu o úspěšném dokončení přenosu.

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

Metoda get načte soubor serveru. Jak již bylo zmíněno v části o klientské straně programu, pro úspěšný přenos souboru potřebujete znát jeho velikost, uloženou v dlouhém celém čísle, takže jsem jej rozdělil na pole 4 bajtů, přenášel je po bajtech do soketu a poté, co jsem je přijal a sestavil na klientovi do čísla zpět, přenesu všechny bajty, které tvoří soubor, načtené ze vstupního proudu ze souboru.


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

Metoda getAndParseInput je stejná jako v klientovi, jediný rozdíl je v tom, že čte data ze soketu, nikoli z klávesnice. Kód je v úložišti, stejně jako selektor.
V tomto případě je inicializace umístěna do samostatného bloku kódu, protože v rámci této implementace jsou po dokončení přenosu zdroje uvolněny a znovu obsazeny – opět pro zajištění ochrany proti únikům paměti.

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

Shrnout:

Právě jsme napsali vlastní variaci na jednoduchý protokol pro přenos dat a přišli na to, jak by to mělo fungovat. Ameriku jsem tu v zásadě neobjevil a moc nových věcí nenapsal, ale na Habrého podobné články nebyly a v rámci psaní série článků o cmd utilitách se toho nedalo nedotknout.

Odkazy:

Úložiště zdrojového kódu
Stručně o TFTP
To samé, ale v ruštině

Zdroj: www.habr.com

Přidat komentář