Pisanje programske opreme s funkcionalnostjo pripomočkov Windows odjemalec-strežnik, 02. del

Če nadaljujemo s tekočo serijo člankov, posvečenih implementacijam pripomočkov konzole Windows po meri, se ne moremo kaj, da se ne bi dotaknili TFTP (Trivial File Transfer Protocol) - preprostega protokola za prenos datotek.

Kot zadnjič, poglejmo na kratko teorijo, poglejmo kodo, ki implementira funkcionalnost, podobno zahtevani, in jo analizirajmo. Več podrobnosti - pod rezom

Ne bom kopiral/prilepil referenčnih informacij, povezave do katerih so običajno na koncu članka, povedal bom le, da je TFTP v svojem bistvu poenostavljena različica protokola FTP, v katerem ima nastavitev nadzora dostopa je bil odstranjen in tukaj pravzaprav ni ničesar razen ukazov za sprejem in prenos datoteke. Da pa bi bila naša izvedba nekoliko bolj elegantna in prilagojena trenutnim principom pisanja kode, smo sintakso nekoliko spremenili – to ne spremeni principov delovanja, ampak vmesnik, IMHO, postane nekoliko bolj logičen in združuje pozitivne vidike FTP in TFTP.

Zlasti ob zagonu odjemalec zahteva naslov IP strežnika in vrata, na katerih je odprt TFTP po meri (zaradi nezdružljivosti s standardnim protokolom se mi je zdelo primerno, da pustim uporabniku možnost izbire vrat), nato pa pride do povezave, zaradi katere lahko odjemalec pošlje enega od ukazov - get ali put, za sprejem ali pošiljanje datoteke na strežnik. Vse datoteke so poslane v binarnem načinu za poenostavitev logike.

Za izvedbo protokola sem tradicionalno uporabljal 4 razrede:

  • TFTPClient
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

Ker testni razredi obstajajo samo za odpravljanje napak glavnih, jih ne bom analiziral, vendar bo koda v skladišču, povezava do nje je na koncu članka. Zdaj bom pogledal glavne razrede.

TFTPClient

Naloga tega razreda je, da se poveže z oddaljenim strežnikom po njegovem IP-ju in številki vrat, prebere ukaz iz vhodnega toka (v tem primeru tipkovnice), ga razčleni, prenese na strežnik in, odvisno od tega, ali morate poslati ali prejeti datoteko, jo prenesti ali prejeti.

Koda za zagon odjemalca za povezavo s strežnikom in čakanje na ukaz iz vhodnega toka izgleda takole. Številne globalne spremenljivke, ki se uporabljajo tukaj, so opisane zunaj članka, v celotnem besedilu programa. Zaradi njihove trivialnosti jih ne navajam, da ne preobremenim članka.

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

Oglejmo si metode, klicane v tem bloku kode:

Tukaj je datoteka poslana - s pomočjo optičnega bralnika predstavimo vsebino datoteke kot niz bajtov, ki jih enega za drugim zapišemo v vtičnico, nato pa jo zapremo in ponovno odpremo (ni najbolj očitna rešitev, vendar zagotavlja sprostitev sredstev), nato pa prikažemo sporočilo o uspešnem prenosu.

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

Ta fragment kode opisuje pridobivanje podatkov s strežnika. Vse je spet trivialno, zanimiv je le prvi blok kode. Da bi natančno razumeli, koliko bajtov je treba prebrati iz vtičnice, morate vedeti, koliko tehta prenesena datoteka. Velikost datoteke na strežniku je predstavljena kot dolgo celo število, zato so tukaj sprejeti 4 bajci, ki se nato pretvorijo v eno številko. To ni zelo javanski pristop, precej podoben je za SI, vendar rešuje njegov problem.

Potem je vse trivialno - iz vtičnice prejmemo znano število bajtov in jih zapišemo v datoteko, nato pa prikažemo sporočilo o uspehu.

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

Če je bil v odjemalsko okno vnesen ukaz, ki ni get ali put, bo poklicana funkcija showErrorMessage, ki nakazuje, da je bil vnos napačen. Zaradi trivialnosti ga ne bom navajal. Nekoliko bolj zanimiva je funkcija sprejema in razdelitve vhodnega niza. Vanj podamo skener, od katerega pričakujemo vrstico, ločeno z dvema presledkoma, ki vsebuje ukaz, izvorni naslov in ciljni naslov.

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

Pošiljanje ukaza—pošilja ukaz, ki ga vnesete iz optičnega bralnika, v vtičnico in prisili, da se pošlje

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

Izbirnik je funkcija, ki določa dejanja programa glede na vneseni niz. Vse tukaj ni zelo lepo in uporabljeni trik ni najboljši s prisilnim izhodom iz kodnega bloka, vendar je glavni razlog za to odsotnost nekaterih stvari v Javi, kot so delegati v C#, funkcijski kazalci iz C++ ali na vsaj strašni in strašni goto, ki vam omogočajo, da to lepo izvedete. Če veste, kako narediti kodo nekoliko bolj elegantno, pozdravljam kritiko v komentarjih. Zdi se mi, da je tukaj potreben slovar za niz-delegate, vendar ni nobenega 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 strežnika se od funkcionalnosti odjemalca na splošno razlikuje le po tem, da ukazi nanj ne prihajajo s tipkovnice, temveč iz vtičnice. Nekatere metode so na splošno enake, zato jih ne bom navajal, dotaknil se bom le razlik.

Za začetek se uporabi metoda run, ki kot vhod sprejme vrata in obdela vhodne podatke iz vtičnice v večni zanki.

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

Metoda put, ki ovije metodo writeToFileFromSocket, ki odpre pisalni tok v datoteko in zapiše vse vhodne bajte iz vtičnice, prikaže sporočilo, ki označuje uspešen zaključek prenosa, ko je pisanje končano.

    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 pridobi strežniško datoteko. Kot je bilo že omenjeno v razdelku o odjemalski strani programa, morate za uspešen prenos datoteke poznati njeno velikost, shranjeno v dolgem celem številu, zato sem jo razdelil v niz 4 bajtov, ki jih prenašam bajt za bajtom v vtičnico, nato pa, ko jih prejmem in sestavim na stranki v številko nazaj, prenesem vse bajte, ki sestavljajo datoteko, prebrane iz vhodnega 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 je enaka kot v odjemalcu, razlika je le v tem, da bere podatke iz vtičnice in ne s tipkovnice. Koda je v repozitoriju, tako kot izbirnik.
V tem primeru je inicializacija postavljena v ločen blok kode, ker v okviru te izvedbe se po končanem prenosu viri sprostijo in ponovno zasedejo – spet zaradi zagotavljanja zaščite pred uhajanjem pomnilnika.

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

Povzeti:

Pravkar smo napisali lastno različico preprostega protokola za prenos podatkov in ugotovili, kako bi moral delovati. Načeloma tukaj nisem odkril Amerike in nisem napisal veliko novih stvari, vendar na Habréju ni bilo podobnih člankov in v okviru pisanja serije člankov o pomožnih programih cmd je bilo nemogoče, da se tega ne bi dotaknili.

Reference:

Repozitorij izvorne kode
Na kratko o TFTP
Ista stvar, vendar v ruščini

Vir: www.habr.com

Dodaj komentar