在前面的文章中,我们学习了 Rust 的语法、所有权、并发、异步和 Web 开发。但大多数时候,我们使用的是 Axum 这样的高层框架------它们封装了底层细节,让我们专注于业务逻辑。然而,理解底层原理是成为系统级工程师的必经之路。本文将从零开始,用 Rust 实现一个支持静态文件服务、路由和并发处理的 HTTP 服务器。通过这个实战,你将深入理解 TCP 监听、HTTP 协议解析、异步 I/O 和优雅关闭的完整流程。这不是要取代 Axum,而是让你在遇到性能瓶颈或诡异 bug 时,能够"向下看一层"。
一、HTTP 服务器需要做什么?
一个最简 HTTP 服务器需要完成以下工作:
监听 TCP 端口:接收客户端连接。
解析 HTTP 请求:从字节流中提取方法、路径、头部和请求体。
路由请求:根据路径决定返回什么内容。
生成 HTTP 响应:构造状态行、响应头和响应体。
并发处理:同时服务多个客户端。
优雅关闭:收到终止信号时,等待进行中的请求完成。
我们将用 Tokio 实现异步版本,同时对比多线程版本,理解两种模型的差异。
二、项目初始化
bash
cargo new rust-http-server
cd rust-http-server
toml
Cargo.toml
package
name = "rust-http-server"
version = "0.1.0"
edition = "2021"
dependencies
tokio = { version = "1.40", features = "full" }
tracing = "0.1"
tracing-subscriber = "0.3"
三、TCP 监听与连接处理
3.1 异步 TCP 监听
rust
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> std::io::Result<()> {
tracing_subscriber::fmt::init();
let listener = TcpListener::bind("127.0.0.1:8080").await?;
tracing::info!("服务器监听在 http://127.0.0.1:8080");
loop {
let (mut socket, addr) = listener.accept().await?;
tracing::info!("新连接来自 {}", addr);
tokio::spawn(async move {
let mut buf = [0u8; 4096];
match socket.read(&mut buf).await {
Ok(0) => return, // 连接关闭
Ok(n) => {
let request = String::from_utf8_lossy(&buf[..n]);
tracing::debug!("收到请求:\n{}", request);
let response = "HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello, Rust!";
if socket.write_all(response.as_bytes()).await.is_err() {
tracing::error!("写入响应失败");
}
}
Err(e) => tracing::error!("读取失败: {}", e),
}
});
}
}
测试:
bash
cargo run
# 另开终端
curl http://127.0.0.1:8080
# 输出: Hello, Rust!
3.2 这个简单版本的问题
只读一次数据,可能没有读到完整的 HTTP 请求。
没有解析请求,无法路由。
响应是硬编码的。
没有优雅关闭。
接下来逐步完善。
四、HTTP 请求解析
HTTP 请求的格式:
text
GET /path HTTP/1.1\r\n
Host: example.com\r\n
User-Agent: curl/7.68.0\r\n
\r\n
请求体
4.1 定义请求结构
rust
#[derive(Debug)]
pub struct HttpRequest {
pub method: String,
pub path: String,
pub version: String,
pub headers: Vec<(String, String)>,
pub body: String,
}
impl HttpRequest {
pub fn parse(raw: &str) -> Option<Self> {
let mut lines = raw.lines();
let request_line = lines.next()?;
let parts: Vec<&str> = request_line.split_whitespace().collect();
if parts.len() != 3 {
return None;
}
let method = parts[0].to_string();
let path = parts[1].to_string();
let version = parts[2].to_string();
let mut headers = Vec::new();
let mut body = String::new();
let mut in_body = false;
for line in lines {
if in_body {
body.push_str(line);
body.push('\n');
continue;
}
if line.is_empty() {
in_body = true;
continue;
}
if let Some((key, value)) = line.split_once(':') {
headers.push((key.trim().to_string(), value.trim().to_string()));
}
}
Some(HttpRequest { method, path, version, headers, body })
}
pub fn header(&self, name: &str) -> Option<&str> {
self.headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(name))
.map(|(_, v)| v.as_str())
}
}
4.2 处理请求体
上面的解析假设请求体在头部之后。对于 POST 请求,需要根据 Content-Length 读取完整的请求体。在异步版本中,需要循环读取直到收完:
rust
async fn read_full_request(socket: &mut TcpStream) -> std::io::Result<String> {
let mut buf = Vec::new();
let mut temp = [0u8; 1024];
loop {
let n = socket.read(&mut temp).await?;
if n == 0 {
break;
}
buf.extend_from_slice(&temp[..n]);
// 检查是否已经收到完整的请求(头部结束 + 请求体完整)
if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
let header_part = String::from_utf8_lossy(&buf[..pos]);
let content_length = header_part
.lines()
.find_map(|line| {
let (k, v) = line.split_once(':')?;
if k.eq_ignore_ascii_case("content-length") {
v.trim().parse::<usize>().ok()
} else {
None
}
})
.unwrap_or(0);
if buf.len() >= pos + 4 + content_length {
break;
}
}
}
Ok(String::from_utf8_lossy(&buf).to_string())
}
五、HTTP 响应生成
5.1 定义响应结构
rust
#[derive(Debug)]
pub struct HttpResponse {
pub status: u16,
pub status_text: &'static str,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
}
impl HttpResponse {
pub fn new(status: u16, status_text: &'static str) -> Self {
HttpResponse {
status,
status_text,
headers: Vec::new(),
body: Vec::new(),
}
}
pub fn text(mut self, body: &str) -> Self {
self.headers.push(("Content-Type".to_string(), "text/plain; charset=utf-8".to_string()));
self.body = body.as_bytes().to_vec();
self
}
pub fn json(mut self, body: &str) -> Self {
self.headers.push(("Content-Type".to_string(), "application/json".to_string()));
self.body = body.as_bytes().to_vec();
self
}
pub fn html(mut self, body: &str) -> Self {
self.headers.push(("Content-Type".to_string(), "text/html; charset=utf-8".to_string()));
self.body = body.as_bytes().to_vec();
self
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut response = format!(
"HTTP/1.1 {} {}\r\n",
self.status, self.status_text
);
for (key, value) in &self.headers {
response.push_str(&format!("{}: {}\r\n", key, value));
}
response.push_str(&format!("Content-Length: {}\r\n", self.body.len()));
response.push_str("Connection: close\r\n");
response.push_str("\r\n");
let mut bytes = response.into_bytes();
bytes.extend_from_slice(&self.body);
bytes
}
// 常用响应
pub fn ok() -> Self { Self::new(200, "OK") }
pub fn not_found() -> Self { Self::new(404, "Not Found") }
pub fn bad_request() -> Self { Self::new(400, "Bad Request") }
pub fn internal_error() -> Self { Self::new(500, "Internal Server Error") }
}
六、路由与处理器
6.1 定义路由
rust
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
type Handler = Arc<
dyn Fn(HttpRequest) -> Pin<Box<dyn Future<Output = HttpResponse> + Send>>
+ Send
+ Sync,
>;
pub struct Router {
routes: HashMap<String, Handler>,
}
impl Router {
pub fn new() -> Self {
Router { routes: HashMap::new() }
}
pub fn route<F, Fut>(mut self, method: &str, path: &str, handler: F) -> Self
where
F: Fn(HttpRequest) -> Fut + Send + Sync + 'static,
Fut: Future<Output = HttpResponse> + Send + 'static,
{
let key = format!("{} {}", method.to_uppercase(), path);
let handler: Handler = Arc::new(move |req| Box::pin(handler(req)));
self.routes.insert(key, handler);
self
}
pub async fn handle(&self, req: HttpRequest) -> HttpResponse {
let key = format!("{} {}", req.method.to_uppercase(), req.path);
if let Some(handler) = self.routes.get(&key) {
handler(req).await
} else {
HttpResponse::not_found().text("404 Not Found")
}
}
}
6.2 定义处理器
rust
async fn index(_req: HttpRequest) -> HttpResponse {
HttpResponse::ok().html("<h1>欢迎来到 Rust HTTP 服务器</h1>")
}
async fn health(_req: HttpRequest) -> HttpResponse {
HttpResponse::ok().json(r#"{"status":"ok"}"#)
}
async fn echo(req: HttpRequest) -> HttpResponse {
HttpResponse::ok().text(&format!("你访问了: {}", req.path))
}
七、组装服务器
rust
#[tokio::main]
async fn main() -> std::io::Result<()> {
tracing_subscriber::fmt::init();
let router = Arc::new(
Router::new()
.route("GET", "/", index)
.route("GET", "/health", health)
.route("GET", "/echo", echo),
);
let listener = TcpListener::bind("127.0.0.1:8080").await?;
tracing::info!("服务器监听在 http://127.0.0.1:8080");
loop {
let (mut socket, addr) = listener.accept().await?;
let router = Arc::clone(&router);
tokio::spawn(async move {
tracing::info!("处理来自 {} 的连接", addr);
let raw = match read_full_request(&mut socket).await {
Ok(r) => r,
Err(e) => {
tracing::error!("读取请求失败: {}", e);
return;
}
};
let response = match HttpRequest::parse(&raw) {
Some(req) => router.handle(req).await,
None => HttpResponse::bad_request().text("无效的 HTTP 请求"),
};
if socket.write_all(&response.to_bytes()).await.is_err() {
tracing::error!("写入响应失败");
}
});
}
}
八、优雅关闭
生产级服务器需要在收到 SIGTERM 时停止接收新连接,等待进行中的请求完成。
rust
use tokio::signal;
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c().await.expect("无法监听 Ctrl+C");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("无法监听 SIGTERM")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => tracing::info!("收到 Ctrl+C,准备关闭"),
_ = terminate => tracing::info!("收到 SIGTERM,准备关闭"),
}
}
在主循环中:
rust
loop {
tokio::select! {
result = listener.accept() => {
let (mut socket, addr) = result?;
// ... 处理连接
}
_ = shutdown_signal() => {
tracing::info!("开始优雅关闭,等待进行中的请求...");
break;
}
}
}
tracing::info!("服务器已关闭");
为了真正等待所有任务完成,可以使用 tokio::task::JoinSet 跟踪所有 spawn 的任务:
rust
let mut join_set = tokio::task::JoinSet::new();
loop {
tokio::select! {
result = listener.accept() => {
let (mut socket, addr) = result?;
let router = Arc::clone(&router);
join_set.spawn(async move {
// ... 处理连接
});
}
_ = shutdown_signal() => {
tracing::info!("开始优雅关闭");
break;
}
}
}
// 等待所有任务完成(最多 30 秒)
let timeout = tokio::time::timeout(
std::time::Duration::from_secs(30),
async {
while join_set.join_next().await.is_some() {}
}
).await;
if timeout.is_err() {
tracing::warn!("等待超时,强制关闭");
}
九、性能对比:异步 vs 多线程

结论:对于 I/O 密集型的 HTTP 服务器,异步模型在并发连接数和内存占用上都有数量级的优势。
十、与 Axum 的对比

结论:不要在生产环境从零造轮子。本文的目的是理解底层原理。实际项目请使用 Axum、Actix-web 等成熟框架。
十一、小结
TCP 监听:TcpListener::bind + accept 循环。
HTTP 解析:按行解析请求行、头部和请求体,根据 Content-Length 读取完整请求。
响应生成:构造状态行、头部和响应体,计算 Content-Length。
路由:使用 HashMap 存储方法+路径到处理器的映射。
并发处理:tokio::spawn 为每个连接创建异步任务。
优雅关闭:tokio::select! 监听信号,JoinSet 等待任务完成。
性能:异步模型在高并发 I/O 场景下远优于多线程模型。