Dando continuidade ao ciclo de artigos sobre implementações personalizadas de utilitários de console do Windows, não podemos deixar de mencionar o TFTP (Protocolo Trivial de Transferência de Arquivos) — um protocolo simples para transferência de arquivos.
Assim como da última vez, passaremos rapidamente pela teoria, veremos o código que implementa uma funcionalidade semelhante à requerida e a analisaremos. Mais detalhes estão abaixo.
Não vou copiar informações de referência, cujos links costumam ser encontrados no final do artigo; apenas direi que, em essência, o TFTP é uma variação simplificada do protocolo FTP, na qual a configuração de controle de acesso foi eliminada, e aqui não temos nada além dos comandos para receber e enviar arquivos. No entanto, para tornar nossa implementação um pouco mais elegante e alinhada com os princípios atuais de codificação, a sintaxe foi ligeiramente alterada — isso não muda os princípios de funcionamento, mas a interface, na minha opinião, torna-se um pouco mais lógica, combinando os aspectos positivos do FTP e do TFTP.
Em particular, ao ser iniciado, o cliente solicita o endereço IP do servidor e a porta onde o TFTP personalizado está aberto (devido à incompatibilidade com o protocolo padrão, achei adequado deixar a escolha da porta ao usuário), após o que ocorre a conexão, resultando na possibilidade do cliente enviar um dos comandos — get ou put, para receber ou enviar um arquivo ao servidor. Todos os arquivos são enviados em modo binário — para simplificar a lógica.
Para implementar o protocolo, utilizei tradicionalmente 4 classes:
- TFTPClient
- TFTPServer
- TFTPClientTester
- TFTPServerTester
Dado que as classes de teste existem apenas para depurar as principais, não vou analisá-las, mas o código estará no repositório, e um link para ele pode ser encontrado no final do artigo. Agora irei analisar as classes principais.
TFTPClient
A tarefa desta classe é conectar-se ao servidor remoto pelo seu IP e número da porta, ler do fluxo de entrada (neste caso — do teclado) o comando, analisá-lo, enviá-lo ao servidor e, dependendo de ser necessário enviar ou receber um arquivo, a operação correspondente será realizada.
O código que inicia o cliente para se conectar ao servidor e aguardar o comando do fluxo de entrada é assim. Uma série de variáveis globais utilizadas aqui são descritas fora do artigo, no texto completo do programa. Devido à sua trivialidade, não as apresento 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 analisar os métodos chamados neste bloco de código:
Aqui ocorre o envio do arquivo — utilizando o scanner, representamos o conteúdo do arquivo como um array de bytes, que são escritos sequencialmente no socket, após o que o fechamos e abrimos novamente (uma solução não tão óbvia, mas que garante a liberação dos recursos), e então exibimos uma mensagem de conclusão com sucesso.
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 trecho de código descreve a obtenção de dados do servidor. Tudo novamente é trivial, a parte interessante é apenas o primeiro bloco de código. Para entender quantos bytes precisam ser lidos do socket, é necessário saber o tamanho do arquivo sendo transferido. O tamanho do arquivo no servidor é representado como um número inteiro longo, portanto, aqui aceitamos 4 bytes, que posteriormente são convertidos em um único número. Essa abordagem não é muito típica em Java, é mais semelhante a C, mas cumpre seu propósito.
Depois, tudo é trivial — recebemos a quantidade conhecida de bytes do socket e os gravamos em um arquivo, após o que 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("nDONEn");
}
catch (Exception e){
System.out.println(e.getMessage());
}
}Caso uma comando diferente de get ou put seja inserido na janela do cliente, a função showErrorMessage será chamada, sinalizando a incorreção do input. Por ser trivial, não detalho. A função para obter e dividir a string de entrada é um pouco mais interessante. Nela, passamos o scanner, do qual esperamos receber uma string separada por dois espaços e que contém o comando, o endereço de origem e o 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("Entrada inválida");
}
}O envio do comando se refere à transmissão do comando inserido pelo scanner para o socket e ao seu envio forçado.
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());
}
}O seletor é uma função que define as ações do programa dependendo da string inserida. Aqui, a implementação não é muito elegante e utiliza uma abordagem não recomendada de saída forçada do bloco de código, mas a principal razão para isso é a falta de certos recursos em Java, como delegados em C#, ponteiros de função em C++ ou até mesmo o temido goto, que poderiam tornar a implementação mais refinada. Se você souber como deixar o código mais elegante, aguardo críticas nos comentários. Acho que aqui seria útil um dicionário String-delegate, mas não há delegados...
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 do cliente principalmente pelo fato de que os comandos não vêm do teclado, mas sim de um socket. Parte dos métodos coincide, por isso não os apresento aqui, apenas toco nas diferenças.
Para a execução, utiliza-se o método run, que recebe a porta como entrada e processa os dados de entrada do socket em um loop infinito.
public void run(int port) {
this.port = port;
initialization();
while (true) {
getAndParseInput();
selector();
}
}O método put, que é uma camada sobre o método writeToFileFromSocket, abre um fluxo de gravação em um arquivo e grava todos os bytes de entrada recebidos do socket. Após a gravação, uma mensagem de confirmação de sucesso é exibida.
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 permite a obtenção de um arquivo do servidor. Como mencionado na seção sobre o lado do cliente do programa, para uma transferência de arquivo bem-sucedida, é necessário conhecer seu tamanho, que é armazenado como um inteiro longo. Por isso, eu divido esse tamanho em um array de 4 bytes, transmito-os um a um pelo socket e, em seguida, ao recebê-los e juntá-los no cliente novamente, envio 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 é equivalente ao análogo no cliente, exceto que ele lê os dados do socket, e não do teclado. O código no repositório é similar ao selector.
Neste caso, a inicialização foi movida para um bloco de código separado, uma vez que, nesta implementação, após a conclusão da transferência, os recursos são liberados e reocasionados novamente - novamente com o objetivo de garantir a proteção contra vazamentos de memória.
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());
}
}Em resumo:
Acabamos de escrever nossa variação sobre um protocolo simples de transferência de dados e entendemos como ele deve funcionar. Em essência, não descobri a América aqui nem escrevi algo muito novo, mas - não havia artigos semelhantes no Habr, e no contexto da redação de um ciclo de artigos sobre utilitários cmd, não poderia deixar de abordá-lo.
Links:
Fonte: habr.com
