Comprender la programación de redes en Rust / Sudo Null IT News

El lenguaje de programación Rust se ha vuelto bastante popular debido a su confiabilidad, seguridad y rendimiento. En este artículo no discutiremos en detalle las ventajas de este lenguaje, ya que ya se han escrito muchos artículos sobre este tema. En su lugar, veremos el desarrollo de una aplicación de red simple que funcione según el principio cliente-servidor.

Los lectores que ya estén familiarizados con Rust pueden omitir la siguiente sección y pasar directamente a la aplicación de red. Bueno, se recomienda a aquellos que no estén familiarizados con este lenguaje que instalen las herramientas necesarias y se familiaricen con las construcciones básicas de Rust.

Banco de trabajo oxidado

Como ejemplo, veamos cómo instalar las herramientas necesarias en Ubuntu. Para descargar y ejecutar el script de instalación, ejecute el siguiente comando:

curl --proto '=https' --tlsv1.3 -sSf | sh

Mientras se ejecuta el script, se le pedirá que seleccione el tipo de instalación. Seleccione el primer elemento 1) Continuar con la instalación (predeterminado).

Para asegurarse de que todo se haya instalado correctamente, ejecute el comando:

$ rustc --version

Bueno, el tradicional Hola mundo. Crea un archivo con la extensión rs.

$ nano hello.rs

Y con el siguiente contenido:

fn main() {
    println!("Hello world!");
}

A continuación, compila usando el comando rustc y ejecuta:

$ rustc test.rs

$ ./test

En este artículo, no consideraremos la sintaxis ni los comandos de Rust, ya que este material también se puede encontrar fácilmente. Así que pasemos directamente al tema principal del artículo: la programación de redes.

Herramientas de red

Rust usa bibliotecas para trabajar con componentes de red. Todas las funciones relacionadas con la red se encuentran en el espacio de nombres std::net; Las funciones de lectura y escritura de std::io también se utilizan para leer y escribir en sockets. La estructura más importante aquí es IpAddr, que es una dirección IP común, que puede ser la versión 4 o 6. SocketAddr, que es una dirección de socket común (una combinación de IP y puerto en el host), un TcpListener y un TcpStream para comunicaciones TCP, UdpSocket para UDP y mucho más.

Entonces, si queremos comenzar a escuchar el puerto 8090 en una máquina en funcionamiento, podemos hacerlo usando el siguiente comando:

    let listener = TcpListener::bind("0.0.0.0:8090").expect("Could not bind");

En la función main() creamos un nuevo TcpListener, que en Rust es un socket TCP que escucha las conexiones entrantes de los clientes. En nuestro ejemplo, codificamos la dirección local y el puerto; un valor de dirección local de 0.0.0.0 le dice al kernel que vincule este socket a todas las interfaces disponibles en este host. Como resultado, cualquier cliente que pueda conectarse a una red conectada a este host podrá comunicarse con este host en el puerto 8090.

En una aplicación real, el número de puerto debe ser un parámetro configurable tomado de la CLI, una variable de entorno o un archivo de configuración.

Si no se puede vincular a un puerto específico, la aplicación se cierra con el mensaje No se pudo vincular.

El método listener.incoming() que utilizamos devuelve un iterador para los subprocesos que se han conectado al servidor. Los revisamos y comprobamos si alguno de ellos encontró un error. En este caso, podemos imprimir el mensaje de error y pasar al siguiente cliente conectado. Tenga en cuenta que en este caso no es práctico bloquear toda la aplicación, ya que el servidor aún puede funcionar normalmente si por alguna razón algunos clientes encuentran errores.

    for stream in listener.incoming() {
        match stream {
            Err(e) => { eprintln!("failed: {}", e) }
            Ok(stream) => {
                thread::spawn(move || {
                    handle_client(stream).unwrap_or_else(|error| eprintln!("{:?}", error));
                });
            }
        }
    }

Ahora nos toca leer datos de cada uno de los clientes en un bucle sin fin. Pero ejecutar un bucle infinito en el hilo principal lo bloqueará y ningún otro cliente podrá conectarse. Este comportamiento definitivamente no es deseable para nosotros. Entonces tenemos que crear un hilo de trabajo para manejar cada conexión de cliente. La lógica para leer de cada hilo y reescribir está encapsulada en una función llamada handle_client.

fn handle_client(mut stream: TcpStream) -> Result<(), Error> {
    println!("Incoming connection from: {}", stream.peer_addr()?);
    let mut buf = (0; 512);
    loop {
        let bytes_read = stream.read(&mut buf)?;
        if bytes_read == 0 { return Ok(()) }
        stream.write(&buf(..bytes_read))?;
    }
}

Cada hilo recibe un cierre que llama a esta función. Este cierre debe ser un movimiento porque debe leer una variable (flujo) del alcance adjunto. En la función, generamos la dirección y el puerto del punto final remoto y luego definimos un búfer para almacenar temporalmente los datos. También nos aseguramos de que el búfer se restablezca a cero. Luego iniciamos un bucle infinito donde leemos todos los datos de la secuencia. El método de lectura en una secuencia devuelve la longitud de los datos que leyó. Puede devolver cero en dos casos: si ha llegado al final del flujo o si la longitud del búfer dado era cero. Sabemos con seguridad que el segundo caso es incorrecto. De esta manera rompemos el bucle (y la función) cuando el método de lectura devuelve nulo. En este caso devolvemos Ok(). Luego escribimos los mismos datos en la secuencia usando la sintaxis de segmento. ¡Tenga en cuenta que utilizamos eprintln! para mostrar errores. Esta macro convierte la cadena dada en error estándar.

Veamos el código fuente de nuestra aplicación en su totalidad.

use std::net::{TcpListener, TcpStream};
use std::thread;
use std::io::{Read, Write, Error};

fn handle_client(mut stream: TcpStream) -> Result<(), Error> {
    println!("Incoming connection from: {}", stream.peer_addr()?);
    let mut buf = (0; 512);
    loop {
        let bytes_read = stream.read(&mut buf)?;
        if bytes_read == 0 { return Ok(()) }
        stream.write(&buf(..bytes_read))?;
    }
}

fn main() {
    let listener = TcpListener::bind("0.0.0.0:8888").expect("Could not bind");
    for stream in listener.incoming() {
        match stream {
            Err(e) => { eprintln!("failed: {}", e) }
            Ok(stream) => {
                thread::spawn(move || {
                    handle_client(stream).unwrap_or_else(|error| eprintln!("{:?}", error));
                });
            }
        }
    }
}

Para compilar, ejecute el comando

$ rustc имя_файла_сервера.rs

trabajar en los errores

Es posible que notes una evidente falta de manejo de errores al leer y escribir en una secuencia. Pero en realidad este no es el caso. Usamos el operador ? para manejar errores en estas llamadas. Esta declaración convertirá el resultado a Ok si todo salió bien; de lo contrario, devuelve un error a la función que llama antes de tiempo. Dada esta configuración, el tipo de retorno de la función debe estar vacío para manejar casos de éxito o escribir io::Error para manejar casos de error. Tenga en cuenta que en tales casos sería una buena idea implementar errores personalizados y devolverlos en lugar de errores integrados. También tenga en cuenta que el operador ? Actualmente no se puede utilizar en la función principal porque la función principal no devuelve un resultado.

Para asegurarse de que nuestro servidor esté funcionando, puede contactarlo y enviar cualquier conjunto de bytes.

Escribimos al cliente

Por supuesto, puede comunicarse con los servidores utilizando NC, pero es mejor escribir un cliente completo. En el siguiente ejemplo, leemos la entrada estándar en un bucle infinito y luego enviamos estos datos al servidor.

En caso de que no podamos leer la entrada o pasarla al servidor, la aplicación sale con los mensajes apropiados.

use std::net::TcpStream;
use std::str;
use std::io::{self, BufRead, BufReader, Write};

fn main() {
    let mut stream = TcpStream::connect("127.0.0.1:8888").expect("Could not connect to server");
    loop {
        let mut input = String::new();
        let mut buffer: Vec<u8> = Vec::new();
        io::stdin().read_line(&mut input).expect("Failed to read from stdin");
        stream.write(input.as_bytes()).expect("Failed to write to server");

        let mut reader = BufReader::new(&stream);

        reader.read_until(b'\n', &mut buffer).expect("Could not read into buffer");
        print!("{}", str::from_utf8(&buffer).expect("Could not write buffer as string"));
    }
}

Compilemos también usando Rustc:

$ rustc имя_файла_клиента.rs

Conclusión

En este artículo, analizamos los puntos principales relacionados con el trabajo en Rust, así como con el desarrollo de aplicaciones de red simples. En el próximo artículo, veremos ejemplos de código más avanzados relacionados con el desarrollo de redes.

Puedes aprender más sobre lenguajes de programación. en cursos en línea bajo la guía de profesionales expertos.

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *