在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();
}
}
}