基于LINUX平台使用C语言实现MQTT协议连接华为云平台(IOT)(网络编程)

一、访问华为云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服务器下发的命令,并进行回应。

相关推荐
吾疾唯君医3 小时前
记录GoLang创建文件并写入文件的中文乱码错误!
开发语言·后端·golang
flashlight_hi3 小时前
LeetCode 分类刷题:74. 搜索二维矩阵
python·算法·leetcode·矩阵
小年糕是糕手3 小时前
【数据结构】算法复杂度
c语言·开发语言·数据结构·学习·算法·leetcode·排序算法
JAVA学习通3 小时前
微服务项目->在线oj系统(Java-Spring)--C端用户(超详细)
java·开发语言·spring
数据知道3 小时前
Go基础:Go语言ORM框架GORM详解
开发语言·jvm·后端·golang·go语言
计算机毕业设计小帅3 小时前
【2026计算机毕业设计】基于jsp的毕业论文管理系统
java·开发语言·毕业设计·课程设计
明天会有多晴朗3 小时前
深度剖析 C++ 之内存管理篇
c语言·开发语言·c++
程序员-King.3 小时前
day86——有效的字母异位词(LeetCode-242)
算法·字符串
potato_may3 小时前
C语言第3讲:分支和循环(上)—— 程序的“决策”与“重复”之旅
c语言·开发语言