Написання програмного забезпечення з функціоналом клієнт-серверних утиліт 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("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
Те саме, але російською

Джерело: habr.com

Додати коментар або відгук