Tvorba softvéru s funkciou klient-server pre nástroje Windows, časť 02

Pokračujúc v cykle článkov venovaných vlastným implementáciám konzolových nástrojov Windows, nemožno sa vyhnúť téme TFTP (Trivial File Transfer Protocol) — jednoduchého protokolu na prenos súborov.

Ako aj minule, zrekapitulujeme teóriu, pozrieme sa na kód, ktorý implementuje funkciu porovnateľnú s požadovanou, a analyzujeme ho. Viac informácií nájdete nižšie.

Nebudem kopírovať referenčné informácie, na ktoré tradične možno nájsť odkazy na konci článku, len poviem, že TFTP je v podstate zjednodušená variácia protokolu FTP, ktorý neumožňuje konfiguráciu kontroly prístupu a v zásade obsahuje len príkazy na prijímanie a odosielanie súborov. Avšak, aby sme našu implementáciu urobili o niečo elegantnejšou a prispôsobenou súčasným princípom písania kódu, bola syntax mierne upravená — princípy fungovania sa nezmenili, ale rozhranie, IMHO, je o niečo logickejšie a kombinuje výhody FTP a TFTP.

Pri spustení klient žiada o IP adresu servera a port, na ktorom je otvorený prispôsobený TFTP (kvôli nekompatibilite so štandardným protokolom som považoval za vhodné nechať používateľovi možnosť výberu portu), následne sa spojí, v dôsledku čoho klient môže odoslať jeden z príkazov — get alebo put, na získanie alebo odoslanie súboru na server. Všetky súbory sa odosielajú v binárnom režime — na zjednodušenie logiky.

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

  • TFTPClient
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

Keďže testovacie triedy existujú iba na ladenie hlavných, nebudem ich rozoberať, ale kód sa nachádza v repozitári, odkaz na ktorý nájdete na konci článku. A teraz sa pozriem na hlavné triedy.

TFTPClient

Úlohou tejto triedy je pripojiť sa na vzdialený server podľa jeho IP adresy a čísla portu, získať príkaz zo vstupného prúdu (v tomto prípade — klávesnice), analyzovať ho, poslať serveru, a v závislosti na tom, či je potrebné odoslanie alebo prijatie súboru, ho odoslať alebo získať.

Kód na spustenie klienta na pripojenie k serveru a čakanie na príkazy z vstupného prúdu vyzerá takto. Rada globálnych premenných, ktoré sa tu používajú, je popísaná mimo článku v plnom texte programu. Vzhľadom na ich triviálnosť ich nezverejňujem, aby som nepreťažil článok.

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

Prejdime si metódy, ktoré sú v tomto bloku kódu volané:

Tu sa odosiela súbor — pomocou skenera predstavujeme obsah súboru ako pole bajtov, ktoré postupne zapisujeme do socketu, po čom ho zatvoríme a znova otvoríme (nie je to najjasnejšie riešenie, ale zaručuje to uvoľnenie zdrojov), po čom na obrazovke zobrazíme správu o úspešnom prenose.

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 dát zo servera. Všetko je opäť triviálne, zaujímavý je len prvý blok kódu. Aby sme vedeli, koľko bajtov treba prečítať zo socketu, potrebujeme vedieť, akú veľkosť má odosielaný súbor. Veľkosť súboru na serveri je reprezentovaná ako dlhé celé číslo, preto tu berieme 4 bajty, ktoré sa neskôr konvertujú na jedno číslo. To nie je veľmi „Javový“ prístup, skôr je to podobné C, ale úlohu to plní.

Ďalej je všetko triviálne — dostávame známe číslo bajtov zo socketu a zapisujeme ich do súboru, po čom zobrazíme správu o úspešnosti.

   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 je do klientského okna zadaný príkaz odlišný od get alebo put, vyvolá sa funkcia showErrorMessage, ktorá zobrazí nesprávnosť vstupu. Z dôvodu jednoduchosti to neuvádzam. Trochu zaujímavejšia je funkcia na prijímanie a rozdelenie vstupného reťazca. Do nej posielame skener, od ktorého očakávame, že nám poskytne reťazec rozdelený dvoma medzerami, 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("Nesprávny vstup");
        }
    }

Odovzdanie príkazu — prenos zadaného príkazu zo skenera do soketu a jeho nútené 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 — funkcia, ktorá určuje akcie programu v závislosti od zadaného reťazca. Tu to nie je veľmi pekné a používa sa nie najlepší spôsob s núteným výstupom z bloku kódu, ale hlavnou príčinou je nedostatok niektorých vecí v Jave, ako sú delegáti v C#, ukazovatele na funkcie z C++ alebo aspoň desivé a hrozné goto, ktoré umožňujú elegantnejšiu realizáciu. Ak viete, ako urobiť kód trochu elegantnejším — čakám na kritiku v komentároch. Myslím si, že tu je potrebný slovník String-delegate, ale delegát nie je…

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

TFTPServer

Funkcionalita servera sa od funkčnosti klienta líši vo veľkej miere len tým, že príkazy k nemu prichádzajú nie z klávesnice, ale zo soketu. Časť metód sa úplne zhoduje, preto ich nebudem uvádzať, dotknem sa len rozdielov.

Na spustenie sa tu používa metóda run, ktorá prijíma port a spracováva vstupné údaje zo soketu v nekonečnej slučke.

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

Metóda put, ktorá je obalovým prvkom metódy writeToFileFromSocket, otvára zariadenie na zápis do súboru a zapisuje všetky bajty vstupu zo socketu. Po dokončení zápisu vytlačí správu o úspešnom 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 zabezpečuje prijímanie súboru zo servera. Ako už bolo spomenuté v časti o klientskej strane programu, na úspešný prenos súboru je potrebné poznať jeho veľkosť, ktorá sa uchováva ako dlhé celé číslo. Preto ju rozdeľujem na pole so 4 bajtmi, ktoré posielam bajtovo do socketu, a potom, po prijatí a zložením späť na klientovi, prenášam všetky bajty, ktoré tvoria súbor, prečítané z vstupného prúdu 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 sa zhoduje s analógiou v kliente, s tým rozdielom, že číta dáta zo socketu, a nie z klávesnice. Kód v úložisku, ako aj selector.
V tomto prípade je inicializácia presunutá do samostatného bloku kódu, pretože v rámci tejto implementácie sa po ukončení prenosu uvoľňujú zdroje a znovu sa zaberajú, opäť s cieľom zabezpečiť ochranu pred únikom pamäte.

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

Zhrňujúc:

Práve sme napísali našu variáciu na tému jednoduchého protokolu prenosu dát a pochopili, ako by mal fungovať. V podstate, Ameriku som tu neobjavil a veľa nového napísal, ale – podobné články na Habre neboli a v rámci písania cyklu článkov o utilitách cmd nebolo možné ho nezahrnúť.

Odkazy:

Úložisko so zdrojovým kódom
Krátko o TFTP
To isté, ale v ruštine

Zdroj: habr.com

Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster