什么是Mqtt
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的“轻量级”通讯协议,该协议构建于 TCP/IP 协议上,由 IBM 于 1999 年发明。MQTT 协议的主要特征是开放、简单、轻量级和易于实现,这些特征使得它适用于受约束的应用环境,如:
网络受限:网络带宽较低且传输不可靠
终端受限:协议运行在嵌入式设备上,嵌入式终端的处理器、内存等是受限的
通过 MQTT 协议,目前已经扩展出了数十种 MQTT 服务器端程序,可以通过 PHP、Java、Python、C、C# 等语言向 MQTT 发送消息。由于开放源代码、耗电量小等特点,MQTT 非常适用于物联网领域,如传感器与服务器的通信、传感器信息采集等。
HTTP与MQTT的区别
HTTP与MQTT都是用于通过互联网传输数据的网络协议,下面我们来看看二者的区别.
HTTP
- 一种 "请求-响应"协议,基于该协议,客户端向服务器发送请求,服务器返回请求的数据
- 主要设计用于在web服务器和浏览器之间传输Web内容,(如 html , 表单 , 图像 等信息)
MQTT
- 一种轻量级 "发布-订阅"消息传递协议,基础该协议,客户端订阅主题并接受客户端围绕主题发布的消息.
- 专为需要重点考虑低带宽,连接稳定性以及硬件设备而设计.
发布/订阅模式
订阅发布模式定义了一种一对多的依赖关系,让多个订阅者对象同时监听某一个主题对象。这个主题对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态。
将一个系统分割成一系列相互协作的类有一个很不好的副作用,那就是需要维护相应对象间的一致性,这样会给维护、扩展和重用都带来不便。当一个对象的改变需要同时改变其他对象,而且它不知道具体有多少对象需要改变时,就可以使用订阅发布模式了。
一个抽象模型有两个方面,其中一方面依赖于另一方面,这时订阅发布模式可以将这两者封装在独立的对象中,使它们各自独立地改变和复用。订阅发布模式所做的工作其实就是在解耦合。让耦合的双方都依赖于抽象,而不是依赖于具体,从而使得各自的变化都不会影响另一边的变化。
发布/订阅模式并不是 MQTT 协议特有的模式,像我们很多消息中间件都有使用发布/订阅模式,这里你是不是想说,这不就是我们所说的观察者模式嘛,还真不是,这两个模式很容易混淆。观察者模式只有观察者 + 被观察者两个角色,而发布/订阅模式还有一个经纪人 Broker;往更深层次的讲观察者和被观察者,是松耦合的关系,而发布者和订阅者,则完全不存在耦合。
MQTT重要概念
1、MQTT Client
publisher 和 subscriber 都属于 MQTT Client,之所以有发布者和订阅者这个概念,其实是一种相对的概念,就是指当前客户端是在发布消息还是在接收消息,发布和订阅的功能也可以由同一个 MQTT Client 实现。
MQTT 客户端是运行 MQTT 库并通过网络连接到 MQTT 代理的任何设备(从微控制器到成熟的服务器)。例如,MQTT 客户端可以是一个非常小的、资源受限的设备,它通过无线网络进行连接并具有一个最低限度的库。基本上,任何使用 TCP/IP 协议使用 MQTT 设备的都可以称之为 MQTT Client。MQTT 协议的客户端实现非常简单直接,易于实施是 MQTT 非常适合小型设备的原因之一。MQTT 客户端库可用于多种编程语言。例如,Android、Arduino、C、C++、C#、Go、iOS、Java、JavaScript 和 .NET。
2、MQTT Broker
与 MQTT Client 对应的就是 MQTT Broker,Broker 是任何发布/订阅协议的核心,根据实现的不同,代理可以处理多达数百万连接的 MQTT Client。
Broker 负责接收所有消息,过滤消息,确定是哪个Client 订阅了每条消息,并将消息发送给对应的 Client,Broker 还负责保存会话数据,这些数据包括订阅的和错过的消息。Broker 还负责客户端的身份验证和授权。
3、MQTT Connection
MQTT 协议基于 TCP/IP。客户端和代理都需要有一个 TCP/IP 协议支持。
MQTT 连接始终位于一个客户端和代理之间。客户端从不直接相互连接。要发起连接,客户端向代理发送 CONNECT 消息。代理使用 CONNACK 消息和状态代码进行响应。建立连接后,代理将保持打开状态,直到客户端发送断开连接命令或连接中断。
4、MQTT主要参数
ClientId:ClientId 的长度可以是 1-23 个字符,在一个服务器上 ClientId 不能重复。如果超过 23 个字符,则服务器返回 CONNACK 消息中的返回码为 Identifier Rejected。在 MQTT 3.1.1 中,如果您不需要代理持有状态,您可以发送一个空的 ClientId。空的 ClientId 导致连接没有任何状态。在这种情况下,clean session 标志必须设置为 true,否则代理将拒绝连接。
Clean Session:Clean Session 标志告诉代理客户端是否要建立持久会话。在持久会话 (CleanSession = false) 中,代理存储客户端的所有订阅以及以服务质量(QoS)级别 1 或 2 订阅的客户端的所有丢失消息。 如果会话不是持久的 (CleanSession = true ),代理不为客户端存储任何内容,并清除任何先前持久会话中的所有信息。
Username/Password:MQTT 可以发送用户名和密码进行客户端认证和授权。但是,如果此信息未加密或散列,则密码将以纯文本形式发送。我们强烈建议将用户名和密码与安全传输一起使用。像 HiveMQ 这样的代理可以使用 SSL 证书对客户端进行身份验证,因此不需要用户名和密码。
Will Message:LastWillxxx 表示的是遗愿,client 在连接 broker 的时候将会设立一个遗愿,这个遗愿会保存在 broker 中,当 client 因为非正常原因断开与 broker 的连接时,broker 会将遗愿发送给订阅了这个 topic(订阅遗愿的 topic)的 client。
KeepAlive:keepAlive 是 client 在连接建立时与 broker 通信的时间间隔,通常以秒为单位。这个时间指的是 client 与 broker 在不发送消息下所能承受的最大时长。
QOS:
此数字表示消息的服务质量 (QoS)。有三个级别:0、1 和 2。服务级别决定了消息到达预期接收者(客户端或代理)的保证类型。
- QoS0,At most once,至多一次;
- QoS1,At least once,至少一次;
- QoS2,Exactly once,确保只有一次。
QoS 是消息的发送方(Sender)和接受方(Receiver)之间达成的一个协议:
- QoS0 代表,Sender 发送的一条消息,Receiver 最多能收到一次,也就是说 Sender 尽力向 Receiver 发送消息,如果发送失败,也就算了;
- QoS1 代表,Sender 发送的一条消息,Receiver 至少能收到一次,也就是说 Sender 向 Receiver 发送消息,如果发送失败,会继续重试,直到 Receiver 收到消息为止,但是因为重传的原因,Receiver 有可能会收到重复的消息;
- QoS2 代表,Sender 发送的一条消息,Receiver 确保能收到而且只收到一次,也就是说 Sender 尽力向 Receiver 发送消息,如果发送失败,会继续重试,直到 Receiver 收到消息为止,同时保证 Receiver 不会因为消息重传而收到重复的消息。
Payload
:这个是每条消息的实际内容。MQTT 是数据无关性的。可以发送任何文本、图像、加密数据以及二进制数据。
timeout:MQTT会尝试接收数据,直到timeout时间到后才会退出。
搭建服务端
docker安装emqx
拉取emqx镜像
docker pull emqx/emqx
启动镜像
docker run -d --name emqx -p 1883:1883 -p 8081:8081 -p 8083:8083 -p 8883:8883 -p 8084:8084 -p 18083:18083 emqx/emqx:latest
访问 12.168.4.xx:18083 (默认账号密码 ; admin/public)
登录之后页面
端口号
-
控制台连接端口号 18083
-
客户端连接端口号 1883
SpringBoot整合MQTT
1、pom.xml文件
<!-- 引入 mqtt 相关依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mqtt</artifactId>
</dependency>
2、properties / yaml 文件
spring.application.name=zkaw
# MQTT服务地址,端口号默认1883,如果有多个,用逗号隔开
spring.mqtt.url=tcp://192.168.4.xx:1883
# 用户名
spring.mqtt.username=admin
# 密码
spring.mqtt.password=xxxxxxx
3、mqtt连接配置
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
/** @Author: best_liu
* @Description:
* @Date: 10:29 2024/6/18
* @Param
* @return
**/
@Configuration
public class MqttConfig {
@Value("${spring.mqtt.username}")
private String username;
@Value("${spring.mqtt.password}")
private String password;
@Value("${spring.mqtt.url}")
private String hostUrl;
@Value("${spring.application.name}")
private String applicationName;
/**
* 客户端对象
*/
private MqttClient client;
/**
* 在bean初始化后连接到服务器
*/
@PostConstruct
public void init() {
this.connect();
}
/**
* 断开连接
*/
@PreDestroy
public void disConnect() {
try {
client.disconnect();
client.close();
} catch (MqttException e) {
e.printStackTrace();
}
}
/**
* 客户端连接服务端
*/
public void connect() {
try {
// 创建MQTT客户端对象
client = new MqttClient(hostUrl, applicationName, new MemoryPersistence());
// 连接设置
MqttConnectOptions options = new MqttConnectOptions();
// 是否清空session,设置false表示服务器会保留客户端的连接记录(订阅主题,qos),客户端重连之后能获取到服务器在客户端断开连接期间推送的消息
// 设置为true表示每次连接服务器都是以新的身份
options.setCleanSession(true);
// 设置连接用户名
options.setUserName(username);
// 设置连接密码
options.setPassword(password.toCharArray());
// 设置超时时间,单位为秒
options.setConnectionTimeout(100);
// 设置心跳时间 单位为秒,表示服务器每隔 1.5*20秒的时间向客户端发送心跳判断客户端是否在线
options.setKeepAliveInterval(20);
// 设置遗嘱消息的话题,若客户端和服务器之间的连接意外断开,服务器将发布客户端的遗嘱信息
options.setWill("willTopic", (applicationName + "与服务器断开连接").getBytes(), 0, false);
// 设置回调
client.setCallback(new MqttCallBack());
// 连接
client.connect(options);
// 订阅主题 (接受此主题的消息)
this.subscribe("warn_topic", 2);
this.subscribe("warn_topic2", 2);
} catch (MqttException e) {
e.printStackTrace();
}
}
/**
* 发布消息
*
* @param topic
* @param message
*/
public boolean publish(String topic, String message) {
MqttMessage mqttMessage = new MqttMessage();
// 0:最多交付一次,可能丢失消息
// 1:至少交付一次,可能消息重复
// 2:只交付一次,既不丢失也不重复
mqttMessage.setQos(2);
// 是否保留最后一条消息
mqttMessage.setRetained(false);
// 消息内容
mqttMessage.setPayload(message.getBytes());
// 主题的目的地,用于发布/订阅信息
MqttTopic mqttTopic = client.getTopic(topic);
// 提供一种机制来跟踪消息的传递进度
// 用于在以非阻塞方式(在后台运行)执行发布是跟踪消息的传递进度
MqttDeliveryToken token;
try {
// 将指定消息发布到主题,但不等待消息传递完成,返回的token可用于跟踪消息的传递状态
// 一旦此方法干净地返回,消息就已被客户端接受发布,当连接可用,将在后台完成消息传递。
token = mqttTopic.publish(mqttMessage);
token.waitForCompletion();
return true;
} catch (MqttException e) {
e.printStackTrace();
}
return false;
}
/**
* 订阅主题
*/
public void subscribe(String topic, int qos) {
try {
client.subscribe(topic, qos);
} catch (MqttException e) {
e.printStackTrace();
}
}
}
4、消息回调
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;
import org.springframework.context.annotation.Configuration;
/**
* @Date 2023/11/21 18:32
*/
@Configuration
public class MqttCallBack implements MqttCallback {
/**
* 与服务器断开的回调
*/
@Override
public void connectionLost(Throwable cause) {
System.out.println("与服务器断开连接");
}
/**
* 消息到达的回调
*/
@Override
public void messageArrived(String topic, MqttMessage message) {
System.out.println(String.format("接收消息主题 : %s", topic));
System.out.println(String.format("接收消息Qos : %d", message.getQos()));
System.out.println(String.format("接收消息内容 : %s", new String(message.getPayload())));
System.out.println(String.format("接收消息retained : %b", message.isRetained()));
}
/**
* 消息发布成功的回调
*/
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
IMqttAsyncClient client = token.getClient();
System.out.println(client.getClientId() + "发布消息成功!");
}
}
5、发送消息
import com.postgre.zkaw.mqtt.MqttConfig;
import lombok.extern.slf4j.Slf4j;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author: best_liu
* @Description:
* @Date Create in 10:30 2024/6/18
* @Modified By:
*/
@RestController
@RequestMapping(value = "/mqtt")
@Slf4j
public class MqttController {
@Autowired
private MqttConfig mqttConfig;
@GetMapping("/sendMessage")
public String sendMessage(@RequestParam("topic") String topic,
@RequestParam("message") String message) {
boolean publish = mqttConfig.publish(topic, message);
if (publish) {
return "ok";
}
return "no";
}
}
6、启动项目
启动项目,进入到mqtt管理页面
两个主题
两个订阅方
7、模拟发送消息
浏览器模拟get请求,请求消息推送接口
8、控制台输出
共享订阅、延迟发布
参考官网