# Tinode 协议详解 > 本文档详细介绍 Tinode 支持的所有通信协议,包括协议原理、消息格式、实现细节和使用方法。 --- ## 目录 - [协议概述](#协议概述) - [WebSocket 协议](#websocket-协议) - [gRPC 协议](#grpc-协议) - [长轮询协议](#长轮询协议) - [HTTP 文件传输协议](#http-文件传输协议) - [消息格式](#消息格式) - [协议对比](#协议对比) --- ## 协议概述 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 的心跳配置: ```go const ( writeWait = 10 * time.Second // 写操作超时 pongWait = idleSessionTimeout // 等待 Pong 超时(默认 45 秒) pingPeriod = 15 * time.Second // 发送 Ping 周期(15 秒) ) ``` **心跳流程**: ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ WebSocket 心跳流程 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ 服务器 writeLoop: │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ for { │ │ │ │ select { │ │ │ │ case <-ticker.C: // 每 15 秒 │ │ │ │ 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 头] ``` --- ### 消息流转完整流程 #### 前端发送消息 ```javascript // 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. 连接升级 ```go // 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. 读循环(接收消息) ```go 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. 写循环(发送消息) ```go 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` | `idleSessionTimeout` | 等待 Pong 超时(默认 45 秒) | | `pingPeriod` | 15 秒 | 发送 Ping 周期 | ### 消息格式 WebSocket 消息使用 **JSON 文本格式**: ```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 服务(客户端实现) ```protobuf service Node { // 消息循环:双向流 rpc MessageLoop(stream ClientMsg) returns (stream ServerMsg) {} // 大文件上传:客户端流 rpc LargeFileReceive(stream FileUpReq) returns (FileUpResp) {} // 大文件下载:服务端流 rpc LargeFileServe(FileDownReq) returns (stream FileDownResp) {} } ``` #### Plugin 服务(插件实现) ```protobuf 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. 消息循环 ```go 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. 服务器启动 ```go 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) ```protobuf 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) ```protobuf message ServerMsg { oneof Message { ServerCtrl ctrl = 1; // 控制响应 ServerData data = 2; // 数据消息 ServerPres pres = 3; // 在线状态 ServerMeta meta = 4; // 元数据 ServerInfo info = 5; // 信息通知 } } ``` ### 文件传输 #### 上传(客户端流) ```protobuf 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 } ``` #### 下载(服务端流) ```protobuf 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. 会话创建 ```go 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. 消息读取 ```go 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. 长轮询等待 ```go 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 头设置 ```go // 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. 文件上传 ```go 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. 文件下载 ```go 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 文件传输 ```go // 文件上传(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 类型前缀: ```go var allowedMimeTypes = []string{ "application/", "audio/", "font/", "image/", "text/", "video/", } ``` 其他类型自动转换为 `application/octet-stream`。 ### 响应格式 #### 上传成功 ```json { "ctrl": { "code": 200, "text": "ok", "params": { "url": "/v0/file/s/abcdef12345.jpg", "expires": "2024-01-02T00:00:00.000Z" } } } ``` #### 重定向(307) 某些情况下媒体处理器返回重定向: ```json { "ctrl": { "code": 307, "text": "Temporary Redirect", "params": { "url": "https://s3.amazonaws.com/bucket/file.jpg" } } } ``` ### 文件垃圾回收 ```go 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 占用 | 中 | 低 | 高 | | 内存占用 | 中 | 低 | 中 | | 带宽占用 | 中 | 低 | 高 | --- ## 附录 ### 配置示例 ```json { "listen": ":6060", "grpc_listen": ":16060", "max_message_size": 262144, "idle_session_timeout": 45000, "ws_compression": true, "api_key_salt": "your-hmac-salt", "expvar": "/debug/vars" } ``` ### 调试技巧 #### 1. WebSocket 调试 使用浏览器控制台: ```javascript 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` 工具: ```bash # 列出服务 grpcurl -plaintext localhost:16060 list # 调用方法 grpcurl -plaintext -d '{"hi": {"ver": "0.22"}}' localhost:16060 pbx.Node/MessageLoop ``` #### 3. 长轮询调试 使用 `curl`: ```bash # 创建会话 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` | 消息结构定义 |