Üdvözlöm.
Ma a kliens-szerver alkalmazások írásának folyamatát szeretném megvizsgálni, amelyek a Windows standard segédprogramjainak, mint például a Telnet, TFTP stb. stb. funkcióit látják el, tiszta Javával. Nyilvánvaló, hogy semmi újat nem hozok be — ezek a segédprogramok már évek óta sikeresen működnek, de azt hiszem, nem mindenki ismeri, mi történik mögöttük.
Erről fog szólni a cikk alján.
Ebben a cikkben, hogy ne legyen túl hosszú, az általános információk mellett csak a Telnet szerverről írok, de jelenleg van anyag más segédprogramokról is — ezek a ciklus későbbi részeiben lesznek.
Mindenekelőtt tisztáznunk kell, mi is az a Telnet, mire való, és hogyan működik. Nem fogom szó szerint idézni a forrásokat (ha szükséges — a cikk végén csatolok hivatkozást a témához), csak annyit mondok, hogy a Telnet távoli hozzáférést biztosít az eszköz parancssorához. Nagyjából itt véget is ér a funkciója (a szerver portjának eléréséről szándékosan hallgattam, erről később). Tehát, a megvalósításához szükségünk van egy sorra a kliens oldalon, amit át kell adni a szervernek, meg kell próbálnunk végrehajtani a parancssorban, olvasni a parancssorból az esetleges választ, azt vissza kell küldeni a kliensnek, és ki kell írni a képernyőre, vagy ha hiba lép fel, jelezni a felhasználónak, hogy valami nem oké.
A fentiek implementálásához szükség van 2 működő osztályra és egy teszt osztályra, ahonnan elindítjuk a szervert, és amivel a kliens működni fog.
Tehát jelenleg az alkalmazás struktúrája a következőket tartalmazza:
- TelnetClient
- TelnetClientTester
- TelnetServer
- TelnetServerTester
Nézzük át mindegyiket:
TelnetClient
Mindannyiunknak, amit ez az osztály csinálnia kell — küldeni a kapott parancsokat és megjeleníteni a kapott válaszokat. Ezen kívül képesnek kell lennie kapcsolódni bármelyik (amit fent említettem) távoli eszköz portjához, és leválni róla.
Ehhez a következő funkciók lettek implementálva:
Egy függvény, amely socket címet vár paraméterül, megnyitja a kapcsolatot és elindítja a bemeneti és kimeneti szálakat (a szál változói fent lettek deklarálva, a teljes forráskód a cikk végén található).
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());
}
}
Ugyanennek a funkciónak a túlterhelése, amely az alapértelmezett porthoz csatlakozik - a telnet esetében ez a 23
public void run(String ip)
{
run(ip, 23);
}
A funkció a billentyűzetről olvas karaktereket, és azokat a kimeneti socketre küldi – jellemzően sor- és nem karaktermódban:
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());
}
}
A funkció az adatokat a socketből veszi és kiírja a képernyőre
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());
}
}
A funkció leállítja az adatok fogadását és küldését
public void stop()
{
reader.stop();
writer.stop();
}
}TelnetServer
Ennek az osztálynak rendelkeznie kell azzal a funkcióval, hogy parancsot fogad a socketből, végrehajtja azt, és a parancs válaszát visszaküldi a socketre. A programban szándékosan nincs bemeneti adatellenőrzés, mert egyrészt a "do-boxtelnetben" is van lehetőség a szerver lemezének formázására, másrészt a biztonsági kérdés ebben a cikkben alapvetően elkerülésre került, így itt egy szó sem esik sem titkosításról, sem SSL-ről.
Itt mindössze 2 funkció van (az egyik túlterhelt), és ez összességében nem egy túl jó gyakorlat, azonban a feladat keretein belül megfelelőnek tartottam mindent úgy hagyni, ahogy van.
boolean isRunning = true;
public void run(int port) {
(new Thread(()->{ try {
ServerSocket ss = new ServerSocket(port); // szerver socket létrehozása és a megadott porthoz való csatolása
System.out.println("Port "+port+" connections-re vár");
Socket socket = ss.accept();
System.out.println("Kapcsolódva");
System.out.println();
// Az socket bemeneti és kimeneti folyamait vesszük, most már adatokat küldhetünk és kaphatunk az ügyféltől.
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 = "Valami elromlott... Hiba kód: "+ 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("Hiba: " + e.getMessage());
}
}).start();
System.out.println(buffer);
buffer = "";
}
}
catch(Exception x) {
System.out.println(x.getMessage());
}})).start();
}
A program megnyit egy szerver portot, olvas adatokat tőle, amíg a parancs végét jelző karaktert nem találja, a parancsot egy új folyamatba küldi, és a folyamat kimenete átirányításra kerül a socket-re. Mindez olyan egyszerű, mint egy Kalasnyikov gépkarabély.
Ezért ennek a funkciónak van egy előtöltése az alapértelmezett porttal:
public void run()
{
run(23);
}És természetesen a szervert leállító funkció is nagyon egyszerű, megszakítja a végtelen ciklust, megszegve annak feltételét.
public void stop()
{
System.out.println("A szerver leállt");
this.isRunning = false;
}Nem hozok ide tesztosztályokat, azok lent találhatók — mindössze annyit tesznek, hogy ellenőrzik a nyilvános metódusok működését. Minden ott van a github-on.
Összefoglalva, néhány este alatt megérthetik a fő konzolos segédprogramok működési elveit. Most, hogy telnetelek egy távoli számítógépre, már értjük, mi történik — a varázslat eltűnt)
Tehát, itt vannak a linkek:
Forrás: habr.com
