跳转至

LuatOS mqtt

作者:朱天华

Hello,大家好,我是朱天华。

欢迎大家来到合宙LuatOS直播课堂,一起学习LuatOS课程。

第一部分:LuatOS课程背景

因为今天是我们LuatOS系列课程的第三讲,所以在这里我就不重复讲解整个LuatOS课程的背景了;

如果您还不清楚LuatOS课程背景,可以访问:LuatOS课程背景 这个链接,进行了解;

第二部分:LuatOS MQTT课程讲哪些内容

今天的LuatOS MQTT课程主要从以下几方面进行讲述:

首先,复习以下三项内容:

  1. TCP/IP总体介绍;
  2. LuatOS上的 4G/WiFi/以太网 三种网络环境下的TCP/IP协议栈总体介绍;
  3. TCP/SSL/证书概念;

以上这三部分理论知识的内容和LuatOS课程的第002讲 socket重复,所以在本讲中仅仅对一些核心知识点进行简单的复习回顾,同时重点说明一下这些理论知识和MQTT的关系;

如果你还不清楚这些理论知识内容,可以访问:LuatOS socket(第三部分到第五部分) 这个链接,再学习一下;

其次,从以下几点,重点学习下MQTT的理论知识:

  1. MQTT协议和业务流程分析;
  2. 一个具体项目的MQTT应用协议报文和业务流程解读;

通过以上两点的学习,让大家对MQTT本身有一个详细的理解;

最后,基于LuatOS上的MQTT能力,从以下几点,重点学习下LuatOS MQTT的应用编程:

  1. LuatOS MQTT核心库功能介绍;
  2. LuatOS MQTT client应用开发框架;
  3. 如何分析LuatOS MQTT日志;

第三部分:基于TCP/IP初步认识MQTT

今天我们讲的LuatOS MQTT是LuatOS开发中最常用到的网络应用之一,用户使用LuatOS MQTT开发网络应用,会接触到TCP/SSL/认证等一些网络核心概念;

而说到网络应用,就绕不开TCP/IP;

所以在本章节中,我们以MQTT为切入点,先来简单的回顾复习一下TCP/IP协议的核心知识;

如果你想了解更详细的TCP/IP知识,可以访问:LuatOS socket(第三部分到第五部分) 这个链接,自行学习;

3.1 MQTT在TCP/IP协议中的位置

TCP/IP五层模型 协议实现
应用层 HTTP,MQTT,FTP,WebSocket,DNS,NTP,SMTP等TLS
传输层 TCP,UDP等
网络层 IP等
数据链路层 以太网,WiFi,4G 等网络各自的数据链路层协议以太网的IEEE 802.3系列WiFi网络的IEEE 802.11系列4G网络的PDCP、RLC、MAC等协议
物理层 光纤,双绞线,无线电波等传输介质

上面这张表格描述的是TCP/IP五层模型,在这里大家关注一下黄色背景的内容,和本讲课程我们重点要学习的LuatOS MQTT关系比较大;

MQTT是一个应用层的协议,不考虑TLS安全协议的情况下:

MQTT可以运行在TCP承载之上,也可以运行在UDP承载之上,也可以运行在WebSocket承载之上;

其中,运行在TCP承载之上的MQTT最为常见,LuatOS MQTT仅支持TCP承载;

所以我们在本讲课程中,仅仅介绍运行在TCP承载之上的MQTT;

在MQTT和TCP之间,根据是否支持TLS加密认证来划分,又可以分为非加密的MQTT和加密的MQTT两大类;

加密的MQTT根据支持的身份认证类型,又可以分为以下三类:

  1. 不支持身份认证的加密MQTT;
  2. 支持单向身份认证的加密MQTT,client验证server的身份;
  3. 支持双向身份认证的加密MQTT,client和server互相验证对方的身份;

再往下看数据链路层,支持以太网,WiFi,4G等常见的数据链路承载,对于MQTT应用来说,并不关心数据链路层的承载是什么类型,只关心有一个可用的数据链路网络就行;

LuatOS软件支持4G,以太网,WiFi三种数据链路承载网络,并且也支持三种网络之间按照配置的优先级无感自动切换。

3.2 MQTT应用数据在TCP/IP数据包中的位置

暂时无法在飞书文档外展示此内容

上面这张图演示的是,应用层以MQTT为例,TCP/IP数据 封装/解封装的过程,一个完整的TCP/IP数据包如下图所示:

暂时无法在飞书文档外展示此内容

  1. 在数据发送端的应用层,MQTT网络协议生成MQTT应用数据
  2. MQTT应用数据向下传,交给传输层,增加了传输层的头部信息,在头部信息中,有很多字段,其中有两个字段分别是源端口号目标端口号,这两个端口号的作用是:定义了发送端和接收端的应用程序,例如MQTT服务器,如果提供未加密的服务,端口号一般为1883;如果提供加密的服务,端口号一般为8883;通过端口号,可以提供两个主机之上端到端的通信;
  3. 传输层头部+MQTT应用数据向下传,交给网络层,增加了网络层的头部信息,在头部信息中,有很多字段,其中有两个字段分别是源IP地址目标IP地址,这两个IP地址的作用是:定义了发送端和接收端的网络设备地址;可以提供主机到主机的通信;
  4. 网络层头部+传输层头部+MQTT应用数据向下传,交给数据链路层,增加了数据链路层的头部信息;在这一层会根据不同的数据链路承载(例如4G网络,WiFi网络,以太网),增加不同的数据链路层头部;
  5. 最终,数据链路层头部+网络层头部+传输层头部+MQTT应用数据的数据包,通过物理层转换为0和1的电信号,然后通过物理传输介质发给了接收端;
  6. 接收端收到数据后,从下到上,依次解析并且剥离出数据链路层头部、网络层头部、传输层头部,最终将MQTT应用数据交给MQTT应用程序去处理;
  7. 接收端处理完应用数据后,如果需要返回应用数据给发送端;此时发送端和接收端的角色互换,再走一遍数据分层的封装和解封装过程;

接下来我在电脑上使用 MQTTX客户端工具(点击此处可以阅读使用说明)创建一个MQTT客户端,去连接地址为"lbsmqtt.airm2m.com",端口为1884的MQTT服务器(这是合宙提供的一个不能商用的测试服务器),抓取了完整的MQTT CONNECT和MQTT CONNACK报文,我们来实际分析一下这两种报文,加深一下对MQTT应用的完整TCP/IP数据包的理解;

接下来我们打开这两张图片看一下:

讲到这里,我们对MQTT在TCP/IP协议栈中的层次位置应该有了一个初步的认识;

接下来,我们重点理解下MQTT协议本身的一些知识点;

第四部分:详细理解MQTT

4.1 MQTT是什么

MQTT协议全称为Message Queuing Telemetry Transport,中文名称为消息队列遥测传输协议

MQTT是一种轻量级、简单高效的数据传输协议,特别适用于物联网(IoT)场景下的受限环境。

4.2 MQTT 核心架构:发布 - 订阅(Publish/Subscribe)模型

4.2.1 三个核心角色

MQTT 采用发布 / 订阅(Publish/Subscribe)架构,包含三个核心角色:

● 发布者(Publisher):发送消息的设备或应用

● 代理服务器(Broker):消息中枢,负责接收和分发消息

● 订阅者(Subscriber):接收特定主题消息的设备或应用

我们可以看到,消息将发布者、代理服务器、订阅者三个核心角色联系到了一起,消息主要由主题(Topic)和承载(Payload)构成;

承载比较好理解,就是消息中携带的具体的业务数据;

接下来我们重点理解一个概念:主题(Topic);

Topic 是 MQTT 中 “消息分类” 的核心,类似 “邮箱地址”,发布者向指定 Topic 发消息,订阅者通过订阅 主题 收消息;其设计有 3 个关键规则:

  1. 层级结构:用 “/” 分隔
  2. 主题 采用 “树形层级”,便于按场景分类,例如:

  3. sensor/room1/temp:房间 1 的温度传感器数据;

  4. sensor/room1/hum:房间 1 的湿度传感器数据;
  5. device/light/room2:房间 2 的灯光设备指令。
  6. 通配符:支持灵活订阅多个 主题
  7. 订阅者可使用 “通配符” 订阅一类 Topic,无需逐个订阅,支持两种通配符:

  8. 单级通配符(+):匹配 “一个层级” 的任意内容,需放在层级中间或末尾。示例:订阅 sensor/+/temp,可接收 sensor/room1/tempsensor/room2/temp,但无法接收 sensor/room1/humsensor/room1/temp/real

  9. 多级通配符(#):匹配 “当前层级及以下所有层级”,必须放在 Topic 末尾(否则无效)。示例:订阅 sensor/room1/#,可接收 sensor/room1/tempsensor/room1/humsensor/room1/temp/real 等所有以 sensor/room1/ 开头的 Topic。
  10. 注意事项
  11. Topic 区分大小写(如 Sensor/Room1sensor/room1 是两个不同 Topic);
  12. Topic 不能以 “/” 开头(如 /sensor/room1 不推荐,可能导致层级混乱);
  13. 不支持空格和特殊字符(建议使用字母、数字、/+#_)。

4.2.2 发布/订阅架构的核心流程

理解了消息,主题(Topic),发布者(Publisher),代理服务器(Broker),订阅者(Subscriber)这几个概念之后,我们再结合下面这张表格和图,理解一下 发布/订阅 架构:

角色 核心职责 典型示例
发布者(Publisher) 消息的 “生产者”,仅负责向 Broker 发送消息,无需知道谁会接收。关键动作:指定消息的 “主题(Topic)” 和内容,发送给 Broker。 温湿度传感器(定时向 sensor/room1/temp 发送 “26℃”)、智能门锁(开门时向 device/lock1/status 发送 “已开启”)。
订阅者(Subscriber) 消息的 “消费者”,仅负责向 Broker 订阅感兴趣的 “主题”,等待接收消息,无需知道谁在发布。关键动作:向 Broker 提交订阅请求(含目标 Topic),接收 Broker 转发的匹配消息。 手机智能家居 APP(订阅 sensor/room1/temp 实时看温度)、物业监控系统(订阅 device/lock1/status 监控门锁状态)。
Broker(服务器) 架构的 “核心中间人”,是连接发布者和订阅者的唯一桥梁。关键动作:1. 接收发布者的消息;2. 匹配订阅列表(找到订阅该 Topic 的所有订阅者);3. 将消息转发给匹配的订阅者。 EMQX、Mosquitto 等 MQTT 服务器,负责处理所有设备的连接和消息路由。

我们以“温湿度传感器设备→手机端APP” 为例,再理解一下发布/订阅架构的核心流程;

整个通信过程可拆解为 4 个步骤,完全围绕 Broker 展开,发布者和订阅者全程 “无感知”:

  1. 订阅者发起订阅:手机 APP(订阅者)与 Broker 建立 MQTT 连接,发送 “订阅请求”,明确要订阅的主题(如 sensor/room1/temp)。Broker 收到后,在本地 “订阅列表” 中记录:sensor/room1/temp 主题对应该 手机APP。
  2. 发布者发布消息:温湿度传感器设备(发布者)与 Broker 建立MQTT 连接,发送 “发布请求”,包含主题(sensor/room1/temp)和消息内容(“26℃”)。发布者发送后无需等待反馈,可继续执行其他任务。
  3. Broker 匹配并转发:Broker 接收消息后,立即查询 “订阅列表”,发现手机 APP 订阅了 sensor/room1/temp 主题。随后,Broker 将 “26℃” 消息封装成 “转发报文”,发送给该手机 APP。
  4. 订阅者接收消息:手机 APP 收到 Broker 转发的消息,解析后在手机APP界面上显示 “房间 1 温度:26℃”,完成一次通信。

4.2.3 发布/订阅架构的核心优势(为什么物联网首选)

相比传统 “点对点(P2P)” 通信(如设备直接互相连接),发布/订阅 架构的优势完全贴合物联网场景需求:

  1. 彻底解耦设备关系:发布者和订阅者无需知道对方的 IP、端口等信息,甚至无需同时在线(如发布者发送消息时订阅者离线,Broker 可保存消息,待订阅者重连后转发)。例如,一个传感器的数据可同时发给手机 APP、云端数据库、本地显示屏,无需传感器单独与三者建立连接。
  2. 灵活的消息筛选(基于 Topic):通过 “主题(Topic)” 实现消息分类,订阅者可按需订阅特定 Topic,只接收关心的消息。例如,订阅 sensor/+/temp 可接收所有房间的温度数据,订阅 sensor/room1/# 可接收房间 1 的温度、湿度等所有数据,无需处理无关信息。
  3. 支持大规模扩展:所有设备只需连接 Broker,无需两两互联,极大降低了网络复杂度。即使设备数量从几十台增加到几十万台,只需扩容 Broker(如搭建集群),无需修改设备的通信逻辑,轻松支撑物联网 “海量设备” 场景。

总的来说,MQTT 的发布/订阅架构,本质是 “通过 Broker 做中间枢纽”,用 “主题” 做消息筛选,让发布者和订阅者彻底解耦。

这种设计既降低了设备通信的复杂度,又能轻松应对物联网 “多设备、多场景、大规模” 的核心需求,因此成为物联网消息通信的主流架构。

4.3 MQTT如何实现可靠通信

MQTT 实现可靠通信的核心在于其设计了多层次的保障机制,通过协议层面的规范确保消息在不可靠网络环境下(如物联网常见的低带宽、高延迟、频繁断连场景)能够高效、准确地传输。

这些机制主要包括消息质量等级(QoS)、会话保持、遗嘱消息、心跳检测、保留消息等,共同构成了 MQTT 可靠通信的完整体系。

4.3.1 消息质量等级(QoS):按需选择可靠性

MQTT 定义了 3 级消息质量等级(Quality of Service),发布者可根据消息的重要性选择合适的等级,在 “可靠性” 和 “传输效率” 之间找到平衡。这是 MQTT 实现可靠通信的最核心机制。

1、 QoS 0:最多一次(At Most Once)

  1. 核心逻辑:“发完即忘”,发布者发送消息后不等待确认,Broker 和订阅者接收后也不返回确认。消息可能丢失,但不会重复。
  2. 实现流程:
  3. 发布者向 Broker 发送 PUBLISH 报文(QoS 0 标识);
  4. Broker 收到后直接转发给订阅者(同样用 QoS 0);
  5. 全程无任何确认报文,传输效率最高,但可靠性最低。
  6. 适用场景:非关键数据,如实时温湿度、设备心跳包(丢失一条不影响整体统计)。

2、QoS 1:至少一次(At Least Once)

  1. 核心逻辑:通过 “一次确认” 确保消息不丢失,但可能因重传导致重复。发布者需等待 Broker 的确认,Broker 也需等待订阅者的确认。
  2. 实现流程(发布者→Broker):
  3. 发布者向 Broker 发送 PUBLISH 报文(QoS 1 + 唯一消息 ID);
  4. Broker 接收后,立即向发布者返回 PUBACK 报文(携带相同消息 ID),表示 “已收到消息”;
  5. 若发布者超时未收到 PUBACK,则重发 PUBLISH 报文(消息 ID 不变),直到收到确认。
  6. 实现流程(Broker→订阅者):
  7. Broker 向订阅者发送 PUBLISH 报文(QoS 1 + 唯一消息 ID);
  8. 订阅者接收后返回 PUBACK 报文(携带相同消息 ID),表示 “已收到消息”;
  9. Broker 超时未收到PUBACK,则重发 PUBLISH 报文(消息 ID 不变),直到收到确认。
  10. 适用场景:关键数据但可容忍重复(如设备告警,重复可通过消息 ID 去重)。

3、QoS 2:恰好一次(Exactly Once)

  1. 核心逻辑:通过 “四步握手” 机制确保消息仅被接收一次,无丢失、无重复,是可靠性最高的等级,但传输延迟和资源消耗最大。
  2. 实现流程(发布者→Broker):
  3. 发布者发送 PUBLISH 报文(QoS 2 + 消息 ID);
  4. Broker 接收后,返回 PUBREC 报文(表示 “已接收,准备处理”);
  5. 发布者收到 PUBREC 后,发送 PUBREL 报文(表示 “允许 Broker 处理并转发”);
  6. Broker 收到 PUBREL 后,完成消息处理,返回 PUBCOMP 报文(表示 “处理完成”),发布者收到后结束流程。
  7. 实现流程(Broker→订阅者):同上,通过四步握手确保订阅者仅接收一次。
  8. 适用场景:绝对不能重复 / 丢失的数据(如金融交易指令)。

4.3.2 会话保持(Session Persistence):断连后消息不丢失

MQTT 通过 “会话保持” 机制解决客户端异常断连后的消息续传问题,确保重连后通信状态可恢复。核心是 Broker 对客户端会话状态的存储策略,由客户端连接时的 Clean Session 参数控制。

1、持久会话(Clean Session = 0)

  1. 核心逻辑:Broker 保存客户端的会话状态,包括:
  2. 客户端的订阅列表(无需重连后重新订阅);
  3. 未完成的 QoS 1/2 消息(如未收到 PUBACK 的消息);
  4. 已发送但未被确认的消息(如 Broker 发给客户端但未收到确认的消息)。
  5. 流程:
  6. 客户端连接时指定 Clean Session = 0,Broker 为其创建持久会话;
  7. 客户端断连后,Broker 保留会话状态;
  8. 客户端重连(使用相同 Client ID)后,Broker 恢复订阅关系,并补发未完成的消息。
  9. 适用场景:长期在线设备(如工业控制器),需确保重连后不丢失消息和订阅。

2、临时会话(Clean Session = 1)

  1. 核心逻辑:Broker 不保存任何会话状态,客户端断连后,所有订阅和未完成消息全部清除。
  2. 流程:
  3. 客户端连接时指定 Clean Session = 1
  4. 断连后,Broker 立即删除该客户端的所有信息;
  5. 重连后,客户端需重新发送订阅请求才能接收消息。
  6. 适用场景:临时连接设备(如手机 APP 偶尔查看数据),减少 Broker 存储压力。

在嵌入式端,为了保证业务逻辑的简单,一般来说,不开启会话保持,大家对这个功能了解一下即可。

4.3.3 遗嘱消息(Last Will and Testament, LWT):设备离线状态可感知

当客户端异常离线(如断电、网络中断)时,Broker 会自动向指定主题发送 “遗嘱消息”,通知其他订阅者该设备的离线状态,确保系统对设备状态的感知可靠性。

1、配置流程

  1. 客户端1在连接阶段(CONNECT 报文)向 Broker 声明遗嘱参数:

  2. Will Topic:遗嘱消息的目标主题(如 device/room1/status);

  3. Will Message:遗嘱消息内容(如 offline);
  4. Will QoS:遗嘱消息的 QoS 等级(确保遗嘱本身可靠送达);
  5. Will Retain:是否保留遗嘱消息(让新订阅者也能收到)。

2、触发机制

  1. 正常断开:客户端1发送 DISCONNECT 报文后断开连接,Broker 不发送遗嘱消息;
  2. 异常断开:Broker 检测到 TCP 连接异常断开(如心跳超时、连接被强制关闭),且未收到 DISCONNECT 报文,则立即向 Will Topic 发送 Will Message。所有订阅了这个Will Topic的其他客户端,都会收到这条遗嘱消息;

3、适用场景

  1. 设备在线状态监控(如智能家居中 “空调离线” 时,APP 接收遗嘱消息并提示用户);
  2. 故障自动告警(如工业传感器离线,监控系统接收遗嘱消息后触发检修流程)。

4.3.4 心跳检测(Keepalive):连接状态实时监控

MQTT 通过 “心跳机制” 确保 Broker 和客户端能及时检测到连接异常,避免因网络中断导致的 “假在线” 状态,为会话恢复和遗嘱消息触发提供依据。

1、工作原理

  1. 客户端连接时指定 Keepalive 时间(如 60 秒),表示 “客户端需在每个 Keepalive 间隔内至少发送一次报文(任何类型,如 PUBLISHPINGREQ)”;
  2. 若客户端长时间未发送报文,Broker 会主动发送 PINGREQ 报文探测客户端是否在线;
  3. 客户端收到 PINGREQ 后需回复 PINGRESP 报文;
  4. 若 Broker 超时未收到 PINGRESP(通常为 1.5 倍 Keepalive 时间),则判定客户端离线,触发遗嘱消息(若配置)并关闭连接。

2、作用

  1. 实时检测连接状态,避免无效连接占用资源;
  2. 为遗嘱消息提供准确的触发条件(确保仅在真正离线时发送)。

因为没有合适的环境模拟客户端不发送心跳的场景,所以下面这张图使用LuatOS软件抓取了一份正常发送心跳的网络包,帮助理解一下心跳机制

4.3.5 保留消息(Retained Message):新订阅者可获取历史数据

当发布者发送消息时,可设置 Retain 标志为 1,Broker 会保存该主题的 “最新保留消息”。当新订阅者订阅该主题时,Broker 会立即将这条保留消息推送给它,无需等待发布者再次发送,确保新订阅者能获取历史数据。

1、特点

  • Broker 仅保存每个主题的最新一条保留消息(新保留消息会覆盖旧消息);

  • 若发布者发送一条空内容的保留消息,Broker 会删除该主题的保留消息;

  • 保留消息与会话无关,即使发布者离线,新订阅者仍能收到。

2、适用场景

  • 设备状态同步(如智能灯的 “开关状态”,新订阅者上线后立即知道当前状态);

  • 历史数据快照(如传感器的最新读数,新监控设备接入时无需等待实时数据)。

4.3.6 总结:MQTT 可靠通信的协同机制

MQTT 的可靠通信不是单一机制的作用,而是多个机制的协同:

  1. QoS 等级:从协议层面确保消息 “不丢失”“不重复”;
  2. 会话保持:解决断连后消息续传和状态恢复问题;
  3. 遗嘱消息:让系统感知设备异常离线;
  4. 心跳检测:实时监控连接状态,为可靠性机制提供判断依据;
  5. 保留消息:确保新订阅者能获取历史数据。

这些机制共同构成了 MQTT 灵活、可控的可靠性体系,使其既能满足物联网 “低带宽、受限设备” 的轻量需求,又能支撑可靠性要求极高的场景。

4.4 MQTT的主要业务流程

MQTT 的业务流程围绕 “客户端与 Broker 之间的连接建立、消息交互、连接断开” 三大阶段展开,每个阶段包含多个标准化的交互步骤,确保通信的可靠性和规范性。以下是核心业务流程的详细拆解:

4.4.1 连接建立阶段:客户端与 Broker 建立通信链路

这是所有 MQTT 通信的前提,客户端(发布者 / 订阅者)需先与 Broker 建立 TCP 连接,核心步骤包括:

1、MQTT如果在传输层使用的是TCP,则首先MQTT客户端会和MQTT Broker服务器建立TCP连接。

2、客户端发起连接请求(CONNECT 报文)客户端向 Broker 发送 CONNECT 报文,包含关键信息:

  1. 客户端 ID(Client ID):唯一标识客户端(如 “sensor_room1_001”),Broker 据此区分不同设备;
  2. 协议版本:如 MQTT 5.0、3.1.1;
  3. 连接参数:是否保持会话(Clean Session)、心跳间隔(Keepalive,如 60 秒,客户端需在此间隔内发送报文证明在线);
  4. 认证信息:可选的用户名 / 密码、TLS 证书(用于身份验证);
  5. 遗嘱消息参数:若客户端异常离线,Broker 需发送的遗嘱主题、内容、QoS 等级。

3、Broker 确认连接(CONNACK 报文)Broker 验证 CONNECT 报文后,返回 CONNACK 报文:

  1. 若验证通过(Client ID 合法、认证成功等),返回 “连接成功” 响应,包含 “会话状态”(如是否恢复之前的持久会话);
  2. 若验证失败(如 Client ID 重复、认证失败),返回错误码(如 “拒绝连接”),并关闭 TCP 连接。

最终结果:连接建立成功后,客户端与 Broker 保持 长连接,进入 “消息交互阶段”。

4.4.2 消息交互阶段:订阅、取消订阅、发布、接收的核心流程

连接建立后,客户端可执行 “订阅主题”“发布接收消息”“取消订阅” 等核心操作,所有交互均通过标准化报文完成:

1、订阅主题(客户端 → Broker)

  1. 订阅者需先订阅感兴趣的主题,才能接收对应消息:

    • 客户端发送 SUBSCRIBE 报文:包含需订阅的主题列表(如 sensor/room1/temp)及每个主题的 QoS 等级(如 QoS 1);
  2. Broker 回复 SUBACK 报文:确认订阅结果,返回每个主题的 “实际 QoS 等级”(可能低于客户端请求,由 Broker 根据自己的服务能力和安全策略决定)。

  3. 示例:手机 APP 发送 SUBSCRIBE 订阅 sensor/room1/temp(QoS 1),Broker 返回 SUBACK 确认 “订阅成功,QoS 1”,此后该 APP 将收到该主题的所有消息。

2、取消订阅主题(客户端 → Broker)

订阅者可随时取消对某个主题的订阅:

  • 客户端发送 UNSUBSCRIBE 报文:包含需取消的主题列表(如 sensor/room1/temp);

  • Broker 回复 UNSUBACK 报文:确认取消成功,此后不再向该客户端转发对应主题的消息。

3、发布/接收消息(客户端 → Broker → 订阅者)

  1. 步骤 2:Broker 接收后,向发布者回复 PUBACK 报文(确认已收到消息);
  2. 步骤 3:Broker 查询订阅列表,向所有订阅该主题的订阅者发送 PUBLISH 报文(内容相同,QoS 等级与订阅时一致);
  3. 步骤 4:每个订阅者接收后,向 Broker 回复 PUBACK 报文(确认已收到)。
  4. 其他:QoS 0 无确认机制(“发完即忘”),QoS 2 需 “四步握手”(PUBLISH→PUBREC→PUBREL→PUBCOMP),确保消息仅被接收一次。

4、心跳保活(客户端 → Broker)

  • 客户端发送 PINGREQ 报文;

  • Broker 回复 PINGRESP 报文。

4.4.3 连接断开阶段:正常 / 异常断开的处理

通信结束后,客户端或 Broker 会主动断开连接,或因异常被动断开,核心流程包括:

1、正常断开客户端发送 DISCONNECT 报文给 Broker,明确表示 “主动断开连接”,Broker 收到后:

  • 关闭 TCP 连接;

  • 若为持久会话(Clean Session=0),保留客户端的订阅列表和未完成消息;

  • 不触发遗嘱消息(因为是 “正常断开”)。

2、异常断开若客户端因断电、网络中断等原因异常离线(Broker 未收到 DISCONNECT 报文,但 TCP 连接断开或心跳超时),Broker 会:

  • 触发 “遗嘱消息”:按客户端连接时声明的参数,向遗嘱主题发送遗嘱消息(如 device/offline);

  • 清理资源:若为临时会话(Clean Session=1),删除该客户端的所有订阅和消息;若为持久会话,保留信息直至客户端重连。

4.4.3 一个实际的MQTT项目业务示例

在理解了MQTT连接/断开连接、订阅/取消订阅、发布/接收的业务流程后,我们结合一个实际的MQTT项目业务,再来总体理解一下这些核心业务

4.5 MQTT协议报文

MQTT 协议的所有通信都通过标准化的报文(Packet) 完成,每种报文有明确的格式和用途。MQTT 报文结构统一分为三部分:固定头(Fixed Header)、可变头(Variable Header) 和负载(Payload),其中后两部分是否存在取决于报文类型。

MQTT报文类型共有14种:

报文类型值 报文名称 作用说明
1 CONNECT 客户端向 Broker 发起连接请求
2 CONNACK Broker 确认连接请求
3 PUBLISH 发布消息
4 PUBACK 确认收到 QoS 1 消息
5 PUBREC QoS 2 第一步确认(已接收)
6 PUBREL QoS 2 第二步(允许处理)
7 PUBCOMP QoS 2 第四步确认(处理完成)
8 SUBSCRIBE 客户端订阅主题
9 SUBACK Broker 确认订阅
10 UNSUBSCRIBE 客户端取消订阅
11 UNSUBACK Broker 确认取消订阅
12 PINGREQ 客户端发送心跳请求
13 PINGRESP Broker 回复心跳响应
14 DISCONNECT 客户端主动断开连接

所有 MQTT 报文都遵循以下基本结构(从前往后依次排列):

部分 存在性 作用
固定头 所有报文必含 标识报文类型、控制标志、剩余长度
可变头 部分报文含(如 PUBLISH、CONNECT) 补充特定信息(如主题、协议版本、消息 ID)
负载 部分报文含(如 PUBLISH、SUBSCRIBE) 实际数据(如消息内容、订阅列表)

每种报文的具体格式,在这里就不详细描述了,网上的资料很多,如果有兴趣,大家可以参考以下几种在线资源自行学习:

第五部分:基于MQTT理解LuatOS上的TCP/IP协议栈

在总体了解了MQTT的核心理论知识之后,接下来我们一起看下LuatOS上对TCP/IP协议栈的支持情况,以及提供了哪些编程接口给LuatOS项目应用脚本来使用,参考下表:

TCP/IP五层模型 LuatOS支持的TCP/IP协议 LuatOS上的编程接口 备注
应用层 HTTP,MQTT,FTP,WebSocket,DNS,NTP,DHCP
SSL/TLS:从OSI七层模型来看,和表示层最接近,所以此处把SSL/TLS放到TCP/IP模型中应用层
socket核心库/libnet扩展库
http核心库/httpplus扩展库/httpsrv核心库
mqtt核心库
ftp核心库
websocket核心库
httpdns扩展库
dhcpsrv扩展库
udpsrv扩展库
应用层提供的这些编程接口和LuatOS项目应用开发关系最为密切;
这些核心库和扩展库的API文档参考:
LuatOS API手册
在后续直播课程中,我们会逐一进行讲解
传输层 TCP,UDP socket核心库/libnet扩展库
网络层 IP,ICMP socket核心库
exnetif扩展库
"IP_READY"、"IP_LOSE"
在这里重点说一下exnetif扩展库,exnetif扩展库有两项核心功能:
1、LuatOS产品做为设备模式来使用,可以配置使用4G网卡,以太网网卡,WiFi网卡中的一种或者多种来上网,使用多种网卡上网时,可以配置使用的多种网卡优先级;
2、LuatOS产品做为路由器模式来使用,支持以下三大类网络路由场景:
(1) 4G网卡连接外网,WiFi设备通过WiFi AP热点接入上网,以太网设备通过LAN口接入上网;
(2) WiFi网卡连接外网,WiFi设备通过WiFi AP热点接入上网,以太网设备通过LAN口接入上网;
(3) 以太网WAN口连接外网,WiFi设备通过WiFi AP热点接入上网,以太网设备通过LAN口接入上网;
数据链路层 以太网,WiFi,4G 等网络各自的数据链路层协议
以太网的IEEE 802.3系列
WiFi网络的IEEE 802.11系列
4G网络的PDCP、RLC、MAC等协议
物理层 双绞线,无线电波等传输介质

具体到LuatOS MQTT应用编程,我们仅需要关注上表中黄色背景的几部分:

  1. mqtt核心库:这个库是LuatOS MQTT应用编程的核心,在下一章节我们会重点讲解;
  2. socket核心库、"IP_READY"和"IP_LOSE"消息:在LuatOS MQTT应用开发中,会主动查询网卡的状态,或者被动等待网卡状态的变化通知,这部分功能就会用到socket核心库、"IP_READY"和"IP_LOSE"消息;在本讲最后我们学习LuatOS MQTT client应用开发框架时,再结合具体的代码演示如何使用这部分功能;
  3. exnetif扩展库:在LuatOS MQTT应用开发中,会使用到单网卡(4G 、WiFi、以太网网卡中的一种)或者多网卡(4G 、WiFi、以太网网卡中的多种),exnetif扩展库可以既简捷又灵活的配置各种网卡,让MQTT应用编程和网卡管理完全解耦;在本讲最后我们学习LuatOS MQTT client应用开发框架时,我们结合具体的代码来分析exnetif扩展库如何使用;

第六部分:LuatOS上的MQTT核心库

LuatOS 提供 mqtt 核心库,可以实现 mqtt 客户端,不支持mqtt server,有以下几点注意事项

1、只支持 mqtt 3.1.1,其他版本例如 3.1 或者 5.0 均不支持!!!

2、只支持mqtt/mqtts over tcp,不支持 mqtt over websocket,不支持mqtt over udp;

3、几个大前提:

  1. 本库基于 TCP 连接,支持加密 TCP 和非加密 TCP;
  2. 任何通信失败都将断开连接,如果开启了自动重连,那么间隔 N 秒后会开始自动重连;
  3. 上行数据均为一次性的,没有缓存机制,更没有上行的重试/重发机制;

4、重要的事情说 3 次:没有重发机制,没有重发机制,没有重发机制

  1. MQTT 协议中规定了重发机制,但那是云端/服务器端才会实现的机制,模块端是没有的;
  2. 上行失败,唯一的可能性就是 TCP 链接出问题了,而 TCP 链接出问题的解决办法就是重连;
  3. 模块端不会保存任何上行数据,重连后也无法实现重发;

5、QoS仅支持0和1,不支持2;

6.1 常量详解

核心库常量,顾名思义是由合宙 LuatOS 内核固件中定义的、不可重新赋值或修改的固定值,在脚本代码中不需要声明,可直接调用;

6.1.1 mqtt.STATE_DISCONNECT

常量含义:mqtt断开
         表示mqtt客户端当前处于断开连接的状态,没有与mqtt服务器建立任何连接
数据类型:number
示例代码:-- 如下方所示,为mqtt.STATE_DISCONNECT常量的基本用法;
         if mqtt.STATE_DISCONNECT == mqttc:state() then
             -- 状态:MQTT断开连接
             log.info("MQTT状态", "当前状态:断开连接")
         end

6.1.2 mqtt.STATE_SCONNECT

常量含义:mqtt socket连接中
         表示mqtt客户端正在尝试建立底层socket连接,但尚未完成连接过程;
数据类型:number
示例代码:-- 如下方所示,为mqtt.STATE_SCONNECT常量的基本用法;
         if mqtt.STATE_SCONNECT == mqttc:state() then
             -- 状态:MQTT Socket连接中
             log.info("MQTT状态", "当前状态:Socket连接中")
         end

6.1.3 mqtt.STATE_MQTT

常量含义:mqtt socket已连接mqtt连接中
         表示底层socket连接已成功建立,但mqtt协议层面的连接(如发送CONNECT消息、等待CONNACK响应)正在进行中;
数据类型:number
示例代码:-- 如下方所示,为mqtt.STATE_MQTT常量的基本用法;
         if mqtt.STATE_MQTT == mqttc:state() then
             -- 状态:Socket已连接,MQTT连接中
             log.info("MQTT状态", "当前状态:MQTT协议连接中")
         end

6.1.4 mqtt.STATE_READY

常量含义:mqtt mqtt已连接
         表示mqtt客户端已成功完成所有连接步骤,处于完全就绪状态,可以进行发布消息、订阅主题等操作;
常量取值:number
示例代码:-- 如下方所示,为mqtt.STATE_READY常量的基本用法;
         if mqtt.STATE_READY == mqttc:state() then
             -- 状态:MQTT已完全连接就绪
             log.info("MQTT状态", "当前状态:已连接就绪")
         end

6.2 函数详解

6.2.1 mqtt.create(adapter, host, port, ssl, isipv6)

功能

mqtt 客户端创建;

注意事项

1. 返回值有返回 nil 的情况,在调用后必须进行检查返回值是否为 nil,且要做逻辑处理;

参数

adapter

参数含义:适配器序号,如果不填,会选择平台自带的方式,然后是最后一个注册的适配器,可选值请查阅socket库的常量表
数据类型:number/nil
取值范围:number类型时,取值范围参考socket api中的常量详解
是否必选:可选传入此参数;
注意事项:如果没有传入此参数,内核固件会自动选择当前时间点其他功能模块设置的默认网卡;
                 除非你mqtt请求时,一定要使用某一种网卡,才设置此参数;
                 如果没什么特别要求,不要设置此参数,使用系统中设置的默认网卡即可;
                 一般来说,LuatOS的网络应用demo中都会有netdrv_device功能模块设置默认网卡
                 所以建议使用mqtt.create接口时,不要设置此参数,直接使用netdrv_device设置的默认网卡就行
参数示例:-- 如下方所示,这是普通TCP连接的写法,第一个参数不填时默认为平台自带方式或最后注册的适配器;
         mqttc = mqtt.create(nil, "120.55.137.106", 1884)
         -- 如下方所示,这是普通TCP连接的写法,查阅socket库的常量表,将需要的常量填到第一个参数的位置;
         mqttc = mqtt.create(socket.LWIP_GP, "120.55.137.106", 1884)

host

参数含义:服务器地址,可以是域名,也可以是ip地址
数据类型:string
取值范围:不可超过191字符;
是否必选:必须传入此参数;
注意事项:支持字符串形式的域名或IP地址
         作为必需参数,若不提供会导致错误;
参数示例:-- 如下方所示,这是普通TCP连接的写法,第二个参数支持域名或者ip地址;
         mqttc = mqtt.create(nil, "120.55.137.106", 1884)
         -- 如下方所示,这是普通TCP连接的写法,第二个参数支持域名或者ip地址;
         mqttc = mqtt.create(nil, "lbsmqtt.airm2m.com", 1884)

port

参数含义:端口号;
数据类型:number
取值范围:无特别限制;
是否必选:必须传入此参数;
注意事项:作为必需参数,若不提供会导致错误;
参数示例:-- 如下方所示,这是普通TCP连接的写法,第三个参数必须填有效的整数值;
         mqttc = mqtt.create(nil, "120.55.137.106", 1884)
         -- 如下方所示,这是普通TCP连接的写法,第三个参数必须填有效的整数值;
         mqttc = mqtt.create(nil, "lbsmqtt.airm2m.com", 1884)

ssl

参数含义:ssl加密配置;默认为不加密;
         true时为无证书最简单的加密
         table时为有证书的加密table内容格式说明如下
                 {
                     -- 参数含义:服务器ca证书数据;
                     -- 数据类型:string/nil;
                     -- 取值范围:无特别限制;
                     -- 是否必选:可选传入此参数;
                     -- 注意事项:当客户端需要验证服务器证书时,需要此参数;
                    --                         如果证书数据在一个文件中,要把文件内容读出来,赋值给server_cert;
                     -- 参数示例:例如通过Luatools烧录了server_ca.crt文件;
                    --                        就可以通过io.readFile("/luadb/server_ca.crt")读出文件内容赋值给赋值给server_cert;
                     server_cert

                    -- 参数含义:客户端证书数据;
                     -- 数据类型:string/nil;
                     -- 取值范围:无特别限制;
                     -- 是否必选:可选传入此参数;
                     -- 注意事项:当服务器需要验证客户端证书时,需要此参数;
                    --                         如果证书数据在一个文件中,要把文件内容读出来,赋值给client_cert;
                     -- 参数示例:例如通过Luatools烧录了client.crt文件;
                    --                         就可以通过io.readFile("/luadb/client.crt")读出文件内容赋值给赋值给client_cert;
                     client_cert

                    -- 参数含义:加密后的客户端私钥数据;
                     -- 数据类型:string/nil;
                     -- 取值范围:无特别限制;
                     -- 是否必选:可选传入此参数;
                     -- 注意事项:当服务器需要验证客户端证书时,需要此参数;
                    --                         如果加密后的私钥数据在一个文件中,要把文件内容读出来,赋值给client_key;
                     -- 参数示例:例如通过Luatools烧录了client.key文件;
                        --                         就可以通过io.readFile("/luadb/client.key")读出文件内容赋值给client.key;
                     client_key

                    -- 参数含义:客户端私钥口令数据;
                     -- 数据类型:string/nil;
                     -- 取值范围:无特别限制;
                     -- 是否必选:可选传入此参数;
                     -- 注意事项:当服务器需要验证客户端证书时,需要此参数;
                    --                         如果加密后的私钥数据在一个文件中,要把文件内容读出来,赋值给client_password;
                     -- 参数示例:例如通过Luatools烧录了clinet.password文件;
                        --                         就可以通过io.readFile("/luadb/clinet.password")读出文件内容赋值给client_password;
                     client_password

                    -- 参数含义:控制是否验证服务器证书的有效性;
                     -- 数据类型:number;
                     -- 取值范围:0 不校验服务器证书;
                    --                        1 可选校验;
                    --                        2 强制校验;
                     -- 是否必选:可选传入此参数,不填时,默认为2;
                     -- 注意事项:测试环境或者自签名证书场景下可根据需要降低校验级别;
                        --                        正式环境建议使用强制校验以提高安全性;
                     -- 参数示例:verify = 1;
                     verify
                 }
数据类型:boolean/table
取值范围:无特别限制;
是否必选:可选传入此参数,不填时默认为不使用加密连接;
注意事项:非必选参数,不填时默认为不使用加密连接;
         证书数据需为字符串格式,通常通过io.readFile接口从文件读取
         当使用表类型参数时,即使只设置了一个证书相关字段,也会启用完整的TLS连接模式
         verify参数会影响连接安全性,正式环境建议使用默认值2(强制校验);
参数示例:-- 如下方所示,这是普通TCP连接的写法,第四个参数不填时默认为不使用加密连接;
         mqttc = mqtt.create(nil, "120.55.137.106", 1884)
         -- 如下方所示,这是加密TCP连接的写法,第四个参数填true为无证书最简单的加密;
                 -- 不验证服务器证书,即我们常说的无证书校验;
         mqttc = mqtt.create(nil, "120.55.137.106", 8883, true)
         -- 如下方所示,这是加密TCP连接的写法,第四个参数现在是表类型;
                 -- 并且只填了一个server_cert参数,为单服务器证书验证,即我们常说的单向认证;
         mqttc = mqtt.create(nil, "120.55.137.106", 8883, {
                                                 server_cert = io.readFile("/luadb/server_ca.crt")
                                                     })
         -- 如下方所示,这是加密TCP连接的写法,第四个参数现在是表类型;
                 -- 并且填了server_cert参数和verify参数,同时verify参数的值为1,为单服务器证书验证+可选认证;
         mqttc = mqtt.create(nil, "120.55.137.106", 8883, {
                                                 server_cert = io.readFile("/luadb/server_ca.crt"), 
                                                 verify=1
                                                     })
         -- 如下方所示,这是加密TCP连接的写法,第四个参数现在是表类型;
                 -- 并且填了server_cert、client_cert、client_key、client_password参数,为双向证书验证,即双向认证;
         mqttc = mqtt.create(nil, "120.55.137.106", 8883, {
                             server_cert = io.readFile("/luadb/server_ca.crt"), 
                             client_cert = io.readFile("/luadb/client.crt"), 
                             client_key = io.readFile("/luadb/client.key"), 
                             client_password = "123456"
                             })

isipv6

参数含义:是否使用ipv6协议以及设置接收缓冲区大小
         默认为ipv4协议
         布尔类型时,true表示使用ipv6协议false表示使用ipv4协议
         表类型时,可同时配置多个扩展参数,表内容规范如下:
                 {
                    -- 参数含义:设置是否使用ipv6协议;
                     -- 数据类型:boolean;
                     -- 取值范围:true/false;
                     -- 是否必选:可选传入此参数;
                     -- 注意事项:ipv6功能需要设备和网络环境支持,否则即使设置为true也无法连接;
                     -- 参数示例:ipv6 = true;
                     ipv6

                    -- 参数含义:设置mqtt接收缓冲区的大小(单位:字节);
                     -- 数据类型:number;
                     -- 取值范围:大于0的整数;
                     -- 是否必选:可选传入此参数,如不填,系统会根据编译时的模组型号选择对应的默认值;
                    --                         带psram的模组默认值为64K,不带psram的模组默认值为4K;
                    --                         模组是否内置psram可通过模组规格书/硬件手册自行查阅);
                     -- 注意事项:增大缓冲区可以提高接收大数据包的能力,但会占用更多内存;
                    --                        减小缓冲区可以节省内存,但可能导致大数据包接收不完整;
                    --                         应根据实际应用场景和设备内存资源合理设置;
                     -- 参数示例:rxSize = 4096;
                     rxSize
                 }
数据类型:boolean/table
取值范围:无特别限制;
是否必选:可选传入此参数,不填时默认为ipv4协议
注意事项:不填或为nil时,默认使用IPv4协议
         不通过rxSize参数自定义接收缓冲区大小时,系统会根据编译时的模组型号选择对应的默认值;
                 psram的模组默认值为64K,不带psram的模组默认值为4K
                 模组是否内置psram可通过模组规格书/硬件手册自行查阅;
         ipv6功能需要设备和网络环境支持,否则即使设置为true也无法连接
         在网络不稳定环境下,适当增大rxSize可以提高消息接收的稳定性
参数示例:-- 如下方所示,这是普通TCP连接的写法,第五个参数不填时为使用ipv4协议;
         mqttc = mqtt.create(nil, "120.55.137.106", 8883, nil)
         -- 如下方所示,这是普通TCP连接的写法,第五个参数填true时为使用ipv6协议;
         mqttc = mqtt.create(nil, "120.55.137.106", 8883, nil, true)
         -- 如下方所示,这是普通TCP连接的写法,第五个参数现在是表类型;
                 -- 并且填了rxSize参数,为自定义设置mqtt接收缓冲区大小为4096字节,不过还是使用ipv4协议;
         mqttc = mqtt.create(nil, "120.55.137.106", 1884, nil, {rxSize = 4096})
         -- 如下方所示,这是普通TCP连接的写法,第五个参数现在是表类型;
                 -- 并且填了ipv6参数和rxSize参数,为自定义设置mqtt接收缓冲区大小为4096字节,且使用ipv6协议;
         mqttc = mqtt.create(nil, "120.55.137.106", 1884, nil, {ipv6 = true, rxSize = 4096})

返回值

local mqttc = mqtt.create(adapter, host, port, ssl, isipv6)

有一个返回值 mqttc

mqttc

含义说明:判断mqtt客户端创建是否成功
                 成功时返回一个userdata类型的mqtt客户端实例对象,该实例对象可用于后续的mqtt操作(如连接、订阅、发布等);
                 失败时返回nil
                 实例对象名称并不是固定的mqttc,可自定义;
数值类型:userdata/nil
取值范围:无特别限制;
注意事项:有返回nil的情况,需做好对应逻辑处理;

示例

-- 如下方所示,这是普通TCP连接的写法;
mqttc = mqtt.create(nil, "120.55.137.106", 1884)
-- 如下方所示,这是普通TCP连接、使用ipv6协议的写法;
mqttc = mqtt.create(nil, "120.55.137.106", 8883, nil, true)
-- 如下方所示,这是普通TCP连接、mqtt接收缓冲区自定义4096字节的写法;
mqttc = mqtt.create(nil, "120.55.137.106", 1884, nil, {rxSize = 4096})
-- 如下方所示,这是加密TCP连接、不验证服务器证书的写法;
mqttc = mqtt.create(nil, "120.55.137.106", 8883, true)
-- 如下方所示,这是加密TCP连接、单服务器证书验证的写法;
mqttc = mqtt.create(nil, "120.55.137.106", 8883, {
                                server_cert=io.readFile("/luadb/server_ca.crt")
                                    })
-- 如下方所示,这是加密TCP连接、单服务器证书验证、可选认证的写法;
mqttc = mqtt.create(nil, "120.55.137.106", 8883, {
                                server_cert=io.readFile("/luadb/server_ca.crt"), 
                                verify=1
                                    })
-- 如下方所示,这是加密TCP连接、双向证书验证的写法;
mqttc = mqtt.create(nil, "120.55.137.106", 8883, {
                    server_cert=io.readFile("server_ca.crt"), 
                    client_cert=io.readFile("/luadb/client.crt"), 
                    client_key=io.readFile("/luadb/client.key"), 
                    client_password="123456"
                    })

6.2.2 mqttc:debug(onoff)

功能

配置是否打开 debug 信息,即控制 mqtt 客户端是否输出调试信息;

注意事项

1. 必须传入布尔类型参数 onoff(true 表示打开调试,false 表示关闭调试);

2. 建议开发调试阶段可打开,方便查看 mqtt 通信的详细过程。调试结束后应关闭,以减少不必要的日志输出;

参数

onoff

参数含义:是否打开debug信息true表示打开调试false表示关闭调试
数据类型:boolean
取值范围:true/false
是否必选:必须传入此参数;
注意事项:必须是布尔类型(true/false);
参数示例:-- 如下方所示,传入值为true,表示打开调试;
         mqttc:debug(true)
         -- 如下方所示,传入值为false,表示关闭调试;
         mqttc:debug(false)

返回值

该接口无返回值,只需调用该接口执行相关操作,无需处理返回结果;

如果一定要把接口调用的结果赋值给一个变量,则这个变量就是一个nil值;

示例

-- 如下方所示,传入值为true,表示打开调试;
mqttc:debug(true)
-- 如下方所示,传入值为true,表示关闭调试;
mqttc:debug(false)

6.2.3 mqttc:auth(client_id, username, password, cleanSession)

功能

配置 mqtt 三元组信息及会话清除策略(cleanSession);

注意事项

1. client_id、username、password 有长度限制,其中 client_id 和 username 限制不超过 192 字符,password 限制不超过 512 字符,超过限制会导致配置失败;

2. 相同的 client_id 在同一服务器上会导致设备互相踢下线,需要确保 client_id 的唯一性;

参数

client_id

参数含义:设备识别id,对于同一个mqtt服务器来说,通常要求唯一,相同client_id会互相踢下线
数据类型:string
取值范围:最大长度为192个字符;
是否必选:可选传入此参数,如不填,服务器会随机生成client_id
注意事项:对于同一个mqtt服务器client_id通常要求是唯一的
                 如果多个客户端使用相同的client_id连接同一服务器,会导致设备互相踢下线;
         client_id最大长度为192字符,超过此长度会导致认证失败,接口返回nil并输出错误日志
参数示例:-- 如下方所示,这是无用户名密码登录的写法,第一个参数的“123456789”为建立mqtt客户端时的client_id;
         mqttc:auth("123456789")
         -- 如下方所示,这是不填client_id时的写法,此时会进入无clientID模式,服务器随机生成client_id;
         mqttc:auth()

username

参数含义:连接mqtt服务器时使用的用户名信息
数据类型:string
取值范围:0-192字符;
是否必选:可选传入此参数,如不填,默认为空字符串;
注意事项:若提供的用户名长度超过限制,会导致接口调用失败(返回nil);
                 并在日志中打印错误信息"mqtt username 太长或者无效!!!!"
         该参数仅用于mqtt服务器的身份验证,具体验证逻辑由服务器决定;
         即使不提供用户名密码(使用默认空字符串),也可以调用该接口进行身份配置;
参数示例:-- 如下方所示,这是不填用户名的写法,不填默认为空字符串;
         mqttc:auth("123456789")
         -- 如下方所示,这是填写用户名的写法,“username”为用户名,“password”为密码;
         mqttc:auth("123456789", "username", "password")

password

参数含义:连接mqtt服务器时使用的密码信息,用于身份验证;
数据类型:string
取值范围:0-192字符;
是否必选:可选传入此参数,如不填,默认为空字符串;
注意事项:若提供的密码长度超过限制,会导致接口调用失败(返回nil);
                 并在日志中打印错误信息"mqtt password 太长或者无效!!!!"
         该参数与username配合使用,用于mqtt服务器的身份验证,具体验证逻辑由服务器决定;
         即使不提供密码(使用默认空字符串),也可以调用该接口进行身份配置;
参数示例:-- 如下方所示,这是不填用户名密码的写法,不填默认为空字符串;
         mqttc:auth("123456789")
         -- 如下方所示,这是填写用户名的写法,“username”为用户名,“password”为密码;
         mqttc:auth("123456789", "username", "password")

cleanSession

参数含义:控制是否清除mqtt会话状态
         当设置为true时,客户端连接服务器时,服务器会清除之前的会话状态(包括未完成的订阅、消息队列等);
         当设置为false时,服务器会保留客户端的会话状态,以便客户端重连后恢复之前的订阅和未接收的消息;
数据类型:boolean
取值范围:true/false
是否必选:可选传入此参数,如不填,默认为true
注意事项:仅当提供了client_id参数时cleanSession参数的配置才有效
         在无clientId模式下,服务器会强制生成一个随机的cliet_id来标识该客户端
                 同时mqtt协议要求cleanSession参数必须为true
                 因此此时如果再手动配置cleanSession参数,服务器端会直接忽略;
                 并强制使用cleanSession参数为true的模式
         该参数的设置会直接影响客户端重连后的行为:
                         设置为false时,重连后服务器会推送客户端离线期间未接收的QoS 1QoS 2消息;
                         设置为true时,重连后不会恢复之前的会话状态;
         若服务器不支持会话保留功能,即使设置为true,也可能无法保留会话状态;
参数示例:-- 如下方所示,不手动设置cleanSession参数时默认为true;
         mqttc:auth("123456789")
         -- 如下方所示,手动设置cleanSession参数为false,不清除mqtt会话状态;
         mqttc:auth("123456789", "username", "password", false)
         -- 如下方所示,无clientId模式,服务器随机生成client_id,cleanSession不可配置;
         mqttc:auth()

返回值

local mqtt_auth_result = mqttc:auth(client_id, username, password, cleanSession)

有一个返回值 mqtt_auth_result

mqtt_auth_result

含义说明:连接mqtt服务器时身份验证配置是否成功
数值类型:boolean
取值范围:成功时返回true,失败时返回nil
注意事项:返回值是2025319日新增的特性;
         实际使用中,应检查返回值以确认身份验证配置是否成功;

示例

-- 如下方所示,这是无clientId模式的写法,此时服务器会强制生成一个随机的cliet_id来标识该客户端;
-- 同时mqtt协议要求cleanSession参数必须为true,因此此时如果再手动配置cleanSession参数,服务器端会直接忽略;
-- 并强制使用cleanSession参数为true的模式;
mqttc:auth()
-- 如下方所示,这是仅clientID的写法,无用户名密码登录;
mqttc:auth("123456789")
-- 如下方所示,这是带用户名密码登录;
mqttc:auth("123456789", "username", "password")
-- 如下方所示,这是手动配置cleanSession参数为false的写法,不清除mqtt会话状态;
mqttc:auth("123456789", "username", "password", false)

6.2.4 mqttc:keepalive(time)

功能

设置 mqtt 客户端的心跳时间间隔,单位为秒(s);

注意事项

1. 参数 time 为可选值,默认值为 240 秒,最小值限制为 15 秒(若传入小于 15 的值,会被强制设为 15 秒),最大值限制为 600 秒(若传入大于 600 的值,会被强制设为 600 秒);

2. 实际发送心跳时间间隔 = 设置的心跳时间间隔 × 0.75,这是系统防止因网络问题导致心跳包未在设定时间到达服务器时所执行的策略,从而确保了连接的稳定性;

-- 时间线示意图:
-- 0秒               180秒                240秒
-- |------------------|-------------------|
-- 连接建立          客户端发送PINGREQ    服务器检测超时
--                   ↑                   ↑
--                   预留1/4时间给服务器响应

3. 建议在 mqtt 连接前设置心跳参数,也可以在连接后动态调整心跳时间;

4. 网络稳定环境可使用默认值 240 秒,或根据服务器要求调整;

5. 心跳间隔过短会增加功耗和网络流量,过长可能导致连接被网络断开;

参数

time

参数含义:用于设置mqtt客户端的心跳时间间隔,即客户端向服务器发送心跳包的时间间隔;
数据类型:number
取值范围:最小值:15秒(若传入小于15的值,会被强制设为15秒);
         最大值:600秒(若传入大于600的值,会被强制设为600秒);
是否必选:可选传入此参数,若不传入,默认值为240秒;
注意事项:单位为秒(s),无需额外转换单位;
         传入值会被自动截断到有效范围内(15-600秒);
                 实际发送心跳时间间隔 = 设置的心跳时间间隔 × 0.75
                 这是系统防止因网络问题导致心跳包未在设定时间到达服务器时所执行的策略,从而确保了连接的稳定性;
                         -- 时间线示意图:
                         -- 0秒               180秒                240秒
                         -- |------------------|-------------------|
                         -- 连接建立          客户端发送PINGREQ    服务器检测超时
                         --                   ↑                   ↑
                         --                   预留1/4时间给服务器响应
         心跳间隔过短会增加设备功耗和网络流量,过长可能导致连接被网络断开;
         该参数可在连接前或连接后设置,连接后设置会覆盖之前的值;
参数示例:-- 如下方所示,这是使用默认值240秒的写法,实际发送心跳时间间隔 = 设置的心跳时间间隔 × 0.75 = 180秒;
         mqttc:keepalive()
         -- 如下方所示,这是设置为30秒的写法,实际发送心跳时间间隔 = 设置的心跳时间间隔 × 0.75 = 22.5秒;
         mqttc:keepalive(30)
         -- 如下方所示,传入超出范围的值会被截断;
         mqttc:keepalive(10)  -- 实际生效为15秒,实际发送心跳时间间隔 = 设置的心跳时间间隔 × 0.75 = 11.25秒;
         mqttc:keepalive(800) -- 实际生效为600秒,实际发送心跳时间间隔 = 设置的心跳时间间隔 × 0.75 = 450秒;

返回值

该接口无返回值,只需调用该接口执行相关操作,无需处理返回结果;

如果一定要把接口调用的结果赋值给一个变量,则这个变量就是一个nil值;

示例

-- 如下方所示,这是使用默认值240秒的写法,实际发送心跳时间间隔 = 设置的心跳时间间隔 × 0.75 = 180秒;
mqttc:keepalive()
-- 如下方所示,这是设置为30秒的写法,实际发送心跳时间间隔 = 设置的心跳时间间隔 × 0.75 = 22.5秒;
mqttc:keepalive(30)
-- 如下方所示,传入超出范围的值会被截断;
mqttc:keepalive(10)  -- 实际生效为15秒,实际发送心跳时间间隔 = 设置的心跳时间间隔 × 0.75 = 11.25秒;
mqttc:keepalive(800) -- 实际生效为600秒,实际发送心跳时间间隔 = 设置的心跳时间间隔 × 0.75 = 450秒;

6.2.5 mqttc:will(topic, payload, qos, retain)

功能

设置 mqtt 客户端的遗嘱消息;

注意事项

1. 必须在调用 mqttc:connect() 之前调用此接口,否则遗嘱消息设置不会生效;

参数

topic

参数含义:遗嘱消息的主题;
数据类型:string
取值范围:根据MQTT 3.1.1标准协议,最小长度为1字节,最大长度为65535字节。LuatOS对此不做限制
是否必选:必须传入此参数;
注意事项:必须在调用mqttc:connect()之前设置,否则无效;
         主题中的特殊字符需要按照MQTT协议规范进行处理
         建议为每个设备或场景设计唯一的遗嘱主题,以便于接收端识别;
         topic和payload共同构成完整的遗嘱消息,应合理设计两者内容;
参数示例:-- 如下方所示,需要在调用mqttc:connect()之前设置,否则无效;
         mqttc:will("/luatos/123456", "123")

                 -- 注意:必须在connect前设置;
         mqttc:connect()

payload

参数含义:mqtt客户端设置的遗嘱消息内容
数据类型:string
取值范围:无特别限制;
是否必选:必须传入此参数;
注意事项:必须在调用mqttc:connect()之前设置,否则无效;
         如果包含非ASCII字符,需要确保编码格式与接收端一致;
         topic和payload共同构成完整的遗嘱消息,应合理设计两者内容;
参数示例:-- 如下方所示,需要在调用mqttc:connect()之前设置,否则无效;
         mqttc:will("/luatos/123456", "123")

                 -- 注意:必须在connect前设置;
         mqttc:connect()

qos

参数含义:mqtt客户端设置的遗嘱消息的服务质量等级
数据类型:number
取值范围:根据MQTT 3.1.1标准协议规范,有效值为
         qos 0(最多一次):消息可能丢失,也可能因为网络层或中间件的意外重放而出现重复,但MQTT协议本身不会主动再发一次
         qos 1(至少一次):发送端会一直重试,直到收到接收端的PUBACK确认
                                                   因此消息至少送达一次,但也可能因确认丢失而被重复投递;
         qos 2(只有一次):通过四步握手(PUBLISH  PUBREC  PUBREL  PUBCOMP)保证消息恰好送达一次;
                                                   若任一步丢失,发送端会重传对应报文,直至整个流程完成,从而避免重复或丢失;
是否必选:可选传入此参数,如不填,默认为0
注意事项:必须在调用mqttc:connect()之前设置,否则无效;
         如果不指定,默认使用qos 0
         确保mqtt服务器支持所设置的qos级别
参数示例:-- 如下方所示,使用默认qos 0(最多一次);
         mqttc:will("/luatos/123456", "123")
         -- 如下方所示,设置qos 1(至少一次);
         mqttc:will("/luatos/123456", "123", 1)
         -- 如下方所示,设置qos 2(恰好一次);
         mqttc:will("/luatos/123456", "123", 2)

         -- 注意:必须在connect前设置;
         mqttc:connect()

retain

参数含义:控制遗嘱消息的保留性;
数据类型:number
取值范围:0/1
是否必选:可选传入此参数,不填时,默认为0
注意事项:必须在调用mqttc:connect()之前设置,连接后设置无效;
         retain=1时,服务器会保留该遗嘱消息,并在有新客户端订阅相应主题时立即发送此消息;
         如果同一主题有新的保留消息发布,旧的保留消息将被替换;
参数示例:-- 如下方所示,使用默认retain参数,默认为0;
         mqttc:will("/luatos/123456", "123")
         -- 如下方所示,设置retain参数为1,保留遗嘱消息;
         mqttc:will("/luatos/123456", "123", , 1)
         -- 如下方所示,同时设置qos参数为1,retain参数为1;
         mqttc:will("device/status", "offline", 1, 1)

         -- 注意:必须在connect前设置;
         mqttc:connect()

返回值

local mqtt_will_result = mqttc:will(topic, payload, qos, retain)

有一个返回值 mqtt_will_result

mqtt_will_result

含义说明:判断设置遗嘱消息操作是否成功;
数值类型:boolean
取值范围:true/false
注意事项:返回值仅表示设置操作是否成功,不保证遗嘱消息最终会被服务器发布,遗嘱消息只有在客户端意外断开连接时才会由服务器发布;
         必须在调用mqttc:connect()之前设置遗嘱消息,连接后设置将返回false

示例

-- 如下方所示,设置遗嘱消息并检查是否成功;
local mqtt_will_result = mqttc:will("/luatos/123456", "123", 1, 1)

if mqtt_will_result then
    log.info("遗嘱消息设置成功")
    -- 继续执行连接操作;
else
    log.info("遗嘱消息设置失败,请检查参数或客户端状态")
    -- 进行错误处理;
end

6.2.6 mqttc:autoreconn(reconnect, reconnect_time)

功能

用于配置 mqtt 客户端的自动重连功能,包括启用/禁用自动重连以及设置重连周期;

注意事项

1. 该接口仅控制自动重连的配置,不直接执行重连操作;

2. 重连功能仅在连接断开后生效,不会主动尝试断开现有连接;

3. 如果项目中使用多网卡功能,一定不要使用此接口,否则内核固件在自动重连时,可能会使用错误的网卡,导致无法上网;

参数

reconnect

参数含义:控制是否启用mqtt客户端的自动重连功能
数据类型:boolean
取值范围:true/false
是否必选:可选传入此参数;
注意事项:若启用自动重连(设置为true),需注意重连周期(reconnect_time参数)会受到限制;
                 若配置的重连周期小于1000ms,系统会强制将其调整为1000ms,以避免过于频繁的重连尝试;
         该参数仅控制自动重连的开关状态,不直接执行重连操作,实际重连行为发生在连接断开后;
         建议在调用mqttc:connect()前配置该参数,以确保连接建立前重连策略已正确设置;
参数示例:-- 如下方所示,启用mqtt客户端的自动重连功能;
         mqttc:autoreconn(true)
         -- 如下方所示,禁用mqtt客户端的自动重连功能;
         mqttc:autoreconn(false)

reconnect_time

参数含义:设置mqtt客户端自动重连的时间间隔
数据类型:number
取值范围:≥1000ms的整数(若设置值小于1000ms,系统会强制将其调整为1000ms),默认值为3000ms
是否必选:可选传入此参数,若不填默认为3000ms
注意事项:参数单位为毫秒(ms),配置时需注意时间单位转换;
         当自动重连功能启用时,系统会确保重连周期不小于1000ms,以避免过于频繁的重连请求对服务器造成压力;
         该参数只有在自动重连功能启用(reconnect为true)时才有实际作用;
         可以单独配置该参数而不改变自动重连开关状态(例如仅调整重连周期,不改变是否自动重连);
参数示例:-- 如下方所示,启用自动重连功能,默认重连周期为3000ms;
         mqttc:autoreconn(true)
         -- 如下方所示,启用自动重连功能,设置重连周期为5000ms;
         mqttc:autoreconn(true, 5000)
         -- 如下方所示,启用自动重连功能,设置重连周期小于1000ms的周期(实际会被调整为1000ms);
         mqttc:autoreconn(true, 500) -- 实际重连周期为1000ms
         -- 如下方所示,不改变自动重连功能的状态,仅调整重连周期为4000ms;
         mqttc:autoreconn(nil, 4000)

返回值

该接口无返回值,只需调用该接口执行相关操作,无需处理返回结果;

如果一定要把接口调用的结果赋值给一个变量,则这个变量就是一个nil值;

示例

-- 如下方所示,启用自动重连功能,默认重连周期为3000ms;
 mqttc:autoreconn(true)
 -- 如下方所示,启用自动重连功能,设置重连周期为5000ms;
 mqttc:autoreconn(true, 5000)
 -- 如下方所示,启用自动重连功能,设置重连周期小于1000ms的周期(实际会被调整为1000ms);
 mqttc:autoreconn(true, 500) -- 实际重连周期为1000ms
 -- 如下方所示,不改变自动重连功能的状态,仅调整重连周期为4000ms;
 mqttc:autoreconn(nil, 4000)

6.2.7 mqttc:on(cb)

功能

注册 mqtt 客户端事件回调函数的接口;

注意事项

1. 回调函数应保持简洁高效 ,避免执行耗时操作(如复杂计算、阻塞 IO 等),否则可能影响 mqtt 客户端的消息处理实时性;

参数

cb

参数含义:mqtt事件回调函数;回调函数的格式为:
                 function callback(mqtt_client, event, data, payload, metas)
                     log.info("callback", mqtt_client, event, data, payload, json.encode(metas))
             end
         该回调函数接收mqtt_clienteventdatapayloadmetas五个参数,在不同事件类型下参数含义有所不同:

                 -- 参数含义:mqtt客户端对象本身;
         -- 数据类型:userdata;
         -- 取值范围:无限制;
         -- 是否必选:必须传入此参数;
         -- 注意事项:所有事件类型都会传递此参数,可用于在回调中操作客户端(如重连、发布消息等);
         mqtt_client

                 -- 参数含义:事件类型标识符;
         -- 数据类型:string;
         -- 取值范围:
                 "conack" -- 当event参数的值为"conack"时,表示服务器鉴权完成,mqtt连接已建立,可以订阅和发布数据;
                 "suback" -- 当event参数的值为"suback"时,表示客户端订阅主题完成;
                 "unsuback" -- 当event参数的值为"unsuback"时,表示客户端取消订阅主题完成;
                 "recv" -- 当event参数的值为"recv"时,表示客户端接收到服务器下发的消息;
                 "sent" -- 当event参数的值为"sent"时,表示客户端发送消息完成;
                 "disconnect" -- 当event参数的值为"disconnect"时,表示mqtt连接已断开;
                 "pong" -- 当event参数的值为"pong"时,表示客户端收到服务器心跳应答;
                 "error" -- 当event参数的值为"error"时,表示发生严重异常,会导致断开连接;
         -- 是否必选:必须传入此参数;
         -- 注意事项:用于标识当前触发的mqtt事件类型,是回调函数中判断事件类型的核心依据;
         event

                 -- 参数含义:事件相关的数据;
         -- 数据类型:根据event类型不同而变化;
         -- 取值范围:根据event类型不同而变化:
                 "conack" -- 当event参数的值为"conack"时,data参数无实际数据,实际打印时为"nil";
                 "suback" -- 当event参数的值为"suback"时,data参数的值表示客户端订阅主题完成状态;
                                                   -- 数据类型为布尔值(true表示客户端订阅主题成功,false表示客户端订阅主题失败);
                 "unsuback" -- 当event参数的值为"unsuback"时,data参数无实际数据,实际打印时为"nil";
                 "recv" -- 当event参数的值为"recv"时,data参数的值表示客户端收到服务器下发消息时的主题(topic);
                                                -- 数据类型为字符串;
                 "sent" -- 当event参数的值为"sent"时,data参数的值表示客户端发送消息时的消息id,数据类型为数值型;
                 "disconnect" -- 当event参数的值为"disconnect"时,data参数无实际数据,实际打印时为"nil";
                 "pong" -- 当event参数的值为"pong"时,data参数无实际数据,实际打印时为"nil";
                 "error" -- 当event参数的值为"error"时,data参数的取值范围有以下四种,数据类型为字符串:
                     "connect" -- 当data参数的值为"connect"时,表示客户端连接不上服务器;
                     "tx" -- 当data参数的值为"tx"时,表示客户端发送数据失败;
                     "conack" -- 当data参数的值为"conack"时,表示服务器鉴权失败;
                     "other" -- 当data参数的值为"other"时,表示其他异常情况;
         -- 是否必选:可选传入此参数;
         -- 注意事项:不同事件类型下数据类型和含义不同,需根据事件类型进行相应处理;
         data

                 -- 参数含义:业务数据或结果码;
         -- 数据类型:根据event类型不同而变化;
         -- 取值范围:根据event类型不同而变化;
                 "conack" -- 当event参数的值为"conack"时,payload参数无实际数据,实际打印时为"nil";
                 "suback" -- 当event参数的值为"suback"时,payload参数的值根据客户端订阅主题是否成功分为两种;
                                                  -- 当客户端订阅主题成功时payload参数的取值为0-2的整数(表示qos等级);
                                                  -- 当客户端订阅主题失败时payload参数的取值为失败码(一般是0x80);
                 "unsuback" -- 当event参数的值为"unsuback"时,payload参数无实际数据,实际打印时为"nil";
                 "recv" -- 当event参数的值为"recv"时,payload参数的值表示收到的业务数据内容,数据类型为字符串;
                 "sent" -- 当event参数的值为"sent"时,payload参数无实际数据,实际打印时为"nil";
                 "disconnect" -- 当event参数的值为"disconnect"时,payload参数无实际数据,实际打印时为"nil";
                 "pong" -- 当event参数的值为"pong"时,payload参数无实际数据,实际打印时为"nil";
                 "error" -- 当event参数的值为"error"时,且data参数的值为"conack"时;
                                                 -- payload参数的值表示整数失败码,data参数的值为其他类型时;
                                                 -- payload参数无实际数据,实际打印时为"nil";
         -- 是否必选:可选传入此参数;
         -- 注意事项:通常与data参数配合使用,提供更详细的事件信息;
         payload

                 -- 参数含义:消息元数据(仅在特定事件中存在)
         -- 数据类型:table;
         -- 取值范围:仅当event参数的值为"recv"时,metas参数的取值才有意义;
                 --                  event参数的值为其他类型时,metas参数无实际数据,实际打印时为"nil";
                 message_id -- 消息id;
                 qos -- 服务器质量等级,取值范围0、1、2;
                 retain -- 保留标志,取值范围0、1;
                 dup -- 重复标志,取值范围0、1;
         -- 是否必选:可选传入此参数;
         -- 注意事项:仅部分事件类型提供,主要用于获取消息的元数据信息,一般情况下可根据需要选择性处理;
         metas
数据类型:function
取值范围:回调函数本身无取值范围这一说法;
是否必选:必须传入此参数;
注意事项:每次调试会覆盖之前注册的回调函数;
参数示例:-- 如下方所示,这是注册mqtt客户端的事件回调函数写法;
         function mqtt_client_cbfunc(mqtt_client, event, data, payload, metas)
             -- 每次进回调函数都打印一次这五个参数;
             log.info("mqtt_client_cbfunc", mqtt_client, event, data, payload, json.encode(metas))

             -- 服务器鉴权完成,表示mqtt连接已建立,可以订阅和发布数据;
             if event == "conack" then
                 log.info("mqtt_client_cbfunc", "conack", "连接成功")
                 -- 此处为用户代码,可在此处订阅主题等;

             -- 订阅完成;
             -- data:订阅应答结果,true为成功,false为失败;
             -- payload:成功时为0-2的整数(表示qos等级),失败时为失败码(一般是0x80);
             elseif event == "suback" then
                 log.info("mqtt_client_cbfunc", "suback", "订阅完成")
                 if data then
                     log.info("mqtt_client_cbfunc", "suback", "订阅结果:", data, "qos:", payload)
                 else
                     log.info("mqtt_client_cbfunc", "suback", "订阅结果:", data, "失败码:", payload)
                 end

             -- 取消订阅完成;
             elseif event == "unsuback" then
                 log.info("mqtt_client_cbfunc", "unsuback", "取消订阅完成")

             -- 接收到服务器下发的数据;
             -- data:字符串类型,表示收到消息的主题;
             -- payload:字符串类型,表示收到消息的业务数据内容;
             -- metas:表类型,表示收到消息的元数据,数据内容格式如下:
             -- {
             --      message_id:数值类型,表示消息id;
             --      qos:数值类型,取值范围0/1/2,表示服务器质量等级;
             --      retain:数值类型,取值范围0/1,表示保留标志;
             --      dup:数值类型,取值范围0/1,表示重复标志;
             -- }
             elseif event == "recv" then
                 log.info("mqtt_client_cbfunc", "recv", "收到消息", "主题:", data, "内容:", payload)
                 -- 打印收到消息的元数据内容;
                 if metas then
                     log.info("mqtt_client_cbfunc", "recv", "消息元数据", "qos:", metas.qos, "retain:", metas.retain)
                 end

             -- 发送消息成功;
             -- data:数值类型,表示消息id;
             elseif event == "sent" then
                 log.info("mqtt_client_cbfunc", "sent", "发送消息成功", "消息id:", data)

             -- 服务器断开mqtt连接;
             -- data:数值类型,表示错误码;
             elseif event == "disconnect" then
                 log.info("mqtt_client_cbfunc", "disconnect", "连接断开", "错误码:", data)

             -- 收到服务器的心跳应答;
             elseif event == "pong" then
                 log.info("mqtt_client_cbfunc", "pong", "收到服务器心跳应答")

             -- 严重异常,本地会主动断开连接;
             -- data:字符串类型,表示具体的异常,有以下几种类型:
             --      "connect":表示tcp连接失败;
             --      "tx":表示数据发送失败;
             --      "conack":表示mqtt connect后,服务器应答CONNACK鉴权失败;
             --                                 错误码通过payload参数查看(数值类型);
             --      "other":其他异常,错误码通过payload参数查看(数值类型);
             elseif event == "error" then
                 log.info("mqtt_client_cbfunc", "error", "发生异常错误")
                 if data == "connect" then
                     log.info("mqtt_client_cbfunc", "error", "tcp连接失败")
                 elseif data == "tx" then
                     log.info("mqtt_client_cbfunc", "error", "数据发送失败")
                 elseif data == "conack" then
                     log.info("mqtt_client_cbfunc", "error", "服务器鉴权失败,错误码:", payload)
                 else
                     log.info("mqtt_client_cbfunc", "error", "未知错误,错误码:", payload)
                 end
             end
         end

         -- 注册mqtt事件回调函数接口,mqtt_client_cbfunc为回调函数;
         mqttc:on(mqtt_client_cbfunc)

返回值

该接口无返回值,只需调用该接口执行相关操作,无需处理返回结果;

如果一定要把接口调用的结果赋值给一个变量,则这个变量就是一个nil值;

示例

-- 如下方所示,这是注册mqtt客户端的事件回调函数写法;
function mqtt_client_cbfunc(mqtt_client, event, data, payload, metas)
    -- 每次进回调函数都打印一次这五个参数;
    log.info("mqtt_client_cbfunc", mqtt_client, event, data, payload, json.encode(metas))

    -- 服务器鉴权完成,表示mqtt连接已建立,可以订阅和发布数据;
    if event == "conack" then
        log.info("mqtt_client_cbfunc", "conack", "连接成功")
        -- 此处为用户代码,可在此处订阅主题等;

    -- 订阅完成;
    -- data:订阅应答结果,true为成功,false为失败;
    -- payload:成功时为0-2的整数(表示qos等级),失败时为失败码(一般是0x80);
    elseif event == "suback" then
        log.info("mqtt_client_cbfunc", "suback", "订阅完成")
        if data then
            log.info("mqtt_client_cbfunc", "suback", "订阅结果:", data, "qos:", payload)
        else
            log.info("mqtt_client_cbfunc", "suback", "订阅结果:", data, "失败码:", payload)
        end

    -- 取消订阅完成;
    elseif event == "unsuback" then
        log.info("mqtt_client_cbfunc", "unsuback", "取消订阅完成")

    -- 接收到服务器下发的数据;
    -- data:字符串类型,表示收到消息的主题;
    -- payload:字符串类型,表示收到消息的业务数据内容;
    -- metas:表类型,表示收到消息的元数据,数据内容格式如下:
    -- {
    --      message_id:数值类型,表示消息id;
    --      qos:数值类型,取值范围0/1/2,表示服务器质量等级;
    --      retain:数值类型,取值范围0/1,表示保留标志;
    --      dup:数值类型,取值范围0/1,表示重复标志;
    -- }
    elseif event == "recv" then
        log.info("mqtt_client_cbfunc", "recv", "收到消息", "主题:", data, "内容:", payload)
        -- 打印收到消息的元数据内容;
        if metas then
            log.info("mqtt_client_cbfunc", "recv", "消息元数据", "qos:", metas.qos, "retain:", metas.retain)
        end

    -- 发送消息成功;
    -- data:数值类型,表示消息id;
    elseif event == "sent" then
        log.info("mqtt_client_cbfunc", "sent", "发送消息成功", "消息id:", data)

    -- 服务器断开mqtt连接;
    -- data:数值类型,表示错误码;
    elseif event == "disconnect" then
        log.info("mqtt_client_cbfunc", "disconnect", "连接断开", "错误码:", data)

    -- 收到服务器的心跳应答;
    elseif event == "pong" then
        log.info("mqtt_client_cbfunc", "pong", "收到服务器心跳应答")

    -- 严重异常,本地会主动断开连接;
    -- data:字符串类型,表示具体的异常,有以下几种类型:
    --      "connect":表示tcp连接失败;
    --      "tx":表示数据发送失败;
    --      "conack":表示mqtt connect后,服务器应答CONNACK鉴权失败,错误码通过payload参数查看(数值类型);
    --      "other":其他异常,错误码通过payload参数查看(数值类型);
    elseif event == "error" then
        log.info("mqtt_client_cbfunc", "error", "发生异常错误")
        if data == "connect" then
            log.info("mqtt_client_cbfunc", "error", "tcp连接失败")
        elseif data == "tx" then
            log.info("mqtt_client_cbfunc", "error", "数据发送失败")
        elseif data == "conack" then
            log.info("mqtt_client_cbfunc", "error", "服务器鉴权失败,错误码:", payload)
        else
            log.info("mqtt_client_cbfunc", "error", "未知错误,错误码:", payload)
        end
    end
end

-- 注册mqtt事件回调函数接口,mqtt_client_cbfunc为回调函数;
mqttc:on(mqtt_client_cbfunc)

6.2.8 mqttc:connect()

功能

连接 mqtt 服务器;

注意事项

1. 该接口返回 true 仅表示连接请求成功发起,不代表与 mqtt 服务器的连接已成功建立;

2. 该接口返回 false 仅表示连接请求失败,通常意味着底层 socket 创建或连接失败;

3. 调用该接口前必须先配置服务器地址、端口、客户端 ID 等参数,如果未配置必要参数,该接口可能返回 false;

参数

无;

返回值

local mqtt_connect_result = mqttc:connect()

有一个返回值 mqtt_connect_result

mqtt_connect_result

含义说明:表示mqtt连接请求是否成功发起
数值类型:boolean
取值范围:true/false
注意事项:必须先通过mqttc:auth接口配置连接参数,否则连接请求可能失败;
         返回值不代表实际连接成功,该接口仅表示连接请求是否成功发送,不代表已与mqtt服务器建立实际连接
         需通过mqttc:ready接口或注册的conack事件回调来确认连接是否真正建立

示例

-- 连接mqtt服务器
-- 本函数仅代表发起成功, 后续仍需根据mqttc:ready接口或注册的conack事件回调判断mqtt是否连接正常
mqtt_connect_request = mqtt_client:connect()

-- 判断连接请求是否成功
if mqtt_connect_request then
    log.info("mqtt_client_task_func", "mqtt_connect_request success")
else
    log.error("mqtt_client_task_func", "mqtt_connect_request error")
end

6.2.9 mqttc:ready()

功能

检查 mqtt 客户端是否处于就绪状态(已成功连接到 mqtt 服务器);

注意事项

1. 该接口仅检查客户端的内部状态标记,不进行实际的网络连通性测试;

2. 返回 true 仅表示客户端当前认为自己处于连接状态,不保证网络连接一定稳定;

3. 即使返回 true,后续的发布/订阅操作仍有可能因网络问题失败,建议做好错误处理;

参数

无;

返回值

local mqtt_ready_result = mqttc:ready()

有一个返回值 mqtt_ready_result

mqtt_ready_result

含义说明:判断客户端是否已经连接就绪;
数值类型:boolean
取值范围:true/false
注意事项:接口返回值仅检查客户端的内部状态标记,不进行实际的网络连通性测试;
         返回true仅表示客户端当前认为自己处于连接状态,不保证网络连接一定稳定;
         即使返回true,后续的发布/订阅操作仍有可能因网络问题失败,建议做好错误处理;

示例

-- 如下方所示,为一个最基本的mqttc:ready()接口的写法;
local mqtt_ready_result = mqttc:ready()

-- 判断mqtt客户端是否已就绪
if mqtt_ready_result then
    log.info("mqtt 客户端已就绪")
    -- 用户处理代码
else
    log.info("mqtt 客户端未就绪,正在尝试重连...")
    -- 执行重连操作
end

6.2.10 mqttc:subscribe(topic, qos)

功能

订阅一个/多个主题(topic);

注意事项

1. 需要先建立 mqtt 客户端且连接成功后再调用;

2. 订阅时 topic 的命名要合法,qos 的值要在规定范围内;

3. 多主题订阅下,只要有任一主题订阅失败,整体返回 nil,需注意订阅失败的处理逻辑;

参数

topic

参数含义:订阅主题时的主题名称;
数据类型:string/table
取值范围:根据MQTT 3.1.1标准协议,最小长度为1字节,最大长度为65535字节。LuatOS对此不做限制
是否必选:必须传入此参数;
注意事项:mqtt topic的命名要符合规范
示例代码:-- 如下方所示,这是订阅单个topic的写法,“/luatos/123456” 为订阅的topic名,其余参数请看后面解释;
         mqttc:subscribe("/luatos/123456", 0)
         -- 如下方所示,这是订阅多个topic的写法,“/luatos/1234567” 和 “/luatos/12345678” 为订阅的两个不同topic名;
                 -- 其余参数请看后面解释;
         mqttc:subscribe({["/luatos/1234567"] = 1, ["/luatos/12345678"] = 2})

qos

参数含义:服务质量等级,决定了一条消息最多被送达一次、至少被送达一次,还是恰好被送达一次;
         qos 0(最多一次):消息可能丢失,也可能因为网络层或中间件的意外重放而出现重复,但mqtt协议本身不会主动再发一次
         qos 1(至少一次):发送端会一直重试,直到收到接收端的PUBACK确认
                                                  因此消息至少送达一次,但也可能因确认丢失而被重复投递;
         qos 2(只有一次):通过四步握手(PUBLISH  PUBREC  PUBREL  PUBCOMP)保证消息恰好送达一次;
                                                  若任一步丢失,发送端会重传对应报文,直至整个流程完成,从而避免重复或丢失;
数值类型:number
取值范围:0/1/2
是否必选:可选传入此参数,若不填,默认为0
注意事项:当订阅与发布的qos等级不一致时,订阅者最终拿到的qos=min(发布qos, 订阅qos)
         topic本身最大64k字节,但高qos下Broker会把topic+payload一起缓存,过长的topic会放大内存占用
                 建议topic控制在100字节以内,尤其在高并发高qos场景
参数示例:-- 如下方所示,这是订阅单个topic的写法,第二个参数为qos等级,默认为0;
         mqttc:subscribe("/luatos/123456", 0)
         -- 如下方所示,这是订阅多个topic的写法;
         mqttc:subscribe({["/luatos/1234567"] = 1, ["/luatos/12345678"] = 2})

返回值

local mqtt_sub_result = mqttc:subscribe(topic, qos)

有一个返回值 mqtt_sub_result

mqtt_sub_result

含义说明:判断订阅主题请求接口是否成功;
                 当请求成功时,返回一个数值类型的消息ID,这个ID在qos为1/2时有效;
                 当底层返回失败时,返回nil
数据类型:number/nil
取值范围:无特别限制;
注意事项:底层失败时返回nil,需做好空值判断;
         该返回值仅表示订阅请求发送成功 ,而非服务器已确认订阅成功;
                 若要判断订阅是否真正成功,可通过注册事件回调函数接口mqttc:on来监听服务器的订阅确认

示例

-- 订阅单个主题(topic),且qos=0;
mqttc:subscribe("/luatos/123456", 0)
-- 订阅单个(topic),且qos=1;
mqttc:subscribe("/luatos/12345678", 1)
-- 订阅多个(topic),且使用不同的qos;
mqttc:subscribe({["/luatos/1234567"] = 1, ["/luatos/12345678"] = 2})

6.2.11 mqttc:publish(topic, data, qos, retain)

功能

向指定的 mqtt 主题发布消息;

注意事项

1. 调用此接口前,必须确保 mqtt 客户端已经成功连接到服务器,否则发布操作可能失败;

2. 遵循 mqtt 主题命名规范,避免使用特殊字符导致的兼容性问题;

参数

topic

参数含义:表示mqtt消息发布的主题,用于标识消息的类别或目的地;
数据类型:string
取值范围:根据MQTT 3.1.1标准协议,最小长度为1字节,最大长度为65535字节。LuatOS对此不做限制
是否必选:必须传入此参数;
注意事项:主题命名应遵循mqtt协议规范,避免使用特殊字符导致路由异常;
参数示例:-- 如下方所示,/luatos/123456是发送数据时的发布主题,123是发送的数据内容;
         mqttc:publish("/luatos/123456", "123")

data

参数含义:表示要发布的mqtt消息内容,是消息的实际负载数据;
数据类型:string/zbuff
取值范围:无特定限制,允许为空;
是否必选:必须传入此参数;
注意事项:仅支持string和zbuff两种数据类型,其他类型会打印日志"only support string or zbuff"
         虽然必填,但允许传递空字符串(长度为0);
         对于二进制数据或较大数据,建议使用zbuff类型以提高传输效率
         调用时如果不提供该参数或参数类型错误,可能导致底层发布函数接收到NULL数据
参数示例:-- 如下方所示,/luatos/123456是发送数据时的发布主题,123是发送的数据内容;
         mqttc:publish("/luatos/123456", "123")
         -- 如下方所示,/luatos/123456是发送数据时的发布主题,data参数允许为空;
         mqttc:publish("/luatos/123456", "")

qos

参数含义:服务质量等级,决定了一条消息最多被送达一次、至少被送达一次,还是恰好被送达一次;
         qos 0(最多一次):消息可能丢失,也可能因为网络层或中间件的意外重放而出现重复,但mqtt协议本身不会主动再发一次
         qos 1(至少一次):发送端会一直重试,直到收到接收端的PUBACK确认
                                                  因此消息至少送达一次,但也可能因确认丢失而被重复投递;
数据类型:number
取值范围:0/1,默认为0
是否必选:可选传入此参数,不填时,默认为0
注意事项:如果不提供该参数,默认使用qos 0
                 该接口的qos等级取值范围仅支持0或1
参数示例:-- 如下方所示,qos参数不填时,默认为0;
         mqttc:publish("/luatos/123456", "123")
         -- 如下方所示,设置qos参数为1;
         mqttc:publish("/luatos/123456", "123", 1)

retain

参数含义:用于控制mqtt消息是否被服务器保留
                 当设置为保留模式时,服务器会保存该主题的最新消息,并在新客户端订阅该主题时自动发送此消息;
数据类型:number
取值范围:0/1,默认为0
是否必选:可选传入此参数,不填时,默认为0
注意事项:服务器对每个主题只保留最新的一条保留消息,新的保留消息会覆盖旧的;
         使用保留消息会增加服务器的存储开销,应根据实际需求谨慎使用;
参数示例:-- 如下方所示,retain参数不填时,默认为0;
         mqttc:publish("/luatos/123456", "123")
         -- 如下方所示,retain参数设置为1,意为服务器保留该消息;
         mqttc:publish("/luatos/123456", "123", , 1)

返回值

local mqtt_pub_result = mqttc:publish(topic, data, qos, retain)

有一个返回值 mqtt_pub_result

mqtt_pub_result

含义说明:用于跟踪消息的发布状态;
数值类型:number
取值范围:成功时,返回一个整数类型的消息ID
         失败时,返回nil
注意事项:当消息发布失败时,接口会返回nil,建议在实际使用中对nil进行判断处理
                 当消息发布成功时,接口会返回消息ID,此时仅表示消息发送请求成功,不能表示消息已经成功发送到服务器;
                 要确认消息是否真正到达服务器,需要结合qos级别和响应的确认机制
                         qos为1:可以通过mqttc:on注册回调接口确认,在收到服务器确认时会得到通知;
                         qos为0:无法直接确认消息是否到达服务器,只能依赖应用层确认机制;
                                        应用层确认机制的核心思想是发送方发送消息并记录发送状态,接收方收到消息后主动发送一条确认消息;
                                        发送方在指定时间内未收到确认则重新发送原消息,这样便可以实现消息的可靠传递;

示例

-- 如下方所示,这是一个基本用法,只设置topic参数和data参数;
mqttc:publish("/luatos/123456", "123")
-- 如下方所示,这是设置qos参数为1的写法,qos为1可以保证消息至少到达一次;
mqttc:publish("/device/status", "online", 1)
-- 如下方所示,这是设置qos参数为0,retain参数为1的写法,retain为1可以使服务器保留该消息;
mqttc:publish("/device/status", "online", 0, 1)

6.2.12 mqttc:unsubscribe(topic)

功能

取消订阅主题(Topic);

注意事项

1. 需要确保取消订阅的主题存在,否则会取消订阅失败;

2. 多主题取消订阅下,只要有任一主题取消订阅失败,整体返回 nil,需注意处理;

参数

topic

参数含义:取消订阅主题时的主题名称;
数据类型:string/table
取值范围:根据MQTT 3.1.1标准协议,最小长度为1字节,最大长度为65535字节。LuatOS对此不做限制
是否必选:必须传入此参数;
注意事项:取消订阅的主题必须与之前订阅的主题完全匹配(包括通配符),MQTT协议不支持部分匹配或模糊取消订阅
参数示例:-- 如下方所示,这是取消订阅单个主题(topic)的写法;
                 -- “/luatos/123456”为取消订阅的主题名;
         mqttc:unsubscribe("/luatos/123456")
         -- 如下方所示,这是取消订阅多个主题(topic)的写法;
                 -- “/luatos/1234567”和“/luatos/12345678”为取消订阅的两个不同主题名;
         mqttc:unsubscribe({"/luatos/1234567","/luatos/12345678"})

返回值

该接口无返回值,只需调用该接口执行相关操作,无需处理返回结果;

如果一定要把接口调用的结果赋值给一个变量,则这个变量就是一个nil值;

示例

-- 取消订阅单个主题(topic);
mqttc:unsubscribe("/luatos/123456")
-- 取消订阅多个主题(topic);
mqttc:unsubscribe({"/luatos/1234567","/luatos/12345678"})

6.2.13 mqttc:disconnect()

功能

用于断开与 mqtt 服务器的连接,但不会释放 mqtt 客户端的资源。

注意事项

1. 该接口仅断开与服务器的连接,但 不会释放 mqtt 客户端实例的资源 (如内存、配置等)

参数

无;

返回值

local mqtt_disconnect_result = mqttc:disconnect()

有一个返回值 mqtt_disconnect_result

mqtt_disconnect_result

含义说明:表示mqtt断开连接操作是否已发起
数值类型:boolean
取值范围:true/false
注意事项:返回值仅表示断开连接操作已发起,不代表断开连接操作是否真正成功完成;
         即使返回true,也不意味着资源已被释放;
         如需确认是否真正断开连接,建议通过注册disconnect事件回调或检查连接状态来判断

示例

-- 断开连接;
mqttc:disconnect()

6.2.14 mqttc:close()

功能

关闭 mqtt 客户端连接并释放相关资源;

注意事项

1. 调用 close() 后,mqtt 客户端相关的所有资源(包括套接字、回调函数引用等)都会被释放;

2. 释放资源后,该 mqtt 客户端实例将无法再使用,若需要重新连接 mqtt 服务器,必须创建新的客户端实例;

3. 此操作是不可逆的,一旦调用,无法通过任何方式恢复客户端的功能;

参数

无;

返回值

该接口无返回值,只需调用该接口执行相关操作,无需处理返回结果;

如果一定要把接口调用的结果赋值给一个变量,则这个变量就是一个nil值;

示例

-- 如下方所示,不再需要连接时,关闭客户端并释放资源;
mqttc:close()

6.2.15 mqttc:state()

功能

获取 mqtt 客户端的当前状态,返回一个代表不同连接阶段或状态的整数值;

注意事项

1. 状态码仅反映客户端内部的状态标记,不保证与实际网络连接状态完全一致;

2. 在使用状态码进行逻辑判断时,建议使用常量而非硬编码的数字,以提高代码可维护性;

3. 与 mqttc:ready() 类似,即使返回就绪状态,实际的发布/订阅操作仍可能因网络问题失败;

参数

无;

返回值

local mqtt_state = mqttc:state()

有一个返回值 mqtt_state

mqtt_state

含义说明:返回客户端当前的状态码,对应不同的mqtt客户端状态
数值类型:number
取值范围:状态码 0,对应常量 mqtt.STATE_DISCONNECT,表示mqtt客户端当前处于断开连接的状态
                                 没有与mqtt服务器建立任何连接
         状态码 1,对应常量 mqtt.STATE_SCONNECT,表示mqtt客户端正在尝试建立底层socket连接,但尚未完成连接过程;
         状态码 2,对应常量 mqtt.STATE_MQTT,表示底层socket连接已成功建立
                                  mqtt协议层面的连接(如发送CONNECT消息、等待CONNACK响应)正在进行中;
         状态码 3,对应常量 mqtt.STATE_READY,表示mqtt客户端已成功完成所有连接步骤,处于完全就绪状态;
                                  可以进行发布消息、订阅主题等操作;
注意事项:返回的状态码仅反映客户端内部的状态标记,不保证与实际网络连接状态完全一致;
                 即使返回已连接状态,网络连接仍可能已经断开但客户端尚未检测到;
         建议在使用时通过常量而非硬编码的数字来引用状态,以提高代码的可维护性;

示例

-- 如下方所示,为mqttc:state接口的基础用法;
local mqtt_state = mqttc:state()

-- 记录当前状态
log.info("mqtt_state", "当前状态:", mqtt_state)

-- 根据不同状态执行不同操作
if mqtt.STATE_DISCONNECT == mqtt_state then
    log.info("mqtt_state", "mqtt客户端未连接,尝试重连...")
    -- 可调用mqttc:connect接口进行重新连接
elseif mqtt.STATE_SCONNECT == mqtt_state then
    log.info("mqtt_state", "建立底层socket连接中,请稍候...")
elseif mqtt.STATE_MQTT == mqtt_state then
    log.info("mqtt_state", "底层socket建立成功,正在进行mqtt连接,请稍后...")
elseif mqtt.STATE_READY == mqtt_state then
    log.info("mqtt_state", "mqtt客户端已就绪,可以进行发布消息、订阅主题等操作")
end

第七部分:LuatOS上的MQTT client 应用开发框架

在理解了mqtt核心库之后,我们再来实际看一个完整的mqtt client长连接的demo项目代码,重点分析下如何在项目中使用LuatOS mqttt核心库;

mqtt client长连接的demo项目代码路径:Air8000 mqtt demo

7.1 总体设计框图

demo项目的总体设计框图如下:

暂时无法在飞书文档外展示此内容

7.2 模拟器上运行这个项目(使用模拟器单网卡演示项目完整的业务逻辑)

首先我们在LuatOS模拟器上运行一下这个demo项目,让大家对实现的功能有一个直观的认识,关于模拟器的使用参考:LuatOS模拟器使用说明

如果要在LuatOS模拟器上运行这个项目,按照以下几点准备好软件环境:

1、netdrv_device.lua中打开pc模拟器的网卡驱动文件require "netdrv_pc",注释掉其他其他网卡文件;

2、因为这个demo项目需要用到uart功能,要在pc上模拟uart收发功能,需要在pc上安装一个虚拟串口工具,生成一组串口,例如串口1和串口2,如果这两个串口id在你的pc上已经被占用,可以自定义生成任意两个id的串口就行;

  1. 虚拟串口工具的使用说明参考如何使用虚拟串口工具生成一对串口;生成的这一对串口可以互相给对方发数据,也能互相接收对方发送过来的数据;
  2. 如果生成的是串口1和串口2,这个demo项目中的uart_app.lua中使用的uart1,不用修改uart_app.lua的代码,此时在pc上使用sscom或者llcom串口工具打开串口2即可,这样的话,模拟器中的uart1就可以和sscom或者llcom打开的uart2进行收发数据;
  3. 如果生成的一对串口没有串口1,假设是串口11和串口12,则需要修改uart_app.lua中的代码,串口id修改为11,pc上使用sscom或者llcom串口工具打开串口12即可,这样的话,模拟器中的uart11就可以和sscom或者llcom打开的uart12进行收发数据;

3、使用 MQTTX客户端工具(点击此处可以阅读使用说明),在PC上创建MQTT客户端,用来:

  1. 订阅以下主题,接收demo中的四个MQTT client发布的消息:
  2. zhutianhua1/uart/up
  3. zhutianhua1/timer/up
  4. 在主题862991234567980/down上发布消息给demo中的四个MQTT client

软件环境准备好之后,接下来我们在模拟器上实际运行一下这个项目看看效果;

双击 cmd 命令行窗口,然后输入下面一行命令,运行 luatos 批处理文件,同时输入要运行的 luatos 项目配置文件

luatos --llt=H:\Luatools\project\Air8000_mqtt.ini

然后按回车键,就可以运行 mqtt 项目软件;

7.3 分析项目代码

这个mqtt demo中的readme文件,以及代码中的注释都比较详细,接下来我用vscode直接打开这份demo项目代码,和大家一起分析下项目代码;

7.4 Air8000开发板上运行演示这个项目(重点演示多网卡的使用)

准备硬件环境:

1、Air8000开发板一块+可上网的sim卡一张+4g天线一根+wifi天线一根+网线一根:

  1. sim卡插入开发板的sim卡槽
  2. 天线装到开发板上
  3. 网线一端插入开发板网口,另外一端连接可以上外网的路由器网口或者交换机网口;

2、TYPE-C USB数据线一根 + USB转串口数据线一根,Air8000开发板和数据线的硬件接线方式为:

  1. Air8000开发板通过TYPE-C USB口供电;(外部供电/USB供电 拨动开关 拨到 USB供电一端)
  2. TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;

准备软件环境:

  1. 手机打开WiFi热点zhutianhua 123qweasd,修改netdrv_multiple.lua中的WiFi信息;
  2. netdrv_device.lua打开多网卡驱动单独演示;
  3. Luatools烧录内核固件以及修改后的demo脚本;

多网卡演示:

开机后,在Luatools运行日志中搜索 new adapter,如下图所示,按照红色文字操作演示

img

单网卡演示:

直接运行并且分析日志;

完整的指南文章参考:Air8000 MQTT应用教程

第八部分:如何分析LuatOS MQTT日志

在mqtt开发使用过程中,或多或少,我们都会遇到一些问题,遇到问题时,如果我们自己可以根据日志进行初步分析,可能会大大提高解决问题的效率;在本章节我们提供三种日志分析方法;

8.1 三种日志分析方法

8.1.1 Luatools抓取的应用日志分析

这部分是Luatools抓取的应用日志,在Luatools的主窗口可以实时显示,如下图所示:

应用日志分为两种类型:

  1. 一部分是Lua脚本中输出的日志,有D/、I/、W/、E/几种前缀,这部分日志,大家根据自己编写的Lua脚本逻辑自行分析即可;
  2. 另一部分是内核固件中输出的一些关键日志,没有D/、I/、W/、E/几种前缀,这部分日志,大家不知道所表示的意义,我们会在本章节的以下内容中针对一些常见的日志进行逐一分析;

8.1.2 抓取LuatOS模拟器上的网络交互包进行分析

mqtt应用完全可以在LuatOS模拟器上运行,所以我们可以使用LuatOS模拟器来运行自己的程序;

在运行过程中使用wireshark抓取网络交互包,进行详细分析,例如下图是在LuatOS模拟器运行过程中,tls+单向认证连接服务器失败的网络交互包,从交互包中可以准确的得知,失败原因是Unknown CA,未识别的CA证书

这种分析方法对于解决socket,http,mqtt,ftp等网络应用的问题,帮助很大!

8.1.3 Luatools抓取的底层日志网络交互包分析

这里所说的抓取底层日志网络交互包分析,和 9.1.2 抓取LuatOS模拟器上的网络交互包进行分析 相比,一个是在真实的硬件板上(例如Air8000)运行抓日志,一个是在LuatOS模拟器上运行抓网络交互包;

在真实的硬件板上抓到日志后,然后再使用底层日志分析工具提取出来网络交互包,然后再使用wireshark对提取出来的网络交互包进行分析;

和模拟器运行直接抓网络交互包相比,这种方式有以下几种明显的缺点:

1、抓取日志以及分析操作繁琐;

2、硬件板内存资源有限,抓到的网络日志会丢包,特别是网络数据交互频繁的时候,丢包问题严重;

虽然有以上两项缺点,但是因为是真实的运行环境,对于LuatOS模拟器运行无法分析解决的问题,最终还是要通过这种方式解决;

如果需要用到这种方式来分析解决问题,当前阶段,客户只需要提交Luatools抓到的日志给合宙技术人员即可,由合宙技术人员进行分析;

后续我们也会在docs.openluat.com写一篇文章,单独讲解如何使用这种方法分析问题;大家如果有兴趣,可自行查看文档学习。

所以,在本章节接下来的内容中,我会针对一些常见的问题,分别使用Luatools应用日志分析LuatOS模拟器上的网络交互包分析 这两种方法来讲述;

8.2 mqtt创建

8.2.1 创建失败

Luatools应用日志分析

使用mqtt.create(adapter, host, port, ssl, isipv6)接口创建mqtt对象时,常见的错误日志,如下图所示:

这种异常日志的意思是:无法为mqtt对象分配资源;

出现这种异常,通常是因为同时存在的mqtt对象数量超过了内核固件限制的最大数量:

  1. Air780系列/Air8000系列的模组,允许同时存在的mqtt对象数量为64个;
  2. Air6101系列/Air8101系列的模组,允许同时存在的mqtt对象数量为32个;

通常是以下两种原因造成的:

  1. 项目中开发业务代码时,mqtt.create和mqtt.close是一对逆操作,如果不断的create,但是没有release,就会出现这种问题,这种错误的使用方式比较常见;
  2. 例如在一个实际项目中,创建mqtt,mqtt连接,mqtt收发业务逻辑处理,如果业务逻辑处理过程中出现异常,就会断开mqtt,然后再销毁mqtt;这是一套完整的操作,出现异常后,会从mqtt创建开始重试;在实际代码开发过程中,有可能会忘记销毁mqtt;这样的话,每次重试都会创建一个新的mqtt;随着重试的次数增多,最终同时存在的mqtt对象就会超过上限而出现错误;例如下图中,如果漏写了红框内的代码,随着时间的推移,最终就会出现问题

  3. 项目中正常业务逻辑,同时使用的mqtt对象数量超过了限制,这种情况下,只能简化业务逻辑,减少对mqtt、的使用;不过这种情况几乎不会出现,因为一个项目的正常业务逻辑,几乎不会出现同时使用几十个mqtt对象的情况;

LuatOS模拟器上的网络交互包分析

mqttt创建失败,不涉及网络交互包,所以不适用于此方法进行分析;

8.3 mqtt连接

8.3.1 dns解析失败

Luatools应用日志分析

如下图所示,解析域名:lbsmqtt.airm2m4289894.com;

一共四个DNS服务器,每个服务器尝试解析3次,最终没有解析成功,提示:dns_run 649:no ipv6, no ipv4

出现此种错误,可以通过以下几步尝试分析解决:

1、确认下域名输入是否正确;

2、参考socket.setDNS(adapter_id, dns_index, ip)的说明配置自定义的DNS服务器;

LuatOS模拟器上的网络交互包分析

8.3.2 tcp握手连接失败

Luatools应用日志分析

当对端ip地址存在,端口不存在时,例如:连接合宙提供的mqtt测试服务器,域名地址为:lbsmqtt.airm2m.com,端口38845(不存在)

会有以下异常日志:net_lwip_tcp_err_cb 662:adapter 1 socket 2 not closing, but error -13

这里有一个错误值:-13,表示:

ERR_ABRT = -13

  • 含义:连接被中止。
  • 场景:TCP 连接被本地或对端异常中止(如收到 RST 包,或本地主动调用tcp_abort)。

具体到本日志,因为对端ip不存在,所以应该是tcp三次握手过程中,超时,内核固件主动断开了连接;

还有一种常见的错误是对端IP不存在,此时在异常日志中的错误值很可能是:-13;

如下图所示,连接一个不存在IP地址:113.126.89.86

net_lwip_tcp_err_cb 662:adapter 1 socket 0 not closing, but error -13

这里有一个错误值:-13,表示:

ERR_ABRT = -13

  • 含义:连接被中止。
  • 场景:TCP 连接被本地或对端异常中止(如收到 RST 包,或本地主动调用tcp_abort)。

具体到本日志,因为对端ip不存在,所以应该是tcp三次握手过程中,超时,内核固件主动断开了连接;

在LuatOS内核固件中,使用的是LwIP协议栈,此处的错误值的完整的取值范围如下所述(大家在平时开发过程中,如果遇到异常,根据日志中的错误值,可以参考这部分说明自行简单分析下是什么原因):

1. ERR_OK = 0无错误,操作成功;

2. ERR_MEM = -1内存分配失败;

3. ERR_BUF = -2

  • 含义:缓冲区错误(不足或大小不匹配)。

  • 场景:发送数据时缓冲区空间不足(如pbuf大小不够存放待发送数据),或接收时缓冲区溢出。

4. ERR_TIMEOUT = -3

  • 含义:操作超时。

  • 场景:TCP 连接超时(未收到 SYN-ACK)、ARP 请求超时(未收到目标 MAC 地址应答)、netconn_recv等待数据超时等。

5. ERR_RTE = -4

  • 含义:路由错误(无可用路由)。

  • 场景:发送 IP 数据包时,lwip 路由表中找不到目标 IP 地址的有效路由(如未配置默认网关且无直连路由)。

6. ERR_INPROGRESS = -5

  • 含义:操作正在进行中。

  • 场景:非阻塞模式下的操作(如tcp_connect)尚未完成,需等待后续回调通知结果。

7. ERR_VAL = -6

  • 含义:无效值(参数值非法)。

  • 场景:传入 API 的参数值超出合法范围(如端口号为 0 或大于 65535,IP 地址格式错误等)。

8. ERR_WOULDBLOCK = -7

  • 含义:操作会阻塞(非阻塞模式下)。

  • 场景:非阻塞模式下调用netconn_recv时暂无数据可接收,或tcp_send时发送窗口未满导致无法立即发送。

9. ERR_USE = -8

  • 含义:地址已被使用。

  • 场景:绑定端口时(udp_bindtcp_bind),指定的端口已被其他连接占用。

10. ERR_ALREADY = -9

  • 含义:已在连接中。

  • 场景:对已处于连接过程中的 TCP 控制块再次调用tcp_connect

11. ERR_ISCONN = -10

  • 含义:连接已建立。

  • 场景:对已连接的 TCP 连接再次调用tcp_connect,或对已连接的netconn执行不需要连接的操作(如bind)。

12. ERR_CONN = -11

  • 含义:未连接状态。

  • 场景:对未建立连接的netconn调用send,或关闭未连接的连接。

13. ERR_IF = -12

  • 含义:底层网络接口(netif)错误。

  • 场景:网络接口未初始化、链路断开(如以太网物理层未连接),导致数据包无法发送。

14. ERR_ABRT = -13

  • 含义:连接被中止。

  • 场景:TCP 连接被本地或对端异常中止(如收到 RST 包,或本地主动调用tcp_abort)。

15. ERR_RST = -14

  • 含义:连接被重置。

  • 场景:TCP 连接收到对端发送的 RST(重置)报文,导致连接强制关闭。

16. ERR_CLSD = -15

  • 含义:连接已关闭。

  • 场景:对已正常关闭的连接执行读写操作(如tcp_send在连接关闭后调用)。

17. ERR_ARG = -16

  • 含义:非法参数(参数类型或指针无效)。

  • 场景:传入 API 的参数为NULL(如netconn_new传入无效的协议类型,tcp_send传入NULL缓冲区)。

18. ERR_IF_HIGH_WATER = -17

  • 含义:底层网络接口达到高水位线(缓冲区满)。

  • 场景:网络接口发送缓冲区已满,暂时无法接收新的发送请求(通常用于流量控制)。

19. ERR_IF_SUSPEND = -18

  • 含义:底层网络接口被挂起。

  • 场景:网络接口因某种原因(如手动暂停、错误恢复中)暂时不可用。

20. ERR_IF_OOS = -19

  • 含义:底层网络接口处于 “OutOfService”(服务中断)状态。

  • 场景:网络接口硬件故障、驱动错误等导致完全无法提供服务。

LuatOS模拟器上的网络交互包分析

8.3.3 tls握手连接失败

Luatools应用日志分析

在tls连接+仅支持单向认证的场景中,如果我们在客户端配置了错误的CA证书,本来应该是openluat_root_ca.crt文件中的内容,现在配置为了错误的baidu_parent_ca.crt文件,如下图所示:

在连接过程中,会有以下异常日志: network_state_shakehand 807:0x2700, 3

这里有一个错误值:0x2700,MBEDTLS_ERR_X509_CERT_VERIFY_FAILED,表示证书验证失败;

在LuatOS内核固件中,使用的是MbedTLS开源库,TLS握手连接过程中,MbedTLS中有以下常见的错误值(大家在平时开发过程中,如果遇到异常,根据日志中的错误值,可以参考这部分说明自行简单分析下是什么原因,如果这里没有覆盖出现的错误值,可以使用AI工具提问,例如错误值为0x2700,可以提问:tls握手连接过程中,0x2700表示什么错误):

1、基础加密 / 解密错误

  1. MBEDTLS_ERR_SSL_DECRYPTION_FAILED(0x7080):解密失败(如对称加密密钥错误、密文损坏)。
  2. MBEDTLS_ERR_SSL_BAD_HMAC(0x7082):HMAC 验证失败(消息完整性校验不通过,可能被篡改)。
  3. MBEDTLS_ERR_SSL_BAD_RECORD_MAC(0x7084):记录层 MAC 校验失败(与 HMAC 类似,针对 TLS 记录的完整性)。

2、协议版本与协商错误

  1. MBEDTLS_ERR_SSL_UNSUPPORTED_VERSION(0x7000):不支持对方的 TLS 版本(如客户端要求 TLS 1.0,服务器仅支持 TLS 1.2+)。
  2. MBEDTLS_ERR_SSL_VERSION_MISMATCH(0x7002):版本协商不匹配(如客户端和服务器协商的版本不一致)。

3、密码套件与算法错误

  1. MBEDTLS_ERR_SSL_NO_SHARED_CIPHER(0x7004):无共同支持的密码套件(客户端与服务器的密码套件列表无交集)。
  2. MBEDTLS_ERR_SSL_UNSUPPORTED_CIPHERSUITE(0x7006):对方选择的密码套件本地不支持。
  3. MBEDTLS_ERR_SSL_UNSUPPORTED_EXTENSION(0x7008):不支持对方发送的 TLS 扩展(如 ALPN、SNI 扩展不被认可)。

4、证书验证错误

  1. MBEDTLS_ERR_X509_CERT_VERIFY_FAILED(0x2700):证书验证失败(如签名无效、过期、吊销)。
  2. MBEDTLS_ERR_X509_UNKNOWN_CA(0x2702):证书链中存在未知 CA(根证书不被信任)。
  3. MBEDTLS_ERR_SSL_CERTIFICATE_REQUIRED(0x7040):服务器要求客户端证书,但客户端未提供。
  4. MBEDTLS_ERR_SSL_BAD_CERTIFICATE(0x7042):证书格式错误或内容无效(如解析失败、字段不合法)。

5、密钥交换与认证错误

  1. MBEDTLS_ERR_SSL_KEY_EXCHANGE_FAILED(0x7020):密钥交换过程失败(如 RSA 密钥解密失败、ECDH 密钥协商错误)。
  2. MBEDTLS_ERR_SSL_BAD_CLIENT_KEY_EXCHANGE(0x7022):客户端密钥交换消息格式错误或内容无效。
  3. MBEDTLS_ERR_SSL_BAD_CERTIFICATE_VERIFY(0x7044):客户端证书验证消息(CertificateVerify)无效(如签名不匹配)。

6、握手流程与消息错误

  1. MBEDTLS_ERR_SSL_UNEXPECTED_MESSAGE(0x7060):收到意外的握手消息(如流程顺序错误,如先收到 Finished 再收到 Certificate)。
  2. MBEDTLS_ERR_SSL_INVALID_HANDSHAKE_MESSAGE(0x7062):握手消息格式无效(如长度错误、字段缺失)。
  3. MBEDTLS_ERR_SSL_HANDSHAKE_FAILURE(0x7064):握手失败(通用错误,可能因上述多种原因导致,如服务器拒绝客户端配置)。
  4. MBEDTLS_ERR_SSL_HANDSHAKE_TIMEOUT(0x7066):握手超时(未在规定时间内收到对方响应)。

7、连接与状态错误

  1. MBEDTLS_ERR_SSL_CONN_EOF(0x70a0):握手过程中连接被关闭(对方发送了关闭通知)。
  2. MBEDTLS_ERR_SSL_CONNECTION_RESET(0x70a2):连接被重置(如底层 TCP 连接断开)。
  3. MBEDTLS_ERR_SSL_WANT_READ / MBEDTLS_ERR_SSL_WANT_WRITE(0x70c0 / 0x70c2):非阻塞模式下需要继续读写数据(非错误,需重试)。

LuatOS模拟器上的网络交互包分析

8.3.4 mqtt connect失败

Luatools应用日志分析

tcp连接建立成功之后,mqtt client首先会发送一个MQTT CONNECT报文,如果mqtt broker服务器收到后,校验CONNECT参数有误,会返回一个MQTT CONNACK报文,报文中有错误码;

如下图所示,连接一个mqtts服务器,要求mqtt客户端提供正确的用户名和密码,代码中故意将用户名和密码配置错误:

在连接过程中,会有以下异常日志:CONACK 0x04

这里有一个错误码:0x04,表示连接被拒绝(用户名 / 密码错误);

在LuatOS内核固件中,使用的MQTT协议是3.1.1版本,

MQTT 3.1.1 定义了 6 种连接失败的返回码:

返回码值 含义说明 常见场景
0x00 连接成功 客户端连接参数合法,认证通过
0x01 连接被拒绝(不支持的协议版本) 客户端使用的 MQTT 版本(如 3.1)与 Broker 支持的版本(如 3.1.1/5.0)不兼容
0x02 连接被拒绝(客户端 ID 无效) 客户端 ID 为空(且 Broker 不允许)、包含非法字符,或与已连接客户端的 ID 冲突
0x03 连接被拒绝(Broker 不可用) Broker 处于维护状态、资源耗尽(如无法处理新连接),或明确拒绝连接
0x04 连接被拒绝(用户名 / 密码错误) 客户端提供的用户名 / 密码不正确,或 Broker 要求认证但客户端未提供
0x05 连接被拒绝(未授权) 客户端的用户名 / 密码通过验证,但无连接权限(如 Broker 配置了 IP 限制、用户黑名单)

LuatOS模拟器上的网络交互包分析

由于是MQTTS服务器,有加密安全限制,所以通过wireshark分析此问题,并不是很直观,但是根据应用报文的顺序,也能大概分析出来是哪一步出错了,如下图所示,mqtt前两条应用报文分别是MQTT CONNECT和CONNACK,就可以初步判断是CONNACK返回了失败

8.4 mqtt收发数据和断开

这些业务逻辑的异常情况,就不再一一分析了,大家遇到问题后,可以参考上面几种异常的分析方法,自行分析;