Rust 用 Axum + Tokio 写 WebSocket 服务:广播、心跳与连接清理
test12026-11-011 次阅读
需求:一个能广播的实时通道
聊天、行情、协作编辑都要服务端主动推。Axum 的 ws 提取器把握手交给框架,我们只需在 on_upgrade 之后拿到 socket 自己处理。下面是一个带广播和心跳的最小可用服务。
全局状态放一个广播通道
use axum::{
extract::ws::{Message, WebSocket, WebSocketUpgrade},
response::Response,
routing::get,
Router, State,
};
use tokio::sync::broadcast;
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
tx: broadcast::Sender<String>,
}
async fn ws_handler(
ws: WebSocketUpgrade,
State(state): State<AppState>,
) -> Response {
ws.on_upgrade(move |socket| handle_socket(socket, state))
}
每个连接:读客户端、广播、心跳
use futures::{SinkExt, StreamExt};
async fn handle_socket(socket: WebSocket, state: AppState) {
let (mut sender, mut receiver) = socket.split();
let mut rx = state.tx.subscribe();
// 收客户端消息,广播给所有人
let mut recv_task = tokio::spawn(async move {
while let Some(Ok(msg)) = receiver.next().await {
if let Message::Text(text) = msg {
let _ = state.tx.send(text.to_string());
}
}
});
// 把广播消息发回这个客户端
let mut send_task = tokio::spawn(async move {
while let Ok(text) = rx.recv().await {
if sender.send(Message::Text(text)).await.is_err() {
break;
}
}
});
// 任一边结束就取消另一边
tokio::select! {
_ = &mut recv_task => send_task.abort(),
_ = &mut send_task => recv_task.abort(),
}
}
心跳与房间
浏览器和代理会掐掉空闲连接,定期发 Message::Ping 保活。要按房间隔离,把全局 broadcast 换成“房间 id -> Sender”的 HashMap,客户端带上房间号再订阅对应通道即可。
// 每 30 秒发一次 Ping
let mut interval = tokio::time::interval(std::time::Duration::from_secs(30));
tokio::spawn(async move {
loop {
interval.tick().await;
if sender.send(Message::Ping(vec![])).await.is_err() {
break;
}
}
});
几个坑
- split 后 sender/receiver 要 move 到各自的 task,不能再借用原 socket。
- 客户端断开要两边都 abort,否则 goroutine 泄漏。
- 广播用 broadcast 是扇出模型;要单收单发的私信,得给每个连接单独建 channel 并维护路由表。
T
test1
文章作者
需求:一个能广播的实时通道 聊天、行情、协作编辑都要服务端主动推。Axum 的 ws 提取器把握手交给框架,我们只需在...
- 分类
- 技术
- 发布时间
- 2026-11-01
- 字数
- 约 1819 字
- 阅读
- 1 次
