第22章 Rust:基于 tokio 的高性能 TCP 代理
学习目标
- 用 tokio 写出 L4 TCP 代理和 SOCKS5 代理,对照 第21章 的 Go 实现
- 理解 Rust async/await 与 Go goroutine 的模型差异(无栈状态机 vs 有栈协程)
- 体会
copy_bidirectional、所有权/move在网络编程中的作用 - 明白为什么 Cloudflare/字节等用 Rust 重写代理(pingora)
前置知识
- 第21章 Go 手写代理、第06章 SOCKS 协议、第09章 L4
- 一点 Rust 基础(所有权、
async/await)
原理
为什么用 Rust 写代理
代理是性能敏感、长期运行、处理不可信输入的程序——这正是 Rust 的甜点:
- 无 GC:没有 GC 停顿,尾延迟稳定(代理最怕 p99 抖动)
- 零成本抽象:高级写法编译成和手写 C 一样快的代码
- 内存安全:所有权 + 借用检查在编译期消灭"野指针/数据竞争"——处理不可信网络输入时这是巨大优势
- tokio:成熟的多线程 work-stealing 异步运行时,性能顶级
代表作:Cloudflare 的 pingora 用 Rust 重写了原本基于 Nginx 的代理,扛着每天万亿级请求,省 CPU 又降尾延迟。
Rust async vs Go goroutine
| Go goroutine | Rust 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 写的。