3. WebSocket握手与订阅报文构造

行情流消费的第一步不是“收数据”,而是“让服务端认可你、并告诉它你要什么”。这一步由两段报文共同完成:HTTP Upgrade 握手负责建立连接并完成鉴权,订阅报文负责声明频道与交易对。两者字段错一个,后续要么连不上,要么连上了却收不到目标行情。本节围绕连接 URL、请求头、订阅消息格式、频道与交易对参数、心跳与鉴权字段的组装展开。

一、连接 URL:把环境、路径与公共参数分开看

行情服务的 WebSocket 地址通常由三部分构成:协议与主机(wss://stream.example-exchange.com)、版本化路径(/v2/market)、以及少量公共查询参数。工程上建议把这三段拆成配置项,而不是在代码里拼一整个字符串,便于在测试网与生产网之间切换。示例配置如下(示例参数,非真实交易所):

{
  "wsUrl": "wss://stream.example-exchange.com/v2/market",
  "publicPath": "/ws/public",
  "privatePath": "/ws/private",
  "pingIntervalMs": 20000,
  "pongTimeoutMs": 5000
}

注意区分“公共行情”和“私有行情”两条路径:公共频道(深度、成交、K 线)一般无需鉴权,私有频道(账户、订单)必须携带鉴权字段。若把私有订阅发到公共路径,服务端通常直接断开或返回错误码,而不是静默忽略。

二、握手请求头:Upgrade 与鉴权字段的组装

WebSocket 握手本质是一次带 Upgrade: websocket 的 HTTP 请求。浏览器环境由 WebSocket 构造函数自动生成 Sec-WebSocket-Key 等头,开发者无法自定义请求头;因此在需要携带 Authorization 或自定义签名头的场景,通常改用“连接后发送登录报文”的方式,或使用支持自定义头的 Node.js/Go 客户端。以 Node.js 的 ws 库为例:

const ws = new WebSocket(url, {
  headers: {
    "X-Api-Key": apiKey,
    "X-Timestamp": String(Date.now()),
    "X-Signature": sign(secret, timestamp + path)
  }
});

签名串的拼接顺序必须与服务端约定完全一致(常见为 timestamp + method + path),时间戳偏差一般限制在 30 秒内,超出即拒绝。若使用浏览器端,则改为连接成功后立即发送一条 {"op":"login","args":[...]} 报文完成鉴权,并在收到 login success 回执后再发订阅,避免订阅被丢弃。

三、订阅消息格式:JSON 与 Protobuf 的取舍

JSON 订阅报文可读性好、调试方便,适合开发联调阶段;Protobuf 体积小、解析快,适合高频深度与逐笔成交。两者在结构上都是“操作类型 + 参数列表”,差异在于编码方式。JSON 示例:

{
  "op": "subscribe",
  "args": [
    { "channel": "books5", "instId": "BTC-USDT" },
    { "channel": "trades", "instId": "ETH-USDT" }
  ]
}

Protobuf 则需先按 .proto 定义消息结构,再用生成的类序列化后以二进制帧发送。选择建议:调试期用 JSON 快速验证频道名与交易对拼写;上线后若单连接订阅超过数十个频道、或消息频率达到每秒数百条,再评估切换 Protobuf,并同步改造心跳与错误处理逻辑。

四、频道与交易对参数:拼写与大小写是高频错误源

频道名(如 books5、trades、candle1m)和交易对(如 BTC-USDT)必须与服务端文档逐字一致。常见错误包括:把 BTC-USDT 写成 BTCUSDT、把 books5 写成 book5、交易对大小写混用。建议在代码中维护一份频道与交易对的枚举常量,订阅前做一次白名单校验,把非法参数在本地拦截并打印,而不是等连接被断开后再排查。

握手与订阅报文组装链路 连接配置 wss 主机 + 版本路径 公共 / 私有路径 心跳间隔参数 握手请求头 Upgrade: websocket X-Api-Key / 时间戳 X-Signature 签名 订阅报文 op: subscribe channel + instId JSON / Protobuf 编码 连接就绪 101 Switching 订阅回执 开始推送行情 心跳与鉴权字段 ping 间隔 20s → 等待 pong 超时 5s → 超时则重连并重新握手、重新订阅 常见失败点 交易对拼写错误 / 频道名大小写不符 / 签名时间戳超 30s / 私有订阅发到公共路径

五、心跳与鉴权字段:让连接“活着”且“被信任”

心跳有两种常见形态:协议层 Ping/Pong 帧,以及应用层 {"op":"ping"} 文本报文。前者由客户端库自动处理,后者需自行定时发送并校验回执。无论哪种,都要设置“发送间隔”和“等待超时”两个参数:例如每 20 秒发一次 ping,若 5 秒内未收到 pong,则判定连接假死,主动关闭并重连。重连后必须重新握手、重新鉴权、重新订阅,不能只重发订阅报文。

鉴权字段的组装顺序建议固定为:读取密钥 → 生成毫秒时间戳 → 按约定拼接签名串 → 计算签名 → 写入请求头或登录报文。下面是一份可直接对照使用的订阅构造检查表(示例值,需替换为实际环境参数):

检查项示例值判断依据
连接地址wss://stream.example-exchange.com/v2/market协议为 wss,路径含版本号
公共/私有路径/ws/public、/ws/private私有频道必须走 private
鉴权头X-Api-Key、X-Timestamp、X-Signature时间戳偏差 ≤ 30s
订阅操作op = subscribe回执含 subscribe success
频道与交易对books5 / BTC-USDT与文档逐字一致
心跳20s 发送,5s 超时连续超时即重连
联调阶段建议先只订阅一个频道、一个交易对,确认收到首条推送后再批量增加。批量订阅时若某一条参数非法,部分服务端会整包拒绝,逐条增加能快速定位问题条目。
不要把 API Secret 写进前端代码或日志。浏览器端应通过后端签发短期令牌,再由前端在登录报文中携带;服务端日志中打印签名头时需脱敏。