Écriture de logiciels avec les fonctionnalités des utilitaires client-serveur Windows, partie 02

Poursuivant la série d'articles en cours consacrée aux implémentations personnalisées des utilitaires de la console Windows, nous ne pouvons nous empêcher d'aborder le TFTP (Trivial File Transfer Protocol) - un simple protocole de transfert de fichiers.

Comme la dernière fois, passons brièvement en revue la théorie, voyons le code qui implémente des fonctionnalités similaires à celle requise et analysons-le. Plus de détails - sous la coupe

Je ne copierai pas les informations de référence, dont les liens se trouvent traditionnellement à la fin de l'article, je dirai seulement qu'à la base, TFTP est une variante simplifiée du protocole FTP, dans laquelle le paramètre de contrôle d'accès a a été supprimé, et en fait il n'y a rien ici à part des commandes pour recevoir et transférer un fichier . Cependant, afin de rendre notre implémentation un peu plus élégante et adaptée aux principes actuels d'écriture de code, la syntaxe a été légèrement modifiée - cela ne change pas les principes de fonctionnement, mais l'interface, à mon humble avis, devient un peu plus logique et combine les aspects positifs de FTP et TFTP.

En particulier, au lancement, le client demande l'adresse IP du serveur et le port sur lequel le TFTP personnalisé est ouvert (en raison d'une incompatibilité avec le protocole standard, j'ai jugé opportun de laisser à l'utilisateur la possibilité de sélectionner un port), après quoi un une connexion se produit, à la suite de laquelle le client peut envoyer l'une des commandes - get ou put, pour recevoir ou envoyer un fichier au serveur. Tous les fichiers sont envoyés en mode binaire pour simplifier la logique.

Pour implémenter le protocole, j'utilisais traditionnellement 4 classes :

  • Client TFTPC
  • Serveur TFTP
  • TFTPClientTester
  • TFTPServerTester

Étant donné que les classes de test n'existent que pour déboguer les principales, je ne les analyserai pas, mais le code sera dans le référentiel ; un lien vers celui-ci se trouve à la fin de l'article. Je vais maintenant regarder les classes principales.

Client TFTPC

La tâche de cette classe est de se connecter à un serveur distant par son adresse IP et son numéro de port, de lire une commande à partir du flux d'entrée (dans ce cas, le clavier), de l'analyser, de la transférer au serveur et, selon que vous besoin d'envoyer ou de recevoir un fichier, de le transférer ou de l'obtenir.

Le code permettant de lancer le client pour se connecter au serveur et attendre une commande du flux d'entrée ressemble à ceci. Un certain nombre de variables globales utilisées ici sont décrites en dehors de l'article, dans le texte intégral du programme. En raison de leur trivialité, je ne les cite pas pour ne pas surcharger 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());
        }
    }

Passons en revue les méthodes appelées dans ce bloc de code :

Ici, le fichier est envoyé - à l'aide d'un scanner, nous présentons le contenu du fichier sous la forme d'un tableau d'octets, que nous écrivons un par un sur le socket, après quoi nous le fermons et l'ouvrons à nouveau (ce n'est pas la solution la plus évidente, mais il garantit la libération des ressources), après quoi nous affichons un message concernant le transfert réussi.

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

Ce fragment de code décrit la récupération des données du serveur. Tout est encore une fois trivial, seul le premier bloc de code présente un intérêt. Afin de comprendre exactement combien d'octets doivent être lus sur le socket, vous devez savoir combien pèse le fichier transféré. La taille du fichier sur le serveur est représentée par un entier long, donc 4 octets sont acceptés ici, qui sont ensuite convertis en un seul nombre. Ce n'est pas une approche très Java, c'est assez similaire pour SI, mais cela résout son problème.

Ensuite, tout est trivial - nous recevons un nombre connu d'octets du socket et les écrivons dans un fichier, après quoi nous affichons un message de réussite.

   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 une commande autre que get ou put a été saisie dans la fenêtre client, la fonction showErrorMessage sera appelée, indiquant que la saisie était incorrecte. Pour des raisons de banalité, je ne le citerai pas. La fonction de réception et de fractionnement de la chaîne d'entrée est un peu plus intéressante. Nous y passons le scanner, à partir duquel nous attendons de recevoir une ligne séparée par deux espaces et contenant la commande, l'adresse source et l'adresse de destination.

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

Envoi d'une commande : transmet la commande saisie depuis le scanner vers le socket et force son envoi.

    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 sélecteur est une fonction qui détermine les actions du programme en fonction de la chaîne saisie. Tout ici n'est pas très beau et l'astuce utilisée n'est pas la meilleure avec une sortie forcée en dehors du bloc de code, mais la raison principale en est l'absence en Java de certaines choses, comme les délégués en C#, les pointeurs de fonctions de C++, ou encore au moins le goto terrible et terrible, qui vous permet de mettre cela en œuvre magnifiquement. Si vous savez comment rendre le code un peu plus élégant, j'apprécie les critiques dans les commentaires. Il me semble qu'un dictionnaire String-délégué est nécessaire ici, mais il n'y a pas de délégué...

    private void selector()
    {
        do{
            if (typeOfCommand.equals("get")){
                get(sourcePath, destPath);
                break;
            }
            if (typeOfCommand.equals("put")){
                put(sourcePath, destPath);
                break;
            }
            showErrorMessage();
        }
        while (false);
    }
}

Serveur TFTP

La fonctionnalité du serveur diffère de la fonctionnalité du client, dans l'ensemble, uniquement en ce que les commandes lui parviennent non pas du clavier, mais du socket. Certaines méthodes sont généralement les mêmes, je ne les citerai donc pas, je n'aborderai que les différences.

Pour commencer, la méthode run est utilisée, qui reçoit un port en entrée et traite les données d'entrée du socket dans une boucle éternelle.

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

La méthode put, qui encapsule la méthode writeToFileFromSocket qui ouvre un flux d'écriture dans un fichier et écrit tous les octets d'entrée à partir du socket, affiche un message indiquant la réussite du transfert une fois l'écriture terminée.

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

La méthode get récupère le fichier du serveur. Comme déjà mentionné dans la section côté client du programme, pour réussir à transférer un fichier, vous devez connaître sa taille, stockée dans un entier long, je l'ai donc divisé en un tableau de 4 octets, je les transfère octet par octet. au socket, puis, après les avoir reçus et assemblés sur le client en un numéro, je transfère tous les octets qui composent le fichier, lus à partir du flux d'entrée du fichier.


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

La méthode getAndParseInput est la même que dans le client, la seule différence étant qu'elle lit les données depuis le socket plutôt que depuis le clavier. Le code est dans le référentiel, tout comme le sélecteur.
Dans ce cas, l'initialisation est placée dans un bloc de code séparé, car dans cette implémentation, une fois le transfert terminé, les ressources sont libérées et réoccupées - encore une fois pour assurer une protection contre les fuites de mémoire.

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

En résumé:

Nous venons d'écrire notre propre variante d'un simple protocole de transfert de données et de comprendre comment cela devrait fonctionner. En principe, je n'ai pas découvert l'Amérique ici et je n'ai pas écrit beaucoup de choses nouvelles, mais il n'y avait pas d'articles similaires sur Habré, et dans le cadre de l'écriture d'une série d'articles sur les utilitaires cmd, il était impossible de ne pas y aborder.

Liens:

Dépôt de code source
En bref sur TFTP
La même chose, mais en russe

Source: habr.com

Ajouter un commentaire