背景
近期公司可能会有物联网设备相关项目内容,提前对用到的mqtt协议做预研和初步使用。
最初接触到mqtt协议应该是早些年的即时通讯吧,现在已经是物联网设备最热门的协议了。
作为记录,也希望能帮助到需要的朋友。
MQTT介绍
《MQTT 协议规范中文版》一书中对 MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)进行了描述:
MQTT 是一种基于客户端服务端架构的发布/订阅模式的消息传输协议。它的设计思想是轻巧、开放、 简单、规范,易于实现。这些特点使得它对很多场景来说都是很好的选择,特别是对于受限的环境如机器与机器的通信(M2M)以及物联网环境(IoT)。----MQTT 协议中文版
以上这段话很好的描述了 MQTT 的全部含义,它是一种轻巧、开放、简单、规范的网络通信协议。与 HTTP 协议一样,MQTT 协议也是应用层协议,工作在 TCP/IP 四层模型中的最上层(应用层),构建于 TCP/IP协议上。MQTT 最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。
如今,MQTT 成为了最受欢迎的物联网协议,已广泛应用于车联网、智能家居、即时聊天应用和工业互联网等领域。目前通过 MQTT 协议连接的设备已经过亿,这些都得益于 MQTT 协议为设备提供了稳定、可靠、易用的通信基础。
MQTT 的主要特性
MQTT 协议是为工作在低带宽、不可靠网络的远程传感器和控制设备之间的通讯而设计的协议,它具 有以下主要的几项特性:
① 使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合。
② 基于 TCP/IP 提供网络连接。主流的 MQTT 是基于 TCP 连接进行数据推送的,但是同样也有基于 UDP 的版本,叫做 MQTT-SN。这两种版本由于基于不同的连接方式,优缺点自然也就各有不同了。
③ 支持 QoS 服务质量等级。根据消息的重要性不同设置不同的服务质量等级。
④ 小型传输,开销很小,协议交换最小化,以降低网络流量。这就是为什么在介绍里说它非常适合"在物联网领域,传感器与服务器的通信,信息的收集",要知道嵌入式设备的运算能力和带宽都相对薄弱,使用这种协议来传递消息再适合不过了,在手机移动应用方面,MQTT 是一种不错的 Android 消息推送方案。
⑤ 使用 will 遗嘱机制来通知客户端异常断线。
⑥ 基于主题发布/订阅消息,对负载内容屏蔽的消息传输。
⑦ 支持心跳机制。
MQTT 协议
MQTT 是一种基于客户端-服务端架构的消息传输协议,所以在 MQTT 协议通信中,有两个最为重要的角色,它们便是服务端和客户端。
服务端
MQTT 服务端通常是一台服务器(broker),它是 MQTT 信息传输的枢纽,负责将 MQTT 客户端发送来的信息传递给 MQTT 客户端;MQTT 服务端还负责管理 MQTT 客户端,以确保客户端之间的通讯顺畅,保证 MQTT 信息得以正确接收和准确投递。
客户端
MQTT 客户端可以向服务端发布信息,也可以从服务端收取信息;我们把客户端发送信息的行为称为 “发布”信息。而客户端要想从服务端收取信息,则首先要向服务端“订阅”信息。“订阅”信息这一操作 很像我们在使用微信时“关注”了某个公众号,当公众号的作者发布新的文章时,微信官方会向关注了该公众号的所有用户发送信息,告诉他们有新文章更新了,以便用户查看。
MQTT 主题
上面我们讲到了,客户端想要从服务器获取信息,首先需要订阅信息,那客户端如何订阅信息呢?这里我们要引入“主题(Topic)”的概念,“主题”在 MQTT 通信中是一个非常重要的概念,客户端发布信息以及订阅信息都是围绕“主题”来进行的,并且 MQTT 服务端在管理 MQTT 信息时,也是使用“主题”来控制的。
客户端发布消息时需要为消息指定一个“主题”,表示将消息发布到该主题;而对于订阅消息的客户端 来说,可通过订阅“主题”来订阅消息,这样当其它客户端或自己(当前客户端)向该主题发布消息时,MQTT 服务端就会将该主题的信息发送给该主题的订阅者(客户端)。
为了便于您更好理解服务端是如何通过“主题”来控制客户端之间的信息通讯,我们来看看下图实例:
在以上图示中一共有三个 MQTT 客户端,它们分别是开发板、手机和电脑。MQTT 服务端在管理 MQTT通信时使用了“主题”来对信息进行管理。比如上图所示,假设我们需要利用手机和电脑获取开发板在运行过程中 SoC 芯片的温度,那么首先电脑和手机这两个客户端需要向 MQTT 服务器订阅主题“芯片温度”;接下来,当开发板客户端向服务端的“芯片温度”主题发布信息(假设信息的内容就是当前的温度值)后,服务端就会首先检查都有哪些客户端订阅了“芯片温度”这一主题的信息,而当它发现订阅了该主题的客户端有一个手机和一个电脑,于是服务端就会将刚刚收到的“芯片温度”信息转发给订阅了该主题的手机和电脑客户端。
通过以上的这种实例,手机和电脑便可以获取到开发板运行时 SoC 芯片的温度值。
以上实例中,开发板是“芯片温度”主题的发布者,而手机和电脑则是该主题的订阅者。
值得注意的是,MQTT 客户端在通信时,角色往往不是单一的,一个客户端既可以作为信息发布者也 可以同时作为信息订阅者。如下图所示:
上图中的所有客户端都是围绕“LED 控制”这一主题进行通信。此时,对于“LED 控制”这一主题来 说,手机和电脑客户端成为了 MQTT 信息的发布者而开发板则成为了 MQTT 信息的订阅者(接收者)。
所以由此可知,针对不同的主题,MQTT 客户端可以切换自己的角色,它们可能对主题 A 来说是信息发布者,但是对于主题 B 就成了信息订阅者,所以一个 MQTT 客户端它的角色并不是固定的,所以大家一定要理解“主题”这个概念。
MQTT 发布/订阅特性
从以上实例我们可以看到,MQTT 通信的核心枢纽是 MQTT 服务端,它负责将 MQTT 客户端发送来的信息传递给 MQTT 客户端,还负责管理 MQTT 客户端,以确保客户端之间的通讯顺畅,保证 MQTT 信息得以正确接收和准确投递。
正是因为有了服务端对 MQTT 信息的接收、储存、处理和发送,客户端在发布和订阅信息时,可以相 互独立、且在空间上可以分离、时间上可以异步,这就是 MQTT 发布/订阅的特性:客户端相互独立、空间上可分离、时间上可异步,具体介绍如下:
⚫ 客户端相互独立:MQTT 客户端是一个个独立的个体,它们无需了解彼此的存在,依然可以实现 信息交流。譬如在上面的实例中,开发板客户端在发布“芯片温度”信息时,开发板客户端本身完全不知道有多少个 MQTT 客户端订阅了“芯片温度”这一主题;而订阅了“芯片温度”主题的手机和电脑客户端也完全不知道彼此的存在,大家只要订阅了“芯片温度”这一主题,MQTT 服务端就会在每次收到新信息时,将信息发送给订阅了“芯片温度”主题的客户端。
⚫ 空间上分离:空间上分离相对容易理解,MQTT 客户端以及 MQTT 服务端它们在通信时是处于同一个通信网络中的,这个网络可以是互联网或者局域网;只要客户端联网,无论他们远在天边还是近在眼前,都可以实现彼此间的通讯交流;其实网络通信本就是如此,所以并不是 MQTT 通信所特有的。
⚫ 时间上可异步:MQTT 客户端在发送和接收信息时无需同步。这一特点对物联网设备尤为重要,前面我们也介绍了,MQTT 从诞生之初就是专为低带宽、高延迟或不可靠的网络而设计的,高延迟和不可靠网络必然就会导致时间上的异步;物联网设备在运行过程中发生意外掉线是非常正常的情况,我们使用上面的实例二的场景来作说明,当开发板在运行过程中,可能会由于突然断电(假设开发板是通过电源适配器供电的)导致掉线,这时开发板会断开与 MQTT 服务端的连接。假设此时我们的手机客户端向开发板客户端所订阅的“LED 控制”主题发布了信息,而开发板恰恰不在线,这时,MQTT 服务端可以将“LED 控制”主题的新信息保存,待开发板客户端再次上线后,服务端再将“LED 控制”信息推送给开发板。所以这就必然导致了,手机发送信息与开发板接收信息在时间上是异步的。
MQTT服务端部署
推荐使用docker部署,一行命令搞定。
docker run -d --name emqx -p 1883:1883 -p 8083:8083 -p 8084:8084 -p 8883:8883 -p 18083:18083 -p 18081:8081 emqx/emqx
查看状态
[root@hqd235 ~]# docker ps|grep emqx
7305ee268494 emqx/emqx "/usr/bin/docker-ent…" 27 hours ago Up 27 hours 4369-4370/tcp, 5369/tcp, 0.0.0.0:1883->1883/tcp, 0.0.0.0:8083-8084->8083-8084/tcp, 6369-6370/tcp, 0.0.0.0:8883->8883/tcp, 0.0.0.0:18083->18083/tcp, 11883/tcp, 0.0.0.0:18081->8081/tcp emqx
查看部署日志
[root@hqd235 ~]# docker logs -f emqx --tail 200
listener.ssl.external.acceptors = "32"
listener.ssl.external.max_connections = "102400"
listener.tcp.external.acceptors = "64"
listener.tcp.external.max_connections = "1024000"
listener.ws.external.acceptors = "16"
listener.ws.external.max_connections = "102400"
listener.wss.external.acceptors = "16"
listener.wss.external.max_connections = "102400"
log.to = "console"
node.max_ets_tables = "2097152"
node.max_ports = "1048576"
node.name = "7305ee268494@172.17.0.2"
node.process_limit = "2097152"
rpc.port_discovery = "manual"
Starting emqx on node 7305ee268494@172.17.0.2
Start mqtt:tcp:internal listener on 127.0.0.1:11883 successfully.
Start mqtt:tcp:external listener on 0.0.0.0:1883 successfully.
Start mqtt:ws:external listener on 0.0.0.0:8083 successfully.
Start mqtt:ssl:external listener on 0.0.0.0:8883 successfully.
Start mqtt:wss:external listener on 0.0.0.0:8084 successfully.
Start http:management listener on 8081 successfully.
Start http:dashboard listener on 18083 successfully.
EMQ X Broker 4.3.11 is running now!
访问web端页面,地址为http://host:port/
, 上述示例访问地址为http://172.16.10.235:18083
, 默认用户名密码为admin/public
登录后的页面如下图:
在页面上提供了监控、客户端信息、告警、统计等实用功能,同时设置中提供了主题和语言的切换。
MQTT客户端安装
客户端推荐mqttfx
,界面简洁好用,测试完全够用。
下载链接:https://pan.baidu.com/s/1kRWp78GpQSTxVqatLJf3yg?pwd=wmmi 提取码:wmmi
下载完成后一路next即可,如果遇到需要输入license key的情况,那一定是下载错版本了,应该下载的是1.7.1的版本。
安装完成后界面如下:
点击齿轮进入设置页面
新增配置文件,broker地址即上面服务端的地址,端口默认是1883,在UserCredentials中配置用户名密码,如果使用默认的话也就是admin/public
配置完成后点击Apply和ok保存即可。
配置完成后点击界面上的Connect按钮,如果右侧出现绿色圆点,说明链接成功了。
在publish
下可以输入要发送的目标topic和内容,在subscribe
中可以配置订阅的主题及收到的主题下的消息内容。
下图为简单示例:
先订阅test主题,然后给test主题发布消息,再去Subscribe模块下查看,可以看到能正常收到消息。
Springboot集成MQTT
springboot中集成Mqtt相对来说流程也比较简单,下面我们做一个简单的例子,仅为了演示流程。
-
新建springboot + maven项目
pom中引入如下依赖:
<!--mqtt相关依赖--> <dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-stream</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-integration</artifactId> </dependency> <dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-mqtt</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency>
-
添加配置文件
mqtt: #MQTT服务地址,端口号默认11883,如果有多个,用逗号隔开 host: tcp://172.16.10.235:1883 #用户名 username: admin #密码 password: public #客户端id(不能重复) clientId: from-springboot-apps
-
添加配置文件对应的类
@Data @Configuration @ConfigurationProperties(prefix = "mqtt") public class MqttConfig { private String host; private String username; private String password; private String clientId; }
-
添加mqtt配置bean
package com.zjtx.tech.message.config; import org.eclipse.paho.client.mqttv3.*; import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.HashMap; import java.util.Map; @Component public class MqttClientConfig { @Autowired private MqttConfig config; private MqttClient client; public static final Map<String, MqttClient> clientMap = new HashMap<>(); @PostConstruct public void init() throws Exception { this.connect(); } /** * 客户端连接服务端 */ public void connect() throws Exception { //创建MQTT客户端对象 client = new MqttClient(config.getHost(), config.getClientId(), new MemoryPersistence()); //连接设置 MqttConnectOptions options = new MqttConnectOptions(); //是否清空session,设置false表示服务器会保留客户端的连接记录(订阅主题,qos),客户端重连之后能获取到服务器在客户端断开连接期间推送的消息 //设置为true表示每次连接服务器都是以新的身份 options.setCleanSession(true); //设置连接用户名 options.setUserName(config.getUsername()); //设置连接密码 options.setPassword(config.getPassword().toCharArray()); //设置超时时间,单位为秒 options.setConnectionTimeout(100); //设置心跳时间 单位为秒,表示服务器每隔 1.5*20秒的时间向客户端发送心跳判断客户端是否在线 options.setKeepAliveInterval(20); //设置遗嘱消息的话题,若客户端和服务器之间的连接意外断开,服务器将发布客户端的遗嘱信息 options.setWill("willTopic", (config.getClientId() + "与服务器断开连接").getBytes(), 0, false); //设置回调 client.setCallback(new MqttProviderCallBack(config.getClientId())); client.connect(options); } /** * 发布消息 */ public void publish(String topic,String message, int qos,boolean retained){ MqttMessage mqttMessage = new MqttMessage(); mqttMessage.setQos(qos); mqttMessage.setRetained(retained); mqttMessage.setPayload(message.getBytes()); //主题的目的地,用于发布/订阅信息 MqttTopic mqttTopic = client.getTopic(topic); //提供一种机制来跟踪消息的传递进度 //用于在以非阻塞方式(在后台运行)执行发布是跟踪消息的传递进度 MqttDeliveryToken token; try { //将指定消息发布到主题,但不等待消息传递完成,返回的token可用于跟踪消息的传递状态 //一旦此方法干净地返回,消息就已被客户端接受发布,当连接可用,将在后台完成消息传递。 token = mqttTopic.publish(mqttMessage); token.waitForCompletion(); } catch (MqttException e) { e.printStackTrace(); } } /** * 断开连接 */ public void disConnect(){ try { client.disconnect(); } catch (MqttException e) { e.printStackTrace(); } } /** * 订阅主题 */ public void subscribe(String topic,int qos){ try { client.subscribe(topic,qos); } catch (MqttException e) { e.printStackTrace(); } } }
-
添加回调类
package com.zjtx.tech.message.config; import lombok.extern.slf4j.Slf4j; import org.eclipse.paho.client.mqttv3.IMqttAsyncClient; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; import org.eclipse.paho.client.mqttv3.MqttCallback; import org.eclipse.paho.client.mqttv3.MqttMessage; @Slf4j public class MqttProviderCallBack implements MqttCallback { public String clientId; public MqttProviderCallBack(String clientId) { this.clientId = clientId; } @Override public void connectionLost(Throwable throwable) { MqttClientConfig.clientMap.remove(clientId); log.info("{}与服务器断开链接", clientId); } @Override public void messageArrived(String topic, MqttMessage message) { log.info("接收消息主题 : {}", topic); log.info("接收消息Qos : {}",message.getQos()); log.info("接收消息内容 : {}",new String(message.getPayload())); } @Override public void deliveryComplete(IMqttDeliveryToken token) { IMqttAsyncClient client = token.getClient(); log.info(client.getClientId() + "发布消息成功!"); } }
-
添加测试用的controller
package com.zjtx.tech.message.controller; import com.cnhqd.common.core.web.domain.ResultBean; import com.cnhqd.message.config.MqttClientConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("mqtt") public class MqttController { @Autowired private MqttClientConfig clientConfig; @GetMapping("publish") public ResultBean<Void> publish(String topic, String message){ clientConfig.publish(topic, message, 2, true); return new ResultBean<>(); } @GetMapping("subscribe") public ResultBean<Void> subscribe(String topic) { clientConfig.subscribe(topic, 2); return new ResultBean<>(); } }
-
测试
通过页面访问,先调用
/mqtt/subscribe?topic=xxx
, 再调用/mqtt/publish?topic=xxx&&message=xxxxxx
,观察控制台输出。如我们执行
http://localhost:9207/mqtt/subscribe?topic=test
,订阅了test主题。再执行
http://localhost:9207/mqtt/publish?topic=test&&message=FromSpringBootApplication
,在test主题下发布了一条消息。查看控制台输出:
可以看到在应用中消息的发布和接收都是成功的。
继续打开mqttfx客户端,查看test主题下是否收到该消息。
mqttfx客户端也可以正常接收到消息。
我们再打开服务端的dashboard,查看下数据,如下所示:
如果需要查看指定主题下的数据需要打开主题监控模块,
启用后进入到统计分析-主题监控模块下新建监控的主题,输入test
再次在网页上请求发布消息的接口,然后观察数据变化,演示如下:
这里我发送了三条消息,有两个客户端订阅了该主题,所以流入3条,流出6条。均为正常数据。
至此,springboot中集成mqtt的整个过程就结束了。
总结
本文介绍了mqtt协议的相关特性,并总结了在springboot应用中集成mqtt的流程并验证。
mqtt作为目前物联网中高效的通讯协议,还是很值得研究的。
作为记录的同时也希望能帮助到需要的朋友们。
针对以上内容有任何问题欢迎留言评论~~~~
创作不易,欢迎一键三连~~~~
参考文章:
一文带你搞懂 MQTT - 知乎 (zhihu.com)