第22章 Rust:基于 tokio 的高性能 TCP 代理

学习目标

  • 用 tokio 写出 L4 TCP 代理和 SOCKS5 代理,对照 第21章 的 Go 实现
  • 理解 Rust async/await 与 Go goroutine 的模型差异(无栈状态机 vs 有栈协程)
  • 体会 copy_bidirectional、所有权/move 在网络编程中的作用
  • 明白为什么 Cloudflare/字节等用 Rust 重写代理(pingora)

前置知识


原理

为什么用 Rust 写代理

代理是性能敏感、长期运行、处理不可信输入的程序——这正是 Rust 的甜点:

  • 无 GC:没有 GC 停顿,尾延迟稳定(代理最怕 p99 抖动)
  • 零成本抽象:高级写法编译成和手写 C 一样快的代码
  • 内存安全:所有权 + 借用检查在编译期消灭"野指针/数据竞争"——处理不可信网络输入时这是巨大优势
  • tokio:成熟的多线程 work-stealing 异步运行时,性能顶级

代表作:Cloudflare 的 pingora 用 Rust 重写了原本基于 Nginx 的代理,扛着每天万亿级请求,省 CPU 又降尾延迟。

Rust async vs Go goroutine

Go goroutineRust async/await
协程模型有栈协程,运行时隐式调度无栈协程,编译成状态机
让出 CPU隐式(运行时在阻塞点切换)显式 .await
运行时内置 runtime选 runtime(tokio/async-std)
心智负担低(像写同步代码)较高(生命周期、Send、Pin)
极致性能/控制好更好(无 GC、可控内存)

一句话:Go 用"简单"换开发速度,Rust 用"显式"换极致性能与安全。 第21章 的 go handle(c) 在 Rust 里是 tokio::spawn(async move { ... })。


代码一:L4 TCP 透传代理

最能体现 tokio 简洁的是 L4 代理——copy_bidirectional 就是 Go io.Copy 双向版的对应物:

// Cargo.toml: tokio = { version = "1", features = ["full"] }
use tokio::io::copy_bidirectional;
use tokio::net::{TcpListener, TcpStream};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("0.0.0.0:8088").await?;
    println!("L4 代理监听 :8088 → 转发 example.com:80");

    loop {
        let (mut inbound, peer) = listener.accept().await?;
        // 每个连接一个异步任务(对应 Go 的 go handle(c))
        tokio::spawn(async move {
            match TcpStream::connect("93.184.216.34:80").await {
                Ok(mut outbound) => {
                    // 双向拷贝,任一端关闭即返回(对应 Go 的两个 io.Copy)
                    if let Err(e) = copy_bidirectional(&mut inbound, &mut outbound).await {
                        eprintln!("{peer} 转发结束: {e}");
                    }
                }
                Err(e) => eprintln!("连后端失败: {e}"),
            }
        });
    }
}
cargo run
curl -s -H "Host: example.com" http://127.0.0.1:8088/ -o /dev/null -w "%{http_code}\n"   # 200

move 把 inbound/peer 的所有权转移进 task——这是 Rust 强制你想清楚"谁拥有这个连接",从根上杜绝了悬垂引用。


代码二:SOCKS5 代理(异步手撕协议)

和 第21章 的 Go SOCKS5 逐字节对应,但用 tokio 的异步读写:

use tokio::io::{copy_bidirectional, AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("0.0.0.0:1080").await?;
    println!("SOCKS5 代理监听 :1080");
    loop {
        let (client, _) = listener.accept().await?;
        tokio::spawn(async move {
            if let Err(e) = handle(client).await {
                eprintln!("会话错误: {e}");
            }
        });
    }
}

async fn handle(mut client: TcpStream) -> std::io::Result<()> {
    // 阶段1:方法协商  VER NMETHODS METHODS...
    let mut head = [0u8; 2];
    client.read_exact(&mut head).await?;
    if head[0] != 0x05 {
        return Ok(());
    }
    let mut methods = vec![0u8; head[1] as usize];
    client.read_exact(&mut methods).await?;
    client.write_all(&[0x05, 0x00]).await?; // 选 0x00 无认证

    // 阶段2:请求  VER CMD RSV ATYP ...
    let mut req = [0u8; 4];
    client.read_exact(&mut req).await?;
    if req[1] != 0x01 {
        // 只支持 CONNECT
        client.write_all(&[0x05, 0x07, 0, 0x01, 0, 0, 0, 0, 0, 0]).await?;
        return Ok(());
    }
    // 按 ATYP 解析目标地址(对应第06章报文)
    let host = match req[3] {
        0x01 => {
            let mut a = [0u8; 4];
            client.read_exact(&mut a).await?;
            format!("{}.{}.{}.{}", a[0], a[1], a[2], a[3])
        }
        0x03 => {
            let mut l = [0u8; 1];
            client.read_exact(&mut l).await?;
            let mut d = vec![0u8; l[0] as usize];
            client.read_exact(&mut d).await?;
            String::from_utf8_lossy(&d).into_owned()
        }
        0x04 => {
            let mut a = [0u8; 16];
            client.read_exact(&mut a).await?;
            std::net::Ipv6Addr::from(a).to_string()
        }
        _ => {
            client.write_all(&[0x05, 0x08, 0, 0x01, 0, 0, 0, 0, 0, 0]).await?;
            return Ok(());
        }
    };
    let mut port = [0u8; 2];
    client.read_exact(&mut port).await?;
    let port = u16::from_be_bytes(port);

    // 阶段3:拨号 + 应答 + 转发
    let mut server = match TcpStream::connect((host.as_str(), port)).await {
        Ok(s) => s,
        Err(_) => {
            client.write_all(&[0x05, 0x05, 0, 0x01, 0, 0, 0, 0, 0, 0]).await?;
            return Ok(());
        }
    };
    client.write_all(&[0x05, 0x00, 0, 0x01, 0, 0, 0, 0, 0, 0]).await?; // 成功
    copy_bidirectional(&mut client, &mut server).await?;
    Ok(())
}
curl --socks5-hostname 127.0.0.1:1080 https://example.com/ -s -o /dev/null -w "%{http_code}\n"  # 200

把这段和 第21章 的 Go 版并排:协议字节完全一样(都是 第06章 的 RFC 1928),区别只在语言——Go 用 io.ReadFull、Rust 用 read_exact().await,错误处理 Rust 用 ? 更显式。


灵活性与性能

维度Rust + tokio 的表现
吞吐/延迟顶级,无 GC 停顿,尾延迟稳定
内存安全编译期保证,处理不可信输入零担忧
并发tokio 多线程 work-stealing,百万连接
零拷贝可用 splice(需手动或 tokio-splice 类库;copy_bidirectional 默认走用户态缓冲,见 第12章)
开发速度慢于 Go(借用检查、Pin/Send 心智负担)
生态tokio/hyper/tower/pingora,云原生代理新势力

何时选 Rust:要榨干性能、要内存安全、长期运行的核心数据面——Cloudflare pingora、字节的部分网关都是例证。代价是开发更慢、门槛更高。


排错

现象根因解决
编译报 value moved所有权被移动后又用clone() 或调整借用
task 不执行忘了 .await 或没 spawn异步函数必须被 await/spawn 驱动
future is not Send跨 await 持有非 Send 类型换 Send 类型或缩小持有范围
连接不关闭、fd 泄漏错误路径没 drop stream用 ?/作用域确保 drop
性能没达预期阻塞调用堵了 runtime阻塞操作放 spawn_blocking

本章小结

  • tokio 写代理:tokio::spawn 对应 Go 的 go,copy_bidirectional 对应双向 io.Copy。
  • SOCKS5 的协议字节与 Go 版完全一致,差异只在语言表达(read_exact().await + ?)。
  • Rust 优势:无 GC、零成本、内存安全、尾延迟稳;代价是开发慢、门槛高。
  • 选 Rust 写核心数据面(pingora 即代表)。

下一章 第23章 Python + asyncio,走向另一极端:用最易读的语言快速写出代理与调试工具——mitmproxy 就是 Python 写的。