Rust TCP 客户端设计的两种路径:从迭代中看架构选择
设计一个 Rust TCP 客户端时,"简单"往往是最难实现的目标。本文记录了两个不同架构方向的实现过程:一个基于 Channel 解耦与 watch 取消令牌,另一个基于控制通道与回调函数。两者都经历了多轮迭代才达到可用状态,文字记录过程中暴露的问题。
第一部分:版本一------Channel 解耦 + watch 取消机制
1.1 初始骨架
最初的代码结构很直观:用 tokio::sync::mpsc 双通道解耦网络层与业务层,tokio::select! 同时驱动读写两个异步任务。
rust
use std::sync::Arc;
use log::error;
use tokio::{
io::{AsyncReadExt, AsyncWriteExt as _},
net::tcp::{ReadHalf, WriteHalf},
sync::{Mutex, mpsc::{Receiver, Sender}},
};
pub enum GameMsg {
Start
}
impl GameMsg {
pub fn from_bytes(_: &[u8]) -> Self {
Self::Start
}
pub fn to_bytes(&self) -> Vec<u8> {
match self { Self::Start => vec![0] }
}
}
pub struct TcpClient {
host: String,
port: u16,
sender_to_client: tokio::sync::mpsc::Sender<GameMsg>,
recv_from_server: Arc<Mutex<tokio::sync::mpsc::Receiver<GameMsg>>>,
sender_to_server: tokio::sync::mpsc::Sender<GameMsg>,
recv_from_client: Arc<Mutex<tokio::sync::mpsc::Receiver<GameMsg>>>,
}
impl TcpClient {
pub fn new(host: String, port: u16) -> Self {
let (sender_to_client, recv_from_server) =
tokio::sync::mpsc::channel::<GameMsg>(100);
let (sender_to_server, recv_from_client) =
tokio::sync::mpsc::channel::<GameMsg>(100);
Self {
host, port,
sender_to_client,
recv_from_server: Arc::new(Mutex::new(recv_from_server)),
sender_to_server,
recv_from_client: Arc::new(Mutex::new(recv_from_client)),
}
}
pub async fn loop_connect(&self) {
loop {
if let Err(e) = self.connect_server().await {
error!("Failed to connect: {}", e);
}
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
}
}
async fn connect_server(&self) -> anyhow::Result<()> {
let addr = format!("{}:{}", self.host, self.port);
let mut stream = tokio::net::TcpStream::connect(addr).await?;
let (read, recv) = stream.split();
let sender_clone = self.sender_to_client.clone();
let read_async = Self::read_msg(read, sender_clone);
let write_async = Self::write_msg(recv, self.recv_from_client.clone());
tokio::select! {
_ = read_async => {},
_ = write_async => {}
}
Ok(())
}
async fn write_msg(
mut write_half: WriteHalf<'_>,
recv: Arc<Mutex<Receiver<GameMsg>>>
) -> anyhow::Result<()> {
loop {
if let Some(game_msg) = recv.lock().await.recv().await {
let bytes = game_msg.to_bytes();
write_half.write_u32(bytes.len() as u32).await?;
write_half.write_all(&bytes).await?;
write_half.flush().await?;
} else { break; }
}
Ok(())
}
async fn read_msg(
mut read_half: ReadHalf<'_>,
sender: Sender<GameMsg>
) -> anyhow::Result<()> {
loop {
let data_len = read_half.read_u32().await?;
let mut data = vec![0; data_len as usize];
read_half.read_exact(&mut data).await?;
let msg = GameMsg::from_bytes(&data);
sender.send(msg).await?;
}
}
}
问题:
select!用_ =通配符,错误被静默丢弃,connect_server永远返回Ok(())- 缺少优雅关闭机制,程序退出可能卡在
sleep或connect Arc<Mutex<Receiver>>API 繁琐,业务层读取要写client.get_recv_from_server().lock().await.recv().awaitread_msg无长度上限,对端发恶意大包直接 OOM
1.2 引入 oneshot 取消(失败尝试)
rust
pub struct TcpClient {
// ... 其他字段
cancel_send_token: Option<tokio::sync::oneshot::Sender<()>>,
}
impl TcpClient {
pub fn stop_server(&mut self) {
if let Some(sender) = self.cancel_send_token.take() {
let _ = sender.send(());
}
}
pub fn start_server(&mut self) {
let (cancel_sender, cancel_recv) = tokio::sync::oneshot::channel::<()>();
self.cancel_send_token = Some(cancel_sender);
tokio::spawn(async move {
loop_connect(addr, cancel_recv, sender, recv).await;
});
}
}
async fn loop_connect(
addr: String,
mut cancel: tokio::sync::oneshot::Receiver<()>,
sender_to_client: tokio::sync::mpsc::Sender<GameMsg>,
recv_from_client: Arc<Mutex<tokio::sync::mpsc::Receiver<GameMsg>>>,
) {
loop {
let cancel_arc = Arc::new(cancel); // 编译错误
let cancel_clone = cancel_arc.clone(); // oneshot::Receiver 没有 Clone
tokio::select! {
cancel = async move { cancel_clone } => { // 逻辑错误:不会等待
info!("Cancel loop: {:?}", cancel);
break;
}
e = connect_server(&addr, sender_to_client.clone(), recv_from_client.clone()) => {
error!("Failed: {:?}", e);
}
}
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
}
}
问题:
oneshot::Receiver不能Clone,Arc::clone编译失败async move { cancel_clone }不会等待,第一次循环就breaksleep(3)在select!外部,stop时可能卡 3 秒update()有竞态:旧任务未结束就启动新任务
1.3 改用 watch<i32> 状态机
rust
pub struct TcpClient {
cancel_token_send: tokio::sync::watch::Sender<i32>,
cancel_token_recv: tokio::sync::watch::Receiver<i32>,
}
impl TcpClient {
pub async fn update(&mut self, host: String, port: u16) -> anyhow::Result<()> {
self.stop_server();
self.cancel_token_recv.changed().await?; // 等待从 1 变到 2
self.host = host;
self.port = port;
self.start_server();
Ok(())
}
pub fn stop_server(&mut self) {
self.cancel_token_send.send_replace(1); // 请求取消
}
pub fn start_server(&mut self) {
self.cancel_token_send.send_replace(0); // 重置为运行中
}
}
async fn loop_connect(
addr: String,
mut cancel: tokio::sync::watch::Receiver<i32>,
cancel_sender: tokio::sync::watch::Sender<i32>,
// ...
) {
loop {
let res = connect_server(&addr, &mut cancel, /* ... */).await;
match res {
Ok(s) => {
if s.eq("cancel") { break; }
},
Err(e) => error!("connect error: {}", e),
}
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
}
cancel_sender.send_replace(2); // 通知已退出
}
async fn connect_server(
addr: &str,
cancel: &mut tokio::sync::watch::Receiver<i32>,
// ...
) -> anyhow::Result<String> {
let mut stream = tokio::net::TcpStream::connect(addr).await?;
let (read, write) = stream.split();
let res = tokio::select! {
res = cancel.changed() => {
let _ = res?;
let val = cancel.borrow();
if *val == 1 {
return Ok("cancel".to_string()); // 直接 return
}
Err(anyhow::anyhow!("cancel expect 1, got {}", *val))
},
res = read_async => { /* ... */ },
res = write_async => { /* ... */ }
};
stream.shutdown().await?; // unreachable!所有分支都 return
res
}
问题:
update可能死锁:stop发1后,connect_server可能卡在连接中,等不到2watch::Sender广播覆盖:start_server的send_replace(0)通知所有Receiver,旧任务可能收到0而非1stream.shutdown()unreachable:select!所有分支都returnconnect无超时:服务器不可达时可能阻塞数十秒
1.4 简化为 watch<bool> + JoinHandle
rust
pub struct TcpClient {
cancel_token_send: tokio::sync::watch::Sender<bool>,
cancel_token_recv: tokio::sync::watch::Receiver<bool>,
handle: Option<tokio::task::JoinHandle<()>>,
}
impl TcpClient {
pub async fn update(&mut self, host: String, port: u16) -> anyhow::Result<()> {
self.stop_server();
if let Some(h) = self.handle.take() {
h.await?; // 等待旧任务结束
}
self.host = host;
self.port = port;
self.start_server();
Ok(())
}
pub fn stop_server(&mut self) {
self.cancel_token_send.send_replace(true);
}
pub fn start_server(&mut self) {
self.cancel_token_send.send_replace(false);
let handle = tokio::spawn(async move {
loop_connect(addr, cancel_recv, sender, recv).await;
});
self.handle = Some(handle);
}
}
async fn connect_server(
addr: &str,
cancel: &mut tokio::sync::watch::Receiver<bool>,
// ...
) -> anyhow::Result<String> {
let mut stream = tokio::net::TcpStream::connect(addr).await?;
let (read, write) = stream.split();
let res = tokio::select! {
res = cancel.changed() => {
let _ = res?;
let val = cancel.borrow();
if *val {
return Ok("cancel".to_string());
}
Err(anyhow::anyhow!("cancel expect true, got {}", *val))
},
res = read_async => {
match res {
Ok(_) => Ok("read exit ok".to_string()),
Err(e) => Err(e),
}
},
res = write_async => {
match res {
Ok(_) => Ok("write exit ok".to_string()),
Err(e) => Err(e),
}
}
};
let _ = stream.shutdown().await;
res
}
问题:
update仍可能永远卡住:select!不会取消connect_server内部已启动的TcpStream::connectstream.shutdown()的可达性争议:经确认,当分支无return时可达
1.5 修正 select! 分支结构
rust
let res = tokio::select! {
res = cancel.changed() => {
let _ = res?;
let val = cancel.borrow();
if *val {
Ok("cancel".to_string())
} else {
Err(anyhow::anyhow!("cancel expect true, got {}", *val))
}
},
res = read_async => {
match res {
Ok(_) => Ok("read exit ok".to_string()),
Err(e) => Err(e),
}
},
res = write_async => {
match res {
Ok(_) => Ok("write exit ok".to_string()),
Err(e) => Err(e),
}
}
};
let _ = stream.shutdown().await; // 现在确实可达
res
此时结构已正确,但仍有隐患:
TcpStream::connect依然没有超时send_to_socket与recv_from_socket函数名写反了
1.6 最终代码
rust
use std::sync::Arc;
use log::{error, info, debug};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt as _},
net::tcp::{ReadHalf, WriteHalf},
sync::{Mutex, mpsc::{Receiver, Sender}, watch},
};
const MAX_PACKET_SIZE: u32 = 5 * 1024 * 1024;
#[derive(Debug)]
pub enum GameMsg {
Start,
}
impl GameMsg {
pub fn from_bytes(_: &[u8]) -> Self {
Self::Start
}
pub fn to_bytes(&self) -> Vec<u8> {
match self { Self::Start => vec![0] }
}
}
pub struct TcpClient {
host: String,
port: u16,
send_to_server_tx: Sender<GameMsg>,
send_to_server_rx: Arc<Mutex<Receiver<GameMsg>>>,
recv_from_server_tx: Sender<GameMsg>,
recv_from_server_rx: Arc<Mutex<Receiver<GameMsg>>>,
cancel_tx: watch::Sender<bool>,
cancel_rx: watch::Receiver<bool>,
task_handle: Option<tokio::task::JoinHandle<()>>,
}
impl TcpClient {
pub fn new(host: String, port: u16) -> Self {
let (send_to_server_tx, send_to_server_rx) =
tokio::sync::mpsc::channel::<GameMsg>(100);
let (recv_from_server_tx, recv_from_server_rx) =
tokio::sync::mpsc::channel::<GameMsg>(100);
let (cancel_tx, cancel_rx) = watch::channel::<bool>(false);
Self {
host, port,
send_to_server_tx,
send_to_server_rx: Arc::new(Mutex::new(send_to_server_rx)),
recv_from_server_tx,
recv_from_server_rx: Arc::new(Mutex::new(recv_from_server_rx)),
cancel_tx, cancel_rx,
task_handle: None,
}
}
pub async fn update(&mut self, host: String, port: u16) -> anyhow::Result<()> {
info!("Updating server to {}:{}", host, port);
self.stop_server_and_wait().await;
if let Some(h) = self.task_handle.take() {
h.await?;
}
info!("Old server task exited.");
self.host = host;
self.port = port;
self.start_server();
Ok(())
}
pub fn get_sender(&self) -> Sender<GameMsg> {
self.send_to_server_tx.clone()
}
pub fn get_receiver(&self) -> Arc<Mutex<Receiver<GameMsg>>> {
self.recv_from_server_rx.clone()
}
pub fn stop_server(&mut self) {
if !*self.cancel_rx.borrow() {
info!("Sending stop signal...");
self.cancel_tx.send_replace(true);
}
}
pub async fn stop_server_and_wait(&mut self) {
self.stop_server();
if let Some(h) = self.task_handle.take() {
h.await.unwrap();
}
}
pub fn start_server(&mut self) {
let addr = format!("{}:{}", self.host, self.port);
let net_to_logic_tx = self.recv_from_server_tx.clone();
let logic_to_net_rx = self.send_to_server_rx.clone();
let cancel_rx = self.cancel_rx.clone();
self.cancel_tx.send_replace(false);
info!("Spawning connection task for {}", addr);
let handle = tokio::spawn(async move {
connection_loop(addr, cancel_rx, net_to_logic_tx, logic_to_net_rx).await;
});
self.task_handle = Some(handle);
}
}
async fn connection_loop(
addr: String,
mut cancel_rx: watch::Receiver<bool>,
net_to_logic_tx: Sender<GameMsg>,
logic_to_net_rx: Arc<Mutex<Receiver<GameMsg>>>,
) {
info!("Connection loop started for {}", addr);
loop {
let res = handle_connection(
&addr, &mut cancel_rx,
net_to_logic_tx.clone(), logic_to_net_rx.clone()
).await;
match res {
Ok(s) => {
if s.eq("cancel") {
info!("Cancelled by user.");
break;
}
info!("Connection closed: {}", s);
},
Err(e) => error!("Connection error: {}. Retry in 3s...", e),
}
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
}
info!("Connection loop ended for {}", addr);
}
async fn handle_connection(
addr: &str,
cancel_rx: &mut watch::Receiver<bool>,
net_to_logic_tx: Sender<GameMsg>,
logic_to_net_rx: Arc<Mutex<Receiver<GameMsg>>>,
) -> anyhow::Result<String> {
info!("Connecting to {}", addr);
let mut stream = tokio::net::TcpStream::connect(addr).await?;
info!("Connected to {}", addr);
let (read, write) = stream.split();
let read_loop = recv_from_socket(read, net_to_logic_tx);
let write_loop = send_to_socket(write, logic_to_net_rx);
let res = tokio::select! {
_ = cancel_rx.changed() => {
let val = cancel_rx.borrow();
if *val {
info!("Cancelled by stop signal.");
Ok("cancel".to_string())
} else {
Err(anyhow::anyhow!("Unexpected cancel value"))
}
},
res = read_loop => {
match res {
Ok(()) => {
info!("Read ended (peer closed).");
Ok("read exit ok".to_string())
}
Err(e) => { error!("Read error: {}", e); Err(e) }
}
},
res = write_loop => {
match res {
Ok(()) => {
info!("Write ended (senders dropped).");
Ok("write exit ok".to_string())
}
Err(e) => { error!("Write error: {}", e); Err(e) }
}
}
};
info!("Shutting down stream for {}", addr);
let _ = stream.shutdown().await;
res
}
async fn send_to_socket(
mut write_half: WriteHalf<'_>,
logic_to_net_rx: Arc<Mutex<Receiver<GameMsg>>>,
) -> anyhow::Result<()> {
loop {
if let Some(game_msg) = logic_to_net_rx.lock().await.recv().await {
let bytes = game_msg.to_bytes();
debug!("Sending msg, {} bytes", bytes.len());
write_half.write_u32(bytes.len() as u32).await?;
write_half.write_all(&bytes).await?;
write_half.flush().await?;
} else {
info!("Channel closed, stopping write.");
break;
}
}
Ok(())
}
async fn recv_from_socket(
mut read_half: ReadHalf<'_>,
net_to_logic_tx: Sender<GameMsg>
) -> anyhow::Result<()> {
loop {
let data_len = read_half.read_u32().await?;
if data_len > MAX_PACKET_SIZE {
return Err(anyhow::anyhow!(
"Packet too large: {} bytes (max {})",
data_len, MAX_PACKET_SIZE
));
}
debug!("Received header, {} bytes", data_len);
let mut data = vec![0; data_len as usize];
read_half.read_exact(&mut data).await?;
let msg = GameMsg::from_bytes(&data);
net_to_logic_tx.send(msg).await?;
}
}
第二部分:版本二------控制通道 + 回调处理
2.1 设计思路
版本一的问题在于:
Arc<Mutex<Receiver>>API 繁琐update等待旧任务退出可能卡住watch广播特性导致状态覆盖
版本二改用独立控制通道(TcpMsg)分发命令,业务层通过回调函数(MsgHandler)处理消息,取消和重连在同一个 select! 中处理。
2.2 消息定义
rust
#[derive(Debug)]
pub enum GameMsg {
ClientId(u32),
StartFunWithStringScript(String),
StartFunFromAssest(String),
StopFun,
Unkown
}
impl GameMsg {
pub fn from_bytes(bytes: &[u8]) -> Self {
let msg_type = Self::get_msg_type(&bytes[0..4]);
match msg_type {
0 => GameMsg::ClientId(0),
1 => GameMsg::StartFunWithStringScript(
String::from_utf8_lossy(&bytes[4..]).to_string()),
2 => GameMsg::StartFunFromAssest(
String::from_utf8_lossy(&bytes[4..]).to_string()),
3 => GameMsg::StopFun,
_ => Unkown
}
}
fn get_msg_type(bytes: &[u8]) -> u32 {
((bytes[0] as u32) << 24)
| ((bytes[1] as u32) << 16)
| ((bytes[2] as u32) << 8)
| (bytes[3] as u32)
}
pub fn to_bytes(&self) -> Vec<u8> {
todo!() // 待实现
}
}
2.3 控制消息与回调类型
rust
enum TcpMsg {
UpdateAddr(String),
Stop,
}
pub type MsgHandler = Box<dyn FnMut(GameMsg) -> () + Send + 'static>;
2.4 客户端结构
rust
pub struct TcpClient {
tcp_msg_sender: Option<Sender<TcpMsg>>,
msg_to_server_sender: Option<Sender<GameMsg>>,
game_msg_join_handle: Option<JoinHandle<()>>,
msg_handler: Option<MsgHandler>
}
impl TcpClient {
pub fn new() -> Self {
Self {
tcp_msg_sender: None,
msg_to_server_sender: None,
game_msg_join_handle: None,
msg_handler: None
}
}
pub fn set_msg_handler(&mut self, handler: MsgHandler) {
self.msg_handler = Some(handler);
}
2.5 启动逻辑
rust
pub fn start(&mut self, host: &str, port: u16) {
if self.msg_handler.is_none() {
error!("msg handler is not set");
return;
}
let (msg_from_server_sender, mut msg_from_server_receiver) =
tokio::sync::mpsc::channel::<GameMsg>(1024);
let (msg_to_server_sender, msg_to_server_receiver) =
tokio::sync::mpsc::channel::<GameMsg>(1024);
let (tcp_msg_sender, tcp_msg_receiver) =
tokio::sync::mpsc::channel::<TcpMsg>(1024);
self.msg_to_server_sender = Some(msg_to_server_sender);
self.tcp_msg_sender = Some(tcp_msg_sender);
let addr = format!("{}:{}", host, port);
TOKIO_RUNTIME.spawn(connect_server_loop(
addr,
msg_to_server_receiver,
msg_from_server_sender,
tcp_msg_receiver
));
if let Some(mut handler) = self.msg_handler.take() {
let handle = TOKIO_RUNTIME.spawn(async move {
while let Some(msg) = msg_from_server_receiver.recv().await {
handler(msg);
}
error!("msg_from_server_receiver closed");
});
self.game_msg_join_handle = Some(handle);
}
}
2.6 控制接口
rust
pub fn update_server_addr(&self, host: &str, port: u16) {
let addr = format!("{}:{}", host, port);
if let Some(sender) = &self.tcp_msg_sender {
if let Err(e) = sender.blocking_send(TcpMsg::UpdateAddr(addr)) {
error!("send update addr failed: {:?}", e);
}
}
}
pub fn stop(&self) {
if let Some(handle) = &self.game_msg_join_handle {
info!("abort game msg handle");
handle.abort();
}
if let Some(sender) = &self.tcp_msg_sender {
if let Err(e) = sender.blocking_send(TcpMsg::Stop) {
error!("send stop failed: {:?}", e);
} else {
info!("send stop msg to tcp client");
}
} else {
error!("tcp client not started");
}
}
pub fn send_msg_to_server(&self, msg: GameMsg) {
if let Some(sender) = &self.msg_to_server_sender {
if let Err(e) = sender.blocking_send(msg) {
error!("send msg to server failed: {:?}", e);
}
} else {
error!("tcp client not started");
}
}
2.7 连接主循环
rust
async fn connect_server_loop(
mut address: String,
mut msg_to_server_receiver: Receiver<GameMsg>,
mut msg_from_server_sender: Sender<GameMsg>,
mut cancel_receiver: Receiver<TcpMsg>
) {
loop {
tokio::select! {
msg_res = cancel_receiver.recv() => {
if let Some(msg) = msg_res {
match msg {
TcpMsg::UpdateAddr(addr) => {
address = addr;
info!("update server address to {}", address);
}
TcpMsg::Stop => {
info!("stop tcp client");
break;
}
}
} else {
error!("cancel_receiver closed");
break;
}
}
res = connect_server(
&address,
&mut msg_to_server_receiver,
&mut msg_from_server_sender
) => {
match res {
Ok(_) => info!("disconnected from server"),
Err(err) => error!("Failed: {:?}", err),
};
info!("try to reconnect in 3 seconds");
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
}
}
}
}
2.8 单次连接处理
rust
async fn connect_server(
addr: &str,
msg_to_server_receiver: &mut Receiver<GameMsg>,
msg_from_server_sender: &mut Sender<GameMsg>
) -> anyhow::Result<()> {
let mut stream = TcpStream::connect(addr).await?;
info!("Connected to server: {}", addr);
let (mut reader, mut writer) = stream.split();
let res = tokio::select! {
res = send_msg_to_server_loop(&mut writer, msg_to_server_receiver) => res,
res = recv_msg_from_server_loop(&mut reader, msg_from_server_sender) => res,
};
let _ = writer.shutdown().await;
res
}
2.9 写循环
rust
async fn send_msg_to_server_loop(
write_half: &mut WriteHalf<'_>,
msg_to_server_receiver: &mut Receiver<GameMsg>
) -> anyhow::Result<()> {
loop {
let msg = msg_to_server_receiver.recv().await;
if let Some(msg) = msg {
let bytes = msg.to_bytes();
let len = bytes.len() as u32;
write_half.write_u32(len).await?;
write_half.write_all(&bytes).await?;
write_half.flush().await?;
} else {
return Err(anyhow::anyhow!("receiver closed"));
}
}
}
2.10 读循环
rust
async fn recv_msg_from_server_loop(
read_half: &mut ReadHalf<'_>,
msg_from_server_sender: &mut Sender<GameMsg>
) -> anyhow::Result<()> {
loop {
let data_len = read_half.read_u32().await?;
if data_len > MAX_PACKET_SIZE {
return Err(anyhow::anyhow!("Packet too large: {}", data_len));
}
let mut buf = vec![0; data_len as usize];
read_half.read_exact(&mut buf).await?;
let msg = GameMsg::from_bytes(&buf);
info!("recv msg: {:?}", msg);
if let Err(e) = msg_from_server_sender.try_send(msg) {
error!("Failed to forward: {}, discard", e);
}
}
}
第三部分:两个版本对比
| 维度 | 版本一:Channel + watch | 版本二:控制通道 + 回调 |
|---|---|---|
| 业务层接收 | Arc<Mutex<Receiver>>,手动 lock + recv |
回调函数 FnMut(GameMsg),自动触发 |
| 业务层发送 | Sender::send |
Sender::blocking_send |
| 取消机制 | watch::Sender<bool> + JoinHandle::await |
独立 mpsc 控制通道 (TcpMsg) |
| 地址更新 | update().await 等待旧任务退出 |
update_server_addr() 非阻塞,通过控制通道通知 |
| 停止方式 | stop_server_and_wait().await 等待退出 |
stop() 发信号 + abort() 强制终止回调任务 |
| 重连控制 | 后台循环自动重连,sleep 期间不响应取消 | 同:sleep 期间不响应控制命令 |
| 代码复杂度 | 较高:Arc<Mutex>、watch、生命周期管理 |
较低:控制通道直接分发命令 |
| 耦合度 | 网络层与业务层通过 Channel 解耦,但 API 繁琐 | 网络层直接调用业务层回调,耦合更高但使用更简单 |
第四部分:关键教训
4.1 tokio::select! 取消的是 Future,不是系统调用
根据 tokio::select! 的文档:
"returning when the first branch completes, cancelling the remaining branches"
这里的 "cancelling" 指的是drop 掉未完成的 Future,而不是中断 Future 内部已经发起的系统调用。
rust
async fn connect_server(addr: &str) -> anyhow::Result<TcpStream> {
// TcpStream::connect 内部:
// 1. 创建 socket(系统调用)
// 2. 发起非阻塞 connect(系统调用,返回 EINPROGRESS)
// 3. 注册到 epoll 等待可写事件
// 4. 事件到达后检查连接状态
TcpStream::connect(addr).await
}
tokio::select! {
res = connect_server(addr) => res,
_ = sleep(Duration::from_secs(3)) => {},
}
// sleep 先触发,connect_server 的 Future 被 drop
Future 被 drop 后会发生什么:
| 阶段 | 状态 |
|---|---|
| socket 文件描述符 | 仍在内核中 |
| TCP 三次握手 | 继续进行,不受 Future drop 影响 |
| epoll 注册 | 被清理,不再接收事件 |
| Future 内的后续代码 | 不会执行 |
结果: TcpStream::connect 发起的系统调用继续运行,但 Future 被 drop 后没人处理结果。JoinHandle::await 要等这个系统调用完成(或超时)才能返回,通常 20~75 秒。
rust
// 解决:用 tokio::time::timeout 控制整个 connect 的超时
// 超时后 timeout 返回 Err,但底层的系统调用仍在运行
// 不过 JoinHandle 可以正常结束,因为 timeout 的 Future 已完成
let stream = tokio::time::timeout(
Duration::from_secs(5),
TcpStream::connect(addr)
).await??;
4.2 watch::Sender 广播给所有 Receiver 克隆
rust
// 问题:旧任务和新任务共享同一个 watch 值
let cancel_rx = self.cancel_rx.clone(); // 旧任务
self.cancel_tx.send_replace(false); // 新任务启动,但旧任务也收到 false
// 解决:版本二改用独立 mpsc 控制通道,每个命令只被消费一次
4.3 select! 分支中的 return 会导致后续代码 unreachable
rust
// 错误:shutdown 不会执行
let res = tokio::select! {
_ = cancel => { return Ok("cancel".to_string()); }
res = read => res,
};
stream.shutdown().await?; // unreachable
// 正确
let res = tokio::select! {
_ = cancel => Ok("cancel".to_string()),
res = read => res,
};
stream.shutdown().await?; // 可达
4.4 协议层必须设长度上限
rust
const MAX_PACKET_SIZE: u32 = 5 * 1024 * 1024;
let data_len = read_half.read_u32().await?;
if data_len > MAX_PACKET_SIZE {
return Err(anyhow::anyhow!("Packet too large"));
}
4.5 回调 vs Channel 的选择
| 场景 | 推荐 |
|---|---|
| 业务层需要精细控制接收时机 | Channel(版本一) |
| 业务层只需被动处理消息 | 回调(版本二) |
| 追求简单 | 回调(版本二) |
| 需要背压、不能丢消息 | Channel + send().await |
4.6 blocking_send 的适用场景
版本二在 update_server_addr、stop、send_msg_to_server 中都用 blocking_send,因为调用方在同步上下文(如游戏主循环)。如果在异步上下文中,应改用 send().await 避免阻塞线程。
结论
版本一通过 Arc<Mutex<Receiver>> 和 watch 实现了严格的生命周期管理(update 等待旧任务退出),但 API 繁琐、有死锁风险。版本二通过控制通道和回调简化了使用方式,但耦合更高、部分问题(超时、sleep 期间不响应)仍未解决。
两个版本的核心改进路径一致:错误传播 → 取消机制 → 任务生命周期管理 → 资源释放可达性确认 。选择取决于业务层是否需要精细控制接收时机,以及是否能容忍 blocking_send 的线程阻塞。