4. MQTT协议基础:发布/订阅模型、报文结构与QoS等级
大家好,我是老张。做充电桩接入这些年,MQTT是我打交道最多的协议之一。今天咱们聊聊它的核心基础——发布/订阅模型、报文结构,还有那个让很多人头疼的QoS等级。
说实话,刚开始接触MQTT时,我也觉得它不就是个轻量级消息协议嘛。但真正在充电桩项目里用起来,才发现里面的门道不少。尤其是QoS等级选不对,轻则丢数据,重则桩控系统出乱子。嗯,咱们一个一个来拆解。
4.1 发布/订阅模型:解耦的艺术
传统的客户端-服务器模型,说白了就是点对点通信。客户端直接找服务器要数据。但MQTT不一样,它用的是发布/订阅模型。
怎么理解呢?
想象一下充电桩集群的场景。你有一百个直流快充桩,每个桩都要上报实时功率、电压、电流。如果每个桩都直接连到你的云平台,那平台得维护一百个长连接,还要处理每个桩的请求。这架构,想想就头大。
MQTT的发布/订阅模型,引入了一个中间人——Broker(代理服务器)。充电桩作为发布者,把数据发到Broker上。云平台作为订阅者,从Broker那里拿数据。两者完全解耦。
核心要点:发布者和订阅者不需要知道对方的存在。充电桩只管发,云平台只管收。中间Broker负责转发。
我在项目中遇到过这样的情况:某天需要临时增加一个数据监控大屏。传统方式得改充电桩的代码,让它们多发一份数据到新地址。但用MQTT,我只需要让新的大屏订阅相同的主题(Topic)就行。充电桩那边完全不用动。这就是解耦的好处。
主题(Topic)是MQTT里的关键概念。它是个字符串,用斜杠分层。比如充电桩的主题可以设计成这样:
evcs/station_001/pile_01/status
evcs/station_001/pile_01/meter
evcs/station_001/pile_02/status
云平台可以订阅 evcs/station_001/# 来接收1号站所有桩的数据。也可以订阅 evcs/station_001/pile_01/+ 只收1号桩的所有数据。这个通配符设计,非常灵活。
我的习惯:主题命名一定要有层次感。我一般用「项目/站点/设备/数据类型」这种结构。别用平铺式的命名,比如 pile1status,后期维护起来你会想哭的。
4.2 MQTT报文结构:固定头、可变头、有效载荷
MQTT的报文结构很精简。一共就三部分:固定头、可变头、有效载荷。说白了,就是「身份证+附加信息+正文」。
4.2.1 固定头(Fixed Header)
固定头是每个MQTT报文都必须有的。它至少2个字节,结构如下:
| Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| Byte 1 | 消息类型 | DUP | QoS | RETAIN | ||||
| Byte 2... | 剩余长度 | |||||||
第一个字节的高4位表示消息类型。MQTT定义了14种报文类型,常用的有:
- CONNECT:客户端请求连接Broker
- CONNACK:Broker确认连接
- PUBLISH:发布消息
- SUBSCRIBE:订阅主题
- SUBACK:订阅确认
- PINGREQ/PINGRESP:心跳保活
- DISCONNECT:断开连接
DUP标志位表示是否为重发报文。QoS占2位,取值0、1、2。RETAIN标志位表示Broker是否需要保留这条消息。
剩余长度字段采用变长编码。1个字节能表示0-127字节。超过127字节时,会用多个字节表示。每个字节的最高位是延续位,1表示后面还有字节。这个设计很巧妙,能节省带宽。
注意:我曾经在调试一个充电桩时,发现桩上报的报文Broker总是解析失败。查了半天,原来是剩余长度字段算错了。桩上报了200字节的数据,但剩余长度只写了100。Broker读到100字节就停了,后面的数据全丢了。嗯,这个坑我踩过。
4.2.2 可变头(Variable Header)
可变头不是所有报文都有。它跟在固定头后面,内容取决于报文类型。
比如CONNECT报文的可变头包含:协议名("MQTT")、协议级别(4表示MQTT 3.1.1)、连接标志(Clean Session、Will Flag等)、Keep Alive(心跳间隔)。
PUBLISH报文的可变头则包含:主题名(Topic Name)、报文标识符(Packet Identifier,仅QoS 1和2需要)。
报文标识符这个东西,我建议你特别留意。QoS 0的PUBLISH不需要它。但QoS 1和2必须有。每个未完成的QoS 1或2消息,都需要一个唯一的标识符。Broker和客户端靠它来匹配请求和响应。
4.2.3 有效载荷(Payload)
有效载荷就是消息的正文。对于PUBLISH报文,这里放的就是你要传输的数据。充电桩上报的电压、电流、SOC,都放在这里。
MQTT对Payload的格式没有限制。你可以传纯文本、JSON、二进制、甚至加密数据。我个人强烈建议用JSON格式,可读性好,解析方便。
举个例子,充电桩上报实时数据:
{
"pile_id": "P001",
"timestamp": 1712563200,
"voltage": 380.5,
"current": 120.3,
"power": 45.7,
"soc": 68,
"status": "charging"
}
这个JSON就是PUBLISH报文的Payload。Broker收到后,转发给所有订阅了相关主题的客户端。
4.3 QoS等级详解:从0到2
QoS(Quality of Service)是MQTT最核心的特性之一。它定义了消息传递的可靠性等级。一共三级:0、1、2。
你想想看,充电桩上报的数据,有些丢了无所谓,有些丢了会出大事。比如心跳包丢了,大不了重连。但充电结束的结算数据丢了,那用户可能多扣钱或少扣钱。所以选对QoS等级很重要。
4.3.1 QoS 0:最多一次(At most once)
QoS 0是最低等级。消息发出去就不管了。Broker收到就收,收不到就丢了。没有确认机制,没有重传。
适用场景:实时性要求高,但允许丢数据的场景。比如充电桩的实时功率上报,每秒一次。丢一两个数据点,对整体监控影响不大。
报文流程:
- 发布者发送PUBLISH报文(QoS=0)
- Broker收到后直接转发给订阅者
- 没有任何确认报文
说白了,就是「发了就完了」。效率最高,但可靠性最低。
4.3.2 QoS 1:至少一次(At least once)
QoS 1保证消息至少被送达一次。但可能重复送达。
适用场景:需要确认消息到达,但能容忍重复的场景。比如充电桩的启动充电指令。发一次没收到确认,就重发。就算Broker收到了两次,但充电桩只会启动一次(需要做幂等处理)。
报文流程:
- 发布者发送PUBLISH报文(QoS=1),包含Packet Identifier
- Broker收到后,回复PUBACK报文(包含相同的Packet Identifier)
- 发布者收到PUBACK,确认消息送达
- 如果发布者超时没收到PUBACK,就重发PUBLISH
关键点:QoS 1的PUBLISH可能被重复发送。订阅者可能收到多条相同的消息。所以你的业务逻辑要做好去重处理。
4.3.3 QoS 2:恰好一次(Exactly once)
QoS 2是最高等级。保证消息恰好被送达一次,不多不少。
适用场景:绝对不能丢,也不能重复的场景。比如充电桩的结算数据、订单信息。多扣或少扣用户一分钱,都是大问题。
报文流程(四步握手):
- 发布者发送PUBLISH报文(QoS=2),包含Packet Identifier
- Broker收到后,回复PUBREC报文(发布收到)
- 发布者收到PUBREC后,回复PUBREL报文(发布释放)
- Broker收到PUBREL后,回复PUBCOMP报文(发布完成),然后转发消息给订阅者
这个流程,说白了就是「确认-确认-再确认」。可靠性最高,但开销也最大。一次QoS 2的消息传递,需要4个报文来回。
我的建议:在充电桩项目中,别滥用QoS 2。只有那些真正关键的、不可逆的数据才用QoS 2。比如订单结算、固件升级结果。其他的,像实时遥测数据,QoS 0就够了。状态变更类,QoS 1足矣。
4.3.4 三种QoS等级对比
| 特性 | QoS 0 | QoS 1 | QoS 2 |
|---|---|---|---|
| 可靠性 | 最低 | 中等 | 最高 |
| 消息重复 | 不会重复 | 可能重复 | 不会重复 |
| 消息丢失 | 可能丢失 | 不会丢失 | 不会丢失 |
| 网络开销 | 最小 | 中等 | 最大 |
| 适用场景 | 实时遥测 | 状态指令 | 结算数据 |
嗯,到这里MQTT的基础就讲得差不多了。发布/订阅模型让你解耦,报文结构让你理解数据怎么封装,QoS等级让你知道怎么选可靠性。下一章咱们聊聊MQTT的安全机制和充电桩的认证流程。到时候我会分享一些实际项目中踩过的坑,保证让你少走弯路。