3. HSMS 通信机制:TCP/IP 连接建立、会话管理与消息分帧

HSMS(High-Speed SECS Message Services)是 SECS/GEM 标准中基于 TCP/IP 的通信层协议。它替代了传统的 RS-232 串口通信,为半导体设备提供了高吞吐、低延迟的消息传输能力。本章深入剖析 HSMS 的核心通信机制,包括连接建立、会话管理、心跳保活以及消息的分帧与重组,这是构建高性能设备端消息解析引擎的基础。

核心要点: HSMS 并非简单的 TCP 套接字通信,它定义了严格的状态机、消息格式和超时机制。理解这些底层细节,是后续优化消息解析性能、保证数据完整性的前提。

3.1 TCP/IP 连接建立与状态机

HSMS 连接建立遵循一个有限状态机模型,确保通信双方在正确的状态下收发消息。设备端通常作为 被动方(Passive) 或 主动方(Active) 发起连接。

3.1.1 连接状态机

HSMS 定义了三个核心状态:未连接(Not Connected)、已连接(Connected) 和 已选择(Selected)。只有处于 Selected 状态时,才能传输 SECS-II 数据消息。

状态迁移关键点: 从 Connected 到 Selected 必须通过 Select.req 和 Select.rsp 握手完成。未经过 Select 的连接无法承载业务数据。
HSMS 连接状态机 Not Connected Connected Selected TCP 连接建立 Select 成功 Deselect / 连接断开 TCP 断开 / 超时 说明:只有处于 Selected 状态,设备才能发送或接收 SECS-II 数据消息(Primary/Secondary Message)。

3.1.2 连接建立流程

  1. TCP 三次握手: 设备端(Active)向主机(Passive)发起 TCP 连接。主机监听指定端口(默认 5000)。
  2. 连接确认: TCP 连接建立后,双方进入 Connected 状态。此时仅能交换 HSMS 控制消息,不能传输数据。
  3. Select 请求: 设备端发送 Select.req 消息,请求建立会话。
  4. Select 响应: 主机回复 Select.rsp,若返回 0x00(成功),则进入 Selected 状态。

3.2 会话管理:Select / Deselect / Reject

会话管理是 HSMS 控制连接的核心,它通过一组控制消息来管理通信双方的逻辑会话。

3.2.1 控制消息类型

消息类型 Session ID 功能描述 典型响应
Select.req 0xFFFF 请求建立会话,将连接从 Connected 提升至 Selected Select.rsp
Select.rsp 0xFFFF 响应 Select 请求,返回状态码(0x00 成功) —
Deselect.req 0xFFFF 请求释放当前会话,回到 Connected 状态 Deselect.rsp
Deselect.rsp 0xFFFF 响应 Deselect 请求 —
Reject.req 0xFFFF 拒绝无效或无法处理的消息 无(单向通知)
避坑指南: 在 Deselect 之后,连接并未断开,仍处于 Connected 状态。如果需要完全断开 TCP 连接,必须发送 Separate.req 或直接关闭套接字。切勿混淆 Deselect 与断开连接。

3.2.2 Select 握手细节

Select 请求的 Session ID 固定为 0xFFFF,表示这是一个控制消息。设备端发送 Select.req 后,必须在 T3 超时(默认 45 秒)内收到 Select.rsp。如果超时未收到,设备端应重试或断开连接。

// HSMS Select.req 消息结构示例(16 字节头部)
// 字节 0-1: Message Length (0x0000 000A)  // 10 字节
// 字节 2-3: Session ID   (0xFFFF)         // 控制消息
// 字节 4:   Header Byte 2 (0x00)
// 字节 5:   Header Byte 3 (0x00)
// 字节 6:   PType (0x00)                  // HSMS 控制消息
// 字节 7:   SType (0x01)                  // Select.req
// 字节 8-9: System Bytes (0x0001)         // 事务 ID
// 字节 10-15: 无数据体(控制消息无数据段)

3.3 心跳机制:Linktest

HSMS 使用 Linktest 消息来检测 TCP 连接的健康状态,防止因网络故障导致“死连接”占用资源。

3.3.1 Linktest 流程

  • 发起方: 通常是设备端或主机,在空闲时间超过 T6 超时(默认 5 秒)后,发送 Linktest.req。
  • 响应方: 收到 Linktest.req 后,必须立即回复 Linktest.rsp。
  • 超时处理: 如果发起方在 T3 超时 内未收到响应,则认为连接已断开,主动关闭 TCP 连接并尝试重连。
Linktest 心跳交互时序 设备端 (Active) 主机 (Passive) Linktest.req (SType=0x05) Linktest.rsp (SType=0x06) 超时规则:若设备端在 T3 (45s) 内未收到 Linktest.rsp,则判定连接失效,立即关闭 Socket 并触发重连。

3.3.2 心跳间隔优化建议

参数 默认值 优化建议 说明
T6 (心跳间隔) 5 秒 3 ~ 10 秒 根据网络延迟调整,避免过于频繁增加 CPU 开销
T3 (响应超时) 45 秒 10 ~ 60 秒 必须大于网络最大往返时间,防止误判
重连次数 无限 3 ~ 5 次 避免无限重连导致资源耗尽,建议指数退避

3.4 消息分帧与重组

HSMS 消息在 TCP 流式传输中必须进行 分帧(Framing),接收方则需要进行 重组(Reassembly)。这是设备端消息解析引擎最核心的性能瓶颈之一。

3.4.1 消息帧格式

每个 HSMS 消息由 16 字节头部 和 可变长度数据体 组成。头部前 4 字节包含消息长度信息。

// HSMS 消息帧结构
// +--------+--------+--------+--------+--------+--------+--------+--------+
// |  Message Length (4 bytes)  |  Session ID (2 bytes)  | Hdr Byte2 | Hdr Byte3 |
// +--------+--------+--------+--------+--------+--------+--------+--------+
// |  PType  |  SType  |  System Bytes (4 bytes)  |  [Data Body ...]          |
// +--------+--------+--------+--------+--------+--------+--------+--------+
//
// Message Length: 表示后续数据体(包括头部剩余 12 字节 + 数据体)的总长度。
// 例如:Message Length = 12 + N,其中 N 为数据体字节数。

3.4.2 分帧与重组策略

由于 TCP 是流协议,一次 recv() 调用可能收到 半包 或 粘包。设备端必须实现一个健壮的分帧缓冲区。

  1. 接收缓冲区: 使用环形缓冲区或链式缓冲区暂存 TCP 数据。
  2. 长度预读: 先读取前 4 字节,解析出 Message Length。
  3. 完整帧判断: 检查缓冲区中是否已累积 Message Length 字节的数据。
  4. 帧提取: 如果完整,则从缓冲区中切出一个完整的 HSMS 消息帧;否则继续等待数据。
  5. 零拷贝优化: 在 C++ 实现中,使用指针偏移或 std::string_view 避免数据拷贝。
TCP 流分帧与重组流程 TCP 流数据(粘包/半包) Step 1: 读取前 4 字节 Step 2: 解析 Message Length Step 3: 检查缓冲区长度 长度不足? 等待更多数据(半包) 提取完整帧(重组)

3.4.3 高性能分帧实现要点

  • 避免逐字节解析: 使用 memcpy 或直接内存映射读取长度字段,减少循环开销。
  • 批量处理: 一次 recv() 可能包含多个完整帧,应在一次处理循环中提取所有帧,减少系统调用。
  • 内存池: 预分配固定大小的消息缓冲区,避免频繁 malloc/free。
// C++ 高性能分帧核心逻辑(伪代码)
class HsmsFramer {
    std::vector<char> buffer_;
    size_t head_ = 0;
public:
    // 返回解析出的完整消息列表
    std::vector<HsmsMessage> feed(const char* data, size_t len) {
        buffer_.insert(buffer_.end(), data, data + len);
        std::vector<HsmsMessage> messages;
        while (true) {
            if (buffer_.size() - head_ < 4) break; // 长度字段不足
            uint32_t msg_len = ntohl(*(uint32_t*)(buffer_.data() + head_));
            if (buffer_.size() - head_ < msg_len) break; // 半包
            // 提取完整帧
            messages.emplace_back(buffer_.data() + head_, msg_len);
            head_ += msg_len;
        }
        // 清理已处理数据
        if (head_ > 0) {
            buffer_.erase(buffer_.begin(), buffer_.begin() + head_);
            head_ = 0;
        }
        return messages;
    }
};
性能提示: 在 Linux 环境下,可以使用 recvmsg 配合 MSG_PEEK 标志位预读长度,避免数据拷贝。但在高并发场景下,建议使用简单的缓冲区 + 内存拷贝策略,因为 MSG_PEEK 的内核开销可能更高。

3.5 本章小结