Scrierea de software cu funcționalitate de utilitare client-server Windows, partea 02

Continuând seria de articole dedicate realizărilor personalizate ale utilitarelor de consolă Windows, nu putem să nu menționăm TFTP (Trivial File Transfer Protocol) — un protocol simplu de transfer de fișiere.

La fel ca și data trecută, vom face o trecere rapidă prin teorie, vom examina codul care implementează funcționalitate similară celei necesare și îl vom analiza. Mai multe detalii — mai jos.

Nu voi copia informații de referință, legăturile cărora pot fi găsite, în mod tradițional, la sfârșitul articolului, voi spune doar că, în esență, TFTP este o variație simplificată a protocolului FTP, care a eliminat configurarea controlului accesului, și de fapt, nu conține nimic altceva decât comenzi pentru obținerea și trimiterea fișierelor. Totuși, pentru a face implementarea noastră puțin mai elegantă și adaptată principiilor actuale de scriere a codului, sintaxa a fost schimbată puțin — principiile de funcționare nu se schimbă, dar interfața, în opinia mea, devine puțin mai logică și combină avantajele FTP și TFTP.

În special, la pornire, clientul solicită adresa IP a serverului și portul pe care este deschis TFTP-ul personalizat (din cauza incompatibilității cu protocolul standard, am considerat potrivit să las utilizatorului posibilitatea de a alege portul), după care are loc conexiunea, rezultând că clientul poate trimite una dintre comenzile — get sau put, pentru a obține sau a trimite fișierul pe server. Toate fișierele sunt trimise în mod binar — pentru a simplifica logica.

Pentru realizarea protocolului, am folosit, în mod tradițional, 4 clase:

  • TFTPClient
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

Având în vedere că clasele de testare există doar pentru debugging-ul celor principale, nu le voi analiza, dar codul va fi disponibil în repository, iar linkul către acesta poate fi găsit la sfârșitul articolului. Acum, să discutăm despre clasele principale.

TFTPClient

Scopul acestei clase este să se conecteze la un server remote prin intermediul adresei IP și al numărului portului, să citească din fluxul de intrare (în acest caz — tastatură) comanda, să o analizeze, să o transmită serverului și, în funcție de faptul că se cere trimiterea sau primirea unui fișier, să-l trimită sau să-l primească.

Codul care începe clientul pentru a se conecta la server și a aștepta comanda din fluxul de intrare arată astfel. O serie de variabile globale, care sunt utilizate aici, sunt descrise în afara articolului, în textul complet al programului. Datorită trivialității lor, nu le menționez pentru a nu aglomera articolul.

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

Să trecem în revistă metodele apelate în acest bloc de cod:

Aici se întâmplă trimiterea fișierului — folosind scannerul, reprezentăm conținutul fișierului ca un array de bite, pe care le scriem pe rând în socket, după care îl închidem și îl deschidem din nou (nu este cea mai evidentă soluție, dar garantează eliberarea resurselor), după care afișăm un mesaj pe ecran despre transferul reușit.

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

Acest fragment de cod descrie obținerea datelor de pe server. Totul este din nou trivial, interesul constă doar în primul bloc de cod. Pentru a înțelege câte bite trebuie citite din socket, trebuie să știm cât cântărește fișierul transmis. Dimensiunea fișierului pe server este reprezentată ca un număr întreg lung, așadar, aici se acceptă 4 bite, care sunt ulterior convertiți într-un singur număr. Aceasta nu este o abordare foarte Java, este mai degrabă asemănătoare cu C, dar își îndeplinește sarcina.

Mai departe, totul este trivial — obținem numărul cunoscut de bite din socket și le scriem în fișier, după care afișăm un mesaj de 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());
       }
    }

În cazul în care s-a introdus un comandă în fereastra clientului diferită de get sau put, va fi apelată funcția showErrorMessage, care va indica o eroare la input. Din cauza trivialității, nu o voi detalia. Funcția de obținere și descompunere a șirului de intrare este ceva mai interesantă. În aceasta, transmitem scannerul, de la care așteptăm să primim un șir, împărțit prin două spații și care conține comandă, adresa sursă și adresa destinație.

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

Trimiterea comenzii — transferul comenzii introduse de la scanner către socket și trimiterea acesteia forțat.

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

Selectorul — funcția care determină acțiunile programului în funcție de șirul introdus. Aici lucrurile nu sunt foarte frumoase și se folosește o metodă nu foarte bună cu ieșirea forțată din blocul de cod, dar cauza principală este absența în Java a unor elemente, cum ar fi delegatele în C#, pointerii de funcție din C++ sau, măcar, groaznicul și teribilul goto, care permit o implementare elegantă. Dacă știți cum să îmbunătățiți codul, aștept critici în comentarii. Mi se pare că ar fi nevoie de un dicționar String-delegate, dar nu există delegate...

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

TFTPServer

Funcționalitatea serverului se deosebește de funcționalitatea clientului prin faptul că comenzile care ajung la acesta nu vin de la tastatură, ci din socket. O parte din metode coincide, așa că nu le voi prezenta, mă voi axa doar pe diferențe.

Pentru pornire, se folosește metoda run, care primește ca parametru portul și procesează datele de intrare din socket într-un ciclu infinit.

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

Metoda put, care este o wrapper pentru metoda writeToFileFromSocket, deschide un flux de scriere într-un fișier și scrie toți octeții de intrare din socket, iar după finalizarea scrierii, afișează un mesaj de succes al transferului.

    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 asigură obținerea fișierului de pe server. Așa cum am menționat în secțiunea despre partea client a programului, pentru a efectua cu succes transferul fișierului, trebuie să cunosc dimensiunea sa, care este stocată într-un întreg lung; prin urmare, o împart în un array de 4 octeți, îi transmit pe rând în socket, iar apoi, primind și reconstruind în client ca un număr, trimit toți octeții care compun fișierul, citiți din fluxul de intrare din fișier.


 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 este similară cu cea de pe client, cu singura diferență că citește datele din socket, nu din tastatură. Codul din repozitoriu este, la fel ca selectorul.
În acest caz, inițializarea a fost scoasă într-un bloc de cod separat, deoarece în cadrul acestei implementări, după finalizarea transferului, resursele sunt eliberate și apoi alocate din nou — tot cu scopul de a asigura protecția împotriva scurgerii de memorie.

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

În rezumat:

Tocmai am scris propria noastră variație asupra unui protocol simplu de transfer de date și am înțeles cum ar trebui să funcționeze. Practic, nu am descoperit America și nu am scris nimic foarte nou, dar — articole similare nu existau pe Habr, iar în cadrul scrierii unui ciclu de articole despre utilitarele cmd, nu am putut să nu-l ating.

Linkuri:

Repozitoriu cu codul sursă
Pe scurt despre TFTP
Și același lucru, dar în limba română

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster