關注零妖的微信公衆号,擷取第一手物聯網的技術幹貨: LINGYAOIOT
1ã MQTT協定是IOT(Internet of Things)領域的一個主流協定
在物聯網的時代,每一個傳感器每一個裝置都想接入網際網路進行資料交換。MQTT協定非常适合這樣的場合。目前國内的主流IOT伺服器供應商均提供對MQTT協定的解析比如百度雲計算,阿裡雲計算等。MQTT協定的實作也非常簡單,對帶寬的要求不高,對網絡連結的可靠性要求也不高,而且協定本身制定了一定的機制來處理突發事件。
MQTT協定不僅可以在物聯網領域發揮重要作用,同時也可以用于多台機器之間的資訊交換比如一個工廠中的房間裡面所有的傳感器之間資料的交換。
MQTT協定也不僅僅局限于運作在網際網路通信上。它是一個通信規則,對通信方式的實作不關心。通常我們提到物聯網指的是通過 TCP/IP 的方式實作了通信,也就是利用網際網路實作,因為網際網路可以提供一個非常可靠的雙向通信。
本學習手冊根據 MQTT V3.1.1 版本編寫
官方手冊下載下傳位址 :
http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.doc下面章節大部分内容均參考此官方手冊。
2ã MQTT 通信協定需要有三個角色參與
這段文字參考百度雲計算的幫助文檔:
https://cloud.baidu.com/doc/IOT/MQTTProtocol.html#.E0.F6.0C.38.86.9F.BE.F8.FD.AC.D9.00.29.12.24.B6MQTT協定提到的一個名詞 “主題”,類似于檔案夾的名字一樣。比如小王是電腦的主人,他的電腦上面有a,b兩個檔案夾,小劉每次存儲的檔案喜歡放到a檔案夾,小宋每次存儲的檔案喜歡放到b檔案夾。那麼當小林想看小劉的檔案時,隻需要看a檔案夾就可以了。上述的“a”檔案夾的名稱,在MQTT協定裡面稱作 主題 。

左圖诠釋了MQTT協定裡的三個角色:釋出者用戶端(負責發送消息),代理伺服器(負責接收和分發消息),訂閱者用戶端(負責接收消息)。
在MQTT協定裡,“主題”就是一個檔案夾,釋出的消息可以送到一個“主題”裡面,訂閱者也可以從“主題”裡面讀取到消息。
代理伺服器在國内有百度的 IoT Hub ,也有阿裡雲的IoT Hub,還有很多其他品牌的伺服器。
釋出者用戶端和訂閱者用戶端既可以是同一台裝置,也可以是不同的裝置,隻要這台裝置可以通過伺服器的認證,并且遵循MQTT協定,就可以釋出或者訂閱消息。本學習手冊的重要内容就是兩個用戶端如何與伺服器“交流”。
3ã MQTT 通信協定和大資料
(1)Â 小劉采集的信号是溫度資訊,他每間隔1分鐘就上傳一次溫度資訊到伺服器,同時他發送的主題是 a 。
(2)Â 伺服器接收到小劉的溫度資訊後,會查找目前都有哪些訂閱者想看主題是 a 的資訊。
(3)Â 小林訂閱了主題是 a 的内容,隻要小劉發送一次資訊,小林就可以立馬接收到對應的資訊。
(4)Â 小劉和小林都需要事先通過賬号密碼的方式連接配接到伺服器。小劉就像在野外工作的從業人員辛苦采集信号,而小林就像在辦公室的老闆千裡之外洞察前線的一手資訊。
(5)Â 如果有1000個小林這樣的角色不停地給伺服器發送溫度資料。我們都知道伺服器有資料儲存和資料處理的能力,這時候就可以結合機器學習的相關知識去處理和分析這些資料,進而為人類的決策提供參考。
4ã 在MQTT 通信協定裡,字元串需要遵守 UTF-8 編碼規範
在MQTT通信協定裡,資料傳送是以Bit(位)為機關的,和我們常見的TTL序列槽類似不過他們本質上不是一個東西。MQTT協定約定:資料傳送時,高位元組在前,同時,每個位元組裡面的最高位先傳輸。
UTF-8 編碼規範簡單點了解的話,可以看作在一個字元串的前方加了一個長度的表示。如下表所示。
字元串 “JiXiaoXin”的UTF-8寫法
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | |
byte 1 | 字元串長度的高8位 0X00 | |||||||
byte 2 | 字元串長度的低8位 0X09 | |||||||
byte 3 | 字元串資料“J” 0X4A | |||||||
byte 4 | 字元串資料“i” 0X69 | |||||||
byte 5 | 字元串資料“X” 0X58 | |||||||
byte 6 | 字元串資料“i” 0X69 | |||||||
byte 7 | 字元串資料“a” 0X61 | |||||||
byte 8 | 字元串資料“o” 0X6F | |||||||
byte 9 | ||||||||
byte 10 | ||||||||
byte 11 | 字元串資料“n” 0X6E |
l 前兩個位元組表示一個16Bit的無符号型整數。這裡的0X0009表示後續跟随了9個字元(這9個字元用ASCII碼表示的)。
l 這個編碼規範限制了字元串的最大長度為65536.
在MQTT通信裡,凡是涉及到一個字元串的發送或者接收,都需要用到UTF-8編碼規範,比如 主題的名稱,用戶端的名稱,伺服器的登入賬号,伺服器的登入密碼等。
5ã MQTT 一幀消息包含的内容
MQTT 的一幀典型的消息最多由三部分組成:
固定頭(所有的消息必須包含)+
可變頭(有些沒有)+
有效内容(有些沒有)MQTT協定約定,根據不同的功能實作,固定頭是必須要的,其他兩部分内容可有可無(比如心跳包的發送和接收隻要固定頭即可,而從機發送的連結請求則包含了三個部分)。
5.1 固定頭很重要,發送一幀消息靠它表達含義
一個固定頭包含的内容
byte 1(一定有) | MQTT 控制包類型 | 對應左邊的控制包,一些标志位的設定 | ||||||
byte 2(一定有) | 固定頭後面所有位元組的長度 ................................ 固定頭後面所有位元組的長度。 | |||||||
byte (可能有) | ||||||||
byte(可能有) |
固定頭除了第一個位元組決定了MQTT控制包的類型之外,剩下的幾個位元組描述了這幀消息除了固定頭本身之外的所有位元組的數量。它是一個特殊的編碼格式,可能占一個位元組也可能占三個位元組,最多占4個位元組。
接下來分别說明一下固定頭第一個位元組的含義和“剩下所有位元組長度”。
5.1.1 固定頭的第一個位元組
MQTT控制包類型是第一個位元組的Bit7-Bit4,一共占用4個位,可以表示16種不同的組合。
MQTT 控制包類型 (一共有14種不同的類型)
名字 | 十進制 | 資訊傳輸方向 | 說明 |
保留,暫時沒用 | 無 | ||
CONNECT | 用戶端 到 伺服器 | 用戶端請求連接配接到伺服器 | |
CONNACK | 伺服器 到 用戶端 | 連接配接确認 | |
PUBLISH | 雙向 | 釋出消息 | |
PUBACK | 釋出消息确認 | ||
PUBREC | 雙向 | Publish received (assured delivery part 1) | |
PUBREL | Publish release (assured delivery part 2) | ||
PUBCOMP | Publish complete (assured delivery part 3) | ||
SUBSCRIBE | 8 | 用戶端請求訂閱 | |
SUBACK | 9 | 訂閱确認 | |
UNSUBSCRIBE | 10 | 用戶端請求取消訂閱 | |
UNSUBACK | 11 | 取消訂閱确認 | |
PINGREQ | 12 | PING 請求 | |
PINGRESP | 13 | PING 響應 | |
DISCONNECT | 14 | 用戶端正在斷開連接配接 | |
15 |
控制包類型的标志位,是第一個位元組的Bit3-Bit0 。除了“釋出消息”的控制包類型對應的标志位比較特殊外,其他的13種控制包類型對應的标志位是固定的,不可變更。
如果一個裝置接收到了一幀資訊發現這個标志位和約定的不一緻,那麼必須馬上斷開和對方的網絡連結。
14 種MQTT控制包類型對應的标志位
控制包類型 | 後面的标志位 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
固定的 | |||||
Used in MQTT 3.1.1 | DUP1 | QoS2 | RETAIN3 | ||
上表中的 DUP ,QoS,RETAIN 是在MQTT V3.1.1協定裡用的,用于描述釋出的這一幀消息的屬性,在後文裡會有詳細的說明。
依據上述規則,我們可以很容易得到14種固定頭的第一個位元組的内容:(16進制表示)
14種固定頭的第一位元組内容固定頭名字 | 第一位元組 | 固定頭含義 | |
0X10 | |||
0X20 | |||
0X30 | 釋出消息(依據釋出消息不同而不同) | ||
0X40 | |||
0X50 | |||
0X62 | |||
0X70 | |||
0X82 | |||
0X90 | |||
0XA2 | |||
0XB0 | |||
0XC0 | |||
0XD0 | |||
0XE0 |
在最簡單的應用中,可以釋出一條品質等級為0,不需要确認的消息,此時設定上表中橙色部分為0X30.
後面的所講述的内容,都和固定頭的名字有關系,不同的固定頭後面跟随的内容不一樣。
5.1.2 “剩餘長度”,描述一幀消息占用幾個位元組
“剩餘長度”指的是
有效内容(有些沒有)所有位元組的數量。
“剩餘長度”采用的是一種 “可變長度”編碼規則 ,這種編碼規則可以使用較少的位元組表達較多的内容。它約定,一個位元組的Bit7位不代表具體的數字,而表示一個标志位,是以一個位元組本身能表達的數值範圍是:0-127 ,如果一個數字超過了127,那麼必須再用一個位元組才可以表示,同時,這個位元組的Bit7要置1 。這個編碼規則本身最多占用四個位元組。舉例如下:
假如數字 68 ,16進制表示 0X44 ,大小小于127 ,是以 編碼規則和正常一樣。就是0X44.
假如數字 321 大于127 ,是以編碼需要遵守編碼規則,應該是 0XC1 0X02,具體的解釋如下:
0XC1 | 0X02 | ||||||||||||||
0X41(十進制表示 65) | 0X02,十進制表示 2 | ||||||||||||||
第一個位元組 | 第二個位元組 | ||||||||||||||
65 + 2*128 = 321 |
“可變長度”編碼規則的範圍
編碼位元組數量 | 最小值 | 最大值 |
0 (0x00) | 127 (0x7F) | |
128 (0x80, 0x01) | 16 383 (0xFF, 0x7F) | |
16 384 (0x80, 0x80, 0x01) | 2 097 151 (0xFF, 0xFF, 0x7F) | |
2 097 152 (0x80, 0x80, 0x80, 0x01) | 268 435 455 (0xFF, 0xFF, 0xFF, 0x7F) |
5.2 可變頭 的簡單描述
可變頭辨別占兩個位元組。一個可變頭包含很多内容,可變頭辨別隻是其中的一部分。
先簡單說一下可變頭辨別的出現場合,後面有詳細說明。可變頭辨別是否有必要和固定頭名字有關。
固定頭名字和第一位元組 | 可變頭辨別 | ||
NO | |||
YES (If QoS > 0) | |||
YES | |||
可變頭的内容裡,有一個位元組叫做“連接配接的辨別符”,它描述了目前的這個連接配接的一些設定的内容。這個位元組在用戶端連接配接伺服器時候用到的。具體如下所示:
“連接配接的辨別符”一個位元組
每一位的含義 | User Name Flag | Password Flag | Will Retain | Will QoS | Will Flag | Clean Session | Reserved | |
取值 | X | |||||||
描述 | 0無使用者名 1有使用者名 | 0沒有密碼 1 有密碼 | 看詳細解釋 | 是否存儲會話狀态 | 必須為0 |
Clean Session:如果該位被設定為0,則該連接配接被認為是持久連接配接,其具體表現為:當該客戶斷開後,任何訂閱的主題和QoS被設定為1或2的資訊都會儲存,直到該用戶端再次連接配接上server端(百度雲物接入服務支援将該消息保留24小時)。若“clean session”被設定為1,當該客戶斷開後,所有的訂閱主題都會被移除。
Will Flag:當一個用戶端斷開連接配接的時候,它希望用戶端可以發送它指定的消息。該消息和普通消息的結構相同。(通過在幀消息的有效封包中設定Will Topic和Will Message實作)
Will QoS:伺服器在發生意外的情況下發送遺囑消息的服務等級。如果 Will Flag 是0,那麼這個必須是0。
Will Retain:遺囑保留,如果勾選遺囑保留,遺囑消息釋出時将會保留且發送給新的訂閱消息。
什麼是臨終遺囑?
MQTT協定利用KeepAlive機制在用戶端異常斷開時發現問題。當用戶端斷開時(例如:電量耗盡、系統崩潰或者網絡斷開),代理伺服器會采取相應措施。用戶端設定“臨終遺囑”(LWT)資訊後,當代理伺服器檢測到用戶端離線後,就會發送儲存在特定主題上的 LWT 資訊,讓其它訂閱該主題的用戶端知道該節點已經意外離線。
釋出者用戶端通過設定PUBLISH封包中的QoS标志位,對于用戶端釋出的消息提供三種服務品質等級,如下:
QoS=0,協定對此等級應用資訊不要求回應确認,也沒有重發機制,這類資訊可能會發生消息丢失或重複,取決于TCP/IP提供的盡最大努力互動的資料包服務。
QoS=1,確定資訊到達,但消息重複可能發生,發送者如果在指定時間内沒有收到PUBACK控制封包,應用資訊會被重新發送。
QoS=2,最進階别的服務品質,消息丢失和重複都是不可接受的。
5.2 幀内容 的簡單描述
幀内容 依據傳輸内容的不一樣,所占位元組的長度也不一樣。比如會包含使用者名,密碼的資訊,不同的伺服器不同的使用者肯定不一樣。注意:幀内容裡的資料主要是字元串,需要符合UTF-8編碼規範。
簡單說一下幀内容的出現場合
幀内容 | ||||
必須要有 | ||||
None | ||||
看情況變化 | ||||
6ã MQTT 連接配接伺服器 + 心跳包
6.1 “CONNECT”,用戶端發送給伺服器的連接配接請求。
當確定網絡連通後,用戶端首先需要連接配接到伺服器。如果連接配接成功,伺服器需要有一個連結成功的傳回。如果連接配接成功,用戶端就不需要再發送連結請求了,隻需要發送資料到伺服器即可,同時發送必要的心跳包來保持連接配接。
驗證網絡是否連通的方法很簡單,隻需要發送“Ping 請求”到伺服器,如果伺服器有響應就證明網絡連結是可靠的。發送(16進制格式) C0 00 ,伺服器必須傳回(16進制格式) D0 00 。
“
固定頭”: 第一個位元組肯定是0X10 ,後續的
幀長度要看後面跟随多少資訊,待定(0X53)。
可變頭”:協定名(UTF-8編碼)+協定版本1位元組+連接配接的辨別符1位元組+心跳包時間2位元組
一個完整的
如下表所示
位元組 | |||||||||
協定名稱(固定值) | |||||||||
byte 1 (0X00) | (0X00) | ||||||||
byte 2 (0X04) | Length LSB (0X04) | ||||||||
byte 3 (0X4D) | ‘M’ | ||||||||
byte 4 (0X51) | ‘Q’ | ||||||||
byte 5 (0X54) | ‘T’ | ||||||||
byte 6 (0X54) | |||||||||
協定版本(固定值) | |||||||||
Description | |||||||||
byte 7 (0X04) | Level (4) | ||||||||
連接配接的辨別符(有使用者名,有密碼,用戶端掉線後伺服器清空用戶端的資訊) | |||||||||
byte 8 (0XC2) | User Name Flag (1) Password Flag (1) Will Retain (0) Will QoS (00) Will Flag (0) Clean Session (1) Reserved (0) | ||||||||
心跳包時間設定,機關是 S (這裡表示 60秒,意思是如果用戶端超過60S沒有發送有效資訊到伺服器,那麼伺服器認為這個用戶端已失聯,會主動斷開與用戶端的連接配接) | |||||||||
byte 9 (0X00) | Keep Alive MSB (0) | ||||||||
byte 10 (0X3C) | Keep Alive LSB (10) |
”: “使用者ID” + “臨終消息主題” + “臨終消息” + “使用者名” + “密碼”
l 使用者ID 必須保持唯一,在一個伺服器上的所有裝置,每個裝置的ID都不一樣;
l 應該從伺服器那裡擷取使用者名和密碼;
l “臨終消息主題”和“臨終消息”如果可變頭裡面的 連接配接辨別符沒有允許,就不要添加了。
注意:我們在測試之前需要配置好伺服器,然後擷取一個使用者名和密碼。比如下面這樣的格式:
使用者名:jixin/jixiaoxin 使用者ID:Ling_Yao(使用者ID是自己起的名字)
密碼:ymjohJfqMO9KFzjKhVqeR78wnRpt0U0Xxrqq5VEHdcI=
依據上述的設定,本例子在連接配接伺服器的時候,有效内容應該如下表所示:
發送密碼,UTF8編碼 | |||
發送使用者ID,UTF8編碼 | Byte36 0X66 | f | |
Byte1 0X00 | Length MSB | Byte37 0X71 | q |
Byte2 0X08 | Length LSB | Byte38 0X4D | M |
Byte3 0X4C | L | Byte39 0X4F | |
Byte4 0X69 | i | Byte40 0X39 | |
Byte5 0X6E | n | Byte41 0X4B | K |
Byte6 0X67 | g | Byte42 0X46 | F |
Byte7 0X5F | _ | Byte43 0X7A | z |
Byte8 0X59 | Y | Byte44 0X6A | j |
Byte9 0X61 | a | Byte45 0X4B | |
Byte10 0X6F | o | Byte46 0X68 | h |
Byte47 0X56 | V | ||
發送使用者名,UTF8編碼 | Byte48 0X71 | ||
Byte11 0X00 | Byte49 0X65 | e | |
Byte12 0X0F | Byte50 0X52 | R | |
Byte13 0X6A | Byte51 0X37 | ||
Byte14 0X69 | Byte52 0X38 | ||
Byte15 0X78 | x | Byte53 0X 77 | w |
Byte16 0X69 | Byte54 0X6E | ||
Byte17 0X6E | Byte55 0X52 | ||
Byte18 0X2F | / | Byte56 0X70 | p |
Byte19 0X6A | Byte57 0X74 | t | |
Byte20 0X69 | Byte58 0X30 | ||
Byte21 0X78 | Byte59 0X55 | U | |
Byte22 0X69 | Byte60 0X30 | ||
Byte23 0X61 | Byte61 0X58 | ||
Byte24 0X6F | Byte62 0X78 | ||
Byte25 0X78 | Byte63 0X72 | r | |
Byte26 0X69 | Byte64 0X71 | ||
Byte27 0X6E | Byte65 0X71 | ||
Byte66 0X35 | |||
Byte67 0X56 | |||
Byte28 0X00 | Byte68 0X45 | E | |
Byte29 0X2C | Byte69 0X48 | H | |
Byte30 0X79 | y | Byte70 0X64 | d |
Byte31 0X6D | m | Byte71 0X63 | c |
Byte32 0X6A | Byte72 0X49 | I | |
Byte33 0X6F | Byte73 0X3D | = | |
Byte34 0X68 | |||
Byte35 0X4A | J |
至此,連接配接伺服器需要發送的資訊都已經完成了。那麼我們計算一下一共需要發送多少個位元組,然後就可以确定固定頭的 “剩餘位元組數” 的參數了。
可變頭10個位元組+有效資訊73個位元組,一共是83個位元組,是以 “剩餘位元組數應該是” 0X53
是以,發送以下資訊到伺服器即可:(16進制格式)
10 53
00 04 4D 51 54 54 04 C2 00 3C
00 08 4C 69 6E 67 5F 59 61 6F
00 0F 6A 69 78 69 6E 2F 6A 69 78 69 61 6F 78 69 6E
00 2C 79 6D 6A 6F 68 4A 66 71 4D 4F 39 4B 46 7A 6A 4B 68 56 71 65 52 37 38 77 6E 52 70 74 30 55 30 58 78 72 71 71 35 56 45 48 64 63 49 3D
發送成功之後,伺服器會傳回(16進制格式) 20 02 00 00
如果接收到上述傳回的消息,證明已經成功和伺服器建立了可靠連接配接。分析一下來自伺服器的消息含義:第一個位元組 20 可以從固定頭那一節查到,是伺服器發送的資料,叫做“連接配接确認”;第二個位元組02表示後面還有兩個資料;後面的兩個資料 00 00 ,表示兩個有效資料。後面發送兩個資料的一層意思是,驗證從機連接配接的協定是否正确,即從機接收到02這個資料後應該判斷是否真的接收到了兩個資料,如果不是,那證明通信時有問題的。
6.2 按時發送心跳包,和伺服器保持聯系
其實這裡的心跳包,是通過發送 “Ping 請求” 給伺服器來實作的。在連接配接伺服器的時候,我們設定了心跳包時間為60S,那麼我們需要每60S之内就和伺服器“Ping 請求”一次,證明網絡連結是可靠的,如果接收不到伺服器的傳回,那麼可能是我們的網絡掉線了。
發送: C0 00 接收: D0 00
結束。