11 KiB
11 KiB
Tinode 消息协议与机制总结
0. WebSocket 传输层协议
连接建立
HTTP GET / → WebSocket Upgrade (gorilla/websocket)
→ readLoop() + writeLoop() 两个 goroutine
帧格式
所有数据通过 WebSocket Text Message 帧传输(websocket.TextMessage),payload 为 JSON 字符串。
唯一的例外:心跳探测时客户端发送单字节 0x31(字符 '1'),服务端回复单字节 0x30(字符 '0')。
输入(客户端→服务端)
WebSocket Text Frame
→ raw []byte
→ json.Unmarshal → ClientComMessage
→ dispatch() → 路由到对应 handler
队列限制:sendQueueLimit = 128,队列满则丢弃并断开连接。
输出(服务端→客户端)
ServerComMessage
→ json.Marshal → []byte
→ 写入 sess.send channel (buffered, size=128)
→ writeLoop() select 消费
→ ws.WriteMessage(websocket.TextMessage, data)
WriteLoop 调度(优先级从高到低):
sess.send— 消息队列(*ServerComMessage或[]*ServerComMessage批量)sess.bkgTimer— 后台超时定时器sess.stop— 停止信号sess.detach— 取消订阅ticker— Ping 帧(每 15s 发送一次)
Keep-Alive
| 参数 | 值 | 说明 |
|---|---|---|
writeWait |
10s | 写超时 |
pongWait |
idleSessionTimeout |
期望收到 Pong 的时间(当前默认 45s) |
pingPeriod |
15s | WebSocket Ping 间隔 |
| 心跳探测 | WebSocket Ping/Pong | 当前 IM 长连接保活以协议层 Ping/Pong 为准;业务 note kp/kpa/kpv 不能替代 Pong |
缓冲区设置
| 参数 | 值 | 说明 |
|---|---|---|
| ReadBufferSize | 1024 | 读缓冲区 |
| WriteBufferSize | 1024 | 写缓冲区 |
| Compression | 可配置 | RFC 7692.4 压缩 |
| CheckOrigin | true |
允许任何 Origin |
1. 消息传输协议
默认传输:WebSocket (WS)
客户端 ←→ WebSocket Server (:6060) ←→ Hub ←→ Topic ←→ Sessions
备用传输:
- Long Polling (HTTP) — 同一套消息结构,不同传输层
- gRPC — 内部服务间通信(
hdl_grpc.go,端口:16060)
消息格式
所有消息通过 JSON 序列化,在 WebSocket 中以文本帧传输。
C2S(客户端→服务端)消息类型
| 消息 | 结构体 | 说明 |
|---|---|---|
{hi} |
MsgClientHi |
握手,上报必填企业码及 UA/版本/设备ID/语言 |
{acc} |
MsgClientAcc |
创建/更新用户账号 |
{login} |
MsgClientLogin |
登录认证 |
{sub} |
MsgClientSub |
订阅 Topic(加入群/私聊) |
{leave} |
MsgClientLeave |
取消订阅 |
{pub} |
MsgClientPub |
发布消息(发送聊天内容) |
{get} |
MsgClientGet |
查询 Topic 状态/历史消息 |
{set} |
MsgClientSet |
更新 Topic 元数据/订阅权限 |
{del} |
MsgClientDel |
删除消息/Topic/订阅 |
{note} |
MsgClientNote |
客户端通知(已读/已收/打字) |
S2C(服务端→客户端)消息类型
| 消息 | 结构体 | 说明 |
|---|---|---|
{ctrl} |
MsgServerCtrl |
控制响应(确认/错误,含 code/text) |
{data} |
MsgServerData |
消息数据(聊天内容投递) |
{meta} |
MsgServerMeta |
元数据更新(描述/订阅/标签/凭证) |
{pres} |
MsgServerPres |
Presence 通知(在线/离线/权限变更) |
{info} |
MsgServerInfo |
非权威通知(已收/已读/打字/通话) |
核心消息结构
发布消息 ({pub}):
{
"pub": {
"id": "client_msg_id",
"topic": "grp_ABC123",
"head": {"key": "value"},
"content": {"txt": "Hello", "fmt": "..."}
}
}
服务端投递 ({data}):
{
"data": {
"topic": "grp_ABC123",
"from": "usr_XYZ",
"ts": "2026-08-04T00:00:00Z",
"seq": 12345,
"head": {},
"content": {"txt": "Hello"}
}
}
控制响应 ({ctrl}):
{
"ctrl": {
"id": "client_msg_id",
"code": 200,
"text": "OK",
"ts": "2026-08-04T00:00:00Z"
}
}
2. 消息类型(按内容语义)
Tinode 的消息内容(content 字段)是 Drafty 格式 — 一种轻量级富文本协议。
Drafty 消息类型
| 类型 | 说明 |
|---|---|
text |
纯文本消息 |
image |
图片(含缩略图、文件引用) |
video |
视频 |
audio |
音频 |
file |
通用文件附件 |
contact |
联系人卡片 |
location |
位置信息 |
payment |
支付信息 |
url |
URL 预览 |
exif |
媒体元信息(EXIF) |
系统消息
| 类型 | 说明 |
|---|---|
| 通知 | 用户被加入/踢出/权限变更 |
| 邀请 | 用户被邀请加入群聊 |
| 标题变更 | Topic 标题修改 |
Access Mode 权限位
消息操作受 8 个权限位控制(store/types/types.go):
| 位 | 值 | 名称 | 说明 |
|---|---|---|---|
| J | 1 | ModeJoin | 允许加入/订阅 |
| R | 2 | ModeRead | 接收广播消息 ({data}, {info}) |
| W | 4 | ModeWrite | 发送消息 ({pub}) |
| P | 8 | ModePres | 接收 Presence 通知 |
| A | 16 | ModeApprove | 批准/踢出成员 |
| S | 32 | ModeShare | 邀请新成员 |
| D | 64 | ModeDelete | 硬删除消息 |
| O | 128 | ModeOwner | 完全访问权 |
有效权限 = modeWant & modeGiven,两者取交集。
3. 消息状态
消息状态通过 seqID / recvID / readID / delID 四个序列号跟踪:
核心状态字段
| 字段 | 说明 | 存储位置 |
|---|---|---|
seqId |
消息全局递增 ID(Topic 级别) | Topic.lastId + DB |
recvID |
客户端确认"已收到"的最大 seqId | perUserData.recvID |
readID |
客户端确认"已读"的最大 seqId | perUserData.readID |
delID |
最新删除操作的 ID | perUserData.delID |
状态流转
发送方 pub ─→ 服务端分配 seqId ─→ 写 DB ─→ broadcast to sessions
│
▼
客户端收到 {data}
│
发送 {note: "recv"} ─→ 更新 recvID
│
用户打开消息 → 发送 {note: "read"} ─→ 更新 readID
{note} 通知
客户端通过 {note} 消息上报状态:
{"note": {"topic": "grp_X", "what": "recv", "seq": 1234}} // 已收到
{"note": {"topic": "grp_X", "what": "read", "seq": 1234}} // 已读
{"note": {"topic": "grp_X", "what": "kp"}} // 打字中 (key pressed)
服务端收到后更新 perUserData.recvID / readID,并通过 {pres} 广播给其他订阅者。
{pres} Presence 通知
{
"pres": {
"topic": "grp_X",
"what": "read|recv|on|off|msg|del|acs",
"src": "usr_XYZ",
"seq": 1234,
"clear": 56
}
}
what 值 |
说明 |
|---|---|
on / off |
用户上线/下线 |
msg |
新消息到达(seqId) |
recv |
用户已收到消息(seqId) |
read |
用户已读消息(seqId) |
del |
消息被删除(clear/delSeq) |
acs |
权限变更 |
Topic 描述中的状态
在 {meta.desc} 中携带 Topic 级别的状态:
{
"meta": {
"desc": {
"seq": 12345, // 最大消息 ID
"read": 12300, // 当前用户的已读 ID
"recv": 12340, // 当前用户的已收 ID
"clear": 56 // 当前用户的删除 ID
}
}
}
4. 消息加密
传输层加密
| 层级 | 技术 | 状态 |
|---|---|---|
| WebSocket | TLS (WSS) — 通过配置 tls_enabled: true + 证书 |
✅ 支持 |
| gRPC | TLS — 可选配置 | ✅ 支持 |
| HTTP Long Polling | TLS (HTTPS) | ✅ 支持 |
当前默认:明文 WebSocket(无 TLS),可通过配置开启 WSS。
端到端加密 (E2E)
Tinode 当前不支持端到端加密。
- 消息在服务端以明文存储(DB 中
messages.content字段为 JSON 明文) - 消息广播时也是明文 JSON
- 服务端可以读取和搜索所有消息内容(
im_message_index.search_text全文索引)
认证加密
- 登录认证通过
{login}+secret字段传输,secret 在服务端做哈希存储 - WebSocket 握手后通过
{hi}+{login}建立身份
5. 同步规则
5.1 订阅同步
客户端 {sub, topic: "grp_X", get: {what: "desc+data+sub"}}
→ 服务端查找/创建 Topic
→ 注册 Session 到 Topic.sessions
→ 返回 {meta}(desc + sub + 历史 data)
→ 后续新消息通过 {data} 实时推送
5.2 消息同步
拉取历史:
客户端 {get, topic: "grp_X", get: {what: "data", data: {since: 100, before: 200, limit: 50}}}
→ 服务端从 DB 查询 messages
→ 返回 {data} 消息列表
实时推送:
发送方 {pub} → 服务端写 DB (seqId++) → 广播到所有订阅 Sessions
广播机制(Redis 改造后):
Pub → DB Save → Redis Publish(topic:{name}:broadcast, msg)
→ 所有节点收到 Pub/Sub
→ 每个节点只遍历本地 attached sessions 投递
5.3 消息去重
- 客户端发送消息时带
id字段(客户端生成的消息 ID) - 服务端在
{ctrl}响应中原样返回id,客户端据此匹配请求/响应 - 服务端内部用
seqId作为唯一递增 ID
5.4 离线消息
- 用户断线重连后,通过
{get}请求since参数拉取缺失消息 since= 本地最大seqId,服务端返回seqId > since的所有消息- 支持
ranges参数批量请求多个区间
5.5 已读/已收同步
- 客户端收到
{data}后发送{note: "recv"}确认 - 用户查看消息后发送
{note: "read"}确认 - 服务端更新
perUserData.recvID/readID - 通过
{pres}广播给其他订阅者("what": "recv" / "read") - 私聊场景下也会推送到对方
metopic
5.6 删除同步
客户端 {del, topic: "grp_X", what: "msg", delseq: [{low: 100, hi: 105}]}
→ 服务端软删除消息(标记 deletedAt)
→ 更新 delID
→ {pres} 广播 "what": "del" 给所有订阅者
5.7 同步时序图
Client A Server Client B
│ │ │
│──── {sub, get} ───────→│ │
│←─── {meta, data} ─────│ │
│ │ │
│──── {pub} ────────────→│ │
│ │← DB save, seqId++ │
│←─── {ctrl, code:200} ─│ │
│ │──── {pres, msg} ──────→│
│ │──── {data, seq:N} ────→│
│ │ │
│ │←── {note: "recv"} ─────│
│←── {pres: "recv"} ────│ │
│ │←── {note: "read"} ─────│
│←── {pres: "read"} ────│ │