Softwareentwicklung mit den Funktionen von Windows-Client-Server-Tools, Teil 02

Fortfahrend mit der begonnenen Artikelreihe über benutzerdefinierte Implementierungen von Windows-Konsolenanwendungen muss TFTP (Trivial File Transfer Protocol) angesprochen werden – ein einfacher Dateitransferprotokoll.

Wie im letzten Mal werfen wir einen kurzen Blick auf die Theorie, sehen den Code, der eine ähnliche Funktionalität bietet, und analysieren ihn. Mehr dazu – unter dem Abschnitt.

Ich werde keine Informationsquellen kopieren, deren Links man traditionell am Ende des Artikels findet. Nur so viel: TFTP ist im Wesentlichen eine vereinfachte Version des FTP-Protokolls, bei dem die Zugangskontrollkonfiguration wegfällt, und eigentlich gibt es hier nur die Befehle zum Empfangen und Übertragen von Dateien. Um jedoch unsere Implementierung etwas eleganter und an die heutigen Programmierprinzipien angepasst zu gestalten, wurde die Syntax leicht verändert – die Funktionsweise bleibt jedoch gleich, aber das Interface wird, meiner Meinung nach, etwas logischer und vereint die positiven Aspekte von FTP und TFTP.

Insbesondere beim Start fragt der Client die IP-Adresse des Servers und den Port ab, an dem der benutzerdefinierte TFTP geöffnet ist (aufgrund der Inkompatibilität mit dem Standardprotokoll halte ich es für sinnvoll, dem Benutzer die Wahl des Ports zu lassen). Danach erfolgt die Verbindung, durch die der Client einen der Befehle — get oder put — zur Dateiübertragung auf den Server senden kann. Alle Dateien werden im Binärmodus gesendet — zur Vereinfachung der Logik.

Für die Implementierung des Protokolls habe ich traditionell 4 Klassen verwendet:

  • TFTPClient
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

Da die Testklassen nur zur Fehlersuche der Hauptklassen existieren, werde ich sie nicht näher behandeln, aber der Code wird im Repository zur Verfügung stehen, dessen Link am Ende des Artikels angegeben ist. Jetzt werde ich die Hauptklassen erläutern.

TFTPClient

Die Aufgabe dieser Klasse ist es, sich mit dem entfernten Server über seine IP und die Portnummer zu verbinden, einen Befehl vom Eingabestrom (in diesem Fall der Tastatur) zu lesen, ihn zu parsen, an den Server zu übertragen und, je nachdem, ob eine Datei gesendet oder empfangen werden muss, sie zu übertragen oder zu empfangen.

Der Client-Code zum Verbinden mit dem Server und Warten auf einen Befehl vom Eingabestrom sieht folgendermaßen aus. Eine Reihe von globalen Variablen, die hier verwendet werden, werden außerhalb des Artikels beschrieben, im vollständigen Text des Programms. Aufgrund ihrer Trivialität führe ich sie nicht auf, um den Artikel nicht zu überladen.

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

Lassen Sie uns die Methoden durchgehen, die in diesem Codeblock aufgerufen werden:

Hier erfolgt das Senden einer Datei – mithilfe des Scanners repräsentieren wir den Inhalt der Datei als Byte-Array, das wir der Reihe nach in den Socket schreiben, bevor wir ihn schließen und erneut öffnen (nicht die offensichtlichste Lösung, aber sie gewährleistet die Freigabe von Ressourcen), und dann geben wir eine Nachricht über die erfolgreiche Übertragung aus.

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

Dieser Codeausschnitt beschreibt den Empfang von Daten vom Server. Alles ist wieder einmal trivial; von Interesse ist nur der erste Block des Codes. Um zu verstehen, wie viele Bytes genau vom Socket gelesen werden müssen, muss man wissen, wie groß die übertragene Datei ist. Die Dateigröße auf dem Server wird als langes ganzzahliges Zahl dargestellt, daher werden hier 4 Bytes verwendet, die später in eine Zahl konvertiert werden. Das ist kein typischer Java-Ansatz, eher ähnlich wie in C, aber es erfüllt seine Aufgabe.

Alles ist dann trivial – wir empfangen eine bekannte Anzahl von Bytes vom Socket und schreiben sie in eine Datei, danach geben wir eine Erfolgsnachricht aus.

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

Falls im Client-Fenster ein Befehl eingegeben wird, der nicht 'get' oder 'put' ist, wird die Funktion showErrorMessage aufgerufen, die auf die Inkonsistenz der Eingabe hinweist. Aufgrund der Trivialität werde ich dies nicht weiter erläutern. Etwas interessanter ist die Funktion zum Empfangen und Aufteilen der Eingabezeile. Sie erwartet einen Scanner, von dem wir eine durch zwei Leerzeichen getrennte Zeile erhalten, die den Befehl, die Quelladresse und die Zieladresse enthält.

    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("Ungültige Eingabe");
        }
    }

Das Senden des Befehls bedeutet, den vom Scanner eingegebenen Befehl an den Socket zu übertragen und ihn zwangsweise zu senden.

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

Der Selektor ist eine Funktion, die die Aktionen des Programms je nach eingegebenem String bestimmt. Hier ist alles nicht sehr schön, und es wird nicht die beste Technik verwendet, um den Block zu verlassen, aber der Hauptgrund dafür ist das Fehlen bestimmter Dinge in Java, wie Delegates in C#, Funktionszeiger aus C++ oder zumindest das gefürchtete und schreckliche goto, das es ermöglicht, dies elegant zu implementieren. Wenn Sie wissen, wie man den Code etwas eleganter gestalten kann, freue ich mich auf Kritik in den Kommentaren. Ich denke, hier sollte ein Wörterbuch String-Delegate vorhanden sein, aber es gibt keinen Delegate...

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

TFTPServer

Die Funktionalität des Servers unterscheidet sich im Wesentlichen nur darin, dass die Befehle nicht von der Tastatur, sondern über einen Socket eingehen. Einige Methoden sind identisch, daher werde ich sie nicht anführen, sondern nur die Unterschiede ansprechen.

Hier wird die Methode run verwendet, die einen Port als Eingabe erhält und die Eingabedaten vom Socket in einer Endlosschleife verarbeitet.

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

Die Methode put ist eine Wrapper-Methode für die Methode writeToFileFromSocket, die einen Schreibstream in eine Datei öffnet und alle Bytes der Eingabe vom Socket aufzeichnet. Nach Abschluss des Schreibvorgangs wird eine Nachricht über den erfolgreichen Abschluss der Übertragung ausgegeben.

    private void put(String source, String dest){
            writeToFileFromSocket();
            System.out.print("nFertig");
    };
    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());
        }
    }

Die get-Methode ermöglicht den Empfang von Dateien vom Server. Wie bereits im Abschnitt über die Client-Seite des Programms erwähnt, ist es für die erfolgreiche Übertragung einer Datei notwendig, ihre Größe zu kennen, die als Long gespeichert wird. Daher teile ich sie in ein Array von 4 Bytes auf, übertrage sie byteweise an den Socket und übergebe dann, nachdem ich sie auf der Client-Seite wieder in eine Zahl zusammengesetzt habe, alle Bytes, die die Datei bilden, die aus dem Eingabestrom der Datei gelesen wurden.


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

Die Methode getAndParseInput entspricht derjenigen auf der Client-Seite, mit dem einzigen Unterschied, dass sie Daten vom Socket und nicht von der Tastatur liest. Der Code im Repository ist wie bei selector.
In diesem Fall wurde die Initialisierung in einen separaten Codeblock ausgelagert, da innerhalb dieser Implementierung die Ressourcen nach Abschluss der Übertragung freigegeben und erneut beansprucht werden – wiederum zum Schutz vor Speicherlecks.

    private void initialization()
    {
        try {
            serverSocket = new ServerSocket(port);
            socket = serverSocket.accept();
            sin = socket.getInputStream();
            sout = socket.getOutputStream();
        }
        catch (Exception e) {
            System.out.print(e.getMessage());
        }
    }

Zusammenfassend:

Gerade haben wir unsere eigene Variation eines einfachen Datenübertragungsprotokolls geschrieben und herausgefunden, wie es funktionieren sollte. Im Grunde genommen habe ich hier nichts Neues entdeckt, aber – ähnliche Artikel gab es auf Habré nicht, und im Rahmen des Schreibens einer Artikelreihe über cmd-Tools durfte dieses Thema nicht fehlen.

Links:

Repository mit Quellcode
Kurz über TFTP
Das Gleiche, aber auf Russisch

Quelle: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Servern 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Servern | ProHoster