Escriptura de programari amb la funcionalitat de les utilitats client-servidor de Windows, part 02

Continuant amb la sèrie d'articles en curs dedicats a les implementacions personalitzades de les utilitats de la consola de Windows, no podem deixar de tocar el TFTP (Trivial File Transfer Protocol), un protocol de transferència de fitxers senzill.

Com l'última vegada, repassem breument la teoria, vegem el codi que implementa una funcionalitat similar a la necessària i analitzem-lo. Més detalls - sota el tall

No copiaré i enganxaré informació de referència, enllaços als quals tradicionalment es poden trobar al final de l'article, només diré que, en el seu nucli, TFTP és una variació simplificada del protocol FTP, en què la configuració del control d'accés té s'ha eliminat i, de fet, no hi ha res aquí excepte ordres per rebre i transferir un fitxer . Tanmateix, per tal de fer que la nostra implementació sigui una mica més elegant i adaptada als principis actuals d'escriptura de codi, la sintaxi s'ha canviat lleugerament; això no canvia els principis de funcionament, però la interfície, IMHO, es torna una mica més lògica i combina els aspectes positius de FTP i TFTP.

En particular, quan es llança, el client sol·licita l'adreça IP del servidor i el port on està obert el TFTP personalitzat (a causa de la incompatibilitat amb el protocol estàndard, vaig considerar oportú deixar a l'usuari la possibilitat de seleccionar un port), després del qual un es produeix la connexió, com a resultat de la qual el client pot enviar una de les ordres: obtenir o posar, rebre o enviar un fitxer al servidor. Tots els fitxers s'envien en mode binari per simplificar la lògica.

Per implementar el protocol, he utilitzat tradicionalment 4 classes:

  • Client TFTPC
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

A causa del fet que les classes de prova només existeixen per depurar les principals, no les analitzaré, però el codi estarà al repositori, es pot trobar un enllaç al final de l'article. Ara miraré les classes principals.

Client TFTPC

La tasca d'aquesta classe és connectar-se a un servidor remot mitjançant la seva ip i el seu número de port, llegir una ordre del flux d'entrada (en aquest cas, el teclat), analitzar-la, transferir-la al servidor i, depenent de si cal enviar o rebre un fitxer, transferir-lo o obtenir.

El codi per iniciar el client per connectar-se al servidor i esperar una ordre del flux d'entrada té aquest aspecte. Una sèrie de variables globals que s'utilitzen aquí es descriuen fora de l'article, al text complet del programa. Per la seva trivialitat, no els cito per no sobrecarregar l'article.

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

Anem a repassar els mètodes anomenats en aquest bloc de codi:

Aquí s'envia el fitxer: mitjançant un escàner, presentem el contingut del fitxer com una matriu de bytes, que escrivim un per un al sòcol, després el tanquem i el tornem a obrir (no és la solució més òbvia, però garanteix l'alliberament de recursos), després del qual mostrem un missatge sobre la transferència correcta.

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

Aquest fragment de codi descriu la recuperació de dades del servidor. Tot torna a ser trivial, només interessa el primer bloc de codi. Per entendre exactament quants bytes s'han de llegir des del sòcol, cal saber quant pesa el fitxer transferit. La mida del fitxer al servidor es representa com un nombre enter llarg, de manera que aquí s'accepten 4 bytes, que posteriorment es converteixen en un sol número. Aquest no és un enfocament molt Java, és més aviat similar per a SI, però resol el seu problema.

Aleshores, tot és trivial: rebem un nombre conegut de bytes del sòcol i els escrivim en un fitxer, després del qual mostrem un missatge d'èxit.

   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 s'ha introduït una ordre diferent de get o put a la finestra del client, es cridarà la funció showErrorMessage, indicant que l'entrada era incorrecta. Per trivialitat, no ho citaré. Una mica més interessant és la funció de rebre i dividir la cadena d'entrada. Hi passem l'escàner, del qual esperem rebre una línia separada per dos espais i que conté l'ordre, l'adreça d'origen i l'adreça de destinació.

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

Enviament d'una ordre: transmet l'ordre introduïda des de l'escàner al sòcol i obliga a enviar-la

    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 és una funció que determina les accions del programa en funció de la cadena introduïda. Tot aquí no és molt bonic i el truc utilitzat no és el millor amb sortida forçada fora del bloc de codi, però la raó principal d'això és l'absència a Java d'algunes coses, com ara delegats en C#, punters de funció de C++ o en almenys el terrible i terrible goto, que us permet implementar-ho de manera meravellosa. Si sabeu com fer que el codi sigui una mica més elegant, accepto les crítiques als comentaris. Em sembla que aquí cal un diccionari String-delegate, però no hi ha cap delegat...

    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 funcionalitat del servidor difereix de la funcionalitat del client, en general, només perquè les ordres no arriben des del teclat, sinó del sòcol. Alguns dels mètodes són generalment els mateixos, així que no els citaré, només tocaré les diferències.

Per començar, s'utilitza el mètode d'execució, que rep un port com a entrada i processa les dades d'entrada del sòcol en un bucle etern.

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

El mètode put, que embolcalla el mètode writeToFileFromSocket que obre un flux d'escriptura en un fitxer i escriu tots els bytes d'entrada del sòcol, mostra un missatge que indica la finalització correcta de la transferència quan es completa l'escriptura.

    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ètode get recupera el fitxer del servidor. Com ja s'ha esmentat a la secció de la part del client del programa, per transferir amb èxit un fitxer cal conèixer la seva mida, emmagatzemat en un nombre enter llarg, així que el vaig dividir en una matriu de 4 bytes, transferir-los byte a byte al sòcol, i després, després d'haver-los rebut i reunit al client en un número, transfereixo tots els bytes que formen el fitxer, llegits del flux d'entrada del fitxer.


 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ètode getAndParseInput és el mateix que al client, l'única diferència és que llegeix dades des del sòcol en lloc del teclat. El codi es troba al repositori, igual que el selector.
En aquest cas, la inicialització es col·loca en un bloc de codi separat, perquè dins d'aquesta implementació, un cop finalitzada la transferència, els recursos s'alliberen i es tornen a ocupar, de nou per proporcionar protecció contra les fuites 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());
        }
    }

Resumir:

Acabem d'escriure la nostra pròpia variació d'un protocol de transferència de dades senzill i hem descobert com hauria de funcionar. En principi, aquí no vaig descobrir Amèrica i no vaig escriure gaire coses noves, però no hi havia articles semblants sobre Habré, i com a part d'escriure una sèrie d'articles sobre les utilitats cmd era impossible no tocar-hi.

Enllaços:

Repositori de codi font
Breument sobre TFTP
El mateix, però en rus

Font: www.habr.com

Afegeix comentari