Написание программного обеспечения с функционалом клиент-серверных утилит Windows, part 02 +7


Продолжая начатый цикл статей, посвященный кастомным реализациям консольных утилит 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
Тоже самое, но на русском




Комментарии (2):

  1. sshikov
    /#20438823

    Типовой джуниорский говнокод. Вы никогда не слышали о том, что ресурсы нужно закрывать либо в finally, либо в варианте try with resources?

    • p07a1330
      /#20438919

      Вы правы, тут утечки не контрятся. Недоработка, учту