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

构建单线程 Web 服务器

Building a Single-Threaded Web Server

我们将从让单线程 Web 服务器运行开始。在开始之前,让我们快速回顾一下构建 Web 服务器所涉及的协议。这些协议的细节超出了本书的范围,但简要概述将为你提供所需的信息。

We’ll start by getting a single-threaded web server working. Before we begin, let’s look at a quick overview of the protocols involved in building web servers. The details of these protocols are beyond the scope of this book, but a brief overview will give you the information you need.

构建 Web 服务器涉及的两个主要协议是超文本传输协议 (HTTP)传输控制协议 (TCP)。这两个协议都是请求-响应 (request-response) 协议,意味着客户端 (client) 发起请求,而服务器 (server) 监听请求并向客户端提供响应。这些请求和响应的内容由协议定义。

The two main protocols involved in web servers are Hypertext Transfer Protocol (HTTP) and Transmission Control Protocol (TCP). Both protocols are request-response protocols, meaning a client initiates requests and a server listens to the requests and provides a response to the client. The contents of those requests and responses are defined by the protocols.

TCP 是底层协议,它描述了信息如何从一台服务器传输到另一台服务器的细节,但没有指定这些信息是什么。HTTP 构建在 TCP 之上,定义了请求和响应的内容。技术上可以将 HTTP 与其他协议配合使用,但在绝大多数情况下,HTTP 通过 TCP 发送数据。我们将处理 TCP 和 HTTP 请求和响应的原始字节。

TCP is the lower-level protocol that describes the details of how information gets from one server to another but doesn’t specify what that information is. HTTP builds on top of TCP by defining the contents of the requests and responses. It’s technically possible to use HTTP with other protocols, but in the vast majority of cases, HTTP sends its data over TCP. We’ll work with the raw bytes of TCP and HTTP requests and responses.

监听 TCP 连接

Listening to the TCP Connection

我们的 Web 服务器需要监听 TCP 连接,所以这是我们要处理的第一部分。标准库提供了一个 std::net 模块,可以让我们做到这一点。让我们以通常的方式创建一个新项目:

Our web server needs to listen to a TCP connection, so that’s the first part we’ll work on. The standard library offers a std::net module that lets us do this. Let’s make a new project in the usual fashion:

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

现在在 src/main.rs 中输入示例 21-1 中的代码开始。此代码将在本地地址 127.0.0.1:7878 监听传入的 TCP 流。当它接收到传入流时,将打印 Connection established!

Now enter the code in Listing 21-1 in src/main.rs to start. This code will listen at the local address 127.0.0.1:7878 for incoming TCP streams. When it gets an incoming stream, it will print Connection established!.

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!");
    }
}

使用 TcpListener,我们可以在地址 127.0.0.1:7878 监听 TCP 连接。在地址中,冒号前的部分是代表你计算机的 IP 地址(每台计算机上都是一样的,并不代表作者的具体计算机),7878 是端口。我们选择这个端口有两个原因:HTTP 通常不在此端口上被接受,因此我们的服务器不太可能与你机器上可能运行的其他 Web 服务器发生冲突;而且 7878 是在电话键盘上打出的 rust

Using TcpListener, we can listen for TCP connections at the address 127.0.0.1:7878. In the address, the section before the colon is an IP address representing your computer (this is the same on every computer and doesn’t represent the authors’ computer specifically), and 7878 is the port. We’ve chosen this port for two reasons: HTTP isn’t normally accepted on this port, so our server is unlikely to conflict with any other web server you might have running on your machine, and 7878 is rust typed on a telephone.

这种情况下的 bind 函数的工作方式类似于 new 函数,因为它将返回一个新的 TcpListener 实例。该函数被称为 bind,是因为在网络编程中,连接到要监听的端口被称为“绑定到端口 (binding to a port)”。

The bind function in this scenario works like the new function in that it will return a new TcpListener instance. The function is called bind because, in networking, connecting to a port to listen to is known as “binding to a port.”

bind 函数返回一个 Result<T, E>,这表明绑定有可能失败,例如,如果我们运行了程序的两个实例,从而有两个程序监听同一个端口。因为我们编写基础服务器只是为了学习目的,所以我们不必担心处理这类错误;相反,如果发生错误,我们使用 unwrap 停止程序。

The bind function returns a Result<T, E>, which indicates that it’s possible for binding to fail, for example, if we ran two instances of our program and so had two programs listening to the same port. Because we’re writing a basic server just for learning purposes, we won’t worry about handling these kinds of errors; instead, we use unwrap to stop the program if errors happen.

TcpListener 上的 incoming 方法返回一个迭代器,它为我们提供一系列流(更具体地说,是 TcpStream 类型的流)。单个流 (stream) 代表客户端与服务器之间的一个打开的连接。连接 (Connection) 是完整的请求和响应过程的名称,其中客户端连接到服务器,服务器生成响应,然后服务器关闭连接。因此,我们将从 TcpStream 读取内容以查看客户端发送了什么,然后将我们的响应写入流中以将数据发送回客户端。总的来说,这个 for 循环将轮流处理每个连接,并产生一系列流供我们处理。

The incoming method on TcpListener returns an iterator that gives us a sequence of streams (more specifically, streams of type TcpStream). A single stream represents an open connection between the client and the server. Connection is the name for the full request and response process in which a client connects to the server, the server generates a response, and the server closes the connection. As such, we will read from the TcpStream to see what the client sent and then write our response to the stream to send data back to the client. Overall, this for loop will process each connection in turn and produce a series of streams for us to handle.

目前,我们对流的处理包括:如果流有任何错误,调用 unwrap 终止程序;如果没有错误,程序将打印一条消息。我们将在下一个代码清单中为成功的情况添加更多功能。当我们客户端连接到服务器时,我们可能会从 incoming 方法接收到错误,原因是我们实际上并没有迭代连接。相反,我们是在迭代连接尝试 (connection attempts)。连接可能由于多种原因而不成功,其中许多原因与操作系统相关。例如,许多操作系统对它们可以支持的并发打开连接数有限制;超过该数量的新连接尝试将产生错误,直到关闭一些打开的连接。

For now, our handling of the stream consists of calling unwrap to terminate our program if the stream has any errors; if there aren’t any errors, the program prints a message. We’ll add more functionality for the success case in the next listing. The reason we might receive errors from the incoming method when a client connects to the server is that we’re not actually iterating over connections. Instead, we’re iterating over connection attempts. The connection might not be successful for a number of reasons, many of them operating system specific. For example, many operating systems have a limit to the number of simultaneous open connections they can support; new connection attempts beyond that number will produce an error until some of the open connections are closed.

让我们试着运行这段代码!在终端中执行 cargo run,然后在 Web 浏览器中加载 127.0.0.1:7878。浏览器应该会显示类似“连接已重置”的错误消息,因为服务器当前没有发回任何数据。但是当你查看终端时,你应该会看到浏览器连接到服务器时打印的几条消息!

Let’s try running this code! Invoke cargo run in the terminal and then load 127.0.0.1:7878 in a web browser. The browser should show an error message like “Connection reset” because the server isn’t currently sending back any data. But when you look at your terminal, you should see several messages that were printed when the browser connected to the server!

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

有时你会看到为一个浏览器请求打印了多条消息;原因可能是浏览器正在请求页面,同时也请求其他资源,例如出现在浏览器标签页中的 favicon.ico 图标。

Sometimes you’ll see multiple messages printed for one browser request; the reason might be that the browser is making a request for the page as well as a request for other resources, like the favicon.ico icon that appears in the browser tab.

也可能是因为浏览器尝试多次连接到服务器,因为服务器没有响应任何数据。当 stream 超出作用域并在循环结束时被丢弃(drop)时,连接作为 drop 实现的一部分被关闭。浏览器有时会通过重试来处理关闭的连接,因为问题可能是暂时的。

It could also be that the browser is trying to connect to the server multiple times because the server isn’t responding with any data. When stream goes out of scope and is dropped at the end of the loop, the connection is closed as part of the drop implementation. Browsers sometimes deal with closed connections by retrying, because the problem might be temporary.

浏览器有时也会在不发送任何请求的情况下打开与服务器的多个连接,以便如果它们以后确实发送请求,这些请求可以更迅速地发生。当这种情况发生时,我们的服务器将看到每个连接,无论该连接上是否有任何请求。例如,许多版本的基于 Chrome 的浏览器都会这样做;你可以通过使用私密浏览模式或使用不同的浏览器来禁用该优化。

Browsers also sometimes open multiple connections to the server without sending any requests so that if they do later send requests, those requests can happen more quickly. When this occurs, our server will see each connection, regardless of whether there are any requests over that connection. Many versions of Chrome-based browsers do this, for example; you can disable that optimization by using private browsing mode or using a different browser.

重要的因素是,我们已经成功获得了一个 TCP 连接的句柄!

The important factor is that we’ve successfully gotten a handle to a TCP connection!

记得在运行完特定版本的代码后,通过按 ctrl-C 停止程序。然后,在每次进行代码更改后,通过调用 cargo run 命令重启程序,以确保你运行的是最新的代码。

Remember to stop the program by pressing ctrl-C when you’re done running a particular version of the code. Then, restart the program by invoking the cargo run command after you’ve made each set of code changes to make sure you’re running the newest code.

读取请求

Reading the Request

让我们实现从浏览器读取请求的功能!为了将首先获得连接和随后对连接采取行动的关注点分开,我们将开始一个处理连接的新函数。在这个新的 handle_connection 函数中,我们将从 TCP 流中读取数据并打印它,以便我们可以看到从浏览器发送的数据。将代码更改为如示例 21-2 所示。

Let’s implement the functionality to read the request from the browser! To separate the concerns of first getting a connection and then taking some action with the connection, we’ll start a new function for processing connections. In this new handle_connection function, we’ll read data from the TCP stream and print it so that we can see the data being sent from the browser. Change the code to look like Listing 21-2.

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:#?}");
}

我们将 std::io::BufReaderstd::io::prelude 引入作用域,以便能够访问允许我们从流中读取和向流中写入的 trait 和类型。在 main 函数的 for 循环中,我们现在不再打印一条说明建立了连接的消息,而是调用新的 handle_connection 函数并将 stream 传递给它。

We bring std::io::BufReader and std::io::prelude into scope to get access to traits and types that let us read from and write to the stream. In the for loop in the main function, instead of printing a message that says we made a connection, we now call the new handle_connection function and pass the stream to it.

handle_connection 函数中,我们创建了一个包装 stream 引用的新 BufReader 实例。BufReader 通过为我们管理对 std::io::Read trait 方法的调用来添加缓冲区。

In the handle_connection function, we create a new BufReader instance that wraps a reference to the stream. The BufReader adds buffering by managing calls to the std::io::Read trait methods for us.

我们创建一个名为 http_request 的变量,用于收集浏览器发送到我们服务器的请求行。我们通过添加 Vec<_> 类型标注来表明我们想要将这些行收集到一个向量中。

We create a variable named http_request to collect the lines of the request the browser sends to our server. We indicate that we want to collect these lines in a vector by adding the Vec<_> type annotation.

BufReader 实现了 std::io::BufRead trait,该 trait 提供了 lines 方法。lines 方法通过在每当看到换行符字节时拆分数据流,返回一个 Result<String, std::io::Error> 的迭代器。为了获得每个 String,我们对每个 Result 进行 mapunwrap。如果数据不是有效的 UTF-8 或者从流中读取时出现问题,Result 可能是个错误。同样,生产环境的程序应该更优雅地处理这些错误,但为了简单起见,我们选择在出现错误的情况下停止程序。

BufReader implements the std::io::BufRead trait, which provides the lines method. The lines method returns an iterator of Result<String, std::io::Error> by splitting the stream of data whenever it sees a newline byte. To get each String, we map and unwrap each Result. The Result might be an error if the data isn’t valid UTF-8 or if there was a problem reading from the stream. Again, a production program should handle these errors more gracefully, but we’re choosing to stop the program in the error case for simplicity.

浏览器通过连续发送两个换行符来发出 HTTP 请求结束的信号,因此为了从流中获取一个请求,我们获取多行,直到获得一个空字符串的行。一旦我们将这些行收集到向量中,我们就使用精美的调试格式打印它们,以便我们可以查看 Web 浏览器发送到我们服务器的指令。

The browser signals the end of an HTTP request by sending two newline characters in a row, so to get one request from the stream, we take lines until we get a line that is the empty string. Once we’ve collected the lines into the vector, we’re printing them out using pretty debug formatting so that we can take a look at the instructions the web browser is sending to our server.

让我们试试这段代码!启动程序并再次在 Web 浏览器中发出请求。请注意,浏览器中仍会得到一个错误页面,但终端中程序的输出现在将类似于:

Let’s try this code! Start the program and make a request in a web browser again. Note that we’ll still get an error page in the browser, but our program’s output in the terminal will now look similar to this:

$ 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",
]

根据你的浏览器,你可能会得到略有不同的输出。现在我们正在打印请求数据,我们可以通过查看请求第一行 GET 之后的路径,来了解为什么一个浏览器请求会产生多个连接。如果重复的连接都在请求 /,我们知道浏览器正在重复获取 /,因为它没有收到我们程序的响应。

Depending on your browser, you might get slightly different output. Now that we’re printing the request data, we can see why we get multiple connections from one browser request by looking at the path after GET in the first line of the request. If the repeated connections are all requesting /, we know the browser is trying to fetch / repeatedly because it’s not getting a response from our program.

让我们分解这个请求数据,以了解浏览器在向我们的程序请求什么。

Let’s break down this request data to understand what the browser is asking of our program.

仔细观察 HTTP 请求

Looking More Closely at an HTTP Request

HTTP 是一种基于文本的协议,一个请求采用如下格式:

HTTP is a text-based protocol, and a request takes this format:

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

第一行是请求行 (request line),它保存着关于客户端正在请求什么的信息。请求行的第一部分指示所使用的方法 (method),如 GETPOST,它描述了客户端如何发出此请求。我们的客户端使用了 GET 请求,这意味着它正在请求信息。

The first line is the request line that holds information about what the client is requesting. The first part of the request line indicates the method being used, such as GET or POST, which describes how the client is making this request. Our client used a GET request, which means it is asking for information.

请求行的下一部分是 /,它指示客户端正在请求的统一资源标识符 (URI):URI 与统一资源定位符 (URL) 几乎相同,但并不完全相同。在本章中,URI 和 URL 之间的区别并不重要,但 HTTP 规范使用了术语 URI,因此我们可以在脑海中用 URL 替换这里的 URI

The next part of the request line is /, which indicates the uniform resource identifier (URI) the client is requesting: A URI is almost, but not quite, the same as a uniform resource locator (URL). The difference between URIs and URLs isn’t important for our purposes in this chapter, but the HTTP spec uses the term URI, so we can just mentally substitute URL for URI here.

最后一部分是客户端使用的 HTTP 版本,然后请求行以 CRLF 序列结尾。(CRLF 代表回车 (carriage return)换行 (line feed),这些是打字机时代的术语!)CRLF 序列也可以写作 \r\n,其中 \r 是回车,\n 是换行。CRLF 序列将请求行与请求数据的其余部分分开。请注意,当打印 CRLF 时,我们看到的是开始新行而不是 \r\n

The last part is the HTTP version the client uses, and then the request line ends in a CRLF sequence. (CRLF stands for carriage return and line feed, which are terms from the typewriter days!) The CRLF sequence can also be written as \r\n, where \r is a carriage return and \n is a line feed. The CRLF sequence separates the request line from the rest of the request data. Note that when the CRLF is printed, we see a new line start rather than \r\n.

查看我们到目前为止通过运行程序收到的请求行数据,我们看到 GET 是方法,/ 是请求 URI,HTTP/1.1 是版本。

Looking at the request line data we received from running our program so far, we see that GET is the method, / is the request URI, and HTTP/1.1 is the version.

在请求行之后,从 Host: 开始的剩余行都是标头 (headers)。GET 请求没有正文 (body)。

After the request line, the remaining lines starting from Host: onward are headers. GET requests have no body.

尝试从不同的浏览器发出请求,或者请求不同的地址,例如 127.0.0.1:7878/test,看看请求数据如何变化。

Try making a request from a different browser or asking for a different address, such as 127.0.0.1:7878/test, to see how the request data changes.

现在我们知道了浏览器在请求什么,让我们发回一些数据!

Now that we know what the browser is asking for, let’s send back some data!

编写响应

Writing a Response

我们将实现发送数据作为对客户端请求的响应。响应具有以下格式:

We’re going to implement sending data in response to a client request. Responses have the following format:

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

第一行是状态行 (status line),其中包含响应中使用的 HTTP 版本、汇总请求结果的数字状态码 (status code),以及对状态码进行文本描述的原因短语 (reason phrase)。在 CRLF 序列之后是任何标头,接着是另一个 CRLF 序列,最后是响应的正文。

The first line is a status line that contains the HTTP version used in the response, a numeric status code that summarizes the result of the request, and a reason phrase that provides a text description of the status code. After the CRLF sequence are any headers, another CRLF sequence, and the body of the response.

这是一个使用 HTTP 1.1 版本、状态码为 200、原因短语为 OK、没有标头且没有正文的响应示例:

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

状态码 200 是标准的成功响应。这段文本是一个极小的成功 HTTP 响应。让我们把这个写入流中,作为我们对成功请求的响应!在 handle_connection 函数中,删除打印请求数据的 println!,并将其替换为示例 21-3 中的代码。

The status code 200 is the standard success response. The text is a tiny successful HTTP response. Let’s write this to the stream as our response to a successful request! From the handle_connection function, remove the println! that was printing the request data and replace it with the code in Listing 21-3.

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

第一行新代码定义了保存成功消息数据的 response 变量。然后,我们在 response 上调用 as_bytes 将字符串数据转换为字节。stream 上的 write_all 方法接受一个 &[u8] 并将这些字节直接发送到连接中。因为 write_all 操作可能会失败,所以我们像之前一样对任何错误结果使用 unwrap。同样,在真实的应用程序中,你会在这里添加错误处理。

The first new line defines the response variable that holds the success message’s data. Then, we call as_bytes on our response to convert the string data to bytes. The write_all method on stream takes a &[u8] and sends those bytes directly down the connection. Because the write_all operation could fail, we use unwrap on any error result as before. Again, in a real application, you would add error handling here.

有了这些更改,让我们运行代码并发出请求。由于我们不再向终端打印任何数据,因此除了来自 Cargo 的输出外,我们不会看到任何输出。当你在 Web 浏览器中加载 127.0.0.1:7878 时,你应该得到一个空白页而不是错误。你刚刚手动编码实现了接收 HTTP 请求并发送响应!

With these changes, let’s run our code and make a request. We’re no longer printing any data to the terminal, so we won’t see any output other than the output from Cargo. When you load 127.0.0.1:7878 in a web browser, you should get a blank page instead of an error. You’ve just handcoded receiving an HTTP request and sending a response!

返回真正的 HTML

Returning Real HTML

让我们实现返回不仅仅是一个空白页的功能。在项目目录的根目录下(而不是在 src 目录中)创建新文件 hello.html。你可以输入任何你想要的 HTML;示例 21-4 显示了一种可能性。

Let’s implement the functionality for returning more than a blank page. Create the new file hello.html in the root of your project directory, not in the src directory. You can input any HTML you want; Listing 21-4 shows one possibility.

<!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>

这是一个带有标题和一些文本的最小 HTML5 文档。为了在接收到请求时从服务器返回此内容,我们将修改 handle_connection(如示例 21-5 所示),以读取 HTML 文件,将其作为正文添加到响应中并发送。

This is a minimal HTML5 document with a heading and some text. To return this from the server when a request is received, we’ll modify handle_connection as shown in Listing 21-5 to read the HTML file, add it to the response as a body, and send it.

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

我们在 use 语句中添加了 fs,以便将标准库的文件系统模块引入作用域。将文件内容读取到字符串的代码应该看起来很熟悉;我们在示例 12-4 的 I/O 项目中读取文件内容时使用过它。

We’ve added fs to the use statement to bring the standard library’s filesystem module into scope. The code for reading the contents of a file to a string should look familiar; we used it when we read the contents of a file for our I/O project in Listing 12-4.

接下来,我们使用 format! 将文件内容作为成功响应的正文添加进去。为了确保 HTTP 响应有效,我们添加了 Content-Length 标头,该标头设置为我们响应正文的大小——在本例中是 hello.html 的大小。

Next, we use format! to add the file’s contents as the body of the success response. To ensure a valid HTTP response, we add the Content-Length header, which is set to the size of our response body—in this case, the size of hello.html.

使用 cargo run 运行此代码并在浏览器中加载 127.0.0.1:7878;你应该能看到你的 HTML 被渲染了!

Run this code with cargo run and load 127.0.0.1:7878 in your browser; you should see your HTML rendered!

目前,我们忽略了 http_request 中的请求数据,只是无条件地发回 HTML 文件的内容。这意味着如果你尝试在浏览器中请求 127.0.0.1:7878/something-else,你仍然会得到这个相同的 HTML 响应。目前,我们的服务器非常有限,没有做到大多数 Web 服务器所做的事情。我们希望根据请求自定义我们的响应,并仅针对格式正确的 / 请求发回 HTML 文件。

Currently, we’re ignoring the request data in http_request and just sending back the contents of the HTML file unconditionally. That means if you try requesting 127.0.0.1:7878/something-else in your browser, you’ll still get back this same HTML response. At the moment, our server is very limited and does not do what most web servers do. We want to customize our responses depending on the request and only send back the HTML file for a well-formed request to /.

验证请求并选择性地响应

Validating the Request and Selectively Responding

现在,我们的 Web 服务器无论客户端请求什么,都会返回文件中的 HTML。让我们添加功能,在返回 HTML 文件之前检查浏览器是否正在请求 /,并在浏览器请求其他任何内容时返回错误。为此,我们需要修改 handle_connection,如示例 21-6 所示。这段新代码将收到的请求内容与我们已知的 / 请求的样子进行比对,并添加 ifelse 块以不同地对待请求。

Right now, our web server will return the HTML in the file no matter what the client requested. Let’s add functionality to check that the browser is requesting / before returning the HTML file and to return an error if the browser requests anything else. For this we need to modify handle_connection, as shown in Listing 21-6. This new code checks the content of the request received against what we know a request for / looks like and adds if and else blocks to treat requests differently.

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
    }
}

我们只打算查看 HTTP 请求的第一行,所以我们不再将整个请求读入一个向量,而是调用 next 从迭代器中获取第一项。第一个 unwrap 处理 Option 并在迭代器没有项时停止程序。第二个 unwrap 处理 Result,其效果与示例 21-2 中添加的 map 中的 unwrap 相同。

We’re only going to be looking at the first line of the HTTP request, so rather than reading the entire request into a vector, we’re calling next to get the first item from the iterator. The first unwrap takes care of the Option and stops the program if the iterator has no items. The second unwrap handles the Result and has the same effect as the unwrap that was in the map added in Listing 21-2.

接下来,我们检查 request_line 是否等于指向 / 路径的 GET 请求的请求行。如果相等,if 块将返回 HTML 文件的内容。

Next, we check the request_line to see if it equals the request line of a GET request to the / path. If it does, the if block returns the contents of our HTML file.

如果 request_line 不等于指向 / 路径的 GET 请求,则意味着我们收到了一些其他请求。稍后我们将在 else 块中添加代码以响应所有其他请求。

If the request_line does not equal the GET request to the / path, it means we’ve received some other request. We’ll add code to the else block in a moment to respond to all other requests.

现在运行此代码并请求 127.0.0.1:7878;你应该得到 hello.html 中的 HTML。如果你进行任何其他请求,例如 127.0.0.1:7878/something-else,你将得到一个连接错误,就像你在运行示例 21-1 和示例 21-2 中的代码时看到的那样。

Run this code now and request 127.0.0.1:7878; you should get the HTML in hello.html. If you make any other request, such as 127.0.0.1:7878/something-else, you’ll get a connection error like those you saw when running the code in Listing 21-1 and Listing 21-2.

现在让我们将示例 21-7 中的代码添加到 else 块中,以返回一个状态码为 404 的响应,这表明未找到请求的内容。我们还将返回一些 HTML 供浏览器渲染,以向终端用户说明响应。

Now let’s add the code in Listing 21-7 to the else block to return a response with the status code 404, which signals that the content for the request was not found. We’ll also return some HTML for a page to render in the browser indicating the response to the end user.

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

在这里,我们的响应有一个状态行,状态码为 404,原因短语为 NOT FOUND。响应的正文将是 404.html 文件中的 HTML。你需要在 hello.html 旁边创建一个 404.html 文件作为错误页面;同样,你可以随意使用任何 HTML,或者使用示例 21-8 中的示例 HTML。

Here, our response has a status line with status code 404 and the reason phrase NOT FOUND. The body of the response will be the HTML in the file 404.html. You’ll need to create a 404.html file next to hello.html for the error page; again, feel free to use any HTML you want, or use the example HTML in Listing 21-8.

<!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>

有了这些更改,再次运行你的服务器。请求 127.0.0.1:7878 应返回 hello.html 的内容,而任何其他请求(如 127.0.0.1:7878/foo)应返回来自 404.html 的错误 HTML。

With these changes, run your server again. Requesting 127.0.0.1:7878 should return the contents of hello.html, and any other request, like 127.0.0.1:7878/foo, should return the error HTML from 404.html.

重构

Refactoring

目前,ifelse 块有很多重复:它们都在读取文件并将文件内容写入流。唯一的区别是状态行和文件名。让我们通过将这些差异提取到单独的 ifelse 行中来简化代码,这些行将状态行和文件名的值分配给变量;然后我们就可以无条件地在读取文件和编写响应的代码中使用这些变量。示例 21-9 展示了替换庞大的 ifelse 块后的结果代码。

At the moment, the if and else blocks have a lot of repetition: They’re both reading files and writing the contents of the files to the stream. The only differences are the status line and the filename. Let’s make the code more concise by pulling out those differences into separate if and else lines that will assign the values of the status line and the filename to variables; we can then use those variables unconditionally in the code to read the file and write the response. Listing 21-9 shows the resultant code after replacing the large if and else blocks.

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

现在 ifelse 块仅在一个元组中返回状态行和文件名的适当值;然后我们使用解构,通过 let 语句中的模式(如第 19 章所述)将这两个值分配给 status_linefilename

Now the if and else blocks only return the appropriate values for the status line and filename in a tuple; we then use destructuring to assign these two values to status_line and filename using a pattern in the let statement, as discussed in Chapter 19.

以前重复的代码现在位于 ifelse 块之外,并使用 status_linefilename 变量。这使得更容易看到两种情况之间的区别,并且这意味着如果我们想要更改读取文件和编写响应的工作方式,我们只需要在一个地方更新代码。示例 21-9 中的代码行为将与示例 21-7 中的相同。

The previously duplicated code is now outside the if and else blocks and uses the status_line and filename variables. This makes it easier to see the difference between the two cases, and it means we have only one place to update the code if we want to change how the file reading and response writing work. The behavior of the code in Listing 21-9 will be the same as that in Listing 21-7.

太棒了!我们现在拥有一个大约 40 行 Rust 代码的简单 Web 服务器,它对一个请求响应一页内容,对所有其他请求响应 404 响应。

Awesome! We now have a simple web server in approximately 40 lines of Rust code that responds to one request with a page of content and responds to all other requests with a 404 response.

目前,我们的服务器在单线程中运行,这意味着它一次只能处理一个请求。让我们通过模拟一些慢速请求来研究这可能产生的问题。然后,我们将修复它,使我们的服务器可以同时处理多个请求。

Currently, our server runs in a single thread, meaning it can only serve one request at a time. Let’s examine how that can be a problem by simulating some slow requests. Then, we’ll fix it so that our server can handle multiple requests at once.