简介

本文主要介绍如何在Browser js,即浏览器环境下,使用 HTML 连接MQTT服务器。

服务器使用EMQX为例。部分代码使用EMQX官方文档。

连接到MQTT服务器分为Websocket方式连接和Websocket TLS/SSL方式连接,使用EMQX的公共服务器的话是 Websocket 方式连接,使用EMQX的私有服务器为Websocket TLS/SSL方式连接。Websocket 和Websocket TLS/SSL连接的区别在于设置服务器地址时前者的协议为ws(或mqtt),后者的协议为wss,其他的协议类型都不行。两种连接的端口也不同。

本文所有的调试信息都输出在控制台,请打开控制台以查看输出。

EMQX公共服务器链接使用说明

EMQX私有服务器链接使用说明


安装依赖

根据官方文档“ MQTT.js 是一个完全开源的 MQTT 协议的客户端库,使用 JavaScript 编写,可用于 Node.js 和浏览器环境。”

CDN地址:https://unpkg.com/mqtt/dist/mqtt.min.js

使用以下代码引入改库

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

定义基本信息

Websocket 使用ws协议,或mqtt

Websocket TLS/SSL使用wss协议,其他的协议类型都不行。

端口需要写在URL的后面

根据官方文档“MQTT-WebSocket 统一使用 /path 作为连接路径,连接时需指明,而 EMQX Broker 使用的路径为 /mqtt。”

如果使用EMQX的公共服务器,使用 Websocket 连接,服务器地址为broker.emqx.io,端口为 8083 ,即 broker.emqx.io:8083/mqtt ,连接用户名为 emqx ,密码为 public ,也可以不填。

如果使用私有服务器,使用 Websocket TLS/SSL 连接,服务器地址根据控制台具体信息,端口为 8084 ,用户名和密码自己在控制台里定义。

const connectUrl = 'ws://'
  //或wws://
const options = {
    connectTimeout: 4000,
    reconnectPeriod: 1000,
    clientId: '',
  //设备ID
    username: '',
  //账号,也可删去不填
    password: '',
  //密码,也可删去不填
    clean: true,
}

设备ID可以自定义,也可以在结尾添加设备的IP地址用于区分设备,因为如果存在相同的设备ID,那么所有拥有相同设备ID的设备将会不断的断开重连。获取IP地址需要调用外部API。使用以下代码在定义的设备ID后添加上IP地址,为了保险起见防止IP地址重复导致设备ID重复,那么还可以在IP地址后再加上几个随机字符。

function getIPAddress() 
{
    return fetch("https://api.ipify.org?format=json")      
        .then(response => response.json())      
            .then(data => {        
                options.clientId += '-'+data.ip;
                options.clientId += '-' + Math.random().toString(16).substring(2, 8); 
            })      
            .catch(error => {        
        console.log("Error:", error);      
    });  
}

建立连接

连接到mqtt服务器的语句在 mqttConnet() 函数内,因为如果在设备ID后添加IP地址,需要从外部API获取IP地址,但是需要一定的时间,所以这里使用了 async/await ,并在 mqttConnet() 函数内调用 getIPAddress() 来对函数进行阻塞,直到获取到IP地址,以确保在连接mqtt服务器时已经获取到IP地址并添加到设备ID后,否则连接到服务器时还未获得到IP地址。

var client;
const qos=0;

async function mqttConnet() {    
    await getIPAddress();    
    client = mqtt.connect(connectUrl, options);

    client.on('error', (error) => {
        console.log('连接失败: ', error);
        client.end();
    });

    client.on('reconnect', () => {
        console.log('重连中...');
    });

    client.on('connect', () => {
        console.log('连接成功:' + options.clientId);
    });

    client.on("close", () => {
        console.log('已经断开连接');
    });
}

订阅主题、接收发送消息等

mqttConnet() 函数结尾添加以下程序用来接收消息

client.on('message', (topic, msg) => {
    console.log('收到消息:');
    console.log('  主题:', topic);
    console.log('  消息:', msg.toString());
});

这里使用了 默认主题 的概念,如果程序只使用一个主题的话,可以用 setDefaultTopic(topic) 函数设置默认主题,之后调用订阅,取消订阅,发布消息的函数时就可以不用再填写主题了,函数默认参数值为默认主题。

也可以订阅、取消订阅到发布消息到多个不同的主题,这时只有其中一个主题可以设置为默认主题,其他主题需要手动填写。


var defaultTopic;

function setDefaultTopic(topic){   //设置默认主题
    defaultTopic = topic;
}

function subscribe(topic=defaultTopic){   //订阅主题
    client.subscribe(topic, {qos} ,(error) => {
        if (error) {
            console.log('订阅失败:',error);
            return;
        }
        console.log('订阅主题:', topic);
    });
}

function unsubscribe(topic=defaultTopic){   //取消订阅主题
    client.unsubscribe(topic, {qos}, (error) => {
        if (error) {
            console.log('取消订阅失败:', error);
            return;
        }
        console.log('取消订阅主题:', topic);
    });
}

function publish(payload, topic=defaultTopic){   //发布消息
    client.publish(topic, payload, { qos }, (error) => {
        if (error) {
            console.log('发布失败:',error);
            return;
        }
        console.log('发布成功:');
        console.log('  主题:', topic);
        console.log('  消息:', payload);
    });
}

function disconnect()   //断开连接
{
    if (client.connected) {
        try {
            client.end(false, () => {
            console.log('断开连接')
            })
        } catch (error) {
            console.log('断开连接失败:', error)
        }
    }
}

完整代码示例

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>AIRCON</title>

        <script src="https://unpkg.com/mqtt@5.0.3/dist/mqtt.min.js"></script>
        <script>
            var IP='';
            const connectUrl = 'ws://broker.emqx.io:8083/mqtt';
            const options = {
                connectTimeout: 4000,
                reconnectPeriod: 1000,
                clientIdHead: 'html',
                clean: true,
            };

            var client;
            var defaultTopic;
            const qos=0;

            function getIPAddress() 
            {
                return fetch("https://api.ipify.org?format=json")      
                    .then(response => response.json())      
                        .then(data => {        
                            options.clientId = options.clientIdHead+'-'+data.ip;
                            options.clientId += '-' + Math.random().toString(16).substring(2, 8); 
                        })      
                        .catch(error => {        
                    console.log("Error:", error);      
                });  
            }
            

            mqttConnect();
            async function mqttConnect() {    
                await getIPAddress();    
                client = mqtt.connect(connectUrl, options);

                client.on('error', (error) => {
                    console.log('连接失败: ', error);
                    client.end();
                });

                client.on('reconnect', () => {
                    console.log('重连中...');
                });

                client.on('connect', () => {
                    console.log('连接成功:' + options.clientId);
                });

                client.on("close", () => {
                    console.log('已经断开连接');
                });

                client.on('message', (topic, msg) => {
                    console.log('收到消息:');
                    console.log('  主题:', topic);
                    console.log('  消息:', msg.toString());
                });
            }

            function setDefaultTopic(topic){   //设置默认主题
                defaultTopic = topic;
                console.log('设置默认主题:', topic);
            }

            function subscribe(topic=defaultTopic){   //订阅主题
                client.subscribe(topic, {qos} ,(error) => {
                    if (error) {
                        console.log('订阅失败:',error);
                        return;
                    }
                    console.log('订阅主题:', topic);
                });
            }

            function unsubscribe(topic=defaultTopic){   //取消订阅主题
                client.unsubscribe(topic, {qos}, (error) => {
                    if (error) {
                        console.log('取消订阅失败:', error);
                        return;
                    }
                    console.log('取消订阅主题:', topic);
                });
            }
            
            function publish(payload, topic=defaultTopic){   //发布消息
                client.publish(topic, payload, { qos }, (error) => {
                    if (error) {
                        console.log('发布失败:',error);
                        return;
                    }
                    console.log('发布成功:');
                    console.log('  主题:', topic);
                    console.log('  消息:', payload);
                });
            }

            function disconnect()   //断开连接
            {
                if (client.connected) {
                    try {
                        client.end(false, () => {
                        console.log('断开连接')
                        })
                    } catch (error) {
                        console.log('断开连接失败:', error)
                    }
                }
            }
        </script>
    </head>

    <body>
        <input type="text" id="defaultTopic" value="输入默认主题"/>
        <button onclick="setDefaultTopic(document.getElementById('defaultTopic').value)" class="inputbutton">设置默认主题</button>
        <button onclick="subscribe()" class="inputbutton">订阅主题</button>
        <button onclick="publish('on')" class="inputbutton">开</button>
        <button onclick="publish('off')" class="inputbutton">关</button>
        <button onclick="unsubscribe()" class="inputbutton">取消订阅主题</button>
        <button onclick="disconnect()" class="inputbutton">断开连接</button>
        <button onclick="mqttConnect()" class="inputbutton">连接</button>
    </body>
</html>

循之际,如星夜般的幻想。