主题
TCP 客户端例程 2:按换行分帧解析 JSON 指令(处理粘包 / 半包)
这一页只讲这一个例程。同组其余例程、以及这个库的函数说明都在 05-03-tcp-client。
适用:同上 | 依赖模块:tcp_client json plc | 难度:★★☆
场景:TCP 是字节流,一次回调收到的不一定是一整帧(可能粘在一起,也可能被拆成半条)。现场最实用的解法是双方约定「一行一条 JSON」,Lua 侧用一个缓冲区把收到的数据攒起来,按 \n 切出一整行再解析。这里处理服务端下发 {"cmd":"set","d":200,"v":1234} 与 {"cmd":"out","y":0,"v":1} 两种指令。
完整代码(可直接下载运行):
lua
-- 约定:一行一条 JSON;收不全就攒着,收到换行才解析
tcpc = tcp_client.creat("192.168.1.100", 8080)
local buf = ""
local function handle_line(line)
local ok, msg = pcall(json.decode, line)
if not (ok and type(msg) == "table") then
print("非法指令行,已忽略:", line)
return
end
if msg.cmd == "set" then -- 写 D 寄存器
local addr, v = tonumber(msg.d), tonumber(msg.v)
if addr and v then
plc.d(addr, v)
print("写 D" .. addr .. " =", v)
end
elseif msg.cmd == "out" then -- 写 Y 输出
local y, v = tonumber(msg.y), tonumber(msg.v)
if y and v then
plc.y(y, v)
print("写 Y" .. y .. " =", v)
end
end
end
tcpc:reg(tcp_client.RECVDATA, function(data)
buf = buf .. data
while true do -- 这里没有 sleep,只是把缓冲区切干净
local pos = buf:find("\n", 1, true)
if not pos then break end
local line = buf:sub(1, pos - 1)
buf = buf:sub(pos + 1)
if #line > 0 then handle_line(line) end
end
if #buf > 4096 then -- 迟迟不见换行,别让缓冲无限长
print("缓冲区超限已清空:", #buf)
buf = ""
end
end)
tcpc:start()要点:
- 必须自己分帧:TCP 没有"一条报文"的概念,
RECVDATA的data只是这一刻收到的字节。 - 缓冲区跨回调保存在
local buf,每次进回调先拼接再按行切(\n快速查找用find(s, 1, true))。 json.decode用pcall包住:半条或被截断的数据会解析失败,不能让回调抛错。- 解析出来的地址/值都要
tonumber校验,没有校验就把用户输入写进 PLC 寄存器是危险动作。 - 缓冲区要设上限,避免对端一直发不换行的数据把内存吃光。
验证方法:
- 服务端同时发两行:
{"cmd":"set","d":200,"v":1234}\n{"cmd":"out","y":0,"v":1}\n(一次 write 发完,制造粘包)。 - 设备日志应分别打印
写 D200 = 1234与写 Y0 = 1,D200 / Y0 都对得上 —— 两条都被完整处理。 - 再把一整条拆成两次发送(先发
{"cmd":"set","d":201,,隔一会再发"v":88}\n):日志只有一条,且 D201=88(证明半包被正确攒起来)。 - 发一行乱码
hello\n:日志打印「非法指令行」且程序继续运行。
注意事项:
- 不要把
data当作一条完整报文直接json.decode,这是这类程序最典型的崩溃来源。 - 回调里别做长耗时处理(所有回调串行执行),解出来的活最好转给定时器或梯形图。
buf:find("\n", 1, true)的第 3 个参数不能漏:不加会按正则找,\n之外的转义容易出错。