MQTT协议与阿里云IoT物联网平台

1.MQTT协议介绍

1.1 MQTT协议

MQTT(消息队列遥测传输) 是基于 TCP/IP 协议栈而构建的支持在各方之间异步通讯的消息协议。MQTT在空间和时间上将消息发送者与接收者分离,所以能够在不可靠的网络环境中进行扩展。虽然叫作消息队列遥测传输,但它与消息队列毫无关系,而是使用了发布和订阅(Pub/Sub)的模型。javascript

MQTT 是一种轻量级的、灵活的网络协议,致力于为 IoT 开发人员实现适当的平衡:php

  • 这个轻量级协议可在严重受限的设备硬件和高延迟/带宽有限的网络上实现。
  • 它的灵活性使得为 IoT 设备和服务的多样化应用场景提供支持成为可能。

image.png | left | 642x320

1.2 MQTT Client库

MQTT Client 库在不少语言中都有实现,包括 Embedded C、C、Java、JavaScript、Python、C++、C#、Go、iOS、Android等。Eclipse Paho的MQTT库下载地址:www.eclipse.org/paho/downlo…html

image.png | left | 747x320

下面开发实践基于Nodejs版mqtt,获取地址 www.npmjs.com/package/mqt…java

1.3 MQTT报文

image.png | left | 456x260

1.3.1 固定报头Fixed header

Bit
7
6
5
4
3
2
1
0
byte 1
MQTT控制报文的类型
用于指定控制报文类型的标志位
byte 2,3,4,5
剩余长度,最大4个字节

控制报文类型算法

名字 报文流动方向 描述
Reserved 0 禁止 保留
CONNECT 1 Client -> Broker device链接IoT平台
CONNACK 2 Broker -> Client IoT平台确认链接结果
PUBLISH 3 双向 发布消息
PUBACK 4 双向 QoS=1消息发布收到确认
PUBREC 5 双向 IoT不支持
PUBREL 6 双向 IoT不支持
PUBCOMP 7 双向 IoT不支持
SUBSCRIBE 8 Client -> Broker device订阅IoT平台Topic
SUBACK 9 Broker -> Client IoT平台确认订阅结果
UNSUBSCRIBE 10 Client -> Broker device取消订阅IoT平台Topic
UNSUBACK 11 Broker -> Client IoT平台确认取消订阅结果
PINGREQ 12 Client -> Broker device发送心跳请求到IoT平台
PINGRESP 13 Broker -> Client IoT平台响应device心跳
DISCONNECT 14 Client -> Broker device断开IoT平台链接
Reserved 15 禁止 保留

控制报文类型标志位npm

控制报文 固定报头标志 Bit 3 Bit 2 Bit 1 Bit 0
PUBLISH MQTT 3.1.1使用 DUP QoS QoS RETAIN

剩余长度安全

字节数 最小值 最大值
1 0 (0x00) 127 (0x7F)
2 128 (0x80, 0x01) 16 383 (0xFF, 0x7F)
3 16 384 (0x80, 0x80, 0x01) 2 097 151 (0xFF, 0xFF, 0x7F)
4 2 097 152 (0x80, 0x80, 0x80, 0x01) 268 435 455 (0xFF, 0xFF, 0xFF, 0x7F)

注:阿里云IoT的单个payload最大256K网络

1.3.2 可变报头Variable header

某些MQTT控制报文包含一个可变报头部分。它在固定报头和负载之间。可变报头的内容根据报文类型的不一样而不一样。可变报头的报文标识符(Packet Identifier)字段存在于在多个类型的报文里。session

报文标识符字节 Packet Identifier bytes
Bit 7 - 0
byte 1 报文标识符 MSB
byte 2 报文标识符 LSB
控制报文 报文标识符
PUBLISH 须要(若是QoS =1,2)
PUBACK 须要
PUBREC 须要
PUBREL 须要
PUBCOMP 须要
SUBSCRIBE 须要
SUBACK 须要
UNSUBSCRIBE 须要
UNSUBACK 须要

1.3.3 有效载荷Payload

如下MQTT控制报文在报文的最后部分包含一个有效载荷。对于PUBLISH来讲有效载荷就是业务消息。dom

控制报文 有效载荷
CONNECT 须要
PUBLISH 可选
SUBSCRIBE 须要
SUBACK 须要
UNSUBSCRIBE 须要

2.与阿里云IoT平台创建链接

image.png | left | 573x260

2.1 CONNECT

阿里云IoT物联网平台的MQTT协议不支持will消息,CONNECT 消息内容参数以下:

参数
说明
cleanSession
此标志指定链接是不是持久性的。
0为持久会话,QoS=1消息不会丢失;
1为非持久会话,清理离线消息。
clientId
客户端标识符
username
代理的身份验证和受权凭证。
password
代理的身份验证和受权凭证。
keepAlive
心跳时间, IoT平台约定心跳范围 30s~1200s

其中clientId,username,password由设备三元组(productKey,deviceName,deviceSecret)按照规则生成,具体规则以下:

clientId
id+"|securemode=3,signmethod=hmacsha1,timestamp="+timestamp+"|"
id :表示客户端ID,64字符内。其中 ||内为扩展参数。
securemode:安全模式;2为TLS加密,3为非加密
signmethod:签名算法类型。 timestamp:当前时间毫秒值。
username
deviceName+"&"+productKey
password
sign_hmac(deviceSecret,content)
sign_hmac为clientId中的signmethod算法类型
content为以下拼接字符串: " clientId${ id} deviceName${deviceName} productKey${productKey} timestamp${timestamp}"

官方文档:help.aliyun.com/document_de…

设备端代码示例(Nodejs版) client.js

/** "dependencies": { "mqtt": "2.18.8" } */
const crypto = require('crypto');
const mqtt = require('mqtt');
//设备身份三元组+区域
const deviceConfig = {
    productKey: "替换",
    deviceName: "替换",
    deviceSecret: "替换",
    regionId: "cn-shanghai"
};
//根据三元组生成mqtt链接参数
const options = initMqttOptions(deviceConfig);
const url = `tcp://${deviceConfig.productKey}.iot-as-mqtt.${deviceConfig.regionId}.aliyuncs.com:1883`;

//2.创建链接
const client = mqtt.connect(url, options);

client.on('packetsend', function (packet){
  console.log('send '+packet.cmd+' packet =>',packet)
})

client.on('packetreceive', function (packet){
  console.log('receive '+packet.cmd+' packet =>',packet)
})


//IoT平台mqtt链接参数初始化
function initMqttOptions(deviceConfig) {

    const params = {
        productKey: deviceConfig.productKey,
        deviceName: deviceConfig.deviceName,
        timestamp: Date.now(),
        clientId: Math.random().toString(36).substr(2),
    }
    //CONNECT参数
    const options = {
        keepalive: 60, //60s
        clean: false, //cleanSession保持持久会话
        protocolVersion: 4 //MQTT v3.1.1
    }
    //1.生成clientId,username,password
    options.password = signHmacSha1(params, deviceConfig.deviceSecret);
    options.clientId = `${params.clientId}|securemode=3,signmethod=hmacsha1,timestamp=${params.timestamp}|`;
    options.username = `${params.deviceName}&${params.productKey}`;

    return options;
}

/* 生成基于HmacSha1的password 参考文档:https://help.aliyun.com/document_detail/73742.html?#h2-url-1 */
function signHmacSha1(params, deviceSecret) {

    let keys = Object.keys(params).sort();
    // 按字典序排序
    keys = keys.sort();
    const list = [];
    keys.map((key) => {
        list.push(`${key}${params[key]}`);
    });
    const contentStr = list.join('');
    return crypto.createHmac('sha1', deviceSecret)
            .update(contentStr)
            .digest('hex');
}
复制代码

2.2 CONNACK

receive connack packet => Packet {
  cmd: 'connack',
  retain: false,
  qos: 0,
  dup: false,
  length: 2,
  topic: null,
  payload: null,
  sessionPresent: false,
  returnCode: 0 
}
复制代码

2.4 PINGRESP

send pingreq packet => { cmd: 'pingreq' }
复制代码

2.5 PINGRESP

receive pingresp packet => Packet {
  cmd: 'pingresp',
  retain: false,
  qos: 0,
  dup: false,
  length: 0,
  topic: null,
  payload: null 
}
复制代码

2.6 DISCONNECT

image.png | left | 590x300

3. 发布数据

3.1 PUBLISH

//3.属性数据上报
const topic = `/sys/${deviceConfig.productKey}/${deviceConfig.deviceName}/thing/event/property/post`;
setInterval(function() {
    //发布数据到topic
    client.publish(topic, getPostData(),{qos:1});
}, 5 * 1000);

function getPostData() {
    const payloadJson = {
        id: Date.now(),
        params: {
            temperature: Math.floor((Math.random() * 20) + 10),
            humidity: Math.floor((Math.random() * 20) + 60)
        },
        method: "thing.event.property.post"
    }

    console.log("===postData\n topic=" + topic)
    console.log(payloadJson)

    return JSON.stringify(payloadJson);
}

复制代码
send publish packet => { cmd: 'publish',
  topic: '/sys/a1hQSwFledE/eud1jXfEgCsAiP2eId9Q/thing/event/property/post',
  payload: '{"id":1543896481106,"params":{"temperature":23,"humidity":73},"method":"thing.event.property.post"}',
  qos: 1,
  retain: false,
  messageId: 38850,
  dup: false 
}
复制代码

3.2 PUBACK

receive puback packet => Packet {
  cmd: 'puback',
  retain: false,
  qos: 0,
  dup: false,
  length: 2,
  topic: null,
  payload: null,
  messageId: 38850 
}
复制代码

4. 接收数据

4.1 SUBSCRIBE

//4.订阅主题,接收指令
const subTopic = `/${deviceConfig.productKey}/${deviceConfig.deviceName}/control`;
client.subscribe(subTopic)
client.on('message', function(topic, message) {
    console.log("topic " + topic)
    console.log("message " + message)
})
复制代码

SUBSCRIBE消息体

send subscribe packet => { cmd: 'subscribe',
  subscriptions: 
   [ { topic: '/a1hQSwFledE/eud1jXfEgCsAiP2eId9Q/control', qos: 0 } ],
  qos: 1,
  retain: false,
  dup: false,
  messageId: 38851 
}
复制代码

4.2 SUBACK

SUBACK消息体

receive suback packet => Packet {
  cmd: 'suback',
  retain: false,
  qos: 0,
  dup: false,
  length: 3,
  topic: null,
  payload: null,
  granted: [ 128 ],
  messageId: 38851 
}
复制代码

4.3 UNSUBSCRIBE

send unsubscribe packet => { cmd: 'unsubscribe',
  qos: 1,
  messageId: 34323,
  unsubscriptions: [ '/a1hQSwFledE/eud1jXfEgCsAiP2eId9Q/control' ] 
}
复制代码

4.4 UNSUBACK

receive unsuback packet => Packet {
  cmd: 'unsuback',
  retain: false,
  qos: 0,
  dup: false,
  length: 2,
  topic: null,
  payload: null,
  messageId: 34323 
}
复制代码

5. 服务质量QoS

服务质量
Quality of Service
描述
阿里云IoT
QoS=0
最多一次的传输,可能会收不到消息
支持
QoS=1
至少一次的传输,必定会收到消息,可能重复
支持
QoS=2
有且仅有一次的传输
不支持

阿里云IoT的QoS.png | left | 500x247

6. 设备掉线重连

设备与阿里云IoT的订阅关系在云端保持,除非设备主动unsubscribe,不然订阅关系不清理。设备重连后,依然保持以前的订阅关系,不须要重复订阅。

7. 传输层安全TLS1.2

设备和IoT平台之间的链路能够经过TLS v1.2加密。 若是使用TLS加密,须要下载根证书。 CONNECT参数中clientId的securemode=2

help.aliyun.com/document_de…

IoT物联网技术

iot-tech-weixin.png | center | 225x224