本文已参与「新人创作礼」活动,一起开启掘金创作之路。
MQTT 介绍
MQTT
是机器对机器(M2M
)/物联网(IoT
)连接协议。它被设计为一个极其轻量级的发布/订阅
消息传输协议。对于需要较小代码占用空间和/或网络带宽非常宝贵的远程连接非常有用,是专为受限设备和低带宽、高延迟或不可靠的网络而设计。这些原则也使该协议成为新兴的“机器到机器”(M2M
)或物联网(IoT
)世界的连接设备,以及带宽和电池功率非常高的移动应用的理想选择。例如,它已被用于通过卫星链路与代理通信的传感器、与医疗服务提供者的拨号连接,以及一系列家庭自动化和小型设备场景。它也是移动应用的理想选择,因为它体积小,功耗低,数据包最小,并且可以有效地将信息分配给一个或多个接收器。
-
MQTT 客户端
一个使用 MQTT 协议的设备、应用程序等,它总是建立到服务器的网络连接。
- 可以发布信息,其他客户端可以订阅该信息
- 订阅其它客户端发布的消息
- 退订或删除应用程序的消息
- 断开与服务器连接
-
MQTT 服务器
MQTT 服务器也称为 Broker(消息代理),以是一个应用程序或一台设备。它是位于消息发布者 和订阅者之间
- 接受来自客户端的网络连接
- 接受客户端发布的应用信息
- 处理来自客户端的订阅和退订请求
- 向订阅的客户转发应用程序消息
-
主题(Topic)
连接到一个应用程序消息的标签,该标签与服务器的订阅相匹配。服务器会将消息发送给订阅所匹配标签的每个客户端。
- 要订阅的主题。一个主题可以有多个级别,级别之间用斜杠字符分隔。例如,
/world
和emq/emqtt/emqx
是有效的主题。 - 订阅者的 Topic name 支持通配符
#
和+
:- # 支持一个主题内任意级别话题
- + 只匹配一个主题级别的通配符
- 客户端成功订阅某个主题后,代理会返回一条 SUBACK 消息,其中包含一个或多个 returnCode 参数
- 要订阅的主题。一个主题可以有多个级别,级别之间用斜杠字符分隔。例如,
-
主题筛选器(Topic Filter)
一个对主题名通配符筛选器,在订阅表达式中使用,表示订阅所匹配到的多个主题。
-
QoS(消息传递的服务质量水平)
服务质量,标志表明此主题范围内的消息传送到客户端所需的一致程度。
- 值 0:不可靠,消息基本上仅传送一次,如果当时客户端不可用,则会丢失该消息。
- 值 1:消息应传送至少 1 次。
- 值 2:消息仅传送一次。
-
会话(Session)
每个客户端与服务器建立连接后就是一个会话,客户端和服务器之间有状态交互。会话存在于一个网络之间,也可能在客户端和服务器之间跨越多个连续的网络连接。
-
订阅(Subscription)
订阅包含主题筛选器(Topic Filter)和最大服务质量(QoS)。订阅会与一个会话(Session)关联。一个会话可以包含多个订阅。每一个会话中的每个订阅都有一个不同的主题筛选器。
- 客户端在成功建立 TCP 连接之后,发送 CONNECT 消息,在得到服务器端授权允许建立彼此连接的 CONNACK 消息之后,客户端会发送 SUBSCRIBE 消息,订阅感兴趣的 Topic 主题列表(至少一个主题)
- 订阅的主题名称采用 UTF-8 编码,然后紧跟着对应的 QoS 值
-
发布(publish)
控制报文是指从客户端向服务端或者服务端向客户端传输一个应用消息,MQTT 客户端发送消息请求,发送完成后返回应用程序线程
- 比如安卓的推送服务,还有一些即时通信软件如微信等也是采用的推送技术。
-
负载(Payload)
消息订阅者所具体接收的内容
简单示例
MQTT 协议主要是根据以下情况设计的:
- M2M(Machine to Machine),机器或设备间端到端通信,比如传感器之间的数据通讯。
- 设备(Machine)中,例如传感器,硬件能力很弱,协议要考虑尽量小的资源消耗,比如计算能力和存储等。
根据 MQTT 的基础了解后并结合简单的架构,在这里做一个简单的示例图,可以更直观的理解 MQTT 协议的通信模型。MQTT Broker 就选择 EMQ 作为示范。比如有1个温度传感器(1个 Machine),1个移动设备,1个电脑,一个服务器(3个 Machine),都可以得到或者显示温度传感器的温度值,需要先通过 MQTT 协议 subscribe(订阅)一个比如叫 temperature 的 topic(主题)如下:
图中移动设备,服务器,电脑需要先通过 EMQ subscribe 一个叫 temperature 的 topic,当温度传感器 publish 温度数据,三个设备就可以收到了。
搭建本地 MQTT 服务器
EMQX(推荐,免安装)
官网下载(开源版):www.emqx.io/zh/download…
注意:必须在 Windows Shell 中运行
# 解压之后进入目录
# 执行命令脚本
$ ./bin/emqx start
emqx 4.0.0 is started successfully!
$ ./bin/emqx_ctl status
Node 'emqx@127.0.0.1' is started
emqx v4.0.0 is running
# 停止 EMQX Broker
$ ./bin/emqx stop
ok
当 EMQ X 成功运行在你的本地计算机上且 EMQ X Dashboard 被默认启用时,你可以访问 http://localhost:18083 来查看你的 Dashboard,默认用户名是 admin,密码是 public。
关于Dashboard的具体使用,可参见:EMQX Dashboard
基本命令
EMQX 提供了 emqx
命令行工具,方便用户对 EMQX 进行启动、关闭、进入控制台等操作。
-
emqx start
后台启动 EMQX Broker;
-
emqx stop
关闭 EMQX Broker;
-
emqx restart
重启 EMQX Broker;
-
emqx console
使用控制台启动 EMQX Broker;
-
emqx foreground
使用控制台启动 EMQX Broker,与
emqx console
不同,emqx foreground
不支持输入 Erlang 命令; -
emqx ping
Ping EMQX Broker。
mosquitto
可自行在网上了解,不做详细介绍。
购买远程 MQTT 服务器(14天免费试用)
EMQX Cloud 官网:www.emqx.com/zh/cloud
可以通过按小时购买或14天免费试用获取服务,控制台界面如下:
Python 程序实例
首先安装相关依赖:
pip3 install -i https://pypi.doubanio.com/simple paho-mqtt
发布端 publish.py
import random
import time
from paho.mqtt import client as mqtt_client
broker = 'broker.emqx.io'
port = 1883
topic = "/python/mqtt"
# generate client ID with pub prefix randomly
client_id = f'python-mqtt-{random.randint(0, 1000)}'
def connect_mqtt():
def on_connect(client, userdata, flags, rc):
if rc == 0:
print("Connected to MQTT Broker!")
else:
print("Failed to connect, return code %d\n", rc)
client = mqtt_client.Client(client_id)
client.on_connect = on_connect
client.connect(broker, port)
return client
def publish(client):
msg_count = 0
while True:
time.sleep(1)
msg = f"messages: {msg_count}"
result = client.publish(topic, msg)
# result: [0, 1]
status = result[0]
if status == 0:
print(f"Send `{msg}` to topic `{topic}`")
else:
print(f"Failed to send message to topic {topic}")
msg_count += 1
def run():
client = connect_mqtt()
client.loop_start()
publish(client)
if __name__ == '__main__':
run()
订阅端 subscribe.py
import random
from paho.mqtt import client as mqtt_client
broker = 'broker.emqx.io'
port = 1883
topic = "/python/mqtt"
# generate client ID with pub prefix randomly
client_id = f'python-mqtt-{random.randint(0, 100)}'
def connect_mqtt() -> mqtt_client:
def on_connect(client, userdata, flags, rc):
if rc == 0:
print("Connected to MQTT Broker!")
else:
print("Failed to connect, return code %d\n", rc)
client = mqtt_client.Client(client_id)
client.on_connect = on_connect
client.connect(broker, port)
return client
def subscribe(client: mqtt_client):
def on_message(client, userdata, msg):
print(f"Received `{msg.payload.decode()}` from `{msg.topic}` topic")
client.subscribe(topic)
client.on_message = on_message
def run():
client = connect_mqtt()
subscribe(client)
client.loop_forever()
if __name__ == '__main__':
run()
Vue 程序实例
安装 MQTT 客户端库:
- 通过命令行安装,可以使用 npm 或 yarn 命令,二者选一
npm install mqtt --save
yarn add mqtt
- 通过 CDN 引入
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
- 下载到本地,然后使用相对路径引入
mqtt.vue
<template>
<div class="home-container">
<el-card shadow="always" style="margin:30px 0;">
<div class="emq-title">
Configuration
</div>
<el-form ref="configForm" hide-required-asterisk size="small" label-position="top" :model="connection">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item prop="host" label="Host">
<el-input v-model="connection.host"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item prop="port" label="Port">
<el-input v-model.number="connection.port" type="number" placeholder="8083/8084"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item prop="endpoint" label="Mountpoint">
<el-input v-model="connection.endpoint" placeholder="/mqtt"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item prop="clientId" label="Client ID">
<el-input v-model="connection.clientId"> </el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item prop="username" label="Username">
<el-input v-model="connection.username"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item prop="password" label="Password">
<el-input v-model="connection.password"></el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-button
type="success"
size="small"
class="conn-btn"
style="margin-right: 20px;"
:disabled="client.connected"
@click="createConnection"
>
{{ client.connected ? 'Connected' : 'Connect' }}
</el-button>
<el-button v-if="client.connected" type="danger" size="small" class="conn-btn" @click="destroyConnection">
Disconnect
</el-button>
</el-col>
</el-row>
</el-form>
</el-card>
<el-card shadow="always" style="margin-bottom:30px;">
<div class="emq-title">
Subscribe
</div>
<el-form ref="subscription" hide-required-asterisk size="small" label-position="top" :model="subscription">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item prop="topic" label="Topic">
<el-input v-model="subscription.topic"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item prop="qos" label="QoS">
<el-select v-model="subscription.qos">
<el-option
v-for="(item, index) in qosList"
:key="index"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-button
:disabled="!client.connected"
type="success"
size="small"
class="subscribe-btn"
@click="doSubscribe"
>
{{ subscribeSuccess ? 'Subscribed' : 'Subscribe' }}
</el-button>
<el-button
:disabled="!client.connected"
type="success"
size="small"
class="subscribe-btn"
style="margin-left:20px"
@click="doUnSubscribe"
v-if="subscribeSuccess"
>
Unsubscribe
</el-button>
</el-col>
</el-row>
</el-form>
</el-card>
<el-card shadow="always" style="margin-bottom:30px;">
<div class="emq-title">
Publish
</div>
<el-form ref="publish" hide-required-asterisk size="small" label-position="top" :model="publish">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item prop="topic" label="Topic">
<el-input v-model="publish.topic"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item prop="payload" label="Payload">
<el-input v-model="publish.payload" size="small"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item prop="qos" label="QoS">
<el-select v-model="publish.qos">
<el-option
v-for="(item, index) in qosList"
:key="index"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-col :span="24">
<el-button :disabled="!client.connected" type="success" size="small" class="publish-btn" @click="doPublish">
Publish
</el-button>
</el-col>
</el-card>
<el-card shadow="always" style="margin-bottom:30px;">
<div class="emq-title">
Receive
</div>
<el-col :span="24">
<el-input type="textarea" :rows="3" style="margin-bottom: 15px" v-model="receiveNews"></el-input>
</el-col>
</el-card>
</div>
</template>
<script>
import mqtt from 'mqtt'
export default {
name: 'Mqtt',
data() {
return {
connection: {
host: 'broker.emqx.io',
port: 8083,
endpoint: '/mqtt',
clean: true, // 保留会话
connectTimeout: 4000, // 超时时间
reconnectPeriod: 4000, // 重连时间间隔
// 认证信息
clientId: 'mqttjs_3be2c321',
username: 'emqx_test',
password: 'emqx_test',
},
subscription: {
topic: 'topic/mqttx',
qos: 0,
},
publish: {
topic: 'topic/browser',
qos: 0,
payload: '{ "msg": "Hello, I am browser." }',
},
receiveNews: '',
qosList: [
{ label: 0, value: 0 },
{ label: 1, value: 1 },
{ label: 2, value: 2 },
],
client: {
connected: false,
},
subscribeSuccess: false,
}
},
created() {
this.createConnection()
},
methods: {
// 创建连接
createConnection() {
// 连接字符串, 通过协议指定使用的连接方式
// ws 未加密 WebSocket 连接
// wss 加密 WebSocket 连接
// mqtt 未加密 TCP 连接
// mqtts 加密 TCP 连接
// wxs 微信小程序连接
// alis 支付宝小程序连接
const { host, port, endpoint, ...options } = this.connection
const connectUrl = `ws://${host}:${port}${endpoint}`
try {
this.client = mqtt.connect(connectUrl, options)
} catch (error) {
console.log('mqtt.connect error', error)
}
this.client.on('connect', () => {
console.log('Connection succeeded!')
})
this.client.on('error', error => {
console.log('Connection failed', error)
})
this.client.on('message', (topic, message) => {
this.receiveNews = this.receiveNews.concat(message)
console.log(`Received message ${message} from topic ${topic}`)
})
},
// 订阅主题
doSubscribe() {
const { topic, qos } = this.subscription
this.client.subscribe(topic, { qos }, (error, res) => {
if (error) {
console.log('Subscribe to topics error', error)
return
}
this.subscribeSuccess = true
console.log('Subscribe to topics res', res)
})
},
// 取消订阅
doUnSubscribe() {
const { topic } = this.subscription
this.client.unsubscribe(topic, error => {
if (error) {
console.log('Unsubscribe error', error)
}
})
},
// 发送消息
doPublish() {
const { topic, qos, payload } = this.publish
this.client.publish(topic, payload, qos, error => {
if (error) {
console.log('Publish error', error)
}
})
},
// 断开连接
destroyConnection() {
if (this.client.connected) {
try {
this.client.end()
this.client = {
connected: false,
}
console.log('Successfully disconnected!')
} catch (error) {
console.log('Disconnect failed', error.toString())
}
}
}
},
}
</script>
<style />
Spring Boot 集成 MQTT
依赖 pom.xml
<!-- MQTT -->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mqtt</artifactId>
</dependency>
配置类 MqttConfig.java
Spring 中集成框架,有消息入站通道(用来接收消息)和出站通道(用来发送消息)
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
@Configuration
public class MqttConfig {
// 消费消息
/**
* 创建MqttPahoClientFactory,设置MQTT Broker连接属性,如果使用SSL验证,也在这里设置。
* @return factory
*/
@Bean
public MqttPahoClientFactory mqttClientFactory() {
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
MqttConnectOptions options = new MqttConnectOptions();
// 设置代理端的URL地址,可以是多个
options.setServerURIs(new String[]{"tcp://127.0.0.1:1883"});
factory.setConnectionOptions(options);
return factory;
}
/**
* 入站通道
*/
@Bean
public MessageChannel mqttInputChannel() {
return new DirectChannel();
}
/**
* 入站
*/
@Bean
public MessageProducer inbound() {
// Paho客户端消息驱动通道适配器,主要用来订阅主题
MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter("consumerClient-paho",
mqttClientFactory(), "boat", "collector", "battery", "+/sensor");
adapter.setCompletionTimeout(5000);
// Paho消息转换器
DefaultPahoMessageConverter defaultPahoMessageConverter = new DefaultPahoMessageConverter();
// 按字节接收消息
// defaultPahoMessageConverter.setPayloadAsBytes(true);
adapter.setConverter(defaultPahoMessageConverter);
adapter.setQos(1); // 设置QoS
adapter.setOutputChannel(mqttInputChannel());
return adapter;
}
@Bean
// ServiceActivator注解表明:当前方法用于处理MQTT消息,inputChannel参数指定了用于消费消息的channel。
@ServiceActivator(inputChannel = "mqttInputChannel")
public MessageHandler handler() {
return message -> {
String payload = message.getPayload().toString();
// byte[] bytes = (byte[]) message.getPayload(); // 收到的消息是字节格式
String topic = message.getHeaders().get("mqtt_receivedTopic").toString();
// 根据主题分别进行消息处理。
if (topic.matches(".+/sensor")) { // 匹配:1/sensor
String sensorSn = topic.split("/")[0];
System.out.println("传感器" + sensorSn + ": 的消息: " + payload);
} else if (topic.equals("collector")) {
System.out.println("采集器的消息:" + payload);
} else {
System.out.println("丢弃消息:主题[" + topic + "],负载:" + payload);
}
};
}
// 发送消息
/**
* 出站通道
*/
@Bean
public MessageChannel mqttOutboundChannel() {
return new DirectChannel();
}
/**
* 出站
*/
@Bean
@ServiceActivator(inputChannel = "mqttOutboundChannel")
public MessageHandler outbound() {
// 发送消息和消费消息Channel可以使用相同MqttPahoClientFactory
MqttPahoMessageHandler messageHandler = new MqttPahoMessageHandler("publishClient", mqttClientFactory());
messageHandler.setAsync(true); // 如果设置成true,即异步,发送消息时将不会阻塞。
messageHandler.setDefaultTopic("command");
messageHandler.setDefaultQos(1); // 设置默认QoS
// Paho消息转换器
DefaultPahoMessageConverter defaultPahoMessageConverter = new DefaultPahoMessageConverter();
// defaultPahoMessageConverter.setPayloadAsBytes(true); // 发送默认按字节类型发送消息
messageHandler.setConverter(defaultPahoMessageConverter);
return messageHandler;
}
}
接口 MqttGateway.java
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.handler.annotation.Header;
@MessagingGateway(defaultRequestChannel = "mqttOutboundChannel")
public interface MqttGateway {
// 定义重载方法,用于消息发送
void sendToMqtt(String payload);
// 指定topic进行消息发送
void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, String payload);
void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, String payload);
void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, byte[] payload);
}
测试
测试方式:使用接口工具,给接口发送消息,从而调用MQTT客户端发布消息
MqttController.java
import com.ioufev.mqtt.domain.MyMessage;
import com.ioufev.mqtt.mqtt.MqttGateway;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class MqttController {
@Resource
private MqttGateway mqttGateway;
@PostMapping("/send")
public String send(@RequestBody MyMessage myMessage) {
// 发送消息到指定主题
mqttGateway.sendToMqtt(myMessage.getTopic(), 1, myMessage.getContent());
return "send topic: " + myMessage.getTopic()+ ", message : " + myMessage.getContent();
}
}
MyMessage.java
public class MyMessage {
private String topic;
private String content;
public String getTopic() {
return topic;
}
public void setTopic(String topic) {
this.topic = topic;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
MQTT 协议监听工具
本地下载地址:MQTT X:跨平台 MQTT 5.0 桌面客户端工具