Написание программного обеспечения с функционалом клиент-серверных утилит 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("\nDone\n");             }          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("\nDONE\n");        }        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("\nDone\n");     };     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
Тоже самое, но на русском


ссылка на оригинал статьи https://habr.com/ru/post/461083/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *