Продолжая начатый цикл статей, посвященный кастомным реализациям консольных утилит 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 нельзя было его не затронуть.
Ссылки:
Источник: habr.com