Scrivere software con funzionalità di utilità client-server per Windows, parte 01

Salve.

Oggi vorrei discutere del processo di scrittura di applicazioni client-server che svolgono le funzioni di normali utility Windows, come Telnet, TFTP, eccetera, eccetera in puro Java. È chiaro che non porterò nulla di nuovo — tutte queste utility funzionano già con successo da anni, ma credo che non tutti sappiano cosa avviene sotto il loro cofano.

Di questo si parlerà nel seguito.

In questo articolo, per non allungarlo troppo, scriverò solo sul server Telnet, ma al momento ho anche materiale su altre utility — sarà nei prossimi articoli del ciclo.

Innanzitutto, è importante comprendere che cos'è Telnet, a cosa serve e come viene utilizzato. Non riporterò letteralmente le fonti (se necessario, alla fine dell'articolo inserisco il link ai materiali sull'argomento), ma dirò solo che Telnet consente l'accesso remoto alla linea di comando del dispositivo. In sostanza, questo è il suo unico scopo (ometterò per ora il discorso sul collegamento a una porta del server, di cui parleremo più avanti). Quindi, per implementarlo, dobbiamo ricevere una stringa dal client, inviarla al server, tentare di passarla alla riga di comando, leggere la risposta della riga di comando, se presente, restituirla al client e visualizzarla sullo schermo oppure, in caso di errore, far capire all'utente che qualcosa non va.

Per realizzare quanto sopra, sono necessari dunque 2 classi operative e una certa classe di test da cui avvieremo il server e attraverso cui funzionerà il client.
Attualmente, quindi, la struttura dell'applicazione comprende:

  • TelnetClient
  • TelnetClientTester
  • TelnetServer
  • TelnetServerTester

Vediamo brevemente ciascuna di esse:

TelnetClient

Tutte le funzioni che questa classe deve eseguire sono inviare comandi ricevuti e mostrare le risposte ricevute. Inoltre, deve essere in grado di connettersi a una porta remota a scelta (come accennato in precedenza) e disconnettersi da essa.

A tal fine, sono state implementate le seguenti funzioni:

Una funzione che accetta come argomento l'indirizzo del socket, apre una connessione e avvia i thread di input e output (le variabili del thread sono dichiarate sopra, il codice sorgente completo è alla fine dell'articolo).

 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());
        }
    }

Sovrascrittura della stessa funzione, che si connette alla porta predefinita — per Telnet è 23.


    public void run(String ip)
    {
        run(ip, 23);
    }

Questa funzione legge i caratteri dalla tastiera e li invia al socket di output — caratteristicamente, in modalità stringa e non in modalità carattere:


    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());
        }
    }

La funzione riceve dati da un socket e li visualizza a schermo.


    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());
        }
    }

La funzione interrompe la ricezione e la trasmissione dei dati.


    public void stop()
    {
        reader.stop();
        writer.stop();
    }
}

TelnetServer

Questa classe deve avere la funzionalità di ricevere un comando da un socket, inviarlo per l'esecuzione e restituire la risposta al comando nuovamente al socket. Nel programma non ci sono controlli dei dati in ingresso poiché, in primo luogo, nel 'telnet di base' è possibile formattare il disco del server, e in secondo luogo, la questione della sicurezza è stata omessa in questo articolo, e per questo motivo non si parla di crittografia o SSL.

Ci sono solo 2 funzioni (una delle quali è sovraccaricata), e in generale non è una pratica molto buona, ma nell'ambito di questo compito mi è sembrato opportuno lasciare tutto com'è.

 boolean isRunning = true;
    public void run(int port)    {

        (new Thread(()->{ try {
            ServerSocket ss = new ServerSocket(port); // creiamo il socket del server e lo associamo alla porta specificata
            System.out.println("Port " + port + " is waiting for connections");

            Socket socket = ss.accept();
            System.out.println("Connesso");
            System.out.println();

            // Prendiamo i flussi di input e output del socket, ora possiamo ricevere e inviare dati al client.
            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 = "Qualcosa è andato storto... Codice errore: " + 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("Errore: " + e.getMessage());
                    }
                }).start();
                System.out.println(buffer);
                buffer = "";

            }
        }
        catch(Exception x) {
            System.out.println(x.getMessage());
        }})).start();

    }

Il programma apre una porta server, legge i dati fino a quando non incontra un simbolo di fine comando, invia il comando a un nuovo processo e l'output del processo è reindirizzato a un socket. È tutto semplice come un Kalashnikov.

Di conseguenza, per questa funzione esiste un sovraccarico con la porta predefinita:

 public void run()
    {
        run(23);
    }

E quindi, la funzione che interrompe il server è del tutto banale: interrompe il ciclo infinito, violando la sua condizione.

    public void stop()
    {
        System.out.println("Il server è stato fermato");
        this.isRunning = false;
    }

Non fornirò qui classi di test, sono disponibili di seguito — tutto ciò che fanno è verificare il funzionamento dei metodi pubblici. Tutto si trova su Git.

In sintesi, in poche serate si possono comprendere i principi di funzionamento delle principali utilità da console. Ora, quando ci colleghiamo a un computer remoto tramite telnet, comprendiamo cosa sta succedendo: la magia è scomparsa.)

Ecco i link:
Tutti i sorgenti sono stati, sono e saranno sempre qui
Su Telnet
Ulteriori informazioni su Telnet

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster