Escrevendo software com funcionalidade de utilitários cliente-servidor do Windows, parte 02

Continuando a série contínua de artigos dedicados a implementações personalizadas de utilitários do console do Windows, não podemos deixar de abordar o TFTP (Trivial File Transfer Protocol) - um protocolo simples de transferência de arquivos.

Como da última vez, vamos repassar brevemente a teoria, ver o código que implementa funcionalidade semelhante à necessária e analisá-lo. Mais detalhes - sob o corte

Não vou copiar e colar informações de referência, cujos links tradicionalmente podem ser encontrados no final do artigo, direi apenas que, em sua essência, o TFTP é uma variação simplificada do protocolo FTP, no qual a configuração de controle de acesso tem foi removido e, na verdade, não há nada aqui, exceto comandos para receber e transferir um arquivo. Porém, para tornar nossa implementação um pouco mais elegante e adaptada aos princípios atuais de escrita de código, a sintaxe foi ligeiramente alterada - isso não altera os princípios de funcionamento, mas a interface, IMHO, torna-se um pouco mais lógica e combina os aspectos positivos do FTP e do TFTP.

Em particular, quando iniciado, 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 ao usuário a possibilidade de selecionar uma porta), após o que um ocorre uma conexão, como resultado da qual o cliente pode 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, tradicionalmente usei 4 classes:

  • Cliente TFTP
  • Servidor TFTP
  • Testador de Cliente TFTP
  • Testador de Servidor TFTP

Como as classes de teste existem apenas para depurar as principais, não irei analisá-las, mas o código estará no repositório, um link para ele pode ser encontrado no final do artigo. Agora vou dar uma olhada nas classes principais.

Cliente TFTP

A tarefa desta classe é conectar-se a um servidor remoto por seu ip e número de porta, ler um comando do fluxo de entrada (neste caso, o teclado), analisá-lo, transferi-lo para o servidor e, dependendo se você precisa enviar ou receber um arquivo, transferi-lo ou recebê-lo.

O código para iniciar o cliente para se conectar 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. Por serem triviais, não os 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());
        }
    }

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

Aqui o arquivo é enviado - usando um scanner, apresentamos o conteúdo do arquivo como um array de bytes, que escrevemos um por um no soquete, após o qual fechamos e abrimos novamente (não é a solução mais óbvia, mas garante a liberação de recursos), após o qual exibimos uma mensagem sobre transferência bem-sucedida.

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

Então tudo é trivial - recebemos 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("nDONEn");
       }
       catch (Exception e){
            System.out.println(e.getMessage());
       }
    }

Se um comando diferente de get ou put for inserido na janela do cliente, a função showErrorMessage será chamada, indicando que a entrada estava incorreta. Por trivialidade, não vou citá-lo. Um pouco mais interessante é a função de receber e dividir a string de entrada. Nele passamos o scanner, do qual esperamos receber uma linha separada por dois espaços e contendo o 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");
        }
    }

Enviando um comando — transmite o comando digitado do scanner para o soquete e força seu 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 do programa dependendo da string inserida. Tudo aqui não é muito bonito e o truque usado não é o melhor com saída forçada fora do bloco de código, mas o principal motivo para isso é a ausência em Java de algumas coisas, como delegados em C#, ponteiros de função de C++, ou pelo menos pelo menos o terrível e terrível goto, que permite implementar isso lindamente. Se você sabe como deixar o código um pouco mais elegante, aceito críticas nos comentários. Parece-me que um dicionário de delegado de String é 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);
    }
}

Servidor TFTP

A funcionalidade do servidor difere da funcionalidade do cliente, em geral, apenas porque os comandos não chegam a ele do teclado, mas do soquete. Alguns dos métodos são geralmente iguais, por isso não os citarei, apenas abordarei as diferenças.

Para começar, é utilizado o método run, que recebe uma porta como entrada e processa os dados de entrada do soquete em um loop eterno.

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

O método put, que envolve o método writeToFileFromSocket que abre um fluxo de gravação em um arquivo e grava todos os bytes de entrada do soquete, exibe uma mensagem indicando a conclusão bem-sucedida da transferência quando a gravação é concluída.

    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 recupera o arquivo do servidor. Como já mencionado na seção do lado cliente do programa, para transferir com sucesso um arquivo você precisa saber seu tamanho, armazenado em um inteiro longo, então eu divido em um array de 4 bytes, transferindo-os byte por byte para o soquete e, depois de recebê-los e montá-los no cliente em um número de volta, transfiro todos os bytes que compõem o arquivo, lidos no 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ê os dados do soquete e não do teclado. O código está no repositório, assim como o seletor.
Neste caso, a inicialização é colocada em um bloco de código separado, porque nesta implementação, após a conclusão da transferência, os recursos são liberados e reocupados novamente - novamente para 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());
        }
    }

Resumindo:

Acabamos de escrever nossa própria variação de um protocolo simples de transferência de dados e descobrimos como ele deveria funcionar. Em princípio, não descobri a América aqui e não escrevi muitas coisas novas, mas não havia artigos semelhantes sobre Habré e, como parte da escrita de uma série de artigos sobre utilitários cmd, era impossível não tocar nisso.

Links:

Repositório de código-fonte
Resumidamente sobre TFTP
A mesma coisa, mas em russo

Fonte: habr.com

Adicionar um comentário