Witam.
Dziś chciałbym omówić proces pisania aplikacji kliencko-serwerowych, które wykonują funkcje standardowych narzędzi systemu Windows, takich jak Telnet, TFTP i tak dalej, w czystej Javie. Oczywiście nie wnoszę nic nowego — wszystkie te narzędzia działają już od lat, ale myślę, że nie wszyscy wiedzą, co dzieje się pod powierzchnią.
Właśnie o tym będzie mowa w dalszej części artykułu.
W tym artykule, aby go nie przedłużać, oprócz ogólnych informacji opiszę tylko serwer Telnet, ale na ten moment mam też materiały o innych narzędziach — będzie to w kolejnych częściach cyklu.
Przede wszystkim warto wyjaśnić, czym właściwie jest Telnet, do czego służy i jak go używać. Nie będę dosłownie cytować źródeł (jeśli trzeba — na końcu artykułu dołączę linki do materiałów w temacie), powiem tylko, że Telnet zapewnia zdalny dostęp do wiersza poleceń urządzenia. W zasadzie to jego funkcjonalność się kończy (o zwracaniu do portu serwera nie wspomniałem świadomie, o tym później). Oznacza to, że do jego realizacji musimy odebrać ciąg na kliencie, przekazać go do serwera, spróbować przekazać go do wiersza poleceń, odczytać odpowiedź z wiersza poleceń, jeśli taka istnieje, przekazać ją z powrotem do klienta i wyświetlić na ekranie, lub w przypadku wystąpienia błędu dać użytkownikowi znać, że coś poszło nie tak.
Aby zrealizować powyższe, potrzebujemy odpowiednio dwóch klas roboczych oraz klasy testowej, z której uruchomimy serwer i przez którą będzie działał klient.
Obecnie struktura aplikacji obejmuje:
- TelnetClient
- TelnetClientTester
- TelnetServer
- TelnetServerTester
Przejdźmy do każdej z nich:
TelnetClient
Wszystko, co ten klas musi umieć, to wysyłać otrzymane polecenia i pokazywać otrzymane odpowiedzi. Ponadto musi umieć łączyć się z dowolnym (o czym wspomniałem powyżej) portem zdalnego urządzenia i rozłączać się z nim.
W tym celu zaimplementowano następujące funkcje:
Funkcja, która przyjmuje jako argument adres gniazda, otwiera połączenie i uruchamia strumienie wejścia i wyjścia (zmienne strumieni są zadeklarowane powyżej, pełne źródła — na końcu artykułu).
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());
}
}
Przeciążenie tej samej funkcji, które łączy się z domyślnym portem — dla telnetu to 23
public void run(String ip)
{
run(ip, 23);
}
Funkcja odczytuje znaki z klawiatury i przesyła je do wyjściowego gniazda — co jest charakterystyczne, w trybie liniowym, a nie znakowym:
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());
}
}
Funkcja odbiera dane z gniazda i wyświetla je na ekranie
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());
}
}
Funkcja zatrzymuje odbiór i przesyłanie danych
public void stop()
{
reader.stop();
writer.stop();
}
}TelnetServer
Ta klasa powinna mieć funkcjonalność przyjmowania polecenia z gniazda, wysyłania go do wykonania i przesyłania odpowiedzi z powrotem do gniazda. W programie celowo nie ma sprawdzania danych wejściowych, ponieważ po pierwsze, w „pudełkowym telnecie” istnieje możliwość sformatowania dysku serwera, a po drugie, kwestia bezpieczeństwa w tym artykule jest pominięta, dlatego nie ma ani słowa o szyfrowaniu lub SSL.
Tutaj są tylko 2 funkcje (jedna z nich jest przeciążona), i ogólnie nie jest to najlepsza praktyka, jednak w ramach tego zadania uznałem, że warto zostawić wszystko, jak jest.
boolean isRunning = true;
public void run(int port) {
(new Thread(()->{ try {
ServerSocket ss = new ServerSocket(port); // tworzymy gniazdo serwera i przypisujemy je do podanego portu
System.out.println("Port "+port+" oczekuje na połączenia");
Socket socket = ss.accept();
System.out.println("Połączono");
System.out.println();
// Pobieramy strumienie wejściowe i wyjściowe gniazda, teraz możemy odbierać i wysyłać dane do klienta.
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 = "Coś poszło nie tak... Kod błędu: " + 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("Błąd: " + e.getMessage());
}
}).start();
System.out.println(buffer);
buffer = "";
}
}
catch(Exception x) {
System.out.println(x.getMessage());
}})).start();
}
Program otwiera port serwera, odczytuje z niego dane, aż natrafi na znak końca komendy, następnie przekazuje komendę do nowego procesu, a wyjście z procesu jest przekierowywane do gniazda. Proste jak automat kałasznikowa.
Odpowiednio, dla tej funkcji istnieje przeciążenie z domyślnym portem:
public void run()
{
run(23);
}No i odpowiednio, funkcja zatrzymująca serwer — też wszystko trywialne, przerywa wieczną pętlę, naruszając jej warunek.
public void stop()
{
System.out.println("Serwer został zatrzymany");
this.isRunning = false;
}Nie będę tu przytaczać klas testowych, są na dole — wszystko, co robią, to sprawdzają działanie publicznych metod. Wszystko znajduje się na gicie.
Podsumowując, w ciągu kilku wieczorów można zrozumieć zasady działania podstawowych narzędzi konsolowych. Teraz, gdy połączymy się z zdalnym komputerem, rozumiemy, co się dzieje — magia zniknęła)
A zatem, linki:
Źródło: habr.com
