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

Continuando con la serie de artículos sobre implementaciones personalizadas de utilidades de consola en Windows, TFTP (Trivial File Transfer Protocol) es un protocolo simple de transferencia de archivos.

Como la última vez, revisamos brevemente la teoría, vemos un código que implementa una función similar a la requerida y la analizamos. Leer más - debajo del corte

No copiaré y pegaré la información de referencia, cuyos enlaces se pueden encontrar tradicionalmente al final del artículo, solo diré que, en esencia, TFTP es una variación simplificada del protocolo FTP en el que se elimina la configuración de control de acceso, y de hecho no hay nada aquí excepto los comandos para recibir y transferir el 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 cambia ligeramente: 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, al iniciar, 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 la opción de seleccionar el puerto para el usuario), después de lo cual se produce una 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 la implementación del protocolo, tradicionalmente he usado 4 clases:

  • TFTPClient
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

Debido al hecho de que las clases de prueba solo existen 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. Y ahora entenderé las clases principales.

TFTPClient


La tarea de esta clase es conectarse al servidor remoto por su IP y número de puerto, leer un comando de la secuencia de entrada (en este caso, el teclado), analizarlo, transferirlo al servidor y, dependiendo de si desea transferir o recibir el archivo, transferirlo o para recibir.

El código de inicio del cliente para conectarse al servidor y esperar un comando del flujo de entrada tiene este aspecto. Una serie de variables globales que se utilizan aquí se describen fuera del artículo, en el texto completo del programa. Debido a su trivialidad, no 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: con el escáner, presentamos el contenido del archivo como una matriz de bytes, que escribimos en el socket uno por uno, luego lo cerramos y lo volvemos a abrir (no es la solución más obvia, pero garantiza la liberación de recursos), después de lo cual mostramos un mensaje sobre el éxito transmisión

 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("\nDone\n"); } catch (Exception e) { System.out.println(e.getMessage()); } } 

Este fragmento de código describe la recepción de datos del servidor. Todo es trivial nuevamente, solo el primer bloque de código es de interés. Para comprender exactamente cuántos bytes necesita leer desde el socket, necesita saber cuánto pesa el archivo transferido. El tamaño del archivo en el servidor parece ser un entero largo, por lo que aquí se aceptan 4 bytes, que posteriormente se convierten en un solo número. Este no es un enfoque muy Java, es bastante similar para SI, pero resuelve su problema.

Entonces todo es trivial: obtenemos un número conocido 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("\nDONE\n"); } catch (Exception e){ System.out.println(e.getMessage()); } } 

Si se ingresó un comando diferente a get o put en la ventana del cliente, se llamará a la función showErrorMessage, que muestra la incorrección de la entrada. Debido a la trivialidad, no cito. Algo más interesante es la función de obtener y dividir la cadena de entrada. Le pasamos un escáner, desde el cual esperamos recibir una línea separada por dos espacios y que contiene un comando, dirección de origen y 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 comandos: envía el comando ingresado desde el escáner al zócalo y obliga a enviarlo

  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 de un programa dependiendo de la cadena de entrada. Aquí no todo es muy hermoso y se usa el truco no tan bueno para forzar el uso 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 a una función de C ++, o al menos goto aterrador y terrible, que deja que te des cuenta maravillosamente. Si sabe cómo hacer que el código sea un poco más elegante, estoy esperando críticas en los comentarios. Me parece que necesitamos un diccionario String-delegate, pero no hay delegado ...

  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 funcionalidad del servidor difiere de la funcionalidad del cliente en general solo en que los comandos no provienen del teclado, sino del zócalo. Algunos de los métodos coinciden, así que no los daré; solo mencionaré las diferencias.

Para comenzar aquí, se utiliza el método de ejecución, que recibe un puerto para la entrada y procesa los datos de entrada del socket en un ciclo eterno.

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

El método put, que es un contenedor del método writeToFileFromSocket, que abre la secuencia de escritura en un archivo y escribe todos los bytes de entrada del socket, después de que se completa la grabación, muestra un mensaje sobre la finalización exitosa de la transferencia.

  private void put(String source, String dest){ writeToFileFromSocket(); System.out.print("\nDone\n"); }; 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 proporciona un archivo de servidor. Como ya se mencionó en la sección en el lado del cliente del programa, para transferir con éxito un archivo, debe conocer su tamaño, almacenado en un entero largo, por lo que lo divido en una matriz de 4 bytes, los transfiero al byte del socket y luego, después de recibirlos y recopilarlos en el cliente De vuelta al número, transfiero todos los bytes que componen el archivo, leídos del 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 los datos del socket y no del teclado. El código en el repositorio, como selector.
En este caso, la inicialización se realiza en un bloque de código separado, porque En el marco de esta implementación, una vez completada la transferencia, los recursos se liberan y se vuelven a ocupar nuevamente, nuevamente con el objetivo de proporcionar 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 variación en un protocolo simple de transferencia de datos y descubrimos cómo debería funcionar. En principio, no descubrí América y no escribí muchas novedades, pero no había artículos similares sobre Habré, y como parte de la redacción de una serie de artículos sobre utilidades de cmd era imposible no tocarlo.

Referencias

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

Source: https://habr.com/ru/post/461083/


All Articles