Поздравявам.
Днес ще разгледаме процеса на написване на клиент-сървърни приложения, които изпълняват функции на стандартни утилити Windows, като Telnet, TFTP и т.н. Разбира се, няма да предложа нищо ново – всички тези утилити съществуват успешно от години, но предполагам, че не всеки знае какво се случва зад сцената.
Точно за това ще стане дума по-долу.
В тази статия, за да не я затягам, освен общата информация, ще пиша само за Telnet сървъра, но в момента имам и материали за други утилити – те ще бъдат в следващите части на цикъла.
Първо, трябва да разберем какво е Telnet, за какво служи и как работи. Няма да цитирам буквално източниците (ако е нужно – ще прикрепя линк към материалите по темата в края на статията), само ще кажа, че Telnet осигурява отдалечен достъп до командния ред на устройството. По същество, на това свършва неговата функционалност (за взаимодействието със сървърния порт умишлено мълча, за което ще стане дума по-късно). Следователно, за неговата реализация, трябва да приемем команда на клиента, да я предадем на сървъра, да опитаме да я предадем в командния ред, да прочетем отговор от командния ред, ако има такъв, да го върнем обратно на клиента и да го покажем на екрана или, в случай на грешка, да уведомим потребителя, че нещо не е наред.
За реализиране на гореописаното, съответно, са нужни 2 работещи класа и един тестов клас, от който ще стартираме сървъра и през който клиентът ще работи.
Съответно, в момента структурата на приложението включва:
- TelnetClient
- TelnetClientTester
- TelnetServer
- TelnetServerTester
Нека разгледаме всеки от тях:
TelnetClient
Всичко, което този клас трябва да може, е да изпраща получените команди и да показва получените отговори. Освен това, трябва да може да се свързва с произволен (както споменах по-горе) порт на отдалеченото устройство и да се изключва от него.
За това са реализирани следните функции:
Функция, приемаща като аргумент адреса на сокета, отваряща връзка и стартираща потоците за въвеждане и извеждане (променливите на потока са обявени по-горе, пълните изходни коди – в края на статията).
публичен void run(String ip, int port)
{
опитай {
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();
}
хващай (Изключение e) {
System.out.println(e.getMessage());
}
}
Презареждане на същата функция, която се свързва с порта по подразбиране — за telnet е 23
публичен void run(String ip)
{
run(ip, 23);
}
Функцията чете символи от клавиатурата и ги изпраща на изходния сокет — характерно е, че в строчен, а не в символен режим:
private void read(Scanner keyboard, OutputStream sout)
{
опитай {
String input = new String();
докато (вярно) {
input = keyboard.nextLine();
за (char i : (input + " n").toCharArray())
sout.write(i);
}
}
хващай (Изключение e) {
System.out.println(e.getMessage());
}
}
Функцията приема данни от сокета и ги извежда на екрана
private void write(InputStream sin)
{
опитай {
int tmp;
докато (вярно){
tmp = sin.read();
System.out.print((char)tmp);
}
}
хващай (Изключение e) {
System.out.println(e.getMessage());
}
}
Функцията спира приемането и предаването на данни
публичен void stop()
{
reader.stop();
writer.stop();
}
}TelnetServer
Този клас трябва да притежава функционалност за приемане на команда от сокета, изпращането й за изпълнение и връщането на отговора обратно на сокета. В програмата няма вариантна проверка на входните данни, защото, от една страна, в "корпусния telnet" съществува възможност за форматиране на диска на сървъра, а от друга страна, въпросът за сигурността тук е изцяло пропуснат, и по тази причина няма и дума за криптиране или SSL.
Тук има само 2 функции (едната от които е презаредена), и като цяло това не е много добра практика, но в рамките на зададената задача ми се стори уместно да оставя всичко както е.
boolean isRunning = true;
public void run(int port) {
(new Thread(() -> { try {
ServerSocket ss = new ServerSocket(port); // създаваме сървърен сокет и го свързваме към посочения порт
System.out.println("Порт " + port + " чака за връзки");
Socket socket = ss.accept();
System.out.println("Свързано");
System.out.println();
// Вземаме входящия и изходящия поток на сокета, сега можем да получаваме и изпращаме данни на клиента.
InputStream sin = socket.getInputStream();
OutputStream sout = socket.getOutputStream();
Map 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 = "Нещо се обърка... Код на грешка: " + 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("Грешка: " + e.getMessage());
}
}).start();
System.out.println(buffer);
buffer = "";
}
}
catch(Exception x) {
System.out.println(x.getMessage());
}})).start();
}
Програмата отваря сървърен порт, чете от него данни, докато не срещне символ за край на командата, предава командата на нов процес, а изхода от процеса се пренасочва в сокета. Всичко е толкова просто като автомат Калашников.
Съответно, за тази функция съществува преозареждане с порт по подразбиране:
public void run()
{
run(23);
}И съответно, функцията, която спира сървъра — също е тривиална, тя прекратява вечния цикъл, нарушавайки условията му.
публичен void stop()
{
System.out.println("Сървърът беше спрян");
this.isRunning = false;
}Няма да давам примери за тестови класове тук, те са в долната част — всичко, което правят, е да проверят работоспособността на публичните методи. Всичко е на гита.
В обобщение, за няколко вечери можете да разберете принципите на основните конзолни утилити. Сега, когато се свържем с отдалечен компютър, разбираме какво се случва — магията изчезна)
И така, линкове:
Източник: habr.com
