Softwareentwicklung mit Funktionen von Client-Server-Dienstprogrammen für Windows, Teil 02

Im Rahmen des begonnenen Zyklus von Artikeln, die sich mit benutzerdefinierten Implementierungen von Windows-Konsolenanwendungen befassen, darf TFTP (Trivial File Transfer Protocol) – ein einfacher Dateiübertragungsprotokoll – nicht unerwähnt bleiben.

Wie beim letzten Mal werfen wir einen kurzen Blick auf die Theorie, sehen den Code, der die gewünschte Funktionalität implementiert, und analysieren ihn. Mehr dazu unter dem Artikel.

Ich werde keine ähnlichen Informationen wiederholen, die traditionell am Ende des Artikels verlinkt sind. Ich sage nur, dass TFTP im Grunde eine vereinfachte Variante des FTP-Protokolls ist, bei der die Zugangskontrolle entfernt wurde und es im Wesentlichen nur die Kommandos zum Empfangen und Senden von Dateien gibt. Um unsere Implementierung jedoch etwas eleganter und an die heutigen Prinzipien des Programmierens anzupassen, wurde die Syntax leicht geändert – die Arbeitsweise bleibt gleich, aber die Schnittstelle wird, IMHO, etwas logischer und vereint die positiven Aspekte von FTP und TFTP.

Bei der Ausführung fragt der Client die IP-Adresse des Servers und den Port ab, an dem das benutzerdefinierte TFTP geöffnet ist (aufgrund der Inkompatibilität mit dem Standardprotokoll hielt 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 – für das Empfangen oder Senden von Dateien an 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 dienen, werde ich sie nicht analysieren. Der Code wird sich in dem Repository befinden, dessen Link am Ende des Artikels angegeben ist. Die Hauptklassen werde ich nun erörtern.

TFTPClient

Die Aufgabe dieser Klasse besteht darin, sich mit dem entfernten Server über dessen IP und Portnummer zu verbinden, einen Befehl vom Eingangsstream (in diesem Fall der Tastatur) zu lesen, ihn zu parsen und an den Server zu übermitteln. Je nachdem, ob eine Datei übertragen oder empfangen werden muss, wird sie gesendet oder empfangen.

Der Code, der den Client mit dem Server verbindet und auf einen Befehl vom Eingabestrom wartet, sieht so aus. Eine Reihe von globalen Variablen, die hier verwendet werden, werden außerhalb des Artikels im vollständigen Programmtext beschrieben. Aufgrund ihrer Trivialität lasse ich sie weg, um den Artikel nicht zu überladen.

 öffentliche void run(String ip, int port)
    {
        this.ip = ip;
        this.port = port;
        versuchen {
            inicialization();
            Scanner tastatur = new Scanner(System.in);
            während (isRunning) {
                getAndParseInput(tastatur);
                sendCommand();
                selector();
                }
            }
        fangen (Ausnahme e) {
            System.out.println(e.getMessage());
        }
    }

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

Hier wird die Datei gesendet – mit Hilfe des Scanners stellen wir den Inhalt der Datei als Byte-Array dar, das wir nacheinander in den Socket schreiben, danach schließen wir ihn und öffnen ihn erneut (nicht die offensichtlichste Lösung, aber sie gewährleistet die Freigabe von Ressourcen), danach geben wir eine Nachricht über den erfolgreichen Transfer aus.

private void put(String sourcePath, String destPath)
    {

        File src = new File(sourcePath);
        versuchen {

            InputStream scanner = new FileInputStream(src);
            byte[] bytes = scanner.readAllBytes();
            für (byte b : bytes)
                sout.write(b);
            sout.close();
            inicialization();
            System.out.println("nDonen");
            }

        fangen (Ausnahme e) {
            System.out.println(e.getMessage());
        }
    }

Dieser Codeabschnitt beschreibt das Empfangen von Daten vom Server. Alles ist wieder einmal trivial, von Interesse ist nur der erste Codeblock. Um zu verstehen, wie viele Bytes genau vom Socket gelesen werden müssen, muss man wissen, wie groß die übertragene Datei ist. Die Größe der Datei auf dem Server wird als lange ganze Zahl dargestellt, deshalb werden hier 4 Bytes angenommen, die später in eine Zahl umgewandelt werden. Das ist nicht gerade eine Java-artige Herangehensweise, eher vergleichbar mit C, aber es erfüllt seine Aufgabe.

Danach ist alles trivial – wir empfangen eine bekannte Anzahl von Bytes vom Socket und schreiben sie in die Datei, danach geben wir eine Erfolgsmeldung aus.

   private void get(String sourcePath, String destPath){
        long sizeOfFile = 0;
        versuchen {


            byte[] sizeBytes = new byte[Long.SIZE];
           für (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));
           für (int i =0; i < sizeOfFile; i++)
           {
               writer.write(sin.read());
           }
           writer.close();
           System.out.println("nDONEn");
       }
       fangen (Ausnahme e){
            System.out.println(e.getMessage());
       }
    }

Falls im Client-Fenster ein Befehl eingegeben wurde, der von 'get' oder 'put' abweicht, wird die Funktion showErrorMessage aufgerufen, die die Ungültigkeit der Eingabe anzeigt. Wegen der Trivialität werde ich das nicht näher ausführen. Etwas interessanter ist die Funktion zum Empfang und zur Zerlegung der Eingabestrings. Hier übergeben wir den Scanner, von dem wir eine Zeile erwarten, die durch zwei Leerzeichen getrennt ist und 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 eines Befehls besteht darin, den über den 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 basierend auf dem eingegebenen String bestimmt. Hier ist nicht alles sehr schön und es wird eine nicht optimalen Methode verwendet, die erzwungen außerhalb des Codeblocks führt, aber der Hauptgrund dafür ist das Fehlen bestimmter Dinge in Java, wie Delegaten in C#, Funktionszeiger aus C++ oder zumindest das schreckliche und abscheuliche goto, die es ermöglichen, das eleganter umzusetzen. Wenn Sie wissen, wie der Code etwas eleganter gestaltet werden kann – ich freue mich auf Kritik in den Kommentaren. Ich denke, dass hier ein Wörterbuch String-Delegate benötigt wird, aber es gibt keinen Delegaten…

    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 den Socket eingehen. Einige Methoden stimmen absolut überein, deswegen werde ich diese nicht erneut auflisten, sondern nur die Unterschiede ansprechen.

Zum Starten wird hier die Methode run verwendet, die den Port 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, die eine Wrapper-Methode für writeToFileFromSocket ist, öffnet einen Schreibstream in eine Datei und schreibt alle Eingabebytes vom Socket. Nach Abschluss des Schreibvorgangs wird eine Erfolgsmeldung über den Abschluss der Übertragung ausgegeben.

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

Die Methode get ermöglicht den Empfang von Dateien vom Server. Wie bereits im Abschnitt über die Clientseite des Programms erwähnt, ist es für eine erfolgreiche Dateiübertragung notwendig, die Größe zu kennen, die in einer langen Ganzzahl gespeichert ist. Daher teile ich sie in ein Array aus 4 Bytes auf, übertrage sie Byte für Byte über den Socket und übermittle, nachdem ich sie am Client in eine Zahl zurückkompiliert habe, alle Bytes, die die Datei aus dem Eingabestream aus der Datei bilden.


 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 der entsprechenden Methode im Client, mit dem einzigen Unterschied, dass sie Daten vom Socket anstelle der Tastatur einliest. Der Code im Repository ist ebenso wie der Selektor.
In diesem Fall wurde die Initialisierung in einen separaten Codeblock ausgelagert, da in dieser Implementierung die Ressourcen nach Beendigung der Übertragung freigegeben und erneut belegt werden — wiederum mit dem Ziel, einen Schutz gegen Speicherlecks zu gewährleisten.

    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 Variante eines einfachen Datenübertragungsprotokolls geschrieben und uns damit beschäftigt, wie es funktionieren sollte. Im Grunde habe ich dabei nichts Neues entdeckt und nicht viel geschrieben, aber — ähnliche Artikel gab es auf Habr nicht, und im Rahmen des Schreibens einer Artikelreihe über cmd-Utilities konnte ich es nicht auslassen.

Links:

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

Quelle: habr.com

60GB SSD 8Gb DDR4