update ch21-01

pull/860/head
kazeno 3 days ago
parent 527b61c76e
commit e4720b91ec

@ -2,15 +2,15 @@
> [ch21-00-final-project-a-web-server.md](https://github.com/rust-lang/book/blob/main/src/ch21-00-final-project-a-web-server.md)
> <br>
> commit 5df6909c57b3ba55f156a4122a42b805436de90c
> commit 56ec353290429e6547109e88afea4de027b0f1a9
这是一次漫长的旅途,不过我们到达了本书的结束。在本章中,我们将一同构建另一个项目,来展示最后几章所学,同时复习更早的章节。
这是一次漫长的旅途,不过我们已经抵达了本书的结尾。在本章中,我们将一同构建另一个项目,来展示最后几章所学,同时复习更早的章节。
作为最后的项目,我们将要实现一个返回 “hello” 的 web server它在浏览器中看起来就如图例 20-1 所示:
作为最后的项目,我们将要实现一个返回 “hello” 的 web server它在浏览器中看起来就如图 21-1 所示:
![hello from rust](img/trpl20-01.png)
![hello from rust](img/trpl21-01.png)
<span class="caption">例 20-1: 我们最后将一起分享的项目</span>
<span class="caption"> 21-1: 我们最后将一起分享的项目</span>
如下是构建 web server 的计划:
@ -20,4 +20,8 @@
4. 创建一个合适的 HTTP 响应
5. 通过线程池改善 server 的吞吐量
不过在开始之前,需要提到一点细节:这里使用的方法并不是使用 Rust 构建 web server 最好的方法。[crates.io](https://crates.io/) 上有很多可用于生产环境的 crate它们提供了比我们所要编写的更为完整的 web server 和线程池实现。然而,本章的目的在于学习,而不是走捷径。因为 Rust 是一个系统编程语言,我们能够选择处理什么层次的抽象,并能够选择比其他语言可能或可用的层次更低的层次。因此我们将自己编写一个基础的 HTTP server 和线程池,以便学习将来可能用到的 crate 背后的通用理念和技术。
在开始之前,我们先提两点说明。首先,这里使用的方法并不是使用 Rust 构建 web server 的最佳方式。[crates.io](https://crates.io/) 上有很多可用于生产环境的 crate它们提供了比我们所要编写的更为完整的 web server 和线程池实现。然而,本章的目的在于学习,而不是走捷径。因为 Rust 是一个系统编程语言,我们能够选择处理什么层次的抽象,并能够选择比其他语言可能或可用的层次更低的层次。
其次,我们不会在这里使用 async 和 await。构建线程池本身已经是一个相当大的挑战无需再加入构建异步运行时的复杂度不过我们会指出 async 和 await 在本章中会遇到的一些问题上的可能应用。
因此我们将手动编写一个基础的 HTTP server 和线程池,以便学习将来可能用到的 crate 背后的通用理念和技术。

@ -2,17 +2,17 @@
> [ch21-01-single-threaded.md](https://github.com/rust-lang/book/blob/main/src/ch21-01-single-threaded.md)
> <br>
> commit 5df6909c57b3ba55f156a4122a42b805436de90c
> commit bf3d2256c82408e89f5cfdaf8ec212739c255dd3
首先让我们创建一个可运行的单线程 web server,不过在开始之前,我们将快速了解一下构建 web server 所涉及到的协议。这些协议的细节超出了本书的范畴,不过一个简单的概括会提供我们所需的信息。
首先让我们创建一个可运行的单线程 web server在开始之前,我们将快速了解一下构建 web server 所涉及到的协议。这些协议的细节超出了本书的范畴,不过一个简单的概括会提供我们所需的信息。
web server 中涉及到的两个主要协议是 **超文本传输协议***Hypertext Transfer Protocol**HTTP*)和 **传输控制协议***Transmission Control Protocol**TCP*)。这两者都是 **请求 - 响应***request-response*)协议,也就是说,有 **客户端***client*)来初始化请求,并有 **服务端***server*)监听请求并向客户端提供响应。请求与响应的内容由协议本身定义。
TCP 是一个底层协议,它描述了信息如何从一个 server 到另一个的细节不过其并不指定信息是什么。HTTP 构建于 TCP 之上,它定义了请求和响应的内容。为此,技术上讲可将 HTTP 用于其他协议之上不过对于绝大部分情况HTTP 通过 TCP 传输。我们将要做的就是处理 TCP 和 HTTP 请求与响应的原始字节数据。
TCP 是一个底层协议,它描述了信息如何从一个 server 到另一个的细节不过其并不指定信息是什么。HTTP 构建于 TCP 之上,它定义了请求和响应的内容。技术上讲可将 HTTP 用于其他协议之上不过对于绝大部分情况HTTP 通过 TCP 传输数据。我们将要做的就是处理 TCP 和 HTTP 请求与响应的原始字节数据。
### 监听 TCP 连接
我们的 web server 所需做的第一件事,是监听 TCP 连接。标准库提供了 `std::net` 模块处理这些功能。让我们一如既往新建一个项目:
我们的 web server 所需做的第一件事,是监听 TCP 连接。标准库提供了 `std::net` 模块处理这些功能。让我们像往常一样新建一个项目:
```console
$ cargo new hello
@ -20,7 +20,7 @@ $ cargo new hello
$ cd hello
```
现在,在 `src/main.rs` 输入示例 20-1 中的代码,作为一个开始。这段代码会在地址 `127.0.0.1:7878` 上监听传入的 TCP 流。当获取到传入的流,它会打印出 `Connection established!`
现在,在 `src/main.rs` 输入示例 21-1 中的代码,作为一个开始。这段代码会在地址 `127.0.0.1:7878` 上监听传入的 TCP 流。当获取到传入的流,它会打印出 `Connection established!`
<span class="filename">文件名src/main.rs</span>
@ -28,7 +28,7 @@ $ cd hello
{{#rustdoc_include ../listings/ch21-web-server/listing-21-01/src/main.rs}}
```
<span class="caption">示例 20-1: 监听传入的流并在接收到流时打印信息</span>
<span class="caption">示例 21-1: 监听传入的流并在接收到流时打印信息</span>
`TcpListener` 用于监听 TCP 连接。我们选择监听本地地址 `127.0.0.1:7878`。将这个地址拆开来看,冒号之前的部分是一个代表本机的 IP 地址(在每台计算机上,这个地址都指本机,并不特指作者的计算机),而 `7878` 是端口。选择这个端口出于两个原因:通常 HTTP 服务器不在这个端口上接受请求,所以它不太可能与你机器上运行的其它 web server 的端口冲突;而且 7878 在电话上打出来就是 "rust"(译者注:九宫格键盘上的英文)。
@ -36,9 +36,9 @@ $ cd hello
`bind` 函数返回 `Result<T, E>`,这表明绑定可能会失败。例如,监听 80 端口需要管理员权限(非管理员用户只能监听大于 1023 的端口),所以如果尝试监听 80 端口而没有管理员权限,则会绑定失败。再比如,如果我们运行这个程序的两个实例,并因此有两个实例监听同一个端口,那么绑定也将失败。我们是出于学习目的来编写一个基础的服务器,不用关心处理这类错误,而仅仅使用 `unwrap` 在出现这些情况时直接停止程序。
`TcpListener``incoming` 方法返回一个迭代器,它提供了一系列的流(更准确的说是 `TcpStream` 类型的流)。**流***stream*)代表一个客户端和服务端之间打开的连接。**连接***connection*)代表客户端连接服务端、服务端生成响应以及服务端关闭连接的全部请求 / 响应过程。为此,我们会从 `TcpStream` 读取客户端发送了什么并接着向流发送响应以向客户端发回数据。总体来说,这个 `for` 循环会依次处理每个连接并产生一系列的流供我们处理。
`TcpListener``incoming` 方法返回一个迭代器,它提供了一系列的流(更准确的说是 `TcpStream` 类型的流)。**流***stream*)代表一个客户端和服务端之间打开的连接。**连接***connection*)代表客户端连接服务端、服务端生成响应以及服务端关闭连接的整个请求 / 响应过程。为此,我们会从 `TcpStream` 读取客户端发送了什么并接着向流发送响应以向客户端发回数据。总体来说,这个 `for` 循环会依次处理每个连接并产生一系列的流供我们处理。
目前,处理流的代码中也有一个 `unwrap` 调用,如果 `stream` 出现任何错误会终止程序;如果没有任何错误,则打印出信息。下一个例子中,我们将为成功的情况增加更多功能。当客户端连接到服务端时,`incoming` 方法是可能返回错误的,因为我们实际上不是在遍历连接,而是遍历 **连接尝试***connection attempts*)。连接的尝试可能会因为多种原因不能成功,大部分是操作系统相关的。例如,很多系统限制同时打开的连接数,超出数量限制的新连接尝试会产生错误,直到一些现有的连接关闭为止。
目前,处理流的代码中也有一个 `unwrap` 调用,如果 `stream` 出现任何错误会终止程序;如果没有任何错误,程序会打印一条消息。下一个示例中,我们将为成功的情况增加更多功能。当客户端连接到服务端时,`incoming` 方法是可能返回错误的,因为我们实际上不是在遍历连接,而是遍历 **连接尝试***connection attempts*)。连接的尝试可能会因为多种原因不能成功,其中大部分是操作系统相关的。例如,很多系统限制它所能支持的同时打开的连接数,超出数量限制的新连接尝试会产生错误,直到一些现有的连接关闭为止。
让我们试试这段代码!首先在终端执行 `cargo run`,接着在浏览器中打开 `127.0.0.1:7878`。浏览器会显示出看起来类似于“连接重置”“Connection reset”的错误信息因为 server 目前并没响应任何数据。如果我们观察终端,会发现当浏览器连接我们的服务端时,会打印出一系列的信息!
@ -49,15 +49,19 @@ Connection established!
Connection established!
```
有时,对于一次浏览器请求,可能会打印出多条信息;这可能是因为,浏览器在请求页面的同时,还请求了其他资源,比如出现在浏览器标签页开头的图标(*favicon.ico*
有时,对于一次浏览器请求可能会打印出多条信息;原因可能是浏览器不仅请求页面,还请求其他资源,比如出现在浏览器标签页中的 *favicon.ico* 图标
这也可能是因为浏览器尝试多次连接服务端,因为服务端没有响应任何数据。作为 `drop` 实现的一部分,当 `stream` 在循环的结尾离开作用域并被丢弃,其连接将被关闭。浏览器有时通过重连来处理关闭的连接,因为对于一般网站而言,这些问题可能是暂时的。这些都不重要;现在重要的是,我们成功的处理了 TCP 连接!
这也可能是因为浏览器尝试多次连接服务端,因为服务端没有响应任何数据。`stream` 在循环结束时离开作用域并被丢弃,其连接将作为 `drop` 实现的一部分被关闭。浏览器有时通过重连来处理关闭的连接,因为这些问题可能是暂时的。
记得当运行完特定版本的代码后,使用 <span class="keystroke">ctrl-C</span> 来停止程序。并通过执行 `cargo run` 命令在做出最新的代码修改之后重启服务。
浏览器有时还会在不发送任何请求的情况下打开多个连接,以便在**稍后**发送请求时能够更快地开始。出现这种情况时,我们的服务端会看到每个连接,而不管该连接上是否有请求。例如,许多基于 Chrome 的浏览器版本都会这样做;你可以通过使用私人浏览模式或更换其他浏览器来禁用该优化。
重要的是,我们已经成功获取了一个 TCP 连接的句柄!
记得当运行完特定版本的代码后,使用 <kbd>ctrl</kbd>-<kbd>c</kbd> 来停止程序。然后在你完成每次代码修改后,通过运行 `cargo run` 命令重新启动程序,以确保你正在运行最新的代码。
### 读取请求
让我们实现读取来自浏览器请求的功能!为了分离“获取连接”以及“接下来对连接的操作”,我们将开始写一个新函数来处理连接。在这个新的 `handle_connection` 函数中,我们从 TCP 流中读取数据,并打印出来,以便观察浏览器发送过来的数据。将代码修改为如示例 20-2 所示:
让我们实现读取来自浏览器请求的功能!为了将首先获取连接和接着对连接采取操作两项职责分离,我们将开始写一个新函数来处理连接。在这个新的 `handle_connection` 函数中,我们从 TCP 流中读取数据,并打印出来,以便观察浏览器发送过来的数据。将代码修改为如示例 21-2 所示:
<span class="filename">文件名src/main.rs</span>
@ -65,17 +69,17 @@ Connection established!
{{#rustdoc_include ../listings/ch21-web-server/listing-21-02/src/main.rs}}
```
<span class="caption">示例 20-2: 读取 `TcpStream` 并打印数据</span>
<span class="caption">示例 21-2: 读取 `TcpStream` 并打印数据</span>
这里将 `std::io::prelude``std::io::BufReader` 引入作用域,来获取读写流所需的特定 trait。在 `main` 函数的 `for` 循环中,相比获取到连接时打印信息,现在调用新的 `handle_connection` 函数并向其传递 `stream`
这里将 `std::io::prelude``std::io::BufReader` 引入作用域,来获取读写流所需的 trait 和类型。在 `main` 函数的 `for` 循环中,相比获取到连接时打印信息,现在调用新的 `handle_connection` 函数并向其传递 `stream`
`handle_connection` 中,我们新建了一个 `BufReader` 实例来封装一个 `stream`可变引用。`BufReader` 增加了缓存来替我们管理 `std::io::Read` trait 方法的调用。
`handle_connection` 中,我们新建了一个 `BufReader` 实例来封装一个 `stream`引用。`BufReader` 通过替我们管理 `std::io::Read` trait 方法的调用增加了缓冲
我们创建了一个 `http_request` 变量来收集浏览器发送给服务端的请求行。这里增加了 `Vec<_>` 类型注解表明希望将这些行收集到一个 vector 中。
`BufReader` 实现了 `std::io::BufRead` trait它提供了 `lines` 方法。`lines` 方法通过遇到换行符newline字节就切分数据流的方式返回一个 `Result<String, std::io::Error>` 的迭代器。为了获取每一个 `String`,通过 map 并 `unwrap` 每一个 `Result`。如果数据不是有效的 UTF-8 编码或者读取流遇到问题时,`Result` 可能是一个错误。一如既往生产环境的程序应该更优雅地处理这些错误,不过出于简单的目的我们选择在错误情况下停止程序。
`BufReader` 实现了 `std::io::BufRead` trait它提供了 `lines` 方法。`lines` 方法通过遇到换行符newline字节就切分数据流返回一个 `Result<String, std::io::Error>` 的迭代器。为了获取每一个 `String`我们通过 map 并 `unwrap` 每一个 `Result`。如果数据不是有效的 UTF-8 编码或者读取流遇到问题时,`Result` 可能是一个错误。同理,用于生产环境的程序应该更优雅地处理这些错误,不过出于简单的目的我们选择在错误情况下停止程序。
浏览器通过连续发送两个换行符来代表一个 HTTP 请求的结束,所以为了从流中获取一个请求,我们获取行直到它们不为空。一旦将这些行收集进 vector就可以使用友好的 debug 格式化打印它们,以便看看 web 浏览器发送给服务端的指令。
浏览器通过连续发送两个换行符来代表一个 HTTP 请求的结束,所以为了从流中获取一个请求,我们会读取行直到遇到一个空字符串的行。一旦将这些行收集进 vector就可以使用友好的 debug 格式化打印它们,以便看看浏览器发送给服务端的指令。
让我们试一试!启动程序并再次在浏览器中发起请求。注意浏览器中仍然会出现错误页面,不过终端中程序的输出现在看起来像这样:
@ -102,7 +106,7 @@ Request: [
]
```
根据使用的浏览器不同可能会出现稍微不同的数据。现在我们打印出了请求数据,可以通过观察第一行 `GET` 之后的路径来解释为何会从浏览器得到多个连接。如果重复的连接都是请求 */*,就知道了浏览器尝试重复获取 */* 因为它没有从程序得到响应。
根据不同的浏览器,可能会出现稍微不同的输出。现在我们打印出了请求数据,可以通过观察第一行 `GET` 之后的路径就可以看出为何会从浏览器得到多个连接。如果重复的连接都是请求 */*,就知道了浏览器尝试重复获取 */* 因为它没有从程序得到响应。
让我们拆开请求数据来理解浏览器向程序请求了什么。
@ -118,21 +122,21 @@ message-body
第一行叫做 **请求行***request line*),它存放了客户端请求了什么的信息。请求行的第一部分是所使用的 *method*,比如 `GET``POST`,这描述了客户端如何进行请求。这里客户端使用了 `GET` 请求,表明它在请求信息。
请求行接下来的部分是 */*,它代表客户端请求的 **统一资源标识符***Uniform Resource Identifier**URI* —— URI 大体上类似,但也不完全类似于 URL**统一资源定位符***Uniform Resource Locators*。URI 和 URL 之间的区别对于本章的目的来说并不重要,不过 HTTP 规范使用术语 URI所以这里可以简单的将 URL 理解为 URI。
请求行接下来的部分是 */*,它代表客户端请求的 **统一资源标识符***Uniform Resource Identifier**URI*URI 大体上但也不完全类似于 URL**统一资源定位符***Uniform Resource Locators*。URI 和 URL 之间的区别对于本章的目的来说并不重要,不过 HTTP 规范使用术语 URI所以这里可以简单的将 URL 理解为 URI。
最后一部分是客户端使用的 HTTP 版本,然后请求行以 **CRLF 序列** CRLF 代表回车和换行,*carriage return line feed*这是打字机时代的术语结束。CRLF 序列也可以写成`\r\n`,其中`\r`是回车符,`\n`是换行符。CRLF 序列将请求行与其余请求数据分开。请注意,打印 CRLF 时,我们会看到一个新行,而不是`\r\n`。
最后一部分是客户端使用的 HTTP 版本,然后请求行以 **CRLF 序列** CRLF 代表回车和换行,*carriage return line feed*这是打字机时代的术语结束。CRLF 序列也可以写成`\r\n`,其中`\r`是回车符,`\n`是换行符。CRLF 序列将请求行与其余请求数据分开。请注意,打印 CRLF 时,我们会看到一个新行开始,而不是`\r\n`。
观察目前运行程序所接收到的数据的请求行,可以看到 `GET` 是 method*/* 是请求 URI`HTTP/1.1` 是版本。
观察目前为止运行程序所接收到的请求行数据,可以看到 `GET` 是 method*/* 是请求 URI`HTTP/1.1` 是版本。
`Host:` 开始的其余的行是 headers`GET` 请求没有 body。
如果你希望的话,尝试用不同的浏览器发送请求,或请求不同的地址,比如 `127.0.0.1:7878/test`,来观察请求数据如何变化。
如果你希望的话,可以尝试用不同的浏览器发送请求,或请求不同的地址,比如 `127.0.0.1:7878/test`,来观察请求数据如何变化。
现在我们知道了浏览器请求了什么。让我们返回一些数据!
### 编写响应
我们将实现在客户端请求的响应中发送数据的功能。响应有如下格式:
我们将实现在客户端请求的响应中发送数据的功能。响应有如下格式:
```text
HTTP-Version Status-Code Reason-Phrase CRLF
@ -140,7 +144,7 @@ headers CRLF
message-body
```
第一行叫做 **状态行***status line*),它包含响应的 HTTP 版本、一个数字状态码用以总结请求的结果和一个描述之前状态码的文本原因短语。CRLF 序列之后是任意 header另一个 CRLF 序列,和响应的 body。
第一行叫做 **状态行***status line*),它包含响应的 HTTP 版本、一个数字状态码status code用以总结请求的结果和一个描述之前状态码的文本原因短语reason phrase。CRLF 序列之后是任意 header另一个 CRLF 序列,和响应的 body。
这里是一个使用 HTTP 1.1 版本的响应例子,其状态码为 200原因短语为 OK没有 header也没有 body
@ -148,7 +152,7 @@ message-body
HTTP/1.1 200 OK\r\n\r\n
```
状态码 200 是一个标准的成功响应。这些文本是一个微型的成功 HTTP 响应。让我们将这些文本写入流作为成功请求的响应!在 `handle_connection` 函数中,我们需要去掉打印请求数据的 `println!`,并替换为示例 20-3 中的代码:
状态码 200 是一个标准的成功响应。这些文本是一个微型的成功 HTTP 响应。让我们将这些文本写入流作为成功请求的响应!在 `handle_connection` 函数中,我们需要去掉打印请求数据的 `println!`,并替换为示例 21-3 中的代码:
<span class="filename">文件名src/main.rs</span>
@ -156,15 +160,15 @@ HTTP/1.1 200 OK\r\n\r\n
{{#rustdoc_include ../listings/ch21-web-server/listing-21-03/src/main.rs:here}}
```
<span class="caption">示例 20-3: 将一个微型成功 HTTP 响应写入流</span>
<span class="caption">示例 21-3: 将一个微型成功 HTTP 响应写入流</span>
新代码中的第一行定义了变量 `response` 来存放将要返回的成功响应的数据。接着,在 `response` 上调用 `as_bytes`因为 `stream``write_all` 方法获取一个 `&[u8]` 并直接将这些字节发送给连接。因为 `write_all` 操作可能会失败,所以像之前那样对任何错误结果使用 `unwrap`。同理,在真实世界的应用中这里需要添加错误处理。
新代码中的第一行定义了变量 `response` 来存放成功消息的数据。接着,我们在 `response` 上调用 `as_bytes` 将字符串数据转换为字节数组。因为 `stream``write_all` 方法获取一个 `&[u8]` 并直接将这些字节发送给连接。因为 `write_all` 操作可能会失败,所以像之前那样对任何错误结果使用 `unwrap`。同理,在真实世界的应用中这里需要添加错误处理。
有了这些修改,运行我们的代码并进行请求!我们不再向终端打印任何数据,所以不会再看到除了 Cargo 以外的任何输出。不过当在浏览器中加载 *127.0.0.1:7878* 时,会得到一个空页面而不是错误。太棒了!我们刚刚手写收发了一个 HTTP 请求与响应。
有了这些修改,运行我们的代码并进行请求。由于不再向终端打印任何数据,所以不会再看到除了 Cargo 以外的任何输出。不过当在浏览器中加载 *127.0.0.1:7878* 时,会得到一个空页面而不是错误。我们刚刚手写了接收 HTTP 请求并发送响应!
### 返回真正的 HTML
让我们实现不只是返回空页面的功能。在项目根目录创建一个新文件,*hello.html* —— 也就是说,不是在 `src` 目录。在此可以放入任何你期望的 HTML列表 20-4 展示了一个可能的文本:
让我们实现不只是返回空页面的功能。在项目根目录创建一个新文件,*hello.html*,不是在 *src* 目录。在此可以放入任何你期望的 HTML 内容;示例 21-4 展示了一个可能的文本:
<span class="filename">文件名hello.html</span>
@ -172,9 +176,9 @@ HTTP/1.1 200 OK\r\n\r\n
{{#include ../listings/ch21-web-server/listing-21-05/hello.html}}
```
<span class="caption">示例 20-4: 一个简单的 HTML 文件用来作为响应</span>
<span class="caption">示例 21-4: 一个示例 HTML 文件作为响应返回</span>
这是一个极小化的 HTML5 文档,它有一个标题和一小段文本。为了在 server 接受请求时返回它,需要如示例 20-5 所示修改 `handle_connection` 来读取 HTML 文件,将其加入到响应的 body 中并发送:
这是一个极简 HTML5 文档包含一个标题和一小段文本。为了在服务端接收请求时返回它,需要如示例 21-5 所示修改 `handle_connection` 来读取 HTML 文件,将其加入到响应的 body 中并发送:
<span class="filename">文件名src/main.rs</span>
@ -182,19 +186,19 @@ HTTP/1.1 200 OK\r\n\r\n
{{#rustdoc_include ../listings/ch21-web-server/listing-21-05/src/main.rs:here}}
```
<span class="caption">示例 20-5: 将 *hello.html* 的内容作为响应 body 发送</span>
<span class="caption">示例 21-5: 将 *hello.html* 的内容作为响应 body 发送</span>
我们在开头 `use` 语句将标准库的文件系统模块 `fs` 引入作用域。打开和读取文件的代码应该看起来很熟悉,因为第十二章 I/O 项目的示例 12-4 中读取文件内容时出现过类似的代码。
我们在开头 `use` 语句将标准库的文件系统模块 `fs` 引入作用域。打开和读取文件的代码应该看起来很熟悉;在第十二章 `I/O` 项目中的 Listing 12-4 就用到了类似的代码。
接下来,使用 `format!` 将文件内容加入到将要写入流的成功响应的 body 中。
接下来,使用 `format!` 将文件内容加入到将要写入流的成功响应的 body 中。为了确保构造出有效的 HTTP 响应,我们添加了 `Content-Length` header其值设为响应 `body` 的大小,在这里也就是 `hello.html` 文件的大小。
使用 `cargo run` 运行程序,在浏览器加载 *127.0.0.1:7878*,你应该会看到渲染出来的 HTML 文件
使用 `cargo run` 运行程序,在浏览器加载 *127.0.0.1:7878*,你应该会看到渲染后的 HTML
目前忽略了 `http_request` 中的请求数据并无条件的发送了 HTML 文件的内容。这意味着如果尝试在浏览器中请求 *127.0.0.1:7878/something-else* 也会得到同样的 HTML 响应。目前我们的 server 的作用是非常有限的,也不是大部分 server 所做的让我们检查请求并只对格式良好well-formed的请求 `/` 发送 HTML 文件。
目前忽略了 `http_request` 中的请求数据并无条件的发送了 HTML 文件的内容。这意味着如果尝试在浏览器中请求 *127.0.0.1:7878/something-else* 也会得到同样的 HTML 响应。目前我们的 server 的作用是非常有限的,也不是大部分 server 所做的那样让我们检查请求并只对格式良好well-formed的请求 `/` 发送 HTML 文件。
### 验证请求并有选择的进行响应
目前我们的 web server 不管客户端请求什么都会返回相同的 HTML 文件。让我们增加在返回 HTML 文件前检查浏览器是否请求 */*,并在其请求任何其他内容时返回错误的功能。为此需要如示例 20-6 那样修改 `handle_connection`。新代码接收到的请求的内容与已知的 */* 请求的一部分做比较,并增加了 `if``else` 块来区别处理请求:
目前我们的 web server 不管客户端请求什么都会返回相同的 HTML 文件。让我们增加在返回 HTML 文件前检查浏览器是否请求 */*,并在其请求任何其他内容时返回错误的功能。为此需要如示例 21-6 那样修改 `handle_connection`。新代码接收到的请求的内容与已知的 */* 请求做比较,并增加了 `if``else` 块来区别处理请求:
<span class="filename">文件名src/main.rs</span>
@ -202,17 +206,17 @@ HTTP/1.1 200 OK\r\n\r\n
{{#rustdoc_include ../listings/ch21-web-server/listing-21-06/src/main.rs:here}}
```
<span class="caption">示例 20-6: 以不同于其它请求的方式处理 */* 请求</span>
<span class="caption">示例 21-6: 以不同于其它请求的方式处理 */* 请求</span>
我们只看 HTTP 请求的第一行,所以不同于将整个请求读取进 vector 中,这里调用 `next` 从迭代器中获取第一项。第一个 `unwrap` 负责处理 `Option` 并在迭代器没有项时停止程序。第二个 `unwrap` 处理 `Result` 并与示例 20-2 中增加的 `map` 中的 `unwrap` 有着相同的效果。
我们只看 HTTP 请求的第一行,所以不同于将整个请求读取进 vector 中,这里调用 `next` 从迭代器中获取第一项。第一个 `unwrap` 负责处理 `Option` 并在迭代器没有项时停止程序。第二个 `unwrap` 处理 `Result` 并与示例 21-2 中增加的 `map` 中的 `unwrap` 有着相同的效果。
接下来检查 `request_line` 是否等于一个 */* 路径的 GET 请求。如果是,`if` 代码块返回 HTML 文件的内容。
如果 `request_line` **不** 等于一个 */* 路径的 GET 请求,就说明接收的是其请求。我们之后会在 `else` 块中增加代码来响应所有其他请求。
如果 `request_line` **不** 等于一个 */* 路径的 GET 请求,就说明接收的是其请求。我们之后会在 `else` 块中增加代码来响应所有其他请求。
现在如果运行代码并请求 *127.0.0.1:7878*,就会得到 *hello.html* 中的 HTML。如果进行任何其他请求比如 *127.0.0.1:7878/something-else*,则会得到像运行示例 20-1 和 20-2 中代码那样的连接错误。
现在如果运行代码并请求 *127.0.0.1:7878*,就会得到 *hello.html* 中的 HTML。如果进行任何其他请求比如 *127.0.0.1:7878/something-else*,则会得到像运行示例 21-1 和 21-2 中代码那样的连接错误。
现在向示例 20-7 的 `else` 块增加代码来返回一个带有 404 状态码的响应,这代表了所请求的内容没有找到。接着也会返回一个 HTML 向浏览器终端用户表明此意:
现在向示例 21-7 的 `else` 块增加代码来返回一个带有 404 状态码的响应,这代表了所请求的内容没有找到。接着也会返回一个 HTML 向浏览器终端用户渲染该响应。
<span class="filename">文件名src/main.rs</span>
@ -220,9 +224,9 @@ HTTP/1.1 200 OK\r\n\r\n
{{#rustdoc_include ../listings/ch21-web-server/listing-21-07/src/main.rs:here}}
```
<span class="caption">示例 20-7: 对于任何不是 */* 的请求返回 `404` 状态码的响应和错误页面</span>
<span class="caption">示例 21-7: 对于任何不是 */* 的请求返回 404 状态码的响应和错误页面</span>
这里,响应的状态行有状态码 404 和原因短语 `NOT FOUND`。仍然没有返回任何 header而其 body 将是 *404.html* 文件中的 HTML。需要在 *hello.html* 同级目录创建 *404.html* 文件作为错误页面;这一次也可以随意使用任何 HTML 或使用示例 20-8 中的示例 HTML
这里,响应的状态行有状态码 404 和原因短语 `NOT FOUND`。仍然没有返回任何 header而其 body 将是 *404.html* 文件中的 HTML。需要在 *hello.html* 同级目录创建 *404.html* 文件作为错误页面;这一次也可以随意使用任何 HTML 或使用示例 21-8 中的示例 HTML
<span class="filename">文件名404.html</span>
@ -230,13 +234,13 @@ HTTP/1.1 200 OK\r\n\r\n
{{#include ../listings/ch21-web-server/listing-21-07/404.html}}
```
<span class="caption">示例 20-8: 任何 404 响应所返回错误页面内容样例</span>
<span class="caption">示例 21-8: 任何 404 响应所返回错误页面内容样例</span>
有了这些修改,再次运行 server。请求 *127.0.0.1:7878* 应该会返回 *hello.html* 的内容,而对于任何其他请求,比如 *127.0.0.1:7878/foo*,应该会返回 *404.html* 中的错误 HTML
有了这些修改,再次运行服务端。请求 *127.0.0.1:7878* 应该会返回 *hello.html* 的内容,而对于任何其他请求,比如 *127.0.0.1:7878/foo*,应该会返回 *404.html* 中的错误 HTML
### 少量代码重构
### 稍加重构
目前 `if``else` 块中的代码有很多的重复:他们都读取文件并将其内容写入流。唯一的区别是状态行和文件名。为了使代码更为简明,将这些区别分别提取到一行 `if``else` 中,对状态行和文件名变量赋值;然后在读取文件和写入响应的代码中无条件使用这些变量。重构后取代了大段 `if``else` 块代码后的结果如示例 20-9 所示:
目前 `if``else` 块中的代码存在大量重复:他们都读取文件并将其内容写入流。唯一的区别是状态行和文件名。为使代码更简洁,将这些区别分别提取到各自的 `if``else` 中,对状态行和文件名变量赋值;然后在读取文件和写入响应的代码中无条件使用这些变量。重构后取代了大段 `if``else` 块代码后的结果如示例 21-9 所示:
<span class="filename">文件名src/main.rs</span>
@ -244,12 +248,12 @@ HTTP/1.1 200 OK\r\n\r\n
{{#rustdoc_include ../listings/ch21-web-server/listing-21-09/src/main.rs:here}}
```
<span class="caption">示例 20-9: 重构使得 `if``else` 块中只包含两个情况所不同的代码</span>
<span class="caption">示例 21-9: 重构使得 `if``else` 块中只包含两个情况所不同的代码</span>
现在 `if``else` 块所做的唯一的事就是在一个元组中返回合适的状态行和文件名的值;接着使用第十九章讲到的使用模式的 `let` 语句通过解构元组的两部分 `filename``header` 赋值。
现在 `if``else` 块所做的唯一的事就是在一个元组中返回合适的状态行和文件名的值;接着使用第十九章讲到的使用模式的 `let` 语句通过解构元组的两部分 `filename``header` 赋值。
之前读取文件和写入响应的冗余代码现在位于 `if``else` 块之外,并会使用变量 `status_line``filename`。这样更易于观察这两种情况真正有何不同,还意味着如果需要改变如何读取文件或写入响应时只需要更新一处的代码。示例 20-9 中代码的行为与示例 20-8 完全一样
之前读取文件和写入响应的冗余代码现在位于 `if``else` 块之外,并会使用变量 `status_line``filename`。这样更易于观察这两种情况真正有何不同,还意味着如果需要改变如何读取文件或写入响应时只需要更新一处的代码。示例 21-9 中代码的行为与示例 21-8 完全相同
好极了!我们有了一个 40 行左右 Rust 代码的小而简单的 server,它对一个请求返回页面内容而对所有其他请求返回 404 响应。
好极了!我们现在有了一个 40 行左右 Rust 代码的小而简单的服务端,它对一个请求返回页面内容而对所有其他请求返回 404 响应。
目前 server 运行于单线程中,它一次只能处理一个请求。让我们模拟一些慢请求来看看这如何会成为一个问题,并进行修复以便 server 可以一次处理多个请求。
目前服务端运行于单线程中,这意味着它一次只能处理一个请求。让我们模拟一些慢请求来看看这如何会成为一个问题。然后我们将修复它使得服务端能够同时处理多个请求。

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Loading…
Cancel
Save