Développement de logiciels avec des fonctionnalités d'utilitaires clients-serveurs Windows, partie 02

En poursuivant ce cycle d'articles consacré aux réalisations personnalisées d'utilitaires de console Windows, il est impossible de ne pas évoquer TFTP (Trivial File Transfer Protocol) — un protocole simple de transfert de fichiers.

Comme la dernière fois, nous allons brièvement passer en revue la théorie, examiner le code qui implémente une fonctionnalité similaire à celle requise, et l'analyser. Plus de détails — ci-dessous.

Je ne vais pas copier-coller d'informations de référence, dont les liens se trouvent traditionnellement à la fin de l'article, je dirai simplement que TFTP est en essence une variante simplifiée du protocole FTP, où la configuration du contrôle d'accès a été supprimée, et fondamentalement, il n'y a rien d'autre que les commandes de réception et d'envoi de fichiers. Cependant, pour rendre notre mise en œuvre un peu plus élégante et adaptée aux principes de codage actuels, la syntaxe a été légèrement modifiée — cela n'affecte pas les principes de fonctionnement, mais l'interface, IMHO, devient un peu plus logique et combine les avantages de FTP et de TFTP.

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

Pour la mise en œuvre du protocole, j'ai traditionnellement utilisé 4 classes :

  • TFTPClient
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

Étant donné que les classes de test existent uniquement pour déboguer les classes principales, je ne vais pas les examiner, mais le code sera disponible dans le dépôt, dont le lien peut être consulté à la fin de l'article. Je vais maintenant aborder les classes principales.

TFTPClient

La tâche de cette classe est de se connecter au serveur distant via son IP et son numéro de port, de lire la commande à partir du flux d'entrée (dans ce cas — le clavier), de l'analyser, de la transmettre au serveur, et, selon que le fichier doit être transmis ou reçu, de le transmettre ou de le recevoir.

Le code pour lancer le client pour se connecter au serveur et attendre une commande du flux d'entrée est comme suit. Une série de variables globales utilisées ici sont décrites en dehors de l'article, dans le texte complet du programme. En raison de leur banalité, je ne les mentionne pas afin de 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());
        }
    }

Examinons les méthodes appelées dans ce bloc de code :

Ici, le fichier est envoyé - nous représentons le contenu du fichier comme un tableau d'octets à l'aide du scanner, que nous écrivons séquentiellement dans le socket, puis nous le fermons et le rouvrons (ce n'est pas la solution la plus évidente, mais cela garantit la libération des ressources), après quoi un message de transmission réussie est affiché à l'écran.

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 depuis le serveur. Tout cela est encore une fois trivial, seul le premier bloc de code présente un intérêt. Pour savoir combien d'octets lire depuis le socket, il est nécessaire de connaître la taille du fichier transmis. La taille du fichier sur le serveur est représentée par un entier long, donc ici nous prenons 4 octets, qui seront ensuite convertis en un seul nombre. Ce n'est pas vraiment une approche très Java, c'est plutôt semblable à celle de C, mais cela résout son problème.

Ensuite, tout est trivial - nous recevons un nombre connu d'octets depuis le socket et les écrivons dans un fichier, après quoi un message de succès est affiché.

   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 différente de get ou put est saisie dans la fenêtre du client, la fonction showErrorMessage sera appelée pour indiquer une entrée incorrecte. Étant donné sa simplicité, je ne l'inclue pas ici. La fonction de réception et de décomposition de la chaîne d'entrée est un peu plus intéressante. Nous lui passons un scanner, dont nous attendons de recevoir une chaîne, séparée par deux espaces et contenant la commande, le chemin source et le chemin 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("Mauvaise entrée");
        }
    }

L'envoi de la commande consiste à transmettre la commande saisie avec le scanner à la socket et à l'envoyer de force.

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

Le sélecteur est une fonction qui détermine les actions du programme en fonction de la chaîne saisie. Ici, ce n'est pas très joli et on utilise une technique pas très bonne avec une sortie forcée du bloc de code, mais la raison principale réside dans l'absence en Java de certaines choses, comme les délégués en C#, les pointeurs de fonction en C++ ou même le terrible goto, qui permettraient de le faire élégamment. Si vous savez comment rendre le code un peu plus élégant, j'attends vos critiques dans les commentaires. Il me semble qu'un dictionnaire String-délégué est nécessaire, mais le délégué n'est pas là…

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

TFTPServer

Les fonctionnalités du serveur diffèrent de celles du client principalement par le fait que les commandes ne viennent pas du clavier, mais de la socket. Certaines méthodes coïncident même, donc je ne vais pas les présenter, je vais seulement aborder les différences.

Ici, la méthode run est utilisée, recevant un port en entrée et traitant en boucle les données entrantes de la socket.

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

La méthode put, qui est un wrapper de la méthode writeToFileFromSocket, ouvre un flux d'écriture dans un fichier et écrit tous les octets d'entrée du socket, puis sort un message indiquant que la transmission est terminée avec succès.

    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 permet d'obtenir un fichier du serveur. Comme mentionné dans la section sur le côté client du programme, pour transférer un fichier avec succès, il faut connaître sa taille, qui est stockée dans un long entier. Je la divise donc en un tableau de 4 octets, les transmet byte par byte au socket, puis, une fois reçus et recomposés sur le client dans un nombre, je transmets tous les octets constituant le fichier, lus depuis le 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 similaire à celle du client, sauf qu'elle lit les données depuis le socket et non depuis le clavier. Le code dans le dépôt est le même que pour le sélecteur.
Dans ce cas, l'initialisation est placée dans un bloc de code séparé, car dans le cadre de cette implémentation, après la transmission, les ressources sont libérées et réoccupées à nouveau — encore une fois dans le but de prévenir 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 version d'un protocole simple de transmission de données et de comprendre comment il doit fonctionner. En principe, je n'ai rien découvert de nouveau, 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 en parler.

Liens :

Dépôt du code source
D'un coup d'œil sur TFTP
La même chose, mais en russe

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster