Escribir software con la funcionalidad de las utilidades cliente-servidor de Windows, parte 01

Bienvenido.

Hoy me gustaría analizar el proceso de escritura de aplicaciones cliente-servidor que realizan las funciones de utilidades estándar de Windows, como Telnet, TFTP, etc., etc., en Java puro. Está claro que no traeré nada nuevo: todas estas utilidades han estado funcionando con éxito durante más de un año, pero creo que no todos saben lo que sucede bajo el capó.

Esto es exactamente lo que se discutirá en el corte.

En este artículo, para no alargarme, además de información general, sólo escribiré sobre el servidor Telnet, pero por el momento también hay material sobre otras utilidades; estará en más partes de la serie.

En primer lugar, debe averiguar qué es Telnet, para qué se necesita y para qué se utiliza. No citaré las fuentes textualmente (si es necesario, adjuntaré un enlace a los materiales sobre el tema al final del artículo), solo diré que Telnet proporciona acceso remoto a la línea de comando del dispositivo. En general, aquí es donde termina su funcionalidad (deliberadamente guardé silencio sobre el acceso al puerto del servidor; hablaremos de esto más adelante). Esto significa que para implementarlo, necesitamos aceptar una línea en el cliente, pasarla al servidor, intentar pasarla a la línea de comando, leer la respuesta de la línea de comando, si hay una, pasarla de regreso al cliente y mostrarlo en la pantalla o, si hay errores, informar al usuario que algo anda mal.

En consecuencia, para implementar lo anterior, necesitamos 2 clases de trabajo y alguna clase de prueba desde la cual iniciaremos el servidor y a través de la cual funcionará el cliente.
En consecuencia, actualmente la estructura de la aplicación incluye:

  • Cliente Telnet
  • Probador de cliente Telnet
  • Servidor Telnet
  • Probador de servidor Telnet

Repasemos cada uno de ellos:

Cliente Telnet

Todo lo que esta clase debería poder hacer es enviar comandos recibidos y mostrar las respuestas recibidas. Además, debe poder conectarse a un puerto arbitrario (como se mencionó anteriormente) de un dispositivo remoto y desconectarse de él.

Para lograr esto se implementaron las siguientes funciones:

Una función que toma una dirección de socket como argumento, abre una conexión e inicia flujos de entrada y salida (las variables de flujo se declaran arriba, las fuentes completas se encuentran al final del artículo).

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

Sobrecargando la misma función, conectándose al puerto predeterminado; para telnet es 23


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

La función lee caracteres del teclado y los envía al socket de salida, lo cual es típico, en modo de línea, no en modo de caracteres:


    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 función recibe datos del socket y los muestra en la pantalla.


    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 función detiene la recepción y transmisión de datos.


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

Servidor Telnet

Esta clase debe tener la funcionalidad de recibir un comando desde un socket, enviarlo para su ejecución y enviar una respuesta del comando al socket. El programa deliberadamente no verifica los datos de entrada, porque en primer lugar, incluso en "telnet en caja" es posible formatear el disco del servidor y, en segundo lugar, la cuestión de la seguridad en este artículo se omite en principio, y es por eso que no hay unas palabras sobre cifrado o SSL.

Solo hay 2 funciones (una de ellas está sobrecargada), y en general esta no es una muy buena práctica, pero para los efectos de esta tarea me pareció apropiado dejar todo como está.

 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<String, String> 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();

    }

El programa abre el puerto del servidor, lee datos hasta que encuentra un carácter de fin de comando, pasa el comando a un nuevo proceso y redirige la salida del proceso al socket. Todo es tan simple como un rifle de asalto Kalashnikov.

En consecuencia, existe una sobrecarga para esta función con un puerto predeterminado:

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

Bueno, en consecuencia, la función que detiene el servidor también es trivial, interrumpe el bucle eterno, violando su condición.

    public void stop()
    {
        System.out.println("Server was stopped");
        this.isRunning = false;
    }

No daré clases de prueba aquí, están a continuación; todo lo que hacen es verificar la funcionalidad de los métodos públicos. Todo está en git.

En resumen, en un par de tardes podrá comprender los principios de funcionamiento de las utilidades principales de la consola. Ahora, cuando conectamos Telenet a una computadora remota, entendemos lo que está sucediendo: la magia ha desaparecido)

Entonces, los enlaces:
Todas las fuentes estuvieron, están y estarán aquí.
Acerca de Telnet
Más sobre Telnet

Fuente: habr.com

Añadir un comentario