4. WebSocket桥接MQTT

任务目标

本案例将配置EMQX(或Mosquitto + WebSocket插件)以支持MQTT over WebSocket,并提供一个纯HTML+JavaScript的浏览器客户端,使其能够通过WebSocket协议连接到MQTT Broker,完成消息的订阅与发布。

环境依赖

  • EMQX Broker(v4.x或v5.x均可),默认已启用WebSocket监听端口8083(ws)和8084(wss)
  • 浏览器(Chrome/Firefox/Edge均可,需支持原生WebSocket API)
  • MQTT.js客户端库(通过CDN引入,无需本地安装)

实现方案

MQTT over WebSocket的本质是将MQTT协议数据包封装在WebSocket帧中传输。EMQX默认在8083端口提供此能力,无需额外配置。浏览器端使用MQTT.js库,通过ws://协议地址连接Broker,即可像原生MQTT客户端一样收发消息。

关键完整代码

创建一个mqtt_ws_client.html文件,内容如下:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>MQTT over WebSocket 浏览器客户端</title>
    <script src="https://unpkg.com/mqtt@5.3.5/dist/mqtt.min.js"></script>
</head>
<body>
    <h3>MQTT over WebSocket 测试工具</h3>
    <div>
        <label>Broker地址: </label>
        <input id="brokerUrl" value="ws://localhost:8083/mqtt" style="width:250px">
        <button onclick="connect()">连接</button>
        <button onclick="disconnect()">断开</button>
        <span id="connStatus">未连接</span>
    </div>
    <div style="margin-top:10px">
        <label>订阅主题: </label>
        <input id="subTopic" value="test/topic">
        <button onclick="subscribe()">订阅</button>
    </div>
    <div style="margin-top:10px">
        <label>发布主题: </label>
        <input id="pubTopic" value="test/topic">
        <label>消息: </label>
        <input id="pubMsg" value="Hello MQTT over WS">
        <button onclick="publish()">发布</button>
    </div>
    <div style="margin-top:10px">
        <h4>消息日志:</h4>
        <div id="log" style="border:1px solid #ccc;height:200px;overflow-y:auto;padding:5px"></div>
    </div>

    <script>
        let client = null;

        function log(msg) {
            const div = document.getElementById('log');
            const p = document.createElement('p');
            p.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
            div.appendChild(p);
            div.scrollTop = div.scrollHeight;
        }

        function connect() {
            const url = document.getElementById('brokerUrl').value.trim();
            if (!url) { log('请输入Broker地址'); return; }

            client = mqtt.connect(url, {
                clientId: 'browser_' + Math.random().toString(16).substr(2, 8),
                clean: true,
                connectTimeout: 5000,
                reconnectPeriod: 0
            });

            client.on('connect', () => {
                document.getElementById('connStatus').textContent = '已连接';
                document.getElementById('connStatus').style.color = 'green';
                log(`连接成功: ${url}`);
            });

            client.on('message', (topic, payload) => {
                log(`收到消息 [${topic}]: ${payload.toString()}`);
            });

            client.on('error', (err) => {
                log(`连接错误: ${err.message}`);
                document.getElementById('connStatus').textContent = '连接失败';
                document.getElementById('connStatus').style.color = 'red';
            });

            client.on('close', () => {
                document.getElementById('connStatus').textContent = '已断开';
                document.getElementById('connStatus').style.color = 'gray';
                log('连接已关闭');
            });
        }

        function disconnect() {
            if (client) {
                client.end(true);
                client = null;
                log('主动断开连接');
            }
        }

        function subscribe() {
            if (!client || !client.connected) { log('请先连接'); return; }
            const topic = document.getElementById('subTopic').value.trim();
            if (!topic) { log('请输入订阅主题'); return; }
            client.subscribe(topic, (err) => {
                if (err) { log(`订阅失败: ${err.message}`); }
                else { log(`已订阅: ${topic}`); }
            });
        }

        function publish() {
            if (!client || !client.connected) { log('请先连接'); return; }
            const topic = document.getElementById('pubTopic').value.trim();
            const msg = document.getElementById('pubMsg').value;
            if (!topic) { log('请输入发布主题'); return; }
            client.publish(topic, msg, { qos: 0 }, (err) => {
                if (err) { log(`发布失败: ${err.message}`); }
                else { log(`已发布到 [${topic}]: ${msg}`); }
            });
        }
    </script>
</body>
</html>

运行与验证

  1. 启动Broker:确保EMQX已启动,默认WebSocket监听端口为8083。可通过访问http://localhost:18083(EMQX Dashboard)确认状态。
  2. 打开页面:直接用浏览器打开mqtt_ws_client.html文件。
  3. 连接Broker:保持默认地址ws://localhost:8083/mqtt,点击“连接”,状态变为“已连接”且日志显示连接成功。
  4. 订阅主题:输入test/topic,点击“订阅”,日志显示“已订阅”。
  5. 发布消息:在发布区域输入相同主题和消息内容,点击“发布”。
  6. 验证闭环:日志中应出现“已发布”记录,随后立即出现“收到消息 [test/topic]: 你输入的内容”。这证明消息经MQTT Broker通过WebSocket完成了浏览器客户端的收发闭环。

常见问题

  • 连接失败:检查EMQX是否运行,以及8083端口是否被防火墙拦截。若使用wss,需配置证书。
  • 跨域限制:EMQX默认允许所有跨域请求,若自定义配置需在etc/emqx.conf中设置listener.ws.external.allow_origin为*。
  • 路径问题:EMQX的WebSocket默认路径为/mqtt,若使用其他Broker(如Mosquitto),需确认其WebSocket路径配置。