输入关键词开始搜索

通信协议设计

03 — 云融(YunRong)· 通信协议详细设计文档

前置文档02-系统架构设计文档 设计思路00-设计思路文档

版本:v0.1 状态:已发布 最后更新:2025-01


1. 文档约定

1.1 字节序

  • 网络传输:大端序(Big-Endian / Network Byte Order)
  • 主机内存:本机序(由 htonl() / ntohl() 转换)

1.2 数据类型缩写

缩写含义C++ 类型字节数
u8无符号 8 位整数uint8_t1
u16无符号 16 位整数uint16_t2
u32无符号 32 位整数uint32_t4
u64无符号 64 位整数uint64_t8
strUTF-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 通用字段定义

字段类型必填说明
verint协议版本号。当前为 1。服务端收到不支持的版本返回 type:"error"
typestring帧类型标识符,见 §3.3
sequ32客户端发起的帧单调递增(从 1 开始,登录后重置)。服务端推送的帧使用服务端序列号(从 2^31 开始,区分客户端和服务端 seq 空间)
tsu64Unix 毫秒时间戳。由发送方在构造帧时填入
payloadobject具体数据,结构由 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"
    }
  }
}
字段说明
tokenJWT 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,服务端推送消息的 seqserver_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 Length04 Bytes从 Type 字段开始的帧总长度(即 1 + Payload 长度),大端序
Type41 Byte帧类型
Payload5变长数据载荷

4.2 帧类型定义

Type 值名称说明
0x01THUMBNAIL图片缩略图
0x02FILE_CHUNK文件传输数据块
0x03FILE_CHUNK_ACK文件块确认
0x04FILE_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 Algo1 Byte0x01 = SHA-256
Chunk Count2 Bytes总分块数
File Size8 Bytes文件总大小(字节)
Chunk Size4 Bytes每块大小(最后一块可能更小)
Name Len1 Byte文件名长度(≤255)
File NameName LenUTF-8 文件名
File Hash32 BytesSHA-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 Index2 Bytes块序号(从 0 开始)
Chunk Offset4 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 Count2 Bytes已收到的块数
Bitmapceil(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 ...         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
字段大小说明
Format1 Byte0x01 = JPEG, 0x02 = PNG, 0x03 = WebP
Width2 Bytes缩略图宽度(≤ 512px)
Height2 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_INTERVAL30 s服务端 Ping 发送间隔
IDLE_TIMEOUT90 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 MB1 MB最多 100 块,HTTP 并发 4 路
> 100 MB2 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 本地凭据存储

平台方案
WindowsCredentialManager API (CredWrite / CredRead)
Linuxlibsecret (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 BytesUTF-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.12025-01初稿