Напісанне праграмнага забеспячэння з функцыяналам кліент-серверных утыліт Windows, part 02

Працягваючы пачаты цыкл артыкулаў, прысвечаны кастамным рэалізацыям кансольных утыліт Windows нельга не закрануць TFTP (Trivial File Transfer Protocol) - просты пратакол перадачы файлаў.

Як і ў мінулы раз, коратка прабяжымся па тэорыі, убачым код, які рэалізуе функцыянал, аналагічны патрабаванаму, і прааналізуем яго. Падрабязней - пад катам

Не буду копіпастить даведачную інфармацыю, спасылкі на якую традыцыйна можна знайсці ў канцы артыкула, скажу толькі, што па сваёй сутнасці TFTP - спрошчаная варыяцыя пратакола FTP, у якой прыбраная настройка кантролю доступу, ды і па сутнасці тут няма нічога акрамя каманд атрымання і перадачы файла . Аднак, каб зрабіць нашу рэалізацыю крыху больш вытанчанай і адаптаванай да цяперашніх прынцыпаў напісання кода, сінтаксіс крыху зменены - прынцыпаў працы гэта не змяняе, але інтэрфейс, ИМХО, становіцца крыху больш за лагічным і спалучае ў сабе станоўчыя бакі FTP і TFTP.

У прыватнасці, пры запуску кліент запытвае ip адрас сервера і порт, на якім адкрыты кастамны TFTP (у сілу несумяшчальнасці са стандартным пратаколам я палічыў дарэчным пакінуць магчымасць выбару порта карыстачу), пасля чаго адбываецца злучэнне, у выніку якога кліент можа аправіць адну з каманд. get ці put, для атрымання ці адпраўкі файла на сервер. Усе файлы адпраўляюцца ў бінарным рэжыме - у мэтах спрашчэння логікі.

Для рэалізацыі протаолу мною было выкарыстана традыцыйна 4 класа:

  • TFTPClient
  • TFTPServer
  • TFTPClientTester
  • TFTPServerTester

У сілу таго, што тэставальныя класы існуюць толькі для адладкі асноўных, разбіраць я іх не буду, але код будзе знаходзіцца ў рэпазітары, са спасылкай на яго можна азнаёміцца ​​ў канцы артыкула. А асноўныя класы зараз разбяру.

TFTPClient

Задача гэтага класа - падлучыцца да выдаленага сервера па ім ip і нумару порта, лічыць з уваходнага струменя (у дадзеным выпадку - клавіятуры) каманду, распарсіць яе, перадаць серверу, і, у залежнасці ад таго, патрабуецца перадача ці атрыманне файла, перадаць яго ці атрымаць.

Код запуску кліента на падлучэнне да сервера і чаканне каманды з патоку ўводу выглядае так. Шэраг глабальных зменных, якія тут выкарыстоўваюцца, апісваюцца па-за межамі артыкула, у поўным тэксце праграмы. У сілу іх трывіяльнасці я не прыводжу, каб не перагружаць артыкул.

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

Прабяжымся па метадах, выкліканым у дадзеным блоку кода:

Тут адбываецца адпраўка файла - з дапамогай сканара мы прадстаўляем змесціва файла як масіў байтаў, якія па чарзе пішам у сокет, пасля чаго зачыняны яго і адчыняны нанова (не самае відавочнае рашэнне, але яно гарантуе вызваленне рэсурсаў), пасля чаго выводны на экран паведамленне аб паспяховай перадачы.

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

Дадзены фрагмент кода апісвае атрыманне даных з сервера. Усё ізноў-ткі трывіяльна, цікавасць уяўляе толькі першы блок кода. Для таго, што б разумець, колькі менавіта байт трэба лічыць з сокета, трэба ведаць, колькі важыць перадаваны файл. Памер файла на серверы ўяўляецца доўгім цэлым лікам, таму тут прымаецца 4 байта, якія ў далейшым канвертуюцца ў адзін лік. Гэта не вельмі дзіўны падыход, такое хутчэй падобна для СІ, але сваю задачу яно вырашае.

Далей усё трывіяльна - мы атрымліваем вядомы лік байтаў з сокета і запісваем іх у файл, пасля чаго выводны паведамленне аб поспеху.

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

У выпадку, калі ў акно кліента была ўведзена каманда, выдатная ад get ці put, будзе выклікана функцыя showErrorMessage, якая паказвае некарэктнасць инпута. У сілу трывіяльнасці - не прыводжу. Некалькі цікавей функцыя атрымання і разбіцця ўваходнага радка. У яе мы перадаем сканер, ад якога чакаем атрымаць радок, падзелены двума прабеламі і якая змяшчае ў сабе каманду, адрас крыніца і адрас прызначэння.

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

Адпраўка каманды - перадача уведзенай са сканара каманды ў сокет і прымусовая адпраўка яе

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

Селектар - функцыя, якая вызначае дзеянні праграмы ў залежнасці ад уведзенага радка. Тут усё не вельмі прыгожа і выкарыстоўваецца не самы добры прыём з прымусовым выхадам за межы блока кода, але асноўнай прычынай гэтага з'яўляецца адсутнасць у Джаве некаторых рэчаў, як дэлегаты ў З#, паказальнікі на функцыю з C++ ці хаця б страшны і жудасны goto, якія дазваляюць рэалізаваць гэта прыгожа. Калі ведаеце, як зрабіць код крыху больш вытанчаным - чакаю крытыку ў каментарах. Мне здаецца, што тут патрэбен слоўнік String-delegate, але дэлегата няма…

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

TFTPServer

Функцыянал сервера адрозніваецца ад функцыяналу кліента па вялікім рахунку толькі тым, што каманды на яго прыходзяць не з клавіятуры, а з сокета. Частка метадаў наогул супадае, таму прыводзіць іх я не буду, закрану толькі адрозненні.

Для запуску тут выкарыстоўваецца метад run, які атрымлівае на ўваход порт і які апрацоўвае ўваходныя дадзеныя з сокета ў вечным цыкле.

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

Метад put, які з'яўляецца абгорткай метаду writeToFileFromSocket, адкрывальнага струмень запісу ў файл і які запісвае ўсе байты ўводу з сокета, пасля завяршэння запісу выводзіць паведамленне аб паспяховым завяршэнні перадачы.

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

Метад get забяспечвае атрыманне файла сервера. Як ужо гаварылася ў раздзеле аб кліенцкім баку праграмы, для паспяховай перадачы файла трэба ведаць яе памер, які захоўваецца ў доўгім цэлым, таму я ажыццяўляю яго разбіццё на масіў з 4 байт, пабайтава перадаю іх у сокет, а потым, атрымаўшы і сабраўшы іх на кліенце у лік зваротна, перадаю ўсе байты, якія складаюць файл, лічаныя з струменя ўводу з файла.


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

Метад getAndParseInput супадае з аналагічным у кліенце, з той толькі розніцай, што ён счытвае дадзеныя з сокета, а не з клавіятуры. Код у рэпазітары, як і selector.
У дадзеным выпадку ініцыялізацыя вынесена ў асобны блок кода, т.я. у рамках дадзенай рэалізацыі пасля заканчэння перадачы рэсурсы вызваляюцца і зноў займаюцца зноўку - ізноў-ткі з мэтай забеспячэння абароны ад уцечкі памяці.

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

Рэзюмуючы:

Толькі што мы напісалі сваю варыяцыю на тэму простага пратакола перадачы даных і разабраліся ў тым, як ён павінен працаваць. У прынцыпе, Амерыкі я тут не адкрыў і моцна новага не напісаў, але - аналагічных артыкулаў на Хабре не было, а ў рамках напісання цыклу артыкулаў аб утылітах cmd нельга было яго не закрануць.

спасылкі:

Рэпазітар з зыходным кодам
Коратка аб TFTP
Тое самае, але на рускай

Крыніца: habr.com

Дадаць каментар