接下来介绍C语言实现MQTT的源代码文件。
数据发送缓冲区
static char MQTTSendBuff[MQTT_BUFF_SIZE] = {0};
定义一个数据发送缓冲区,用来存储需要发送的数据,其中宏定义MQTT_BUFF_SIZE 在头文件中已定义,因为该缓冲区只在该C文件中使用,所以可以加上static 关键字。
接口
发送数据
这是客户端向服务端发送数据的接口,需要我们根据自己平台来实现该函数。
static char MQTTSendData(const char *data, unsigned int length)
{
}
毫秒级延时
static void MQTTDelayms(unsigned int ms)
{
}
接收数据
这是客户端接收到服务端的数据所要调用的函数,请根据自己的需要更改完善函数,比如我们接收到一个开关量的数据a,这时候就需要根据需要添加代码。
void MQTTReceiveData(const 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;
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);
}
模块化函数
将数据写入到MQTT的数据发送缓冲区
这是将数据写入到MQTT的数据发送缓冲区MQTTSendBuff ,其中包括了写入字符和实数。
static void MQTTSendDataToBuff(void *data, unsigned int n, DataType_t dataType)
{
if(data == NULL)
return;
if(dataType == NUM)
data = (char*)data + n - 1;
while(n--)
{
*mqtt.sendBuffPointNow++ = *(char*)data;
data = (char*)data + dataType;
}
}
这里解释一下数量类型类型DataType_t 。
typedef enum
{
NUM = -1,
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数据发送缓冲区,因为编码剩余长度先得到是低字节的数据,而我们是高字节在前发送出去,故需要反转。
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;
}
解码剩余长度
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) {
return;
}
}
while ((encodedByte & 128) != 0);
mqtt.remainLength = value;
}
初始化函数
主要是初始化MQTT的结构体变量mqtt,把数据发送缓冲区偏移5个字节来存储报头和剩余长度,因为剩余长度需要根据可变报头和有效载荷才能计算。
void MQTTInit(void)
{
mqtt.messageType = 0;
mqtt.sendBuff = MQTTSendBuff + 4;
mqtt.sendBuffPointNow = MQTTSendBuff + 5;
mqtt.resultCode = MQTT_RESULT_CODE_INIT;
mqtt.returnData = NULL;
}
功能函数
这里我只介绍某些函数,比如CONNECT连接服务端函数。
CONNECT连接服务端函数
连接服务端函数,先初始化结构体,存储指向CONNACK结构体变量的指针的值,根据写入固定报头,可变报头,有效载荷,之后等待服务端响应,在客户端接收到服务端函数MQTTReceiveData 中判断连接状态,如果没有等待超时,则返回0。我们可以根据变量s2来判断连接状态并做出响应动作比如错误处理或者重新连接。
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, NUM);
MQTTSendDataToBuff("MQTT", 4, CHAR);
MQTTSendDataToBuff(&temp, 1, NUM);
MQTTSendDataToBuff(&s1->connectFlag, 1, NUM);
MQTTSendDataToBuff(&s1->keepAliveTime, 2, NUM);
temp = strlen(s1->clientID);
MQTTSendDataToBuff(&temp, 2, NUM);
MQTTSendDataToBuff((void*)s1->clientID, temp, CHAR);
temp = strlen(s1->userName);
MQTTSendDataToBuff(&temp, 2, NUM);
MQTTSendDataToBuff((void*)s1->userName, temp, CHAR);
temp = strlen(s1->password);
MQTTSendDataToBuff(&temp, 2, NUM);
MQTTSendDataToBuff((void*)s1->password, temp, CHAR);
codeRemainLengthAndSendMessageType();
MQTTSendData(mqtt.sendBuff, mqtt.sendBuffPointNow - mqtt.sendBuff);
temp = 0;
while(mqtt.resultCode == MQTT_RESULT_CODE_INIT)
{
MQTTDelayms(1);
temp++;
if(temp > MQTT_MAX_TIMES_OUT)
{
return 1;
}
}
return 0;
}
PUBLISH发布消息函数
客户端发布消息至服务端。调用模块化函数来编写,记得初始化MQTT结构体变量。
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, NUM);
MQTTSendDataToBuff((void*)s1->topic, temp, CHAR);
if(s1->QoS)
{
MQTTSendDataToBuff(&s1->messageID, 2, NUM);
}
temp = strlen(s1->payload);
MQTTSendDataToBuff((void*)s1->payload, temp, CHAR);
codeRemainLengthAndSendMessageType();
MQTTSendData(mqtt.sendBuff, mqtt.sendBuffPointNow - mqtt.sendBuff);
if(!s1->QoS)
return 0;
temp = 0;
while(mqtt.resultCode == MQTT_RESULT_CODE_INIT)
{
MQTTDelayms(1);
temp++;
if(temp > MQTT_MAX_TIMES_OUT)
{
return 1;
}
}
return 0;
}
就贴上2个函数的代码,其他的函数差不多。
连接阿里云
CONNECT连接服务端
连接标志
保活时间
查阅阿里云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();
至此,可以得到
void CONNECT(void)
{
MQTTConnectStruct_t s1 = {0};
MQTTConnACKStruct_t s2 = {0};
s1.connectFlag = 0xC2;
s1.keepAliveTime = 300;
s1.clientID = CLIENT_ID;
s1.userName = USER_NAME;
s1.password = PASSWORD;
MQTTConnect(&s1, &s2);
}
PUBLISH发布消息
可变报头按顺序包含主题名和报文标识符。 只有当 QoS 等级是 1 或 2 时,报文标识符(Packet Identifier) 字段才能出现在 PUBLISH 报文中,为了简单,QoS 等级为 0。 查阅阿里云帮助文档,获得主题名格式与有效载荷格式。
void PUBLSH(void)
{
MQTTPublishStruct_t s1;
MQTTPubACKStruct_t s2;
s1.DUP = 0;
s1.QoS = 0;
s1.RETAIN = 0;
s1.topic = TOPIC;
s1.payload = PAYLOAD;
MQTTPublish(&s1, &s2);
}
其他的功能函数的使用方式类似,就不举例了。
后续
不足与改进
本次用C语言编写MQTT协议,并没有全部实现,这里我只是根据自己的需求编写,可能有考虑不到的地方,欢迎大家批评指正;后续可以添加错误代码,错误回调函数,我们可以根据不同的错误代码做出不同的解决措施。
C语言实现MQTT协议(一)协议讲解
C语言实现MQTT协议(二)头文件介绍
C语言实现MQTT协议(三)源代码介绍及连接阿里云
源代码文件下载链接
|