基于C语言从0开始手撸MQTT协议代码连接标准的MQTT服务器,完成数据上传和命令下发响应(华为云IOT服务器)
一、前言
近年来,物联网的发展如火如荼,已经渗透到我们生活的方方面面。从智能家居到工业自动化,从智慧城市到智慧农业,物联网正在以前所未有的速度改变着我们的生活。 大家现在可能已经习惯了通过手机控制家里的灯光、空调和电视,这就是物联网在智能家居领域的应用,如果在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/
因为我这里只需要用到C++和C语言编程,那么安装的时候可以自己选择需要安装的包。
安装好之后,创建项目。
三、网络编程基础概念科普
如果是老手了,这章节可以直接忽略。 如果对网络编程是 0基础 的小伙伴,那么就认真看一下,了解下基本知识。
3.1 什么是网络编程
网络编程是通过使用IP地址和端口号等网络信息,使两台以上的计算机能够相互通信,按照规定的协议交换数据的编程方式。
在网络编程中,程序员使用各种协议和技术,使得不同的设备可以通过网络进行数据交换和信息共享。
要实现网络编程,程序员需要了解并掌握各种网络通信协议,比如TCP/IP协议族,包括TCP、UDP、IP等,这些协议是实现设备间通信的基础。网络编程内部涉及到数据的打包、组装、发送、接收、解析等一系列过程,以实现信息的正确传输。
在TCP/IP协议族中,TCP和UDP是位于IP协议之上的传输层协议。 在OSI模型中,传输层是第四层,负责总体数据传输和数据控制,为会话层等高三层提供可靠的传输服务,为网络层提供可靠的目的地点信息。在TCP/IP协议族中,TCP和UDP正是位于这一层的协议。
这篇文章主要介绍 TCP 和 UDP 协议 以及 使用方法。
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包,此时连接建立成功。建立连接后,数据传输就可以开始了。
四、Windows下的网络编程相关API介绍
因为当前文章是在Windows下介绍MQTT协议,要用到网络编程的知识,需要使用Windows系统提供的API完成网络编程。Windows本身就有一套原生的网络编程接口可以直接使用。 在Linux系统下也是一样,都有自己一套原生的网络编程接口。
如果没有接触这些API的小伙伴不用慌~~~。 你至少用过C语言里的printf、scanf、strlen之类的函数吧? 下面介绍的这些网络编程API函数其实和它们没什么区别,都是普通的函数,功能不一样而已。 对你来说,只是多学了几个库函数,只要了解每个函数的功能就可以调用了。
那么接下来就学习一下常用的网络编程相关的函数。
微软的官方文档地址:https://learn.microsoft.com/zh-cn/windows/win32/api/_winsock/
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结构的指针,包含要绑定的本地地址信息。
- namelen:name结构的长度。
(4)listen:
int listen(SOCKET s, int backlog);
- s:要监听的套接字。
- backlog:等待连接队列的最大长度。
(5)accept:
SOCKET accept(SOCKET s, struct sockaddr* addr, int* addrlen);
- s:监听套接字。
- addr:用于存储客户端地址信息的sockaddr结构。
- addrlen:addr结构的长度。
(6)connect:
int connect(SOCKET s, const struct sockaddr* name, int namelen);
- s:要连接的套接字。
- name:指向目标地址信息的sockaddr结构指针。
- namelen:name结构的长度。
(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结构指针。
- tolen:to结构的长度。
(10)recvfrom:
int recvfrom(SOCKET s, char* buf, int len, int flags, struct sockaddr* from, int* fromlen);
- s:要接收数据的套接字。
- buf:用于存储接收数据的缓冲区。
- len:要接收的数据长度。
- flags:额外选项。
- from:用于存储发送方地址信息的sockaddr结构指针。
- fromlen:from结构的长度。
(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:用于接收主机名的缓冲区。
- namelen:name缓冲区的长度。
4.3 编写代码体验网络编程
上面了解了这些函数,可能不知道如何使用。 这里就写一个例子,以TCP客户端的身份去连接TCP服务器,完成数据传输。
**下面代码实现一个TCP客户端,连接到指定的服务器并完成通信。 ** 可以直接将代码贴到你的工程里,运行,体验效果。
#include #include #include #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 std::cout std::cout std::cout std::cout "device_id": "65697df3585c81787ad4da82_stm32", "secret": "12345678" } "services": [{"service_id": "stm32","properties":{"TEMP":36.2}}]} unsigned char encodedByte = DataLen % 128; DataLen = DataLen / 128; // if there are more data to encode, set the top bit of this byte if (DataLen 0) encodedByte = encodedByte | 128; mqtt_txbuf[mqtt_txlen++] = encodedByte; } while (DataLen 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; } std::cout 0x20,0x02}; if (mqtt_rxbuf[0] == parket_connetAck[0] && mqtt_rxbuf[1] == parket_connetAck[1]) //连接成功 { return 0;//连接成功 } 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 byte if (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 0); 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++; } 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; //指定地址族为IPv4 inet_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 /*登录服务器*/ 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 int result = recv(connectSocket, (char*)buff,200, 0); //从服务器接收数据,检查返回值 if (result == SOCKET_ERROR) { std::cout device_id}/sys/commands/response/request_id={request_id} 数据格式: { "result_code": 0, "response_name": "COMMAND_RESPONSE", "paras": { "result": "success" } } "paras":{"lock":true},"service_id":"lock","command_name":"锁开关控制"} "result_code":0,"response_name":"COMMAND_RESPONSE","paras":{"result":"success"}} printf("CreateThread failed.\n"); return 1; } // 接收数据 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 printf("服务器下发消息:\r\n"); //接收下发的数据 for (int i = 0; i
- name:要查询的主机名。
- s:要关闭的套接字。