Rust 网络编程实战:用 tokio 写并发 TCP 服务器并优雅关闭
test12026-10-181 次阅读
从 HTTP 框架退一步看原生 TCP
写业务大多直接用 axum/actix,但理解底层 TCP 有助于排查连接泄漏、背压与优雅关闭问题。tokio 的 TcpListener 配合 per-connection 的异步任务,可以很自然地支撑上万并发连接,而优雅关闭则靠信号 + JoinSet 把在途连接跑完。
一个会回显的并发服务器
// src/main.rs
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tokio::signal;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
println!("listening on 127.0.0.1:8080");
// JoinSet 统一持有所有连接任务,便于一起等待结束
let mut handles = tokio::task::JoinSet::new();
loop {
tokio::select! {
// 收到 Ctrl+C 后跳出 accept 循环,进入收尾
_ = signal::ctrl_c() => {
println!("shutting down, draining connections...");
break;
}
accepted = listener.accept() => {
let (mut socket, addr) = match accepted {
Ok(v) => v,
Err(_) => continue,
};
// 每连接派一个任务,互不影响
handles.spawn(async move {
let mut buf = [0u8; 1024];
loop {
match socket.read(&mut buf).await {
Ok(0) => break, // 对端关闭
Ok(n) => {
if socket.write_all(&buf[..n]).await.is_err() { break; }
}
Err(_) => break,
}
}
println!("disconnected: {addr}");
});
}
}
}
// 等待所有在途连接自然结束,不强制 kill
while handles.join_next().await.is_some() {}
println!("bye");
Ok(())
}
关键点拆解
- tokio::select! 让"收信号"和"接新连接"两条路并发进行,信号一来立即停止接新客。
- 每个连接独立 spawn,单连接阻塞不会拖垮整体;JoinSet 比手动存 JoinHandle 更好回收。
- read 返回 0 表示对端已关闭,务必 break,否则会空转读。
- 真要做广播或多客户端互发,把 socket 写端放进 Arc<Mutex<TcpStream>> 或用 tokio::sync::broadcast 频道。
T
test1
文章作者
从 HTTP 框架退一步看原生 TCP 写业务大多直接用 axum/actix,但理解底层 TCP 有助于排查连接泄漏...
- 分类
- 技术
- 发布时间
- 2026-10-18
- 字数
- 约 1975 字
- 阅读
- 1 次
