3、Python WebSocket客户端实战:使用websockets库建立连接、发送订阅消息、接收行情数据

好,咱们直接进入正题。前面两章我们把WebSocket协议和行情数据的格式讲清楚了,这一章就是动手干活的时刻。我个人习惯用Python的websockets库来做客户端,原因很简单:它原生支持asyncio,代码写起来干净利落,不像某些库回调套回调,调试起来头疼。

3.1 环境准备与库安装

先装库,这个没啥好说的:

pip install websockets

嗯,就这一行。如果你用的是Python 3.7以上版本,标准库里的asyncio已经够用了,不需要额外装。我在项目中遇到过有人装了websocket-client(注意少了个s),结果API完全不同,折腾了半天才发现装错了库。你想想看,这种低级错误多耽误事。

3.2 建立连接——别小看这一步

连接WebSocket服务器,说白了就是创建一个长连接。代码其实很简单:

import asyncio
import websockets

async def connect():
    uri = "wss://api.example.com/market"
    async with websockets.connect(uri) as websocket:
        print("连接成功")
        # 后续操作在这里写

这里有个坑,我曾经踩过:生产环境一定要加超时和重试机制。你想想看,行情服务器偶尔会挂,或者网络抖动一下,连接就断了。不加重试的话,你的策略可能就傻等在那里,错过一波行情。

避坑指南: 我曾经在实盘环境中只写了简单的connect(),结果凌晨三点服务器重启,我的程序直接崩溃,第二天起来亏了十几个点。后来我加了心跳检测和自动重连,才算是稳住了。

带重试的写法是这样的:

async def connect_with_retry(uri, max_retries=5):
    for i in range(max_retries):
        try:
            async with websockets.connect(uri, ping_interval=20) as ws:
                print(f"连接成功,第{i+1}次尝试")
                return ws
        except Exception as e:
            print(f"连接失败: {e},5秒后重试")
            await asyncio.sleep(5)
    raise Exception("连接失败,已达最大重试次数")

3.3 发送订阅消息——格式要对

连接建立之后,下一步就是告诉服务器:我要哪些数据。不同的交易所,订阅消息的格式不一样。以常见的JSON格式为例:

async def subscribe(websocket, symbols):
    sub_msg = {
        "op": "subscribe",
        "args": [f"trade.{s}" for s in symbols]
    }
    await websocket.send(json.dumps(sub_msg))
    print(f"已订阅: {symbols}")

这里要注意的是:订阅消息一定要等连接完全建立后再发。我见过有人刚connect完就send,结果消息丢了,数据一直收不到。正确的做法是等服务器返回一个"welcome"或者"connected"的确认消息后再发订阅。

小技巧: 我个人习惯在订阅前先接收一条服务器的心跳或欢迎消息,确认连接正常。这样能避免"假连接"的问题。

3.4 接收行情数据——循环与解析

接收数据是个持续的过程,需要用循环来不断读取。这里有个关键点:不要阻塞事件循环。如果你在接收循环里做耗时操作,比如写数据库、计算指标,那后面的数据就会堆积,延迟越来越高。

async def receive_market_data(websocket):
    async for message in websocket:
        data = json.loads(message)
        # 这里只做轻量处理
        await process_data(data)

那耗时操作怎么办?我的做法是:用队列解耦。接收线程只管把数据丢进队列,另一个消费者线程慢慢处理。

import asyncio
from collections import deque

data_queue = asyncio.Queue()

async def receive(websocket):
    async for msg in websocket:
        await data_queue.put(json.loads(msg))

async def consumer():
    while True:
        data = await data_queue.get()
        # 在这里做耗时操作,比如存数据库
        await save_to_db(data)

你看,这样接收和消费就分开了,互不干扰。我在做高频行情接入时,这个模式帮了大忙。

3.5 完整示例代码

把上面几块拼起来,就是一个完整的客户端了:

import asyncio
import json
import websockets

class MarketDataClient:
    def __init__(self, uri, symbols):
        self.uri = uri
        self.symbols = symbols
        self.queue = asyncio.Queue()
    
    async def connect(self):
        async with websockets.connect(self.uri) as ws:
            await self._subscribe(ws)
            await asyncio.gather(
                self._receive(ws),
                self._consumer()
            )
    
    async def _subscribe(self, ws):
        msg = {"op": "subscribe", "args": [f"trade.{s}" for s in self.symbols]}
        await ws.send(json.dumps(msg))
    
    async def _receive(self, ws):
        async for msg in ws:
            await self.queue.put(json.loads(msg))
    
    async def _consumer(self):
        while True:
            data = await self.queue.get()
            # 处理数据,比如打印
            print(f"收到行情: {data['symbol']} 价格: {data['price']}")

# 运行
async def main():
    client = MarketDataClient("wss://api.example.com/market", ["BTCUSDT", "ETHUSDT"])
    await client.connect()

asyncio.run(main())

3.6 核心逻辑流程图

下面这张图把整个流程串起来了,你一看就明白:

WebSocket行情客户端核心流程 1. 建立连接 2. 发送订阅消息 3. 循环接收行情数据 4. 放入队列异步处理 关键要点 • 必须加超时重试 • 订阅前确认连接状态 • 接收循环不能阻塞 • 用队列解耦耗时操作 • 注意JSON格式正确 • 心跳检测保活 • 异常捕获要全面 • 日志记录不可少

3.7 常见问题与调试技巧

最后分享几个我在实战中遇到的坑:

问题 现象 解决方案
连接超时 程序卡在connect()不动 设置connect_timeout参数,比如5秒
数据断流 收不到行情,但连接没断开 加ping/pong心跳,超时自动重连
JSON解析错误 抛出json.decoder.JSONDecodeError 用try/except包裹,记录原始数据
内存泄漏 程序运行越久,内存占用越大 检查队列是否无限增长,设置最大长度

嗯,这一章的内容就这些。说白了,WebSocket客户端就是个"连接-订阅-接收"的循环,但细节决定成败。你按照上面的代码和注意事项去写,基本不会出大问题。

核心总结: 连接要稳、订阅要准、接收要快、处理要异步。把这四点做到位,你的行情接入系统就成功了一大半。

公众号:蓝海数据掘金营,微信deep3321