# Tinode 话题系统详解 > Tinode 的核心设计理念:**一切皆话题 (Topic-Oriented)** ## 概述 在 Tinode 中,私聊、群聊、个人信息、搜索等功能都通过"话题 (Topic)"来实现。话题是一个隔离的通信通道,不同话题之间互不干扰。 ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ Tinode 话题架构 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ 【通信类话题】- 可以发送消息 │ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │ │ p2p 私聊 │ │ grp 群聊 │ │ slf 笔记 │ │ │ │ p2pXXX │ │ grpXXX │ │ slfXXX │ │ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │ │ │ 【系统类话题】- 特殊用途 │ │ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │ │ me 个人 │ │ fnd 搜索 │ │ sys 系统 │ │ │ │ usrXXX │ │ fnd │ │ sys │ │ │ └───────────────┘ └───────────────┘ └───────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` --- ## 话题类型 ### 类型定义 ```go // server/store/types/types.go type TopicCat int const ( TopicCatMe TopicCat = iota // 'me' 话题 - 用户个人信息 TopicCatFnd // 'fnd' 话题 - 用户搜索 TopicCatP2P // 'p2p' 话题 - 私聊 (点对点) TopicCatGrp // 'grp' 话题 - 群聊 TopicCatSys // 'sys' 话题 - 系统话题 TopicCatSlf // 'slf' 话题 - 自我话题 (笔记/收藏) ) ``` ### 命名规则 ```go func GetTopicCat(name string) TopicCat { switch name[:3] { case "usr": return TopicCatMe // usrABC123 → me 话题 case "p2p": return TopicCatP2P // p2pABC... → 私聊 case "grp": return TopicCatGrp // grpABC... → 群聊 case "chn": return TopicCatGrp // chnABC... → 频道 (也是群聊) case "fnd": return TopicCatFnd // fnd → 搜索话题 case "sys": return TopicCatSys // sys → 系统话题 case "slf": return TopicCatSlf // slfABC... → 自我话题 } } ``` --- ## 六种话题详解 ### 1. P2P 私聊话题 (`p2p`) **命名格式**: `p2p` + 两用户ID排序拼接 **示例**: Alice (usrA) 和 Bob (usrB) 的私聊话题名为 `p2pusrAusrB` ``` 用户 Alice (usrA) 想给用户 Bob (usrB) 发消息 ┌─────────┐ ┌─────────┐ │ Alice │ │ Bob │ │ usrA │ │ usrB │ └────┬────┘ └────┬────┘ │ │ │ 订阅 p2pusrAusrB │ 订阅 p2pusrAusrB │ │ │ ┌───────────────────────┴────────────────┐ │ │ │ │ │ P2P Topic: p2pusrAusrB │ │ │ │ │ │ perUser: │ │ │ usrA → {modeWant, modeGiven} │ │ │ usrB → {modeWant, modeGiven} │ │ │ │ │ │ messages: [msg1, msg2, ...] │ │ │ │ │ └────────────────────────────────────────┘ │ │ └──────── 发送消息 ───────────────┘ ↓ 广播给所有订阅者 (Alice 和 Bob 都能收到) ``` **关键特性**: | 特性 | 说明 | |------|------| | 自动创建 | 当 Alice 给 Bob 发第一条消息时自动创建 | | 双向订阅 | 两个用户都是订阅者,权限对等 | | 自动删除 | 当两个用户都取消订阅时,话题自动删除 | | 消息存储 | 存储两个人的聊天记录 | | 无管理员 | 没有管理员概念,两人权限相同 | **创建流程**: ``` 1. Alice 发送订阅请求 {sub: {topic: "usrB"}} (注意:订阅的是 Bob 的用户ID,不是 p2p 话题名) 2. 服务器检测到这是 P2P 订阅 - 解析出目标用户: usrB - 生成话题名: p2pusrAusrB (排序后) 3. 检查话题是否存在 - 不存在 → 创建新话题 - 存在 → 直接订阅 4. 通知 Bob 有新的订阅请求 - 如果 Bob 在线,推送 {pres: {what: "knock"}} - Bob 可以选择订阅或忽略 5. Alice 发送消息 → 广播给 Alice 和 Bob ``` --- ### 2. GRP 群聊话题 (`grp` / `chn`) **命名格式**: `grp` 或 `chn` + 唯一ID **示例**: `grpTechGroup123`, `chnAnnouncements456` ``` 群组: "技术交流群" 话题名: grpTechGroup123 ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Alice │ │ Bob │ │ Carol │ │ Dave │ │ (创建者) │ │ (管理员) │ │ (成员) │ │ (成员) │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ │ │ └────────────┴────────────┴────────────┘ │ ▼ ┌───────────────────────────────────────┐ │ GRP Topic: grpTechGroup123 │ │ │ │ owner: usrA (Alice) │ │ │ │ perUser: │ │ usrA → {owner, admin, write} │ │ usrB → {admin, write} │ │ usrC → {write} │ │ usrD → {write} │ │ │ │ messages: [msg1, msg2, ...] │ │ │ │ tags: ["技术", "编程", "Go"] │ │ │ │ public: { │ │ "name": "技术交流群", │ │ "avatar": "group.jpg" │ │ } │ └───────────────────────────────────────┘ ``` **关键特性**: | 特性 | 说明 | |------|------| | 手动创建 | 需要显式创建,设置群名、头像等 | | 多人订阅 | 可以有很多成员,无上限 | | 权限管理 | 拥有者 > 管理员 > 普通成员 > 访客 | | 持久化 | 不会因为用户离开而删除 | | 可搜索 | 通过 tags 可以被搜索到 | | 邀请机制 | 可以邀请他人加入 | **权限层级**: ``` ┌─────────────────────────────────────────────────────┐ │ 群组权限层级 │ ├─────────────────────────────────────────────────────┤ │ │ │ Owner (拥有者) │ │ ├── 可以删除群组 │ │ ├── 可以转让拥有者 │ │ ├── 可以任命/撤销管理员 │ │ └── 拥有所有权限 │ │ │ │ Admin (管理员) │ │ ├── 可以修改群信息 │ │ ├── 可以踢出成员 │ │ ├── 可以邀请新成员 │ │ └── 可以设置成员权限 │ │ │ │ Member (普通成员) │ │ ├── 可以发送消息 │ │ ├── 可以查看群信息 │ │ └── 可以主动退出 │ │ │ │ Guest (访客) │ │ ├── 只能接收消息 │ │ └── 不能发送 │ │ │ └─────────────────────────────────────────────────────┘ ``` **频道模式 (chn)**: `chn` 前缀表示频道,是群聊的一种特殊形式: ``` ┌─────────────────────────────────────────────────────┐ │ 频道 vs 群聊 │ ├─────────────────────────────────────────────────────┤ │ │ │ 群聊 (grp) │ │ ├── 所有成员都能发言 │ │ ├── 消息对所有人可见 │ │ └── 适合团队协作 │ │ │ │ 频道 (chn) │ │ ├── 只有管理员能发言 │ │ ├── 类似微信公众号 │ │ ├── 订阅者只能接收 │ │ └── 适合公告、广播 │ │ │ └─────────────────────────────────────────────────────┘ ``` --- ### 3. ME 个人话题 (`usr`) **命名格式**: `usr` + 用户ID (就是用户自己的ID) **示例**: 用户 Alice 的 ID 是 `usrABC123`,她的 me 话题名也是 `usrABC123` ``` 用户: Alice 话题名: usrABC123 (用户自己的 ID) ┌─────────────────────────────────────────────────────────────┐ │ ME Topic: usrABC123 │ │ │ │ 这是用户的"影子"话题,不存储消息,只管理: │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ perSubs (联系人列表) │ │ │ │ │ │ │ │ p2pusrAusrB → {unread: 5, lastSeen: ...} │ │ │ │ grpTechGroup → {unread: 12, lastSeen: ...} │ │ │ │ p2pusrAusrC → {unread: 0, lastSeen: ...} │ │ │ │ │ │ │ │ 这些是 Alice 订阅的所有话题的引用 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ 功能: │ │ - 管理联系人列表 │ │ - 推送在线状态 (pres: {what: "online"/"offline"}) │ │ - 设备管理 (多设备登录) │ │ - 收到新消息的推送 │ └─────────────────────────────────────────────────────────────┘ ``` **关键特性**: | 特性 | 说明 | |------|------| | 临时话题 | 不存储在 topics 表中,是用户 ID 的引用 | | 不能发消息 | 只用于管理,不能发送聊天消息 | | 联系人管理 | 管理用户订阅的所有话题 | | 在线状态 | 推送 "online" / "offline" 状态变化 | | 设备信息 | 管理多设备登录 | | 单用户 | 只有用户自己订阅 | **联系人列表结构**: ```javascript // 用户订阅 me 话题后,会收到联系人列表 { "ctrl": { "params": { "sub": [ { "topic": "p2pusrAusrB", "unread": 5, "lastSeen": "2024-01-15T10:30:00Z" }, { "topic": "grpTechGroup", "unread": 12, "lastSeen": "2024-01-15T09:00:00Z" } ] } } } ``` --- ### 4. FND 搜索话题 (`fnd`) **命名格式**: 固定为 `fnd` ``` 用户 Alice 想搜索其他用户 Alice 发送请求: { "sub": { "topic": "fnd", "set": { "desc": { "public": {"tags": ["Go", "程序员"]} } }, "get": { "what": "desc", "desc": {"limit": 20} } } } │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ FND Topic: fnd │ │ │ │ 根据用户设置的搜索条件,返回匹配的用户列表 │ │ │ │ 搜索逻辑: │ │ 1. Alice 设置 tags: ["Go", "程序员"] │ │ 2. 服务器搜索所有包含这些 tags 的用户 │ │ 3. 返回匹配的用户列表 │ │ │ │ 返回: │ │ - usrB (Bob) - tags: ["Go", "后端"] │ │ - usrC (Carol) - tags: ["程序员", "Java"] │ │ - usrD (Dave) - tags: ["Go", "架构师"] │ └─────────────────────────────────────────────────────────────┘ ``` **关键特性**: | 特性 | 说明 | |------|------| | 搜索专用 | 用于搜索其他用户 | | 基于标签 | 通过 tags 进行搜索匹配 | | 单用户 | 只有搜索者自己订阅 | | 不持久化 | 搜索结果不存储 | **搜索请求示例**: ```javascript // 订阅 fnd 话题并设置搜索条件 { "sub": { "id": "search123", "topic": "fnd", "set": { "desc": { "public": { "tags": ["Go", "后端", "架构师"] } } }, "get": { "what": "desc" } } } // 服务器返回匹配的用户 { "pres": { "topic": "fnd", "what": "acs" }, "desc": { "topic": "usrB", "public": { "fn": "Bob", "tags": ["Go", "后端"] } } } ``` --- ### 5. SLF 自我话题 (`slf`) **命名格式**: `slf` + 唯一ID **示例**: `slfNotes123` ``` 用户: Alice 话题名: slfNotes123 ┌─────────────────────────────────────────────────────────────┐ │ SLF Topic: slfNotes123 │ │ │ │ 这是"给自己发消息"的话题 │ │ │ │ 用途: │ │ - 草稿箱 │ │ - 收藏的消息 │ │ - 个人备忘录 │ │ - 跨设备同步的笔记 │ │ │ │ 特点: │ │ - 只有自己一个订阅者 │ │ - 可以发消息给自己 │ │ - 消息只有自己能看到 │ └─────────────────────────────────────────────────────────────┘ ``` **关键特性**: | 特性 | 说明 | |------|------| | 个人笔记 | 给自己发消息 | | 跨设备同步 | 手机、电脑都能看到 | | 收藏功能 | 可以收藏其他话题的消息 | | 私密 | 只有自己能看到 | **使用场景**: ```javascript // 保存一条笔记 { "pub": { "id": "note123", "topic": "slfNotes123", "content": "明天开会,记得带资料" } } // 收藏一条消息到笔记 // 1. 先复制消息内容 // 2. 发送到 slf 话题 { "pub": { "id": "note456", "topic": "slfNotes123", "content": { "from": "grpTechGroup", "text": "重要的技术讨论记录..." } } } ``` --- ### 6. SYS 系统话题 (`sys`) **命名格式**: 固定为 `sys` ``` 系统全局广播 ┌─────────────────────────────────────────────────────────────┐ │ SYS Topic: sys │ │ │ │ 用于系统级通知 │ │ │ │ 用途: │ │ - 系统公告 │ │ - 维护通知 │ │ - 版本更新 │ │ - 紧急通知 │ │ │ │ 特点: │ │ - 只有 root 用户可以发送 │ │ - 广播给所有用户 │ │ - 强制推送 │ └─────────────────────────────────────────────────────────────┘ ``` **关键特性**: | 特性 | 说明 | |------|------| | 系统广播 | 发送系统级通知 | | Root 专用 | 只有 root 用户可以发送 | | 全员推送 | 广播给所有用户 | | 不可取消 | 用户不能取消订阅 | --- ## 话题类型对比 | 话题类型 | 命名前缀 | 能发消息 | 持久化 | 订阅人数 | 用途 | |---------|---------|---------|--------|---------|------| | **P2P** | `p2p` | ✅ | ✅ | 2人 | 私聊 | | **GRP** | `grp`/`chn` | ✅ | ✅ | 多人 | 群聊/频道 | | **SLF** | `slf` | ✅ | ✅ | 1人 | 笔记/收藏 | | **ME** | `usr` | ❌ | ❌ | 1人 | 个人信息/联系人 | | **FND** | `fnd` | ❌ | ❌ | 1人 | 搜索用户 | | **SYS** | `sys` | ✅ | ✅ | 所有人 | 系统广播 | --- ## 私聊 vs 群聊 ### 核心差异 ``` 私聊 (P2P): ┌─────────────────────────────────────────────────────┐ │ p2pusrAusrB │ │ │ │ • 订阅者固定为2人 (usrA + usrB) │ │ • 任一人发消息,另一人必收 │ │ • 最后一人取消订阅 → 话题删除 │ │ • 没有管理员概念,权限对等 │ │ • 订阅即创建,无需手动创建 │ │ • 适合私密对话 │ └─────────────────────────────────────────────────────┘ 群聊 (GRP): ┌─────────────────────────────────────────────────────┐ │ grpTechGroup123 │ │ │ │ • 订阅者可以是任意多人 │ │ • 有创建者、管理员、普通成员之分 │ │ • 创建者离开不会删除话题 │ │ • 可以踢人、禁言、设置权限 │ │ • 需要显式创建 │ │ • 适合团队协作、社区交流 │ └─────────────────────────────────────────────────────┘ ``` ### 创建流程对比 **私聊创建**: ``` 1. Alice 发送: {sub: {topic: "usrB"}} 2. 服务器: - 解析目标: usrB - 生成话题名: p2pusrAusrB - 检查是否存在 - 不存在则创建 3. Alice 订阅成功 4. Bob 收到通知: {pres: {what: "knock"}} 5. Bob 订阅: {sub: {topic: "usrA"}} 6. 双方可以发消息 ``` **群聊创建**: ``` 1. Alice 发送: { acc: { "user": "new", // 创建新话题 "scheme": "basic", "secret": "群密码", "desc": { "public": { "fn": "技术交流群", "note": "讨论技术问题" }, "defacs": { "auth": "JRWS", // 默认权限 "anon": "N" } } } } 2. 服务器: - 创建群话题: grpXXX - Alice 自动成为拥有者 - 初始化群信息 3. Alice 可以邀请他人加入 ``` --- ## 话题核心数据结构 ```go // server/topic.go type Topic struct { // 话题名称 name string // 话题类型 cat types.TopicCat // 创建/更新时间 created time.Time updated time.Time // 最后消息ID lastID int // 拥有者 owner types.Uid // 默认访问权限 accessAuth types.AccessMode // 认证用户默认权限 accessAnon types.AccessMode // 匿名用户默认权限 // 发现标签 tags []string // 扩展数据 aux map[string]any // 公开数据 (群名、头像等) public any trusted any // 订阅者数据 perUser map[types.Uid]perUserData // 会话列表 sessions map[*Session]perSessionData // 消息通道 clientMsg chan *ClientComMessage serverMsg chan *ServerComMessage } ``` --- ## 订阅者数据结构 ```go // 每个用户在话题中的数据 type perUserData struct { // 权限 modeWant types.AccessMode // 用户想要的权限 modeGiven types.AccessMode // 话题给予的权限 // 实际权限 = modeWant & modeGiven // 状态 deleted bool // 私有数据 private any } ``` --- ## 访问权限说明 权限用字符串表示,如 `"JRWS"`: | 权限 | 含义 | 说明 | |------|------|------| | J | Join | 加入话题 | | R | Read | 读取消息 | | W | Write | 发送消息 | | P | Presence | 接收在线状态 | | A | Admin | 管理权限 | | S | Share | 分享权限 | | D | Delete | 删除权限 | | O | Owner | 拥有者权限 | **权限组合示例**: - `"JRWP"` - 普通成员:可以加群、读消息、发消息、看在线状态 - `"JRWS"` - 可分享成员:额外可以分享群链接 - `"JRWPA"` - 管理员:额外可以管理成员 - `"JRWPAO"` - 拥有者:完全控制 --- ## 消息流转 ### 发布消息流程 ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ 消息发布流程 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ ① 客户端发送 │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ {pub: {topic: "grpTechGroup", content: "Hello!"}} │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ② Session.dispatch() → Session.publish() │ │ │ │ │ ▼ │ │ ③ Hub 路由到 Topic │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ hub.topicGet("grpTechGroup") → topic │ │ │ │ topic.clientMsg <- msg │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ④ Topic 处理消息 │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ 1. 检查发送者权限 (W) │ │ │ │ 2. 生成消息ID (lastID++) │ │ │ │ 3. 存储到数据库 │ │ │ │ 4. 准备广播 │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ⑤ 广播给所有订阅者 │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ for session := range topic.sessions { │ │ │ │ session.send <- {data: {topic: "grpTechGroup", content: ...}} │ │ │ │ } │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ⑥ 订阅者的 writeLoop 发送 │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ ws.WriteMessage(TextMessage, JSON) │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` --- ## 总结 Tinode 的话题系统是其核心架构: 1. **统一模型**: 私聊、群聊、个人信息都通过话题实现 2. **命名规则**: 通过前缀区分类型 (`p2p`, `grp`, `usr` 等) 3. **权限控制**: 细粒度的访问控制 (J/R/W/P/A/S/D/O) 4. **发布订阅**: 消息通过话题广播给所有订阅者 理解话题系统是理解 Tinode 整体架构的关键。