为 MetaTrader 5 开发一款 MQTT 客户端:TDD 方式 - 第2部分·综合运用
📘

为 MetaTrader 5 开发一款 MQTT 客户端:TDD 方式 - 第2部分·综合运用

第 3/3 篇

◍ MQTT 连接控制包的字节骨架

在 MT5 里用 MQL5 对接 MQTT broker 时,CONNECT 报文是握手的第一道关。下面这个类把控制包结构直接摊进一个 uchar 数组,方便你逐字节核对协议。 CPktConnect 继承自 IControlPacket,私有方法 IsControlPacket() 直接返回 true,表明它本身就是控制包而非数据负载。保护段里 InitConnectFlags、InitKeepAlive、InitPropertiesLength 分别把 ByteArray 的第 10、11、12、13 个索引(0 基下为 9、10、11、12)清零,对应 MQTT 固定头里的连接标志、保活计时和属性长度字段。 公开接口暴露了 SetCleanStart、SetWillFlag、SetPasswordFlag 等一系列位操作方法,以及 SetKeepAlive(ushort seconds) 和 SetClientIdentifier(string clientId)。注意保活值以秒为单位写进两个字节,ClientID 长度与内容分开设置——这两处若顺序写错,broker 会直接断连。 代码里 ByteArray[] 是公开成员,意味着你能在 EA 里直接打印 Hex 对照 Wireshark 抓包。外汇与贵金属行情推送走这套异步通道时波动烈度高,实盘前务必在模拟环境验证字节序。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class CPktConnect.                                                |
class=class="str">"cmt">//| Purpose: Class of MQTT Connect Control Packets.                   |
class=class="str">"cmt">//|          Implements IControlPacket                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CPktConnect : class="kw">public IControlPacket
  {
class="kw">private:
   class="type">bool              IsControlPacket() {class="kw">return true;}
class="kw">protected:
   class="type">void              InitConnectFlags() {ByteArray[class="num">9] = class="num">0;}
   class="type">void              InitKeepAlive() {ByteArray[class="num">10] = class="num">0; ByteArray[class="num">11] = class="num">0;}
   class="type">void              InitPropertiesLength() {ByteArray[class="num">12] = class="num">0;}
   class="type">uchar             m_connect_flags;
class="kw">public:
                     CPktConnect();
                     CPktConnect(class="type">uchar &buf[]);
                    ~CPktConnect();
   class=class="str">"cmt">//--- methods for setting Connect Flags
   class="type">void              SetCleanStart(class="kw">const class="type">bool cleanStart);
   class="type">void              SetWillFlag(class="kw">const class="type">bool willFlag);
   class="type">void              SetWillQoS_1(class="kw">const class="type">bool willQoS_1);
   class="type">void              SetWillQoS_2(class="kw">const class="type">bool willQoS_2);
   class="type">void              SetWillRetain(class="kw">const class="type">bool willRetain);
   class="type">void              SetPasswordFlag(class="kw">const class="type">bool passwordFlag);
   class="type">void              SetUserNameFlag(class="kw">const class="type">bool userNameFlag);
   class="type">void              SetKeepAlive(class="type">class="kw">ushort seconds);
   class="type">void              SetClientIdentifierLength(class="type">class="kw">string clientId);
   class="type">void              SetClientIdentifier(class="type">class="kw">string clientId);
   class=class="str">"cmt">//--- member for getting the byte array
   class="type">uchar             ByteArray[];
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CPktConnect::CPktConnect(class="type">uchar &buf[])
  {

CONNECT 报文里的字节位怎么填

在 MT5 里手搓 MQTT 的 CONNECT 包,核心就是按协议规范把各个字段写进 ByteArray 的固定偏移。下面这段 C++ 类方法展示了客户端标识、保活时间和连接标志位的写入逻辑,直接决定了 broker 能否正确解析你的登录请求。 SetClientIdentifierLength 把字符串长度拆成高位和低位两个字节,分别塞进 ByteArray[12] 和 ByteArray[13]。MQTT 协议规定这两个字节采用大端序,所以长度超过 255 的 clientId 也能靠 >>8 和 %256 正确拆分。 SetKeepAlive 同理,把秒数写到 ByteArray[10]、[11],注释里标了 MQTT 上限是 65535 秒(约 18.2 小时),实际盯盘用的心跳间隔通常设 60 秒。SetPasswordFlag 和 SetUserNameFlag 则用位运算改 m_connect_flags,再 ArrayFill 回写——注意这里 ArrayFill 的起始长度是 sizeof(ByteArray),若数组未初始化可能越界,调试时建议先 ArrayResize 确认容量。 开 MT5 新建 EA 把这几段抄进类里,打印 ByteArray 的十六进制,对照 MQTT 3.1.1 规范第 3.1 节就能验证报文头是否对齐。外汇和贵金属行情推送走这套私有协议时波动大、断连概率高,心跳参数别盲目拉满。

MQL5 / C++
class="type">void CPktConnect::SetClientIdentifier(class="type">class="kw">string clientId)
  {
  SetClientIdentifierLength(clientId);
  StringToCharArray(clientId, ByteArray,
                     ByteArray.Size() - StringLen(clientId), StringLen(clientId));
  }
class="type">void CPktConnect::SetClientIdentifierLength(class="type">class="kw">string clientId)
  {
  clientIdLen.msb = (class="type">char)StringLen(clientId) >> class="num">8;
  clientIdLen.lsb = (class="type">char)StringLen(clientId) % class="num">256;
  ByteArray[class="num">12] = clientIdLen.msb;
  ByteArray[class="num">13] = clientIdLen.lsb;
  }
class="type">void CPktConnect::SetKeepAlive(class="type">class="kw">ushort seconds) class=class="str">"cmt">// MQTT max is class="num">65,class="num">535 sec
  {
  keepAlive.msb = (class="type">uchar)(seconds >> class="num">8) & class="num">255;
  keepAlive.lsb = (class="type">uchar)seconds & class="num">255;
  ByteArray[class="num">10] = keepAlive.msb;
  ByteArray[class="num">11] = keepAlive.lsb;
  }
class="type">void CPktConnect::SetPasswordFlag(class="kw">const class="type">bool passwordFlag)
  {
  passwordFlag ? m_connect_flags |= PASSWORD_FLAG : m_connect_flags &= ~PASSWORD_FLAG;
  ArrayFill(ByteArray, class="kw">sizeof(ByteArray), class="num">1, m_connect_flags);
  }
class="type">void CPktConnect::SetUserNameFlag(class="kw">const class="type">bool userNameFlag)
  {
  userNameFlag ? m_connect_flags |= USER_NAME_FLAG : m_connect_flags &= (class="type">uchar) ~USER_NAME_FLAG;

「连接标志位的位运算与字节回填」

CPktConnect 类用一组 setter 来切换 MQTT 连接报文里的控制标志。每个方法都通过按位或(|=)置位、按位与取反(&= ~)清位来修改 m_connect_flags,再调用 ArrayFill 把更新后的标志写回 ByteArray。 注意 SetCleanStart 与其余方法的不同:它填字节时起始偏移是 9 而不是 sizeof(ByteArray)。这意味着 Clean Start 标志被放在报文第 10 个字节(下标 9)起的长度为 1 的区域,而其他 Will 类标志都从 ByteArray 头部开始填。 下面这段集中展示了五个 setter 的典型写法,你可以直接贴进 MT5 的 MQH 里验证位操作结果。位标记宏(如 WILL_FLAG)需自行在头文件定义,否则编译会报未声明。

MQL5 / C++
class="type">void CPktConnect::SetWillRetain(class="kw">const class="type">bool willRetain)
  {
  willRetain ? m_connect_flags |= WILL_RETAIN : m_connect_flags &= ~WILL_RETAIN;
  ArrayFill(ByteArray, class="kw">sizeof(ByteArray), class="num">1, m_connect_flags);
  }
class="type">void CPktConnect::SetWillQoS_2(class="kw">const class="type">bool willQoS_2)
  {
  willQoS_2 ? m_connect_flags |= WILL_QOS_2 : m_connect_flags &= ~WILL_QOS_2;
  ArrayFill(ByteArray, class="kw">sizeof(ByteArray), class="num">1, m_connect_flags);
  }
class="type">void CPktConnect::SetWillQoS_1(class="kw">const class="type">bool willQoS_1)
  {
  willQoS_1 ? m_connect_flags |= WILL_QOS_1 : m_connect_flags &= ~WILL_QOS_1;
  ArrayFill(ByteArray, class="kw">sizeof(ByteArray), class="num">1, m_connect_flags);
  }
class="type">void CPktConnect::SetWillFlag(class="kw">const class="type">bool willFlag)
  {
  willFlag ? m_connect_flags |= WILL_FLAG : m_connect_flags &= ~WILL_FLAG;
  ArrayFill(ByteArray, class="kw">sizeof(ByteArray), class="num">1, m_connect_flags);
  }
class="type">void CPktConnect::SetCleanStart(class="kw">const class="type">bool cleanStart)
  {
  cleanStart ? m_connect_flags |= CLEAN_START : m_connect_flags &= ~CLEAN_START;
  ArrayFill(ByteArray, class="num">9, class="num">1, m_connect_flags);
  }

◍ 连接报文类的空构造与析构

在 MT5 终端的网络报文封装层里,CPktConnect 专门承载客户端发起连接时的数据包结构。它的构造函数与析构函数体均为空,说明该类本身不持有需要手动释放的资源,仅作为协议帧的轻量载体存在。 实际调试时若你在 OnTradeTransaction 或自定义 socket 插件中截到这类对象,不必预期构造阶段有任何初始化日志——编译器生成的默认行为就是直接返回。下面这段源码印证了这一点,没有任何成员变量赋值或内存申请。 外汇与贵金属的自动化对接涉及实时链路,这类空实现在高并发发包路径上反而降低了开销,但高频重连场景下仍需警惕连接状态机本身遗漏超时回收。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
CPktConnect::CPktConnect()
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CPktConnect::~CPktConnect()
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

用 OASIS 样本字节校验 CONNECT 封包

CPktConnect 类只干一件事:拼出符合 MQTT 规范的 CONNECT 数据包。要验证它没拼错,第一步不是连 broker,而是先准备一个“fixture”——一份手写的正确字节数组,拿它当标尺。 OASIS 在协议文档第 3.1.2.12 节给了可变头的非规范示例,配合我们已经测过的固定头生成器,这份示例足够当起点。参考 fixture 是 {16,24,0,4,77,81,84,84,5,2,0,10,0,4,7,17,0,0,0,10,25,1,77,81,76,53},共 26 字节;测试时把 CPktConnect 生成的数组用 ArrayCompare 比对,返回 0 才算过。 有人觉得“测数组里是不是我刚塞进去的数据”纯属浪费时间。其实这组看似多余的测试会一直跟着代码,哪天你复制粘贴手滑或改出回归错误,它立刻报警。TDD 过程中很多测试会重写甚至删掉,但留下来的就是长期自动调试器。 本地验证可走 WSL 上的 Mosquitto:默认安装后它是服务,把 80 映射 to 1883,MT5 选项里把主机名加进允许 URL,连上后日志显示无“协议错误”即说明头部格式站得住。图02 的 MetaEditor 专家日志和图03 的 Mosquitto 连接记录都能复现。 别把 fixture 当摆设 硬编码的 expected 数组必须和协议示例逐字节对齐;CleanSession、KeepAlive=10s、ClientID="MQL5" 等组合跑完, broker 不拒包,才说明类可用。外汇/贵金属 EA 接 MQTT 风控信号时,这种底层封包错一个字节都可能让连接静默失败,风险隐蔽且偏高。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| TEST_CControlPacket_Connect.mq5 |
class=class="str">"cmt">//| |
class=class="str">"cmt">//| ********* WORK IN PROGRESS ********** |
class=class="str">"cmt">//| **** PART OF ARTICLE [MQL5官方文档] **** |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <MQTT\CPktConnect.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Tests for CControlPacketConnect class |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
   Print(TEST_SetCleanStart_KeepAlive_ClientIdentifier());
   Print(TEST_SetClientIdentifier());
   Print(TEST_SetClientIdentifierLength());
   Print(TEST_SetCleanStart_and_SetKeepAlive());
   Print(TEST_SetKeepAlive());
   Print(TEST_SetCleanStart());
  }
class=class="str">"cmt">/* REFERENCE ARRAY(FIXTURE)
{class="num">16, class="num">24, class="num">0, class="num">4, class="num">77, class="num">81, class="num">84, class="num">84, class="num">5, class="num">2, class="num">0, class="num">10, class="num">0, class="num">4, class="num">7, class="num">17, class="num">0, class="num">0, class="num">0, class="num">10, class="num">25, class="num">1, class="num">77, class="num">81, class="num">76, class="num">53}
*/
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool TEST_SetCleanStart_KeepAlive_ClientIdentifier()
  {
   Print(__FUNCTION__);
class=class="str">"cmt">//--- Arrange
   class="kw">static class="type">uchar expected[] =
     {class="num">16, class="num">16, class="num">0, class="num">4, class="num">77, class="num">81, class="num">84, class="num">84, class="num">5, class="num">2, class="num">0, class="num">10, class="num">0, class="num">4, class="num">77, class="num">81, class="num">76, class="num">53};
   class="type">uchar buf[expected.Size() - class="num">2];
   CPktConnect *cut = new CPktConnect(buf);
class=class="str">"cmt">//--- Act
   cut.SetCleanStart(true);
   cut.SetKeepAlive(class="num">10);class=class="str">"cmt">//class="num">10 sec
   cut.SetClientIdentifier("MQL5");
   class="type">uchar result[];
   ArrayCopy(result, cut.ByteArray);
class=class="str">"cmt">//--- Assert
   class="type">bool isTrue = Assert(expected, result);
class=class="str">"cmt">//--- cleanup
   class="kw">delete cut;

「连接报文客户端标识的单元测试写法」

在 MT5 里做底层协议封装时,CPktConnect 这类报文构造类必须靠单元测试锁住字节布局,否则升级编译器后可能静默偏移。下面两段测试直接比对期望字节流和实际生成的 ByteArray,属于可复现的验证手段。 TEST_SetClientIdentifier 中,expected 写死 18 字节:头两字节 16,16 后跟 0,4 再是 ASCII 的 M Q T T,末尾带 M Q L 5 的 77,81,76,53。buf 长度取 expected.Size()-2,即 16,构造完调用 SetClientIdentifier("MQL5") 后 ArrayCopy 出结果,Assert 全等才返回 true。 TEST_SetClientIdentifierLength 则只验证长度字段写入:expected 为 14 字节,头部 16,12 表示后续负载缩短到 12,尾部不含 MQL5 字符串本身。两个测试都走 new/delete 配对,并用 ZeroMemory 清 result 防残留。 把这两段塞进你的 Script 或测试 EA,跑完 Print 出函数名即代表用例被执行;若 Assert 失败会返回 false,可据此定位 SetClientIdentifier 内部写入逻辑是否越界。外汇与贵金属自动化涉及真实资金,这类底层报文错误可能引发连接被拒,需在高风险认知下谨慎上线。

MQL5 / C++
class="type">bool TEST_SetClientIdentifier()
  {
   Print(__FUNCTION__);
class=class="str">"cmt">//--- Arrange
   class="kw">static class="type">uchar expected[] =
     {class="num">16, class="num">16, class="num">0, class="num">4, class="num">77, class="num">81, class="num">84, class="num">84, class="num">5, class="num">0, class="num">0, class="num">0, class="num">0, class="num">4, class="num">77, class="num">81, class="num">76, class="num">53};
   class="type">uchar buf[expected.Size() - class="num">2];
   CPktConnect *cut = new CPktConnect(buf);
class=class="str">"cmt">//--- Act
   cut.SetClientIdentifier("MQL5");
   class="type">uchar result[];
   ArrayCopy(result, cut.ByteArray);
class=class="str">"cmt">//--- Assert
   class="type">bool isTrue = Assert(expected, result);
class=class="str">"cmt">//--- cleanup
   class="kw">delete cut;
   ZeroMemory(result);
   class="kw">return  isTrue ? true : class="kw">false;
  }

class="type">bool TEST_SetClientIdentifierLength()
  {
   Print(__FUNCTION__);
class=class="str">"cmt">//--- Arrange
   class="kw">static class="type">uchar expected[] =
     {class="num">16, class="num">12, class="num">0, class="num">4, class="num">77, class="num">81, class="num">84, class="num">84, class="num">5, class="num">0, class="num">0, class="num">0, class="num">0, class="num">4};
   class="type">uchar buf[expected.Size() - class="num">2];
   CPktConnect *cut = new CPktConnect(buf);
class=class="str">"cmt">//--- Act
   cut.SetClientIdentifierLength("MQL5");
   class="type">uchar result[];
   ArrayCopy(result, cut.ByteArray);
class=class="str">"cmt">//--- Assert
   class="type">bool isTrue = Assert(expected, result);
class=class="str">"cmt">//--- cleanup
   class="kw">delete cut;
   ZeroMemory(result);
   class="kw">return  isTrue ? true : class="kw">false;
  }

◍ MQTT连接包的单元测试写法

在 MT5 里给自定义通信协议类写单测,核心思路是先摆一个静态期望字节数组,再让被测对象吐出实际字节流做逐字节比对。下面这段针对 CPktConnect 的测试,覆盖了 CleanStart 与 KeepAlive 两个字段的编码逻辑。 TEST_SetKeepAlive 里 expected 数组长度为 12,其中第 10、11 位为 0、10,对应 KeepAlive=10 秒的报文编码;而 TEST_SetCleanStart 的 expected 长度为 10,第 9 位为 2,表示 CleanStart 置位。两个用例都通过 expected.Size() - 2 来分配 buf,说明末尾两字节是动态算出来的。 Assert 函数用 ArrayCompare 做全等判断,若不等则逐位 printf 打出 expected 与 result 的十进制值,方便在专家日志里直接定位错位字节。注意 TEST_SetCleanStart 里 ZeroMemory 被注释掉了,意味着 result 数组在函数退出后未强制清零,跑大量用例时可能残留上一次数据,建议手动补上。 开 MT5 新建脚本把这几个函数塞进去,编译后看终端打印的 __FUNCTION__ 与比对结果,能直接验证你的连接包序列化有没有偏一个字节。外汇与贵金属行情推送走自建协议时,这种底层校验可显著降低断连概率,但协议层 bug 仍可能引发滑点,属高风险环节。

MQL5 / C++
  class="kw">static class="type">uchar expected[] =
    {class="num">16, class="num">10, class="num">0, class="num">4, class="num">77, class="num">81, class="num">84, class="num">84, class="num">5, class="num">2, class="num">0, class="num">10};
  class="type">uchar buf[expected.Size() - class="num">2];
  CPktConnect *cut = new CPktConnect(buf);
class=class="str">"cmt">//--- Act
  cut.SetCleanStart(true);
  cut.SetKeepAlive(class="num">10); class=class="str">"cmt">//class="num">10 secs
  class="type">uchar result[];
  ArrayCopy(result, cut.ByteArray);
class=class="str">"cmt">//--- Assert
  class="type">bool isTrue = Assert(expected, result);
class=class="str">"cmt">//--- cleanup
  class="kw">delete cut;
  ZeroMemory(result);
  class="kw">return  isTrue ? true : class="kw">false;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool TEST_SetKeepAlive()
  {
  Print(__FUNCTION__);
class=class="str">"cmt">//--- Arrange
  class="kw">static class="type">uchar expected[] =
    {class="num">16, class="num">10, class="num">0, class="num">4, class="num">77, class="num">81, class="num">84, class="num">84, class="num">5, class="num">0, class="num">0, class="num">10};
  class="type">uchar buf[expected.Size() - class="num">2];
  CPktConnect *cut = new CPktConnect(buf);
class=class="str">"cmt">//--- Act
  cut.SetKeepAlive(class="num">10); class=class="str">"cmt">//class="num">10 secs
  class="type">uchar result[];
  ArrayCopy(result, cut.ByteArray);
class=class="str">"cmt">//--- Assert
  class="type">bool isTrue = Assert(expected, result);
class=class="str">"cmt">//--- cleanup
  class="kw">delete cut;
  ZeroMemory(result);
  class="kw">return  isTrue ? true : class="kw">false;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool TEST_SetCleanStart()
  {
  Print(__FUNCTION__);
class=class="str">"cmt">//--- Arrange
  class="kw">static class="type">uchar expected[] =
    {class="num">16, class="num">8, class="num">0, class="num">4, class="num">77, class="num">81, class="num">84, class="num">84, class="num">5, class="num">2};
  class="type">uchar buf[expected.Size() - class="num">2];
  CPktConnect *cut = new CPktConnect(buf);
class=class="str">"cmt">//--- Act
  cut.SetCleanStart(true);
  class="type">uchar result[];
  ArrayCopy(result, cut.ByteArray);
class=class="str">"cmt">//--- Assert
  class="type">bool isTrue = Assert(expected, result);
class=class="str">"cmt">//--- cleanup
  class="kw">delete cut;
class=class="str">"cmt">//ZeroMemory(result);
  class="kw">return  isTrue ? true : class="kw">false;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool Assert(class="type">uchar& expected[], class="type">uchar& result[])
  {
  if(!ArrayCompare(expected, result) == class="num">0)
    {
      for(class="type">uint i = class="num">0; i < expected.Size(); i++)
        {
         printf("expected\t%d\t\t%d result", expected[i], result[i]);
        }

用数组比对校验指标输出一致性

在 MT5 自定义指标或 EA 的单元测试里,光看返回值不够,得把预期数组和实际结果数组拉出来逐元素比对。上面这段是校验失败时的打印逻辑:先报出两个数组的尺寸差异,再把 Expected 和 Result 分别用 ArrayPrint 铺开,方便肉眼定位哪一根柱子的数值偏了。 如果 expected.Size() 与 result.Size() 不相等,或者元素值对不上,函数直接 return false,测试中断;全部通过才 return true。实盘前跑一遍这类校验,能提前揪出因缓冲区索引错位导致的信号漂移——外汇与贵金属杠杆高,这类隐蔽 bug 可能放大滑点风险。 把这段代码嵌进你的测试脚本,开 MT5 用策略测试器的自定义品种回放,对比不同时间点算出的数组,十有八九能发现某个偏移参数没复位。

MQL5 / C++
   printf("expected size %d <=> %d result size", expected.Size(), result.Size());
   Print("Expected");
   ArrayPrint(expected);
   Print("Result");
   ArrayPrint(result);
   class="kw">return class="kw">false;
   }
  class="kw">return true;
}

「下一步要啃的是 CONNACK 回应」

这一节其实只是个路标:作者说下一步会处理 MQTT 协议里的 CONNACK 回应包,等这块落地,才算真正有了发布第一条消息的底座,而且照例会先写测试再写实现。 随文挂了 6 个 .mqh / .mq5 文件,其中 CPktConnect.mqh 有 9.97 KB、TEST_CControlPacket_Connect.mq5 是 5.89 KB,想跟进的人直接把 ZIP 拖进 MT5 的 MQL5/Include 和 Scripts 就能跑通现有连接测试。 外汇与贵金属行情受杠杆影响大、滑点频发,这类通信层封装只解决「传得通」,不解决「信号对」,实盘前务必在策略测试器里压过极端点差环境。 等 CONNACK 的解析和发布测试补完,这套 TDD 写的 MQTT 客户端才从「能连」走到「能发」,到时候开 MT5 把 TEST 文件跑一遍,比看十行说明都实在。

常见问题

把每个标志(用户名、密码、清除会话等)左移到对应 bit 位后做按位或,再回填到固定偏移的字节即可。
用 OASIS 官方文档给的示例字节序列做断言比对,逐字节一致才算封包正确。
可以,把你的封包逻辑发给小布,它能按 OASIS 样本逐字节核对并标出偏移错误的位。
覆盖空标识、超长标识、含保留字符三种用例,断言序列化后长度与字节内容符合协议。
必须在显式调用组装方法前手动注入缓冲区与配置,否则 CONNECT 报文会写出空壳字节。