
我们为什么自研网络栈
Unicode Digital 自建 TLS / HTTP2 / WebSocket 协议栈的业务考量与技术决策。
对接 10+ 家交易场所的交易系统里,"连接断了"从来不只是网络层一个人的事 —— 它是整个系统的事件源。把这个起点握在自己手里,值得自写一整套网络栈。
Pandora 是我们用 C++ 写的多市场交易框架,接 10+ 家交易场所。在网络这一层,它做了一个不太常见的决定:TCP / HTTP / HTTP/2 / WebSocket 这一整条栈基本自写,只把底层 TLS 加解密与 HTTP/2 帧解析交给成熟的协议库去做。
听起来像在重新发明轮子。boost::asio、libcurl、libwebsockets 都是现成且能用的好库,生产里跑得也都不差。我们最初其实就是用它们拼起来的 —— 一个 HTTP 客户端 + 一个 WS 客户端 + 一套自己的连接管理 + 一套自己的限频 + 一套自己的重连。能跑,但接缝过多。
最后促使我们把这一层全部收回自写的,不是性能,而是事件源这件事 —— 在一个交易系统里,"连接断了"远远不只是网络层的事。它会一路传到订单管理、传到风控、传到策略,触发挂单状态重核、风控阈值收紧、暂停某个 vendor 的下单。这种全栈级的事件源,必须有一个单一、可信、可观测的出处。如果它散落在几段彼此不一致的胶水代码里,你就找不到唯一的真理源 —— 谁先察觉、谁还没赶上来,几秒钟内就分不清了。
用现成库时,几个不那么明显的问题
在 24/7 全球市场里,一个交易系统同时要面对三种性格完全不同的连接:
在多数现成组合里,这三种连接最终会落在两到三套客户端、各自的状态机和重连语义上(WS Stream 和 WS API 通常共用一套 WebSocket 客户端,HTTP 另算一套)。具体的痛点是:
我们的取舍:保留协议层,其余自写
自写网络栈最常见的失败模式,是把所有东西都自写。TLS 协议、HTTP/2 流量控制、WebSocket 帧解析 —— 这些是标准化、变化慢、写错了就只能交学费的东西。自己重写它们的边际收益几乎为零。
所以我们的划法是:
留下来的是协议规范本身;自写的是把这两个低层胶接成"一个能用的 Session"的所有粘合代码 —— 连接状态机、心跳节奏、错误分类、重连策略、TCP 参数、event loop 集成、缓冲池管理、HTTP/2 请求生命周期管理。
在这之上,我们给整个系统提供的对外抽象只有一个 —— SessionBundle。无论底下是 HTTP/1.1、HTTP/2 还是 WebSocket,上层看到的接口形状是一样的:send() / recv() / state,以及两条事件 —— Session_Connected 与 Session_Disconnected。差异只在语义 —— REST 上 send() 与 recv() 是一次配对的请求/响应;WS Stream 上数据面基本只收不发;WS API 仍然是请求/响应配对,但由消息内的 id 字段关联,而不是 TCP 层的同步配对。
真正的回报:断线重连只有一个真理源
当上层只看一个事件源,系统的整体行为就突然变得可推导了。
大多数交易系统的失败模式不是单点错误,而是"重连风暴" —— 网络断了一下,网络层重连、HTTP 客户端重连、WebSocket 客户端重连、订单管理器重新订阅、策略层试图重新对账。这些动作互相触发、互相打架,几秒钟之内系统就进入了一个谁也说不清的状态。
重连被钉死在最底层,默认 1 秒间隔。上层每一处只通过两条事件感知连接状态,不做任何自己的重连尝试:
// 以下为伪代码 · pseudocode
// SessionBundle 的默认配置 —— 整个系统唯一一处重连发生地
SessionBundle::Config {
reconnect = true,
reconnectInterval = 1s,
heartbeatInterval = 15s,
...
};
// 上层只感知事件,不重连
void Executor::onEvent(const Event& ev) {
switch (ev.type) {
case Session_Disconnected:
status_ = DISCONNECTED;
if (kind_ == WS_STREAM)
oms_->onSubscribeDisconnected(); // 通知 OMS:挂单状态需要重核
break;
case Session_Connected:
status_ = (kind_ == WS_API) ? CONNECTED : READY;
if (kind_ == WS_API)
sendLogon(); // 重连后还要登录,Logon 回包后再切 READY
break;
}
}这套设计让整个系统出现断线时的行为完全可枚举:
- SessionBundle 检测到断开,广播
Session_Disconnected,并在 1s 后开始重连尝试。 - 策略侧通过
Session_Disconnected拿到信号,自己决定要不要 cancel 旧单、要不要对账。 - 底层重连成功 → emit
Session_Connected→ Executor 状态切到 READY(HTTP / WS_STREAM),或先 Logon、收到回包后再切 READY(WS_API)。
整套流程只有一个起点。你不需要追问"是不是 WS 客户端先重连了所以 OMS 还不知道";也不会出现"网络层认为连着、应用层认为断了"的分裂状态。系统出问题时,只需要看 SessionBundle 这一处的日志。
代价
一点反思
自研的边界,不在于"我能不能写"或"这层值不值得",而在于这层的事件,有没有进入业务逻辑。
TLS 加解密的事件不会进业务逻辑 —— 它要么成功要么失败,对上层而言是黑盒,所以留给成熟的协议库。HTTP/2 的多路复用和流控也不会进业务逻辑 —— 它的工作是"把帧调度好",上层只看流,同样留给成熟实现。
但"连接断了"不一样。它会触发挂单状态重核、风控阈值调整、策略反应。它已经进入了业务逻辑。这种"已经进入业务的事件",必须从一个我们自己掌控的源头发出 —— 否则当问题出现在凌晨三点的某个 vendor 上时,你会发现你既不知道"为什么发生",也不知道"接下来会怎样"。
Pandora 是我们正在持续打磨的交易框架。如果你也对网络栈、连接生命周期、低延迟系统的"事件源"设计感兴趣,欢迎一起聊聊。