From ecd2e9e6da07788ebd40fe1711bbf2904f278e79 Mon Sep 17 00:00:00 2001 From: "rust-lang.xfoss.com" Date: Fri, 1 Dec 2023 14:09:46 +0800 Subject: [PATCH] Finished re-constructure --- ...ect_Building_a_Multithreaded_Web_Server.md | 1613 +---------------- src/Ch21_Appendix.md | 948 ---------- src/SUMMARY.md | 12 + src/appdendix/derivable_traits.md | 116 ++ src/appdendix/dev_tools.md | 162 ++ src/appdendix/editions.md | 27 + src/appdendix/keywords.md | 118 ++ src/appdendix/notes.md | 43 + src/appdendix/ops_and_symbols.md | 202 +++ src/appdendix/releases.md | 141 ++ src/appdendix/terminology_list.md | 164 ++ src/appdendix/translations.md | 4 + src/final_project/graceful_shutdown.md | 453 +++++ src/final_project/multithreaded.md | 723 ++++++++ src/final_project/single-threaded.md | 457 +++++ 15 files changed, 2627 insertions(+), 2556 deletions(-) create mode 100644 src/appdendix/derivable_traits.md create mode 100644 src/appdendix/dev_tools.md create mode 100644 src/appdendix/editions.md create mode 100644 src/appdendix/keywords.md create mode 100644 src/appdendix/notes.md create mode 100644 src/appdendix/ops_and_symbols.md create mode 100644 src/appdendix/releases.md create mode 100644 src/appdendix/terminology_list.md create mode 100644 src/appdendix/translations.md create mode 100644 src/final_project/graceful_shutdown.md create mode 100644 src/final_project/multithreaded.md create mode 100644 src/final_project/single-threaded.md diff --git a/src/Ch20_Final_Project_Building_a_Multithreaded_Web_Server.md b/src/Ch20_Final_Project_Building_a_Multithreaded_Web_Server.md index 2a98e56..290be3c 100644 --- a/src/Ch20_Final_Project_Building_a_Multithreaded_Web_Server.md +++ b/src/Ch20_Final_Project_Building_a_Multithreaded_Web_Server.md @@ -15,1617 +15,14 @@ 以下是构建这个 web 服务器的计划: 1. 学习一点有关 TCP 与 HTTP 方面的知识; + 2. 在某个套接字上监听 TCP 连接; + 3. 解析少数几个 HTTP 请求; + 4. 创建出某种恰当的 HTTP 响应; + 5. 运用线程池,提升咱们服务器的吞吐量。 + 在咱们开始动手前,咱们应注意到一个情况:咱们将运用的方法,将不会是在 Rust 下构建 web 服务器的最佳方法。在 [crates.io](https://crates.io/) 上,一些社区成员已经发布了数个,适合用于生产环境,提供了更完整功能的 web 服务器,以及咱们将要构建的线程池实现的代码箱。但是,本章中咱们的意图,是要帮助咱们学习掌握,而非走那样的捷径。由于 Rust 是门系统编程语言,因此咱们可以选择咱们打算着手的抽象层次,并可以触及到相比其他语言中,可行的或可操作的更低级别。因此咱们将亲自编写这个基本的 HTTP 服务器与线程池,如此咱们便可以学习这些代码箱之后的,今后可能会用到的一些一般概念与技巧。 - - -## 构建一个单线程的 Web 服务器 - -咱们将通过让一个单线程的 web 服务器工作起来而开始。在咱们开始前,先来看看在构建 web 服务器中涉及到的一些协议的快速概览。这些协议的细节,超出了本书范围,而简要概述,就将给到咱们所需的信息。 - -Web 服务器中涉及的两种主要谢谢,分别是 *超文本传输协议,Hypertext Transfer Protocol, HTTP* 与 *传输控制协议,Transmission Control Protocol, TCP*。两种协议都是 *请求-响应,request-response* 的协议,表示 *客户端,client* 发起请求,而 *服务器,server* 监听到请求并提供给客户端一个响应。这些请求和响应的内容是由两种协议定义的。 - -TCP 是种描述了信息如何从一台服务器到达另一服务器,但并未指明信息为何的低级别。HTTP 则是经由定义请求与响应的内容,而于 TCP 之上构建的。技术上要在其他协议上使用 HTTP 是可行的,但在绝大多数情况下,HTTP 都在 TCP 上发送他的数据。咱们将在 TCP 的原始字节,与 HTTP 请求和响应下,进行工作。 - -### 监听 TCP 连接 - -**Listen to the TCP Connection** - -咱们的 web 服务器需要监听某个 TCP 连接,因此那便是咱们将要做的第一部分工作。标准库提供了一个 `std::net` 模组,允许咱们完成这一点。咱们来以寻常方式构造一个新的项目: - -```console -$ cargo new hello --vcs none - Created binary (application) `hello` package -$ cd hello -``` - -现在请输入下面清单 20-1 中 `src/main.rs` 里的代码来开始。这段代码会在本地地址 `127.0.0.1:7878` 处监听传入的 TCP 流。当他获取到一个传入流时,他就会打印 `连接已建立!`。 - -文件名:`src/main.rs` - -```rust -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! ("连接已建立!"); - } -} -``` - -*清单 20-1:监听传入流并在咱们接收到某个流时打印一条消息* - -运用 `TcpListener`,咱们就可以在地址 `127.0.0.1:7878` 处监听 TCP 连接。在这个地址中,冒号之前的部分,是个表示咱们的计算机的 IP 地址(在所有计算机上这都是同样的,而并不特别表示本书作者的计算机),同时 `7878` 为端口。咱们之所以选择了这个端口,有两个原因:通常不是在这个端口上接收 HTTP,因此咱们的服务器,大概率不会与咱们可能在咱们的机器上运行的任何别的 web 服务器冲突,而 `7878` 则是电话机上输入的 *rust*。 - -这个场景中的 `bind` 函数,会像将返回一个新 `TcpListener` 实例的 `new` 函数一样工作。该函数之所以叫做 `bind`,是因为在网络通信中,连接到要监听的端口,被称为 “绑定到端口”。 - -`bind` 函数返回的是个 `Result`,表明有可能绑定失败。比如,连接到端口 `80` 需要管理员权限(非管理员只可以监听高于 `1023` 的那些端口,译注:在 *nix 平台上有此限制,但在 Win 平台上没有),因此若咱们在非管理员下尝试连接到端口 `80`,端口绑定就不会工作。在比如咱们运行了这个程序的两个实例,而因此有两个程序在监听同一端口时,端口绑定也不会工作。由于咱们仅是处于学习目的,而编写的一个基本服务器,因此咱们就不会纠结于处理这些类别的错误;相反,咱们使用 `unwrap` 来在错误发生时停止这个程序。 - -`TcpListener` 上的 `incoming` 方法,会返回一个给到咱们流(更具体的,是一些类型 `TcpStream` 的流)序列的迭代器,an iterator that gives us a sequence of streams。单一的 *流,stream* 表示了客户端与服务器之间的一个打开的连接,an open connection。而一个 *连接,connection* 则是客户端连接到服务器过程中,完整的请求与响应的叫法,服务器会生成一个响应,且服务器会关闭这个连接。就这样,咱们将从那个 `TcpStream` 读取,来看看客户端发送了什么,并于随后把咱们的响应写到这个流,以将数据发送回客户端。总的来说,这个 `for` 循环将依次处理每个连接,并为咱们产生一系列要处理的流。 - -至于现在,咱们对流的处理,是由在流有任何错误时,调用 `unwrap` 来终止咱们的程序所构成;若没有任何错误,那么这个程序就会打印一条消息。在下一代码清单中,咱们将为流成功的情形,添加更多功能。在客户端连接到服务器时,咱们可能会从那个 `incoming` 方法收到错误的原因,便是咱们没有真正在一些连接上迭代。相反,咱们是在一些 *连接尝试,connection attempts* 上迭代。连接可能因为数种原因而不成功,许多的这些原因都是特定于操作系统的。比如,许多操作系统都有他们所支持的并发开启连接数限制,a limit to the number of simultaneous open connecitons;超出那个数目的新建连接尝试就会产生错误,除非一些开启的连接关闭。 - -咱们来尝试运行这段代码!在终端里运行 `cargo run` 并随后在 web 浏览器中加载 `127.0.0.1:7878`。由于服务器没有正确发回任何数据,因此浏览器应给出像是 `Connection reset,` 的错误消息。但当咱们看着终端时,应看到在浏览器连接到服务器时,有数条打印处的消息! - -> 注:可使用 `$curl 127.0.0.1:7878` 命令进行调试,且使用 `curl` 也是网络编程调试中常用的方法。 - -```console - Running `target/debug/hello` -连接已建立! -连接已建立! -连接已建立! -连接已建立! -``` - -有的时候,咱们会看到一次浏览器请求下打印出的多条消息;原因可能是浏览器在构造页面请求时,也会构造其他资源的请求,像是出现在浏览器 tab 分页中的 `favicon.ico` 图标。 - -也可由可能是由于这个服务器没有响应任何数据,浏览器因此会尝试多次连接到这个服务器。在 `stream` 超出作用域,而在那个循环结束出被丢弃时,连接就会作为 `drop` 实现的一部分而被关闭。由于故障可能是临时的,因此浏览器有时会以重试处理关闭的连接。重要的是,咱们已然成功得到了到 TCP 连接的句柄,a handle to a TCP connection! - -请记得在咱们完成运行代码的特定版本时,要通过按下 `Ctrl-c` 来停止这个程序。以后在完成了各套代码修改后,要通过运行 `cargo run` 命令重启这个程序,来确保咱们是在运行最新的代码。 - - -### 读取请求 - -**Reading the Request** - -咱们来实现读取来自浏览器请求的功能!为将首选获取到连接,及随后对连接采取一些措施这两个关注点分离,咱们将开启一个用于处理连接的新函数。在这个新的 `handle_connection` 函数中,咱们将从 TCP 流读取数据,并将其打印出来,从而咱们就可以看到从浏览器发出的数据。请将代码修改为清单 20-2 这样。 - -文件名:`src/main.rs` - -```rust -#![allow(warnings)] -use std::{ - io::{prelude::*, BufReader}, - 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_conn(stream); - } -} - -fn handle_conn(mut stream: TcpStream) { - let buf_reader = BufferedReader::new(stream); - let http_req: Vec<_> = buf_reader - .lines() - .map(|res| res.unwrap()) - .take_while(|line| !line.is_empty()) - .collect(); - - println! ("请求:{:#?}", http_request); -} -``` - -*清单 20-2:从 `TcpStream` 读取并打印出数据* - - -咱们将 `std::io::prelude` 与 `std::io::BufReader` 带入作用域,来获取到实现从 TCP 流读取和写入的那些特质与类型的访问。在 `main` 函数的那个 `for` 循环中,不再是打印一条声称咱们已构造一个连接的消息,咱们限制调用了新的 `handle_conn` 函数,并把那个 `stream` 传递给他。 - -在 `handle_conn` 函数中,咱们创建了一个新的,封装着到 `stream` 的一个可变引用的 `BufReader` 实例。`BufReader` 会通过管理到 `std::io::Read` 特质一些方法的调用,为咱们添加缓冲。 - -咱们创建了一个名为 `http_req` 的变量,来收集浏览器发送到咱们服务器的请求的那些行。通过添加那个 `Vec<_>` 类型注解,咱们表明了咱们打算把这些行收集到一个矢量值中。 - -`BufReader` 实现了 `std::io::BufRead` 特质,该特质提供了 `lines` 方法。`lines` 方法会经由当其发现一个新行字节,a newline byte, 时分割数据流,而返回一个 `Result 注:使用 `curl --noproxy '*' 127.0.0.1:7878` 的输出,如下面这样: - - -```console -$ cargo run - Finished dev [unoptimized + debuginfo] target(s) in 0.00s - Running `target/debug/hello` -请求:[ - "GET / HTTP/1.1", - "Host: 127.0.0.1:7878", - "User-Agent: curl/7.68.0", - "Accept: */*", -] -``` - -根据咱们的浏览器,咱们可能会得到些许不同的输出。既然咱们打印了请求数据,咱们就可以通过查看请求第一行中 `GET` 之后的路径,而发现为何咱们会从一次浏览器请求,得到多个连接。若重复的连接都是在请求 `/`,咱们就知道由于浏览器没有从咱们的程序得到响应,因此其是在尝试重复获取 `/`。 - -下面来对这一请求数据加以细分,以搞清楚浏览器是在询问咱们的程序些什么。 - - -### 近观 HTTP 请求 - -**A closer Look at an HTTP Request** - - -HTTP 是种基于文本的协议,而请求会采用下面这种格式: - -```text -Method Request-URI HTTP-Version CRLF -headers CRLF -message-body -``` - -第一行是保存着有关该客户端正请求什么的信息的 *请求行,request line*。该请求行的第一部分,表示正使用的 *方法,method*,比如 `GET` 或 `POST`,描述了客户端是如何构造此请求的。咱们的客户端使用了一个 `GET` 请求,这意味着其是在询问信息。 - -请求行接下来的部分为 `/`,表示客户端正请求的 *同一资源标识符,Uniform Resource Identifier, URI*:URI 几乎是,但不完全与 *同一资源定位符,Uniform Resource Locator, URL* 一样。URIs 与 URLs 之间的区别对于这章中咱们的目的不重要,但 HTTP 的规格使用了 URI 这个词,因此咱们只能在此处暗自用 URL 代替 URI。 - -最后部分是客户端所用的 HTTP 版本,而随后这个请求行便以一个 *CRLF 序列,CRLF sequence* (CRLF 代表的是 *回车,carriage return* 与 *换行,line fedd*,是打字机时代的术语!)结束了。这个 CRLF 序列还可以写作 `\r\n`,其中的 `\r` 是个回车,而 `\n` 是个换行。CRLF 序列将请求行与其余的请求数据分开。请注意当 CRLF 被打印时,咱们会看到一个新行开始,而非 `\r\n`。 - -查看如今咱们从运行这个程序所接收到的请求行数据,咱们发现 `GET` 即为请求方法,`/` 便是请求的 URI,而 `HTTP/1.1` 则是请求的 HTTP 版本。 - -在请求行之后,从 `Host:` 开始的其余那些行,就是些头了。`GET` 请求没有请求体。 - -请从不同浏览器构造请求,或是询问不同地址,比如 `127.0.0.1:7878/test`,来发现请求数据会怎样变化。 - -> 注:运行 `curl --noproxy '*' 127.0.0.1:7878/test` 时,请求数据如下所示: - - -```console -请求:[ - "GET /test HTTP/1.1", - "Host: 127.0.0.1:7878", - "User-Agent: curl/7.68.0", - "Accept: */*", -] -``` - -既然咱们明白了浏览器是在询问什么,下面就来发回一些数据吧! - - -### 写下响应 - -**Writing a Response** - -咱们将要实现发送响应客户端请求数据。响应有着下面的格式: - -```text -HTTP-Version Status-Code Reason-Phrase CRLF -headers CRLF -message-body -``` - -其中第一行是包含在响应中用到的 HTTP 版本的 *状态行,status line*、汇总了请求结果的一个数字的状态码、以及提供了状态码文字描述的一个原因短语,a reason phrase。在那个 CRLF 之后是一些 HTTP 头、另一个 CRLF 序列、及响应的响应体。 - -下面就是一个使用了 HTTP 版本 1.1 的示例响应,有着状态码 `200`、一个 `OK` 的原因短语、没有头部、也没有响应体。 - -```text -HTTP/1.1 200 OK\r\n\r\n -``` - -状态代码 `200` 是标准的成功响应。这个文本便是个极小的成功 HTTP 响应。下面来把这个响应,作为咱们到成功请求的响应,写到 TCP 流!在那个 `handle_conn` 函数中,移除曾是打印请求数据的 `println!`,而将其替换为下面清单 20-3 中的代码。 - -文件名:`src/main.rs` - -```rust -fn handle_conn(mut stream: TcpStream) { - let buf_reader = BufReader::new(&mut stream); - let http_req: Vec<_> = buf_reader - .lines() - .map(|res| res.unwrap()) - .take_while(|line| !line.is_empty()) - .collect(); - - let resp = "HTTP/1.1 200 OK\r\n\r\n"; - - stream.write_all(resp.as_bytes()).unwrap(); -} -``` - -*清单 20-3:将一个极小的成功 HTTP 响应写到 TCP 流* - -那第一行定义了保存成功消息数据的 `resp` 变量。随后咱们在咱们的 `resp` 上调用 `as_bytes`,将字符串数据转换为一些字节。`stream` 上的 `write_all` 方法,会取一个 `&[u8]` 并将那些字节直接发送到 TCP 连接。由于 `write_all` 操作可能失败,咱们就像前面一样,于任何的错误结果上使用 `unwrap`。再次,在真实应用中,咱们会在这里加上错误处理。 - -有了这些修改,咱们来运行咱们的代码,并构造一次请求。咱们就不再打印任何数据到终端,因此咱们不会看到除 Cargo 的输出外,其他任何的输出。当咱们在 web 浏览器中加载 `127.0.0.1:7878` 时,咱们应得到一个空白页而非报错。咱们刚刚已经硬编码了接收 HTTP 请求并发送一次响应了! - - -### 返回真正的 HTML - -**Returning Real HTML** - -下面来实现返回相比空白页更多内容的功能。请在咱们的项目目录根处,而非 `src` 目录中创建一个新文件 `hello.html`。咱们可放入任何咱们想要的 HTML;下面清单 20-4 给出了一种可能。 - -文件名:`hello.html` - -```html - - - - - 你好! - - -

你好!

-

来自 Rust 的问好

- - -``` - -*清单 20-4:要在响应中返回的一个样例 HTML 文件* - -这是个带有一个与一些文本的最小 HTML5 文档。要在收到一个请求时从服务器返回这个文档,咱们将如下清单 20-5 中所示那样,修改 `handle_conn` 来读取这个 HTML 文件,将其作为响应体,添加到一个响应,并将其发送。 - -文件名:`src/main.rs` - -```rust -#![allow(warnings)] -use std::{ - fs, - io::{prelude::*, BufReader}, - 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_conn(stream); - } -} - -fn handle_conn(mut stream: TcpStream) { - let buf_reader = BufReader::new(&mut stream); - let http_req: Vec<_> = buf_reader - .lines() - .map(|res| res.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 resp = - format! ("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); - - stream.write_all(resp.as_bytes()).unwrap(); -} -``` - -*清单 20-5:将 `hello.html` 的内容作为响应的响应体发送* - -咱们已添加 `fs` 到那个 `use` 语句,来将标准库的文件系统模组带入到作用域中。把文件内容读取到一个字符串的代码应看起来不陌生;在第 12 章,于清单 12-4 中为咱们的 I/O 项目读取一个文件的内容时,咱们曾用到过他。 - -接下来,咱们使用了 `format!` 宏,来将那个文件的内容,添加为这个成功响应的响应体。为确保一个有效的 HTTP 响应,咱们添加了被设置为咱们的响应体大小的一个 `Content-Length` 头部,在这个示例中就是 `hello.html` 的大小。 - -以 `cargo run` 运行这段代码,并在浏览器中加载 `127.0.0.1:7878`;咱们应看到咱们的 HTML 被渲染了! - -目前,咱们忽略了 `http_req` 中的响应数据,而只是无条件地发回那个 HTML 文件的内容。那就意味着当咱们在浏览器中尝试请求 `127.0.0.1:7878/something-else` 时,咱们将仍然得到这同样的 HTML 响应。此刻,咱们的服务器是非常有限的,且不会完成绝大多数 web 服务器所完成的那些事情。咱们打算根据请求定制咱们的响应,并只为格式良好的到 `/` 请求,发回这个 HTML 文件。 - - -### 对请求加以验证并有选择地进行响应 - -**Validating the Request and Selectively Responding** - -现在,咱们的 web 服务器将始终返回那个文件中的 HTML,而不管客户端请求的是什么。下面来添加在返回那个 HTML 文件前,检查浏览器是在请求 `/`,并在浏览器请求其他路径时,返回一个错误的功能。为此,咱们需要如下面清单 20-6 中所示的那样修改 `handle_conn`。这段新代码会将收到的请求,与咱们所知的 `/` 请求看起来的样子对比检查,并添加了 `if` 及 `else` 代码块来分别对待各种请求。 - -文件名:`src/main.rs` - -```rust -fn handle_conn(mut stream: TcpStream) { - let buf_reader = BufReader::new(&mut stream); - let req_line = buf_reader.lines().next().unwrap().unwrap(); - - if req_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 resp = - format! ("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); - - stream.write_all(resp.as_bytes()).unwrap(); - } else { - // 别的一些请求 - } -} -``` - -*清单 20-6:以不同于其他请求方式,处理到 `/` 的请求* - -咱们只打算看看 HTTP 请求的第一行,因此就不再将整个请求读取到一个矢量值了,咱们调用了 `next` 来从那个迭代器得到第一个条目。这里的首个 `unwrap` 会注意其中的 `Option`,并在迭代器没有条目时停止这个程序。第二个 `unwrap` 则会处理其中的 `Result`,并与清单 20-2 中所添加的 `map` 里的那个 `unwrap` 有着同样的效果。 - -接下来,咱们检查了 `req_line`,来看看其是否等于到 `/` 路径 `GET` 请求的请求行。在其等于时,那个 `if` 代码块就会返回咱们 HTML 文件的内容。 - -若 `req_line` *不* 等于到 `/` 路径 `GET` 请求的第一行时,就意味着咱们收到了一些别的请求。稍后咱们将添加代码到那个 `else` 代码块,来响应全部其他请求。 - -现在请运行此代码,并请求 `127.0.0.1:7878`;咱们应获取到 `hello.html` 中的 HTML。在咱们构造任何其他请求时,比如 `127.0.0.1:7878/something-else`,就将得到像是咱们曾在运行清单 20-1 及清单 20-2 中的代码时,所看到连接错误。 - -现在来将清单 20-7 中的代码,添加到那个 `else` 代码块,以返回一个带有状态代码 `404` 的响应,这通告了请求的内容未找到。咱们还将返回一些在浏览器中要渲染页面的 HTML,将这种响应表示给终端用户。 - -文件名:`src/main.rs` - -```rust - // --跳过代码-- - } else { - let status_line = "HTTP/1.1 404 NOT FOUND"; - let contents = fs::read_to_string("404.html").unwrap(); - let length = contents.len(); - - let resp = - format! ("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); - - stream.write_all(resp.as_bytes()).unwrap(); - } -``` - -*清单 20-7:在请求的是除 `/` 外的其他路径时以状态代码 `404` 及一个错误页面进行响应* - -此处,咱们的响应有着一个代码状态代码 `404`,及原因短语 `NOT FOUND` 的状态行。该响应的响应体,将是文件 `404.html` 中的 HTML。咱们将需要创建 `hello.html` 旁边,用于错误页面的 `404.html` 文件;请再次随意使用咱们想要的任何 HTML,或使用下面清单 20-8 中的示例 HTML。 - -文件名:`404.html` - -```html - - - - - 你好! - - -

糟糕!

-

抱歉,我不明白你要什么。

- - -``` - -*清单 20-8:全部 404 响应下要发回页面的示例内容* - -在这些修改下,请再次运行咱们的服务器。请求 `127.0.0.1:7878` 应返回 `hello.html` 的内容,而任何别的请求,像是 `127.0.0.1:foo`,就应返回 `404.html` 中的报错 HTML。 - - -### 初试重构 - -*A Touch of Refactoring* - - -此时的 `if` 与 `else` 两个代码块,有着很多重复:他们都在读取文件及将文件内容写到 TCP 流。唯二区别就是响应的状态行与文件名。下面就来通过抽取处这些差异到单独的 `if` 和 `else` 行,这些行将把响应状态行与文件名,赋值给两个变量;随后咱们就可以在代码中,不带条件地使用这两个变量,来读取文件并写下响应。下面清单 20-9 给出了替换了大段的 `if` 与 `else` 代码块后的最终代码。 - -文件名:`src/main.rs` - -```rust -// --跳过代码-- -fn handle_conn(mut stream: TcpStream) { - let buf_reader = BufReader::new(&mut stream); - let req_line = buf_reader.lines().next().unwrap().unwrap(); - - let (status_line, filename) = if req_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 resp = - format! ("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); - - stream.write_all(resp.as_bytes()).unwrap(); -} -``` - -*清单 20-9:将 `if` 和 `else` 代码块重构为只包含两种情况下不同的代码* - -现在 `if` 与 `else` 两个代码块,就只会返回一个元组中,响应状态行与文件名的相应值了;随后咱们运用第 18 章中曾讨论过的 `let` 语句中的模式,而使用了解构特性,来将这两个值复制给 `status_line` 与 `filename`。 - -原先那些重复代码,现在便是在 `if` 与 `else` 两个代码块外面,并使用了 `status_line` 与 `filename` 两个变量。这令到看出两种情况之间的差别更为容易,并意味着在咱们打算修改文件读取与响应写入工作方式时,只有一处要更新代码。清单 20-9 中代码的行为,将与清单 20-8 中的一致。 - -相当棒!现在咱们就有了一个以差不多 40 行 Rust 代码编写的,以一个内容页面响应一个到 `/` 的请求,并以一个 `404` 响应回应全部其他请求的简单 web 服务器了。 - -当前,咱们的服务器是运行在单线程下的,意味着其只能一次服务一个请求。接下来就要通过模拟一下低速请求,检查那怎样会称为一个问题。随后咱们将修复这个问题,从而让咱们的服务器可以一次处理多个请求。 - - -## 将咱们的单线程服务器改写为多线程服务器 - -**Turning Our Single-Thread Server into a Multithreaded Server** - -现在,这个服务器将依次处理每个请求,这意味着其将不会在前一个连接完成处理前,处理后一连接。若服务器收到了越来越多的请求,这种顺序执行就会越来越差。而若该服务器收到了一个要耗费较长时间处理的请求,即使后续的新请求可被快速处理,但其仍将不得不等待直到那个长时间请求完成。咱们需要修复这个问题,但首选,咱们将具体看看这个问题。 - - -### 在当前服务器实现下模拟一个慢速请求 - -**Simulating a Slow Request in the Current Server Implemenation** - -咱们将看看一个慢速处理的请求,能怎样影响那些到咱们当前服务器实现的其他请求。下面清单 20-10 以一个将导致服务器在响应前睡眠 5 秒的模拟慢速请求,实现了对到 `/sleep` 请求的处理。 - -文件名:`src/main.rs` - -```rust -#![allow(warnings)] -use std::{ - fs, - io::{prelude::*, BufReader}, - net::{TcpListener, TcpStream}, - thred, - time::Duration, -}; -// --跳过代码-- - -fn handle_conn(mut stream: TcpStream) { - // --跳过代码-- - - let (status_line, filename) = match &req_line[..] { - "GET / HTTP/1.1" => ( "HTTP/1.1 200 OK", "hello.html"), - "GET /sleep HTTP/1.1" => { - thread::sleep(Duration::from_secs(5)); - ("HTTP/1.1 200 0K", "hello.html") - } - _ => ("HTTP/1.1 404 NOT FOUND", "404.html"), - }; - - // --跳过代码-- -} -``` - -*清单 20-10:通过睡眠 5 秒模拟慢速请求* - -现在咱们有了三种情况,于是就已从 `if` 切换到了 `match`。咱们需要显式地在 `req_line` 切片上,与那三个字符串字面值进行模式匹配;`match` 不会像相等比较方式所做的那样,执行自动引用与解引用。 - -首条支臂与清单 20-9 的 `if` 代码块是一样的。第二条支臂,是将请求与 `/sleep` 匹配。在收到那个请求时,服务器将在渲染那个成功 HTML 页面之前,睡眠 5 秒。第三支臂则与清单 20-9 的那个 `else` 代码块是一样的。 - -咱们可以看出,咱们的服务器有多原始:真正的库将以一种不那么冗长的方式,处理多种请求的识别! - -请使用 `cargo run` 启动服务器。随后打开两个浏览器窗口:一个用于 `http://127.0.0.1/7878`,另一个用于 `http://127.0.0.1:7878/sleep`。若咱们像之前一样进入那个 `/` URI 几次,咱们将看到其响应很快。但在进入 `/sleep` 并于随后加载 `/` 时,就会看到那个 `/` 会一直等待,知道 `sleep` 已经于加载之前睡眠了 5 秒。 - -咱们可以用来避免慢速请求后面那些请求滞后的技巧有多种;咱们将实现的技巧,便是线程池。 - - -### 使用线程池提升吞吐量 - -**Improving Throughput with a Thread Pool** - -所谓 *线程池,thread pool*,是指处于等待中,并准备好处理某项任务的一组生成的线程。在程序收到一项新任务时,他便指派线程池中的一个线程给该项任务,而那个线程就会处理这个任务。池中的剩余线程,则是可以处理任何的于这首个线程进行处理时,进来的那些任务的。在这首个线程完成其任务处理时,他就会回到空闲线程的线程池,准备处理某项新任务。线程池实现了连接的并发处理,从而提升咱们服务器的吞吐能力。 - -咱们将把池中线程数量,先知道一个较小的数目,以保护咱们免于拒绝服务攻击,Denial of Service(DoS) attacks;若咱们让咱们的程序在每个请求进入时,创建一个新线程,那么构造出一千万个请求到咱们的服务器的某人,就能经由耗尽咱们服务器的全部资源,而使得这些请求的处理陷入停滞,而造成极大破坏。 - -这种技巧只是提供 web 服务器吞吐量的许多方法之一。咱们可能探讨的其他选项分别是 *分叉汇合模型,fork/join model*、*单线程异步 I/O 模型,single-threaded async I/O model*,抑或 *多线程异步 I/O 模型,multi-threaded async I/O model*。若对此问题感兴趣,那么可以阅读有关其他解决方案的资料,并尝试实现他们;对于 Rust 这种底层编程语言,所有这些选项都是可行的。 - - -在开始实现线程池前,咱们来聊聊用到这个池子的东西会是什么样子。在咱们正要尝试设计代码时,首先编写客户端界面,可有助于引导咱们的设计。要以咱们打算调用代码 API 的方式,编写出这些有组织架构的代码 API;随后在那种组织架构下实现功能,而非先实现功能而随后设计那些公开 API。 - -与第 12 章中项目里用到的测试驱动方式的开发,test-driven development,类似,这里咱们将运用编译器驱动的开发,compiler-driven development。咱们将先编写出咱们打算调用那些函数的代码,而随后会看看来自编译器的那些报错,以确定出接下来咱们应修改些什么,来让代码运作起来。在咱们进行那一步之前,咱们将探讨一下咱们并不会用到的一种技巧,作为开头。 - -#### 为每个请求生成一个线程 - -**Spawning a Thread for Each Request** - -首先,咱们来探讨一下若咱们的代码给每隔连接创建一个新线程,他看起来会怎样。正如早先所提到的,由于潜在地生成无限数目线程的那些问题,这样做不是咱们的最终计划,但其为首先得到一个运作多线程服务器的起点。随后咱们将添加线程池作为一项改进,且将这两种方案进行对比将更容易一些。下面清单 20-11 给出了把 `main` 构造为于那个 `for` 循环里,生成一个新线程来处理每个 TCP 流的一些修改。 - -文件名:`src/main.rs` - -```rust -fn main() { - let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); - - for stream in listener.incoming() { - let stream = stream.unwrap(); - - thread::spawn(|| { - handle_conn(stream); - }); - } -} -``` - -*清单 20-11:为每个 TCP 流生成一个新线程* - -如同咱们在第 16 章中所学到的,`thread::spawn` 讲创建出一个新线程,并于随后在新线程中,运行那个闭包中的代码。当咱们运行此代码,并在浏览器中加载 `/sleep`,随后在另外两个浏览器 Tab 页中加载 `/`,咱们就会看到到 `/` 的请求就不必等待 `/sleep` 请求完毕了。不过,如同咱们曾提到过的,因为咱们正不带任何限制地构造新线程,而最终将使系统不堪重负。 - - -#### 创建有限数目的线程 - -**Creating a Finite Number of Threads** - - -咱们想要咱们的线程池,以类似的、熟悉的方式运作,而无需那些用到咱们 API 的代码有较大修改。下面清单 20-12 给出了咱们打算用到的 `ThreadPool`,而非 `thread::spawn`,的假想接口。 - -文件名:`src/main.rs` - -```rust -fn main() { - let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); - let pool = ThreadPool::new(4); - - for stream in listener.incoming() { - let stream = stream.unwrap(); - - pool.execute(|| { - handle_conn(stream); - }); - } -} -``` - -*清单 20-12:咱们设想的 `ThreadPool` 接口* - -咱们使用了 `ThreadPool::new` 来创建出有着可配置线程数目的新线程,在此示例中为四个线程。随后,在那个 `for` 循环中,`pool.execute` 有着与 `thread::spawn` 类似的接口,其中他会取个闭包,并将其给到线程池中的某个线程运行。这段代码尚不会编译,但咱们将进行尝试,如此编译器就会引导咱们如何修复他。 - - -#### 运用编译器驱动的开发,构建出 `ThreadPool` - -**Building `ThreadPool` Using Compiler Driven Development** - -请完成清单 20-12 中对 `src/main.rs` 的修改,然后咱们就来运用 `cargo check` 给出的编译器报错,驱动咱们的开发。下面就是咱们所得到的第一个报错: - -```console -$ cargo check - Checking hello v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/hello) -error[E0433]: failed to resolve: use of undeclared type `ThreadPool` - --> src/main.rs:12:16 - | -12 | let pool = ThreadPool::new(4); - | ^^^^^^^^^^ use of undeclared type `ThreadPool` - -For more information about this error, try `rustc --explain E0433`. -error: could not compile `hello` due to previous error -``` - -很棒!这个错误告诉我们,咱们需要一个 `ThreadPool` 类型或模组,因此咱们现在就将构建一个出来。咱们的 `ThreadPool` 实现,将独立于咱们的 web 服务器所完成工作的类型。因此,咱们就来将这个 `hello` 代码箱,从二进制代码箱切换为一个库代码箱,来保存咱们的 `ThreadPool` 实现。在咱们改变为库代码箱后,咱们就可以在打算用到线程池的任何项目,而不只是用来服务 web 请求中,也可以使用这个独立的线程池了。 - -请创建一个包含了下面这个咱们目前所能有的 `ThreadPool` 结构体极简定义的 `src/lib.rs` 文件: - -文件名:`src/lib.rs` - -```rust -pub struct ThreadPool; -``` - -随后编辑 `main.rs`,来通过加入下面的代码到 `src/main.rs` 顶部,将 `ThreadPool` 从那个库代码箱,带入作用域: - -文件名:`src/main.rs` - -```rust -use hello::ThreadPool; -``` - -这段代码仍不会工作,但咱们就来再检查一边,以得到咱们需要解决的下一报错: - -```console -$ cargo check - Checking hello v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/hello) -error[E0599]: no function or associated item named `new` found for struct `ThreadPool` in the current scope - --> src/main.rs:14:28 - | -14 | let pool = ThreadPool::new(4); - | ^^^ function or associated item not found in `ThreadPool` - -For more information about this error, try `rustc --explain E0599`. -error: could not compile `hello` due to previous error -``` - -此报错表明,接下来咱们就要给 `ThreadPool` 创建一个名为 `new` 的关联函数。咱们还知道了那个 `new` 需要有一个可将 `4` 作为实参接收的形参,并应返回一个 `ThreadPool` 的实例。下面就来实现将有着那些特性的这个极简 `new` 函数: - -文件名:`src/lib.rs` - -```rust -pub struct ThreadPool; - -impl ThreadPool { - pub fn new(size: usize) -> ThreadPool { - ThreadPool - } -} -``` - -由于咱们清楚一个负的线程数目不会有任何意义,因此咱们选择了 `usize` 作为那个 `size` 参数的类型。咱们还知道咱们将使用这个 `4` 作为线程集合中原始的个数,那即使这个 `usize` 类型的目的所在,正如第三章的 [整数类型](Ch03_Common_Programming_Concepts.md#整形) 小节中曾讨论过的。 - -下面来再次检查: - -```console -$ cargo check - Checking hello v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/hello) -error[E0599]: no method named `execute` found for struct `ThreadPool` in the current scope - --> src/main.rs:19:14 - | -19 | pool.execute(|| { - | ^^^^^^^ method not found in `ThreadPool` - -For more information about this error, try `rustc --explain E0599`. -error: could not compile `hello` due to previous error -``` - -现在的报错之所以出现,是因为在 `ThreadPool` 上咱们没有一个 `execute` 方法。回顾 ["创建有限数目的线程"](#创建有限数目的线程) 小节到,咱们已决定咱们的线程池,应有一个类似与 `thread::spawn` 的接口。此外,咱们将实现这个 `execute` 函数,如此其便会取那个给到他的闭包,并将其交给线程池中的某个空闲进程运行。 - -咱们将在 `ThreadPool` 上定义这个 `execute` 方法,来取一个闭包作为参数。回顾第 13 章中 [“将捕获值迁移出闭包与 `Fn` 特质”](Ch13_Functional_Language_Features_Iterators_and_Closures.md#将捕获到的值迁移出闭包与-fn-特质) 到咱们可以三种不同特质,将闭包取作参数:`Fn`、`FnMut` 与 `FnOnce`。咱们需要确定出这里要使用何种类别的闭包。咱们清楚咱们将以完成一些类似于标准库的 `thread::spawn` 实现类似的东西结束,因此咱们就可以看看 `thread::spawn` 的签名在其参数上有些什么。文档给出咱们下面的东西: - -```rust -pub fn spawn(f: F) -> JoinHandle - where - F: FnOnce() -> T, - F: Send + 'static, - T: Send + 'static, -``` - -其中的 `F` 类型参数,就是咱们在这里所关心的那个;那个 `T` 类型参数于返回值相关,而咱们并不关心那个。咱们可以看出,`spawn` 使用 `FnOnce` 作为 `F` 上的特质边界。由于咱们将最终将把咱们在 `execute` 中获得的实参,传递给 `spawn`,因此这或许也正是咱们想要的。由于为运行某个请求的线程,将只执行那个请求的闭包一次,而这是与 `FnOnce` 中的 `Once` 是相匹配的,故咱们可以进一步确信,`FnOnce` 便是咱们要用到的特质。 - -其中的 `F` 类型参数,还有着特质边界 `Send` 与生命周期边界 `'static`,在咱们这种情况下他们是有用的:咱们需要 `Send` 来将闭包,从一个线程转移到另一线程,并由于咱们不知道那个线程将耗时多久来执行,因此而需要 `'static`。下面咱们就来在 `ThreadPool` 上,创建出将取到有着这些边界的,类型 `F` 的泛型参数的 `execute` 方法: - -文件名:`src/lib.rs` - -```rust -#![allow(warnings)] -pub struct ThreadPool; - -impl ThreadPool { - pub fn execute(&self, f: F) - where - F: FnOnce() + Send + 'static, - { - } -} -``` - -由于这个 `FnOnce` 表示一个不会取参数,且返回的是单元类型 `()` 的闭包,因此咱们仍旧使用了 `FnOnce` 后的 `()`。就跟函数定义一样,返回值类型可以在签名中省略,但即使咱们没有参数,咱们仍需这对括号。 - -又一次,这仍是那个 `execute` 方法的极简实现:他什么也没做,但咱们只是在试着让咱们的代码编译。咱们再来对其加以检查: - -```console -$ cargo check - Checking hello v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/hello) - Finished dev [unoptimized + debuginfo] target(s) in 0.36s -``` - -他编译了!不过请注意,当咱们尝试 `cargo run` 并在浏览器中构造一次请求时,咱们将看到浏览器中,一些咱们在本章开头曾看到过的报错。咱们的库尚未真正调用传递给 `execute` 的那个闭包! - -> 注意:咱们或许听说过与有着严格编译器语言,比如 Haskell 与 Rust,有关的一种说法,即 “若代码编译了,他就会工作。” 然而这种说法并非一概而论。咱们的项目编译了,但他绝对什么也没干!若咱们是在构建一个真实、完整的项目,那么此时就将是开始编写检查代码编译与否,*以及* 是否具有咱们想要的行为的单元测试的好时机。 - -#### 在 `new` 中验证线程数目 - -**Validating the Number of Threads in `new`** - -咱们没有对 `new` 与 `execute` 的参数做任何事情。下面就来以咱们打算的行为,实现这两个函数的函数体。咱们来构思一下 `new`,作为开始。早先由于负的线程数目没有意义,因此咱们给那个 `size` 参数,选择了一个无符号整数类型。不过尽管零也是相当有效的 `usize`,但零个线程的线程池,同样是无意义的。咱们将在返回一个 `ThreadPool` 实例前,添加检查 `size` 大于零的代码,并在程序收到一个零时,通过使用 `assert!` 宏,让程序终止运行,如下面清单 20-13 中所示。 - -文件名:`src/lib.rs` - -```rust -impl ThreadPool { - /// 创建出一个新的 ThreadPool。 - /// - /// 其中的 size 为线程池中线程的数目。 - /// - /// # 终止运行 - /// - /// 这个 `new` 函数将在 size 为零时终止运行。 - pub fn new(size: usize) -> ThreadPool { - assert! (size > 0); - - ThreadPool - } - - // --跳过代码-- -} -``` - -*清单 20-13:将 `ThreadPool` 实现为在 `size` 为零时终止运行* - -咱们还以一些文档注释,doc comments,给咱们的 `ThreadPool` 结构体添加了一些文档。请注意咱们通过添加如同第 14 章中曾讨论过的,一个会呼出咱们的函数可能终止运行时的那些情形的小节,而遵循了良好的文档实践。请尝试运行 `cargo doc --open` 并点击那个 `ThreadPool` 结构体,来看到为 `new` 生成的文档看起来是怎样的! - -与其如咱们在这里所做的添加这个 `assert!` 宏,咱们则可把 `new` 改为 `build`,并像咱们曾在清单 12-9 中那个 I/O 项目里的 `Config::build` 下所做的那样,返回一个 `Result`。但咱们已经决定,在此示例中是在尝试创建一个,其中全部线程都不应是不可恢复错误的线程池。若你觉得信心满满,那就请编写一个名为 `build`,有着下面签名的函数,来与这个 `new` 函数相比较: - -```rust -pub fn build(size: usize) -> Result { -``` - -#### 创建空间来存储这些线程 - -**Creating Space to Store the Threads** - -既然咱们有了获悉咱们有着要在线程池中存储线程有效数目的一种办法了,咱们便可以创建出这些线程,并在返回这个 `ThreadPool` 结构体前,将他们存储在该结构体中。但是咱们要怎么 “存储” 一个线程呢?下面又来看看那个 `thread::spawn` 签名: - -```rust -pub fn spawn(f: F) -> JoinHandle - where - F: FnOnce() -> T, - F: Send + 'static, - T: Send + 'static, -``` - -`spawn` 函数返回了一个 `JoinHandle`,其中的 `T` 为闭包所返回的类型。咱们也来尝试使用 `JoinHandle`,并观察会发生什么。在咱们的用例中,咱们传递给线程池的闭包,将处理 TCP 连接,而不会返回任何东西,因此其中的 `T` 将是单元类型 `()`。 - -下面清单 20-14 中的代码将会编译,但尚不会创建任何线程。咱们已将 `ThreadPool` 的定义,修改为保存了一个 `thread::JoinHandle<()>` 实例的矢量值,以 `size` 大小初始化了这个矢量值,还建立了一个将运行某些代码来创建出那些线程的 `for` 循环,并返回了一个包含着这些线程的 `ThreadPool` 实例。 - -文件名:`src/lib.rs` - -```rust -use std::thread; - -pub struct ThreadPool { - threads: Vec>, -} - -impl ThreadPool { - // --跳过代码-- - pub fn new(size: usize) -> ThreadPool { - assert! (size > 0); - - let mut threads = Vec::with_capacity(size); - - for _ in 0..size { - // 创建出一些线程并将他们存储在那个矢量中 - } - - ThreadPool { threads } - } - // --跳过代码-- -} -``` - -*清单 20-14:为保存那些线程而给 `ThreadPool` 创建一个矢量值* - -由于咱们正使用 `thread::JoinHandle` 作为 `ThreadPool` 中那个矢量值条目的类型,因此咱们已将 `std::thread` 带入到这个库代码箱的作用域。 - -一旦收到有效的大小,咱们的 `ThreadPool` 就会创建出可保存 `size` 个条目的一个新矢量值。那个 `with_capacity` 函数,执行的是与 `Vec::new` 同意的任务,但有个重要的不同之处:他会预先分配那个矢量值中的空间。由于咱们清楚咱们需要在那个矢量值中存储 `size` 个元素,那么预先完成这种分配,相比使用在有元素插入时调整自身的 `Vec::new`,就会稍微更具效率。 - -在再度运行 `cargo check` 时,其应成功。 - -#### 负责将代码从 `ThreadPool` 发送给某个线程的 `Worker` 结构体 - -**A `Worker` Struct Responsible for Sending Code from the `ThreadPoll` to a Thread** - -在清单 20-14 中的那个 `for` 循环里,咱们留了一个有关线程创建过程的注释。这里,咱们将看看咱们具体要怎么创建出那些线程来。标准库提供了 `thread::spawn` 作为创建线程的一种方式,而 `thread::spawn` 则期望得到一些线程在其一创建出来,就应立即运行的代码。然而,在咱们的示例中,咱们打算创建出这些线程,并让他们 *等待,wait* 咱们稍后将要发送的那些代码。标准库的线程实现,没有包含任何实现那样做法的方式;咱们必须亲自实现他。 - -咱们将通过引入介于 `ThreadPool` 与那些线程之间,将对这种新行为加以管理的一种新数据结构,来实现这样的行为。咱们将把这种数据结构称作 `Worker`,在线程池实现中,这是个常见的术语。`Worker` 会拾取需要运行的代码,并在该 `Worker` 的线程中运行那些代码。设想某家饭馆中工作的人们:工人们会一直等待,直到有顾客点的菜单进来,而随后他们就负责接下这些菜单,并让顾客们满意。 - -在线程池中存储的,不再是 `JoinHandle<()>` 实例的矢量值,咱们将存储这个 `Worker` 结构体的实例。每个 `Worker` 都将存储一个单独的 `JoinHandler<()>` 实例。随后咱们将在 `Worker` 上实现一个,将取得要运行代码的闭包,并将其发送到已经运行着的线程去执行的方法。咱们还将给到每个 `Worker` 一个 `id`,如此咱们就可以在日志记录或调试时,区分出线程池中那些不同的 `Worker`。 - - -以下便是在咱们创建一个 `ThreadPool` 时,将发生的一个新过程。咱们将在以此方式建立起 `Worker` 结构体后,再实现把闭包发送给线程的那些代码: - -1. 定义出一个保存了一个 `id` 与一个 `JoinHandler<()>` 的 `Worker` 结构体; -2. 把 `ThreadPool` 修改为保存一个 `Worker` 实例构成的矢量值; -3. 定义出会取一个 `id` 数字,并返回保存着这个 `id`,以及带有所生成的有着一个空闭包的线程的一个 `Worker` 实例,这样一个 `Worker::new` 函数; -4. 在 `Thread::new` 中,会使用那个 `for` 循环的计数器,来生成一个 `id`、用那个 `id` 创建出一个新的 `Worker`,并将该 `Worker` 存储在那个矢量值中。 - - -若咱们准备挑战一下,那么请尝试在查看清单 20-15 中代码之前,自己实现这些修改。 - -准备好了吗?下面就是有着一种做出前面那些修改的一种方式的清单 20-15。 - -文件名:`src/lib.rs` - -```rust -use std::thread; - -pub struct ThreadPool { - workers: Vec, -} - -impl ThreadPool { - // --跳过代码-- - pub fn new(size: usize) -> ThreadPool { - assert! (size > 0); - - let mut threads = Vec::with_capacity(size); - - for _ in 0..size { - workers.push(Worker::new(id)); - } - - ThreadPool { workers } - } - // --跳过代码-- -} - -struct Worker { - id: usize, - thread: thread::JoinHandle<()>, -} - -impl Worker { - fn new(id: usize) -> Worker { - let thread = thread::spawn(|| {}); - - Worker { id, thread } - } -} -``` - -*清单 20-15:将 `ThreadPool` 修改为保存 `Worker` 实例而非直接保存线程* - -由于 `ThreadPool` 现在保存的是一些 `Worker` 实例而非 `JoinHandle<()>` 实例,因此咱们已将其上那个字段的名字,从 `threads` 修改为了 `workers`。咱们将那个 `for` 循环中的计数器,用作给 `Worker::new` 的参数,同时咱们将每个新的 `Worker`,存储在那个名为 `workers` 的矢量值中。 - -外部代码(就像 `src/main.rs` 中咱们的服务器),无需知悉 `ThreadPool` 里某个 `Worker` 结构体使用方面的实现细节,因此咱们是将这个 `Worker` 结构体及其 `new` 函数,构造为了私有。`Worker::new` 函数使用了咱们给他的那个 `id`,并将经由使用空闭包而生成一个新线程,而创建出的一个 `JoinHandle<()>` 实例存储起来。 - - -> 注意:若操作系统因没有足够系统资源而无法创建出一个线程,那么 `thread::spawn` 就将终止运行。那样的话,即使一些线程创建可能成功,也将导致咱们整个服务器终止运行。为简化起见,这种实现做法是无可厚非的,但在生产的线程池实现中,咱们就大概打算使用 [`std::thread::Builder`](https://doc.rust-lang.org/std/thread/struct.Builder.html) 与他的返回 `Result` 的 [`spawn`](https://doc.rust-lang.org/std/thread/struct.Builder.html#method.spawn) 方法了。 - -这段代码将编译,并将咱们指定给 `ThreadPool::new` 数目的 `Worker` 实例存储起来。但咱们 *仍* 未处理咱们在 `execute` 中得到的闭包。接下来就要看看怎样完成那一步。 - -#### 经由通道把请求发送给线程 - -**Sending Requests to Threads via Channels** - -接下来咱们将要解决的,便是所给到 `thread::spawn` 的那些闭包什么也没做的问题。当前,咱们在那个 `execute` 方法中,获取到了咱们打算执行的那个闭包。但咱们需要于那个 `ThreadPool` 创建期间,在咱们创建出各个 `Worker` 时,给到 `thread::spawn` 一个闭包。 - -咱们想要咱们刚创建出的那些 `Worker` 结构体,从一个保存在 `ThreadPool` 中的队列中获取要运行的代码,并把那些代码发送他的线程运行。 - -第 16 章中咱们学过的通道 -- 两个线程间通信的一种简单方式 -- 对于这个用例将是最佳的。咱们将使用一个函数的通道,作为作业队列,the queue of jobs,而 `execute` 将把来自 `ThreadPool` 的某项作业,发送到那些 `Worker` 实例,其将把该项作业,发送给他的线程。下面便是这个方案: - -1. `ThreadPool` 将创建出一个通道,并保存于 `sender` 上; -2. 每个 `Worker` 实例,将保存于 `receiver` 上; -3. 咱们将创建出将保存那些咱们打算下发到通道上闭包的一个新 `Job` 结构体; -4. `execute` 方法将经由那个 `sender`,发送其打算执行的作业; -5. 在 `Worker` 实例的线程中,其将遍历其 `receiver` 并执行其所接收到的任何作业的闭包。 - -咱们来通过在 `ThreadPool::new` 中创建一个通道,并在 `ThreadPool` 实例中保存 `send` 开始,如下清单 20-16 中所示。其中的 `Job` 结构体现在没有保存任何东西,但将保存咱们下发到通道项目类型。 - -文件名:`src/lib.rs` - -```rust -use std::{sync::mpsc, thread}; - -pub struct ThreadPool { - workers: Vec, - sender: mpsc::Sender, -} - -struct Job; - -impl ThreadPool { - // --跳过代码-- - pub fn new(size: usize) -> ThreadPool { - assert! (size > 0); - - let (sender, receiver) = mpsc::channel(); - - let mut workers = Vec::with_capacity(size); - - for id in 0..size { - workers.push(Worker::new(id)); - } - - ThreadPool { workers, sender } - } - - // --跳过代码-- -} -``` - -*清单 20-16:将 `ThreadPool` 修改为存储传递 `Job` 实例通道的 `sender`* - -在 `ThreadPool::new` 中,咱们创建出来咱们的新通道,并让线程池保存了该通道的 `sender`。这段代码将成功编译。 - -下面就来尝试在这个线程池创建出该通道时,把其 `receiver` 传入各个 `worker`。咱们清楚咱们是要在那些 `workers` 生成的线程中使用这个 `receiver`,因此咱们将在那个闭包中,引用这个 `receiver` 参数。下面清单 20-17 中的代码尚不会很好地编译。 - -文件名:`src/lib.rs` - -```rust -impl ThreadPool { - // --跳过代码-- - pub fn new(size: usize) -> ThreadPool { - assert! (size > 0); - - let (sender, receiver) = mpsc::channel(); - - let mut workers = Vec::with_capacity(size); - - for id in 0..size { - workers.push(Worker::new(id, receiver)); - } - - ThreadPool { workers, sender } - } - - // --跳过代码-- -} - -// --跳过代码-- -impl Worker { - fn new(id: usize, receiver: mpsc::Receiver) -> Worker { - let thread = thread::spawn(|| { - receiver; - }); - - Worker { id, thread } - } -} -``` - -*清单 20-17:将 `receiver` 传递给 `workers`* - -咱们作出了一些小而简单直接的修改:咱们把那个 `receiver` 传入到 `Worker::new`,并随后在那个闭包里使用了他。 - -当咱们尝试检查这段代码时,就会得到如下报错: - -```console -$ cargo check - Checking hello v0.1.0 (/home/peng/rust-lang-zh_CN/hello) -error[E0382]: use of moved value: `receiver` - --> src/lib.rs:27:42 - | -22 | let (sender, receiver) = mpsc::channel(); - | -------- move occurs because `receiver` has type `std::sync::mpsc::Receiver`, which does not implement the `Copy` trait -... -27 | workers.push(Worker::new(id, receiver)); - | ^^^^^^^^ value moved here, in previous iteration of loop - -For more information about this error, try `rustc --explain E0382`. -error: could not compile `hello` due to previous error -``` - -这段代码试图将 `receiver` 传递给多个 `Worker` 实例。正如回顾到第 16 章,这样做不会工作:Rust 所提供的通道实现,属于多 `producer`、单 `consumer` 的。这意味着咱们不能只克隆通道的消费端来修复这段代码。咱们也不打算将一条消息,多次发送给多个消费者;咱们是要一个带有多个 `worker` 的消息列表,如此每条消息,都将被一次性处理。 - -此外,从通道队列里取出一项作业,还涉及到令 `receiver` 可变,因此这些县城就需要一种共用与修改 `receiver` 的安全方式;否则,咱们就会面临竞争情形(如同第 16 章中所讲到的)。 - -回顾第 16 章中曾讨论过的线程安全灵巧指针:为在多个线程间共用所有权,以及实现这些线程修改值,咱们需要用到 `Arc>`。`Arc` 类型将实现多个 `worker` 都拥有那个 `receiver`,而 `Mutex` 将确保某个时刻只有一个 `worker` 从 `receiver` 获取一项作业。下面清单 20-18 给出了咱们需要作出的修改。 - -文件名:`src/lib.rs` - -```rust -use std::{ - sync::{mpsc, Arc, Mutex}, - thread, -}; -// --跳过代码-- - -impl ThreadPool { - // --跳过代码-- - pub fn new(size: usize) -> ThreadPool { - assert! (size > 0); - - let (sender, receiver) = mpsc::channel(); - - let receiver = Arc::new(Mutex::new(receiver)); - - let mut workers = Vec::with_capacity(size); - - for id in 0..size { - workers.push(Worker::new(id, Arc::clone(&receiver))); - } - - ThreadPool { workers, sender } - } - - // --跳过代码-- -} - -// --跳过代码-- - -impl Worker { - fn new(id: usize, receiver: Arc>>) -> Worker { - // --跳过代码-- - } -} -``` - -*清单 20-18:运用 `Arc` 与 `Mutex` 在那些 `worker` 间共用 `receiver`* - -在 `ThreadPool::new` 中,咱们把 `receiver` 放入到一个 `Arc` 与一个 `Mutex` 中。对于各个新 `worker`,咱们克隆了那个 `Arc`,从而增加了引用计数,这样那些 `worker` 就可以共用 `receiver` 的所有权。 - -有了这些修改,代码就会编译了!咱们就要达到目的了! - - -#### 实现 `execute` 方法 - -**Implementing the `execute` method** - -咱们来最终实现那个 `ThreadPool` 上的 `execute` 方法。咱们还将把 `Job` 从结构体,修改为保存着 `execute` 接收到闭包类型的特质对象的类型别名。正如第 19 章 [“使用类型别名创建类型义词”](Ch19_Advanced_Features.md#使用类型别名创建类型同义词) 小节中曾讨论过的,类型别名实现了为易于使用而将长类型构造缩短。请看看下面清单 20-19. - -文件名:`src/lib.rs` - -```rust -// --跳过代码-- - -type Job = Box; - -impl ThreadPool { - // --跳过代码-- - - pub fn execute(&self, f: F) - where - F: FnOnce() + Send + 'static, - { - let job = Box::new(f); - - self.sender.send(job).unwrap(); - } -} -// --跳过代码-- -``` - -*清单 20-19:为保存着各个闭包的 `Box` 创建出 `Job` 类型别名,并于随后把作业下发到通道* - -使用咱们在 `execute` 中得到的闭包创建出一个新的 `Job` 实例后,咱们便把那项作业下发到通道的发送端。对于发送失败的情形,咱们调用了 `send` 上的 `unwrap` 方法。在比如咱们停止全部线程执行,即表示接收端已停止接收新消息时,发送失败就可能发生。在那个时刻,咱们是无法停止咱们的线程执行的:只要这个线程池存在,咱们的线程就会继续执行。咱们使用 `unwrap` 的原因,就是咱们清楚这样的失败情况不会发生,但编译器是不了解这点的。 - -但咱们还没有大功告成!在 `worker` 里,传递给 `thread::spawn` 的闭包,仍然只 *引用* 了通道的接收端。相反,咱们需要闭包一直循环,向通道接收端请求一项作业,并在其获取到一项作业时运行该项作业。下面咱们就来完成下面清单 20-20 中所给出的对 `Worker::new` 的修改。 - -文件名:`src/lib.rs` - -```rust -// --跳过代码-- - -impl Worker { - fn new(id: usize, receiver: Arc>>) -> Worker { - let thread = thread::spawn(move || loop { - let job = receiver.lock().unwrap().recv().unwrap(); - - println! ("Worker {id} 获取到一项作业;执行中。"); - - job(); - }); - - Worker { id, thread } - } -} -``` - -*清单 20-20:在 `worker` 的线程中接收并执行作业* - -这里,咱们首选调用了 `receiver` 上的 `lock` 来请求那个互斥量,mutex,并于随后调用 `unwrap` 来在出现任何错误时终止运行。在互斥量处于 *中毒,poisoned* 状态时,请求锁就会失败,在有别的某个线程终止运行的同时,持有着而没有释放该锁时,这种情况便会发生。在这种情况下,调用 `unrap` 来让这个线程终止运行,便是要采取的正确措施。请放心地把这个 `unwrap`,修改为一个带有对咱们有意义报错信息的 `expect`。 - -当咱们获得了那个互斥量上的锁时,咱们就会调用 `recv` 来从通道接收一个 `Job`。最后的 `unwrap` 也会带过这里的任何错误,在持有 `sender` 的线程已关闭时就会发生这些错误,就跟 `receiver` 关闭时那个 `send` 方法会返回 `Err` 类似。 - -到 `recv` 的调用会阻塞,因此在尚无作业时,当前线程将等待,直到有某项作业可用。`Mutex` 确保了一次只有一个 `Worker` 线程是在尝试请求作业。 - -咱们的线程池现在就处于工作状态了!给他一次 `cargo run` 并构造一些请求: - - -```console -$ cargo run - Compiling hello v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/hello) -warning: field `workers` is never read - --> src/lib.rs:7:5 - | -6 | pub struct ThreadPool { - | ---------- field in this struct -7 | workers: Vec, - | ^^^^^^^ - | - = note: `#[warn(dead_code)]` on by default - -warning: fields `id` and `thread` are never read - --> src/lib.rs:48:5 - | -47 | struct Worker { - | ------ fields in this struct -48 | id: usize, - | ^^ -49 | thread: thread::JoinHandle<()>, - | ^^^^^^ - -warning: `hello` (lib) generated 2 warnings - Finished dev [unoptimized + debuginfo] target(s) in 0.60s - Running `target/debug/hello` -Worker 1 获取到一项作业;执行中。 -Worker 0 获取到一项作业;执行中。 -Worker 2 获取到一项作业;执行中。 -Worker 3 获取到一项作业;执行中。 -Worker 1 获取到一项作业;执行中。 -Worker 0 获取到一项作业;执行中。 -``` - -成功了!咱们现在有了一个会异步执行 TCP 连接的线程池。绝不会有超过四个线程被创建出来,因此在服务器收到很多请求时,咱们的系统将不会过载。在咱们构造了一个到 `/sleep` 的请求时,服务器通过让另一线程运行别的一些请求,而将能服务这些请求。 - -> 注意:若咱们在多个窗口同时打开 `/sleep`,他们可能会在设置的时间间隔每次加载一个。有些 web 浏览器会出于缓存原因,而顺序执行同一请求的多个实例。这样的局限并不是由咱们的服务器导致的。 - -在了解了第 18 章中的 `while let` 循环后,咱们可能想知道,为何咱们没有如下清单 20-21 中所示的那样,编写 `worker` 线程的代码。 - -文件名:`src/lib.rs` - -```rust -impl Worker { - fn new(id: usize, receiver: Arc>>) -> Worker { - let thread = thread::spawn(move || { - while let Ok(job) = receiver.lock().unwrap().recv() { - println! ("Worker {id} 获取到一项作业;执行中。"); - - job(); - } - }); - - Worker { id, thread } - } -} -``` - -*清单 20-21:使用 `while let` 的一种 `Worker::new` 替代实现* - -这段代码将会编译及运行,但不会产生所需的线程行为:慢速请求仍将导致别的请求等待被处理。至于原因则有点微妙:由于锁的所有权是基于 `lock` 方法返回的 `LockResult>` 中,`MutexGuard` 的生命周期,因此这个 `Mutex` 结构体没有公开的 `unlock` 方法。在编译时,借用检查器可于随后,就除非咱们拿着 `Mutex` 所守卫的某项资源的锁,否则无法访问该项资源这一规则强制加以检查。但是,若咱们没有注意到 `MutexGuard` 的生命周期,那么这样的实现同样能导致锁相较预期被占用更长时间。 - -由于在 `let` 之下,等号右侧的表达式中用到的任何临时值,都会在 `let` 语句结束时被立即丢弃,因此使用了 `let job = receiver.lock().unwrap().recv().unwrap();` 的清单 20-20 中代码是工作的。但是,`while let`(以及 `if let` 与 `match`) 则是在相关代码块结束前,不会丢弃那些临时值。在清单 20-21 中,锁会在到 `job()` 的调用其将保持被持有,这意味着别的 `worker` 就没法收到作业。 - - -## 优雅有序关闭与清理 - -**Graceful Shutdown and Cleanup** - -清单 20-20 中的代码,经由线程池而如咱们所设想的那样,异步响应请求。咱们会收到有关 `workers`、`id` 及 `thread` 这三个,咱们未以直接方式用到字段的一些告警,这些告警就提醒了咱们,咱们没有清理任何东西。当咱们使用不那么优雅的 `Ctrl + c` 方式,来挂起主线程时,全部其他线程也会被立即停止,即使他们处于服务某个请求中。 - -接下来,咱们随后将实现 `Drop` 特质,以在线程池中各个线程上调用 `join`,如此这些线程便可以在关闭前,完成他们正工作于其上的请求。随后咱们将实现一种告知线程他们应停止接受新请求并关闭的方法。为观察这方面代码的运作,咱们将把咱们的服务器,修改为在有序关闭其线程池之前,只接受两个请求。 - - -### 实现 `ThreadPool` 上的 `Drop` 特质 - -**Implementing the `Drop` Trait on `ThreadPool`** - -咱们来以在咱们的线程池上实现 `Drop` 开始。当线程池被丢弃时,咱们的那些线程就都应归拢,join 一下,以确保他们完成他们的工作。下面清单 20-22 给出了 `Drop` 实现的首次尝试;此代码尚不会很好地编译。 - -文件名:`src/lib.rs` - -```rust -impl Drop for ThreadPool { - fn drop(&mut self) { - for worker in &mut self.workers { - println! ("关闭 worker {}", worker.id); - - worker.thread.join().unwrap(); - } - } -} -``` - -*清单 20-22:在线程池超出作用域时归拢各个线程* - -> 注:关于线程的 `join` 方法,请参考 [Java Thread.join详解](https://zhuanlan.zhihu.com/p/57927767),[Joining Threads in Java](https://www.geeksforgeeks.org/joining-threads-in-java/)。 - -首选,咱们遍历了线程池中 `workers` 的各个线程。由于 `self` 是个可变引用,且咱们还需要能修改 `worker`,因此咱们为这个遍历使用了 `&mut`。对于各个 `worker`,咱们打印了讲到这个特定 `worker` 正要关闭的一条消息,并在随后在那个 `worker` 的线程上调用了 `join`。当到 `join` 的这个调用失败时,咱们便使用 `unwrap` 来令到 Rust 终止运行,并进入到非优雅有序关闭。 - -下面时在咱们编译这代码时,得到的报错信息: - -```console -$ cargo check - Checking hello v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/hello) -error[E0507]: cannot move out of `worker.thread` which is behind a mutable reference - --> src/lib.rs:71:13 - | -71 | worker.thread.join().unwrap(); - | ^^^^^^^^^^^^^ ------ `worker.thread` moved due to this method call - | | - | move occurs because `worker.thread` has type `JoinHandle<()>`, which does not implement the `Copy` trait - | -note: `JoinHandle::::join` takes ownership of the receiver `self`, which moves `worker.thread` - --> /rustc/2c8cc343237b8f7d5a3c3703e3a87f2eb2c54a74/library/std/src/thread/mod.rs:1589:17 - -For more information about this error, try `rustc --explain E0507`. -error: could not compile `hello` due to previous error -``` - -这个报错告诉我们,由于咱们只有各个 `worker` 的可变借用,而 `join` 会取得其参数的所有权,因此咱们无法调用 `join`。为解决这个额外难题,咱们就需要将线程从拥有 `thread` 的 `Worker` 实例迁出,如此 `join` 就可以消费那个线程了。咱们曾在清单 17-15 中这样做过:若 `Worker` 保存的是一个 `Option>`,那么咱们就可以在 `Option` 上调用 `take` 方法,来将 `Some` 变种中的那个值迁出,并在其位置处留下一个 `None`。也就是说,正运行的一个 `Worker`,将有着 `thread` 中的一个 `Some` 变种,而当咱们打算清理某个 `Worker` 时,咱们就将以 `None` 来替换 `Some`,如此那个 `Worker` 就没有了要运行的线程了。 - -因此咱们就明白了咱们是要如下更新 `Worker` 的定义: - -文件名:`src/lib.rs` - -```rust -struct Worker { - id: usize, - thread: Option>, -} -``` - -现在咱们来依靠编译器,找出其他需要修改的地方。对此代码进行检查,咱们会得到两个报错: - -```console -$ cargo check - Checking hello v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/hello) -error[E0308]: mismatched types - --> src/lib.rs:62:22 - | -62 | Worker { id, thread } - | ^^^^^^ expected enum `Option`, found struct `JoinHandle` - | - = note: expected enum `Option>` - found struct `JoinHandle<_>` -help: try wrapping the expression in `Some` - | -62 | Worker { id, thread: Some(thread) } - | +++++++++++++ + - -error[E0599]: no method named `join` found for enum `Option` in the current scope - --> src/lib.rs:71:27 - | -71 | worker.thread.join().unwrap(); - | ^^^^ method not found in `Option>` - | -note: the method `join` exists on the type `JoinHandle<()>` - --> /rustc/2c8cc343237b8f7d5a3c3703e3a87f2eb2c54a74/library/std/src/thread/mod.rs:1589:5 -help: consider using `Option::expect` to unwrap the `JoinHandle<()>` value, panicking if the value is an `Option::None` - | -71 | worker.thread.expect("REASON").join().unwrap(); - | +++++++++++++++++ - -Some errors have detailed explanations: E0308, E0599. -For more information about an error, try `rustc --explain E0308`. -error: could not compile `hello` due to 2 previous errors -``` - -咱们来解决那第二个错误,其指向了 `Worker::new` 末尾的代码;在创建新 `Worker` 时,咱们需要把那个 `thread` 值封装在 `Some` 中。请做出如下修改来修复这个错误: - -文件名:`src/lib.rs` - -```rust -impl Worker { - fn new(id: usize, receiver: Arc>>) -> Worker { - // --跳过代码-- - - Worker { - id, - thread: Some(thread), - } - } -} -``` - -第一个错误是在咱们的 `Drop` 实现中,早先咱们曾提到,咱们原本打算调用这个 `Option` 值上的 `take`,来将 `thread` 从 `worker` 中迁出。下面的修改就将这样做: - -文件名:`src/lib.rs` - -```rust -impl Drop for ThreadPool { - fn drop(&mut self) { - for worker in &mut self.workers { - println! ("关闭 worker {}", worker.id); - - if let Some(thread) = worker.thread.take() { - thread.join().unwrap(); - } - } - } -} -``` - -正如曾在第 17 章中讨论过的那样,`Option` 上的 `take` 方法,会将那个 `Some` 变种取出,并在其位置处留下 `None`。咱们使用了 `if let` 来解构那个 `Some` 而得到了那个线程;随后咱们在线程上调用了 `join`。若某个 `worker` 的线程已经是 `None`,那么咱们就知道那个 `worker` 已经让他的线程清理掉了,因此在那种情况下什么也不会发生。 - - -### 通知线程停止收听作业 - -**Signaling to the Threads to Stop Listening for Jobs** - -在咱们已做出的全部修改下,咱们的代码会不带任何错误的编译了。但是,坏消息是这些代码尚不会按照咱们想要的方式运作。问题关键在于,由 `Worker` 实例的线程运行的闭包中的逻辑:此刻,咱们调用了 `join`,但由于线程是在一直 `loop` 查找作业,所以那样做将不会关闭线程。若咱们以咱们当前的 `drop` 实现丢弃咱们的 `ThreadPool`,那么主线程将一直阻塞于等待第一个线程结束。 - -为修复这个问题,咱们将需要 `ThreadPool` 的 `drop` 实现中的一个修改,以及其后的 `Worker` 循环中的一个修改。 - -首选,咱们将把 `ThreadPool` 的 `drop` 实现,修改为在等待线程结束前显式地丢弃 `sender`。下面清单 20-23 给出了对 `ThreadPool` 显示丢弃 `sender` 的修改。为能将 `send` 从 `ThreadPool` 迁出,咱们使用了与咱们曾对线程做过的同样 `Option` 于 `take` 技巧: - -文件名:`src/lib.rs` - -```rust -pub struct ThreadPool { - workers: Vec, - sender: Option>, -} -// --跳过代码-- -impl ThreadPool { - pub fn new(size: usize) -> ThreadPool { - // --跳过代码-- - - ThreadPool { - workers, - sender: Some, - } - } - - pub fn execute(&self, f: F) - where - F: FnOnce() + Send + 'static, - { - let job = Box::new(f); - - self.sender.as_ref().unwrap().send(job).unwrap(); - } -} - -impl Drop for ThreadPool { - fn drop(&mut self) { - drop(self.sender.take()); - - for worker in &mut self.workers { - println! ("关闭 worker {}", worker.id); - - if let Some(thread) = worker.thread.take() { - thread.join().unwrap(); - } - } - } -} -``` - -*清单 20-23:在归拢那些 `worker` 线程前显式丢弃 `sender`* - -丢弃 `sender` 就会关闭通道,这表明将不会有更多消息发出。当那发生时,在无限循环中那些 `worker` 所做的到 `recv` 的全部全部调用,就会返回错误。在下面清单 20-24 中,咱们修改了 `Worker` 的循环,来在那种情况下优雅有序地退出循环,这就意味着在 `ThreadPool` 的 `drop` 实现在那些线程上调用 `join` 时,他们将结束。 - - -文件名:`src/lib.rs` - -```rust -impl Worker { - fn new(id: usize, receiver: Arc>>) -> Worker { - let thread = thread::spawn(move || loop { - let message = receiver.lock().unwrap().recv(); - - match message { - Ok(job) => { - println! ("Worker {id} 获取到一项作业;执行中。"); - - job(); - } - Err(_) => { - println! ("Worker {id} 已断开链接;关闭中。"); - break; - } - } - }); - - Worker { - id, - thread: Some(thread), - } - } -} -``` - -*清单 20-24:在 `recv` 返回错误时显式跳出循环* - -要看到运作中的代码,咱们就来把 `main` 修改为在有序关闭服务器钱,只接收两个请求,如下清单 20-25 中所示。 - -文件名:`src/main.rs` - -```rust -fn main() { - let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); - let pool = ThreadPool::new(4); - - for stream in listener.incoming().take(2) { - let stream = stream.unwrap(); - - pool.execute(|| { - handle_conn(stream); - }); - } - - println! ("关闭中。"); -} -``` - -*清单 20-25: 在服务两个请求后通过退出循环关闭服务器* - -咱们是不会想要真实世界的 web 服务器在仅服务两个请求后就关闭的。这段代码只演示了这种有序关闭与清理是在正常工作。 - -其中的 `take` 方法,是定义在 `Iterator` 特质中的,且将迭代限制到最多头两个项目。在 `main` 的结束处,`ThreadPool` 将超出作用域,而 `drop` 实现将运行。 - -请以 `cargo run` 启动服务器,并构造三个请求。第三个请求应会出错,而在终端里咱们应看到类似于下面这样的输出: - -```console -$ cargo run 16s - Finished dev [unoptimized + debuginfo] target(s) in 0.00s - Running `target/debug/hello` -Worker 0 获取到一项作业;执行中。 -关闭中。 -关闭 worker 0 -Worker 1 获取到一项作业;执行中。 -Worker 3 已断开链接;关闭中。 -Worker 2 已断开链接;关闭中。 -Worker 1 已断开链接;关闭中。 -Worker 0 已断开链接;关闭中。 -关闭 worker 1 -关闭 worker 2 -关闭 worker 3 -``` - -咱们可能会看到不同顺序的 `worker` 与消息打印出来。咱们能从这些消息,看出代码是如何工作的:`worker` `1` 与 `2` 获取到了头两个请求。服务器在第二个 TCP 连接之后,便停止了接收连接,而 `ThreadPool` 上的 `Drop` 实现,在 `worker` `2` 还没开始其作业前,便开始了执行。丢弃 `sender` 会断开全部 `worker` 并告诉他们要关闭。那些 `worker` 在他们断开连接时,都各自打印了一条消息,而随后线程池便调用了 `join` 来等待各个 `worker` 线程结束。 - -请注意这次特定执行的一个有趣方面:`ThreadPool` 弃用了 `sender`,而在有任何 `worker` 接收到错误前,咱们就尝试归拢了 `worker` `0`。`worker` `0` 还不曾从 `recv` 获取到一个错误,因此主线程就阻塞于等待 `worker` `0` 结束。与此同时,`worker` `1` 收到了一项作业,而随后全部线程都收到了错误。在 `worker` `0` 结束时,主线程就等待其余 `worker` 结束。而在那个时候,他们都已退出了他们的循环并停止了。 - -恭喜!咱们先进已经完成了咱们的项目;咱们有了一个运用线程池来异步响应的基本 web 服务器。咱们能够完成服务器有序关闭,这会清理掉线程池中的全部线程。 - -以下是用于参考的全部代码: - -文件名:`src/main.rs` - -```rust -use hello::ThreadPool; - -use std::{ - fs, - thread, - io::{prelude::*, BufReader}, - net::{TcpListener, TcpStream}, - time::Duration, -}; - -fn main() { - let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); - let pool = ThreadPool::new(4); - - for stream in listener.incoming().take(2) { - let stream = stream.unwrap(); - - pool.execute(|| { - handle_conn(stream); - }); - } - - println! ("关闭中。"); -} - -fn handle_conn(mut stream: TcpStream) { - let buf_reader = BufReader::new(&mut stream); - let req_line = buf_reader.lines().next().unwrap().unwrap(); - - let (status_line, filename) = match &req_line[..] { - "GET / HTTP/1.1" => ( "HTTP/1.1 200 OK", "hello.html"), - "GET /sleep HTTP/1.1" => { - thread::sleep(Duration::from_secs(10)); - ("HTTP/1.1 200 0K", "hello.html") - } - _ => ("HTTP/1.1 404 NOT FOUND", "404.html"), - }; - - let contents = fs::read_to_string(filename).unwrap(); - let length = contents.len(); - - let resp = - format! ("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); - - stream.write_all(resp.as_bytes()).unwrap(); -} -``` - - -文件名:`src/lib.rs` - -```rust -use std::{ - sync::{mpsc, Arc, Mutex}, - thread, -}; - -pub struct ThreadPool { - workers: Vec, - sender: Option>, -} - -type Job = Box; - -impl ThreadPool { - /// 创建出一个新的 ThreadPool。 - /// - /// 其中的 size 为线程池中线程的数目。 - /// - /// # 终止运行 - /// - /// 这个 `new` 函数将在 size 为零时终止运行。 - pub fn new(size: usize) -> ThreadPool { - assert! (size > 0); - - let (sender, receiver) = mpsc::channel(); - - let receiver = Arc::new(Mutex::new(receiver)); - - let mut workers = Vec::with_capacity(size); - - for id in 0..size { - workers.push(Worker::new(id, Arc::clone(&receiver))); - } - - ThreadPool { - workers, - sender: Some(sender), - } - } - - pub fn execute(&self, f: F) - where - F: FnOnce() + Send + 'static, - { - let job = Box::new(f); - - self.sender.as_ref().unwrap().send(job).unwrap(); - } -} - -impl Drop for ThreadPool { - fn drop(&mut self) { - drop(self.sender.take()); - - for worker in &mut self.workers { - println! ("关闭 worker {}", worker.id); - - if let Some(thread) = worker.thread.take() { - thread.join().unwrap(); - } - } - } -} - -struct Worker { - id: usize, - thread: Option>, -} - -impl Worker { - fn new(id: usize, receiver: Arc>>) -> Worker { - let thread = thread::spawn(move || loop { - let message = receiver.lock().unwrap().recv(); - - match message { - Ok(job) => { - println! ("Worker {id} 获取到一项作业;执行中。"); - - job(); - } - Err(_) => { - println! ("Worker {id} 已断开链接;关闭中。"); - break; - } - } - }); - - Worker { - id, - thread: Some(thread), - } - } -} -``` - -这里咱们可以做更多事情!若咱们打算继续加强这个项目,下面是一些想法: - -- 给 `ThreadPool` 及其公开方法添加更多文档; -- 给这个库的功能添加测试; -- 把一些调用修改为 `unwrap`,以获得更多的错误处理鲁棒性; -- 运用 `ThreadPool` 来完成除服务 web 请求外的一些别的任务; -- 在 [crates.io](https://crates.io/) 上找到某个线程池代码箱,并用该代码箱实现一个类似的 web 服务器。随后将其 API 及鲁棒性,与咱们实现的线程池相比较。 - - -## 本章小结 - -干得好!咱们已经读完了这整本书!要感谢咱们加入到这次 Rust 之旅中来。咱们现在已经准备好实现咱们自己的 Rust 项目,以及帮助其他人的项目了。请记住有那么一个由热衷于就咱们在 Rust 道路上,所遇到的任何挑战,而帮助咱们的其他 Rust 公民的热情社区。 diff --git a/src/Ch21_Appendix.md b/src/Ch21_Appendix.md index fd0c2a5..e797052 100644 --- a/src/Ch21_Appendix.md +++ b/src/Ch21_Appendix.md @@ -1,951 +1,3 @@ # 附录 以下小节包含了在咱们的 Rust 路途中,会发现有用的一些参考资料。 - - -## 附录 A:关键字 - -以下清单包含了 Rust 语言当前或今后要用到的一些关键字。由此,他们便不能被用作标识符(除在 [“原始标识符”](#原始标识符) 小节中咱们将讨论的那些外)了。所谓标识符,是函数、变量、参数、结构体字段、模组、代码箱、常量、宏、静态值、属性、类型、特质或生命周期等的名字。 - - -### 当前在用的关键字 - -下面是当前在用关键字的清单,带有其作用描述。 - -- `as` - 执行原生强制转换,primitive casting,消除包含着某个项目的特定特质歧义,disambiguate the specific trait containing a item,或重命名 `use` 语句中的项目; -- `async` - 返回一个 `Future` 类型值,而非阻塞当前线程; -- `await` - 在某个 `Future` 值的结果准备好前,暂停程序执行; -- `break` - 立即退出某个循环; -- `const` - 定义出常量项目或常量原始指针; -- `continue` - 继续下一循环迭代; -- `crate` - 在模组路径中,指向代码箱根; -- `dyn` - 动态调遣到某个特质对象,参考 [特质对象执行动态调遣](Ch17_Object_Oriented_Programming_Features_of_Rust.md#特质对象执行动态调遣); -- `else` - `if` 的回退,及 `if let` 控制流的构件; -- `extern` - 链接外部函数或变量; -- `false` - 布尔值假的字面值; -- `fn` - 定义出某个函数或函数指针类型; -- `for` - 对某个迭代器的项目加以迭代、实现某个特质,或指明某个更高级别的生命周期,a higher-ranked lifetime; -- `if` - 基于某个条件表达式结果的分支; -- `impl` - 实现固有或特质功能,implement inherent or trait functionality; -- `in` - `for` 循环语法的一部分; -- `let` - 绑定某个变量; -- `loop` - 无条件地循环; -- `match` - 将某个值与模式匹配; -- `mod` - 定义出模组; -- `move` - 领导闭包取得其所有捕获值的所有权; -- `mut` - 注解出引用、原始指针或模式绑定等中的可变性; -- `pub` - 注解出结构体、`impl` 代码块或模组等中的公开可见性; -- `ref` - 按引用绑定; -- `return` - 自函数返回值; -- `Self` - 咱们正定义或实现中类型的类型别名; -- `self` - 方法主体,method subject,或当前模组; -- `static` - 在整个程序执行过程持续有效的全局变量或生命周期; -- `struct` - 定义出某个结构体; -- `super` - 当前模组的父模组; -- `trait` - 定义出某个特质; -- `true` - 布尔值真的字面值; -- `type` - 定义出某个类型别名或关联类型; -- `union` - 定义出某个 [联合体](https://doc.rust-lang.org/reference/items/unions.html),是在联合体声明时用到的唯一关键字; -- `unsafe` - 注解非安全代码、函数、特质或一些实现; -- `use` - 将符号带入到作用域; -- `where` - 注解约束某个类型的子句; -- `while` - 基于某个表达式结果而有条件的循环。 - -### 为今后使用保留的关键字 - -以下关键字尚无任何功能,但被 Rust 为今后的潜在使用而保留。 - -- `abstract` -- `become` -- `box` -- `do` -- `final` -- `macro` -- `override` -- `priv` -- `try` -- `typeof` -- `unsized` -- `virtual` -- `yield` - -### 原始标识符 - -*原始标识符,raw identifiers* 属于允许实现使用一般不被允许关键字的语法。是通过在关键字前加上前缀 `r#`,使用原始标识符的。 - -比如,`match` 是个关键字。在咱们尝试编译下面这个使用 `match` 作其名字的函数时: - -文件名:`src/main.rs` - -```rust -fn match(needle: &str, haystack: &str) -> bool { - haystack.contains(needle) -} -``` - -咱们将得到这样的报错: - -```console -error: expected identifier, found keyword `match` - --> src/main.rs:1:4 - | -1 | fn match(needle: &str, haystack: &str) -> bool { - | ^^^^^ expected identifier, found keyword -``` - -该报错显示咱们无法将关键字 `match` 用作函数标识符。要将 `match` 用作函数名字,咱们就需要使用原始标识符语法,像下面这样: - -文件名:`src/main.rs` - -```rust -fn r#match(needle: &str, haystack: &str) -> bool { - haystack.contains(needle) -} - -fn main() { - assert! (r#match("foo", "foobar")); -} -``` - -此代码将不带任何错误地编译。请注意那个函数的定义中,与 `main` 中该函数被调用处其名字上的 `r#` 前缀。 - -原始标识符实现了将任何咱们所选的词语用作标识符,即使那个词语碰巧是个保留的关键字。这给到咱们更自由地选择标识符名字,以及实现与一些以其中这些词语不属于关键字的语言,所编写的程序集成。此外,原始标识符实现了,对那些以不同于咱们代码箱 Rust 版本编写库加以运用。比如,在 2015 版中 `try` 就不是个关键字,但在 2018 版本中却是。若咱们依赖于一个使用 2015 版本编写的库,而该库有一个 `try` 函数,那么咱们就将需要在这种情况下,使用原始标识符 `r#try`,来从咱们的 2018 版本的代码,调用那个函数。请参阅 [附录 E](#appendix-e) 了解更多有关版本的信息。 - - -## 附录 B:运算符与符号 - -此附录包含了 Rust 语法的词汇表,包括运算符及别的一些,自己单独出现或出现于路径、泛型、特质边界、宏、属性、注释、元组及方括符等上下文中的符号。 - -### 运算符 - -表 B-1 包含了 Rust 中的符号、该符号将如何出现于上下文中的一个示例、简单的解释,以及该运算符是否可过载。若某个运算符可以过载,就会列出过载那个运算符要用到的相关特质。 - -**表 B-1:运算符** - -| 运算符 | 示例 | 说明 | 是否可以过载 | -| :--- | :--- | :--- | :--- | -| `!` | `ident! (...)`
`ident! {...}`
`ident! [...]` | 宏扩展 | | -| `!` | `!expr` | 按位或逻辑求补运算 | 否 | -| `!=` | `expr != expr` | 不等比较 | `PartialEq` | -| `%` | `expr % expr` | 算术求余运算 | `Rem` | -| `%=` | `var %= expr` | 算术求余并赋值 | `RemAssign` | -| `&` | `&expr`, `&mut expr` | 借用 | | -| `&` | `&type`, `&mut type`, `&'a type`, `&'a mut type` | 借用指针类型 | | -| `&` | `expr & expr` | 按位与(AND)运算 | `BitAnd` | -| `&=` | `var &= expr` | 按位与(AND)运算并赋值 | `BitAndAssign` | -| `&&` | `expr && expr` | 短路逻辑与(AND)运算,short-circuit logical AND | | -| `*` | `expr * expr` | 算术乘法运算 | `Mul` | -| `*=` | `var *= expr` | 算术乘法运算并赋值 | `MulAssign` | -| `*` | `*expr` | 解引用运算 | `Deref` | -| `*` | `*const type`, `*mut type` | 原始指针运算 | | -| `+` | `trait + trait`, `'a + trait` | 复合类型约束运算 | | -| `+` | `expr + expr` | 算术加法运算 | `Add` | -| `+=` | `var += expr` | 算术加法运算并赋值 | `AddAssign` | -| `,` | `expr, expr` | 参数与元素分隔符 | | -| `-` | `- expr` | 算术取反运算 | `Neg` | -| `-` | `expr - expr` | 算术减法运算 | `Sub` | -| `-=` | `var -= expr` | 算术减法运算并赋值 | `SubAssign` | -| `->` | `fn(...) -> type`, |...| -> type | 函数与闭包的返回值类型 | | -| `.` | `expr.ident` | 成员访问 | | -| `..` | `..`, `expr..`, `..expr`, `expr..expr` | 排除右侧的范围语法字面值 | `PartialOrd` | -| `..=` | `..=expr`, `expr..=expr` | 包含右侧范围语法字面值 | `PartialOrd` | -| `..` | `..expr` | 结构体更新语法 | | -| `..` | `variant(x, ..)`, `struct_type { x, .. }` | “等等” 模式绑定,"And the rest" pattern binding | | -| `...` | `expr...expr` | (已弃用,请使用 `..=` 代替)在模式中:包含式范围模式 | | -| `/` | `expr / expr` | 算术除法运算 | `Div` | -| `/=` | `var /= expr` | 算术除法并赋值 | `DivAssign` | -| `:` | `pat: type`, `ident: type` | 约束 | | -| `:` | `ident: expr` | 结构体字段初始化 | | -| `:` | `'a: loop {...}` | 循环标签 | | -| `;` | `expr;` | 语句及项目的终止符 | | -| `;` | `[..., len]` | 固定大小数组语法的一部分 | | -| `<<` | `expr << expr` | 向左移位运算 | `Shl` | -| `<<=` | `var <<= expr` | 向左移位运算并赋值 | `ShlAssign` | -| `<` | `expr < expr` | 小于比较 | `PartialOrd` | -| `<=` | `expr <= expr` | 小于等于比较 | `PartialOrd` | -| `=` | `var = expr`, `ident = type` | 赋值/等价,equivalence | | -| `==` | `expr == expr` | 相等比较 | `PartialEq` | -| `=>` | `pat => expr` | 匹配支臂语法的一部分 | | -| `>` | `expr > expr` | 大于比较 | `PartialOrd` | -| `>=` | `expr >= expr` | 大于等于比较 | `PartialOrd` | -| `>>` | `expr >> expr` | 向右位移运算 | `Shr` | -| `>>=` | `var >>= expr` | 向右位移运算并赋值 | `ShrAssign` | -| `@` | `ident @ pat` | 模式绑定 | | -| `^` | `var ^ expr` | 按位异或运算 | `BitXor` | -| `^=` | `var ^= expr` | 按位异或运算并赋值 | `BitXorAssign` | -| | | pat | pat | 模式选择,pattern alternatives | | -| | | expr | expr | 按位或(OR)运算 | `BitOr` | -| |= | var |= expr | 按位或(OR)运算并赋值 | `BitOrAssign` | -| || | expr || expr | 短路逻辑或运算,Short-circuiting logical OR | | -| `?` | `expr?` | 错误传递 | | - -### 非运算符的符号 - -**Non-operator Symbols** - -以下清单包含了不以运算符发挥作用的全部符号;那就是说,他们不会表现得像函数或方法调用。 - -表 B-2 给出了自己单独出现,并在多种场合有效的一些符号。 - -**表 B-2:独立语法,Stand-Alone Syntax** - -| 符号 | 说明 | -| :--- | :--- | -| `'ident` | 命名的生命周期或循环标签 | -| `...u8`, `...i32`, `...f64`, `...usize` 等等 | 指定类型的数字字面值 | -| `"..."` | 字符串字面值 | -| `r"..."`, `r#"..."#`, `r##"..."##` 等等 | 原始字符串字面值,其中的转义字符不会被处理 | -| `b"..."` | 字节字符串字面值;构造出一个字节数组而非字符串 | -| `br"..."`, `br#"..."`, `br##"..."##` 等等 | 原始字节字符串字面值,是原始与字节字符串字面值的结合 | -| `'...'` | 字符字面值 | -| `b'...'` | ASCII 字节字面值 | -| |...| expr | 闭包 | -| `!` | 发散函数下总是空的底部类型,always empty bottom type for diverging functions | -| `_` | “忽略,ignored” 模式绑定;还用于令到整数字面值可读,also used to make integer literals readable | - - -表 B-3 展示了出现在模组层次结构中到某个项目路径上下文中的一些符号。 - -**表 B-3:路径相关的语法** - -| 符号 | 说明 | -| :--- | :--- | -| `ident::ident` | 命名空间路径 | -| `::path` | 相对于代码箱根的路径(比如,某个显式绝对路径) | -| `self::path` | 相对于当前模组的路径(比如,某个显式相对路径) | -| `super::path` | 相对于当前模组父模组的路径 | -| `type::ident`, `::ident` | 关联的常量、函数及类型 | -| `::...` | 无法直接命名的某个类型的关联项目(比如,`<&T>::...`, `<[T]>::...` 等等) | -| `trait::method(...)` | 通过命名出定义方法的类型,消除该方法调用的歧义 | -| `::method(...)` | 通过命名出特质与类型,消除方法调用的歧义 | - -表 B-4 展示了出现在运用泛型参数上下文中的一些符号。 - -**表 B-4:泛型** - -| 符号 | 说明 | -| :-- | :-- | -| `path<...>` | 指明类型中的泛型参数(比如,`Vec`) | -| `path::<...>`, `method::<...>` | 指明表达式中泛型、函数或方法的参数;通常这被称作涡轮鱼语法,turbofish(比如,`"42".parse::()`,关于 Rust 的 turbofish 语法,请参考:[What is Rust's turbofish](https://techblog.tonsser.com/posts/what-is-rusts-turbofish)),[RUST 中的 turbofish 语法(一)](https://www.jianshu.com/p/9107685ece03) ... | -| `fn ident<...> ...` | 定义出泛型函数 | -| `struct ident<...> ...` | 定义出泛型结构体 | -| `enum ident<...> ...` | 定义出泛型枚举 | -| `impl<...> ...` | 定义出泛型实现 | -| `for<...> type` | 高阶声明周期边界,higher-ranked lifetime bounds | -| `type` | 其中一个或更多的关联类型有着指定赋值的某种泛型(a generic type where one or more associated types have specific assignments,比如,`Iterator`) | - -下表 B-5 展示了出现在使用特质边界的约束性泛型参数上下文中的一些符号,table B-5 shows symbols that appear in the context of constraining generic type parameters with trait bounds。 - -**B-5:特质边界约束,Trait Bound Constrains** - -| 符号 | 说明 | -| :--- | :--- | -| `T: U` | 泛型参数 `T` 受实现了 `U` 的类型约束 | -| `T: 'a` | 泛型 `T` 必须要比生命周期 `'a` 活得更久,generic type `T` must outlive lifetime `'a`(意思是该类型不能间接地包含任何生命周期短于 `'a` 的引用) | -| `T: 'static` | 泛型 `T` 不包含除 `'static` 的引用外的其他引用 | -| `'b: 'a` | 泛型生命周期 `'b` 必须要比 `'a` 存活得更久 | -| `T: ?Sized` | 允许泛型参数为动态大小类型 | -| `'a + trait`, `trait + trait` | 复合的类型约束 | - -下表 B-6 展示了出现在宏调用或定义上下文中,并指明了某个项目上属性的一些符号。 - -**B-6:宏与属性** - -| 符号 | 说明 | -| :--- | :--- | -| `#[meta]` | 外层属性 | -| `#![meta]` | 内层熟悉 | -| `$ident` | 宏代换,macro substitution | -| `$ident:kind` | 宏捕获 | -| `$(...) ...` | 宏重复,macro repetition | -| `ident! (...)`, `ident! {...}`, `ident! [...]` | 宏调用,macro invocation | - -下表 B-7 展示了创建注释的一些符号。 - -**表 B-7:注释** - -| 符号 | 说明 | -| :--- | :--- | -| `//` | 注释行 | -| `//!` | 内层行文档注释,inner line doc comment | -| `///` | 外层行文档注释,outter line doc comment | -| `/*...*/` | 注释块 | -| `/*!...*/` | 内层块文档注释,inner block doc comment | -| `/**...*/` | 外层块文档注释,outter block doc comment | - -下表 B-8 展示了出现于用到元组上下文中的一些符号。 - -**元组** - -| 符号 | 说明 | -| :--- | :--- | -| `()` | 空元组(又叫单元值),同时属于字面值与类型 | -| `(expr)` | 元括号括起来的表达式,parenthesized expression | -| `(expr,)` | 单一元素的元组表达式 | -| `(type,)` | 单一元素的元组类型,single-element tuple type | -| `(expr, ...)` | 元组表达式 | -| `(type, ...)` | 元组类型,tuple type | -| `expr(expr, ...)` | 函数调用表达式;还用于初始化一些元组的 `struct` 以及元组的 `enum` 变种,function call expression; also used to initialize tuple `struct`s and tuple `enum` vairants | -| `expr.0`, `expr.1` 等等 | 对元组进行索引 | - -下表 B-9 展示了其中用到花括号上下文中的一些符号。 - -**表 B-9:花括号** - -| 符号 | 说明 | -| :--- | :--- | -| `{...}` | 代码块表达式 | -| `Type {...}` | `struct` 的字面值 | - -下表 B-10 展示了其中用到方括号上下文中的一些符号。 - -**表 B-10:方括号** - -| 符号 | 说明 | -| :--- | :--- | -| `[...]` | 数组的字面值 | -| `[expr; len]` | 包含着 `expr` 的 `len` 拷贝数组的字面值 | -| `[type; len]` | 包含着 `len` 个 `type` 的实例数组的字面值 | -| `expr[expr]` | 对集合进行索引,collection indexing。是可过载的 `(Index, IndexMut)`,overloadable `(Index, IndexMut)` | -| `expr[..]`, `expr[a..]`, `expr[..b]`, `expr[a..b]` | 用到了 `Range`、`RangeFrom`、`RangeTo` 或 `RangeFull` 作为 “索引”的,带有集合切片集合索引,collection indexing pretending to be collection slicing, using `Range`, `RangeFrom`, `RangeTo`, or `RangeFull` as the "index" | - - -## 附录 C:派生特质 - -**Appendix C: Derivable Traits** - -本书的多个不同地方,咱们都曾讨论过 `derive` 属性,咱们可将其应用到结构体或枚举定义。`derive` 属性会在咱们以 `derive` 语法注解的类型上,生成将以某个特质自身默认实现,而实现该特质的代码。 - -在这个附录中,咱们会提供到标准库中,咱们可以与 `derive` 一起使用的全部特质的参考。以下各个小节均会讲到: - -- 此特质将启用那些操作符与方法; -- 由 `derive` 所提供到的该特质实现会做些什么; -- 实现该特质对那个类型意味着什么; -- 允许及不允许实现该特质的情况; -- 需要该特质操作的示例。 - -若咱们想要不同于由 `derive` 属性所提供的行为,请参考 [标准库文档](https://doc.rust-lang.org/std/index.html),了解如何亲自实现各个特质的详细信息。 - -这里列出的这些特质,只是一些由标准库所提供的,可使用 `derive` 实现于咱们类型上的那些。定义在标准库中别的一些特质,则没有什么合理的默认行为,因此是否要以对于咱们正尝试完成的东西有意义的方式,实现他们就取决于咱们自己了。 - -不能派生的一个特质示例便是 `Display`,其为终端用户处理格式化。咱们应始终要考虑将某个类型显示给用户的恰当方式。终端用户应被允许看到该类型的哪些部分?他们会发现哪些部分是相关的?数据的何种形式才是与他们最为密切相关的?Rust 编译器并无这种见解,因此他就无法为咱们提供到恰当的默认行为。 - -这个附录中所提供到的派生特质清单并不详尽:库可以为他们自己的特质实现 `derive`,从而领导咱们可使用 `derive` 的特质清单为真正开放的。实现 `derive` 设计到使用程序性宏,这在第 19 章的 [“关于宏”](Ch19_Advanced_Features.md#关于宏) 小节讲到过。 - -### 输出给编程者的 `Debug` - -**`Debug` for Programmer Output** - -`Debug` 特质实现了格式字符串中的格式化,所谓格式字符串,即咱们通过在 `{}` 里添加 `:?` 所表示的。 - -`Debug` 特质允许咱们为调试目的打印某种类型的实例,如此咱们以及用到咱们类型的其他编程者,就可以在程序执行的某个特定时刻,就其某个实例加以探查。 - -在比如用到 `assert_eq!` 宏中等情况下,`Debug` 特质便是要求使用的。`assert_eq!` 这个宏在相等断言失败时,就会打印出作为参数所给到的两个实例值,如此编程者就可以看到为何这两个实例不相等。 - - -### 用于相等比较的 `PartialEq` 与 `Eq` - -`PartialEq` 特质允许咱们比较某种类型的两个实例,来检查他们是否相等,并实现 `==` 与 `!=` 运算符的应用。 - -对 `PartialEq` 进行派生,就会实现 `eq` 方法。当 `ParitalEq` 实在结构体上实现的时,只有在两个实例的 *全部* 字段都相等时,他们才是相等的,且在有任何字段不等时,两个实例便不相等。当在枚举上派生时,枚举的各个变种与自身相等,而不等于其他任何变种。 - -在使用需要能够比较某个类型的两个实例是否相等的 `assert_eq!` 宏时,就需要这个 `PartialEq` 特质。 - -而 `Eq` 特质则没有方法。他的目的是要表明,所注解的类型的每个值,其值都等于他自身。尽管并非所有实现 `PartialEq` 的类型都可以实现 `Eq`,但 `Eq` 特质却只可应用到那些同时实现了 `PartialEq` 的类型。这方面的一个示例,便是浮点数类型:浮点数的实现,就表明两个非数字(the not-a-number, `NaN`)的值,是各自不相等的。 - -要求 `Eq` 的一个示例,就是 `HashMap` 中的那些键,如此 `HashMap` 就可以区分出两个键是否一致。 - - -### 用于排序比较的 `PartialOrd` 与 `Ord` - -**`PartialOrd` and `Ord` for Ordering Comparisons** - -`PartialOrd` 特质实现为排序目的,而比较某种类型的那些实例。实现了 `PartialOrd` 的类型,便可与 `<`、`>`、`<=` 及 `>=` 符号一起使用了。咱们只能对那些同时实现了 `PartialEq` 的类型,应用这个 `PartialOrd` 特质。 - -派生 `PartialOrd`,会实现 `partial_cmp` 方法,该方法会返回一个在所给的那些值不会产生出顺序时,将为 `None` 的一个 `Option`。至于即使那种类型的大多数值都可被比较,但仍不会产生出顺序的值的一个示例,便是非数字(`NaN`)浮点值。在任何浮点数和非数字浮点值下调用 `partial_cmp`,都会返回 `None`。 - -在于结构体上派生时,`PartialOrd` 会通过字段出现在结构体定义中的顺序,比较每个字段中的值,比较两个实例。而当于枚举上派生时,枚举定义中较早声明的枚举变种,被当作是小于后面所列出的那些变种的。 - -在比如会产生出由范围表达式所指定范围中一个随机数的, `rand` 代码箱的 `gen_range` 方法来说,`PartialOrd` 特质便是需要的。 - -`Ord` 特质实现对所注解类型的任何两个值,将存在有效顺序的掌握。`Ord` 特质会实现 `cmp` 方法,由于有效排序将始终可行,因此该方法返回的是 `Ordering` 而非 `Option`。咱们只可对那些同时实现了 `PartialOrd` 及 `Eq` (而 `Eq` 要求 `PartialEq`) 的类型,实现这个 `Ord` 特质。当于结构体及枚举上派生 `Ord` 时,`cmp` 就会以与 `PartialOrd` 下 `partial_cmp` 的派生实现同样方式行事。 - -要求 `Ord` 的一个示例,即为将一些值存储在 `BTreeSet` 这种根据值的排序,而存储数据的数据结构中时。 - -### 用于复制值的 `Clone` 与 `Copy` - -**`Clone` and `Copy` for Duplicating Values** - -`Clone` 特质实现了显式创建值的深拷贝,而该复制过程则可能涉及运行一些任意代码,arbitary code,与拷贝内存堆数据。请参阅第 4 章中 [“变量与数据交互方式:克隆”](Ch04_Understanding_Ownership.md#变量与数据交互方式之二克隆) 小节,了解更多有关 `Clone` 的信息。 - -派生 `Clone` 会实现 `clone` 方法,当对整个类型实现了这个方法时,其就会在该类型的各个部分上调用 `clone`。这意味着类型要派生 `Clone` 其中的全部字段或值,都必须同时实现 `Clone`。 - -需要 `Clone` 特质的一个示例,便是在切片上调用 `to_vec` 方法时。切片不持有其包含的那些类型实例,但自 `to_vec` 所返回的那个矢量值,却将需要持有他的那些实例,从而 `to_vec` 会调用各个条目上的 `clone`。因此,存储在切片中的类型,就必须实现 `Clone`。 - -`Copy` 特质实现了只通过拷贝存储在栈上的二进制位,而复制某个值;任意代码并无必要。请参阅第 4 章中 [“唯栈数据:拷贝”](Ch04_Understanding_Ownership.md#唯栈数据拷贝stack-only-data-copy),了解更多有关 `Copy` 的信息。 - -`Copy` 特质没有定义阻止编程者过载那些方法,及破坏不会有任意代码运行这个假设的任何方法。那样的话,所有编程者就都可以假定,拷贝值将会非常快。 - -咱们可在其组成部分都实现了 `Copy` 的任何类型上派生 `Copy` 特质。由于实现 `Copy` 的类型,都有着执行与 `Copy` 同样任务的一个 `Clone` 的简单实现,因此实现 `Copy` 的类型必须同时实现 `Clone`。 - -很少需要 `Copy` 特质;实现了 `Copy` 的类型,有着可供选择的优化方案,意味着咱们不必调用 `clone`,而调用 `clone` 会令到代码更简洁。 - -对于 `Copy` 下每种可能情况,咱们都可同时以 `Clone` 完成,除了代码可能更慢,或在一些地方不得不使用 `clone`。 - - -### 用于将值映射到固定大小值的 `Hash` - -**`Hash` for Mapping a Value to a Value of Fixed Size** - - -`Hash` 特质实现了取某种任意大小类型的实例,并通过使用散列函数,将那个实例映射到固定大小的值。派生 `Hash` 会实现 `hash` 方法。`hash` 放的派生实现,会将在该类型各个组成部分上调用 `hash` 的结果结合起来,这就意味着类型要派生 `Hash`,那么其全部字段,都必须同时实现 `Hash`。 - -要求 `Hash` 的一个示例,便是为了高效地存储数据,而在 `Hash` 中存储那些键时。 - - -### 用于默认值的 `Default` - -**`Default` for Default Values** - -`Default` 特质实现了为类型创建出一个默认值。派生 `Default` 会实现 `default` 函数。`default` 函数的派生实现,会在类型的各个部分上调用 `default` 函数,意味类型要派生 `Defualt`,其中的全部字段或值,都必须同时实现 `Default`。 - -`Default::default` 函数,通常是与第 5 章中 [“使用结构体更新语法从其他实例创建出实例”](Ch05_Using_Structs_to_Structure_Related_Data.md#使用结构体更新语法从其他实例创建出实例) 小节里曾讨论过的结构体更新语法结合使用的。咱们可以定制结构体的几个字段,并在随后通过使用 `..Default::default()`,为其余字段设置并使用默认值。 - -在 `Option` 实例上使用 `unwrap_or_default` 方法时,便是需要 `Default` 特质的一个示例。当那个 `Option` 为 `None` 时,方法 `unwrap_or_default` 就将返回存储在 `Option` 中,那个类型 `T` 的 `Default::default` 结果。 - - -## 附录 D:一些有用开发工具 - -在此附录中,咱们会讲到 Rust 项目所提供的一些有用的开发工具。咱们将看看自动格式化、应用警告修复的一些快速方法、一种代码静态分析工具,a linter,以及与多种 IDE 的集成。 - - -### 使用 `rustfmt` 的自动格式化 - -**Automatic Formatting with `rustfmt`** - -`rustfmt` 工具会依据社区编码风格,重新格式化咱们的代码。许多协作项目,都使用了 `rustfmt` 来防止有关编写 Rust 时使用何种风格方面的争论:每个人都使用这个工具来格式化他们的代码。 - -要安装 `rustfmt`,请键入下面的命令: - -```console -$ rustup component add rustfmt -``` - -如同 Rust 会同时给到 `rustc` 与 `cargo` 一样,此命令会给到咱们 `rustfmt` 与 `cargo-fmt`。要格式化任何 Cargo 项目,请敲入下面的命令: - -```console -$ cargo fmt -``` - -运行此命令,会重新格式化当前代码箱中全部的 Rust 代码。这只会改变编码风格,而不会改变代码语义。关于 `rustfmt` 的更多信息,请参阅 [其文档](https://github.com/rust-lang/rustfmt). - - -### 使用 `rustfix` 修复咱们的代码 - -**Fix Your Code with `rustfix`** - -`rustfix` 工具已被 Rust 安装所包含,并可大致以咱们想要的方式,修复那些有着明确纠正问题方法的一些编译器告警。咱们之前大概率已经见到过编译器告警了。比如,设想有下面这段代码: - -文件名:`src/main.rs` - -```rust -fn do_something() {} - -fn main() { - for i in 0..100 { - do_something(); - } -} -``` - -此处,咱们正调用 `do_something` 函数 100 次,但咱们在 `for` 循环的代码体中,从未用到那个变量 `i`。Rust 就会就此对咱们发出告警: - -```console -$ cargo build - Compiling rustfix_demo v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/rustfix_demo) -warning: unused variable: `i` - --> src/main.rs:4:9 - | -4 | for i in 0..100 { - | ^ help: if this is intentional, prefix it with an underscore: `_i` - | - = note: `#[warn(unused_variables)]` on by default - -warning: `rustfix_demo` (bin "rustfix_demo") generated 1 warning - Finished dev [unoptimized + debuginfo] target(s) in 0.29s -``` - -这个告警建议咱们要使用 `_i` 做名字:其中的下划线表示咱们有意不使用这个变量。通过运行 `cargo fix` 命令,咱们就可以使用 `rustfix`,自动应用那项建议: - -```console -$ cargo fix --allow-no-vcs - Checking rustfix_demo v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/rustfix_demo) - Fixed src/main.rs (1 fix) - Finished dev [unoptimized + debuginfo] target(s) in 0.17s -``` - -当咱们再次看到 `src/main.rs`,就将发现 `cargo fix` 已修改了这段代码: - -文件名:`src/main.rs` - -```rust -fn do_something() {} - -fn main() { - for _i in 0..100 { - do_something(); - } -} -``` - -那个 `for` 循环变量,现在就被命名为了 `_i`,同时那条告警也不再出现了。 - -咱们还可使用 `cargo fix` 命令,将咱们的代码在不同 Rust 版本之间转换。有关这些 Rust 版本,在附录 E 中有讲到。 - - -### 使用 Clippy 获得更多的代码静态分析 - -**More Lints with Clippy** - -Clippy 工具是用于分析咱们代码,从而咱们可以捕获到一些常见错误,而改进咱们 Rust 代码的一套代码静态分析集合。 - -要安装 Clippy,请输入以下命令: - -```console -$ rustup component add Clippy -``` - -在任何 Cargo 项目上要运行 Clippy 的静态分析,请输入以下命令: - -```console -$ cargo clippy -``` - -比如说咱们编写了像下面这个程序这样,用到某个数学常量近似值,好比说 `pi`,的一个程序: - -文件名:`src/main.rs` - -```rust -fn main() { - let x = 3.1415; - let r = 8.0; - println!("圆的面积为 {}", x * r * r); -} -``` - -在这个项目上运行 `cargo clippy` 就会得到下面的报错: - -```console -$ cargo clippy - Checking clippy_demo v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/clippy_demo) -error: approximate value of `f{32, 64}::consts::PI` found - --> src/main.rs:2:13 - | -2 | let x = 3.1415; - | ^^^^^^ - | - = help: consider using the constant directly - = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant - = note: `#[deny(clippy::approx_constant)]` on by default - -error: could not compile `clippy_demo` due to previous error -``` - -此报错让咱们明白,Rust 已经定义了一个更精确的 `PI` 常量,且当咱们使用这个常量时,咱们的程序将更为正确。那么咱们随后就应修改咱们代码为使用这个 `PI` 常量。下面的代码就捕获导致 Clippy 的任何错误或告警: - -文件名:`src/main.rs` - -```rust -fn main() { - let x = std::f64::consts::PI; - let r = 8.0; - println!("圆的面积为 {}", x * r * r); -} -``` - -有关 Clippy 的更多信息,请参阅 [其文档](https://github.com/rust-lang/rust-clippy)。 - -### 用到 `rust-analyzer` 的 IDE 集成 - -**IDE Integration Using `rust-analyzer`** - -为帮助 IDE 集成,Rust 社区建议使用 [`rust-analyzer`](https://rust-analyzer.github.io/)。此工具是一套以编译器为中心,操 [语言服务器协议,Language Server Protocol](http://langserver.org/) 的实用工具;而所谓语言服务器协议,则是用于各种 IDEs 和编程语言,二者相互之间通信的一种规格。有多种不同客户端可使用 `rust-analyzer`,比如 [Visual Studio Code 的 Rust 分析器插件](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)。 - -请访问 `rust-analyzer` 项目 [主页](https://rust-analyzer.github.io/),了解其安全说明,随后在咱们的特定 IDE 中安装该语言的服务器支持。咱们的 IDE 就能获得诸如自动补全、跳至定义及行内报错等能力。 - - -## 附录 E:关于版本 - -**Appendix E - Editions** - -在第一章中,咱们曾看到 `cargo new` 会把一点有关某个版的元数据,添加到咱们的 `Cargo.toml` 文件。此附录就会讲到那意味着什么! - -Rust 语言及编译器有着六周的发布周期,意味着用户会得到源源不断的新功能。其他编程语言会不经常地发布较大变更;Rust 则会更频繁发布较小的更新。不久之后,全部这些小修改就会堆积起来。不过这一个个发布中,回头看看而讲到,“噢,从版本 1.10 到 1.31,Rust 改变了很多!”。则是不容易的。 - -每两三年,Rust 团队都会产生一个新的 Rust *版本,edition*。每个版本都会以完全更新的文档与工具,将那些业已落地到一个明确包中的特性放到一起。新版本会作为寻常的六周发布过程而交付。 - -这些版本服务了不同人群的不同目的: - -- 对于活跃的 Rust 用户,新版本会把那些增量变更,一起放入到一个易于掌握的包中; -- 对于那些非用户,新版本释放了一些已落地的大进展信号,这会让 Rust 或许值得再看一看; -- 对于开发 Rust 的人们,新版本会提供这个项目作为整体的集结点。 - -在本书编写时,已有三个 Rust 版本可用:Rust 2015、Rust 2018 与 Rust 2021。本书是用 Rust 2021 版本的习惯用语编写的。 - -`Cargo.toml` 中的 `edition` 键,表示应对咱们的代码使用哪个版本的编译器。若该键不存在,Rust 就会以向后兼容原因,而使用 `2015` 作为版本值。 - -每个项目都可以选择一个不同于默认 2015 的版本。这些版本可能包含了不兼容的变更,比如包含了与代码中标识符冲突的新关键字。但是,除非咱们选到这些变更,那么即使咱们更新了所使用的 Rust 编译器,咱们的代码将继续编译。 - -全部 Rust 编译器版本,都会支持先于那个编译器发布而存在的任何版本,且他们可将任何受支持版本的代码箱连接起来。版本变更只会影响编译器于编译初期解析代码的方式。因此,当咱们正使用着 Rust 2015,而咱们的一项依赖使用了 Rust 2018 时,咱们的项目将编译,并能够使用那项依赖。与之相反,在咱们的项目使用 Rust 2018,而一项依赖使用了 Rust 2015 的情形下,也会工作。 - -要明确的是:绝大多数特性,在所有版本上都将可用。使用任何 Rust 版本的开发者,都将在新的稳定发布构造出来时,发现一些改进。但是,在一些情况下,主要是在新曾了关键字时,一些新特性就会只在稍后版本中可用了。若咱们打算利用上这些新特性,咱们将需要切换版本。 - -有关更多细节,[版本指南,Edition Guide](https://doc.rust-lang.org/stable/edition-guide/) 是本列举了不同版本间差异,并解释了怎样通过 `cargo fix`,而自动将咱们的代码更新到新版的一本完整的书。 - - -## 附录 F - 本书的一些译本 - -<略> - -## 附录 G - Rust 是怎样构造出来的与每日发布 - -**How Rust is Made and "Nightly Rust"** - -此附录是有关 Rust 被怎样构造出来,及那会怎样作为一名 Rust 开发者的你。 - -### 强调稳定却并无止步不前 - -**Stability Without Stagnation** - -作为一门语言,Rust 在注重咱们代码稳定性方面 *用心良苦*。咱们希望 Rust 成为你可以在其上构建软件的稳固基础,而若那些物件都一直变动,那将是不可能实现的。而与此同时,若咱们无法实验一些新特性,那么直到这些特性发布后咱们不能在修改一些东西时,咱们也不会发现一些重大缺陷。 - -对于这个问题,咱们(Rust 团队)的解决方案就是咱们称作 “强调稳定又不要止步不前”,而咱们的直到原则是这样的:你永不必害怕升级到稳定的Rust 新版本。每次升级都应是无痛的,而又应带给你一些新特性、更少的程序错误,以及更快的编译时间。 - - -### 啾,啾!发布通道与搭上快车 - -**Choo, Choo! Release Channels and Riding the Trains** - -Rust 的开发,是运作在 *火车时刻表,train schedule* 上的。那就是说,全部开发都是在 Rust 代码仓库的 `master` 分支上完成的。各个发布遵循了软件发布列车模型,a software release train model,该发布模型业已为 Cisco IOS 及其他软件项目所使用。Rust 有着以下三个 *发布通道,release channels*: - -- 每日发布,nightly -- Beta 发布,beta -- 稳定发布,stable - -多数 Rust 开发者主要使用稳定通道,而那些希望尝试实验性新特性的人们,则会使用每日发布或 beta 通道。 - -下面是个开发与发布流程运作方式的一个示例:咱们来假定 Rust 团队正工作于 Rust 1.5 的发布上。那个发布发生于 2015 年 11 月,但其将提供到我们实际版本数字。有个新特性被添加到 Rust:一次新提交落在了 `master` 分支。每天晚上,都有一个新的 Rust 每日版本被产生出来。每天都是个发布日,而这些发布是由咱们的发布基础设施自动创建的。因此随着时间流逝,咱们的发布看起来就像下面这样,每晚一次: - -```text -nightly: * - - * - - * -``` - -每隔六周,便是要准备一个新发布的时候了!Rust 代码仓库的 `beta` 分支,便会从由每日发布所使用的 `master` 分支分叉开来。现在,就有了两个分支: - -```text -nightly: * - - * - - * - | -beta: * -``` - -多数 Rust 使用者不会积极使用这些 beta 发布,但会在他们的 CI 系统中就 beta 发布加以测试,以帮助 Rust 发现可能出现的倒退。与此同时,仍有着每晚的每日发布: - -```text -nightly: * - - * - - * - - * - - * - | -beta: * -``` - -在首个 beta 版创建出来六周后,就是稳定发布的时候了!`stable` 分支就被从 `beta` 分支创建出来: - -```text -nightly: * - - * - - * - - * - - * - - * - * - * - | -beta: * - - - - - - - - * - | -stable: * -``` - -好!Rust 1.5 便完成了!不过,咱们忘了一件事:由于这六个星期以及过去,而咱们还需要 Rust *下一* 版本,1.6,的一个新的 beta 发布。因此在 `stale` 分支从 `beta` 分支分叉出来后,下一版本的 `beta` 又会从 `nightly` 再度分叉出来: - -```text -nightly: * - - * - - * - - * - - * - - * - * - * - | | -beta: * - - - - - - - - * * - | -stable: * -``` - -每六周就有一个发布 “离站”,但发布过程仍务必要在其抵达稳定发布前,经由这个 beta 通道行驶一段路程,由此这个过程便被称为 “列车模型”。 - -Rust 每六周发布,像时刻表一样。若咱们知道了一个 Rust 发布的日期,那么就能直到下一发布的日期:那便是六周后。每六周安排一次发布的一个好处,便是下一班列车很快就会到来。若某项特性刚好错过了某个特定发布,那么无需担心:另一发布将在不久后发生!这有助于减少在临近发布截止日期时,有可能未完善的功能偷偷潜入的压力。 - - -归功于这个流程,咱们可以始终检出,check out,下一构建的 Rust,并自己验证到升级是容易的:若 beta 发布没有如预期那样工作,咱们就可以将其报告给 Rust 团队,并在下一稳定发布发生前修好他!beta 发布中的损坏相对较少,但 `rustc` 仍属于一个软件,而确实存在一些错误。 - - -### 不稳定特性 - -**Unstable Features** - -这种发布模型下,还有一个好处:不稳定特性。Rust 使用了一种名为 “特性标识,feature flags” 的技巧,来确定出给定发布中启用了哪些特性。若某项新特性处于活跃开发中,他就会落地在 `master` 分支上,而由此就会在每日发布中,但会有着一个 *特性标识*。而咱们,作为用户,希望尝试这个进展中的特性,the work-in-progress feature,时,咱们是可以尝试的,但必须使用 Rust 的每日发布,并使用恰当的标识来注解咱们的代码,来选用该特性。 - -若咱们使用着 beta 或稳定发布的 Rust,那么就不能使用任何特性标识。这是 Rust 团队在声明那些新特性永久稳定前,允许咱们实际用到他们的关键。希望选用最新特性的人们,便可这样做,而想要一种扎实体验的人,则可坚持使用稳定发布,而清楚他们的代码不会破坏。这便是稳定但并非止步不前。 - -由于那些工作中的特性仍在便会,且在本书写作时和他们在稳定构建中启用时,其间他们肯定将有所不同,因此本书只包含了那些稳定特性的信息。咱们可以在线上找到那些仅每日发布有的特性文档。 - -### Rustup 与 Rust 每日发布所扮演的角色 - -**Rustup and the Role of Rust Nightly** - -Rust 令到易于在全局或每个项目基础上,从不同发布通道的 Rust 之间改变。默认情况下,咱们将安装稳定发布的 Rust。而比如要安装每日发布: - -```console -$ rustup toolchain install nightly -``` - -咱们也可以使用 `rustup`,查看全部的 *工具链,toolchains* (Rust 的各个发布与关联组件)。下面就是本书一位作者的 Windows 计算机上的示例: - -```powershell -> rustup toolchain list -stable-x86_64-pc-windows-msvc (default) -beta-x86_64-pc-windows-msvc -nightly-x86_64-pc-windows-msvc -``` - -> 在 Linux 系统上的输出如下: - -```console -$ rustup toolchain list -stable-x86_64-unknown-linux-gnu (default) -``` - -可以看到,稳定发布的工具链是默认的。绝大多数 Rust 用户会在多数时候使用稳定发布。咱们可能想要在多数时候使用稳定发布,又因为咱们关心某项最新特性,而会在特定项目使用每日发布。要这样做,就可以在那个项目目录下,使用 `rustup override` 来将每日发布工具链,设置为当咱们位处那个目录中时,`rustup` 使用的那个工具链: - -```console -$ cd ~/projects/needs-nightly -$ rustup override set nightly -``` - -现在,当咱们每次在 `~/projects/needs-nightly` 目录下调用 `rustc` 或 `cargo` 时,`rustup` 都会确保咱们在使用每日发布的 Rust,而非咱们默认的稳定发布 Rust 了。再有很多 Rust 项目时,这就会排上用场! - -### 请求评议流程与各种团队 - -**The RFC Process and Teams** - -那么咱们该怎么了解到这些新特性呢?Rust 的开发模型,遵循了 *请求评议流程,Request For Comments(RFC) process*。如你想要 Rust 的一项改进,那么就可以编写一个名为请求评议,RFC 的提议。 - -人人都可以编写请求评议来改进 Rust,同时这些提议会经过由许多议题子团队所组成的 Rust 团队审阅和讨论。[在 Rust 网站上](https://www.rust-lang.org/governance) 有这些团队的完整清单,其中包括了该项目各领域:语言设计、编译器实现、基础设施、文档及其他等的团队。恰当的团队会阅读提议与评论,撰写出他们自己的一些评论,并在最后,便有了接受或拒绝该特性的共识。 - -若该特性被接受了,就会在 Rust 代码仓库上开出一个 issue,同时某个人就可以实现他。将其实现得非常棒的那个人,可能不是最早提议这项特性的那人!在实现准备好时,其就会落地于 `master` 分支的特性门,a feature gate,之后,如同咱们曾在 [“不稳定特性”](#不稳定特性) 小节中曾讨论过的那样。 - -过了一段时间后,一旦那些用到每日发布的 Rust 开发者们,能够试用这项新特性,那么 Rust 团队成员将讨论这项特性,怎样将其编制到每日发布上,并决定其是否有那个被构造到稳定发布 Rust。而若决定是继续推进,那么特性门就会被移除,同时这项特性就被认为是稳定的了!他就会搭上列车,进到一个新的稳定发布 Rust 中。 - - -## 附录 H - 有用笔记 - -此处记录学习及应用 Rust 编程软件过程中,觉得有用的一些东西。 - - -### `cargo-binutils` - -[这个项目](https://github.com/rust-embedded/cargo-binutils) 是 Embbeded-Rust 项目的,而不是 Rust 官方的,但提供了有用的功能。比如查看构建出的二进制程序文件的那些头部: - - -```console -$ cargo readobj --bin clippy_demo -- --file-headers - Finished dev [unoptimized + debuginfo] target(s) in 0.00s -ELF Header: - Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 - Class: ELF64 - Data: 2's complement, little endian - Version: 1 (current) - OS/ABI: UNIX - System V - ABI Version: 0 - Type: DYN (Shared object file) - Machine: Advanced Micro Devices X86-64 - Version: 0x1 - Entry point address: 0x86D0 - Start of program headers: 64 (bytes into file) - Start of section headers: 4305200 (bytes into file) - Flags: 0x0 - Size of this header: 64 (bytes) - Size of program headers: 56 (bytes) - Number of program headers: 12 - Size of section headers: 64 (bytes) - Number of section headers: 42 - Section header string table index: 41 -``` - -使用前需要进行如下安装: - -```console -$ cargo install cargo-binutils -$ rustup component add llvm-tools-preview -``` - -## 附录 I - 术语清单 - - -- 命令行界面 - -Command-Line Interface,在终端里运行的应用,与在 GUI 窗口中应用不同。 - -- 模组系统 - -The module system,大型程序中,组织代码的方式。 - - -- 迁移所有权 - -在闭包参数清单前,使用 `move` 关键字,让闭包取得其用到的所在环境中的值所有权。 - -- 关联类型 - -Associated type, 是通过 `type` 关键字,定义在特质下的类型。咱们知道方法即为关联函数,associated function,那么关联类型自然与关联函数有些类似。 - - -- 消费适配器 - -Consuming adaptor, `Iterator` 特质上,会调用到迭代器 `next` 方法的一些方法,由于这些方法会耗尽迭代器,故他们被称为消费适配器。 - - -- 迭代器适配器 - -Iterator adaptor,`Iterator` 特质上,通过改变原迭代器某些方面而产生出另一迭代器的一些方法。 - - -- 零成本抽象 - -Zero-cost abstractions,相较于一般实现,语言提供的高级抽象在编译后生成的代码,与自己努力编写出的优化低级别代码类似。故使用高级抽象是没有运行时开销的。 - - -- 展开优化 - -Unrolling,Rust 编译器在编译迭代器代码时,会把已知的历次迭代展开为重复代码,而实现性能优化。 - - -- 文档注释 - -Documentation comment, 将产生出 HTML 的注释。 - - -- 重导出程序项目 - -Re-export, 使用 `pub use` 重新导出程序项目。 - - -- 语义版本控制规则 - -Semantic Versioning rules, 又大版本、小版本及补丁版本构成的,形如 `MAJOR.MINOR.PATCH` 的版本编号规则。参考:[semver.org](https://semver.org)。 - - -- 工作区 - -Workspace,为有着多个库代码箱的大型项目组织的一项 Cargo 特性。 - - -- 编译出的物件 - -The compiled artifacts - - -- 路径依赖 - -A path dependency - - -- 匣子类型(数据结构) - -`Box`,由存储在栈上的指针,与存储在堆上的数据,实现的一种数据结构。 - - -- 间接 - -Indirection, 匣子类型的变量,通过保存指向数据在内存堆上的地址,而间接保存了数据。 - - -- 解引用强制转换 - -Deref coercion,类似于其他语言的开箱操作。 - - -- 元组结构体 - -A tuple struct, 形式为 `struct MyBox(T)`,是保持着只有一个元素元组的结构体,`Box` 的数据结构为元组结构体。 - - -- 前奏 - -The Rust Prelude, `std::prelude` 模组。前奏是 Rust 自动导入到每个 Rust 程序中的东西的列表。他被保持在尽可能小的范围内,并且专注于几乎每个 Rust 程序都会用到的东西,特别是特质。参见:[`std::prelude`](https://doc.rust-lang.org/std/prelude/index.html)。 - - -- 内部可变性模式 - -The interior mutability pattern, Rust 的一种设计模式,用于改变不可变值内部的某个值。 - - -- 内存泄漏 - -Memory leak, 出现未清理内存的情况。 - - -- 关联类型 - -An associated type, 通过 `type Target = t;` 这种语法声明出的类型,是声明泛型参数的一种稍微不同的方式。 - - -- 单态化 - -所谓 *单态化,monomorphization*,是指即通过把在编译后用到的具体类型填入到泛型位置,而将通用代码转换为具体代码的过程。参考 [使用泛型代码的性能问题](Ch10_Generic_Types_Traits_and_Lifetimes.md#使用泛型参数代码的性能问题)。 - - -- 内聚属性 - -a property called *coherence*,参见 [在类型上实现某个特质](Ch10_Generic_Types_Traits_and_Lifetimes.md#在类型上实现某个特质)。 - - -- 孤儿规则 - -the orphan rule, 参见 [在类型上实现某个特质](Ch10_Generic_Types_Traits_and_Lifetimes.md#在类型上实现某个特质)。 - - -- `impl Trait` 语法 - -`impl Trait` syntax, 在函数参数清单中,将特质用作参数类型注解的语法。参见:[作为参数的特质](Ch10_Generic_Types_Traits_and_Lifetimes.md#作为参数的特质) - - -- 特质边界语法 - -Trait bound syntax, 参见 [特质边界语法](Ch10_Generic_Types_Traits_and_Lifetimes.md#特质边界语法) - - -- 语法糖 - -Sugar syntax, 参见 [特质边界语法](Ch10_Generic_Types_Traits_and_Lifetimes.md#特质边界语法) - - -- 指明多个特质边界的 `+` 语法 - -The `+` syntax for specifying multiple trait bounds, 参见:[使用 + 语法,指定多个特质边界](Ch10_Generic_Types_Traits_and_Lifetimes.md#使用--语法指定多个特质边界) - - -- `where` 子句 - -`where` clauses, 参见 []() - - -- 生命周期省略规则 - -Lifetime elision rules, 编程到 Rust 引用分析中的一些确定性模式。 - - -- 输入生命周期 - -Input lifetimes,函数或方法上的生命周期 - - -- 输出生命周期 - -Output lifetimes, 返回值上的生命周期 - diff --git a/src/SUMMARY.md b/src/SUMMARY.md index f03c30f..9f24a30 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -134,5 +134,17 @@ - [关于宏](advanced_features/macros.md) - [最后项目:构建一个多线程的 Web 服务器](Ch20_Final_Project_Building_a_Multithreaded_Web_Server.md) + - [构建一个单线程的 Web 服务器](final_project/single-threaded.md) + - [将这个单线程服务器修改为多线程服务器](final_project/multithreaded.md) + - [优雅关机与内存清理](final_project/graceful_shutdown.md) - [附录](Ch21_Appendix.md) + - [A - 关键字](appendix/keywords.md) + - [B - 运算符与符号](appendix/ops_and_symbols.md) + - [C - 派生特质](appendix/derivable_traits.md) + - [D - 有用开发工具](appendix/dev_tools.md) + - [E - 关于版本](appendix/editions.md) + - [F - 本书的一些译本](appendix/translations.md) + - [G - Rust 是怎样构造出来的与 “每日 Rust”](appendix/releases.md) + - [H - 有用笔记](appendix/notes.md) + - [I - 术语清单](appendix/terminology_list.md) diff --git a/src/appdendix/derivable_traits.md b/src/appdendix/derivable_traits.md new file mode 100644 index 0000000..664b734 --- /dev/null +++ b/src/appdendix/derivable_traits.md @@ -0,0 +1,116 @@ +# 附录 C:派生特质 + +**Appendix C: Derivable Traits** + + +本书的多个不同地方,咱们都曾讨论过 `derive` 属性,咱们可将其应用到结构体或枚举定义。`derive` 属性会在咱们以 `derive` 语法注解的类型上,生成将以某个特质自身默认实现,而实现该特质的代码。 + +在这个附录中,咱们会提供到标准库中,咱们可以与 `derive` 一起使用的全部特质的参考。以下各个小节均会讲到: + +- 此特质将启用那些操作符与方法; + +- 由 `derive` 所提供到的该特质实现会做些什么; + +- 实现该特质对那个类型意味着什么; + +- 允许及不允许实现该特质的情况; + +- 需要该特质操作的示例。 + + + +若咱们想要不同于由 `derive` 属性所提供的行为,请参考 [标准库文档](https://doc.rust-lang.org/std/index.html),了解如何亲自实现各个特质的详细信息。 + +这里列出的这些特质,只是一些由标准库所提供的,可使用 `derive` 实现于咱们类型上的那些。定义在标准库中别的一些特质,则没有什么合理的默认行为,因此是否要以对于咱们正尝试完成的东西有意义的方式,实现他们就取决于咱们自己了。 + +不能派生的一个特质示例便是 `Display`,其为终端用户处理格式化。咱们应始终要考虑将某个类型显示给用户的恰当方式。终端用户应被允许看到该类型的哪些部分?他们会发现哪些部分是相关的?数据的何种形式才是与他们最为密切相关的?Rust 编译器并无这种见解,因此他就无法为咱们提供到恰当的默认行为。 + +这个附录中所提供到的派生特质清单并不详尽:库可以为他们自己的特质实现 `derive`,从而领导咱们可使用 `derive` 的特质清单为真正开放的。实现 `derive` 设计到使用程序性宏,这在第 19 章的 [“关于宏”](Ch19_Advanced_Features.md#关于宏) 小节讲到过。 + + +## 输出给编程者的 `Debug` + +**`Debug` for Programmer Output** + + +`Debug` 特质实现了格式字符串中的格式化,所谓格式字符串,即咱们通过在 `{}` 里添加 `:?` 所表示的。 + +`Debug` 特质允许咱们为调试目的打印某种类型的实例,如此咱们以及用到咱们类型的其他编程者,就可以在程序执行的某个特定时刻,就其某个实例加以探查。 + +在比如用到 `assert_eq!` 宏中等情况下,`Debug` 特质便是要求使用的。`assert_eq!` 这个宏在相等断言失败时,就会打印出作为参数所给到的两个实例值,如此编程者就可以看到为何这两个实例不相等。 + + +## 用于相等比较的 `PartialEq` 与 `Eq` + + +`PartialEq` 特质允许咱们比较某种类型的两个实例,来检查他们是否相等,并实现 `==` 与 `!=` 运算符的应用。 + +对 `PartialEq` 进行派生,就会实现 `eq` 方法。当 `ParitalEq` 实在结构体上实现的时,只有在两个实例的 *全部* 字段都相等时,他们才是相等的,且在有任何字段不等时,两个实例便不相等。当在枚举上派生时,枚举的各个变种与自身相等,而不等于其他任何变种。 + +在使用需要能够比较某个类型的两个实例是否相等的 `assert_eq!` 宏时,就需要这个 `PartialEq` 特质。 + +而 `Eq` 特质则没有方法。他的目的是要表明,所注解的类型的每个值,其值都等于他自身。尽管并非所有实现 `PartialEq` 的类型都可以实现 `Eq`,但 `Eq` 特质却只可应用到那些同时实现了 `PartialEq` 的类型。这方面的一个示例,便是浮点数类型:浮点数的实现,就表明两个非数字(the not-a-number, `NaN`)的值,是各自不相等的。 + +要求 `Eq` 的一个示例,就是 `HashMap` 中的那些键,如此 `HashMap` 就可以区分出两个键是否一致。 + + +## 用于排序比较的 `PartialOrd` 与 `Ord` + + +**`PartialOrd` and `Ord` for Ordering Comparisons** + +`PartialOrd` 特质实现为排序目的,而比较某种类型的那些实例。实现了 `PartialOrd` 的类型,便可与 `<`、`>`、`<=` 及 `>=` 符号一起使用了。咱们只能对那些同时实现了 `PartialEq` 的类型,应用这个 `PartialOrd` 特质。 + +派生 `PartialOrd`,会实现 `partial_cmp` 方法,该方法会返回一个在所给的那些值不会产生出顺序时,将为 `None` 的一个 `Option`。至于即使那种类型的大多数值都可被比较,但仍不会产生出顺序的值的一个示例,便是非数字(`NaN`)浮点值。在任何浮点数和非数字浮点值下调用 `partial_cmp`,都会返回 `None`。 + +在于结构体上派生时,`PartialOrd` 会通过字段出现在结构体定义中的顺序,比较每个字段中的值,比较两个实例。而当于枚举上派生时,枚举定义中较早声明的枚举变种,被当作是小于后面所列出的那些变种的。 + +在比如会产生出由范围表达式所指定范围中一个随机数的, `rand` 代码箱的 `gen_range` 方法来说,`PartialOrd` 特质便是需要的。 + +`Ord` 特质实现对所注解类型的任何两个值,将存在有效顺序的掌握。`Ord` 特质会实现 `cmp` 方法,由于有效排序将始终可行,因此该方法返回的是 `Ordering` 而非 `Option`。咱们只可对那些同时实现了 `PartialOrd` 及 `Eq` (而 `Eq` 要求 `PartialEq`) 的类型,实现这个 `Ord` 特质。当于结构体及枚举上派生 `Ord` 时,`cmp` 就会以与 `PartialOrd` 下 `partial_cmp` 的派生实现同样方式行事。 + +要求 `Ord` 的一个示例,即为将一些值存储在 `BTreeSet` 这种根据值的排序,而存储数据的数据结构中时。 + + +## 用于复制值的 `Clone` 与 `Copy` + +**`Clone` and `Copy` for Duplicating Values** + + +`Clone` 特质实现了显式创建值的深拷贝,而该复制过程则可能涉及运行一些任意代码,arbitary code,与拷贝内存堆数据。请参阅第 4 章中 [“变量与数据交互方式:克隆”](Ch04_Understanding_Ownership.md#变量与数据交互方式之二克隆) 小节,了解更多有关 `Clone` 的信息。 + +派生 `Clone` 会实现 `clone` 方法,当对整个类型实现了这个方法时,其就会在该类型的各个部分上调用 `clone`。这意味着类型要派生 `Clone` 其中的全部字段或值,都必须同时实现 `Clone`。 + +需要 `Clone` 特质的一个示例,便是在切片上调用 `to_vec` 方法时。切片不持有其包含的那些类型实例,但自 `to_vec` 所返回的那个矢量值,却将需要持有他的那些实例,从而 `to_vec` 会调用各个条目上的 `clone`。因此,存储在切片中的类型,就必须实现 `Clone`。 + +`Copy` 特质实现了只通过拷贝存储在栈上的二进制位,而复制某个值;任意代码并无必要。请参阅第 4 章中 [“唯栈数据:拷贝”](Ch04_Understanding_Ownership.md#唯栈数据拷贝stack-only-data-copy),了解更多有关 `Copy` 的信息。 + +`Copy` 特质没有定义阻止编程者过载那些方法,及破坏不会有任意代码运行这个假设的任何方法。那样的话,所有编程者就都可以假定,拷贝值将会非常快。 + +咱们可在其组成部分都实现了 `Copy` 的任何类型上派生 `Copy` 特质。由于实现 `Copy` 的类型,都有着执行与 `Copy` 同样任务的一个 `Clone` 的简单实现,因此实现 `Copy` 的类型必须同时实现 `Clone`。 + +很少需要 `Copy` 特质;实现了 `Copy` 的类型,有着可供选择的优化方案,意味着咱们不必调用 `clone`,而调用 `clone` 会令到代码更简洁。 + +对于 `Copy` 下每种可能情况,咱们都可同时以 `Clone` 完成,除了代码可能更慢,或在一些地方不得不使用 `clone`。 + + +## 用于将值映射到固定大小值的 `Hash` + +**`Hash` for Mapping a Value to a Value of Fixed Size** + + +`Hash` 特质实现了取某种任意大小类型的实例,并通过使用散列函数,将那个实例映射到固定大小的值。派生 `Hash` 会实现 `hash` 方法。`hash` 放的派生实现,会将在该类型各个组成部分上调用 `hash` 的结果结合起来,这就意味着类型要派生 `Hash`,那么其全部字段,都必须同时实现 `Hash`。 + +要求 `Hash` 的一个示例,便是为了高效地存储数据,而在 `Hash` 中存储那些键时。 + + +## 用于默认值的 `Default` + +**`Default` for Default Values** + + +`Default` 特质实现了为类型创建出一个默认值。派生 `Default` 会实现 `default` 函数。`default` 函数的派生实现,会在类型的各个部分上调用 `default` 函数,意味类型要派生 `Defualt`,其中的全部字段或值,都必须同时实现 `Default`。 + +`Default::default` 函数,通常是与第 5 章中 [“使用结构体更新语法从其他实例创建出实例”](Ch05_Using_Structs_to_Structure_Related_Data.md#使用结构体更新语法从其他实例创建出实例) 小节里曾讨论过的结构体更新语法结合使用的。咱们可以定制结构体的几个字段,并在随后通过使用 `..Default::default()`,为其余字段设置并使用默认值。 + +在 `Option` 实例上使用 `unwrap_or_default` 方法时,便是需要 `Default` 特质的一个示例。当那个 `Option` 为 `None` 时,方法 `unwrap_or_default` 就将返回存储在 `Option` 中,那个类型 `T` 的 `Default::default` 结果。 diff --git a/src/appdendix/dev_tools.md b/src/appdendix/dev_tools.md new file mode 100644 index 0000000..82295dc --- /dev/null +++ b/src/appdendix/dev_tools.md @@ -0,0 +1,162 @@ +# 附录 D:一些有用开发工具 + +在此附录中,咱们会讲到 Rust 项目所提供的一些有用的开发工具。咱们将看看自动格式化、应用警告修复的一些快速方法、一种代码静态分析工具,a linter,以及与多种 IDE 的集成。 + + +## 使用 `rustfmt` 的自动格式化 + +**Automatic Formatting with `rustfmt`** + + +`rustfmt` 工具会依据社区编码风格,重新格式化咱们的代码。许多协作项目,都使用了 `rustfmt` 来防止有关编写 Rust 时使用何种风格方面的争论:每个人都使用这个工具来格式化他们的代码。 + +要安装 `rustfmt`,请键入下面的命令: + +```console +$ rustup component add rustfmt +``` + +如同 Rust 会同时给到 `rustc` 与 `cargo` 一样,此命令会给到咱们 `rustfmt` 与 `cargo-fmt`。要格式化任何 Cargo 项目,请敲入下面的命令: + +```console +$ cargo fmt +``` + +运行此命令,会重新格式化当前代码箱中全部的 Rust 代码。这只会改变编码风格,而不会改变代码语义。关于 `rustfmt` 的更多信息,请参阅 [其文档](https://github.com/rust-lang/rustfmt). + + +## 使用 `rustfix` 修复咱们的代码 + +**Fix Your Code with `rustfix`** + + +`rustfix` 工具已被 Rust 安装所包含,并可大致以咱们想要的方式,修复那些有着明确纠正问题方法的一些编译器告警。咱们之前大概率已经见到过编译器告警了。比如,设想有下面这段代码: + +文件名:`src/main.rs` + +```rust +fn do_something() {} + +fn main() { + for i in 0..100 { + do_something(); + } +} +``` + +此处,咱们正调用 `do_something` 函数 100 次,但咱们在 `for` 循环的代码体中,从未用到那个变量 `i`。Rust 就会就此对咱们发出告警: + +```console +$ cargo build + Compiling rustfix_demo v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/rustfix_demo) +warning: unused variable: `i` + --> src/main.rs:4:9 + | +4 | for i in 0..100 { + | ^ help: if this is intentional, prefix it with an underscore: `_i` + | + = note: `#[warn(unused_variables)]` on by default + +warning: `rustfix_demo` (bin "rustfix_demo") generated 1 warning + Finished dev [unoptimized + debuginfo] target(s) in 0.29s +``` + +这个告警建议咱们要使用 `_i` 做名字:其中的下划线表示咱们有意不使用这个变量。通过运行 `cargo fix` 命令,咱们就可以使用 `rustfix`,自动应用那项建议: + +```console +$ cargo fix --allow-no-vcs + Checking rustfix_demo v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/rustfix_demo) + Fixed src/main.rs (1 fix) + Finished dev [unoptimized + debuginfo] target(s) in 0.17s +``` + +当咱们再次看到 `src/main.rs`,就将发现 `cargo fix` 已修改了这段代码: + +文件名:`src/main.rs` + +```rust +fn do_something() {} + +fn main() { + for _i in 0..100 { + do_something(); + } +} +``` + +那个 `for` 循环变量,现在就被命名为了 `_i`,同时那条告警也不再出现了。 + +咱们还可使用 `cargo fix` 命令,将咱们的代码在不同 Rust 版本之间转换。有关这些 Rust 版本,在附录 E 中有讲到。 + + +## 使用 Clippy 获得更多的代码静态分析 + +**More Lints with Clippy** + +Clippy 工具是用于分析咱们代码,从而咱们可以捕获到一些常见错误,而改进咱们 Rust 代码的一套代码静态分析集合。 + +要安装 Clippy,请输入以下命令: + +```console +$ rustup component add Clippy +``` + +在任何 Cargo 项目上要运行 Clippy 的静态分析,请输入以下命令: + +```console +$ cargo clippy +``` + +比如说咱们编写了像下面这个程序这样,用到某个数学常量近似值,好比说 `pi`,的一个程序: + +文件名:`src/main.rs` + +```rust +fn main() { + let x = 3.1415; + let r = 8.0; + println!("圆的面积为 {}", x * r * r); +} +``` + +在这个项目上运行 `cargo clippy` 就会得到下面的报错: + +```console +$ cargo clippy + Checking clippy_demo v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/clippy_demo) +error: approximate value of `f{32, 64}::consts::PI` found + --> src/main.rs:2:13 + | +2 | let x = 3.1415; + | ^^^^^^ + | + = help: consider using the constant directly + = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant + = note: `#[deny(clippy::approx_constant)]` on by default + +error: could not compile `clippy_demo` due to previous error +``` + +此报错让咱们明白,Rust 已经定义了一个更精确的 `PI` 常量,且当咱们使用这个常量时,咱们的程序将更为正确。那么咱们随后就应修改咱们代码为使用这个 `PI` 常量。下面的代码就捕获导致 Clippy 的任何错误或告警: + +文件名:`src/main.rs` + +```rust +fn main() { + let x = std::f64::consts::PI; + let r = 8.0; + println!("圆的面积为 {}", x * r * r); +} +``` + +有关 Clippy 的更多信息,请参阅 [其文档](https://github.com/rust-lang/rust-clippy)。 + + +## 用到 `rust-analyzer` 的 IDE 集成 + +**IDE Integration Using `rust-analyzer`** + + +为帮助 IDE 集成,Rust 社区建议使用 [`rust-analyzer`](https://rust-analyzer.github.io/)。此工具是一套以编译器为中心,操 [语言服务器协议,Language Server Protocol](http://langserver.org/) 的实用工具;而所谓语言服务器协议,则是用于各种 IDEs 和编程语言,二者相互之间通信的一种规格。有多种不同客户端可使用 `rust-analyzer`,比如 [Visual Studio Code 的 Rust 分析器插件](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)。 + +请访问 `rust-analyzer` 项目 [主页](https://rust-analyzer.github.io/),了解其安全说明,随后在咱们的特定 IDE 中安装该语言的服务器支持。咱们的 IDE 就能获得诸如自动补全、跳至定义及行内报错等能力。 diff --git a/src/appdendix/editions.md b/src/appdendix/editions.md new file mode 100644 index 0000000..8f30660 --- /dev/null +++ b/src/appdendix/editions.md @@ -0,0 +1,27 @@ +# 附录 E:关于版本 + +**Appendix E - Editions** + +在第一章中,咱们曾看到 `cargo new` 会把一点有关某个版的元数据,添加到咱们的 `Cargo.toml` 文件。此附录就会讲到那意味着什么! + +Rust 语言及编译器有着六周的发布周期,意味着用户会得到源源不断的新功能。其他编程语言会不经常地发布较大变更;Rust 则会更频繁发布较小的更新。不久之后,全部这些小修改就会堆积起来。不过这一个个发布中,回头看看而讲到,“噢,从版本 1.10 到 1.31,Rust 改变了很多!”。则是不容易的。 + +每两三年,Rust 团队都会产生一个新的 Rust *版本,edition*。每个版本都会以完全更新的文档与工具,将那些业已落地到一个明确包中的特性放到一起。新版本会作为寻常的六周发布过程而交付。 + +这些版本服务了不同人群的不同目的: + +- 对于活跃的 Rust 用户,新版本会把那些增量变更,一起放入到一个易于掌握的包中; +- 对于那些非用户,新版本释放了一些已落地的大进展信号,这会让 Rust 或许值得再看一看; +- 对于开发 Rust 的人们,新版本会提供这个项目作为整体的集结点。 + +在本书编写时,已有三个 Rust 版本可用:Rust 2015、Rust 2018 与 Rust 2021。本书是用 Rust 2021 版本的习惯用语编写的。 + +`Cargo.toml` 中的 `edition` 键,表示应对咱们的代码使用哪个版本的编译器。若该键不存在,Rust 就会以向后兼容原因,而使用 `2015` 作为版本值。 + +每个项目都可以选择一个不同于默认 2015 的版本。这些版本可能包含了不兼容的变更,比如包含了与代码中标识符冲突的新关键字。但是,除非咱们选到这些变更,那么即使咱们更新了所使用的 Rust 编译器,咱们的代码将继续编译。 + +全部 Rust 编译器版本,都会支持先于那个编译器发布而存在的任何版本,且他们可将任何受支持版本的代码箱连接起来。版本变更只会影响编译器于编译初期解析代码的方式。因此,当咱们正使用着 Rust 2015,而咱们的一项依赖使用了 Rust 2018 时,咱们的项目将编译,并能够使用那项依赖。与之相反,在咱们的项目使用 Rust 2018,而一项依赖使用了 Rust 2015 的情形下,也会工作。 + +要明确的是:绝大多数特性,在所有版本上都将可用。使用任何 Rust 版本的开发者,都将在新的稳定发布构造出来时,发现一些改进。但是,在一些情况下,主要是在新曾了关键字时,一些新特性就会只在稍后版本中可用了。若咱们打算利用上这些新特性,咱们将需要切换版本。 + +有关更多细节,[版本指南,Edition Guide](https://doc.rust-lang.org/stable/edition-guide/) 是本列举了不同版本间差异,并解释了怎样通过 `cargo fix`,而自动将咱们的代码更新到新版的一本完整的书。 diff --git a/src/appdendix/keywords.md b/src/appdendix/keywords.md new file mode 100644 index 0000000..058ed1b --- /dev/null +++ b/src/appdendix/keywords.md @@ -0,0 +1,118 @@ +# 附录 A:关键字 + +以下清单包含了 Rust 语言当前或今后要用到的一些关键字。由此,他们便不能被用作标识符(除在 [“原始标识符”](#原始标识符) 小节中咱们将讨论的那些外)了。所谓标识符,是函数、变量、参数、结构体字段、模组、代码箱、常量、宏、静态值、属性、类型、特质或生命周期等的名字。 + + +## 当前在用的关键字 + +**Keywords Currently in Use** + + +下面是当前在用关键字的清单,带有其作用描述。 + +- `as` - 执行原生强制转换,primitive casting,消除包含着某个项目的特定特质歧义,disambiguate the specific trait containing a item,或重命名 `use` 语句中的项目; +- `async` - 返回一个 `Future` 类型值,而非阻塞当前线程; +- `await` - 在某个 `Future` 值的结果准备好前,暂停程序执行; +- `break` - 立即退出某个循环; +- `const` - 定义出常量项目或常量原始指针; +- `continue` - 继续下一循环迭代; +- `crate` - 在模组路径中,指向代码箱根; +- `dyn` - 动态调遣到某个特质对象,参考 [特质对象执行动态调遣](Ch17_Object_Oriented_Programming_Features_of_Rust.md#特质对象执行动态调遣); +- `else` - `if` 的回退,及 `if let` 控制流的构件; +- `extern` - 链接外部函数或变量; +- `false` - 布尔值假的字面值; +- `fn` - 定义出某个函数或函数指针类型; +- `for` - 对某个迭代器的项目加以迭代、实现某个特质,或指明某个更高级别的生命周期,a higher-ranked lifetime; +- `if` - 基于某个条件表达式结果的分支; +- `impl` - 实现固有或特质功能,implement inherent or trait functionality; +- `in` - `for` 循环语法的一部分; +- `let` - 绑定某个变量; +- `loop` - 无条件地循环; +- `match` - 将某个值与模式匹配; +- `mod` - 定义出模组; +- `move` - 领导闭包取得其所有捕获值的所有权; +- `mut` - 注解出引用、原始指针或模式绑定等中的可变性; +- `pub` - 注解出结构体、`impl` 代码块或模组等中的公开可见性; +- `ref` - 按引用绑定; +- `return` - 自函数返回值; +- `Self` - 咱们正定义或实现中类型的类型别名; +- `self` - 方法主体,method subject,或当前模组; +- `static` - 在整个程序执行过程持续有效的全局变量或生命周期; +- `struct` - 定义出某个结构体; +- `super` - 当前模组的父模组; +- `trait` - 定义出某个特质; +- `true` - 布尔值真的字面值; +- `type` - 定义出某个类型别名或关联类型; +- `union` - 定义出某个 [联合体](https://doc.rust-lang.org/reference/items/unions.html),是在联合体声明时用到的唯一关键字; +- `unsafe` - 注解非安全代码、函数、特质或一些实现; +- `use` - 将符号带入到作用域; +- `where` - 注解约束某个类型的子句; +- `while` - 基于某个表达式结果而有条件的循环。 + + +## 为今后使用保留的关键字 + +**Keywords Reserved for Future Use** + + +以下关键字尚无任何功能,但被 Rust 为今后的潜在使用而保留。 + +- `abstract` +- `become` +- `box` +- `do` +- `final` +- `macro` +- `override` +- `priv` +- `try` +- `typeof` +- `unsized` +- `virtual` +- `yield` + + +## 原始标识符 + +**Raw Identifiers** + + +*原始标识符,raw identifiers* 属于允许实现使用一般不被允许关键字的语法。是通过在关键字前加上前缀 `r#`,使用原始标识符的。 + +比如,`match` 是个关键字。在咱们尝试编译下面这个使用 `match` 作其名字的函数时: + +文件名:`src/main.rs` + +```rust +fn match(needle: &str, haystack: &str) -> bool { + haystack.contains(needle) +} +``` + +咱们将得到这样的报错: + +```console +error: expected identifier, found keyword `match` + --> src/main.rs:1:4 + | +1 | fn match(needle: &str, haystack: &str) -> bool { + | ^^^^^ expected identifier, found keyword +``` + +该报错显示咱们无法将关键字 `match` 用作函数标识符。要将 `match` 用作函数名字,咱们就需要使用原始标识符语法,像下面这样: + +文件名:`src/main.rs` + +```rust +fn r#match(needle: &str, haystack: &str) -> bool { + haystack.contains(needle) +} + +fn main() { + assert! (r#match("foo", "foobar")); +} +``` + +此代码将不带任何错误地编译。请注意那个函数的定义中,与 `main` 中该函数被调用处其名字上的 `r#` 前缀。 + +原始标识符实现了将任何咱们所选的词语用作标识符,即使那个词语碰巧是个保留的关键字。这给到咱们更自由地选择标识符名字,以及实现与一些以其中这些词语不属于关键字的语言,所编写的程序集成。此外,原始标识符实现了,对那些以不同于咱们代码箱 Rust 版本编写库加以运用。比如,在 2015 版中 `try` 就不是个关键字,但在 2018 版本中却是。若咱们依赖于一个使用 2015 版本编写的库,而该库有一个 `try` 函数,那么咱们就将需要在这种情况下,使用原始标识符 `r#try`,来从咱们的 2018 版本的代码,调用那个函数。请参阅 [附录 E](#appendix-e) 了解更多有关版本的信息。 diff --git a/src/appdendix/notes.md b/src/appdendix/notes.md new file mode 100644 index 0000000..d6fa24e --- /dev/null +++ b/src/appdendix/notes.md @@ -0,0 +1,43 @@ +# 附录 H - 有用笔记 + +此处记录学习及应用 Rust 编程软件过程中,觉得有用的一些东西。 + + +## `cargo-binutils` + +[这个项目](https://github.com/rust-embedded/cargo-binutils) 是 Embbeded-Rust 项目的,而不是 Rust 官方的,但提供了有用的功能。比如查看构建出的二进制程序文件的那些头部: + + +```console +$ cargo readobj --bin clippy_demo -- --file-headers + Finished dev [unoptimized + debuginfo] target(s) in 0.00s +ELF Header: + Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 + Class: ELF64 + Data: 2's complement, little endian + Version: 1 (current) + OS/ABI: UNIX - System V + ABI Version: 0 + Type: DYN (Shared object file) + Machine: Advanced Micro Devices X86-64 + Version: 0x1 + Entry point address: 0x86D0 + Start of program headers: 64 (bytes into file) + Start of section headers: 4305200 (bytes into file) + Flags: 0x0 + Size of this header: 64 (bytes) + Size of program headers: 56 (bytes) + Number of program headers: 12 + Size of section headers: 64 (bytes) + Number of section headers: 42 + Section header string table index: 41 +``` + +使用前需要进行如下安装: + +```console +$ cargo install cargo-binutils +$ rustup component add llvm-tools-preview +``` + + diff --git a/src/appdendix/ops_and_symbols.md b/src/appdendix/ops_and_symbols.md new file mode 100644 index 0000000..cb2639d --- /dev/null +++ b/src/appdendix/ops_and_symbols.md @@ -0,0 +1,202 @@ +# 附录 B:运算符与符号 + +此附录包含了 Rust 语法的词汇表,包括运算符及别的一些,自己单独出现或出现于路径、泛型、特质边界、宏、属性、注释、元组及方括符等上下文中的符号。 + + +## 运算符 + +**Operators** + + +表 B-1 包含了 Rust 中的符号、该符号将如何出现于上下文中的一个示例、简单的解释,以及该运算符是否可过载。若某个运算符可以过载,就会列出过载那个运算符要用到的相关特质。 + +**表 B-1:运算符** + +| 运算符 | 示例 | 说明 | 是否可以过载 | +| :--- | :--- | :--- | :--- | +| `!` | `ident! (...)`
`ident! {...}`
`ident! [...]` | 宏扩展 | | +| `!` | `!expr` | 按位或逻辑求补运算 | 否 | +| `!=` | `expr != expr` | 不等比较 | `PartialEq` | +| `%` | `expr % expr` | 算术求余运算 | `Rem` | +| `%=` | `var %= expr` | 算术求余并赋值 | `RemAssign` | +| `&` | `&expr`, `&mut expr` | 借用 | | +| `&` | `&type`, `&mut type`, `&'a type`, `&'a mut type` | 借用指针类型 | | +| `&` | `expr & expr` | 按位与(AND)运算 | `BitAnd` | +| `&=` | `var &= expr` | 按位与(AND)运算并赋值 | `BitAndAssign` | +| `&&` | `expr && expr` | 短路逻辑与(AND)运算,short-circuit logical AND | | +| `*` | `expr * expr` | 算术乘法运算 | `Mul` | +| `*=` | `var *= expr` | 算术乘法运算并赋值 | `MulAssign` | +| `*` | `*expr` | 解引用运算 | `Deref` | +| `*` | `*const type`, `*mut type` | 原始指针运算 | | +| `+` | `trait + trait`, `'a + trait` | 复合类型约束运算 | | +| `+` | `expr + expr` | 算术加法运算 | `Add` | +| `+=` | `var += expr` | 算术加法运算并赋值 | `AddAssign` | +| `,` | `expr, expr` | 参数与元素分隔符 | | +| `-` | `- expr` | 算术取反运算 | `Neg` | +| `-` | `expr - expr` | 算术减法运算 | `Sub` | +| `-=` | `var -= expr` | 算术减法运算并赋值 | `SubAssign` | +| `->` | `fn(...) -> type`, |...| -> type | 函数与闭包的返回值类型 | | +| `.` | `expr.ident` | 成员访问 | | +| `..` | `..`, `expr..`, `..expr`, `expr..expr` | 排除右侧的范围语法字面值 | `PartialOrd` | +| `..=` | `..=expr`, `expr..=expr` | 包含右侧范围语法字面值 | `PartialOrd` | +| `..` | `..expr` | 结构体更新语法 | | +| `..` | `variant(x, ..)`, `struct_type { x, .. }` | “等等” 模式绑定,"And the rest" pattern binding | | +| `...` | `expr...expr` | (已弃用,请使用 `..=` 代替)在模式中:包含式范围模式 | | +| `/` | `expr / expr` | 算术除法运算 | `Div` | +| `/=` | `var /= expr` | 算术除法并赋值 | `DivAssign` | +| `:` | `pat: type`, `ident: type` | 约束 | | +| `:` | `ident: expr` | 结构体字段初始化 | | +| `:` | `'a: loop {...}` | 循环标签 | | +| `;` | `expr;` | 语句及项目的终止符 | | +| `;` | `[..., len]` | 固定大小数组语法的一部分 | | +| `<<` | `expr << expr` | 向左移位运算 | `Shl` | +| `<<=` | `var <<= expr` | 向左移位运算并赋值 | `ShlAssign` | +| `<` | `expr < expr` | 小于比较 | `PartialOrd` | +| `<=` | `expr <= expr` | 小于等于比较 | `PartialOrd` | +| `=` | `var = expr`, `ident = type` | 赋值/等价,equivalence | | +| `==` | `expr == expr` | 相等比较 | `PartialEq` | +| `=>` | `pat => expr` | 匹配支臂语法的一部分 | | +| `>` | `expr > expr` | 大于比较 | `PartialOrd` | +| `>=` | `expr >= expr` | 大于等于比较 | `PartialOrd` | +| `>>` | `expr >> expr` | 向右位移运算 | `Shr` | +| `>>=` | `var >>= expr` | 向右位移运算并赋值 | `ShrAssign` | +| `@` | `ident @ pat` | 模式绑定 | | +| `^` | `var ^ expr` | 按位异或运算 | `BitXor` | +| `^=` | `var ^= expr` | 按位异或运算并赋值 | `BitXorAssign` | +| | | pat | pat | 模式选择,pattern alternatives | | +| | | expr | expr | 按位或(OR)运算 | `BitOr` | +| |= | var |= expr | 按位或(OR)运算并赋值 | `BitOrAssign` | +| || | expr || expr | 短路逻辑或运算,Short-circuiting logical OR | | +| `?` | `expr?` | 错误传递 | | + + +## 非运算符的符号 + +**Non-operator Symbols** + + +以下清单包含了不以运算符发挥作用的全部符号;那就是说,他们不会表现得像函数或方法调用。 + +表 B-2 给出了自己单独出现,并在多种场合有效的一些符号。 + +**表 B-2:独立语法,Stand-Alone Syntax** + +| 符号 | 说明 | +| :--- | :--- | +| `'ident` | 命名的生命周期或循环标签 | +| `...u8`, `...i32`, `...f64`, `...usize` 等等 | 指定类型的数字字面值 | +| `"..."` | 字符串字面值 | +| `r"..."`, `r#"..."#`, `r##"..."##` 等等 | 原始字符串字面值,其中的转义字符不会被处理 | +| `b"..."` | 字节字符串字面值;构造出一个字节数组而非字符串 | +| `br"..."`, `br#"..."`, `br##"..."##` 等等 | 原始字节字符串字面值,是原始与字节字符串字面值的结合 | +| `'...'` | 字符字面值 | +| `b'...'` | ASCII 字节字面值 | +| |...| expr | 闭包 | +| `!` | 发散函数下总是空的底部类型,always empty bottom type for diverging functions | +| `_` | “忽略,ignored” 模式绑定;还用于令到整数字面值可读,also used to make integer literals readable | + + +表 B-3 展示了出现在模组层次结构中到某个项目路径上下文中的一些符号。 + +**表 B-3:路径相关的语法** + +| 符号 | 说明 | +| :--- | :--- | +| `ident::ident` | 命名空间路径 | +| `::path` | 相对于代码箱根的路径(比如,某个显式绝对路径) | +| `self::path` | 相对于当前模组的路径(比如,某个显式相对路径) | +| `super::path` | 相对于当前模组父模组的路径 | +| `type::ident`, `::ident` | 关联的常量、函数及类型 | +| `::...` | 无法直接命名的某个类型的关联项目(比如,`<&T>::...`, `<[T]>::...` 等等) | +| `trait::method(...)` | 通过命名出定义方法的类型,消除该方法调用的歧义 | +| `::method(...)` | 通过命名出特质与类型,消除方法调用的歧义 | + +表 B-4 展示了出现在运用泛型参数上下文中的一些符号。 + +**表 B-4:泛型** + +| 符号 | 说明 | +| :-- | :-- | +| `path<...>` | 指明类型中的泛型参数(比如,`Vec`) | +| `path::<...>`, `method::<...>` | 指明表达式中泛型、函数或方法的参数;通常这被称作涡轮鱼语法,turbofish(比如,`"42".parse::()`,关于 Rust 的 turbofish 语法,请参考:[What is Rust's turbofish](https://techblog.tonsser.com/posts/what-is-rusts-turbofish)),[RUST 中的 turbofish 语法(一)](https://www.jianshu.com/p/9107685ece03) ... | +| `fn ident<...> ...` | 定义出泛型函数 | +| `struct ident<...> ...` | 定义出泛型结构体 | +| `enum ident<...> ...` | 定义出泛型枚举 | +| `impl<...> ...` | 定义出泛型实现 | +| `for<...> type` | 高阶声明周期边界,higher-ranked lifetime bounds | +| `type` | 其中一个或更多的关联类型有着指定赋值的某种泛型(a generic type where one or more associated types have specific assignments,比如,`Iterator`) | + +下表 B-5 展示了出现在使用特质边界的约束性泛型参数上下文中的一些符号,table B-5 shows symbols that appear in the context of constraining generic type parameters with trait bounds。 + +**B-5:特质边界约束,Trait Bound Constrains** + +| 符号 | 说明 | +| :--- | :--- | +| `T: U` | 泛型参数 `T` 受实现了 `U` 的类型约束 | +| `T: 'a` | 泛型 `T` 必须要比生命周期 `'a` 活得更久,generic type `T` must outlive lifetime `'a`(意思是该类型不能间接地包含任何生命周期短于 `'a` 的引用) | +| `T: 'static` | 泛型 `T` 不包含除 `'static` 的引用外的其他引用 | +| `'b: 'a` | 泛型生命周期 `'b` 必须要比 `'a` 存活得更久 | +| `T: ?Sized` | 允许泛型参数为动态大小类型 | +| `'a + trait`, `trait + trait` | 复合的类型约束 | + +下表 B-6 展示了出现在宏调用或定义上下文中,并指明了某个项目上属性的一些符号。 + +**B-6:宏与属性** + +| 符号 | 说明 | +| :--- | :--- | +| `#[meta]` | 外层属性 | +| `#![meta]` | 内层熟悉 | +| `$ident` | 宏代换,macro substitution | +| `$ident:kind` | 宏捕获 | +| `$(...) ...` | 宏重复,macro repetition | +| `ident! (...)`, `ident! {...}`, `ident! [...]` | 宏调用,macro invocation | + +下表 B-7 展示了创建注释的一些符号。 + +**表 B-7:注释** + +| 符号 | 说明 | +| :--- | :--- | +| `//` | 注释行 | +| `//!` | 内层行文档注释,inner line doc comment | +| `///` | 外层行文档注释,outter line doc comment | +| `/*...*/` | 注释块 | +| `/*!...*/` | 内层块文档注释,inner block doc comment | +| `/**...*/` | 外层块文档注释,outter block doc comment | + +下表 B-8 展示了出现于用到元组上下文中的一些符号。 + +**元组** + +| 符号 | 说明 | +| :--- | :--- | +| `()` | 空元组(又叫单元值),同时属于字面值与类型 | +| `(expr)` | 元括号括起来的表达式,parenthesized expression | +| `(expr,)` | 单一元素的元组表达式 | +| `(type,)` | 单一元素的元组类型,single-element tuple type | +| `(expr, ...)` | 元组表达式 | +| `(type, ...)` | 元组类型,tuple type | +| `expr(expr, ...)` | 函数调用表达式;还用于初始化一些元组的 `struct` 以及元组的 `enum` 变种,function call expression; also used to initialize tuple `struct`s and tuple `enum` vairants | +| `expr.0`, `expr.1` 等等 | 对元组进行索引 | + +下表 B-9 展示了其中用到花括号上下文中的一些符号。 + +**表 B-9:花括号** + +| 符号 | 说明 | +| :--- | :--- | +| `{...}` | 代码块表达式 | +| `Type {...}` | `struct` 的字面值 | + +下表 B-10 展示了其中用到方括号上下文中的一些符号。 + +**表 B-10:方括号** + +| 符号 | 说明 | +| :--- | :--- | +| `[...]` | 数组的字面值 | +| `[expr; len]` | 包含着 `expr` 的 `len` 拷贝数组的字面值 | +| `[type; len]` | 包含着 `len` 个 `type` 的实例数组的字面值 | +| `expr[expr]` | 对集合进行索引,collection indexing。是可过载的 `(Index, IndexMut)`,overloadable `(Index, IndexMut)` | +| `expr[..]`, `expr[a..]`, `expr[..b]`, `expr[a..b]` | 用到了 `Range`、`RangeFrom`、`RangeTo` 或 `RangeFull` 作为 “索引”的,带有集合切片集合索引,collection indexing pretending to be collection slicing, using `Range`, `RangeFrom`, `RangeTo`, or `RangeFull` as the "index" | diff --git a/src/appdendix/releases.md b/src/appdendix/releases.md new file mode 100644 index 0000000..781fe50 --- /dev/null +++ b/src/appdendix/releases.md @@ -0,0 +1,141 @@ +# 附录 G - Rust 是怎样构造出来的与每日发布 + +**How Rust is Made and "Nightly Rust"** + +此附录是有关 Rust 被怎样构造出来,及那会怎样作为一名 Rust 开发者的你。 + + +## 强调稳定却并无止步不前 + +**Stability Without Stagnation** + + +作为一门语言,Rust 在注重咱们代码稳定性方面 *用心良苦*。咱们希望 Rust 成为你可以在其上构建软件的稳固基础,而若那些物件都一直变动,那将是不可能实现的。而与此同时,若咱们无法实验一些新特性,那么直到这些特性发布后咱们不能在修改一些东西时,咱们也不会发现一些重大缺陷。 + +对于这个问题,咱们(Rust 团队)的解决方案就是咱们称作 “强调稳定又不要止步不前”,而咱们的直到原则是这样的:你永不必害怕升级到稳定的Rust 新版本。每次升级都应是无痛的,而又应带给你一些新特性、更少的程序错误,以及更快的编译时间。 + + +## 啾,啾!发布通道与搭上快车 + +**Choo, Choo! Release Channels and Riding the Trains** + + +Rust 的开发,是运作在 *火车时刻表,train schedule* 上的。那就是说,全部开发都是在 Rust 代码仓库的 `master` 分支上完成的。各个发布遵循了软件发布列车模型,a software release train model,该发布模型业已为 Cisco IOS 及其他软件项目所使用。Rust 有着以下三个 *发布通道,release channels*: + +- 每日发布,nightly +- Beta 发布,beta +- 稳定发布,stable + +多数 Rust 开发者主要使用稳定通道,而那些希望尝试实验性新特性的人们,则会使用每日发布或 beta 通道。 + +下面是个开发与发布流程运作方式的一个示例:咱们来假定 Rust 团队正工作于 Rust 1.5 的发布上。那个发布发生于 2015 年 11 月,但其将提供到我们实际版本数字。有个新特性被添加到 Rust:一次新提交落在了 `master` 分支。每天晚上,都有一个新的 Rust 每日版本被产生出来。每天都是个发布日,而这些发布是由咱们的发布基础设施自动创建的。因此随着时间流逝,咱们的发布看起来就像下面这样,每晚一次: + +```text +nightly: * - - * - - * +``` + +每隔六周,便是要准备一个新发布的时候了!Rust 代码仓库的 `beta` 分支,便会从由每日发布所使用的 `master` 分支分叉开来。现在,就有了两个分支: + +```text +nightly: * - - * - - * + | +beta: * +``` + +多数 Rust 使用者不会积极使用这些 beta 发布,但会在他们的 CI 系统中就 beta 发布加以测试,以帮助 Rust 发现可能出现的倒退。与此同时,仍有着每晚的每日发布: + +```text +nightly: * - - * - - * - - * - - * + | +beta: * +``` + +在首个 beta 版创建出来六周后,就是稳定发布的时候了!`stable` 分支就被从 `beta` 分支创建出来: + +```text +nightly: * - - * - - * - - * - - * - - * - * - * + | +beta: * - - - - - - - - * + | +stable: * +``` + +好!Rust 1.5 便完成了!不过,咱们忘了一件事:由于这六个星期以及过去,而咱们还需要 Rust *下一* 版本,1.6,的一个新的 beta 发布。因此在 `stale` 分支从 `beta` 分支分叉出来后,下一版本的 `beta` 又会从 `nightly` 再度分叉出来: + +```text +nightly: * - - * - - * - - * - - * - - * - * - * + | | +beta: * - - - - - - - - * * + | +stable: * +``` + +每六周就有一个发布 “离站”,但发布过程仍务必要在其抵达稳定发布前,经由这个 beta 通道行驶一段路程,由此这个过程便被称为 “列车模型”。 + +Rust 每六周发布,像时刻表一样。若咱们知道了一个 Rust 发布的日期,那么就能直到下一发布的日期:那便是六周后。每六周安排一次发布的一个好处,便是下一班列车很快就会到来。若某项特性刚好错过了某个特定发布,那么无需担心:另一发布将在不久后发生!这有助于减少在临近发布截止日期时,有可能未完善的功能偷偷潜入的压力。 + + +归功于这个流程,咱们可以始终检出,check out,下一构建的 Rust,并自己验证到升级是容易的:若 beta 发布没有如预期那样工作,咱们就可以将其报告给 Rust 团队,并在下一稳定发布发生前修好他!beta 发布中的损坏相对较少,但 `rustc` 仍属于一个软件,而确实存在一些错误。 + + +## 不稳定特性 + +**Unstable Features** + + +这种发布模型下,还有一个好处:不稳定特性。Rust 使用了一种名为 “特性标识,feature flags” 的技巧,来确定出给定发布中启用了哪些特性。若某项新特性处于活跃开发中,他就会落地在 `master` 分支上,而由此就会在每日发布中,但会有着一个 *特性标识*。而咱们,作为用户,希望尝试这个进展中的特性,the work-in-progress feature,时,咱们是可以尝试的,但必须使用 Rust 的每日发布,并使用恰当的标识来注解咱们的代码,来选用该特性。 + +若咱们使用着 beta 或稳定发布的 Rust,那么就不能使用任何特性标识。这是 Rust 团队在声明那些新特性永久稳定前,允许咱们实际用到他们的关键。希望选用最新特性的人们,便可这样做,而想要一种扎实体验的人,则可坚持使用稳定发布,而清楚他们的代码不会破坏。这便是稳定但并非止步不前。 + +由于那些工作中的特性仍在便会,且在本书写作时和他们在稳定构建中启用时,其间他们肯定将有所不同,因此本书只包含了那些稳定特性的信息。咱们可以在线上找到那些仅每日发布有的特性文档。 + + +## Rustup 与 Rust 每日发布所扮演的角色 + +**Rustup and the Role of Rust Nightly** + + +Rust 令到易于在全局或每个项目基础上,从不同发布通道的 Rust 之间改变。默认情况下,咱们将安装稳定发布的 Rust。而比如要安装每日发布: + +```console +$ rustup toolchain install nightly +``` + +咱们也可以使用 `rustup`,查看全部的 *工具链,toolchains* (Rust 的各个发布与关联组件)。下面就是本书一位作者的 Windows 计算机上的示例: + +```powershell +> rustup toolchain list +stable-x86_64-pc-windows-msvc (default) +beta-x86_64-pc-windows-msvc +nightly-x86_64-pc-windows-msvc +``` + +> 在 Linux 系统上的输出如下: + +```console +$ rustup toolchain list +stable-x86_64-unknown-linux-gnu (default) +``` + +可以看到,稳定发布的工具链是默认的。绝大多数 Rust 用户会在多数时候使用稳定发布。咱们可能想要在多数时候使用稳定发布,又因为咱们关心某项最新特性,而会在特定项目使用每日发布。要这样做,就可以在那个项目目录下,使用 `rustup override` 来将每日发布工具链,设置为当咱们位处那个目录中时,`rustup` 使用的那个工具链: + +```console +$ cd ~/projects/needs-nightly +$ rustup override set nightly +``` + +现在,当咱们每次在 `~/projects/needs-nightly` 目录下调用 `rustc` 或 `cargo` 时,`rustup` 都会确保咱们在使用每日发布的 Rust,而非咱们默认的稳定发布 Rust 了。再有很多 Rust 项目时,这就会排上用场! + + +## 请求评议流程与各种团队 + +**The RFC Process and Teams** + + +那么咱们该怎么了解到这些新特性呢?Rust 的开发模型,遵循了 *请求评议流程,Request For Comments(RFC) process*。如你想要 Rust 的一项改进,那么就可以编写一个名为请求评议,RFC 的提议。 + +人人都可以编写请求评议来改进 Rust,同时这些提议会经过由许多议题子团队所组成的 Rust 团队审阅和讨论。[在 Rust 网站上](https://www.rust-lang.org/governance) 有这些团队的完整清单,其中包括了该项目各领域:语言设计、编译器实现、基础设施、文档及其他等的团队。恰当的团队会阅读提议与评论,撰写出他们自己的一些评论,并在最后,便有了接受或拒绝该特性的共识。 + +若该特性被接受了,就会在 Rust 代码仓库上开出一个 issue,同时某个人就可以实现他。将其实现得非常棒的那个人,可能不是最早提议这项特性的那人!在实现准备好时,其就会落地于 `master` 分支的特性门,a feature gate,之后,如同咱们曾在 [“不稳定特性”](#不稳定特性) 小节中曾讨论过的那样。 + +过了一段时间后,一旦那些用到每日发布的 Rust 开发者们,能够试用这项新特性,那么 Rust 团队成员将讨论这项特性,怎样将其编制到每日发布上,并决定其是否有那个被构造到稳定发布 Rust。而若决定是继续推进,那么特性门就会被移除,同时这项特性就被认为是稳定的了!他就会搭上列车,进到一个新的稳定发布 Rust 中。 diff --git a/src/appdendix/terminology_list.md b/src/appdendix/terminology_list.md new file mode 100644 index 0000000..67bce52 --- /dev/null +++ b/src/appdendix/terminology_list.md @@ -0,0 +1,164 @@ +# 附录 I - 术语清单 + + +- 命令行界面 + +Command-Line Interface,在终端里运行的应用,与在 GUI 窗口中应用不同。 + +- 模组系统 + +The module system,大型程序中,组织代码的方式。 + + +- 迁移所有权 + +在闭包参数清单前,使用 `move` 关键字,让闭包取得其用到的所在环境中的值所有权。 + +- 关联类型 + +Associated type, 是通过 `type` 关键字,定义在特质下的类型。咱们知道方法即为关联函数,associated function,那么关联类型自然与关联函数有些类似。 + + +- 消费适配器 + +Consuming adaptor, `Iterator` 特质上,会调用到迭代器 `next` 方法的一些方法,由于这些方法会耗尽迭代器,故他们被称为消费适配器。 + + +- 迭代器适配器 + +Iterator adaptor,`Iterator` 特质上,通过改变原迭代器某些方面而产生出另一迭代器的一些方法。 + + +- 零成本抽象 + +Zero-cost abstractions,相较于一般实现,语言提供的高级抽象在编译后生成的代码,与自己努力编写出的优化低级别代码类似。故使用高级抽象是没有运行时开销的。 + + +- 展开优化 + +Unrolling,Rust 编译器在编译迭代器代码时,会把已知的历次迭代展开为重复代码,而实现性能优化。 + + +- 文档注释 + +Documentation comment, 将产生出 HTML 的注释。 + + +- 重导出程序项目 + +Re-export, 使用 `pub use` 重新导出程序项目。 + + +- 语义版本控制规则 + +Semantic Versioning rules, 又大版本、小版本及补丁版本构成的,形如 `MAJOR.MINOR.PATCH` 的版本编号规则。参考:[semver.org](https://semver.org)。 + + +- 工作区 + +Workspace,为有着多个库代码箱的大型项目组织的一项 Cargo 特性。 + + +- 编译出的物件 + +The compiled artifacts + + +- 路径依赖 + +A path dependency + + +- 匣子类型(数据结构) + +`Box`,由存储在栈上的指针,与存储在堆上的数据,实现的一种数据结构。 + + +- 间接 + +Indirection, 匣子类型的变量,通过保存指向数据在内存堆上的地址,而间接保存了数据。 + + +- 解引用强制转换 + +Deref coercion,类似于其他语言的开箱操作。 + + +- 元组结构体 + +A tuple struct, 形式为 `struct MyBox(T)`,是保持着只有一个元素元组的结构体,`Box` 的数据结构为元组结构体。 + + +- 前奏 + +The Rust Prelude, `std::prelude` 模组。前奏是 Rust 自动导入到每个 Rust 程序中的东西的列表。他被保持在尽可能小的范围内,并且专注于几乎每个 Rust 程序都会用到的东西,特别是特质。参见:[`std::prelude`](https://doc.rust-lang.org/std/prelude/index.html)。 + + +- 内部可变性模式 + +The interior mutability pattern, Rust 的一种设计模式,用于改变不可变值内部的某个值。 + + +- 内存泄漏 + +Memory leak, 出现未清理内存的情况。 + + +- 关联类型 + +An associated type, 通过 `type Target = t;` 这种语法声明出的类型,是声明泛型参数的一种稍微不同的方式。 + + +- 单态化 + +所谓 *单态化,monomorphization*,是指即通过把在编译后用到的具体类型填入到泛型位置,而将通用代码转换为具体代码的过程。参考 [使用泛型代码的性能问题](Ch10_Generic_Types_Traits_and_Lifetimes.md#使用泛型参数代码的性能问题)。 + + +- 内聚属性 + +a property called *coherence*,参见 [在类型上实现某个特质](Ch10_Generic_Types_Traits_and_Lifetimes.md#在类型上实现某个特质)。 + + +- 孤儿规则 + +the orphan rule, 参见 [在类型上实现某个特质](Ch10_Generic_Types_Traits_and_Lifetimes.md#在类型上实现某个特质)。 + + +- `impl Trait` 语法 + +`impl Trait` syntax, 在函数参数清单中,将特质用作参数类型注解的语法。参见:[作为参数的特质](Ch10_Generic_Types_Traits_and_Lifetimes.md#作为参数的特质) + + +- 特质边界语法 + +Trait bound syntax, 参见 [特质边界语法](Ch10_Generic_Types_Traits_and_Lifetimes.md#特质边界语法) + + +- 语法糖 + +Sugar syntax, 参见 [特质边界语法](Ch10_Generic_Types_Traits_and_Lifetimes.md#特质边界语法) + + +- 指明多个特质边界的 `+` 语法 + +The `+` syntax for specifying multiple trait bounds, 参见:[使用 + 语法,指定多个特质边界](Ch10_Generic_Types_Traits_and_Lifetimes.md#使用--语法指定多个特质边界) + + +- `where` 子句 + +`where` clauses, 参见 []() + + +- 生命周期省略规则 + +Lifetime elision rules, 编程到 Rust 引用分析中的一些确定性模式。 + + +- 输入生命周期 + +Input lifetimes,函数或方法上的生命周期 + + +- 输出生命周期 + +Output lifetimes, 返回值上的生命周期 diff --git a/src/appdendix/translations.md b/src/appdendix/translations.md new file mode 100644 index 0000000..0831616 --- /dev/null +++ b/src/appdendix/translations.md @@ -0,0 +1,4 @@ +# 附录 F - 本书的一些译本 + +<略> + diff --git a/src/final_project/graceful_shutdown.md b/src/final_project/graceful_shutdown.md new file mode 100644 index 0000000..9b0989c --- /dev/null +++ b/src/final_project/graceful_shutdown.md @@ -0,0 +1,453 @@ +## 优雅关机与内存清理 + +**Graceful Shutdown and Cleanup** + + +清单 20-20 中的代码,经由线程池而如咱们所设想的那样,异步响应请求。咱们会收到有关 `workers`、`id` 及 `thread` 这三个,咱们未以直接方式用到字段的一些告警,这些告警就提醒了咱们,咱们没有清理任何东西。当咱们使用不那么优雅的 `Ctrl + c` 方式,来挂起主线程时,全部其他线程也会被立即停止,即使他们处于服务某个请求中。 + +接下来,咱们随后将实现 `Drop` 特质,以在线程池中各个线程上调用 `join`,如此这些线程便可以在关闭前,完成他们正工作于其上的请求。随后咱们将实现一种告知线程他们应停止接受新请求并关闭的方法。为观察这方面代码的运作,咱们将把咱们的服务器,修改为在有序关闭其线程池之前,只接受两个请求。 + + +## 实现 `ThreadPool` 上的 `Drop` 特质 + +**Implementing the `Drop` Trait on `ThreadPool`** + + +咱们来以在咱们的线程池上实现 `Drop` 开始。当线程池被丢弃时,咱们的那些线程就都应归拢,join 一下,以确保他们完成他们的工作。下面清单 20-22 给出了 `Drop` 实现的首次尝试;此代码尚不会很好地编译。 + +文件名:`src/lib.rs` + +```rust +impl Drop for ThreadPool { + fn drop(&mut self) { + for worker in &mut self.workers { + println! ("关闭 worker {}", worker.id); + + worker.thread.join().unwrap(); + } + } +} +``` + +*清单 20-22:在线程池超出作用域时归拢各个线程* + +> 注:关于线程的 `join` 方法,请参考 [Java Thread.join详解](https://zhuanlan.zhihu.com/p/57927767),[Joining Threads in Java](https://www.geeksforgeeks.org/joining-threads-in-java/)。 + +首选,咱们遍历了线程池中 `workers` 的各个线程。由于 `self` 是个可变引用,且咱们还需要能修改 `worker`,因此咱们为这个遍历使用了 `&mut`。对于各个 `worker`,咱们打印了讲到这个特定 `worker` 正要关闭的一条消息,并在随后在那个 `worker` 的线程上调用了 `join`。当到 `join` 的这个调用失败时,咱们便使用 `unwrap` 来令到 Rust 终止运行,并进入到非优雅有序关闭。 + +下面时在咱们编译这代码时,得到的报错信息: + +```console +$ cargo check + Checking hello v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/hello) +error[E0507]: cannot move out of `worker.thread` which is behind a mutable reference + --> src/lib.rs:71:13 + | +71 | worker.thread.join().unwrap(); + | ^^^^^^^^^^^^^ ------ `worker.thread` moved due to this method call + | | + | move occurs because `worker.thread` has type `JoinHandle<()>`, which does not implement the `Copy` trait + | +note: `JoinHandle::::join` takes ownership of the receiver `self`, which moves `worker.thread` + --> /rustc/2c8cc343237b8f7d5a3c3703e3a87f2eb2c54a74/library/std/src/thread/mod.rs:1589:17 + +For more information about this error, try `rustc --explain E0507`. +error: could not compile `hello` due to previous error +``` + +这个报错告诉我们,由于咱们只有各个 `worker` 的可变借用,而 `join` 会取得其参数的所有权,因此咱们无法调用 `join`。为解决这个额外难题,咱们就需要将线程从拥有 `thread` 的 `Worker` 实例迁出,如此 `join` 就可以消费那个线程了。咱们曾在清单 17-15 中这样做过:若 `Worker` 保存的是一个 `Option>`,那么咱们就可以在 `Option` 上调用 `take` 方法,来将 `Some` 变种中的那个值迁出,并在其位置处留下一个 `None`。也就是说,正运行的一个 `Worker`,将有着 `thread` 中的一个 `Some` 变种,而当咱们打算清理某个 `Worker` 时,咱们就将以 `None` 来替换 `Some`,如此那个 `Worker` 就没有了要运行的线程了。 + +因此咱们就明白了咱们是要如下更新 `Worker` 的定义: + +文件名:`src/lib.rs` + +```rust +struct Worker { + id: usize, + thread: Option>, +} +``` + +现在咱们来依靠编译器,找出其他需要修改的地方。对此代码进行检查,咱们会得到两个报错: + +```console +$ cargo check + Checking hello v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/hello) +error[E0308]: mismatched types + --> src/lib.rs:62:22 + | +62 | Worker { id, thread } + | ^^^^^^ expected enum `Option`, found struct `JoinHandle` + | + = note: expected enum `Option>` + found struct `JoinHandle<_>` +help: try wrapping the expression in `Some` + | +62 | Worker { id, thread: Some(thread) } + | +++++++++++++ + + +error[E0599]: no method named `join` found for enum `Option` in the current scope + --> src/lib.rs:71:27 + | +71 | worker.thread.join().unwrap(); + | ^^^^ method not found in `Option>` + | +note: the method `join` exists on the type `JoinHandle<()>` + --> /rustc/2c8cc343237b8f7d5a3c3703e3a87f2eb2c54a74/library/std/src/thread/mod.rs:1589:5 +help: consider using `Option::expect` to unwrap the `JoinHandle<()>` value, panicking if the value is an `Option::None` + | +71 | worker.thread.expect("REASON").join().unwrap(); + | +++++++++++++++++ + +Some errors have detailed explanations: E0308, E0599. +For more information about an error, try `rustc --explain E0308`. +error: could not compile `hello` due to 2 previous errors +``` + +咱们来解决那第二个错误,其指向了 `Worker::new` 末尾的代码;在创建新 `Worker` 时,咱们需要把那个 `thread` 值封装在 `Some` 中。请做出如下修改来修复这个错误: + +文件名:`src/lib.rs` + +```rust +impl Worker { + fn new(id: usize, receiver: Arc>>) -> Worker { + // --跳过代码-- + + Worker { + id, + thread: Some(thread), + } + } +} +``` + +第一个错误是在咱们的 `Drop` 实现中,早先咱们曾提到,咱们原本打算调用这个 `Option` 值上的 `take`,来将 `thread` 从 `worker` 中迁出。下面的修改就将这样做: + +文件名:`src/lib.rs` + +```rust +impl Drop for ThreadPool { + fn drop(&mut self) { + for worker in &mut self.workers { + println! ("关闭 worker {}", worker.id); + + if let Some(thread) = worker.thread.take() { + thread.join().unwrap(); + } + } + } +} +``` + +正如曾在第 17 章中讨论过的那样,`Option` 上的 `take` 方法,会将那个 `Some` 变种取出,并在其位置处留下 `None`。咱们使用了 `if let` 来解构那个 `Some` 而得到了那个线程;随后咱们在线程上调用了 `join`。若某个 `worker` 的线程已经是 `None`,那么咱们就知道那个 `worker` 已经让他的线程清理掉了,因此在那种情况下什么也不会发生。 + + +## 通知线程停止收听作业 + +**Signaling to the Threads to Stop Listening for Jobs** + + +在咱们已做出的全部修改下,咱们的代码会不带任何错误的编译了。但是,坏消息是这些代码尚不会按照咱们想要的方式运作。问题关键在于,由 `Worker` 实例的线程运行的闭包中的逻辑:此刻,咱们调用了 `join`,但由于线程是在一直 `loop` 查找作业,所以那样做将不会关闭线程。若咱们以咱们当前的 `drop` 实现丢弃咱们的 `ThreadPool`,那么主线程将一直阻塞于等待第一个线程结束。 + +为修复这个问题,咱们将需要 `ThreadPool` 的 `drop` 实现中的一个修改,以及其后的 `Worker` 循环中的一个修改。 + +首选,咱们将把 `ThreadPool` 的 `drop` 实现,修改为在等待线程结束前显式地丢弃 `sender`。下面清单 20-23 给出了对 `ThreadPool` 显示丢弃 `sender` 的修改。为能将 `send` 从 `ThreadPool` 迁出,咱们使用了与咱们曾对线程做过的同样 `Option` 于 `take` 技巧: + +文件名:`src/lib.rs` + +```rust +pub struct ThreadPool { + workers: Vec, + sender: Option>, +} +// --跳过代码-- +impl ThreadPool { + pub fn new(size: usize) -> ThreadPool { + // --跳过代码-- + + ThreadPool { + workers, + sender: Some, + } + } + + pub fn execute(&self, f: F) + where + F: FnOnce() + Send + 'static, + { + let job = Box::new(f); + + self.sender.as_ref().unwrap().send(job).unwrap(); + } +} + +impl Drop for ThreadPool { + fn drop(&mut self) { + drop(self.sender.take()); + + for worker in &mut self.workers { + println! ("关闭 worker {}", worker.id); + + if let Some(thread) = worker.thread.take() { + thread.join().unwrap(); + } + } + } +} +``` + +*清单 20-23:在归拢那些 `worker` 线程前显式丢弃 `sender`* + +丢弃 `sender` 就会关闭通道,这表明将不会有更多消息发出。当那发生时,在无限循环中那些 `worker` 所做的到 `recv` 的全部全部调用,就会返回错误。在下面清单 20-24 中,咱们修改了 `Worker` 的循环,来在那种情况下优雅有序地退出循环,这就意味着在 `ThreadPool` 的 `drop` 实现在那些线程上调用 `join` 时,他们将结束。 + + +文件名:`src/lib.rs` + +```rust +impl Worker { + fn new(id: usize, receiver: Arc>>) -> Worker { + let thread = thread::spawn(move || loop { + let message = receiver.lock().unwrap().recv(); + + match message { + Ok(job) => { + println! ("Worker {id} 获取到一项作业;执行中。"); + + job(); + } + Err(_) => { + println! ("Worker {id} 已断开链接;关闭中。"); + break; + } + } + }); + + Worker { + id, + thread: Some(thread), + } + } +} +``` + +*清单 20-24:在 `recv` 返回错误时显式跳出循环* + +要看到运作中的代码,咱们就来把 `main` 修改为在有序关闭服务器钱,只接收两个请求,如下清单 20-25 中所示。 + +文件名:`src/main.rs` + +```rust +fn main() { + let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); + let pool = ThreadPool::new(4); + + for stream in listener.incoming().take(2) { + let stream = stream.unwrap(); + + pool.execute(|| { + handle_conn(stream); + }); + } + + println! ("关闭中。"); +} +``` + +*清单 20-25: 在服务两个请求后通过退出循环关闭服务器* + +咱们是不会想要真实世界的 web 服务器在仅服务两个请求后就关闭的。这段代码只演示了这种有序关闭与清理是在正常工作。 + +其中的 `take` 方法,是定义在 `Iterator` 特质中的,且将迭代限制到最多头两个项目。在 `main` 的结束处,`ThreadPool` 将超出作用域,而 `drop` 实现将运行。 + +请以 `cargo run` 启动服务器,并构造三个请求。第三个请求应会出错,而在终端里咱们应看到类似于下面这样的输出: + +```console +$ cargo run 16s + Finished dev [unoptimized + debuginfo] target(s) in 0.00s + Running `target/debug/hello` +Worker 0 获取到一项作业;执行中。 +关闭中。 +关闭 worker 0 +Worker 1 获取到一项作业;执行中。 +Worker 3 已断开链接;关闭中。 +Worker 2 已断开链接;关闭中。 +Worker 1 已断开链接;关闭中。 +Worker 0 已断开链接;关闭中。 +关闭 worker 1 +关闭 worker 2 +关闭 worker 3 +``` + +咱们可能会看到不同顺序的 `worker` 与消息打印出来。咱们能从这些消息,看出代码是如何工作的:`worker` `1` 与 `2` 获取到了头两个请求。服务器在第二个 TCP 连接之后,便停止了接收连接,而 `ThreadPool` 上的 `Drop` 实现,在 `worker` `2` 还没开始其作业前,便开始了执行。丢弃 `sender` 会断开全部 `worker` 并告诉他们要关闭。那些 `worker` 在他们断开连接时,都各自打印了一条消息,而随后线程池便调用了 `join` 来等待各个 `worker` 线程结束。 + +请注意这次特定执行的一个有趣方面:`ThreadPool` 弃用了 `sender`,而在有任何 `worker` 接收到错误前,咱们就尝试归拢了 `worker` `0`。`worker` `0` 还不曾从 `recv` 获取到一个错误,因此主线程就阻塞于等待 `worker` `0` 结束。与此同时,`worker` `1` 收到了一项作业,而随后全部线程都收到了错误。在 `worker` `0` 结束时,主线程就等待其余 `worker` 结束。而在那个时候,他们都已退出了他们的循环并停止了。 + +恭喜!咱们先进已经完成了咱们的项目;咱们有了一个运用线程池来异步响应的基本 web 服务器。咱们能够完成服务器有序关闭,这会清理掉线程池中的全部线程。 + +以下是用于参考的全部代码: + +文件名:`src/main.rs` + +```rust +use hello::ThreadPool; + +use std::{ + fs, + thread, + io::{prelude::*, BufReader}, + net::{TcpListener, TcpStream}, + time::Duration, +}; + +fn main() { + let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); + let pool = ThreadPool::new(4); + + for stream in listener.incoming().take(2) { + let stream = stream.unwrap(); + + pool.execute(|| { + handle_conn(stream); + }); + } + + println! ("关闭中。"); +} + +fn handle_conn(mut stream: TcpStream) { + let buf_reader = BufReader::new(&mut stream); + let req_line = buf_reader.lines().next().unwrap().unwrap(); + + let (status_line, filename) = match &req_line[..] { + "GET / HTTP/1.1" => ( "HTTP/1.1 200 OK", "hello.html"), + "GET /sleep HTTP/1.1" => { + thread::sleep(Duration::from_secs(10)); + ("HTTP/1.1 200 0K", "hello.html") + } + _ => ("HTTP/1.1 404 NOT FOUND", "404.html"), + }; + + let contents = fs::read_to_string(filename).unwrap(); + let length = contents.len(); + + let resp = + format! ("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); + + stream.write_all(resp.as_bytes()).unwrap(); +} +``` + + +文件名:`src/lib.rs` + +```rust +use std::{ + sync::{mpsc, Arc, Mutex}, + thread, +}; + +pub struct ThreadPool { + workers: Vec, + sender: Option>, +} + +type Job = Box; + +impl ThreadPool { + /// 创建出一个新的 ThreadPool。 + /// + /// 其中的 size 为线程池中线程的数目。 + /// + /// # 终止运行 + /// + /// 这个 `new` 函数将在 size 为零时终止运行。 + pub fn new(size: usize) -> ThreadPool { + assert! (size > 0); + + let (sender, receiver) = mpsc::channel(); + + let receiver = Arc::new(Mutex::new(receiver)); + + let mut workers = Vec::with_capacity(size); + + for id in 0..size { + workers.push(Worker::new(id, Arc::clone(&receiver))); + } + + ThreadPool { + workers, + sender: Some(sender), + } + } + + pub fn execute(&self, f: F) + where + F: FnOnce() + Send + 'static, + { + let job = Box::new(f); + + self.sender.as_ref().unwrap().send(job).unwrap(); + } +} + +impl Drop for ThreadPool { + fn drop(&mut self) { + drop(self.sender.take()); + + for worker in &mut self.workers { + println! ("关闭 worker {}", worker.id); + + if let Some(thread) = worker.thread.take() { + thread.join().unwrap(); + } + } + } +} + +struct Worker { + id: usize, + thread: Option>, +} + +impl Worker { + fn new(id: usize, receiver: Arc>>) -> Worker { + let thread = thread::spawn(move || loop { + let message = receiver.lock().unwrap().recv(); + + match message { + Ok(job) => { + println! ("Worker {id} 获取到一项作业;执行中。"); + + job(); + } + Err(_) => { + println! ("Worker {id} 已断开链接;关闭中。"); + break; + } + } + }); + + Worker { + id, + thread: Some(thread), + } + } +} +``` + +这里咱们可以做更多事情!若咱们打算继续加强这个项目,下面是一些想法: + +- 给 `ThreadPool` 及其公开方法添加更多文档; + +- 给这个库的功能添加测试; + +- 把一些调用修改为 `unwrap`,以获得更多的错误处理鲁棒性; + +- 运用 `ThreadPool` 来完成除服务 web 请求外的一些别的任务; + +- 在 [crates.io](https://crates.io/) 上找到某个线程池代码箱,并用该代码箱实现一个类似的 web 服务器。随后将其 API 及鲁棒性,与咱们实现的线程池相比较。 + + +# 本章小结 + +干得好!咱们已经读完了这整本书!要感谢咱们加入到这次 Rust 之旅中来。咱们现在已经准备好实现咱们自己的 Rust 项目,以及帮助其他人的项目了。请记住有那么一个由热衷于就咱们在 Rust 道路上,所遇到的任何挑战,而帮助咱们的其他 Rust 公民的热情社区。 diff --git a/src/final_project/multithreaded.md b/src/final_project/multithreaded.md new file mode 100644 index 0000000..09683de --- /dev/null +++ b/src/final_project/multithreaded.md @@ -0,0 +1,723 @@ +# 将咱们的单线程服务器改写为多线程服务器 + +**Turning Our Single-Thread Server into a Multithreaded Server** + + +现在,这个服务器将依次处理每个请求,这意味着其将不会在前一个连接完成处理前,处理后一连接。若服务器收到了越来越多的请求,这种顺序执行就会越来越差。而若该服务器收到了一个要耗费较长时间处理的请求,即使后续的新请求可被快速处理,但其仍将不得不等待直到那个长时间请求完成。咱们需要修复这个问题,但首选,咱们将具体看看这个问题。 + + +## 在当前服务器实现下模拟一个慢速请求 + +**Simulating a Slow Request in the Current Server Implemenation** + + +咱们将看看一个慢速处理的请求,能怎样影响那些到咱们当前服务器实现的其他请求。下面清单 20-10 以一个将导致服务器在响应前睡眠 5 秒的模拟慢速请求,实现了对到 `/sleep` 请求的处理。 + +文件名:`src/main.rs` + +```rust +#![allow(warnings)] +use std::{ + fs, + io::{prelude::*, BufReader}, + net::{TcpListener, TcpStream}, + thred, + time::Duration, +}; +// --跳过代码-- + +fn handle_conn(mut stream: TcpStream) { + // --跳过代码-- + + let (status_line, filename) = match &req_line[..] { + "GET / HTTP/1.1" => ( "HTTP/1.1 200 OK", "hello.html"), + "GET /sleep HTTP/1.1" => { + thread::sleep(Duration::from_secs(5)); + ("HTTP/1.1 200 0K", "hello.html") + } + _ => ("HTTP/1.1 404 NOT FOUND", "404.html"), + }; + + // --跳过代码-- +} +``` + +*清单 20-10:通过睡眠 5 秒模拟慢速请求* + +现在咱们有了三种情况,于是就已从 `if` 切换到了 `match`。咱们需要显式地在 `req_line` 切片上,与那三个字符串字面值进行模式匹配;`match` 不会像相等比较方式所做的那样,执行自动引用与解引用。 + +首条支臂与清单 20-9 的 `if` 代码块是一样的。第二条支臂,是将请求与 `/sleep` 匹配。在收到那个请求时,服务器将在渲染那个成功 HTML 页面之前,睡眠 5 秒。第三支臂则与清单 20-9 的那个 `else` 代码块是一样的。 + +咱们可以看出,咱们的服务器有多原始:真正的库将以一种不那么冗长的方式,处理多种请求的识别! + +请使用 `cargo run` 启动服务器。随后打开两个浏览器窗口:一个用于 `http://127.0.0.1/7878`,另一个用于 `http://127.0.0.1:7878/sleep`。若咱们像之前一样进入那个 `/` URI 几次,咱们将看到其响应很快。但在进入 `/sleep` 并于随后加载 `/` 时,就会看到那个 `/` 会一直等待,知道 `sleep` 已经于加载之前睡眠了 5 秒。 + +咱们可以用来避免慢速请求后面那些请求滞后的技巧有多种;咱们将实现的技巧,便是线程池。 + + +## 使用线程池提升吞吐量 + +**Improving Throughput with a Thread Pool** + + +所谓 *线程池,thread pool*,是指处于等待中,并准备好处理某项任务的一组生成的线程。在程序收到一项新任务时,他便指派线程池中的一个线程给该项任务,而那个线程就会处理这个任务。池中的剩余线程,则是可以处理任何的于这首个线程进行处理时,进来的那些任务的。在这首个线程完成其任务处理时,他就会回到空闲线程的线程池,准备处理某项新任务。线程池实现了连接的并发处理,从而提升咱们服务器的吞吐能力。 + +咱们将把池中线程数量,先知道一个较小的数目,以保护咱们免于拒绝服务攻击,Denial of Service(DoS) attacks;若咱们让咱们的程序在每个请求进入时,创建一个新线程,那么构造出一千万个请求到咱们的服务器的某人,就能经由耗尽咱们服务器的全部资源,而使得这些请求的处理陷入停滞,而造成极大破坏。 + +这种技巧只是提供 web 服务器吞吐量的许多方法之一。咱们可能探讨的其他选项分别是 *分叉汇合模型,fork/join model*、*单线程异步 I/O 模型,single-threaded async I/O model*,抑或 *多线程异步 I/O 模型,multi-threaded async I/O model*。若对此问题感兴趣,那么可以阅读有关其他解决方案的资料,并尝试实现他们;对于 Rust 这种底层编程语言,所有这些选项都是可行的。 + + +在开始实现线程池前,咱们来聊聊用到这个池子的东西会是什么样子。在咱们正要尝试设计代码时,首先编写客户端界面,可有助于引导咱们的设计。要以咱们打算调用代码 API 的方式,编写出这些有组织架构的代码 API;随后在那种组织架构下实现功能,而非先实现功能而随后设计那些公开 API。 + +与第 12 章中项目里用到的测试驱动方式的开发,test-driven development,类似,这里咱们将运用编译器驱动的开发,compiler-driven development。咱们将先编写出咱们打算调用那些函数的代码,而随后会看看来自编译器的那些报错,以确定出接下来咱们应修改些什么,来让代码运作起来。在咱们进行那一步之前,咱们将探讨一下咱们并不会用到的一种技巧,作为开头。 + + +### 为每个请求生成一个线程 + +**Spawning a Thread for Each Request** + + +首先,咱们来探讨一下若咱们的代码给每隔连接创建一个新线程,他看起来会怎样。正如早先所提到的,由于潜在地生成无限数目线程的那些问题,这样做不是咱们的最终计划,但其为首先得到一个运作多线程服务器的起点。随后咱们将添加线程池作为一项改进,且将这两种方案进行对比将更容易一些。下面清单 20-11 给出了把 `main` 构造为于那个 `for` 循环里,生成一个新线程来处理每个 TCP 流的一些修改。 + +文件名:`src/main.rs` + +```rust +fn main() { + let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); + + for stream in listener.incoming() { + let stream = stream.unwrap(); + + thread::spawn(|| { + handle_conn(stream); + }); + } +} +``` + +*清单 20-11:为每个 TCP 流生成一个新线程* + +如同咱们在第 16 章中所学到的,`thread::spawn` 讲创建出一个新线程,并于随后在新线程中,运行那个闭包中的代码。当咱们运行此代码,并在浏览器中加载 `/sleep`,随后在另外两个浏览器 Tab 页中加载 `/`,咱们就会看到到 `/` 的请求就不必等待 `/sleep` 请求完毕了。不过,如同咱们曾提到过的,因为咱们正不带任何限制地构造新线程,而最终将使系统不堪重负。 + + +### 创建有限数目的线程 + +**Creating a Finite Number of Threads** + + +咱们想要咱们的线程池,以类似的、熟悉的方式运作,而无需那些用到咱们 API 的代码有较大修改。下面清单 20-12 给出了咱们打算用到的 `ThreadPool`,而非 `thread::spawn`,的假想接口。 + +文件名:`src/main.rs` + +```rust +fn main() { + let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); + let pool = ThreadPool::new(4); + + for stream in listener.incoming() { + let stream = stream.unwrap(); + + pool.execute(|| { + handle_conn(stream); + }); + } +} +``` + +*清单 20-12:咱们设想的 `ThreadPool` 接口* + +咱们使用了 `ThreadPool::new` 来创建出有着可配置线程数目的新线程,在此示例中为四个线程。随后,在那个 `for` 循环中,`pool.execute` 有着与 `thread::spawn` 类似的接口,其中他会取个闭包,并将其给到线程池中的某个线程运行。这段代码尚不会编译,但咱们将进行尝试,如此编译器就会引导咱们如何修复他。 + + +### 运用编译器驱动的开发,构建出 `ThreadPool` + +**Building `ThreadPool` Using Compiler Driven Development** + + +请完成清单 20-12 中对 `src/main.rs` 的修改,然后咱们就来运用 `cargo check` 给出的编译器报错,驱动咱们的开发。下面就是咱们所得到的第一个报错: + +```console +$ cargo check + Checking hello v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/hello) +error[E0433]: failed to resolve: use of undeclared type `ThreadPool` + --> src/main.rs:12:16 + | +12 | let pool = ThreadPool::new(4); + | ^^^^^^^^^^ use of undeclared type `ThreadPool` + +For more information about this error, try `rustc --explain E0433`. +error: could not compile `hello` due to previous error +``` + +很棒!这个错误告诉我们,咱们需要一个 `ThreadPool` 类型或模组,因此咱们现在就将构建一个出来。咱们的 `ThreadPool` 实现,将独立于咱们的 web 服务器所完成工作的类型。因此,咱们就来将这个 `hello` 代码箱,从二进制代码箱切换为一个库代码箱,来保存咱们的 `ThreadPool` 实现。在咱们改变为库代码箱后,咱们就可以在打算用到线程池的任何项目,而不只是用来服务 web 请求中,也可以使用这个独立的线程池了。 + +请创建一个包含了下面这个咱们目前所能有的 `ThreadPool` 结构体极简定义的 `src/lib.rs` 文件: + +文件名:`src/lib.rs` + +```rust +pub struct ThreadPool; +``` + +随后编辑 `main.rs`,来通过加入下面的代码到 `src/main.rs` 顶部,将 `ThreadPool` 从那个库代码箱,带入作用域: + +文件名:`src/main.rs` + +```rust +use hello::ThreadPool; +``` + +这段代码仍不会工作,但咱们就来再检查一边,以得到咱们需要解决的下一报错: + +```console +$ cargo check + Checking hello v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/hello) +error[E0599]: no function or associated item named `new` found for struct `ThreadPool` in the current scope + --> src/main.rs:14:28 + | +14 | let pool = ThreadPool::new(4); + | ^^^ function or associated item not found in `ThreadPool` + +For more information about this error, try `rustc --explain E0599`. +error: could not compile `hello` due to previous error +``` + +此报错表明,接下来咱们就要给 `ThreadPool` 创建一个名为 `new` 的关联函数。咱们还知道了那个 `new` 需要有一个可将 `4` 作为实参接收的形参,并应返回一个 `ThreadPool` 的实例。下面就来实现将有着那些特性的这个极简 `new` 函数: + +文件名:`src/lib.rs` + +```rust +pub struct ThreadPool; + +impl ThreadPool { + pub fn new(size: usize) -> ThreadPool { + ThreadPool + } +} +``` + +由于咱们清楚一个负的线程数目不会有任何意义,因此咱们选择了 `usize` 作为那个 `size` 参数的类型。咱们还知道咱们将使用这个 `4` 作为线程集合中原始的个数,那即使这个 `usize` 类型的目的所在,正如第三章的 [整数类型](Ch03_Common_Programming_Concepts.md#整形) 小节中曾讨论过的。 + +下面来再次检查: + +```console +$ cargo check + Checking hello v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/hello) +error[E0599]: no method named `execute` found for struct `ThreadPool` in the current scope + --> src/main.rs:19:14 + | +19 | pool.execute(|| { + | ^^^^^^^ method not found in `ThreadPool` + +For more information about this error, try `rustc --explain E0599`. +error: could not compile `hello` due to previous error +``` + +现在的报错之所以出现,是因为在 `ThreadPool` 上咱们没有一个 `execute` 方法。回顾 ["创建有限数目的线程"](#创建有限数目的线程) 小节到,咱们已决定咱们的线程池,应有一个类似与 `thread::spawn` 的接口。此外,咱们将实现这个 `execute` 函数,如此其便会取那个给到他的闭包,并将其交给线程池中的某个空闲进程运行。 + +咱们将在 `ThreadPool` 上定义这个 `execute` 方法,来取一个闭包作为参数。回顾第 13 章中 [“将捕获值迁移出闭包与 `Fn` 特质”](Ch13_Functional_Language_Features_Iterators_and_Closures.md#将捕获到的值迁移出闭包与-fn-特质) 到咱们可以三种不同特质,将闭包取作参数:`Fn`、`FnMut` 与 `FnOnce`。咱们需要确定出这里要使用何种类别的闭包。咱们清楚咱们将以完成一些类似于标准库的 `thread::spawn` 实现类似的东西结束,因此咱们就可以看看 `thread::spawn` 的签名在其参数上有些什么。文档给出咱们下面的东西: + +```rust +pub fn spawn(f: F) -> JoinHandle + where + F: FnOnce() -> T, + F: Send + 'static, + T: Send + 'static, +``` + +其中的 `F` 类型参数,就是咱们在这里所关心的那个;那个 `T` 类型参数于返回值相关,而咱们并不关心那个。咱们可以看出,`spawn` 使用 `FnOnce` 作为 `F` 上的特质边界。由于咱们将最终将把咱们在 `execute` 中获得的实参,传递给 `spawn`,因此这或许也正是咱们想要的。由于为运行某个请求的线程,将只执行那个请求的闭包一次,而这是与 `FnOnce` 中的 `Once` 是相匹配的,故咱们可以进一步确信,`FnOnce` 便是咱们要用到的特质。 + +其中的 `F` 类型参数,还有着特质边界 `Send` 与生命周期边界 `'static`,在咱们这种情况下他们是有用的:咱们需要 `Send` 来将闭包,从一个线程转移到另一线程,并由于咱们不知道那个线程将耗时多久来执行,因此而需要 `'static`。下面咱们就来在 `ThreadPool` 上,创建出将取到有着这些边界的,类型 `F` 的泛型参数的 `execute` 方法: + +文件名:`src/lib.rs` + +```rust +#![allow(warnings)] +pub struct ThreadPool; + +impl ThreadPool { + pub fn execute(&self, f: F) + where + F: FnOnce() + Send + 'static, + { + } +} +``` + +由于这个 `FnOnce` 表示一个不会取参数,且返回的是单元类型 `()` 的闭包,因此咱们仍旧使用了 `FnOnce` 后的 `()`。就跟函数定义一样,返回值类型可以在签名中省略,但即使咱们没有参数,咱们仍需这对括号。 + +又一次,这仍是那个 `execute` 方法的极简实现:他什么也没做,但咱们只是在试着让咱们的代码编译。咱们再来对其加以检查: + +```console +$ cargo check + Checking hello v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/hello) + Finished dev [unoptimized + debuginfo] target(s) in 0.36s +``` + +他编译了!不过请注意,当咱们尝试 `cargo run` 并在浏览器中构造一次请求时,咱们将看到浏览器中,一些咱们在本章开头曾看到过的报错。咱们的库尚未真正调用传递给 `execute` 的那个闭包! + +> 注意:咱们或许听说过与有着严格编译器语言,比如 Haskell 与 Rust,有关的一种说法,即 “若代码编译了,他就会工作。” 然而这种说法并非一概而论。咱们的项目编译了,但他绝对什么也没干!若咱们是在构建一个真实、完整的项目,那么此时就将是开始编写检查代码编译与否,*以及* 是否具有咱们想要的行为的单元测试的好时机。 + + +### 在 `new` 中验证线程数目 + +**Validating the Number of Threads in `new`** + + +咱们没有对 `new` 与 `execute` 的参数做任何事情。下面就来以咱们打算的行为,实现这两个函数的函数体。咱们来构思一下 `new`,作为开始。早先由于负的线程数目没有意义,因此咱们给那个 `size` 参数,选择了一个无符号整数类型。不过尽管零也是相当有效的 `usize`,但零个线程的线程池,同样是无意义的。咱们将在返回一个 `ThreadPool` 实例前,添加检查 `size` 大于零的代码,并在程序收到一个零时,通过使用 `assert!` 宏,让程序终止运行,如下面清单 20-13 中所示。 + +文件名:`src/lib.rs` + +```rust +impl ThreadPool { + /// 创建出一个新的 ThreadPool。 + /// + /// 其中的 size 为线程池中线程的数目。 + /// + /// # 终止运行 + /// + /// 这个 `new` 函数将在 size 为零时终止运行。 + pub fn new(size: usize) -> ThreadPool { + assert! (size > 0); + + ThreadPool + } + + // --跳过代码-- +} +``` + +*清单 20-13:将 `ThreadPool` 实现为在 `size` 为零时终止运行* + +咱们还以一些文档注释,doc comments,给咱们的 `ThreadPool` 结构体添加了一些文档。请注意咱们通过添加如同第 14 章中曾讨论过的,一个会呼出咱们的函数可能终止运行时的那些情形的小节,而遵循了良好的文档实践。请尝试运行 `cargo doc --open` 并点击那个 `ThreadPool` 结构体,来看到为 `new` 生成的文档看起来是怎样的! + +与其如咱们在这里所做的添加这个 `assert!` 宏,咱们则可把 `new` 改为 `build`,并像咱们曾在清单 12-9 中那个 I/O 项目里的 `Config::build` 下所做的那样,返回一个 `Result`。但咱们已经决定,在此示例中是在尝试创建一个,其中全部线程都不应是不可恢复错误的线程池。若你觉得信心满满,那就请编写一个名为 `build`,有着下面签名的函数,来与这个 `new` 函数相比较: + +```rust +pub fn build(size: usize) -> Result { +``` + + +### 创建空间来存储这些线程 + +**Creating Space to Store the Threads** + + +既然咱们有了获悉咱们有着要在线程池中存储线程有效数目的一种办法了,咱们便可以创建出这些线程,并在返回这个 `ThreadPool` 结构体前,将他们存储在该结构体中。但是咱们要怎么 “存储” 一个线程呢?下面又来看看那个 `thread::spawn` 签名: + +```rust +pub fn spawn(f: F) -> JoinHandle + where + F: FnOnce() -> T, + F: Send + 'static, + T: Send + 'static, +``` + +`spawn` 函数返回了一个 `JoinHandle`,其中的 `T` 为闭包所返回的类型。咱们也来尝试使用 `JoinHandle`,并观察会发生什么。在咱们的用例中,咱们传递给线程池的闭包,将处理 TCP 连接,而不会返回任何东西,因此其中的 `T` 将是单元类型 `()`。 + +下面清单 20-14 中的代码将会编译,但尚不会创建任何线程。咱们已将 `ThreadPool` 的定义,修改为保存了一个 `thread::JoinHandle<()>` 实例的矢量值,以 `size` 大小初始化了这个矢量值,还建立了一个将运行某些代码来创建出那些线程的 `for` 循环,并返回了一个包含着这些线程的 `ThreadPool` 实例。 + +文件名:`src/lib.rs` + +```rust +use std::thread; + +pub struct ThreadPool { + threads: Vec>, +} + +impl ThreadPool { + // --跳过代码-- + pub fn new(size: usize) -> ThreadPool { + assert! (size > 0); + + let mut threads = Vec::with_capacity(size); + + for _ in 0..size { + // 创建出一些线程并将他们存储在那个矢量中 + } + + ThreadPool { threads } + } + // --跳过代码-- +} +``` + +*清单 20-14:为保存那些线程而给 `ThreadPool` 创建一个矢量值* + +由于咱们正使用 `thread::JoinHandle` 作为 `ThreadPool` 中那个矢量值条目的类型,因此咱们已将 `std::thread` 带入到这个库代码箱的作用域。 + +一旦收到有效的大小,咱们的 `ThreadPool` 就会创建出可保存 `size` 个条目的一个新矢量值。那个 `with_capacity` 函数,执行的是与 `Vec::new` 同意的任务,但有个重要的不同之处:他会预先分配那个矢量值中的空间。由于咱们清楚咱们需要在那个矢量值中存储 `size` 个元素,那么预先完成这种分配,相比使用在有元素插入时调整自身的 `Vec::new`,就会稍微更具效率。 + +在再度运行 `cargo check` 时,其应成功。 + + +### 负责将代码从 `ThreadPool` 发送给某个线程的 `Worker` 结构体 + +**A `Worker` Struct Responsible for Sending Code from the `ThreadPoll` to a Thread** + + +在清单 20-14 中的那个 `for` 循环里,咱们留了一个有关线程创建过程的注释。这里,咱们将看看咱们具体要怎么创建出那些线程来。标准库提供了 `thread::spawn` 作为创建线程的一种方式,而 `thread::spawn` 则期望得到一些线程在其一创建出来,就应立即运行的代码。然而,在咱们的示例中,咱们打算创建出这些线程,并让他们 *等待,wait* 咱们稍后将要发送的那些代码。标准库的线程实现,没有包含任何实现那样做法的方式;咱们必须亲自实现他。 + +咱们将通过引入介于 `ThreadPool` 与那些线程之间,将对这种新行为加以管理的一种新数据结构,来实现这样的行为。咱们将把这种数据结构称作 `Worker`,在线程池实现中,这是个常见的术语。`Worker` 会拾取需要运行的代码,并在该 `Worker` 的线程中运行那些代码。设想某家饭馆中工作的人们:工人们会一直等待,直到有顾客点的菜单进来,而随后他们就负责接下这些菜单,并让顾客们满意。 + +在线程池中存储的,不再是 `JoinHandle<()>` 实例的矢量值,咱们将存储这个 `Worker` 结构体的实例。每个 `Worker` 都将存储一个单独的 `JoinHandler<()>` 实例。随后咱们将在 `Worker` 上实现一个,将取得要运行代码的闭包,并将其发送到已经运行着的线程去执行的方法。咱们还将给到每个 `Worker` 一个 `id`,如此咱们就可以在日志记录或调试时,区分出线程池中那些不同的 `Worker`。 + + +以下便是在咱们创建一个 `ThreadPool` 时,将发生的一个新过程。咱们将在以此方式建立起 `Worker` 结构体后,再实现把闭包发送给线程的那些代码: + +1. 定义出一个保存了一个 `id` 与一个 `JoinHandler<()>` 的 `Worker` 结构体; +2. 把 `ThreadPool` 修改为保存一个 `Worker` 实例构成的矢量值; +3. 定义出会取一个 `id` 数字,并返回保存着这个 `id`,以及带有所生成的有着一个空闭包的线程的一个 `Worker` 实例,这样一个 `Worker::new` 函数; +4. 在 `Thread::new` 中,会使用那个 `for` 循环的计数器,来生成一个 `id`、用那个 `id` 创建出一个新的 `Worker`,并将该 `Worker` 存储在那个矢量值中。 + + +若咱们准备挑战一下,那么请尝试在查看清单 20-15 中代码之前,自己实现这些修改。 + +准备好了吗?下面就是有着一种做出前面那些修改的一种方式的清单 20-15。 + +文件名:`src/lib.rs` + +```rust +use std::thread; + +pub struct ThreadPool { + workers: Vec, +} + +impl ThreadPool { + // --跳过代码-- + pub fn new(size: usize) -> ThreadPool { + assert! (size > 0); + + let mut threads = Vec::with_capacity(size); + + for _ in 0..size { + workers.push(Worker::new(id)); + } + + ThreadPool { workers } + } + // --跳过代码-- +} + +struct Worker { + id: usize, + thread: thread::JoinHandle<()>, +} + +impl Worker { + fn new(id: usize) -> Worker { + let thread = thread::spawn(|| {}); + + Worker { id, thread } + } +} +``` + +*清单 20-15:将 `ThreadPool` 修改为保存 `Worker` 实例而非直接保存线程* + +由于 `ThreadPool` 现在保存的是一些 `Worker` 实例而非 `JoinHandle<()>` 实例,因此咱们已将其上那个字段的名字,从 `threads` 修改为了 `workers`。咱们将那个 `for` 循环中的计数器,用作给 `Worker::new` 的参数,同时咱们将每个新的 `Worker`,存储在那个名为 `workers` 的矢量值中。 + +外部代码(就像 `src/main.rs` 中咱们的服务器),无需知悉 `ThreadPool` 里某个 `Worker` 结构体使用方面的实现细节,因此咱们是将这个 `Worker` 结构体及其 `new` 函数,构造为了私有。`Worker::new` 函数使用了咱们给他的那个 `id`,并将经由使用空闭包而生成一个新线程,而创建出的一个 `JoinHandle<()>` 实例存储起来。 + + +> 注意:若操作系统因没有足够系统资源而无法创建出一个线程,那么 `thread::spawn` 就将终止运行。那样的话,即使一些线程创建可能成功,也将导致咱们整个服务器终止运行。为简化起见,这种实现做法是无可厚非的,但在生产的线程池实现中,咱们就大概打算使用 [`std::thread::Builder`](https://doc.rust-lang.org/std/thread/struct.Builder.html) 与他的返回 `Result` 的 [`spawn`](https://doc.rust-lang.org/std/thread/struct.Builder.html#method.spawn) 方法了。 + +这段代码将编译,并将咱们指定给 `ThreadPool::new` 数目的 `Worker` 实例存储起来。但咱们 *仍* 未处理咱们在 `execute` 中得到的闭包。接下来就要看看怎样完成那一步。 + + +### 经由通道把请求发送给线程 + +**Sending Requests to Threads via Channels** + + +接下来咱们将要解决的,便是所给到 `thread::spawn` 的那些闭包什么也没做的问题。当前,咱们在那个 `execute` 方法中,获取到了咱们打算执行的那个闭包。但咱们需要于那个 `ThreadPool` 创建期间,在咱们创建出各个 `Worker` 时,给到 `thread::spawn` 一个闭包。 + +咱们想要咱们刚创建出的那些 `Worker` 结构体,从一个保存在 `ThreadPool` 中的队列中获取要运行的代码,并把那些代码发送他的线程运行。 + +第 16 章中咱们学过的通道 -- 两个线程间通信的一种简单方式 -- 对于这个用例将是最佳的。咱们将使用一个函数的通道,作为作业队列,the queue of jobs,而 `execute` 将把来自 `ThreadPool` 的某项作业,发送到那些 `Worker` 实例,其将把该项作业,发送给他的线程。下面便是这个方案: + +1. `ThreadPool` 将创建出一个通道,并保存于 `sender` 上; +2. 每个 `Worker` 实例,将保存于 `receiver` 上; +3. 咱们将创建出将保存那些咱们打算下发到通道上闭包的一个新 `Job` 结构体; +4. `execute` 方法将经由那个 `sender`,发送其打算执行的作业; +5. 在 `Worker` 实例的线程中,其将遍历其 `receiver` 并执行其所接收到的任何作业的闭包。 + +咱们来通过在 `ThreadPool::new` 中创建一个通道,并在 `ThreadPool` 实例中保存 `send` 开始,如下清单 20-16 中所示。其中的 `Job` 结构体现在没有保存任何东西,但将保存咱们下发到通道项目类型。 + +文件名:`src/lib.rs` + +```rust +use std::{sync::mpsc, thread}; + +pub struct ThreadPool { + workers: Vec, + sender: mpsc::Sender, +} + +struct Job; + +impl ThreadPool { + // --跳过代码-- + pub fn new(size: usize) -> ThreadPool { + assert! (size > 0); + + let (sender, receiver) = mpsc::channel(); + + let mut workers = Vec::with_capacity(size); + + for id in 0..size { + workers.push(Worker::new(id)); + } + + ThreadPool { workers, sender } + } + + // --跳过代码-- +} +``` + +*清单 20-16:将 `ThreadPool` 修改为存储传递 `Job` 实例通道的 `sender`* + +在 `ThreadPool::new` 中,咱们创建出来咱们的新通道,并让线程池保存了该通道的 `sender`。这段代码将成功编译。 + +下面就来尝试在这个线程池创建出该通道时,把其 `receiver` 传入各个 `worker`。咱们清楚咱们是要在那些 `workers` 生成的线程中使用这个 `receiver`,因此咱们将在那个闭包中,引用这个 `receiver` 参数。下面清单 20-17 中的代码尚不会很好地编译。 + +文件名:`src/lib.rs` + +```rust +impl ThreadPool { + // --跳过代码-- + pub fn new(size: usize) -> ThreadPool { + assert! (size > 0); + + let (sender, receiver) = mpsc::channel(); + + let mut workers = Vec::with_capacity(size); + + for id in 0..size { + workers.push(Worker::new(id, receiver)); + } + + ThreadPool { workers, sender } + } + + // --跳过代码-- +} + +// --跳过代码-- +impl Worker { + fn new(id: usize, receiver: mpsc::Receiver) -> Worker { + let thread = thread::spawn(|| { + receiver; + }); + + Worker { id, thread } + } +} +``` + +*清单 20-17:将 `receiver` 传递给 `workers`* + +咱们作出了一些小而简单直接的修改:咱们把那个 `receiver` 传入到 `Worker::new`,并随后在那个闭包里使用了他。 + +当咱们尝试检查这段代码时,就会得到如下报错: + +```console +$ cargo check + Checking hello v0.1.0 (/home/peng/rust-lang-zh_CN/hello) +error[E0382]: use of moved value: `receiver` + --> src/lib.rs:27:42 + | +22 | let (sender, receiver) = mpsc::channel(); + | -------- move occurs because `receiver` has type `std::sync::mpsc::Receiver`, which does not implement the `Copy` trait +... +27 | workers.push(Worker::new(id, receiver)); + | ^^^^^^^^ value moved here, in previous iteration of loop + +For more information about this error, try `rustc --explain E0382`. +error: could not compile `hello` due to previous error +``` + +这段代码试图将 `receiver` 传递给多个 `Worker` 实例。正如回顾到第 16 章,这样做不会工作:Rust 所提供的通道实现,属于多 `producer`、单 `consumer` 的。这意味着咱们不能只克隆通道的消费端来修复这段代码。咱们也不打算将一条消息,多次发送给多个消费者;咱们是要一个带有多个 `worker` 的消息列表,如此每条消息,都将被一次性处理。 + +此外,从通道队列里取出一项作业,还涉及到令 `receiver` 可变,因此这些县城就需要一种共用与修改 `receiver` 的安全方式;否则,咱们就会面临竞争情形(如同第 16 章中所讲到的)。 + +回顾第 16 章中曾讨论过的线程安全灵巧指针:为在多个线程间共用所有权,以及实现这些线程修改值,咱们需要用到 `Arc>`。`Arc` 类型将实现多个 `worker` 都拥有那个 `receiver`,而 `Mutex` 将确保某个时刻只有一个 `worker` 从 `receiver` 获取一项作业。下面清单 20-18 给出了咱们需要作出的修改。 + +文件名:`src/lib.rs` + +```rust +use std::{ + sync::{mpsc, Arc, Mutex}, + thread, +}; +// --跳过代码-- + +impl ThreadPool { + // --跳过代码-- + pub fn new(size: usize) -> ThreadPool { + assert! (size > 0); + + let (sender, receiver) = mpsc::channel(); + + let receiver = Arc::new(Mutex::new(receiver)); + + let mut workers = Vec::with_capacity(size); + + for id in 0..size { + workers.push(Worker::new(id, Arc::clone(&receiver))); + } + + ThreadPool { workers, sender } + } + + // --跳过代码-- +} + +// --跳过代码-- + +impl Worker { + fn new(id: usize, receiver: Arc>>) -> Worker { + // --跳过代码-- + } +} +``` + +*清单 20-18:运用 `Arc` 与 `Mutex` 在那些 `worker` 间共用 `receiver`* + +在 `ThreadPool::new` 中,咱们把 `receiver` 放入到一个 `Arc` 与一个 `Mutex` 中。对于各个新 `worker`,咱们克隆了那个 `Arc`,从而增加了引用计数,这样那些 `worker` 就可以共用 `receiver` 的所有权。 + +有了这些修改,代码就会编译了!咱们就要达到目的了! + + +### 实现 `execute` 方法 + +**Implementing the `execute` method** + + +咱们来最终实现那个 `ThreadPool` 上的 `execute` 方法。咱们还将把 `Job` 从结构体,修改为保存着 `execute` 接收到闭包类型的特质对象的类型别名。正如第 19 章 [“使用类型别名创建类型义词”](Ch19_Advanced_Features.md#使用类型别名创建类型同义词) 小节中曾讨论过的,类型别名实现了为易于使用而将长类型构造缩短。请看看下面清单 20-19. + +文件名:`src/lib.rs` + +```rust +// --跳过代码-- + +type Job = Box; + +impl ThreadPool { + // --跳过代码-- + + pub fn execute(&self, f: F) + where + F: FnOnce() + Send + 'static, + { + let job = Box::new(f); + + self.sender.send(job).unwrap(); + } +} +// --跳过代码-- +``` + +*清单 20-19:为保存着各个闭包的 `Box` 创建出 `Job` 类型别名,并于随后把作业下发到通道* + +使用咱们在 `execute` 中得到的闭包创建出一个新的 `Job` 实例后,咱们便把那项作业下发到通道的发送端。对于发送失败的情形,咱们调用了 `send` 上的 `unwrap` 方法。在比如咱们停止全部线程执行,即表示接收端已停止接收新消息时,发送失败就可能发生。在那个时刻,咱们是无法停止咱们的线程执行的:只要这个线程池存在,咱们的线程就会继续执行。咱们使用 `unwrap` 的原因,就是咱们清楚这样的失败情况不会发生,但编译器是不了解这点的。 + +但咱们还没有大功告成!在 `worker` 里,传递给 `thread::spawn` 的闭包,仍然只 *引用* 了通道的接收端。相反,咱们需要闭包一直循环,向通道接收端请求一项作业,并在其获取到一项作业时运行该项作业。下面咱们就来完成下面清单 20-20 中所给出的对 `Worker::new` 的修改。 + +文件名:`src/lib.rs` + +```rust +// --跳过代码-- + +impl Worker { + fn new(id: usize, receiver: Arc>>) -> Worker { + let thread = thread::spawn(move || loop { + let job = receiver.lock().unwrap().recv().unwrap(); + + println! ("Worker {id} 获取到一项作业;执行中。"); + + job(); + }); + + Worker { id, thread } + } +} +``` + +*清单 20-20:在 `worker` 的线程中接收并执行作业* + +这里,咱们首选调用了 `receiver` 上的 `lock` 来请求那个互斥量,mutex,并于随后调用 `unwrap` 来在出现任何错误时终止运行。在互斥量处于 *中毒,poisoned* 状态时,请求锁就会失败,在有别的某个线程终止运行的同时,持有着而没有释放该锁时,这种情况便会发生。在这种情况下,调用 `unrap` 来让这个线程终止运行,便是要采取的正确措施。请放心地把这个 `unwrap`,修改为一个带有对咱们有意义报错信息的 `expect`。 + +当咱们获得了那个互斥量上的锁时,咱们就会调用 `recv` 来从通道接收一个 `Job`。最后的 `unwrap` 也会带过这里的任何错误,在持有 `sender` 的线程已关闭时就会发生这些错误,就跟 `receiver` 关闭时那个 `send` 方法会返回 `Err` 类似。 + +到 `recv` 的调用会阻塞,因此在尚无作业时,当前线程将等待,直到有某项作业可用。`Mutex` 确保了一次只有一个 `Worker` 线程是在尝试请求作业。 + +咱们的线程池现在就处于工作状态了!给他一次 `cargo run` 并构造一些请求: + + +```console +$ cargo run + Compiling hello v0.1.0 (/home/lenny.peng/rust-lang-zh_CN/hello) +warning: field `workers` is never read + --> src/lib.rs:7:5 + | +6 | pub struct ThreadPool { + | ---------- field in this struct +7 | workers: Vec, + | ^^^^^^^ + | + = note: `#[warn(dead_code)]` on by default + +warning: fields `id` and `thread` are never read + --> src/lib.rs:48:5 + | +47 | struct Worker { + | ------ fields in this struct +48 | id: usize, + | ^^ +49 | thread: thread::JoinHandle<()>, + | ^^^^^^ + +warning: `hello` (lib) generated 2 warnings + Finished dev [unoptimized + debuginfo] target(s) in 0.60s + Running `target/debug/hello` +Worker 1 获取到一项作业;执行中。 +Worker 0 获取到一项作业;执行中。 +Worker 2 获取到一项作业;执行中。 +Worker 3 获取到一项作业;执行中。 +Worker 1 获取到一项作业;执行中。 +Worker 0 获取到一项作业;执行中。 +``` + +成功了!咱们现在有了一个会异步执行 TCP 连接的线程池。绝不会有超过四个线程被创建出来,因此在服务器收到很多请求时,咱们的系统将不会过载。在咱们构造了一个到 `/sleep` 的请求时,服务器通过让另一线程运行别的一些请求,而将能服务这些请求。 + +> 注意:若咱们在多个窗口同时打开 `/sleep`,他们可能会在设置的时间间隔每次加载一个。有些 web 浏览器会出于缓存原因,而顺序执行同一请求的多个实例。这样的局限并不是由咱们的服务器导致的。 + +在了解了第 18 章中的 `while let` 循环后,咱们可能想知道,为何咱们没有如下清单 20-21 中所示的那样,编写 `worker` 线程的代码。 + +文件名:`src/lib.rs` + +```rust +impl Worker { + fn new(id: usize, receiver: Arc>>) -> Worker { + let thread = thread::spawn(move || { + while let Ok(job) = receiver.lock().unwrap().recv() { + println! ("Worker {id} 获取到一项作业;执行中。"); + + job(); + } + }); + + Worker { id, thread } + } +} +``` + +*清单 20-21:使用 `while let` 的一种 `Worker::new` 替代实现* + +这段代码将会编译及运行,但不会产生所需的线程行为:慢速请求仍将导致别的请求等待被处理。至于原因则有点微妙:由于锁的所有权是基于 `lock` 方法返回的 `LockResult>` 中,`MutexGuard` 的生命周期,因此这个 `Mutex` 结构体没有公开的 `unlock` 方法。在编译时,借用检查器可于随后,就除非咱们拿着 `Mutex` 所守卫的某项资源的锁,否则无法访问该项资源这一规则强制加以检查。但是,若咱们没有注意到 `MutexGuard` 的生命周期,那么这样的实现同样能导致锁相较预期被占用更长时间。 + +由于在 `let` 之下,等号右侧的表达式中用到的任何临时值,都会在 `let` 语句结束时被立即丢弃,因此使用了 `let job = receiver.lock().unwrap().recv().unwrap();` 的清单 20-20 中代码是工作的。但是,`while let`(以及 `if let` 与 `match`) 则是在相关代码块结束前,不会丢弃那些临时值。在清单 20-21 中,锁会在到 `job()` 的调用其将保持被持有,这意味着别的 `worker` 就没法收到作业。 diff --git a/src/final_project/single-threaded.md b/src/final_project/single-threaded.md new file mode 100644 index 0000000..07baa1d --- /dev/null +++ b/src/final_project/single-threaded.md @@ -0,0 +1,457 @@ +# 构建一个单线程的 Web 服务器 + +咱们将通过让一个单线程的 web 服务器工作起来而开始。在咱们开始前,先来看看在构建 web 服务器中涉及到的一些协议的快速概览。这些协议的细节,超出了本书范围,而简要概述,就将给到咱们所需的信息。 + +Web 服务器中涉及的两种主要谢谢,分别是 *超文本传输协议,Hypertext Transfer Protocol, HTTP* 与 *传输控制协议,Transmission Control Protocol, TCP*。两种协议都是 *请求-响应,request-response* 的协议,表示 *客户端,client* 发起请求,而 *服务器,server* 监听到请求并提供给客户端一个响应。这些请求和响应的内容是由两种协议定义的。 + +TCP 是种描述了信息如何从一台服务器到达另一服务器,但并未指明信息为何的低级别。HTTP 则是经由定义请求与响应的内容,而于 TCP 之上构建的。技术上要在其他协议上使用 HTTP 是可行的,但在绝大多数情况下,HTTP 都在 TCP 上发送他的数据。咱们将在 TCP 的原始字节,与 HTTP 请求和响应下,进行工作。 + + +## 监听 TCP 连接 + +**Listen to the TCP Connection** + + +咱们的 web 服务器需要监听某个 TCP 连接,因此那便是咱们将要做的第一部分工作。标准库提供了一个 `std::net` 模组,允许咱们完成这一点。咱们来以寻常方式构造一个新的项目: + +```console +$ cargo new hello --vcs none + Created binary (application) `hello` package +$ cd hello +``` + +现在请输入下面清单 20-1 中 `src/main.rs` 里的代码来开始。这段代码会在本地地址 `127.0.0.1:7878` 处监听传入的 TCP 流。当他获取到一个传入流时,他就会打印 `连接已建立!`。 + +文件名:`src/main.rs` + +```rust +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! ("连接已建立!"); + } +} +``` + +*清单 20-1:监听传入流并在咱们接收到某个流时打印一条消息* + +运用 `TcpListener`,咱们就可以在地址 `127.0.0.1:7878` 处监听 TCP 连接。在这个地址中,冒号之前的部分,是个表示咱们的计算机的 IP 地址(在所有计算机上这都是同样的,而并不特别表示本书作者的计算机),同时 `7878` 为端口。咱们之所以选择了这个端口,有两个原因:通常不是在这个端口上接收 HTTP,因此咱们的服务器,大概率不会与咱们可能在咱们的机器上运行的任何别的 web 服务器冲突,而 `7878` 则是电话机上输入的 *rust*。 + +这个场景中的 `bind` 函数,会像将返回一个新 `TcpListener` 实例的 `new` 函数一样工作。该函数之所以叫做 `bind`,是因为在网络通信中,连接到要监听的端口,被称为 “绑定到端口”。 + +`bind` 函数返回的是个 `Result`,表明有可能绑定失败。比如,连接到端口 `80` 需要管理员权限(非管理员只可以监听高于 `1023` 的那些端口,译注:在 *nix 平台上有此限制,但在 Win 平台上没有),因此若咱们在非管理员下尝试连接到端口 `80`,端口绑定就不会工作。在比如咱们运行了这个程序的两个实例,而因此有两个程序在监听同一端口时,端口绑定也不会工作。由于咱们仅是处于学习目的,而编写的一个基本服务器,因此咱们就不会纠结于处理这些类别的错误;相反,咱们使用 `unwrap` 来在错误发生时停止这个程序。 + +`TcpListener` 上的 `incoming` 方法,会返回一个给到咱们流(更具体的,是一些类型 `TcpStream` 的流)序列的迭代器,an iterator that gives us a sequence of streams。单一的 *流,stream* 表示了客户端与服务器之间的一个打开的连接,an open connection。而一个 *连接,connection* 则是客户端连接到服务器过程中,完整的请求与响应的叫法,服务器会生成一个响应,且服务器会关闭这个连接。就这样,咱们将从那个 `TcpStream` 读取,来看看客户端发送了什么,并于随后把咱们的响应写到这个流,以将数据发送回客户端。总的来说,这个 `for` 循环将依次处理每个连接,并为咱们产生一系列要处理的流。 + +至于现在,咱们对流的处理,是由在流有任何错误时,调用 `unwrap` 来终止咱们的程序所构成;若没有任何错误,那么这个程序就会打印一条消息。在下一代码清单中,咱们将为流成功的情形,添加更多功能。在客户端连接到服务器时,咱们可能会从那个 `incoming` 方法收到错误的原因,便是咱们没有真正在一些连接上迭代。相反,咱们是在一些 *连接尝试,connection attempts* 上迭代。连接可能因为数种原因而不成功,许多的这些原因都是特定于操作系统的。比如,许多操作系统都有他们所支持的并发开启连接数限制,a limit to the number of simultaneous open connecitons;超出那个数目的新建连接尝试就会产生错误,除非一些开启的连接关闭。 + +咱们来尝试运行这段代码!在终端里运行 `cargo run` 并随后在 web 浏览器中加载 `127.0.0.1:7878`。由于服务器没有正确发回任何数据,因此浏览器应给出像是 `Connection reset,` 的错误消息。但当咱们看着终端时,应看到在浏览器连接到服务器时,有数条打印处的消息! + +> 注:可使用 `$curl 127.0.0.1:7878` 命令进行调试,且使用 `curl` 也是网络编程调试中常用的方法。 + +```console + Running `target/debug/hello` +连接已建立! +连接已建立! +连接已建立! +连接已建立! +``` + +有的时候,咱们会看到一次浏览器请求下打印出的多条消息;原因可能是浏览器在构造页面请求时,也会构造其他资源的请求,像是出现在浏览器 tab 分页中的 `favicon.ico` 图标。 + +也可由可能是由于这个服务器没有响应任何数据,浏览器因此会尝试多次连接到这个服务器。在 `stream` 超出作用域,而在那个循环结束出被丢弃时,连接就会作为 `drop` 实现的一部分而被关闭。由于故障可能是临时的,因此浏览器有时会以重试处理关闭的连接。重要的是,咱们已然成功得到了到 TCP 连接的句柄,a handle to a TCP connection! + +请记得在咱们完成运行代码的特定版本时,要通过按下 `Ctrl-c` 来停止这个程序。以后在完成了各套代码修改后,要通过运行 `cargo run` 命令重启这个程序,来确保咱们是在运行最新的代码。 + + +## 读取请求 + +**Reading the Request** + + +咱们来实现读取来自浏览器请求的功能!为将首选获取到连接,及随后对连接采取一些措施这两个关注点分离,咱们将开启一个用于处理连接的新函数。在这个新的 `handle_connection` 函数中,咱们将从 TCP 流读取数据,并将其打印出来,从而咱们就可以看到从浏览器发出的数据。请将代码修改为清单 20-2 这样。 + +文件名:`src/main.rs` + +```rust +#![allow(warnings)] +use std::{ + io::{prelude::*, BufReader}, + 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_conn(stream); + } +} + +fn handle_conn(mut stream: TcpStream) { + let buf_reader = BufferedReader::new(stream); + let http_req: Vec<_> = buf_reader + .lines() + .map(|res| res.unwrap()) + .take_while(|line| !line.is_empty()) + .collect(); + + println! ("请求:{:#?}", http_request); +} +``` + +*清单 20-2:从 `TcpStream` 读取并打印出数据* + + +咱们将 `std::io::prelude` 与 `std::io::BufReader` 带入作用域,来获取到实现从 TCP 流读取和写入的那些特质与类型的访问。在 `main` 函数的那个 `for` 循环中,不再是打印一条声称咱们已构造一个连接的消息,咱们限制调用了新的 `handle_conn` 函数,并把那个 `stream` 传递给他。 + +在 `handle_conn` 函数中,咱们创建了一个新的,封装着到 `stream` 的一个可变引用的 `BufReader` 实例。`BufReader` 会通过管理到 `std::io::Read` 特质一些方法的调用,为咱们添加缓冲。 + +咱们创建了一个名为 `http_req` 的变量,来收集浏览器发送到咱们服务器的请求的那些行。通过添加那个 `Vec<_>` 类型注解,咱们表明了咱们打算把这些行收集到一个矢量值中。 + +`BufReader` 实现了 `std::io::BufRead` 特质,该特质提供了 `lines` 方法。`lines` 方法会经由当其发现一个新行字节,a newline byte, 时分割数据流,而返回一个 `Result 注:使用 `curl --noproxy '*' 127.0.0.1:7878` 的输出,如下面这样: + + +```console +$ cargo run + Finished dev [unoptimized + debuginfo] target(s) in 0.00s + Running `target/debug/hello` +请求:[ + "GET / HTTP/1.1", + "Host: 127.0.0.1:7878", + "User-Agent: curl/7.68.0", + "Accept: */*", +] +``` + +根据咱们的浏览器,咱们可能会得到些许不同的输出。既然咱们打印了请求数据,咱们就可以通过查看请求第一行中 `GET` 之后的路径,而发现为何咱们会从一次浏览器请求,得到多个连接。若重复的连接都是在请求 `/`,咱们就知道由于浏览器没有从咱们的程序得到响应,因此其是在尝试重复获取 `/`。 + +下面来对这一请求数据加以细分,以搞清楚浏览器是在询问咱们的程序些什么。 + + +## 近观 HTTP 请求 + +**A closer Look at an HTTP Request** + + +HTTP 是种基于文本的协议,而请求会采用下面这种格式: + +```text +Method Request-URI HTTP-Version CRLF +headers CRLF +message-body +``` + +第一行是保存着有关该客户端正请求什么的信息的 *请求行,request line*。该请求行的第一部分,表示正使用的 *方法,method*,比如 `GET` 或 `POST`,描述了客户端是如何构造此请求的。咱们的客户端使用了一个 `GET` 请求,这意味着其是在询问信息。 + +请求行接下来的部分为 `/`,表示客户端正请求的 *同一资源标识符,Uniform Resource Identifier, URI*:URI 几乎是,但不完全与 *同一资源定位符,Uniform Resource Locator, URL* 一样。URIs 与 URLs 之间的区别对于这章中咱们的目的不重要,但 HTTP 的规格使用了 URI 这个词,因此咱们只能在此处暗自用 URL 代替 URI。 + +最后部分是客户端所用的 HTTP 版本,而随后这个请求行便以一个 *CRLF 序列,CRLF sequence* (CRLF 代表的是 *回车,carriage return* 与 *换行,line fedd*,是打字机时代的术语!)结束了。这个 CRLF 序列还可以写作 `\r\n`,其中的 `\r` 是个回车,而 `\n` 是个换行。CRLF 序列将请求行与其余的请求数据分开。请注意当 CRLF 被打印时,咱们会看到一个新行开始,而非 `\r\n`。 + +查看如今咱们从运行这个程序所接收到的请求行数据,咱们发现 `GET` 即为请求方法,`/` 便是请求的 URI,而 `HTTP/1.1` 则是请求的 HTTP 版本。 + +在请求行之后,从 `Host:` 开始的其余那些行,就是些头了。`GET` 请求没有请求体。 + +请从不同浏览器构造请求,或是询问不同地址,比如 `127.0.0.1:7878/test`,来发现请求数据会怎样变化。 + +> 注:运行 `curl --noproxy '*' 127.0.0.1:7878/test` 时,请求数据如下所示: + + +```console +请求:[ + "GET /test HTTP/1.1", + "Host: 127.0.0.1:7878", + "User-Agent: curl/7.68.0", + "Accept: */*", +] +``` + +既然咱们明白了浏览器是在询问什么,下面就来发回一些数据吧! + + +## 写下响应 + +**Writing a Response** + + +咱们将要实现发送响应客户端请求数据。响应有着下面的格式: + +```text +HTTP-Version Status-Code Reason-Phrase CRLF +headers CRLF +message-body +``` + +其中第一行是包含在响应中用到的 HTTP 版本的 *状态行,status line*、汇总了请求结果的一个数字的状态码、以及提供了状态码文字描述的一个原因短语,a reason phrase。在那个 CRLF 之后是一些 HTTP 头、另一个 CRLF 序列、及响应的响应体。 + +下面就是一个使用了 HTTP 版本 1.1 的示例响应,有着状态码 `200`、一个 `OK` 的原因短语、没有头部、也没有响应体。 + +```text +HTTP/1.1 200 OK\r\n\r\n +``` + +状态代码 `200` 是标准的成功响应。这个文本便是个极小的成功 HTTP 响应。下面来把这个响应,作为咱们到成功请求的响应,写到 TCP 流!在那个 `handle_conn` 函数中,移除曾是打印请求数据的 `println!`,而将其替换为下面清单 20-3 中的代码。 + +文件名:`src/main.rs` + +```rust +fn handle_conn(mut stream: TcpStream) { + let buf_reader = BufReader::new(&mut stream); + let http_req: Vec<_> = buf_reader + .lines() + .map(|res| res.unwrap()) + .take_while(|line| !line.is_empty()) + .collect(); + + let resp = "HTTP/1.1 200 OK\r\n\r\n"; + + stream.write_all(resp.as_bytes()).unwrap(); +} +``` + +*清单 20-3:将一个极小的成功 HTTP 响应写到 TCP 流* + +那第一行定义了保存成功消息数据的 `resp` 变量。随后咱们在咱们的 `resp` 上调用 `as_bytes`,将字符串数据转换为一些字节。`stream` 上的 `write_all` 方法,会取一个 `&[u8]` 并将那些字节直接发送到 TCP 连接。由于 `write_all` 操作可能失败,咱们就像前面一样,于任何的错误结果上使用 `unwrap`。再次,在真实应用中,咱们会在这里加上错误处理。 + +有了这些修改,咱们来运行咱们的代码,并构造一次请求。咱们就不再打印任何数据到终端,因此咱们不会看到除 Cargo 的输出外,其他任何的输出。当咱们在 web 浏览器中加载 `127.0.0.1:7878` 时,咱们应得到一个空白页而非报错。咱们刚刚已经硬编码了接收 HTTP 请求并发送一次响应了! + + +## 返回真正的 HTML + +**Returning Real HTML** + + +下面来实现返回相比空白页更多内容的功能。请在咱们的项目目录根处,而非 `src` 目录中创建一个新文件 `hello.html`。咱们可放入任何咱们想要的 HTML;下面清单 20-4 给出了一种可能。 + +文件名:`hello.html` + +```html + + + + + 你好! + + +

你好!

+

来自 Rust 的问好

+ + +``` + +*清单 20-4:要在响应中返回的一个样例 HTML 文件* + +这是个带有一个与一些文本的最小 HTML5 文档。要在收到一个请求时从服务器返回这个文档,咱们将如下清单 20-5 中所示那样,修改 `handle_conn` 来读取这个 HTML 文件,将其作为响应体,添加到一个响应,并将其发送。 + +文件名:`src/main.rs` + +```rust +#![allow(warnings)] +use std::{ + fs, + io::{prelude::*, BufReader}, + 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_conn(stream); + } +} + +fn handle_conn(mut stream: TcpStream) { + let buf_reader = BufReader::new(&mut stream); + let http_req: Vec<_> = buf_reader + .lines() + .map(|res| res.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 resp = + format! ("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); + + stream.write_all(resp.as_bytes()).unwrap(); +} +``` + +*清单 20-5:将 `hello.html` 的内容作为响应的响应体发送* + +咱们已添加 `fs` 到那个 `use` 语句,来将标准库的文件系统模组带入到作用域中。把文件内容读取到一个字符串的代码应看起来不陌生;在第 12 章,于清单 12-4 中为咱们的 I/O 项目读取一个文件的内容时,咱们曾用到过他。 + +接下来,咱们使用了 `format!` 宏,来将那个文件的内容,添加为这个成功响应的响应体。为确保一个有效的 HTTP 响应,咱们添加了被设置为咱们的响应体大小的一个 `Content-Length` 头部,在这个示例中就是 `hello.html` 的大小。 + +以 `cargo run` 运行这段代码,并在浏览器中加载 `127.0.0.1:7878`;咱们应看到咱们的 HTML 被渲染了! + +目前,咱们忽略了 `http_req` 中的响应数据,而只是无条件地发回那个 HTML 文件的内容。那就意味着当咱们在浏览器中尝试请求 `127.0.0.1:7878/something-else` 时,咱们将仍然得到这同样的 HTML 响应。此刻,咱们的服务器是非常有限的,且不会完成绝大多数 web 服务器所完成的那些事情。咱们打算根据请求定制咱们的响应,并只为格式良好的到 `/` 请求,发回这个 HTML 文件。 + + +## 对请求加以验证并有选择地进行响应 + +**Validating the Request and Selectively Responding** + + +现在,咱们的 web 服务器将始终返回那个文件中的 HTML,而不管客户端请求的是什么。下面来添加在返回那个 HTML 文件前,检查浏览器是在请求 `/`,并在浏览器请求其他路径时,返回一个错误的功能。为此,咱们需要如下面清单 20-6 中所示的那样修改 `handle_conn`。这段新代码会将收到的请求,与咱们所知的 `/` 请求看起来的样子对比检查,并添加了 `if` 及 `else` 代码块来分别对待各种请求。 + +文件名:`src/main.rs` + +```rust +fn handle_conn(mut stream: TcpStream) { + let buf_reader = BufReader::new(&mut stream); + let req_line = buf_reader.lines().next().unwrap().unwrap(); + + if req_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 resp = + format! ("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); + + stream.write_all(resp.as_bytes()).unwrap(); + } else { + // 别的一些请求 + } +} +``` + +*清单 20-6:以不同于其他请求方式,处理到 `/` 的请求* + +咱们只打算看看 HTTP 请求的第一行,因此就不再将整个请求读取到一个矢量值了,咱们调用了 `next` 来从那个迭代器得到第一个条目。这里的首个 `unwrap` 会注意其中的 `Option`,并在迭代器没有条目时停止这个程序。第二个 `unwrap` 则会处理其中的 `Result`,并与清单 20-2 中所添加的 `map` 里的那个 `unwrap` 有着同样的效果。 + +接下来,咱们检查了 `req_line`,来看看其是否等于到 `/` 路径 `GET` 请求的请求行。在其等于时,那个 `if` 代码块就会返回咱们 HTML 文件的内容。 + +若 `req_line` *不* 等于到 `/` 路径 `GET` 请求的第一行时,就意味着咱们收到了一些别的请求。稍后咱们将添加代码到那个 `else` 代码块,来响应全部其他请求。 + +现在请运行此代码,并请求 `127.0.0.1:7878`;咱们应获取到 `hello.html` 中的 HTML。在咱们构造任何其他请求时,比如 `127.0.0.1:7878/something-else`,就将得到像是咱们曾在运行清单 20-1 及清单 20-2 中的代码时,所看到连接错误。 + +现在来将清单 20-7 中的代码,添加到那个 `else` 代码块,以返回一个带有状态代码 `404` 的响应,这通告了请求的内容未找到。咱们还将返回一些在浏览器中要渲染页面的 HTML,将这种响应表示给终端用户。 + +文件名:`src/main.rs` + +```rust + // --跳过代码-- + } else { + let status_line = "HTTP/1.1 404 NOT FOUND"; + let contents = fs::read_to_string("404.html").unwrap(); + let length = contents.len(); + + let resp = + format! ("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); + + stream.write_all(resp.as_bytes()).unwrap(); + } +``` + +*清单 20-7:在请求的是除 `/` 外的其他路径时以状态代码 `404` 及一个错误页面进行响应* + +此处,咱们的响应有着一个代码状态代码 `404`,及原因短语 `NOT FOUND` 的状态行。该响应的响应体,将是文件 `404.html` 中的 HTML。咱们将需要创建 `hello.html` 旁边,用于错误页面的 `404.html` 文件;请再次随意使用咱们想要的任何 HTML,或使用下面清单 20-8 中的示例 HTML。 + +文件名:`404.html` + +```html + + + + + 你好! + + +

糟糕!

+

抱歉,我不明白你要什么。

+ + +``` + +*清单 20-8:全部 404 响应下要发回页面的示例内容* + +在这些修改下,请再次运行咱们的服务器。请求 `127.0.0.1:7878` 应返回 `hello.html` 的内容,而任何别的请求,像是 `127.0.0.1:foo`,就应返回 `404.html` 中的报错 HTML。 + + +## 初试重构 + +**A Touch of Refactoring** + + +此时的 `if` 与 `else` 两个代码块,有着很多重复:他们都在读取文件及将文件内容写到 TCP 流。唯二区别就是响应的状态行与文件名。下面就来通过抽取处这些差异到单独的 `if` 和 `else` 行,这些行将把响应状态行与文件名,赋值给两个变量;随后咱们就可以在代码中,不带条件地使用这两个变量,来读取文件并写下响应。下面清单 20-9 给出了替换了大段的 `if` 与 `else` 代码块后的最终代码。 + +文件名:`src/main.rs` + +```rust +// --跳过代码-- +fn handle_conn(mut stream: TcpStream) { + let buf_reader = BufReader::new(&mut stream); + let req_line = buf_reader.lines().next().unwrap().unwrap(); + + let (status_line, filename) = if req_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 resp = + format! ("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); + + stream.write_all(resp.as_bytes()).unwrap(); +} +``` + +*清单 20-9:将 `if` 和 `else` 代码块重构为只包含两种情况下不同的代码* + +现在 `if` 与 `else` 两个代码块,就只会返回一个元组中,响应状态行与文件名的相应值了;随后咱们运用第 18 章中曾讨论过的 `let` 语句中的模式,而使用了解构特性,来将这两个值复制给 `status_line` 与 `filename`。 + +原先那些重复代码,现在便是在 `if` 与 `else` 两个代码块外面,并使用了 `status_line` 与 `filename` 两个变量。这令到看出两种情况之间的差别更为容易,并意味着在咱们打算修改文件读取与响应写入工作方式时,只有一处要更新代码。清单 20-9 中代码的行为,将与清单 20-8 中的一致。 + +相当棒!现在咱们就有了一个以差不多 40 行 Rust 代码编写的,以一个内容页面响应一个到 `/` 的请求,并以一个 `404` 响应回应全部其他请求的简单 web 服务器了。 + +当前,咱们的服务器是运行在单线程下的,意味着其只能一次服务一个请求。接下来就要通过模拟一下低速请求,检查那怎样会称为一个问题。随后咱们将修复这个问题,从而让咱们的服务器可以一次处理多个请求。 + + +