Schreiben von Software mit der Funktionalität von Windows-Client-Server-Dienstprogrammen, Teil 02

In Fortsetzung der laufenden Artikelserie über benutzerdefinierte Implementierungen von Windows-Konsolendienstprogrammen können wir nicht umhin, auf TFTP (Trivial File Transfer Protocol) einzugehen – ein einfaches Dateiübertragungsprotokoll.

Lassen Sie uns wie beim letzten Mal kurz auf die Theorie eingehen, uns den Code ansehen, der eine ähnliche Funktionalität wie die erforderliche implementiert, und ihn analysieren. Weitere Details - unter dem Schnitt

Ich werde keine Referenzinformationen kopieren und einfügen, deren Links traditionell am Ende des Artikels zu finden sind. Ich möchte nur sagen, dass TFTP im Kern eine vereinfachte Variante des FTP-Protokolls ist, in der die Zugriffskontrolleinstellung enthalten ist entfernt, und tatsächlich gibt es hier nichts außer Befehlen zum Empfangen und Übertragen einer Datei. Um unsere Implementierung jedoch etwas eleganter zu gestalten und an die aktuellen Prinzipien des Code-Schreibens anzupassen, wurde die Syntax leicht geändert – an den Funktionsprinzipien ändert sich dadurch nichts, aber die Schnittstelle wird meiner Meinung nach etwas logischer und vereint die positiven Aspekte von FTP und TFTP.

Insbesondere fordert der Client beim Start die IP-Adresse des Servers und den Port an, auf dem benutzerdefiniertes TFTP geöffnet ist (aufgrund der Inkompatibilität mit dem Standardprotokoll hielt ich es für angemessen, dem Benutzer die Möglichkeit zu lassen, einen Port auszuwählen), woraufhin a Es kommt zu einer Verbindung, wodurch der Client einen der Befehle get oder put senden kann, um eine Datei zu empfangen oder an den Server zu senden. Alle Dateien werden im Binärmodus gesendet, um die Logik zu vereinfachen.

Um das Protokoll zu implementieren, habe ich traditionell 4 Klassen verwendet:

  • TFTPClient
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

Da es Testklassen nur zum Debuggen der Hauptklassen gibt, werde ich sie nicht analysieren, aber der Code befindet sich im Repository; einen Link dazu finden Sie am Ende des Artikels. Jetzt schaue ich mir die Hauptklassen an.

TFTPClient

Die Aufgabe dieser Klasse besteht darin, über seine IP-Adresse und Portnummer eine Verbindung zu einem Remote-Server herzustellen, einen Befehl aus dem Eingabestream (in diesem Fall die Tastatur) zu lesen, ihn zu analysieren, an den Server zu übertragen und, je nachdem, ob Sie Sie müssen eine Datei senden oder empfangen, übertragen oder abrufen.

Der Code zum Starten des Clients, um eine Verbindung zum Server herzustellen und auf einen Befehl aus dem Eingabestream zu warten, sieht folgendermaßen aus. Eine Reihe globaler Variablen, die hier verwendet werden, werden außerhalb des Artikels im vollständigen Text des Programms beschrieben. Aufgrund ihrer Trivialität verzichte ich darauf, sie zu zitieren, 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());
        }
    }

Sehen wir uns die in diesem Codeblock aufgerufenen Methoden an:

Hier wird die Datei gesendet – mit einem Scanner präsentieren wir den Inhalt der Datei als Array von Bytes, die wir einzeln in den Socket schreiben, ihn anschließend schließen und wieder öffnen (nicht die naheliegendste Lösung, aber es garantiert die Freigabe von Ressourcen), woraufhin wir eine Meldung über die erfolgreiche Übertragung anzeigen.

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

Dieses Codefragment beschreibt das Abrufen von Daten vom Server. Alles ist wieder trivial, nur der erste Codeblock ist von Interesse. Um genau zu verstehen, wie viele Bytes aus dem Socket gelesen werden müssen, müssen Sie wissen, wie viel die übertragene Datei wiegt. Die Dateigröße auf dem Server wird als Long Integer dargestellt, daher werden hier 4 Bytes akzeptiert, die anschließend in eine Zahl umgewandelt werden. Dies ist kein sehr Java-Ansatz, er ist für SI ziemlich ähnlich, aber er löst sein Problem.

Dann ist alles trivial – wir erhalten eine bekannte Anzahl von Bytes vom Socket und schreiben sie in eine Datei, woraufhin wir eine Erfolgsmeldung anzeigen.

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

Wenn ein anderer Befehl als get oder put in das Client-Fenster eingegeben wurde, wird die Funktion showErrorMessage aufgerufen, um anzuzeigen, dass die Eingabe falsch war. Aus Gründen der Trivialität werde ich es nicht zitieren. Etwas interessanter ist die Funktion zum Empfangen und Aufteilen des Eingabestrings. Darin übergeben wir den Scanner, von dem wir eine durch zwei Leerzeichen getrennte Zeile mit dem Befehl, der Quelladresse und der Zieladresse erwarten.

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

Senden eines Befehls – überträgt den eingegebenen Befehl vom Scanner an den Socket und erzwingt dessen 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());
        }
    }

Ein Selektor ist eine Funktion, die die Aktionen des Programms abhängig von der eingegebenen Zeichenfolge bestimmt. Alles hier ist nicht sehr schön und der verwendete Trick ist nicht der beste mit erzwungenem Verlassen außerhalb des Codeblocks, aber der Hauptgrund dafür ist das Fehlen einiger Dinge in Java, wie Delegaten in C#, Funktionszeiger aus C++ oder at Zumindest die schrecklichen und schrecklichen Goto, mit denen Sie dies wunderbar umsetzen können. Wenn Sie wissen, wie Sie den Code etwas eleganter gestalten können, freue ich mich über Kritik in den Kommentaren. Es scheint mir, dass hier ein String-Delegate-Wörterbuch 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 von der Funktionalität des Clients im Großen und Ganzen nur dadurch, dass Befehle nicht über die Tastatur, sondern über den Socket eingehen. Einige der Methoden sind im Allgemeinen gleich, daher werde ich sie nicht zitieren, sondern nur auf die Unterschiede eingehen.

Zum Start wird die run-Methode verwendet, die einen Port als Eingabe empfängt und die Eingabedaten vom Socket in einer ewigen Schleife verarbeitet.

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

Die put-Methode, die die writeToFileFromSocket-Methode umschließt, die einen Schreibstream in eine Datei öffnet und alle Eingabebytes aus dem Socket schreibt, zeigt eine Meldung an, die den erfolgreichen Abschluss der Übertragung angibt, wenn der Schreibvorgang abgeschlossen ist.

    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 get-Methode ruft die Serverdatei ab. Wie bereits im Abschnitt über die Client-Seite des Programms erwähnt, müssen Sie zum erfolgreichen Übertragen einer Datei deren Größe kennen, die in einer langen Ganzzahl gespeichert ist. Deshalb teile ich sie in ein Array von 4 Bytes auf und übertrage sie Byte für Byte an den Socket, und nachdem ich sie dann auf dem Client empfangen und zu einer Zahl zusammengefügt habe, übertrage ich alle Bytes, aus denen die Datei besteht, und lese sie aus dem Eingabestream aus der Datei.


 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 getAndParseInput-Methode ist dieselbe wie im Client, der einzige Unterschied besteht darin, dass sie Daten vom Socket und nicht von der Tastatur liest. Der Code befindet sich im Repository, genau wie der Selektor.
In diesem Fall wird die Initialisierung in einem separaten Codeblock platziert, weil Innerhalb dieser Implementierung werden Ressourcen nach Abschluss der Übertragung freigegeben und erneut belegt – wiederum zum Schutz vor Speicherlecks.

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

Zusammenfassend:

Wir haben gerade unsere eigene Variante eines einfachen Datenübertragungsprotokolls geschrieben und herausgefunden, wie es funktionieren sollte. Im Prinzip habe ich Amerika hier nicht entdeckt und nicht viel Neues geschrieben, aber es gab keine ähnlichen Artikel über Habré, und als ich eine Reihe von Artikeln über cmd-Dienstprogramme schrieb, war es unmöglich, nicht darauf einzugehen.

Links:

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

Source: habr.com

Kommentar hinzufügen