Continuando con la serie de artículos dedicada a las implementaciones personalizadas de utilidades de consola de Windows, no se puede dejar de mencionar el TFTP (Trivial File Transfer Protocol) - un protocolo simple de transferencia de archivos.
Al igual que en la ocasión anterior, haremos un breve repaso de la teoría, veremos el código que implementa funcionalidades análogas a las requeridas y lo analizaremos. Más detalles a continuación.
No voy a copiar información de referencia, cuyos enlaces se pueden encontrar tradicionalmente al final del artículo; solo diré que, en esencia, el TFTP es una variación simplificada del protocolo FTP, donde se elimina la configuración del control de acceso, y en realidad solo hay comandos para recibir y enviar archivos. Sin embargo, para hacer nuestra implementación un poco más elegante y adaptada a los principios actuales de escritura de código, la sintaxis ha sido ligeramente modificada; esto no cambia los principios de funcionamiento, pero el interface, IMHO, se vuelve un poco más lógico y combina las ventajas del FTP y del TFTP.
En particular, al iniciar, el cliente solicita la dirección IP del servidor y el puerto en el que se abre el TFTP personalizado (debido a la incompatibilidad con el protocolo estándar, he considerado apropiado dejar la opción de elección de puerto al usuario), después de lo cual se establece una conexión, como resultado de la cual el cliente puede enviar uno de los comandos - get o put, para obtener o enviar un archivo al servidor. Todos los archivos se envían en modo binario, con el fin de simplificar la lógica.
Para la implementación del protocolo, utilicé tradicionalmente 4 clases:
- TFTPClient
- TFTPServer
- TFTPClientTester
- TFTPServerTester
Dado que las clases de prueba existen solo para depurar las principales, no las discutiré, pero el código estará en el repositorio, y se puede consultar el enlace al final del artículo. Ahora analizaré las clases principales.
TFTPClient
La tarea de esta clase es conectarse a un servidor remoto por su IP y número de puerto, leer del flujo de entrada (en este caso, del teclado) un comando, parsearlo, enviarlo al servidor, y, dependiendo de si se requiere enviar o recibir un archivo, transferirlo o recibirlo.
El código que inicia el cliente para conectarse al servidor y esperar comandos desde el flujo de entrada se ve así. Un conjunto de variables globales que se utilizan aquí se describen fuera del artículo, en el texto completo del programa. Debido a su trivialidad, no las incluyo 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 un arreglo de bytes, que escribimos secuencialmente en el socket, después cerramos y lo volvemos a abrir (no es la solución más obvia, pero garantiza la liberación de recursos), tras lo cual mostramos un mensaje de transmisión 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 obtención de datos del servidor. Todo es nuevamente trivial, pero el primer bloque de código es interesante. Para saber cuántos bytes se deben leer del socket, es necesario conocer el tamaño del archivo transmitido. El tamaño del archivo en el servidor se presenta como un número entero largo; por lo tanto, aquí se aceptan 4 bytes que luego se convierten en un solo número. Este enfoque no es muy propio de Java, es más parecido a C, pero cumple su función.
Luego, todo es trivial: obtenemos el 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("nDONEn");
}
catch (Exception e){
System.out.println(e.getMessage());
}
}Si se introduce en la ventana del cliente un comando diferente de get o put, se llamará a la función showErrorMessage, que muestra la incorrectitud de la entrada. Dada su trivialidad, no la incluyo. Es un poco más interesante la función de obtención y descomposición de la cadena de entrada. En ella, pasamos el escáner, del cual esperamos obtener una cadena dividida por dos espacios y que contenga 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("Entrada incorrecta");
}
}Enviar comando — transferir el comando ingresado desde el escáner al socket y forzar 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());
}
}El selector es una función que determina las acciones del programa según la cadena introducida. Aquí todo no es muy bonito y se utiliza un método cuestionable con la salida forzada 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 funciones de C++ o, al menos, el terrible y horrible goto, que permitirían implementar esto de manera elegante. Si sabes cómo hacer el código un poco más elegante, espero críticas en los comentarios. Me parece que aquí se necesita 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 del cliente en gran medida solo en que los comandos llegan a él no desde el teclado, sino desde el socket. Parte de los métodos coinciden en la mayoría, así que no los incluiré, solo mencionaré las diferencias.
Para su ejecución, se utiliza el método run, que recibe como entrada un puerto y procesa los datos de entrada desde el 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 envoltorio del método writeToFileFromSocket, abre un flujo de escritura en un archivo y escribe todos los bytes de entrada desde el socket; al finalizar la escritura, muestra un mensaje de éxito en la transferencia.
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 se encarga de obtener un archivo del servidor. Como ya se mencionó en la sección sobre el lado del cliente del programa, para una transferencia exitosa del archivo se debe conocer su tamaño, que se almacena en un long, por lo que lo divido en un array de 4 bytes, los envío byte a byte a través del socket, y luego, al recibirlos y reconstruirlos en el cliente, transmito todos los bytes que componen el archivo, leídos 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 coincide con el de cliente, con la única diferencia de que lee datos del socket en lugar de del teclado. El código en el repositorio, al igual que el selector.
En este caso, la inicialización se ha trasladado a un bloque de código separado, ya que en esta implementación, después de finalizar la transferencia, los recursos se liberan y se vuelven a ocupar; nuevamente, con el objetivo de proteger contra fugas de memoria.
private void initialization()
{
try {
serverSocket = new ServerSocket(port);
socket = serverSocket.accept();
sin = socket.getInputStream();
sout = socket.getOutputStream();
}
catch (Exception e) {
System.out.print(e.getMessage());
}
}Resumiendo:
Acabamos de escribir nuestra variación sobre un protocolo simple de transmisión de datos y entendimos cómo debería funcionar. En principio, no he descubierto América y no he escrito nada muy nuevo, pero no había artículos similares en Habr, y en el marco de la redacción de un ciclo de artículos sobre utilidades de cmd, no podía dejar de mencionarlo.
Enlaces:
Fuente: habr.com
