Files
im_server/docs/TOPICS-CN.md
T
2026-08-11 15:59:57 +08:00

32 KiB

Tinode 话题系统详解

Tinode 的核心设计理念:一切皆话题 (Topic-Oriented)

概述

在 Tinode 中,私聊、群聊、个人信息、搜索等功能都通过"话题 (Topic)"来实现。话题是一个隔离的通信通道,不同话题之间互不干扰。

┌─────────────────────────────────────────────────────────────────────────────┐
│                           Tinode 话题架构                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  【通信类话题】- 可以发送消息                                                 │
│  ┌───────────────┐  ┌───────────────┐  ┌───────────────┐                   │
│  │   p2p 私聊    │  │   grp 群聊    │  │   slf 笔记    │                   │
│  │   p2pXXX      │  │   grpXXX      │  │   slfXXX      │                   │
│  └───────────────┘  └───────────────┘  └───────────────┘                   │
│                                                                             │
│  【系统类话题】- 特殊用途                                                     │
│  ┌───────────────┐  ┌───────────────┐  ┌───────────────┐                   │
│  │   me 个人     │  │   fnd 搜索    │  │   sys 系统    │                   │
│  │   usrXXX      │  │   fnd         │  │   sys         │                   │
│  └───────────────┘  └───────────────┘  └───────────────┘                   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

话题类型

类型定义

// server/store/types/types.go
type TopicCat int

const (
    TopicCatMe  TopicCat = iota  // 'me' 话题 - 用户个人信息
    TopicCatFnd                   // 'fnd' 话题 - 用户搜索
    TopicCatP2P                   // 'p2p' 话题 - 私聊 (点对点)
    TopicCatGrp                   // 'grp' 话题 - 群聊
    TopicCatSys                   // 'sys' 话题 - 系统话题
    TopicCatSlf                   // 'slf' 话题 - 自我话题 (笔记/收藏)
)

命名规则

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" 状态变化
设备信息 管理多设备登录
单用户 只有用户自己订阅

联系人列表结构:

// 用户订阅 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 进行搜索匹配
单用户 只有搜索者自己订阅
不持久化 搜索结果不存储

搜索请求示例:

// 订阅 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                      │
│                                                             │
│  这是"给自己发消息"的话题                                    │
│                                                             │
│  用途:                                                      │
│  - 草稿箱                                                   │
│  - 收藏的消息                                               │
│  - 个人备忘录                                               │
│  - 跨设备同步的笔记                                          │
│                                                             │
│  特点:                                                      │
│  - 只有自己一个订阅者                                        │
│  - 可以发消息给自己                                         │
│  - 消息只有自己能看到                                        │
└─────────────────────────────────────────────────────────────┘

关键特性:

特性 说明
个人笔记 给自己发消息
跨设备同步 手机、电脑都能看到
收藏功能 可以收藏其他话题的消息
私密 只有自己能看到

使用场景:

// 保存一条笔记
{
  "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 可以邀请他人加入

话题核心数据结构

// 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
}

订阅者数据结构

// 每个用户在话题中的数据
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 整体架构的关键。