HTML连接MQTT

简介

本文主要介绍如何在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

使用以下代码引入改库

1
<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 ,用户名和密码自己在控制台里定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
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地址后再加上几个随机字符。

1
2
3
4
5
6
7
8
9
10
11
12
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地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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() 函数结尾添加以下程序用来接收消息

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

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)
}
}
}

完整代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
<!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>