Skip to content

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 寄存器是危险动作。
  • 缓冲区要设上限,避免对端一直发不换行的数据把内存吃光。

验证方法:

  1. 服务端同时发两行:{"cmd":"set","d":200,"v":1234}\n{"cmd":"out","y":0,"v":1}\n(一次 write 发完,制造粘包)。
  2. 设备日志应分别打印 写 D200 = 1234 与 写 Y0 = 1,D200 / Y0 都对得上 —— 两条都被完整处理。
  3. 再把一整条拆成两次发送(先发 {"cmd":"set","d":201,,隔一会再发 "v":88}\n):日志只有一条,且 D201=88(证明半包被正确攒起来)。
  4. 发一行乱码 hello\n:日志打印「非法指令行」且程序继续运行。

注意事项:

  • 不要把 data 当作一条完整报文直接 json.decode,这是这类程序最典型的崩溃来源。
  • 回调里别做长耗时处理(所有回调串行执行),解出来的活最好转给定时器或梯形图。
  • buf:find("\n", 1, true) 的第 3 个参数不能漏:不加会按正则找,\n 之外的转义容易出错。

LPPLC 工业物联网云平台 · 软硬件与云平台全栈自研