笔者希望能为一些选择了EMQ作为消息推送服务的同学启发,并将使用EMQ过程中笔者遇到的问题暴露出来,当然也希望其他使用EMQ的同学能够给笔者更好的建议。本文的食用人群为对EMQ做过调研或者有相关实践经验的同学,如果您不属于该类,烦请移步:http://www.emqtt.com/。
1.部署简单;2.支持集群部署;3.官方文档全面;4.上手简单;5:开箱即用,6.百万级分布式开源物联网MQTT消息服务器
topic设计、由谁推送如客户端-客户端;服务端-客户端两种方式、安全性设计:怎么避免每个MQTT客户端订阅别人的topic窃听消息、账户打通:如何让系统的用户可以连接到EMQ服务器、如何集成到现有系统。本文围绕这几个主要问题,给出我司采用EMQ消息推送服务的解决方案。
用户登录系统->登陆成功->使用当前账号连接EMQ服务器->调用接口订阅Topic->客户端接收消息并提示->退出登录->服务端sessionDestroyed方法调用断开连接方法->结束。
笔者需要消息推送的两个个业务场景为:A.用户上传文件提醒,B.创建一个协同推送消息给指定相关人,收到推送消息用户有Android、Web、PC端在线用户。
当笔者选用EMQ作为消息推送服务后,面临的第一个需要解决的问题就是
Topic如何设计?
方式一:一个接收人为一个topic,采用userName作为topic
方式二:每个操作对象比如一个文件或者协同作为topic
两种方式比较:
方式一:每个用户在连接到EMQ服务器后只需要订阅自己userName即可,所有的推送消息通过userName的topic去接收,但是在推送的时候需要对每个需要进行消息提醒的对象比如文件或者协同的相关人进行for循环发送消息到topic。
方式二:这种方式,用户需要订阅的topic比较灵活,而且会动态变化,比如我修改了协同的相关人,那么该相关人需要取消对协同对象topic的订阅,或者在每次连接到EMQ服务器都需要重新将所有的topic订阅一遍。
笔者采用了方式一,这里笔者的业务推送人不会很多,for循环就for循环了,并且使用方式一笔者只需要专注于业务逻辑就好了,不用管topic的动态改变。
推送方式的选择
客户端-客户端:客户端调用服务端接口成功后,直接通过客户端API发送消息到指定人的topic就好了(也可在创建协同或者上传文件接口中保存消息或者通过服务端订阅所有topic并将所有收到的topic消息落库)
服务端-客户端:服务端在上传接口或者创建协同的业务逻辑中保存并推送消息
笔者更倾向于服务端-客户端,当然选择哪种方式看个人喜好,有些同学可能会问第一种不是减轻了服务器压力吗,至少省掉了服务端推送;PS:这里可以采用异步推送。
当topic设计和推送方式确定后如何集成到现有系统已经很明了了,笔者公司采用的架构设计为:
采用其他业务系统将推送消息发送到RabbitMq,然后消息推送系统消费RabbitMq消息,并创建Mqtt客户端,发送指定消息到topic。这样既做到了解耦,又做到了异步。
/**
**异步发送推送消息到rabbitMq中
**/
@Async("pusMsgSendExecutor")
@Override
public void sendMsg(Supplier supplier) {
try {
rabbitMqService.send(this.exchangeName, this.queueName,"", supplier.get());
} catch (Exception e) {
logger.error("发送动态消息失败",e);
}
}
/*
* 执行消息推送逻辑
*/
private void doPushMsg() {
EXECUTOR_SERVICE.submit(() -> {
try {
rabbitMqService.receive(MsgConst.RabbitMq.PUSH_MSG_EXCHANGE, MsgConst.RabbitMq.PUSH_MSG_QUEUE, new ReciveCallBackByRoutingKey() {
@Override
public void process(String routingKey, String jsonStr) {
try {
//您的mqttClient客户端连接或者连接池的send发送
mqttclient.publish
} catch (Exception e) {
LOGGER.error("doPushMsg error,msg={}", jsonStr, e);
}
}
});
} catch (Exception e) {
LOGGER.error("receive msg from rabbitmq error,exchange={},queue={}", MsgConst.RabbitMq.PUSH_MSG_EXCHANGE, MsgConst.RabbitMq.PUSH_MSG_QUEUE, e);
}
});
}
配置EMQ:开启EMQ的mysql安全认证插件,打通EMQ用户与现有账号系统
1. 修改emq.conf
vim /emqttd/etc/emq.conf修改配置项的值
mqtt.allow_anonymous =false
2. 修改emq_auth_mysql.conf
vim /emqttd/etc/plugins/emq_auth_mysql.conf
#数据库连接地址
auth.mysql.server = 192.168.1.231:3306
#数据库连接池大小
auth.mysql.pool = 8
#数据库连接用户名
auth.mysql.username = username
#数据库连接密码
auth.mysql.password = pwd
#数据库名
auth.mysql.database = account
#数据库密码加密方式,这里用的是md5+salt的方式
auth.mysql.password_hash = md5,salt
#emq连接客户端用户sql配置,注意这里的fap_user表即为您的账户系统表,这样子便解决了原业务系统用户登录EMQ的问题
auth.mysql.auth_query = select password,salt from fap_user where userName = '%u' union select password,salt from mqtt_user where userName = '%u' limit 1
3. 导入sql文件至配置文件指定的数据库中mqtt_acl、mqtt_user
4. 重启emq服务
5. 登录emq控制台->Plugins
打开emq_auth_mysql开关使其状态为Running如下图
笔者采用了将EMQ的数据库表和账号系统数据库放在一起并修改了配置文件中的auth.mysql.auth_query属性,实现了业务系统用户与EMQ的打通,当然还可以采用另外一种方式:新建账号的时候将账号同步到emq的用户表中。
怎么样防止客户端mqtt订阅别人的topic?
EMQ会用到两张表:mqtt_user、mqtt_acl
通过配置mqtt_acl表并添加如下几条记录,限制所有mqtt客户端订阅别人的topic
#拒绝所有用户订阅topic
INSERT INTO `mqtt_acl` VALUES ('1', '0', null, '$all', null, '1', '#');
#拒绝所有用户发布消息
INSERT INTO `mqtt_acl` VALUES ('1', '0', null, '$all', null, '2', '#');
这里就有个问题了,既然任何人不能订阅topic,但是在topic设计的时候提到需要让客户端订阅自身的topic,该怎么做呢?
这里我们通过EMQ对外的http服务可以跨越acl表帮助客户端订阅。
public JSONObject subscribe(String clientId, QosType qosType, String topic) throws Exception {
String reqUrl = emqServerConfig.getBrokerPrefixUrl() + SUBSCRIBE_URL;
JSONObject jsonParam = new JSONObject();
jsonParam.put("client_id", clientId);
jsonParam.put("qos", qosType.type());
jsonParam.put("topic", topic);
return restTemplate.postForObject(reqUrl, jsonParam, JSONObject.class);
}
这里又引出个问题,如果要帮助客户端的EMQ连接订阅topic那么需要知道clientId,和topic?
可以通过登录接口返回,并将clientId存储到session中,这样子通过封装下emq的Http Api,并将该服务对外提供出去,解决了客户端的订阅问题。
@GetMapping(path = "/subscribe")
public ResponseResult subscribe() throws Exception {
try {
return ResponseResult.buildResultFromJsonObject(gyMqttClientService.subscribe(getMqttClientId(), getUserName()));
} catch (Exception e) {
logger.error("subscribe error,clientId={},topic={}", getMqttClientId(), getUserName(), e);
throw e;
}
}
当session销毁时通过sessionListener销毁EMQ链接,这样解决了EMQ的登出问题
@Override
public void sessionDestroyed(HttpSessionEvent arg0) {
//从session中获取clientId,并调用EMQ对外提供的HttpApi断开连接
String clientId = arg0.getSession().getAttribute("clientId");
String reqUrl = emqServerConfig.getBrokerPrefixUrl() + CLIENT_MANAGE_URL + "/" + clientId;
restTemplate.execute(reqUrl, HttpMethod.DELETE, null, new HttpMessageConverterExtractor(JSONObject.class, restTemplate.getMessageConverters()), (Object) null);
}
添加允许服务器端连接到EMQ的用户并实现推送消息和订阅topic。
#允许mqtt_client用户发布消息
INSERT INTO `mqtt_acl` VALUES ('1', '0', null, 'mqtt_client', null, '2', '#');
#允许mqtt_client订阅topic
INSERT INTO `mqtt_acl` VALUES ('1', '0', null, 'mqtt_client', null, 1', '#');
#服务端使用的EMQ账号配置到mqtt_user表中,
INSERT INTO `mqtt_user` VALUES ('1', 'mqtt_client', '4b96eee45f8e92e562472126bc87cdfd', 'cb4f6cdc', '0', '2018-09-18 14:44:00');