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 网络编程世界的大门,激发你探索更多可能性的热情。

参考

相关推荐
小蒜学长5 小时前
springboot多功能智能手机阅读APP设计与实现(代码+数据库+LW)
java·spring boot·后端·智能手机
追逐时光者6 小时前
精选 4 款开源免费、美观实用的 MAUI UI 组件库,助力轻松构建美观且功能丰富的应用程序!
后端·.net
你的人类朋友7 小时前
【Docker】说说卷挂载与绑定挂载
后端·docker·容器
间彧7 小时前
在高并发场景下,如何平衡QPS和TPS的监控资源消耗?
后端
间彧7 小时前
QPS和TPS的区别,在实际项目中,如何准确测量和监控QPS和TPS?
后端
间彧8 小时前
消息队列(RocketMQ、RabbitMQ、Kafka、ActiveMQ)对比与选型指南
后端·消息队列
brzhang9 小时前
AI Agent 干不好活,不是它笨,告诉你一个残忍的现实,是你给他的工具太难用了
前端·后端·架构
brzhang9 小时前
一文说明白为什么现在 AI Agent 都把重点放在上下文工程(context engineering)上?
前端·后端·架构
Roye_ack9 小时前
【项目实战 Day9】springboot + vue 苍穹外卖系统(用户端订单模块 + 商家端订单管理模块 完结)
java·vue.js·spring boot·后端·mybatis
AAA修煤气灶刘哥11 小时前
面试必问的CAS和ConcurrentHashMap,你搞懂了吗?
后端·面试