Saludos.
Hoy me gustaría discutir el proceso de escritura de aplicaciones cliente-servidor que realizan funciones de utilidades estándar de Windows, como Telnet, TFTP, etcétera, etcétera en Java puro. Es evidente que no aportaré nada nuevo: todas estas utilidades ya han estado funcionando con éxito durante muchos años, pero creo que no todos conocen lo que sucede bajo el capó.
De esto es de lo que hablaremos a continuación.
En este artículo, para no hacerlo extenso, además de información general, solo escribiré sobre el servidor Telnet, pero en este momento hay material sobre otras utilidades; este será parte de próximos capítulos de la serie.
Primero que nada, debemos entender qué es Telnet, para qué se necesita y cómo se utiliza. No citaré fuentes al pie de la letra (si es necesario, incluiré 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 comandos del dispositivo. En términos generales, esa es la extensión de su funcionalidad (no mencioné el acceso al puerto del servidor intencionadamente, de eso hablaré más adelante). Entonces, para implementarlo, necesitamos recibir una cadena en el cliente, transmitirla al servidor, intentar enviarla a la línea de comandos, leer la respuesta de la línea de comandos, si la hay, enviarla de nuevo al cliente y mostrarla en pantalla, o, en caso de error, hacer que el usuario entienda que algo no está bien.
Para la implementación de lo anterior, se necesitan 2 clases de trabajo y alguna clase de prueba desde la que ejecutaremos el servidor y a través de la cual trabajará el cliente.
Por lo tanto, actualmente la estructura de la aplicación incluye:
- TelnetClient
- TelnetClientTester
- TelnetServer
- TelnetServerTester
Pasemos rápidamente por cada uno de ellos:
TelnetClient
Todo lo que esta clase debe hacer es enviar los comandos recibidos y mostrar las respuestas obtenidas. Además, debe ser capaz de conectarse a un puerto arbitrario (de lo que hablé anteriormente) de un dispositivo remoto y desconectarse de él.
Para ello se implementaron las siguientes funciones:
Función que toma como argumento la dirección del socket, abre la conexión y lanza los hilos de entrada y salida (las variables de los hilos se declararon arriba, el código completo está 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());
}
}
Sobrecarga de esta misma función, conectándose al puerto predeterminado — para telnet es el 23
public void run(String ip)
{
run(ip, 23);
}
La función lee caracteres del teclado y los envía al socket de salida — notablemente, en modo de línea, no en modo de carácter:
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 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();
}
}TelnetServer
Esta clase debe tener la funcionalidad de recibir comandos del socket, ejecutarlos y enviar la respuesta de vuelta al socket. El programa deliberately no incluye validaciones de entrada porque, en primer lugar, en el "telnet de caja" hay la posibilidad de formatear el disco del servidor, y en segundo lugar, el asunto de la seguridad se omite en este artículo en general, y por eso aquí no hay una palabra sobre cifrado o SSL.
Aquí hay solo 2 funciones (una de ellas está sobrecargada), y en general no es una buena práctica, sin embargo, en el contexto de esta tarea me pareció adecuado dejar todo tal como está.
boolean isRunning = true;
public void run(int port) {
(new Thread(()->{ try {
ServerSocket ss = new ServerSocket(port); // creamos el socket del servidor y lo vinculamos al puerto indicado
System.out.println("El puerto " + port + " está esperando conexiones");
Socket socket = ss.accept();
System.out.println("Conectado");
System.out.println();
// Obtenemos los flujos de entrada y salida del socket, ahora podemos enviar y recibir datos del cliente.
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 = "Algo salió mal... Código de error: " + 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 un puerto de servidor, lee datos de él hasta encontrar el carácter de finalización del comando, pasa el comando a un nuevo proceso y redirige la salida del proceso al socket. Todo tan sencillo como un Kalashnikov.
Por lo tanto, existe una sobrecarga para esta función con el puerto por defecto:
public void run()
{
run(23);
}Y, por supuesto, la función que detiene el servidor también es trivial, interrumpe el ciclo infinito alterando su condición.
public void stop()
{
System.out.println("El servidor ha sido detenido");
this.isRunning = false;
}No proporcionaré las clases de prueba aquí, están al final: lo único que hacen es verificar el funcionamiento de los métodos públicos. Todo está en Git.
Resumiendo, en un par de noches se pueden entender los principios de funcionamiento de las principales utilidades de consola. Ahora, cuando hacemos Telnet a una computadora remota, entendemos lo que está sucediendo: la magia ha desaparecido)
Así que, los enlaces:
Fuente: habr.com
