Construyendo un servidor web de un solo hilo

RustRustBeginner
Practicar Ahora

This tutorial is from open-source community. Access the source code

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Bienvenido a Construyendo un servidor web de un solo hilo. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, construiremos un servidor web de un solo hilo que utiliza los protocolos HTTP y TCP para manejar las solicitudes de los clientes y proporcionar respuestas.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/ControlStructuresGroup(["Control Structures"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/ControlStructuresGroup -.-> rust/for_loop("for Loop") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100452{{"Construyendo un servidor web de un solo hilo"}} rust/string_type -.-> lab-100452{{"Construyendo un servidor web de un solo hilo"}} rust/for_loop -.-> lab-100452{{"Construyendo un servidor web de un solo hilo"}} rust/function_syntax -.-> lab-100452{{"Construyendo un servidor web de un solo hilo"}} rust/expressions_statements -.-> lab-100452{{"Construyendo un servidor web de un solo hilo"}} rust/method_syntax -.-> lab-100452{{"Construyendo un servidor web de un solo hilo"}} rust/operator_overloading -.-> lab-100452{{"Construyendo un servidor web de un solo hilo"}} end

Construyendo un servidor web de un solo hilo

Comenzaremos haciendo que funcione un servidor web de un solo hilo. Antes de comenzar, echemos un vistazo rápido a una descripción general de los protocolos implicados en la construcción de servidores web. Los detalles de estos protocolos están fuera del alcance de este libro, pero una descripción general les dará la información que necesitan.

Los dos principales protocolos implicados en los servidores web son el Hypertext Transfer Protocol (HTTP) y el Transmission Control Protocol (TCP). Ambos protocolos son de tipo solicitud-respuesta, lo que significa que un cliente inicia solicitudes y un servidor escucha las solicitudes y proporciona una respuesta al cliente. El contenido de esas solicitudes y respuestas está definido por los protocolos.

TCP es el protocolo de nivel inferior que describe los detalles de cómo la información llega de un servidor a otro pero no especifica cuál es esa información. HTTP se basa en TCP definiendo el contenido de las solicitudes y respuestas. En principio, es posible utilizar HTTP con otros protocolos, pero en la gran mayoría de los casos, HTTP envía sus datos a través de TCP. Trabajaremos con los bytes crudos de las solicitudes y respuestas de TCP y HTTP.

Escuchando la conexión TCP

Nuestro servidor web necesita escuchar una conexión TCP, por lo que esa es la primera parte en la que trabajaremos. La biblioteca estándar ofrece un módulo std::net que nos permite hacer esto. Vamos a crear un nuevo proyecto de la forma habitual:

$ cargo new hello
     Creado proyecto binario (aplicación) `hello`
$ cd hello

Ahora, ingrese el código de la Lista 20-1 en src/main.rs para comenzar. Este código escuchará en la dirección local 127.0.0.1:7878 para flujos TCP entrantes. Cuando reciba un flujo entrante, imprimirá Conexión establecida!.

Nombre del archivo: src/main.rs

use std::net::TcpListener;

fn main() {
  1 let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

  2 for stream in listener.incoming() {
      3 let stream = stream.unwrap();

      4 println!("Conexión establecida!");
    }
}

Lista 20-1: Escuchando flujos entrantes y imprimiendo un mensaje cuando recibimos un flujo

Utilizando TcpListener, podemos escuchar conexiones TCP en la dirección 127.0.0.1:7878 [1]. En la dirección, la sección antes de los dos puntos es una dirección IP que representa su computadora (esto es lo mismo en cada computadora y no representa específicamente la computadora de los autores), y 7878 es el puerto. Hemos elegido este puerto por dos razones: HTTP normalmente no se acepta en este puerto, por lo que es improbable que nuestro servidor entre en conflicto con cualquier otro servidor web que pueda tener en su máquina, y 7878 es rust tecleado en un teléfono.

La función bind en este escenario funciona como la función new en el sentido de que devolverá una nueva instancia de TcpListener. La función se llama bind porque, en red, conectarse a un puerto para escuchar se conoce como "enlazarse a un puerto".

La función bind devuelve un Result<T, E>, lo que indica que es posible que el enlace falle. Por ejemplo, conectarse al puerto 80 requiere privilegios de administrador (los no administradores solo pueden escuchar en puertos superiores a 1023), por lo que si intentamos conectarnos al puerto 80 sin ser administrador, el enlace no funcionará. El enlace también no funcionaría, por ejemplo, si ejecutamos dos instancias de nuestro programa y, por lo tanto, tenemos dos programas escuchando en el mismo puerto. Debido a que estamos escribiendo un servidor básico solo con fines de aprendizaje, no nos preocuparemos por manejar este tipo de errores; en cambio, usamos unwrap para detener el programa si ocurren errores.

El método incoming en TcpListener devuelve un iterador que nos da una secuencia de flujos [2] (más específicamente, flujos del tipo TcpStream). Un solo flujo representa una conexión abierta entre el cliente y el servidor. Una conexión es el nombre para todo el proceso de solicitud y respuesta en el que un cliente se conecta al servidor, el servidor genera una respuesta y el servidor cierra la conexión. En consecuencia, leeremos desde el TcpStream para ver lo que el cliente envió y luego escribiremos nuestra respuesta en el flujo para enviar datos de regreso al cliente. En general, este for loop procesará cada conexión por turnos y generará una serie de flujos para que los manejemos.

Por ahora, nuestro manejo del flujo consiste en llamar a unwrap para terminar nuestro programa si el flujo tiene algún error [3]; si no hay errores, el programa imprime un mensaje [4]. Agregaremos más funcionalidad para el caso de éxito en la siguiente lista. La razón por la que podríamos recibir errores del método incoming cuando un cliente se conecta al servidor es que en realidad no estamos iterando sobre conexiones. En cambio, estamos iterando sobre intentos de conexión. La conexión puede no tener éxito por una serie de razones, muchas de ellas específicas del sistema operativo. Por ejemplo, muchos sistemas operativos tienen un límite al número de conexiones abiertas simultáneas que pueden admitir; nuevos intentos de conexión más allá de ese número producirán un error hasta que se cierren algunas de las conexiones abiertas.

Vamos a probar a ejecutar este código! Invocar cargo run en la terminal y luego cargar 127.0.0.1:7878 en un navegador web. El navegador debe mostrar un mensaje de error como "Conexión cerrada" porque el servidor actualmente no está enviando ningún dato de vuelta. Pero cuando mire su terminal, debería ver varios mensajes que se imprimieron cuando el navegador se conectó al servidor!

     Ejecutando `target/debug/hello`
Conexión establecida!
Conexión establecida!
Conexión establecida!

A veces verá varios mensajes impresos para una sola solicitud del navegador; la razón podría ser que el navegador está realizando una solicitud para la página así como una solicitud para otros recursos, como el icono favicon.ico que aparece en la pestaña del navegador.

También podría ser que el navegador esté intentando conectarse al servidor varias veces porque el servidor no está respondiendo con ningún dato. Cuando stream sale del ámbito y se elimina al final del bucle, la conexión se cierra como parte de la implementación de drop. Los navegadores a veces manejan las conexiones cerradas reintentando, porque el problema podría ser temporal. El factor importante es que hemos obtenido con éxito un controlador para una conexión TCP!

Recuerde detener el programa presionando ctrl-C cuando haya terminado de ejecutar una versión particular del código. Luego reinicie el programa invocando el comando cargo run después de haber realizado cada conjunto de cambios de código para asegurarse de que está ejecutando el código más reciente.

Leyendo la solicitud

¡Implementemos la funcionalidad para leer la solicitud del navegador! Para separar las preocupaciones de primero obtener una conexión y luego tomar alguna acción con la conexión, comenzaremos una nueva función para procesar conexiones. En esta nueva función handle_connection, leeremos datos del flujo TCP y los imprimiremos para que podamos ver los datos que el navegador está enviando. Cambie el código para que se vea como en la Lista 20-2.

Nombre del archivo: src/main.rs

1 use std::{
    io::{prelude::*, BufReader},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

      2 handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
  3 let buf_reader = BufReader::new(&mut stream);
  4 let http_request: Vec<_> = buf_reader
      5.lines()
      6.map(|result| result.unwrap())
      7.take_while(|line|!line.is_empty())
       .collect();

  8 println!("Solicitud: {:#?}", http_request);
}

Lista 20-2: Leyendo del TcpStream e imprimiendo los datos

Traemos std::io::prelude y std::io::BufReader al alcance para tener acceso a los tratos y tipos que nos permiten leer y escribir en el flujo [1]. En el for loop de la función main, en lugar de imprimir un mensaje que diga que hemos establecido una conexión, ahora llamamos a la nueva función handle_connection y le pasamos el stream [2].

En la función handle_connection, creamos una nueva instancia de BufReader que envuelve una referencia mutable al stream [3]. BufReader agrega un buffer administrando las llamadas a los métodos del trato std::io::Read para nosotros.

Creamos una variable llamada http_request para recopilar las líneas de la solicitud que el navegador envía a nuestro servidor. Indicamos que queremos recopilar estas líneas en un vector agregando la anotación de tipo Vec<_> [4].

BufReader implementa el trato std::io::BufRead, que proporciona el método lines [5]. El método lines devuelve un iterador de Result<String, std::io::Error> dividiendo el flujo de datos cada vez que ve un byte de nueva línea. Para obtener cada String, mapeamos y unwrap cada Result [6]. El Result podría ser un error si los datos no son UTF-8 válidos o si hubo un problema al leer del flujo. Una vez más, un programa de producción debería manejar estos errores de manera más elegante, pero estamos eligiendo detener el programa en el caso de error por simplicidad.

El navegador señala el final de una solicitud HTTP enviando dos caracteres de nueva línea seguidos, por lo que para obtener una solicitud del flujo, tomamos líneas hasta que obtenemos una línea que es la cadena vacía [7]. Una vez que hemos recopilado las líneas en el vector, las estamos imprimiendo con un formato de depuración bonito [8] para que podamos ver las instrucciones que el navegador web está enviando a nuestro servidor.

¡Probemos este código! Inicie el programa y haga una solicitud en un navegador web nuevamente. Tenga en cuenta que todavía obtendremos una página de error en el navegador, pero la salida de nuestro programa en la terminal ahora se verá similar a esto:

$ cargo run
   Compilando hello v0.1.0 (file:///projects/hello)
    Terminada compilación en modo desarrollo [no optimizada + información de depuración] en 0.42s
     Ejecutando `target/debug/hello`
Solicitud: [
    "GET / HTTP/1.1",
    "Host: 127.0.0.1:7878",
    "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0)
Gecko/20100101 Firefox/99.0",
    "Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*
;q=0.8",
    "Accept-Language: en-US,en;q=0.5",
    "Accept-Encoding: gzip, deflate, br",
    "DNT: 1",
    "Connection: keep-alive",
    "Upgrade-Insecure-Requests: 1",
    "Sec-Fetch-Dest: document",
    "Sec-Fetch-Mode: navegar",
    "Sec-Fetch-Site: ninguno",
    "Sec-Fetch-User:?1",
    "Cache-Control: max-age=0",
]

Dependiendo de su navegador, es posible que obtenga una salida ligeramente diferente. Ahora que estamos imprimiendo los datos de la solicitud, podemos ver por qué obtenemos múltiples conexiones a partir de una solicitud del navegador al ver la ruta después de GET en la primera línea de la solicitud. Si las conexiones repetidas están todas solicitando /, sabemos que el navegador está intentando obtener / repetidamente porque no está recibiendo una respuesta de nuestro programa.

Analicemos estos datos de solicitud para entender lo que el navegador está pidiendo a nuestro programa.

Un vistazo más detallado a una solicitud HTTP

HTTP es un protocolo basado en texto, y una solicitud tiene este formato:

Método URI-de-solicitud Versión-HTTP CRLF
encabezados CRLF
cuerpo-del-mensaje

La primera línea es la línea de solicitud que contiene información sobre lo que el cliente está solicitando. La primera parte de la línea de solicitud indica el método que se está utilizando, como GET o POST, que describe cómo el cliente está realizando esta solicitud. Nuestro cliente utilizó una solicitud GET, lo que significa que está pidiendo información.

La siguiente parte de la línea de solicitud es /, que indica el identificador de recurso uniforme (URI) que el cliente está solicitando: un URI es casi, pero no exactamente, lo mismo que un localizador de recurso uniforme (URL). La diferencia entre URIs y URLs no es importante para nuestros propósitos en este capítulo, pero la especificación HTTP utiliza el término URI, así que podemos simplemente sustituir mentalmente URL por URI aquí.

La última parte es la versión de HTTP que el cliente utiliza, y luego la línea de solicitud termina en una secuencia CRLF. (CRLF significa retorno de carro y salto de línea, que son términos de los días de la máquina de escribir!) La secuencia CRLF también se puede escribir como \r\n, donde \r es un retorno de carro y \n es un salto de línea. La secuencia CRLF separa la línea de solicitud del resto de los datos de la solicitud. Tenga en cuenta que cuando se imprime la CRLF, vemos que comienza una nueva línea en lugar de \r\n.

Mirando los datos de la línea de solicitud que recibimos al ejecutar nuestro programa hasta ahora, vemos que GET es el método, / es el URI de solicitud y HTTP/1.1 es la versión.

Después de la línea de solicitud, las líneas restantes que empiezan por Host: en adelante son encabezados. Las solicitudes GET no tienen cuerpo.

Intente hacer una solicitud desde un navegador diferente o pedir una dirección diferente, como 127.0.0.1:7878/test, para ver cómo cambian los datos de la solicitud.

Ahora que sabemos lo que el navegador está pidiendo, ¡vamos a enviar algunos datos de vuelta!

Escribiendo una respuesta

Vamos a implementar el envío de datos como respuesta a una solicitud del cliente. Las respuestas tienen el siguiente formato:

Versión-HTTP Código-de-estado Frase-de-motivo CRLF
encabezados CRLF
cuerpo-del-mensaje

La primera línea es una línea de estado que contiene la versión de HTTP utilizada en la respuesta, un código de estado numérico que resume el resultado de la solicitud y una frase de motivo que proporciona una descripción textual del código de estado. Después de la secuencia CRLF están cualquier encabezado, otra secuencia CRLF y el cuerpo de la respuesta.

Aquí hay un ejemplo de respuesta que utiliza la versión 1.1 de HTTP, tiene un código de estado de 200, una frase de motivo OK, ningún encabezado y ningún cuerpo:

HTTP/1.1 200 OK\r\n\r\n

El código de estado 200 es la respuesta de éxito estándar. El texto es una pequeña respuesta HTTP exitosa. Vamos a escribir esto en el flujo como respuesta a una solicitud exitosa! Desde la función handle_connection, elimine la llamada println! que estaba imprimiendo los datos de la solicitud y reemplacela con el código de la Lista 20-3.

Nombre del archivo: src/main.rs

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&mut stream);
    let http_request: Vec<_> = buf_reader
     .lines()
     .map(|result| result.unwrap())
     .take_while(|line|!line.is_empty())
     .collect();

  1 let response = "HTTP/1.1 200 OK\r\n\r\n";

  2 stream.write_all(response.3 as_bytes()).unwrap();
}

Lista 20-3: Escribiendo una pequeña respuesta HTTP exitosa en el flujo

La primera línea nueva define la variable response que contiene los datos del mensaje de éxito [1]. Luego llamamos a as_bytes en nuestra response para convertir los datos de cadena a bytes [3]. El método write_all en stream toma un &[u8] y envía esos bytes directamente a través de la conexión [2]. Debido a que la operación write_all podría fallar, usamos unwrap en cualquier resultado de error como antes. Una vez más, en una aplicación real agregaría manejo de errores aquí.

Con estos cambios, ejecutemos nuestro código y hagamos una solicitud. Ya no estamos imprimiendo ningún dato en la terminal, por lo que no veremos ninguna salida aparte de la salida de Cargo. Cuando cargue 127.0.0.1:7878 en un navegador web, debería obtener una página en blanco en lugar de un error. ¡Acabas de codificar a mano la recepción de una solicitud HTTP y el envío de una respuesta!

Devolviendo HTML real

Implementemos la funcionalidad para devolver más que una página en blanco. Cree el nuevo archivo hello.html en la raíz de su directorio de proyecto, no en el directorio src. Puede ingresar cualquier HTML que desee; la Lista 20-4 muestra una posibilidad.

Nombre del archivo: hello.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>¡Hola!</title>
  </head>
  <body>
    <h1>¡Hola!</h1>
    <p>Hola desde Rust</p>
  </body>
</html>

Lista 20-4: Un archivo HTML de muestra para devolver en una respuesta

Este es un documento HTML5 mínimo con un encabezado y algunos textos. Para devolver esto desde el servidor cuando se recibe una solicitud, modificaremos handle_connection como se muestra en la Lista 20-5 para leer el archivo HTML, agregarlo a la respuesta como cuerpo y enviarlo.

Nombre del archivo: src/main.rs

use std::{
  1 fs,
    io::{prelude::*, BufReader},
    net::{TcpListener, TcpStream},
};
--snip--

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&mut stream);
    let http_request: Vec<_> = buf_reader
       .lines()
       .map(|result| result.unwrap())
       .take_while(|line|!line.is_empty())
       .collect();

    let status_line = "HTTP/1.1 200 OK";
    let contents = fs::read_to_string("hello.html").unwrap();
    let length = contents.len();

  2 let response = format!(
        "{status_line}\r\n\
         Content-Length: {length}\r\n\r\n\
         {contents}"
    );

    stream.write_all(response.as_bytes()).unwrap();
}

Lista 20-5: Enviando el contenido de hello.html como cuerpo de la respuesta

Hemos agregado fs a la declaración use para traer el módulo de sistema de archivos de la biblioteca estándar al alcance [1]. El código para leer el contenido de un archivo a una cadena debería sonar familiar; lo usamos cuando leímos el contenido de un archivo para nuestro proyecto de E/S en la Lista 12-4.

Luego, usamos format! para agregar el contenido del archivo como cuerpo de la respuesta de éxito [2]. Para garantizar una respuesta HTTP válida, agregamos el encabezado Content-Length que se establece en el tamaño de nuestro cuerpo de respuesta, en este caso el tamaño de hello.html.

Ejecute este código con cargo run y cargue 127.0.0.1:7878 en su navegador; debería ver su HTML renderizado.

Actualmente, estamos ignorando los datos de la solicitud en http_request y solo devolviendo el contenido del archivo HTML incondicionalmente. Eso significa que si intenta solicitar 127.0.0.1:7878/something-else en su navegador, todavía recibirá esta misma respuesta HTML. En este momento, nuestro servidor es muy limitado y no hace lo que la mayoría de los servidores web hacen. Queremos personalizar nuestras respuestas dependiendo de la solicitud y solo devolver el archivo HTML para una solicitud bien formada a /.

Validando la solicitud y respondiendo selectivamente

En este momento, nuestro servidor web devolverá el HTML del archivo sin importar lo que el cliente haya solicitado. Agregemos funcionalidad para comprobar que el navegador está solicitando / antes de devolver el archivo HTML, y devolver un error si el navegador solicita algo más. Para hacer esto, necesitamos modificar handle_connection, como se muestra en la Lista 20-6. Este nuevo código comprueba el contenido de la solicitud recibida en comparación con lo que sabemos que es una solicitud a / y agrega bloques if y else para tratar las solicitudes de manera diferente.

Nombre del archivo: src/main.rs

--snip--

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&mut stream);
  1 let request_line = buf_reader
      .lines()
      .next()
      .unwrap()
      .unwrap();

  2 if request_line == "GET / HTTP/1.1" {
        let status_line = "HTTP/1.1 200 OK";
        let contents = fs::read_to_string("hello.html").unwrap();
        let length = contents.len();

        let response = format!(
            "{status_line}\r\n\
             Content-Length: {length}\r\n\r\n\
             {contents}"
        );

        stream.write_all(response.as_bytes()).unwrap();
  3 } else {
        // alguna otra solicitud
    }
}

Lista 20-6: Manejando solicitudes a / de manera diferente a otras solicitudes

Solo vamos a mirar la primera línea de la solicitud HTTP, por lo que en lugar de leer toda la solicitud en un vector, estamos llamando a next para obtener el primer elemento del iterador [1]. El primer unwrap se encarga de la Option y detiene el programa si el iterador no tiene elementos. El segundo unwrap maneja el Result y tiene el mismo efecto que el unwrap que estaba en el map agregado en la Lista 20-2.

Luego, comprobamos la request_line para ver si es igual a la línea de solicitud de una solicitud GET al camino / [2]. Si es así, el bloque if devuelve el contenido de nuestro archivo HTML.

Si la request_line no es igual a la solicitud GET al camino /, significa que hemos recibido alguna otra solicitud. Agregaremos código al bloque else [3] en un momento para responder a todas las demás solicitudes.

Ejecute este código ahora y solicite 127.0.0.1:7878; debería obtener el HTML en hello.html. Si realiza cualquier otra solicitud, como 127.0.0.1:7878/something-else, obtendrá un error de conexión como los que vio al ejecutar el código en la Lista 20-1 y la Lista 20-2.

Ahora agregemos el código de la Lista 20-7 al bloque else para devolver una respuesta con el código de estado 404, que indica que no se encontró el contenido de la solicitud. También devolveremos algunos HTML para una página que se renderice en el navegador indicando la respuesta al usuario final.

Nombre del archivo: src/main.rs

--snip--
} else {
  1 let status_line = "HTTP/1.1 404 NOT FOUND";
  2 let contents = fs::read_to_string("404.html").unwrap();
    let length = contents.len();

    let response = format!(
        "{status_line}\r\n\
         Content-Length: {length}\r\n\r\n
         {contents}"
    );

    stream.write_all(response.as_bytes()).unwrap();
}

Lista 20-7: Respondiendo con código de estado 404 y una página de error si se solicita algo diferente a /

Aquí, nuestra respuesta tiene una línea de estado con código de estado 404 y la frase de motivo NOT FOUND [1]. El cuerpo de la respuesta será el HTML en el archivo 404.html [1]. Necesitará crear un archivo 404.html junto a hello.html para la página de error; una vez más, puede usar cualquier HTML que desee, o use el HTML de ejemplo en la Lista 20-8.

Nombre del archivo: 404.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>¡Hola!</title>
  </head>
  <body>
    <h1>Uy!</h1>
    <p>Lo siento, no sé lo que estás pidiendo.</p>
  </body>
</html>

Lista 20-8: Contenido de muestra para la página que se enviará de vuelta con cualquier respuesta 404

Con estos cambios, ejecute su servidor nuevamente. Solicitar 127.0.0.1:7878 debería devolver el contenido de hello.html, y cualquier otra solicitud, como 127.0.0.1:7878/foo, debería devolver el HTML de error de 404.html.

Un toque de refactorización

En este momento, los bloques if y else tienen mucha repetición: ambos están leyendo archivos y escribiendo el contenido de los archivos en el flujo. Las únicas diferencias son la línea de estado y el nombre del archivo. Hagamos que el código sea más conciso extrayendo esas diferencias en líneas if y else separadas que asignarán los valores de la línea de estado y el nombre del archivo a variables; luego podemos usar esas variables incondicionalmente en el código para leer el archivo y escribir la respuesta. La Lista 20-9 muestra el código resultante después de reemplazar los grandes bloques if y else.

Nombre del archivo: src/main.rs

--snip--

fn handle_connection(mut stream: TcpStream) {
    --snip--

    let (status_line, filename) =
        if request_line == "GET / HTTP/1.1" {
            ("HTTP/1.1 200 OK", "hello.html")
        } else {
            ("HTTP/1.1 404 NOT FOUND", "404.html")
        };

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();

    let response = format!(
        "{status_line}\r\n\
         Content-Length: {length}\r\n\r\n\
         {contents}"
    );

    stream.write_all(response.as_bytes()).unwrap();
}

Lista 20-9: Refactorizando los bloques if y else para contener solo el código que difiere entre los dos casos

Ahora los bloques if y else solo devuelven los valores adecuados para la línea de estado y el nombre del archivo en una tupla; luego usamos la desestructuración para asignar estos dos valores a status_line y filename usando un patrón en la declaración let, como se discutió en el Capítulo 18.

El código previamente duplicado ahora está fuera de los bloques if y else y usa las variables status_line y filename. Esto hace que sea más fácil ver la diferencia entre los dos casos, y significa que solo tenemos un lugar para actualizar el código si queremos cambiar cómo funciona la lectura de archivos y la escritura de respuestas. El comportamiento del código en la Lista 20-9 será el mismo que el de la Lista 20-8.

¡Genial! Ahora tenemos un servidor web simple en aproximadamente 40 líneas de código de Rust que responde a una solicitud con una página de contenido y responde a todas las demás solicitudes con una respuesta 404.

Actualmente, nuestro servidor se ejecuta en un solo hilo, lo que significa que solo puede atender una solicitud a la vez. Vamos a examinar cómo eso puede ser un problema simulando algunas solicitudes lentas. Luego lo solucionaremos para que nuestro servidor pueda manejar múltiples solicitudes a la vez.

Resumen

¡Felicidades! Has completado el laboratorio de Construcción de un servidor web de un solo hilo. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.