Приветствую.
Сегодня хотелось бы разобрать процесс написания клиент-серверных приложений, выполняющих функции стандартных утилит 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 :("Connectednnr".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;
}
Тестовые классы я тут приводить не буду, они есть внизу — все что они делают — проверяют работоспособность публичных методов. Все есть на гите.
Резюмируя, за пару вечеров можно понять принципы действия основных консольных утилит. Теперь, когда мы телнетимся к удаленному компу, мы понимаем, что происходит — магия исчезла)
Итак, ссылки:
Источник: habr.com