Merge pull request #817 from showier-drastic/patch-2

Update ch20-01-single-threaded.md
This commit is contained in:
KaiserY 2024-09-17 00:34:14 +08:00 committed by GitHub
commit 30ab1e63d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -12,7 +12,7 @@ TCP 是一个底层协议,它描述了信息如何从一个 server 到另一
### 监听 TCP 连接 ### 监听 TCP 连接
所以我们的 web server 所需做的第一件事便是能够监听 TCP 连接。标准库提供了 `std::net` 模块处理这些功能。让我们一如既往新建一个项目: 我们的 web server 所需做的第一件事,是监听 TCP 连接。标准库提供了 `std::net` 模块处理这些功能。让我们一如既往新建一个项目:
```console ```console
$ cargo new hello $ cargo new hello
@ -20,7 +20,7 @@ $ cargo new hello
$ cd hello $ cd hello
``` ```
`src/main.rs` 输入示例 20-1 中的代码作为开始。这段代码会在地址 `127.0.0.1:7878` 上监听传入的 TCP 流。当获取到传入的流,它会打印出 `Connection established!` 现在,`src/main.rs` 输入示例 20-1 中的代码作为一个开始。这段代码会在地址 `127.0.0.1:7878` 上监听传入的 TCP 流。当获取到传入的流,它会打印出 `Connection established!`
<span class="filename">文件名src/main.rs</span> <span class="filename">文件名src/main.rs</span>
@ -30,17 +30,17 @@ $ cd hello
<span class="caption">示例 20-1: 监听传入的流并在接收到流时打印信息</span> <span class="caption">示例 20-1: 监听传入的流并在接收到流时打印信息</span>
`TcpListener` 用于监听 TCP 连接。我们选择监听本地地址 `127.0.0.1:7878`。将这个地址拆开,冒号之前的部分是一个代表本机的 IP 地址(这个地址在每台计算机上都相同,并不特指作者的计算机),而 `7878` 是端口。选择这个端口出于两个原因:通常 HTTP 不接受这个端口的请求所以它不太与你机器上运行的其它 web server 的端口冲突,而且 7878 在电话上打出来就是 "rust"(译者注:九宫格键盘上的英文)。 `TcpListener` 用于监听 TCP 连接。我们选择监听本地地址 `127.0.0.1:7878`。将这个地址拆开来看,冒号之前的部分是一个代表本机的 IP 地址(在每台计算机上,这个地址都指本机,并不特指作者的计算机),而 `7878` 是端口。选择这个端口出于两个原因:通常 HTTP 服务器不在这个端口上接受请求,所以它不太可能与你机器上运行的其它 web server 的端口冲突;而且 7878 在电话上打出来就是 "rust"(译者注:九宫格键盘上的英文)。
在这个场景中 `bind` 函数类似于 `new` 函数,在这里它返回一个新的 `TcpListener` 实例。这个函数叫做 `bind` 是因为,在网络领域,连接到监听端口称为 “绑定到一个端口”“binding to a port” 在这个场景中 `bind` 函数类似于 `new` 函数,在这里它返回一个新的 `TcpListener` 实例。这个函数叫做 `bind` 是因为,在网络领域,连接到监听端口称为“绑定到端口”“binding to a port”
`bind` 函数返回 `Result<T, E>`,这表明绑定可能会失败,例如,连接 80 端口需要管理员权限(非管理员用户只能监听大于 1023 的端口),所以如果不是管理员尝试连接 80 端口,则会绑定失败。例如如果运行两个此程序的实例这样会有两个程序监听相同的端口,绑定会失败。因为我们是出于学习目的来编写一个基础的 server将不用关心处理这类错误使用 `unwrap` 在出现这些情况时直接停止程序。 `bind` 函数返回 `Result<T, E>`,这表明绑定可能会失败。例如,监听 80 端口需要管理员权限(非管理员用户只能监听大于 1023 的端口),所以如果尝试监听 80 端口而没有管理员权限,则会绑定失败。再比如,如果我们运行这个程序的两个实例,并因此有两个实例监听同一个端口,那么绑定也将失败。我们是出于学习目的来编写一个基础的服务器,不用关心处理这类错误,而仅仅使用 `unwrap` 在出现这些情况时直接停止程序。
`TcpListener``incoming` 方法返回一个迭代器,它提供了一系列的流(更准确的说是 `TcpStream` 类型的流)。**流***stream*)代表一个客户端和服务端之间打开的连接。**连接***connection*)代表客户端连接服务端、服务端生成响应以及服务端关闭连接的全部请求 / 响应过程。为此,我们会从 `TcpStream` 读取客户端发送了什么并接着向流发送响应以向客户端发回数据。总体来说,这个 `for` 循环会依次处理每个连接并产生一系列的流供我们处理。 `TcpListener``incoming` 方法返回一个迭代器,它提供了一系列的流(更准确的说是 `TcpStream` 类型的流)。**流***stream*)代表一个客户端和服务端之间打开的连接。**连接***connection*)代表客户端连接服务端、服务端生成响应以及服务端关闭连接的全部请求 / 响应过程。为此,我们会从 `TcpStream` 读取客户端发送了什么并接着向流发送响应以向客户端发回数据。总体来说,这个 `for` 循环会依次处理每个连接并产生一系列的流供我们处理。
目前为止,处理流的过程包含 `unwrap` 调用,如果出现任何错误会终止程序,如果没有任何错误,则打印出信息。下一个例我们将为成功的情况增加更多功能。当客户端连接到服务端时 `incoming` 方法返回错误是可能的,因为我们实际上没有遍历连接,而是遍历 **连接尝试***connection attempts*)。连接可能会因为多原因不能成功,大部分是操作系统相关的。例如,很多系统限制同时打开的连接数;新连接尝试产生错误,直到一些打开的连接关闭为止。 目前,处理流的代码中也有一个 `unwrap` 调用,如果 `stream` 出现任何错误会终止程序;如果没有任何错误,则打印出信息。下一个例子中,我们将为成功的情况增加更多功能。当客户端连接到服务端时`incoming` 方法是可能返回错误的,因为我们实际上不是在遍历连接,而是遍历 **连接尝试***connection attempts*)。连接的尝试可能会因为多原因不能成功,大部分是操作系统相关的。例如,很多系统限制同时打开的连接数,超出数量限制的新连接尝试会产生错误,直到一些现有的连接关闭为止。
让我们试试这段代码!首先在终端执行 `cargo run`,接着在浏览器中加载 `127.0.0.1:7878`。浏览器会显示出看起来类似于“连接重置”“Connection reset”的错误信息因为 server 目前并没响应任何数据。但是如果我们观察终端,会发现当浏览器连接 server 时会打印出一系列的信息! 让我们试试这段代码!首先在终端执行 `cargo run`,接着在浏览器中打开 `127.0.0.1:7878`。浏览器会显示出看起来类似于“连接重置”“Connection reset”的错误信息因为 server 目前并没响应任何数据。如果我们观察终端,会发现当浏览器连接我们的服务端时,会打印出一系列的信息!
```text ```text
Running `target/debug/hello` Running `target/debug/hello`
@ -49,15 +49,15 @@ Connection established!
Connection established! Connection established!
``` ```
有时会看到对于一次浏览器请求会打印出多条信息;这可能是因为浏览器在请求页面的同时还请求了其他资源,比如出现在浏览器 tab 标签中的 *favicon.ico* 有时,对于一次浏览器请求,可能会打印出多条信息;这可能是因为,浏览器在请求页面的同时,还请求了其他资源,比如出现在浏览器标签页开头的图标(*favicon.ico*
这也可能是因为浏览器尝试多次连接 server因为 server 没有响应任何数据。`stream` 在循环的结尾离开作用域并被丢弃,其连接将被关闭,作为 `drop` 实现的一部分。浏览器有时通过重连来处理关闭的连接,因为这些问题可能是暂时的。现在重要的是我们成功的处理了 TCP 连接! 这也可能是因为浏览器尝试多次连接服务端,因为服务端没有响应任何数据。作为 `drop` 实现的一部分,`stream` 在循环的结尾离开作用域并被丢弃,其连接将被关闭。浏览器有时通过重连来处理关闭的连接,因为对于一般网站而言,这些问题可能是暂时的。这些都不重要;现在重要的是我们成功的处理了 TCP 连接!
记得当运行完特定版本的代码后使用 <span class="keystroke">ctrl-C</span> 来停止程序。并通过执行 `cargo run` 命令在做出最新的代码修改之后重启服务。 记得当运行完特定版本的代码后使用 <span class="keystroke">ctrl-C</span> 来停止程序。并通过执行 `cargo run` 命令在做出最新的代码修改之后重启服务。
### 读取请求 ### 读取请求
让我们实现读取来自浏览器请求的功能!为了分离获取连接和接下来对连接的操作的相关内容,我们将开始一个新函数来处理连接。在这个新的 `handle_connection` 函数中,我们从 TCP 流中读取数据并打印出来以便观察浏览器发送过来的数据。将代码修改为如示例 20-2 所示: 让我们实现读取来自浏览器请求的功能!为了分离“获取连接”以及“接下来对连接的操作”,我们将开始写一个新函数来处理连接。在这个新的 `handle_connection` 函数中,我们从 TCP 流中读取数据并打印出来以便观察浏览器发送过来的数据。将代码修改为如示例 20-2 所示:
<span class="filename">文件名src/main.rs</span> <span class="filename">文件名src/main.rs</span>
@ -67,7 +67,7 @@ Connection established!
<span class="caption">示例 20-2: 读取 `TcpStream` 并打印数据</span> <span class="caption">示例 20-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 方法的调用。