51 KiB
Tinode 协议详解
本文档详细介绍 Tinode 支持的所有通信协议,包括协议原理、消息格式、实现细节和使用方法。
目录
协议概述
Tinode 支持四种主要的通信协议:
| 协议 | 端点 | 用途 | 特点 |
|---|---|---|---|
| WebSocket | /v0/channels |
实时消息推送 | 双向通信、低延迟 |
| gRPC | grpc_listen 端口 |
高性能客户端通信 | 强类型、流式传输 |
| 长轮询 | /v0/channels/lp |
兼容性客户端 | HTTP 兼容、防火墙友好 |
| HTTP | /v0/file/* |
文件上传下载 | RESTful、大文件支持 |
协议架构图
┌─────────────────────────────────────────────────────────────────────────────┐
│ 客户端层 │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Web App │ │ Android │ │ iOS │ │ CLI/Bot │ │
│ │ (WS/LP) │ │ (WS/gRPC) │ │ (WS/gRPC) │ │ (gRPC) │ │
│ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
└────────┼──────────────┼──────────────┼──────────────┼──────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 协议层 │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ WebSocket │ │ gRPC │ │Long Polling│ │ HTTP │ │
│ │JSON/Text │ │ Protobuf │ │ JSON/Text │ │Multipart │ │
│ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
└────────┼──────────────┼──────────────┼──────────────┼──────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 会话层 │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ Session Store │ │
│ │ (统一会话管理,协议无关) │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 业务层 │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Hub │ │ Topic │ │ User │ │ Auth │ │
│ │ 连接中心 │ │ 消息路由 │ │ 用户管理 │ │ 认证模块 │ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
WebSocket 协议
概述
WebSocket 是 Tinode 主要的实时通信协议,用于 Web 应用和移动应用的实时消息推送。
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,它通过 HTTP 升级握手建立连接,然后切换到 WebSocket 协议进行双向数据传输。
端点
ws://host:port/v0/channels
wss://host:port/v0/channels (TLS)
协议层交互详解
1. 握手阶段(HTTP Upgrade)
WebSocket 连接通过 HTTP Upgrade 机制建立:
客户端请求:
GET /v0/channels?apikey=xxx HTTP/1.1
Host: server.example.com
Upgrade: websocket ← 请求升级协议
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZQ== ← 随机 Base64 值
Sec-WebSocket-Version: 13 ← 协议版本
Origin: https://example.com
服务器响应:
HTTP/1.1 101 Switching Protocols ← 同意切换
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ... ← 根据Key计算出的验证值
验证值计算:
Sec-WebSocket-Accept = base64(sha1(Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
2. 数据帧格式
WebSocket 数据帧格式(RFC 6455):
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
|1|2|3|4| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued... |
+-------------------------------+-------------------------------+
| |Masking-key (if MASK set) |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
关键字段:
| 字段 | 位数 | 说明 |
|---|---|---|
| FIN | 1 | 是否为最后一帧(消息可能分多帧) |
| RSV1-3 | 3 | 保留位(用于扩展) |
| opcode | 4 | 帧类型 |
| MASK | 1 | 是否掩码(客户端必须掩码) |
| Payload len | 7+ | 数据长度 |
Opcode 类型:
| Opcode | 类型 | 说明 |
|---|---|---|
| 0x0 | Continuation | 续帧(分片消息的后续部分) |
| 0x1 | Text | 文本帧(Tinode 使用此类型传输 JSON) |
| 0x2 | Binary | 二进制帧 |
| 0x8 | Close | 连接关闭 |
| 0x9 | Ping | 心跳请求 |
| 0xA | Pong | 心跳响应 |
3. 心跳机制
WebSocket 协议层的 Ping/Pong 心跳:
客户端 服务器
│ │
│────── Ping 帧 (opcode=0x9) ────────────>│
│ │
│<───── Pong 帧 (opcode=0xA) ─────────────│
│ │
Tinode 的心跳配置:
const (
writeWait = 10 * time.Second // 写操作超时
pongWait = idleSessionTimeout // 等待 Pong 超时(默认 60 秒)
pingPeriod = (pongWait * 9) / 10 // 发送 Ping 周期(54 秒)
)
心跳流程:
┌─────────────────────────────────────────────────────────────────────────────┐
│ WebSocket 心跳流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 服务器 writeLoop: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ for { │ │
│ │ select { │ │
│ │ case <-ticker.C: // 每 54 秒 │ │
│ │ ws.WriteMessage(PingMessage, nil) // 发送 Ping │ │
│ │ case msg <- sess.send: // 有消息待发送 │ │
│ │ ws.WriteMessage(TextMessage, msg) // 发送消息 │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 服务器 readLoop: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ws.SetReadDeadline(time.Now().Add(pongWait)) // 设置读取超时 │ │
│ │ │ │
│ │ ws.SetPongHandler(func(string) error { // 收到 Pong 时 │ │
│ │ ws.SetReadDeadline(time.Now().Add(pongWait)) // 重置超时 │ │
│ │ return nil │ │
│ │ }) │ │
│ │ │ │
│ │ for { │ │
│ │ _, raw, err := ws.ReadMessage() // 读取消息 │ │
│ │ sess.dispatchRaw(raw) // 处理消息 │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
4. 消息分片
大消息会被分成多帧传输:
帧1: FIN=0, opcode=0x1, payload="Hello "
帧2: FIN=0, opcode=0x0 (continuation), payload="World "
帧3: FIN=1, opcode=0x0 (continuation), payload="!"
接收方根据 FIN 位判断消息是否完整。
5. 连接关闭
正常关闭流程:
客户端 服务器
│ │
│────── Close 帧 (opcode=0x8) ───────────>│
│ payload: 1000 (正常关闭) │
│ │
│<───── Close 帧 (opcode=0x8) ────────────│
│ payload: 1000 │
│ │
│====== TCP 连接断开 =====================│
状态码:
| 状态码 | 说明 |
|---|---|
| 1000 | 正常关闭 |
| 1001 | 端点离开 |
| 1002 | 协议错误 |
| 1003 | 不支持的数据类型 |
| 1008 | 策略违规 |
| 1011 | 服务器错误 |
WebSocket vs 应用层
Tinode 在 WebSocket 之上有一套应用层协议:
┌─────────────────────────────────────────────────────────────────────────────┐
│ 协议层次结构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 应用层 (Tinode 协议) │ │
│ │ │ │
│ │ 消息类型: {hi}, {login}, {sub}, {pub}, {get}, {set}, {del}... │ │
│ │ 数据格式: JSON 文本 │ │
│ │ 业务逻辑: 认证、订阅、消息路由、权限控制 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ WebSocket 协议层 │ │
│ │ │ │
│ │ 帧类型: Text (0x1), Binary (0x2), Ping (0x9), Pong (0xA) │ │
│ │ 传输: 建立连接、心跳保活、分片传输、关闭连接 │ │
│ │ 特性: 掩码、压缩扩展 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ TCP 传输层 │ │
│ │ │ │
│ │ 可靠传输、流控制、拥塞控制 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
示例:发送一条聊天消息
应用层 JSON:
{"pub": {"id": "msg123", "topic": "grpTechGroup", "content": "Hello!"}}
│
▼
WebSocket 帧:
┌────────────────────────────────────────────────┐
│ FIN=1 | opcode=0x1 | MASK=1 | payload='...' │
│ payload = {"pub": {"id": "msg123", ...}} │
└────────────────────────────────────────────────┘
│
▼
TCP 数据包:
[WebSocket 握手 + TLS 加密 + TCP 头 + IP 头]
消息流转完整流程
前端发送消息
// 1. 构造 JSON 消息
const message = {
pub: {
id: "msg123",
topic: "grpTechGroup",
content: "Hello World"
}
};
// 2. 通过 WebSocket 发送
websocket.send(JSON.stringify(message));
后端接收处理流程
┌─────────────────────────────────────────────────────────────────────────────┐
│ 消息流转全景图 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ① WebSocket 帧到达 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ TCP → WebSocket 帧 → JSON 文本 │ │
│ │ opcode=0x1, payload='{"pub":{"id":"msg123",...}}' │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ② hdl_websock.go: serveWebSocket() │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • HTTP Upgrade 握手 │ │
│ │ • 创建 Session │ │
│ │ • 启动 readLoop() + writeLoop() │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ③ readLoop() 读取消息 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ _, raw, err := sess.ws.ReadMessage() │ │
│ │ // raw = []byte('{"pub":{"id":"msg123",...}}') │ │
│ │ sess.dispatchRaw(raw) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ④ dispatchRaw() 解析 JSON │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ var msg ClientComMessage │ │
│ │ json.Unmarshal(raw, &msg) │ │
│ │ // msg.Pub = {Id: "msg123", Topic: "grpTechGroup", ...} │ │
│ │ sess.dispatch(&msg) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ⑤ dispatch() 路由到处理器 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ switch { │ │
│ │ case msg.Pub != nil: │ │
│ │ handler = checkVers(checkUser(s.publish)) │ │
│ │ msg.Id = msg.Pub.Id │ │
│ │ msg.Original = msg.Pub.Topic │ │
│ │ } │ │
│ │ handler(msg) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ⑥ publish() 业务处理 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • 验证用户权限 (W) │ │
│ │ • 获取 Topic: hub.topicGet("grpTechGroup") │ │
│ │ • 发送到 Topic 消息通道: topic.clientMsg <- msg │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ⑦ Topic 处理并广播 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • 生成消息 ID (lastID++) │ │
│ │ • 存储到数据库 │ │
│ │ • 遍历所有订阅者: │ │
│ │ for session := range topic.sessions { │ │
│ │ session.send <- ServerComMessage{Data: {...}} │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ⑧ writeLoop() 发送给客户端 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ case msg := <-sess.send: │ │
│ │ bits := json.Marshal(msg) │ │
│ │ ws.WriteMessage(TextMessage, bits) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
连接流程
┌─────────┐ ┌─────────┐
│ Client │ │ Server │
└────┬────┘ └────┬────┘
│ │
│──── HTTP GET /v0/channels ──>│ WebSocket 握手
│<─── HTTP 101 Switching ─────│
│ │
│<─────── {ctrl} ──────────────│ 服务器响应 (含 sid, ver)
│ │
│─────── {hi} ────────────────>│ 客户端握手
│<─────── {ctrl} ──────────────│ 握手确认
│ │
│─────── {login} ─────────────>│ 用户认证
│<─────── {ctrl} ──────────────│ 认证结果
│ │
│─────── {sub} ───────────────>│ 订阅话题
│<─────── {ctrl} ──────────────│
│<─────── {meta} ──────────────│ 话题元数据
│ │
│─────── {pub} ───────────────>│ 发送消息
│<─────── {ctrl} ──────────────│
│<─────── {data} ──────────────│ 消息推送
│ │
核心实现
文件: server/hdl_websock.go
1. 连接升级
// WebSocket 升级器配置
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
EnableCompression: globals.wsCompression,
// 允许来自任意 Origin 的连接
CheckOrigin: func(r *http.Request) bool { return true },
}
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. 创建新会话
sess, count := globals.sessionStore.NewSession(ws, "")
// 4. 启动读写循环
go sess.writeLoop() // 发送消息
go sess.readLoop() // 接收消息
}
2. 读循环(接收消息)
func (sess *Session) readLoop() {
defer func() {
sess.closeWS()
sess.cleanUp(false)
}()
// 设置读取限制和超时
sess.ws.SetReadLimit(globals.maxMessageSize)
sess.ws.SetReadDeadline(time.Now().Add(pongWait))
// Pong 处理器:收到 Pong 后重置超时
sess.ws.SetPongHandler(func(string) error {
sess.ws.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
for {
// 读取客户端消息
_, raw, err := sess.ws.ReadMessage()
if err != nil {
return
}
// 统计
statsInc("IncomingMessagesWebsockTotal", 1)
// 分发消息到业务层
sess.dispatchRaw(raw)
}
}
3. 写循环(发送消息)
func (sess *Session) writeLoop() {
ticker := time.NewTicker(pingPeriod) // 定时发送 Ping
defer ticker.Stop()
for {
select {
case msg := <-sess.send:
// 发送消息到客户端
if !sess.sendMessage(msg) {
return
}
case <-ticker.C:
// 发送 Ping 保持连接
if err := wsWrite(sess.ws, websocket.PingMessage, nil); err != nil {
return
}
case msg := <-sess.stop:
// 关闭会话
if msg != nil {
wsWrite(sess.ws, websocket.TextMessage, msg)
}
return
case topic := <-sess.detach:
// 从话题分离
sess.delSub(topic)
}
}
}
心跳机制
| 参数 | 值 | 说明 |
|---|---|---|
writeWait |
10秒 | 写操作超时 |
pongWait |
配置项 | 等待 Pong 超时(默认 60 秒) |
pingPeriod |
pongWait * 0.9 |
发送 Ping 周期 |
消息格式
WebSocket 消息使用 JSON 文本格式:
{
"sub": {
"id": "unique-id",
"topic": "me",
"get": {
"what": "desc sub data"
}
}
}
配置项
| 配置项 | 说明 |
|---|---|
listen |
WebSocket 监听地址 |
ws_compression |
是否启用压缩 |
max_message_size |
最大消息大小 |
gRPC 协议
概述
gRPC 提供高性能、强类型的客户端通信,适合移动应用、CLI 工具和插件系统。
端点
host:grpc_listen_port
默认端口:16060
服务定义
文件: pbx/model.proto
Node 服务(客户端实现)
service Node {
// 消息循环:双向流
rpc MessageLoop(stream ClientMsg) returns (stream ServerMsg) {}
// 大文件上传:客户端流
rpc LargeFileReceive(stream FileUpReq) returns (FileUpResp) {}
// 大文件下载:服务端流
rpc LargeFileServe(FileDownReq) returns (stream FileDownResp) {}
}
Plugin 服务(插件实现)
service Plugin {
// 消息管道:处理所有客户端消息
rpc FireHose(ClientReq) returns (ServerResp) {}
// 用户发现
rpc Find(SearchQuery) returns (SearchFound) {}
// 事件通知
rpc Account(AccountEvent) returns (Unused) {}
rpc Topic(TopicEvent) returns (Unused) {}
rpc Subscription(SubscriptionEvent) returns (Unused) {}
rpc Message(MessageEvent) returns (Unused) {}
}
核心实现
文件: server/hdl_grpc.go
1. 消息循环
func (*grpcNodeServer) MessageLoop(stream pbx.Node_MessageLoopServer) error {
// 1. 创建新会话
sess, count := globals.sessionStore.NewSession(stream, "")
if p, ok := peer.FromContext(stream.Context()); ok {
sess.remoteAddr = p.Addr.String()
}
defer func() {
sess.closeGrpc()
sess.cleanUp(false)
}()
// 2. 启动写循环
go sess.writeGrpcLoop()
// 3. 读循环
for {
in, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
// 统计
statsInc("IncomingMessagesGrpcTotal", 1)
// 分发消息
sess.dispatch(pbCliDeserialize(in))
}
}
2. 服务器启动
func serveGrpc(addr string, kaEnabled bool, tlsConf *tls.Config) (*grpc.Server, error) {
lis, err := netListener(addr)
var opts []grpc.ServerOption
opts = append(opts, grpc.MaxRecvMsgSize(int(globals.maxMessageSize)))
// TLS 配置
if tlsConf != nil {
opts = append(opts, grpc.Creds(credentials.NewTLS(tlsConf)))
}
// Keepalive 配置
if kaEnabled {
kepConfig := keepalive.EnforcementPolicy{
MinTime: 1 * time.Second,
PermitWithoutStream: true,
}
opts = append(opts, grpc.KeepaliveEnforcementPolicy(kepConfig))
kpConfig := keepalive.ServerParameters{
Time: 60 * time.Second,
Timeout: 20 * time.Second,
}
opts = append(opts, grpc.KeepaliveParams(kpConfig))
}
srv := grpc.NewServer(opts...)
pbx.RegisterNodeServer(srv, &grpcNodeServer{})
go srv.Serve(lis)
return srv, nil
}
消息类型
客户端消息 (ClientMsg)
message ClientMsg {
oneof Message {
ClientHi hi = 1; // 握手
ClientAcc acc = 2; // 账户
ClientLogin login = 3; // 登录
ClientSub sub = 4; // 订阅
ClientLeave leave = 5; // 离开
ClientPub pub = 6; // 发布
ClientGet get = 7; // 查询
ClientSet set = 8; // 更新
ClientDel del = 9; // 删除
ClientNote note = 10; // 通知
}
ClientExtra extra = 13; // 额外参数
}
服务器消息 (ServerMsg)
message ServerMsg {
oneof Message {
ServerCtrl ctrl = 1; // 控制响应
ServerData data = 2; // 数据消息
ServerPres pres = 3; // 在线状态
ServerMeta meta = 4; // 元数据
ServerInfo info = 5; // 信息通知
}
}
文件传输
上传(客户端流)
message FileUpReq {
string id = 1; // 请求 ID
Auth auth = 2; // 认证信息
string topic = 3; // 所属话题
FileMeta meta = 4; // 文件元数据
bytes content = 5; // 文件内容(分块)
}
message FileUpResp {
string id = 1;
int32 code = 2; // 状态码
string text = 3;
FileMeta meta = 4;
string redir_url = 5; // 重定向 URL
}
下载(服务端流)
message FileDownReq {
string id = 1;
Auth auth = 2;
string uri = 3; // 文件 URI
string if_modified = 4; // ETag
}
message FileDownResp {
string id = 1;
int32 code = 2;
string text = 3;
FileMeta meta = 4;
string redir_url = 5;
bytes content = 6; // 文件内容(分块)
}
Keepalive 配置
| 参数 | 值 | 说明 |
|---|---|---|
MinTime |
1秒 | 客户端最小 Ping 间隔 |
PermitWithoutStream |
true | 允许无活动流时 Ping |
Time |
60秒 | 服务端 Ping 间隔 |
Timeout |
20秒 | Ping 超时时间 |
特殊功能
gRPC 协议支持 WebSocket 不支持的功能:
| 功能 | 说明 |
|---|---|
| 代理发送 | root 用户可代表其他用户发送消息 |
| 用户删除 | 完整删除用户账户 |
| 文件流式传输 | 高效的大文件传输 |
长轮询协议
概述
长轮询是 WebSocket 的兼容替代方案,适用于不支持 WebSocket 的环境(如老旧浏览器、严格防火墙)。
端点
POST/GET http://host:port/v0/channels/lp
工作原理
┌─────────┐ ┌─────────┐
│ Client │ │ Server │
└────┬────┘ └────┬────┘
│ │
│── POST /v0/channels/lp ────>│ 创建会话
│<── 201 Created + sid ───────│
│ │
│── POST /v0/channels/lp ────>│ 登录 (sid + {login})
│<── {ctrl} ──────────────────│
│ │
│── GET /v0/channels/lp?sid=X │ 长轮询(等待消息)
│ │ (阻塞等待)
│<── {data} ──────────────────│ 有消息时返回
│ │
│── GET /v0/channels/lp?sid=X │ 继续轮询
│<── (超时返回空) ─────────────│
│ │
核心实现
文件: server/hdl_longpoll.go
1. 会话创建
func serveLongPoll(wrt http.ResponseWriter, req *http.Request) {
// 获取会话 ID
sid := req.FormValue("sid")
if sid == "" {
// 创建新会话
sess, count := globals.sessionStore.NewSession(wrt, "")
sess.remoteAddr = getRemoteAddr(req)
// 返回会话 ID
wrt.WriteHeader(http.StatusCreated)
pkt := NoErrCreated(req.FormValue("id"), "", now)
pkt.Ctrl.Params = map[string]string{"sid": sess.sid}
enc.Encode(pkt)
return
}
// 获取已有会话
sess = globals.sessionStore.Get(sid)
if sess == nil {
wrt.WriteHeader(http.StatusForbidden)
enc.Encode(ErrSessionNotFound(now))
return
}
// 处理请求
if req.ContentLength != 0 {
// 有消息体:读取并处理
sess.readOnce(wrt, req)
return
}
// 无消息体:长轮询等待
sess.writeOnce(wrt, req)
}
2. 消息读取
func (sess *Session) readOnce(wrt http.ResponseWriter, req *http.Request) (int, error) {
// 检查消息大小
if req.ContentLength > globals.maxMessageSize {
return http.StatusExpectationFailed, errors.New("request too large")
}
// 读取消息体
raw, err := io.ReadAll(req.Body)
if err == nil {
sess.lock.Lock()
statsInc("IncomingMessagesLongpollTotal", 1)
sess.dispatchRaw(raw)
sess.lock.Unlock()
return 0, nil
}
return 0, err
}
3. 长轮询等待
func (sess *Session) writeOnce(wrt http.ResponseWriter, req *http.Request) {
for {
select {
case msg := <-sess.send:
// 有消息待发送
sess.sendMessageLp(wrt, msg)
return
case <-time.After(pingPeriod):
// 超时:返回空包
wrt.Write([]byte{})
return
case msg := <-sess.stop:
// 会话关闭
globals.sessionStore.Delete(sess)
if msg != nil {
lpWrite(wrt, msg)
}
return
case <-req.Context().Done():
// HTTP 请求取消
return
}
}
}
HTTP 头设置
// CORS 支持
wrt.Header().Set("Access-Control-Allow-Origin", "*")
// 禁用缓存
wrt.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
wrt.Header().Set("Pragma", "no-cache")
wrt.Header().Set("Expires", "0")
// 内容类型
wrt.Header().Set("Content-Type", "text/plain")
请求参数
| 参数 | 方法 | 说明 |
|---|---|---|
sid |
GET/POST | 会话 ID(创建后必需) |
id |
GET/POST | 客户端消息 ID |
apikey |
GET/POST/Header | API Key |
消息格式
与 WebSocket 相同,使用 JSON 文本格式。
HTTP 文件传输协议
概述
HTTP 文件传输协议用于大文件的带外上传和下载,独立于主消息通道。
端点
| 端点 | 方法 | 用途 |
|---|---|---|
/v0/file/u |
POST | 文件上传 |
/v0/file/s/[id] |
GET | 文件下载 |
核心实现
文件: server/hdl_files.go
1. 文件上传
func largeFileReceiveHTTP(wrt http.ResponseWriter, req *http.Request) {
// 1. 验证 API Key
if isValid, _ := checkAPIKey(getAPIKey(req)); !isValid {
writeHttpResponse(ErrAPIKeyRequired(now), nil)
return
}
// 2. 认证检查
authMethod, secret := getHttpAuth(req)
uid, challenge, err := authFileRequest(authMethod, secret, req.FormValue("sid"), getRemoteAddr(req))
// 3. 读取文件
file, header, err := req.FormFile("file")
// 4. 检测 MIME 类型
buff := make([]byte, 512)
file.Read(buff)
mimeType := http.DetectContentType(buff)
// 5. 创建文件定义
fdef := &types.FileDef{
Id: store.Store.GetUidString(),
User: uid.String(),
MimeType: mimeType,
}
// 6. 调用媒体处理器上传
url, size, err := mh.Upload(fdef, file)
// 7. 完成上传
fdef, err = store.Files.FinishUpload(fdef, true, size)
// 8. 返回结果
params := map[string]string{"url": url}
writeHttpResponse(NoErrParams(msgID, "", now, params), nil)
}
2. 文件下载
func largeFileServeHTTP(wrt http.ResponseWriter, req *http.Request) {
// 1. 验证 API Key
if isValid, _ := checkAPIKey(getAPIKey(req)); !isValid {
writeHttpResponse(ErrAPIKeyRequired(now), err)
return
}
// 2. 认证检查
authMethod, secret := getHttpAuth(req)
uid, challenge, err := authFileRequest(authMethod, secret, req.FormValue("sid"), getRemoteAddr(req))
// 3. 调用媒体处理器下载
fd, rsc, err := mh.Download(req.URL.String())
defer rsc.Close()
// 4. 设置响应头
wrt.Header().Set("Content-Type", fd.MimeType)
if asAttachment {
wrt.Header().Set("Content-Disposition", "attachment")
}
// 5. 流式传输文件
http.ServeContent(wrt, req, "", fd.UpdatedAt, rsc)
}
3. gRPC 文件传输
// 文件上传(gRPC)
func (*grpcNodeServer) LargeFileReceive(stream pbx.Node_LargeFileReceiveServer) error {
// 接收第一个请求(包含元数据)
req, err := stream.Recv()
// 认证
authMethod, secret := req.Auth.Scheme, req.Auth.Secret
uid, challenge, err := authFileRequest(authMethod, secret, "", remoteAddr)
// 创建管道用于流式读取
reader, writer := io.Pipe()
// 后台协程读取分块数据
go func() {
defer writer.Close()
for {
if req, err := stream.Recv(); err == nil {
chunk := req.GetContent()
writer.Write(chunk)
}
}
}()
// 上传到媒体处理器
url, size, err := mh.Upload(fdef, reader)
// 关闭并返回
stream.SendAndClose(&pbx.FileUpResp{...})
}
// 文件下载(gRPC)
func (*grpcNodeServer) LargeFileServe(req *pbx.FileDownReq, stream pbx.Node_LargeFileServeServer) error {
// 认证
uid, challenge, err := authFileRequest(...)
// 下载文件
fd, rsc, err := mh.Download(req.GetUri())
defer rsc.Close()
// 分块发送
resp.Content = make([]byte, 1024*1024*2) // 2MB 缓冲
for {
n, err := rsc.Read(resp.Content)
if err == io.EOF {
break
}
resp.Content = resp.Content[:n]
stream.Send(&resp)
}
}
认证方式
HTTP 文件传输支持多种认证方式(按优先级):
| 优先级 | 方式 | 格式 |
|---|---|---|
| 1 | HTTP 头 | Authorization: Basic base64(scheme:secret) |
| 2 | URL 参数 | ?auth=basic&secret=xxx |
| 3 | 表单值 | auth=basic&secret=xxx |
| 4 | Cookie | auth=basic; secret=xxx |
| 5 | 会话 ID | sid=xxx(已认证会话) |
MIME 类型限制
允许的 MIME 类型前缀:
var allowedMimeTypes = []string{
"application/",
"audio/",
"font/",
"image/",
"text/",
"video/",
}
其他类型自动转换为 application/octet-stream。
响应格式
上传成功
{
"ctrl": {
"code": 200,
"text": "ok",
"params": {
"url": "/v0/file/s/abcdef12345.jpg",
"expires": "2024-01-02T00:00:00.000Z"
}
}
}
重定向(307)
某些情况下媒体处理器返回重定向:
{
"ctrl": {
"code": 307,
"text": "Temporary Redirect",
"params": {
"url": "https://s3.amazonaws.com/bucket/file.jpg"
}
}
}
文件垃圾回收
func largeFileRunGarbageCollection(period time.Duration, blockSize int) chan<- bool {
stop := make(chan bool)
go func() {
// 添加随机抖动避免集群节点同时运行
period = (period >> 1) + (period >> 2) + time.Duration(rand.Intn(int(period>>1)))
gcTicker := time.Tick(period)
for {
select {
case <-gcTicker:
// 删除未使用的文件
store.Files.DeleteUnused(time.Now().Add(-time.Hour), blockSize)
case <-stop:
return
}
}
}()
return stop
}
消息格式
通用消息结构
所有协议共享相同的消息语义,只是序列化格式不同:
| 协议 | 序列化格式 | 特点 |
|---|---|---|
| WebSocket | JSON | 文本、易调试 |
| 长轮询 | JSON | 与 WebSocket 相同 |
| gRPC | Protobuf | 二进制、高效 |
消息类型总览
客户端到服务端
| 消息 | 说明 | 用途 |
|---|---|---|
{hi} |
握手 | 建立连接 |
{acc} |
账户 | 创建/修改账户 |
{login} |
登录 | 认证 |
{sub} |
订阅 | 订阅话题 |
{leave} |
离开 | 离开话题 |
{pub} |
发布 | 发送消息 |
{get} |
查询 | 获取数据 |
{set} |
更新 | 修改数据 |
{del} |
删除 | 删除数据 |
{note} |
通知 | 临时通知 |
服务端到客户端
| 消息 | 说明 | 用途 |
|---|---|---|
{ctrl} |
控制响应 | 请求响应 |
{data} |
数据消息 | 消息内容 |
{meta} |
元数据 | 话题信息 |
{pres} |
在线状态 | 状态变更 |
{info} |
信息 | 转发的通知 |
协议对比
功能对比
| 功能 | WebSocket | gRPC | 长轮询 | HTTP |
|---|---|---|---|---|
| 实时消息 | ✅ | ✅ | ⚠️ | ❌ |
| 双向通信 | ✅ | ✅ | ❌ | ❌ |
| 流式传输 | ❌ | ✅ | ❌ | ⚠️ |
| 文件传输 | ❌ | ✅ | ❌ | ✅ |
| 浏览器支持 | ✅ | ❌ | ✅ | ✅ |
| 防火墙友好 | ⚠️ | ❌ | ✅ | ✅ |
| 性能 | 高 | 最高 | 低 | 中 |
| 带宽效率 | 中 | 高 | 低 | 中 |
使用场景
| 场景 | 推荐协议 | 原因 |
|---|---|---|
| Web 应用 | WebSocket | 实时性、浏览器原生支持 |
| 移动应用 | gRPC | 高性能、省流量 |
| CLI 工具 | gRPC | 强类型、易开发 |
| 聊天机器人 | gRPC | Plugin API、流式事件 |
| 老旧浏览器 | 长轮询 | 兼容性 |
| 严格防火墙环境 | 长轮询 | 纯 HTTP |
| 文件上传下载 | HTTP | 大文件支持 |
性能对比
| 指标 | WebSocket | gRPC | 长轮询 |
|---|---|---|---|
| 连接建立 | ~50ms | ~30ms | ~100ms |
| 消息延迟 | ~10ms | ~5ms | ~100ms |
| CPU 占用 | 中 | 低 | 高 |
| 内存占用 | 中 | 低 | 中 |
| 带宽占用 | 中 | 低 | 高 |
附录
配置示例
{
"listen": ":6060",
"grpc_listen": ":16060",
"max_message_size": 262144,
"idle_session_timeout": 60000,
"ws_compression": true,
"api_key_salt": "your-hmac-salt",
"expvar": "/debug/vars"
}
调试技巧
1. WebSocket 调试
使用浏览器控制台:
const ws = new WebSocket('ws://localhost:6060/v0/channels');
ws.onopen = () => console.log('Connected');
ws.onmessage = (e) => console.log('Received:', e.data);
ws.send(JSON.stringify({hi: {ver: "0.22", ua: "test"}}));
2. gRPC 调试
使用 grpcurl 工具:
# 列出服务
grpcurl -plaintext localhost:16060 list
# 调用方法
grpcurl -plaintext -d '{"hi": {"ver": "0.22"}}' localhost:16060 pbx.Node/MessageLoop
3. 长轮询调试
使用 curl:
# 创建会话
curl -X POST http://localhost:6060/v0/channels/lp
# 发送消息
curl -X POST "http://localhost:6060/v0/channels/lp?sid=SESSION_ID" \
-d '{"login": {"scheme": "basic", "secret": "dGVzdDp0ZXN0"}}'
# 长轮询
curl "http://localhost:6060/v0/channels/lp?sid=SESSION_ID"
相关文件
| 文件 | 说明 |
|---|---|
server/hdl_websock.go |
WebSocket 处理 |
server/hdl_grpc.go |
gRPC 处理 |
server/hdl_longpoll.go |
长轮询处理 |
server/hdl_files.go |
文件传输处理 |
pbx/model.proto |
gRPC 协议定义 |
server/http.go |
HTTP 服务器 |
server/session.go |
会话管理 |
server/datamodel.go |
消息结构定义 |