Lua应用示例
【Lua示例】读取LM2-Q系列激光位移传感器的测量值
【一越智能并联机器人】读取编码器值并发送视觉抓取坐标指令到机器人
【Lua示例】按照日期动态创建目录并保存输入的图片
【Lua示例】单字节字符串拆分归类
【Lua示例】ASCII码转换
【Lua示例】获取指定月份天数
【Lua示例】TCP通讯发送十六进制字符串
【Lua示例】TCP 报文粘包拆包解析脚本说明文档
本站点使用 MrDoc 构建
-
+
首页
【Lua示例】TCP 报文粘包拆包解析脚本说明文档
# TCP 报文粘包拆包解析脚本说明文档 ## 概述 接收 TCP 通讯通道输入的数据流片段,通过全局缓存拼接处理粘包与半包,按 `{`、`}` 边界拆出完整报文,经长度与首尾校验后提取 `dsID`、`reqType`、`camID` 三个字段;当识别到相机抓拍指令时触发 VG3 流程。适用于设备指令接收、TCP 流数据解析、按状态自动触发流程等场景。 ## 技术说明 环境:NASI 单字节,VG3 Lua Runtime 依赖:`LuaLog`\(Debug\)、`LuaSystem`\(TriggerLineByName\) 排序:无 边界: 1. 协议为扁平单层 JSON,以 `{` 开头、`}` 结尾,**不支持嵌套结构**; 2. 报文字段值内部不允许出现 `{`、`}`,否则拆包边界会错位; 3. `buffer` 为全局变量(不加 `local`),跨周期保留半包,否则粘包功能失效; 4. 缓存超过上限自动整体清空,防止异常情况下内存持续膨胀; 5. 一个周期最多取出一条完整报文,剩余包留在缓存等待下一周期。 ## 函数列表 ``` --1. trimSpace(str) 去除字符串首尾空白(空格、Tab、换行、回车) 参数: str (string) 原始字符串,支持 nil 返回: pktStr (string) 清理后的字符串;输入 nil 返回 nil ``` 示例: ``` local pktStr = trimSpace(" {\"a\":1} ") ``` ``` --2. checkPacketLength(pktStr, minLen, maxLen) 报文长度区间校验,过滤过短垃圾包与超长异常包 参数: pktStr (string) 已 trim 的报文 minLen (number) 最小长度,默认 10 maxLen (number) 最大长度,默认 256 返回: ok (boolean) true 长度合法;false 长度非法 ``` ``` --3. checkPacketHeadTail(pktStr) 校验报文首字符为 {、尾字符为 } 参数: pktStr (string) 已 trim 的报文 返回: ok (boolean) true 首尾括号匹配正确 ``` ``` --4. checkPacketValid(rawStr) 综合校验入口:去首尾空白 → 长度校验 → 首尾 {} 校验 参数: rawStr (string) 待校验原始报文 返回: valid (boolean) 全部校验通过为 true pktStr (string) 清理后的报文;校验失败时为 nil 或原串 ``` ``` --5. parsePacket(rawStr) 从合法报文中提取 dsID、reqType、camID 三个字段 参数: rawStr (string) 单条完整报文 返回: cmd (table) {dsID=字符串, reqType=字符串, camID=整数} 提取失败对应 key 为 nil ``` 字段提取规则: - `dsID":"xxx"`、`reqType":"xxx"`:双引号包裹字符串; - `camID":123`:数字类型,终止符为逗号或 `}`,兼容末尾无逗号场景。 ``` --6. popOnePacket(buf) 从缓存中取出一条完整 {} 报文(核心拆包函数) 参数: buf (string) 当前缓存字符串 返回: onePkt (string/nil) 取出的完整报文;无闭合包时返回 nil remain (string) 取走后剩余的缓存;半包时原样返回 ``` 逻辑:找第一个 `{` → 向后找第一个 `}` → 截取为一条报文;脏前缀自动跳过丢弃。 ``` --7. statBuffer(buf) 统计缓存状态,用于调试日志 参数: buf (string) 当前缓存字符串 返回: totalLen (number) 缓存总长度 fullPktCount (number) 估算完整包数({ 与 } 计数取小值,仅供参考) hasHalfPkt (boolean) { 数量 > } 数量,存在未闭合半包 ``` ``` --8. LuaSystem:TriggerLineByName(flowType, name) 触发 VG3 指定流程 参数: flowType (number) 0 配方流程 / 1 全局流程 name (string) 流程名称 ``` 示例: ``` LuaSystem:TriggerLineByName(0, "N点标定") ``` ## 运行实例 输入:收到完整相机抓拍报文 ``` {"dsID":"www.hc-system.com.cam","reqType":"photo","camID":1} ``` 输出: ``` oDsID = www.hc-system.com.cam oReqType = photo oCamID = 1 ``` 触发 N点标定流程。 输入:收到 PLC 报文 / 半包 / 粘包残留 ``` oHasPkt = false (半包等待下一轮) ``` ## 报文参考 | 命令 | 报文示例 | 说明 | | ------ | -------------------------------------------------------------- | --------------- | | 相机抓拍 | `{"dsID":"www.hc-system.com.cam","reqType":"photo","camID":1}` | cam1 抓拍,触发 N点标定 | | PLC 报文 | `{"dsID":"www.hc-system.com.plc","reqType":"read",...}` | 合法但非目标指令,跳过 | ## 配置参数 | 参数 | 默认值 | 说明 | | ---------------- | --- | -------------------- | | `BUFFER_MAX_LEN` | 512 | 全局缓存最大字节长度,超过自动清空并告警 | | `PKT_MIN_LEN` | 10 | 单条报文最小长度 | | `PKT_MAX_LEN` | 256 | 单条报文最大长度 | ## 场景一:粘包与半包处理 缓存接收两条粘包报文: - 本轮取出第一条(cam1),第二条留在 buffer; - 下一周期取出第二条(cam2),严格 FIFO,顺序不乱。 半包(只有 `{` 没有 `}`): - 无闭合 `}`,整段保留在 buffer; - 下一周期收到剩余字节后拼接成完整报文再拆出。 ## 场景二:缓存溢出保护 持续乱发数据导致缓存长度超过 512: ``` 【缓存溢出告警】缓存长度= 600 超过上限= 512,自动清空buffer! ``` 整体清空缓存,丢弃所有排队报文,防止内存膨胀。 ## 场景三:错误输出(oErr) | 错误场景 | oErr 内容 | | ------------ | ------------------------- | | 报文为空 | `ERR:EMPTY_PKT` | | 长度超限 | `ERR:LENGTH_OUT_OF_RANGE` | | 缺少 `{` 或 `}` | `ERR:BRACKET_MISS` | | 缓存溢出 | `ERR:BUFFER_OVERFLOW` | | 关键字段缺失 | `ERR:FIELD_MISSING` | ## 完整代码 ```lua --输入端口:inCmd 字符串 --输出端口:oDsID 字符串 --输出端口:oReqType 字符串 --输出端口:oCamID 整数 --输出端口:oErr 字符串 --[[************ 上方为输入输出口定义,请勿删除 ************************]] -- ========== 配置常量(按需修改) ========== local BUFFER_MAX_LEN = 512 -- 全局缓存最大长度,超过则自动清空并告警 local PKT_MIN_LEN = 10 -- 单条报文最小长度 local PKT_MAX_LEN = 256 -- 单条报文最大长度 -- 全局缓冲区buffer,不加local!! buffer = buffer or "" -- 去除字符串首尾空白字符(空格、Tab、换行、回车) local function trimSpace(str) if not str then return nil end return str:gsub("^%s+", ""):gsub("%s+$", "") end -- 报文长度范围校验 local function checkPacketLength(pktStr, minLen, maxLen) minLen = minLen or PKT_MIN_LEN maxLen = maxLen or PKT_MAX_LEN if not pktStr then return false end local l = #pktStr return l >= minLen and l <= maxLen end -- 校验报文首字符是 { 、尾字符是 } local function checkPacketHeadTail(pktStr) if not pktStr then return false end local first = string.sub(pktStr, 1, 1) local last = string.sub(pktStr, #pktStr, #pktStr) return first == "{" and last == "}" end -- 综合校验入口:去首尾空白 → 长度校验 → 首尾{}校验 local function checkPacketValid(rawStr) local pktStr = trimSpace(rawStr) if not pktStr then LuaLog:Debug("【校验失败】原始报文为空") return false, nil end if not checkPacketLength(pktStr) then LuaLog:Debug("【校验失败】长度超出范围, len=", #pktStr) return false, pktStr end if not checkPacketHeadTail(pktStr) then LuaLog:Debug("【校验失败】缺少起始{或结束}") return false, pktStr end return true, pktStr end -- 从合法报文中提取 dsID、reqType、camID 三个字段 local function parsePacket(rawStr) local dsID, reqType, camID = nil, nil, nil local valid, pktStr = checkPacketValid(rawStr) if not valid then return { dsID = nil, reqType = nil, camID = nil } end local s, e = string.find(pktStr, 'dsID":"') if s then local startPos = e + 1 local endPos = string.find(pktStr, '"', startPos) if endPos then dsID = string.sub(pktStr, startPos, endPos - 1) end end s, e = string.find(pktStr, 'reqType":"') if s then local startPos = e + 1 local endPos = string.find(pktStr, '"', startPos) if endPos then reqType = string.sub(pktStr, startPos, endPos - 1) end end s, e = string.find(pktStr, 'camID":') if s then local startPos = e + 1 local endPos = string.find(pktStr, '[%,%}]', startPos) if endPos then camID = tonumber(string.sub(pktStr, startPos, endPos - 1)) else camID = tonumber(string.sub(pktStr, startPos)) end end return { dsID = dsID, reqType = reqType, camID = camID } end -- 从缓冲区取出一条完整 {} 报文 local function popOnePacket(buf) local idxStart = string.find(buf, "{", 1, true) if not idxStart then return nil, "" end local idxEnd = string.find(buf, "}", idxStart, true) if not idxEnd then return nil, buf end local onePkt = string.sub(buf, idxStart, idxEnd) local remain = string.sub(buf, idxEnd + 1) return onePkt, remain end -- 统计缓冲区状态:总长度、完整包数、是否有半包 local function statBuffer(buf) if not buf or #buf == 0 then return 0, 0, false end local totalLen = #buf local openCount = 0 string.gsub(buf, "{", function() openCount = openCount + 1 return "" end) local closeCount = 0 string.gsub(buf, "}", function() closeCount = closeCount + 1 return "" end) local fullPktCount = math.min(openCount, closeCount) local hasHalfPkt = (openCount > closeCount) return totalLen, fullPktCount, hasHalfPkt end --==================== 主循环流程 ==================== -- 输出端口初始化 oHasPkt = false oIsValid = false oDsID = nil oReqType = nil oCamID = nil oBufferOverflow = false -- 获取本轮输入报文 local newData = inCmd or "" buffer = buffer .. newData -- 缓存状态统计 local bufLen, fullCnt, hasHalf = statBuffer(buffer) LuaLog:Debug("【缓存状态】缓存长度=", bufLen, " 完整包数=", fullCnt, " 是否有半包=", tostring(hasHalf)) -- 缓存上限保护:超过上限自动清空 if bufLen > BUFFER_MAX_LEN then oBufferOverflow = true LuaLog:Debug("【缓存溢出告警】缓存长度=", bufLen, " 超过上限=", BUFFER_MAX_LEN, ",自动清空buffer!") buffer = "" end -- 从缓冲区拆出一条完整报文 local onePacket onePacket, buffer = popOnePacket(buffer) if onePacket then oHasPkt = true LuaLog:Debug("【拆包成功】得到一条报文:", onePacket) local cmd = parsePacket(onePacket) oIsValid, _ = checkPacketValid(onePacket) oDsID = cmd.dsID oReqType = cmd.reqType oCamID = cmd.camID LuaLog:Debug("解析结果 dsID:", cmd.dsID) LuaLog:Debug("解析结果 reqType:", cmd.reqType) LuaLog:Debug("解析结果 camID:", cmd.camID) if cmd.dsID ~= nil and cmd.reqType ~= nil and cmd.camID ~= nil then if cmd.dsID == "www.hc-system.com.cam" and cmd.reqType == "photo" and cmd.camID == 1 then LuaLog:Debug("【业务匹配】识别到相机抓拍指令,触发N点标定流程") LuaSystem:TriggerLineByName(0,"N点标定") else LuaLog:Debug("【业务跳过】报文合法,但不是cam1抓拍指令") end else LuaLog:Debug("【业务跳过】报文框架合法,但关键字段提取不全") end else LuaLog:Debug("【拆包等待】无完整报文,半包保留在buffer,等待后续数据") end ```
李玉军
2026年10月21日 15:29
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
PDF文档(打印)
分享
链接
类型
密码
更新密码