mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2024-11-09 08:51:18 +08:00
update ch20-03
This commit is contained in:
parent
0233a610ba
commit
29fe5bbb9c
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
### 在 Linux 或 Mac 上安装
|
### 在 Linux 或 Mac 上安装
|
||||||
|
|
||||||
如果你使用 Linux 或 Mac,你需要做的全部,就是打开一个终端并输入:
|
如果你使用 Linux 或 Mac,你需要做的全部就是打开一个终端并输入:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ curl https://sh.rustup.rs -sSf | sh
|
$ curl https://sh.rustup.rs -sSf | sh
|
||||||
|
@ -193,4 +193,201 @@ fn handle_connection(mut stream: TcpStream) {
|
|||||||
|
|
||||||
`write` 可能会失败,所以 `write` 返回 `Result<T, E>`;我们继续使用 `unwrap` 以继续本章的核心内容而不是处理错误。最后,`flush` 会等待直到所有字节都被写入连接中;`TcpStream` 包含一个内部缓冲区来最小化对底层操作系统的调用。
|
`write` 可能会失败,所以 `write` 返回 `Result<T, E>`;我们继续使用 `unwrap` 以继续本章的核心内容而不是处理错误。最后,`flush` 会等待直到所有字节都被写入连接中;`TcpStream` 包含一个内部缓冲区来最小化对底层操作系统的调用。
|
||||||
|
|
||||||
有了这些修改,运行我们的代码并进行请求!
|
有了这些修改,运行我们的代码并进行请求!我们不再向终端打印任何数据,所以不会再看到除了 Cargo 以外的任何输出。不过当在浏览器中加载 `127.0.0.1:8080` 时,会得到一个空页面而不是错误。太棒了!我们刚刚手写了一个 HTTP 请求与响应。
|
||||||
|
|
||||||
|
### 返回真正的 HTML
|
||||||
|
|
||||||
|
让我们不只是返回空页面。在项目根目录创建一个新文件,*hello.html*,也就是说,不是在 `src` 目录。在此可以放入任何你期望的 HTML;列表 20-4 展示了本书作者所采用的:
|
||||||
|
|
||||||
|
<span class="filename">文件名: hello.html</span>
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Hello!</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello!</h1>
|
||||||
|
<p>Hi from Rust</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
<span class="caption">列表 20-4:一个简单的 HTML 文件用来作为响应</span>
|
||||||
|
|
||||||
|
这是一个极小化的 HTML 5 文档,它有一个标题和一小段文本。如列表 20-5 所示修改 `handle_connection` 来读取 HTML 文件,将其加入到响应的 body 中,并发送:
|
||||||
|
|
||||||
|
<span class="filename">文件名: src/main.rs</span>
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# use std::io::prelude::*;
|
||||||
|
# use std::net::TcpStream;
|
||||||
|
use std::fs::File;
|
||||||
|
|
||||||
|
// ...snip...
|
||||||
|
|
||||||
|
fn handle_connection(mut stream: TcpStream) {
|
||||||
|
let mut buffer = [0; 512];
|
||||||
|
stream.read(&mut buffer).unwrap();
|
||||||
|
|
||||||
|
let mut file = File::open("hello.html").unwrap();
|
||||||
|
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents).unwrap();
|
||||||
|
|
||||||
|
let response = format!("HTTP/1.1 200 OK\r\n\r\n{}", contents);
|
||||||
|
|
||||||
|
stream.write(response.as_bytes()).unwrap();
|
||||||
|
stream.flush().unwrap();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<span class="caption">列表 20-5:将 *hello.html* 的内容作为响应 body 发送</span>
|
||||||
|
|
||||||
|
在开头增加了一行来将标准库中的 `File` 引入作用域,打开和读取文件的代码应该看起来很熟悉,因为第十二章 I/O 项目的列表 12-4 中读取文件内容时出现过类似的代码。
|
||||||
|
|
||||||
|
接下来,使用 `format!` 将文件内容加入到将要写入流的成功响应的 body 中。
|
||||||
|
|
||||||
|
使用 `cargo run` 运行程序,在浏览器加载 `127.0.0.1:8080`,你应该会看到渲染出来的 HTML 文件!
|
||||||
|
|
||||||
|
注意目前忽略了 `buffer` 中的请求数据并无条件的发送了 HTML 文件的内容。尝试在浏览器中请求 `127.0.0.1:8080/something-else` 也会得到同样的 HTML。对于所有请求都发送相同的响应其作用是非常有限的,也不是大部分 server 所做的;让我们检查请求并只对格式良好(well-formed)的请求 `/` 发送 HTML 文件。
|
||||||
|
|
||||||
|
### 验证请求并有选择的响应
|
||||||
|
|
||||||
|
目前我们的 server 不管客户端请求什么都会返回相同的 HTML 文件。让我们检查浏览器是否请求 `/`, 并在其请求其他内容时返回错误。如列表 20-6 所示修改 `handle_connection` ,它增加了所需的那部分代码。这一部分将接收到的请求的内容与已知的 `/` 请求的一部分做比较,并增加了 `if` 和 `else` 块来加入处理不同请求的代码:
|
||||||
|
|
||||||
|
<span class="filename">Filename: src/main.rs</span>
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# use std::io::prelude::*;
|
||||||
|
# use std::net::TcpStream;
|
||||||
|
# use std::fs::File;
|
||||||
|
// ...snip...
|
||||||
|
|
||||||
|
fn handle_connection(mut stream: TcpStream) {
|
||||||
|
let mut buffer = [0; 512];
|
||||||
|
stream.read(&mut buffer).unwrap();
|
||||||
|
|
||||||
|
let get = b"GET / HTTP/1.1\r\n";
|
||||||
|
|
||||||
|
if buffer.starts_with(get) {
|
||||||
|
let mut file = File::open("hello.html").unwrap();
|
||||||
|
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents).unwrap();
|
||||||
|
|
||||||
|
let response = format!("HTTP/1.1 200 OK\r\n\r\n{}", contents);
|
||||||
|
|
||||||
|
stream.write(response.as_bytes()).unwrap();
|
||||||
|
stream.flush().unwrap();
|
||||||
|
} else {
|
||||||
|
// some other request
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<span class="caption">列表 20-6:将请求与期望的 `/` 请求内容做匹配,并设置对 `/` 和其他请求的条件化处理</span>
|
||||||
|
|
||||||
|
这里在变量 `get` 中硬编码了所需的请求相关的数据。因为我们从缓冲区中读取原始字节,所以使用了字节字符串,使用 `b""` 使得 `get` 也是一个字节字符串。接着检查 `buffer` 是否以 `get` 中的字节开头。如果是,这就是一个格式良好的 `/` 请求,也就是 `if` 块中期望处理的成功情况。`if` 块中包含列表 20-5 中增加的返回 HTML 文件内容的代码。
|
||||||
|
|
||||||
|
如果 `buffer` 不以 `get` 中的字节开头,就说明是其他请求。对于所有其他请求都将使用 `else` 块中增加的代码来响应。
|
||||||
|
|
||||||
|
如果运行代码并请求 `127.0.0.1:8080`,就会得到 *hello.html* 中的 HTML。如果进行其他请求,比如 `127.0.0.1:8080/something-else`,则会得到像运行列表 20-1 和 20-2 中代码那样的连接错误。
|
||||||
|
|
||||||
|
如列表 20-7 所示向 `else` 块增加代码来返回一个带有 `404` 状态码的响应,这代表了所请求的内容没有找到。接着也会返回一个 HTML 向浏览器终端用户表明此意:
|
||||||
|
|
||||||
|
<span class="filename">Filename: src/main.rs</span>
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# use std::io::prelude::*;
|
||||||
|
# use std::net::TcpStream;
|
||||||
|
# use std::fs::File;
|
||||||
|
# fn handle_connection(mut stream: TcpStream) {
|
||||||
|
# if true {
|
||||||
|
// ...snip...
|
||||||
|
|
||||||
|
} else {
|
||||||
|
let header = "HTTP/1.1 404 NOT FOUND\r\n\r\n";
|
||||||
|
let mut file = File::open("404.html").unwrap();
|
||||||
|
let mut contents = String::new();
|
||||||
|
|
||||||
|
file.read_to_string(&mut contents).unwrap();
|
||||||
|
|
||||||
|
let response = format!("{}{}", header, contents);
|
||||||
|
|
||||||
|
stream.write(response.as_bytes()).unwrap();
|
||||||
|
stream.flush().unwrap();
|
||||||
|
}
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
|
||||||
|
<span class="caption">列表 20-7:对于任何不是 `/` 的请求返回 `404` 状态码的响应和错误页面</span>
|
||||||
|
|
||||||
|
这里,响应头有状态码 `404` 和原因短语 `NOT FOUND`。仍然没有任何 header,而其 body 将是 *404.html* 文件中的 HTML。也在 *hello.html* 同级目录创建 *404.html* 文件作为错误页面;这一次也可以随意使用任何 HTML 或使用列表 20-8 中的示例 HTML:
|
||||||
|
|
||||||
|
<span class="filename">文件名: 404.html</span>
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Hello!</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Oops!</h1>
|
||||||
|
<p>Sorry, I don't know what you're asking for.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
<span class="caption">列表 20-8:任何 `404` 响应所返回错误页面内容样例</span>
|
||||||
|
|
||||||
|
有了这些修改,再次运行 server。请求 `127.0.0.1:8080` 应该会返回 *hello.html*,而对于任何其他请求,比如 `127.0.0.1:8080/foo`,应该会返回 *404.html* 中的错误 HTML!
|
||||||
|
|
||||||
|
`if` 和 `else` 块中的代码有很多的重复:他们都读取文件并将其内容写入流。这两个情况唯一的区别是状态行和文件名。将这些区别分别提取到一行 `if` 和 `else` 中,对状态行和文件名变量赋值;然后在读取文件和写入响应的代码中无条件的使用这些变量。重构后代码后的结果如列表 20-9 所示:
|
||||||
|
|
||||||
|
<span class="filename">文件名: src/main.rs</span>
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# use std::io::prelude::*;
|
||||||
|
# use std::net::TcpStream;
|
||||||
|
# use std::fs::File;
|
||||||
|
// ...snip...
|
||||||
|
|
||||||
|
fn handle_connection(mut stream: TcpStream) {
|
||||||
|
# let mut buffer = [0; 512];
|
||||||
|
# stream.read(&mut buffer).unwrap();
|
||||||
|
#
|
||||||
|
# let get = b"GET / HTTP/1.1\r\n";
|
||||||
|
// ...snip...
|
||||||
|
|
||||||
|
let (status_line, filename) = if buffer.starts_with(get) {
|
||||||
|
("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
|
||||||
|
} else {
|
||||||
|
("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut file = File::open(filename).unwrap();
|
||||||
|
let mut contents = String::new();
|
||||||
|
|
||||||
|
file.read_to_string(&mut contents).unwrap();
|
||||||
|
|
||||||
|
let response = format!("{}{}", status_line, contents);
|
||||||
|
|
||||||
|
stream.write(response.as_bytes()).unwrap();
|
||||||
|
stream.flush().unwrap();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<span class="caption">列表 20-9:重构代码使得 `if` 和 `else` 块中只包含两个情况所不同的代码</span>
|
||||||
|
|
||||||
|
这里,`if` 和 `else` 块所做的唯一的事就是在一个元组中返回合适的状态行和文件名的值;接着使用第十八章讲到的使用模式的 `let` 语句通过解构元组的两部分给 `filename` 和 `header` 赋值。
|
||||||
|
|
||||||
|
读取文件和写入响应的冗余代码现在位于 `if` 和 `else` 块之外,并会使用变量 `status_line` 和 `filename`。这样更易于观察这两种情况真正有何不同,并且如果需要改变如何读取文件或写入响应时只需要更新一处的代码。列表 20-9 中代码的行为与列表 20-8 完全一样。
|
||||||
|
|
||||||
|
好极了!我们有了一个 40 行左右 Rust 代码的小而简单的 server,它对一个请求返回页面内容而对所有其他请求返回 `404` 响应。
|
||||||
|
|
||||||
|
不过因为 server 运行于单线程中,它一次只能处理一个请求。让我们模拟一些慢请求来看看这如何会称为一个问题。
|
66
src/ch20-02-slow-requests.md
Normal file
66
src/ch20-02-slow-requests.md
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
## 慢请求如何影响吞吐率
|
||||||
|
|
||||||
|
> [ch20-02-slow-requests.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch20-02-slow-requests.md)
|
||||||
|
> <br>
|
||||||
|
> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9
|
||||||
|
|
||||||
|
目前 server 会依次处理每一个请求。这对于向我们这样并不期望有非常大量请求的服务来说是可行的,不过随着程序变得更复杂,这样的串行处理并不是最优的。
|
||||||
|
|
||||||
|
因为当前的程序顺序处理处理连接,在完成第一个连接的处理之前不会处理第二个连接。如果一个请求花费很长时间来处理,这段时间接收的请求则不得不等待这个长请求结束,即便这些新请求可以很快就处理完。让我们实际尝试一下。
|
||||||
|
|
||||||
|
### 在当前 server 实现中模拟慢请求
|
||||||
|
|
||||||
|
让我们看看一个花费很长时间处理的请求对当前的 server 实现有何影响。列表 20-10 展示了对另一个请求的响应代码,`/sleep`,它会使 server 在响应之前休眠五秒。这将模拟一个慢请求以便体现出 server 在串行的处理请求。
|
||||||
|
|
||||||
|
<span class="filename">文件名: src/main.rs</span>
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
# use std::io::prelude::*;
|
||||||
|
# use std::net::TcpStream;
|
||||||
|
# use std::fs::File;
|
||||||
|
// ...snip...
|
||||||
|
|
||||||
|
fn handle_connection(mut stream: TcpStream) {
|
||||||
|
# let mut buffer = [0; 512];
|
||||||
|
# stream.read(&mut buffer).unwrap();
|
||||||
|
// ...snip...
|
||||||
|
|
||||||
|
let get = b"GET / HTTP/1.1\r\n";
|
||||||
|
let sleep = b"GET /sleep HTTP/1.1\r\n";
|
||||||
|
|
||||||
|
let (status_line, filename) = if buffer.starts_with(get) {
|
||||||
|
("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
|
||||||
|
} else if buffer.starts_with(sleep) {
|
||||||
|
thread::sleep(Duration::from_secs(5));
|
||||||
|
("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
|
||||||
|
} else {
|
||||||
|
("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
|
||||||
|
};
|
||||||
|
|
||||||
|
// ...snip...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<span class="caption">列表 20-10:通过识别 `/sleep` 并休眠五秒来模拟慢请求</span>
|
||||||
|
|
||||||
|
这段代码有些凌乱,不过对于模拟的目的来说已经足够!这里创建了第二个请求 `sleep`,我们会识别其数据。在 `if` 块之后增加了一个 `else if` 来检查 `/sleep` 请求,当发现这个请求时,在渲染欢迎页面之前会先休眠五秒。
|
||||||
|
|
||||||
|
现在就可以真切的看出我们的 server 有多么的原始;真实的库将会以更简洁的方式处理多请求识别问题。
|
||||||
|
|
||||||
|
使用 `cargo run` 启动 server,并接着打开两个浏览器窗口:一个请求 `http://localhost:8080/` 而另一个请求 `http://localhost:8080/sleep`。如果像之前一样多次请求 `/`,会发现响应的比较快速。不过如果请求`/sleep` 之后在请求 `/`,就会看到 `/` 会等待直到 `sleep` 休眠完五秒之后才出现。
|
||||||
|
|
||||||
|
这里有多种办法来改变我们的 web server 使其避免所有请求都排在慢请求之后;其一便是实现一个线程池。
|
||||||
|
|
||||||
|
### 使用线程池改善吞吐量
|
||||||
|
|
||||||
|
**线程池**(*thread pool*)是一组预先分配的用来处理任务的线程。当程序收到一个新任务,线程池中的一个线程会被分配任务并开始处理。其余的线程则可用于处理在第一个线程处理任务的同时处理其他接收到的任务。当第一个线程处理完任务时,它会返回空闲线程池中等待处理新任务。
|
||||||
|
|
||||||
|
线程池允许我们并发处理连接:可以在老连接处理完之前就开始处理新连接。这增加了 server 的吞吐量。
|
||||||
|
|
||||||
|
如下是我们将要实现的:不再等待每个请求处理完才开始下一个,我们将每个连接的处理发送给不同的线程。这些线程来此程序启动时分配的四个线程的线程池。限制较少的线程数的原因是如果为每个新来的请求都创建一个新线程,则千万级的请求就造成灾难,他们会用尽服务器的资源并导致所有请求的处理都被终止。
|
||||||
|
|
||||||
|
不同于分配无限的线程,线程池中将有固定数量的等待线程。当新进请求时,将请求发送到线程池中做处理。线程池会维护一个接收请求的队列。每一个线程会从队列中取出一个请求,处理请求,接着向对队列索取另一个请求。通过这种设计,则可以并发处理 `N` 个请求,其中 `N` 为线程数。这仍然意味着 `N` 个慢请求会阻塞队列中的请求,不过确实将能够处理的慢请求数量从一增加到了 `N`。
|
||||||
|
|
||||||
|
这个设计是多种改善 web server 吞吐量的方法之一。不过本书并不是有关 web server 的,所以这一种方法是我们将要涉及的。其他的方法有 fork/join 模型和单线程异步 I/O 模型。如果你对这个主题感兴趣,则可以阅读更多关于其他解决方案的内容并尝试用 Rust 实现他们;对于一个像 Rust 这样的底层语言,所有这些方法都是可能的。
|
6
src/ch20-03-designing-the-interface.md
Normal file
6
src/ch20-03-designing-the-interface.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
## 设计线程池接口
|
||||||
|
|
||||||
|
> [ch20-03-designing-the-interface.md](https://github.com/rust-lang/book/blob/master/second-edition/src/ch20-03-designing-the-interface.md)
|
||||||
|
> <br>
|
||||||
|
> commit d06a6a181fd61704cbf7feb55bc61d518c6469f9
|
||||||
|
|
Loading…
Reference in New Issue
Block a user