Здравейте.
Днес бих искал да разгледам процеса на писане на клиент-серверни приложения, изпълняващи функции на стандартни утилити за Windows, като Telnet, TFTP и т.н., на чиста Java. Ясно е, че няма да внеса нищо ново — всички тези утилити вече успешно функционират от години, но смятам, че не всеки знае какво става под капака им.
Точно за това ще става въпрос по-долу.
В тази статия, за да не я проточвам, освен общата информация, ще пиша само за 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());
}
}
Пренастройка на същата функция, която се свързва с порта по подразбиране — за Telnet това е 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
Този клас трябва да има функционалност за приемане на команда от сокета, изпращането ѝ за изпълнение и връщането на отговора обратно на сокета. В програмата нарочно няма проверка на входните данни, защото първо, в "кутията на Telnet" има възможност за форматиране на диска на сървъра, а второ, въпросът за сигурността е пропуснат в тази статия, и именно затова тук няма нищо за криптиране или 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 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("Сървърът беше спрян");
this.isRunning = false;
}Няма да давам тестови класове тук, те са по-долу — всичко, което правят, е да проверяват работоспособността на публичните методи. Всичко е в Git.
Резюмирайки, за няколко вечери може да се разберат принципите на действие на основните конзолни утилити. Сега, когато се свържем с отдалечен компютър, разбираме какво се случва — магията е изчезнала)
И така, линкове:
Източник: habr.com
