Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Construindo um servidor web single-threaded

Começaremos fazendo um servidor web single-threaded funcionar. Antes de começarmos, vamos ver rapidamente os protocolos envolvidos na construção de servidores web. Os detalhes desses protocolos estão além do escopo deste livro, mas uma breve visão geral fornecerá as informações de que você precisa.

Os dois principais protocolos envolvidos em servidores web são Hypertext Transfer Protocol (HTTP) e Transmission Control Protocol (TCP). Ambos os protocolos são protocolos de requisição e resposta, o que significa que um cliente inicia requisições e um server escuta essas solicitações e fornece uma resposta ao cliente. O conteúdo dessas solicitações e respostas é definido pelos protocolos.

TCP é o protocolo de nível inferior que descreve os detalhes de como as informações vão de uma máquina para outra, mas não especifica quais são essas informações. O HTTP se baseia no TCP, definindo o conteúdo das solicitações e respostas. É tecnicamente possível usar HTTP com outros protocolos, mas, na grande maioria dos casos, ele envia seus dados por TCP. Trabalharemos com os bytes brutos de solicitações e respostas TCP e HTTP.

Escutando a conexão TCP

Nosso servidor web precisa escutar uma conexão TCP, então essa é a primeira parte com a qual vamos trabalhar. A biblioteca padrão oferece um módulo std::net que nos permite fazer isso. Vamos fazer um novo projeto da maneira usual:

$ cargo new hello
     Created binary (application) `hello` project
$ cd hello

Agora insira o código da Listagem 21-1 em src/main.rs para começar. Esse código irá escutar no endereço local 127.0.0.1:7878 por streams TCP de entrada. Quando receber um stream, ele imprimirá Connection established!.

Filename: src/main.rs
use std::net::TcpListener;

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

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

        println!("Connection established!");
    }
}
Listing 21-1: Escutando streams de entrada e imprimindo uma mensagem quando recebemos um stream

Usando TcpListener, podemos escutar conexões TCP no endereço 127.0.0.1:7878. Nesse endereço, a parte antes dos dois pontos é um endereço IP que representa o seu computador (ele é o mesmo em todos os computadores e não representa especificamente o computador dos autores), e 7878 é a porta. Nós escolhemos essa porta por dois motivos: HTTP normalmente não usa essa porta, então é improvável que nosso servidor entre em conflito com algum outro servidor web que você tenha rodando na máquina; além disso, 7878 corresponde a rust digitado em um telefone.

A função bind, nesse cenário, funciona como a função new, pois retorna uma nova instância de TcpListener. A função se chama bind porque, em redes, conectar-se a uma porta para escutá-la é conhecido como “fazer bind em uma porta”.

A função bind retorna um Result<T, E>, o que indica que o bind pode falhar, por exemplo, se executarmos duas instâncias do nosso programa e acabarmos com dois programas escutando a mesma porta. Como estamos escrevendo um servidor básico apenas para fins de aprendizado, não vamos nos preocupar em tratar esse tipo de erro; em vez disso, usamos unwrap para interromper o programa se algum erro acontecer.

O método incoming em TcpListener retorna um iterator que nos fornece uma sequência de streams (mais especificamente, streams do tipo TcpStream). Um único stream representa uma conexão aberta entre o cliente e o servidor. Conexão é o nome do processo completo de solicitação e resposta no qual um cliente se conecta ao servidor, o servidor gera uma resposta e o servidor fecha a conexão. Assim, leremos o TcpStream para ver o que o cliente enviou e depois escreveremos nossa resposta no stream para enviar os dados de volta ao cliente. No geral, esse loop for processará cada conexão por vez e produzirá uma série de streams para tratarmos.

Por enquanto, nosso tratamento do stream consiste em chamar unwrap para encerrar nosso programa se o stream apresentar algum erro; se não houver erros, o programa imprime uma mensagem. Adicionaremos mais funcionalidades para o caso de sucesso na próxima listagem. O motivo pelo qual podemos receber erros do método incoming quando um cliente se conecta ao servidor é que não estamos realmente iterando conexões. Em vez disso, estamos iterando sobre tentativas de conexão. A conexão pode não ser bem-sucedida por vários motivos, muitos deles específicos do sistema operacional. Por exemplo, muitos sistemas operacionais têm um limite para o número de conexões abertas simultâneas que podem suportar; novas tentativas de conexão acima desse limite produzirão erro até que algumas das conexões abertas sejam fechadas.

Vamos tentar executar esse código! Invoque cargo run no terminal e carregue 127.0.0.1:7878 em um navegador. O navegador deve mostrar uma mensagem de erro como “Redefinição de conexão” porque o servidor não está enviando de volta nenhum dados. Mas, ao olhar para o terminal, você verá várias mensagens impressas quando o navegador se conectou ao servidor.

     Running `target/debug/hello`
Connection established!
Connection established!
Connection established!

Às vezes, você verá várias mensagens impressas para uma única solicitação do navegador; o motivo pode ser que o navegador esteja fazendo uma solicitação para a página, bem como uma solicitação de outros recursos, como o ícone favicon.ico que aparece na aba do navegador.

Também pode ser que o navegador esteja tentando se conectar ao servidor várias vezes porque o servidor não está respondendo com nenhum dado. Quando stream sai do escopo e é descartado no final do loop, a conexão é fechada como parte da implementação de drop. Os navegadores às vezes lidam com conexões fechadas tentando novamente, pois o problema pode ser temporário.

Às vezes, os navegadores também abrem múltiplas conexões com o servidor sem enviar quaisquer solicitações para que, se eles fizerem solicitações depois, essas solicitações possam acontecer mais rapidamente. Quando isso ocorrer, nosso servidor verá cada conexão, independentemente de haver alguma solicitação nela. Muitas versões de navegadores baseados no Chrome fazem isso, por exemplo; você pode desabilitar essa otimização usando o modo de navegação privada ou um navegador diferente.

O importante é que conseguimos lidar com uma conexão TCP.

Lembre-se de parar o programa pressionando ctrl-C quando você terminar de executar uma versão específica do código. Em seguida, reinicie o programa invocando o comando cargo run depois de cada conjunto de alterações no código para ter certeza de que você está executando o código mais recente.

Lendo a solicitação

Vamos implementar a funcionalidade de leitura da solicitação do navegador. Para separar as responsabilidades de primeiro obter uma conexão e depois tomar alguma ação com ela, criaremos uma nova função para processar conexões. Nessa nova função handle_connection, leremos os dados do stream TCP e os imprimiremos para que possamos ver o que está sendo enviado pelo navegador. Altere o código para se parecer com a Listagem 21-2.

Filename: src/main.rs
use std::{
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

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

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

        handle_connection(stream);
    }
}

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

    println!("Request: {http_request:#?}");
}
Listing 21-2: Lendo do TcpStream e imprimindo os dados

Colocamos std::io::BufReader e std::io::prelude no escopo para obter acesso a traits e tipos que nos permitem ler e escrever no stream. No loop for da função main, em vez de imprimir uma mensagem dizendo que recebemos uma conexão, agora chamamos a nova função handle_connection e passamos o stream para ela.

Na função handle_connection, criamos uma nova instância de BufReader que envolve uma referência a stream. O BufReader adiciona buffering ao gerenciar as chamadas aos métodos da trait std::io::Read para nós.

Criamos uma variável chamada http_request para coletar as linhas da solicitação que o navegador envia ao nosso servidor. Indicamos que queremos coletar essas linhas em um vetor adicionando a anotação de tipo Vec<_>.

BufReader implementa a trait std::io::BufRead, que fornece o método lines. O método lines retorna um iterator de Result<String, std::io::Error>, dividindo o stream de dados sempre que encontra um byte de nova linha. Para obter cada String, aplicamos map e unwrap a cada Result. O Result pode conter erro se os dados não forem UTF-8 válidos ou se houver algum problema de leitura do stream. Novamente, um programa de produção deveria tratar esses erros de forma mais elegante, mas estamos optando por interromper o programa em caso de erro por simplicidade.

O navegador sinaliza o fim de uma solicitação HTTP enviando dois caracteres de nova linha em sequência; portanto, para obter uma solicitação do stream, pegamos linhas até encontrarmos uma linha que seja a string vazia. Depois de coletarmos as linhas no vetor, nós as imprimimos usando uma formatação de debug mais legível para que possamos examinar as instruções que o navegador está enviando ao nosso servidor.

Vamos testar esse código. Inicie o programa e faça uma solicitação em um navegador novamente. Observe que ainda receberemos uma página de erro no navegador, mas a saída do programa no terminal agora será semelhante a esta:

$ cargo run
   Compiling hello v0.1.0 (file:///projects/hello)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/hello`
Request: [
    "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: navigate",
    "Sec-Fetch-Site: none",
    "Sec-Fetch-User: ?1",
    "Cache-Control: max-age=0",
]

Dependendo do seu navegador, você poderá obter resultados ligeiramente diferentes. Agora que estamos imprimindo os dados da solicitação, podemos ver por que obtemos várias conexões de uma solicitação do navegador observando o caminho após GET na primeira linha da solicitação. Se todas as conexões repetidas solicitarem /, sabemos que o navegador está tentando buscar / repetidamente porque não está obtendo resposta do nosso programa.

Vamos analisar esses dados da solicitação para entender o que o navegador está pedindo ao nosso programa.

Analisando mais de perto uma requisição HTTP

HTTP é um protocolo baseado em texto e uma solicitação assume este formato:

Method Request-URI HTTP-Version CRLF
headers CRLF
message-body

A primeira linha é a linha de solicitação que contém informações sobre o que o cliente está solicitando. A primeira parte da linha de solicitação indica o método em uso, como GET ou POST, que descreve como o cliente está fazendo essa solicitação. Nosso cliente usou uma requisição GET, o que significa que está solicitando informação.

A próxima parte da linha de solicitação é /, que indica o uniform resource identifier (URI) solicitado pelo cliente. Um URI é quase, mas não exatamente, o mesmo que um uniform resource locator (URL). A diferença entre URIs e URLs não é importante para nossos propósitos neste capítulo, mas a especificação HTTP usa o termo URI, então podemos substituir mentalmente URL por URI aqui.

A última parte é a versão HTTP usada pelo cliente e, em seguida, a linha de solicitação termina em uma sequência CRLF. (CRLF significa carriage return e line feed, termos da época das máquinas de escrever.) A sequência CRLF também pode ser escrita como \r\n, em que \r é um carriage return e \n é um line feed. A sequência CRLF separa a linha de solicitação do restante dos dados da requisição. Observe que, quando o CRLF é impresso, vemos o início de uma nova linha em vez de \r\n.

Observando os dados da linha de solicitação que recebemos ao executar nosso programa até agora, vemos que GET é o método, / é o URI da solicitação e HTTP/1.1 é a versão.

Após a linha de solicitação, as linhas restantes a partir de Host: são cabeçalhos. As solicitações GET não possuem corpo.

Tente fazer uma solicitação usando um navegador diferente ou pedindo um endereço diferente, como 127.0.0.1:7878/test, para ver como os dados da solicitação mudam.

Agora que sabemos o que o navegador está pedindo, vamos enviar alguns dados!

Escrevendo uma resposta

Vamos implementar o envio de dados em resposta a uma requisição do cliente. As respostas têm o seguinte formato:

HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body

A primeira linha é uma linha de status que contém a versão HTTP usada na resposta, um código de status numérico que resume o resultado da solicitação e uma reason phrase que fornece uma descrição textual do código de status. Depois da sequência CRLF vêm os cabeçalhos, outra sequência CRLF e o corpo da resposta.

Aqui está um exemplo de resposta que usa HTTP versão 1.1 e tem código de status 200, a reason phrase OK, sem cabeçalhos e sem corpo:

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

O código de status 200 é a resposta padrão de sucesso. Esse texto forma uma pequena resposta HTTP bem-sucedida. Vamos escrevê-la no stream como resposta a uma solicitação bem-sucedida. Na função handle_connection, remova o println! que estava imprimindo os dados da solicitação e substitua-o pelo código da Listagem 21-3.

Filename: src/main.rs
use std::{
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

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

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

        handle_connection(stream);
    }
}

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

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

    stream.write_all(response.as_bytes()).unwrap();
}
Listing 21-3: Escrevendo uma pequena resposta HTTP de sucesso no stream

A primeira linha nova define a variável response, que contém os dados da mensagem de sucesso. Em seguida, chamamos as_bytes em response para converter a string em bytes. O método write_all em stream recebe um &[u8] e envia esses bytes diretamente pela conexão. Como a operação write_all pode falhar, usamos unwrap em qualquer erro, como antes. Novamente, em uma aplicação real, você adicionaria tratamento de erros aqui.

Com essas alterações, vamos executar nosso código e fazer uma solicitação. Já não estamos imprimindo dados no terminal, então não veremos nenhuma saída além da saída do Cargo. Ao carregar 127.0.0.1:7878 em um navegador, você deve obter uma página em branco em vez de um erro. Você acabou de implementar manualmente o recebimento de uma solicitação HTTP e o envio de uma resposta.

Retornando HTML de verdade

Vamos implementar a funcionalidade de retornar mais do que uma página em branco. Crie o novo arquivo hello.html na raiz do diretório do seu projeto, não no diretório src. Você pode colocar qualquer HTML que desejar; a Listagem 21-4 mostra uma possibilidade.

Filename: hello.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello!</title>
  </head>
  <body>
    <h1>Hello!</h1>
    <p>Hi from Rust</p>
  </body>
</html>
Listing 21-4: Um arquivo HTML de exemplo para retornar em uma resposta

Esse é um documento HTML5 mínimo com um título e algum texto. Para devolvê-lo do servidor quando uma solicitação for recebida, modificaremos handle_connection, como mostrado na Listagem 21-5, para ler o arquivo HTML, adicioná-lo à resposta como corpo e enviá-lo.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};
// --snip--

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

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

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&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();

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

    stream.write_all(response.as_bytes()).unwrap();
}
Listing 21-5: Enviando o conteúdo de hello.html como corpo da resposta

Adicionamos fs à instrução use para trazer para o escopo o módulo de sistema de arquivos da biblioteca padrão. O código para ler o conteúdo de um arquivo para uma string deve parecer familiar; nós o usamos quando lemos o conteúdo de um arquivo para nosso projeto de E/S na Listagem 12-4.

Em seguida, usamos format! para adicionar o conteúdo do arquivo como corpo da resposta de sucesso. Para garantir uma resposta HTTP válida, adicionamos o cabeçalho Content-Length, que é definido com o tamanho do nosso corpo de resposta; neste caso, o tamanho de hello.html.

Execute esse código com cargo run e carregue 127.0.0.1:7878 no navegador; você deve ver seu HTML renderizado.

Atualmente, estamos ignorando os dados da solicitação em http_request e apenas enviando de volta o conteúdo do arquivo HTML incondicionalmente. Isso significa que, se você tentar solicitar 127.0.0.1:7878/something-else no navegador, ainda receberá essa mesma resposta HTML. No momento, nosso servidor é muito limitado e não faz o que a maioria dos servidores web faz. Queremos personalizar nossas respostas dependendo da solicitação e enviar o arquivo HTML apenas para uma solicitação bem-formada para /.

Validando a solicitação e respondendo seletivamente

Neste momento, nosso servidor web retornará o HTML do arquivo, não importa o que o cliente tenha solicitado. Vamos adicionar funcionalidade para verificar se o navegador está solicitando / antes de retornar o arquivo HTML e retornar um erro se o navegador solicitar qualquer outra coisa. Para isso, precisamos modificar handle_connection, conforme mostrado na Listagem 21-6. Este novo código verifica o conteúdo da solicitação recebida em relação ao que sabemos ser uma solicitação para / e adiciona blocos if e else para tratar requisições de maneira diferente.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

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

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

        handle_connection(stream);
    }
}
// --snip--

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

    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\nContent-Length: {length}\r\n\r\n{contents}"
        );

        stream.write_all(response.as_bytes()).unwrap();
    } else {
        // some other request
    }
}
Listing 21-6: Tratando requisições para / de forma diferente das demais requisições

Vamos olhar apenas para a primeira linha da solicitação HTTP; então, em vez de ler a solicitação inteira em um vetor, chamamos next para obter o primeiro item do iterator. O primeiro unwrap cuida do Option e interrompe o programa se o iterator não tiver itens. O segundo unwrap lida com o Result e tem o mesmo efeito que o unwrap que estava no map adicionado na Listagem 21-2.

Em seguida, verificamos se request_line é igual à linha de solicitação de uma requisição GET para o caminho /. Se for esse o caso, o bloco if retorna o conteúdo do nosso arquivo HTML.

Se request_line não for igual à requisição GET para o caminho /, isso significa que recebemos alguma outra solicitação. Daqui a pouco adicionaremos código ao bloco else para responder a todas as outras requisições.

Execute esse código agora e solicite 127.0.0.1:7878; você deve obter o HTML de hello.html. Se fizer qualquer outra solicitação, como 127.0.0.1:7878/something-else, receberá um erro de conexão como aqueles que você viu ao executar o código das Listagens 21-1 e 21-2.

Agora vamos adicionar o código da Listagem 21-7 ao bloco else para retornar uma resposta com o código de status 404, que sinaliza que o conteúdo solicitado não foi encontrado. Também retornaremos algum HTML para que uma página seja renderizada no navegador, indicando a resposta ao usuário final.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

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

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

        handle_connection(stream);
    }
}

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

    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\nContent-Length: {length}\r\n\r\n{contents}"
        );

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

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

        stream.write_all(response.as_bytes()).unwrap();
    }
}
Listing 21-7: Respondendo com código de status 404 e uma página de erro se qualquer coisa diferente de / for requisitada

Aqui, nossa resposta tem uma linha de status com o código 404 e a reason phrase NOT FOUND. O corpo da resposta será o HTML do arquivo 404.html. Você precisará criar um arquivo 404.html ao lado de hello.html para a página de erro; novamente, fique à vontade para usar qualquer HTML que desejar ou o HTML de exemplo da Listagem 21-8.

Filename: 404.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello!</title>
  </head>
  <body>
    <h1>Oops!</h1>
    <p>Sorry, I don't know what you're asking for.</p>
  </body>
</html>
Listing 21-8: Conteúdo de exemplo para a página enviada em qualquer resposta 404

Com essas alterações, execute seu servidor novamente. Solicitar 127.0.0.1:7878 deve retornar o conteúdo de hello.html, e qualquer outra solicitação, como 127.0.0.1:7878/foo, deve retornar a página de erro em 404.html.

Refatorando

No momento, os blocos if e else têm muita repetição: ambos leem arquivos e escrevem seu conteúdo no stream. As únicas diferenças são a linha de status e o nome do arquivo. Vamos tornar o código mais conciso, separando essas diferenças em ramos if e else que atribuirão os valores da linha de status e do nome do arquivo a variáveis; então poderemos usar essas variáveis incondicionalmente no código que lê o arquivo e escreve a resposta. A Listagem 21-9 mostra o código resultante após substituir os grandes blocos if e else.

Filename: src/main.rs
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

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

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

        handle_connection(stream);
    }
}
// --snip--

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

    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\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}
Listing 21-9: Refatorando os blocos if e else para conter apenas o código que difere entre os dois casos

Agora os blocos if e else retornam apenas os valores apropriados para a linha de status e o nome do arquivo em uma tupla; então usamos desestruturação para atribuir esses dois valores a status_line e filename, usando um padrão na instrução let, como discutido no Capítulo 19.

O código anteriormente duplicado agora está fora dos blocos if e else e usa as variáveis status_line e filename. Isso torna mais fácil ver a diferença entre os dois casos e significa que temos apenas um lugar para atualizar o código caso queiramos mudar a forma como a leitura do arquivo e a escrita da resposta funcionam. O comportamento do código da Listagem 21-9 é o mesmo do código da Listagem 21-7.

Incrível! Agora temos um servidor web simples em aproximadamente 40 linhas de código Rust que responde a uma solicitação com uma página de conteúdo e responde a todas as outras solicitações com uma resposta 404.

Atualmente, nosso servidor roda em uma única thread, o que significa que ele só pode atender uma requisição por vez. Vamos examinar como isso pode se tornar um problema simulando algumas requisições lentas. Depois, vamos corrigir isso para que nosso servidor possa lidar com vários pedidos ao mesmo tempo.