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

Continuando la serie di articoli dedicati alle implementazioni personalizzate delle utilità della console Windows, non possiamo fare a meno di toccare TFTP (Trivial File Transfer Protocol), un semplice protocollo di trasferimento file.

Come l'ultima volta, ripercorriamo brevemente la teoria, vediamo il codice che implementa funzionalità simili a quella richiesta e analizziamolo. Maggiori dettagli - sotto il taglio

Non copierò e incollerò le informazioni di riferimento, i cui collegamenti tradizionalmente si trovano alla fine dell'articolo, dirò solo che, in sostanza, TFTP è una variazione semplificata del protocollo FTP, in cui l'impostazione del controllo dell'accesso ha stato rimosso, e in effetti qui non c'è nulla tranne i comandi per ricevere e trasferire un file . Tuttavia, per rendere la nostra implementazione un po' più elegante e adattata agli attuali principi di scrittura del codice, la sintassi è stata leggermente modificata: ciò non cambia i principi di funzionamento, 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 (causa incompatibilità con il protocollo standard, ho ritenuto opportuno lasciare all'utente la possibilità di selezionare una porta), dopodiché viene visualizzato un si verifica una connessione, a seguito della 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, tradizionalmente utilizzavo 4 classi:

  • TFTP Client
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

Dato che esistono classi di test solo per il debug di quelle principali, non le analizzerò, ma il codice sarà nel repository, il collegamento ad esso può essere trovato alla fine dell'articolo. Adesso esaminerò le classi principali.

TFTP Client

Il compito di questa classe è connettersi a un server remoto tramite il suo IP e numero di porta, leggere un comando dal flusso di input (in questo caso, la tastiera), analizzarlo, trasferirlo al server e, a seconda che tu è necessario inviare o ricevere un file, trasferirlo o ottenerlo.

Il codice per avviare il client per connettersi al server e attendere un comando dal flusso di input è simile al seguente. Un certo numero di variabili globali utilizzate qui sono descritte all'esterno dell'articolo, nel testo completo del programma. Data la loro banalità non li cito per non sovraccaricare 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 il file viene inviato: utilizzando uno scanner, presentiamo il contenuto del file come un array di byte, che scriviamo uno per uno nel socket, dopodiché lo chiudiamo e lo apriamo di nuovo (non la soluzione più ovvia, ma garantisce il rilascio delle risorse), dopodiché visualizziamo un messaggio di avvenuto trasferimento.

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 il recupero dei dati dal server. Tutto è ancora una volta banale, interessa solo il primo blocco di codice. Per capire esattamente quanti byte devono essere letti dal socket, è necessario sapere quanto pesa il file trasferito. La dimensione del file sul server è rappresentata come un numero intero lungo, quindi qui vengono accettati 4 byte, che vengono successivamente convertiti in un numero. Questo non è un approccio molto Java, è piuttosto simile per SI, ma risolve il problema.

Quindi tutto è banale: riceviamo un 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());
       }
    }

Se nella finestra del client è stato immesso un comando diverso da get o put, verrà richiamata la funzione showErrorMessage, che indica che l'input non era corretto. Per banalità non lo citerò. Un po' più interessante è la funzione di ricevere e dividere la stringa di input. Passiamo al suo interno lo scanner, dal quale ci aspettiamo di ricevere una riga separata 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("Bad input");
        }
    }

Invio di un comando: trasmette il comando immesso dallo scanner al socket e ne forza l'invio

    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 selettore è una funzione che determina le azioni del programma in base alla stringa immessa. Qui non è tutto molto bello e il trucco utilizzato non è dei migliori, quello con l'uscita forzata all'esterno del blocco di codice, ma la ragione principale di ciò è l'assenza in Java di alcune cose, come i delegati in C#, i puntatori a funzioni da C++, o a almeno il terribile e terribile goto, che ti consente di implementarlo magnificamente. Se sai come rendere il codice un po' più elegante, accolgo con favore le critiche nei commenti. Mi sembra che qui sia necessario un dizionario String-delegato, ma non esiste alcun 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 dalla funzionalità del client, in generale, solo per il fatto che i comandi non arrivano dalla tastiera, ma dal socket. Alcuni metodi sono generalmente gli stessi, quindi non li citerò, toccherò solo le differenze.

Per iniziare viene utilizzato il metodo run, che riceve una porta come input ed elabora i dati di input dal socket in un ciclo eterno.

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

Il metodo put, che esegue il wrapper del metodo writeToFileFromSocket che apre un flusso di scrittura su un file e scrive tutti i byte di input dal socket, visualizza un messaggio che indica il completamento positivo del trasferimento al termine della scrittura.

    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 recupera il file del server. Come già accennato nella sezione sul lato client del programma, per trasferire con successo un file è necessario conoscerne la dimensione, memorizzata in un intero lungo, quindi l'ho diviso in un array di 4 byte, li trasferisco byte per byte al socket, quindi, dopo averli ricevuti e assemblati 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 è lo stesso del client, l'unica differenza è che legge i dati dal socket anziché dalla tastiera. Il codice è nel repository, proprio come il selettore.
In questo caso, l'inizializzazione viene inserita in un blocco di codice separato, perché all'interno di questa implementazione, una volta completato il trasferimento, le risorse vengono rilasciate e rioccupate nuovamente, sempre per fornire protezione contro perdite di memoria.

    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:

Abbiamo appena scritto la nostra variazione su un semplice protocollo di trasferimento dati e abbiamo capito come dovrebbe funzionare. In linea di principio, non ho scoperto l'America qui e non ho scritto molte cose nuove, ma non c'erano articoli simili su Habré e, come parte della scrittura di una serie di articoli sulle utilità cmd, era impossibile non toccarlo.

Links:

Repository del codice sorgente
Brevemente sul TFTP
La stessa cosa, ma in russo

Fonte: habr.com

Aggiungi un commento