4. WebSocket基础:协议原理、与REST的区别、连接建立与心跳维持
各位同学好。今天我们来聊聊WebSocket。
做量化交易,尤其是做高频或者中频策略的同学,一定绕不开WebSocket。为什么?因为行情数据是实时流,你不可能每隔一秒去问交易所「有没有新价格」——那样太慢了,也太蠢了。
我个人习惯把WebSocket理解为「交易所主动给你打电话」。而REST嘛,是你每隔几秒打一次电话问「有变化吗?」。你想想看,哪个效率更高?
4.1 WebSocket协议原理
WebSocket是一个全双工通信协议。什么意思?就是客户端和服务器可以同时说话,不用等对方说完。
它建立在TCP之上。但和HTTP不同,WebSocket一旦连接建立,就不会再发送HTTP头部了。它只发送很小的数据帧。我做过测试,一个WebSocket帧的头部开销只有2到14个字节,而HTTP请求光头部就几百字节。做量化交易,毫秒必争,这个差距很关键。
协议的核心流程是这样的:
- 客户端发起HTTP升级请求
- 服务器返回101状态码,表示切换协议
- 连接从HTTP切换到WebSocket
- 之后双方通过帧(frame)来传输数据
帧的类型主要有几种:文本帧、二进制帧、Ping/Pong帧、关闭帧。做行情订阅时,我们主要用文本帧(JSON格式)和Ping/Pong帧(心跳)。
核心要点:WebSocket不是HTTP的替代品,而是对HTTP的补充。它专门解决「实时推送」这个场景。
4.2 与REST的区别
很多新手会问:我直接用REST轮询不行吗?
行,但代价很大。我拿一个真实项目举例。
之前有个团队做币安现货行情监控,他们用REST每500毫秒请求一次深度数据。结果呢?交易所直接把他们IP限流了。而且他们拿到的数据永远是「上一刻的快照」,不是实时流。做高频策略,这种延迟根本没法用。
我整理了一个对比表,你一看就明白:
| 对比维度 | REST | WebSocket |
|---|---|---|
| 通信模式 | 请求-响应(一问一答) | 全双工(双方随时发) |
| 数据推送 | 客户端主动轮询 | 服务器主动推送 |
| 连接开销 | 每次请求都建立新连接 | 一次连接,长期保持 |
| 头部大小 | 几百字节 | 2-14字节 |
| 实时性 | 取决于轮询间隔 | 毫秒级 |
| 适用场景 | 查询历史数据、下单 | 行情订阅、实时成交 |
说白了,REST适合「你问它答」的场景,比如查账户余额、查历史K线。WebSocket适合「它主动告诉你」的场景,比如价格变了、订单成交了。
我的建议:做量化交易系统,REST和WebSocket要配合使用。REST做控制(下单、查询),WebSocket做数据流(行情、成交回报)。两者缺一不可。
4.3 连接建立过程
WebSocket的连接建立,其实就是一个HTTP升级握手。我画了一张流程图,帮你理解这个过程:
握手过程其实很简单。客户端发一个HTTP GET请求,带上Upgrade: websocket头。服务器验证通过后,返回101状态码。之后连接就「升级」成了WebSocket。
这里有个细节:服务器返回的Sec-WebSocket-Accept字段,是对客户端Sec-WebSocket-Key的哈希结果。这是为了防止缓存代理误把WebSocket请求当成普通HTTP请求。嗯,这个你了解就行,写代码时库会帮你处理。
4.4 心跳维持
连接建立之后,最怕什么?断线。
交易所的WebSocket服务器通常不会一直保持连接。如果一段时间没有数据交互,网关或者防火墙可能会把连接断开。所以我们需要心跳机制。
心跳说白了就是:客户端定期给服务器发一个Ping帧,服务器回复Pong帧。如果连续几次Pong没收到,就认为连接断了,需要重连。
我曾经踩过一个坑。有个交易所的WebSocket,心跳间隔是30秒。我一开始设了60秒发一次Ping,结果经常断线。后来查文档才发现,交易所要求客户端必须在30秒内发送任意数据帧,否则服务器会主动断开。
注意:不同交易所的心跳策略不一样。有的要求客户端发Ping,有的服务器主动发Ping,有的只需要发任意数据帧即可。一定要仔细看文档。
下面是我常用的心跳代码模板:
import asyncio
import websockets
async def heartbeat(ws, interval=20):
"""每20秒发送一次Ping"""
while True:
try:
await ws.ping()
print(f"[{time.time():.3f}] 发送Ping")
await asyncio.sleep(interval)
except Exception as e:
print(f"心跳异常: {e}")
break
async def receive_messages(ws):
"""接收消息并处理"""
async for msg in ws:
# 处理行情数据
print(f"收到: {msg[:50]}...")
async def main():
uri = "wss://stream.binance.com:9443/ws/btcusdt@trade"
async with websockets.connect(uri) as ws:
# 同时运行心跳和接收
await asyncio.gather(
heartbeat(ws, interval=20),
receive_messages(ws)
)
asyncio.run(main())
这段代码里,我用了asyncio.gather同时跑两个协程。一个负责发心跳,一个负责收数据。这样互不干扰。
避坑指南:我曾经遇到过一个问题——心跳协程和接收协程共用同一个连接,如果接收协程卡住了,心跳也发不出去。后来我加了一个超时机制:如果5秒内没有收到任何数据(包括Pong),就主动重连。这个策略帮我避免了很多「假连接」的问题。
4.5 重连策略
连接断了怎么办?重连。
但重连不是简单的「断了就重连」。如果交易所服务器正在重启,你疯狂重连只会把自己IP封掉。
我常用的策略是指数退避:
- 第一次重连等待1秒
- 第二次等待2秒
- 第三次等待4秒
- 第四次等待8秒
- ...最多等待60秒
同时,重连后要重新订阅行情。因为连接断了,之前的订阅关系也就丢了。
嗯,这里有个小技巧:重连时不要立即订阅所有品种。先订阅一个测试品种,确认连接正常后,再批量订阅。这样可以避免因为订阅消息太多导致连接不稳定。
好了,WebSocket的基础就讲到这里。总结一下:WebSocket是量化交易中获取实时行情的关键技术。它通过HTTP升级握手建立连接,之后用轻量级的帧传输数据。心跳和重连是保证连接稳定性的核心手段。
下一节我们会深入实战,用Python写一个完整的WebSocket行情订阅客户端。到时候我会带你手撕代码,把今天讲的理论都用上。
课后思考:如果你同时订阅了100个交易对的深度数据,WebSocket连接会不会成为瓶颈?该怎么优化?