Как и в прошлой раз, кратко пробежимся по теории, увидим код, реализующий функционал, аналогичный требуемому, и проанализируем его. Подробнее — под катом
Не буду копипастить справочную информацию, ссылки на которую традиционно можно найти в конце статьи, скажу лишь, что по своей сути 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/
Добавить комментарий