一、前言
前段时间公司预研了设备app端与服务端的交互方案,出于多方面考量最终选用了阿里云的微服务队列MQTT方案,基于此方案,本人主要实践有:
1. 封装了RocketMQ实现MQTT订阅与发布的实现细节;
2. 实现了注解式分发处理,可利用如MqttController
, MqttTopicMapping
等相关自定义注解的方式来统一订阅MQTT的Topic以及消息处理的分发;
3. 使用了一套请求和响应的同步机制来达到PUB/SUB异步通信的伪同步调用。
Github 地址点此链接
二、RocketMQ的接入细节
1. 为什么服务端要使用RocketMQ接入
阿里云微消息队列MQTT是在以消息队列 RocketMQ 为核心存储的基础上,实现更适合移动互联网和IoT领域的无状态网关,两者之间具备天然的数据互通性。MQTT实例本身并不提供消息数据持久化功能,消息数据持久化需要搭配后端的消息存储实例来使用。因此现阶段每一个阿里云MQTT实例都必须配套一个消息存储实例,即RocketMQ实例来提供消息数据持久化功能,因此他们之间可以说是消息互通的,即可用RocketMQ订阅的方式来消费用MQTT协议发布的消息,同理也可用 MQTT协议订阅的方式来消费RocketMQ发布的消息。
帮助文档也给出了以下两种产品的区别说明:
微消息队列MQTT基于MQTT协议实现,单个客户端的处理能力较弱。因此,微消息队列MQTT适用于拥有大量在线客户端(很多企业设备端过万,甚至上百万),但每个客户端消息较少的场景。
相比之下,消息队列RocketMQ是面向服务端的消息引擎,主要用于服务组件之间的解耦、异步通知、削峰填谷等,服务器规模较小(极少企业服务器规模过万),但需要大量的消息处理,吞吐量要求高。因此,消息队列RocketMQ适用于服务端进行大批量的数据处理和分析的场景。
基于以上区别,官方也推荐在移动端设备上使用微消息队列MQTT,而在服务端应用中则使用消息队列RocketMQ,具体则可以通过 MQTT SDK 以公网访问方式来实现设备间的通信,通过MQ SDK以内网方式来实现服务端通信。
2. RocketMQ如何对接
RocketMQ与MQTT在消息结构和一些属性字段上都有一定的映射关系,具体内容(摘自帮助文档)如下。
微消息队列MQTT使用MQTT协议接入,而消息队列RocketMQ使用的是私有协议,因此,两者的关键概念存在如下映射关系。
如上图所示,MQTT协议中Topic是多级结构,而消息队列RocketMQ的Topic 仅有一级,因此,MQTT中的一级Topic映射到消息队列RocketMQ的Topic,而二级和三级Topic则映射到消息队列RocketMQ的消息属性(Properties)中。
消息队列 RocketMQ 协议中的消息(Message)可以拥有自定义属性(Properties),而MQTT协议目前的版本不支持属性,但为了方便对MQTT协议中的Header信息和设备信息进行溯源,MQTT的部分信息将被映射到 RocketMQ的消息属性中,方便使用消息队列RocketMQ的SDK接入的用户获取。
目前,微消息队列MQTT和消息队列RocketMQ支持的属性字段映射表如下图所示。使用消息队列RocketMQ的SDK的应用和使用消息队列MQTT的SDK的应用进行交互时,可以通过读写这些属性字段来达到信息获取或者设置的目的。
3. RocketMQ对MQTT消息订阅的实现
Properties properties = new Properties();
// 在控制台创建的Group ID
properties.put(PropertyKeyConst.GROUP_ID, "xxx");
// 阿里云AccessKey
properties.put(PropertyKeyConst.AccessKey, "xxx");
// 阿里云SecretKey
properties.put(PropertyKeyConst.SecretKey, "xxx");
// 在RocketMQ控制台的实例基本信息中可查看到的TCP协议接入点
properties.put(PropertyKeyConst.NAMESRV_ADDR,
"xxx");
Consumer consumer = ONSFactory.createConsumer(properties);
consumer.subscribe("topic", "*", new MessageListener() {
//订阅全部 Tag
public Action consume(Message message, ConsumeContext context) {
//获得mqtt消息中的第一级topic
String mqttFirstTopic = message.getTopic();
//获得mqtt消息中除去1级后的所有topic
String mqttSecondTopic = message.getUserProperties(PropertyKeyConst.MqttSecondTopic);
//获得mqtt消息中的messageId
String messageId = message.getUserProperties("UNIQ_KEY");
//获得mqtt消息中的消息体
String messageBody = new String(message.getBody());
//...
return Action.CommitMessage;
}
});
consumer.start();
实现主要注意2点:
- 这边的 MQ 只需要订阅 MQTT 的一级 Topic 。如果 MQTT 会发布2个 Topic 的消息
robot/alarm
和robot/task/test
,则在此处只需要订阅robot
这个第一级Topic即可。 - MQTT 的一些属性字段可以从 RocketMQ 消息
Message
的userProperties
字段中获得,比如上面代码中通过message.getUserProperties(PropertyKeyConst.MqttSecondTopic);
可以获得 MQTT 中的 除去1级后的所有 Topic 字符串,如上述举例的2个 Topic 可分别获得/alarm
和/task/test
。 具体能够获得哪些字段可以参考上一节的属性字段映射表,也可自行查看PropertyKeyConst
类中定义的一些字符串常量来大概知晓。
使用阿里云MQTT控制台发送一个MQTT消息,如图所示:
在程序中加一个断点获得当前Message
对象的字段如下:
上图可看到userProperties
中的一些值,比如qoslevel
,mqttSecondTopic
等,这些字段都可以在PropertyKeyConst
类中找到对应的字符串常量,但是UNIQ_KEY
,cleansessionflag
等PropertyKeyConst
类中并没有对应的字符串常量,这边暂时就message.getUserProperties("UNIQ_KEY")
这样使用自定义字符量来获得。
4. RocketMQ对MQTT消息发布的实现
Properties properties = new Properties();
// 在控制台创建的Group ID
properties.put(PropertyKeyConst.GROUP_ID, "xxx");
// 阿里云AccessKey
properties.put(PropertyKeyConst.AccessKey, "xxx");
// 阿里云SecretKey
properties.put(PropertyKeyConst.SecretKey, "xxx");
// 在RocketMQ控制台的实例基本信息中可查看到的TCP协议接入点
properties.put(PropertyKeyConst.NAMESRV_ADDR,
"xxx");
//设置发送超时时间,单位毫秒
properties.setProperty(PropertyKeyConst.SendMsgTimeoutMillis, "3000");
Producer producer = ONSFactory.createProducer(properties);
// 在发送消息前,必须调用 start 方法来启动 Producer,只需调用一次即可
producer.start();
//发送一个mqtt消息
String parentTopic = topic.substring(0, topic.indexOf("/"));
String subTopic = topic.substring(topic.indexOf("/"));
Message msg = new Message(parentTopic, "", message.getBytes());
msg.putUserProperties(PropertyKeyConst.MqttSecondTopic, subTopic);
msg.putUserProperties(PropertyKeyConst.MqttQOS, qos);
msg.putUserProperties("cleansessionflag", "" + cleanSessionFlag);
SendResult result = producer.send(msg);
- 该代码仅实现了普通消息的同步发送,若需发送顺序消息、延时消息等,可参考SDK帮助文档创建不同的
Producer
实现即可。 - 上述代码将需要发送的MQTT全量Topic拆分成1级与2级,1级Topic设置为MQ中的Topic参数,2级Topic字符串则设为
userProperties
中PropertyKeyConst.MqttSecondTopic
的,其他属相如qoslevel
和cleansessionflag
等也是通过userProperties
的相关字段来设置。
三、注解式分发处理的实现
1. 前置知识点
1.1 BeanPostProcessor
BeanPostProcessor是Spring IOC容器给我们提供的一个扩展接口。BeanPostProcessor接口定义了两个方法:
public interface BeanPostProcessor {
// 前置处理
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
// 后置处理
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}
Spring中Bean的整个生命周期如图所示:
postProcessBeforeInitialization()方法与postProcessAfterInitialization()分别对应图中前置处理和后置处理两个步骤将执行的方法。这两个方法中都传入了bean对象实例的引用,为扩展容器的对象实例化过程提供了很大便利,在这儿几乎可以对传入的实例执行任何操作。
可以看到,Spring容器通过BeanPostProcessor给了我们一个机会对Spring管理的bean进行再加工,注解、AOP等功能的实现均大量使用了BeanPostProcessor。通过实现BeanPostProcessor的接口,在其中处理方法中判断bean对象上是否有自定义的一些注解,如果有,则可以对这个bean实例继续进行其他操作,这也是本例中使用该接口要实现的主要目的。
1.2 ApplicationListener
在IOC的容器的启动过程,当所有的bean都已经处理完成之后,spring ioc容器会有一个发布事件的动作。从 AbstractApplicationContext 的源码中就可以看出:
protected void finishRefresh() {
// Initialize lifecycle processor for