Scrierea de software cu funcționalitatea utilităților client-server Windows, partea 02

Continuând seria în curs de desfășurare de articole dedicate implementărilor personalizate ale utilităților consolei Windows, nu putem să nu atingem TFTP (Trivial File Transfer Protocol) - un protocol simplu de transfer de fișiere.

Ca și data trecută, să trecem pe scurt peste teorie, să vedem codul care implementează o funcționalitate similară cu cea necesară și să-l analizăm. Mai multe detalii - sub tăietură

Nu voi copia și lipi informații de referință, link-uri către care pot fi găsite în mod tradițional la sfârșitul articolului, voi spune doar că, în esență, TFTP este o variantă simplificată a protocolului FTP, în care setarea de control al accesului a fost eliminat și, de fapt, nu există nimic aici, cu excepția comenzilor pentru primirea și transferul unui fișier. Cu toate acestea, pentru a face implementarea noastră puțin mai elegantă și adaptată la principiile actuale de scriere a codului, sintaxa a fost ușor modificată - acest lucru nu schimbă principiile de funcționare, dar interfața, IMHO, devine puțin mai logică și combină aspectele pozitive ale FTP și TFTP.

În special, la lansare, clientul solicită adresa IP a serverului și portul pe care este deschis TFTP personalizat (din cauza incompatibilității cu protocolul standard, am considerat că este potrivit să lase utilizatorului posibilitatea de a selecta un port), după care un are loc conexiunea, în urma căreia clientul poate trimite una dintre comenzi - get sau put, pentru a primi sau trimite un fișier către server. Toate fișierele sunt trimise în modul binar pentru a simplifica logica.

Pentru a implementa protocolul, am folosit în mod tradițional 4 clase:

  • Client TFTPC
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

Datorită faptului că clasele de testare există doar pentru depanarea celor principale, nu le voi analiza, dar codul va fi în depozit un link către acesta poate fi găsit la sfârșitul articolului. Acum mă voi uita la clasele principale.

Client TFTPC

Sarcina acestei clase este să se conecteze la un server la distanță folosind ip-ul și numărul de port, să citească o comandă din fluxul de intrare (în acest caz, tastatura), să o analizeze, să o transfere pe server și, în funcție de faptul că trebuie să trimiteți sau să primiți un fișier, să îl transferați sau să obțineți.

Codul pentru lansarea clientului pentru a se conecta la server și a aștepta o comandă din fluxul de intrare arată astfel. O serie de variabile globale care sunt utilizate aici sunt descrise în afara articolului, în textul integral al programului. Din cauza trivialității lor, nu le citez pentru a nu supraîncărca 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 peste metodele numite în acest bloc de cod:

Aici se trimite fișierul - folosind un scaner, prezentăm conținutul fișierului ca o matrice de octeți, pe care îi scriem unul câte unul î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 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 preluarea datelor de pe server. Totul este din nou banal, interesează doar primul bloc de cod. Pentru a înțelege exact câți octeți trebuie citiți din socket, trebuie să știți cât cântărește fișierul transferat. Mărimea fișierului de pe server este reprezentată ca un întreg lung, deci aici sunt acceptați 4 octeți, care sunt ulterior convertiți într-un număr. Aceasta nu este o abordare foarte Java, este destul de similară pentru SI, dar își rezolvă problema.

Apoi totul este banal - primim un număr cunoscut de octeți de la socket și îi scriem într-un 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());
       }
    }

Dacă în fereastra clientului a fost introdusă o altă comandă decât get sau put, funcția showErrorMessage va fi apelată, indicând faptul că intrarea a fost incorectă. Din cauza trivialității, nu o voi cita. Ceva mai interesantă este funcția de primire și împărțire a șirului de intrare. Trecem scanerul în el, de la care ne așteptăm să primim o linie separată de două spații și care conține comanda, adresa sursă și adresa destinației.

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

Trimiterea unei comenzi—transmite comanda introdusă de la scaner la soclu și forțează să fie trimisă

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

Un selector este o funcție care determină acțiunile programului în funcție de șirul introdus. Totul aici nu este foarte frumos și trucul folosit nu este cel mai bun cu ieșire forțată în afara blocului de cod, dar motivul principal pentru aceasta este absența în Java a unor lucruri, cum ar fi delegații în C#, pointerii de funcție din C++ sau la cel puțin groaznicul și teribilul goto, care vă permit să implementați acest lucru frumos. Dacă știi să faci codul puțin mai elegant, salut criticile în comentarii. Mi se pare că aici este nevoie de un dicționar String-delegate, dar nu există niciun 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

Funcționalitatea serverului diferă de funcționalitatea clientului, în general, doar prin aceea că comenzile nu vin de la tastatură, ci de la soclu. Unele dintre metode sunt în general aceleași, așa că nu le voi cita, voi atinge doar diferențele.

Pentru început, este folosită metoda de rulare, care primește un port ca intrare și procesează datele de intrare de la soclu într-o buclă eternă.

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

Metoda put, care include metoda writeToFileFromSocket care deschide un flux de scriere într-un fișier și scrie toți octeții de intrare din socket, afișează un mesaj care indică finalizarea cu succes a transferului când scrierea se încheie.

    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 preia fișierul serverului. După cum sa menționat deja în secțiunea din partea client a programului, pentru a transfera cu succes un fișier, trebuie să cunoașteți dimensiunea acestuia, stocat într-un număr întreg lung, așa că l-am împărțit într-o matrice de 4 octeți, transferați-i octet cu octet la soclu și apoi, după ce le-am primit și asamblat pe client într-un număr înapoi, transfer toți octeții care alcătuiesc 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 aceeași ca și în client, singura diferență fiind că citește mai degrabă datele de la socket decât de la tastatură. Codul este în depozit, la fel ca selectorul.
În acest caz, inițializarea este plasată într-un bloc de cod separat, deoarece în cadrul acestei implementări, după ce transferul este finalizat, resursele sunt eliberate și reocupate - din nou pentru a oferi protecție împotriva scurgerilor 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());
        }
    }

A rezuma:

Tocmai am scris propria noastră variantă pe un protocol simplu de transfer de date și ne-am dat seama cum ar trebui să funcționeze. În principiu, nu am descoperit America aici și nu am scris prea multe lucruri noi, dar nu au existat articole similare despre Habré și, ca parte a scrierii unei serii de articole despre utilitare cmd, era imposibil să nu ating el.

referințe:

Depozitul de cod sursă
Pe scurt despre TFTP
Același lucru, dar în rusă

Sursa: www.habr.com

Adauga un comentariu