Mqtt(emqx)上下线监听

在mqtt协议中,存在LTW(Last Will and Testament)遗言机制,该机制只能捕捉客户端异常离线的通知,而无法获取正常通过disconnect断开连接的通知。

LTW(Last Will and Testament)遗言机制
客户端在连接到Mqtt服务器时,需指定will topic和will message遗言信息,
之后若在客户端异常断开(弱网络、服务被终止,而非正常disconnet)时会由mqtt服务器主动向will topic发送will message,
此时其他监听will topic的用户即可获得客户端离线的will遗言通知;

而在Emqx,存在系统主题订阅,其中的系统主题$SYS/brokers/${node}/clients/${clientId}/connected, $SYS/brokers/${node}/clients/${clientId}/disconnected同样支持客户端上线、下线的通知;

LWT和Emqx SYS主题通知效果对比

机制\事件 connect disconnect 异常disconnect
$SYS/brokers/${node}/clients/${clientId}/connected
$SYS/brokers/${node}/clients/${clientId}/disconnected
LWT

通过实际测试对比发现,LWT仅支持异常disconnect的通知,而emqx的$SYS/brokers/${node}/clients/${clientId}/disconnected系统主题可以捕捉到所有离线通知(主动diconnect、异常disconnect),即emqx的系统主题即可完整支持客户端上线、下线监听的需求。可以根据实际的需求选择适当的机制。
若使用LWT机制、结合retained messages亦可以实现客户端上线、下线的监听,LWT上下线监听参考方案如下:

client1在connect时指定lastWillMessage “Offline”,同时设置lastWillRetain为true, lastWillTopic设置为client1/status,
client1在连接成功(包括之后每次重连成功)时主动publish消息"Online"(且消息retained设置为true)到同一主题client1/status,
client1在主动disconnect之前,都要主动publish消息"Offline"(且消息retained设置为true)到同一主题client1/status,
client1在异常disconnect时,mqtt服务器都会主动publish消息"Offline"(且消息retained设置为true)到同一主题client1/status,
如此,只要client1在线时,client1/status中的消息都是Online, 而只要client1离线时,client1/status中的消息都是offline,
而使用retained消息(保留最新的一条消息)即可保证其他订阅该主题的客户端总能获得client1的最新在线状态,
该方式每个客户端指定唯一will topic如clientId/status,而关注用户上下线的客户端可以统一订阅主题+/status以获得其他所有客户端的上下线状态。
参考链接:MQTT Essentials: Part 9 last-will-and-testament

若mqtt服务器为emqx,推荐使用SYS主题方式。

emqx设置

在这里插入图片描述
参考官网:用户指南->$SYS-系统主题

(1)修改etc/acl.conf
设置allow所有用户订阅$SYS/brokers/+/clients/#主题

{allow, {user, "dashboard"}, subscribe, ["$SYS/#"]}.

{allow, {ipaddr, "127.0.0.1"}, pubsub, ["$SYS/#", "#"]}.

{allow, all, subscribe, ["$SYS/brokers/+/clients/#"]}.

{deny, all, subscribe, ["$SYS/#", {eq, "#"}]}.

{allow, all}.

参看官网:配置说明->匿名认证与 ACL 文件

(2)重新加载acl文件(或者重启emqx服务)

$ ./bin/emqx_ctl acl reload

参看官网:管理命令->acl命令

示例代码

示例代码均采用hivemq客户端、mqtt5

客户端代码

package com.mx.mqtt.sys;

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.config.Consts;
import com.mx.mqtt.jwt.JwtUtils;
import com.mx.mqtt.utils.CommonUtils;

/**
 * emqx - 客户端(指定LWT配置)
 *
 * @Ahthor luohq
 * @Date 2020-04-09
 */
public class EmqxClient {

    private static final String SYS_TOPIC_CONNECTED = "$SYS/brokers/+/clients/+/connected";
    private static final String SYS_TOPIC_DISCONNECTED = "$SYS/brokers/+/clients/+/disconnected";

    private static final String MQTT_JWT_SECRET = "xxxx";
    private static final String MQTT_SERVER_HOST = "192.168.xx.xxx";
    private static final Integer MQTT_SERVER_PORT = 1883;
    private static final String MQTT_CLIENT_ID = "luohq" + CommonUtils.uuid();
    private static final String MQTT_LWT_TOPIC = "lwt";


    public static void main(String[] args) {
        Mqtt5BlockingClient client = buildMqtt5Client();
        Mqtt5AsyncClient asyncClient = client.toAsync();
    }


    public static Mqtt5BlockingClient buildMqtt5Client() {
        /** blocking客户端 */
        Mqtt5BlockingClient client = Mqtt5Client.builder()
                .identifier(MQTT_CLIENT_ID)
                .serverHost(MQTT_SERVER_HOST)
                .serverPort(MQTT_SERVER_PORT)
                .addConnectedListener(new MqttClientConnectedListener() {
                    @Override
                    public void onConnected(MqttClientConnectedContext context) {
                        System.out.println("mqtt onConnected context");
                    }
                })
                .addDisconnectedListener(new MqttClientDisconnectedListener() {
                    @Override
                    public void onDisconnected(MqttClientDisconnectedContext context) {
                        System.out.println("mqtt onDisconnected context");
                    }
                })
                //自动重连(指数级延迟重连(起始延迟1s,之后每次2倍,到2分钟封顶) delay : 1s-> 2s -> 4s -> ... -> 2min)
                .automaticReconnectWithDefaultConfig()
                /** 指定LWT */
                .willPublish()
                    .topic(MQTT_LWT_TOPIC)
                    .qos(MqttQos.EXACTLY_ONCE)
                    //will内容:clientId
                    .payload(MQTT_CLIENT_ID.getBytes())
                    .applyWillPublish()
                .buildBlocking();
        /** 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;
        try {
            connAck = client.connectWith()
                    .simpleAuth(auth)
                    /** cleanSession=false */
                    .cleanStart(false)
                    /** session 7天过期 */
                    .sessionExpiryInterval(Consts.MqttConsts.SESSION_EXPIRATION)
                    /** keepalive 时长*/
                    .keepAlive(60)
                    .send();
        } catch (Mqtt5ConnAckException e) {
            e.printStackTrace();
            connAck = e.getMqttMessage();
        }


        /** 连接(普通无密码连接) */
        //Mqtt5ConnAck connAck = client.connect();

        System.out.println(connAck.getReasonCode() + ":" + connAck.getReasonString() + ":" + connAck.getResponseInformation());
        if (connAck.getReasonCode().isError()) {
            System.err.println("Mqtt5连接失败!");
            System.exit(-1);
        }
        return client;
    }
}

监听端代码

package com.mx.mqtt.sys;

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.hivemq.client.mqtt.mqtt5.message.publish.Mqtt5Publish;
import com.mx.mqtt.config.Consts;
import com.mx.mqtt.jwt.JwtUtils;
import com.mx.mqtt.utils.CommonUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.UnsupportedEncodingException;

/**
 * emqx - SYS - 主题监控
 *
 * @Ahthor luohq
 * @Date 2020-04-09
 */
public class EmqxSysListener {

    /**
     * 日志
     */
    private static final Logger logger = LogManager.getLogger(EmqxSysListener.class);

    private static final String SYS_TOPIC_CONNECTED = "$SYS/brokers/+/clients/+/connected";
    private static final String SYS_TOPIC_DISCONNECTED = "$SYS/brokers/+/clients/+/disconnected";

    private static final String MQTT_JWT_SECRET = "xxxx";
    private static final String MQTT_SERVER_HOST = "192.168.xx.xxx";
    private static final Integer MQTT_SERVER_PORT = 1883;
    private static final String MQTT_CLIENT_ID = "luohq";
    private static final String MQTT_LWT_TOPIC = "lwt";


    public static void main(String[] args) {
        Mqtt5BlockingClient client = buildMqtt5Client();
        Mqtt5AsyncClient asyncClient = client.toAsync();

        //订阅主题
        asyncClient.subscribeWith()
                .topicFilter(SYS_TOPIC_CONNECTED)
                .qos(MqttQos.EXACTLY_ONCE)
                //消费主题消息(异步)
                .callback(mqtt5Publish -> {
                    printMsg("CONNECTED", mqtt5Publish);
                })
                .send();

        //订阅主题
        asyncClient.subscribeWith()
                .topicFilter(SYS_TOPIC_DISCONNECTED)
                .qos(MqttQos.EXACTLY_ONCE)
                //消费主题消息(异步)
                .callback(mqtt5Publish -> {
                    printMsg("DISCONNECTED", mqtt5Publish);
                })
                .send();

        //订阅主题
        asyncClient.subscribeWith()
                .topicFilter(MQTT_LWT_TOPIC)
                .qos(MqttQos.EXACTLY_ONCE)
                //消费主题消息(异步)
                .callback(mqtt5Publish -> {
                    printMsg("LWT", mqtt5Publish);
                })
                .send();

    }


    public static Mqtt5BlockingClient buildMqtt5Client() {
        /** blocking客户端 */
        Mqtt5BlockingClient client = Mqtt5Client.builder()
                .identifier(MQTT_CLIENT_ID)
                .serverHost(MQTT_SERVER_HOST)
                .serverPort(MQTT_SERVER_PORT)
                .addConnectedListener(new MqttClientConnectedListener() {
                    @Override
                    public void onConnected(MqttClientConnectedContext context) {
                        System.out.println("mqtt onConnected context");
                    }
                })
                .addDisconnectedListener(new MqttClientDisconnectedListener() {
                    @Override
                    public void onDisconnected(MqttClientDisconnectedContext context) {
                        System.out.println("mqtt onDisconnected context");
                    }
                })
                //自动重连(指数级延迟重连(起始延迟1s,之后每次2倍,到2分钟封顶) delay : 1s-> 2s -> 4s -> ... -> 2min)
                .automaticReconnectWithDefaultConfig()
                .buildBlocking();
        /** 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;
        try {
            connAck = client.connectWith()
                    .simpleAuth(auth)
                    /** cleanSession=false */
                    .cleanStart(false)
                    /** session 7天过期 */
                    .sessionExpiryInterval(Consts.MqttConsts.SESSION_EXPIRATION)
                    .send();
        } catch (Mqtt5ConnAckException e) {
            e.printStackTrace();
            connAck = e.getMqttMessage();
        }


        /** 连接(普通无密码连接) */
        //Mqtt5ConnAck connAck = client.connect();

        System.out.println(connAck.getReasonCode() + ":" + connAck.getReasonString() + ":" + connAck.getResponseInformation());
        if (connAck.getReasonCode().isError()) {
            logger.error("Mqtt5连接失败!");
            System.exit(-1);
        }
        return client;
    }

    private static void printMsg(String label, Mqtt5Publish mqtt5Publish) {
        try {
            String msgStr = new String(mqtt5Publish.getPayloadAsBytes(), "UTF-8");
            logger.info(CommonUtils.buildStr("【", label, "】", msgStr));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }
}

发布了57 篇原创文章 · 获赞 7 · 访问量 3万+

猜你喜欢

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