Rust 网络编程实战
wxk1991 Lv5

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 网络程序时,建议按这个顺序思考:

  1. 协议边界是什么,是行、长度头,还是现成协议。
  2. 每个连接是否有读写超时。
  3. 错误日志是否保留了客户端地址和操作上下文。
  4. 是否会在异步任务里调用阻塞代码。
  5. 共享状态是否会造成锁竞争。

Rust 的类型系统能帮你挡掉很多内存安全问题,但网络编程的复杂性主要来自协议、超时、并发和资源管理。把这些边界设计清楚,Rust 写出来的网络服务会非常稳。