Files
qiming/qimingclaw/specs/agent-client/vendorsdoc/qiming-rustdesk.md

701 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# qiming-rustdesk
## 项目概述
RustDesk 远程桌面客户端的定制版本支持远程控制、文件传输、音视频服务、剪贴板同步等功能。作为本项目的核心通信库提供双向通信、P2P 直连和中继传输能力。
**本地路径**: `vendors/qiming-rustdesk`
## 目录结构
```
qiming-rustdesk/
├── src/
│ ├── lib.rs # 库入口导出公共API
│ ├── client.rs # 客户端连接处理核心模块
│ ├── server.rs # 服务端音视频/剪贴板/输入服务
│ ├── common.rs # 公共工具函数和类型定义
│ ├── rendezvous_mediator.rs # 远程连接中介(核心)
│ ├── ipc.rs # 进程间通信
│ └── platform/ # 平台特定代码
│ ├── linux/
│ ├── macos/
│ └── windows/
├── libs/
│ ├── hbb_common/ # 通用工具库(最重要)
│ │ ├── src/
│ │ │ ├── lib.rs
│ │ │ ├── message_proto/ # protobuf 消息定义
│ │ │ ├── net/ # 网络相关TCP/KCP/WebSocket
│ │ │ ├── crypto/ # 加密模块
│ │ │ ├── config/ # 配置管理
│ │ │ └── util/ # 工具函数
│ │ └── Cargo.toml
│ ├── scrap/ # 屏幕捕获库
│ ├── enigo/ # 键盘鼠标模拟库
│ ├── clipboard/ # 剪贴板库
│ └── virtual_display/ # 虚拟显示驱动
├── proto/ # protobuf 协议定义
└── Cargo.toml # workspace 配置
```
## 核心通信架构
### 整体架构
```
┌─────────────────────────────────────────────────────────────────┐
│ agent-client │
│ │
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
│ │ UI 界面 │ │ 核心通信模块 │ │
│ │ - 聊天窗口 │ │ - rendezvous_mediator (连接管理)│ │
│ │ - 设置界面 │ │ - client (数据收发) │ │
│ │ - 依赖管理 │ │ - server (音视频服务) │ │
│ └─────────────────┘ └─────────────────────────────────┘ │
│ │ │
└────────────────────────────────┼────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ data-server (rustdesk-server) │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ hbbs │ │ hbbr │ │
│ │ (信令服务器) │ │ (中继服务器) │ │
│ │ - 端口: 21116 │ │ - 端口: 21117 │ │
│ │ - ID 注册 │ │ - 数据中继 │ │
│ │ - 连接协商 │ │ - 带宽限制 │ │
│ │ - NAT 穿透 │ │ │ │
│ └─────────────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ agent-server-admin (管理端) │
└─────────────────────────────────────────────────────────────────┘
```
## 连接管理 (rendezvous_mediator.rs)
### 核心结构体
```rust
// src/rendezvous_mediator.rs
pub struct RendezvousMediator {
addr: TargetAddr<'static>,
host: String,
host_prefix: String,
keep_alive: i32,
}
impl RendezvousMediator {
/// 启动连接管理器(根据配置选择 UDP 或 TCP 模式)
pub async fn start(server: ServerPtr, host: String) -> ResultType<()> {
log::info!("start rendezvous mediator of {}", host);
// 根据配置选择通信模式
if cfg!(debug_assertions) && option_env!("TEST_TCP").is_some()
|| Config::is_proxy()
|| use_ws()
|| crate::is_udp_disabled()
{
// WebSocket/代理模式
Self::start_tcp(server, host).await
} else {
// UDP 直连模式
Self::start_udp(server, host).await
}
}
}
```
### 双协议支持
```rust
// UDP 直连模式(优先使用,性能更好)
async fn start_udp(server: ServerPtr, host: String) -> ResultType<()> {
// 注册超时管理
const MIN_REG_TIMEOUT: i64 = 3_000;
const MAX_REG_TIMEOUT: i64 = 30_000;
let mut reg_timeout = MIN_REG_TIMEOUT;
loop {
// 心跳延迟计算EMA 指数移动平均)
if ema_latency == 0 {
ema_latency = latency;
} else {
ema_latency = latency / 30 + (ema_latency * 29 / 30);
}
// 动态调整注册间隔
let mut n = latency / 5;
if n < 3000 { n = 3000; }
// 心跳保活
if !send_heartbeat(...).await {
reg_timeout = (reg_timeout * 2).min(MAX_REG_TIMEOUT);
}
}
}
// TCP/WebSocket 模式(代理环境或 UDP 被禁用时)
async fn start_tcp(server: ServerPtr用于, host: String) -> ResultType<()> {
// 建立 TCP 连接
let mut socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?;
// 发送注册消息
let mut msg_out = Message::new();
let mut rr = RegisterRelay {
socket_addr: socket_addr.into(),
version: crate::VERSION.to_owned(),
..Default::default()
};
msg_out.set_register_relay(rr);
socket.send(&msg_out).await?;
// 处理中继响应
if let Some(msg) = socket.next().await {
let response = msg?.payload;
// 建立中继连接...
}
}
```
## P2P 直连流程 (打洞)
### 打洞请求处理
```rust
// src/rendezvous_mediator.rs
async fn handle_punch_hole(&self, ph: PunchHole, server: ServerPtr) -> ResultType<()> {
// 1. 解析对端地址
let mut peer_addr = AddrMangle::decode(&ph.socket_addr);
// 2. 去重处理100ms 内重复消息忽略)
let last = *LAST_MSG.lock().await;
if last.0 == peer_addr && last.1.elapsed().as_millis() < 100 {
return Ok(()); // 忽略重复消息
}
// 3. 判断是否需要中继
let relay = use_ws() || Config::is_proxy() || ph.force_relay;
let nat_type = NatType::from_i32(Config::get_nat_type())?;
// 对称型 NAT 或强制中继时降级到中继模式
if nat_type == NatType::SYMMETRIC || relay || (config::is_disable_tcp_listen() && ph.udp_port <= 0) {
return self.create_relay(...).await;
}
// 4. UDP 打洞
if ph.udp_port > 0 {
peer_addr.set_port(ph.udp_port as u16);
return self.punch_udp_hole(peer_addr, server, msg_punch, control_permissions).await;
}
// 5. TCP 打洞
let mut socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?;
allow_err!(socket_client::connect_tcp_local(peer_addr, Some(local_addr), 30).await);
}
```
### NAT 类型与连接策略
```rust
// NAT 类型决定连接策略
enum NatType {
NO_NAT, // 无 NAT优先 P2P 直连
FULL_CONE, // 完全锥形 NAT优先 P2P 直连
RESTRICTED, // 限制锥形 NAT可能需要中继
SYMMETRIC, // 对称型 NAT必须中继
}
// 连接策略选择
fn select_connection_strategy(nat_type: NatType, force_relay: bool) -> ConnectionMode {
if force_relay || nat_type == NatType::SYMMETRIC {
ConnectionMode::Relay // 必须中继
} else if nat_type == NatType::RESTRICTED {
ConnectionMode::TryP2PThenRelay // 尝试 P2P失败则中继
} else {
ConnectionMode::P2P // 优先 P2P
}
}
```
## 中继模式 (Relay)
### 创建中继连接
```rust
// src/rendezvous_mediator.rs
async fn create_relay(
&self,
socket_addr: Vec<u8>,
relay_server: String,
uuid: String,
server: ServerPtr,
secure: bool,
initiate: bool,
socket_addr_v6: bytes::Bytes,
control_permissions: Option<ControlPermissions>,
) -> ResultType<()> {
// 1. 建立到 rendezvous server 的 TCP 连接
let mut socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?;
// 2. 发送中继请求
let mut msg_out = Message::new();
let mut rr = RelayResponse {
socket_addr: socket_addr.into(),
version: crate::VERSION.to_owned(),
socket_addr_v6,
..Default::default()
};
msg_out.set_relay_response(rr);
socket.send(&msg_out).await?;
// 3. 建立 relay 连接
crate::create_relay_connection(
server,
relay_server,
uuid,
peer_addr,
secure,
is_ipv4(&self.addr),
control_permissions,
).await;
Ok(())
}
```
### 心跳保活机制
```rust
// 心跳机制确保连接活跃
async fn send_heartbeat(&self, socket: &mut impl FramedWrite) -> bool {
let mut msg = Message::new();
msg.set_ping(Ping {
timestamp: get_time().await,
..Default::default()
});
if let Err(e) = socket.send(&msg).await {
log::error!("Heartbeat failed: {}", e);
return false;
}
true
}
// 心跳超时检测
fn check_heartbeat_timeout(last_heartbeat: Option<DateTime<Utc>>, timeout_secs: i64) -> bool {
if let Some(timestamp) = last_heartbeat {
let elapsed = Utc::now() - timestamp;
elapsed.num_seconds() > timeout_secs
} else {
false
}
}
```
## Protocol Buffers 消息定义
### 主消息类型
```protobuf
// libs/hbb_common/src/message_proto/mod.rs
// 主消息类型oneof 模式支持多种消息)
message Message {
int32 id = 1; // 消息 ID
MessageType type = 2; // 消息类型
oneof payload {
LoginRequest login_request = 3;
LoginResponse login_response = 4;
AudioFrame audio_frame = 5;
VideoFrame video_frame = 6;
InputEvent input_event = 7;
ClipboardData clipboard_data = 8;
FileTransfer file_transfer = 9;
CloseReason close_reason = 10;
Ping ping = 11;
Pong pong = 12;
PunchHole punch_hole = 13; // 打洞消息
RelayResponse relay_response = 14; // 中继响应
}
}
enum MessageType {
NONE = 0;
LOGIN_REQUEST = 1;
LOGIN_RESPONSE = 2;
AUDIO_FRAME = 3;
VIDEO_FRAME = 4;
INPUT_EVENT = 5;
CLIPBOARD = 6;
FILE_TRANSFER = 7;
CLOSE = 8;
PING = 9;
PONG = 10;
PUNCH_HOLE = 11;
RELAY_RESPONSE = 12;
}
```
### 打洞消息
```protobuf
message PunchHole {
string peer_id = 1; // 对端 ID
bytes socket_addr = 2; // 序列化地址
int32 socket_addr_version = 3; // 地址版本
int32 peer_port = 4; // 对端端口
bool force_relay = 5; // 强制中继
int32 nat_type = 6; // NAT 类型
bytes local_addr = 7; // 本地地址
int32 local_port = 8; // 本地端口
int32 udp_port = 9; // UDP 端口
int32 conn_type = 10; // 连接类型
bytes socket_addr_v6 = 11; // IPv6 地址
bytes local_addr_v6 = 12; // IPv6 本地地址
}
```
### 文件传输(支持分片和断点续传)
```protobuf
message FileTransfer {
string id = 1; // 传输 ID
FileTransferAction action = 2;
bytes data = 3; // 文件数据
int64 offset = 4; // 文件偏移(支持断点续传)
bool compressed = 5; // 是否压缩
}
enum FileTransferAction {
FILE_TRANSFER_NONE = 0;
FILE_TRANSFER_REQUEST = 1; // 请求传输
FILE_TRANSFER_RESPONSE = 2; // 响应
FILE_TRANSFER_DATA = 3; // 数据块
FILE_TRANSFER_CANCEL = 4; // 取消
FILE_TRANSFER_DONE = 5; // 完成
}
// 文件元数据
message FileMeta {
string name = 1;
int64 size = 2;
int64 modified = 3;
bool is_directory = 4;
}
// 文件块
message FileChunk {
int32 id = 1;
int32 file_num = 2;
bytes data = 3;
bool compressed = 4;
int64 offset = 5;
}
```
### 输入事件
```protobuf
message InputEvent {
oneof event {
KeyEvent key = 1;
MouseEvent mouse = 2;
WheelEvent wheel = 3;
ClipboardEvent clipboard = 4;
}
}
message KeyEvent {
Direction direction = 1;
int32 code = 2; // 扫描码
int32 key = 3; // 虚拟键码
bool scan = 4;
bool raw = 5;
}
message MouseEvent {
Direction direction = 1;
int32 button = 2;
int32 x = 3;
int32 y = 4;
}
message WheelEvent {
Axis axis = 1;
int32 delta = 2;
}
enum Direction {
DOWN = 0;
UP = 1;
}
enum Axis {
HORIZONTAL = 0;
VERTICAL = 1;
}
```
## 在本项目中的使用
### 1. 客户端连接管理
```rust
// agent-client/src/connection/mod.rs
use hbb_common::{rendezvous_proto::*, ResultType};
use std::sync::Arc;
pub struct ConnectionManager {
mediator: Arc<RendezvousMediator>,
state: ConnectionState,
}
pub enum ConnectionState {
Disconnected,
Connecting,
Connected(PeerConnection),
Relay(RelayConnection),
}
impl ConnectionManager {
/// 连接到远程客户端
pub async fn connect(
&self,
peer_id: &str,
key: &str,
) -> ResultType<PeerConnection> {
// 1. 从 hbbs 获取对端信息
let peer_info = self.mediator.get_peer_info(peer_id).await?;
// 2. 尝试 P2P 直连
if let Some(conn) = self.try_p2p_connect(&peer_info, key).await {
return Ok(conn);
}
// 3. P2P 失败,降级到中继
self.connect_via_relay(&peer_info, key).await
}
/// 监听远程连接
pub async fn listen(&self, id: &str, key: &str) -> ResultType<()> {
self.mediator.start(id, key).await
}
}
```
### 2. 消息收发
```rust
// agent-client/src/connection/message.rs
use hbb_common::message_proto::{Message, MessageType};
pub struct MessageChannel {
stream: Box<dyn FramedStream>,
}
impl MessageChannel {
/// 发送消息
pub async fn send(&mut self, msg: &Message) -> ResultType<()> {
self.stream.send(msg).await?;
Ok(())
}
/// 接收消息
pub async fn recv(&mut self) -> ResultType<Option<Message>> {
match self.stream.next().await {
Some(Ok(msg)) => Ok(Some(msg)),
Some(Err(e)) => Err(e.into()),
None => Ok(None),
}
}
/// 发送聊天消息
pub async fn send_chat(&mut self, content: &str) -> ResultType<()> {
let msg = Message {
id: generate_id(),
type_: MessageType::MESSAGE_CHAT as i32,
payload: Some(ChatMessage {
content: content.to_string(),
timestamp: get_timestamp(),
}.into()),
..Default::default()
};
self.send(&msg).await
}
}
```
### 3. 连接状态指示
```rust
// agent-client/src/ui/status_bar.rs
use hbb_common::rendezvous_proto::PunchHole;
pub struct ConnectionIndicator {
state: ConnectionState,
last_heartbeat: Option<DateTime<Utc>>,
}
impl Render for ConnectionIndicator {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let status = match self.state {
ConnectionState::Connected => Status::Online,
ConnectionState::Relay => Status::Relay, // 中继模式
ConnectionState::Connecting => Status::Connecting,
ConnectionState::Disconnected => Status::Offline,
};
let color = match status {
Status::Online => cx.theme().colors.success,
Status::Relay => cx.theme().colors.warning,
Status::Connecting => cx.theme().colors.info,
Status::Offline => cx.theme().colors.error,
};
div()
.flex()
.items_center()
.gap_2()
.child(
div()
.w_3()
.h_3()
.rounded_full()
.bg(color)
)
.child(match status {
Status::Online => "已连接 (P2P)",
Status::Relay => "已连接 (中继)",
Status::Connecting => "连接中...",
Status::Offline => "未连接",
})
}
}
```
## 文件传输功能
### 文件分片传输
```rust
// agent-client/src/connection/file_transfer.rs
use hbb_common::message_proto::{FileTransfer, FileTransferAction};
const CHUNK_SIZE: usize = 64 * 1024; // 64KB 分片
pub struct FileTransferManager {
upload_jobs: HashMap<String, UploadJob>,
download_jobs: HashMap<String, DownloadJob>,
}
pub struct UploadJob {
file: File,
path: PathBuf,
offset: i64,
peer_id: String,
}
impl FileTransferManager {
/// 上传文件(支持断点续传)
pub async fn upload(&mut self, path: &Path, peer_id: &str) -> ResultType<()> {
let file = File::open(path).await?;
let metadata = file.metadata().await?;
let file_name = path.file_name().unwrap().to_string_lossy();
// 检查断点续传
let offset = self.get_checkpoint(&peer_id, &file_name).unwrap_or(0);
let job = UploadJob {
file,
path: path.to_path_buf(),
offset,
peer_id: peer_id.to_string(),
};
// 开始分片传输
self.upload_with_offset(job).await
}
async fn upload_with_offset(&mut self, mut job: UploadJob) -> ResultType<()> {
job.file.seek(SeekFrom::Start(job.offset as u64)).await?;
let mut buffer = vec![0u8; CHUNK_SIZE];
let mut offset = job.offset;
while let Ok(n) = job.file.read(&mut buffer).await {
if n == 0 { break; }
// 发送分片
let chunk = FileTransfer {
id: job.peer_id.clone(),
action: FileTransferAction::FILE_TRANSFER_DATA as i32,
data: buffer[..n].to_vec(),
offset,
compressed: false,
};
self.send_chunk(&chunk).await?;
// 保存断点
self.save_checkpoint(&job.peer_id, &job.path, offset);
offset += n as i64;
}
// 传输完成
self.send_done(&job.peer_id).await?;
Ok(())
}
}
```
## 可复用代码
### 核心复用模块
| 模块 | 路径 | 用途 |
|------|------|------|
| **hbb_common** | `libs/hbb_common/` | 消息编解码、网络工具、配置管理 |
| **scrap** | `libs/scrap/` | 屏幕捕获、视频编码 |
| **enigo** | `libs/enigo/` | 键盘鼠标输入模拟 |
| **clipboard** | `libs/clipboard/` | 跨平台剪贴板同步 |
### 推荐集成方式
```toml
[dependencies]
hbb_common = { path = "vendors/qiming-rustdesk/libs/hbb_common" }
```
## 关键设计模式
1. **双协议支持**UDP 直连优先,失败降级 TCP/WebSocket 中继
2. **NAT 打洞**:根据 NAT 类型自动选择连接策略
3. **心跳保活**EMA 动态调整心跳间隔
4. **断点续传**:支持文件传输中断后继续
5. **分片传输**:大文件分块传输,提高可靠性
## 与本项目的集成点
| 功能 | 使用模块 | 说明 |
|------|----------|------|
| 双向通信 | `hbb_common::message_proto` | 消息编解码 |
| P2P 直连 | `rendezvous_mediator` | NAT 穿透 |
| 中继传输 | `relay_connection` | 数据中转 |
| 文件传输 | `file_transfer` | 分片传输 |
| 输入模拟 | `enigo` | 远程控制 |
| 屏幕捕获 | `scrap` | 远程桌面 |