第十四篇:《系统编程实战:用 Rust 编写高性能 HTTP 服务器》

在前面的文章中,我们学习了 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 场景下远优于多线程模型。

相关推荐
Amos_Web5 小时前
Rspack 源码解析(十一):资产 Hook 与增量构建
前端·rust·源码阅读
孙启超6 小时前
【AI开发之Rust】第 12 课:并发模型与同步原语
开发语言·后端·rust
柯南46686 小时前
【AI开发之Rust】第 13 课:async/await 与 tokio 异步运行时
rust·编程语言
传奇开心果编程7 小时前
【Rust入门练中学】第4课:函数与所有权入门
开发语言·学习·rust
右耳朵猫AI7 小时前
Rust周刊2026W38 | mold重写Rust、认证级Rust裸机、Slint 1.18发布、lint提速3133倍
后端·rust·系统编程
Source.Liu8 小时前
【A11】时间单位与精度设计笔记
rust
梦醒沉醉1 天前
std1.97.1——cmp模块细览
rust