1165 lines
40 KiB
Markdown
1165 lines
40 KiB
Markdown
# Tinode Chat Server 代码通读指南
|
||
|
||
> 本指南帮助你快速理解 Tinode 代码架构,掌握开发流程,能够独立完成新需求开发和项目部署。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [架构总览](#架构总览)
|
||
2. [启动流程详解](#启动流程详解)
|
||
3. [核心数据结构](#核心数据结构)
|
||
4. [消息处理流程](#消息处理流程)
|
||
5. [关键模块详解](#关键模块详解)
|
||
6. [开发实战指南](#开发实战指南)
|
||
7. [测试与调试](#测试与调试)
|
||
8. [部署指南](#部署指南)
|
||
|
||
---
|
||
|
||
## 架构总览
|
||
|
||
### 整体架构图
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
│ 客户端层 │
|
||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||
│ │ WebSocket │ │ Long Poll │ │ gRPC │ │ HTTP │ │
|
||
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
|
||
└──────────┼─────────────────┼─────────────────┼─────────────────┼───────────┘
|
||
│ │ │ │
|
||
▼ ▼ ▼ ▼
|
||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
│ 协议处理层 │
|
||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||
│ │ hdl_websock.go / hdl_longpoll.go │ │
|
||
│ │ hdl_grpc.go / http.go │ │
|
||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
│ 会话层 (Session) │
|
||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||
│ │ Session 结构体 - 管理单个客户端连接 │ │
|
||
│ │ - 认证状态、订阅列表、消息队列 │ │
|
||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
│ Hub (核心路由) │
|
||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||
│ │ - 消息路由分发 │ │
|
||
│ │ - Topic 生命周期管理 │ │
|
||
│ │ - 全局状态维护 │ │
|
||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
│ Topic 层 │
|
||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||
│ │ me │ │ fnd │ │ p2p │ │ grp │ │ sys │ │
|
||
│ │ (用户) │ │ (发现) │ │ (单聊) │ │ (群聊) │ │ (系统) │ │
|
||
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
||
└─────────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
│ 存储层 (Store) │
|
||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||
│ │ store.go - 存储抽象层 │ │
|
||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||
│ │ MySQL │ │PostgreSQL│ │ MongoDB │ │RethinkDB│ │
|
||
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
||
└─────────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 核心组件职责
|
||
|
||
| 组件 | 文件 | 职责 |
|
||
|------|------|------|
|
||
| **main.go** | 入口点 | 配置加载、组件初始化、服务启动 |
|
||
| **Hub** | hub.go | 全局消息路由、Topic 生命周期管理 |
|
||
| **Session** | session.go | 客户端连接管理、消息收发 |
|
||
| **Topic** | topic.go | 聊天室/会话逻辑、消息处理 |
|
||
| **Store** | store/store.go | 数据库操作抽象层 |
|
||
| **Plugins** | plugins.go | 插件系统、事件钩子 |
|
||
|
||
---
|
||
|
||
## 启动流程详解
|
||
|
||
### 1. 入口函数 (main.go)
|
||
|
||
```go
|
||
// main.go 启动流程
|
||
func main() {
|
||
// 1. 解析命令行参数
|
||
flag.Parse()
|
||
|
||
// 2. 加载配置文件
|
||
config := loadConfig(configFileName)
|
||
|
||
// 3. 初始化日志系统
|
||
logs.Init()
|
||
|
||
// 4. 初始化数据库适配器
|
||
store.Open(config.StoreConfig)
|
||
|
||
// 5. 初始化认证模块
|
||
auth.Init(config.AuthConfig)
|
||
|
||
// 6. 初始化推送通知
|
||
push.Init(config.PushConfig)
|
||
|
||
// 7. 初始化媒体处理
|
||
media.Init(config.MediaConfig)
|
||
|
||
// 8. 创建 Hub(核心路由)
|
||
globals.hub = newHub()
|
||
|
||
// 9. 创建 SessionStore
|
||
globals.sessionStore = newSessionStore()
|
||
|
||
// 10. 初始化插件
|
||
pluginsInit(config.PluginConfig)
|
||
|
||
// 11. 启动 HTTP/WebSocket 服务
|
||
listenAndServe(addr, mux, tlsConfig, stop)
|
||
}
|
||
```
|
||
|
||
### 2. 配置加载流程
|
||
|
||
配置文件 `tinode.conf` 是 JSON 格式,主要配置项:
|
||
|
||
```json
|
||
{
|
||
"listen": ":6060", // HTTP 监听地址
|
||
"grpc_listen": ":16060", // gRPC 监听地址
|
||
"api_key_salt": "...", // API 密钥盐值
|
||
"max_message_size": 131072, // 最大消息大小
|
||
|
||
"store_config": { // 数据库配置
|
||
"use_adapter": "mysql",
|
||
"adapters": {
|
||
"mysql": { "dsn": "..." }
|
||
}
|
||
},
|
||
|
||
"auth_config": { // 认证配置
|
||
"basic": { "add_to_tags": true }
|
||
},
|
||
|
||
"push_config": { // 推送配置
|
||
"fcm": { "credentials_file": "..." }
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. 数据库初始化 (tinode-db)
|
||
|
||
```bash
|
||
# 初始化数据库并加载测试数据
|
||
./tinode-db -config=tinode.conf -data=data.json
|
||
|
||
# 主要步骤:
|
||
# 1. 连接数据库
|
||
# 2. 创建表/集合
|
||
# 3. 创建索引
|
||
# 4. 加载初始数据(用户、话题)
|
||
```
|
||
|
||
---
|
||
|
||
## 核心数据结构
|
||
|
||
### 1. Hub 结构体
|
||
|
||
Hub 是整个系统的核心路由器:
|
||
|
||
```go
|
||
// server/hub.go
|
||
type Hub struct {
|
||
// Topic 缓存 (name -> *Topic)
|
||
topics *sync.Map
|
||
|
||
// 客户端消息路由通道
|
||
routeCli chan *ClientComMessage
|
||
|
||
// 服务端消息路由通道
|
||
routeSrv chan *ServerComMessage
|
||
|
||
// 订阅请求通道
|
||
join chan *ClientComMessage
|
||
|
||
// Topic 注销通道
|
||
unreg chan *topicUnreg
|
||
|
||
// 用户状态变更通道
|
||
userStatus chan *userStatusReq
|
||
|
||
// 关闭通道
|
||
shutdown chan chan<- bool
|
||
}
|
||
```
|
||
|
||
**Hub 的核心方法**:
|
||
|
||
```go
|
||
// hub.go - 主事件循环
|
||
func (h *Hub) run() {
|
||
for {
|
||
select {
|
||
case msg := <-h.routeCli:
|
||
// 路由客户端消息到对应 Topic
|
||
h.routeClientMessage(msg)
|
||
|
||
case msg := <-h.routeSrv:
|
||
// 路由服务端消息
|
||
h.routeServerMessage(msg)
|
||
|
||
case req := <-h.join:
|
||
// 处理订阅请求,创建或加载 Topic
|
||
h.processJoin(req)
|
||
|
||
case req := <-h.unreg:
|
||
// 注销 Topic
|
||
h.processUnregister(req)
|
||
|
||
case done := <-h.shutdown:
|
||
// 关闭所有 Topic
|
||
h.gracefulShutdown()
|
||
done <- true
|
||
return
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. Topic 结构体
|
||
|
||
Topic 是聊天的核心单元:
|
||
|
||
```go
|
||
// server/topic.go
|
||
type Topic struct {
|
||
// Topic 名称 (如 "usrABC", "grpXYZ")
|
||
name string
|
||
|
||
// Topic 类型
|
||
cat types.TopicCat // me, fnd, p2p, grp, sys, slf
|
||
|
||
// 订阅者数据
|
||
perUser map[types.Uid]perUserData
|
||
|
||
// 附加的会话
|
||
sessions map[*Session]perSessionData
|
||
|
||
// 消息通道
|
||
clientMsg chan *ClientComMessage // 客户端消息
|
||
serverMsg chan *ServerComMessage // 服务端消息
|
||
reg chan *ClientComMessage // 订阅请求
|
||
unreg chan *ClientComMessage // 取消订阅
|
||
meta chan *ClientComMessage // 元数据操作
|
||
exit chan *shutDown // 关闭信号
|
||
|
||
// 状态
|
||
status int32 // 0=none, 1=new, 2=ready, 3=paused
|
||
}
|
||
```
|
||
|
||
**Topic 类型分类**:
|
||
|
||
| 类型 | 前缀 | 说明 |
|
||
|------|------|------|
|
||
| `me` | `me` | 用户个人话题,管理联系人列表 |
|
||
| `fnd` | `fnd` | 搜索话题,用于发现用户 |
|
||
| `p2p` | `p2p` | 点对点聊天 |
|
||
| `grp` | `grp` | 群组聊天 |
|
||
| `chn` | `chn` | 频道(只读群组) |
|
||
| `sys` | `sys` | 系统话题 |
|
||
| `slf` | `slf` | 自我话题(草稿/收藏) |
|
||
|
||
### 3. Session 结构体
|
||
|
||
Session 代表一个客户端连接:
|
||
|
||
```go
|
||
// server/session.go
|
||
type Session struct {
|
||
// 协议类型
|
||
proto SessionProto // WEBSOCK, LPOLL, GRPC, PROXY
|
||
|
||
// 会话 ID
|
||
sid string
|
||
|
||
// 连接句柄
|
||
ws *websocket.Conn // WebSocket
|
||
grpcnode pbx.Node_MessageLoopServer // gRPC
|
||
|
||
// 用户信息
|
||
uid types.Uid // 用户 ID
|
||
authLvl auth.Level // 认证级别
|
||
|
||
// 消息队列
|
||
send chan any // 发送队列
|
||
stop chan any // 停止信号
|
||
detach chan string // 分离话题
|
||
|
||
// 订阅的话题
|
||
subs map[string]*Subscription
|
||
|
||
// 状态标记
|
||
terminating int32 // 是否正在终止
|
||
background bool // 是否后台会话
|
||
}
|
||
```
|
||
|
||
### 4. 消息结构体
|
||
|
||
客户端到服务端的消息定义:
|
||
|
||
```go
|
||
// server/datamodel.go
|
||
|
||
// 握手消息
|
||
type MsgClientHi struct {
|
||
Id string `json:"id,omitempty"`
|
||
UserAgent string `json:"ua,omitempty"`
|
||
Version string `json:"ver,omitempty"`
|
||
DeviceID string `json:"dev,omitempty"`
|
||
Platform string `json:"platf,omitempty"`
|
||
}
|
||
|
||
// 登录消息
|
||
type MsgClientLogin struct {
|
||
Id string `json:"id,omitempty"`
|
||
Scheme string `json:"scheme,omitempty"`
|
||
Secret []byte `json:"secret,omitempty"`
|
||
}
|
||
|
||
// 订阅消息
|
||
type MsgClientSub struct {
|
||
Id string `json:"id,omitempty"`
|
||
Topic string `json:"topic,omitempty"`
|
||
Get *MsgGetQuery `json:"get,omitempty"`
|
||
Set *MsgSetQuery `json:"set,omitempty"`
|
||
}
|
||
|
||
// 发布消息
|
||
type MsgClientPub struct {
|
||
Id string `json:"id,omitempty"`
|
||
Topic string `json:"topic,omitempty"`
|
||
Content any `json:"content,omitempty"`
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 消息处理流程
|
||
|
||
### 1. 完整消息流转图
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||
│ 客户端发送: {"hi", "ver":"0.25", "ua":"Chrome/120"} │
|
||
└──────────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||
│ WebSocket Handler (hdl_websock.go) │
|
||
│ serveWebSocket() -> sess.readLoop() -> sess.dispatchRaw(raw) │
|
||
└──────────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||
│ Session 消息分发 (session.go) │
|
||
│ dispatchRaw() -> 解析 JSON -> 识别消息类型 -> 调用对应 handler │
|
||
└──────────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
┌───────────────────────┼───────────────────────┐
|
||
▼ ▼ ▼
|
||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||
│ hi │ │ login │ │ sub │
|
||
│ handler │ │ handler │ │ handler │
|
||
└──────────┘ └──────────┘ └──────────┘
|
||
│ │ │
|
||
└───────────────────────┼───────────────────────┘
|
||
▼
|
||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||
│ Hub 路由 (hub.go) │
|
||
│ routeCli <- ClientComMessage -> h.routeClientMessage() │
|
||
│ 根据 RcptTo 找到对应 Topic -> 发送到 Topic.clientMsg 通道 │
|
||
└──────────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||
│ Topic 处理 (topic.go) │
|
||
│ t.clientMsg <- msg -> t.processClientMessage() │
|
||
│ 根据 msg.Original 类型调用对应处理函数 │
|
||
│ - hi -> t.handleHi() │
|
||
│ - login -> t.handleLogin() │
|
||
│ - sub -> t.handleSubscribe() │
|
||
│ - pub -> t.handlePublish() │
|
||
└──────────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||
│ 数据库操作 (store/store.go) │
|
||
│ store.MessageSave() / store.TopicGet() / store.UserCreate() │
|
||
└──────────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||
│ 响应消息生成 │
|
||
│ ServerComMessage{Ctrl/Data/Pres/Meta/Info} │
|
||
│ -> 序列化为 JSON -> 发送到 Session.send 通道 │
|
||
└──────────────────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||
│ Session 发送 (session.go) │
|
||
│ sess.writeLoop() -> sess.send <- msg -> WebSocket.WriteMessage() │
|
||
└──────────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 2. 消息类型与处理函数映射
|
||
|
||
| 消息类型 | 结构体 | 处理函数 | 说明 |
|
||
|----------|--------|----------|------|
|
||
| `hi` | MsgClientHi | handleHi() | 握手,建立连接 |
|
||
| `login` | MsgClientLogin | handleLogin() | 用户登录认证 |
|
||
| `acc` | MsgClientAcc | handleAccount() | 创建/更新账户 |
|
||
| `sub` | MsgClientSub | handleSubscribe() | 订阅话题 |
|
||
| `leave` | MsgClientLeave | handleLeave() | 取消订阅 |
|
||
| `pub` | MsgClientPub | handlePublish() | 发布消息 |
|
||
| `get` | MsgClientGet | handleGet() | 获取元数据/消息 |
|
||
| `set` | MsgClientSet | handleSet() | 更新元数据 |
|
||
| `del` | MsgClientDel | handleDelete() | 删除消息/话题 |
|
||
| `note` | MsgClientNote | handleNote() | 发送通知 |
|
||
|
||
### 3. WebSocket 消息处理示例
|
||
|
||
```go
|
||
// server/hdl_websock.go
|
||
|
||
// 处理 WebSocket 连接
|
||
func serveWebSocket(wrt http.ResponseWriter, req *http.Request) {
|
||
// 1. 验证 API Key
|
||
if isValid, _ := checkAPIKey(getAPIKey(req)); !isValid {
|
||
wrt.WriteHeader(http.StatusForbidden)
|
||
return
|
||
}
|
||
|
||
// 2. 升级 HTTP 连接为 WebSocket
|
||
ws, err := upgrader.Upgrade(wrt, req, nil)
|
||
|
||
// 3. 创建 Session
|
||
sess, count := globals.sessionStore.NewSession(ws, "")
|
||
|
||
// 4. 启动读写协程
|
||
go sess.writeLoop() // 发送消息
|
||
go sess.readLoop() // 接收消息
|
||
}
|
||
|
||
// 读循环
|
||
func (sess *Session) readLoop() {
|
||
for {
|
||
_, raw, err := sess.ws.ReadMessage()
|
||
if err != nil {
|
||
return
|
||
}
|
||
sess.dispatchRaw(raw) // 分发消息
|
||
}
|
||
}
|
||
|
||
// 写循环
|
||
func (sess *Session) writeLoop() {
|
||
for {
|
||
select {
|
||
case msg := <-sess.send:
|
||
sess.sendMessage(msg)
|
||
case <-sess.stop:
|
||
return
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 关键模块详解
|
||
|
||
### 1. 认证模块 (server/auth/)
|
||
|
||
认证模块采用插件式架构:
|
||
|
||
```
|
||
server/auth/
|
||
├── auth.go # 认证接口定义
|
||
├── basic/ # 用户名密码认证
|
||
├── token/ # Token 认证
|
||
├── anon/ # 匿名认证
|
||
├── code/ # 验证码认证
|
||
└── rest/ # REST 外部认证
|
||
```
|
||
|
||
**认证接口定义**:
|
||
|
||
```go
|
||
// server/auth/auth.go
|
||
type Authenticator interface {
|
||
// 初始化
|
||
Init(jsonconf json.RawMessage, opts ...any) error
|
||
|
||
// 认证检查
|
||
Authenticate(secret []byte) (types.Uid, Level, time.Time, error)
|
||
|
||
// 生成秘密值
|
||
GenSecret(uid types.Uid, authLvl Level, expires time.Time) ([]byte, error)
|
||
}
|
||
```
|
||
|
||
**添加新认证方式步骤**:
|
||
|
||
1. 在 `server/auth/` 下创建新目录,如 `oauth/`
|
||
2. 实现 `Authenticator` 接口
|
||
3. 在 `main.go` 中导入: `_ "github.com/tinode/chat/server/auth/oauth"`
|
||
4. 在配置文件中添加相应配置
|
||
|
||
### 2. 数据库适配器 (server/db/)
|
||
|
||
数据库层采用适配器模式:
|
||
|
||
```go
|
||
// server/db/adapter.go
|
||
type Adapter interface {
|
||
// 连接管理
|
||
Open(config json.RawMessage) error
|
||
Close() error
|
||
IsOpen() bool
|
||
|
||
// 用户管理
|
||
UserCreate(user *t.User) error
|
||
UserGet(uid t.Uid) (*t.User, error)
|
||
UserUpdate(uid t.Uid, update map[string]any) error
|
||
UserDelete(uid t.Uid, hard bool) error
|
||
|
||
// 话题管理
|
||
TopicCreate(topic *t.Topic) error
|
||
TopicGet(name string) (*t.Topic, error)
|
||
|
||
// 消息管理
|
||
MessageSave(msg *t.Message) error
|
||
MessageGet(topic string, opts *MsgGetOpts) ([]t.Message, error)
|
||
|
||
// 订阅管理
|
||
SubscriptionGet(topic string, uid t.Uid) (*t.Subscription, error)
|
||
}
|
||
```
|
||
|
||
**添加新数据库支持**:
|
||
|
||
1. 在 `server/db/` 下创建新目录,如 `cockroach/`
|
||
2. 实现 `Adapter` 接口
|
||
3. 在 `main.go` 中导入: `_ "github.com/tinode/chat/server/db/cockroach"`
|
||
4. 构建时指定 tag: `go build -tags cockroach`
|
||
|
||
### 3. 推送通知 (server/push/)
|
||
|
||
```go
|
||
// server/push/push.go
|
||
type PushHandler interface {
|
||
// 初始化
|
||
Init(jsonconf json.RawMessage) error
|
||
|
||
// 发送推送
|
||
Push(userId types.Uid, data *PushData) error
|
||
}
|
||
```
|
||
|
||
**FCM 推送配置**:
|
||
|
||
```json
|
||
// tinode.conf
|
||
"push_config": {
|
||
"fcm": {
|
||
"credentials_file": "/path/to/credentials.json"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4. 文件存储 (server/media/)
|
||
|
||
支持本地文件系统和 S3:
|
||
|
||
```go
|
||
// server/media/media.go
|
||
type Handler interface {
|
||
// 上传
|
||
Upload(uri string, reader io.Reader) (string, error)
|
||
|
||
// 下载
|
||
Download(uri string) (io.ReadCloser, error)
|
||
|
||
// 删除
|
||
Delete(uri string) error
|
||
}
|
||
```
|
||
|
||
### 5. 插件系统 (server/plugins.go)
|
||
|
||
插件可以拦截和处理所有消息:
|
||
|
||
```go
|
||
// 插件事件类型
|
||
const (
|
||
plgHi = 1 << iota // 握手
|
||
plgAcc // 账户操作
|
||
plgLogin // 登录
|
||
plgSub // 订阅
|
||
plgPub // 发布
|
||
plgGet // 获取
|
||
plgSet // 设置
|
||
plgDel // 删除
|
||
)
|
||
|
||
// 插件接口
|
||
type Plugin interface {
|
||
// 处理客户端消息
|
||
HandleClientMessage(msg *ClientComMessage) (*ClientComMessage, error)
|
||
|
||
// 处理服务端消息
|
||
HandleServerMessage(msg *ServerComMessage) (*ServerComMessage, error)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 开发实战指南
|
||
|
||
### 场景 1: 添加新的消息类型
|
||
|
||
**步骤 1**: 定义消息结构体
|
||
|
||
```go
|
||
// server/datamodel.go
|
||
type MsgClientTyping struct {
|
||
Id string `json:"id,omitempty"`
|
||
Topic string `json:"topic,omitempty"`
|
||
IsTyping bool `json:"typing"`
|
||
}
|
||
```
|
||
|
||
**步骤 2**: 添加解析逻辑
|
||
|
||
```go
|
||
// server/session.go - dispatchRaw()
|
||
func (sess *Session) dispatchRaw(raw []byte) {
|
||
var msg struct {
|
||
Hi *MsgClientHi `json:"hi"`
|
||
// ... 其他类型
|
||
Typing *MsgClientTyping `json:"typing"` // 新增
|
||
}
|
||
json.Unmarshal(raw, &msg)
|
||
|
||
if msg.Typing != nil {
|
||
sess.handleTyping(msg.Typing)
|
||
}
|
||
}
|
||
```
|
||
|
||
**步骤 3**: 实现处理函数
|
||
|
||
```go
|
||
// server/topic.go
|
||
func (t *Topic) handleTyping(msg *MsgClientTyping) {
|
||
// 广报给其他订阅者
|
||
for sess := range t.sessions {
|
||
if sess.uid != msg.sess.uid {
|
||
sess.queueOut(&ServerComMessage{
|
||
Info: &MsgServerInfo{
|
||
Topic: t.name,
|
||
From: msg.sess.uid.String(),
|
||
Content: map[string]any{"typing": msg.IsTyping},
|
||
},
|
||
})
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 场景 2: 添加新的 API 端点
|
||
|
||
**步骤 1**: 定义 HTTP Handler
|
||
|
||
```go
|
||
// server/http.go
|
||
func serveCustomAPI(wrt http.ResponseWriter, req *http.Request) {
|
||
// 验证请求
|
||
if req.Method != http.MethodPost {
|
||
wrt.WriteHeader(http.StatusMethodNotAllowed)
|
||
return
|
||
}
|
||
|
||
// 处理请求
|
||
var data struct {
|
||
UserID string `json:"user_id"`
|
||
}
|
||
json.NewDecoder(req.Body).Decode(&data)
|
||
|
||
// 返回响应
|
||
wrt.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(wrt).Encode(map[string]any{
|
||
"status": "ok",
|
||
})
|
||
}
|
||
```
|
||
|
||
**步骤 2**: 注册路由
|
||
|
||
```go
|
||
// server/http.go - initHTTP()
|
||
func initHTTP() *http.ServeMux {
|
||
mux := http.NewServeMux()
|
||
// ... 其他路由
|
||
mux.HandleFunc("/v1/custom", serveCustomAPI) // 新增
|
||
return mux
|
||
}
|
||
```
|
||
|
||
### 场景 3: 添加新的数据库字段
|
||
|
||
**步骤 1**: 修改数据模型
|
||
|
||
```go
|
||
// server/store/types/types.go
|
||
type User struct {
|
||
Id types.Uid
|
||
CreatedAt time.Time
|
||
// 新增字段
|
||
Nickname string `json:"nickname"`
|
||
}
|
||
```
|
||
|
||
**步骤 2**: 修改数据库适配器
|
||
|
||
```go
|
||
// server/db/mysql/adapter.go
|
||
func (a *adapter) UserCreate(user *t.User) error {
|
||
_, err := a.db.Exec(`
|
||
INSERT INTO users (id, createdat, nickname)
|
||
VALUES (?, ?, ?)
|
||
`, user.Id.String(), user.CreatedAt, user.Nickname)
|
||
return err
|
||
}
|
||
```
|
||
|
||
**步骤 3**: 更新数据库 Schema
|
||
|
||
```sql
|
||
-- server/db/mysql/schema.sql
|
||
ALTER TABLE users ADD COLUMN nickname VARCHAR(64);
|
||
```
|
||
|
||
### 场景 4: 开发插件
|
||
|
||
创建一个消息过滤插件:
|
||
|
||
```go
|
||
// myplugin/filter.go
|
||
package main
|
||
|
||
import (
|
||
"github.com/tinode/chat/pbx"
|
||
)
|
||
|
||
type FilterPlugin struct {
|
||
bannedWords []string
|
||
}
|
||
|
||
func (p *FilterPlugin) HandleClientMessage(msg *pb.ClientMsg) (*pb.ClientMsg, error) {
|
||
// 检查消息内容
|
||
if msg.Pub != nil {
|
||
content := string(msg.Pub.Content)
|
||
for _, word := range p.bannedWords {
|
||
if strings.Contains(content, word) {
|
||
// 过滤敏感词
|
||
content = strings.ReplaceAll(content, word, "***")
|
||
msg.Pub.Content = []byte(content)
|
||
}
|
||
}
|
||
}
|
||
return msg, nil
|
||
}
|
||
|
||
func (p *FilterPlugin) HandleServerMessage(msg *pb.ServerMsg) (*pb.ServerMsg, error) {
|
||
return msg, nil
|
||
}
|
||
```
|
||
|
||
**注册插件**:
|
||
|
||
```go
|
||
// main.go
|
||
func main() {
|
||
plugin := &FilterPlugin{
|
||
bannedWords: []string{"badword1", "badword2"},
|
||
}
|
||
globals.plugins = append(globals.plugins, plugin)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 测试与调试
|
||
|
||
### 1. 单元测试
|
||
|
||
```bash
|
||
# 运行所有测试
|
||
go test ./server/...
|
||
|
||
# 运行特定测试
|
||
go test -run TestTopicPublish ./server/
|
||
|
||
# 查看覆盖率
|
||
go test -cover ./server/...
|
||
```
|
||
|
||
### 2. 集成测试
|
||
|
||
使用 tn-cli 进行集成测试:
|
||
|
||
```bash
|
||
# 登录
|
||
python tn-cli.py --login-basic=alice:alice123
|
||
|
||
# 执行测试脚本
|
||
python tn-cli.py < test-script.txt
|
||
```
|
||
|
||
### 3. 调试技巧
|
||
|
||
**开启详细日志**:
|
||
|
||
```go
|
||
// 在 tinode.conf 中
|
||
{
|
||
"logging": {
|
||
"level": "debug"
|
||
}
|
||
}
|
||
```
|
||
|
||
**使用 pprof 性能分析**:
|
||
|
||
```bash
|
||
# 启动服务器时开启 pprof
|
||
./server -expvar=/debug/vars
|
||
|
||
# 访问性能分析
|
||
go tool pprof http://localhost:6060/debug/pprof/profile
|
||
```
|
||
|
||
**调试 WebSocket**:
|
||
|
||
```javascript
|
||
// 浏览器控制台
|
||
const ws = new WebSocket('ws://localhost:6060/v0/channels');
|
||
|
||
ws.onopen = () => {
|
||
ws.send(JSON.stringify({hi: {ver: "0.25", ua: "test"}}));
|
||
};
|
||
|
||
ws.onmessage = (e) => {
|
||
console.log('收到:', JSON.parse(e.data));
|
||
};
|
||
```
|
||
|
||
### 4. 数据库调试
|
||
|
||
```bash
|
||
# MySQL
|
||
mysql -u root -p tinode -e "SELECT * FROM users LIMIT 10;"
|
||
|
||
# MongoDB
|
||
mongosh tinode --eval "db.users.find().limit(10)"
|
||
|
||
# PostgreSQL
|
||
psql -U postgres -d tinode -c "SELECT * FROM users LIMIT 10;"
|
||
```
|
||
|
||
---
|
||
|
||
## 部署指南
|
||
|
||
### 1. 本地开发环境
|
||
|
||
```bash
|
||
# 1. 克隆代码
|
||
git clone https://github.com/tinode/chat.git
|
||
cd chat
|
||
|
||
# 2. 安装依赖
|
||
go mod download
|
||
|
||
# 3. 启动数据库(以 MySQL 为例)
|
||
docker run -d --name mysql \
|
||
-e MYSQL_ROOT_PASSWORD=root \
|
||
-e MYSQL_DATABASE=tinode \
|
||
-p 3306:3306 \
|
||
mysql:8.0
|
||
|
||
# 4. 初始化数据库
|
||
go run ./tinode-db -tags mysql -data=./tinode-db/data.json
|
||
|
||
# 5. 启动服务器
|
||
go run ./server -tags mysql
|
||
```
|
||
|
||
### 2. 生产环境部署
|
||
|
||
#### 方式 1: 直接部署
|
||
|
||
```bash
|
||
# 构建
|
||
go build -tags mysql -ldflags "-X main.buildstamp=`git describe --tags`" ./server
|
||
|
||
# 创建配置目录
|
||
mkdir -p /etc/tinode
|
||
cp server/tinode.conf /etc/tinode/
|
||
|
||
# 创建 systemd 服务
|
||
cat > /etc/systemd/system/tinode.service << 'EOF'
|
||
[Unit]
|
||
Description=Tinode Chat Server
|
||
After=network.target mysql.service
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=tinode
|
||
ExecStart=/usr/local/bin/server -config=/etc/tinode/tinode.conf
|
||
Restart=on-failure
|
||
RestartSec=5
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
# 启动服务
|
||
systemctl daemon-reload
|
||
systemctl enable tinode
|
||
systemctl start tinode
|
||
```
|
||
|
||
#### 方式 2: Docker 部署
|
||
|
||
```bash
|
||
# 使用官方镜像
|
||
docker run -d --name tinode \
|
||
-p 6060:18080 \
|
||
-e MYSQL_DSN="root@tcp(mysql:3306)/tinode?parseTime=true" \
|
||
tinode/tinode-mysql:latest
|
||
|
||
# Docker Compose
|
||
cat > docker-compose.yml << 'EOF'
|
||
version: '3'
|
||
services:
|
||
mysql:
|
||
image: mysql:8.0
|
||
environment:
|
||
MYSQL_ROOT_PASSWORD: root
|
||
MYSQL_DATABASE: tinode
|
||
volumes:
|
||
- mysql_data:/var/lib/mysql
|
||
|
||
tinode:
|
||
image: tinode/tinode-mysql:latest
|
||
ports:
|
||
- "6060:18080"
|
||
depends_on:
|
||
- mysql
|
||
environment:
|
||
STORE_CONFIG_ADAPTERS_MYSQL_DSN: "root@tcp(mysql:3306)/tinode?parseTime=true"
|
||
|
||
volumes:
|
||
mysql_data:
|
||
EOF
|
||
|
||
docker-compose up -d
|
||
```
|
||
|
||
### 3. 集群部署
|
||
|
||
```yaml
|
||
# tinode.conf 集群配置
|
||
{
|
||
"cluster": {
|
||
"enabled": true,
|
||
"self": "node1",
|
||
"nodes": [
|
||
{"id": "node1", "addr": "node1.example.com:12000"},
|
||
{"id": "node2", "addr": "node2.example.com:12000"},
|
||
{"id": "node3", "addr": "node3.example.com:12000"}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4. 监控配置
|
||
|
||
```yaml
|
||
# Prometheus 监控
|
||
"expvar": "/debug/vars"
|
||
|
||
# Grafana 仪表盘
|
||
# 导入: https://grafana.com/grafana/dashboards/...
|
||
```
|
||
|
||
### 5. 生产环境配置建议
|
||
|
||
```json
|
||
{
|
||
// 安全配置
|
||
"tls": {
|
||
"enabled": true,
|
||
"strict_max_age": 604800
|
||
},
|
||
|
||
// 性能配置
|
||
"max_message_size": 262144,
|
||
"max_subscriber_count": 256,
|
||
|
||
// 日志配置
|
||
"logging": {
|
||
"level": "info"
|
||
},
|
||
|
||
// 数据库连接池
|
||
"store_config": {
|
||
"adapters": {
|
||
"mysql": {
|
||
"dsn": "user:pass@tcp(db-host:3306)/tinode?parseTime=true&max_open_conns=100&max_idle_conns=10"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 常见问题排查
|
||
|
||
### 1. 连接问题
|
||
|
||
```bash
|
||
# 检查端口是否监听
|
||
netstat -tlnp | grep 6060
|
||
|
||
# 检查防火墙
|
||
iptables -L -n | grep 6060
|
||
|
||
# 测试连接
|
||
curl http://localhost:6060/v0/hi
|
||
```
|
||
|
||
### 2. 数据库问题
|
||
|
||
```bash
|
||
# 检查数据库连接
|
||
mysql -u root -p -e "SHOW PROCESSLIST;"
|
||
|
||
# 检查表结构
|
||
mysql -u root -p tinode -e "SHOW TABLES;"
|
||
|
||
# 查看错误日志
|
||
tail -f /var/log/mysql/error.log
|
||
```
|
||
|
||
### 3. 性能问题
|
||
|
||
```bash
|
||
# CPU 分析
|
||
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
|
||
|
||
# 内存分析
|
||
go tool pprof http://localhost:6060/debug/pprof/heap
|
||
|
||
# 查看统计
|
||
curl http://localhost:6060/debug/vars | jq .
|
||
```
|
||
|
||
---
|
||
|
||
## 扩展阅读
|
||
|
||
- [API 文档](docs/API.md) - 完整的 API 参考
|
||
- [Drafty 格式](docs/drafty.md) - 富文本消息格式
|
||
- [通话建立流程](docs/call-establishment.md) - 音视频通话信令
|
||
- [Docker 部署](docker/README.md) - 容器化部署详解
|
||
|
||
---
|
||
|
||
## 开发流程总结
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 开发新功能流程 │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 1. 理解需求 │
|
||
│ - 阅读需求文档 │
|
||
│ - 确认技术方案 │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 2. 设计数据结构 │
|
||
│ - 修改 datamodel.go 添加消息类型 │
|
||
│ - 修改 store/types/types.go 添加数据模型 │
|
||
│ - 更新数据库 schema │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 3. 实现业务逻辑 │
|
||
│ - 在 topic.go 添加处理函数 │
|
||
│ - 在 store.go 添加数据库操作 │
|
||
│ - 实现必要的适配器方法 │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 4. 编写测试 │
|
||
│ - 单元测试 *_test.go │
|
||
│ - 集成测试(使用 tn-cli) │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 5. 测试验证 │
|
||
│ - go test ./... │
|
||
│ - 本地运行验证 │
|
||
│ - 性能测试 │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 6. 代码审查 & 提交 │
|
||
│ - 代码审查 │
|
||
│ - 更新文档 │
|
||
│ - Git commit │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
祝开发顺利! |