作为最终项目,我们需要建一个 web server。大致包含的内容是:
- 在一个 socket 上监听 TCP 连接
- 解析 HTTP 请求
- 响应 HTTP 请求
- 使用线程池优化性能(在另一篇)
20.1 单线程 web server
首先用一个单线程实现解决基本需求。
接收请求
use std::net::{TcpStream, TcpListener};
fn handle_client(stream: TcpStream) {
println!("connection established");
}
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
match stream {
Ok(stream) => handle_client(stream),
Err(e) => println!("connection failed: {}", e),
}
}
}
首先使用 bind
来监听 Tcp 端口。它会返回一个 Result
,因为它不一定成功。失败的原因可能是:
- 权限不够(0 到 1023 端口需要管理员权限)
- 其他程序已经占用这个端口
但是由于我们只是一个示例程序,我们不会处理这些错误,直接 unwrap()
。
然后,incoming
方法会返回一个迭代器,其中每个都是一个 client 到 server 的连接。这些连接也可能不成功,例如,操作系统对同时最大连接数有限制,超出时会报错。因此,需要用 match
来处理。
运行这个程序,并通过浏览器访问 127.0.0.1:7878
,看到下面输出即可:
Running `target/debug/web_server`
connection established
connection established
connection established
connection established
多次连接可能是因为:
- 浏览器同时请求了多个资源
-
stream
drop时,Tcp 连接终止,浏览器可能会重连。
处理请求
我们已经创建好了 TcpStream
对象,通过它可以连接到远程主机,并通过 read
writle
来传输数据。
下面的代码打印出收到的 HTTP 请求:
fn handle_client(mut stream: TcpStream) {
let mut buffer = [0; 1024];
println!("size of element: {}", std::mem::size_of_val(&buffer[0]));
stream.read(&mut buffer).unwrap();
println!("Got request: {}", String::from_utf8_lossy(&buffer[..]));
}
- 首先,在栈上分配了一个 1024 bytes 的数组
buffer
,为了简化,我们并不支持任意长度的请求。
注意,Rust 的类型推断非常智能,这里推断的 buffer 元素类型是
u8
,是因为后面使用了read
,否则数组的默认类型是i32
。
然后,我们将
TcpStream
中的内容读取进这个 buffer,注意,虽然是读取,但是更改了TcpStream
的内部状态,因此函数参数需要加mut
。最后,我们将这个 buffer 转换成
String
并打印。String::from_utf8_lossy
方法接收&[u8]
并生成一个String
,"lossy" 意思是指如果发现了无法decode 的 UTF-8 序列,用�
代替。
再次运行,可以看到收到的 HTTP 请求为:
size of element: 1
Got request: GET / HTTP/1.1
Host: localhost:7878
Upgrade-Insecure-Requests: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Safari/605.1.15
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: keep-alive
响应请求
HTTP 响应的格式是:
HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body
其中, CRLF
是 Carriage Return and Line Feed 的简称,/r
是carriage return,/n
是 line feed。
例如下面是一个简单的请求成功响应:
HTTP/1.1 200 OK\r\n\r\n
我们改写函数来给 client 发送这个响应:
fn handle_client(mut stream: TcpStream) {
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let response = "HTTP/1.1 200 OK\r\n\r\n";
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
值得注意的是 flush
。TcpStream
内部实现有一个缓冲区来减少 socket 读写的系统调用,flush
强制将所有缓冲区内容写入 socket。
现在再访问 127.0.0.1:7878
,就不会报错,而是看到一个空白页。
我们可以在 crate 根目录下新建一个 hello.html
:
Hello!
Hello!
Hi from Rust
在响应中使用:
fn handle_client(mut stream: TcpStream) {
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let contents = fs::read_to_string("hello.html").unwrap();
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}",
contents.len(),
contents
);
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
现在我们可以访问一个有东西的页面了,然而,即使我们请求 127.0.0.1:7878/something-else
,甚至不是一个 GET 请求,它还是返回这个页面,这是因为我们忽略掉了请求内容。
验证请求并有选择地响应
现在我们检查请求内容,并仅对 GET 请求作出响应,否则返回 404 错误。
首先再加入一个 404 页面:
Page not found!
Oops!
Sorry, I don't know what you're asking for.
再修改响应逻辑,根据请求的前缀加以判断:
fn handle_client(mut stream: TcpStream) {
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
// GET request prefix
let get = b"GET / HTTP/1.1\r\n";
let mut status_line = "HTTP/1.1 200 OK";
let mut filename = "hello.html";
if buffer.starts_with(get) == false {
status_line = "HTTP/1.1 404 NOT FOUND";
filename = "404.html";
}
let contents = fs::read_to_string(filename).unwrap();
let response = format!(
"{}\r\nContent-Length: {}\r\n\r\n{}",
status_line,
contents.len(),
contents
);
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
现在,单线程版本的 web server 就搭建好了。它对 GET 请求返回一个页面,对其他请求返回 404 错误。