Pisanje softvera s funkcionalnošću Windows klijent-poslužitelj pomoćnih programa, dio 02

Nastavljajući niz članaka koji su u tijeku posvećen prilagođenim implementacijama uslužnih programa Windows konzole, ne možemo a da se ne dotaknemo TFTP-a (Trivial File Transfer Protocol) - jednostavnog protokola za prijenos datoteka.

Kao i prošli put, prođimo ukratko kroz teoriju, pogledajmo kod koji implementira funkcionalnost sličnu potrebnoj i analizirajmo ga. Više detalja - ispod reza

Neću copy-paste referentne informacije, veze na koje se tradicionalno mogu pronaći na kraju članka, samo ću reći da je u svojoj srži TFTP pojednostavljena varijacija FTP protokola, u kojoj postavka kontrole pristupa ima je uklonjen, i zapravo ovdje nema ničega osim naredbi za primanje i prijenos datoteke. No, kako bi naša implementacija bila malo elegantnija i prilagođena trenutnim principima pisanja koda, sintaksa je malo promijenjena - time se ne mijenjaju principi rada, ali sučelje, IMHO, postaje malo logičnije i kombinira pozitivne aspekte FTP-a i TFTP-a.

Konkretno, kada se pokrene, klijent traži IP adresu poslužitelja i port na kojem je otvoren prilagođeni TFTP (zbog nekompatibilnosti sa standardnim protokolom, smatrao sam prikladnim ostaviti korisniku mogućnost odabira porta), nakon čega se dolazi do povezivanja, uslijed čega klijent može poslati jednu od naredbi - get ili put, za primanje ili slanje datoteke poslužitelju. Sve se datoteke šalju u binarnom načinu radi pojednostavljenja logike.

Za implementaciju protokola tradicionalno sam koristio 4 klase:

  • TFTPClijent
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

Zbog činjenice da klase testiranja postoje samo za otklanjanje pogrešaka glavnih, neću ih analizirati, ali kod će biti u repozitoriju, a poveznica na njega nalazi se na kraju članka. Sada ću pogledati glavne klase.

TFTPClijent

Zadatak ove klase je povezati se s udaljenim poslužiteljem pomoću njegove ip adrese i broja porta, pročitati naredbu iz ulaznog toka (u ovom slučaju tipkovnice), analizirati je, prenijeti na poslužitelj i, ovisno o tome hoćete li trebate poslati ili primiti datoteku, prenijeti je ili dobiti.

Kod za pokretanje klijenta za spajanje na poslužitelj i čekanje naredbe iz ulaznog toka izgleda ovako. Brojne globalne varijable koje se ovdje koriste opisane su izvan članka, u punom tekstu programa. Zbog njihove trivijalnosti ne navodim ih da ne opterećujem članak.

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

Prođimo kroz metode koje se pozivaju u ovom bloku koda:

Ovdje se datoteka šalje - pomoću skenera prikazujemo sadržaj datoteke kao niz bajtova koje jedan po jedan upisujemo u socket, nakon čega ga zatvaramo i ponovno otvaramo (nije najočitije rješenje, ali jamči oslobađanje resursa), nakon čega prikazujemo poruku o uspješnom prijenosu.

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

Ovaj fragment koda opisuje dohvaćanje podataka s poslužitelja. Sve je opet trivijalno, interesantan je samo prvi blok koda. Da biste točno razumjeli koliko bajtova treba pročitati iz utičnice, morate znati koliko teži prenesena datoteka. Veličina datoteke na poslužitelju predstavljena je kao dugi cijeli broj, tako da su ovdje prihvaćena 4 bajta, koji se naknadno pretvaraju u jedan broj. Ovo nije baš Java pristup, prilično je sličan za SI, ali rješava njegov problem.

Tada je sve trivijalno - primamo poznati broj bajtova iz utičnice i upisujemo ih u datoteku, nakon čega prikazujemo poruku o uspješnosti.

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

Ako je u prozor klijenta unesena naredba koja nije get ili put, bit će pozvana funkcija showErrorMessage, pokazujući da je unos bio netočan. Zbog trivijalnosti ga neću navoditi. Nešto zanimljivija je funkcija primanja i cijepanja ulaznog niza. U njega propuštamo skener od kojeg očekujemo redak odvojen s dva razmaka koji sadrži naredbu, adresu izvora i adresu odredišta.

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

Slanje naredbe—prenosi naredbu unesenu sa skenera u utičnicu i prisiljava je na slanje

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

Selektor je funkcija koja određuje radnje programa ovisno o unesenom nizu. Sve ovdje nije baš lijepo i korišteni trik nije najbolji s prisilnim izlazom izvan bloka koda, ali glavni razlog za to je nedostatak nekih stvari u Javi, poput delegata u C#, pokazivača na funkcije iz C++ ili na najmanje strašni i strašni goto, koji vam omogućuju da ovo lijepo implementirate. Ako znate kako kod učiniti malo elegantnijim, pozdravljam kritike u komentarima. Čini mi se da je ovdje potreban String-delegate rječnik, ali nema delegata...

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

TFTPServer

Funkcionalnost poslužitelja razlikuje se od funkcionalnosti klijenta, uglavnom, samo po tome što mu naredbe ne dolaze s tipkovnice, već iz utičnice. Neke su metode općenito iste, pa ih neću navoditi, dotaknut ću se samo razlika.

Za početak se koristi metoda run koja prima port kao ulaz i obrađuje ulazne podatke iz utičnice u vječnoj petlji.

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

Metoda put, koja obavija metodu writeToFileFromSocket koja otvara tok pisanja u datoteku i zapisuje sve ulazne bajtove iz utičnice, prikazuje poruku koja označava uspješan završetak prijenosa kada pisanje završi.

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

Metoda get dohvaća datoteku poslužitelja. Kao što je već spomenuto u odjeljku o klijentskoj strani programa, za uspješan prijenos datoteke trebate znati njezinu veličinu, pohranjenu u dugom cijelom broju, pa sam je podijelio u niz od 4 bajta, prenio ih bajt po bajt u utičnicu, a zatim, nakon što sam ih primio i sklopio na klijentu u broj natrag, prenosim sve bajtove koji čine datoteku, pročitane iz ulaznog toka iz datoteke.


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

Metoda getAndParseInput ista je kao u klijentu, jedina je razlika u tome što čita podatke iz utičnice, a ne s tipkovnice. Kod je u repozitoriju, baš kao i selektor.
U ovom slučaju, inicijalizacija se nalazi u zasebnom bloku koda, jer unutar ove implementacije, nakon dovršetka prijenosa, resursi se oslobađaju i ponovno zauzimaju - ponovno kako bi se osigurala zaštita od curenja memorije.

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

Sažeti:

Upravo smo napisali vlastitu varijaciju jednostavnog protokola za prijenos podataka i shvatili kako bi trebao funkcionirati. U principu, ovdje nisam otkrio Ameriku i nisam napisao puno novih stvari, ali na Habréu nije bilo sličnih članaka, au sklopu pisanja serije članaka o cmd uslužnim programima bilo je nemoguće ne dotaknuti ga se.

reference:

Repozitorij izvornog koda
Ukratko o TFTP-u
Ista stvar, ali na ruskom

Izvor: www.habr.com

Dodajte komentar