会话层设计:会话建立与心跳机制、序列号管理、重连与恢复策略
会话层,说白了就是交易系统里那条「看不见的绳子」。
它把客户端和交易所的网关拴在一起。绳子断了,交易就断了。我见过太多团队,业务逻辑写得飞起,结果会话层一塌糊涂——断连了不知道、重连了丢单、序列号乱跳。嗯,今天咱们就把这块硬骨头啃下来。
一、会话建立:别上来就发单
很多新手会犯一个错误:TCP 一握手成功,立马开始发交易指令。
这不对。会话建立有三个阶段,缺一不可:
- TCP 三次握手——网络层的连接
- 应用层握手——交换会话 ID、认证信息
- 状态同步——恢复未完成的订单、确认序列号
我个人习惯,在应用层握手时,客户端先发一个 SessionRequest 包,带上 API Key 和时间戳签名。服务端验证通过后,返回 SessionGrant,里面包含一个 32 位的会话 ID。
核心原则:会话 ID 必须唯一且不可预测。我见过有人用自增整数做会话 ID,结果被恶意客户端猜到别人的会话,直接越权操作。血的教训。
代码示例(伪代码,但逻辑真实):
// 客户端发起会话
function establishSession():
tcpSocket = connect(exchangeHost, port)
// 应用层握手
request = {
type: "SessionRequest",
apiKey: "my_key",
timestamp: now(),
signature: HMAC_SHA256(apiKey + timestamp, secret)
}
send(tcpSocket, request)
response = recv(tcpSocket)
if response.type == "SessionGrant":
sessionId = response.sessionId
startHeartbeat(sessionId)
return sessionId
else:
throw "Session rejected: " + response.reason
二、心跳机制:别让绳子松了
TCP 本身有 Keep-Alive,但我不建议依赖它。为什么?因为 TCP Keep-Alive 默认间隔 2 小时,太慢了。交易系统里,5 秒没心跳就该报警。
我建议自己实现应用层心跳:
- 心跳包格式:一个极简的
Heartbeat消息,只包含会话 ID 和当前序列号 - 发送间隔:3-5 秒。太频繁浪费带宽,太慢延迟高
- 超时判定:连续 3 次没收到心跳回复,判定连接断开
一个小技巧:心跳包可以「捎带」数据。比如把未确认的序列号列表塞进心跳包里,这样既保活,又做了数据确认。一举两得。
我曾经在一个高频项目里,心跳间隔设成了 1 秒。结果交易所网关扛不住,直接把我们 IP 拉黑了。后来改成 3 秒,配合指数退避重试,稳如老狗。
三、序列号管理:交易的「身份证」
每一笔交易指令,都必须有一个唯一的序列号。这不仅是去重的依据,也是重连恢复的关键。
序列号设计要点:
| 特性 | 说明 | 我的建议 |
|---|---|---|
| 单调递增 | 每个新指令的序列号比上一个 +1 | 用 64 位整数,别用 32 位,怕溢出 |
| 会话内唯一 | 不同会话的序列号可以重复 | 序列号 + 会话 ID 联合唯一 |
| 可回推 | 服务端能根据序列号判断是否收到过 | 服务端维护一个「已确认序列号」窗口 |
序列号管理最怕什么?最怕「跳号」。我遇到过一种情况:客户端发了序列号 100、101、102,结果 101 在网络里丢了。服务端收到了 100 和 102,但 101 没收到。这时候如果客户端继续发 103,服务端会认为 101 丢了,要求重传。但客户端如果没保存 101 的指令内容,那就完蛋了。
避坑指南:我曾经因为没保存已发送指令的缓存,导致重连后无法重传丢失的订单。后来我强制要求:每个会话维护一个「待确认队列」,保存最近 1000 条已发送但未确认的指令。序列号一旦确认,就从队列里移除。
四、重连与恢复策略:断了别慌
网络一定会断。不是如果,是当。
重连策略我分三层:
- 快速重连:检测到心跳超时,立即尝试重连。间隔 1 秒、2 秒、4 秒……指数退避,最大间隔 30 秒
- 会话恢复:重连成功后,发送
SessionResume请求,带上旧的会话 ID 和最后一个已确认的序列号 - 数据恢复:服务端根据序列号,把未确认的指令结果重新推送
这里有个关键点:幂等性。重传的指令,服务端必须能识别出是重复的,不能重复成交。怎么做?服务端维护一个「已处理序列号集合」,收到指令先查这个集合。
代码示例:
// 重连恢复逻辑
function reconnect(sessionId, lastConfirmedSeq):
newSocket = connect(exchangeHost, port)
resumeRequest = {
type: "SessionResume",
sessionId: sessionId,
lastConfirmedSeq: lastConfirmedSeq
}
send(newSocket, resumeRequest)
response = recv(newSocket)
if response.type == "ResumeGrant":
// 服务端返回了断连期间未确认的指令结果
for each msg in response.pendingMessages:
processMessage(msg)
// 重新发送未确认的指令
for each pending in pendingQueue:
if pending.seq > lastConfirmedSeq:
send(newSocket, pending)
else:
// 会话已过期,重新建立新会话
establishSession()
五、整体架构图
下面这张图,把会话层的核心流程串起来了。我画了三个主要模块:会话管理、心跳监控、序列号追踪。你看一眼就明白它们怎么配合的。
六、避坑总结
会话层设计,说白了就是三个字:稳、快、准。
- 稳:心跳不能断,断了要能恢复
- 快:重连要快,指数退避别太激进
- 准:序列号不能乱,重传要幂等
我曾经在一个跨境交易项目里,因为没处理好会话恢复,导致断连后重复下单,亏了十几万。从那以后,我每次设计会话层,都会先画一张像上面那样的架构图,把每个模块的职责和交互理清楚。你想想看,如果连会话都保不住,交易策略再牛又有什么用?
最后送你一句话:会话层是交易系统的「地基」。地基不稳,楼盖得越高,摔得越惨。