Salut.
Astăzi aș dori să discut despre procesul de scriere a aplicațiilor client-server, care îndeplinesc funcțiile standard ale utilitarurilor Windows, cum ar fi Telnet, TFTP și altele, utilizând Java pur. Este clar că nu aduc nimic nou — toate aceste utilitare funcționează cu succes de ani buni, dar cred că nu toată lumea știe ce se întâmplă sub capotă.
Exact despre asta vom vorbi mai departe.
În acest articol, pentru a nu-l lungi, pe lângă informațiile generale, voi scrie doar despre serverul Telnet, dar momentan există și materiale despre alte utilitare — acestea vor fi în părțile ulterioare ale ciclului.
În primul rând, trebuie să ne lămurim ce este Telnet, la ce folosește și cum funcționează. Nu voi cita sursele literar (dacă este necesar — la sfârșitul articolului voi atașa linkuri către materiale pe această temă), voi spune doar că Telnet asigură accesul la linia de comandă a dispozitivului de la distanță. Pe scurt, aceasta este limita funcționalității sale (voi omite deliberat referirea la portul serverului, despre care voi discuta mai târziu). Așa că, pentru a-l implementa, trebuie să primim un șir pe client, să-l trimitem pe server, să încercăm să-l trimitem în linia de comandă, să citim răspunsul din linia de comandă, dacă există, să-l trimitem înapoi pe client și să-l afișăm pe ecran, sau, în cazul în care apare o eroare, să informăm utilizatorul că ceva nu este în regulă.
Pentru a implementa cele de mai sus, avem nevoie de 2 clase funcționale și de o clasă de testare din care vom lansa serverul și prin care va funcționa clientul.
Prin urmare, în acest moment, structura aplicației include:
- TelnetClient
- TelnetClientTester
- TelnetServer
- TelnetServerTester
Să trecem în revistă fiecare dintre ele:
TelnetClient
Tot ce trebuie să facă această clasă este să trimită comenzile primite și să afișeze răspunsurile primite. De asemenea, trebuie să știe să se conecteze la un port ales (despre care am vorbit mai sus) al unui dispozitiv de la distanță și să se deconecteze de la el.
Pentru aceasta, au fost implementate următoarele funcții:
Funcția care primește ca argument adresa socket-ului, deschide conexiunea și pornește fluxurile de intrare și ieșire (variabilele fluxului sunt declarate mai sus, sursele complete — la sfârșitul articolului).
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());
}
}
Suprascrierea aceleași funcții, conectându-se la portul implicit — pentru telnet acesta este 23
public void run(String ip)
{
run(ip, 23);
}
Funcția citește caractere de la tastatură și le trimite pe socket-ul de ieșire — ceea ce este caracteristic, în modul de linie, nu de caracter:
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());
}
}
Funcția primește date de pe socket și le afișează pe ecran
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());
}
}
Funcția oprește primirea și transmiterea de date
public void stop()
{
reader.stop();
writer.stop();
}
}TelnetServer
Această clasă ar trebui să aibă funcționalitatea de a primi comenzi de pe socket, de a le trimite spre execuție și de a trimite răspunsul comenzii înapoi pe socket. În program, intenționat nu există verificare a datelor de intrare, deoarece, pe de-o parte, în „telnet-ul de cutie” există posibilitatea de a formata discul serverului, iar pe de altă parte, problema securității este omisă în acest articol, de aceea nu există niciun cuvânt despre criptare sau SSL.
Aici sunt doar 2 funcții (una dintre ele este suprascrisă), și, în general, aceasta nu este o practică foarte bună, dar în cadrul acestei sarcini mi s-a părut adecvat să las totul așa cum este.
boolean isRunning = true;
public void run(int port) {
(new Thread(() -> { try {
ServerSocket ss = new ServerSocket(port); // creăm socketul serverului și îl legăm de portul specificat
System.out.println("Port " + port + " așteaptă conexiuni");
Socket socket = ss.accept();
System.out.println("Conectat");
System.out.println();
// Obținem fluxurile de intrare și ieșire ale socket-ului, acum putem primi și trimite date clientului.
InputStream sin = socket.getInputStream();
OutputStream sout = socket.getOutputStream();
Map env = System.getenv();
String wayToTemp = env.get("TEMP") + "tmp.txt";
for (int i : ("Conectatnnr".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 = "Ceva nu a mers bine... Cod eroare: " + 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("Eroare: " + e.getMessage());
}
}).start();
System.out.println(buffer);
buffer = "";
}
}
catch(Exception x) {
System.out.println(x.getMessage());
}})).start();
}
Programul deschide un port de server, citește date de la acesta până întâlnește simbolul de sfârșit al comenzii, transmite comanda într-un nou proces, iar ieșirea din proces este direcționată către socket. Totul este la fel de simplu ca un automat Kalashnikov.
Prin urmare, pentru această funcție există o suprasarcină cu un port implicit:
public void run()
{
run(23);
}Și, prin urmare, funcția care oprește serverul este, de asemenea, trivială, aceasta întrerupe bucla infinită, încălcând condiția acesteia.
public void stop()
{
System.out.println("Server was stopped");
this.isRunning = false;
}Nu voi aduce exemple de clase de test aici, acestea sunt prezentate mai jos — tot ce fac este să verifice funcționalitatea metodelor publice. Totul este pe Git.
Rezumând, în câteva seri putem înțelege principiile de funcționare ale principalelor utilitare de consolă. Acum, când ne conectăm la un computer remote, înțelegem ce se întâmplă — magia a dispărut)
Așadar, linkurile:
Sursa: habr.com
