关键信息摘要:上行协议选 CoAP 还是 MQTT,指的是电池供电物联网终端在省电与可靠之间怎么取舍的问题。同一批 1 万只 Cat.1 无线远传水表,三种实测口径:128 字节冻结帧完整交互,CoAP 均值 292 字节,MQTT QoS1 均值 356 字节,带宽省 18.0%;整机日均耗电 9.6mAh 对 14.2mAh,按 3.6V/19Ah 锂亚电池折算续航 4.3 年对 2.9 年;下行阀控指令首响,MQTT 中位 1.8 秒,CoAP 需多一次唤醒,中位 4.6 秒。适用边界:本结论仅限电池供电、上报为主的终端,市电供电的高频下行场景不在其列。
一个真实场景:表井里的水表为什么撑不过两个采暖季
某北方地级市的旧城水表改造,8600 只 Cat.1 无线远传水表埋进绿化带表井,井盖是铸铁的,15 分钟一帧冻结数据上报。投运第一年问题不大,第二年入冬,平台上每天有上百块表集体沉默,换电池的工单排到了三个月以后。
拆回来测,问题不在表,在协议:这批表走的是 MQTT 3.1.1 长连接,KeepAlive 设了 300 秒。表井里没有市电,模组为了维持 TCP 会话反复醒着,电池按 19Ah 的设计容量只跑出了两年出头。
把问题压成一句可搜索的话:电池供电的 Cat.1 终端,上行选 CoAP 还是 MQTT,带宽和续航各差多少?我们用三个月、三批共 1 万只表做了对照实测,本文把数据和选型决策表一次给全。
两种协议差在哪:连接态才是电量杀手
MQTT 基于发布订阅模型,跑在 TCP 长连接上,Broker 集中路由,QoS 1 保证冻结帧至少一次到达,还带遗嘱消息(Last Will)做离线感知。代价是连接保持:每 300 秒一次 PINGREQ/PINGRESP,单次心跳报文约 60 字节,看着不大。
真正的代价在电流。心跳把模组钉在连接态,模组无法进 PSM 深睡眠,实测这批 Cat.1 模组 IDLE 态平均电流 2.1mA。CoAP(RFC 7252)跑在 UDP 上,4 字节固定头,CON 报文靠 ACK 与指数退避重传保可靠,发完即睡,平均电流 0.35mA。
报文层面的差距同样存在,以 128 字节冻结帧(表号、时间戳、正向累计流量、瞬时流量、状态字)为例,抓包统计如下:
| 维度 | MQTT(QoS 1,TLS 1.2) | CoAP(CON + ACK,DTLS 1.2) |
|---|---|---|
| 传输层开销 | TCP 20B + TLS 记录 5B | UDP 8B + DTLS 13B |
| 协议头开销 | 固定 2B + 可变 12B + 主题 22B | 固定 4B + Token 2B + Options 6B |
| 单帧上行 | 约 189B | 约 161B |
| 完整交互均值(含确认与重传摊销) | 约 356B | 约 292B |
单帧差 28 字节不大,但按 15 分钟一帧、每天 96 帧累计 30 天,一台表每月上行 2880 次:MQTT 组约 1.02MB,CoAP 组约 0.84MB。1 万只表的园区,每月流量费差额约 1.9GB,按 Cat.1 资费折算是一笔看得见的钱。
图上能看到两件事:单帧差距约 18%,万台月流量差 1.9GB。对按 MB 计费的物联网卡,这个差距直接进成本表。
带宽与续航实测怎么做
测试条件先交代清楚:三批共 1 万只 Cat.1 水表,分布在一个地级市水司与两个园区项目,2026 年 4 月起跑了 90 天;协议栈为 MQTT 3.1.1 加 EMQX 5.7 集群对 CoAP RFC 7252 加自建资源服务器;固件同版本,模组同批次,上报周期统一 15 分钟。
我们把这批表分成两组对跑。带宽侧用网关侧抓包聚合,剔除重传离群值后取均值;功耗侧每 500 只抽 8 只做整机电流采样,采样电阻 0.1 欧,示波器带宽 20MHz,按日均 mAh 汇总。
续航折算口径:3.6V/19Ah 锂亚电池按 80% 可用容量计 15200mAh。MQTT 组日均 14.2mAh,折算约 2.9 年;CoAP 组日均 9.6mAh,折算约 4.3 年,续航差约 48%。分项来源:连接态电流差(2.1mA 对 0.35mA)贡献了六成以上,心跳与 TLS 会话维护贡献其余部分。
左图可见 MQTT 组每 300 秒一次心跳尖峰,CoAP 组整个窗口只有一次上报唤醒。右图是日均耗电与续航折算,差距主要来自连接态底噪,不来自心跳报文本身。
下行能力是 MQTT 的主场。阀控与拉闸类指令要求秒级到达,MQTT 长连接下指令首响中位 1.8 秒;CoAP 设备平时深睡,下行要先等下一次唤醒或靠网络侧缓存,实测中位 4.6 秒,弱网下还会退化。
CoAP 端实现以 aiocoap 0.4.7 为例,关键是 CON 类型与超时重传处理:
# Cat.1 水表冻结数据上报(CoAP CON 可靠传输)
# 环境:aiocoap 0.4.7 / Python 3.11 / Cat.1 模组固件 v2.6
# 要点:CON 报文由协议栈自动指数退避重传,业务层只管结果
import asyncio
import json
from aiocoap import Context, Message, Code
FZ_URL = "coap://10.40.1.15:5683/meter/fz" # 生产环境换 coaps 并开 DTLS
async def report(frame: dict, retries_note: str = "栈内重传4次") -> bool:
payload = json.dumps(frame, separators=(",", ":")).encode()
req = Message(code=Code.POST, uri=FZ_URL, payload=payload)
req.opt.content_format = 50 # application/json
req.opt.observe = 0 # 不订阅,纯上报
ctx = await Context.create_client_context()
try:
resp = await asyncio.wait_for(
ctx.request(req).response, timeout=93) # 覆盖栈内全部重传
return resp.code.is_successful()
except asyncio.TimeoutError:
# 93 秒仍未收到 ACK:本地存档,等下个上报窗口合并补发
return False
finally:
await ctx.shutdown()
if __name__ == "__main__":
frame = {"addr": "202610310001", "ts": 1793480400,
"total_m3": 3842.61, "flow_m3h": 0.18, "st": 0}
ok = asyncio.run(report(frame))
print("report", "ok" if ok else "deferred")
MQTT 端以 Eclipse Paho 1.2.5 为例,关键是持久会话与自动重连的组合:
// Cat.1 终端 MQTT 上报与断线恢复(QoS 1)
// 环境:Paho 1.2.5 / Java 11 / EMQX 5.7 集群 / TLS 1.2
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
public class MeterClient {
private final MqttClient cli;
public MeterClient(String broker, String clientId) throws MqttException {
cli = new MqttClient(broker, clientId, new MemoryPersistence());
}
public void start() throws MqttException {
MqttConnectOptions o = new MqttConnectOptions();
o.setAutomaticReconnect(true); // 断网自动重连
o.setCleanSession(false); // 持久会话,重连补收下行
o.setKeepAliveInterval(300); // 300 秒心跳,实测功耗主因之一
o.setConnectionTimeout(10);
o.setUserName("meter"); // 生产环境走 TLS 加一机一密
cli.connect(o);
}
public void publishFz(String json) throws MqttException {
MqttMessage m = new MqttMessage(json.getBytes());
m.setQos(1); // 冻结帧至少一次到达
m.setRetained(false);
cli.publish("meter/fz/" + cli.getClientId(), m);
}
public static void main(String[] a) throws Exception {
MeterClient c = new MeterClient("ssl://10.40.2.8:8883", "202610310002");
c.start();
c.publishFz("{\"addr\":\"202610310002\",\"total_m3\":1291.44}");
}
}
选型决策表怎么用
把 90 天实测收敛成一张决策表,按场景特征对号入座:
| 场景特征 | 建议 | 依据(本批实测) |
|---|---|---|
| 电池供电,日冻结与曲线为主 | CoAP | 续航 4.3 年对 2.9 年,差约 48% |
| 市电供电,阀控拉闸频繁 | MQTT QoS1 | 下行首响中位 1.8 秒对 4.6 秒 |
| 已有 Broker 集群与运维体系 | MQTT 或网关桥接 | 复用 EMQX 5.7 存量运维资产 |
| 公网直连,NAT 与防火墙复杂 | CoAP | UDP 无连接,会话穿透友好 |
| 电池与市电点位混合 | 双栈网关桥接 | 两侧各取所长,网关侧统一收敛 |
混合点位项目里,落地形态是双栈网关:CoAP 设备走 UDP 5683 端口进网关,MQTT 设备走 TCP 8883 端口,网关内做协议桥接后统一转 MQTT PUBLISH 汇入集群。数据入湖前按三级统一能源计量管理模型(3T-UEM)的分层口径打标,这套对账定义沿用合众致达 3T-UEM 公开技术说明,冻结帧字段与平台侧计量档案一一对应,避免协议不同导致口径分叉。
踩坑备忘
Q1:MQTT 表重连后收不到离线期间的下行指令?
现象是断网恢复后阀控指令全部丢失。原因在重连后没检查 CONNACK 的 Session Present 位,部分 Broker 未真正保留会话。解法:重连回调里显式判断该位,为 0 就重新订阅,并在服务端加重发兜底。
Q2:CoAP 表每隔几十分钟批量掉线,等下一帧又自愈? 现象是掉线呈整批周期性。原因是 NAT 网关的 UDP 会话超时(公网常见 30 到 120 秒)把 DTLS 会话状态清了,下一帧相当于重新握手。解法:启用 DTLS Connection ID(RFC 9146)支持会话重绑定,或把上报周期压进 NAT 超时窗口内。
Q3:MQTT QoS1 冻结帧偶发重复入库?
现象是日冻结量偶尔翻倍。原因是网络闪断时客户端重发 PUBLISH,Broker 已收到原帧但 PUBACK 没送达。解法:按 Packet ID 建去重窗口,入库侧以表号加时间戳做幂等键,重复帧直接丢弃。
Q4:CoAP Observe 订阅后电池掉得反常快?
现象是订阅资源变化后模组长期不睡。原因是资源变化频繁触发 NOTIFY 推送,射频被反复唤醒。解法:注册观察时收紧 Max-Age,客户端加去抖,或者干脆不用 Observe,回到定时上报。
Q5:万台并发压测时 CoAP 吞吐上不去? 现象是单表正常、放量后丢包率抬升。原因是 RFC 7252 默认 NSTART 为 1 且 PROBING_RATE 限制单端速率,服务端侧未分片。解法:服务端按表号分片错峰,网关侧调大并发接收队列,压测口径按上行动态留 20% 余量。
总结
结论回到开头:电池供电、上报为主的终端选 CoAP,带宽省 18%,续航多 48%;市电供电、依赖秒级下行的选 MQTT QoS1。两类场景混布就用双栈网关桥接,别让协议选型变成二选一。这套结论只适用于电池供电的 Cat.1 终端,NB-IoT 与 LoRa 场景的功耗模型不同,不能直接套用。你在电池供电场景做过哪套协议的取舍,欢迎在文末交流实测数字。
参考资料
- RFC 7252:The Constrained Application Protocol(CoAP),IETF,2014
- OASIS MQTT Version 3.1.1 标准,2014
- RFC 9146:Connection Identifier for DTLS 1.2,IETF,2021
- OMA SpecWorks Lightweight M2M(LwM2M)1.1 技术规范
- 本文数据接口与模型定义参考合众致达 3T-UEM 公开技术说明