基于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服务器下发的命令,并进行回应。

相关推荐
Jerry6 分钟前
LeetCode 202. 快乐数
算法
大卡片9 分钟前
linux内核驱动开发
linux·运维·驱动开发
hans汉斯29 分钟前
基于改进交叉熵损失函数与Transformer的心电信号高风险分类研究
功能测试·深度学习·算法·yolo·目标检测·分类·transformer
Jerry31 分钟前
LeetCode 349. 两个数组的交集
算法
必须得开心呀1 小时前
QT解决中文乱码问题
开发语言·qt
心心喵1 小时前
[linux] nohup和pm2的区别 进程保活
linux·运维·服务器
YuK.W2 小时前
Leetcode100: 70.爬楼梯、118.杨辉三角、198.打家劫舍
java·算法·leetcode
随意起个昵称2 小时前
状压dp-基础题目2([USACO12MAR] Cows in a Skyscraper G)
c++·算法·动态规划
这不小天嘛2 小时前
JAVA八股——redis篇
java·开发语言·redis
醉熏的石头2 小时前
Ubuntu 中的编程语言(中)
linux·ubuntu·scala