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

Continuando il ciclo di articoli dedicati alle implementazioni personalizzate delle utilità console di Windows, non si può non menzionare il TFTP (Trivial File Transfer Protocol) — un protocollo semplice per la trasmissione di file.

Come la volta scorsa, daremo un'occhiata alla teoria, vedremo il codice che implementa funzionalità simili a quelle richieste e lo analizzeremo. Maggiori dettagli — di seguito.

Non copierò informazioni di riferimento, i cui link possono essere tradizionalmente trovati alla fine dell'articolo, dirò solo che, essenzialmente, il TFTP è una versione semplificata del protocollo FTP, in cui è stata rimossa la configurazione del controllo accessi, e in effetti qui non c'è altro che comandi per ricevere e inviare file. Tuttavia, per rendere la nostra implementazione un po' più elegante e adattata ai moderni principi 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 i lati positivi di FTP e TFTP.

In particolare, quando si avvia, il client richiede l'indirizzo IP del server e la porta su cui è aperto il TFTP personalizzato (data l'incompatibilità con il protocollo standard, ho ritenuto opportuno lasciare all'utente la possibilità di scegliere la porta), dopodiché avviene la connessione, grazie alla quale il client può inviare uno dei comandi — get o put, per ricevere o inviare un 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

Dato che le classi di test esistono solo per il debug delle classi principali, non le esaminerò, ma il codice sarà disponibile nel repository e un link ad esso sarà fornito alla fine dell'articolo. Ora esaminerò le classi principali.

TFTPClient

Lo scopo di questa classe è connettersi a un server remoto tramite il suo IP e il numero di porta, leggere dal flusso di input (in questo caso — dalla tastiera) il comando, analizzarlo, inviarlo al server e, a seconda che sia necessaria la trasmissione o la ricezione di un file, trasmetterlo o riceverlo.

Il codice di avvio del client per connettersi al server e attendere un comando dal flusso di input appare così. Una serie di variabili globali utilizzate qui è descritta al di fuori dell'articolo, nel testo completo del programma. A causa della loro banalità, non le riporto 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: con l'ausilio dello scanner rappresentiamo il contenuto del file come un array di byte, che scriviamo sequenzialmente nel socket, dopo di che lo chiudiamo e lo riapriamo (una soluzione non così ovvia, ma garantisce il rilascio delle risorse), dopodiché visualizziamo sullo schermo 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 da un server. Tutto è piuttosto semplice, l'interesse è solo nel primo blocco di codice. Per capire quanti byte devono essere letti dal socket, è necessario sapere quanto pesa il file trasmesso. La dimensione del file sul server è rappresentata da un intero lungo, quindi qui vengono presi 4 byte, che successivamente vengono convertiti in un numero. Questo non è un metodo proprio di Java, è più simile a C, ma risolve il suo compito.

Poi è tutto semplice: riceviamo un numero noto di byte dal socket e li registriamo 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());
       }
    }

Se viene inserito un comando diverso da get o put nella finestra del client, verrà chiamata la funzione showErrorMessage, che mostra l'errore di input. Poiché è banale, non lo elenco. Più interessante è la funzione che riceve e divide la stringa di input. A questa passiamo uno scanner, dal quale ci aspettiamo di ricevere una stringa divisa da due spazi e contenente il comando, l'indirizzo di origine 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 errato");
        }
    }

Invio del comando — trasferimento del comando inserito dallo scanner al socket e invio forzato.

    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 definisce le azioni del programma in base alla stringa di input fornita. Qui le cose non sono molto eleganti e viene utilizzato un metodo non ottimale per forzare l'uscita dal blocco di codice, ma la ragione principale è l'assenza in Java di alcuni elementi, come i delegati in C#, i puntatori a funzione da C++ o persino il temuto e terribile goto, che permetterebbero di realizzare il codice in modo più elegante. Se hai suggerimenti su come rendere il codice più raffinato, attendo le tue critiche nei commenti. Mi sembra che serva un dizionario String-delegate, ma manca il delegato…

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

TFTPServer

La funzionalità del server differisce da quella del client principalmente perché i comandi arrivano non dalla tastiera, ma da un socket. Alcuni metodi coincidono, quindi non li elencherò, ma toccherò solo le differenze.

Per l'avvio qui si usa il metodo run, che riceve in input una porta e gestisce i dati in entrata dal socket in un ciclo infinito.

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

Il metodo put, che è un involucro del metodo writeToFileFromSocket, apre uno stream di scrittura in un file e scrive tutti i byte in ingresso dal socket; al termine della scrittura, visualizza un messaggio di successo nella trasmissione.

    private void put(String source, String dest){
            writeToFileFromSocket();
            System.out.print("nFatto");
    };
    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 consente di ottenere il file dal server. Come già accennato nella sezione riguardante il lato client dell'applicazione, per una corretta trasmissione del file è necessario conoscere la sua dimensione, memorizzata in un intero lungo, quindi lo suddivido in un array di 4 byte, li trasmetto singolarmente al socket e poi, dopo averli ricevuti e ricomposti sul client in un numero, trasferisco 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 è simile a quello del client, con l'unica differenza che legge i dati dal socket e non dalla tastiera. Il codice nel repository è lo stesso di selector.
In questo caso, l'inizializzazione è stata estratta in un blocco di codice separato, poiché in questa implementazione, dopo la conclusione della trasmissione, le risorse vengono liberate e di nuovo impiegate — ancora una volta con l'obiettivo di garantire la protezione contro il memory leak.

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

In sintesi:

Appena abbiamo scritto la nostra variazione su un semplice protocollo di trasferimento dati e abbiamo compreso come dovrebbe funzionare. In sostanza, non ho scoperto nulla di nuovo, ma — non c'erano articoli simili su Habr, e nell'ambito della scrittura di un ciclo di articoli sulle utilità cmd non si poteva non menzionarlo.

Link:

Repository con il codice sorgente
Brevemente su TFTP
Stessa cosa, ma in russo

Fonte: habr.com

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