Background
In many business scenarios, your backend services need to track and analyze when clients connect or disconnect, and push messages based on their online status. ApsaraMQ for MQTT provides asynchronous lifecycle event notifications to help you track the online status of clients. When an MQTT client connects or disconnects, the MQTT service triggers a notification message. You can receive these notifications in one of the following ways:
-
Use a cloud SDK to connect to the ApsaraMQ for MQTT service and retrieve the client's online status. For more information, see Get the online status of ApsaraMQ for MQTT clients.
-
Create a client status notification rule to push status notifications to ApsaraMQ for RocketMQ. You can then subscribe to messages in ApsaraMQ for RocketMQ to retrieve the client's online status.
This topic explains how to create a client status notification rule that enables a backend application to retrieve the online status of clients.

Network access
ApsaraMQ for MQTT provides Public Endpoint and VPC Endpoint.
Public Endpoint is an IP address that is used to access ApsaraMQ for MQTT over the Internet. In most cases, public endpoints are used in the IoT and mobile Internet scenarios.
VPC Endpoint is an IP address that is used to access ApsaraMQ for MQTT in a private virtual cloud (VPC). In most cases, VPC endpoints are used by cloud applications to connect to ApsaraMQ for MQTT.
Important If you want to use an endpoint to connect a client to ApsaraMQ for MQTT, use the domain name instead of the IP address because the IP address dynamically changes. The ApsaraMQ for MQTT technical team is not liable for faults and direct or indirect losses in the following scenarios:
You use an IP address to access your client to ApsaraMQ for MQTT. After the technical team of ApsaraMQ for MQTT updates the domain name resolution, the original IP address becomes invalid.
A firewall policy on IP addresses is set in the network in which your client is running. After the technical team of ApsaraMQ for MQTT updates the domain name resolution, new IP addresses are blocked due to the firewall policy.
Prerequisites
The integrated development environment (IDE) is installed. For more information, see IDE. You can use IntelliJ IDEA or Eclipse. In the example, IntelliJ IDEA is used.
Java 8 or 11 is installed. For more information, see Java Downloads.
An ApsaraMQ for MQTT instance is created, and a topic and a group are created on the instance. For more information, see Create resources.
An ApsaraMQ for RocketMQ instance is created, and a topic and a group are created on the instance. For more information, see Step 2: Create resources.
Important
-
The client status notification rules for ApsaraMQ for MQTT support only ApsaraMQ for RocketMQ 4.x series instances.
-
You cannot use client status notification rules for ApsaraMQ for MQTT across different regions. Therefore, your ApsaraMQ for MQTT and ApsaraMQ for RocketMQ resources must be created in the same region.
1. Create a client status notification rule
Log on to the ApsaraMQ for MQTT console. In the left-side navigation pane, click Instances.
In the top navigation bar, select the region where the instance that you want to manage resides. On the Instances page, click the instance name to go to the Instance Details page.
-
In the left-side navigation pane, click Rules.
-
On the Rules page, click the RocketMQ Rule tab, and then click Create Rule.
-
On the Create Rule page, complete the following steps.
-
Configure Basic Information. Enter a rule ID and select a rule type for client status notifications.
The rule ID must be 3 to 64 characters long and can contain only letters, digits, hyphens (-), and underscores (_). After you complete the configuration, click Next.
-
Configure Rule Source. Select the group ID you created for ApsaraMQ for MQTT.
Click Next.
-
Configure Rule Destination. Select the instance and topic you have created for ApsaraMQ for RocketMQ.
After you complete the selection, click Create.
2. Prepare the test code
You will use code to handle client status changes and process status notifications. This topic provides the required Java demo code.
2.1 Download the sample code
Download the mqtt-java-demo demo project and decompress the demo project package to a folder on your on-premises machine.
In the decompressed demo project, find the lmq-java-demo folder, import the folder to IntelliJ IDEA, and then confirm whether the following dependencies are included in the pom.xml file:
<dependencies>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.10</version>
</dependency>
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>com.aliyun.openservices</groupId>
<artifactId>ons-client</artifactId>
<version>1.8.5.Final</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-onsmqtt</artifactId>
<version>1.0.3</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.0</version>
</dependency>
</dependencies>
Configure access credentials.
Obtain an AccessKey pair. For information about how to obtain an AccessKey pair, see Create an AccessKey.
Configure the environment variables. The environment variable name of the AccessKey ID that is used to access ApsaraMQ for MQTT is MQTT_AK_ENV, and the environment variable name of the AccessKey secret that is used to access ApsaraMQ for MQTT is MQTT_SK_ENV. For information about how to configure environment variables, see Configure access credentials.
2.2 Client connection and disconnection code
In the MQ4IoTSendMessageToMQ4IoTUseSignatureMode.java class, fill in the parameters for the ApsaraMQ for MQTT resource according to the instructions in the code comments.
For this test, you only need to simulate connection and disconnection operations, so you do not need to send messages. You can remove the message-sending code. The following is the sample code.
Sample code for client connection and disconnection
import com.aliyun.openservices.lmq.example.util.ConnectionOptionWrapper;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
public class MQ4IoTSendMessageToMQ4IoTUseSignatureMode {
public static void main(String[] args) throws Exception {
/**
* The ID of the ApsaraMQ for MQTT instance. Obtain this from the console after you purchase the instance.
*/
String instanceId = "XXXXX";
/**
* The endpoint. Obtain the endpoint after you purchase and configure an ApsaraMQ for MQTT instance.
* You must use the assigned domain name for the endpoint. Using a direct IP address can cause client exceptions.
*/
String endPoint = "XXXXX.mqtt.aliyuncs.com";
/**
* The AccessKey ID of your Alibaba Cloud account. Obtain this from the console.
* The AccessKey pair of an Alibaba Cloud account has permissions on all API operations. We recommend that you use a RAM user for API access or routine O&M to prevent security risks.
* We strongly recommend that you do not hardcode your AccessKey ID and AccessKey secret in your project code. This can lead to security risks if the credentials are leaked.
* This example shows how to store the AccessKey ID and AccessKey secret in environment variables. Before running this code, configure the MQTT_AK_ENV and MQTT_SK_ENV environment variables.
* For example: export MQTT_AK_ENV=<access_key_id>
* export MQTT_SK_ENV=<access_key_secret>
* Replace <access_key_id> with your AccessKey ID and <access_key_secret> with your AccessKey secret.
*/
String accessKey = System.getenv("MQTT_AK_ENV");
/**
* The AccessKey secret of your Alibaba Cloud account. Obtain this from the console. This is required only for signature-based authentication.
*/
String secretKey = System.getenv("MQTT_SK_ENV");
/**
* The client ID for ApsaraMQ for MQTT, which is assigned by the business system. Each TCP connection must have a unique client ID.
* If different client objects (TCP connections) use the same client ID, connection exceptions will occur.
* The client ID consists of two parts in the GroupID@@@DeviceId format. The group ID is obtained from the ApsaraMQ for MQTT console, and the DeviceId is set by your business. The total length of the client ID cannot exceed 64 characters.
*/
String clientId = "GID_XXXXX@@@XXXXX";
ConnectionOptionWrapper connectionOptionWrapper = new ConnectionOptionWrapper(instanceId, accessKey, secretKey, clientId);
final MemoryPersistence memoryPersistence = new MemoryPersistence();
/**
* The protocol and port used by the client must match. For more information, see https://www.alibabacloud.com/help/en/message-queue-for-mqtt/latest/limits
* For SSL encryption, set the endpoint to ssl://endpoint:8883.
*/
final MqttClient mqttClient = new MqttClient("tcp://" + endPoint + ":1883", clientId, memoryPersistence);
/**
* Set a timeout for the client to prevent it from blocking indefinitely.
*/
mqttClient.setTimeToWait(5000);
final ExecutorService executorService = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
mqttClient.setCallback(new MqttCallbackExtended() {
@Override
public void connectComplete(boolean reconnect, String serverURI) {
System.out.println("connect success");
}
@Override
public void connectionLost(Throwable throwable) {
throwable.printStackTrace();
}
@Override
public void messageArrived(String s, MqttMessage mqttMessage) throws Exception {
System.out.println("receive msg from topic " + s + " , body is " + new String(mqttMessage.getPayload()));
}
@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
System.out.println("send msg succeed topic is : " + iMqttDeliveryToken.getTopics()[0]);
}
});
mqttClient.connect(connectionOptionWrapper.getMqttConnectOptions());
Thread.sleep(Long.MAX_VALUE);
}
}
2.3 Processing status notification messages
After status notification messages are pushed to ApsaraMQ for RocketMQ, a consumer must subscribe to the messages and process them.
In the MQTTClientStatusNoticeProcessDemo.java class, specify the parameters for the ApsaraMQ for RocketMQ resources as instructed in the code comments. The following code provides an example.
Sample code for processing status notification messages
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.aliyun.openservices.ons.api.Action;
import com.aliyun.openservices.ons.api.ConsumeContext;
import com.aliyun.openservices.ons.api.Consumer;
import com.aliyun.openservices.ons.api.Message;
import com.aliyun.openservices.ons.api.MessageListener;
import com.aliyun.openservices.ons.api.ONSFactory;
import com.aliyun.openservices.ons.api.PropertyKeyConst;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
public class MQTTClientStatusNoticeProcessDemo {
public static void main(String[] args) {
/**
* Initialize the ApsaraMQ for RocketMQ receiving client. In a real-world scenario, this is typically deployed in a backend application.
*/
Properties properties = new Properties();
/**
* Set the ApsaraMQ for RocketMQ group ID. Create this in the ApsaraMQ for RocketMQ console.
*/
properties.setProperty(PropertyKeyConst.GROUP_ID, "GID_XXXX");
/**
* The AccessKey ID for Alibaba Cloud authentication. Create this in the Alibaba Cloud RAM console.
* The AccessKey pair of an Alibaba Cloud account has permissions on all API operations. We recommend that you use a RAM user for API access or routine O&M to prevent security risks.
* We strongly recommend that you do not hardcode your AccessKey ID and AccessKey secret in your project code. This can lead to security risks if the credentials are leaked.
* This example shows how to store the AccessKey ID and AccessKey secret in environment variables.
*/
properties.put(PropertyKeyConst.AccessKey, System.getenv("MQTT_AK_ENV"));
/**
* The AccessKey secret for Alibaba Cloud authentication. Create this in the Alibaba Cloud RAM console. This is required only for signature-based authentication.
*/
properties.put(PropertyKeyConst.SecretKey, System.getenv("MQTT_SK_ENV"));
/**
* Set the TCP endpoint for the ApsaraMQ for RocketMQ instance. Obtain this from the instance details page in the ApsaraMQ for RocketMQ console.
*/
properties.put(PropertyKeyConst.NAMESRV_ADDR, "XXXX");
/**
* When using an ApsaraMQ for RocketMQ consumer to process MQTT client status notifications, subscribe to the status notification topic.
*/
final String parentTopic = "GID_XXXX_MQTT";
/**
* Client status data. In a production environment, we recommend using an external persistent store like a database or Redis to save this information.
* This prevents data loss if the application restarts. This example uses a simple in-memory implementation.
*/
MqttClientStatusStore mqttClientStatusStore = new MemoryHashMapStoreImpl();
Consumer consumer = ONSFactory.createConsumer(properties);
/**
* This example only processes whether the client is online, so we only need to be concerned with 'connect' and 'tcpclean' events.
*/
consumer.subscribe(parentTopic, "connect||tcpclean", new MqttClientStatusNoticeListener(mqttClientStatusStore));
consumer.start();
String clientId = "GID_XXXXX@@@XXXXX";
while (true) {
System.out.println("ClientStatus :" + checkClientOnline(clientId, mqttClientStatusStore));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* The logic for processing connection and disconnection notifications.
* In a real deployment, applications that consume status notifications may be deployed on multiple machines.
* Therefore, client online status data should be maintained in a shared external store like a database or Redis.
* Additionally, you should handle message idempotence to prevent state machine errors from duplicate messages.
*/
static class MqttClientStatusNoticeListener implements MessageListener {
private MqttClientStatusStore mqttClientStatusStore;
public MqttClientStatusNoticeListener(
MqttClientStatusStore mqttClientStatusStore) {
this.mqttClientStatusStore = mqttClientStatusStore;
}
@Override
public Action consume(Message message, ConsumeContext context) {
try {
JSONObject msgBody = JSON.parseObject(new String(message.getBody()));
System.out.println(msgBody);
String eventType = msgBody.getString("eventType");
String clientId = msgBody.getString("clientId");
String channelId = msgBody.getString("channelId");
ClientStatusEvent event = new ClientStatusEvent();
event.setChannelId(channelId);
event.setClientIp(msgBody.getString("clientIp"));
event.setEventType(eventType);
event.setTime(msgBody.getLong("time"));
/**
* First, store the new event.
*/
mqttClientStatusStore.addEvent(clientId, channelId, eventType, event);
/**
* Read the event list for the current channel.
*/
Set<ClientStatusEvent> events = mqttClientStatusStore.getEvent(clientId, channelId);
if (events == null || events.isEmpty()) {
return Action.CommitMessage;
}
/**
* If both the online and offline events have been received in the event list, the current channel is disconnected.
* You can then clear the data for this channel.
*/
boolean findOnlineEvent = false;
boolean findOfflineEvent = false;
for (ClientStatusEvent clientStatusEvent : events) {
if (clientStatusEvent.isOnlineEvent()) {
findOnlineEvent = true;
} else {
findOfflineEvent = true;
}
}
if (findOnlineEvent && findOfflineEvent) {
mqttClientStatusStore.deleteEvent(clientId, channelId);
}
return Action.CommitMessage;
} catch (Throwable e) {
e.printStackTrace();
}
return Action.ReconsumeLater;
}
}
/**
* Checks if a client ID has an active TCP connection based on the status table.
* 1. If there is no channel map, the client is definitely offline.
* 2. If the channel map is not empty, check if the channel data contains only an online event. If so, it means there is an active connection, and the client is online.
* If all channels have disconnection events, the client is definitely offline.
*
* @param clientId
* @param mqttClientStatusStore
* @return
*/
public static boolean checkClientOnline(String clientId,
MqttClientStatusStore mqttClientStatusStore) {
Map<String, Set<ClientStatusEvent>> channelMap = mqttClientStatusStore.getEventsByClientId(clientId);
if (channelMap == null) {
return false;
}
for (Set<ClientStatusEvent> events : channelMap.values()) {
boolean findOnlineEvent = false;
boolean findOfflineEvent = false;
for (ClientStatusEvent event : events) {
if (event.isOnlineEvent()) {
findOnlineEvent = true;
} else {
findOfflineEvent = true;
}
}
if (findOnlineEvent & !findOfflineEvent) {
return true;
}
}
return false;
}
}
3. Verify the result
-
Run the main function in the MQ4IoTSendMessageToMQ4IoTUseSignatureMode.java class to simulate a client going online. You can perform the following operations to check the client status and message delivery status.
Note
To simulate a client disconnection, stop the main method's execution.
-
Query the client status. In the ApsaraMQ for MQTT console, go to the Device Status Query page. Use the Device ID to query the client and verify that it is online.
-
Query the event message delivery. In the ApsaraMQ for RocketMQ console, go to the Message Query page. Query by topic and verify that the online event message was delivered.
In the query results, the Tag for this message is connect, which indicates a client online event.
-
Run the main method in the MQTTClientStatusNoticeProcessDemo.java class. The consumer receives the online event message, and the ClientStatus state changes from false to true.
D:\tools\jdk1.8.0_361\bin\java.exe ...
ClientStatus :false
ClientStatus :false
ClientStatus :false
{"clientId":"GID_xxx1","clientIp":"2xxx9","eventIndex":1726205886624,"eventType":"connect","time":1726205886624,"channelId":"3xxx1245b8cd"}
ClientStatus :true
ClientStatus :true
ClientStatus :true
ClientStatus :true
ClientStatus :true