资讯详情

资讯详情

MQTT物联网协议实战:从Broker搭建到客户端开发与水表采集

做物联网项目这些年我几乎每个项目都要跟MQTT打交道。如果你正被设备数据传不上来、协议五花八门、服务器不知道怎么搭这些问题卡住那这篇笔记应该能帮你从头梳理一遍。这里不会只讲概念而是会把MQTT从协议原理、服务器搭建到客户端发布订阅再到水表采集这种实际场景串起来适合刚入门物联网的新手也适合准备把MQTT落到生产环境的工程师参考。MQTT的全称是Message Queuing Telemetry Transport最早是IBM为卫星通信设计的轻量级协议。后来在物联网领域越用越广现在几乎成了智能硬件、工业采集、车联网、智慧水务等场景的标配。我最早接触MQTT的时候也很懵Broker、Topic、QoS这些概念绕来绕去后来在一套水表远程采集系统里从零开始落地了一遍才真正把整个链路吃透。这篇就把这些经验拆分出来重点放在“怎么快速跑起来”和“跑起来之后怎么避坑”上。1. 先搞明白MQTT为什么能在物联网里“一统天下”1.1 它解决的是设备端“弱、小、乱”的通信问题物联网设备跟手机、电脑不太一样很多是单片机或者低功耗嵌入式设备内存可能只有几百KBCPU主频低网络还是2G、NB-IoT、LoRa这种不稳定链路。如果硬套HTTP那一套每个请求都是请求-响应模型头部大、要频繁握手设备电池很快就耗光了。MQTT的设计目标就是在这种“弱、小、乱”的网络里把字节数压到最低同时还能保证一定程度的消息可靠性。它跑在TCP之上但应用层头部最小只有2个字节一条PUBLISH消息的固定头可能就几个字节。相比HTTP动辄几百字节的Header省出来的流量非常可观。再加上它的长连接特性不需要设备反复发起握手设备侧功耗、网络开销都小了很多。这一点在电池供电的水表、烟感、定位器等设备上尤其明显。MQTT的“发布-订阅”模型也解决了设备端网络不稳定的问题。设备不直接跟业务系统点对点通信而是先连上中间的服务器Broker再按主题发布消息。哪怕业务系统临时下线设备也能把数据先发给Broker等业务系统恢复后再订阅拿到。这种解耦能力是HTTP很难做到的。1.2 MQTT和HTTP、TCP长连接放在一起怎么选有朋友会问既然TCP长连接也能解决实时通信为什么还要MQTT这里要区分“能用”和“好用”。TCP长连接要自己维护心跳、断线重连、消息边界、重传机制这些逻辑每个项目都重新造一遍轮子工程量不小。HTTP轮询倒是简单但实时性差设备状态变化不能第一时间知道平台也扛不住大量设备高频轮询。我做过对比下面这个表格比较直观对比维度MQTTHTTP裸TCP长连接通信模型发布/订阅一对多请求/响应一对一需自己定义协议实时性高长连接推送一般需轮询高消息可靠性QoS 0/1/2可选依赖HTTP状态码需自己实现重传头部开销很小较大取决于协议设计断线重连客户端支持遗嘱、会话机制不涉及需自己实现服务端压力可承载海量连接高每个请求都要解析需处理连接管理实际项目里如果业务简单、设备量少HTTP也不是不能用。但设备量一上来需要做控制指令下发、状态实时同步MQTT的优势就很明显了。裸TCP长连接更适合对带宽和协议格式有特殊要求的场景比如一些私有协议采集但它没有统一规范后期维护成本高。1.3 核心概念先过一遍Broker、Topic、Client、QoSMQTT的核心角色只有三个Broker、Publisher、Subscriber。Publisher和Subscriber都是Client。Broker是消息中枢负责接收所有消息再根据Topic转发给对应订阅者。可以用小区物业来类比你是业主Client物业Broker负责登记你的门牌号Topic别人送快递消息来物业只投递到对应门牌。Topic是消息的“地址”用斜杠分级比如device/water_meter/001/data。它支持通配符匹配单层#匹配多层后面第三节会细讲。QoS是消息质量等级一共有三级。QoS 0是“不管送没送到”QoS 1是“至少送到一次”QoS 2是“只送到一次”。很多人一上来就把所有消息设成QoS 2觉得越可靠越好结果把小设备搞卡了后面第四部分我会展开讲怎么选。还有一个容易忽略的概念叫Clean Session。它决定了客户端断线后Broker是否保存连接状态和离线消息。设备端如果只做周期上报通常把Clean Session设成1省内存如果需要离线指令补发就要考虑设成0配合持久化机制。2. MQTT服务器的快速搭建与实际选型2.1 Windows上安装并启动MQTT服务mosquitto刚开始学习MQTT最省事的服务器就是mosquitto轻量、开源、跨平台。Windows下安装基本是下一步下一步装完可以在开始菜单里找到。这里有个坑是安装目录默认没有生成配置文件需要手动创建一个mosquitto.conf或者在安装目录的示例配置文件上改。我在Windows上常用的启动方式是在mosquitto.exe所在目录打开命令行然后执行mosquitto -v-v是打印详细日志启动后控制台会显示监听端口。默认监听1883端口如果本地防火墙开着记得放行TCP 1883不然局域网其他设备连不上。如果需要修改监听端口、允许匿名访问、开启持久化可以在mosquitto.conf里这样配置listener 1883 allow_anonymous true persistence true persistence_location /var/lib/mosquitto/改完配置后用这个命令指定配置文件启动mosquitto -c mosquitto.conf -vWindows上如果遇到“could not open config file”这种报错多半是路径没写对尽量用绝对路径。还有一次我遇到端口被占用执行netstat -ano | findstr 1883查了才发现是之前启动的实例没退干净。2.2 Linux上离线安装MQTT含麒麟V10 ARM离线安装笔记生产环境里一般用Linux服务器。有网的环境很简单Ubuntu/Debian执行apt install mosquittoCentOS/Rocky执行dnf install mosquitto装完直接就能用。但国内很多项目是内网环境尤其是国产化平台比如麒麟V10 ARM架构没法去公网拉包这时候就得走离线安装。我这里记录一下当时在麒麟V10 ARM上的操作思路。首先找一台同样架构的机器或者从软件源镜像下载对应的安装包。mosquitto在麒麟这种系统上一般能从EPEL源或欧拉源里找到需要准备mosquitto本体以及依赖库比如libmosquitto、libwebsockets、cjson、openssl相关包。拿到rpm包之后在目标机器上执行rpm -ivh mosquitto-*.rpm如果提示缺少依赖就用下面的命令检查ldd /usr/sbin/mosquittoldd会列出需要哪些动态库缺哪个就从有网的机器上对应拷过来。这个过程容易遇到glibc版本不匹配的问题所以最好是找同一大版本的系统包不要跨版本硬安装。装好之后mosquitto不会自动启动需要手动创建参数文件并启动mkdir -p /etc/mosquitto/conf.d mkdir -p /var/lib/mosquitto cp /etc/mosquitto/mosquitto.conf.example /etc/mosquitto/mosquitto.conf systemctl enable mosquitto systemctl start mosquitto如果系统里没有systemctl可能就是老式SysVinit或嵌入式环境那直接执行/usr/sbin/mosquitto -c /etc/mosquitto/mosquitto.conf 也能跑起来注意加-d参数可以后台运行。2.3 服务器启动后的验证和排错服务器搭完第一件事就是验证能不能正常收发消息。我习惯先在本机跑两个命令行客户端做端到端测试。一个终端订阅测试主题mosquitto_sub -h 127.0.0.1 -p 1883 -t test/hello -v另一个终端发布消息mosquitto_pub -h 127.0.0.1 -p 1883 -t test/hello -m Hello MQTT如果订阅端能打印出test/hello Hello MQTT说明服务端通信链路没有问题。这个操作虽然简单但能快速区分是服务端问题还是客户端问题。启动失败最常见的几类报错我整理一下现象可能原因排查方法Address already in use端口被占用ss -lntp查看占用进程杀掉后重启Error: Invalid user配置文件里删除了默认用户但没设置把user mosquitto注释掉或创建系统用户permission denied持久化目录没有写权限确认/var/lib/mosquitto属主或改用当前用户配置文件不生效conf.d目录里的配置冲突检查include_dir和主配置是否有重复项另外要注意mosquitto默认在新版本里可能不允许匿名访问客户端连接会报not authorised。如果是内网测试可以在配置里显式加上allow_anonymous true生产环境则一定要用账号密码或者TLS。2.4 MQTT Explorer调试MQTT必备的可视化工具命令行工具只能看到单个主题项目里主题一多想看整个主题树就费劲了。MQTT Explorer是很好的可视化调试工具直接去官网下载安装包Windows、Linux、macOS都有。打开MQTT Explorer后填上Broker地址和端口点连接就能看到左侧主题树。它能展示完整层级结构点任何一个节点能看到最近的消息内容、保留消息状态还可以直接向指定主题发布消息。调试设备上报逻辑时一边看设备日志一边盯着主题树数据变化一目了然。我经常用它的一个功能是监视主题在想要关注的父级主题上用“#”通配符订阅比如water_meter/#这样所有水表的数据都会实时刷出来。这个工具还可以保存多个连接配置切换测试环境和生成环境很方便。需要注意MQTT Explorer适合开发调试不建议长期挂在生产环境上因为订阅所有主题会产生额外流量而且它会记录敏感消息安全上不划算。3. MQTT客户端开发订阅/发布消息全流程3.1 准备Python客户端环境服务端搞定后就得写客户端了。Python生态里最常用的库是paho-mqtt全平台支持API稳定社区资料多。安装就一行pip install paho-mqtt如果你的执行环境没有外网可以在开发机上把paho-mqtt的whl文件拷到目标机器再用pip install paho-mqtt-*.whl安装。这个库既支持同步阻塞模式也支持多线程后台运行。下面我分别写一个发布端和一个订阅端。代码里连接信息统一放在常量里这样换成真实Broker地址时改一处就够了。3.2 发布端把传感器数据推上Broker发布端最常见的场景是设备采集到数据后定时上报。比如一个水表采集网关每30秒读取一次水表里的累计流量然后打包成JSON消息推到Broker。import paho.mqtt.client as mqtt import json import time BROKER_HOST 127.0.0.1 BROKER_PORT 1883 TOPIC water_meter/001/properties/post def build_report(total_flow): payload { device_id: wm_001, total_flow: total_flow, timestamp: int(time.time() * 1000) } return json.dumps(payload) client mqtt.Client(client_idwm_001_pub) client.connect(BROKER_HOST, BROKER_PORT, keepalive60) client.loop_start() total_flow 0 while True: total_flow 0.1 payload build_report(total_flow) result client.publish(TOPIC, payload, qos1) # 如果发布失败会自动重试这里只打印结果 if result.rc ! mqtt.MQTT_ERR_SUCCESS: print(fpublish failed: {result.rc}) time.sleep(30)这里我用了loop_start()它会起一个后台线程处理网络IO。如果只用publish但不开loop消息可能一直堆积在缓冲区里发不出去。这个操作非常常见不少新手都卡在“明明调用了publish服务端却收不到”。如果是纯同步脚本也可以先用publish再loop()等待发完但生产环境里loop_start更方便。keepalive60表示心跳包间隔是60秒。在弱网环境下这个值要调小一点比如30秒设备断线后Broker能更快感知但也不能太小否则会多耗流量。3.3 订阅端接收消息并处理业务逻辑订阅端一般跑在平台侧它订阅设备上报的Topic收到消息后落库、告警、展示。下面这段代码订阅了所有水表的消息import paho.mqtt.client as mqtt import json BROKER_HOST 127.0.0.1 BROKER_PORT 1883 TOPIC water_meter//properties/post def on_connect(client, userdata, flags, rc): if rc 0: print(connected to broker) client.subscribe(TOPIC, qos1) else: print(fconnect failed, rc{rc}) def on_message(client, userdata, msg): try: data json.loads(msg.payload.decode(utf-8)) # 业务逻辑入库或实时展示 device_id data.get(device_id) total_flow data.get(total_flow) print(f{device_id} - total_flow{total_flow}) except Exception as e: print(fhandle message error: {e}) client mqtt.Client(client_idplatform_01) client.on_connect on_connect client.on_message on_message client.connect(BROKER_HOST, BROKER_PORT, keepalive60) client.loop_forever()client.subscribe(TOPIC, qos1)里的主题用了通配符表示匹配任意设备ID。这样新增设备时平台不用改代码就能自动订阅到新设备的消息。on_message回调里尽量不要做耗时操作比如写数据库、调第三方接口否则会影响后续消息的处理速度。可以把消息扔进一个队列由独立工作线程去消费我这里为了演示直接打印了。3.4 主题命名与通配符设计别踩坑主题命名是MQTT项目里最容易被忽视的坑。我见过一个项目把主题设计成毫无分级的test/1、test/2设备一多立刻失控。合理的主题应该按“层级语义”来拆分第一层建议放业务域第二层放设备类型第三层放设备ID后面再放具体操作类型。比较通用的模板是{业务域}/{设备类型}/{设备ID}/{操作类型}比如water_meter/device/wm_001/properties/post # 设备上报属性 water_meter/device/wm_001/properties/set # 平台下发设置 water_meter/device/wm_001/service/invoke # 平台调用设备服务通配符只能匹配一层比如water_meter//wm_001/#能匹配water_meter/device/wm_001/...但不能跨层。#必须放在主题末尾而且它既能匹配同层也能匹配剩余所有层级。设计时还有几个原则主题不要以斜杠开头不然会出现空层级容易跟无空层级的订阅区分不开。层级不要过多四级左右就够层级越多服务端匹配开销越大。不要在主题里带敏感信息比如账号密码、用户身份证号因为很多Broker会保存主题记录。订阅端和发布端必须约定好同一套主题规范建议写在项目设计文档里不然接手的同事会崩溃。4. 进阶消息质量、遗嘱消息与保留消息4.1 QoS 0、1、2怎么选代价是什么QoS是MQTT里最容易引起误解的概念。它的等级不是越高越好每一级都有额外开销。QoS 0是“最多一次”消息发出去就不管了可能丢失但消耗最小。适合高频周期上报、丢失一两帧无所谓的场景比如温度、湿度、地理位置这种持续更新的数据。QoS 1是“至少一次”Broker收到后会返回PUBACK发送方没收到就重发。流量翻倍但消息可能重复消费端需要做幂等处理。适合指令下发这类重要消息偶尔重复能通过消息ID过滤。QoS 2是“恰好一次”它经过四次握手代价最高但不会丢失也不会重复。适合计费、交易等绝对不允许重复的消息。我在项目里用得很少因为绝大多数物联网数据业务上都能容忍重复没必要为此付出带宽代价。QoS语义重发机制典型场景0最多一次无高频传感数据1至少一次有可能重复设备报警、平台指令2恰好一次有保证不重复计费、精确交易还要注意QoS是对单条Topic和订阅关系生效的。发布端设置QoS 1订阅端也设置QoS 1实际服务质量就是1如果订阅端设置QoS 0那么即使发布端设成1实际也可能降级为0。所以两端都要按需求配置。4.2 遗嘱消息Last Will在设备掉线监测中的实际用法设备掉线监测是物联网里很常见的需求。如果设备拔电、断网平台怎么及时感知有心跳吗MQTT的心跳机制只能让Broker知道连接超时但不会主动通知其他订阅者。遗嘱消息就是干这个的。客户端连接Broker时可以携带遗嘱主题和遗嘱内容。如果客户端正常发送DISCONNECT包断开Broker会清除遗嘱如果连接异常断开比如断电、网络断开超过keepalive时间Broker就会向遗嘱主题发布遗嘱消息。在Python客户端里设置遗嘱很直接client.will_set(water_meter/device/wm_001/status, payloadoffline, qos1, retainTrue) client.connect(BROKER_HOST, BROKER_PORT, keepalive30)这段代码在连接前把遗嘱设置好。当设备异常掉线时平台订阅了water_meter//status主题就能收到offline消息从而在主界面把这个设备标记为离线。需要注意的一点是遗嘱只会在连接异常断开时触发。如果你在代码里主动调用了client.disconnect()Broker收到正常的DISCONNECT包后不会发遗嘱。所以监测设备掉线时要根据业务判断是“主动关机”还是“异常掉线”这两者的遗嘱行为是不一样的。4.3 保留消息与“设备状态”查询保留消息是MQTT的另一个实用功能。发布时设置retainTrueBroker保存这条消息新订阅者订阅该主题时会立即收到这条保留消息。这非常适合设备状态查询新接入的平台订阅端不需要等设备下一次上报就能立刻拿到设备当前状态。比如设备在线状态就是这么做的。设备上报在线消息时带着retainTrue因为遗嘱消息是离线状态也建议设置retainTrue。这样无论新老平台侧订阅端一订阅就能看到设备当前是online还是offline。client.publish(water_meter/device/wm_001/status, online, qos1, retainTrue)使用保留消息要小心一点它会一直存在Broker里直到同名主题又被发布一条新的保留消息或发布空内容清除。如果设备长期不上报Broker里那个状态还是旧的容易造成误判。所以平台侧最好定期清理或设置数据过期策略避免脏数据一直挂着。mosquitto里的持久化配置可以控制保留消息是否写入磁盘重启后还在这个要根据业务需求决定。5. 把MQTT落到真实项目水表采集场景与物模型5.1 一个真实的水表采集项目通信架构我参与过的水表采集项目是一个典型的“采集网关云平台”架构。现场有大量RS485水表采用Modbus或者DL/T645规约。这些水表不能直接上网需要一台采集网关把RS485信号接入到TCP/IP网络里。网关通过串口定时轮询水表寄存器拿到累计流量、瞬时流量、阀门状态等数据再由网关作为MQTT客户端把数据推送到云端Broker。整个链路的通信方向有三条水表到网关走Modbus或DL/T645串行总线属于设备层协议。网关到云平台走MQTT网关是发布者/订阅者Broker部署在云服务器上。平台到网关平台通过MQTT发布指令网关注销后通过串口下发给水表执行。我负责的是网关侧和平台侧的MQTT通信模块。网关选了支持MQTT的采集网关固件里自带MQTT客户端需要配置Broker地址、账号、主题前缀。平台侧用Python服务订阅这些主题这样网关上报的消息才能被实时入库。在实际部署时还有一个关键点同一个站点可能有几十台水表网关作为唯一出口每台水表的ID都需要体现在Topic或消息体里。否则平台分不清数据是哪块表来的。我们在Topic里用了wm_site01/device/{meter_id}/properties/post这样每个水表一个子主题平台用wm_site01/device//properties/post统一订阅。5.2 Modbus数据如何转成MQTT消息网关思路网关采集水表数据通常分两步先轮询再协议解析。轮询就是按照寄存器地址周期读取数据。以Modbus RTU为例网关读到水表返回的数据帧后会把累计流量、瞬时流量等字段解析出来。然后这些“半成品”数据要转成MQTT消息。常见的做法是网关内部维护一个统一数据模型拿到解析结果后填充到JSON结构里再发布到MQTT。我在项目里用的JSON模板长这样{ device_id: wm_site01_001, total_flow: 1234.56, instant_flow: 0.35, valve_status: open, signal_strength: 22, timestamp: 1711000000000 }网关把传感器数据转换成这样一条JSON然后发布到指定Topic。平台订阅到这个Topic后不需要关心底层是Modbus还是DL/T645只按JSON字段解释就行。这样就把设备协议差异性隔离在了网关层平台侧逻辑变得干净。需要留意的是水表数据里有整数也有浮点。比如瞬时流量可能是以0.001立方米为单位网关解析时一定要按规约里的单位说明换算好否则平台看到的数据差一千倍轻则显示异常重则影响计费。5.3 物模型驱动的Topic和数据模板设计做物联网平台时如果每类设备都单独写一套Topic规则和JSON格式后面维护会很痛苦。物模型Thing Model就是为这个而生的它把设备抽象成属性、事件、服务三部分然后用统一模板表达。属性是设备的状态数据比如水表的累计流量、阀门状态事件是设备主动上报的告警比如电池电量低、非法拆卸服务是平台下发指令让设备执行的动作比如远程关阀、读取当前读数。基于物模型Topic设计可以这样/sys/{productKey}/{deviceName}/thing/event/property/post /sys/{productKey}/{deviceName}/thing/service/property/set这是很多云平台采用的格式。productKey区分产品型号deviceName区分具体设备。我在自建项目里简化成了这种风格wm/site01/dev001/property/post wm/site01/dev001/event/post wm/site01/dev001/service/set数据模板也跟物模型对齐。属性上报用统一字段名事件和服务用独立的method字段区分{ method: thing.event.property.post, params: { total_flow: 2234.56, valve_status: open }, timestamp: 1711000000000 }这种方式的好处是网关侧逻辑可以标准化平台侧一条规则能解析所有同类设备。如果项目里设备类型很多强烈建议一开始就引入物模型哪怕现在只有水表以后接电表、气表、烟感时接入成本会低很多。5.4 项目上线后我遇到过的几个真实问题第一个问题是大量设备同时上线时Broker连接数暴涨mosquitto默认配置会拒绝新连接。当时报的错误是Maximum number of connections reached。后来在mosquitto.conf里调大了max_connections同时优化了设备上线时间分批接入才把问题压下去。对于大并发的场景单节点mosquitto的极限可能不够可以换EMQX这种支持集群的Broker。第二个问题是网络抖动导致消息重复。网关配置了QoS 1一抖就重发平台侧没做幂等库存里累积流量被重复累加报表数据就虚高了。后来在消费端加了消息ID去重同一设备同一消息ID只处理一次。这个坑提醒我QoS 1的“至少一次”语义背后就是重复业务侧必须有兜底。第三个问题是主题权限没做控制。默认mosquitto开启匿名访问后任何客户端都能订阅所有主题也能向任意主题发消息。这不只是隐私问题更危险的是有人可以伪造指令。后来我们启用了ACL只允许特定账号订阅指定主题前缀并对发布操作做了限制。这里用mosquitto的acl_file配置简单直接。第四个问题是设备OTA升级时网关短暂断网遗嘱消息立即触发平台把设备标记成离线但实际上网关下一秒又连回来了。后来我们在网关掉线监测里加了“软离线”机制只记录心跳超时后的真正离线而不是遗嘱消息一响就判定离线。遗嘱消息适合判断异常断开但平台侧还是要有自己的超时策略。6. 一些实在的建议踩过的坑都在这儿如果你也准备做MQTT项目最后我再分享几条在实际操作里摸索出来的经验。调试阶段一定要用好MQTT Explorer这种可视化工具不要只盯着代码和日志。通过主题树看消息流能快速定位是“没发出去”还是“订阅没匹配上”还是“消息内容格式错了”。客户端的Client ID必须唯一。如果有两个设备或两个进程用了同一个Client ID连接同一个Broker后连接的会把先连接的顶下线。这种问题很难排查我当时在网关侧和服务端调试时平台偶尔掉线查了一圈才发现是测试工具和正式服务用了同一个Client ID。消息内容尽量用JSON这种自带结构的文本格式不要用裸字符串比如“1,0,22,3”这种。裸串只适合协议非常固定的嵌入式内部通信放到MQTT里一旦字段顺序调整下游全得改代码。JSON至少有字段名即使加字段也不影响老消费端。设备端实现MQTT时如果条件允许优先选择已经适配过MQTT的通信模块或者成熟的嵌入式库不要自己从零写协议。MQTT的握手、心跳、重连细节很多自己实现很容易踩坑比如没有正确处理重连后的回包。最后是关于Broker选型。小项目、学习阶段用mosquitto完全够了配置简单占资源少。设备量上千、需要集群和高可用、需要有可视化控制台时再考虑EMQX或者云厂商托管的MQTT服务。Broker不是越复杂越好够用最重要。“物联网协议 MQTT 快速开发与实践”这个题目看起来很大但实际拆开也就是从协议理解到服务器搭建再到客户端收发消息和业务落地这几步。把这些环节都走通一遍后面再接触别的设备协议基本上就是替换数据解析层的事情通信骨架已经稳了。希望这份实践笔记能帮你少走点弯路快速把MQTT跑起来。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →