Software de escritura coa funcionalidade das utilidades cliente-servidor de Windows, parte 02

Continuando coa serie de artigos en curso dedicados ás implementacións personalizadas das utilidades da consola de Windows, non podemos deixar de tocar o TFTP (Trivial File Transfer Protocol), un protocolo de transferencia de ficheiros sinxelo.

Como a última vez, repasemos brevemente a teoría, vexamos o código que implementa unha funcionalidade similar á requirida e analízao. Máis detalles - baixo o corte

Non vou copiar e pegar información de referencia, as ligazóns ás que tradicionalmente se poden atopar ao final do artigo, só direi que, no seu núcleo, TFTP é unha variación simplificada do protocolo FTP, no que a configuración de control de acceso ten foi eliminado e, de feito, non hai nada aquí excepto comandos para recibir e transferir un ficheiro . Non obstante, para facer a nosa implementación un pouco máis elegante e adaptada aos principios actuais de escritura de código, a sintaxe cambiouse lixeiramente; isto non cambia os principios de funcionamento, pero a interface, en mi humilde opinión, faise un pouco máis lóxica e combina os aspectos positivos de FTP e TFTP.

En particular, cando se inicia, o cliente solicita o enderezo IP do servidor e o porto no que está aberto o TFTP personalizado (debido á incompatibilidade co protocolo estándar, considerei adecuado deixar ao usuario a posibilidade de seleccionar un porto), despois de que un conexión ocorre, como resultado do cal o cliente pode enviar un dos comandos - get ou put, para recibir ou enviar un ficheiro ao servidor. Todos os ficheiros son enviados en modo binario para simplificar a lóxica.

Para implementar o protocolo, tradicionalmente usei 4 clases:

  • Cliente TFTPC
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

Debido ao feito de que as clases de proba só existen para depurar as principais, non as analizarei, pero o código estará no repositorio; pódese atopar unha ligazón ao final do artigo. Agora vou mirar as clases principais.

Cliente TFTPC

A tarefa desta clase é conectarse a un servidor remoto polo seu ip e número de porto, ler un comando do fluxo de entrada (neste caso, o teclado), analizalo, transferilo ao servidor e, dependendo de se precisa enviar ou recibir un ficheiro, transferilo ou obtelo.

O código para iniciar o cliente para conectarse ao servidor e esperar un comando do fluxo de entrada é así. Unha serie de variables globais que se usan aquí descríbense fóra do artigo, no texto completo do programa. Pola súa trivialidade non os cito para non sobrecargar 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());
        }
    }

Imos repasar os métodos chamados neste bloque de código:

Aquí envíase o ficheiro: mediante un escáner, presentamos o contido do ficheiro como unha matriz de bytes, que escribimos un por un no socket, despois de que o pechamos e o abrimos de novo (non é a solución máis obvia, pero garante a liberación de recursos), despois de que amosamos unha mensaxe sobre a transferencia 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 a recuperación de datos do servidor. Todo volve ser trivial, só interesa o primeiro bloque de código. Para comprender exactamente cantos bytes hai que ler desde o socket, cómpre saber canto pesa o ficheiro transferido. O tamaño do ficheiro no servidor represéntase como un número enteiro longo, polo que aquí se aceptan 4 bytes, que posteriormente se converten nun só número. Este non é un enfoque moi Java, é bastante similar para SI, pero resolve o seu problema.

Entón todo é trivial: recibimos un número coñecido de bytes do socket e escribimos nun ficheiro, despois de que amosamos unha mensaxe 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());
       }
    }

Se se introduciu un comando distinto de get ou put na xanela do cliente, chamarase á función showErrorMessage, indicando que a entrada foi incorrecta. Por trivialidade, non o citarei. Algo máis interesante é a función de recibir e dividir a cadea de entrada. Pasámoslle o escáner, do que esperamos recibir unha liña separada por dous espazos e que contén o comando, o enderezo de orixe e 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");
        }
    }

Enviar un comando: transmite o comando introducido desde o escáner ao socket e obriga 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 é unha función que determina as accións do programa dependendo da cadea introducida. Todo aquí non é moi bonito e o truco empregado non é o mellor con saída forzada fóra do bloque de código, pero a principal razón para iso é a ausencia en Java dalgunhas cousas, como delegados en C#, punteiros de función de C++ ou en Java. polo menos o terrible e terrible goto, que che permite implementar isto ben. Se sabes como facer o código un pouco máis elegante, agradezo as críticas nos comentarios. Paréceme que aquí se necesita un dicionario String-delegate, pero non hai ningún 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 difire da funcionalidade do cliente, en xeral, só en que os comandos chegan a el non desde o teclado, senón desde o socket. Algúns dos métodos son en xeral os mesmos, polo que non os citarei, só tocarei as diferenzas.

Para comezar, utilízase o método de execución, que recibe un porto como entrada e procesa os datos de entrada do socket nun bucle 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 un fluxo de escritura nun ficheiro e escribe todos os bytes de entrada do socket, mostra unha mensaxe que indica a finalización exitosa da transferencia cando se completa a escritura.

    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 ficheiro do servidor. Como xa se mencionou na sección do lado do cliente do programa, para transferir con éxito un ficheiro, cómpre coñecer o seu tamaño, almacenado nun número enteiro longo, polo que o dividín nunha matriz de 4 bytes, transfiraos byte a byte ao socket, e despois, recibilos e reunilos no cliente nun número de volta, transfiro todos os bytes que compoñen o ficheiro, lidos desde o fluxo de entrada do ficheiro.


 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 que no cliente, a única diferenza é que le os datos desde o socket e non desde o teclado. O código está no repositorio, igual que o selector.
Neste caso, a inicialización colócase nun bloque de código separado, porque dentro desta implementación, despois de que se complete a transferencia, os recursos son liberados e reocupados de novo, de novo para proporcionar protección contra fugas 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());
        }
    }

Para resumir:

Acabamos de escribir a nosa propia variación nun protocolo de transferencia de datos sinxelo e descubrimos como debería funcionar. En principio, aquí non descubrín América e non escribín moitas cousas novas, pero non había artigos similares sobre Habré, e como parte de escribir unha serie de artigos sobre as utilidades cmd era imposible non tocar nel.

Referencias:

Repositorio de código fonte
Brevemente sobre TFTP
O mesmo, pero en ruso

Fonte: www.habr.com

Engadir un comentario