每一家交易所都会公布速率限制,也都会强制执行。对于触发限速的 REST 调用,Binance 会返回 HTTP 429 以及一个你需要遵守的 Retry-After 头;如果某个 WebSocket 连接每秒发送超过 5 条消息,它就会关闭这个连接。两份客户报告在几天之内先后到达,各自对应其中的一半,合起来暴露出三个值得认真修复的问题。
这三个问题现在都已修复。其中一个的影响范围远远超出了 Binance。
失败的请求过去会把响应头丢掉
当通过某个现成的 API 客户端发出的请求以 HTTP 错误状态失败时,组件会抛出一个携带状态码、状态文本和响应体的异常。响应头则不见了。不是被隐藏,而是彻底消失。异常还在沿调用栈向上传递时,HTTP 对象就已经被销毁,响应头也随之消失。
这使得无法在组件之上构建符合 Binance 规范的退避逻辑。Retry-After 会准确告诉你需要等待多久,忽略它会让 429 升级为 HTTP 418 以及临时的 IP 封禁。Binance 返回的限速计数器,例如 X-MBX-USED-WEIGHT-1M,同样无法获取,因此应用程序也无法在撞上限制之前自行调整节奏。
现在抛出的异常是 EsgcHTTPAPIProtocolException,它会一并携带响应头。它继承自之前抛出的那个异常,因此已有的处理代码仍然可以捕获它,除非你想使用这些新信息,否则无需做任何改动。
try
vResponse := oBinance.GetAggregateTrades('BTCUSDT');
except
on E: EsgcHTTPAPIProtocolException do
begin
if E.ErrorCode = 429 then
Sleep(E.RetryAfterMs);
vWeight := E.GetHeader('X-MBX-USED-WEIGHT-1M');
// E.ResponseHeaders holds the complete list
end;
end;
RetryAfterMs 返回以毫秒为单位的延迟,并且能识别该头允许的两种形式:纯粹的秒数,以及 HTTP 日期。当该头不存在时,它返回 -1。这项修复适用于全部八个请求方法,而不只是 Get;Post 和 Query 原本就接受的 ResponseHeaders 参数,现在在请求失败时也会被填充,而那正是你最需要它的时候。
与此相关,还有一点值得了解:自 2026.7.0 起,HTTP 客户端也可以替你完成等待。
oBinance.RetryOptions.Enabled := True;
oBinance.RetryOptions.MaxRetries := 3;
// HonorRetryAfter is already True by default
一个帧,而不是十二个
Binance 的 WebSocket 协议允许在单个 SUBSCRIBE 帧中传入一组数据流列表。组件此前并没有利用这一点。每个 Subscribe* 辅助方法都会立即写出一个恰好只包含一个数据流的帧,因此一个普通的自选列表,六个交易对乘以两个频道,会在几毫秒之内发送十二个帧。Binance 关闭了连接,与文档描述的完全一致。
现在提供了批量方法,可以把整个列表作为一个帧发送:
oBinance.SubscribeStreams(['btcusdt@aggTrade', 'btcusdt@depth',
'ethusdt@aggTrade', 'ethusdt@depth']);
即使有数百个数据流,一个帧也远低于 8 KB 的请求上限,因此在实践中一个自选列表始终只需一个帧。UnsubscribeStreams 则是与之对应的反向操作。
谁也绕不过去的那一部分
这是最要紧的一个问题,而两份报告都没有完全指出它。
每个交易所 API 都会保存你订阅过的频道,以便在重连之后恢复它们。这个重放过程为每个数据流连续发送一个帧,完全没有任何节流,而目标连接才刚刚建立几毫秒。只要自选列表有一点实际规模,这个新连接就会立刻被关闭。随后 WatchDog 重新连接,再次重放同样的突发流量,然后又被关闭。一次短暂的断线就这样变成了永久的重连循环。
由于这一突发流量是在组件内部产生的,应用程序无法规避它。自己编写帧而不使用辅助方法也无济于事,因为重放并不是由你来调用的。
对库的其余部分进行审查后发现,同样的重放循环被复制到了 17 个交易所 API 中:Binance、Bitfinex、Bitget、Bitmex、Bitstamp、Bybit、Cex、CexPlus、Coinbase、CryptoCom、Deribit、GateIO、Huobi、Kraken、Kucoin、MEXC 和 OKX。每一个都在冲撞各自交易所的限制。
现在 WebSocket API 客户端上提供了 Throttle 选项:
oBinance.Throttle.Enabled := True; // paces your own Subscribe calls
oBinance.Throttle.MaxMessages := 4; // default, leaves room for PING/PONG
oBinance.Throttle.IntervalMs := 1000; // default
这里的默认值值得解释一下,因为它是刻意不对称的。Throttle.Enabled 为 False,因此除非你主动要求,你的应用程序所发出调用的时序不会有任何改变。但 Throttle.PaceResubscribe 为 True,因此重连后的重放开箱即用地受到节流。
道理很简单。你的订阅循环由你自己掌控,也可以自行批量处理,因此那里是否节流由你决定。重放不由你掌控,所以由组件替你节流。这样一来,所有现有应用程序都不必改动任何代码,重连循环的问题就已经解决。在 Binance 上,重放同样以合并帧的方式发送,因此一个包含十二个数据流的自选列表现在只需重放一个帧,而不是十二个。如果你想要之前的行为,把 PaceResubscribe 设为 False。
如果你同时使用多家交易所,还有一个细节:每秒 4 条消息的默认额度适合 Binance,其文档中的限制是 5。OKX 文档中的限制是 3,因此 OKX 客户端的默认值就是 3。在调高 MaxMessages 之前,请先确认你所用交易所的限制。
升级
可以直接替换。新异常继承自之前抛出的那个异常,批量方法是新增的,而 Throttle 对所有情况都默认关闭,唯一的例外就是那个此前会破坏重连的重放。请从 sgcWebSockets 下载页面下载最新版本。
这两份报告都来自愿意花时间阅读源码、复现问题并把它准确写清楚的客户。第三个也是最严重的一个缺陷正是因此被发现的。如果组件中有什么行为让你无法解释,请告诉我们。
有疑问、反馈或需要迁移方面的帮助?联系我们,回复你的将是编写这些代码的人。
