Mqtt5 - 会话保留与离线消息接收

       最近在做IM(及时通信)相关的应用,长连接(消息收发)采用的Mqtt5,mqtt支持会话保存(cleanStart)、会话超期(sessionExpire)、Qos等设置,可以实现移动端弱网络(或网络断开后再重连)的离线消息接收。
       之所以选择Mqtt协议,因为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

ClientId用于唯一标识用户session。
CleanStart设置为0,表示创建一个持久会话,在客户端断开连接时,会话仍然保持并保存离线消息,直到会话超时注销。CleanStart设置为1,表示创建一个新的临时会话,在客户端断开时,会话自动销毁。
SessionExpiry即指定在CleanStart为0时,会话的保存时长,如果客户端未在用户定义的时间段内连接,则可以丢弃状态(例如,订阅和缓冲的消息)而无需进行清理。
Qos即消息的Quality of Service,若要支持离线消息,需要订阅端、发布端Qos >= 1

如ClientId=1, CleanStart=false, SessionExpiry=3600s, Qos=2即指定clientId=1的会话为持久会话,用户在离线后3600s的的离线消息都会被Mqtt服务器保存,用户在离线时间不超过3600s且再次以ClientId=1重新上线时,是可以收到离线期间消息的补充推送的,同时Qos=2(exactly once)保证消息只会被客户端收到一次且一定一次。

以HiveMq客户端代码为例:

/**
 * 构建mqtt客户端连接
 *
 * @param clientId 客户端ID
 * @return
 */
public Mqtt5BlockingClient buildMqtt5Client(String clientId) {
    /** blocking客户端 */
    Mqtt5BlockingClient client = Mqtt5Client.builder()
            .identifier(clientId)
            .serverHost(mqttConfig.getInternalHost())
            .serverPort(mqttConfig.getPort())
            //自动重连(指数级延迟重连(起始延迟1s,之后每次2倍,到2分钟封顶) delay : 1s-> 2s -> 4s -> ... -> 2min)
            .automaticReconnectWithDefaultConfig()
            .buildBlocking();
    /** Emqx JWT认证 */
    String authJwt = jwtManager.generateMqttAuthJwt(clientId);
    Mqtt5SimpleAuth auth = Mqtt5SimpleAuth.builder()
            .password(authJwt.getBytes())
            .build();
    logger.info("连接mqtt参数:internalHost={}, port={}, clientId={}, authPassword={}", mqttConfig.getInternalHost(), mqttConfig.getPort(), clientId, authJwt);
    Mqtt5ConnAck connAck = null;
    try {
        connAck = client.connectWith()
                  .simpleAuth(auth)
                    /** cleanSession=false */
                    .cleanStart(false)
                    /** session 7天过期 */
                    .sessionExpiryInterval(MqttConsts.SESSION_EXPIRATION)
                    .send();
        logger.info("连接mqtt结果:reasonCode={}, reasonStr={}, reasonInfo={}", connAck.getReasonCode(), connAck.getReasonString(), connAck.getResponseInformation());
    } catch (Mqtt5ConnAckException e) {
        logger.warn("连接mqtt - ack异常 - ".concat(e.getMessage()));
        connAck = e.getMqttMessage();
    } catch (ConnectionFailedException cfe) {
        logger.warn("连接mqtt - fail异常 - ".concat(cfe.getMessage()));
        throw new MqttException("连接mqtt - fail异常 - ".concat(cfe.getMessage()));
    } catch (Exception e) {
        logger.warn("连接mqtt - 系统异常!", e);
        throw new MqttException("连接mqtt - 系统异常");
    }
    if (connAck.getReasonCode().isError()) {
        throw new MqttException("Mqtt5连接失败 - ".concat(connAck.getReasonCode().toString()));
    }
    return client;
}


/**
 * 订阅主题、处理消息
 *
 * @param topic    主题
 * @param callback 消息处理回调
 */
public void subscribeWith(String topic, Consumer<Mqtt5Publish> callback) {
    //订阅主题
    asyncClient.subscribeWith()
            .topicFilter(topic)
            //Qos: 2(EXACTLY_ONCE)
            .qos(MqttQos.EXACTLY_ONCE)
            //消费主题消息(异步)
            .callback(callback)
            .send();
}

以上的几个核心设置:
clientId,
cleanStart=fasle,
sessionExpiry > 0,
Qos>=1,
缺一不可,少一项设置便无法实现离线消息的接受。

发布了56 篇原创文章 · 获赞 6 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/luo15242208310/article/details/103971457