赞
踩
接下来介绍C语言实现MQTT的源代码文件。
static char MQTTSendBuff[MQTT_BUFF_SIZE] = { 0 };
定义一个数据发送缓冲区,用来存储需要发送的数据,其中宏定义MQTT_BUFF_SIZE
在头文件中已定义,因为该缓冲区只在该C文件中使用,所以可以加上static
关键字。
这是客户端向服务端发送数据的接口,需要我们根据自己平台来实现该函数。
这里我给出2个demo。
demo1
将数据发送到控制台窗口中
/** \brief 发送数据-----接口 示例 * * \param data 指向需要发送的数据的指针 * \param length 数据的长度 单位字节 * \return 返回1则发送成功,返回0则发送失败 * */ static int Demo_MQTTSendDataToServer(const char* data, unsigned int length) { /* 这里使用了C语言库函数实现该接口 如果要自己实现请把下面的代码删除, 并将lMQTT.h文件中头文件 #include <stdio.h> #include <time.h> 去除 */ unsigned char temp = 0; while (length--) { temp = *data++; printf("%02X ", temp); } return 1; }
demo2
通过TCP/IP发送数据。
/** \brief 发送数据-----接口 示例 * * \param data 指向需要发送的数据的指针 * \param length 数据的长度 单位字节 * \return 返回1则发送成功,返回0则发送失败 * */ static int Demo_MQTTSendDataToServer(const char* data, unsigned int length) { /* 这里使用了C语言库函数实现该接口 如果要自己实现请把下面的代码删除, 并将lMQTT.h文件中头文件 #include <time.h> #include <windows.h> #pragma comment (lib, "ws2_32.lib") //加载 ws2_32.dll 去除 */ if (send(sock, data, length, 0) > 0) { return 1; } return 0; }
实现毫秒级延时
/** \brief 延时毫秒函数-----接口 示例 * * \param ms 毫秒数 * \return 无 * */ static void Demo_MQTTDelayms(unsigned int ms) { /* 这里使用了C语言库函数实现该接口 如果要自己实现请把下面的代码删除, 并将lMQTT.h文件中头文件 #include <stdio.h> #include <time.h> 去除 */ clock_t start = clock(); while (clock() - start < ms); }
这是客户端接收到服务端的数据所要调用的函数,请根据自己的需要更改完善函数,比如我们接收到一个开关量的数据a,这时候就根据需求添加代码。
/** \brief 从服务器接收到数据-----接口 示例 * (未写QoS = 1/2的响应代码和收到来自服务器的数据并解析数据,这个需要结合自己的情景需要修改代码) * * \param data 指向接收到的数据的指针 * \param length 数据的长度 单位字节 * \return 无 * */ void Demo_MQTTReceiveDadaFromServer(unsigned char* data, unsigned int length) { switch (*data) { /* 确认连接请求 */ case MQTT_CONNACK: //避免野指针 if (mqtt.returnData) { //获取当前会话标志 ((MQTTConnACKStruct_t*)mqtt.returnData)->sp = *(data + 2); //获取连接返回码 ((MQTTConnACKStruct_t*)mqtt.returnData)->code = *(data + 3); } mqtt.resultCode = ~MQTT_RESULT_CODE_INIT; break; /* 确认发布消息请求(未写QoS = 1/2的响应代码) */ case MQTT_PUBACK: case MQTT_PUBREC: case MQTT_PUBREL: case MQTT_PUBCOMP: mqtt.resultCode = ~MQTT_RESULT_CODE_INIT; break; /* 订阅确认 */ case MQTT_SUBACK: //避免野指针 if (mqtt.returnData) { ((MQTTSubACKStruct_t*)mqtt.returnData)->messageID = *(data + 2) * 256 + *(data + 3); ((MQTTSubACKStruct_t*)mqtt.returnData)->code = *(data + 4); } mqtt.resultCode = ~MQTT_RESULT_CODE_INIT; break; /* 取消订阅确认 */ case MQTT_UNSUBACK: //避免野指针 if (mqtt.returnData) { ((MQTTUnsubACKStruct_t*)mqtt.returnData)->messageID = *(data + 2) * 256 + *(data + 3); } mqtt.resultCode = ~MQTT_RESULT_CODE_INIT; break; /* 心跳响应 */ case MQTT_PINGRESP: mqtt.resultCode = ~MQTT_RESULT_CODE_INIT; break; /* 在这里添加自己的业务代码 */ default: break; } //清空缓冲区 memset((void*)data, 0, length); }
注意一下:
函数参数data
是unsigned char *
类型
但是MQTT_SUBACK(0x90)订阅响应报文,MQTT_UNSUBACK(0xB0)取消订阅响应报文等等报文,在switch代码块里,这些枚举类型是被当成int
类型,如MQTT_SUBACK(0x90)等于十进制数144,假如我们把函数参数data
定义为char *
类型,data 16进制数为MQTT_SUBACK(0x90),但当我们取值时(0x90)被解释为char
类型的数故*data
等于-122,故我们需要将函数参数data
定义为unsigned char *
类型
或者我们可以在每一个case
后加一个强制类型转换,如
/* 确认连接请求 */ case (char)MQTT_CONNACK break; /* 确认发布消息请求(未写QoS = 1/2的响应代码) */ case (char)MQTT_PUBACK: case (char)MQTT_PUBREC: case (char)MQTT_PUBREL: case (char)MQTT_PUBCOMP: break; /* 订阅确认 */ case (char)MQTT_SUBACK: break; /* 取消订阅确认 */ case (char)MQTT_UNSUBACK: break; /* 心跳响应 */ case (char)MQTT_PINGRESP: break; default: break;
这是将数据写入到MQTT的数据发送缓冲区MQTTSendBuff
,其中包括了写入字符和实数。
/** \brief 将数据写入MQTT的发送缓冲区 * \param data 指向要写入的数据源,类型强制转换为 void* 指针。 * \param n 要被写入的字节数。 * \param dataType 说明数据源 data 的数据类型,如果是字符则传入CHAR(1),不是则传入NUM(0)。 * \return 无。 */ static void MQTTSendDataToBuff(void* data, unsigned int n, DataType_t dataType) { if (data == NULL) return; if (dataType == MQTT_NUM) data = (char*)data + n - 1; while (n--) { *mqtt.sendBuffPointNow++ = *(char*)data; data = (char*)data + dataType; } }
这里解释一下数量类型类型DataType_t
。
/* 数据类型枚举 */
typedef enum
{
MQTT_NUM = -1, //数据是实数
MQTT_CHAR = 1, //数据是字符
}DataType_t;
为什么实数的枚举值是-1
,而字符的枚举值是1
。
首先mqtt.sendBuffPointNow
指针指向的是MQTT数据发送缓冲区的下一个要写入的地址。
在小端模式下,发送字符串的时,传入的字符指针指向的是第一个字符的地址,如字符串char *str = Hello;
str
指向的是字符H
的地址,当我们调用这个函数的时候情况就如下图,每写入一个字符后,mqtt.sendBuffPointNow
会指向下一个地址,str
也就是函数中变量data
也会自增,指向下一个要写入的字符的地址,与我们所期望的情况是一致。
mqtt.sendBuffPointNow | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
str | H | e | l | l | o |
但是当我们发送实数的时候,在小端模式下,如short s = 0x1122;
&s
指向的是低字节的数据的地址。
变量s在内存的存储情况:
地址值 | 数据 |
---|---|
0x00000000&s | 0x22 |
0x00000001((char*)&s) + 1 | 0x11 |
假如我们不加调整。就会出现下图所示的情况。
mqtt.sendBuffPointNow | 0 | 1 |
---|---|---|
s | 22 | 11 |
这与我们期望的情况正好相反,因为MQTT协议是先发高字节,高字节数据在前。
所以我们需要将函数中变量data
偏移至指向最高高字节数据的地址,然后每写入一个字节后data
自减,这样才能符号我们期望的情况。
根据剩余长度规则,编写了编码函数。
其中定义一个数组先暂存已经编码的剩余长度,然后反向将数组写入到MQTT数据发送缓冲区,因为编码剩余长度先得到是低字节的数据,而我们是高字节在前发送出去,故需要反转。
/** \brief 编码剩余长度并向发送缓冲区写入报文的类型 * * \param 无 * \return 无 * */ static void codeRemainLengthAndSendMessageType(void) { char temp[4] = { 0 }; unsigned char length = 0; char encodedByte = 0; unsigned int X = 0; //计算剩余长度 X = mqtt.sendBuffPointNow - mqtt.sendBuff - 1; //编码 do { encodedByte = X % 128; X = X / 128; if (X > 0) { encodedByte = encodedByte | 128; } //得到已编码的剩余长度 temp[length] = encodedByte; //记录编码的剩余长度占用的字节数 length++; } while (X > 0); //反转 while (length--) { *mqtt.sendBuff = temp[length]; mqtt.sendBuff--; } //写入报文的类型 *mqtt.sendBuff = mqtt.messageType; }
/** \brief 解码剩余长度 * * \param data 指向已编码的剩余长度数组的首个元素的指针 * \return 无 * */ void decodeRemainLength(const char* data) { unsigned int multiplier = 1; unsigned int value = 0; unsigned char encodedByte = 0; do { encodedByte = *data++; value += (encodedByte & 127) * multiplier; multiplier *= 128; if (multiplier > 128 * 128 * 128) { // throw Error(Malformed Remaining Length) // error return; } } while ((encodedByte & 128) != 0); //得到已解码的剩余长度 mqtt.remainLength = value; }
主要是初始化MQTT的结构体变量mqtt,把数据发送缓冲区偏移5个字节来存储报头和剩余长度,因为剩余长度需要根据可变报头和有效载荷才能计算。
/** \brief MQTT初始化 * * \param 无 * \return 无 * */ void MQTTInit(void) { mqtt.messageType = 0; mqtt.sendBuff = MQTTSendBuff + 4; mqtt.sendBuffPointNow = MQTTSendBuff + 5; mqtt.resultCode = MQTT_RESULT_CODE_INIT; mqtt.returnData = NULL; if (mqtt.MQTTSendDataToServer == NULL) { mqtt.MQTTSendDataToServer = Demo_MQTTSendDataToServer; } if (mqtt.MQTTDelayms == NULL) { mqtt.MQTTDelayms = Demo_MQTTDelayms; } if (mqtt.MQTTReceiveDadaFromServer == NULL) { mqtt.MQTTReceiveDadaFromServer = Demo_MQTTReceiveDadaFromServer; } }
这里我只介绍某些函数,比如CONNECT连接服务端函数。
连接服务端函数,先初始化结构体,存储指向CONNACK结构体变量的指针的值,根据写入固定报头,可变报头,有效载荷,之后等待服务端响应,在客户端接收到服务端函数MQTTReceiveData
中判断连接状态,如果没有等待超时,则返回0。我们可以根据变量s2来判断连接状态并做出响应动作比如错误处理或者重新连接。
/** \brief MQTT连接报文 * * \param s1 指向连接报文结构体的指针 * \param s2 指向确认连接请求结构体的指针 * \return 返回 0 连接成功 * 返回 1 连接超时,等待连接的时间已经超过最大超时等待时间(MQTT_MAX_TIMES_OUT) * */ char MQTTConnect(MQTTConnectStruct_t* s1, MQTTConnACKStruct_t* s2) { unsigned short temp = 0; MQTTInit(); mqtt.returnData = s2; //报文的类型 mqtt.messageType = MQTT_CONNECT; /* 可变报头 */ //协议名 temp = 0x0004; MQTTSendDataToBuff(&temp, 2, MQTT_NUM); MQTTSendDataToBuff("MQTT", 4, MQTT_CHAR); //协议级别 MQTTSendDataToBuff(&temp, 1, MQTT_NUM); //连接标志 MQTTSendDataToBuff(&s1->connectFlag, 1, MQTT_NUM); //保持连接时间 MQTTSendDataToBuff(&s1->keepAliveTime, 2, MQTT_NUM); /* 有效载荷 */ //客户端标识长度 temp = strlen(s1->clientID); MQTTSendDataToBuff(&temp, 2, MQTT_NUM); //客户端标识 MQTTSendDataToBuff((void*)s1->clientID, temp, MQTT_CHAR); //用户名长度 temp = strlen(s1->userName); MQTTSendDataToBuff(&temp, 2, MQTT_NUM); //用户名 MQTTSendDataToBuff((void*)s1->userName, temp, MQTT_CHAR); //密码长度 temp = strlen(s1->password); MQTTSendDataToBuff(&temp, 2, MQTT_NUM); //密码 MQTTSendDataToBuff((void*)s1->password, temp, MQTT_CHAR); /* 编码剩余长度 */ codeRemainLengthAndSendMessageType(); /* 计算需要发送的数据的长度并将数据发送到服务器 */ mqtt.MQTTSendDataToServer(mqtt.sendBuff, mqtt.sendBuffPointNow - mqtt.sendBuff); /* 等待服务器响应 */ temp = 0; while (mqtt.resultCode == MQTT_RESULT_CODE_INIT) { mqtt.MQTTDelayms(1); temp++; if (temp > MQTT_MAX_TIMES_OUT) { return 1;//超时返回 } } return 0; }
客户端发布消息至服务端。调用模块化函数来编写,记得初始化MQTT结构体变量。
/** \brief MQTT发布消息(可结合自己的情景修改) * * \param s1 指向发布消息结构体的指针 * \param s2 指向发布确认结构体的指针(如果QoS = 0,则可以将传入NULL给s2) * \return 返回 0 成功 * 返回 1 连接超时,等待连接的时间已经超过最大超时等待时间(MQTT_MAX_TIMES_OUT) * */ char MQTTPublish(MQTTPublishStruct_t* s1, MQTTPubACKStruct_t* s2) { unsigned short temp = 0; MQTTInit(); mqtt.returnData = s2; //报文的类型 mqtt.messageType = MQTT_PUBLISH; mqtt.messageType |= s1->RETAIN; mqtt.messageType |= s1->QoS << 1; mqtt.messageType |= s1->DUP << 3; /* 可变报头 */ //主题长度 temp = strlen(s1->topic); MQTTSendDataToBuff(&temp, 2, MQTT_NUM); //主题 MQTTSendDataToBuff((void*)s1->topic, temp, MQTT_CHAR); //报文标识符 if (s1->QoS) { MQTTSendDataToBuff(&s1->messageID, 2, MQTT_NUM); } /* 有效载荷 */ temp = strlen(s1->payload); MQTTSendDataToBuff((void*)s1->payload, temp, MQTT_CHAR); /* 编码剩余长度 */ codeRemainLengthAndSendMessageType(); /* 计算需要发送的数据的长度并将数据发送到服务器 */ MQTTSendData(mqtt.sendBuff, mqtt.sendBuffPointNow - mqtt.sendBuff); //QoS = 0 服务器没有响应动作 if (!s1->QoS) return 0; /* 等待服务器响应 */ temp = 0; while (mqtt.resultCode == MQTT_RESULT_CODE_INIT) { mqtt.MQTTDelayms(1); temp++; if (temp > MQTT_MAX_TIMES_OUT) { return 1;//超时返回 } } return 0; }
就贴上2个函数的代码,其他的函数差不多。
查阅阿里云MQTT协议规范,由于使用阿里云平台必须使用用户名和密码登录,所以位7和位6为1,位5到位2为遗嘱标志,阿里云平台不支持,都为0,位1为清理会话功能,此处设为1,位0保持为0,所以连接标志位为0xC2。
我们设置为300秒。
CONNECT 报文的有效载荷(payload)包含一个或多个以长度为前缀的字段,可变报头中的标志决定是否 包含这些字段。如果包含的话,必须按这个顺序出现:客户端标识符,遗嘱主题,遗嘱消息,用户名,密码。根据标志位来确定这些字段是否出现。
连接标志为C2也就是只有用户名和密码。
根据MQTT-TCP连接通信文档,得到客户端标识符,用户名,密码格式。
假设clientId = 12345,deviceName = device, productKey = pk, timestamp = 789,signmethod=hmacsha1,deviceSecret=secret,那么使用TCP方式提交给MQTT的参数如下:
mqttclientId=12345|securemode=3,signmethod=hmacsha1,timestamp=789|
mqttUsername=device&pk
mqttPassword=hmacsha1("secret","clientId12345deviceNamedeviceproductKeypktimestamp789").toHexString();
至此,可以得到
int Connect(void)
{
/* MQTT连接报文 */
MQTTConnectStruct_t s1 = { 0 };
MQTTConnACKStruct_t s2 = { 0 };
s1.connectFlag = 0xC2;//连接标志
s1.keepAliveTime = MQTT_KEEP_ALIVE;//保活时间
s1.clientID = CLIENTID;//填入你的客户端标识符
s1.userName = USERNAME;//你的用户名标识符
s1.password = PASSWORD;//你的密码
return MQTTConnect(&s1, &s2);//连接阿里云
}
可变报头按顺序包含主题名和报文标识符。
只有当 QoS 等级是 1 或 2 时,报文标识符(Packet Identifier) 字段才能出现在 PUBLISH 报文中,为了简单,QoS 等级为 0。
查阅阿里云帮助文档,获得主题名格式与有效载荷格式。
int Publish(void)
{
MQTTPublishStruct_t s1 = { 0 };
MQTTPubACKStruct_t s2 = { 0 };
s1.DUP = 0;
s1.QoS = 0;//QoS = 0 故没有报文标识符 与 服务端不会发送PUBLISHACK
//一般来说QoS = 0已经够用了,再加上阿里云有 post_reply 主题就能知道发布消息是否成功
s1.RETAIN = 0;
//主题
s1.topic = TOPIC;//填写你的TOPIC
//有效载荷
s1.payload = PAYLOAD;//填写你的有效载荷
return MQTTPublish(&s1, &s2);
}
其他的功能函数的使用方式类似,就不举例了。
用Visual Studio写的一个demo
测试结果
可以看见,
Connect = 0
Publish = 0
Subscribe = 0
UnSubscribe = 0
Ping = 0
证明功能正常,测试通过。
最后这个与服务端通信时发生异常
请忽略。
有需要的可以下拉到文章末尾下载demo工程。
本次用C语言编写MQTT协议,并没有全部实现,这里我只是根据自己的需求编写,可能有考虑不到的地方,欢迎大家批评指正;后续可以添加错误代码,错误回调函数,我们可以根据不同的错误代码做出不同的解决措施。
源代码下载链接
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。