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

Приветствую.
Сегодня хотелось бы разобрать процесс написания клиент-серверных приложений, выполняющих функции стандартных утилит Windows, как то Telnet, TFTP, et cetera, et cetera на чистой Javа. Понятно, что ничего нового я не привнесу — все эти утилиты уже успешно работают не один год, но, полагаю, что происходит под капотом у них знают не все.
Именно об этом и пойдет речь под катом.

В этой статье, для того, что бы ее не затягивать, помимо общей информации я напишу только о Telnet сервере, но на данный момент есть еще материал по другим утилитам — он будет в дальнейших частях цикла.

Прежде всего, следует разобраться, что же такое Telnet, для чего он нужен и с чем его едят. Не буду дословно цитировать источники(если надо — в конце статьи прикреплю ссылку на материалы по теме), скажу лишь, что Telnet обеспечивает удаленный доступ к командной строке устройства. По большому, на этом его функционал заканчивается (о обращении к серверному порту умолчал осознанно, об этом позднее). Значит, для его реализации, нам нужно принять строку на клиенте, передать ее на сервер, попытаться передать ее в командную строку, считать ответ командной строки, если он есть, передать его обратно на клиент и вывести на экран, либо же, в случае возникновения ошибки, дать пользователю понять, что что-то не так.

Для реализации вышеизложенного, соответственно, нужно 2 рабочих класса, и некоторый тестовый класс, из которого мы будем запускать сервер и через который будет работать клиент.
Соответственно, на данный момент структура приложения включает в себя:

  • TelnetClient
  • TelnetClientTester
  • TelnetServer
  • TelnetServerTester

Пробежимся по каждому из них:
TelnetClient
Все, что должен уметь делать этот класс — отправлять полученные команды и показывать полученные ответы. Кроме того, нужно уметь подключаться к произвольному (о чем говорил выше) порту удаленного устройства и отключаться от него.
Для этого были реализованы следующие функции:

Функция, принимающая в качестве аргумента адрес сокета, открывающая соединение и запискающая потоки ввода и вывода (переменыые потока объявлены выше, полные исходники — в конце статьи)

 public void run(String ip, int port)     {         try {             Socket socket = new Socket(ip, port);             InputStream sin = socket.getInputStream();             OutputStream sout = socket.getOutputStream();             Scanner keyboard = new Scanner(System.in);             reader = new Thread(()->read(keyboard, sout));             writer = new Thread(()->write(sin));             reader.start();             writer.start();         }         catch (Exception e) {             System.out.println(e.getMessage());         }     } 

Перегрузка этой же функции, поключающаяся к порту по умолчанию — для телнета это 23

     public void run(String ip)     {         run(ip, 23);     } 

Функция читает символы с клавиатуры и отправляет их на выходной сокет — что характерно, в строчом, а не символьном режиме

     private void read(Scanner keyboard, OutputStream sout)     {         try {             String input = new String();             while (true) {                 input = keyboard.nextLine();                 for (char i : (input + " \n").toCharArray())                     sout.write(i);             }         }         catch (Exception e) {             System.out.println(e.getMessage());         }     } 

Функция принимает данные с сокета и выводит их на экран

     private void write(InputStream sin)     {         try {             int tmp;             while (true){                 tmp = sin.read();                 System.out.print((char)tmp);             }         }         catch (Exception e) {             System.out.println(e.getMessage());         }     } 

Функция останавливает прием и передачу данных

     public void stop()     {         reader.stop();         writer.stop();     } }

TelnetServer
Этот класс должен обладать функционалом принятия команды с сокета, отправки ее на исполнение и отправки ответа от команды обратно на сокет. В программе умышленно нет проверки входных данных, потому что во-первых, и в «коробочном телнете» есть возможность форматнуть диск сервера, а во-вторых, вопрос безопасности в этой статье опущен в принципе, и именно поэтому тут нет ни слова о шифровании или SSL.
Тут всего 2 функции(одна из них перегружена), и в целом это не очень хорошая практика, однако в рамках данной задачи мне показалось уместным оставить все как есть.

 boolean isRunning = true;     public void run(int port)    {          (new Thread(()->{ try {             ServerSocket ss = new ServerSocket(port); // создаем сокет сервера и привязываем его к вышеуказанному порту             System.out.println("Port "+port+" is waiting for connections");              Socket socket = ss.accept();             System.out.println("Connected");             System.out.println();              // Берем входной и выходной потоки сокета, теперь можем получать и отсылать данные клиенту.             InputStream sin = socket.getInputStream();             OutputStream sout = socket.getOutputStream();              Map<String, String> env = System.getenv();             String wayToTemp = env.get("TEMP") + "\\tmp.txt";             for (int i :("Connected\n\n\r".toCharArray()))                 sout.write(i);             sout.flush();              String buffer = new String();             while (isRunning) {                  int intReader = 0;                 while ((char) intReader != '\n') {                     intReader = sin.read();                     buffer += (char) intReader;                 }                   final String inputToSubThread = "cmd /c " + buffer.substring(0, buffer.length()-2) + " 2>&1";                   new Thread(()-> {                     try {                          Process p = Runtime.getRuntime().exec(inputToSubThread);                         InputStream out = p.getInputStream();                         Scanner fromProcess = new Scanner(out);                         try {                              while (fromProcess.hasNextLine()) {                                 String temp = fromProcess.nextLine();                                 System.out.println(temp);                                 for (char i : temp.toCharArray())                                     sout.write(i);                                 sout.write('\n');                                 sout.write('\r');                             }                         }                         catch (Exception e) {                             String output = "Something gets wrong... Err code: "+ e.getStackTrace();                             System.out.println(output);                             for (char i : output.toCharArray())                                 sout.write(i);                             sout.write('\n');                             sout.write('\r');                         }                          p.getErrorStream().close();                         p.getOutputStream().close();                         p.getInputStream().close();                         sout.flush();                      }                     catch (Exception e) {                         System.out.println("Error: " + e.getMessage());                     }                 }).start();                 System.out.println(buffer);                 buffer = "";              }         }         catch(Exception x) {             System.out.println(x.getMessage());         }})).start();      } 

Программа открывает серверный порт, читает с него данные, пока не встретит символ окончания команды, передает команду в новый процесс, а вывод из процесса перенаправлен в сокет. Все просто как автомат Калашникова.
Соответственно, для этой функции существует перегрузка с портом по умолчанию

 public void run()     {         run(23);     }

Ну и соответственно, функция, останавливающая сервер — тоже все тривиально, она прерывает вечный цикл, нарушая его условие.

    public void stop()     {         System.out.println("Server was stopped");         this.isRunning = false;     }

Тестовые классы я тут приводить не буду, они есть внизу — все что они делают — проверяют работоспособность публичных методов. Все есть на гите

Резюмируя, за пару вечеров можно понять принципы действия основных консольных утилит. Теперь, когда мы телнетимся к удаленному компу, мы понимаем, что происходит — магия исчезла)

Итак, ссылки:
github.com/Toxa-p07a1330/Temviewer — все исходники были, есть и будут есть здесь
www.extrahop.com/resources/protocols/telnet — о Телнете
www.lifewire.com/what-does-telnet-do-2483642 — еще о Телнете


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

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

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