1.MQTT協議介紹
1.1 MQTT協議
MQTT(訊息佇列遙測傳輸) 是基於 TCP/IP 協議棧而構建的支援在各方之間非同步通訊的訊息協議。MQTT在空間和時間上將訊息傳送者與接收者分離,因此可以在不可靠的網路環境中進行擴充套件。雖然叫做訊息佇列遙測傳輸,但它與訊息佇列毫無關係,而是使用了釋出和訂閱(Pub/Sub)的模型。
MQTT 是一種輕量級的、靈活的網路協議,致力於為 IoT 開發人員實現適當的平衡:
- 這個輕量級協議可在嚴重受限的裝置硬體和高延遲/頻寬有限的網路上實現。
- 它的靈活性使得為 IoT 裝置和服務的多樣化應用場景提供支援成為可能。
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…
下面開發實踐基於Nodejs版mqtt,獲取地址 www.npmjs.com/package/mqt…
1.3 MQTT報文
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訊息釋出收到確認 |
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 | 禁止 | 保留 |
控制報文型別標誌位
控制報文 | 固定報頭標誌 | 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)欄位存在於在多個型別的報文裡。
報文識別符號位元組 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來說有效載荷就是業務訊息。
控制報文 | 有效載荷 |
---|---|
CONNECT | 需要 |
PUBLISH | 可選 |
SUBSCRIBE | 需要 |
SUBACK | 需要 |
UNSUBSCRIBE | 需要 |
2.與阿里雲IoT平臺建立連線
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
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
|
有且僅有一次的傳輸
|
不支援
|
6. 裝置掉線重連
裝置與阿里雲IoT的訂閱關係在雲端保持,除非裝置主動unsubscribe,否則訂閱關係不清理。裝置重連後,依然保持之前的訂閱關係,不需要重複訂閱。
7. 傳輸層安全TLS1.2
裝置和IoT平臺之間的鏈路可以通過TLS v1.2加密。 如果使用TLS加密,需要下載根證照。 CONNECT引數中clientId的securemode=2