MQTT(message queuing telemetry transport)是IBM开发的即时通讯协议,是一种发布/订阅极其轻量级的消息传输协议,专门为网络受限设备、低宽带以及高延迟和不可靠的网络而设计的。由于以上轻量级的特点,是实现智能家居的首选传输协议,相比于XMPP,更加轻量级而且占用宽带低。
MQTT官网:http://mqtt.org/
MQTT介绍:http://www.ibm.com
MQTT Android github:https://github.com/eclipse/paho.mqtt.android
MQTT API:http://www.eclipse.org/paho/files/javadoc/index.html
MQTT Android API: http://www.eclipse.org/paho/files/android-javadoc/index.html
a.由于采用发布/订阅的消息模式,可以提供一对多的消息发布
b.轻量级,网络开销小
c.对负载内容会有屏蔽的消息传输
d.有三种消息发布质量(Qos):
qos=0:“至多一次”,这一级别会发生消息丢失或重复,消息发布依赖于TCP/IP网络
qos=1:“至少一次”,确保消息到达,但消息重复可能会发生
qos=2:“只有一次”,确保消息到达一次
e.通知机制,异常中断时会通知双方
14523188625918865.png
MQTT协议有三种身份:发布者、代理、订阅者,发布者和订阅者都为客户端,代理为服务器,同时消息的发布者也可以是订阅者(为了节约内存和流量发布者和订阅者一般都会定义在一起)。
MQTT传输的消息分为主题(Topic,可理解为消息的类型,订阅者订阅后,就会收到该主题的消息内容(payload))和负载(payload,可以理解为消息的内容)两部分。
添加依赖
repositories {
maven {
url "https://repo.eclipse.org/content/repositories/paho-releases/"
}
}
dependencies {
compile 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0'
compile 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.0'
}
添加限权
注册Service
服务质量 | 具体含义 |
---|---|
QoS0 | 代表最多分发一次 |
QoS1 | 代表至少达到一次 |
QoS2 | 代表仅分发一次 |
cleanSession
cleanSession 标志是 MQTT 协议中对一个客户端建立 TCP 连接后是否关心之前状态的定义。具体语义如下:
cleanSession | 具体含义 |
---|---|
true | 非持久化连接,客户端再次上线时,将不再关心之前所有的订阅关系以及离线消息 |
false | 持久化连接,客户端再次上线时,还需要处理之前的离线消息,而之前的订阅关系也会持续生效 |
QoS 和 cleanSession 的不同组合产生的结果如下表所示:
QoS 级别 | cleanSession=true | cleanSession=false |
---|---|---|
QoS0 | 无离线消息,在线消息只尝试推一次 | 无离线消息,在线消息只尝试推一次 |
QoS1 | 无离线消息,在线消息保证可达 | 有离线消息,所有消息保证可达 |
QoS2 | 无离线消息,在线消息保证只推一次 | 有离线消息,所有消息保证只推一次 |
对于 QoS > 0的消息,如果是持久化连接,当客户端不在线时,发送消息会保存离线消息到broker,当客户端上线时,mqtt会从broker拉取消息推送给客户端。
mqtt 的主题一个层级的概念. 我们订阅多个主题就需要用到这个技术点
功能是在主题中引入层次。层次又分主题层级分隔符,多层通配符和单层通配符
需要注意的一点是: 这些层级不能用在发布消息的 Publish 接口中
层级分类
主题层级分隔符: /
多层通配符: #
单层通配符: +
主题层级分隔符
"/" 被用来分割主题树的每一层,并给主题空间提供分等级的结构。当两个通配符在一个主题中出现的时候,主题层次分隔符的使用是很重要的。
// 主题Topic1: 分了三层/test/child/aaa// 主题Topic2: 分了四层/test/child/aaa2/bbb2
多层通配符
多层通配符"#"是一个匹配主题中任意层次数的通配符。我们用案例说明
案例1
订阅主题: /test/child/#
我们将收到这些主题发送来的消息:
/test/child /test/child/aaa/test/child/ccc/test/child/aaa/bbb/test/child/aaa/bbb/ddd
多层通配符可以理解为大于等于0的层次。
多层通配符只可以确定当前层或者下一层
常见错误 和正确表示案例
# // Success, 会接收到不以 / 开头的所有主题/# // Success/test/#/child // Error, #必须是最后一个字符/test/# // Success/test/child# //Error 无效的通配符/test/child/# // Success
单层通配符
单层通配符 "+" 只匹配主题的一层
案例1
订阅主题: /test/child/+
我们将收到这些主题发送来的消息:注意:接收不到 /test/child 主题发送的消息
/test/child/aaa/test/child/bbb/test/child/ccc
常见错误 和正确表示案例
+ //Success/+ // Success/test/+/child // Success, /test/+ // Success/test/child+ //Error 无效的通配符/test/child/+ // Success
主题语法和用法
当你建立一个应用,设计主题树的时候应该考虑以下的主题名字的语法和语义:
主题至少有一个字符长。
主题名字是大小写敏感的。比如说,ACCOUNTS和Accounts是两个不同的主题。
主题名字可以包含空格。比如,Accounts payable是一个有效的主题。
以/开头会产生一个不同的主题。比如说,/finnace与finance不同。/finance匹配"+/+"和/+,但不匹配+
不要在任何主题中包含null(Unicode \x0000)字符。
以下的原则应用于主题树的建造和内容
在主题树中,长度被限制于64k内但是在这以内没有限制层级的数目 。
可以有任意数目的根节点;也就是说,可以有任意数目的主题树
————————————————
Mqtt 客户端多主题订阅:https://blog.csdn.net/qq_22889431/article/details/105321843
mqtt离线消息的实现: https://www.jianshu.com/p/e85cdaae65bd
mqtt是一种极其轻量级的发布/订阅消息传输协议(专为受限设备和低带宽、高延迟或不可靠的网络而设计),且代码体积小、功耗低,适合移动设备、车机等终端,且需要支持手机、车机等在网络信号不稳定(弱网、断网、进隧道没有网络等)且之后再恢复网络时,可以继续收发消息、且可以收到之前离线时消息的补充推送。关于离线消息的补充推送亦可由IM服务端自己控制,但若Mqtt协议原生支持离线推送,岂不是省的开发者再去自己处理。同时秉承着用新不用旧的观点,果断选用Mqtt5而弃用Mqtt3,Mqtt5相较于Mqtt3有了很多升级,如:原因代码(PUBACK / PUBREC)、共享订阅、会话过期、请求/响应模式(ResponseTopic, CorrelationData)、Will Delay等。
关于Mqtt的服务端、客户端选型可参考如下链接:
Mqtt官网
Mqtt中文网
Mqtt Server端
Mqtt Client端
实际开发过程中,Server端选用的Emq,Client端选用的HiveMq,二者均支持Mqtt5。
Mqtt5支持离线消息接收的几个核心设置:
ClientId
CleanStart: false
SessionExpiry
Qos:2
CONNACK中的session present flag
ClientId用于唯一标识用户session。
CleanStart设置为0,表示创建一个持久会话,在客户端断开连接时,会话仍然保持并保存离线消息,直到会话超时注销。CleanStart设置为1,表示创建一个新的临时会话,在客户端断开时,会话自动销毁。
SessionExpiry即指定在CleanStart为0时,会话的保存时长,如果客户端未在用户定义的时间段内连接,则可以丢弃状态(例如,订阅和缓冲的消息)而无需进行清理。
Qos即消息的Quality of Service,若要支持离线消息,需要订阅端、发布端Qos >= 1
session present即在connect到mqtt服务器的返回结果ConnAck中,包含session present标识,该标识表示当前clientId是否存在之前的持久会话(persistent session),若之前已存在session(此时千万不要再次重复订阅topic,若再次订阅则之前的消息都将收不到),则session会保留之前的订阅关系、客户端离线时的消息(Qos>=1)、未ack的消息。重点说明一下session present的使用,在客户端连接到mqtt服务器并获取到connack中的isSessionPresent标识时,若isSessionPresent=true则已存在会话,此时无需再重复订阅topic(订阅关系已保存到session中,若再重复订阅则收不到之前的离线消息),可通过全局接收来处理离线消息和之后的新消息;若isSessionPresent=false则不存在session(又或者session已超期),此时需要重新订阅topic,且之前离线的消息都已接收不到,只能通过其他方式获取离线消息(例如IM后端服务的全量同步服务)。
如ClientId=1, CleanStart=false, SessionExpiry=3600s, Qos=2即指定clientId=1的会话为持久会话,用户在离线后3600s的的离线消息都会被Mqtt服务器保存,用户在离线时间不超过3600s且再次以ClientId=1重新上线时,是可以收到离线期间消息的补充推送的,同时Qos=2(exactly once)保证消息只会被客户端收到一次且一定一次。
以HiveMq客户端代码为例:
注意:asyncClient.publishes全局消息接收一定要放在connect方法调用之前
package com.mx.mqtt.sys;
import com.hivemq.client.mqtt.MqttGlobalPublishFilter;
import com.hivemq.client.mqtt.datatypes.MqttQos;
import com.hivemq.client.mqtt.lifecycle.MqttClientConnectedContext;
import com.hivemq.client.mqtt.lifecycle.MqttClientConnectedListener;
import com.hivemq.client.mqtt.lifecycle.MqttClientDisconnectedContext;
import com.hivemq.client.mqtt.lifecycle.MqttClientDisconnectedListener;
import com.hivemq.client.mqtt.mqtt5.Mqtt5AsyncClient;
import com.hivemq.client.mqtt.mqtt5.Mqtt5BlockingClient;
import com.hivemq.client.mqtt.mqtt5.Mqtt5Client;
import com.hivemq.client.mqtt.mqtt5.exceptions.Mqtt5ConnAckException;
import com.hivemq.client.mqtt.mqtt5.message.auth.Mqtt5SimpleAuth;
import com.hivemq.client.mqtt.mqtt5.message.connect.connack.Mqtt5ConnAck;
import com.mx.mqtt.jwt.JwtUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.UnsupportedEncodingException;
/**
* emqx - Session
*
* @Ahthor luohq
* @Date 2020-04-09
*/
public class EmqxOfflineClient {
/**
* 日志
*/
private static final Logger logger = LogManager.getLogger(EmqxOfflineClient.class);
private static final String MQTT_JWT_SECRET = "xxxx";
private static final String MQTT_SERVER_HOST = "192.168.xxx.xxx";
private static final Integer MQTT_SERVER_PORT = 1883;
private static final String MQTT_CLIENT_ID = "luohq-offline";
public static final String MQTT_SUB_TOPIC = "luohq/offline";
public static final Long SESSION_EXPIRATION = 5 * 60L;
private static Boolean isSessionPresent = false;
private static Mqtt5BlockingClient client;
private static Mqtt5AsyncClient asyncClient;
public static void main(String[] args) {
/** 构建mqtt客户端 */
buildMqtt5Client();
/** 若session不存在,则需要再订阅主题 */
if (!isSessionPresent) {
logger.info("【CLIENT-SUB】订阅主题:" + MQTT_SUB_TOPIC);
//订阅主题
asyncClient.subscribeWith()
.topicFilter(MQTT_SUB_TOPIC)
.qos(MqttQos.EXACTLY_ONCE)
.send();
}
}
public static Mqtt5BlockingClient buildMqtt5Client() {
/** blocking客户端 */
client = Mqtt5Client.builder()
.identifier(MQTT_CLIENT_ID)
.serverHost(MQTT_SERVER_HOST)
.serverPort(MQTT_SERVER_PORT)
.addConnectedListener(new MqttClientConnectedListener() {
@Override
public void onConnected(MqttClientConnectedContext context) {
logger.info("mqtt onConnected context");
}
})
.addDisconnectedListener(new MqttClientDisconnectedListener() {
@Override
public void onDisconnected(MqttClientDisconnectedContext context) {
logger.info("mqtt onDisconnected context");
}
})
//自动重连(指数级延迟重连(起始延迟1s,之后每次2倍,到2分钟封顶) delay : 1s-> 2s -> 4s -> ... -> 2min)
.automaticReconnectWithDefaultConfig()
.buildBlocking();
asyncClient = client.toAsync();
/** Emqx JWT认证 */
String authJwt = JwtUtils.generateJwt(MQTT_CLIENT_ID, MQTT_JWT_SECRET);
Mqtt5SimpleAuth auth = Mqtt5SimpleAuth.builder()
.username(MQTT_CLIENT_ID)
.password(authJwt.getBytes())
.build();
Mqtt5ConnAck connAck = null;
/** 全局消息处理(放在connect之前) */
asyncClient.publishes(MqttGlobalPublishFilter.ALL, mqtt5Publish -> {
try {
byte[] msg = mqtt5Publish.getPayloadAsBytes();
String msgStr = new String(mqtt5Publish.getPayloadAsBytes(), "UTF-8");
logger.info("【CLIENT-RECV】" + msgStr);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
});
/** 连接逻辑 */
try {
connAck = client.connectWith()
.simpleAuth(auth)
/** cleanSession=false */
.cleanStart(false)
/** session 7天过期 */
.sessionExpiryInterval(SESSION_EXPIRATION)
/** keepalive 时长*/
//.keepAlive(60)
.send();
} catch (Mqtt5ConnAckException e) {
e.printStackTrace();
connAck = e.getMqttMessage();
}
/** 连接(普通无密码连接) */
//Mqtt5ConnAck connAck = client.connect();
/** 检查之前是否已存在session */
isSessionPresent = connAck.isSessionPresent();
if (connAck.isSessionPresent()) {
logger.info("session is present: " + connAck.getSessionExpiryInterval().orElse(-1));
}
logger.info(connAck.getReasonCode() + ":" + connAck.getReasonString() + ":" + connAck.getResponseInformation());
if (connAck.getReasonCode().isError()) {
logger.error("Mqtt5连接失败!");
System.exit(-1);
}
return client;
}
}
以上的几个核心设置:
clientId,
cleanStart=fasle,
sessionExpiry > 0,
Qos>=1,
CONNACK session present处理,
缺一不可,少一项设置便无法实现离线消息的接受。
————————————————
版权声明:本文为CSDN博主「罗小爬EX」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/luo15242208310/article/details/103971457