新增: 跨端WS隧道脚手架(df-tunnel客户端+df-relay云中继)

df-tunnel:桌面端出站WS客户端(TunnelClient trait+骨架,穿NAT连云后端)
df-relay:axum WS中继服务(转发小程序↔桌面端,广播骨架)
双crate独立消息骨架,不依赖src-tauri/df-types避跨crate强耦合
workspace crates/* glob自动注册,Phase2填充WS握手/鉴权/重连/路由
This commit is contained in:
2026-06-22 01:24:01 +08:00
parent 566bbcb394
commit 5d1749a58f
11 changed files with 818 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
//! df-relay 中继服务骨架
//!
//! 设计依据:设计文档「Layer2」—— axum WS Server + 广播中继。
//! 接受两类连接:小程序(device_id 鉴权)+ 桌面端(token 配对)。
//! 纯转发,无业务逻辑。
//!
//! 脚手架范围:RelayServer trait + axum WS 路由骨架(占位 handler),
//! Phase2 填:
//! - 客户端注册表(HashMap<device_id, 连接集>)
//! - 广播分发(按 device_id 路由)
//! - 鉴权中间件
use async_trait::async_trait;
use axum::{
extract::{
ws::{WebSocket, WebSocketUpgrade},
State,
},
response::IntoResponse,
routing::get,
Router,
};
use std::sync::Arc;
use crate::broadcast::BroadcastMessage;
use crate::error::Result;
/// 中继服务抽象
///
/// 设计为 trait 便于测试 mock + 未来替换实现(如换 tonic gRPC 网关)。
#[async_trait]
pub trait RelayServer: Send + Sync {
/// 启动 HTTP/WS 服务监听指定地址
async fn start(&self, addr: &str) -> Result<()>;
/// 广播消息给指定 device_id 绑定的对端
async fn broadcast(&self, msg: BroadcastMessage) -> Result<()>;
/// 注册新连接(连接建立后入表)
async fn register_client(&self, device_id: String) -> Result<()>;
/// 注销连接(断开后出表)
async fn unregister_client(&self, device_id: String) -> Result<()>;
/// 查询 device 是否有在线连接(离线降级判断用)
fn is_device_online(&self, device_id: &str) -> bool;
}
/// 共享状态(axum State 传递)
///
/// 脚手架占位:Phase2 填 tokio::sync::RwLock<HashMap<device_id, ConnectionSet>>。
/// 当前用 unit 占位以满足 Router state 约束。
#[derive(Clone, Default)]
pub struct RelayState {
/// Phase2:客户端注册表
/// 例:`clients: Arc<RwLock<HashMap<String, ConnectionSet>>>`
#[allow(dead_code)]
_reserved: (),
}
/// axum WS 路由构造
///
/// 暴露 `/ws/device`(桌面端连入)与 `/ws/miniapp`(小程序连入)两个端点,
/// 共享 RelayState。Phase2 在 handler 内填鉴权 + 连接生命周期管理。
pub fn build_router(state: Arc<RelayState>) -> Router {
Router::new()
.route("/ws/device", get(device_ws_handler))
.route("/ws/miniapp", get(miniapp_ws_handler))
.with_state(state)
}
/// 桌面端 WS 连接 handler(脚手架占位)
///
/// 当前仅接受升级并立即返回(不消费流),Phase2 填:
/// - 握手鉴权(token)
/// - register_client
/// - 循环 recv 转发 BroadcastMessage
/// - 断开 unregister_client
async fn device_ws_handler(
ws: WebSocketUpgrade,
State(_state): State<Arc<RelayState>>,
) -> impl IntoResponse {
tracing::info!("桌面端 WS 连接接入(脚手架占位)");
ws.on_upgrade(handle_connection)
}
/// 小程序 WS 连接 handler(脚手架占位)
async fn miniapp_ws_handler(
ws: WebSocketUpgrade,
State(_state): State<Arc<RelayState>>,
) -> impl IntoResponse {
tracing::info!("小程序 WS 连接接入(脚手架占位)");
ws.on_upgrade(handle_connection)
}
/// 通用 WS 连接处理(脚手架:接收一次即关闭)
///
/// Phase2 替换为完整读写循环 + 广播分发。
async fn handle_connection(mut socket: WebSocket) {
// 接收首条消息(占位),Phase2 改为持续循环
if let Some(Ok(msg)) = futures_util::StreamExt::next(&mut socket).await {
tracing::debug!(?msg, "收到首条消息(脚手架占位,Phase2 填路由)");
}
// 脚手架:静默关闭,不报错(连接生命周期管理留 Phase2)
}
/// 默认中继服务骨架
///
/// 脚手架阶段:trait 方法返回未实现错误,不静默假成功。
/// Phase2 替换为完整实现(启动 axum::serve + 注册表 + 广播)。
pub struct DefaultRelayServer {
#[allow(dead_code)]
bind_addr: Option<String>,
}
impl DefaultRelayServer {
pub fn new() -> Self {
Self { bind_addr: None }
}
}
impl Default for DefaultRelayServer {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl RelayServer for DefaultRelayServer {
async fn start(&self, addr: &str) -> Result<()> {
tracing::warn!(addr = %addr, "DefaultRelayServer.start 尚未实现(Phase2 填充)");
Err(crate::error::RelayError::Other(anyhow::anyhow!(
"DefaultRelayServer.start 尚未实现(Phase2)"
)))
}
async fn broadcast(&self, _msg: BroadcastMessage) -> Result<()> {
Err(crate::error::RelayError::Other(anyhow::anyhow!(
"DefaultRelayServer.broadcast 尚未实现(Phase2)"
)))
}
async fn register_client(&self, _device_id: String) -> Result<()> {
Err(crate::error::RelayError::Other(anyhow::anyhow!(
"DefaultRelayServer.register_client 尚未实现(Phase2)"
)))
}
async fn unregister_client(&self, _device_id: String) -> Result<()> {
Err(crate::error::RelayError::Other(anyhow::anyhow!(
"DefaultRelayServer.unregister_client 尚未实现(Phase2)"
)))
}
fn is_device_online(&self, _device_id: &str) -> bool {
false
}
}