Escritura de software con funcionalidade de utilidade cliente-servidor Windows, parte 02

Continuando a serie de artigos dedicados ás implementacións personalizadas de utilidades de consola Windows É imposible non mencionar o TFTP (Protocolo trivial de transferencia de ficheiros), un protocolo sinxelo de transferencia de ficheiros.

Como a última vez, revisaremos brevemente a teoría, observaremos o código que implementa unha funcionalidade similar á requirida e analizarémolo. Máis detalles a continuación.

Non vou copiar nin pegar a información de referencia, cuxas ligazóns se atopan tradicionalmente ao final do artigo. Só direi que TFTP é esencialmente unha versión simplificada do protocolo FTP, que elimina a configuración de control de acceso e consiste esencialmente en nada máis que comandos de transferencia e recuperación de ficheiros. Non obstante, para facer a nosa implementación un pouco máis elegante e adaptable aos principios de codificación modernos, modificouse lixeiramente a sintaxe. Isto non cambia os principios de funcionamento, pero a interface, na miña opinión, vólvese un pouco máis lóxica e combina os aspectos positivos de FTP e TFTP.

En concreto, ao iniciarse, o cliente solicita o enderezo IP do servidor e o porto no que está aberto o TFTP personalizado (debido á incompatibilidade co protocolo estándar, pensei que era apropiado deixarlle a selección do porto ao usuario). A continuación, establécese unha conexión, o que permite ao cliente enviar un dos comandos (get ou put) para recuperar ou enviar un ficheiro ao servidor. Todos os ficheiros envíanse en modo binario para simplificar a lóxica.

Para implementar o protocolo, tradicionalmente empregaba 4 clases:

  • Cliente TFTPC
  • Servidor TFTP
  • Proba de clientes TFTPClientTester
  • Probador de servidor TFTP

Dado que as clases de proba só existen para depurar as clases principais, non as abordarei. O código estará no repositorio; pódese atopar unha ligazón a el ao final do artigo. Agora abordarei as clases principais.

Cliente TFTPC

A tarefa desta clase é conectarse a un servidor remoto mediante o seu enderezo IP e número de porto, ler un comando do fluxo de entrada (neste caso, o teclado), analizalo, transmitilo ao servidor e, dependendo de se hai que transmitir ou recibir un ficheiro, transmitilo ou recibilo.

O código para iniciar o cliente para conectarse ao servidor e esperar un comando do fluxo de entrada ten este aspecto. Varias variables globais empregadas aquí descríbense fóra do artigo, no texto completo do programa. Debido á súa trivialidade, non as inclúo aquí para evitar sobrecargar o artigo.

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

Vexamos os métodos chamados neste bloque de código:

Aquí é onde se envía o ficheiro: usando o escáner, representamos o contido do ficheiro como unha matriz de bytes, que escribimos un por un no socket, logo pechámolo e volvémolo abrir (non é a solución máis obvia, pero garante a liberación de recursos), tras o cal mostramos unha mensaxe sobre a transferencia correcta.

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

Este fragmento de código describe a recepción de datos do servidor. De novo, todo é trivial; só o primeiro bloque de código é de interese. Para comprender exactamente cantos bytes hai que ler do socket, precisamos saber o tamaño do ficheiro que se está a transferir. O tamaño do ficheiro no servidor represéntase como un enteiro longo, polo que aquí se reciben catro bytes, que posteriormente se converten nun único número. Esta non é unha estratexia moi semellante a Java; é máis ben unha estratexia semellante a C, pero cumpre a súa función.

Entón todo é trivial: recibimos un número coñecido de bytes do socket e escribímolos nun ficheiro, despois do cal mostramos unha mensaxe de éxito.

   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 se introduciu un comando diferente a "get" ou "put" na xanela do cliente, chamarase a función showErrorMessage, indicando que a entrada non era válida. Debido á súa trivialidade, non a incluirei. A función para recibir e dividir a cadea de entrada é algo máis interesante. Pasámoslle un escáner, do que esperamos recibir unha cadea separada por dous espazos, que contén o comando, o enderezo de orixe e o enderezo de destino.

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

Enviar un comando: transferir un comando introducido desde o escáner ao socket e forzar o seu envío

    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 é unha función que determina as accións do programa en función da cadea de entrada. Non é moi elegante e emprega unha técnica bastante pouco sofisticada para forzar o código a saír do bloque, pero a razón principal disto é que Java carece de certas características, como delegados en C#, punteiros de función en C++ ou mesmo o temido goto, que permitiría unha implementación máis elegante. Se sabes como facer o código un pouco máis elegante, comparte as túas ideas nos comentarios. Creo que aquí se necesita un dicionario de delegados de cadeas, pero non hai ningún delegado...

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

Servidor TFTP

A funcionalidade do servidor difire da funcionalidade do cliente esencialmente só en que os comandos se reciben a través dun socket en lugar dun teclado. Algúns métodos son en realidade iguais, polo que non os abordarei aquí; só mencionarei as diferenzas.

Para iniciar o programa, utilízase o método run, que recibe un porto como entrada e procesa os datos de entrada do socket nun bucle infinito.

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

O método put, que encapsula o método writeToFileFromSocket, abre un fluxo de escritura nun ficheiro e escribe todos os bytes de entrada desde o socket e imprime unha mensaxe sobre a finalización correcta da transferencia unha vez finalizada a escritura.

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

O método get recupera un ficheiro do servidor. Como se mencionou na sección do lado do cliente, para transferir un ficheiro correctamente, preciso coñecer o seu tamaño, almacenado como un enteiro longo. Polo tanto, divídoo nunha matriz de 4 bytes, transfíroo byte a byte ao socket e, despois de recibilo e reensamblalo nun número no cliente, transfiro todos os bytes que compoñen o ficheiro, lidos do fluxo de entrada do ficheiro.


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

O método getAndParseInput é idéntico ao método do cliente, agás que le os datos dun socket en lugar de do teclado. O código está no repositorio, do mesmo xeito que o selector.
Neste caso, a inicialización móvese a un bloque de código separado, xa que no marco desta implementación, unha vez completada a transferencia, os recursos libéranse e reasignanse de novo, de novo para garantir a protección contra as fugas de 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());
        }
    }

Para resumir:

Acabamos de escribir a nosa propia variación do protocolo simple de transferencia de datos e de descubrir como debería funcionar. Non abrín nada novo nin dixen nada particularmente novo aquí, pero non había artigos semellantes sobre Habr e, como parte dunha serie de artigos sobre utilidades cmd, era imposible non mencionalo.

Referencias:

Repositorio de código fonte
Brevemente sobre TFTP
O mesmo, pero en ruso.

Fonte: www.habr.com

Compre hospedaxe fiable para sitios con protección DDoS, servidores VPS VDS 🔥 Compra aloxamento web fiable con protección DDoS, servidores VPS VDS | ProHoster