当前位置: 首页 > news >正文

基于C语言从0开始手撸MQTT协议代码连接标准的MQTT服务器,完成数据上传和命令下发响应(华为云IOT服务器)

文章目录

  • 一、前言
  • 二、搭建开发环境
  • 三、网络编程基础概念科普
    • 3.1 什么是网络编程
    • 3.2 TCP 和 UDP协议介绍
    • 3.3 TCP通信的实现过程
  • 四、Windows下的网络编程相关API介绍
    • 4.1 常用的函数介绍
    • 4.2 函数参数介绍
    • 4.3 编写代码体验网络编程
  • 五、访问华为云IOT服务器创建一个产品和设备
    • 5.2 开通物联网服务
    • 5.3 创建产品
      • (1)创建产品
      • (2)填写产品信息
      • (3)产品创建成功
      • (4)添加自定义模型
    • 3.4 添加设备
      • (1)注册设备
      • (2)根据自己的设备填写
      • (3)保存设备信息
    • 3.5 MQTT协议主题订阅与发布
      • (1)华为云平台MQTT协议使用限制
      • (2)主题订阅格式
      • (3)主题发布格式
    • 3.6 MQTT三元组
      • (1)MQTT服务器地址
      • (2)生成MQTT三元组
      • (3)MQTT登录测试参数总结
  • 六、开始学习MQTT协议
    • 6.1 先了解下MQTT协议
    • 6.2 MQTT协议官网介绍
    • 6.3 需要实现的3个函数
    • 6.4 查看协议文档,了解如何组合协议报文
    • 6.5 实现`MQTT_Connect`函数
      • 【1】固定报文头
      • 【2】协议名
      • 【3】协议级别
      • 【4】连接标志
      • 【5】保持连接的时间
      • 【6】 可变报头非规范示例
      • 【7】最后部分:填写客户端ID、用户名、密码。
      • 【8】响应
      • 【9】完整代码
    • 6.6 实现MQTT_PublishData函数
      • 【1】查看文档说明
      • 【2】固定报文头
      • 【3】剩余字段长度
      • 【4】可变报头
      • 【5】完整代码
      • 【6】发布确认
    • 6.7 实现 MQTT_SubscribeTopic 函数
      • 【1】查看文档:订阅主题的格式
      • 【2】查看文档:取消订阅的格式
      • 【3】固定报头
      • 【4】可变报头
      • 【5】完整代码
  • 七、运行项目、连接华为云服务器
    • 7.1 整个项目的完整代码
    • 7.2 代码里核心的地方
    • 7.3 编译运行代码
    • 7.4 登录华为云IOT云端查看数据
  • 八、下发命令的处理
    • 8.1 添加命令
    • 8.2 下发命令测试
    • 8.3 编写代码
    • 8.4 运行代码测试
  • 九、总结

从0开始编写MQTT协议代码连接标准MQTT服务器(精讲MQTT协议)

一、前言

近年来,物联网的发展如火如荼,已经渗透到我们生活的方方面面。从智能家居到工业自动化,从智慧城市到智慧农业,物联网正在以前所未有的速度改变着我们的生活。 大家现在可能已经习惯了通过手机控制家里的灯光、空调和电视,这就是物联网在智能家居领域的应用,如果在10年前看到这种设备的应用肯定觉得很牛批,而现在只要是个设备都能上云,这种家电设备的远程控制已经成了大家习以为常的配置了。而在工业领域,物联网技术可以帮助企业实现自动化生产、设备监控和预防性维护,提高生产效率和产品质量。在智慧城市建设中,物联网技术可以用于交通管理、环境监测和公共安全等方面,提升城市管理和居民生活的质量。

从物联网开始兴起的时候,各大厂家都纷纷推出了自家的IOT物联网平台。 比如: 机智云、中国移动的onenet、阿里云的IOT、百度的天工物接入、华为云的IOT、腾讯云IOT等等。 这些大厂家的物联网服务器都支持标准的MQTT协议接入,大家不用自己搭建MQTT服务器可以直接使用这些现成的服务器接入设备开发是非常的方便的。

我在这几年也写了很多物联网开发的案例,不管是、中国移动的onenet、阿里云的IOT、百度的天工物接入、华为云的IOT、腾讯云IOT 这些服务器都写了很多教程,演示设备接入平台,完成设备上云,手机APP对接,电脑程序对接,微信小程序接入,实现远程数据监测控制等等。这些案例都放在了智能家居与物联网项目实战专栏里。 这些案例里设备实现上云的方式主要是两种方式:HTTP协议、MQTT协议方式上云。 MQTT协议是标准的物联网协议,支持双向数据传输,也就是可以上传数据到服务器,也可以接收服务器下发的控制命令完成远程控制。 我写的这些案例里硬件端联网的模块主要是用到了4G模块、ESP8266-WIFI模块、GSM模块、NBIOT模块等等,通过它们联网,让单片机设备实现上云。

这些设备中有些是支持MQTT协议的(也就是本身的固件就支持MQTT协议),有些不支持的(可能有固件支持,需要自己烧写)。 如果说固件不支持MQTT协议,但只要设备支持TCP协议,那么我们也可以自己封装MQTT协议完成与MQTT服务器之间的通信。 比如:ESP8266-WIFI模块,正常的官方默认固件中,ESP8266-WIFI是不支持MQTT协议的,如果我们不烧写固件的情况下,如何自己实现MQTT协议上云? 这篇文章就介绍,通过TCP协议自己封装MQTT协议报文,完成数据上云。 直接从0开始手撸MQTT协议报文,组合报文,完成与服务器之间的通信。

MQTT协议也是分为两种,分MQTT和MQTTS,就像HTTP协议一样也分HTTP和HTTPS,那么区别呢? 带S就是要支持SSL协议,支持认证,更加安全,那么复杂度自然就上来了。 MQTT协议的端口是1883,MQTTS的端口是8883。 当前这篇文章介绍非加密的MQTT协议,也就是1883端口。MQTTS协议也手撸不了,这玩意涉及到SSL协议,那就很复杂了,如果要用,直接使用现成的开源库就行,但本篇文章不讨论这个,后面文章再单独介绍如何实现MQTTS协议。

本篇文章的环境是在windows下,利用VS2022开发程序,使用windows下网络编程接口作为基础,封装MQTT协议连接华为云MQTT服务器,完成数据上云。

所以,大家只要有一台windows电脑,电脑上安装了VS开发环境,任何版本都可以(VS2010、VS2013、VS2015、VS2017、VS2019、VS2022等等都可以的) 跟着这篇文章进行学习,不需要其他任何硬件设备,我们现在是单纯的去学习MQTT协议。

前提呢,大家还是要懂得一点网络编程的知识,了解TCP协议,大致知道TCP协议通信的简单过程,如果网络编程知识是完全0基础,建议先看另一篇文章学习下网络编程(我博客有专门讲解网络编程相关知识的文章)。 这篇文章也会简单介绍下TCP协议和基本网络编程知识

那么接下来,我们就开始动手学习吧。

二、搭建开发环境

如果大家电脑已经有开发环境,这章节直接忽略。 这里贴出来为了给 完全0基础 的小伙伴学习

我这里介绍下我用的环境安装过程。 所有版本的VS都可以的。

我当前环境是在Windows下,IDE用的是地表最强IDE VS2022。

下载地址:https://visualstudio.microsoft.com/zh-hans/downloads/

image-20230913173131481

因为我这里只需要用到C++和C语言编程,那么安装的时候可以自己选择需要安装的包。

image-20230913173258088

安装好之后,创建项目。

image-20230913173330580

image-20230913173349914

三、网络编程基础概念科普

如果是老手了,这章节可以直接忽略。 如果对网络编程是 0基础 的小伙伴,那么就认真看一下,了解下基本知识。

3.1 什么是网络编程

网络编程是通过使用IP地址和端口号等网络信息,使两台以上的计算机能够相互通信,按照规定的协议交换数据的编程方式。

在网络编程中,程序员使用各种协议和技术,使得不同的设备可以通过网络进行数据交换和信息共享。

要实现网络编程,程序员需要了解并掌握各种网络通信协议,比如TCP/IP协议族,包括TCP、UDP、IP等,这些协议是实现设备间通信的基础。网络编程内部涉及到数据的打包、组装、发送、接收、解析等一系列过程,以实现信息的正确传输。

在TCP/IP协议族中,TCP和UDP是位于IP协议之上的传输层协议。 在OSI模型中,传输层是第四层,负责总体数据传输和数据控制,为会话层等高三层提供可靠的传输服务,为网络层提供可靠的目的地点信息。在TCP/IP协议族中,TCP和UDP正是位于这一层的协议。

这篇文章主要介绍 TCP 和 UDP 协议 以及 使用方法。

img

3.2 TCP 和 UDP协议介绍

TCP协议

TCP(传输控制协议)是一种面向连接的、可靠的传输层协议。在传输数据之前需要先建立连接,确保数据的顺序和完整性。TCP通过三次握手建立连接,并通过确认、超时和重传机制确保数据的可靠传输。TCP采用流量控制和拥塞控制机制,以避免网络拥塞,确保数据的顺利传输。因为TCP的这些特性,通常被应用于需要高可靠性和顺序性的应用,如网页浏览、电子邮件等。

UDP协议

UDP(用户数据报协议)是一种无连接的、不可靠的传输层协议。与TCP不同,UDP在传输数据之前不需要建立连接,直接将数据打包成数据报并发送出去。因此,UDP没有TCP的那些确认、超时和重传机制,也就不保证数据的可靠传输。UDP也没有TCP的流量控制和拥塞控制机制。因为UDP的简单性和高效性,通常被应用于实时性要求较高,但对数据可靠性要求不高的应用,如语音通话、视频直播等。

3.3 TCP通信的实现过程

要实现TCP通信,两端必须要知道对方的IP和端口号:

(1)IP地址:TCP协议是基于IP协议进行通信的,因此需要知道对方的IP地址,才能建立连接。

(2)端口号:每个TCP连接都有一个唯一的端口号,用于标识进程和应用程序。建立连接时,需要指定本地端口号和远端端口号。

(3)应用层协议:TCP协议只提供数据传输服务,应用程序需要定义自己的应用层协议,用于解析报文和处理数据。例如,HTTP协议就是基于TCP协议的应用层协议。

在正常的TCP通信过程中,第一步需要建立连接,这个过程称为“三次握手”。建立连接时,客户端向服务器发送一个SYN包,表示请求建立连接;服务器接收到SYN包后,向客户端发送一个ACK包,表示确认收到了SYN包;最后客户端再向服务器发送一个ACK包,表示确认收到了服务器的ACK包,此时连接建立成功。建立连接后,数据传输就可以开始了。

img

四、Windows下的网络编程相关API介绍

因为当前文章是在Windows下介绍MQTT协议,要用到网络编程的知识,需要使用Windows系统提供的API完成网络编程。Windows本身就有一套原生的网络编程接口可以直接使用。 在Linux系统下也是一样,都有自己一套原生的网络编程接口。

如果没有接触这些API的小伙伴不用慌~~~。 你至少用过C语言里的printf、scanf、strlen之类的函数吧? 下面介绍的这些网络编程API函数其实和它们没什么区别,都是普通的函数,功能不一样而已。 对你来说,只是多学了几个库函数,只要了解每个函数的功能就可以调用了。

那么接下来就学习一下常用的网络编程相关的函数。

微软的官方文档地址:https://learn.microsoft.com/zh-cn/windows/win32/api/_winsock/

image-20231031132547022

4.1 常用的函数介绍

在Windows下进行网络编程,可以使用Winsock API(Windows Sockets API)来实现。Winsock API是Windows平台上的标准网络编程接口,提供了一系列函数和数据结构,用于创建、连接、发送和接收网络数据等操作。

下面是常用的Winsock API接口函数:

(1)WSAStartup:初始化Winsock库,必须在使用其他Winsock函数之前调用。

(2)socket:创建一个套接字,用于网络通信。

(3)bind:将套接字与本地地址(IP地址和端口号)绑定。

(4)listen:开始监听连接请求,将套接字设置为被动模式。

(5)accept:接受客户端的连接请求,创建一个新的套接字用于与客户端通信。

(6)connect:与远程服务器建立连接。

(7)send:发送数据到已连接的套接字。

(8)recv:从已连接的套接字接收数据。

(9)sendto:发送数据到指定的目标地址。

(10)recvfrom:从指定的地址接收数据。

(11)closesocket:关闭套接字。

(12)getaddrinfo:根据主机名和服务名获取地址信息。

(13)gethostbyname:根据主机名获取主机的IP地址。

(14)gethostname:获取本地主机名。

4.2 函数参数介绍

下面是常用的几个Winsock API函数及其函数原型和参数含义的介绍:

(1)WSAStartup

int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
  • wVersionRequested:请求的Winsock版本号。
  • lpWSAData:指向WSADATA结构的指针,用于接收初始化结果和相关信息。

(2)socket

SOCKET socket(int af, int type, int protocol);
  • af:地址族(Address Family),如AF_INET表示IPv4。
  • type:套接字类型,如SOCK_STREAM表示面向连接的TCP套接字。
  • protocol:指定协议。通常为0,表示根据type自动选择合适的协议。

(3)bind

int bind(SOCKET s, const struct sockaddr* name, int namelen);
  • s:要绑定的套接字。
  • name:指向sockaddr结构的指针,包含要绑定的本地地址信息。
  • namelenname结构的长度。

(4)listen

int listen(SOCKET s, int backlog);
  • s:要监听的套接字。
  • backlog:等待连接队列的最大长度。

(5)accept

SOCKET accept(SOCKET s, struct sockaddr* addr, int* addrlen);
  • s:监听套接字。
  • addr:用于存储客户端地址信息的sockaddr结构。
  • addrlenaddr结构的长度。

(6)connect

int connect(SOCKET s, const struct sockaddr* name, int namelen);
  • s:要连接的套接字。
  • name:指向目标地址信息的sockaddr结构指针。
  • namelenname结构的长度。

(7)send

int send(SOCKET s, const char* buf, int len, int flags);
  • s:要发送数据的套接字。
  • buf:要发送的数据缓冲区。
  • len:要发送的数据长度。
  • flags:额外选项,如MSG_DONTROUTE等。

(8)recv

int recv(SOCKET s, char* buf, int len, int flags);
  • s:要接收数据的套接字。
  • buf:用于存储接收数据的缓冲区。
  • len:要接收的数据长度。
  • flags:额外选项。

(9)sendto

int sendto(SOCKET s, const char* buf, int len, int flags, const struct sockaddr* to, int tolen);
  • s:要发送数据的套接字。
  • buf:要发送的数据缓冲区。
  • len:要发送的数据长度。
  • flags:额外选项。
  • to:指向目标地址信息的sockaddr结构指针。
  • tolento结构的长度。

(10)recvfrom

int recvfrom(SOCKET s, char* buf, int len, int flags, struct sockaddr* from, int* fromlen);
  • s:要接收数据的套接字。
  • buf:用于存储接收数据的缓冲区。
  • len:要接收的数据长度。
  • flags:额外选项。
  • from:用于存储发送方地址信息的sockaddr结构指针。
  • fromlenfrom结构的长度。

(11)closesocket

int closesocket(SOCKET s);
  • s:要关闭的套接字。

(12)getaddrinfo

int getaddrinfo(const char* nodename, const char* servname, const struct addrinfo* hints, struct addrinfo** res);
  • nodename:目标主机名或IP地址。
  • servname:服务名或端口号。
  • hints:指向addrinfo结构的指针,提供关于地址查找的提示。
  • res:指向addrinfo结构链表的指针,用于接收查找结果。

(13)gethostbyname

struct hostent* gethostbyname(const char* name);
  • name:要查询的主机名。

(14)gethostname

int gethostname(char* name, int namelen);
  • name:用于接收主机名的缓冲区。
  • namelenname缓冲区的长度。

4.3 编写代码体验网络编程

上面了解了这些函数,可能不知道如何使用。 这里就写一个例子,以TCP客户端的身份去连接TCP服务器,完成数据传输。

**下面代码实现一个TCP客户端,连接到指定的服务器并完成通信。 ** 可以直接将代码贴到你的工程里,运行,体验效果。

#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>#pragma comment(lib, "ws2_32.lib") //告诉编译器链接Winsock库int main()
{WSADATA wsaData; //创建一个结构体变量,用于存储关于Winsock库的信息int result = WSAStartup(MAKEWORD(2, 2), &wsaData); //初始化Winsock库,指定版本号2.2,检查返回值if (result != 0){std::cout << "WSAStartup failed: " << result << std::endl; //输出错误信息并退出程序return 1;}SOCKET connectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建一个TCP套接字,检查返回值if (connectSocket == INVALID_SOCKET){std::cout << "socket failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序WSACleanup(); //清除Winsock库return 1;}sockaddr_in service; //创建一个结构体变量,用于存储服务器地址信息service.sin_family = AF_INET; //指定地址族为IPv4inet_pton(AF_INET, "127.0.0.1", &service.sin_addr); //将字符串类型的IP地址转换为二进制网络字节序的IP地址,并存储在结构体中service.sin_port = htons(12345); //将端口号从主机字节序转换为网络字节序,并存储在结构体中result = connect(connectSocket, (SOCKADDR*)&service, sizeof(service)); //连接到服务器,检查返回值if (result == SOCKET_ERROR){std::cout << "connect failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序closesocket(connectSocket); //关闭套接字WSACleanup(); //清除Winsock库return 1;}std::cout << "Connected to server." << std::endl; //连接成功,输出消息char sendBuffer[1024] = "Hello, server!"; //创建发送缓冲区,存储待发送的数据result = send(connectSocket, sendBuffer, sizeof(sendBuffer), 0); //向服务器发送数据,检查返回值if (result == SOCKET_ERROR){std::cout << "send failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序closesocket(connectSocket); //关闭套接字WSACleanup(); //清除Winsock库return 1;}char recvBuffer[1024]; //创建接收缓冲区,用于存储从服务器接收到的数据result = recv(connectSocket, recvBuffer, sizeof(recvBuffer), 0); //从服务器接收数据,检查返回值if (result == SOCKET_ERROR){std::cout << "recv failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序closesocket(connectSocket); //关闭套接字WSACleanup(); //清除Winsock库return 1;}std::cout << "Received message from server: " << recvBuffer << std::endl; //输出从服务器收到的数据closesocket(connectSocket); //关闭套接字WSACleanup(); //清除Winsock库return 0;
}

运行效果:

image-20231031134243184

五、访问华为云IOT服务器创建一个产品和设备

5.2 开通物联网服务

地址: https://www.huaweicloud.com/product/iothub.html

image-20221204194233414

现在可以免费创建的,按需付费。 只要是不超过规格,就可以免费使用,对于个人项目Demo来说,完全够用的

image-20231128160932256

点击立即创建之后,会开始创建实例,需要等待片刻,再刷新浏览器就可以看到创建成功了。

image-20231128161057219

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

image-20231128161544523

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

image-20231128161714301

总结:

端口号:   MQTT (1883)
接入地址: 252d4bd608.st1.iotda-device.cn-north-4.myhuaweicloud.com

**根据域名地址得到IP地址信息: ** 打开windows的CMD窗口

Microsoft Windows [版本 10.0.19045.3693]
(c) Microsoft Corporation。保留所有权利。C:\Users\11266>ping 252d4bd608.st1.iotda-device.cn-north-4.myhuaweicloud.com正在 Ping 252d4bd608.st1.iotda-device.cn-north-4.myhuaweicloud.com [117.78.5.125] 具有 32 字节的数据:
来自 117.78.5.125 的回复: 字节=32 时间=41ms TTL=94
来自 117.78.5.125 的回复: 字节=32 时间=38ms TTL=94
来自 117.78.5.125 的回复: 字节=32 时间=37ms TTL=94
来自 117.78.5.125 的回复: 字节=32 时间=39ms TTL=94117.78.5.125 的 Ping 统计信息:数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):最短 = 37ms,最长 = 41ms,平均 = 38msC:\Users\11266>

image-20231128161930198

MQTT协议接入端口号有两个,1883是非加密端口,8883是证书加密端口,单片机无法加载证书,所以使用1883端口比较合适。 接下来的ESP8266就采用1883端口连接华为云物联网平台。

5.3 创建产品

(1)创建产品

点击产品页,再点击左上角创建产品。

image-20231128163411971

(2)填写产品信息

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

image-20231201143214237

(3)产品创建成功

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

image-20231201143237590

(4)添加自定义模型

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

image-20231128164716733

模型简单来说: 就是存放设备上传到云平台的数据。

先点击自定义模型。

image-20231128164722543

再创建一个服务ID。

image-20231128164736524

接着点击新增属性。

image-20231128164752998

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

image-20231201143611800

image-20231201143658599

3.4 添加设备

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

(1)注册设备

image-20231128165130340

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

image-20231201143815785

(3)保存设备信息

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

{"device_id": "65697df3585c81787ad4da82_stm32","secret": "12345678"
}

点击设备名称可以进入到设备详情页。

image-20231201143934356

3.5 MQTT协议主题订阅与发布

(1)华为云平台MQTT协议使用限制

描述限制
支持的MQTT协议版本3.1.1
与标准MQTT协议的区别支持Qos 0和Qos 1支持Topic自定义不支持QoS2不支持will、retain msg
MQTTS支持的安全等级采用TCP通道基础 + TLS协议(最高TLSv1.3版本)
单帐号每秒最大MQTT连接请求数无限制
单个设备每分钟支持的最大MQTT连接数1
单个MQTT连接每秒的吞吐量,即带宽,包含直连设备和网关3KB/s
MQTT单个发布消息最大长度,超过此大小的发布请求将被直接拒绝1MB
MQTT连接心跳时间建议值心跳时间限定为30至1200秒,推荐设置为120秒
产品是否支持自定义Topic支持
消息发布与订阅设备只能对自己的Topic进行消息发布与订阅
每个订阅请求的最大订阅数无限制

(2)主题订阅格式

帮助文档地址:https://support.huaweicloud.com/devg-iothub/iot_02_2200.html

image-20221207153310037

对于设备而言,一般会订阅平台下发消息给设备 这个主题。

设备想接收平台下发的消息,就需要订阅平台下发消息给设备 的主题,订阅后,平台下发消息给设备,设备就会收到消息。

如果设备想要知道平台下发的消息,需要订阅上面图片里标注的主题。

以当前设备为例,最终订阅主题的格式如下:
$oc/devices/{device_id}/sys/messages/down最终的格式:
$oc/devices/65697df3585c81787ad4da82_stm32/sys/messages/down

(3)主题发布格式

对于设备来说,主题发布表示向云平台上传数据,将最新的传感器数据,设备状态上传到云平台。

这个操作称为:属性上报。

帮助文档地址:https://support.huaweicloud.com/usermanual-iothub/iot_06_v5_3010.html

image-20221207153637391

根据帮助文档的介绍, 当前设备发布主题,上报属性的格式总结如下:

发布的主题格式:
$oc/devices/{device_id}/sys/properties/report最终的格式:
$oc/devices/65697df3585c81787ad4da82_stm32/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

image-20231128165826027

MQTT协议的端口支持1883和8883,它们的区别是:8883 是加密端口更加安全。但是单片机上使用比较困难,所以当前的设备是采用1883端口进连接的。

根据上面的域名和端口号,得到下面的IP地址和端口号信息: 如果设备支持填写域名可以直接填域名,不支持就直接填写IP地址。 (IP地址就是域名解析得到的)

华为云的MQTT服务器地址:117.78.5.125
华为云的MQTT端口号:1883

(2)生成MQTT三元组

华为云提供了一个在线工具,用来生成MQTT鉴权三元组: https://iot-tool.obs-website.cn-north-4.myhuaweicloud.com/

打开这个工具,填入设备的信息(也就是刚才创建完设备之后保存的信息),点击生成,就可以得到MQTT的登录信息了。

下面是打开的页面:

image-20221207154917230

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

直接得到三元组信息。

image-20231201144219530

得到三元组之后,设备端通过MQTT协议登录鉴权的时候,填入参数即可。

ClientId  65697df3585c81787ad4da82_stm32_0_0_2023120106
Username  65697df3585c81787ad4da82_stm32
Password  12cc9b1f637da8d755fa2cbd007bb669e6f292e3e63017538b5e6e13eef0cf58

到此,云平台的部署已经完成,设备已经可以正常上传数据了。

(3)MQTT登录测试参数总结

IP地址:117.78.5.125
端口号:1883ClientId  65697df3585c81787ad4da82_stm32_0_0_2023120106
Username  65697df3585c81787ad4da82_stm32
Password  12cc9b1f637da8d755fa2cbd007bb669e6f292e3e63017538b5e6e13eef0cf58订阅主题:$oc/devices/65697df3585c81787ad4da82_stm32/sys/messages/down发布主题:$oc/devices/65697df3585c81787ad4da82_stm32/sys/properties/report
发布数据:{"services": [{"service_id": "stm32","properties":{"TEMP":36.2}}]}

六、开始学习MQTT协议

6.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协议可以用于交通管理、环境监测和公共安全等方面,提升城市管理和居民生活的质量。

6.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

image-20231130173124909

在文档的下方就是介绍MQTT协议的每个包如何封装的。照着协议写代码就行了。

image-20231130173137530

6.3 需要实现的3个函数

整个MQTT协议里,主要是实现3个函数就行了(其他的接口看自己需求)。

下面列出的3个函数,在一般的MQTT通信里是必备的。我们只要实现了这3个函数,那么完成基本的MQTT通信就没有问题了。

//发布主题
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);

6.4 查看协议文档,了解如何组合协议报文

【1】打开文档,打开目录,翻到第3章节—MQTT控制报文。

image-20231201150505249

【2】在第3章,控制报文里,找到对应的子章节,也就是我们接线需要照着文档实现协议组包的章节。

image-20231201150657961

6.5 实现MQTT_Connect函数

先认真阅读文档: 了解这个报文的规则,以及出现错误之后错误代码的含义。

客户端到服务端的网络连接建立后,客户端发送给服务端的第一个报文必须是 CONNECT 报文。 在一个网络连接上,客户端只能发送一次CONNECT 报文。服务端必须将客户端发送的第二个 CONNECT 报文当作协议违规处理并断开客户端的连接。

image-20231201151031703

接下来就按顺序查看文档,了解协议报文里每个字节如何组成的。

image-20231201151146312

接下来就开始编写代码,按照文档的提示,组合报文。

首先,定义一个数组,用来存放我们按照MQTT协议封装的数据。

unsigned char mqtt_txbuf[256];//接收数据缓存区

在定义一个变量,用来保存数组的下标,每赋值一次,就自增++;

int mqtt_txlen = 0;

【1】固定报文头

文档说了,数组的第一字节固定为:0x10。 (不懂为什么是0X10,仔细看下面文档里的红色框框,文档已经把二进制位的每个位都标注出来了,如果还是看不懂,就需要补习一下C语言的位运算,熟悉位运算之后,再来看应该就很容易了)。

image-20231201151323636

那么第一行代码就是这么写:

//固定报头
//控制报文类型
mqtt_txbuf[mqtt_txlen++] = 0x10;		//MQTT Message Type CONNECT

接下来第2个字节,文档让看2.2.3小节的说明,那么翻到2.2.3小节,了解如何填写剩余长度值。

image-20231201152240500

image-20231201152449794

根据文档说明,那么编写代码如下:

//剩余长度(不包括固定头部)
do
{unsigned char encodedByte = DataLen % 128;DataLen = DataLen / 128;// if there are more data to encode, set the top bit of this byteif (DataLen > 0)encodedByte = encodedByte | 128;mqtt_txbuf[mqtt_txlen++] = encodedByte;
} while (DataLen > 0);

好了,现在第2个字节赋值已经完毕。

【2】协议名

那么接着看剩下的字节如何填写:

image-20231201152607923

根据文档说明,编写代码如下: (继续按顺序赋值就行了)

//可变报头
//协议名
mqtt_txbuf[mqtt_txlen++] = 0;        	// Protocol Name Length MSB    
mqtt_txbuf[mqtt_txlen++] = 4;           // Protocol Name Length LSB    
mqtt_txbuf[mqtt_txlen++] = 'M';        	// ASCII Code for M    
mqtt_txbuf[mqtt_txlen++] = 'Q';        	// ASCII Code for Q    
mqtt_txbuf[mqtt_txlen++] = 'T';        	// ASCII Code for T    
mqtt_txbuf[mqtt_txlen++] = 'T';        	// ASCII Code for T    

【3】协议级别

看文档说明。

image-20231201152846219

编写代码:

//协议级别
mqtt_txbuf[mqtt_txlen++] = 4;        		// MQTT Protocol version = 4   

【4】连接标志

关于每个标志的含义,文档向下翻,下面有详细的介绍,每个标志位的含义。

image-20231201152912933

image-20231201153643742

编写代码:

//连接标志
mqtt_txbuf[mqtt_txlen++] = 0xc2;        	// conn flags 

【5】保持连接的时间

查看文档说明:

image-20231201153711580

编写代码:

mqtt_txbuf[mqtt_txlen++] = 0;        		// Keep-alive Time Length MSB    
mqtt_txbuf[mqtt_txlen++] = 100;        	// Keep-alive Time Length LSB  100S心跳包 

【6】 可变报头非规范示例

image-20231201153928782

【7】最后部分:填写客户端ID、用户名、密码。

这里面提到的客户端标识符、用户名、密码。 就是在前面章节创建华为云IOT服务器,得到的MQTT三元组信息。

查看文档:

image-20231201154144687

编写代码:

mqtt_txbuf[mqtt_txlen++] = BYTE1(ClientIDLen);// Client ID length MSB    
mqtt_txbuf[mqtt_txlen++] = BYTE0(ClientIDLen);// Client ID length LSB  	
memcpy(&mqtt_txbuf[mqtt_txlen], ClientID, ClientIDLen);
mqtt_txlen += ClientIDLen;
if (UsernameLen > 0)
{mqtt_txbuf[mqtt_txlen++] = BYTE1(UsernameLen);		//username length MSB    mqtt_txbuf[mqtt_txlen++] = BYTE0(UsernameLen);    	//username length LSB    memcpy(&mqtt_txbuf[mqtt_txlen], Username, UsernameLen);mqtt_txlen += UsernameLen;
}if (PasswordLen > 0)
{mqtt_txbuf[mqtt_txlen++] = BYTE1(PasswordLen);		//password length MSB    mqtt_txbuf[mqtt_txlen++] = BYTE0(PasswordLen);    	//password length LSB  memcpy(&mqtt_txbuf[mqtt_txlen], Password, PasswordLen);mqtt_txlen += PasswordLen;
}

【8】响应

上面报文封装完毕之后,直接就可以通过网络接口发送出去就行了。

前提是创建套接字,连接上MQTT服务器,然后再将上面封装好的报文发送过去就行了。

关于如何Windows下如何创建套接字,连接服务器,前面章节专门讲过了,忘记了可以回去再看看。

编写代码发送出去:

MQTT_SendBuf(mqtt_txbuf, mqtt_txlen);

这个函数里面的代码: 就是直接调用的网络接口函数发送的。

int result = send(connectSocket,(const char*)buff, len, 0); //向服务器发送数据,检查返回值
if (result == SOCKET_ERROR)
{std::cout << "send failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序return -1;
}

发送过去之后,服务器肯定有返回值的。 失败?成功? 那么我们得判断。

通过响应章节的文档说明,这里提到一个CONNACK响应报文,是由服务器下发给客户端的。

image-20231202123812617

在3.2小节,讲解了CONNACK报文的字段含义。如果客户端的连接报文是正确的,服务器会下发0x20 0x00 的确认连接报文给客户端,告诉客户端连接成功。

image-20231202124201134

编写代码: 写代码接收服务器返回的数据,判断返回的数据是不是符合要求。

Client_GetData(buff);//从服务器获取数据const unsigned char parket_connetAck[] = { 0x20,0x02};if (mqtt_rxbuf[0] == parket_connetAck[0] && mqtt_rxbuf[1] == parket_connetAck[1]) //连接成功			   
{return 0;//连接成功
}

CONNACK 报文除了固定报文头之外还有可变报头。也就是后面还有2个字节。 我们可以继续看文档下面的介绍。

一个叫连接确认标志,一个叫连接返回码。正确的情况下,这两个值应该为0x00 0x00.

image-20231202125348117

关于返回码的值,我们可以对它进行判断。如果连接失败,也可以知道具体的原因。

image-20231202125505479

【9】完整代码

/*
函数功能: 登录服务器
函数返回值: 0表示成功 1表示失败
*/
unsigned char MQTT_Connect(char* ClientID, char* Username, char* Password)
{unsigned short i, j;int ClientIDLen = (int)strlen(ClientID);int UsernameLen = (int)strlen(Username);int PasswordLen = (int)strlen(Password);unsigned int DataLen;mqtt_txlen = 0;unsigned int size = 0;unsigned char buff[256];//可变报头+Payload  每个字段包含两个字节的长度标识DataLen = 10 + (ClientIDLen + 2) + (UsernameLen + 2) + (PasswordLen + 2);//固定报头//控制报文类型mqtt_txbuf[mqtt_txlen++] = 0x10;		//MQTT Message Type CONNECT//剩余长度(不包括固定头部)do{unsigned char encodedByte = DataLen % 128;DataLen = DataLen / 128;// if there are more data to encode, set the top bit of this byteif (DataLen > 0)encodedByte = encodedByte | 128;mqtt_txbuf[mqtt_txlen++] = encodedByte;} while (DataLen > 0);//可变报头//协议名mqtt_txbuf[mqtt_txlen++] = 0;        	// Protocol Name Length MSB    mqtt_txbuf[mqtt_txlen++] = 4;           // Protocol Name Length LSB    mqtt_txbuf[mqtt_txlen++] = 'M';        	// ASCII Code for M    mqtt_txbuf[mqtt_txlen++] = 'Q';        	// ASCII Code for Q    mqtt_txbuf[mqtt_txlen++] = 'T';        	// ASCII Code for T    mqtt_txbuf[mqtt_txlen++] = 'T';        	// ASCII Code for T    //协议级别mqtt_txbuf[mqtt_txlen++] = 4;        		// MQTT Protocol version = 4    //连接标志mqtt_txbuf[mqtt_txlen++] = 0xc2;        	// conn flags mqtt_txbuf[mqtt_txlen++] = 0;        		// Keep-alive Time Length MSB    mqtt_txbuf[mqtt_txlen++] = 100;        	// Keep-alive Time Length LSB  100S心跳包  mqtt_txbuf[mqtt_txlen++] = BYTE1(ClientIDLen);// Client ID length MSB    mqtt_txbuf[mqtt_txlen++] = BYTE0(ClientIDLen);// Client ID length LSB  	memcpy(&mqtt_txbuf[mqtt_txlen], ClientID, ClientIDLen);mqtt_txlen += ClientIDLen;if (UsernameLen > 0){mqtt_txbuf[mqtt_txlen++] = BYTE1(UsernameLen);		//username length MSB    mqtt_txbuf[mqtt_txlen++] = BYTE0(UsernameLen);    	//username length LSB    memcpy(&mqtt_txbuf[mqtt_txlen], Username, UsernameLen);mqtt_txlen += UsernameLen;}if (PasswordLen > 0){mqtt_txbuf[mqtt_txlen++] = BYTE1(PasswordLen);		//password length MSB    mqtt_txbuf[mqtt_txlen++] = BYTE0(PasswordLen);    	//password length LSB  memcpy(&mqtt_txbuf[mqtt_txlen], Password, PasswordLen);mqtt_txlen += PasswordLen;}for (i = 0; i < 5; i++){memset(mqtt_rxbuf, 0, mqtt_rxlen);MQTT_SendBuf(mqtt_txbuf, mqtt_txlen);size = Client_GetData(buff);//从服务器获取数据if (size <= 0)continue;memcpy(mqtt_rxbuf, buff, size);printf("登录应答:\r\n");for (j = 0; j < size; j++){printf("%#X ", buff[j]);}printf("\r\n");if (mqtt_rxbuf[0] == parket_connetAck[0] && mqtt_rxbuf[1] == parket_connetAck[1]) //连接成功			   {return 0;//连接成功}}return 1;
}

6.6 实现MQTT_PublishData函数

【1】查看文档说明

和前面一章节一样,看文档说明编写代码。

image-20231201155454028

【2】固定报文头

通过文档了解到,发布消息的固定报文头由2个字节组成。第一个字节每个位的组成含义可看文档的表格介绍。

image-20231202125731456

最高4位是MQTT控制报文类型,固定的值:0x3

后面4个字节分为是DUP(重发标志)、QOS等级(服务质量等级)、RETAIN(消息的保留标志)。

DUP(重发标志):

image-20231202130033301

QOS等级(服务质量等级):

image-20231202130109342

RETAIN(保留标志–固定位0):

image-20231202130509553

那么经过文档的解释,我们编写代码如下: (关于后面4个字节可以根据自己的需求设置)

//固定报头
//控制报文类型
mqtt_txbuf[mqtt_txlen++] = 0x30;    // MQTT Message Type PUBLISH  

【3】剩余字段长度

固定报文头的第2个字节是填写剩余字段长度。

image-20231202131201868

在文档里对剩余长度字段计算的介绍:

image-20231202131112673

剩余长度字段: 等于可变报头的长度加上有效载荷的长度。

这里先要了解: 什么是可变报头? 什么是有效载荷的长度?

往下翻文档,可看到可变报头的介绍。 **可变报头是:按顺序包含主题名和报文标识符。 ** 而报文标识符只有在QOS为1或者2的时候才有。

image-20231202132449071

再往下翻文档,可看到有效载荷的长度的介绍。有效载荷包含将被发布的应用消息。数据的内容和格式是应用特定的。有效载荷的长度这样计算:用固定 报头中的剩余长度字段的值减去可变报头的长度。包含零长度有效载荷的 PUBLISH 报文是合法的。

image-20231202132740579

经过文档的介绍,我们知道了剩余长度字段如何介绍。

那么这个剩余长度字段计算出来之后,如何赋值到报文数组里去? 其实在Connect报文的固定字段第2个字节也是要天剩余长度字段,在Connect报文的章节已经介绍过,在文档的2.2.3小节的有详细说明,那么翻到2.2.3小节,了解如何填写剩余长度值。 (其实我们上一节已经介绍了一遍)

image-20231201152240500

image-20231201152449794

接下来就编写代码:

unsigned int topicLength = (int)strlen(topic);  //计算主题的长度
unsigned int messageLength = (int)strlen(message); //计算发送的消息长度
unsigned int DataLen; //保存最终长度//有效载荷的长度这样计算:用固定报头中的剩余长度字段的值减去可变报头的长度
//QOS为0时没有标识符
//数据长度             主题名   报文标识符   有效载荷
if (qos)	DataLen = (2 + topicLength) + 2 + messageLength;
else	DataLen = (2 + topicLength) + messageLength;//剩余长度
do
{unsigned char encodedByte = DataLen % 128;DataLen = DataLen / 128;// if there are more data to encode, set the top bit of this byteif (DataLen > 0)encodedByte = encodedByte | 128;mqtt_txbuf[mqtt_txlen++] = encodedByte;
} while (DataLen > 0);

【4】可变报头

image-20231202130801113

PUBLISH 报文可变报头非规范示例:

image-20231202133246434

编写代码:

mqtt_txbuf[mqtt_txlen++] = BYTE1(topicLength);//主题长度MSB
mqtt_txbuf[mqtt_txlen++] = BYTE0(topicLength);//主题长度LSB 
memcpy(&mqtt_txbuf[mqtt_txlen], topic, topicLength);//拷贝主题
mqtt_txlen += topicLength;//报文标识符
if (qos)
{mqtt_txbuf[mqtt_txlen++] = BYTE1(id);mqtt_txbuf[mqtt_txlen++] = BYTE0(id);id++;
}

接着就添加有效载荷,也就是实际发送的消息内容。

image-20231202135619158

编写代码:

memcpy(&mqtt_txbuf[mqtt_txlen], message, messageLength);
mqtt_txlen += messageLength;

最后将报文发送出去:

MQTT_SendBuf(mqtt_txbuf, mqtt_txlen);

【5】完整代码

//MQTT发布数据打包函数
//topic   主题 
//message 消息
//qos     消息等级 
unsigned char MQTT_PublishData(char* topic, char* message, unsigned char qos)
{unsigned int topicLength = (int)strlen(topic);unsigned int messageLength = (int)strlen(message);unsigned short id = 0;unsigned int DataLen;mqtt_txlen = 0;printf("上报JSON消息长度:%d\r\n", messageLength);printf("message=%s\r\n", message);//有效载荷的长度这样计算:用固定报头中的剩余长度字段的值减去可变报头的长度//QOS为0时没有标识符//数据长度             主题名   报文标识符   有效载荷if (qos)	DataLen = (2 + topicLength) + 2 + messageLength;else	DataLen = (2 + topicLength) + messageLength;//固定报头//控制报文类型mqtt_txbuf[mqtt_txlen++] = 0x30;    // MQTT Message Type PUBLISH  //剩余长度do{unsigned char encodedByte = DataLen % 128;DataLen = DataLen / 128;// if there are more data to encode, set the top bit of this byteif (DataLen > 0)encodedByte = encodedByte | 128;mqtt_txbuf[mqtt_txlen++] = encodedByte;} while (DataLen > 0);mqtt_txbuf[mqtt_txlen++] = BYTE1(topicLength);//主题长度MSBmqtt_txbuf[mqtt_txlen++] = BYTE0(topicLength);//主题长度LSB memcpy(&mqtt_txbuf[mqtt_txlen], topic, topicLength);//拷贝主题mqtt_txlen += topicLength;//报文标识符if (qos){mqtt_txbuf[mqtt_txlen++] = BYTE1(id);mqtt_txbuf[mqtt_txlen++] = BYTE0(id);id++;}memcpy(&mqtt_txbuf[mqtt_txlen], message, messageLength);mqtt_txlen += messageLength;MQTT_SendBuf(mqtt_txbuf, mqtt_txlen);return mqtt_txlen;
}

【6】发布确认

如果消息质量大于0,那么可以继续看文档下面的发布确认,对本次发送的消息进行响应处理。

image-20231202140427509

image-20231202140536152

6.7 实现 MQTT_SubscribeTopic 函数

【1】查看文档:订阅主题的格式

和前面一样,查看文档的说明,编写代码。

image-20231201155946444

【2】查看文档:取消订阅的格式

image-20231201160155317

【3】固定报头

订阅主题和取消订阅主题格式一样的,只是固定报头不一样。

可以封装一个函数,传入一个参数实现两种功能。

编写判断代码:

//固定报头
//控制报文类型
if (whether)mqtt_txbuf[mqtt_txlen++] = 0x82; //消息类型和标志订阅
else	mqtt_txbuf[mqtt_txlen++] = 0xA2;    //取消订阅

剩余长度字段:

image-20231202141516154

编写代码:

unsigned int topiclen = (int)strlen(topic);
unsigned int DataLen = 2 + (topiclen + 2) + (whether ? 1 : 0);//可变报头的长度(2字节)加上有效载荷的长度

剩余长度字段的填写规则与前面一样。

编写代码:

//剩余长度
do
{unsigned char encodedByte = DataLen % 128;DataLen = DataLen / 128;// if there are more data to encode, set the top bit of this byteif (DataLen > 0)encodedByte = encodedByte | 128;mqtt_txbuf[mqtt_txlen++] = encodedByte;
} while (DataLen > 0);

【4】可变报头

image-20231202141435800

编写代码:

//可变报头
mqtt_txbuf[mqtt_txlen++] = 0;			//消息标识符 MSB
mqtt_txbuf[mqtt_txlen++] = 0x01;        //消息标识符 LSB

image-20231202140915523

【5】完整代码

/*
函数功能: MQTT订阅/取消订阅数据打包函数
函数参数:topic       主题qos         消息等级 0:最多分发一次  1: 至少分发一次  2: 仅分发一次whether     订阅/取消订阅请求包 (1表示订阅,0表示取消订阅)
返回值: 0表示成功 1表示失败
*/
unsigned char MQTT_SubscribeTopic(char* topic, unsigned char qos, unsigned char whether)
{unsigned char i, j;mqtt_txlen = 0;unsigned int size = 0;unsigned char buff[256];unsigned int topiclen = (int)strlen(topic);unsigned int DataLen = 2 + (topiclen + 2) + (whether ? 1 : 0);//可变报头的长度(2字节)加上有效载荷的长度//固定报头//控制报文类型if (whether)mqtt_txbuf[mqtt_txlen++] = 0x82; //消息类型和标志订阅else	mqtt_txbuf[mqtt_txlen++] = 0xA2;    //取消订阅//剩余长度do{unsigned char encodedByte = DataLen % 128;DataLen = DataLen / 128;// if there are more data to encode, set the top bit of this byteif (DataLen > 0)encodedByte = encodedByte | 128;mqtt_txbuf[mqtt_txlen++] = encodedByte;} while (DataLen > 0);//可变报头mqtt_txbuf[mqtt_txlen++] = 0;			//消息标识符 MSBmqtt_txbuf[mqtt_txlen++] = 0x01;        //消息标识符 LSB//有效载荷mqtt_txbuf[mqtt_txlen++] = BYTE1(topiclen);//主题长度 MSBmqtt_txbuf[mqtt_txlen++] = BYTE0(topiclen);//主题长度 LSB   memcpy(&mqtt_txbuf[mqtt_txlen], topic, topiclen);mqtt_txlen += topiclen;if (whether){mqtt_txbuf[mqtt_txlen++] = qos;//QoS级别}for (i = 0; i < 100; i++){memset(mqtt_rxbuf, 0, mqtt_rxlen);MQTT_SendBuf(mqtt_txbuf, mqtt_txlen);//printf("订阅消息发布成功\n");size = Client_GetData(buff);//从服务器获取数据if (size <= 0){continue;}memcpy(mqtt_rxbuf, buff, size);printf("订阅应答:\r\n");for (j = 0; j < size; j++){printf("%#X ", buff[j]);}printf("\r\n");if (mqtt_rxbuf[0] == parket_subAck[0] && mqtt_rxbuf[1] == parket_subAck[1]) //连接成功			   {return 0;//连接成功}Sleep(1000);}return 1; //失败
}

七、运行项目、连接华为云服务器

7.1 整个项目的完整代码

前面章节陆续已经编写好了重要的函数,那么这里就贴出我编写好的整体的代码,进行运行测试:

image-20231201160401486

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#pragma warning(disable:4996)#include <string.h>
#include <stdio.h>#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>#pragma comment(lib, "ws2_32.lib") //告诉编译器链接Winsock库//---------------------------------------MQTT协议相关的子函数声明-------------------------------------------------------//发布主题
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);//MQTT协议缓冲区初始化
void MQTT_Init(void);//调用底层接口发送数据包
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);//从服务器获取数据//---------------------------------------全局变量定义--------------------------------------------------------------------#define BYTE0(dwTemp)       (*( char *)(&dwTemp))
#define BYTE1(dwTemp)       (*((char *)(&dwTemp) + 1))
#define BYTE2(dwTemp)       (*((char *)(&dwTemp) + 2))
#define BYTE3(dwTemp)       (*((char *)(&dwTemp) + 3))unsigned char mqtt_rxbuf[1024 * 1024];//发送数据缓存区
unsigned char mqtt_txbuf[256];//接收数据缓存区
unsigned int mqtt_rxlen;
unsigned int mqtt_txlen;typedef enum
{//名字 	    值 			报文流动方向 	描述M_RESERVED1 = 0,	//	禁止	保留M_CONNECT,	//	客户端到服务端	客户端请求连接服务端M_CONNACK,	//	服务端到客户端	连接报文确认M_PUBLISH,	//	两个方向都允许	发布消息M_PUBACK,	//	两个方向都允许	QoS 1消息发布收到确认M_PUBREC,	//	两个方向都允许	发布收到(保证交付第一步)M_PUBREL,	//	两个方向都允许	发布释放(保证交付第二步)M_PUBCOMP,	//	两个方向都允许	QoS 2消息发布完成(保证交互第三步)M_SUBSCRIBE,	//	客户端到服务端	客户端订阅请求M_SUBACK,	//	服务端到客户端	订阅请求报文确认M_UNSUBSCRIBE,	//	客户端到服务端	客户端取消订阅请求M_UNSUBACK,	//	服务端到客户端	取消订阅报文确认M_PINGREQ,	//	客户端到服务端	心跳请求M_PINGRESP,	//	服务端到客户端	心跳响应M_DISCONNECT,	//	客户端到服务端	客户端断开连接M_RESERVED2,	//	禁止	保留
}_typdef_mqtt_message;//连接成功服务器回应 20 02 00 00
//客户端主动断开连接 e0 00
const unsigned char parket_connetAck[] = { 0x20,0x02,0x00,0x00 };
const unsigned char parket_disconnet[] = { 0xe0,0x00 };
const unsigned char parket_heart[] = { 0xc0,0x00 };
const unsigned char parket_heart_reply[] = { 0xc0,0x00 };
const unsigned char parket_subAck[] = { 0x90,0x03 };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);int UsernameLen = (int)strlen(Username);int PasswordLen = (int)strlen(Password);unsigned int DataLen;mqtt_txlen = 0;unsigned int size = 0;unsigned char buff[256];//可变报头+Payload  每个字段包含两个字节的长度标识DataLen = 10 + (ClientIDLen + 2) + (UsernameLen + 2) + (PasswordLen + 2);//固定报头//控制报文类型mqtt_txbuf[mqtt_txlen++] = 0x10;		//MQTT Message Type CONNECT//剩余长度(不包括固定头部)do{unsigned char encodedByte = DataLen % 128;DataLen = DataLen / 128;// if there are more data to encode, set the top bit of this byteif (DataLen > 0)encodedByte = encodedByte | 128;mqtt_txbuf[mqtt_txlen++] = encodedByte;} while (DataLen > 0);//可变报头//协议名mqtt_txbuf[mqtt_txlen++] = 0;        	// Protocol Name Length MSB    mqtt_txbuf[mqtt_txlen++] = 4;           // Protocol Name Length LSB    mqtt_txbuf[mqtt_txlen++] = 'M';        	// ASCII Code for M    mqtt_txbuf[mqtt_txlen++] = 'Q';        	// ASCII Code for Q    mqtt_txbuf[mqtt_txlen++] = 'T';        	// ASCII Code for T    mqtt_txbuf[mqtt_txlen++] = 'T';        	// ASCII Code for T    //协议级别mqtt_txbuf[mqtt_txlen++] = 4;        		// MQTT Protocol version = 4    //连接标志mqtt_txbuf[mqtt_txlen++] = 0xc2;        	// conn flags mqtt_txbuf[mqtt_txlen++] = 0;        		// Keep-alive Time Length MSB    mqtt_txbuf[mqtt_txlen++] = 100;        	// Keep-alive Time Length LSB  100S心跳包  mqtt_txbuf[mqtt_txlen++] = BYTE1(ClientIDLen);// Client ID length MSB    mqtt_txbuf[mqtt_txlen++] = BYTE0(ClientIDLen);// Client ID length LSB  	memcpy(&mqtt_txbuf[mqtt_txlen], ClientID, ClientIDLen);mqtt_txlen += ClientIDLen;if (UsernameLen > 0){mqtt_txbuf[mqtt_txlen++] = BYTE1(UsernameLen);		//username length MSB    mqtt_txbuf[mqtt_txlen++] = BYTE0(UsernameLen);    	//username length LSB    memcpy(&mqtt_txbuf[mqtt_txlen], Username, UsernameLen);mqtt_txlen += UsernameLen;}if (PasswordLen > 0){mqtt_txbuf[mqtt_txlen++] = BYTE1(PasswordLen);		//password length MSB    mqtt_txbuf[mqtt_txlen++] = BYTE0(PasswordLen);    	//password length LSB  memcpy(&mqtt_txbuf[mqtt_txlen], Password, PasswordLen);mqtt_txlen += PasswordLen;}for (i = 0; i < 5; i++){memset(mqtt_rxbuf, 0, mqtt_rxlen);MQTT_SendBuf(mqtt_txbuf, mqtt_txlen);size = Client_GetData(buff);//从服务器获取数据if (size <= 0)continue;memcpy(mqtt_rxbuf, buff, size);printf("登录应答:\r\n");for (j = 0; j < size; j++){printf("%#X ", buff[j]);}printf("\r\n");if (mqtt_rxbuf[0] == parket_connetAck[0] && mqtt_rxbuf[1] == parket_connetAck[1]) //连接成功			   {return 0;//连接成功}}return 1;
}/*
函数功能: MQTT订阅/取消订阅数据打包函数
函数参数:topic       主题qos         消息等级 0:最多分发一次  1: 至少分发一次  2: 仅分发一次whether     订阅/取消订阅请求包 (1表示订阅,0表示取消订阅)
返回值: 0表示成功 1表示失败
*/
unsigned char MQTT_SubscribeTopic(char* topic, unsigned char qos, unsigned char whether)
{unsigned char i, j;mqtt_txlen = 0;unsigned int size = 0;unsigned char buff[256];unsigned int topiclen = (int)strlen(topic);unsigned int DataLen = 2 + (topiclen + 2) + (whether ? 1 : 0);//可变报头的长度(2字节)加上有效载荷的长度//固定报头//控制报文类型if (whether)mqtt_txbuf[mqtt_txlen++] = 0x82; //消息类型和标志订阅else	mqtt_txbuf[mqtt_txlen++] = 0xA2;    //取消订阅//剩余长度do{unsigned char encodedByte = DataLen % 128;DataLen = DataLen / 128;// if there are more data to encode, set the top bit of this byteif (DataLen > 0)encodedByte = encodedByte | 128;mqtt_txbuf[mqtt_txlen++] = encodedByte;} while (DataLen > 0);//可变报头mqtt_txbuf[mqtt_txlen++] = 0;			//消息标识符 MSBmqtt_txbuf[mqtt_txlen++] = 0x01;        //消息标识符 LSB//有效载荷mqtt_txbuf[mqtt_txlen++] = BYTE1(topiclen);//主题长度 MSBmqtt_txbuf[mqtt_txlen++] = BYTE0(topiclen);//主题长度 LSB   memcpy(&mqtt_txbuf[mqtt_txlen], topic, topiclen);mqtt_txlen += topiclen;if (whether){mqtt_txbuf[mqtt_txlen++] = qos;//QoS级别}for (i = 0; i < 100; i++){memset(mqtt_rxbuf, 0, mqtt_rxlen);MQTT_SendBuf(mqtt_txbuf, mqtt_txlen);//printf("订阅消息发布成功\n");size = Client_GetData(buff);//从服务器获取数据if (size <= 0){continue;}memcpy(mqtt_rxbuf, buff, size);printf("订阅应答:\r\n");for (j = 0; j < size; j++){printf("%#X ", buff[j]);}printf("\r\n");if (mqtt_rxbuf[0] == parket_subAck[0] && mqtt_rxbuf[1] == parket_subAck[1]) //连接成功			   {return 0;//连接成功}Sleep(1000);}return 1; //失败
}//MQTT发布数据打包函数
//topic   主题 
//message 消息
//qos     消息等级 
unsigned char MQTT_PublishData(char* topic, char* message, unsigned char qos)
{unsigned int topicLength = (int)strlen(topic);unsigned int messageLength = (int)strlen(message);unsigned short id = 0;unsigned int DataLen;mqtt_txlen = 0;printf("上报JSON消息长度:%d\r\n", messageLength);printf("message=%s\r\n", message);//有效载荷的长度这样计算:用固定报头中的剩余长度字段的值减去可变报头的长度//QOS为0时没有标识符//数据长度             主题名   报文标识符   有效载荷if (qos)	DataLen = (2 + topicLength) + 2 + messageLength;else	DataLen = (2 + topicLength) + messageLength;//固定报头//控制报文类型mqtt_txbuf[mqtt_txlen++] = 0x30;    // MQTT Message Type PUBLISH  //剩余长度do{unsigned char encodedByte = DataLen % 128;DataLen = DataLen / 128;// if there are more data to encode, set the top bit of this byteif (DataLen > 0)encodedByte = encodedByte | 128;mqtt_txbuf[mqtt_txlen++] = encodedByte;} while (DataLen > 0);mqtt_txbuf[mqtt_txlen++] = BYTE1(topicLength);//主题长度MSBmqtt_txbuf[mqtt_txlen++] = BYTE0(topicLength);//主题长度LSB memcpy(&mqtt_txbuf[mqtt_txlen], topic, topicLength);//拷贝主题mqtt_txlen += topicLength;//报文标识符if (qos){mqtt_txbuf[mqtt_txlen++] = BYTE1(id);mqtt_txbuf[mqtt_txlen++] = BYTE0(id);id++;}memcpy(&mqtt_txbuf[mqtt_txlen], message, messageLength);mqtt_txlen += messageLength;MQTT_SendBuf(mqtt_txbuf, mqtt_txlen);return mqtt_txlen;
}void MQTT_SendBuf(unsigned char* buf, unsigned short len)
{Client_SendData(buf, len);//发送数据到服务器
}//-----------------------------------------MQTT服务器的参数------------------------------------------------------------
//服务器IP
#define SERVER_IP "117.78.5.125"
#define SERVER_PORT 1883 //端口号//MQTT三元组
#define ClientID "65697df3585c81787ad4da82_stm32_0_0_2023120106"
#define Username "65697df3585c81787ad4da82_stm32"
#define Password "12cc9b1f637da8d755fa2cbd007bb669e6f292e3e63017538b5e6e13eef0cf58"//密文 //订阅主题:
#define SET_TOPIC  "$oc/devices/65697df3585c81787ad4da82_stm32/sys/messages/down"//订阅
//发布主题:
#define POST_TOPIC "$oc/devices/65697df3585c81787ad4da82_stm32/sys/properties/report"//发布//-----------------------------------------主函数------------------------------------------------------------char mqtt_message[1024];//数据缓存区SOCKET connectSocket; //网络套接字
WSADATA wsaData; //创建一个结构体变量,用于存储关于Winsock库的信息double TEMP = 10.0;int main()
{int result = WSAStartup(MAKEWORD(2, 2), &wsaData); //初始化Winsock库,指定版本号2.2,检查返回值if (result != 0){printf("WSAStartup failed: %d\r\n", result);//输出错误信息并退出程序return 1;}connectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建一个TCP套接字,检查返回值if (connectSocket == INVALID_SOCKET){printf("socket failed with error: %d", WSAGetLastError());//输出错误信息并退出程序WSACleanup(); //清除Winsock库return 1;}sockaddr_in service; //创建一个结构体变量,用于存储服务器地址信息service.sin_family = AF_INET; //指定地址族为IPv4inet_pton(AF_INET, SERVER_IP, &service.sin_addr); //将字符串类型的IP地址转换为二进制网络字节序的IP地址,并存储在结构体中service.sin_port = htons(SERVER_PORT); //将端口号从主机字节序转换为网络字节序,并存储在结构体中result = connect(connectSocket, (SOCKADDR*)&service, sizeof(service)); //连接到服务器,检查返回值if (result == SOCKET_ERROR){std::cout << "connect failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序closesocket(connectSocket); //关闭套接字WSACleanup(); //清除Winsock库return 1;}std::cout << "Connected to server." << std::endl; //连接成功,输出消息MQTT_Init();while (1){/*登录服务器*/if (MQTT_Connect((char*)ClientID, (char*)Username, (char*)Password) == 0){break;}// 延时1000毫秒,即1秒Sleep(1000);printf("MQTT服务器登录校验中....\n");}printf("连接成功_666\r\n");//订阅物联网平台数据int stat = MQTT_SubscribeTopic((char*)SET_TOPIC, 1, 1);if (stat){printf("订阅失败\r\n");closesocket(connectSocket); //关闭套接字WSACleanup(); //清除Winsock库return 1;}printf("订阅成功\r\n");/*创建线程*/while (1){sprintf(mqtt_message, "{\"services\": [{\"service_id\": \"stm32\",\"properties\":{\"TEMP\":%.1f}}]}", (double)(TEMP+=0.2));//温度//发布主题MQTT_PublishData((char*)POST_TOPIC, mqtt_message, 0);printf("发布消息成功\r\n");Sleep(5000);}
}/*发送数据到服务器*/
int Client_SendData(unsigned char* buff, unsigned int len)
{int result = send(connectSocket,(const char*)buff, len, 0); //向服务器发送数据,检查返回值if (result == SOCKET_ERROR){std::cout << "send failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序return -1;}return 0;
}/*获取服务器下发数据*/
int Client_GetData(unsigned char* buff)
{int result = recv(connectSocket, (char*)buff,200, 0); //从服务器接收数据,检查返回值if (result == SOCKET_ERROR){std::cout << "recv failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序return -1;}return result;
}

7.2 代码里核心的地方

这里填写MQTT服务器的信息,也就是前面创建华为云IOT服务器得到的信息。

image-20231201160441966

这里是主函数,登录服务器后订阅主题,发布消息。

image-20231201160547892

7.3 编译运行代码

按下Ctrl+F5 运行代码。 弹出控制台窗口之后,可以看到,我们已经连接了华为云MQTT服务器,并且完成数据上传。

image-20231201160621635

7.4 登录华为云IOT云端查看数据

可以看到设备已经在线了。

image-20231201160759752

可以看到我们的消息也在实时的上传。

image-20231201160823360

到此,说明我们的MQTT协议已经封装完成,可以正常的运行了。

八、下发命令的处理

一般MQTT设备端除了上传数据以外,还需要接收MQTT服务器下发的控制命令。

那么我们接下来就完善一下代码,接收华为云MQTT服务器下发的命令,并进行回应。

8.1 添加命令

要测试命令下发,那么首先需要再华为云IOT平台添加命令。

image-20231202180926999

添加一个控制命令。

image-20231202181202901

image-20231202181233447

命令添加完成:

image-20231202181248551

8.2 下发命令测试

注意:下发命令是同步的,设备端必须在线才可以下发命令。

image-20231202182312070

这个下发的命令是有反馈。设备端收到之后,可以向服务器反馈状态,这样服务器才能知道刚才的控制命令确实发送成功了。

设备收到信息之后,上传回应给服务器的主题和内容格式:

Topic:$oc/devices/{device_id}/sys/commands/response/request_id={request_id}
数据格式:  
{"result_code": 0,"response_name": "COMMAND_RESPONSE","paras": {"result": "success"}
}

云端发送控制命令之后,设备收到的消息如下:

$oc/devices/65697df3585c81787ad4da82_stm32/sys/commands/request_id=d49f0bb9-ba87-4c9b-b915-98a1f0fcf689{"paras":{"lock":true},"service_id":"lock","command_name":"锁开关控制"}

其中request_id=d49f0bb9-ba87-4c9b-b915-98a1f0fcf689 就是本次的请求ID。回应的时候需要加上请求ID。服务器才好对应。

以当前设备为例:

发布的主题这样填: $oc/devices/65113d05a559fd7cd41435f8_lock1/sys/commands/response/request_id=ce49181e-7636-4b24-946d-c034ca083c1c发布的内容这样填:
{"result_code":0,"response_name":"COMMAND_RESPONSE","paras":{"result":"success"}}

8.3 编写代码

为了能够实时接收服务器的代码,我们单独增加一个线程来接收服务器的消息。

在主函数里MQTT服务器连接成功之后,增加以下代码:

/*创建线程*/
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ReceiveData, NULL, 0, NULL);
if (hThread == NULL) {printf("CreateThread failed.\n");return 1;
}

编写线程的工作函数:

// 处理服务器下发的数据
void ReceiveData(void)
{// 接收数据char buffer[1024];char request_id[100];char send_cmd[500];int recvSize;while (1) {//等待服务器下发消息recvSize = recv(connectSocket, buffer, 1024, 0);if (recvSize == SOCKET_ERROR){std::cout << "网络断开连接: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序return;}if (recvSize > 0){printf("服务器下发消息:\r\n");//接收下发的数据for (int i = 0; i < recvSize; i++){printf("%c", buffer[i]);}printf("\r\n");//下发指令请求回应给服务器(命令下发)if (strstr((char*)&buffer[5], "sys/commands/request_id=")){char* p = NULL;p = strstr((char*)&buffer[5], "request_id=");if (p){//解析数据//$oc/devices/65697df3585c81787ad4da82_stm32/sys/commands/request_id=6e925cc1-4a8d-4eab-8d85-6c7f15d72189strncpy(request_id, p, 47);}//上报数据sprintf(mqtt_message, "{\"result_code\":0,\"response_name\":\"COMMAND_RESPONSE\",\"paras\":{\"result\":\"success\"}}");sprintf(send_cmd, "$oc/devices/65697df3585c81787ad4da82_stm32/sys/commands/response/%s", request_id);MQTT_PublishData(send_cmd, mqtt_message, 0);printf("(命令)发布主题:%s\r\n", send_cmd);printf("(命令)发布数据:%s\r\n", mqtt_message);}}		}
}

8.4 运行代码测试

先运行客户端的代码,登录MQTT服务器。

image-20231202182720027

然后,在华为云IOT平台下发命令。 如果点击下发之后,右上角弹出了 命令下发成功,就表示我们客户端代码写OK了。

image-20231202182758488

我们看设备端收到的消息:

image-20231202182913279

九、总结

到此,我们的MQTT协议已经开发完成了。如果大家详细阅读了文章,并且跟着步骤操作了一次,相信你此刻对MQTT协议应该有所认识了。我是DS小龙哥,欢迎关注我,后续会有更多的技术文章、项目文章发布。

image-20231202183108654

相关文章:

基于C语言从0开始手撸MQTT协议代码连接标准的MQTT服务器,完成数据上传和命令下发响应(华为云IOT服务器)

文章目录 一、前言二、搭建开发环境三、网络编程基础概念科普3.1 什么是网络编程3.2 TCP 和 UDP协议介绍3.3 TCP通信的实现过程 四、Windows下的网络编程相关API介绍4.1 常用的函数介绍4.2 函数参数介绍4.3 编写代码体验网络编程 五、访问华为云IOT服务器创建一个产品和设备5.2…...

程序员面试中的“八股文”:敲门砖还是绊脚石?

在现代技术行业中&#xff0c;“八股文”成为了程序员面试中的常见问题。“八股文”究竟能否在实际工作中发挥应有的作用&#xff0c;成了一个备受争议的话题。许多IT从业者都提出疑问&#xff1a;程序员面试到底考察的是什么&#xff1f;是工作能力、工作经验&#xff0c;还是…...

液位传感器- 从零开始认识各种传感器【二十四期】

液位传感器|从零开始认识各种传感器 1、什么是液位传感器 &#xff1f; 液位传感器是一种用于检测和测量液体位置和高度的装置&#xff0c;广泛应用于工业、农业、环保和家庭等领域。液位传感器可以实时监测液体的水平&#xff0c;以实现自动化控制和安全防护。 2、液位传感器…...

【c++】爬虫到底违不违法?

很多小伙伴都想知道爬虫到底违法吗&#xff0c;今天博主就给大家科普一下 爬虫本身并不违法&#xff0c;但使用爬虫采集数据可能涉及违法风险&#xff0c;具体取决于采集行为是否侵犯了他人的合法权益&#xff0c;尤其是隐私权和个人信息权。以下是对爬虫是否违法的详细分析&am…...

Python基础知识笔记——特殊符号

1. #&#xff1a;注释符号。在它后面的内容直到行尾都会被 Python 解释器忽略&#xff0c;通常用于添加注释说明代码。 2. &#xff1a;赋值运算符。用于将右侧的值赋给左侧的变量。 3. &#xff1a;等于运算符。用于比较两个值是否相等。 4. !&#xff1a;不等于运算符。用…...

Thinkphp仿华为商城源码/红色风格电脑手机数码商城系统网站源码

Thinkphp仿华为商城&#xff0c;主要实现了商品首页展示、用户意见、商品分类列表、商品搜索、商品详细展示、购物车、订单生成、在线付款、以及个人中心完善个人资料、用户修改收货地址、余额查询、消费查询、订单管理、商品评价、热销商品和最近商品浏览&#xff1b; 后台是…...

超有用的数据恢复方法!你一定不要错过!

无论我们当下所使用的是何种设备&#xff0c;例如电脑、U 盘、硬盘、相机、行车记录仪&#xff0c;都难以避免出现误删文件的情况。那么&#xff0c;这些被误删的数据究竟应当通过何种方式找回&#xff1f; 今日&#xff0c;为大家分享若干极为实用的数据恢复方法&#xff0c;望…...

CDH清理磁盘空间完全攻略和完整实现自动化脚本(大数据清除日志)

在CDH集群中,自动清除日志的意义非常重大。尤其是在内网环境下,运维人员无法随时登录服务器进行操作,或者是因为放长假等原因不能每天进行运维工作。这时,如果日志不自动清理,就会面临日志空间满了的问题,这可能造成CDH各组件无法正常工作,离线数仓计算完全停止。 考虑…...

vulhub:Apache解析漏洞apache_parsing

在Apache1.x/2.x中Apache 解析文件的规则是从右到左开始判断解析&#xff0c;如果后缀名为不可识别文件解析&#xff0c;就再往左判断。如 1.php.xxxxx 漏洞原理 Apache HTTPD 支持一个文件拥有多个后缀&#xff0c;并为不同后缀执行不同的指令。比如如下配置文件 AddType te…...

Raspberry Pi Docker 运行 IRIS

在 Raspberry Pi 上成功安装 Docker 后可以安装 IRIS 数据库。 安装的命令为&#xff1a; docker run --name my-iris -d --publish 1972:1972 --publish 52773:52773 intersystems/irishealth-community:latest-em-linux-arm64v8 注意&#xff0c;我们这里暴露了 2 个端口&a…...

【SQL Server】默认端口与自定义端口

目录 第4章&#xff1a;默认端口与自定义端口 SQL Server 默认端口号 更改 SQL Server 端口号 使用自定义端口的好处 示例&#xff1a;更改 SQL Server 端口为 1434 示例代码&#xff1a;更新连接字符串 安全注意事项 第4章&#xff1a;默认端口与自定义端口 SQL Serve…...

【笔记】Android 驻网(网络注册)状态变化的代码实现

背景 基于 Android U/V 代码。 疑问:注册状态是仅看数据吗?通过hasRegistered 确认?从代码看是data 和voice combine的 。 目标:分析注册过程中,Modem返回的code值得含义,以及MD和AP code的映射关系。 功能说明 网络已注册(驻网成功)跟能上网是两码事,比如驻网了也…...

SAP执行董事会变动

以下是SAP发布的新闻&#xff1a; SAP正在进行重大管理层重组&#xff0c;以配合其持续的云转型战略。首席营收官Scott Russell和首席营销与解决方案官Julia White将卸任其职务。SAP首席执行官Christian Klein将暂时接管销售组织的职责。 Scott Russell在全球范围内帮助SAP客…...

css-grid布局之美

一&#xff0c;grid布局概述 网格布局&#xff08;Grid&#xff09;是最强大的 CSS 布局方案。 它将网页划分成一个个网格&#xff0c;可以任意组合不同的网格&#xff0c;做出各种各样的布局。以前&#xff0c;只能通过复杂的 CSS 框架达到的效果&#xff0c;现在浏览器内置…...

Python面试整理-异常处理

在Python中,异常处理是一种通过捕获和处理运行时错误来使程序更加健壮和稳定的机制。以下是有关Python异常处理的详细介绍: 异常处理基础 try-except 结构 基本的异常处理结构是try-except块。当代码块中的某些代码可能会引发异常时,可以使用try块将其包含起来,并使用excep…...

linux服务之DHCP(centos7.6)

DHCP服务 1. DHCP介绍 DHCP(Dynamic Host Configuration Protocol&#xff0c;动态主机配置协议)&#xff0c;被应用在局域网环境中&#xff0c;主要作用是集中管理、分配IP地址&#xff0c;使网络环境中主机动态的获取IP地址、网关地址、DNS服务器地址等信息&#xff0c;并能…...

2024最新的软件测试面试八股文

&#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 前言 最近有很多粉丝问我&#xff0c;有什么方法能够快速提升自己&#xff0c;通过阿里、腾讯、字节跳动、京东等互联网大厂的面试&#xff0c;我觉得短时间提升…...

C++ STL transform_reduce 用法

一&#xff1a;功能 计算两个向量的内积&#xff0c;它是 std::inner_product 的泛化版本&#xff0c;支持lambda表达式自定义运算。 二&#xff1a;用法 #include <iostream> #include <vector> #include <numeric> #include <execution>int main()…...

MySQL5.7 排序

一、不分组排序 (1).排序-并列数据随机顺序 select col1, col2, rank : rank 1 as rank from (select A as col1,100 as col2union all select B as col1,130 as col2union all select C as col1,120 as col2union all select D as col1,120 as col2order by col2 desc ) a,…...

【Unity】3D功能开发入门系列(一)

Unity3D功能开发入门系列&#xff08;一&#xff09; 一、开发环境&#xff08;一&#xff09;安装 Unity&#xff08;二&#xff09;创建项目&#xff08;三&#xff09;Unity 窗口布局 二、场景与视图&#xff08;一&#xff09;场景&#xff08;二&#xff09;游戏物体&…...

【Python体验】第五天:目录搜索、数据爬虫(评论区里写作业)

文章目录 目录搜索 os、shutil库数据爬虫 request、re作业&#xff1a;爬取案例的top250电影的关键信息&#xff08;名称、类型、日期&#xff09;&#xff0c;并保存在表格中 目录搜索 os、shutil库 os 模块提供了非常丰富的方法用来处理文件和目录。 os.listdir(path)&#x…...

elasticsearch性能调优方法原理与实战

❃博主首页 &#xff1a; 「码到三十五」 &#xff0c;同名公众号 :「码到三十五」&#xff0c;wx号 : 「liwu0213」 ☠博主专栏 &#xff1a; <mysql高手> <elasticsearch高手> <源码解读> <java核心> <面试攻关> ♝博主的话 &#xff1a…...

python print 函数参数:sep 自定义分隔符,end 自定义结尾符

1. 简述 print 函数可以将内容打印到标准输出&#xff0c;如果不指定 end 参数&#xff0c;默认在输出的内容之后加一个 “回车符\n”。 以下是 print 函数常用的参数用法&#xff1a; print(object, …, sepstr, endstr) object, …&#xff1a;要打印的内容&#xff0c;可以…...

git 使用场景

拉取分支 feature 以develop为基础 git checkout -b feature-x develop git checkout demobranch git branch 合并分支 idea 在feature 选 develop 合并到feature...

Ubuntu22.04 Docker更换阿里云镜像

由于运营商网络原因&#xff0c;会导致您拉取Docker Hub镜像变慢&#xff0c;甚至下载失败。那么可以更换阿里云镜像加速器&#xff0c;从而加速官方镜像的下载。 1.获取镜像加速器地址 登录容器镜像服务控制台&#xff0c;在左侧导航栏选择镜像工具 > 镜像加速器&#xf…...

Windows下Rust OpenCV环境配置

首发于Enaium的个人博客 安装Chocolatey 首先我们需要安装Chocolatey&#xff0c;Chocolatey是一个Windows的包管理器。 我们点击右上角的Install进入到Installing Chocolatey&#xff0c;选择Individual 复制命令 Set-ExecutionPolicy Bypass -Scope Process -Force; [Sys…...

PostgreSQL(二十三)TOAST技术

目录 一、TOAST简介 二、TOAST的存储方式 1、存储方式概述 2、实验&#xff1a;创建TOAST表 三、TOAST的4种压缩策略 1、策略说明 2、TOAST表额外的三个字段 四、TOAST表的计算方式 1、说明 2、实验&#xff1a;计算表大小 五、TOAST表的特点 1、优点 2、缺点 3、…...

文章解读与仿真程序复现思路——电力系统自动化EI\CSCD\北大核心《海上风氢系统与沿海电网能量协同优化调度》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…...

MySQL update set语句中 逗号与and的区别

语法 以下是 UPDATE 命令修改 MySQL 数据表数据的通用 SQL 语法&#xff1a; UPDATE table_name SET column1 value1, column2 value2, ... WHERE condition; 参数说明&#xff1a; table_name 是你要更新数据的表的名称。column1, column2, ... 是你要更新的列的名称。v…...

C++面试---小米

一、static 关键字的作用&#xff0c;及和const的区别 static关键字作用&#xff1a; 1、在类的成员变量前使用&#xff0c;表示该变量属于类本身&#xff0c;而不是任何类的实例。 2、在类的成员函数前使用&#xff0c;表示该函数不需要对象实例即可调用&#xff0c;且只能访问…...

淘宝客领券网站怎么做/百度公司网站推广怎么做

有一个字符串&#xff0c;每次只能从两端取&#xff0c;求取得的字典序最小的子符序列。 每次比较剩余字符串的正向和反向字符串的大小&#xff0c;正向大就在尾取&#xff0c;否则在头取&#xff0c;相当于贪心了吧。 View Code 1 program pku3617(input,output); 2 var 3 …...

新办公司网上核名在哪个网站做/百度站长工具怎么关闭教程视频

原文地址为&#xff1a; 从Matlab的for循环说开……因为学习和工作的原因&#xff0c;最近又开始使用已经许久没有接触的Matlab。在没有什么特殊考虑的情况下&#xff0c;信手写下了下面的m代码片段&#xff1a; 1 for i 1 : 1 :(imgHeight - tmpHeight 1 ) 2 for j 1 : 1…...

海南论坛网站建设/企业推广的网站

Visual Studio Team System 2008 Team Suite&#xff08;90 天试用版&#xff09; Visual Studio Team System 2008 Team Suite 可为多种团队成员角色提供一组集成工具&#xff0c;以便进行应用程序的架构、设计、开发、数据库开发以及测试。版本&#xff1a;trial发布日期&…...

seo推广手段/seo营销推广服务公司

Java从入门到精通——字符串 1 获取字符串信息 已知字符串&#xff1a; String str new String(); 1.1 获取字符串长度 str.length(); 1.2 字符串查找 str.indexOf(substr);//返回substr在str中首次出现的索引位置&#xff0c;若没有&#xff0c;则返回-1 str.lastInde…...

wordpress主题进的慢/深圳全网营销推广平台

DELETE FROM table_name t where t......; 报1064 错误&#xff0c;原因MySQL 中delete 语句不能给表名起别名。 另外。如果记录不存在&#xff0c;delete from 不会报错&#xff01; 麻蛋&#xff0c;烦了快一整天转载于:https://www.cnblogs.com/nankeyimengningchenlun/p/9…...

做会计网站的流程图/如何优化网络

2.6 相对和绝对路径 绝对路径: 从根开始的路径,例如下图中的 /etc/sysconfig/network-scripts/ 就是一个绝对路径:相对路径: 相对于当前目录所在位置的路径,例如下图,.ssh目录位于当前目录下,通过cd .ssh可进入该目录, 其中的.ssh就是相对路径:区别:不管当前在何目录下,通过绝对…...