主题
HTTP 客户端例程 3:周期异步上报(推荐做法)+ 失败退避
这一页只讲这一个例程。同组其余例程、以及这个库的函数说明都在 05-01-http。
适用:同上 | 依赖模块:http json timer plc | 难度:★★☆
场景:每 30 秒把现场数据 POST 到第三方平台,不能卡住主循环(对照很多设备上习以为常的 while true do sleep(30000) end 写法 —— 那样会把定时器和所有回调一起冻死)。异步接口不阻塞;连续失败时按 2 倍退避,最长停 16 个周期再试,避免网络恢复前一直无效重发。
完整代码(可直接下载运行):
lua
-- 周期上报的标准写法:异步请求 + 在途保护 + 指数退避
local API = "http://192.168.1.10:8080/api/upload"
local INTERVAL = 30000 -- 上报周期,定时器最小 10ms,实际别低于 1000
httpc = http.creat(API)
httpc:add_head("Content-Type", "application/json")
httpc:add_head("X-Device-Id", plc.plc_name())
local busy = false -- 上一包还没回来,别再发
local fails = 0 -- 连续失败次数
local skip = 0 -- 退避:还要跳过几个周期
timer.start(INTERVAL, function()
if skip > 0 then skip = skip - 1 return end
if busy then print("上一包未回,跳过本轮") return end
busy = true
local body = json.encode({
dev = plc.plc_name(),
temp = plc.df(100), -- 浮点必须 df(),plc.DF100 读出来是 nil
y0 = plc.y(0),
ts = os.time()
})
httpc:post_asy(API, body, function(errcode, data, datalen)
busy = false
if errcode == 0 then
fails = 0
print("上报成功", datalen, "字节")
else
fails = fails + 1
skip = 2 ^ math.min(fails, 4) - 1 -- 退避 1/3/7/15 个周期,再往上封顶
print("上报失败 err=", errcode, "退避", skip + 1, "个周期")
end
end)
end)要点:
post_asy是唯一适合周期任务的写法:请求在后台跑,结果通过消息队列回到主循环执行回调,期间定时器和其它回调照常工作。busy是在途保护:异步请求不会因为慢就自动排队,一个周期发一包、对端卡住时会堆积。- 退避用「跳过 N 个周期」实现,不改定时器周期 —— 定时器周期由固件持有,改不了也没必要重启它。
plc.df(100)读浮点;plc.d(100)只能读 16 位,两者别混。- 失败计数只在成功时清零,失败次数累加到 4 就封顶,退避不会无限变长。
验证方法:
- 下载运行后,每 30 秒日志出现一次
上报成功 N 字节;服务端能看到带dev/temp/ts的 JSON。 - 拔网线后日志出现
上报失败 err=...并开始退避,间隔越来越稀疏(最长约 16×30s)。 - 插回网线,下一次成功后退避归零、恢复 30 秒节奏。
- 整个过程设备还活着:另起一个
timer.start(1000, function() print("alive") end)应一直打印 —— 这是「没有阻塞主循环」的直接证据。
注意事项:
- 别用
while true do ... system.sleep(30000) ... end做周期上报:sleep不泵消息队列,HTTP 回调、定时器、串口回调会全部永不触发。 errcode是 HTTP 层的错误码,服务端回了 500 也算失败,但data里可能有 body,需要时照样能读。- 客户端对象、定时器都不用刻意用全局持有(定时器由固件持有),但 HTTP client 建议长期持有:请求在途时被回收会丢响应。