1. 协议基础简介
在构建 Web 服务器时,涉及到两个最重要的协议:
- TCP(传输控制协议):作为一个底层协议,TCP 负责在客户端和服务器之间建立可靠的字节流连接。
- HTTP(超文本传输协议):HTTP 构建在 TCP 之上,定义了请求和响应的格式。它规定了如何请求资源以及服务器如何返回资源。
HTTP 与 TCP 都遵循“请求-响应”的模式,即客户端发送请求,服务器解析请求并返回响应。
2. 监听 TCP 连接
首先,我们需要让服务器监听一个 TCP 端口,以便能够接收客户端连接。Rust 标准库中的 std::net 模块提供了 TcpListener 类型来实现这一功能。
创建项目
在终端中执行以下命令来创建一个新的 Rust 项目:
$ cargo new hello
$ cd hello
编写监听代码
在 src/main.rs 文件中,我们可以写下如下代码来监听本地地址 127.0.0.1:7878:
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!("Connection established!");
}
}
运行该程序后,每当有客户端(比如浏览器)访问 127.0.0.1:7878 时,终端都会打印出“Connection established!”。注意,由于浏览器可能会发送多次请求(例如请求 favicon.ico),你可能会看到多条连接信息。
3. 读取 HTTP 请求
接下来,我们需要从 TCP 流中读取 HTTP 请求数据。为此,我们将使用 std::io::BufReader 来增强读取体验,并解析出请求中的各个部分。
读取请求内容
修改 main.rs 文件,添加一个新的 handle_connection 函数,用于读取并打印 HTTP 请求数据:
use std::io::prelude::*;
use std::io::BufReader;
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();
handle_connection(stream);
}
}
fn handle_connection(mut stream: std::net::TcpStream) {
let buf_reader = BufReader::new(&mut stream);
let http_request: Vec<String> = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();
println!("Request: {:#?}", http_request);
}
当你在浏览器中访问 127.0.0.1:7878 时,你将在终端看到类似下面的 HTTP 请求数据输出:
[
"GET / HTTP/1.1",
"Host: 127.0.0.1:7878",
"User-Agent: Mozilla/5.0 …",
// 其它头部信息…
]
4. 写入 HTTP 响应
HTTP 响应也遵循固定格式:
HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body
返回基本响应
我们先编写一个简单的响应,它只返回一个空白页面。修改 handle_connection 函数,写入如下代码:
fn handle_connection(mut stream: std::net::TcpStream) {
// 读取请求数据…
// 此处省略读取请求部分的代码
let response = "HTTP/1.1 200 OK\\r\\n\\r\\n";
stream.write_all(response.as_bytes()).unwrap();
}
再次运行服务器并在浏览器中访问 127.0.0.1:7878,虽然页面为空,但浏览器将不再显示连接错误。
5. 返回真实 HTML 页面
为了让浏览器显示真实内容,我们可以创建一个 HTML 文件并在响应中返回它的内容。
创建 HTML 文件
在项目根目录下创建一个名为 hello.html 的文件,内容如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>欢迎来到 Rust Web 服务器</title>
</head>
<body>
<h1>Hello, Rust!</h1>
<p>这是一个使用 Rust 编写的简单 Web 服务器。</p>
</body>
</html>
返回 HTML 内容
修改 handle_connection 函数,从文件中读取 HTML 内容,并返回带有 Content-Length 头的完整 HTTP 响应:
use std::fs;
fn handle_connection(mut stream: std::net::TcpStream) {
let buf_reader = BufReader::new(&mut stream);
let http_request: Vec<String> = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();
let get_root = "GET / HTTP/1.1";
let (status_line, filename) = if http_request[0] == get_root {
("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!(
"{}\\r\\nContent-Length: {}\\r\\n\\r\\n{}",
status_line, length, contents
);
stream.write_all(response.as_bytes()).unwrap();
}
提示:在上述代码中,我们根据请求的第一行判断是否为根路径 / 的请求。如果是,则返回 hello.html 的内容;否则返回一个状态码为 404 的响应。别忘了在项目根目录下创建一个 404.html 文件,作为错误页面。
例如,你可以创建 404.html 文件,内容如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>页面未找到</title>
</head>
<body>
<h1>404 – 页面未找到</h1>
<p>对不起,你请求的页面不存在。</p>
</body>
</html>
6. 代码重构
在实现了基本的请求判断后,我们发现 if 和 else 分支中有大量重复代码。为了让代码更简洁,我们可以将仅有差异的部分(状态行和文件名)提取出来,然后统一使用这些变量来读取文件和写入响应。
重构后的代码如下:
fn handle_connection(mut stream: std::net::TcpStream) {
let buf_reader = BufReader::new(&mut stream);
let request_line = buf_reader
.lines()
.next()
.unwrap()
.unwrap();
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!(
"{}\\r\\nContent-Length: {}\\r\\n\\r\\n{}",
status_line, length, contents
);
stream.write_all(response.as_bytes()).unwrap();
}
通过这种重构,我们不仅减少了重复代码,也让逻辑更加清晰。如果将来需要修改文件读取或响应生成的细节,只需在一处进行调整即可。
7. 总结与展望
在本文中,我们从零开始构建了一个简单的单线程 Web 服务器,涵盖了以下几个关键步骤:
- 监听 TCP 连接:使用 TcpListener 绑定到指定地址和端口。
- 读取 HTTP 请求:借助 BufReader 将 TCP 流转换为可迭代的请求行集合。
- 写入 HTTP 响应:构建符合 HTTP 协议格式的响应字符串,并写入流中。
- 返回真实 HTML 页面:根据请求的路径选择合适的 HTML 文件进行响应。
- 代码重构:提取公共部分,使代码更具可维护性。
目前我们的服务器是单线程的,只能处理一个请求。接下来,你可以尝试改进服务器,使其能够处理多线程请求,从而提高并发性能。
希望这篇博客能帮助你理解 Web 服务器的基本工作原理,并激发你进一步探索 Rust 编程的热情。Happy coding!
评论前必须登录!
注册