项目购买了阿里云消息队列MQTT,为了更好的监听客户端的在线状态,项目使用了MQTT的特性,异步监听客户端的在线状态。但是借鉴阿里云的官方文档,进展不顺利,提工单后,阿里云的客服回复文档说明有误。
这里把在实现中遇到的问题记录下来。
顺便在这里附上官方文档(当前最新的):
RocketMQ: https://help.aliyun.com/product/29530.html
MQTT: https://help.aliyun.com/product/100973.html
1.环境准备,需要购买阿里云的MQTT以及RocketMQ。
2.在阿里云账号控制台找到RocketMQ,选择你所购买的region,创建一个TOPIC,并创建一个GROUP以备用。
3.切换到MQTT,创建一个GROUP,例如:DEVICE-PUSH。
4.在消息存储处,再创建一个主题(DEVICE-PUSH_MQTT),该主题为MQ通知消息主题,设备上下线,都会通知一条消息到该主题下。
服务端的DEMO代码:
public class RocketMQSendMessageToMQ4IoT {
public static void main(String[] args) throws Exception {
/**
* 初始化消息队列 for RocketMQ 发送客户端,实际业务中一般部署在服务端应用中。
*/
Properties properties = new Properties();
/**
* 设置 RocketMQ 客户端的 GroupID,注意此处的 groupId 和 MQ4IoT 实例中的 GroupId 是2个概念,请按照各自产品的说明申请填写
*/
properties.setProperty(PropertyKeyConst.GROUP_ID, "GID-DEVICE-RocketMQ");
/**
* 账号 accesskey,从账号系统控制台获取
*/
properties.put(PropertyKeyConst.AccessKey, "******");
/**
* 账号 secretKey,从账号系统控制台获取,仅在Signature鉴权模式下需要设置
*/
properties.put(PropertyKeyConst.SecretKey, "******");
/**
* 设置 TCP 接入域名
*/
properties.put(PropertyKeyConst.NAMESRV_ADDR, "*****");
/**
* MQ4IoT 和 RocketMQ 配合使用时,RocketMQ 客户端仅操作一级 Topic。
*/
final String parentTopic = "Payment_Result_Push";
Producer producer = ONSFactory.createProducer(properties);
producer.start();
/**
*消费者订阅通知消息主题,当设备掉线后,会异步接收该主题的消息。
*参见官方文档:https://help.aliyun.com/document_detail/50069.html?
* spm=a2c4g.11174283.6.551.573e42f001VjiO
*/
Properties pnroperties = new Properties();
pnroperties.setProperty(PropertyKeyConst.GROUP_ID, "GID-DEVICE-RocketMQ"); //这里需要添加rocketmq下创建的groupid,不然订阅主题失败。
pnroperties.put(PropertyKeyConst.AccessKey, "******");
pnroperties.put(PropertyKeyConst.SecretKey, "******");
pnroperties.put(PropertyKeyConst.NAMESRV_ADDR, "******");
properties.put(PropertyKeyConst.MessageModel, PropertyValueConst.CLUSTERING);
Consumer consumer = ONSFactory.createConsumer(pnroperties);
consumer.subscribe("GID-DEVICE-PUSH_MQTT", "connect||disconnect||tcpclean", new MessageListener() { //订阅多个 Tag
public Action consume(Message message, ConsumeContext context) {
System.out.println(JSONObject.parse(message.getBody()).toString());
System.out.println("Receive: " + message);
return Action.CommitMessage;
}
});
consumer.start();
System.out.println("Consumer Started");
String clientId = "GID-DEVICE@@@device_sn_1";
for (int i = 0; i < 10; i++) {
/**
* 使用 RocketMQ 客户端发消息给 MQ4IoT客户端时,Topic 指定为一级父Topic,Tag指定为 MQ2MQTT
*/
Message msg = new Message(parentTopic, "MQ2MQTT", "hello mq send mqtt msg".getBytes());
/**
* 发送 P2P 消息时,子级 topic如下设置, 具体参考https://help.aliyun.com/document_detail/96176.html?spm=a2c4g.11186623.6.565.10ac1cf7NKGfjI
*/
msg.putUserProperties(PropertyKeyConst.MqttSecondTopic, "/p2p/" + clientId);
msg.putUserProperties(PropertyKeyConst.MqttQOS,"1");
msg.putUserProperties("cleansessionflag","false");
SendResult result = producer.send(msg);
System.out.println(result);
}
Thread.sleep(1000*30*2);
producer.shutdown();
System.out.println("结束");
System.exit(0);
}
}
客户端代码:
public class MQClient {
public static void main(String[] args) throws MqttException, InterruptedException, InvalidKeyException, NoSuchAlgorithmException {
/*
* MQ4IOT 实例 ID,购买后控制台获取
*/
String instanceId = "*****";
/*
* 接入点地址,购买 MQ4IOT 实例,且配置完成后即可获取,接入点地址必须填写分配的域名,不得使用 IP 地址直接连接,否则可能会导致客户端异常。
*/
String endPoint = "*****";
/*
* 账号 accesskey,从账号系统控制台获取
*/
String accessKey = "*****";
/*
* 账号 secretKey,从账号系统控制台获取,仅在Signature鉴权模式下需要设置
*/
String secretKey = "*****";
/*
* MQ4IOT clientId,由业务系统分配,需要保证每个 tcp 连接都不一样,保证全局唯一,如果不同的客户端对象(tcp 连接)使用了相同的 clientId 会导致连接异常断开。
* clientId 由两部分组成,格式为 GroupID@@@DeviceId,其中 groupId 在 MQ4IOT 控制台申请,DeviceId 由业务方自己设置,clientId 总长度不得超过64个字符。
*/
String clientId = "GID-DEVICE-PUSH@@@device_sn_1";
/*
* MQ4IOT支持子级 topic,用来做自定义的过滤,此处为示意,可以填写任何字符串,具体参考https://help.aliyun.com/document_detail/42420.html?spm=a2c4g.11186623.6.544.1ea529cfAO5zV3
* 需要注意的是,完整的 topic 长度不得超过128个字符。
*/
//final String subTopic = "/mq4Iot";
final String mq4IotTopic = "****" ; //在rocketmq端创建的TOPIC
/*
* QoS参数代表传输质量,可选0,1,2,根据实际需求合理设置,具体参考 https://help.aliyun.com/document_detail/42420.html?spm=a2c4g.11186623.6.544.1ea529cfAO5zV3
*/
final int qosLevel = 1;
ConnectionOptionWrapper connectionOptionWrapper = new ConnectionOptionWrapper(instanceId, accessKey, secretKey, clientId);
final MemoryPersistence memoryPersistence = new MemoryPersistence();
/*
* 客户端使用的协议和端口必须匹配,具体参考文档 https://help.aliyun.com/document_detail/44866.html?spm=a2c4g.11186623.6.552.25302386RcuYFB
* 如果是 SSL 加密则设置ssl://endpoint:8883
*/
final MqttClient mqttClient = new MqttClient("tcp://" + endPoint + ":1883", clientId, memoryPersistence);
/*
* 客户端设置好发送超时时间,防止无限阻塞
*/
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) {
/**
* 客户端连接成功后就需要尽快订阅需要的 topic
*/
System.out.println("connect success");
executorService.submit(new Runnable() {
@Override
public void run() {
try {
final String topicFilter[] = {mq4IotTopic};
final int[] qos = {qosLevel};
mqttClient.subscribe(topicFilter, qos);
} catch (MqttException e) {
e.printStackTrace();
}
}
});
}
@Override
public void connectionLost(Throwable throwable) {
throwable.printStackTrace();
}
@Override
public void messageArrived(String s, MqttMessage mqttMessage) throws Exception {
/**
* 消费消息的回调接口,需要确保该接口不抛异常,该接口运行返回即代表消息消费成功。
* 消费消息需要保证在规定时间内完成,如果消费耗时超过服务端约定的超时时间,对于可靠传输的模式,服务端可能会重试推送,业务需要做好幂等去重处理。超时时间约定参考限制
* https://help.aliyun.com/document_detail/63620.html?spm=a2c4g.11186623.6.546.229f1f6ago55Fj
*/
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(1000*60*60);
}
}