本篇文章主要讲解MQTT 消息的发布和订阅
MQTT协议介绍:https://mcxiaoke.gitbooks.io/mqtt-cn/content/mqtt/01-Introduction.html
MQTT使用的是EMQ,官网地址:https://www.emqx.io/cn/products/broker
MQTT协议官方测试工具:http://tools.emqx.io
maven 导入 MQTT依赖
<!--mqtt-->
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.1</version>
</dependency>
1. 客户端 Client 与 服务端 Server
在MQTT协议中,并不是发送消息的是客户端,接收消息的是服务端,客户端和服务端都是可以发送消息和接收消息的。
1.1 客户端
客户端总是通过网络连接到服务端
- 发布应用消息给其它相关的客户端。
- 订阅以请求接受相关的应用消息。
- 取消订阅以移除接受应用消息的请求。
- 从服务端断开连接
我们正常用来发送消息的和接收消息的都是属于客户端。
1.2 服务端
作为发送消息的客户端和请求订阅的客户端之间的中介
- 接受来自客户端的网络连接。
- 接受客户端发布的应用消息。
- 处理客户端的订阅和取消订阅请求。
- 转发应用消息给符合条件的已订阅客户端。
服务端我使用的是EMQ Linux 下 EMQ X 服务器部署
2、创建客户端
发布者和订阅者都是属于客户端,服务器是 EMQ Broker。
发布者把消息发送给 Broker,Broker 接收到消息后再把消息发送给订阅者,发布者和订阅者之间实际上是没有直接关系的。
// 全局创建 client 引用
public static MqttClient client = null;
/**
* 创建客户端
*/
public static void createClient(){
if(client != null){
logger.info("MQTT 客户端已存在,不可再重复创建");
return;
}
// 创建客户端, MemoryPersistence 设置 client 的保存方式默认以内存保存
// clientId 是客户端的唯一标识, 相同的 clientId 客户端连接时会导致已连接的客户端断开连接
String clientId = AddressUtils.getLocalMac() + IPPortUtil.getLocalPort(); // 为保证每次连接都是用同一个客户端Id, 我这边是获取机器的MAC地址作为客户端的唯一标识
try {
client = new MqttClient(HOST,clientId,new MemoryPersistence());
} catch (MqttException e) {
e.printStackTrace();
}
// 设置回调函数
client.setCallback(new MqttCallbackExtended(){
@Override
public void connectComplete(boolean reconnect, String serverURI) {
logger.info("[MQTT] 连接已完成...");
isConnect = true;
// 连接完成可以做一些初始化操作(我在这边发送客户端上线通知)
mqttServiceStatic.callBackBusinessProcessing(reconnect,serverURI);
}
@Override
public void connectionLost(Throwable cause) {
logger.info("[MQTT] 连接已断开...");
isConnect = false;
// 不设置断线自动连接时,可在这边自定义重连
mqttServiceStatic.connectionLost(cause);
}
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
// logger.info("[MQTT] 数据已到达...");
// 接收订阅的消息后执行的方法
String content = new String(message.getPayload(),"UTF-8");
MqttData mqttData = new MqttData(new String(message.getPayload(),"UTF-8"));
if (!passList.contains(mqttData.getServiceId())) {
logger.info("【MQTT】订阅主题:" + topic);
logger.info("【MQTT】订阅质量:" + message.getQos());
logger.info("【MQTT】订阅内容:" + content);
}
if(StringUtils.isNotBlank(mqttData.getServiceId())){
mqttServiceStatic.callBackBusinessProcessing(mqttData.getServiceId(), mqttData);
}
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
if(token.getMessageId() == 0){
// qos 为0
} else {
// qos 为1或者2 则会有 MessageId
logger.info("[MQTT] 数据推送成功,MessageId:"+token.getMessageId());
}
}
});
}
服务器不允许同一个 clientId 的客户端多次接入,当相同 clientId 多次接入时会导致客户端反复的断线-连接
2、连接客户端
public static Boolean isConnect = false; // 是否连接
/**
* 连接客户端
*/
public static void connectClient(String topic){
// 未开启 MQTT 服务
if(!ServiceOpen){
logger.info("【未开启 MQTT 服务】");
return;
}
// 且网络正常才连接MQTT
if (!AddressUtils.isConnect(linuxServiceUrl)) {
logger.info("【MQTT 网络异常,终止连接客户端!!】");
return;
}
// 客户端已连接则结束
if(isConnect){
logger.info("【MQTT 客户端已连接!!】");
return;
}
// 客户端不存在则先创建客户端
if(client == null){
logger.info("【MQTT 客户端未创建】");
createClient();
}
MqttConnectOptions connectOptions = new MqttConnectOptions();
// 设置是否关闭连接会话
// 设置为 true 时,客户端每次都会以新的身份连接
// 设置为 false 时,客户端再次连接会继续之前的会话,可以收到断线后发布的消息,并且再次连接后无需再次订阅即可收到之前已订阅过的主题消息。
// 设置为 false 有个前置条件就是 clientId 不变
connectOptions.setCleanSession(CleanSession);
connectOptions.setUserName(USERNAME);
connectOptions.setPassword(PASSWORD.toCharArray());
// 断线后是否自动重连
connectOptions.setAutomaticReconnect(true);
connectOptions.setConnectionTimeout(3); // 连接报错:已在进行连接,尝试设置连接超时时间为3s
try {
// 设置遗嘱消息
MqttContentDataDto contentDataDto = new MqttContentDataDto();
contentDataDto.setServiceId("sync.client.offline");
contentDataDto.setMsg("客户端下线通知");
String content = JSONObject.toJSONString(contentDataDto);
connectOptions.setWill(topic,content.getBytes("UTF-8"),1,false);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
try {
logger.info("【MQTT 开始连接客户端!】");
// 客户端重复连接会报错:已连接客户机(32100)
client.connect(connectOptions);
} catch (MqttException e) {
logger.error("[MQTT] 客户端连接失败!"+ e.getMessage());
}
}
如果需要在客户端断线重连之后还能接收到掉线期间所发布的消息,clientId 必须保持不变设置 cleanSession 为 false。
3、发布消息
/**
* 推送消息
*
* @param qos 消息质量
* @param topic 主题
* @param contentDataDto 内容
*/
public static void PublishSample(int qos, String topic, boolean retained, MqttContentDataDto contentDataDto) throws ApiProcessException {
if(!isConnect){
logger.info("【MQTT 客户端未连接,先连接客户端】");
connectClient(topic);
try {
Thread.sleep(10000);
if(!isConnect){
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String content = "";
if (contentDataDto == null) {
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("time", DateUtils.getDateTime());
content = JSONObject.toJSONString(dataMap);
} else {
content = JSONObject.toJSONString(contentDataDto);
if (!passList.contains(contentDataDto.getServiceId())) {
logger.info("【MQTT】推送主题:" + topic);
logger.info("【MQTT】推送内容:" + content + "\n");
}
}
try {
// 创建消息
MqttMessage message = new MqttMessage();
try {
message.setPayload(content.getBytes("utf-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// 设置消息的服务质量
message.setQos(qos);
// 设置消息保留
// 设置为 true 则 Broker 会保留对该主题发生的最后一条消息,每有客户端订阅时都会推送一次
// 设置为 true 后推送的那条消息会被一直保留,需要取消只有在设置为 true 下发送一条空的 payload 消息
message.setRetained(retained);
// 发布消息
client.publish(topic, message);
} catch (MqttException me) {
logger.info("MQTT 消息推送失败 " + me);
throw ErrorCode.MQTT_SERVICE_ERROR;
}
}
消息的服务质量详情可查看 消息质量等级分析篇
4、消息订阅
/**
* 订阅主题
*
* @param topic 主题
*/
public static JSONObject SubscribeSample(String topic){
JSONObject result = new JSONObject();
if(!isConnect || client == null){
logger.info("【MQTT 客户端未连接,无法订阅】");
return result;
}
try {
client.subscribe(topic);
} catch (MqttException e) {
e.printStackTrace();
}
result.put("client",client);
result.put("topic",topic);
result.put("QOS",QOS);
return result;
}
/**
* 取消订阅主题
*
* @param topic 主题
*/
public static JSONObject UnSubscribeSample(String topic){
JSONObject result = new JSONObject();
if(!isConnect || client == null){
logger.info("【MQTT 客户端未连接,无法取消订阅】");
return result;
}
try {
client.unsubscribe(topic);
} catch (MqttException e) {
e.printStackTrace();
}
result.put("client",client);
result.put("topic",topic);
result.put("QOS",QOS);
return result;
}
在发布和订阅过程中,如果出现异常而没有处理会导致客户端反复的断线-连接
5、关闭客户端
/**
* 关闭客户端
*/
public static void closeClient(){
if(client == null){
isConnect = false;
return;
}
try {
// 断开连接
client.disconnect();
// 关闭客户端
client.close();
client = null;
isConnect = false;
} catch (MqttException e) {
e.printStackTrace();
}
}
主题(Topic)通过’/‘分割层级,支持’+’, '#'通配符:
‘+’: 表示通配一个层级,例如a/+,匹配a/x, a/y
‘#’: 表示通配多个层级,例如a/#,匹配a/x, a/b/c/d
ps:订阅者可以订阅含通配符主题,但发布者不允许向含通配符主题发布消息。