通信协议设计
03 — 云融(YunRong)· 通信协议详细设计文档
前置文档:02-系统架构设计文档 设计思路:00-设计思路文档
版本:v0.1 状态:已发布 最后更新:2025-01
1. 文档约定
1.1 字节序
- 网络传输:大端序(Big-Endian / Network Byte Order)
- 主机内存:本机序(由
htonl()/ntohl()转换)
1.2 数据类型缩写
| 缩写 | 含义 | C++ 类型 | 字节数 |
|---|---|---|---|
| u8 | 无符号 8 位整数 | uint8_t | 1 |
| u16 | 无符号 16 位整数 | uint16_t | 2 |
| u32 | 无符号 32 位整数 | uint32_t | 4 |
| u64 | 无符号 64 位整数 | uint64_t | 8 |
| str | UTF-8 字符串 | std::string | 变长(前缀长度) |
1.3 字符串编码
- 所有文本:UTF-8
- JSON 帧:UTF-8,不支持 BOM
- 长度前缀:2 字节大端序(最大 65535 字节的字符串)
2. 协议体系总览
┌─────────────────────────────────────────────────────────┐
│ 应用层协议 │
│ │
│ ┌──────────────────────┐ ┌─────────────────────────┐ │
│ │ JSON 文本帧 │ │ 二进制帧 │ │
│ │ (WebSocket Text) │ │ (WebSocket Binary) │ │
│ │ │ │ │ │
│ │ • 消息收发 │ │ • 文件分块传输 │ │
│ │ • 通知推送 │ │ • 缩略图传输 │ │
│ │ • 心跳 Ping/Pong │ │ • 大块二进制数据 │ │
│ │ • ACK 确认 │ │ │ │
│ │ • 状态同步 │ │ │ │
│ └──────────┬───────────┘ └────────────┬────────────┘ │
│ │ │ │
├─────────────┼───────────────────────────┼───────────────┤
│ │ WebSocket (RFC 6455) │ │
│ └─────────────┬─────────────┘ │
│ │ │
├───────────────────────────┼─────────────────────────────┤
│ │ │
│ TLS 1.3 (加密) │
│ │ │
├───────────────────────────┼─────────────────────────────┤
│ │ │
│ TCP (传输) │
└───────────────────────────┴─────────────────────────────┘
2.1 两种帧的选用规则
消息类型是文本或结构化数据 (JSON 可表达)?
├── 是 → JSON 文本帧
└── 否 (原始二进制、缩略图)?
└── 二进制帧
文件传输(数据块)不走 WebSocket 二进制帧,统一通过 HTTP REST + Range 分块传输。 详见 06-接口设计文档 §5。
| 场景 | 帧类型 | 理由 |
|---|---|---|
| 文本消息 | JSON 文本帧 | 内容本身是文本,JSON 自然表达 |
| 图片消息(元数据) | JSON 文本帧 | URL + 尺寸 + 缩略图 ID,都是结构化字段 |
| 图片消息(缩略图) | 二进制帧 | JPEG/PNG 原始字节,小数据量(≤200KB)适合 WS |
| 文件消息(元数据) | JSON 文本帧 | 文件名 + 大小 + Hash + URL |
| 文件传输(数据块) | HTTP REST | 利用 HTTP Range 断点续传、不占用 WS 带宽 |
| ACK / 心跳 / 通知 | JSON 文本帧 | 结构化控制信息 |
3. JSON 文本帧协议
3.1 基础帧结构
{
"ver": 1,
"type": "msg",
"seq": 12345,
"ts": 1704067200000,
"payload": { }
}
3.2 通用字段定义
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
ver | int | 是 | 协议版本号。当前为 1。服务端收到不支持的版本返回 type:"error" |
type | string | 是 | 帧类型标识符,见 §3.3 |
seq | u32 | 是 | 客户端发起的帧单调递增(从 1 开始,登录后重置)。服务端推送的帧使用服务端序列号(从 2^31 开始,区分客户端和服务端 seq 空间) |
ts | u64 | 是 | Unix 毫秒时间戳。由发送方在构造帧时填入 |
payload | object | 是 | 具体数据,结构由 type 决定 |
3.3 帧类型清单
3.3.1 客户端 → 服务端 (C→S)
| type | 用途 | 是否需要 ACK |
|---|---|---|
auth | 登录认证(Token) | ✅ |
msg | 发送消息 | ✅ |
msg_read | 标记会话已读 | ✅ |
msg_revoke | 撤回消息(2 分钟内) | ✅ |
msg_edit | 编辑消息 | ✅ |
pong | 心跳响应 | ❌ |
sync | 请求增量同步(拉取离线消息) | ✅ |
subscribe | 订阅通知频道 | ✅ |
注意:客户端不主动发送
ack帧。服务端→客户端推送路径依赖 TCP 可靠传输 +seq去重,不需要客户端回复 ACK。
3.3.2 服务端 → 客户端 (S→C)
| type | 用途 | 是否需要 ACK |
|---|---|---|
auth_ok / auth_fail | 认证结果 | ❌ (对 auth 的响应) |
msg | 推送消息 | ❌ (TCP + seq 去重) |
notify | 系统/任务通知 | ❌ (TCP + seq 去重) |
ping | 服务端心跳(检测死连接) | ❌ |
sync_data | 增量同步数据 | ❌ (对 sync 的响应) |
ack | 确认收到客户端消息 | ❌ |
error | 错误 | ❌ |
state_change | 其他用户状态变更 | ❌ |
3.4 各帧类型 payload 详细定义
3.4.1 auth(C→S)
{
"ver": 1,
"type": "auth",
"seq": 1,
"ts": 1704067200000,
"payload": {
"token": "eyJhbGciOi...",
"client_info": {
"platform": "windows",
"version": "0.1.0",
"device_id": "uuid-xxxx"
}
}
}
| 字段 | 说明 |
|---|---|
token | JWT Access Token,登录时从 REST API 获取 |
platform | "windows" 或 "linux" |
version | 客户端版本号 |
device_id | 设备唯一标识(UUID v4),用于多端区分 |
3.4.2 auth_ok / auth_fail(S→C)
// 成功
{
"ver": 1,
"type": "auth_ok",
"seq": 0,
"ts": 1704067200100,
"payload": {
"user_id": 1001,
"user_name": "张三",
"server_seq_start": 2147483648,
"expires_in": 7200,
"features": ["im", "task", "file_transfer"]
}
}
// 失败
{
"ver": 1,
"type": "auth_fail",
"seq": 0,
"ts": 1704067200100,
"payload": {
"code": 401,
"reason": "token_expired"
}
}
3.4.3 msg(双向)
{
"ver": 1,
"type": "msg",
"seq": 42,
"ts": 1704067205000,
"payload": {
"msg_id": "uuid-or-snowflake",
"conv_id": 2001,
"conv_type": "private",
"content": {
"type": "text",
"text": "你好,请查收这份报告"
},
"quote_msg_id": null,
"mentions": []
}
}
| 字段 | 说明 |
|---|---|
msg_id | 消息全局唯一 ID(服务端生成,客户端发送时可为 null,服务端填充后广播) |
conv_id | 会话 ID |
conv_type | "private" 或 "group" |
content.type | "text" / "image" / "file" / "system" |
quote_msg_id | 引用的消息 ID(可选) |
mentions | [user_id, ...] @提及的用户列表 |
content.type 变体
// 图片消息
{
"type": "image",
"url": "https://server/files/img_001.webp",
"thumbnail_id": "thumb_001",
"width": 1920,
"height": 1080,
"size": 245760
}
// 文件消息
{
"type": "file",
"name": "Q3季度报告.pdf",
"url": "https://server/files/doc_042.pdf",
"size": 1048576,
"hash": "sha256:abc123def456...",
"ext": ".pdf"
}
// 系统消息
{
"type": "system",
"subtype": "group_create",
"actor_id": 1001,
"extra": { "group_name": "项目讨论组" }
}
3.4.4 ack(单向确认,S→C 仅用于确认客户端消息)
{
"ver": 1,
"type": "ack",
"seq": 0,
"ts": 1704067205100,
"payload": {
"ack_seq": 42,
"status": "ok"
}
}
| 字段 | 说明 |
|---|---|
ack_seq | 被确认的帧的 seq |
status | "ok" — 成功; "duplicate" — 重复消息(已收到过); "invalid" — 帧解析失败 |
3.4.5 ping / pong(服务端主导心跳)
// S→C(服务端每 30s 发送)
{ "ver": 1, "type": "ping", "seq": 0, "ts": 1704067230000, "payload": {} }
// C→S(客户端收到后立即回复)
{ "ver": 1, "type": "pong", "seq": 0, "ts": 1704067230050, "payload": {} }
- 服务端每 30 秒发送一次
ping(RFC 6455 建议由服务端主导心跳,更利于检测死连接和回收资源) - 客户端收到后必须立即回复
pong - 服务端超过 90 秒未收到任何帧(包括
pong),主动关闭连接 - 客户端超过 90 秒未收到任何帧(包括
ping),判定连接断开,触发重连
3.4.6 notify(S→C)
{
"ver": 1,
"type": "notify",
"seq": 2147483700,
"ts": 1704067300000,
"payload": {
"notify_id": "n_12345",
"notify_type": "task_assign",
"title": "新的审批任务",
"body": "张三 邀请你审批「请假申请」",
"action_url": "/tasks/5678",
"priority": "normal"
}
}
| 字段 | 说明 |
|---|---|
notify_type | "task_assign", "task_done", "task_reject", "system_announce" |
priority | "low", "normal", "high" — high 优先级的通知在客户端上弹窗 + 声音 |
3.4.7 sync / sync_data(拉取离线消息)
server_seq是服务端为每条消息分配的全局单调递增序列号(跨所有会话),用作增量同步游标。server_seq与帧级别的seq(用于 ACK 匹配)是同一个字段——客户端消息的seq由客户端分配用于 ACK,服务端推送消息的seq即server_seq。
// C→S (上线/重连后请求同步)
{
"ver": 1, "type": "sync", "seq": 100, "ts": 1704067400000,
"payload": {
"since_seq": 37, // 客户端已有的最大 server_seq
"limit": 200
}
}
// S→C
{
"ver": 1, "type": "sync_data", "seq": 0, "ts": 1704067400100,
"payload": {
"has_more": false,
"messages": [ /* server_seq > 37 的消息 */ ],
"notifications": [ /* 离线期间的通知 */ ],
"end_seq": 55 // 本次返回的最大 server_seq
}
}
since_seq:客户端本地最大的server_seq,服务端返回所有server_seq > since_seq的消息limit:单次最多返回 200 条has_more:true 表示还有更多,客户端应继续sync(带上新的since_seq = end_seq)
3.4.8 msg_read(C→S)
{
"ver": 1, "type": "msg_read", "seq": 101, "ts": 1704067500000,
"payload": {
"conv_id": 2001,
"read_to_seq": 2147483695
}
}
告知服务端”此会话中 seq ≤ read_to_seq 的消息都已读”。服务端转通知对方”已读”。
3.4.8b msg_revoke(C→S,撤回消息)
{
"ver": 1, "type": "msg_revoke", "seq": 102, "ts": 1704067510000,
"payload": {
"conv_id": 2001,
"msg_id": "m_abc123"
}
}
- 仅消息发送方可在 2 分钟 内撤回
- 服务端收到后:① 将消息
content_body标记为{"revoked": true}② 转发msg_revoke给会话所有成员 - 客户端收到后:将对应消息气泡替换为”你撤回了一条消息”/“对方撤回了一条消息”
3.4.8c msg_edit(C→S,编辑消息)
{
"ver": 1, "type": "msg_edit", "seq": 103, "ts": 1704067520000,
"payload": {
"conv_id": 2001,
"msg_id": "m_abc123",
"new_body": "修正后的消息内容"
}
}
- 仅文本消息可编辑,服务端保留编辑历史(
edited_at字段) - 服务端转发
msg_edit给会话所有成员 - 客户端收到后:更新消息气泡内容,显示”(已编辑)“标记
3.4.9 state_change(S→C)
{
"ver": 1, "type": "state_change", "seq": 0, "ts": 1704067600000,
"payload": {
"user_id": 1002,
"new_state": "online",
"device": "windows"
}
}
3.4.10 error(S→C)
{
"ver": 1, "type": "error", "seq": 0, "ts": 1704067700000,
"payload": {
"ref_seq": 55,
"code": 4002,
"message": "会话不存在或无权访问"
}
}
3.5 错误码体系
| 范围 | 类别 | 示例 |
|---|---|---|
| 1000–1999 | 认证错误 | 1001 Token 过期, 1002 Token 无效, 1003 无权限 |
| 2000–2999 | 消息错误 | 2001 会话不存在, 2002 消息过长, 2003 内容违规 |
| 3000–3999 | 同步错误 | 3001 同步范围过大, 3002 增量已丢失(需全量) |
| 4000–4999 | 协议错误 | 4001 JSON 解析失败, 4002 缺少必填字段, 4003 不支持的帧类型 |
| 5000–5999 | 服务端内部 | 5001 内部错误(请重试), 5002 服务过载(限流) |
| 9000–9999 | 文件传输错误 | 9001 文件不存在, 9002 分块序号越界, 9003 Hash 不匹配 |
4. 二进制帧协议
4.1 帧结构
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type (8) | Payload Data ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 字段 | 偏移 | 大小 | 说明 |
|---|---|---|---|
| Total Length | 0 | 4 Bytes | 从 Type 字段开始的帧总长度(即 1 + Payload 长度),大端序 |
| Type | 4 | 1 Byte | 帧类型 |
| Payload | 5 | 变长 | 数据载荷 |
4.2 帧类型定义
| Type 值 | 名称 | 说明 |
|---|---|---|
0x01 | THUMBNAIL | 图片缩略图 |
0x02 | FILE_CHUNK | 文件传输数据块 |
0x03 | FILE_CHUNK_ACK | 文件块确认 |
0x04 | FILE_META | 文件元数据(文件名/Hash/块数) |
4.3 FILE_META (0x04)
在文件传输开始前,先发送元数据帧,告知对方文件信息和分块策略。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type=0x04 | Hash Algo | Chunk Count |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| File Size (u64) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Chunk Size (u32) | Name Len | File Name ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| File Hash (32 bytes for SHA-256) |
| |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 字段 | 大小 | 说明 |
|---|---|---|
| Hash Algo | 1 Byte | 0x01 = SHA-256 |
| Chunk Count | 2 Bytes | 总分块数 |
| File Size | 8 Bytes | 文件总大小(字节) |
| Chunk Size | 4 Bytes | 每块大小(最后一块可能更小) |
| Name Len | 1 Byte | 文件名长度(≤255) |
| File Name | Name Len | UTF-8 文件名 |
| File Hash | 32 Bytes | SHA-256 哈希(根据 Hash Algo) |
4.4 FILE_CHUNK (0x02)
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type=0x02 | Chunk Index (u16) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Chunk Offset (u32) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Chunk Data ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 字段 | 大小 | 说明 |
|---|---|---|
| Chunk Index | 2 Bytes | 块序号(从 0 开始) |
| Chunk Offset | 4 Bytes | 块在文件中的字节偏移 |
| Chunk Data | 变长 | 块数据(≤ Chunk Size) |
4.5 FILE_CHUNK_ACK (0x03)
接收方每收到 N 个块(默认每 10 块)发送一次批量确认。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type=0x03 | Received Count (u16) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Bitmap (variable length, 1 bit per chunk: 1=received) ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 字段 | 大小 | 说明 |
|---|---|---|
| Received Count | 2 Bytes | 已收到的块数 |
| Bitmap | ceil(ChunkCount/8) Bytes | 位图,bit=1 表示该块已收到 |
发送方根据 Bitmap 只重传缺失的块,避免全量重传。
4.6 THUMBNAIL (0x01)
// 通过 JSON 帧先发元数据,指向一个 thumbnail_id
// 然后通过二进制帧发送缩略图本身的 JPEG/PNG 数据
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Type=0x01 | Format (8) | Width (u16) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Height (u16) | Image Data ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 字段 | 大小 | 说明 |
|---|---|---|
| Format | 1 Byte | 0x01 = JPEG, 0x02 = PNG, 0x03 = WebP |
| Width | 2 Bytes | 缩略图宽度(≤ 512px) |
| Height | 2 Bytes | 缩略图高度(≤ 512px) |
| Image Data | 变长 | 图片字节流 |
5. ACK 与重传机制(详细状态机)
适用范围:ACK/重传仅用于客户端→服务端的消息发送路径。服务端→客户端的推送消息依赖 TCP 可靠传输 +
seq去重,不要求客户端回复 ACK。
5.1 数据结构
// 客户端(发送方)维护的待确认表
struct PendingFrame {
uint32_t seq;
uint64_t send_time_ms; // 首次发送时间
uint8_t retry_count; // 已重试次数 (0-3)
uint64_t next_retry_ms; // 下次重试时间
std::string raw_json; // 原始帧 JSON(重发用)
std::function<void(bool, std::string)> callback; // 最终结果回调
};
// 客户端(接收方)维护:仅用于去重,不发送 ACK
uint32_t last_recv_seq = 0; // 最后收到的消息 seq
// 服务端维护
uint32_t last_ack_seq = 0; // 批量 ACK 用(服务端→客户端方向)
5.2 发送方状态机
┌───────────────┐
send(msg) ───► │ PENDING │
│ (等待 ACK) │
└───┬───────┬───┘
│ │
收到 ACK ──┘ │ 超时 (5s) & retry < 3
│ │
┌────▼───┐ │
│ DONE │ │──► 重发 + retry++
└────────┘ │
│ 超时 & retry >= 3
┌────▼───────┐
│ FAILED │
│ (通知上层) │
└────────────┘
5.3 定时器扫描逻辑
// 每 1 秒执行一次
void ConnectionManager::tickRetransmit() {
auto now = steady_clock::now();
for (auto& [seq, frame] : pending_frames_) {
if (now >= frame.next_retry_ms) {
if (frame.retry_count >= MAX_RETRIES) {
frame.callback(false, "max retries exceeded");
pending_frames_.erase(seq);
} else {
sendRaw(frame.raw_json);
frame.retry_count++;
frame.next_retry_ms = now + retryInterval(frame.retry_count);
}
}
}
}
// 重试间隔:1s, 2s, 4s
std::chrono::milliseconds retryInterval(int retryCount) {
return std::chrono::milliseconds(1000 * (1 << retryCount));
}
5.4 服务端接收行为(收到客户端消息后 ACK)
void Protocol::onFrameReceived(const json& frame) {
uint32_t seq = frame["seq"];
// 1. 去重检查
if (seq <= last_recv_seq_ && seq != 0) {
sendAck(seq, "duplicate"); // 重复帧,告知客户端
return;
}
last_recv_seq_ = seq;
// 2. 发 ACK(快速路径,不等待业务处理完成)
if (seq > 0) {
scheduleBatchAck(seq);
}
// 3. 分发处理(存储 + 路由到目标用户)
dispatch(frame);
}
5.5 客户端接收行为(收到服务端推送消息后,不 ACK)
void Protocol::onPushReceived(const json& frame) {
uint32_t seq = frame["seq"];
// 仅做去重,不回复 ACK
if (seq <= last_recv_seq_ && seq != 0) {
return; // TCP 重传导致的重复帧
}
last_recv_seq_ = seq;
// 分发到 Worker Pool 解码 → GUI 更新 + DB 持久化
dispatchToWorkerPool(frame);
}
5.6 批量 ACK 优化(服务端→客户端方向)
为减少服务端发出的 ACK 帧数量,服务端可以批量确认(在上一条 ACK 发出后的 100ms 内):
void Protocol::scheduleBatchAck(uint32_t seq) {
pending_ack_seqs_.push_back(seq);
if (!batch_ack_timer_running_) {
batch_ack_timer_running_ = true;
// 100ms 后发送批量 ACK(包含这 100ms 内所有 seq 的最大值)
timer_->schedule(100ms, [this] {
uint32_t maxSeq = *std::max_element(pending_ack_seqs_.begin(),
pending_ack_seqs_.end());
sendAck(maxSeq);
pending_ack_seqs_.clear();
batch_ack_timer_running_ = false;
});
}
}
6. 序列号管理
6.1 序列号空间划分
客户端序列号空间:1 .. 2,147,483,647 (2^31 - 1)
服务端序列号空间:2,147,483,648 .. 4,294,967,295
通过 auth_ok 帧中的 server_seq_start 告知客户端服务端的起始 seq。
为什么分开:避免客户端和服务端的 seq 碰撞。接收方可以仅凭 seq 判断消息来源而无需额外字段。
6.2 客户端 seq 生命周期
登录成功 → seq = 1
每次 sendMessage() → seq++
重连 → seq 不重置(继续递增)
登出 / Token 过期 → seq 失效
重新登录 → seq = 1(从 1 重新开始,服务端根据 msg_id 去重)
6.3 服务端 seq 管理
服务端为每个连接维护独立的递增计数器,从 server_seq_start 开始。推送消息的 seq 是该连接内的序号,用于:
- 客户端检测消息丢失(seq 不连续 → 请求 sync)
- 已读回执的定位(“已读到 seq X”)
7. 心跳机制(服务端主导)
按 RFC 6455 建议,由服务端主动发送 Ping 帧。服务端比客户端更需要检测死连接以回收资源(goroutine、内存、文件描述符)。
7.1 参数配置
| 参数 | 默认值 | 说明 |
|---|---|---|
PING_INTERVAL | 30 s | 服务端 Ping 发送间隔 |
IDLE_TIMEOUT | 90 s | 双方:超时未收到任何帧则判定断开 |
7.2 时序
Server Client
│ │
│──── Ping ────────────────────►│
│◄─── Pong ────────────────────│
│ │
│ ... 30s ... │
│ │
│──── Ping ─── X (丢包) │
│ ... 60s ... │
│──── Ping ─── X (再次丢包) │
│ ... 累计 90s 无数据 ... │
│ │
│ 服务端: 90s 未收到 Pong │
│ → 主动关闭连接 │
│ │
│ 客户端: 90s 未收到 Ping │
│ → 判定断开,触发重连 │
7.3 双向超时检测
- 服务端:90 秒内未收到任何帧(含
pong),关闭连接并清理Hub中的Client - 客户端:90 秒内未收到任何帧(含
ping),将连接状态设为RECONNECTING并启动重连
8. 断线重连机制
8.1 重连状态机
CONNECTED ──(检测到断开)──► RECONNECTING
│
┌───────────┴───────────┐
│ │
(重连成功) (重试 N 次失败)
│ │
▼ ▼
CONNECTED ERROR
(重新 auth + sync) (通知用户手动操作)
8.2 退避算法
std::chrono::milliseconds ReconnectStrategy::nextDelay(int attempt) {
// attempt: 1 2 3 4 5 6 7 8 9 10
// delay(ms): 1000 2000 4000 8000 16000 30000 60000 60000 60000 60000
if (attempt <= 4) {
return std::chrono::milliseconds(1000 * (1 << (attempt - 1)));
} else if (attempt <= 6) {
return std::chrono::milliseconds(attempt == 5 ? 30000 : 60000);
} else {
return std::chrono::milliseconds(60000); // 封顶 60s
}
}
8.3 重连后的恢复流程
1. TCP + TLS 握手
2. WebSocket Upgrade
3. 发送 auth 帧
4. 收到 auth_ok
5. 检查 last_recv_seq vs auth_ok 中的 server_seq_start
├── 一致 → 无丢失,直接进入 CONNECTED
└── 不一致 → 发送 sync 帧拉取丢失的消息
6. 重发 pending_frames_ 中所有未 ACK 的消息
7. 标记所有离线期间发送失败的消息
8. 状态 → CONNECTED
9. 文件传输协议
文件上传/下载统一通过 HTTP REST 通道(
multipart/form-data上传 +Range分块下载),不占用 WebSocket 长连接带宽。 详细 API 定义见 06-接口设计文档 §5。
9.1 上传流程(HTTP REST)
Client Server
│ │
│── POST /api/v1/files/check │
│ { hash: "sha256:..." } │
│ │
│◄── { exists: false, upload_id: "u_xxx" } │
│ │
│── POST /api/v1/files/upload/u_xxx/chunks/0 │
│ Content-Type: application/octet-stream │
│ (body: 1MB 原始字节) │
│◄── { ok: true, chunk: 0 } │
│ │
│── POST .../chunks/1 ───────────────────────►│
│── POST .../chunks/2 ───────────────────────►│
│ ... (可并发 4-6 块) │
│ │
│── POST /api/v1/files/upload/u_xxx/complete │
│◄── { url: "https://...", hash_verified } │
│ │
│── [WS] { type:"msg", payload:{ type:"file", │
│ url:"https://..." } } │
9.2 分块策略
| 文件大小 | 块大小 | 说明 |
|---|---|---|
| ≤ 1 MB | 不分块 | 单个 PUT 上传 |
| 1 MB – 100 MB | 1 MB | 最多 100 块,HTTP 并发 4 路 |
| > 100 MB | 2 MB | 最多 500 块(2GB 文件) |
9.3 断点续传
客户端本地维护传输状态(与 04 号数据库设计中的 file_transfers 表对应):
struct TransferState {
std::string file_path;
std::string file_hash;
uint64_t file_size;
uint32_t chunk_size;
uint16_t total_chunks;
std::vector<bool> received_chunks;
uint16_t next_chunk_index;
TransferDirection direction;
};
重连或恢复后,检查 received_chunks 位图,跳过已完成的块,从 next_chunk_index 继续。
10. 流量控制与限速
10.1 消息流控
防止服务端推送过快导致客户端积压:
客户端维护: pending_msg_count (已收到但未展示的消息数)
当 pending_msg_count > 100:
→ 发送 { type: "flow_control", action: "slow_down" }
→ 服务端降速(合并通知、降低推送频率)
当 pending_msg_count < 20:
→ 发送 { type: "flow_control", action: "resume" }
→ 服务端恢复正常推送
10.2 文件传输限速
class RateLimiter {
size_t bytes_per_second_;
size_t tokens_; // 当前可用令牌
steady_clock::time_point last_refill_;
// 令牌桶算法
bool tryConsume(size_t bytes) {
refill();
if (tokens_ >= bytes) {
tokens_ -= bytes;
return true;
}
return false; // 需要等待
}
};
默认不限速,用户可在设置中手动启用(如”上传限速 10MB/s”)。
11. TLS 安全配置
11.1 最低要求
| 参数 | 配置 |
|---|---|
| 最低 TLS 版本 | TLS 1.2 |
| 推荐 TLS 版本 | TLS 1.3 |
| 密钥交换 | ECDHE (前向安全性) |
| 加密套件 (TLS 1.3) | TLS_AES_256_GCM_SHA384, TLS_AES_128_GCM_SHA256, TLS_CHACHA20_POLY1305_SHA256 |
| 证书验证 | 强制验证(不接受自签名证书,除非开发模式) |
| SNI | 支持(Server Name Indication) |
11.2 Token 安全
登录流程:
1. HTTPS POST /api/auth/login { username, password }
2. 返回 { access_token (短期, 2h), refresh_token (长期, 7d) }
3. access_token 用于 WS 连接 auth 帧和所有 REST 请求的 Bearer header
4. access_token 过期前 5 分钟自动用 refresh_token 续期
5. refresh_token 过期 → 跳转登录页面
11.3 本地凭据存储
| 平台 | 方案 |
|---|---|
| Windows | CredentialManager API (CredWrite / CredRead) |
| Linux | libsecret (D-Bus Secret Service) |
不将 Token 明文写入文件或 SQLite。
12. Mock Server 设计(协议测试用)
12.1 架构
┌─────────────────────────────────────────────┐
│ Mock Server │
│ (本地进程,可执行文件 mock_server) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
│ │ WS Server │ │HTTP Server│ │File Server│ │
│ │ (port 9001)│ │(port 9002)│ │(port 9003)│ │
│ └──────────┘ └──────────┘ └───────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Scenario Engine │ │
│ │ • 预设场景脚本 (JSON) │ │
│ │ • 延迟模拟 / 丢包模拟 / 错误注入 │ │
│ │ • 协议合规性校验 │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
12.2 测试场景示例
{
"scenario": "normal_chat",
"steps": [
{ "recv": "auth", "send": "auth_ok", "delay_ms": 50 },
{ "send": "msg", "recv": "ack", "delay_ms": 100 },
{ "send": "msg", "recv": "ack", "delay_ms": 100 },
{ "send": "notify", "recv": "ack", "delay_ms": 50 }
]
}
{
"scenario": "network_loss",
"steps": [
{ "recv": "auth", "send": "auth_ok" },
{ "recv": "msg", "action": "drop" }, // 模拟丢包
{ "recv": "msg", "send": "ack" }, // 重发的收到
{ "expect": "retransmit_count == 1" } // 验证只重传了 1 次
]
}
12.3 Mock Server 命令接口
./mock_server --scenario normal_chat.json --ws-port 9001 --http-port 9002 -v
--scenario:指定场景文件--ws-port/--http-port:端口配置-v:详细日志模式(打印所有收发的帧)- 支持运行时通过 HTTP
POST /scenario/load热加载新场景
13. 协议扩展机制
13.1 版本协商
客户端在 auth 帧中声明 ver。服务端:
| 客户端 ver | 服务端行为 |
|---|---|
| == 服务端 ver | 正常通信 |
| < 服务端 ver | 降级兼容(使用旧版本支持的字段子集)或返回 auth_fail + “版本过旧请升级” |
| > 服务端 ver | 返回 auth_fail + “服务端版本过低” |
13.2 扩展字段
所有 JSON 帧的 payload 允许携带未知字段(additionalProperties: true),接收方应忽略不认识的字段。这保证前后向兼容——老客户端和新服务端可以互操作。
13.3 新帧类型注册
新增帧类型需在本文档中注册,并遵循命名规范:
- 客户端发起:小写下划线(
file_check,msg_read) - 服务端发起:小写下划线(
state_change,sync_data) - 保留前缀:
x_用于实验性扩展,生产环境禁止使用
附录 A — 帧大小限制
| 帧类型 | 最大尺寸 | 说明 |
|---|---|---|
| JSON 文本帧 | 64 KB | 单帧 JSON 字符串上限(超出分片或多个帧) |
| 文本消息体 | 10 KB | 单条消息文本上限 |
| 二进制帧 | 2 MB | 单帧 Payload 上限(≈ Chunk Size) |
| 文件名 | 255 Bytes | UTF-8 编码 |
附录 B — 推荐阅读
- RFC 6455 — The WebSocket Protocol
- RFC 8446 — TLS 1.3
- RFC 7230 — HTTP/1.1 Message Syntax and Routing
- RFC 7233 — HTTP Range Requests
- Qt WebSocket Documentation —
QWebSocket/QNetworkAccessManager
附录 C — 文档修订记录
| 版本 | 日期 | 作者 | 变更说明 |
|---|---|---|---|
| v0.1 | 2025-01 | — | 初稿 |