前言
之前有些开发者在实现App入口大厅让用户可以实时互动时,会用一些聊天室的方式来实现,但是这种方式的局限是聊天室支持人数有限,并不能做到全员互动,如果拆分成不同的聊天室,还需要考虑的消息同步,人员联系等各种情况,增加工作量不说,效果还并不好
在改造忘忧首页弹幕匹配信息时想到了一种方案,使用MQTT协议进行操作,轻量级消息传输协议,海量级设备连接,弱网友好,支持协议多等等
正好环信推出了MQTT云服务,支持千万级设备接入,免费版峰值100个会话,300万的消息量,忘忧社交系统只是首页匹配消息同步,完全可以白嫖啊,搞一波😁
使用
首先就是和自己搭建不同,使用三方云服务比较省心,后端不需要太操心,客户端只需要按照文档一步步链接就好,
我这里是在之前开源的设计系统的基础上进行开发,服务器和App端代码都在github和gitee上有,可以先获取下源码,配合食用效果更佳
后端部分
首先就是后端部分,这里分两部分:配置和接口
配置部分
配置部分只需要按照环信后台配置信息进行替换就好,配置在config目录下的config.xxx.json文件内
/**
* Easemob MQTT 配置 <https://console.easemob.com/app/generalizeMsg/overviewService>
*/
config.mqtt = {
host: 'mqtt host', // MQTT 链接地址
appId: 'appId', // MQTT AppId
port: [ 1883, 1884, 80, 443 ], // MQTT 端口 1883(mqtt),1884(mqtts),80(ws),443(wss)
restHost: '<https://api.cn1.mqtt.chat/app/8igtc0>', // MQTT 服务 API 地址
clientId: 'client id', // 替换环信后台 clientId
clientSecret: 'client secret', // 替换环信后台 clientSecret
};
接口实现
这里主要是客户端链接所需要的token生成,官方文档是只简单的列出了在客户端直接生成token的java代码,这是不安全的,为了安全token肯定是要放在服务器端生成的,废话不多说,上代码:
/**
* Create by lzan13 2022/03/21
* 描述:调用环信 MQTT REST API 处理服务
*/
'use strict';
const Service = require('egg').Service;
class MQTTService extends Service {
/**
* 统一进行请求
* @param url api 地址
* @param method 请求方式 GET POST DELETE PUT
* @param customHeaders 自定义请求头
* @param data 请求参数
*/
async apiRequest(url, method, customHeaders, data) {
const headers = {
'Content-Type': 'application/json',
};
Object.assign(headers, customHeaders);
return await this.ctx.curl(url, {
method,
headers,
data,
dataType: 'json',
});
}
/**
* 请求 token
*/
async token() {
const { app, ctx } = this;
const apiUrl = `${app.config.mqtt.restHost}/openapi/rm/app/token`;
const data = {
appClientId: app.config.mqtt.clientId,
appClientSecret: app.config.mqtt.clientSecret,
};
const result = await this.apiRequest(apiUrl, 'POST', {}, data);
const mqtt = {};
if (result.status === 200) {
mqtt.token = result.data.body.access_token;
mqtt.time = Date.now() / 1000 + result.data.body.expires_in;
}
ctx.common.mqtt = mqtt;
return mqtt;
}
/**
* 检查 token 是否过期,过期了就重新请求一下缓存起来
* @return {Promise<void>}
*/
async checkToken() {
const { ctx } = this;
let mqtt = ctx.common.mqtt;
if (mqtt && mqtt.token) {
// 过期 1 分钟前都重新请求
if (mqtt.time > Date.now() / 1000 + 60) {
return mqtt.token;
}
}
mqtt = await this.token();
return mqtt.token;
}
/**
* --------------------------------------------------------------
* 用户操作
*/
/**
* 获取 MQTT 链接需要的用户 Token
* @return {Promise<void>}
*/
async userToken(id) {
const { app } = this;
// 获取 token
const token = await this.checkToken();
if (!token) {
return '';
}
const apiUrl = `${app.config.mqtt.restHost}/openapi/rm/user/token`;
const data = {
username: id,
cid: `${id}@${app.config.mqtt.appId}`,
};
const result = await this.apiRequest(apiUrl, 'POST', { Authorization: ` ${token}` }, data);
if (result.status === 200) {
return result.data.body.access_token;
}
return '';
}
}
module.exports = MQTTService;
客户端实现
客户端实现也比较简单,主要就是在vmmqtt这个module封装了几个方法,实现了mqtt的链接建立,消息接收与发,这里只是贴一下主要代码,真正使用是在App层,首先说去上边服务器生成的token,然后链接mqtt服务器,接着监听mqtt的消息就OK了,收什么消息呢,就是App层自己的心情变化
首先引入sdk
这一步官方文档比较明确,就是根据自己的平台引入相应的mqtt客户端sdk,这里简单贴一下AndroidStudio的引入配置
// 在根目录 build.gradle repositories 下加入配置
maven { url "<https://repo.eclipse.org/content/repositories/paho-snapshots/>" }
...
// 然后在需要引入 MQTT 的地方加入 MQTT 依赖
// MQTT sdk <https://docs-im.easemob.com/mqtt/qsandroidsdk>
implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0'
implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1'
链接封装
这里贴一下对mqtt相关方法的简单封装,代码在vmmqtt模块儿的MQTTHelper类下:
/**
* Create by lzan13 on 2022/3/22
* 描述:MQTT 帮助类
*/
object MQTTHelper {
private var mqttClient: MqttAndroidClient? = null
// 缓存主题集合
private val topicList = mutableListOf<String>()
/**
* 链接MQTT
* @param id 用户 Id
* @param token 用户链接 MQTT 的 Token
* @param topic 需要订阅的主题,不为空就会在连接成功后进行订阅
*/
fun connect(id: String, token: String, topic: String = "") {
// 处理订阅主题
if (topic.isNotEmpty()) topicList.add(topic)
// 拼接链接地址
val url = "tcp://${MQTTConstants.mqttHost()}:${MQTTConstants.mqttPort()}"
// 拼接 clientId
val clientId = "${id}@${MQTTConstants.mqttAppId()}"
mqttClient = MqttAndroidClient(VMTools.context, url, clientId)
//连接参数
val options = MqttConnectOptions()
options.isAutomaticReconnect = true //设置自动重连
options.isCleanSession = true // 缓存
options.connectionTimeout = CConstants.timeMinute.toInt() // 设置超时时间,单位:秒
options.keepAliveInterval = CConstants.timeMinute.toInt() // 心跳包发送间隔,单位:秒
options.userName = id // 用户名
options.password = token.toCharArray() // 密码
options.mqttVersion = MqttConnectOptions.MQTT_VERSION_3_1_1;
// 设置MQTT监听
mqttClient?.setCallback(object : MqttCallback {
override fun connectionLost(t: Throwable) {
// 通知链接断开
VMLog.d("MQTT 链接断开 $t")
}
@Throws(Exception::class)
override fun messageArrived(topic: String, message: MqttMessage) {
// 通知收到消息
VMLog.d("MQTT 收到消息:$message")
// 如果未订阅则直接丢弃
if (!topicList.contains(topic)) return
notifyEvent(topic, String(message.payload))
}
override fun deliveryComplete(token: IMqttDeliveryToken) {}
})
//进行连接
mqttClient?.connect(options, null, object : IMqttActionListener {
override fun onSuccess(token: IMqttToken) {
VMLog.d("MQTT 链接成功")
// 链接成功,循环订阅缓存的主题
topicList.forEach { subscribe(it) }
}
override fun onFailure(token: IMqttToken, t: Throwable) {
VMLog.d("MQTT 链接失败 $t")
}
})
}
/**
* 订阅主题
* @param topic 主题
*/
fun subscribe(topic: String) {
if (!topicList.contains(topic)) {
topicList.add(topic)
}
try {
//连接成功后订阅主题
mqttClient?.subscribe(topic, 0, null, object : IMqttActionListener {
override fun onSuccess(token: IMqttToken) {
VMLog.d("MQTT 订阅成功 $topic")
}
override fun onFailure(token: IMqttToken, t: Throwable) {
VMLog.d("MQTT 订阅失败 $topic $t")
}
})
} catch (e: MqttException) {
e.printStackTrace()
}
}
/**
* 取消订阅
* @param topic 主题
*/
fun unsubscribe(topic: String) {
if (topicList.contains(topic)) {
topicList.remove(topic)
}
try {
mqttClient?.unsubscribe(topic)
} catch (e: MqttException) {
e.printStackTrace()
}
}
/**
* 发送 MQTT 消息
* @param topic 主题
* @param content 内容
*/
fun sendMsg(topic: String, content: String) {
val msg = MqttMessage()
msg.payload = content.encodeToByteArray() // 设置消息内容
msg.qos = 0 //设置消息发送质量,可为0,1,2.
// 设置消息的topic,并发送。
mqttClient?.publish(topic, msg, null, object : IMqttActionListener {
override fun onSuccess(asyncActionToken: IMqttToken) {
VMLog.d("MQTT 消息发送成功")
}
override fun onFailure(asyncActionToken: IMqttToken, exception: Throwable) {
VMLog.d("MQTT 消息发送失败 ${exception.message}")
}
})
}
/**
* 通知 MQTT 事件
*/
private fun notifyEvent(topic: String, data: String) {
LDEventBus.post(topic, data)
}
}
业务交互
和业务相关的就是在启动App后,先调用服务获取MQTT链接所需token,然后调用上边封装的链接MQTT服务器方法,同时订阅MQTT过来的消息
// 请求 token 成功后,调用MQTTHelper.connect()链接 MQTT 服务器,这里会同时传递监听的主题
MQTTHelper.connect(mUser.id, token, MQTTConstants.Topic.newMatchInfo)
/**
* 发送匹配信息
*/
private fun sendMatchInfo() {
if (selfMatch.user.nickname.isEmpty()) return
// 提交自己的匹配信息到服务器
mViewModel.submitMatch(selfMatch)
val json = JSONObject()
json.put("content", selfMatch.content)
json.put("emotion", selfMatch.emotion)
json.put("gender", selfMatch.gender)
json.put("type", selfMatch.type)
val jsonUser = JSONObject()
jsonUser.put("avatar", mUser.avatar)
jsonUser.put("id", mUser.id)
jsonUser.put("nickname", mUser.nickname)
jsonUser.put("username", mUser.username)
json.put("user", jsonUser)
MQTTHelper.sendMsg(MQTTConstants.Topic.newMatchInfo, json.toString())
}
// 监听消息这里使用了一个事件总线进行通知,在上边封装 MQTTHelper 发送消息也使用了这个,
// 订阅 MQTT 事件
LDEventBus.observe(this, MQTTConstants.Topic.newMatchInfo, String::class.java) {
val match = JsonUtils.fromJson<Match>(it, Match::class.java)
// 这里收到匹配信息之后就增加一条弹幕
addBarrage(match)
}
总结
核心代码不多,不超过500行,这里主要是并没有关注消息失败的重试等逻辑,只是简单的引入MQTT链接和收发消息,好了就到这里了,关于开源项目的介绍可以看下我的另外几篇文章,或者自己拉代码跑一下,毕竟看着代码运行起来才能比较清晰的了解
源码获取
搜一搜『穿裤衩闯天下』关注我的公众号,查看更多精彩文章
公众号回复『忘忧』『社交』即可获取项目源码和安装体验包