W5500如何通过 MQTT协议连接阿里�
关键词:W5500通过MQTT协议连接阿里� W5500 WIZnet
简�
1� 开发环境与连接平台�
本文主要介绍如何通过MQTT协议将W5500设备连接到阿里云IoT平台,并通过MQTT协议实现通信。MQTT协议是基于TCP的协议,所以我们只需要在单片机端实现TCP客户端代码之后就很容易移植MQTT了,W5500实现TCP客户端的代码我们以前已经实现过,程序下载地址为(http://www.w5500.com/�
- 软件环境:Windows
- 硬件环境:STM32F103+W5500
- 开发工具:Keil uVision5
- 调试工具:Wireshark、串口调试助�
- 连接平台:阿里云-华东2节点(https://www.aliyun.com�
2� MQTT简介:
MQTT官网地址:(http://mqtt.org/�
1) MQTT协议特点
MQTT是一个基于客户端-服务器的消息发布/订阅传输协议。MQTT协议是轻量、简单、开放和易于实现的,这些特点使它在很多情况下甚至受限的环境中得到很多的应用;如:机器与机器(M2M)通信和物联网(IoT)等。并且其在卫星链路通信传感器、偶尔拨号的医疗设备、智能家居、以及一些小型化设备中也被广泛使用�
MQTT协议当前版本(截止到本文章编辑前)为2014年发布的MQTT v3.1.1。除标准版外,还有一个简化版MQTT-SN,该协议主要针对嵌入式设备,这些设备一般工作于百TCP/IP网络,如:ZigBee�
MQTT协议运行在TCP/IP或其他网络协议,提供有序、无损、双向连接。其特点包括:使用的发布/订阅消息模式,它提供了一对多消息分发,以实现与应用程序的解耦。对负载内容屏蔽的消息传输机制�
对传输消息有三种服务质量(QoS):
- 最多一次,这一级别会发生消息丢失或重复,消息发布依赖于底层TCP/IP网络。即�<=1
- 至多一次,这一级别会确保消息到达,但消息可能会重复。即�>=1
- 只有一次,确保消息只有一次到达。即:=1。在一些要求比较严格的计费系统中,可以使用此级�
数据传输和协议交换的最小化(协议头部只�2字节),以减少网络流�
通知机制,异常中断时通知传输双方
2) MQTT协议原理及实现方�
实现MQTT协议需要:客户端和服务器端
MQTT协议中有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者�
payload,可以理解为消息的内容,是指订阅者具体要使用的内�
二� 连接
1. 阿里云连接步骤:
- 以aliyun账号直接进入IoT控制台,如果还没有开通阿里云物联网套件服务,则申请开�
- 接入引导�1)、创建产品(2)、添加设备(3)、获取设备的Topic
2. MQTT移植步骤�
MQTT代码源码下载地址:(http://www.eclipse.org/paho/�
MQTT的移植非常简单,将C/C++ MQTT Embedded clients的代码添加到工程中,然后我们只需要再次封�4个函数即可:
- int transport_sendPacketBuffer(unsigned char* buf, int buflen);
通过网络以TCP的方式发送数据; - int transport_getdata(unsigned char* buf, int count);
TCP方式从服务器端读取数据,该函数目前属于阻塞函数; - int transport_open(void);
打开一个网络接口,其实就是和服务器建立一个TCP连接� - int transport_close(void);
关闭网络接口�
如果已经移植好了socket方式的TCP客户端的程序,那么这几个函数的封装也是非常简单的,程序代码如下所示:
/** * @brief 通过TCP方式发送数据到TCP服务� * @param buf数据首地址 * @param buflen数据长度 * @retval 小于0表示发送失� */ /*订阅消息*/ int Subscribe_sendPacketBuffer(unsigned char* buf, int buflen) { return send(SOCK_TCPS,buf,buflen); } /*发布消息*/ int Published_sendPacketBuffer(unsigned char* buf, int buflen) { return send(SOCK_TCPC,buf,buflen); } /** * @brief 阻塞方式接收TCP服务器发送的数据 * @param buf数据存储首地址� * @param count数据缓冲区长� * @retval 小于0表示接收数据失败 */ int Subscribe_getdata(unsigned char* buf, int count) { return recv(SOCK_TCPS,buf,count); } int Published_getdata(unsigned char* buf, int count) { return recv(SOCK_TCPC,buf,count); } /** * @brief 打开一个socket并连接到服务� * @param � * @retval 小于0表示打开失败 */ int Subscribe_open(void) { int32_t ret; //新建一个socket并绑定本地端�5000 ret = socket(SOCK_TCPS,Sn_MR_TCP,50000,0x00); if (ret != 1) { printf("%d:Socket Error\r\n",SOCK_TCPS); while (1);} else { printf("%d:Opened\r\n",SOCK_TCPS);} while (getSn_SR(SOCK_TCPS)!=SOCK_ESTABLISHED) { printf("connecting\r\n"); //连接TCP服务� ret = connect(SOCK_TCPS,server_ip,1883); //端口必须�1883 } if (ret != 1) { printf("%d:Socket Connect Error\r\n",SOCK_TCPS); while (1); } else { printf("%d:Connected\r\n",SOCK_TCPS);} return 0; } int Published_open(void) { int32_t ret; ret = socket(SOCK_TCPC,Sn_MR_TCP,5001,0x00); if (ret != 1) { printf("%d:Socket1 Error1\r\n",SOCK_TCPC); while (1); } else { printf("%d:socket1 Opened\r\n",SOCK_TCPC);} while (getSn_SR(SOCK_TCPC)!=SOCK_ESTABLISHED) { ret = connect(SOCK_TCPC,server_ip,1883); //端口必须�1883 } if (ret != 1) { printf("%d:Socket Connect1 Error\r\n",SOCK_TCPC); while (1); } else { printf("%d:Connected1\r\n",SOCK_TCPC); } return 0; } /** * @brief 关闭socket * @param � * @retval 小于0表示关闭失败 */ int Subscribe_close(void) { disconnect(SOCK_TCPS); printf("close0\n\r"); while (getSn_SR(SOCK_TCPC)!=SOCK_CLOSED) {;} return 0; } int Published_close(void) { disconnect(SOCK_TCPC); printf("close1\n\r"); while (getSn_SR(SOCK_TCPC)!=SOCK_CLOSED) {; } return 0; }
3� MQTT配置
1) MQTT连接参数说明

举例�

clientId= 192.168.207.115|securemode=3,signmethod=hmacsha1,timestamp=789|
username= MQTT1&TKKMt4nMF8U
password= 9076b0ebc04dba8a8ebba1f0003552dbc862c9b9
其中password的获取方法有工具生成和手动拼接两种:

本文章是采用阿里云小工具生成的密码,工具可在本文档所在文件夹内找到。第二种方式是根据阿里云要求手动生成密码。如上所述通过该方法拼接的结果如下�
clientId192.168.207.115deviceNameMQTT1productKeyTKKMt4nMF8Utimestamp789
最后将拼接的结果通过clientID中指定的加密方式使用deviceSecret(秘钥)加密即可得到password值�
2) MQTT与阿里云连接函数�
参考阿里云内MQTT设备接入手册,计算出设备连接的各项参数,例如下列程序中框中的部分为本例程MQTT与阿里云连接的参数的配置,详细内容如下:
clientId = 192.168.207.115
deviceName = MQTT1
productKey = TKKMt4nMF8U
timestamp = 789(毫秒值)
signmethod = hmacsha1(算法类型)
deviceSecret = secret
那么使用tcp方式提交给mqtt参数分别如下�
- mqttClientId:clientId+"|securemode=3,signmethod=hmacsha1,timestamp=789|"clientId=192.168.207.115|securemode=3,signmethod=hmacsha1,timestamp=789|
- keepalive时间需要设置超�60秒以上,否则会拒绝连接�
- Cleansession�1�
- mqttUsername: deviceName+"&"+productKey
username = "MQTT1&TKKMt4nMF8U" - (5) password=9076b0ebc04dba8a8ebba1f0003552dbc862c9b9
password由阿里云提供的工具生成也可由自己手动生成�
MQTT连接函数原型,tcp_client.c文件中的MQTT_CON_ALI函数中调用make_con_msg函数并通过阿里云设备的参数,设置MQTT连接阿里云函数的参数�
void make_con_msg(char* clientID,int keepalive, uint8 cleansession,char*username,char* password,unsigned char*buf,int buflen) { int32_t len,rc; MQTTPacket_connectData data = MQTTPacket_connectData_initializer; data.clientID.cstring = clientID; data.keepAliveInterval = keepalive; data.cleansession = cleansession; data.username.cstring = username; data.password.cstring = password; len = MQTTSerialize_connect(buf, buflen, &data); //构造链接报� return; }
MQTT连接过程�
void MQTT_CON_ALI(void) { int len; int type; switch (getSn_SR(0)) { //获取socket0的状� case SOCK_INIT: //Socket处于初始化完成(打开)状� connect(0, server_ip,server_port); //配置Sn_CR为CONNECT,并向TCP服务器发出连接请求� break; case SOCK_ESTABLISHED: // Socket处于连接建立状� if (getSn_IR(0) & Sn_IR_CON) { setSn_IR(0, Sn_IR_CON); // Sn_IR的CON位置1,通知W5500连接已建� } memset(msgbuf,0,sizeof(msgbuf)); if ((len=getSn_RX_RSR(0))==0) { if (1==CONNECT_FLAG) { printf("send connect\r\n"); /*MQTTÆ拼接连接报文 *根据阿里云平台MQTT设备接入手册配置 */ //void make_con_msg(char* clientID,int keepalive, uint8 cleansession,char*username, char* password,unsigned char*buf, int buflen) make_con_msg("192.168.207.115|securemode=3, signmethod=hmacsha1,timestamp=789|",180, 1,"MQTT1&TKKMt4nMF8U", "9076b0ebc04dba8a8ebba1f0003552dbc862c9b9" ,msgbuf,sizeof(msgbuf)); //printf(" server_ip: %d.%d.%d.%d\r\n", server_ip[0], server_ip[1],server_ip[2],server_ip[3]); //printf("connect ALY\r\n"); CONNECT_FLAG = 0; send(0,msgbuf,sizeof(msgbuf)); Delay_s(2); while ((len=getSn_RX_RSR(0))==0) { Delay_s(2); send(0,msgbuf,sizeof(msgbuf)); }; recv(0,msgbuf,len); while (mqtt_decode_msg(msgbuf)!=CONNACK) { //判断是不是CONNACK printf("wait ack\r\n"); } } else if (SUB_FLAG == 1) { memset(msgbuf,0,sizeof(msgbuf)); make_sub_msg(topic,msgbuf,sizeof(msgbuf)); // make_pub_msg(topic,msgbuf,sizeof(msgbuf),"hello"); send(0,msgbuf,sizeof(msgbuf)); // 接收到数据后再回给服务器,完成数据回� SUB_FLAG = 0; Delay_s(2); while ((len=getSn_RX_RSR(0))==0) { Delay_s(2); send(0,msgbuf,sizeof(msgbuf)); }; recv(0,msgbuf,len); while (mqtt_decode_msg(msgbuf)!=SUBACK) { //判断是不是SUBACK printf("wait suback\r\n"); } TIM_Cmd(TIM2, ENABLE); printf("send sub\r\n"); } #if 1 else { //count++; // Delay_s(2); if (count>10000) { count = 0; make_ping_msg(msgbuf,sizeof(msgbuf)); send(0,msgbuf,sizeof(msgbuf)); while ((len=getSn_RX_RSR(0))==0) { //Delay_s(2); //send(0,msgbuf,sizeof(msgbuf)); printf("wait pingresponse"); }; recv(0,msgbuf,len); printf("ping len : %d\r\n",len); if (len>2) { if (PUBLISH==mqtt_decode_msg(msgbuf+2)) { printf("publish\r\n"); MQTTDeserialize_publish(&dup, &qos, &retained, &mssageid, &receivedTopic, &payload_in, &payloadlen_in, msgbuf+2, len-2); // printf("message arrived %d: %s\n\r", payloadlen_in, payload_in); memset(topic,0,sizeof(topic)); memset(ser_cmd,0,sizeof(ser_cmd)); memcpy(topic,receivedTopic.lenstring.data, receivedTopic.lenstring.len); replace_string(new_topic,topic , "request", "response"); printf("topic:%s\r\n",topic); strcpy(ser_cmd,(const char *)payload_in); //parse_topic(ser_cmd); // printf("message is %s\r\n",ser_cmd); memset(msgbuf,0,sizeof(msgbuf)); make_pub_msg(new_topic,msgbuf,sizeof( msgbuf),"hello"); send(0,msgbuf,sizeof(msgbuf)); } } } } #endif #if 0 if (PUB_FLAG==1) { memset(msgbuf,0,sizeof(msgbuf)); // make_sub_msg(topic,msgbuf,sizeof(msgbuf)); make_pub_msg(topic,msgbuf,sizeof(msgbuf),"hello"); if (count == 10000) { PUB: send(0,msgbuf,sizeof(msgbuf)); // 接收到数据后再回给服务器,完成数据回� ÷£¬Íê³ÉÊý¾Ý»Ø»� Delay_s(2); // while((len=getSn_RX_RSR(0))==0) // { // Delay_s(2); //send(0,msgbuf,sizeof(msgbuf)); // printf("puback\r\n"); // }; // recv(0,msgbuf,len); // if(mqtt_decode_msg(msgbuf)!=PUBACK) //ÅжÏÊDz»ÊÇSUBACK // { // goto PUB; // printf("wait Puback\r\n"); // } printf("send Pub\r\n"); } } #endif } #if 1 if ((len=getSn_RX_RSR(0))>0) { recv(0,msgbuf,len); if (PUBLISH== mqtt_decode_msg(msgbuf)) { printf("publish\r\n"); MQTTDeserialize_publish(&dup, &qos, &retained, &mssageid, &receivedTopic, &payload_in, &payloadlen_in, msgbuf, len); // printf("message arrived %d: %s\n\r", payloadlen_in, payload_in); memset(topic,0,sizeof(topic)); memcpy(topic,receivedTopic.lenstring.data, receivedTopic.lenstring.len); replace_string(new_topic,topic , "request","response"); printf("topic:%s\r\n",topic); memset(ser_cmd,0,sizeof(ser_cmd)); memcpy(ser_cmd,(const char *)payload_in,strlen((char*) payload_in)); memset(msgbuf,0,sizeof(msgbuf)); make_pub_msg(new_topic,msgbuf,sizeof(msgbuf),rebuf); send(0,msgbuf,sizeof(msgbuf)); //printf("%s\n",msgbuf); } else if (PINGRESP== mqtt_decode_msg(msgbuf)) { if (len>2) { if (PUBLISH==mqtt_decode_msg(msgbuf+2)) { printf("publish\r\n"); MQTTDeserialize_publish(&dup, &qos, &retained, &mssageid, &receivedTopic, &payload_in, &payloadlen_in, msgbuf+ 2, len-2); // printf("message arrived %d: %s\n\r", payloadlen_in, payload_in); memset(topic,0,sizeof(topic)); memcpy(topic,receivedTopic.lenstring.data, receivedTopic.lenstring.len); replace_string(new_topic,topic,"request", "response"); printf("topic:%s\r\n",topic); memset(ser_cmd,0,sizeof(ser_cmd)); strcpy(ser_cmd,(const char *)payload_in); // printf("message is %s\r\n",ser_cmd); //parse_topic(ser_cmd); memset(msgbuf,0,sizeof(msgbuf)); make_pub_msg(new_topic,msgbuf,sizeof(msgbuf), "hello"); send(0,msgbuf,sizeof(msgbuf)); } } } else { printf("wait publish\r\n"); } } // printf("send ping\r\n"); #endif break; case SOCK_CLOSE_WAIT: //Socket处于等待关闭状� close(0); // 关闭Socket0 break; case SOCK_CLOSED: // Socket处于关闭状� socket(0,Sn_MR_TCP,local_port,Sn_MR_ND); // 打开Socket0,并配置为TCP无延时模式,打开一个本地端� break; } }
Password手动拼接有两种获得方法:
- 通过网页“在线加密解密”HamcSHA1获得;(http://encode.chahuo.com/�
- 通过hmacsha1算法解析获得解析步骤如下�
void hmac_sha1(uint8_t *key, uint16_t key_length, uint8_t *data, uint16_t data_length, uint8_t *digest) { uint8_t b = 64; /* blocksize */ uint8_t ipad = 0x36; uint8_t opad = 0x5c; uint8_t k0[64]; uint8_t k0xorIpad[64]; uint8_t step7data[64]; uint8_t step5data[MAX_MESSAGE_LENGTH+128]; uint8_t step8data[64+20]; uint16_t i; for (i=0; i<64; i++) { k0[i] = 0x00; } /* Step 1 */ if (key_length != b) { //判断秘钥K字节长度是否等于B /* Step 2 */ if (key_length > b) { //如果大于B,则另K0=H(K) sha1(key, key_length, digest); for (i=0; i<20; i++) { k0[i]=digest[i]; } } /* Step 3 */ else if (key_length < b) { //如果小于B,则在末尾添加B-length(K) 位的0 for (i=0; i < key_length; i++) { k0[i] = key[i]; } } } else { for (i=0; i< b; i++) { k0[i] = key[i]; } } #ifdef HMAC_DEBUG debug_out("k0",k0,64); #endif /* Step 4 */ for (i=0; i<64; i++) { k0xorIpad[i] = k0[i] ^ ipad; //将K0和ipad进行异或运算 } #ifdef HMAC_DEBUG debug_out("k0 xor ipad",k0xorIpad,64); #endif /* Step 5 */ for (i=0; i<64; i++) { step5data[i] = k0xorIpad[i]; } for (i=0; i< data_length; i++) { step5data[i+64] = data[i]; //将数据添加在�4步生成的字节� 后面 } #ifdef HMAC_DEBUG debug_out("(k0 xor ipad) || text",step5data,data_length+64); #endif /* Step 6 */ sha1(step5data, data_length+b, digest); //将第5步的结果运用H函数 #ifdef HMAC_DEBUG debug_out("Hash((k0 xor ipad) || text)",digest,20); #endif /* Step 7 */ for (i=0; i<64; i++) { step7data[i] = k0[i] ^ opad; //将K0和opad进行异或运算 } #ifdef HMAC_DEBUG debug_out("(k0 xor opad)",step7data,64); #endif /* Step 8 */ for (i=0; i<64; i++) { step8data[i] = step7data[i]; } for (i=0; i<20; i++) { step8data[i+64] = digest[i]; //将第6步的结果加在�7步生成的�� �节串上 } #ifdef HMAC_DEBUG debug_out("(k0 xor opad) || Hash((k0 xor ipad) || text)",step8data, 20+64); #endif /* Step 9 */ sha1(step8data, b+20, digest); #ifdef HMAC_DEBUG debug_out("HASH((k0 xor opad) || Hash((k0 xor ipad) || text))", digest,20); #endif }
3) 配置远程服务器IP地址和服务器端口
通过域名解析获取IP地址有两种方法:a、通过在终端下ping域名的方法获取IP地址;b、通过DNS域名解析的方法获取IP地址
a) 通过在终端下ping域名的方法获取IP地址

� ${productKey}替换为您的产品key,并在终端对MQTT进行ping操作,来获取服务器IP地址

举例�

b) 通过DNS域名解析的方法获取IP地址
首先完成W5500的DNS域名解析例程的移植,把DNS相关部分移植到本程序中,再进行相关配置即可完�(DNS相关例程下载地址http://www.w5500.com/) ,DNS解析域名成功后,把解析出的IP地址赋值给MQTT� server_ip,用于MQTT与阿里云的连接,完成MQTT协议通信
连接成功后,通过串口调试助手验证DNS域名解析是否正确,若正确则MQTT与阿里云连接成功,并可成功的发布订阅消息�

4) 设置发布订阅的主题:
在tcp_client.c文件中设置MQTT与阿里云连接参数,并通过调用mqtt_fun.c文件中的相关底层函数来完成MQTT与阿里云连接�

底层的订阅发布函�
/*****************拼接订阅报文**************************************/ void make_sub_msg(char *Topic,unsigned char*msgbuf,int buflen) { int msgid = 1; int req_qos = 0; unsigned char topic[100]; MQTTString topicString= MQTTString_initializer; memcpy(topic,Topic,strlen(Topic)); topicString.cstring = (char*)topic; //topicString.lenstring.len=4; MQTTSerialize_subscribe(msgbuf, buflen, 0, msgid, 1, &topicString, &req_qos); return; } /*********拼接发布报文******************/ void make_pub_msg(char *Topic,unsigned char*msgbuf,int buflen,char*msg) { unsigned char topic[100]; int msglen = strlen(msg); MQTTString topicString = MQTTString_initializer; memset(topic,0,sizeof(topic)); memcpy(topic,Topic,strlen(Topic)); topicString.cstring = (char*)topic; MQTTSerialize_publish(msgbuf, buflen, 0, 2, 0, 0, topicString, ( unsigned char*)msg, msglen); return; }
此发布订阅的主题根据阿里云中设备管理的Topic列表设置

设备可以基于Topic列表中的Topic进行Pub/Sub通信,例如列表中�/TKKMt4nMF8U/MQTT1/mqtt,且设备拥有的权限是发布和订阅,这就意味着设备可以往这个Topic发布消息,同样设备可以从这个Topic订阅消息�

4� 简单测试:
把程序下载到测试板并连接,登陆阿里云,到添加的设备,开启测试板,状态显示在线,说明MQTT与阿里云已经初步连接�

通过设备的Topic列表,选择程序中设置的发布订阅的Topic进行发布消息的操作:

串口打印接收到的服务器端发送的消息�

同时可在日志服务中查询相关设备的相关消息�

此时MQTT协议通信成功�
说明:在串口通信中会一直打印消息,是因为程序中设置了对MQTT的ping操作,防止MQTT离线�

三� 注意�
- 在MQTT与阿里云连接时,会出现离线的状态,在离线状态时重启测试板并手动刷新阿里云即可。因为状态不是实时的显示,会有一段时间的延迟,可耐心等待�
- MQTT CONNECT协议设置时的注意事项�
- 错误�
例程下载�【STM32F103+W5500_MQTT�
编译环境:keil V5.11
硬件要求�W5500EVB开发板