Softvér na písanie s funkciami obslužných programov klient-server Windows, časť 02

Pokračujeme v prebiehajúcej sérii článkov venovaných vlastným implementáciám pomocných programov konzoly Windows a nemôžeme sa nedotknúť TFTP (Trvial File Transfer Protocol) – jednoduchého protokolu na prenos súborov.

Tak ako minule, prejdime si stručne teóriu, pozrime si kód, ktorý implementuje funkčnosť podobnú tej požadovanej, a analyzujme ju. Ďalšie podrobnosti - pod rezom

Referenčné informácie, ktorých odkazy nájdete tradične na konci článku, nebudem kopírovať a vkladať, len poviem, že v jadre je TFTP zjednodušená variácia protokolu FTP, v ktorej sa nastavenie riadenia prístupu bol odstránený a v skutočnosti tu nie je nič okrem príkazov na príjem a prenos súboru . Aby však bola naša implementácia o niečo elegantnejšia a prispôsobená súčasným princípom písania kódu, syntax bola mierne zmenená – tým sa síce nemení princípy fungovania, ale rozhranie sa IMHO stáva trochu logickejším a spája pozitívne aspekty FTP a TFTP.

Pri spustení klient požaduje najmä IP adresu servera a port, na ktorom je otvorený vlastný TFTP (z dôvodu nekompatibility so štandardným protokolom som považoval za vhodné ponechať používateľovi možnosť výberu portu), po čom dôjde k pripojeniu, v dôsledku čoho môže klient odoslať jeden z príkazov - get alebo put, na prijatie alebo odoslanie súboru na server. Všetky súbory sa odosielajú v binárnom režime, aby sa zjednodušila logika.

Na implementáciu protokolu som tradične používal 4 triedy:

  • TFTPClient
  • Server TFTP
  • TFTPClientTester
  • TFTPServerTester

Vzhľadom na to, že testovacie triedy existujú len na ladenie tých hlavných, nebudem ich analyzovať, ale kód bude v úložisku, odkaz naň nájdete na konci článku. Teraz sa pozriem na hlavné triedy.

TFTPClient

Úlohou tejto triedy je pripojiť sa k vzdialenému serveru pomocou jeho IP a čísla portu, prečítať príkaz zo vstupného toku (v tomto prípade z klávesnice), analyzovať ho, preniesť na server a v závislosti od toho, či potrebujete odoslať alebo prijať súbor, preniesť ho alebo získať.

Kód na spustenie klienta na pripojenie k serveru a čakanie na príkaz zo vstupného toku vyzerá takto. Množstvo globálnych premenných, ktoré sa tu používajú, je popísané mimo článku, v úplnom znení programu. Kvôli ich triviálnosti ich necitujem, aby som článok 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());
        }
    }

Pozrime sa na metódy volané v tomto bloku kódu:

Tu je súbor odoslaný - pomocou skenera prezentujeme obsah súboru ako pole bajtov, ktoré po jednom zapisujeme do soketu, potom ho zatvoríme a znova otvoríme (nie je to najzrejmejšie riešenie, ale garantuje uvoľnenie zdrojov), po ktorom zobrazíme správu o úspešnom prevode.

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 získavanie údajov zo servera. Všetko je opäť triviálne, zaujímavý je len prvý blok kódu. Aby ste presne pochopili, koľko bajtov je potrebné prečítať zo zásuvky, musíte vedieť, koľko váži prenášaný súbor. Veľkosť súboru na serveri je reprezentovaná ako dlhé celé číslo, takže tu sú akceptované 4 bajty, ktoré sa následne prevedú na jedno číslo. Toto nie je veľmi Java prístup, je to skôr podobné pre SI, ale rieši jeho problém.

Potom je všetko triviálne - zo soketu dostaneme známy počet bajtov a zapíšeme ich do súboru, po čom zobrazíme správu o úspechu.

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

Ak bol do okna klienta zadaný iný príkaz ako get alebo put, zavolá sa funkcia showErrorMessage, čo znamená, že zadanie bolo nesprávne. Kvôli triviálnosti to nebudem citovať. O niečo zaujímavejšia je funkcia príjmu a rozdelenia vstupného reťazca. Prejdeme do nej skener, od ktorého očakávame, že dostaneme riadok oddelený dvomi medzerami a obsahujúci príkaz, zdrojovú adresu a cieľovú 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");
        }
    }

Odoslanie príkazu—prenesie zadaný príkaz zo skenera do zásuvky a vynúti jeho odoslanie

    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 funkcia, ktorá určuje akcie programu v závislosti od zadaného reťazca. Všetko tu nie je veľmi pekné a použitý trik nie je najlepší s vynúteným ukončením mimo bloku kódu, ale hlavným dôvodom je absencia niektorých vecí v Jave, ako sú delegáti v C#, ukazovatele funkcií z C++ alebo at prinajmenšom hrozné a hrozné goto, ktoré vám umožňujú krásne implementovať. Ak viete, ako urobiť kód trochu elegantnejším, uvítam kritiku v komentároch. Zdá sa mi, že tu je potrebný String-delegate slovník, ale neexistuje žiadny 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);
    }
}

Server TFTP

Funkčnosť servera sa líši od funkčnosti klienta vo všeobecnosti iba tým, že príkazy k nemu neprichádzajú z klávesnice, ale zo zásuvky. Niektoré metódy sú vo všeobecnosti rovnaké, preto ich nebudem citovať, dotknem sa iba rozdielov.

Na spustenie sa používa metóda run, ktorá prijíma port ako vstup a spracováva vstupné dáta zo zásuvky vo večnej slučke.

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

Metóda put, ktorá zabalí metódu writeToFileFromSocket, ktorá otvorí tok zápisu do súboru a zapíše všetky vstupné bajty zo soketu, zobrazí po dokončení zápisu správu označujúcu úspešné dokončenie prenosu.

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

Metóda get načíta súbor servera. Ako už bolo spomenuté v časti o klientskej strane programu, na úspešný prenos súboru potrebujete poznať jeho veľkosť, uloženú v dlhom integer, preto som ho rozdelil na pole 4 bajtov, preniesol ich bajt po byte do zásuvky a potom, keď som ich prijal a zostavil na klientovi do čísla späť, prenesiem všetky bajty, ktoré tvoria súbor, načítané zo vstupného toku zo súboru.


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

Metóda getAndParseInput je rovnaká ako v klientovi, len s tým rozdielom, že číta údaje zo zásuvky a nie z klávesnice. Kód je v úložisku, rovnako ako selektor.
V tomto prípade je inicializácia umiestnená v samostatnom bloku kódu, pretože v rámci tejto implementácie sa po dokončení prenosu zdroje uvoľnia a znovu obsadia – opäť na zabezpečenie ochrany pred únikom pamäte.

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

Zhrnúť:

Práve sme napísali vlastnú variáciu na jednoduchý protokol prenosu dát a prišli sme na to, ako by to malo fungovať. V zásade som tu neobjavil Ameriku a nenapísal veľa nových vecí, no na Habrého podobné články neboli a v rámci písania série článkov o cmd utilitách sa toho nedalo nedotknúť.

odkazy:

Úložisko zdrojového kódu
Stručne o TFTP
To isté, ale v ruštine

Zdroj: hab.com

Pridať komentár