会话层设计:会话建立与心跳机制、序列号管理、重连与恢复策略

会话层,说白了就是交易系统里那条「看不见的绳子」。

它把客户端和交易所的网关拴在一起。绳子断了,交易就断了。我见过太多团队,业务逻辑写得飞起,结果会话层一塌糊涂——断连了不知道、重连了丢单、序列号乱跳。嗯,今天咱们就把这块硬骨头啃下来。

一、会话建立:别上来就发单

很多新手会犯一个错误:TCP 一握手成功,立马开始发交易指令。

这不对。会话建立有三个阶段,缺一不可:

  1. TCP 三次握手——网络层的连接
  2. 应用层握手——交换会话 ID、认证信息
  3. 状态同步——恢复未完成的订单、确认序列号

我个人习惯,在应用层握手时,客户端先发一个 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. 快速重连:检测到心跳超时,立即尝试重连。间隔 1 秒、2 秒、4 秒……指数退避,最大间隔 30 秒
  2. 会话恢复:重连成功后,发送 SessionResume 请求,带上旧的会话 ID 和最后一个已确认的序列号
  3. 数据恢复:服务端根据序列号,把未确认的指令结果重新推送

这里有个关键点:幂等性。重传的指令,服务端必须能识别出是重复的,不能重复成交。怎么做?服务端维护一个「已处理序列号集合」,收到指令先查这个集合。

代码示例:

// 重连恢复逻辑
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()

五、整体架构图

下面这张图,把会话层的核心流程串起来了。我画了三个主要模块:会话管理、心跳监控、序列号追踪。你看一眼就明白它们怎么配合的。

会话层核心架构图 客户端会话管理器 心跳发送 (3s间隔) 序列号生成器 (64位) 待确认队列 (缓存1000条) TCP网络 服务端会话管理器 心跳监控 (超时3次判定) 已确认序列号集合 会话恢复处理器 会话建立 → 心跳保活 → 序列号追踪 → 断线重连 → 数据恢复 客户端 服务端 网络

六、避坑总结

会话层设计,说白了就是三个字:稳、快、准

  • :心跳不能断,断了要能恢复
  • :重连要快,指数退避别太激进
  • :序列号不能乱,重传要幂等

我曾经在一个跨境交易项目里,因为没处理好会话恢复,导致断连后重复下单,亏了十几万。从那以后,我每次设计会话层,都会先画一张像上面那样的架构图,把每个模块的职责和交互理清楚。你想想看,如果连会话都保不住,交易策略再牛又有什么用?

最后送你一句话:会话层是交易系统的「地基」。地基不稳,楼盖得越高,摔得越惨。

专注资料整理