ونڈوز کلائنٹ سرور یوٹیلیٹیز کی فعالیت کے ساتھ سافٹ ویئر لکھنا، حصہ 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
Тоже самое, но на русском

ماخذ: www.habr.com

نیا تبصرہ شامل کریں