MQTT知识要点

一.MQTT介绍

1.简介

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

2.特点

a.由于采用发布/订阅的消息模式,可以提供一对多的消息发布
b.轻量级,网络开销小
c.对负载内容会有屏蔽的消息传输
d.有三种消息发布质量(Qos):
qos=0:“至多一次”,这一级别会发生消息丢失或重复,消息发布依赖于TCP/IP网络
qos=1:“至少一次”,确保消息到达,但消息重复可能会发生
qos=2:“只有一次”,确保消息到达一次
e.通知机制,异常中断时会通知双方

3.原理

14523188625918865.png

 

MQTT协议有三种身份:发布者、代理、订阅者,发布者和订阅者都为客户端,代理为服务器,同时消息的发布者也可以是订阅者(为了节约内存和流量发布者和订阅者一般都会定义在一起)。
MQTT传输的消息分为主题(Topic,可理解为消息的类型,订阅者订阅后,就会收到该主题的消息内容(payload))和负载(payload,可以理解为消息的内容)两部分。

二.MQTT通用使用

添加依赖

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'
}

添加限权

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />

注册Service

        <!-- Mqtt Service -->
        <service android:name="org.eclipse.paho.android.service.MqttService" />
        <service android:name="com.dongyk.service.MQTTService"/>

 

QoS(Quality of Service)
指代消息传输的服务质量。它包括以下级别:

服务质量具体含义
QoS0代表最多分发一次
QoS1代表至少达到一次
QoS2代表仅分发一次

cleanSession
cleanSession 标志是 MQTT 协议中对一个客户端建立 TCP 连接后是否关心之前状态的定义。具体语义如下:

cleanSession具体含义
true非持久化连接,客户端再次上线时,将不再关心之前所有的订阅关系以及离线消息
false持久化连接,客户端再次上线时,还需要处理之前的离线消息,而之前的订阅关系也会持续生效

QoS 和 cleanSession 的不同组合产生的结果如下表所示:

QoS 级别cleanSession=truecleanSession=false
QoS0无离线消息,在线消息只尝试推一次无离线消息,在线消息只尝试推一次
QoS1无离线消息,在线消息保证可达有离线消息,所有消息保证可达
QoS2无离线消息,在线消息保证只推一次有离线消息,所有消息保证只推一次

对于 QoS > 0的消息,如果是持久化连接,当客户端不在线时,发送消息会保存离线消息到broker,当客户端上线时,mqtt会从broker拉取消息推送给客户端。

 

Mqtt 客户端多主题订阅

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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值