高级4-5 小时·系统编程

从零实现一个 HTTP 服务器

使用 Tokio + Hyper 构建一个支持路由、中间件、静态文件服务的异步 HTTP 服务器

Rusttokiohyperhttpasync

从零实现一个 HTTP 服务器

使用 Tokio + Hyper 构建一个支持路由、中间件、静态文件服务的异步 HTTP 服务器

你将学到

  • 使用 Tokio 构建异步运行时
  • 使用 Hyper 处理 HTTP 请求和响应
  • 实现路由系统
  • 添加中间件支持

前置知识

架构设计

http-server/
├── src/
│   ├── main.rs         # 入口
│   ├── server.rs       # 服务器核心
│   ├── router.rs       # 路由系统
│   ├── handler.rs      # 请求处理
│   └── middleware.rs   # 中间件
└── Cargo.toml

实现步骤

第一步:项目初始化

cargo new http-server
cd http-server
# Cargo.toml
[package]
name = "http-server"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "1", features = ["full"] }
hyper = { version = "1", features = ["full"] }
hyper-util = { version = "0.1", features = ["tokio"] }
http-body-util = "0.1"
bytes = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

第二步:路由系统

// src/router.rs
use hyper::{Method, Request, Response, StatusCode};
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;

type Handler = Box<dyn Fn(Request<hyper::body::Incoming>) -> Pin<Box<dyn Future<Output = Response<String>> + Send>> + Send + Sync>;

pub struct Router {
    routes: HashMap<(Method, String), Handler>,
}

impl Router {
    pub fn new() -> Self {
        Router {
            routes: HashMap::new(),
        }
    }

    pub fn add<F, Fut>(&mut self, method: Method, path: &str, handler: F)
    where
        F: Fn(Request<hyper::body::Incoming>) -> Fut + Send + Sync + 'static,
        Fut: Future<Output = Response<String>> + Send + 'static,
    {
        let path = path.to_string();
        self.routes.insert(
            (method, path),
            Box::new(move |req| Box::pin(handler(req))),
        );
    }

    pub async fn handle(&self, req: Request<hyper::body::Incoming>) -> Response<String> {
        let method = req.method().clone();
        let path = req.uri().path().to_string();

        if let Some(handler) = self.routes.get(&(method, path)) {
            handler(req).await
        } else {
            Response::builder()
                .status(StatusCode::NOT_FOUND)
                .body("404 Not Found".to_string())
                .unwrap()
        }
    }
}

第三步:请求处理

// src/handler.rs
use hyper::{Request, Response, StatusCode};
use serde_json::json;

pub async fn index(_req: Request<hyper::body::Incoming>) -> Response<String> {
    Response::new("Hello, World!".to_string())
}

pub async fn api_users(_req: Request<hyper::body::Incoming>) -> Response<String> {
    let users = json!({
        "users": [
            {"id": 1, "name": "Alice"},
            {"id": 2, "name": "Bob"},
        ]
    });

    Response::builder()
        .header("Content-Type", "application/json")
        .body(users.to_string())
        .unwrap()
}

pub async fn not_found(_req: Request<hyper::body::Incoming>) -> Response<String> {
    Response::builder()
        .status(StatusCode::NOT_FOUND)
        .body("Page not found".to_string())
        .unwrap()
}

第四步:服务器核心

// src/server.rs
use hyper::server::conn::http1;
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use std::sync::Arc;
use crate::router::Router;

pub struct Server {
    router: Arc<Router>,
    addr: String,
}

impl Server {
    pub fn new(router: Router, addr: &str) -> Self {
        Server {
            router: Arc::new(router),
            addr: addr.to_string(),
        }
    }

    pub async fn run(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let listener = TcpListener::bind(&self.addr).await?;
        println!("Server running on http://{}", self.addr);

        loop {
            let (stream, _) = listener.accept().await?;
            let io = TokioIo::new(stream);
            let router = self.router.clone();

            tokio::spawn(async move {
                let service = hyper::service::service_fn(move |req| {
                    let router = router.clone();
                    async move { Ok::<_, hyper::Error>(router.handle(req).await) }
                });

                if let Err(err) = http1::Builder::new().serve_connection(io, service).await {
                    eprintln!("Error serving connection: {}", err);
                }
            });
        }
    }
}

第五步:主程序

// src/main.rs
mod server;
mod router;
mod handler;

use hyper::Method;
use router::Router;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let mut router = Router::new();

    // 注册路由
    router.add(Method::GET, "/", handler::index);
    router.add(Method::GET, "/api/users", handler::api_users);

    // 启动服务器
    let server = server::Server::new(router, "127.0.0.1:3000");
    server.run().await?;

    Ok(())
}

完整项目结构

http-server/
├── src/
│   ├── main.rs         # 程序入口
│   ├── server.rs       # TCP 监听和连接处理
│   ├── router.rs       # 路由匹配和分发
│   ├── handler.rs      # 具体请求处理函数
│   └── middleware.rs   # 中间件(日志/CORS等)
└── Cargo.toml

最佳实践

  1. 使用 Arc 共享路由:多个连接需要共享路由表
  2. 异步处理每个连接:使用 tokio::spawn 并发处理
  3. 错误处理:使用 Result 传播错误
  4. 中间件模式:在路由前添加通用逻辑

常见问题

Q: 如何处理 POST 请求体? A: 使用 hyper::body::to_bytes 读取请求体。

Q: 如何添加 CORS 支持? A: 在响应中添加 CORS 头。

Q: 如何支持 HTTPS? A: 使用 tokio-rustlshyper-tls

扩展挑战

  1. 添加静态文件服务
  2. 实现 WebSocket 支持
  3. 添加请求日志中间件

相关课程

课程相关章节
Rust异步编程:async/await
RustTokio 运行时基础
Rust结构体(Struct)
Rust错误处理:Result 与 Option