源码加更03_TCP 封装和基础编解码为什么决定稳定性
[!abstract] 这一篇完整公开 TCP 读写封装、Remaining Length 编解码、字符串和载荷拼接。它是 MQTT 报文能否稳定落到 PLC 缓冲区的基础。
适合谁收藏
正在处理 TCP 粘包、半包、读写失败和缓冲区问题的工程师。 想把 MQTT Remaining Length 从规范条文落到 ST 代码的读者。 需要判断“报文错”和“传输层错”边界的人。
本篇核心图

读图重点:先看源码对象之间的职责边界,再看数据、状态和错误如何沿着调用链流动。源码加更不是把文件名列出来,而是把完整代码、工程意图和验证口径一起讲清楚。
先给结论
TCP 层只管字节通道,MQTT 层要自己判断完整报文、载荷长度和缓冲区边界。这层写不稳,上面所有 QoS 都会变成假稳定。
从理论到代码实现链路
MQTT 标准给的是报文类型、固定头、可变头、载荷、QoS 交互和会话语义;PLC 工程真正要解决的是周期扫描、缓冲区长度、错误锁存、在线变量、连接重入和现场可诊断性。
所以这套开源实现不能只按协议章节拆,也不能只按文件名拆。正确读法是把标准约束翻译成程序对象:入口程序负责给命令和观测点,GVL 和 DUT 定义容量与数据模型,主功能块负责调度状态机,构建方法负责出站报文,处理方法负责入站报文,辅助方法负责长度、队列、事务、主题和诊断边界。
本篇完整公开 TCP 封装、Remaining Length 工具、字符串载荷拼接和读缓冲方法。
再往下一层看,这里其实有两条线同时存在。第一条是协议线:固定头、Remaining Length、PacketId、QoS、Topic、Payload 和 Reason Code 必须能按 MQTT 规则组合起来。第二条是 PLC 工程线:每个周期只能推进有限步骤,所有中间状态都要能被在线变量观察,所有错误都要能被锁存并归类,所有缓冲区长度都要在写入前被检查。
这就是源码加更必须完整公开的原因。只给几段核心片段,读者最多能看懂某个判断;把完整对象放出来,读者才能看到对象之间如何传递状态、长度、错误和诊断信息。完整源码讲解不是为了堆代码,而是为了让读者能从标准约束一路追到可运行的 ST 对象,再从现场现象反向定位到具体边界。
本篇公开的完整源码范围
FB_MqttClient.M_TcpClient.st | ||
FB_MqttClient.M_TcpRead.st | ||
FB_MqttClient.M_TcpWrite.st | ||
FB_EncodeRemainingLength.st | ||
FB_DecodeRemainingLength.st | ||
M_EncodeRemainingLength.st | ||
M_DecodeRemainingLength.st | ||
M_AppendString.st | ||
M_AppendPayload.st | ||
M_CopyBytesToString.st | ||
M_ReadIntoBuffer.st | ||
M_IsValidUtf8String.st |
怎么读这些源码
第一遍只看对象职责:这个文件解决哪一层问题,是入口、模型、状态、构建、接收、事务,还是诊断。
第二遍看边界变量:长度、索引、PacketId、QoS、状态枚举、错误码、缓冲区水位和在线观测量。PLC 通信代码最怕的是“能跑但不可诊断”,所以每个关键对象都要问一句:现场出问题时,我能不能从它留下的变量看出原因。
第三遍再看具体语句。源码全部公开,不等于读者要从第一行顺序读到最后一行。更稳的方式是用图和表先建立地图,再回到完整代码里确认每个边界确实落地。
工程验证路径
用抓包和在线变量对照:TCP 读到的字节数、Remaining Length 解码结果、有效载荷长度和字符串转换结果必须一致。
本篇完整开源代码
完整代码 1:FB_MqttClient.M_TcpClient.st
这一段完整公开 FB_MqttClient.M_TcpClient.st。读代码时先看对象职责,再看状态、长度、错误和返回值,不要只抄几行赋值。
/// =======================================================================/// 名称 : M_TcpClient/// 功能 : 控制 TCP 客户端的连接与断开/// 说明 : 封装 NBS TCP 客户端连接调用,输出连接状态与错误信息。/// 编程人员 : ControlRookie/// 时间 : 2026-01-10/// 版本 : V1.1/// ======================================================================={attribute 'hide_all_locals'}METHOD M_TcpClientVAR_INPUTbEnable : BOOL; // TCP 连接使能标志sIP : STRING; // 服务器 IP 地址uiPortNum : UINT; // 目标 Broker 提供 MQTT 服务的监听端口号END_VARVAR_OUTPUTbIsConnected : BOOL; // 连接成功标志bError : BOOL; // 错误标志eError : NBS.ERROR; // NBS 错误码END_VARVARstIP : NBS.IP_ADDR; // 服务器地址结构END_VAR// === IMPLEMENTATION ===// 把字符串地址先转成 NBS 需要的地址结构,再交给底层 TCP_Client。stIP.sAddr := sIP;fbTcpClient(xEnable := bEnable,ipAddr := stIP,uiPort := uiPortNum,eError => eError,hConnection => hConnection);// 统一把底层错误状态透传到上层状态机。bError := fbTcpClient.xError;// 只有底层已经 active 且句柄有效时,才认为 TCP 连接真正建立成功。IF fbTcpClient.xActive AND fbTcpClient.hConnection <> 0 THENbIsConnected := TRUE;ELSEbIsConnected := FALSE;END_IF
完整代码 2:FB_MqttClient.M_TcpRead.st
这一段完整公开 FB_MqttClient.M_TcpRead.st。读代码时先看对象职责,再看状态、长度、错误和返回值,不要只抄几行赋值。
/// =======================================================================/// 名称 : M_TcpRead/// 功能 : 从 TCP 连接读取数据到接收缓冲区/// 说明 : 封装 NBS.TCP_Read,读取成功后输出完成标志和实际字节数。/// 编程人员 : ControlRookie/// 时间 : 2026-05-05/// 版本 : V1.2/// ======================================================================={attribute 'hide_all_locals'}METHOD M_TcpReadVAR_INPUTbEnable : BOOL; // TCP 读取使能标志pDataReceive : POINTER TO BYTE; // 接收数据区首地址udiDataSize : UDINT; // 本次允许写入接收缓冲区的最大字节数[byte]END_VARVAR_OUTPUTbDone : BOOL; // 读取完成标志bError : BOOL; // 错误标志eError : NBS.ERROR; // NBS 错误码udiBytesRead : UDINT; // 本次实际从 TCP 连接读取的字节数[byte]END_VAR// === IMPLEMENTATION ===// 每周期持续调用底层 TCP_Read,是否真的读由 bEnable 控制。fbTcpRead(xEnable := bEnable,xError => bError,hConnection := hConnection,szSize := udiDataSize,pData := pDataReceive,eError => eError);// 只有底层声明 ready 且无错误时,才把本次读取结果上报给上层状态机。IF fbTcpRead.xReady AND NOT fbTcpRead.xError THENbDone := TRUE;udiBytesRead := TO_UDINT(fbTcpRead.szCount);ELSEbDone := FALSE;udiBytesRead := 0;END_IF// 关闭读取使能时,显式把完成标志和本次字节数清零,避免上层误判复用旧结果。IF NOT bEnable THENbDone := FALSE;udiBytesRead := 0;END_IF
完整代码 3:FB_MqttClient.M_TcpWrite.st
这一段完整公开 FB_MqttClient.M_TcpWrite.st。读代码时先看对象职责,再看状态、长度、错误和返回值,不要只抄几行赋值。
/// =======================================================================/// 名称 : M_TcpWrite/// 功能 : 向 TCP 连接发送缓冲区数据/// 说明 : 包装 NBS.TCP_Write,发送成功后置位完成标志。/// 编程人员 : ControlRookie/// 时间 : 2026-05-05/// 版本 : V1.1/// ======================================================================={attribute 'hide_all_locals'}METHOD M_TcpWriteVAR_INPUTbExecute : BOOL; // TCP 写入执行标志pDataSend : POINTER TO BYTE; // 发送数据区首地址udiDataSize : UDINT; // 本次计划发送的字节数[byte]END_VARVAR_OUTPUTbDone : BOOL; // 发送完成标志bError : BOOL; // 错误标志eError : NBS.ERROR; // NBS 错误码END_VAR// === IMPLEMENTATION ===// 每周期持续调用底层 TCP_Write,是否真正触发发送由 bExecute 控制。fbTcpWrite(xExecute := bExecute,xError => bError,hConnection := hConnection,szSize := udiDataSize,pData := pDataSend,eError => eError);// 发送脉冲撤销时立即清掉完成锁存,确保下一次发送重新形成独立 done 事件。IF NOT bExecute THENbWriteDoneLatched := FALSE;ELSIF fbTcpWrite.xError THENbWriteDoneLatched := FALSE;ELSIF fbTcpWrite.xDone THEN// xDone 只要出现一次就锁存住,交给上层状态机在下一拍读取。bWriteDoneLatched := TRUE;END_IFbDone := bWriteDoneLatched;
完整代码 4:FB_EncodeRemainingLength.st
这一段完整公开 FB_EncodeRemainingLength.st。读代码时先看对象职责,再看状态、长度、错误和返回值,不要只抄几行赋值。
/// =======================================================================/// 名称 : FB_EncodeRemainingLength/// 功能 : 编码 MQTT 剩余长度字段/// 说明 : 将载荷长度编码为 MQTT 可变字节整数格式/// 编程人员 : ControlRookie/// 时间 : 2026-01-10/// 版本 : V1.1/// ======================================================================={attribute 'hide_all_locals'}FUNCTION_BLOCK FB_EncodeRemainingLengthVAR_INPUTuiNumberOfBytes : UINT; // 待写入报文头的 MQTT 剩余长度[byte]END_VARVAR_OUTPUTiEncodedBytesCount : INT; // MQTT 剩余长度编码结果的字节数[byte]aEncodedBytes : ARRAY[0..3] OF BYTE; // MQTT 剩余长度编码结果缓冲区[byte]END_VARVARbyEncodedByte : BYTE; // 当前编码字节uiTemp : UINT; // 中间长度变量bContinue : BOOL; // 是否继续编码下一个字节END_VAR// === IMPLEMENTATION ===// Remaining Length 采用 MQTT 可变字节整数编码:// 每个字节低 7 位承载数值,最高位表示“后面还有没有后续字节”。uiTemp := uiNumberOfBytes;iEncodedBytesCount := 0;REPEATbyEncodedByte := TO_BYTE(uiTemp MOD 128);uiTemp := uiTemp / 128;// 只要后面还有剩余数值,就把续位标志位置 1。IF uiTemp > 0 THENbyEncodedByte := byEncodedByte OR 16#80;bContinue := TRUE;ELSEbContinue := FALSE;END_IF;aEncodedBytes[iEncodedBytesCount] := byEncodedByte;iEncodedBytesCount := iEncodedBytesCount + 1;// MQTT 标准规定 Remaining Length 最多只允许编码成 4 个字节。IF iEncodedBytesCount >= 4 THENbContinue := FALSE;END_IF;UNTIL (NOT bContinue)END_REPEAT;
完整代码 5:FB_DecodeRemainingLength.st
这一段完整公开 FB_DecodeRemainingLength.st。读代码时先看对象职责,再看状态、长度、错误和返回值,不要只抄几行赋值。
/// =======================================================================/// 名称 : FB_DecodeRemainingLength/// 功能 : 解码 MQTT 剩余长度字段/// 说明 : 将 MQTT 可变字节整数格式解码为实际长度值/// 编程人员 : ControlRookie/// 时间 : 2026-01-10/// 版本 : V1.1/// ======================================================================={attribute 'hide_all_locals'}FUNCTION_BLOCK FB_DecodeRemainingLengthVAR_INPUTpEncodedBytes : POINTER TO BYTE; // 指向 MQTT Remaining Length 编码起始字节的缓冲区指针END_VARVAR_OUTPUTuiDecodedLength : UINT; // 解码得到的 MQTT 剩余长度[byte]uiBytesUsed : UINT; // 解码该长度字段实际消耗的字节数[byte]bDone : BOOL; // 解码是否成功END_VARVARuiMultiplier : UINT := 1; // 可变字节整数权重uiValue : UINT := 0; // 解码中间值byByte : BYTE; // 当前读取字节pCurrent : POINTER TO BYTE; // 当前读取指针uiCount : UINT := 0; // 当前已读取的编码字节数[byte]END_VAR// === IMPLEMENTATION ===// Remaining Length 采用 MQTT 的可变字节整数编码,最多 4 个字节。// 这里逐字节累乘 128 权重,把编码值还原成真实长度。pCurrent := pEncodedBytes;bDone := FALSE;uiCount := 0;WHILE uiCount < 4 DOIF uiCount = 0 THENuiValue := 0;uiMultiplier := 1;END_IF// 低 7 位是本字节贡献的值,最高位是“后面是否还有字节”的续位标志。byByte := pCurrent^;uiValue := uiValue + (BYTE_TO_UINT(byByte AND 16#7F) * uiMultiplier);uiMultiplier := uiMultiplier * 128;uiCount := uiCount + 1;IF (byByte AND 16#80) = 0 THENbDone := TRUE;EXIT;END_IF;pCurrent := pCurrent + 1;END_WHILE;IF bDone THENuiDecodedLength := uiValue;uiBytesUsed := uiCount;ELSE// 超过 4 字节仍未结束时视为非法编码,输出清零并报失败。uiDecodedLength := 0;uiBytesUsed := 0;END_IF;
完整代码 6:M_EncodeRemainingLength.st
这一段完整公开 M_EncodeRemainingLength.st。读代码时先看对象职责,再看状态、长度、错误和返回值,不要只抄几行赋值。
/// =======================================================================/// 名称 : M_EncodeRemainingLength/// 功能 : 编码 MQTT 剩余长度字段/// 说明 : 将剩余长度按 MQTT 可变长度格式写入缓冲区并返回字节数。/// 编程人员 : ControlRookie/// 时间 : 2026-05-05/// 版本 : V1.0/// ======================================================================={attribute 'hide_all_locals'}METHOD M_EncodeRemainingLength : UINTVAR_INPUTudiLength : UDINT; // 待编码写入报文头的 MQTT 剩余长度[byte]pBuffer : POINTER TO BYTE; // 指向剩余长度编码输出起始地址的指针END_VARVARuiBytes : UINT := 0; // 当前已写入的 VBI 编码字节数[byte]byEncodedByte: BYTE; // 当前准备写入缓冲区的 1 个 VBI 编码字节udiX : UDINT; // Remaining Length 编码过程中的剩余待分解数值[byte]pByte : POINTER TO BYTE; // 当前 VBI 编码字节写入位置指针uiLoopGuard : UINT := 0; // VBI 编码循环保护计数器(最多允许写 4 个字节)END_VAR// === IMPLEMENTATION ===// Remaining Length 采用 MQTT 可变字节整数编码:// 每个字节低 7 位承载数值,最高位表示“后面是否还有后续字节”。pByte := pBuffer;udiX := udiLength;REPEATbyEncodedByte := UDINT_TO_BYTE(udiX MOD 128);udiX := udiX / 128;// 只要后面还有剩余数值,就把续位标志位置 1。IF udiX > 0 THENbyEncodedByte := byEncodedByte OR 16#80;END_IFpByte^ := byEncodedByte;pByte := pByte + 1;uiBytes := uiBytes + 1;// MQTT 标准规定 Remaining Length 最多只允许编码成 4 个字节,这里用保护计数强制收口。uiLoopGuard := uiLoopGuard + 1;UNTIL(udiX = 0 OR uiLoopGuard >= 4)END_REPEATM_EncodeRemainingLength := uiBytes;
完整代码 7:M_DecodeRemainingLength.st
这一段完整公开 M_DecodeRemainingLength.st。读代码时先看对象职责,再看状态、长度、错误和返回值,不要只抄几行赋值。
/// =======================================================================/// 名称 : M_DecodeRemainingLength/// 功能 : 解码 MQTT 剩余长度字段/// 说明 : 从缓冲区解析可变长度编码,返回消耗字节数并输出长度值。/// 编程人员 : ControlRookie/// 时间 : 2026-05-05/// 版本 : V1.0/// ======================================================================={attribute 'hide_all_locals'}METHOD M_DecodeRemainingLength : UINTVAR_INPUTpBuffer : POINTER TO BYTE; // 指向 MQTT 剩余长度编码起始字节的指针END_VARVAR_OUTPUTuiLength : UINT; // 解码得到的 MQTT 剩余长度[byte]END_VARVARuiBytes : UINT := 0; // 当前已消费的编码字节数[byte]uiMultiplier : UINT := 1; // 可变字节整数权重byEncodedByte : BYTE; // 当前编码字节pByte : POINTER TO BYTE;// 当前读取指针END_VAR// === IMPLEMENTATION ===pByte := pBuffer;uiLength := 0;REPEATbyEncodedByte := pByte^;uiLength := uiLength + ((byEncodedByte AND 127) * uiMultiplier);uiMultiplier := uiMultiplier * 128;pByte := pByte + 1;uiBytes := uiBytes + 1;UNTIL (byEncodedByte AND 128) = 0 OR uiBytes >= 4END_REPEATM_DecodeRemainingLength := uiBytes;
完整代码 8:M_AppendString.st
这一段完整公开 M_AppendString.st。读代码时先看对象职责,再看状态、长度、错误和返回值,不要只抄几行赋值。
/// =======================================================================/// 名称 : M_AppendString/// 功能 : 追加 UTF-8 字符串到发送缓冲区/// 说明 : 先写入 2 字节长度前缀,再写入字符串内容并返回总字节数。/// 编程人员 : ControlRookie/// 时间 : 2026-05-05/// 版本 : V1.0/// ======================================================================={attribute 'hide_all_locals'}METHOD M_AppendString : UINTVAR_INPUTsStr : STRING(GVL_Mqtt.cnMaxPayloadSize); // 需要按 MQTT UTF-8 字符串格式写入的文本内容pBuffer : POINTER TO BYTE; // 指向目标字符串写入起始地址的缓冲区指针END_VARVARuiLen : UINT; // 待写入字符串内容长度[byte]i : DINT; // 逐字节复制字符串内容时使用的循环索引pWrite : POINTER TO BYTE; // 当前字符串写入位置指针END_VAR// === IMPLEMENTATION ===uiLen := TO_UINT(LEN(sStr));// 长度前缀(MSB + LSB)pWrite := pBuffer;pWrite^ := UINT_TO_BYTE(SHR(uiLen, 8));pWrite := pWrite + 1;pWrite^ := UINT_TO_BYTE(uiLen AND 16#FF);// 字符串内容pWrite := pBuffer + 2;IF uiLen > 0 THENFOR i := 1 TO TO_DINT(uiLen) DOpWrite^ := TO_BYTE(sStr[i - 1]);pWrite := pWrite + 1;END_FOREND_IFM_AppendString := uiLen + 2;
完整代码 9:M_AppendPayload.st
这一段完整公开 M_AppendPayload.st。读代码时先看对象职责,再看状态、长度、错误和返回值,不要只抄几行赋值。
/// =======================================================================/// 名称 : M_AppendPayload/// 功能 : 追加消息载荷到发送缓冲区/// 说明 : 按字节拷贝字符串载荷并返回写入字节数。/// 编程人员 : ControlRookie/// 时间 : 2026-05-05/// 版本 : V1.0/// ======================================================================={attribute 'hide_all_locals'}METHOD M_AppendPayload : UINTVAR_INPUTsPayload : STRING(GVL_Mqtt.cnMaxPayloadSize); // 需要原样拷贝到 MQTT 载荷区的字符串内容pBuffer : POINTER TO BYTE; // 指向目标载荷写入起始地址的缓冲区指针END_VARVARuiLen : UINT; // 待写入字符串载荷长度[byte]i : DINT; // 逐字节复制载荷时使用的循环索引pWrite : POINTER TO BYTE; // 当前载荷写入位置指针END_VAR// === IMPLEMENTATION ===uiLen := TO_UINT(LEN(sPayload));pWrite := pBuffer;IF uiLen > 0 THENFOR i := 1 TO TO_DINT(uiLen) DOpWrite^ := TO_BYTE(sPayload[i - 1]);pWrite := pWrite + 1;END_FOREND_IFM_AppendPayload := uiLen;
完整代码 10:M_CopyBytesToString.st
这一段完整公开 M_CopyBytesToString.st。读代码时先看对象职责,再看状态、长度、错误和返回值,不要只抄几行赋值。
/// =======================================================================/// 名称 : M_CopyBytesToString/// 功能 : 将字节缓冲区复制到字符串/// 说明 : 用顺序拷贝替代逐字符 CONCAT,降低接收解析中的字符串构造开销。/// 编程人员 : ControlRookie/// 时间 : 2026-05-07/// 版本 : V2.0/// ======================================================================={attribute 'hide_all_locals'}METHOD M_CopyBytesToString : BOOLVAR_INPUTpSource : POINTER TO BYTE; // 源字节缓冲区首地址uiByteCount : UINT; // 计划从源缓冲区复制到字符串的字节数[byte]END_VARVAR_IN_OUTsTarget : STRING; // 目标字符串END_VARVARpRead : POINTER TO BYTE; // 当前源读取指针pWrite : POINTER TO BYTE; // 当前目标写入指针uiCopyLen : UINT; // 实际复制字节数uiTargetLen : UINT; // 目标字符串最大可写字符数i : DINT; // 循环索引END_VAR// === IMPLEMENTATION ===pWrite := ADR(sTarget);// 零长度时按“合法空字符串”处理,显式写入终止符。IF uiByteCount = 0 THENpWrite^ := 0;M_CopyBytesToString := TRUE;RETURN;END_IF// 空指针代表上游解析上下文异常,返回 FALSE 并输出空字符串。IF pSource = 0 THENpWrite^ := 0;M_CopyBytesToString := FALSE;RETURN;END_IF// 目标缓冲区至少要能放下 1 个字符串结束符。IF SIZEOF(sTarget) <= 1 THENpWrite^ := 0;M_CopyBytesToString := FALSE;RETURN;END_IF// 实际复制长度永远裁剪到目标字符串可容纳的最大字符数,避免越界写入。uiTargetLen := TO_UINT(SIZEOF(sTarget) - 1);uiCopyLen := uiByteCount;IF uiCopyLen > uiTargetLen THENuiCopyLen := uiTargetLen;END_IFpRead := pSource;IF uiCopyLen > 0 THEN// 逐字节顺序拷贝比反复 CONCAT 更轻量,适合接收路径里的高频字符串组装。FOR i := 1 TO TO_DINT(uiCopyLen) DOpWrite^ := pRead^;pWrite := pWrite + 1;pRead := pRead + 1;END_FOREND_IF// 手动补 0 结束符,把原始字节缓冲区显式收口成 IEC STRING。pWrite^ := 0;M_CopyBytesToString := TRUE;
完整代码 11:M_ReadIntoBuffer.st
这一段完整公开 M_ReadIntoBuffer.st。读代码时先看对象职责,再看状态、长度、错误和返回值,不要只抄几行赋值。
/// =======================================================================/// 名称 : M_ReadIntoBuffer/// 功能 : 统一执行接收并累加到缓冲区/// 说明 : 读取 TCP 数据并做边界检查,成功返回 TRUE/// 编程人员 : ControlRookie/// 时间 : 2026-05-08/// 版本 : V2.0/// ======================================================================={attribute 'hide_all_locals'}METHOD M_ReadIntoBuffer : BOOLVAREND_VAR// === IMPLEMENTATION ===// 先打开底层读取请求;真正是否拿到数据,由 M_TcpRead 回填的 bHasRead 决定。bTcpRead := TRUE;// 本周期底层还没拿到数据时,先退出,等待下一个扫描周期继续。IF NOT bHasRead THENM_ReadIntoBuffer := FALSE;RETURN;END_IFbTcpRead := FALSE;// 把本次读取结果拼到接收缓冲区尾部前,先做容量保护。IF uiRxLength + TO_UINT(udiBytesRead) > SIZEOF(aRxBuf) THENM_SetError(uiErrorCode := TO_UINT(E_ReasonCode.uiErrBufferOverflow),sMessage := 'Receive buffer overflow');eState := E_MqttState.iTcpDisconnect;M_ReadIntoBuffer := FALSE;RETURN;END_IFuiRxLength := uiRxLength + TO_UINT(udiBytesRead);M_ReadIntoBuffer := TRUE;
完整代码 12:M_IsValidUtf8String.st
这一段完整公开 M_IsValidUtf8String.st。读代码时先看对象职责,再看状态、长度、错误和返回值,不要只抄几行赋值。
/// =======================================================================/// 名称 : M_IsValidUtf8String/// 功能 : 校验字符串是否满足 MQTT UTF-8 基础约束/// 说明 : 当前实现做 PLC 可承受范围内的基础校验,拒绝 NUL、代理区与非法续字节/// 编程人员 : ControlRookie/// 时间 : 2026-05-05/// 版本 : V1.0/// ======================================================================={attribute 'hide_all_locals'}METHOD M_IsValidUtf8String : BOOLVAR_INPUTsValue : STRING(GVL_Mqtt.cnMaxTopicLen); // 待校验字符串END_VARVARuiLen : UINT; // 字符串长度uiIndex : UINT; // 当前扫描到待校验字符串的第几个字节位置byLead : BYTE; // 首字节byCont1 : BYTE; // 第 1 个续字节byCont2 : BYTE; // 第 2 个续字节byCont3 : BYTE; // 第 3 个续字节END_VAR// === IMPLEMENTATION ===/// MQTT 字符串在协议层要求使用 UTF-8。/// 这里按字节扫描,拒绝最常见的非法场景:/// - NUL 字节/// - 残缺多字节序列/// - 非法续字节/// - 代理区和超出 Unicode 上限的编码uiLen := TO_UINT(LEN(sValue));uiIndex := 1;WHILE uiIndex <= uiLen DObyLead := TO_BYTE(sValue[uiIndex - 1]);/// MQTT UTF-8 字符串中不允许出现 NUL。IF byLead = 0 THENM_IsValidUtf8String := FALSE;RETURN;END_IFIF byLead < 16#80 THEN/// 单字节 ASCII。uiIndex := uiIndex + 1;ELSIF (byLead >= 16#C2) AND (byLead <= 16#DF) THEN/// 2 字节序列:110xxxxx 10xxxxxxIF uiIndex + 1 > uiLen THENM_IsValidUtf8String := FALSE;RETURN;END_IFbyCont1 := TO_BYTE(sValue[uiIndex]);IF (byCont1 < 16#80) OR (byCont1 > 16#BF) THENM_IsValidUtf8String := FALSE;RETURN;END_IFuiIndex := uiIndex + 2;ELSIF byLead = 16#E0 THEN/// 3 字节序列特例:限制第二字节下界,避免过短编码。IF uiIndex + 2 > uiLen THENM_IsValidUtf8String := FALSE;RETURN;END_IFbyCont1 := TO_BYTE(sValue[uiIndex]);byCont2 := TO_BYTE(sValue[uiIndex + 1]);IF (byCont1 < 16#A0) OR (byCont1 > 16#BF) OR(byCont2 < 16#80) OR (byCont2 > 16#BF) THENM_IsValidUtf8String := FALSE;RETURN;END_IFuiIndex := uiIndex + 3;ELSIF ((byLead >= 16#E1) AND (byLead <= 16#EC)) OR ((byLead >= 16#EE) AND (byLead <= 16#EF)) THEN/// 常规 3 字节序列。IF uiIndex + 2 > uiLen THENM_IsValidUtf8String := FALSE;RETURN;END_IFbyCont1 := TO_BYTE(sValue[uiIndex]);byCont2 := TO_BYTE(sValue[uiIndex + 1]);IF (byCont1 < 16#80) OR (byCont1 > 16#BF) OR(byCont2 < 16#80) OR (byCont2 > 16#BF) THENM_IsValidUtf8String := FALSE;RETURN;END_IFuiIndex := uiIndex + 3;ELSIF byLead = 16#ED THEN/// 代理区特例:第二字节只能到 16#9F,避免落入 UTF-16 surrogate 范围。IF uiIndex + 2 > uiLen THENM_IsValidUtf8String := FALSE;RETURN;END_IFbyCont1 := TO_BYTE(sValue[uiIndex]);byCont2 := TO_BYTE(sValue[uiIndex + 1]);IF (byCont1 < 16#80) OR (byCont1 > 16#9F) OR(byCont2 < 16#80) OR (byCont2 > 16#BF) THENM_IsValidUtf8String := FALSE;RETURN;END_IFuiIndex := uiIndex + 3;ELSIF byLead = 16#F0 THEN/// 4 字节序列下界特例:避免过短编码。IF uiIndex + 3 > uiLen THENM_IsValidUtf8String := FALSE;RETURN;END_IFbyCont1 := TO_BYTE(sValue[uiIndex]);byCont2 := TO_BYTE(sValue[uiIndex + 1]);byCont3 := TO_BYTE(sValue[uiIndex + 2]);IF (byCont1 < 16#90) OR (byCont1 > 16#BF) OR(byCont2 < 16#80) OR (byCont2 > 16#BF) OR(byCont3 < 16#80) OR (byCont3 > 16#BF) THENM_IsValidUtf8String := FALSE;RETURN;END_IFuiIndex := uiIndex + 4;ELSIF (byLead >= 16#F1) AND (byLead <= 16#F3) THEN/// 常规 4 字节序列。IF uiIndex + 3 > uiLen THENM_IsValidUtf8String := FALSE;RETURN;END_IFbyCont1 := TO_BYTE(sValue[uiIndex]);byCont2 := TO_BYTE(sValue[uiIndex + 1]);byCont3 := TO_BYTE(sValue[uiIndex + 2]);IF (byCont1 < 16#80) OR (byCont1 > 16#BF) OR(byCont2 < 16#80) OR (byCont2 > 16#BF) OR(byCont3 < 16#80) OR (byCont3 > 16#BF) THENM_IsValidUtf8String := FALSE;RETURN;END_IFuiIndex := uiIndex + 4;ELSIF byLead = 16#F4 THEN/// 4 字节序列上界特例:Unicode 最高只允许到 U+10FFFF。IF uiIndex + 3 > uiLen THENM_IsValidUtf8String := FALSE;RETURN;END_IFbyCont1 := TO_BYTE(sValue[uiIndex]);byCont2 := TO_BYTE(sValue[uiIndex + 1]);byCont3 := TO_BYTE(sValue[uiIndex + 2]);IF (byCont1 < 16#80) OR (byCont1 > 16#8F) OR(byCont2 < 16#80) OR (byCont2 > 16#BF) OR(byCont3 < 16#80) OR (byCont3 > 16#BF) THENM_IsValidUtf8String := FALSE;RETURN;END_IFuiIndex := uiIndex + 4;ELSE/// 其余首字节模式都不属于 MQTT 可接受的 UTF-8 编码。M_IsValidUtf8String := FALSE;RETURN;END_IFEND_WHILEM_IsValidUtf8String := TRUE;
这一篇你最该记住的几句话
源码加更不是片段展示,而是完整源码对象公开讲解。 先建立对象地图,再读状态、报文和事务,现场调试才不会迷路。 判断源码成熟度,不只看功能是否实现,还要看边界、错误和在线观测量是否闭环。
系列导航
系列定位:MqttClient 系列教程,源码加更阶段,第 13 篇 / 共 16 篇 上一篇:源码加更02 下一篇:源码加更04
夜雨聆风