java后端应用服务器mqtt框架源码及框架介绍
假设我们项目中 有2种客户端 一个是app客户端(Android、ios都是app客户端) ,另一个是设备客户端(比如:智能家居、机器人等) 。 当然客户端还可以有其他比如web等。发布订阅设计图 如下:
简述一下整个流程:
1 mqtt服务器 (EMQ) 启动 .
2 应用服务器启动,作为一个mqtt客户端 去连接 mqtt服务器。出于性能考虑,下面提供的应用服务器源码中与mqtt服务器建立了2个连接。一个专门用于发的mqttPublishClient,另一个专门用于收的mqttClient。(注:客户端就不要分2个连接了,mqtt服务器的资源有限!!!)
用于收的mqttClient 连接mqtt服务器成功后,订阅2个主题 ( 下面主题中的 ‘+’ 是单层通配符,比如app/+/client 可以匹配 app/123/client,app/abc/client ,不能匹配 app/123/abc/client)
3 客户端启动,作为一个客户端与mqtt服务器建一个连接,假设客户端是app且用户的id 是110,那么连接mqtt服务器成功后,订阅用于接收服务器消息的主题 app/110/server
4 客户端发布一个消息: 主题 app/110/client , 内容是 {“cmd”:"/test"} ,
由于应用服务器订阅了主题 app/+/client , 所以mqtt服务器会将客户端发布的那个消息推给应用服务器,
应用服务器收到消息: 主题 app/110/client , 内容是 {“cmd”:"/test"} , 处理并返回(发布)消息: 主题 app/110/server , 内容是 {“cmd”:"/test/response" , “ret”:“0” , “msg”:“测试消息已经处成功处理”} ,同理由于app客户端110已经订阅了 app/110/server , 客户端110能收到这个消息。
下面源码是一个完整的demo源码,业务处理模块需要根据你们自己的项目框架完善。
用到的jar包:
org.eclipse.paho.client.mqttv3-1.2.0.jar ,链接:https://pan.baidu.com/s/1SJL0A7zCsG9vPhPntttFSA
提取码:jbsp
json jar包,链接:https://pan.baidu.com/s/1rHyj05NXZpZxT4hYUI146A
提取码:8eee
在最后,会将整个完整代码分享,如有需要,自行下载
package frame.mqtt;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
/**
* @author zx
*/
public class MqttClientUtil {
public static MqttAsyncClient mqttClient;
public static MqttAsyncClient mqttPublishClient;
public static String TOPIC_PREFIX_DEVICE = "device";
public static String TOPIC_PREFIX_APP = "app";
private static MqttClientCallback mqttClientCallback = new MqttClientCallback();
/**
* 要发布的消息队列
*/
private static LinkedBlockingQueue<String[]> queue = new LinkedBlockingQueue<String[]>();
public static ExecutorService executorService = Executors.newFixedThreadPool(2 * Runtime.getRuntime().availableProcessors() + 1);
/**
* mqtt broker 连接配置,填自己的mqtt地址,及账号密码
*/
private static String broker = "tcp://xxx.xx.xxx.xxx:1883";
private static String username = "xxx";
private static String password = "xxx";
static {
}
public static void createClient() {
System.out.println("mqtt createClient, " + broker);
String clientId = "mqttserver" + String.valueOf(System.currentTimeMillis());
System.out.println("mqtt sub_clientId="+clientId+",pub_clientId="+clientId+"-publish");
try {
//subscribe client
mqttClient = new MqttAsyncClient(broker, clientId, new MemoryPersistence());
mqttClient.setCallback(mqttClientCallback);
//publish client
mqttPublishClient = new MqttAsyncClient(broker, clientId + "-publish", new MemoryPersistence());
mqttPublishClient.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable arg0) {
MqttClientUtil.publishReconnect();
}
@Override
public void deliveryComplete(IMqttDeliveryToken arg0) {
}
@Override
public void messageArrived(String arg0, MqttMessage arg1) throws Exception {
}
});
subscribeReconnect();
publishReconnect();
} catch (MqttException me) {
System.out.println("reason " + me.getReasonCode());
System.out.println("msg " + me.getMessage());
System.out.println("loc " + me.getLocalizedMessage());
System.out.println("cause " + me.getCause());
System.out.println("excep " + me);
me.printStackTrace();
}
//启动发布消息的线程, 循环从队列queue 获取要发布的消息 发布。也就是说要发消息,只要将消息写入队列queue
new Thread(new PublishThread()).start();
}
/**
* 发布消息
* @param topic
* @param content
*/
public static void publish_common(String topic, String content) {
String[] array = { topic, content };
//要发布的消息,入队列。
//PublishThread 会循环从这个队列获取消息发布.
queue.offer(array);
}
public static String[] poll() {
return (String[]) queue.poll();
}
public static String getDeviceIdByTopic(String topic) {
String[] array = topic.split("/");
if (array.length >= 2) {
return array[1];
}
return "";
}
public static String getDevicePublishTopic(String deviceId) {
return TOPIC_PREFIX_DEVICE + "/" + deviceId + "/server";
}
public static String getAppPublishTopic(String deviceId) {
return TOPIC_PREFIX_APP + "/" + deviceId + "/server";
}
/**
* 订阅连接 重连
*/
public static void subscribeReconnect() {
if (mqttClient != null) {
System.out.println("mqtt subscribeReconnect");
try {
MqttConnectOptions connOpts = new MqttConnectOptions();
connOpts.setCleanSession(true);
connOpts.setMaxInflight(100000);
if (username != null && !"".equals(username)) {
connOpts.setUserName(username);
connOpts.setPassword(password.toCharArray());
}
mqttClient.connect(connOpts, null, new IMqttActionListener() {
@Override
public void onSuccess(IMqttToken asyncActionToken) {
try {
System.out.println("mqtt 已连接!");
MqttClientUtil.mqttClient.subscribe(MqttClientUtil.TOPIC_PREFIX_DEVICE + "/+/client", 0);
MqttClientUtil.mqttClient.subscribe(MqttClientUtil.TOPIC_PREFIX_APP + "/+/client", 0);
System.out.println("订阅主题:"+MqttClientUtil.TOPIC_PREFIX_DEVICE + "/+/client");
System.out.println("订阅主题:"+MqttClientUtil.TOPIC_PREFIX_APP + "/+/client");
} catch (MqttException me) {
System.out.println("reason " + me.getReasonCode());
System.out.println("msg " + me.getMessage());
System.out.println("loc " + me.getLocalizedMessage());
System.out.println("cause " + me.getCause());
System.out.println("excep " + me);
me.printStackTrace();
}
}
@Override
public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
System.out.println("mqtt 没有连接上:" + exception.getMessage());
try {
Thread.sleep(60000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
MqttClientUtil.subscribeReconnect();
}
});
} catch (MqttException me) {
System.out.println("reason " + me.getReasonCode());
System.out.println("msg " + me.getMessage());
System.out.println("loc " + me.getLocalizedMessage());
System.out.println("cause " + me.getCause());
System.out.println("excep " + me);
me.printStackTrace();
}
}
}
/**
* 发布连接 重连
*/
public static void publishReconnect() {
if (mqttPublishClient != null) {
System.out.println("mqtt publishReconnect");
try {
MqttConnectOptions connOpts = new MqttConnectOptions();
connOpts.setCleanSession(true);
connOpts.setMaxInflight(100000);
if (username != null && !"".equals(username)) {
connOpts.setUserName(username);
connOpts.setPassword(password.toCharArray());
}
mqttPublishClient.connect(connOpts, null, new IMqttActionListener() {
@Override
public void onFailure(IMqttToken arg0, Throwable arg1) {
System.out.println("mqtt publish client connect is failed" + arg1.getMessage());
try {
Thread.sleep(30000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
MqttClientUtil.publishReconnect();
}
@Override
public void onSuccess(IMqttToken arg0) {
System.out.println("mqtt publish client is connected");
}
});
} catch (MqttException me) {
System.out.println("reason " + me.getReasonCode());
System.out.println("msg " + me.getMessage());
System.out.println("loc " + me.getLocalizedMessage());
System.out.println("cause " + me.getCause());
System.out.println("excep " + me);
me.printStackTrace();
}
}
}
}
MqttClientCallback
package frame.mqtt;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttMessage;
/**
* @author
*/
public class MqttClientCallback implements MqttCallback {
@Override
public void connectionLost(Throwable arg0) {
System.out.println("mqtt 失去了连接");
MqttClientUtil.subscribeReconnect();
}
@Override
public void deliveryComplete(IMqttDeliveryToken arg0) {
System.out.println("mqtt 发送完成!");
}
@Override
public void messageArrived(String topic, MqttMessage message)
throws Exception {
String content = new String(message.getPayload(), "utf-8");
System.out.println("收到mqtt消息,topic: " + topic + " ,content: " + content);
if (MqttClientUtil.executorService != null) {
MqttClientUtil.executorService.execute(new HandlerThread(topic, content));
}
}
}
PublishThread
package frame.mqtt;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.MqttPersistenceException;
import java.io.UnsupportedEncodingException;
/**
* @author
*/
public class PublishThread implements Runnable {
@Override
public void run()
{
String[] obj = null;
System.out.println("mqtt publish thread start");
while (true)
{
obj = MqttClientUtil.poll();
if (obj != null) {
String topic = obj[0];
String content = obj[1];
System.out.println("mqtt从队列取出topic:"+topic+",content:"+content);
byte[] array = null;
try {
array = content.getBytes("utf-8");
}
catch (UnsupportedEncodingException e1) {
e1.printStackTrace();
}
MqttMessage message2 = new MqttMessage(array);
message2.setQos(1);
try
{
MqttClientUtil.mqttPublishClient.publish(topic, message2);
System.out.println("发送mqtt消息,topic: "+topic+" ,content: "+content);
} catch (MqttPersistenceException e) {
System.out.println("发消息给设备,topic:"+topic+",content:"+content);
e.printStackTrace();
} catch (MqttException e) {
System.out.println("发消息给设备,topic:"+topic+",content:"+content);
e.printStackTrace();
} catch (Exception e) {
System.out.println("发消息给设备,topic:"+topic+",content:"+content);
e.printStackTrace();
}
}
else{
try {
Thread.sleep(1000L);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
HandlerThread
package frame.mqtt;
import org.json.JSONObject;
/**
* @author
*/
public class HandlerThread implements Runnable {
private String topic;
private String content;
public HandlerThread(String topic, String content) {
this.topic = topic;
this.content = content;
}
@Override
public void run() {
if (this.content.length() > 0) {
//注: 下面逻辑处理,仅仅是我提供的简单案例,需要你们根据自己实际项目框架处理
//最好是用工厂模式创建每个接口对应的处理对象,或调用每个接口对应的处理方法.
try {
JSONObject json = new JSONObject(content);
String cmd_id = json.optString("cmd");
if(topic.startsWith(MqttClientUtil.TOPIC_PREFIX_DEVICE)){
//来自device的消息
//...
}else if(topic.startsWith(MqttClientUtil.TOPIC_PREFIX_APP)){
//来自app的消息
if("/test".equals(cmd_id)){
//从top 获取app 用户的 id
String id = topic.substring( topic.indexOf("app/") + 4 , topic.lastIndexOf("/client") );
//获取 发送给 指定id 的app用户的topic
String topic = MqttClientUtil.getAppPublishTopic(id);
//要发送消息的内容
JSONObject content = new JSONObject();
content.put("cmd",cmd_id+"/response");
content.put("ret","0");
content.put("msg","测试消息已经处成功处理");
//发布消息
MqttClientUtil.publish_common(topic,content.toString());
}
}
} catch (Throwable e) {
System.out.println("mqtt HandlerThread run");
e.printStackTrace();
}
}
}
}
All
package main;
import frame.mqtt.MqttClientUtil;
/**
* @author zx
*/
public class All {
public static void main(String[] args){
//mqtt服务启动
MqttClientUtil.createClient();
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
//释放工作
//...
} catch (Throwable e) {
e.printStackTrace();
}
}
});
}
}
注: mqtt服务器ip地址 ,屏蔽了,更换你们自己的mqtt服务器ip .
1 启动应用服务器程序
2 打开mqtt图形客户端(如果还没有mqtt图形客户端,看下这篇博文https://blog.csdn.net/a704397849/article/details/88533875
3 mqtt图形客户端,连接mqtt服务器,假设这个app客户端用户id 是 110 ,那么这个app客户端需要订阅
app/110/server 用来接收服务器发布给自己的消息。
4 mqtt图形客户端,发布消息: 主题 app/110/client ,内容 {“cmd”:"/test"}
5 可以看到结果是应用服务器收到了 app客户端发布的消息 并且处理 返回(发布) 消息 {“ret”:“0”,“msg”:“测试消息已经处成功处理”,“cmd”:"/test/response"}
链接:https://pan.baidu.com/s/1mUWMaMXSm4vTumQziOGIlA
提取码:yvrv
连接mqtt的ip和账号密码,我屏蔽了,填写你们自己的mqtt信息,如果还没有mqtt服务器,那么看下我之前mqtt相关文章 。
1 app、设备 客户端连接mqtt服务器的clientId 不要给随机的,这样的话用不了mqtt的离线消息。(应用服务器不打算保存离线消息,所以上面应用服务器代码中clientId是不固定的,是启动程序时当前时间搓。)
我们app客户端clientId 是用户id + mac 地址 ,连接mqtt服务器时 不要设置清除session,这样的话客户端就可以收到离线消息了。当然clientId这样创建,如果app用户下线期间有离线消息,然后在别的手机登录会收不到这期间的离线消息。
注: 上面提到的用户id 是app客户端用http请求应用服务器 登录接口获取的id
2 我现在用到mqtt的项目中, app 客户端 有2个登录 首先要 http登录获取用户id和token ,然后再连接mqtt服务器,发布一个mqtt 登录请求给服务器。
问题来了!为什么要有2个登录?首先http登录是为了获取后续http请求的令牌token ,那么为什么还要mqtt登录呢?mqtt登录是要让应用服务器知道app客户端与mqtt建立了长连接。通过mqtt, 应用服务器可以推消息给指定的客户端。
设备客户端 不需要token验证,所以只有一个mqtt登录。
3 app、设备 客户端连接mqtt时,记得设置遗愿,例如上述 app客户端id是110的用户 应该设置遗愿,主题 app/110/client ,内容是下线信息,如 {“cmd”:"/user/logout"}
本文只是介绍了 java应用服务器 mqtt框架设计及源码 ,没有使用ssl/tls安全协议。如果需要mqtt服务器的安装及安全连接 可以看看我之前关于mqtt的文章.
mqtt服务器搭建以及mqtt客户端图形调试测试工具安装使用:https://blog.csdn.net/a704397849/article/details/88533875
emqtt安全连接 单向认证连接 加密 ssl/tls mosquitto客户端ssl单向认证连接测试
https://blog.csdn.net/a704397849/article/details/88885198
如果本文对你有所帮助,点赞关注支持一波,谢谢(〃‘▽’〃)