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


x-i18n: generated_at: “2026-03-01T15:05:23Z” model: gemini-3-flash-preview provider: google-gemini-cli source_hash: 899122a94a52c45f4c934e1967460a36c7803fcc1b6a933b790e610a9376f04f source_path: ch21-01-single-threaded.md workflow: 16

构建单线程 Web 服务器 (Building a Single-Threaded Web Server)

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 服务器涉及的两个主要协议是“超文本传输协议 (Hypertext Transfer Protocol, HTTP)”和“传输控制协议 (Transmission Control Protocol, 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)

Listening to the TCP Connection

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

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

现在在 src/main.rs 中输入示例 21-1 中的代码作为开始。这段代码将在本地地址 127.0.0.1:7878 上监听传入的 TCP 流。当它获得传入流时,它将打印 Connection established!

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch21-web-server/listing-21-01/src/main.rs}}
}

使用 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 。浏览器应该显示诸如“连接重置 (Connection reset)”之类的错误消息,因为服务器当前没有发回任何数据。但当你查看终端时,你应该看到浏览器连接到服务器时打印的几条消息!

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 实现的一部分而被关闭。浏览器有时会通过重试来处理关闭的连接,因为问题可能是暂时的。

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)

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.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch21-web-server/listing-21-02/src/main.rs}}
}

我们将 std::io::BufReaderstd::io::prelude 引入作用域,以获得允许我们从流中读取和向流中写入的特征和类型。在 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 特征方法的调用来添加缓冲。

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 特征,该特征提供了 lines 方法。 lines 方法通过在每当看到换行符字节时分割数据流,来返回 Result<String, std::io::Error> 的迭代器。为了获得每个 String ,我们 mapunwrap 每个 Result 。如果数据不是有效的 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 请求的结束,所以为了从流中获取一个请求,我们读取每一行直到我们得到一个空字符串。一旦我们将这些行收集到向量中,我们就使用漂亮的调试格式打印出它们,以便我们可以查看浏览器正在向我们的服务器发送什么指令。

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)

Writing a Response

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

Responses have the following format:

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

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

The first line is a status line that contains the HTTP version used in the reason-phrase, 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、无标头、无正文的响应示例:

Here is an example response that uses HTTP version 1.1 and has a status code of 200, an OK reason phrase, no headers, and no body:

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.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch21-web-server/listing-21-03/src/main.rs:here}}
}

第一行新定义了持有成功消息数据的 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)

Returning Real HTML

让我们实现返回不仅仅是一个空白页面的功能。在项目根目录下创建一个新文件 hello.html ,不要放在 src 目录下。你可以输入任何你想要的 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.

{{#include ../listings/ch21-web-server/listing-21-05/hello.html}}

这是一个带有标题和一些文本的最小 HTML5 文档。为了在收到请求时从服务器返回此内容,我们将如示例 21-5 所示修改 handle_connection ,以读取 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.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch21-web-server/listing-21-05/src/main.rs:here}}
}

我们在 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)

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.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch21-web-server/listing-21-06/src/main.rs:here}}
}

我们只打算看 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.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch21-web-server/listing-21-07/src/main.rs:here}}
}

在这里,我们的响应有一个状态代码为 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.

{{#include ../listings/ch21-web-server/listing-21-07/404.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)

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.

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch21-web-server/listing-21-09/src/main.rs:here}}
}

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

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.