Files
DevFlow/crates/df-relay/src/conn.rs

203 lines
6.3 KiB
Rust

//! df-relay 连接注册表
//!
//! 设计依据:设计文档「Layer2」—— 按 device_id 配对路由(非全局广播)。
//!
//! 注册表结构:`HashMap<conn_id, ConnHandle>` + `device_id → conn_id` 索引。
//! 配对语义:小程序 device_id ↔ 桌面端绑定该 device_id 的 token(同 device_id
//! 的小程序与桌面端互为对端)。relay 按 device_id 找到对端连接转发消息。
//!
//! 并发:tokio::sync::Mutex 单锁(注册表操作高频但临界区小,先简单后优化)。
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use tokio::sync::{mpsc, Mutex};
use crate::broadcast::{BroadcastMessage, ClientKind};
/// 连接唯一标识(u64 自增,进程内唯一)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct ConnId(pub u64);
impl ConnId {
/// 伪值(用于无来源场景,如服务端构造的控制消息)
pub const NIL: ConnId = ConnId(0);
}
static NEXT_CONN_ID: AtomicU64 = AtomicU64::new(1);
/// 分配下一个 conn_id(进程内递增,从 1 起,0 留作 NIL)
pub fn next_conn_id() -> ConnId {
ConnId(NEXT_CONN_ID.fetch_add(1, Ordering::Relaxed))
}
/// 连接句柄(注册表条目)
///
/// 每个连接持一个 mpsc::UnboundedSender,广播时向其投递 BroadcastMessage,
/// 由连接自己的读取任务转发给对端 socket。
#[derive(Debug)]
pub struct ConnHandle {
/// 连接 ID
pub id: ConnId,
/// 客户端类型
pub kind: ClientKind,
/// 配对绑定的设备 ID(桌面端首次配置生成,小程序携带)
pub device_id: String,
/// 广播投递通道(连接读取任务消费 → 写入 socket)
pub sender: mpsc::UnboundedSender<BroadcastMessage>,
}
impl ConnHandle {
pub fn new(
id: ConnId,
kind: ClientKind,
device_id: String,
sender: mpsc::UnboundedSender<BroadcastMessage>,
) -> Self {
Self {
id,
kind,
device_id,
sender,
}
}
}
/// 连接注册表(RelayState 内部状态)
#[derive(Default)]
pub struct ConnRegistry {
/// conn_id → 连接句柄
by_id: HashMap<ConnId, ConnHandle>,
/// device_id → 绑定的连接集合(同 device_id 的小程序 + 桌面端)
/// 设计为 Vec:兼容同 device_id 多连接(多端登录),MVP 一般 1+1。
by_device: HashMap<String, Vec<ConnId>>,
}
impl ConnRegistry {
pub fn new() -> Self {
Self::default()
}
/// 注册连接
pub fn add(&mut self, handle: ConnHandle) {
let id = handle.id;
let device_id = handle.device_id.clone();
self.by_device.entry(device_id).or_default().push(id);
self.by_id.insert(id, handle);
}
/// 注销连接(返回其 device_id 用于清理索引)
pub fn remove(&mut self, id: ConnId) -> Option<String> {
let handle = self.by_id.remove(&id)?;
let device_id = handle.device_id.clone();
if let Some(ids) = self.by_device.get_mut(&device_id) {
ids.retain(|c| *c != id);
if ids.is_empty() {
self.by_device.remove(&device_id);
}
}
Some(device_id)
}
/// 查询 device_id 是否有在线连接(离线降级判断用)
pub fn is_device_online(&self, device_id: &str) -> bool {
self.by_device
.get(device_id)
.map(|ids| !ids.is_empty())
.unwrap_or(false)
}
/// 投递消息给指定 device_id 配对的对端连接(排除来源 source)
///
/// 路由规则:
/// - Event(Device 发):转发给同 device_id 的所有 Miniapp 连接
/// - Command(Miniapp 发):转发给同 device_id 的所有 Device 连接
/// - Control:转发给同 device_id 的所有连接(除来源)
///
/// 返回成功投递的连接数(用于日志/降级判断)。
pub fn route(&self, msg: &BroadcastMessage) -> usize {
let Some(ids) = self.by_device.get(&msg.device_id) else {
return 0;
};
let mut delivered = 0usize;
for &id in ids {
if id == msg.source {
// 不回环给来源
continue;
}
let Some(handle) = self.by_id.get(&id) else {
continue;
};
match (msg.kind, msg.from, handle.kind) {
// Event:仅投递给 Miniapp
(crate::broadcast::MessageKind::Event, _, ClientKind::Miniapp) => {}
// Command:仅投递给 Device
(crate::broadcast::MessageKind::Command, _, ClientKind::Device) => {}
// Control:全投递(除来源)
(crate::broadcast::MessageKind::Control, _, _) => {}
// 其余方向不匹配(Event 不投递给 Device,Command 不投递给 Miniapp)
_ => continue,
}
if handle.sender.send(msg.clone()).is_ok() {
delivered += 1;
}
}
delivered
}
/// 当前总连接数(诊断用)
pub fn len(&self) -> usize {
self.by_id.len()
}
/// 是否为空(诊断用)
pub fn is_empty(&self) -> bool {
self.by_id.is_empty()
}
}
/// 共享中继状态(axum State 传递)
///
/// 注册表用 tokio Mutex 包裹:广播路径在锁内调用 route,锁粒度小。
#[derive(Clone, Default)]
pub struct RelayState {
inner: Arc<Mutex<ConnRegistry>>,
}
impl RelayState {
pub fn new() -> Self {
Self::default()
}
/// 从 Arc 直接构造(用于 axum state)
pub fn from_arc(inner: Arc<Mutex<ConnRegistry>>) -> Self {
Self { inner }
}
/// 暴露内部 Arc(测试/外部 start 使用)
pub fn inner(&self) -> Arc<Mutex<ConnRegistry>> {
self.inner.clone()
}
/// 注册连接
pub async fn add_conn(&self, handle: ConnHandle) {
self.inner.lock().await.add(handle);
}
/// 注销连接
pub async fn remove_conn(&self, id: ConnId) -> Option<String> {
self.inner.lock().await.remove(id)
}
/// 查询 device 在线状态
pub async fn is_device_online(&self, device_id: &str) -> bool {
self.inner.lock().await.is_device_online(device_id)
}
/// 投递消息给对端连接(返回成功投递数)
pub async fn route(&self, msg: &BroadcastMessage) -> usize {
self.inner.lock().await.route(msg)
}
}