Rust 网络编程实战
Rust 做网络编程时,常见路线有两条:用标准库写阻塞 TCP 服务,或者用 Tokio 写异步网络服务。前者适合理解基本模型,后者更接近真实项目。
这篇文章用几个小例子串起来,重点讲怎么写得稳定,而不是只让 demo 跑起来。
一、最小 TCP 服务
标准库已经提供了 TCP 监听能力:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| use std::io::{Read, Write}; use std::net::TcpListener;
fn main() -> std::io::Result<()> { let listener = TcpListener::bind("127.0.0.1:9000")?; println!("listening on 127.0.0.1:9000");
for stream in listener.incoming() { let mut stream = stream?; let mut buf = [0; 1024]; let n = stream.read(&mut buf)?; stream.write_all(&buf[..n])?; }
Ok(()) }
|
这个 echo server 可以工作,但它一次只能认真处理一个连接。真实服务一般不会这样写。
二、阻塞模型可以加线程
最直接的并发方式是每个连接一个线程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| use std::io::{Read, Write}; use std::net::{TcpListener, TcpStream}; use std::thread;
fn handle_client(mut stream: TcpStream) -> std::io::Result<()> { let mut buf = [0; 1024];
loop { let n = stream.read(&mut buf)?; if n == 0 { break; } stream.write_all(&buf[..n])?; }
Ok(()) }
fn main() -> std::io::Result<()> { let listener = TcpListener::bind("127.0.0.1:9000")?;
for stream in listener.incoming() { let stream = stream?; thread::spawn(move || { if let Err(err) = handle_client(stream) { eprintln!("client error: {}", err); } }); }
Ok(()) }
|
这个版本更接近可用服务,但连接很多时线程数量会膨胀。对高并发网络服务,通常会转向 Tokio。
三、Tokio 异步 TCP 服务
添加依赖:
1 2
| [dependencies] tokio = { version = "1", features = ["full"] }
|
异步版本可以这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpListener;
#[tokio::main] async fn main() -> std::io::Result<()> { let listener = TcpListener::bind("127.0.0.1:9000").await?;
loop { let (mut socket, addr) = listener.accept().await?; println!("client connected: {}", addr);
tokio::spawn(async move { let mut buf = [0; 1024];
loop { let n = match socket.read(&mut buf).await { Ok(0) => return, Ok(n) => n, Err(err) => { eprintln!("read error: {}", err); return; } };
if let Err(err) = socket.write_all(&buf[..n]).await { eprintln!("write error: {}", err); return; } } }); } }
|
Tokio 的优势不是让单个请求更快,而是让大量连接在少量线程上协作运行。
四、一定要处理半包和粘包
TCP 是字节流,不是消息队列。一次 read 不一定刚好读到一条完整消息,也可能一次读到多条消息的一部分。
不要假设这样就够了:
1 2
| let n = socket.read(&mut buf).await?; let message = std::str::from_utf8(&buf[..n])?;
|
更稳妥的做法是设计协议边界。常见方式有三种:
- 每行一条消息,例如用
\n 分隔
- 固定长度头部,头部里声明 body 长度
- 使用成熟协议,例如 HTTP、WebSocket、gRPC
如果是按行协议,可以用 BufReader:
1 2 3 4 5 6 7 8
| use tokio::io::{AsyncBufReadExt, BufReader};
let reader = BufReader::new(socket); let mut lines = reader.lines();
while let Some(line) = lines.next_line().await? { println!("message: {}", line); }
|
网络程序里很多诡异 bug,本质上都是没有认真处理消息边界。
五、设置超时,避免连接一直挂住
外部网络不可靠,客户端可能连上后不发数据,也可能中途断开。服务端应该设置超时:
1 2 3 4
| use std::time::Duration; use tokio::time::timeout;
let n = timeout(Duration::from_secs(5), socket.read(&mut buf)).await??;
|
超时不是锦上添花,而是服务稳定性的基本要求。没有超时,资源迟早会被慢连接占满。
六、共享状态要选对工具
网络服务通常需要共享配置、连接池、缓存或计数器。只读配置可以用 Arc:
1 2 3 4 5 6 7 8 9 10
| use std::sync::Arc;
#[derive(Clone)] struct AppState { app_name: String, }
let state = Arc::new(AppState { app_name: "demo".to_string(), });
|
需要修改共享数据时,可以用 tokio::sync::Mutex,但不要把锁持有太久,尤其不要在持锁期间执行慢 I/O。
1 2 3 4
| use std::sync::Arc; use tokio::sync::Mutex;
let counter = Arc::new(Mutex::new(0_u64));
|
如果共享的是数据库连接,优先使用连接池,而不是自己用锁包一个连接。
七、实战建议
写 Rust 网络程序时,建议按这个顺序思考:
- 协议边界是什么,是行、长度头,还是现成协议。
- 每个连接是否有读写超时。
- 错误日志是否保留了客户端地址和操作上下文。
- 是否会在异步任务里调用阻塞代码。
- 共享状态是否会造成锁竞争。
Rust 的类型系统能帮你挡掉很多内存安全问题,但网络编程的复杂性主要来自协议、超时、并发和资源管理。把这些边界设计清楚,Rust 写出来的网络服务会非常稳。