一、访问华为云IOT服务器创建一个产品和设备
1.1 开通物联网服务
地址: https://www.huaweicloud.com/product/iothub.html

现在可以免费创建的,按需付费。 只要是不超过规格,就可以免费使用,对于个人项目Demo来说,完全够用的
点击立即创建之后,会开始创建实例,需要等待片刻,再刷新浏览器就可以看到创建成功了。


创建完成,点击实例,即可进入实例详情页面。

进入实例详情页面后,可以看到接入信息的描述,我们当前设备准备采用MQTT协议接入华为云平台,这里可以看到MQTT协议的地址和端口号等信息。

总结:
bash
端口号:MQTTS(8883) MQTT(1883)
接入地址:b89ae024d0.st1.iotda-device.cn-east-3.myhuaweicloud.com
**根据域名地址得到IP地址信息: ** 直接PING接入地址即可

MQTT协议接入端口号有两个,1883是非加密端口,8883是证书加密端口,单片机无法加载证书,所以使用1883端口比较合适。 接下来的ESP8266就采用1883端口连接华为云物联网平台。
1.2 创建产品
(1)创建产品
点击产品页,再点击左上角创建产品。

(2)填写产品信息
根据自己产品名字填写,下面的设备类型选择自定义类型。

(3)产品创建成功
创建成功之后,点击产品的名字就可以进入到产品的详情页。

(4)添加自定义模型
产品创建完成之后,点击进入产品详情页面,翻到最下面可以看到模型定义。

模型简单来说: 就是存放 设备上传到云平台的数据。
先点击自定义模型。

再创建一个服务ID。

接着点击新增属性。

这里就创建一个温度的属性。我们这个设备用来测温的。


1.3 添加设备
产品是属于上层的抽象模型,接下来在产品模型下添加实际的设备。添加的设备最终需要与真实的设备关联在一起,完成数据交互。
(1)注册设备

(2)根据自己的设备填写

(3)保存设备信息
创建完毕之后,点击保存并关闭,得到创建的设备密匙信息。该信息在后续生成MQTT三元组的时候需要使用。

1.4 MQTT协议主题订阅与发布
(1)华为云平台MQTT协议使用限制
描述限制支持的MQTT协议版本3.1.1与标准MQTT协议的区别支持Qos 0和Qos 1支持Topic自定义不支持QoS2不支持will、retain msgMQTTS支持的安全等级采用TCP通道基础 + TLS协议(最高TLSv1.3版本)单帐号每秒最大MQTT连接请求数无限制单个设备每分钟支持的最大MQTT连接数1单个MQTT连接每秒的吞吐量,即带宽,包含直连设备和网关3KB/sMQTT单个发布消息最大长度,超过此大小的发布请求将被直接拒绝1MBMQTT连接心跳时间建议值心跳时间限定为30至1200秒,推荐设置为120秒产品是否支持自定义Topic支持消息发布与订阅设备只能对自己的Topic进行消息发布与订阅每个订阅请求的最大订阅数无限制。
(2)主题订阅格式
帮助文档地址:https://support.huaweicloud.com/devg-iothub/iot_02_2200.html
对于设备而言,一般会订阅平台下发消息给设备 这个主题。
设备想接收平台下发的消息,就需要订阅平台下发消息给设备 的主题,订阅后,平台下发消息给设备,设备就会收到消息。
如果设备想要知道平台下发的消息,需要订阅上面图片里标注的主题。
以当前设备为例,最终订阅主题的格式如下: $oc/devices/{device_id}/sys/messages/down
(3)主题发布格式
对于设备来说,主题发布表示向云平台上传数据,将最新的传感器数据,设备状态上传到云平台。
这个操作称为:属性上报。
帮助文档地址:https://support.huaweicloud.com/usermanual-iothub/iot_06_v5_3010.html
根据帮助文档的介绍, 当前设备发布主题,上报属性的格式总结如下:
发布的主题格式: $oc/devices/{device_id}/sys/properties/report
发布主题时,需要上传数据,这个数据格式是JSON格式。 上传的JSON数据格式如下: { "services": [ { "service_id": <填服务ID>, "properties": { "<填属性名称1>": <填属性值>, "<填属性名称2>": <填属性值>, .......... } } ] } 根据JSON格式,一次可以上传多个属性字段。 这个JSON格式里的,服务ID,属性字段名称,属性值类型,在前面创建产品的时候就已经介绍了,不记得可以翻到前面去查看。 根据这个格式,组合一次上传的属性数据: {"services": [{"service_id": "stm32","properties":{"TEMP":36.2}}]}
3.6 MQTT三元组
MQTT协议登录需要填用户ID,设备ID,设备密码等信息,就像我们平时登录QQ,微信一样要输入账号密码才能登录。MQTT协议登录的这3个参数,一般称为MQTT三元组。
接下来介绍,华为云平台的MQTT三元组参数如何得到。
(1)MQTT服务器地址
要登录MQTT服务器,首先记得先知道服务器的地址是多少,端口是多少。
帮助文档地址:https://console.huaweicloud.com/iotdm/?region=cn-north-4#/dm-portal/home
MQTT协议的端口支持1883和8883,它们的区别是:8883 是加密端口更加安全。但是单片机上使用比较困难,所以当前的设备是采用1883端口进连接的。
根据上面的域名和端口号,得到下面的IP地址和端口号信息: 如果设备支持填写域名可以直接填域名,不支持就直接填写IP地址。 (IP地址就是域名解析得到的)
华为云的MQTT服务器地址:124.70.218.131 华为云的MQTT端口号:1883
(2)生成MQTT三元组
华为云提供了一个在线工具,用来生成MQTT鉴权三元组: https://iot-tool.obs-website.cn-north-4.myhuaweicloud.com/
打开这个工具,填入设备的信息(也就是刚才创建完设备之后保存的信息),点击生成,就可以得到MQTT的登录信息了。
下面是打开的页面:

填入设备的信息: (上面两行就是设备创建完成之后保存得到的)
直接得到三元组信息。

得到三元组之后,设备端通过MQTT协议登录鉴权的时候,填入参数即可。
bash
ClientId 68e111e854ed1814a9b023f2_stm32_0_1_2025100417
Username 68e111e854ed1814a9b023f2_stm32
Password 84cb13d8da01d233eb093ded7e453dd2522c79a5a45a73d76ef1b81ef3edaeac
到此,云平台的部署已经完成,设备已经可以正常上传数据了。(注意:ClientId和Password是实时更新变化的,每次链接需要重新获取)
(3)MQTT登录测试参数总结
bash
IP地址:124.70.218.131 端口号:1883
ClientId 68e111e854ed1814a9b023f2_stm32_0_1_2025100417
Username 68e111e854ed1814a9b023f2_stm32
Password 84cb13d8da01d233eb093ded7e453dd2522c79a5a45a73d76ef1b81ef3edaeac
订阅主题:$oc/devices/68e111e854ed1814a9b023f2_stm32/sys/messages/down
发布主题:$oc/devices/68e111e854ed1814a9b023f2_stm32/sys/properties/report
发布数据:{"services": [{"service_id": "stm32","properties":{"TEMP":36.2}}]}
二、学习MQTT协议
2.1 MQTT简介
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅模式的"轻量级"的消息协议,可在发布者和订阅者之间传递消息。MQTT协议构建于TCP/IP协议上,由IBM在1999年发布,当前已经成为了一种主流的物联网通信协议。
MQTT最大的优点在于,能够以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。它是一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。由于其小巧、高效和可靠的特点,MQTT在物联网领域得到了广泛的应用。在很多情况下,包括受限的环境中,如:机器与机器(M2M)通信和物联网(IoT),且已经广泛应用于通过卫星链路通信传感器、偶尔拨号的医疗设备、智能家居、及一些小型化设备中。
MQTT协议的工作原理是基于发布/订阅模式。在这种模式下,发布者可以向一个或多个主题发布消息,而订阅者可以订阅这些主题以接收相关消息。这种模式允许多个发布者和订阅者同时存在,实现了一种灵活的消息传递机制。此外,MQTT协议还支持三种消息传递质量等级,可根据需要进行选择。
MQTT协议的另一个重要特点是其轻量级和简单的设计。它的消息头非常小,只有2个字节,这意味着在网络带宽有限的环境下也能够实现高效的消息传递。此外,MQTT协议还支持持久化连接和消息队列等高级功能,可进一步提高消息的可靠性和传递效率。
MQTT协议的应用范围非常广泛。例如,在智能家居领域,可以使用MQTT协议将各种智能设备连接在一起,实现设备的远程控制和监测。在工业领域,MQTT协议可以用于实现设备的远程监控和维护,提高生产效率和产品质量。在智慧城市建设中,MQTT协议可以用于交通管理、环境监测和公共安全等方面,提升城市管理和居民生活的质量。
2.2 MQTT协议官网介绍
目前MQTT协议主要是3.1.1 和 5.0 两个版本。 本篇文章是介绍3.1.1版本的MQTT协议。 各大标准的MQTT服务器都支持3.1.1.
链接:https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html
在文档的下方就是介绍MQTT协议的每个包如何封装的。照着协议写代码就行了。
2.3 需要实现的3个函数
整个MQTT协议里,主要是实现3个函数就行了(其他的接口看自己需求)。
下面列出的3个函数,在一般的MQTT通信里是必备的。我们只要实现了这3个函数,那么完成基本的MQTT通信就没有问题了。
cpp
//发布主题
unsigned char MQTT_PublishData(char* topic, char* message, unsigned char qos);
//订阅或者取消订阅主题
unsigned char MQTT_SubscribeTopic(char* topic, unsigned char qos, unsigned char whether); //登录MQTT服务器
unsigned char MQTT_Connect(char* ClientID, char* Username, char* Password);
2.4 查看协议文档,了解如何组合协议报文。
2.4.1 MQTT协议与服务器和客户的具体关系(订阅)

2.4.2 MQTT协议的组成

固定报头:


可变报头:

2.4.3 MQTT协议的请求连接

2.4.4 MQTT协议的具体连接实现

三、运行项目、连接华为云服务器
3.1 整个项目的完整代码
这里就贴出我编写好的整体的代码,进行运行测试(文件名称:mqtt_client.c):

cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <pthread.h>
#include <errno.h>
#include <signal.h>
#include <netinet/tcp.h>
// Linux Socket头文件
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
// 跨平台定义
#define Sleep(x) usleep((x) * 1000) // 将毫秒转换为微秒的休眠函数
#define SOCKET int // 定义SOCKET类型为int
#define INVALID_SOCKET -1 // 定义无效socket值
#define SOCKET_ERROR -1 // 定义socket错误值
// 字节操作宏
#define BYTE0(dwTemp) ((dwTemp) & 0xFF) // 获取低字节
#define BYTE1(dwTemp) (((dwTemp) >> 8) & 0xFF) // 获取高字节
//---------------------------------------MQTT协议相关的子函数声明-------------------------------------------------------
unsigned char MQTT_PublishData(char *topic, char *message, unsigned char qos); // MQTT发布数据函数
unsigned char MQTT_SubscribeTopic(char *topic, unsigned char qos, unsigned char whether); // MQTT订阅/取消订阅函数
unsigned char MQTT_Connect(char *ClientID, char *Username, char *Password); // MQTT连接函数
void MQTT_Init(void); // MQTT初始化函数
void MQTT_SendBuf(unsigned char *buf, unsigned short len); // MQTT发送缓冲区函数
int Client_SendData(unsigned char *buff, unsigned int len); // 客户端发送数据函数
int Client_GetData(unsigned char *buff); // 客户端接收数据函数
int set_socket_timeout(SOCKET sock, int recv_timeout_sec, int send_timeout_sec); // 设置socket超时函数
void send_mqtt_ping(void); // 发送MQTT心跳函数
//---------------------------------------全局变量定义--------------------------------------------------------------------
unsigned char mqtt_rxbuf[1024]; // MQTT接收缓冲区
unsigned char mqtt_txbuf[1024]; // MQTT发送缓冲区
unsigned int mqtt_rxlen; // MQTT接收数据长度
unsigned int mqtt_txlen; // MQTT发送数据长度
// MQTT消息类型枚举
typedef enum
{
M_RESERVED1 = 0, // 保留类型1
M_CONNECT, // 连接请求
M_CONNACK, // 连接确认
M_PUBLISH, // 发布消息
M_PUBACK, // 发布确认
M_PUBREC, // 发布收到(QoS2)
M_PUBREL, // 发布释放(QoS2)
M_PUBCOMP, // 发布完成(QoS2)
M_SUBSCRIBE, // 订阅请求
M_SUBACK, // 订阅确认
M_UNSUBSCRIBE, // 取消订阅
M_UNSUBACK, // 取消订阅确认
M_PINGREQ, // 心跳请求
M_PINGRESP, // 心跳响应
M_DISCONNECT, // 断开连接
M_RESERVED2, // 保留类型2
} mqtt_message_type;
//-----------------------------------------MQTT服务器的参数------------------------------------------------------------
#define SERVER_IP "124.70.218.131" // MQTT服务器IP地址
#define SERVER_PORT 1883 // MQTT服务器端口号
// MQTT三元组
#define CLIENT_ID "68e111e854ed1814a9b023f2_stm32_0_1_2025100417" // 客户端ID
#define USER_NAME "68e111e854ed1814a9b023f2_stm32" // 用户名
#define PASSWORD "84cb13d8da01d233eb093ded7e453dd2522c79a5a45a73d76ef1b81ef3edaeac" // 密码
// 订阅主题:
#define SET_TOPIC "$oc/devices/68e111e854ed1814a9b023f2_stm32/sys/messages/down" // 下行消息主题
// 发布主题:
#define POST_TOPIC "$oc/devices/68e111e854ed1814a9b023f2_stm32/sys/properties/report" // 属性上报主题
//-----------------------------------------全局变量------------------------------------------------------------
char mqtt_message[1024]; // MQTT消息缓冲区
SOCKET connectSocket; // 连接socket
double TEMP = 10.0; // 温度值,初始为10.0
volatile int connected = 0; // 连接状态标志,volatile确保多线程可见性
time_t last_ping_time = 0; // 上次心跳时间
//-----------------------------------------工具函数------------------------------------------------------------
// 设置socket超时函数
int set_socket_timeout(SOCKET sock, int recv_timeout_sec, int send_timeout_sec)
{
struct timeval timeout; // 时间结构体
// 设置接收超时
timeout.tv_sec = recv_timeout_sec; // 秒数
timeout.tv_usec = 0; // 微秒数
if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) // 设置接收超时选项
{
printf("设置接收超时失败: %s\n", strerror(errno)); // 打印错误信息
return -1; // 返回错误
}
// 设置发送超时
timeout.tv_sec = send_timeout_sec; // 秒数
timeout.tv_usec = 0; // 微秒数
if (setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) < 0) // 设置发送超时选项
{
printf("设置发送超时失败: %s\n", strerror(errno)); // 打印错误信息
return -1; // 返回错误
}
return 0; // 返回成功
}
// 检查socket是否仍然连接
int is_socket_connected(SOCKET sock)
{
int error = 0; // 错误码
socklen_t len = sizeof(error); // 错误码长度
if (getsockopt(sock, SOL_SOCKET, SO_ERROR, &error, &len) < 0) // 获取socket错误状态
{
return 0; // 获取失败返回未连接
}
return (error == 0); // 错误码为0表示连接正常
}
// 发送MQTT心跳
void send_mqtt_ping(void)
{
if (!connected) // 如果未连接则直接返回
return;
unsigned char ping_packet[2] = {0xC0, 0x00}; // PINGREQ报文,固定格式
printf("发送心跳请求...\n"); // 打印日志
MQTT_SendBuf(ping_packet, sizeof(ping_packet)); // 发送心跳包
last_ping_time = time(NULL); // 更新最后心跳时间
}
//-----------------------------------------MQTT函数实现------------------------------------------------------------
// MQTT初始化函数
void MQTT_Init(void)
{
mqtt_rxlen = sizeof(mqtt_rxbuf); // 设置接收缓冲区长度
mqtt_txlen = sizeof(mqtt_txbuf); // 设置发送缓冲区长度
memset(mqtt_rxbuf, 0, mqtt_rxlen); // 清空接收缓冲区
memset(mqtt_txbuf, 0, mqtt_txlen); // 清空发送缓冲区
}
/*
函数功能: 登录服务器
函数返回值: 0表示成功 1表示失败
*/
unsigned char MQTT_Connect(char *ClientID, char *Username, char *Password)
{
unsigned short i, j; // 循环变量
int ClientIDLen = (int)strlen(ClientID); // 客户端ID长度
int UsernameLen = (int)strlen(Username); // 用户名长度
int PasswordLen = (int)strlen(Password); // 密码长度
unsigned int remaining_length; // 剩余长度
unsigned int temp_len; // 临时长度变量
mqtt_txlen = 0; // 发送长度清零
unsigned int size = 0; // 接收数据大小
unsigned char buff[256]; // 接收缓冲区
printf("尝试连接MQTT服务器...\n"); // 打印连接信息
printf("Server: %s:%d\n", SERVER_IP, SERVER_PORT); // 打印服务器地址和端口
printf("ClientID: %s (长度: %d)\n", ClientID, ClientIDLen); // 打印客户端ID信息
printf("Username: %s (长度: %d)\n", Username, UsernameLen); // 打印用户名信息
printf("Password: %s (长度: %d)\n", Password, PasswordLen); // 打印密码信息
// 设置socket超时
set_socket_timeout(connectSocket, 10, 10); // 10秒超时
// 计算剩余长度
remaining_length = 10; // 可变报头固定长度
remaining_length += 2 + ClientIDLen; // 加上客户端ID长度
if (UsernameLen > 0) // 如果有用户名
{
remaining_length += 2 + UsernameLen; // 加上用户名长度
}
if (PasswordLen > 0) // 如果有密码
{
remaining_length += 2 + PasswordLen; // 加上密码长度
}
printf("计算剩余长度: %d\n", remaining_length); // 打印剩余长度
// 固定报头
mqtt_txbuf[mqtt_txlen++] = 0x10; // CONNECT报文,固定头
// 编码剩余长度
temp_len = remaining_length; // 临时变量存储剩余长度
do
{
unsigned char encodedByte = temp_len % 128; // 取低7位
temp_len = temp_len / 128; // 右移7位
if (temp_len > 0) // 如果还有数据
{
encodedByte = encodedByte | 128; // 设置最高位为1
}
mqtt_txbuf[mqtt_txlen++] = encodedByte; // 存储编码后的字节
} while (temp_len > 0); // 循环直到所有数据编码完成
// 可变报头 - MQTT 3.1.1
mqtt_txbuf[mqtt_txlen++] = 0x00; // Protocol Name Length MSB // 协议名长度高字节
mqtt_txbuf[mqtt_txlen++] = 0x04; // Protocol Name Length LSB // 协议名长度低字节
mqtt_txbuf[mqtt_txlen++] = 'M'; // M // 协议名字符
mqtt_txbuf[mqtt_txlen++] = 'Q'; // Q // 协议名字符
mqtt_txbuf[mqtt_txlen++] = 'T'; // T // 协议名字符
mqtt_txbuf[mqtt_txlen++] = 'T'; // T // 协议名字符
mqtt_txbuf[mqtt_txlen++] = 0x04; // Protocol Level = 4 (MQTT 3.1.1) // 协议级别
// 连接标志
unsigned char connect_flags = 0xC2; // Clean Session=1, UserName=1, Password=1 // 连接标志位
mqtt_txbuf[mqtt_txlen++] = connect_flags; // 存储连接标志
// 保持连接时间 (60秒)
mqtt_txbuf[mqtt_txlen++] = 0x00; // Keep Alive MSB // 保持连接时间高字节
mqtt_txbuf[mqtt_txlen++] = 0x3C; // Keep Alive LSB (60秒) // 保持连接时间低字节
// 有效载荷 - ClientID
mqtt_txbuf[mqtt_txlen++] = BYTE1(ClientIDLen); // 客户端ID长度高字节
mqtt_txbuf[mqtt_txlen++] = BYTE0(ClientIDLen); // 客户端ID长度低字节
memcpy(&mqtt_txbuf[mqtt_txlen], ClientID, ClientIDLen); // 拷贝客户端ID
mqtt_txlen += ClientIDLen; // 更新发送长度
// 用户名
mqtt_txbuf[mqtt_txlen++] = BYTE1(UsernameLen); // 用户名长度高字节
mqtt_txbuf[mqtt_txlen++] = BYTE0(UsernameLen); // 用户名长度低字节
memcpy(&mqtt_txbuf[mqtt_txlen], Username, UsernameLen); // 拷贝用户名
mqtt_txlen += UsernameLen; // 更新发送长度
// 密码
mqtt_txbuf[mqtt_txlen++] = BYTE1(PasswordLen); // 密码长度高字节
mqtt_txbuf[mqtt_txlen++] = BYTE0(PasswordLen); // 密码长度低字节
memcpy(&mqtt_txbuf[mqtt_txlen], Password, PasswordLen); // 拷贝密码
mqtt_txlen += PasswordLen; // 更新发送长度
printf("CONNECT报文构造完成,总长度: %d\n", mqtt_txlen); // 打印报文长度
// 打印发送的数据包用于调试
printf("发送的数据包(前32字节): "); // 打印调试信息
for (i = 0; i < (mqtt_txlen < 32 ? mqtt_txlen : 32); i++) // 循环打印前32字节
{
printf("%02X ", mqtt_txbuf[i]); // 打印十六进制数据
}
printf("\n"); // 换行
for (i = 0; i < 3; i++) // 重试3次
{
printf("连接尝试 %d/3\n", i + 1); // 打印重试次数
memset(mqtt_rxbuf, 0, sizeof(mqtt_rxbuf)); // 清空接收缓冲区
// 发送连接请求
MQTT_SendBuf(mqtt_txbuf, mqtt_txlen); // 发送连接报文
printf("CONNECT报文已发送,等待响应...\n"); // 打印发送完成信息
// 接收响应
size = Client_GetData(buff); // 接收服务器响应
if (size <= 0) // 如果接收失败或超时
{
if (size == 0) // 如果是超时
{
printf("连接超时,未收到响应\n"); // 打印超时信息
}
else // 如果是错误
{
printf("接收错误: %s\n", strerror(errno)); // 打印错误信息
}
Sleep(2000); // 等待2秒
continue; // 继续重试
}
memcpy(mqtt_rxbuf, buff, size); // 拷贝接收数据到接收缓冲区
printf("收到响应(%d字节): ", size); // 打印响应信息
for (j = 0; j < size; j++) // 循环打印响应数据
{
printf("%02X ", buff[j]); // 打印十六进制数据
}
printf("\n"); // 换行
// 解析CONNACK
if (size >= 4) // 如果响应数据足够长
{
unsigned char packet_type = buff[0]; // 包类型
unsigned char remaining_len = buff[1]; // 剩余长度
unsigned char connack_flags = buff[2]; // 连接确认标志
unsigned char return_code = buff[3]; // 返回码
printf("CONNACK解析 - 包类型: 0x%02X, 剩余长度: %d, 标志: 0x%02X, 返回码: 0x%02X\n",
packet_type, remaining_len, connack_flags, return_code); // 打印解析结果
if (packet_type == 0x20 && remaining_len == 0x02) // 如果是CONNACK报文且长度正确
{
switch (return_code) // 根据返回码处理
{
case 0x00: // 连接成功
printf("MQTT连接成功!\n"); // 打印成功信息
connected = 1; // 设置连接状态
// 设置正常通信的超时时间
set_socket_timeout(connectSocket, 5, 5); // 设置5秒超时
last_ping_time = time(NULL); // 记录心跳时间
return 0; // 返回成功
case 0x01: // 不支持的协议版本
printf("错误: 不支持的协议版本\n"); // 打印错误信息
break;
case 0x02: // 客户端标识符不合格
printf("错误: 客户端标识符不合格\n"); // 打印错误信息
break;
case 0x03: // 服务器不可用
printf("错误: 服务器不可用\n"); // 打印错误信息
break;
case 0x04: // 用户名或密码错误
printf("错误: 用户名或密码错误\n"); // 打印错误信息
return 1; // 返回失败
case 0x05: // 未授权
printf("错误: 未授权\n"); // 打印错误信息
return 1; // 返回失败
default: // 未知错误
printf("错误: 未知返回码 0x%02X\n", return_code); // 打印错误信息
break;
}
}
else // 不是有效的CONNACK响应
{
printf("不是有效的CONNACK响应\n"); // 打印错误信息
}
}
else // 响应数据长度不足
{
printf("响应数据长度不足,期望至少4字节\n"); // 打印错误信息
}
Sleep(2000); // 等待2秒
}
printf("连接失败,已达到最大重试次数\n"); // 打印最终失败信息
return 1; // 返回失败
}
/*
函数功能: MQTT订阅/取消订阅数据打包函数
*/
unsigned char MQTT_SubscribeTopic(char *topic, unsigned char qos, unsigned char whether)
{
if (!connected) // 如果未连接
{
printf("未连接,无法订阅\n"); // 打印错误信息
return 1; // 返回失败
}
static unsigned short packet_id = 1; // 包ID,静态变量保持连续性
unsigned char i, j; // 循环变量
mqtt_txlen = 0; // 发送长度清零
unsigned int size = 0; // 接收数据大小
unsigned char buff[256]; // 接收缓冲区
unsigned int topiclen = (int)strlen(topic); // 主题长度
unsigned int remaining_length; // 剩余长度
unsigned int temp_len; // 临时长度变量
remaining_length = 2 + (2 + topiclen); // 计算基本剩余长度
if (whether) // 如果是订阅操作
{
remaining_length += 1; // 加上QoS字节
}
if (whether) // 如果是订阅
mqtt_txbuf[mqtt_txlen++] = 0x82; // SUBSCRIBE // 订阅报文类型
else // 如果是取消订阅
mqtt_txbuf[mqtt_txlen++] = 0xA2; // UNSUBSCRIBE // 取消订阅报文类型
temp_len = remaining_length; // 临时变量存储剩余长度
do
{
unsigned char encodedByte = temp_len % 128; // 取低7位
temp_len = temp_len / 128; // 右移7位
if (temp_len > 0) // 如果还有数据
encodedByte = encodedByte | 128; // 设置最高位为1
mqtt_txbuf[mqtt_txlen++] = encodedByte; // 存储编码后的字节
} while (temp_len > 0); // 循环直到所有数据编码完成
mqtt_txbuf[mqtt_txlen++] = BYTE1(packet_id); // 包ID高字节
mqtt_txbuf[mqtt_txlen++] = BYTE0(packet_id); // 包ID低字节
mqtt_txbuf[mqtt_txlen++] = BYTE1(topiclen); // 主题长度高字节
mqtt_txbuf[mqtt_txlen++] = BYTE0(topiclen); // 主题长度低字节
memcpy(&mqtt_txbuf[mqtt_txlen], topic, topiclen); // 拷贝主题
mqtt_txlen += topiclen; // 更新发送长度
if (whether) // 如果是订阅操作
{
mqtt_txbuf[mqtt_txlen++] = qos; // 添加QoS级别
}
for (i = 0; i < 3; i++) // 重试3次
{
memset(mqtt_rxbuf, 0, mqtt_rxlen); // 清空接收缓冲区
MQTT_SendBuf(mqtt_txbuf, mqtt_txlen); // 发送订阅/取消订阅报文
printf("%s请求已发送, Packet ID: %d\n", whether ? "订阅" : "取消订阅", packet_id); // 打印发送信息
size = Client_GetData(buff); // 接收响应
if (size <= 0) // 如果接收失败或超时
{
printf("未收到响应,重试 %d/3\n", i + 1); // 打印重试信息
Sleep(1000); // 等待1秒
continue; // 继续重试
}
memcpy(mqtt_rxbuf, buff, size); // 拷贝接收数据到接收缓冲区
printf("%s应答: ", whether ? "订阅" : "取消订阅"); // 打印响应信息
for (j = 0; j < size; j++) // 循环打印响应数据
{
printf("%02X ", buff[j]); // 打印十六进制数据
}
printf("\n"); // 换行
if (whether) // 如果是订阅操作
{
if (size >= 4 && // 响应数据足够长
mqtt_rxbuf[0] == 0x90 && // 是SUBACK报文
mqtt_rxbuf[2] == BYTE1(packet_id) && // 包ID高字节匹配
mqtt_rxbuf[3] == BYTE0(packet_id)) // 包ID低字节匹配
{
if (size >= 5) // 如果有返回码
{
unsigned char return_code = mqtt_rxbuf[4]; // 获取返回码
if (return_code <= 0x02) // 返回码有效
{
printf("订阅成功, 返回码: 0x%02X\n", return_code); // 打印成功信息
packet_id++; // 包ID递增
if (packet_id == 0) // 如果包ID溢出
packet_id = 1; // 重置为1
return 0; // 返回成功
}
}
else // 没有返回码
{
printf("订阅成功\n"); // 打印成功信息
packet_id++; // 包ID递增
if (packet_id == 0) // 如果包ID溢出
packet_id = 1; // 重置为1
return 0; // 返回成功
}
}
}
else // 如果是取消订阅操作
{
if (size >= 4 && // 响应数据足够长
mqtt_rxbuf[0] == 0xB0 && // 是UNSUBACK报文
mqtt_rxbuf[2] == BYTE1(packet_id) && // 包ID高字节匹配
mqtt_rxbuf[3] == BYTE0(packet_id)) // 包ID低字节匹配
{
printf("取消订阅成功\n"); // 打印成功信息
packet_id++; // 包ID递增
if (packet_id == 0) // 如果包ID溢出
packet_id = 1; // 重置为1
return 0; // 返回成功
}
}
printf("响应不匹配,重试 %d/3\n", i + 1); // 打印重试信息
Sleep(1000); // 等待1秒
}
printf("%s失败\n", whether ? "订阅" : "取消订阅"); // 打印最终失败信息
return 1; // 返回失败
}
// MQTT发布数据打包函数
unsigned char MQTT_PublishData(char *topic, char *message, unsigned char qos)
{
if (!connected) // 如果未连接
{
printf("未连接,无法发布消息\n"); // 打印错误信息
return 0; // 返回失败
}
static unsigned short packet_id = 1; // 包ID,静态变量保持连续性
unsigned int topicLength = (int)strlen(topic); // 主题长度
unsigned int messageLength = (int)strlen(message); // 消息长度
unsigned int remaining_length; // 剩余长度
unsigned int temp_len; // 临时长度变量
mqtt_txlen = 0; // 发送长度清零
printf("上报JSON消息长度:%d\n", messageLength); // 打印消息长度
printf("message=%s\n", message); // 打印消息内容
remaining_length = 2 + topicLength + messageLength; // 计算基本剩余长度
if (qos > 0) // 如果QoS大于0
{
remaining_length += 2; // 加上包ID长度
}
if (qos == 0) // QoS 0
{
mqtt_txbuf[mqtt_txlen++] = 0x30; // PUBLISH, QoS=0 // 发布报文,QoS=0
}
else if (qos == 1) // QoS 1
{
mqtt_txbuf[mqtt_txlen++] = 0x32; // PUBLISH, QoS=1 // 发布报文,QoS=1
}
else // QoS 2
{
mqtt_txbuf[mqtt_txlen++] = 0x34; // PUBLISH, QoS=2 // 发布报文,QoS=2
}
temp_len = remaining_length; // 临时变量存储剩余长度
do
{
unsigned char encodedByte = temp_len % 128; // 取低7位
temp_len = temp_len / 128; // 右移7位
if (temp_len > 0) // 如果还有数据
{
encodedByte = encodedByte | 128; // 设置最高位为1
}
mqtt_txbuf[mqtt_txlen++] = encodedByte; // 存储编码后的字节
} while (temp_len > 0); // 循环直到所有数据编码完成
mqtt_txbuf[mqtt_txlen++] = BYTE1(topicLength); // 主题长度高字节
mqtt_txbuf[mqtt_txlen++] = BYTE0(topicLength); // 主题长度低字节
memcpy(&mqtt_txbuf[mqtt_txlen], topic, topicLength); // 拷贝主题
mqtt_txlen += topicLength; // 更新发送长度
if (qos > 0) // 如果QoS大于0
{
mqtt_txbuf[mqtt_txlen++] = BYTE1(packet_id); // 包ID高字节
mqtt_txbuf[mqtt_txlen++] = BYTE0(packet_id); // 包ID低字节
packet_id++; // 包ID递增
if (packet_id == 0) // 如果包ID溢出
packet_id = 1; // 重置为1
}
memcpy(&mqtt_txbuf[mqtt_txlen], message, messageLength); // 拷贝消息内容
mqtt_txlen += messageLength; // 更新发送长度
printf("发布数据包长度: %d\n", mqtt_txlen); // 打印数据包长度
MQTT_SendBuf(mqtt_txbuf, mqtt_txlen); // 发送发布报文
return mqtt_txlen; // 返回发送长度
}
// MQTT发送缓冲区函数
void MQTT_SendBuf(unsigned char *buf, unsigned short len)
{
Client_SendData(buf, len); // 调用客户端发送数据函数
}
//-----------------------------------------网络函数实现------------------------------------------------------------
// 客户端发送数据函数
int Client_SendData(unsigned char *buff, unsigned int len)
{
int result = send(connectSocket, buff, len, 0); // 发送数据
if (result == SOCKET_ERROR) // 如果发送失败
{
printf("发送失败: %s\n", strerror(errno)); // 打印错误信息
return -1; // 返回错误
}
printf("发送 %d 字节数据成功\n", result); // 打印成功信息
return 0; // 返回成功
}
// 客户端接收数据函数
int Client_GetData(unsigned char *buff)
{
// 使用select检查是否有数据可读
fd_set read_fds; // 读文件描述符集合
struct timeval timeout; // 超时结构体
FD_ZERO(&read_fds); // 清空文件描述符集合
FD_SET(connectSocket, &read_fds); // 添加socket到集合
timeout.tv_sec = 3; // 3秒超时
timeout.tv_usec = 0; // 0微秒
int select_result = select(connectSocket + 1, &read_fds, NULL, NULL, &timeout); // 等待数据可读
if (select_result == 0) // 超时
{
return 0; // 超时,没有数据
}
else if (select_result < 0) // 错误
{
printf("select错误: %s\n", strerror(errno)); // 打印错误信息
return -1; // 返回错误
}
// 有数据可读
int result = recv(connectSocket, buff, 200, 0); // 接收数据
if (result == SOCKET_ERROR) // 如果接收失败
{
if (errno == EAGAIN || errno == EWOULDBLOCK) // 如果是非阻塞错误
{
return 0; // 返回没有数据
}
printf("接收失败: %s\n", strerror(errno)); // 打印错误信息
return -1; // 返回错误
}
else if (result == 0) // 连接关闭
{
printf("服务器关闭连接\n"); // 打印连接关闭信息
connected = 0; // 设置连接状态为断开
return -1; // 返回错误
}
printf("接收 %d 字节数据\n", result); // 打印接收数据长度
return result; // 返回接收数据长度
}
//-----------------------------------------消息接收线程------------------------------------------------------------
// 消息接收线程函数
void *message_receiver(void *arg)
{
unsigned char buffer[512]; // 接收缓冲区
printf("消息接收线程启动\n"); // 打印线程启动信息
while (connected) // 当连接状态为真时循环
{
if (!is_socket_connected(connectSocket)) // 检查socket是否仍然连接
{
printf("Socket连接已断开\n"); // 打印连接断开信息
connected = 0; // 设置连接状态为断开
break; // 退出循环
}
// 使用select检查是否有数据可读,避免阻塞
fd_set read_fds; // 读文件描述符集合
struct timeval timeout; // 超时结构体
FD_ZERO(&read_fds); // 清空文件描述符集合
FD_SET(connectSocket, &read_fds); // 添加socket到集合
timeout.tv_sec = 5; // 5秒超时
timeout.tv_usec = 0; // 0微秒
int select_result = select(connectSocket + 1, &read_fds, NULL, NULL, &timeout); // 等待数据可读
if (select_result == 0) // 超时
{
// 超时,正常情况,继续循环
continue;
}
else if (select_result < 0) // 错误
{
if (errno != EINTR) // 如果不是中断错误
{
printf("select错误: %s\n", strerror(errno)); // 打印错误信息
connected = 0; // 设置连接状态为断开
break; // 退出循环
}
continue; // 如果是中断错误,继续循环
}
// 有数据可读
if (FD_ISSET(connectSocket, &read_fds)) // 如果socket有数据可读
{
int received = recv(connectSocket, buffer, sizeof(buffer), 0); // 接收数据
if (received > 0) // 如果接收到数据
{
printf(">>> 收到MQTT消息(%d字节): ", received); // 打印接收信息
for (int i = 0; i < received; i++) // 循环打印接收数据
{
printf("%02X ", buffer[i]); // 打印十六进制数据
}
printf("\n"); // 换行
// 解析消息类型
unsigned char messageType = buffer[0] >> 4; // 获取消息类型
switch (messageType) // 根据消息类型处理
{
case M_PUBLISH: // 发布消息
printf(">>> 收到平台下发消息\n"); // 打印消息类型
break;
case M_PINGRESP: // 心跳响应
printf(">>> 心跳响应\n"); // 打印消息类型
break;
case M_SUBACK: // 订阅确认
printf(">>> 订阅确认\n"); // 打印消息类型
break;
case M_PUBACK: // 发布确认
printf(">>> 发布确认\n"); // 打印消息类型
break;
case M_CONNACK: // 连接确认
printf(">>> 连接确认\n"); // 打印消息类型
break;
default: // 其他消息类型
printf(">>> 其他消息类型: %d\n", messageType); // 打印消息类型
break;
}
}
else if (received == 0) // 连接关闭
{
printf("服务器关闭连接\n"); // 打印连接关闭信息
connected = 0; // 设置连接状态为断开
break; // 退出循环
}
else // 接收错误
{
if (errno != EAGAIN && errno != EWOULDBLOCK) // 如果不是非阻塞错误
{
printf("接收错误: %s\n", strerror(errno)); // 打印错误信息
connected = 0; // 设置连接状态为断开
break; // 退出循环
}
}
}
usleep(100000); // 100ms休眠,避免CPU占用过高
}
printf("消息接收线程退出\n"); // 打印线程退出信息
return NULL; // 返回空指针
}
//-----------------------------------------主函数------------------------------------------------------------
// 主函数
int main()
{
printf("MQTT客户端启动...\n"); // 打印启动信息
printf("目标服务器: %s:%d\n", SERVER_IP, SERVER_PORT); // 打印服务器信息
// 创建socket
connectSocket = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP socket
if (connectSocket == INVALID_SOCKET) // 如果创建失败
{
printf("socket创建失败: %s\n", strerror(errno)); // 打印错误信息
return 1; // 返回错误
}
// 设置socket选项
int reuse = 1; // 地址重用标志
if (setsockopt(connectSocket, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) // 设置地址重用
{
printf("设置SO_REUSEADDR失败: %s\n", strerror(errno)); // 打印错误信息
}
// 设置TCP_NODELAY
int nodelay = 1; // 禁用Nagle算法标志
if (setsockopt(connectSocket, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)) < 0) // 设置TCP_NODELAY
{
printf("设置TCP_NODELAY失败: %s\n", strerror(errno)); // 打印错误信息
}
struct sockaddr_in service; // 服务器地址结构
memset(&service, 0, sizeof(service)); // 清空地址结构
service.sin_family = AF_INET; // IPv4地址族
service.sin_port = htons(SERVER_PORT); // 端口号,转换为网络字节序
if (inet_pton(AF_INET, SERVER_IP, &service.sin_addr) <= 0) // 转换IP地址
{
printf("IP地址转换失败: %s\n", strerror(errno)); // 打印错误信息
close(connectSocket); // 关闭socket
return 1; // 返回错误
}
printf("正在连接服务器...\n"); // 打印连接信息
int result = connect(connectSocket, (struct sockaddr *)&service, sizeof(service)); // 连接服务器
if (result == SOCKET_ERROR) // 如果连接失败
{
printf("连接服务器失败: %s\n", strerror(errno)); // 打印错误信息
close(connectSocket); // 关闭socket
return 1; // 返回错误
}
printf("TCP连接成功,开始MQTT协议握手...\n"); // 打印连接成功信息
MQTT_Init(); // 初始化MQTT
// 连接MQTT服务器
if (MQTT_Connect((char *)CLIENT_ID, (char *)USER_NAME, (char *)PASSWORD) != 0) // 连接MQTT服务器
{
printf("MQTT连接失败,程序退出\n"); // 打印连接失败信息
close(connectSocket); // 关闭socket
return 1; // 返回错误
}
printf("MQTT连接成功\n"); // 打印连接成功信息
// 订阅主题
int stat = MQTT_SubscribeTopic((char *)SET_TOPIC, 1, 1); // 订阅主题
if (stat) // 如果订阅失败
{
printf("订阅失败\n"); // 打印订阅失败信息
close(connectSocket); // 关闭socket
return 1; // 返回错误
}
printf("订阅成功\n"); // 打印订阅成功信息
// 创建接收线程
pthread_t recv_thread; // 接收线程变量
if (pthread_create(&recv_thread, NULL, message_receiver, NULL) != 0) // 创建接收线程
{
printf("接收线程创建失败: %s\n", strerror(errno)); // 打印线程创建失败信息
close(connectSocket); // 关闭socket
return 1; // 返回错误
}
printf("消息接收线程已启动\n"); // 打印线程启动成功信息
printf("开始发布温度数据...\n"); // 打印开始发布信息
int publish_count = 0; // 发布计数器
while (connected && publish_count < 50) // 当连接且发布次数小于50次时循环
{
// 每30秒发送一次心跳
time_t current_time = time(NULL); // 获取当前时间
if (current_time - last_ping_time >= 30) // 如果距离上次心跳超过30秒
{
send_mqtt_ping(); // 发送心跳
}
// 发布温度数据,使用QoS 1确保收到确认
sprintf(mqtt_message, "{\"services\": [{\"service_id\": \"stm32\",\"properties\":{\"TEMP\":%.1f}}]}", (double)(TEMP += 0.2)); // 格式化温度消息
if (MQTT_PublishData((char *)POST_TOPIC, mqtt_message, 1) > 0) // 发布温度数据
{
printf("发布消息成功 (#%d, 温度: %.1f°C)\n", ++publish_count, TEMP); // 打印发布成功信息
}
else // 发布失败
{
printf("发布消息失败\n"); // 打印发布失败信息
}
// 等待5秒,期间可以处理接收的数据
for (int i = 0; i < 50 && connected; i++) // 循环50次,每次100ms,总共5秒
{
usleep(100000); // 100ms休眠
}
}
printf("程序运行完成,清理资源...\n"); // 打印程序完成信息
connected = 0; // 设置连接状态为断开
// 等待接收线程退出
pthread_join(recv_thread, NULL); // 等待接收线程结束
close(connectSocket); // 关闭socket
printf("程序退出\n"); // 打印程序退出信息
return 0; // 返回成功
}
3.2 代码最核心的地方
这里填写MQTT服务器的信息,也就是前面创建华为云IOT服务器得到的信息。

(注:因为实时更新的问题,我在实际代码中的服务器参数和本文上面获取的不一致)
3.3 编译运行代码
编译命令:
bash
gcc -o mqtt_client mqtt_client.c -lpthread

3.4 登录华为云IOT云端查看数据
可以看到设备已经在线了。

可以看到我们的消息也在实时的上传。
到此,说明我们的MQTT协议已经封装完成,可以正常的运行了。
四、下发命令的处理(暂略)
一般MQTT设备端除了上传数据以外,还需要接收MQTT服务器下发的控制命令。
那么我们接下来就完善一下代码,接收华为云MQTT服务器下发的命令,并进行回应。