Escribir software con la funcionalidad de las utilidades cliente-servidor de Windows, parte 02

Continuando con la serie actual de artículos dedicados a implementaciones personalizadas de las utilidades de la consola de Windows, no podemos evitar tocar el TFTP (Protocolo trivial de transferencia de archivos), un protocolo simple de transferencia de archivos.

Como la última vez, repasemos brevemente la teoría, veamos el código que implementa una funcionalidad similar a la requerida y analicémoslo. Más detalles - debajo del corte

No copiaré y pegaré información de referencia, cuyos enlaces tradicionalmente se pueden encontrar al final del artículo, solo diré que, en esencia, TFTP es una variación simplificada del protocolo FTP, en el que la configuración de control de acceso Se ha eliminado y, de hecho, aquí no hay nada excepto comandos para recibir y transferir un archivo. Sin embargo, para que nuestra implementación sea un poco más elegante y adaptada a los principios actuales de escritura de código, la sintaxis se ha cambiado ligeramente; esto no cambia los principios de funcionamiento, pero la interfaz, en mi humilde opinión, se vuelve un poco más lógica y combina los aspectos positivos de FTP y TFTP.

En particular, cuando se inicia, el cliente solicita la dirección IP del servidor y el puerto en el que está abierto el TFTP personalizado (debido a la incompatibilidad con el protocolo estándar, consideré apropiado dejar al usuario la posibilidad de seleccionar un puerto), después de lo cual se Se produce la conexión, como resultado de lo cual el cliente puede enviar uno de los comandos: obtener o poner, para recibir o enviar un archivo al servidor. Todos los archivos se envían en modo binario para simplificar la lógica.

Para implementar el protocolo, tradicionalmente utilicé 4 clases:

  • Cliente TFTP
  • Servidor TFTP
  • TFTPClienteProbador
  • Probador de servidor TFTP

Debido al hecho de que las clases de prueba existen solo para depurar las principales, no las analizaré, pero el código estará en el repositorio, se puede encontrar un enlace al final del artículo. Ahora miraré las clases principales.

Cliente TFTP

La tarea de esta clase es conectarse a un servidor remoto por su IP y número de puerto, leer un comando del flujo de entrada (en este caso, el teclado), analizarlo, transferirlo al servidor y, dependiendo de si necesita enviar o recibir un archivo, transferirlo u obtenerlo.

El código para iniciar el cliente para conectarse al servidor y esperar un comando del flujo de entrada se ve así. Varias variables globales que se utilizan aquí se describen fuera del artículo, en el texto completo del programa. Por su trivialidad no los cito para no sobrecargar el artículo.

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

Repasemos los métodos llamados en este bloque de código:

Aquí se envía el archivo: usando un escáner, presentamos el contenido del archivo como una matriz de bytes, que escribimos uno por uno en el socket, después de lo cual lo cerramos y lo abrimos nuevamente (no es la solución más obvia, pero garantiza la liberación de recursos), después de lo cual mostramos un mensaje sobre la transferencia exitosa.

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 la recuperación de datos del servidor. Todo vuelve a ser trivial, sólo el primer bloque de código es de interés. Para saber exactamente cuántos bytes se deben leer del socket, necesita saber cuánto pesa el archivo transferido. El tamaño del archivo en el servidor se representa como un número entero largo, por lo que aquí se aceptan 4 bytes, que posteriormente se convierten en un número. Este no es un enfoque muy Java, es bastante similar para SI, pero resuelve su problema.

Entonces todo es trivial: recibimos una cantidad conocida de bytes del socket y los escribimos en un archivo, después de lo cual mostramos un mensaje 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());
       }
    }

Si se ingresó un comando distinto de get o put en la ventana del cliente, se llamará a la función showErrorMessage, lo que indica que la entrada fue incorrecta. Por trivialidad no lo citaré. Algo más interesante es la función de recibir y dividir la cadena de entrada. Le pasamos el escáner, del cual esperamos recibir una línea separada por dos espacios y que contiene el comando, la dirección de origen y la dirección 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");
        }
    }

Envío de un comando: transmite el comando ingresado desde el escáner al socket y fuerza su 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 es una función que determina las acciones del programa dependiendo de la cadena ingresada. Aquí no todo es muy bonito y el truco utilizado no es el mejor, con salida forzada fuera del bloque de código, pero la razón principal de esto es la ausencia en Java de algunas cosas, como delegados en C#, punteros de función de C++ o en al menos el terrible y terrible goto, que te permite implementar esto maravillosamente. Si sabes cómo hacer que el código sea un poco más elegante, agradezco las críticas en los comentarios. Me parece que aquí se necesita un diccionario de delegado de cadenas, pero no hay 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

La funcionalidad del servidor difiere de la funcionalidad del cliente, en general, solo en que los comandos no llegan desde el teclado, sino desde el socket. Algunos de los métodos son generalmente los mismos, por lo que no los citaré, solo mencionaré las diferencias.

Para empezar se utiliza el método run, que recibe un puerto como entrada y procesa los datos de entrada del socket en un bucle eterno.

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

El método put, que encapsula el método writeToFileFromSocket que abre una secuencia de escritura en un archivo y escribe todos los bytes de entrada desde el socket, muestra un mensaje que indica la finalización exitosa de la transferencia cuando se completa la 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());
        }
    }

El método get recupera el archivo del servidor. Como ya se mencionó en la sección sobre el lado del cliente del programa, para transferir exitosamente un archivo necesita saber su tamaño, almacenado en un número entero largo, así que lo dividí en una matriz de 4 bytes, los transfiero byte por byte. al socket, y luego, después de recibirlos y ensamblarlos en el cliente en un número, transfiero todos los bytes que componen el archivo, leo desde el flujo de entrada del archivo.


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

El método getAndParseInput es el mismo que en el cliente, la única diferencia es que lee datos desde el socket en lugar de desde el teclado. El código está en el repositorio, al igual que el selector.
En este caso, la inicialización se coloca en un bloque de código separado, porque Dentro de esta implementación, una vez completada la transferencia, los recursos se liberan y se vuelven a ocupar, nuevamente para brindar protección contra pérdidas 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());
        }
    }

En resumen:

Acabamos de escribir nuestra propia variación de un protocolo de transferencia de datos simple y descubrimos cómo debería funcionar. En principio, no descubrí América aquí y no escribí muchas cosas nuevas, pero no había artículos similares sobre Habré y, como parte de la escritura de una serie de artículos sobre las utilidades cmd, era imposible no tocarlo.

Enlaces:

Repositorio de código fuente
Brevemente sobre TFTP
Lo mismo pero en ruso.

Fuente: habr.com

Añadir un comentario