Scrittura di software con funzionalità di utilità client-server per Windows, parte 02

Proseguendo con il ciclo di articoli dedicati alle implementazioni personalizzate delle utility console di Windows, non si può non menzionare TFTP (Trivial File Transfer Protocol) — un protocollo semplice per il trasferimento di file.

Come la volta scorsa, faremo una rapida panoramica sulla teoria, vedremo il codice che implementa funzionalità simili a quelle richieste e lo analizzeremo. Maggiori dettagli — sotto il tag.

Non copierò e incollerò informazioni di riferimento, i link a cui si possono tradizionalmente trovare alla fine dell'articolo; dirò solo che, fondamentalmente, TFTP è una versione semplificata del protocollo FTP, in cui è stata rimossa la configurazione del controllo degli accessi e, di fatto, non c'è nulla se non i comandi di ricezione e invio file. Tuttavia, per rendere la nostra implementazione un po' più elegante e adattata ai principi correnti di scrittura del codice, la sintassi è stata leggermente modificata — i principi di funzionamento non cambiano, ma l'interfaccia, IMHO, diventa un po' più logica e combina gli aspetti positivi di FTP e TFTP.

In particolare, all'avvio, il client richiede l'indirizzo IP del server e la porta su cui è aperto il TFTP personalizzato (a causa della incompatibilità con il protocollo standard, ho ritenuto opportuno lasciare la possibilità di scelta della porta all'utente), dopodiché si stabilisce la connessione, a seguito della quale il client può inviare uno dei comandi — get o put, per ricevere o inviare file al server. Tutti i file vengono inviati in modalità binaria — per semplificare la logica.

Per implementare il protocollo, ho utilizzato tradizionalmente 4 classi:

  • TFTPClient
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

Poiché le classi di test esistono solo per il debug di quelle principali, non le analizzerò, ma il codice sarà disponibile nel repository, il cui link può essere consultato alla fine dell'articolo. Ora analizzerò le classi principali.

TFTPClient

Il compito di questa classe è di connettersi a un server remoto tramite il suo IP e il numero di porta, leggere dal flusso di ingresso (in questo caso — dalla tastiera) il comando, analizzarlo, inviarlo al server e, a seconda che sia richiesta la trasmissione o la ricezione del file, trasferirlo o riceverlo.

Il codice per avviare il client alla connessione con il server e aspettare un comando dal flusso di input appare così. Una serie di variabili globali, che qui vengono utilizzate, sono descritte al di fuori dell'articolo, nel testo completo del programma. A causa della loro trivialità, non le fornisco, per non appesantire l'articolo.

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

Esaminiamo i metodi chiamati in questo blocco di codice:

Qui avviene l'invio del file: utilizzando lo scanner, rappresentiamo il contenuto del file come un array di byte, che scriviamo uno dopo l'altro nel socket, dopo di che lo chiudiamo e lo riapriamo (una soluzione non ovvia, ma che garantisce il rilascio delle risorse), dopodiché visualizziamo un messaggio di avvenuta trasmissione.

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

Questo frammento di codice descrive l'acquisizione dei dati dal server. Tutto è di nuovo piuttosto triviale, ciò che interessa è solo il primo blocco di codice. Per comprendere quanti byte devono essere letti dal socket, è necessario conoscere il peso del file trasferito. La dimensione del file sul server è rappresentata come un numero intero lungo, quindi qui vengono presi 4 byte, che successivamente vengono convertiti in un numero. Questo non è propriamente un approccio Java, è piuttosto simile al C, ma risolve il suo compito.

Successivamente, tutto è triviale: riceviamo il numero noto di byte dal socket e li scriviamo in un file, dopodiché visualizziamo un messaggio di successo.

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

In caso venga inserito nel client un comando diverso da get o put, verrà chiamata la funzione showErrorMessage, che indica l'erroneità dell'input. Per motivi di semplicità, non lo riporto. È leggermente più interessante la funzione che riceve e analizza la stringa di input. A essa passiamo lo scanner, dal quale ci aspettiamo di ricevere una stringa divisa da due spazi e contenente il comando, l'indirizzo sorgente e l'indirizzo di destinazione.

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

Invio del comando — trasmissione del comando inserito dallo scanner al socket e invio forzato dello stesso.

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

Il selettore — è una funzione che determina le azioni del programma a seconda della stringa inserita. Qui la situazione non è molto elegante e si utilizza un non ottimo approccio con l'uscita forzata dal blocco di codice, ma la ragione principale è l'assenza in Java di alcune feature, come i delegate in C#, puntatori a funzioni da C++ o almeno il temuto goto, che permetterebbero di implementarlo in modo più elegante. Se conoscete un modo per rendere il codice un po' più raffinato, sono aperto a critiche nei commenti. Mi sembra che ci vorrebbe un dizionario String-delegate, ma il delegate non esiste...

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

TFTPServer

Le funzionalità del server differiscono da quelle del client in quanto le comandi non arrivano dalla tastiera, ma dal socket. Alcuni metodi coincidono, quindi non li riporterò, tratterò solo le differenze.

Per avviare, qui si utilizza il metodo run, che riceve in input la porta e gestisce i dati in entrata dal socket in un ciclo infinito.

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

Il metodo put, che è un involucro del metodo writeToFileFromSocket, apre un flusso di scrittura in un file e registra tutti i byte in ingresso dal socket; al termine della scrittura, visualizza un messaggio di completamento della trasmissione con successo.

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

Il metodo get permette di ricevere un file dal server. Come già detto nella sezione relativa al lato client del programma, per una trasmissione di file riuscita è necessario conoscerne la dimensione, che viene memorizzata in un long, quindi la scompongo in un array di 4 byte, li invio byte per byte al socket e poi, ricevendo e assemblando nuovamente in un numero sul client, invio tutti i byte che compongono il file, letti dal flusso di input dal file.


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

Il metodo getAndParseInput coincide con l'analogo nel client, con l'unica differenza che legge i dati dal socket, non dalla tastiera. Il codice nel repository, così come il selector.
In questo caso, l'inizializzazione è stata estratta in un blocco di codice separato, poiché nell'ambito di questa implementazione, al termine della trasmissione, le risorse vengono liberate e nuovamente occupate — ancora una volta per garantire la protezione dalle perdite di memoria.

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

In sintesi:

Abbiamo appena scritto la nostra variazione su un semplice protocollo di trasmissione dei dati e compreso come dovrebbe funzionare. In sostanza, non ho scoperto nulla di nuovo e non ho scritto niente di particolarmente innovativo, ma — non c'erano articoli analoghi su Habr, e nel contesto della scrittura di una serie di articoli sugli strumenti cmd, non potevo evitarlo.

Link:

Repository con il codice sorgente
Breve introduzione a TFTP
Lo stesso, ma in russo

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS 🔥 Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS - ProHoster