多租户更改

This commit is contained in:
richjohnrambo
2026-08-11 15:59:57 +08:00
parent 6c800de67d
commit c2076721fc
453 changed files with 102349 additions and 47 deletions
+383
View File
@@ -0,0 +1,383 @@
# 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 调度**(优先级从高到低):
1. `sess.send` — 消息队列(`*ServerComMessage` 或 `[]*ServerComMessage` 批量)
2. `sess.bkgTimer` — 后台超时定时器
3. `sess.stop` — 停止信号
4. `sess.detach` — 取消订阅
5. `ticker` — Ping 帧(每 `pongWait * 9/10` 发送一次)
### Keep-Alive
| 参数 | 值 | 说明 |
|------|-----|------|
| `writeWait` | 10s | 写超时 |
| `pongWait` | `idleSessionTimeout` | 期望收到 Pong 的时间(默认 180s) |
| `pingPeriod` | `pongWait * 9/10` | Ping 间隔(约 162s) |
| `pingPeriod` | `pongWait * 9/10` | Ping 间隔(约 162s) |
| 心跳探测 | `0x31` / `0x30` | 网络探针(单字节) |
### 缓冲区设置
| 参数 | 值 | 说明 |
|------|-----|------|
| 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}`)**:
```json
{
"pub": {
"id": "client_msg_id",
"topic": "grp_ABC123",
"head": {"key": "value"},
"content": {"txt": "Hello", "fmt": "..."}
}
}
```
**服务端投递 (`{data}`)**:
```json
{
"data": {
"topic": "grp_ABC123",
"from": "usr_XYZ",
"ts": "2026-08-04T00:00:00Z",
"seq": 12345,
"head": {},
"content": {"txt": "Hello"}
}
}
```
**控制响应 (`{ctrl}`)**:
```json
{
"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}` 消息上报状态:
```json
{"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 通知
```json
{
"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 级别的状态:
```json
{
"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 已读/已收同步
1. 客户端收到 `{data}` 后发送 `{note: "recv"}` 确认
2. 用户查看消息后发送 `{note: "read"}` 确认
3. 服务端更新 `perUserData.recvID/readID`
4. 通过 `{pres}` 广播给其他订阅者("what": "recv" / "read")
5. 私聊场景下也会推送到对方 `me` topic
### 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"} ────│ │
```