diff --git a/src/advance/concurrency-with-threads/web-server.md b/src/advance/concurrency-with-threads/web-server.md index fa04b1d2..bf16f6bc 100644 --- a/src/advance/concurrency-with-threads/web-server.md +++ b/src/advance/concurrency-with-threads/web-server.md @@ -145,3 +145,205 @@ Request: [ ### HTTP 请求长啥样 +刚才的文本挺长的,但其实符合以下的格式: + +```text +Method Request-URI HTTP-Version +headers CRLF +message-body +``` + +- 第一行 Method 是请求的方法,例如 `GET`、`POST` 等,Request-URI 是该请求希望访问的目标资源路径,例如 `/`、`/hello/world` 等 +- 类似 JSON 格式的数据都是 HTTP 请求报头 headers,例如 `"Host: 127.0.0.1:7878"` +- 至于 message-body 是消息体, 它包含了用户请求携带的具体数据,例如更改用户名的请求,就要提交新的用户名数据,至于刚才的 `GET` 请求,它是没有 message-body 的 + +大家可以尝试换一个浏览器再访问一次,看看不同的浏览器请求携带的 headers 是否不同。 + +### 请求应答 + +目前为止,都是在服务器端的操作,浏览器的请求依然还会报错,是时候给予相应的请求应答了,HTTP 格式类似: + +```text +HTTP-Version Status-Code Reason-Phrase CRLF +headers CRLF +message-body +``` + +应答的格式与请求相差不大,其中 Status-Code 是最重要的,它用于告诉客户端,当前的请求是否成功,若失败,大概是什么原因,它就是著名的 HTTP 状态码,常用的有 `200`: 请求成功,`404` 目标不存在,等等。 + +为了帮助大家更直观的感受下应答格式第一行长什么样,下面给出一个示例: + +```text +HTTP/1.1 200 OK\r\n\r\n +``` + +下面将该应答发送回客户端: + +```rust +fn handle_connection(mut stream: TcpStream) { + let buf_reader = BufReader::new(&mut stream); + let http_request: Vec<_> = buf_reader + .lines() + .map(|result| result.unwrap()) + .take_while(|line| !line.is_empty()) + .collect(); + + let response = "HTTP/1.1 200 OK\r\n\r\n"; + + stream.write_all(response.as_bytes()).unwrap(); +} +``` + +由于 `write_all` 方法接受 `&[u8]` 类型作为参数,这里需要用 `as_bytes` 将字符串转换为字节数组。 + +重新启动服务器,然后再观察下浏览器中的输出,这次应该不再有报错,而是一个空白页面,因为没有返回任何具体的数据( message-body ),上面只是一条最简单的符合 HTTP 格式的数据。 + +### 返回 HTML 页面 + +空白页面显然会让人不知所措,那就返回一个简单的 HTML 页面,给用户打给招呼。 + +在项目的根目录下创建 `hello.html` 文件并写入如下内容: + +```html + + +
+ +Hi from Rust
+ + +``` + +看得出来,这是一个非常简单的 HTML5 网页文档,基本上没人读不懂吧 :) + +```rust +use std::{ + fs, + io::{prelude::*, BufReader}, + net::{TcpListener, TcpStream}, +}; +// --snip-- + +fn handle_connection(mut stream: TcpStream) { + let buf_reader = BufReader::new(&mut stream); + let http_request: Vec<_> = buf_reader + .lines() + .map(|result| result.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 response = + format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); + + stream.write_all(response.as_bytes()).unwrap(); +} +``` + +新修改的代码中,读取了新增 HTML 的内容,并按照 HTTP 格式,将内容传回给客户端。 + +具体的运行验证就不再赘述,我们再来看看如何增加一些验证和选择性回复。 + +> 用这么奇怪的格式返回应答数据,原因只有一个,我们在模拟实现真正的 http web 服务器框架。事实上,写逻辑代码时,只需使用现成的 web 框架( 例如 [`rocket`](https://rocket.rs) )去启动 web 服务即可,解析请求数据和返回应答数据都已经被封装在 API 中,非常简单易用 + + +### 验证请求和选择性应答 + +用户想要获取他的个人信息,你给他 say hi,用户想要查看他的某篇文章内容,你给他 say hi, 好吧用户想要骂你,你还是给它 say hi。 + +是的,这种服务态度我们很欣赏,但是这种服务质量属实令人堪忧。因此我们要针对用户的不同请求给出相应的不同回复,让场景模拟更加真实。 + +```rust +fn handle_connection(mut stream: TcpStream) { + let buf_reader = BufReader::new(&mut stream); + let request_line = buf_reader.lines().next().unwrap().unwrap(); + + if request_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 response = format!( + "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}" + ); + + stream.write_all(response.as_bytes()).unwrap(); + } else { + // some other request + } +} +``` + +注意迭代器方法 `next` 的使用,原因在于我们只需要读取第一行,判断具体的 HTTP METHOD 是什么。 + +接着判断了用户是否请求了 `/` 根路径,如果是,返回之前的 `hello.html` 页面;如果不是...尚未实现。 + +重新运行服务器,如果你继续访问 `127.0.0.1:7878` ,那么看到的依然是 `hello.html` 页面,因为默认访问根路径,但是一旦换一个路径访问,例如 `127.0.0.1:7878/something-else`,那你将继续看到之前看过多次的连接错误。 + +下面来完善下,当用户访问根路径之外的页面时,给他展示一个友好的 404 页面( 相比直接报错 )。 + +```rust + // --snip-- + } else { + let status_line = "HTTP/1.1 404 NOT FOUND"; + let contents = fs::read_to_string("404.html").unwrap(); + let length = contents.len(); + + let response = format!( + "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}" + ); + + stream.write_all(response.as_bytes()).unwrap(); + } +``` + +哦对了,别忘了在根路径下创建 `404.html`并填入下面内容: + +```html + + + + +由于运维删库跑路,我们的数据全部丢失,总监也已经准备跑路,88
+ + +``` + +最后,上面的代码其实有很多重复,可以提取出来进行简单重构: + +```rust +// --snip-- + +fn handle_connection(mut stream: TcpStream) { + // --snip-- + + let (status_line, filename) = if request_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 response = + format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); + + stream.write_all(response.as_bytes()).unwrap(); +} +``` + +至此,单线程版本的服务器已经完成,但是说实话,没啥用,总不能让你的用户排队等待访问吧,那也太糟糕了... + +## 多线程 web 服务器