Rust 网络编程实战:用 Tokio 手写一个迷你 TCP 反向代理 (minginx)

Rust 网络编程实战:用 Tokio 手写一个迷你 TCP 反向代理 (minginx)

Nginx 作为强大的反向代理服务器,是我们日常开发中的得力助手。但你是否想过,它的核心原理------TCP流量转发,究竟是如何实现的?我们能否用 Rust 和 Tokio 亲手构建一个迷你版的 Nginx 呢?

本文旨在通过一个名为 minginx 的实战项目,带领读者从零开始,用不到100行代码实现一个功能完备的异步TCP反向代理。我们将深入探讨如何利用 tokio::spawn 处理并发连接,以及如何通过 tokio::io::copy 高效地在客户端和上游服务器之间建立双向数据流。为了验证我们的代理,我们还会搭建一个基于 axum 的后端Web服务。

读完本文,你不仅能理解TCP反向代理的本质,还能掌握一套使用Rust构建高性能网络服务的实用技能。

一、核心组件:TCP代理 (minginx.rs)

🗼 TCP代理 minginx.rs 文件

rust 复制代码
use std::sync::Arc;

use anyhow::Result;
use serde::{Deserialize, Serialize};
use tokio::{
    io,
    net::{TcpListener, TcpStream},
};
use tracing::{info, level_filters::LevelFilter, warn};
use tracing_subscriber::{Layer as _, fmt::Layer, layer::SubscriberExt, util::SubscriberInitExt};

#[derive(Debug, Deserialize, Serialize)]
struct Config {
    listen_addr: String,
    upstream_addr: String,
}

#[tokio::main]
async fn main() -> Result<()> {
    // proxy client traffic to upstream
    let layer = Layer::new().pretty().with_filter(LevelFilter::INFO);
    tracing_subscriber::registry().with(layer).init();

    let config = resolve_config();
    let config = Arc::new(config);
    info!("Upstream: {}", config.upstream_addr);
    info!("Listen: {}", config.listen_addr);

    let listener = TcpListener::bind(&config.listen_addr).await?;
    loop {
        let (client, addr) = listener.accept().await?;
        // let cloned_config = config.clone(); 阅读代码 方便使用 Arc::clone
        let cloned_config = Arc::clone(&config);
        info!("Accept connection from {}", addr);
        tokio::spawn(async move {
            let upstream = TcpStream::connect(&cloned_config.upstream_addr).await?;
            proxy(client, upstream).await?;
            Ok::<(), anyhow::Error>(())
        });
    }
}

async fn proxy(mut client: TcpStream, mut upstream: TcpStream) -> Result<()> {
    let (mut client_rd, mut client_wr) = client.split();
    let (mut upstream_rd, mut upstream_wr) = upstream.split();

    let client_to_upstream = io::copy(&mut client_rd, &mut upstream_wr);

    let upstream_to_client = io::copy(&mut upstream_rd, &mut client_wr);

    if let Err(e) = tokio::try_join!(client_to_upstream, upstream_to_client) {
        warn!("Error in proxy: {}", e)
    }
    Ok(())
}

fn resolve_config() -> Config {
    Config {
        listen_addr: "127.0.0.1:8081".to_string(),
        upstream_addr: "127.0.0.1:8080".to_string(),
    }
}

这段 Rust 代码实现了一个简单的 TCP 代理服务器 (TCP proxy)。

程序首先在本地 127.0.0.1:8081 地址上监听传入的 TCP 连接,当接收到一个新的客户端连接后,它会立即为该连接创建一个新的异步任务(tokio::spawn)。在这个新任务中,程序会连接到预设的上游服务器地址(127.0.0.1:8081),然后调用 proxy 函数。proxy 函数的核心功能是使用 io::copy 在客户端和上游服务器之间建立一个双向数据流 ,将从客户端收到的数据原封不动地转发给上游服务器,同时也将上游服务器返回的数据转发给客户端,从而完成代理的功能。

运行

bash 复制代码
rust-ecosystem-learning on  main [!?] is 📦 0.1.0 via 🦀 1.88.0 took 2m 4.9s 
➜ cargo run --example minginx
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/examples/minginx`
  2025-07-03T15:02:50.964166Z  INFO minginx: Upstream: 0.0.0.0:8080
    at examples/minginx.rs:26

  2025-07-03T15:02:50.964204Z  INFO minginx: Listen: 0.0.0.0:8081
    at examples/minginx.rs:27

二、后端验证:Web API 服务 (axum_serde.rs)

⚙️ 后端服务 axum_serde.rs 文件

rust 复制代码
use std::sync::{Arc, Mutex};

use anyhow::Result;
use axum::{
    Json, Router,
    extract::State,
    routing::{get, patch},
};
use serde::{Deserialize, Serialize};
use tokio::net::TcpListener;
use tracing::{info, instrument, level_filters::LevelFilter};
use tracing_subscriber::{
    Layer as _,
    fmt::{self, format::FmtSpan},
    layer::SubscriberExt,
    util::SubscriberInitExt,
};

#[derive(Debug, Clone, PartialEq, Serialize)]
struct User {
    name: String,
    age: u8,
    skills: Vec<String>,
}

#[derive(Debug, Clone, Deserialize)]
struct UserUpdate {
    age: Option<u8>,
    skills: Option<Vec<String>>,
}

#[tokio::main]
async fn main() -> Result<()> {
    let console = fmt::Layer::new()
        .with_span_events(FmtSpan::CLOSE)
        .pretty()
        .with_filter(LevelFilter::DEBUG);

    tracing_subscriber::registry().with(console).init();

    let user = User {
        name: "Alice".to_string(),
        age: 30,
        skills: vec!["Rust".to_string(), "Python".to_string()],
    };

    let user = Arc::new(Mutex::new(user));

    let addr = "0.0.0.0:8080";
    let listener = TcpListener::bind(addr).await?;
    info!("Listening on {}", addr);

    let app = Router::new()
        .route("/", get(user_handler))
        .route("/", patch(update_handler))
        .with_state(user);

    axum::serve(listener, app.into_make_service()).await?;

    Ok(())
}

#[instrument]
async fn user_handler(State(user): State<Arc<Mutex<User>>>) -> Json<User> {
    (*user.lock().unwrap()).clone().into()
}

#[instrument]
async fn update_handler(
    State(user): State<Arc<Mutex<User>>>,
    Json(user_update): Json<UserUpdate>,
) -> Json<User> {
    let mut user = user.lock().unwrap();
    if let Some(age) = user_update.age {
        user.age = age;
    }

    if let Some(skills) = user_update.skills {
        user.skills = skills;
    }
    (*user).clone().into()
}

这段 Rust 代码使用 axum 框架创建了一个简单的 Web API,用于查看和更新单个用户的个人资料。为了在多个并发请求之间安全地共享用户数据,它将用户状态包裹在 Arc<Mutex<User>> 中:Arc 允许多个任务共享数据所有权,而 Mutex 则确保同一时间只有一个任务能修改数据,从而防止数据竞争。该服务器运行在 tokio 异步运行时上,并开放了两个根路径 (/) 的 HTTP 接口:一个 GET 请求用于获取当前用户信息,另一个 PATCH 请求用于通过 JSON 数据更新用户的年龄或技能。此外,代码还配置了 tracing 库,以便为服务器事件和请求提供结构化的日志输出。

三、联调测试:验证代理功能

🚀 联调测试

Request:

http 复制代码
### update_handler
PATCH http://localhost:8081/ HTTP/1.1
Content-Type: application/json

{
    "skills": ["Go", "Python", "Java"]
}

Response:

bash 复制代码
HTTP/1.1 200 OK
content-type: application/json
content-length: 57
connection: close
date: Thu, 03 Jul 2025 15:04:15 GMT

{
  "name": "Alice",
  "age": 30,
  "skills": [
    "Go",
    "Python",
    "Java"
  ]
}

minginx 运行详情

bash 复制代码
rust-ecosystem-learning on  main [!?] is 📦 0.1.0 via 🦀 1.88.0 took 2m 4.9s 
➜ cargo run --example minginx
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/examples/minginx`
  2025-07-03T15:02:50.964166Z  INFO minginx: Upstream: 0.0.0.0:8080
    at examples/minginx.rs:26

  2025-07-03T15:02:50.964204Z  INFO minginx: Listen: 0.0.0.0:8081
    at examples/minginx.rs:27

  2025-07-03T15:03:25.513586Z  INFO minginx: Accept connection from 127.0.0.1:60605
    at examples/minginx.rs:34

  2025-07-03T15:04:15.831749Z  INFO minginx: Accept connection from 127.0.0.1:60821
    at examples/minginx.rs:34

axum_serde 运行详情

bash 复制代码
rust-ecosystem-learning on  main [!?] is 📦 0.1.0 via 🦀 1.88.0 took 3m 7.4s 
➜ cargo run --example axum_serde
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
     Running `target/debug/examples/axum_serde`
  2025-07-03T15:02:43.690337Z  INFO axum_serde: Listening on 0.0.0.0:8080
    at examples/axum_serde.rs:51

  2025-07-03T15:03:25.516468Z  INFO axum_serde: close, time.busy: 31.6µs, time.idle: 24.0µs
    at examples/axum_serde.rs:68
    in axum_serde::update_handler with user: Mutex { data: User { name: "Alice", age: 30, skills: ["Rust", "Python"] }, poisoned: false, .. }, user_update: UserUpdate { age: None, skills: Some(["Go"]) }

  2025-07-03T15:04:15.836860Z  INFO axum_serde: close, time.busy: 8.62µs, time.idle: 12.9µs
    at examples/axum_serde.rs:68
    in axum_serde::update_handler with user: Mutex { data: User { name: "Alice", age: 30, skills: ["Go"] }, poisoned: false, .. }, user_update: UserUpdate { age: None, skills: Some(["Go", "Python", "Java"]) }

总结

通过 minginx 这个项目,我们从零开始,用短短几十行代码就实现了一个功能完备的 TCP 反向代理的核心。这不仅展示了 Rust 语言的强大表达力和安全性,更凸显了 Tokio 生态在构建高性能网络服务方面的巨大优势。我们看到,tokio::spawn 提供了简单的并发模型,而 io::copy 则优雅地处理了底层复杂的数据拷贝和流控制。

本文的 minginx 虽然简单,但它抓住了反向代理的精髓。在此基础上,可以进一步扩展,例如实现负载均衡、SSL/TLS卸载、健康检查等高级功能,最终构建出企业级的网络中间件。

希望这个小项目能为你打开一扇通往 Rust 网络编程世界的大门,激发你探索更多可能性的热情。

参考

相关推荐
谭光志16 分钟前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..20 分钟前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects26 分钟前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰26 分钟前
一些基础和问题解决
后端
陈随易44 分钟前
前端项目部署只要30秒
前端·后端·程序员
YIAN1 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp
Imchendiana1 小时前
《狂人日记NO.9》— 前后端一把梭,我的全栈实录
前端·后端
学渣超1 小时前
记一次分布式事务数据不一致的排查之旅:从超时到索引,层层剥茧
java·后端·架构
北冥you鱼1 小时前
Go 语言读取链上数据:从基础到实战
开发语言·后端·golang
程序员cxuan2 小时前
Claude Code 为了封禁中国用户,竟然在代码里下毒
人工智能·后端·程序员