Software schrijven met de functionaliteit van Windows client-server-hulpprogramma's, deel 02

Voortbordurend op de lopende reeks artikelen gewijd aan aangepaste implementaties van Windows-consolehulpprogramma's, kunnen we niet anders dan TFTP (Trivial File Transfer Protocol) aanroeren - een eenvoudig protocol voor bestandsoverdracht.

Laten we, net als de vorige keer, kort de theorie doornemen, de code bekijken die functionaliteit implementeert die vergelijkbaar is met de vereiste functionaliteit, en deze analyseren. Meer details - onder de snit

Ik zal de referentie-informatie, waarnaar traditioneel aan het eind van het artikel wordt verwezen, niet kopiëren en plakken. Ik wil alleen zeggen dat TFTP in essentie een vereenvoudigde variant is van het FTP-protocol, waarbij de toegangscontrole-instelling verwijderd, en in feite is er hier niets anders dan opdrachten voor het ontvangen en overbrengen van een bestand. Om onze implementatie echter iets eleganter te maken en aan te passen aan de huidige principes van het schrijven van code, is de syntaxis enigszins gewijzigd - dit verandert niets aan de werkingsprincipes, maar de interface wordt, IMHO, een beetje logischer en combineert de positieve aspecten van FTP en TFTP.

Wanneer de client wordt gestart, vraagt ​​hij met name om het IP-adres van de server en de poort waarop aangepast TFTP open is (vanwege de incompatibiliteit met het standaardprotocol vond ik het passend om de gebruiker de mogelijkheid te geven een poort te selecteren), waarna een Er vindt een verbinding plaats, waardoor de client een van de opdrachten kan verzenden: get of put, om een ​​bestand naar de server te ontvangen of te verzenden. Alle bestanden worden in binaire modus verzonden om de logica te vereenvoudigen.

Om het protocol te implementeren, gebruikte ik traditioneel 4 klassen:

  • TFTPClient
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

Vanwege het feit dat testklassen alleen bestaan ​​voor het debuggen van de belangrijkste, zal ik ze niet analyseren, maar de code zal in de repository staan; een link ernaar is te vinden aan het einde van het artikel. Nu zal ik naar de hoofdklassen kijken.

TFTPClient

De taak van deze klasse is om via het IP- en poortnummer verbinding te maken met een externe server, een opdracht uit de invoerstroom (in dit geval het toetsenbord) te lezen, deze te parseren, over te dragen naar de server en, afhankelijk van of u een bestand moet verzenden of ontvangen, overbrengen of ophalen.

De code voor het starten van de client om verbinding te maken met de server en te wachten op een opdracht uit de invoerstroom ziet er als volgt uit. Een aantal globale variabelen die hier gebruikt worden, worden buiten het artikel beschreven, in de volledige tekst van het programma. Vanwege hun trivialiteit citeer ik ze niet om het artikel niet te overbelasten.

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

Laten we de methoden bekijken die in dit codeblok worden aangeroepen:

Hier wordt het bestand verzonden - met behulp van een scanner presenteren we de inhoud van het bestand als een array van bytes, die we één voor één naar de socket schrijven, waarna we deze sluiten en opnieuw openen (niet de meest voor de hand liggende oplossing, maar het garandeert de vrijgave van middelen), waarna we een bericht weergeven over een succesvolle overdracht.

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

Dit codefragment beschrijft het ophalen van gegevens van de server. Alles is weer triviaal, alleen het eerste codeblok is interessant. Om precies te begrijpen hoeveel bytes uit de socket moeten worden gelezen, moet u weten hoeveel het overgedragen bestand weegt. De bestandsgrootte op de server wordt weergegeven als een lang geheel getal, dus hier worden 4 bytes geaccepteerd, die vervolgens worden omgezet in één getal. Dit is niet echt een Java-aanpak, het is eerder vergelijkbaar voor SI, maar het lost het probleem op.

Dan is alles triviaal: we ontvangen een bekend aantal bytes uit de socket en schrijven deze naar een bestand, waarna we een succesbericht weergeven.

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

Als er een ander commando dan get of put in het clientvenster is ingevoerd, wordt de functie showErrorMessage aangeroepen, wat aangeeft dat de invoer onjuist was. Vanwege trivialiteit zal ik het niet citeren. Iets interessanter is de functie van het ontvangen en splitsen van de invoerreeks. We geven de scanner erin door, van waaruit we een regel verwachten, gescheiden door twee spaties, met daarin het commando, het bronadres en het bestemmingsadres.

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

Een opdracht verzenden: verzendt de ingevoerde opdracht van de scanner naar de socket en dwingt verzending ervan af

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

Een selector is een functie die de acties van het programma bepaalt, afhankelijk van de ingevoerde string. Alles is hier niet erg mooi en de gebruikte truc is niet de beste met gedwongen uitgang buiten het codeblok, maar de belangrijkste reden hiervoor is de afwezigheid in Java van sommige dingen, zoals afgevaardigden in C#, functieaanwijzers uit C++, of op in ieder geval de vreselijke en verschrikkelijke goto, waarmee je dit prachtig kunt implementeren. Als je weet hoe je de code iets eleganter kunt maken, verwelkom ik kritiek in de reacties. Het lijkt mij dat hier een String-delegate-woordenboek nodig is, maar er is geen afgevaardigde...

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

TFTPServer

De functionaliteit van de server verschilt over het algemeen van de functionaliteit van de client, alleen doordat de opdrachten niet via het toetsenbord komen, maar via de socket. Sommige methoden zijn over het algemeen hetzelfde, dus ik zal ze niet noemen, ik zal alleen de verschillen bespreken.

Om te beginnen wordt gebruik gemaakt van de run-methode, die een poort als invoer ontvangt en de invoergegevens uit de socket in een eeuwige lus verwerkt.

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

De put-methode, die de writeToFileFromSocket-methode omhult die een schrijfstroom naar een bestand opent en alle invoerbytes uit de socket schrijft, geeft een bericht weer dat de succesvolle voltooiing van de overdracht aangeeft wanneer het schrijven is voltooid.

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

De get-methode haalt het serverbestand op. Zoals al vermeld in het gedeelte over de clientkant van het programma, moet je, om een ​​bestand succesvol over te dragen, de grootte ervan kennen, opgeslagen in een lang geheel getal, dus ik heb het opgesplitst in een array van 4 bytes, en ze byte voor byte overbrengen naar de socket, en vervolgens, nadat ik ze op de client heb ontvangen en samengevoegd tot een nummer terug, breng ik alle bytes waaruit het bestand bestaat over, gelezen uit de invoerstroom van het bestand.


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

De getAndParseInput-methode is hetzelfde als in de client, met als enige verschil dat deze gegevens van de socket leest in plaats van van het toetsenbord. De code bevindt zich in de repository, net als selector.
In dit geval wordt de initialisatie in een apart codeblok geplaatst, omdat binnen deze implementatie worden bronnen, nadat de overdracht is voltooid, vrijgegeven en opnieuw bezet - opnieuw om bescherming te bieden tegen geheugenlekken.

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

Samenvatten:

We hebben zojuist onze eigen variant op een eenvoudig gegevensoverdrachtprotocol geschreven en ontdekt hoe het zou moeten werken. In principe heb ik Amerika hier niet ontdekt en heb ik niet veel nieuwe dingen geschreven, maar er waren geen soortgelijke artikelen over Habré, en als onderdeel van het schrijven van een reeks artikelen over cmd-hulpprogramma's was het onmogelijk om er niet over te praten.

referenties:

Broncodeopslagplaats
Kort over TFTP
Hetzelfde, maar dan in het Russisch

Bron: www.habr.com

Voeg een reactie