使用 WebSocket 客戶端連線 MQTT 伺服器

weixin_33860722發表於2018-07-22

使用 WebSocket 客戶端連線 MQTT 伺服器

[TOC]

簡介

近年來隨著 Web 前端的快速發展,瀏覽器新特性層出不窮,越來越多的應用可以在瀏覽器端或通過瀏覽器渲染引擎實現,Web 應用的即時通訊方式 WebSocket 得到了廣泛的應用。

WebSocke 是一種在單個 TCP 連線上進行全雙工通訊的協議。WebSocket 通訊協議於2011年被 IETF 定為標準 RFC 6455,並由 RFC 7936 補充規範。WebSocket API 也被 W3C 定為標準。

WebSocket 使得客戶端和伺服器之間的資料交換變得更加簡單,允許服務端主動向客戶端推送資料。在 WebSocket API 中,瀏覽器和伺服器只需要完成一次握手,兩者之間就直接可以建立永續性的連線,並進行雙向資料傳輸。 —— 摘自 維基百科 WebSocket

MQTT 協議第 6 章詳細約定了 MQTT 在 WebSocket [RFC6455] 連線上傳輸需要滿足的條件,協議內容筆者不在此累述有興趣的讀者可以自行檢視。由於協議實現細節較為複雜,本文選取兩個常用的 JavaScript MQTT 客戶端進行連線測試。

兩款客戶端比較

Paho.mqtt.js

Paho 是 Eclipse 的一個 MQTT 客戶端專案,Paho JavaScript Client 是其中一個基於瀏覽器的庫,它使用 WebSockets 連線到 MQTT 伺服器。相較於另一個 JavaScript 連線庫來說,其功能較少,不推薦使用。

MQTT.js

MQTT.js 一個 MQTT 協議的客戶端庫,用 JavaScript 編寫,可用於 Node.js 和瀏覽器。在 Node.js 端可以通過全域性安裝使用命令列連線,同時還支援 MQTT ,MQTT TLS 證照連線;值得一提的是 MQTT.js 還對微信小程式有較好的支援。

EMQ 君將以 MQTT.js 庫進行連線講解。

安裝 MQTT.js

如果讀者機器上裝有 Node.js 執行環境,可使用 npm 命令安裝 MQTT.js

在當前目錄安裝

npm i mqtt

全域性安裝

將註冊 mqtt mqtt_pub mqtt_sub 命令到當前使用者,此處藉助 iot.eclipse.org 講解一下命令列的使用

# 全域性安裝
npm i mqtt -g

# 使用命令列訂閱
$ mqtt sub -t 'hello' -h 'iot.eclipse.org' -v
> hello 09860

# 成功連線到伺服器並訂閱了主題 hello, 命令列將阻塞等待訊息


# 在另一個終端上使用命令列釋出
mqtt pub -t 'hello' -h 'iot.eclipse.org' -m 'from MQTT.js'

# 命令列將進行 連線 -> 釋出 -> 斷開連線 操作,此時讀者會到訂閱命令列,應當收到來自 hello 主題的訊息

> hello from MQTT.js

npm 在當前目錄安裝仍然可以使用 ./node_module/.bin/mqtt 命令來執行以上操作。

CDN 引用

MQTT.js 包可以通過 http://unpkg.com 獲得

<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>

<script>
    // 將在全域性初始化一個 mqtt 變數
    console.log(mqtt)
</script>

連線至 MQTT 伺服器

幾個公共的用於 WebSocket 測試連線伺服器:

  • test.mosquitto.org - 使用埠 8080 未加密,8081 用於 SSL 上的 WebSocket;
  • iot.eclipse.org - 使用埠 80 未加密,443 用於 SSL 上的 WebSocket;
  • broker.hivemq.com - 使用埠 8000 未加密,不支援 SSL 上的 WebSocket。

由於需要展示客戶端認證部分內容,但上述伺服器未提供客戶端認證服務,筆者特通過 ActorCloud 平臺註冊了一個裝置進行接入連線。

EMQ 使用 8083 埠用於普通連線,8084 用於 SSL 上的 WebSocket 連線。

// <script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
// const mqtt = require('mqtt')
import mqtt from 'mqtt'

// 連線選項
const options = {
      connectTimeout: 4000, // 超時時間
      // 認證資訊
      clientId: 'emqx-connect-via-websocket',
      username: 'emqx-connect-via-websocket',
      password: 'emqx-connect-via-websocket',
}

const client = mqtt.connect('wss://iot.actorcloud.io:8084/mqtt', options)

client.on('reconnect', (error) => {
    console.log('正在重連:', error)
})

client.on('error', (error) => {
    console.log('連線失敗:', error)
})

連線地址

上文示範的連線地址可以拆分為: wss: // iot . actorcloud.io : 8084 /mqtt

協議 // 主機名 . 域名 : / 路徑

初學者容易出現以下幾個錯誤:

  • 連線地址沒有指明協議:WebSocket 作為一種通訊協議,其使用 ws(非加密)、wss(SSL 加密) 作為協議標識。MQTT.js 客戶端支援多種協議,連線地址需指明協議型別;

  • 連線地址沒有指明埠:MQTT 並未對 WebSocket 接入埠做出規定,EMQ 上預設使用 8083 8084 分別作為非加密連線、加密連線埠。而 WebSocket 協議預設埠同 HTTP 保持一致 (80/443),不填寫埠則表明使用 WebSocket 的預設埠連線;而使用標準 MQTT 連線時則無需指定埠,如 MQTT.js 在 Node.js 端可以使用 mqtt://localhost 連線至標準 MQTT 8083 埠,當連線地址是 mqtts://localhost 則連線到 8884 埠;

  • 連線地址無路徑:MQTT-WebSoket 統一使用 /path 作為連線路徑,連線時需指明;

  • 協議與埠不符:使用了 wss 連線卻連線到 8083 埠;

  • 在 HTTPS 下使用非加密的 WebSocket 連線: Google 等機構在推進 HTTPS 的同時也通過瀏覽器約束進行了安全限定,即 HTTPS 連線下瀏覽器會自動禁止使用非加密的 ws 協議發起連線請求;

  • 證照與連線地址不符: 篇幅較長,詳見下文 "EMQ WebSocket 配置證照連線"。

連線選項

上面程式碼中, options 是客戶端連線選項,以下是主要引數說明,其餘引數詳見https://www.npmjs.com/package/mqtt#connect

  • keepalive:心跳時間,預設 60秒,設定 0 為禁用;

  • clientId: 客戶端 ID ,預設通過 'mqttjs_' + Math.random().toString(16).substr(2, 8) 隨機生成;

  • username:連線使用者名稱(如果有);

  • password:連線密碼(如果有);

  • clean:true,設定為 false 以在離線時接收 QoS 1 和 2 訊息;

  • reconnectPeriod:預設 1000 毫秒,兩次重新連線之間的間隔,客戶端 ID 重複、認證失敗等客戶端會重新連線;

  • connectTimeout:預設 30 * 1000毫秒,收到 CONNACK 之前等待的時間,即連線超時時間。

訂閱/取消訂閱

連線成功之後才能訂閱,且訂閱的主題必須符合 MQTT 訂閱主題規則;

注意 JavaScript 非同步非阻塞特性,只有在 connect 事件後才能確保客戶端已成功連線,或通過 client.connected 判斷是否連線成功:

// 錯誤示例
client.on('connect', handleConnect)
client.subscribe('hello')
client.publish('hello', 'Hello EMQ')

// 正確示例

client.on('connect', (e) => {
    console.log('成功連線伺服器')
    
    // 訂閱一個主題
    client.subscribe('hello', { qos: 1 }, (error) => {
        if (!error) {
            cosnole.log('訂閱成功')
            client.publish('hello', 'Hello EMQ', { qos: 1, rein: false }, (error) => {
                cosnole.log(error || '釋出成功')
            })
        }
    })
    
    // 訂閱多個主題
    client.subscribe(['hello', 'one/two/three/#', '#'], { qos: 1 },  onSubscribeSuccess)
    
    // 訂閱不同 qos 的不同主題
    client.subscribe(
        [
            { hello: 1 }, 
            { 'one/two/three': 2 }, 
            { '#': 0 }
        ], 
        onSubscribeSuccess,
    )
})

// 取消訂閱
client.unubscribe(
    // topic, topic Array, topic Array-Onject
    'hello',
    onUnubscribeSuccess,
)

釋出/接收訊息

釋出訊息到某主題,釋出的主題必須符合 MQTT 釋出主題規則,否則將斷開連線。釋出之前無需訂閱該主題,但要確保客戶端已成功連線:

// 監聽接收訊息事件
client.on('message', (topic, message) => {
    console.log('收到來自', topic, '的訊息', message.toString())
})

// 釋出訊息
if (!client.connected) {
    console.log('客戶端未連線')
    return
}

client.publish('hello', 'hello EMQ', (error) => {
    console.log(error || '訊息釋出成功')
})

微信小程式

MQTT.js 庫對微信小程式特殊處理,使用 wxs 協議識別符號。注意小程式開發規範中要求必須使用加密連線,連線地址應類似為wxs://iot.actorcloud.io:8084/mqtt

EMQ 啟用 SSL/TLS 加密連線

EMQ 內建自簽名證照,預設已經啟動了加密的 WebSocket 連線,但大部分瀏覽器會報證照無效錯誤如net::ERR_CERT_COMMON_NAME_INVALID (Chrome、360 等 webkit 核心瀏覽器在開發者模式下, Console 選項卡 可以檢視大部分連線錯誤)。

準備工作

這篇文章 https流程和原理 中對證照認證進行了詳細的闡述,EMQ 君總結啟用 SSL/TLS 證照需要具備的條件是:

  • 將域名繫結到 EMQ 伺服器公網地址:CA 機構簽發的證照籤名是針對域名的;

  • 申請證照:向 CA 機構申請所用域名的證照,注意選擇一個可靠的 CA 機構且證照要區分泛域名與主機名;

  • 使用加密連線的時候選擇 wss 協議,並使用域名連線:繫結域名-證照之後,必須使用域名而非 IP 地址進行連線,這樣瀏覽器才會根據域名去校驗證照以在通過校驗後建立連線。

在 EMQ 上配置

開啟 etc/emqx.conf 配置檔案,修改以下配置

# wss 監聽地址
listener.wss.external = 8084

# 修改金鑰檔案地址
listener.wss.external.keyfile = etc/certs/cert.key

# 修改證照檔案地址
listener.wss.external.certfile = etc/certs/cert.pem

重啟 EMQ 即可。

可以使用你的證照與金鑰檔案直接替換到 etc/certs/ 下。

在 nginx 上配置反向代理與證照

使用 nginx 來反向代理並加密 WebSocket 可以減輕 EMQ 伺服器計算壓力,同時實現域名複用,同時通過 nginx 的負載均衡可以分配多個後端服務實體。


# 建議 WebSocket 也繫結到 443 埠
listen 443, 8084;
server_name example.com;

ssl on;

ssl_certificate /etc/cert.crt;  # 證照路徑
ssl_certificate_key /etc/cert.key; # 金鑰路徑


# upstream 伺服器列表
upstream emq_server {
    server 10.10.1.1:8883 weight=1;
    server 10.10.1.2:8883 weight=1;
    server 10.10.1.3:8883 weight=1;
}

# 普通網站應用
location / {
    root www;
    index index.html;
}

# 反向代理到 EMQ 非加密 WebSocket
location / {
    proxy_redirect off;
    # upstream
    proxy_pass http://emq_server;
    
    proxy_set_header Host $host;
    # 反向代理保留客戶端地址
    proxy_set_header X-Real_IP $remote_addr;
    proxy_set_header X-Forwarded-For $remote_addr:$remote_port;
    # WebSocket 額外請求頭
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection “upgrade”;
}

其他資源

MQTT.js 官方例子給出了詳細的連線與使用操作例項程式碼,讀者可前往檢視;

EMQ Dashboard 中的 WebSocket 工具、ActorCloud 測試工具 -> MQTT 客戶端 (需到 ActorCloud 商城開通),均使用 MQTT.js 構建,讀者可體驗參考。

相關文章