- 后端
- 即时通讯
- 微服务
【免费下载链接】goim
goim
本篇技术指南以 docs/en/proto.md 为核心骨架,系统讲解 goim 中 comet 长连接服务与客户端之间的两种通讯协议——WebSocket(JSON 帧)与 TCP(二进制帧),并结合仓库源码逐字节拆解协议头布局、操作码(Operation)语义、心跳与认证握手流程。读完本文,你将掌握 goim 协议帧的编解码细节、客户端如何构造认证与心跳包,以及如何对照源码验证协议实现的正确性。
协议概览:comet 支持的两类客户端通讯通道
goim 架构中,comet是负责维持海量客户端长连接的接入层组件,与客户端通讯支持两种协议:
| 协议 | 传输层 | 数据封装 | 适用场景 |
|---|---|---|---|
| WebSocket | HTTP/WS(也可启用 TLS 形成 WSS) | JSON Frame | 浏览器端(如 examples/javascript/index.html) |
| TCP | 原生 TCP | 二进制帧 | 移动端 / 高性能长连接客户端 |
两种协议的请求与返回协议一致:即客户端发出的认证请求、心跳请求与服务端返回的响应、下行推送,使用完全相同的帧结构,区别仅在于承载层是 WebSocket 消息还是裸 TCP 字节流。
WebSocket 协议:ws://DOMAIN/sub
请求 URL
ws://DOMAIN/subDOMAIN替换为实际部署的 comet 域名或 IP;- 路径
/sub是 comet 的固定 WebSocket 订阅路径。这一要求写死在源码中:在 internal/comet/server_websocket.go 的ServeWebsocket里,通过websocket.ReadRequest(rr)读取 HTTP 握手请求后,直接校验req.RequestURI != "/sub",路径不匹配即关闭连接并退出握手流程; - 若在 comet 配置中开启
tlsOpen,则对应wss://DOMAIN/sub,监听端口由websocket.tlsBind指定(见 cmd/comet/comet-example.toml)。
请求与返回 JSON 示例
WebSocket 使用 JSON 帧,请求与返回结构一致:
{ "ver": 102, "op": 10, "seq": 10, "body": {"data": "xxx"} }注意:文档中的op: 10对应源码中的OpProtoReady(协议就绪),这里用于示意「任意合法操作码」,实际业务推送时op多为下行消息操作码。
请求和返回参数说明
| 参数 | 必选 | 类型 | 说明 |
|---|---|---|---|
| ver | true | int | 协议版本号 |
| op | true | int | 指令(Operation),决定该帧是认证、心跳还是业务消息 |
| seq | true | int | 序列号,服务端返回的 seq 与客户端发送的 seq 一一对应,用于请求响应配对 |
| body | true | json | 包体:WebSocket 场景下在认证帧中承载授权令牌,用于校验并获取真实用户 ID |
WebSocket 帧的二进制本质
虽然 WebSocket 层封装为 JSON,但 goim 实际传输的是二进制消息(Binary Frame)+ 定长二进制协议头。从源码 api/protocol/protocol.go 的WriteWebsocket/ReadWebsocket可以看出,服务端先写一个websocket.BinaryMessage类型的消息头,再在消息体内写入与 TCP 完全一致的 16 字节二进制协议头,最后写入 JSON body。也就是说,JSON 只是 body 的呈现形式,头部仍是紧凑的二进制布局。
TCP 协议:tcp://DOMAIN 与二进制帧
请求 URL
tcp://DOMAINcomet 的 TCP 监听端口默认配置为:3101(见 internal/comet/conf/conf.go 中的TCP.Bind与 cmd/comet/comet-example.toml 的[tcp] bind = [":3101"])。客户端连接后需立刻进入认证流程,认证前发送的任意非认证帧都会被拒绝。
协议格式
二进制,请求和返回协议一致。每个数据包由16 字节定长协议头 + 变长 body组成。
请求 & 返回参数
| 参数 | 必选 | 类型 | 说明 |
|---|---|---|---|
| package length | true | int32 bigendian | 包总长度(协议头 + body) |
| header Length | true | int16 bigendian | 协议头长度,固定为 16 |
| ver | true | int16 bigendian | 协议版本 |
| operation | true | int32 bigendian | 协议指令(操作码) |
| seq | true | int32 bigendian | 序列号(jsonp 回调场景下也可承载回调标识) |
| body | false | binary | 包体,长度 = package length - header length |
协议头逐字节布局(源码级)
源码 api/protocol/protocol.go 通过常量精确定义了头部各字段的偏移与宽度:
| 字段 | 偏移(字节) | 宽度(字节) | 编码 |
|---|---|---|---|
| packLen(包长度) | 0 | 4 | big-endian int32 |
| headerLen(包头长度) | 4 | 2 | big-endian int16 |
| ver(版本) | 6 | 2 | big-endian int16 |
| op(操作码) | 8 | 4 | big-endian int32 |
| seq(序列号) | 12 | 4 | big-endian int32 |
| body | 16 | 变长 | 原始二进制 |
对应源码中的常量_rawHeaderSize = _packSize + _headerSize + _verSize + _opSize + _seqSize = 4 + 2 + 2 + 4 + 4 = 16。
同时源码做了两层长度合法性校验(ReadTCP/ReadWebsocket):
MaxBodySize = 1 << 12(4096 字节),若packLen > MaxBodySize + 16(即_maxPackSize),返回ErrProtoPackLen;- 若
headerLen != 16(_rawHeaderSize),返回ErrProtoHeaderLen。
这两条校验是 goim 客户端接入时必须遵守的硬性约束:单帧最大包长 = 4096 + 16 = 4112 字节。
心跳帧的特殊性:body 携带房间在线人数
普通帧的 body 为业务消息,但心跳回复帧(op=3)例外。源码提供了两个专用方法WriteTCPHeart与WriteWebsocketHeart:在 16 字节头部之后再写入 4 字节的online(int32 bigendian),即该帧包长为16 + 4 = 20字节。internal/comet的 dispatch 循环中,当收到客户端心跳(op=2)并回写OpHeartbeatReply(op=3)时,会取当前房间在线数ch.Room.OnlineNum()一并下发。因此客户端解析心跳回复时,应读取 body 前 4 字节作为该房间的实时在线人数(参考 internal/comet/server_tcp.go 的dispatchTCP)。
指令(Operation)定义
docs/en/proto.md 列出的核心指令为:
| 指令 | 说明 |
|---|---|
| 2 | 客户端请求心跳 |
| 3 | 服务端心跳答复 |
| 7 | auth 认证 |
| 8 | auth 认证返回 |
以上仅为最常用的 4 个指令。完整操作码由 api/protocol/operation.go 统一定义,共 18 个:
| 操作码 | 常量名 | 说明 |
|---|---|---|
| 0 | OpHandshake | 握手 |
| 1 | OpHandshakeReply | 握手返回 |
| 2 | OpHeartbeat | 心跳 |
| 3 | OpHeartbeatReply | 心跳返回 |
| 4 | OpSendMsg | 发送消息 |
| 5 | OpSendMsgReply | 发送消息返回 |
| 6 | OpDisconnectReply | 断开连接返回 |
| 7 | OpAuth | 认证 |
| 8 | OpAuthReply | 认证返回 |
| 9 | OpRaw | 原始(未解析)消息 |
| 10 | OpProtoReady | 协议就绪 |
| 11 | OpProtoFinish | 协议结束 |
| 12 | OpChangeRoom | 切换房间 |
| 13 | OpChangeRoomReply | 切换房间返回 |
| 14 | OpSub | 订阅指令 |
| 15 | OpSubReply | 订阅返回 |
| 16 | OpUnsub | 取消订阅 |
| 17 | OpUnsubReply | 取消订阅返回 |
除心跳、认证外的操作码(如 12/14/16)在 comet 的Operate方法(internal/comet/operation.go)中处理:OpChangeRoom调用 bucket 切换房间并回包OpChangeRoomReply,OpSub/OpUnsub通过逗号分隔的指令列表更新通道的 Watch 集合并回包 Reply;其余未知操作码会走Receive上报给 logic 处理。
连接生命周期与认证握手
三步握手:从建立连接到认证完成
参考 docs/handshake.png 所示的流程,一次完整的连接建立包含:
- TCP/WS 连接建立:客户端连上 comet 后,必须先发送认证帧(op=7)。comet 的认证循环会持续读取并校验
p.Op == protocol.OpAuth,在此之前收到的任何其他操作码都会被记录为"request operation(%d) not auth"并忽略(见 internal/comet/server_tcp.go 的authTCP与 internal/comet/server_websocket.go 的authWebsocket); - 认证请求(op=7):认证帧的 body 承载授权令牌(token)。comet 通过
s.Connect(ctx, p, cookie)将token、cookie、serverID组装为logic.ConnectReq,经 gRPC 调用 logic 服务完成身份校验与真实用户 ID(mid)获取(见 internal/comet/operation.go 的Connect与 api/logic/logic.proto 的ConnectReq/ConnectReply); - 认证返回(op=8):认证成功后 comet 将帧的 op 改为
OpAuthReply、清空 body 后写回客户端,随后将通道注册进对应的 bucket 并启动读/写双 goroutine 开始长连接服务。握手阶段受protocol.handshakeTimeout限制(默认 5 秒,cmd/comet/comet-example.toml 中配置为 8s),超时未完成认证连接会被强制关闭。
心跳保活:客户端主动、服务端应答
连接建立后,客户端需周期性发送心跳帧(op=2,无 body,包长固定 16 字节)。comet 收到后:
- 更新该通道的定时器,把 op 改写为
OpHeartbeatReply(op=3); - 若帧属于某个房间,则带上房间在线人数(20 字节帧)回写;
- 每隔
serverHeartbeat(随机化,避免同时刻风暴)还会向 logic 上报一次在线心跳,刷新用户在线状态与过期时间。
客户端应据服务端返回的heartbeat时长(来自ConnectReply)设定心跳周期,超过该周期未发送心跳的连接将被服务端判定超时并回收。
客户端实现参考:JavaScript WebSocket 客户端
仓库提供了可直接运行的前端示例 examples/javascript/client.js,其协议处理逻辑与上述二进制布局一一对应:
- 头部常量:
rawHeaderLen = 16,与源码_rawHeaderSize一致; - 认证:构造 16 字节头 + JSON token body,
op = 7,body 示例为'{"mid":123, "room_id":"live://1000", "platform":"web", "accepts":[1000,1001,1002]}'——即 token 中包含用户 mid、房间 ID、平台与可订阅指令列表; - 心跳:发送纯 16 字节头帧,
op = 2,每 30 秒一次; - 响应解析:按
packetOffset/headerOffset/verOffset/opOffset/seqOffset逐字段解析头部;对op=9(OpRaw 批量消息)按rawHeaderLen循环切分多个子帧;其余 op 按headerLen..packetLen切片解码 body; - 断线重连:
ws.onclose后按指数退避策略(初始 15 秒、逐次翻倍、最多 10 次)自动重连。
用go run examples/javascript/main.go启动静态服务器后,浏览器访问:1999即可观察完整的认证、心跳、推送收发过程。
协议相关的配置要点
在 cmd/comet/comet-example.toml 中,与客户端通讯协议直接相关的配置项:
| 配置节 | 字段 | 说明 |
|---|---|---|
| [tcp] | bind = [":3101"] | TCP 监听地址(客户端连接入口) |
| [websocket] | bind = [":3102"] | WebSocket 监听地址 |
| [websocket] | tlsOpen / tlsBind / certFile / privateFile | 是否开启 WSS 及证书配置 |
| [protocol] | cliProto = 5 | 客户端上行协议缓冲(读环大小) |
| [protocol] | svrProto = 10 | 服务端下行协议缓冲(写环大小) |
| [protocol] | handshakeTimeout = "8s" | 握手超时时间 |
其中cliProto/svrProto直接决定了单连接内上、下行在途帧的缓冲容量,若业务推送量大,可适当调大svrProto以降低背压丢弃风险。
小结
goim 的客户端通讯协议设计非常克制:WebSocket 与 TCP 共用同一套 16 字节二进制协议头,body 分别是 JSON 与原始二进制;操作码以 0~17 的枚举覆盖握手、认证、心跳、消息、订阅/退订、房间切换等全生命周期行为;心跳回复帧额外携带房间在线人数,一条消息同时完成保活与状态同步。理解这套协议帧布局与操作码语义,是编写 goim 客户端 SDK、排查长连接异常、或基于协议头自行实现跨语言客户端的起点。协议头结构总览见 docs/protocol.png。
延伸阅读
- 协议帧编解码实现:api/protocol/protocol.go
- 操作码完整定义:api/protocol/operation.go
- 协议消息结构定义:api/protocol/protocol.proto
- TCP 服务端接入处理:internal/comet/server_tcp.go
- WebSocket 服务端接入处理:internal/comet/server_websocket.go
- 服务端配置结构:internal/comet/conf/conf.go
- 前端 JS 客户端示例:examples/javascript/client.js
- 中文版协议文档:docs/proto.md
- 下行推送 HTTP 接口协议:docs/en/push.md
- 后端
- 即时通讯
- 微服务
【免费下载链接】goim
goim
相关推荐
goim 客户端通讯协议完全指南:WebSocket 与 TCP 二进制包格式、指令详解与源码级解析
goim 客户端通讯协议完全指南:WebSocket 与 TCP 二进制包格式、指令详解与源码级解析 本文以 goim 开源仓库的官方通讯协议文档 docs/p
后端即时通讯微服务VPet 虚拟桌宠 MOD 制作完整教程:从命名规则到代码插件,从零开始定制你的桌宠
VPet 虚拟桌宠 MOD 制作完整教程:从命名规则到代码插件,从零开始定制你的桌宠 VPet https://link.gitcode.com/i/5a0ad
桌面应用游戏开发Cherry Studio LAN 传输协议(v1)完全解析:mDNS 发现、TCP 握手与二进制分帧文件传输
Cherry Studio LAN 传输协议(v1)完全解析:mDNS 发现、TCP 握手与二进制分帧文件传输 本文以 docs/references/lan
人工智能大模型AI 应用交互助手本地部署
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考