云计算百科
云计算领域专业知识百科平台

构建单线程 Web 服务器之旅

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!

赞(0)
未经允许不得转载:网硕互联帮助中心 » 构建单线程 Web 服务器之旅
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!