3、会话管理与登录:初始化交易会话、登录流程详解、登录状态机、心跳机制与重连策略
做量化交易,说白了就是跟交易所的服务器打交道。你写的策略再牛,如果连不上服务器,一切都是白搭。这一章咱们就聊聊「会话」这件事——怎么连、怎么登、怎么保活、断了怎么重连。
我个人习惯把交易系统的会话管理看作一个「状态机」。嗯,没错,就是那种有状态、有转移、有边界的东西。你想想看,一个连接不可能永远在线,它总得经历:未连接 → 已连接 → 已登录 → 已断开 → 重连中…… 这一套流程。
3.1 初始化交易会话
初始化会话,我习惯叫它「握手前的准备」。这一步其实很简单,但很多人会忽略细节。
举个例子,我在对接 CTP 接口时,第一步不是直接发登录请求,而是先创建一个 CTPTradeAdapter 实例,然后调用 Init() 方法。这个 Init() 背后做了什么?
- 加载配置文件(交易前置地址、经纪商代码、用户ID、密码、行情前置地址等)
- 创建网络连接池(如果是长连接)
- 注册回调函数(OnFrontConnected、OnRspUserLogin 等)
- 设置心跳超时参数
// 伪代码示例:初始化交易会话
void InitTradeSession(const Config& cfg) {
// 1. 创建交易适配器
auto adapter = std::make_shared<CTPTradeAdapter>();
// 2. 注册回调
adapter->RegisterSpi(this);
// 3. 初始化(内部会连接前置机)
int ret = adapter->Init(cfg.front_addr.c_str());
if (ret != 0) {
// 我曾经在这里踩过坑:Init 失败后直接重试,结果导致内存泄漏
// 正确做法:先释放资源,再重新创建
LogError("Init failed, ret=%d", ret);
return;
}
// 4. 等待 OnFrontConnected 回调
// 这里一般用条件变量或 Promise 来同步等待
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait_for(lock, std::chrono::seconds(5), [this]{
return connected_;
});
}
3.2 登录流程详解
登录,说白了就是向交易所证明「我是谁」。但这个过程比你想的要复杂一点。
标准的登录流程是这样的:
- 前置连接成功(OnFrontConnected 回调触发)
- 发送登录请求(ReqUserLogin)
- 等待登录响应(OnRspUserLogin)
- 确认登录成功(检查错误码和会话参数)
- 获取交易日信息(有些接口需要额外请求交易日)
这里有个坑,我遇到过好几次。有些交易所的登录响应不是一次性返回的。比如 CTP,它会先返回一个 OnRspUserLogin,告诉你登录成功了,但紧接着还会返回一个 OnRtnTradingDay,告诉你当前交易日。如果你只处理了第一个回调,忽略了第二个,那你的系统可能拿不到正确的交易日。
// 登录请求示例
void OnFrontConnected() {
// 前置连接成功,开始登录
CThostFtdcReqUserLoginField req = {0};
strcpy(req.BrokerID, broker_id_.c_str());
strcpy(req.UserID, user_id_.c_str());
strcpy(req.Password, password_.c_str());
int ret = adapter_->ReqUserLogin(&req, 0);
if (ret != 0) {
// 登录请求发送失败,需要重试
LogError("ReqUserLogin failed, ret=%d", ret);
// 我一般会在这里启动一个重试定时器
StartRetryTimer();
}
}
void OnRspUserLogin(CThostFtdcRspUserLoginField* pRsp,
CThostFtdcRspInfoField* pRspInfo,
int nRequestID, bool bIsLast) {
if (pRspInfo && pRspInfo->ErrorID != 0) {
// 登录失败
LogError("Login failed: %s", pRspInfo->ErrorMsg);
HandleLoginFailure(pRspInfo->ErrorID);
return;
}
// 登录成功,保存会话参数
session_id_ = pRsp->SessionID;
front_id_ = pRsp->FrontID;
trading_day_ = pRsp->TradingDay;
LogInfo("Login success, session=%d, front=%d, day=%s",
session_id_, front_id_, trading_day_);
}
3.3 登录状态机
为什么要用状态机?因为登录过程不是线性的。你可能会遇到:连接断开、重连、登录超时、重复登录…… 如果没有状态机,代码会变成一团乱麻。
我设计的状态机一般包含这几个状态:
| 状态 | 说明 | 触发条件 |
|---|---|---|
| DISCONNECTED | 未连接 | 初始状态 / 连接断开后 |
| CONNECTING | 正在连接 | 调用 Init() 后 |
| CONNECTED | 已连接,未登录 | OnFrontConnected 触发 |
| LOGIN_SENT | 已发送登录请求 | 调用 ReqUserLogin 后 |
| LOGIN_SUCCESS | 登录成功 | OnRspUserLogin 成功 |
| RECONNECTING | 正在重连 | 连接断开后自动触发 |
下面是我画的一张状态机图,你可以直观地看到状态之间的流转关系:
3.4 心跳机制与重连策略
登录成功后,你以为就万事大吉了?不,真正的挑战才刚刚开始。网络是不稳定的,交易所的服务器也可能重启。你需要一套机制来保活连接。
心跳机制,说白了就是定时给服务器发一个「我还活着」的信号。如果服务器长时间没收到心跳,它会认为客户端已经断开,然后主动关闭连接。
我一般这样设计心跳:
- 发送频率: 每 30 秒发送一次心跳请求(具体看交易所要求)
- 超时判断: 如果连续 3 次心跳没有收到响应,认为连接已断开
- 心跳内容: 有些接口有心跳专用 API(如 CTP 的
ReqHeartBeat),有些则用查询类请求代替
// 心跳定时器示例
void StartHeartBeat() {
heart_beat_timer_ = std::make_shared<Timer>();
heart_beat_timer_->Start(30000, [this]() {
// 每 30 秒发送一次心跳
int ret = adapter_->ReqHeartBeat(nullptr, 0);
if (ret != 0) {
heart_beat_fail_count_++;
if (heart_beat_fail_count_ >= 3) {
// 连续 3 次心跳失败,触发重连
LogError("Heart beat failed 3 times, start reconnecting");
StartReconnect();
}
} else {
heart_beat_fail_count_ = 0;
}
});
}
重连策略,这个我踩过不少坑。最 naive 的做法是:断开后立即重连。但这样会带来两个问题:
- 服务器压力: 如果服务器正在重启,你疯狂重连只会加重它的负担
- 死循环: 如果网络一直不通,你的程序会陷入「断开-重连-断开」的死循环
我现在的做法是「指数退避 + 随机抖动」:
| 重连次数 | 等待时间 | 说明 |
|---|---|---|
| 第 1 次 | 1 秒 | 快速重连 |
| 第 2 次 | 2 秒 | 翻倍 |
| 第 3 次 | 4 秒 | 继续翻倍 |
| 第 4 次 | 8 秒 | 继续翻倍 |
| 第 5 次 | 16 秒 | 继续翻倍 |
| 第 6 次及以上 | 30 秒(上限) | 不再增加 |
每次等待时间还要加上一个随机值(比如 0~3 秒),防止多个客户端同时重连造成「惊群效应」。
// 重连策略实现
void StartReconnect() {
if (reconnect_count_ > MAX_RECONNECT_COUNT) {
LogError("Reconnect failed after %d attempts, giving up", MAX_RECONNECT_COUNT);
// 通知上层:需要人工介入
NotifyAdmin("Connection lost, manual intervention required");
return;
}
// 计算等待时间:指数退避
int wait_time = std::min(1 << reconnect_count_, 30); // 上限 30 秒
// 加上随机抖动
int jitter = rand() % 3000; // 0~3 秒
wait_time = wait_time * 1000 + jitter;
LogInfo("Reconnecting in %d ms (attempt %d)", wait_time, reconnect_count_ + 1);
reconnect_timer_->Start(wait_time, [this]() {
reconnect_count_++;
// 重新初始化会话
InitTradeSession(config_);
});
}
嗯,会话管理这块内容其实挺多的。但核心就三点:初始化要稳、登录要全、重连要聪明。你把这三点做好了,交易系统的稳定性至少能提升一个档次。
最后说一句,心跳和重连的日志一定要打全。我每次排查线上问题,第一件事就是看心跳日志。如果发现心跳中断了 10 分钟才重连,那肯定是策略有问题。