ok,因为接到一个数据资源池的工作,会用到MQTT协议,我来学习记录一下。
概念:
MQTT协议是一种构建在TCP/IP协议族上,基于发布/订阅模式的消息可靠传输和分发协议。
组件:
MQTT客户端:
运行MQTT库的客户端都是MQTT客户端,MQTT测试工具也是客户端,使用MQTT进行通讯的应用传感器也是客户端.......
MQTT Broker:
负责处理客户端请求的关键组件,包括建立连接、断开连接、订阅、取消订阅、消息转发等,可以快速构建MQTT应用。
发布-订阅模式:
与客户端-服务器模式不同的是,发布者(发送消息的客户端)和订阅者(接受消息的客户端)无需直接连接,而是通过MQTT BRoker进行路由和消息分发。
主题:
MQTT协议根据主题来转发消息,类似于URL路径,比如test/topic1,就类似一个通道
QoS:
全程是quality of service,就是消息传递服务质量的保障协议。
消息传递有两方面:
发布者把消息传递给Broker
Broker把消息传递给订阅者
发布者把消息传递给Broker的时候会定义一个Qos级别,Broker使用订阅者定义的Qos级别把消息传递给订阅者,但如果后者的级别低于前者,就会以较低的级别去传递消息。
QoS 0:“即发即弃”,当MQTT客户端以QoS 0发布消息时,Broker不确认收到,而且发布者也不会存储和重复消息,只是以最大努力发送消息。
QoS 1:保证消息至少一次成功传递给订阅者。Broker会使用PUBACK确认收到,发布者会存储消息,直到他收到一个PUBACK确认。
发送者会使用每个数据包中的数据包标识符对PUBLISH数据包和PUBACK数据包匹配,如果一定时间内发布者没有收到PUBACK数据包,发布者就会重新发送PUBLISH数据包
QoS 2:发布者以QoS 2级别向Broker发送PUBLISH数据包,Broker收到后会使用PUBLISH数据包的PUBREC回复确认,如果发布者没有收到确认,会再次发送带重复DUP标志的PUBLISH数据包,直到收到确认。
当你使用的时候,发现你明明就想发送一条消息,但是日志显示你发了多条,难道你是真的发了多条吗?非也,可以好好检查一下你设置的QoS。
工作流程:
1、客户端使用TCP/IP协议与Broker建立连接,选择加密(TLS/SSL)实现安全通信,客户端需要提供认证信息,并指定会话类型(clean session或persistent session)
2、客户端可以向指定主题发布消息,也可以订阅主题来接受消息。客户端发布消息时,会把消息发送给Broker,客户端订阅消息时,会接受与订阅主题有关的消息。
3、Broker接受到发布的消息,把这些消息转发给订阅了该主题的客户端。根据QoS等级确保消息可靠传输。根绝会话类型断开连接。
代码实现:
1、导入MQTT协议依赖
<!-- MQTT v3.1 -->
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.5</version>
</dependency>
<!-- MQTT 5.0 https://mvnrepository.com/artifact/org.eclipse.paho/org.eclipse.paho.mqttv5.client -->
<!-- <dependency>-->
<!-- <groupId>org.eclipse.paho</groupId>-->
<!-- <artifactId>org.eclipse.paho.mqttv5.client</artifactId>-->
<!-- <version>1.2.5</version>-->
<!-- </dependency>-->
2、配置
Broker这里暂时用免费的公共的服务
# http
server.port=8091
#server.servlet.context-path=/hub
# mqtt服务器的地址和端口号
mqtt.url=tcp://broker.emqx.io:1883
# 用户名密码
mqtt.username=
mqtt.password=
# ClientId需要唯一
mqtt.clientId=java-mqtt-client
# 默认主题
mqtt.defaultTopic=test/topic
# clean session为true表示建立一个薪的临时会话,客户端断开会自动销毁
# clean session为false表示建立一个永久会话,客户端断开,会话仍在切保存离线信息,直至会话超时注销
# 持久会话恢复的前提是客户端使用固定的ClientId,如果是动态的,重连后会生成一个新的永久会话
mqtt.cleanSession=true
基于MQTT的消息发送服务
@Service
@Slf4j
public class MqttService implements MqttCallBack {
@Value("${mqtt.url}")
private String url;
@Value("${mqtt.username}")
private String userName;
@Value("${mqtt.password}")
private String passWord;
@Value("${mqtt.clientId}")
private String clientId;
@Value("${mqtt.cleanSession}")
private boolean cleanSession;
private MqttClient client;
private int reconnectDelay = 2000; // 初始化重连延迟时间=2s
private int maxReconnectAttempts = 3; // 初始化最大重连尝试次数
private int reconnectAttempt = 0;
// @PostConstruct标记方法,该方法没有参数,在bean的依赖注入之后被调用
@PostConstruct
public void connect() {
try {
client = new MqttClient(url, clientId);
MqttConnectOptions options = new MqttConnectOptions();
// 取消自动重连,自己控制
options.setAutomaticReconnect(false);
if(userName != null && !userName.isEmpty()){
options.setUserName(userName);
options.setPassWord(passWord);
}
client.setCallBack(this);
client.connect(options);
// 订阅主题,可以多个
client.subscribe("test/topic1");
} catch (MqttException e) {
log.error("------mqtt connect fail", e);
}
}
@Override
public void connectionLost(){
log.info("------Connection lost! " + cause.getMessage());
reconnect();
}
private void reconnect(){
if(reconnectAttempt < maxReconnectAttempts){
reconnectAttempt++;
log.info("------Attempting to reconnect in " + reconnectDelay + "ms");
// 这里用sleep模拟延迟
try{
Thread.sleep(reconnectDelay);
}catch(InterrputedException e){
Thread.currentThread().interrupt();
}
}else{
log.warn("------max reconnect attempts reached")
}
}
@Override
public void messageArrived(String topic, MqttMessage message) throw Exception{
// 消息到达后调用
log.info("------message arrived, Topic: " + topic +", message: " + new String(message.getPayload()));
// 这里做处理消息的逻辑
}
@Override
public void deliveryComplete(IMqttDeliveryToken token){
// 消息完全传递出去后调用
log.info("------delivery complete!");
// 这里做一些传递完消息后的清理操作
}
// 消息传递的方法
public void publish(String topic, String payload) throw MqttException {
MqttMessage message = new MqttMessage(payload.getBytes());
message.setQos(2);
client.publish(topic, message);
log.info("------message published: {}", payload);
}
// 断开连接的方法
@PreDestroy
public void disconnect() throw MqttException{
if(client != null &&& client.isConnected()){
client.disconnect();
log.info("------disconnected");
}
}
}
Controller
@RestController
@RequestMappping("/mqtt")
public class MqttController{
@Autowired
private MqttService mqttSerice;
@RequestMappping(value = "/send", method = {RequestMethod.GET, RequestMethod.POST})
public ResponseEntity<String> sendMessage(@ReuqestParam String topic, @ReuqestParam String message){
topic = "test/topic1";
try{
mqttSerice.publish(topic, message);
return ResponseEntity.ok("------message sent successfully");
}catch(MqttException e){
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("------failed to sent message: " + e.getMessage());
}
}
}
请求:http://localhost:8091/mqtt/send?topic&message=hello world