Software de gravação com a funcionalidade dos utilitários cliente-servidor Windows, parte 02

Continuando a série de artigos sobre implementações personalizadas de utilitários de console no Windows, o TFTP (Trivial File Transfer Protocol) é um protocolo simples de transferência de arquivos.

Como da última vez, revisamos brevemente a teoria, vemos um código que implementa uma funcionalidade semelhante à necessária e a analisamos. Leia mais - sob o corte

Não copio e colo as informações de referência, cujos links tradicionalmente podem ser encontrados no final do artigo, apenas digo que, em essência, o TFTP é uma variação simplificada do protocolo FTP no qual a configuração do controle de acesso é removida e, na verdade, não há nada aqui, exceto os comandos para receber e transferir o arquivo . No entanto, para tornar nossa implementação um pouco mais elegante e adaptada aos princípios atuais de escrever código, a sintaxe é ligeiramente alterada - ela não altera os princípios de trabalho, mas a interface IMHO se torna um pouco mais lógica e combina os aspectos positivos do FTP e TFTP.

Em particular, ao iniciar, o cliente solicita o endereço IP do servidor e a porta na qual o TFTP personalizado está aberto (devido à incompatibilidade com o protocolo padrão, considerei apropriado deixar a opção de selecionar a porta para o usuário), após o que ocorre uma conexão, como resultado do qual o cliente pode enviar um dos comandos - obter ou colocar, para receber ou enviar um arquivo para o servidor. Todos os arquivos são enviados no modo binário - para simplificar a lógica.

Para a implementação do protocolo, tradicionalmente usei 4 classes:

  • TFTPClient
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

Devido ao fato de as classes de teste existirem apenas para depuração das principais, não as analisarei, mas o código estará no repositório, um link para ele pode ser encontrado no final do artigo. E agora vou entender as principais classes.

TFTPClient


A tarefa desta classe é conectar-se ao servidor remoto por seu número de IP e porta, ler um comando do fluxo de entrada (nesse caso, o teclado), analisá-lo, transferi-lo para o servidor e, dependendo de você deseja transferir ou receber o arquivo, transferi-lo ou para receber.

O código de ativação do cliente para conectar-se ao servidor e aguardar um comando do fluxo de entrada é semelhante a este. Várias variáveis ​​globais usadas aqui são descritas fora do artigo, no texto completo do programa. Devido à sua trivialidade, não cito para não sobrecarregar 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()); } } 

Vamos examinar os métodos chamados neste bloco de código:

Aqui o arquivo é enviado - usando o scanner, apresentamos o conteúdo do arquivo como uma matriz de bytes, que escrevemos no soquete um a um, depois o fechamos e o reabrimos (não é a solução mais óbvia, mas garante a liberação de recursos), após o qual exibimos uma mensagem sobre o sucesso transmissão.

 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 descreve o recebimento de dados do servidor. Tudo é trivial novamente, apenas o primeiro bloco de código é interessante. Para entender exatamente quantos bytes você precisa ler do soquete, você precisa saber quanto pesa o arquivo transferido. O tamanho do arquivo no servidor parece ser um número inteiro longo; portanto, são aceitos 4 bytes aqui, que são posteriormente convertidos em um único número. Esta não é uma abordagem muito Java, é bastante semelhante para o SI, mas resolve seu problema.

Então tudo é trivial - obtemos um número conhecido de bytes do soquete e os gravamos em um arquivo, após o qual exibimos uma mensagem de sucesso.

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

Se um comando diferente de get ou put tiver sido inserido na janela do cliente, a função showErrorMessage será chamada, mostrando a incorreta da entrada. Devido à trivialidade - não cito. Um pouco mais interessante é a função de obter e dividir a sequência de entrada. Passamos um scanner para ele, do qual esperamos receber uma linha separada por dois espaços e contendo um comando, endereço de origem e endereço 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"); } } 

Envio de comando - enviando o comando digitado do scanner para o soquete e forçando o envio

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

Um seletor é uma função que determina as ações de um programa, dependendo da sequência de entrada. Tudo não é muito bonito aqui e é usado o truque não tão bom de forçar o bloco de código a ser usado, mas a principal razão para isso é a ausência em algumas coisas do Java, como delegados em C #, ponteiros para uma função do C ++ ou pelo menos assustador e terrível, que deixe você perceber isso lindamente. Se você sabe como tornar o código um pouco mais elegante, estou aguardando críticas nos comentários. Parece-me que um dicionário de delegado de Cadeia de caracteres é necessário aqui, mas não há 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


A funcionalidade do servidor difere da funcionalidade do cliente em geral apenas no sentido de que os comandos não vêm do teclado, mas do soquete. Alguns dos métodos coincidem, então eu não os darei; apenas mencionarei as diferenças.

Para começar aqui, é usado o método run, que recebe uma porta para entrada e processa dados de entrada do soquete em um ciclo eterno.

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

O método put, que é um invólucro do método writeToFileFromSocket, que abre o fluxo de gravação no arquivo e grava todos os bytes de entrada do soquete, após a conclusão da gravação, exibe uma mensagem sobre a conclusão bem-sucedida da transferência.

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

O método get fornece um arquivo do servidor. Como já mencionado na seção no lado do cliente do programa, para transferir com êxito um arquivo, você precisa saber seu tamanho, armazenado em um número inteiro longo, então eu o divido em uma matriz de 4 bytes, transfiro-os para o byte de soquete e, depois de recebê-los e colecioná-los no cliente de volta ao número, transfiro todos os bytes que compõem o arquivo, lidos do fluxo de entrada do arquivo.

 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 é o mesmo do cliente, a única diferença é que ele lê dados do soquete e não do teclado. O código no repositório, como seletor.
Nesse caso, a inicialização é feita em um bloco de código separado, porque na estrutura desta implementação, após a conclusão da transferência, os recursos são liberados e ocupados novamente, novamente com o objetivo de fornecer proteção contra vazamentos de memória.

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

Em resumo:

Acabamos de escrever nossa variação em um protocolo simples de transferência de dados e descobrimos como deve funcionar. Em princípio, não descobri a América e não escrevi muito novo, mas - não havia artigos semelhantes sobre Habré e, como parte de escrever uma série de artigos sobre os utilitários do cmd, era impossível não tocá-lo.

Referências:

Repositório do código fonte
Brevemente sobre TFTP
A mesma coisa, mas em russo

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


All Articles