什麼是MQTT協議
本博客源碼:https://github.com/xushuoAI/broker-moqutte-springboot
https://github.com/mcxiaoke/mqtt
這是一位大佬的中文翻譯,大家可以看看。
在SpringBoot中啓動moquette.broker.Server
https://segmentfault.com/a/1190000016456748
這是一位大佬的整合
客戶端連接到broker並使用
通過上面的操作,我們可以在啓動一個SpringBoot項目的同時啓動好moqutte.broker服務。現在我們只需要創建客戶端連接到broker服務就可以實現客戶端之間的通信了。下面這段代碼,大家肯定很熟悉了,網上基本都是這個版本,通過指定服務器的IP地址和端口,我們創建了一個客戶端連接到我們啓動的Broker服務。然後這裏使用的連接方式是回調式鏈接。
1.採用阻塞式的連接的(BlockingConnection)
2.採用回調式的連接 (CallbackConnection)
3.採用Future樣式的連接(FutureConnection)
https://blog.csdn.net/woaijianning/article/details/17417409參考文章
public class MQTTServer {
private static final Logger LOG = LoggerFactory.getLogger(MQTTServer.class);
private final static String CONNECTION_STRING = "tcp://0.0.0.0:1883";
private final static boolean CLEAN_START = true;
private final static short KEEP_ALIVE = 30;// 低耗網絡,但是又需要及時獲取數據,心跳30s
public static Topic[] topics = {
new Topic("china/beijing", QoS.EXACTLY_ONCE),
new Topic("china/tianjin", QoS.AT_LEAST_ONCE),
new Topic("china/henan", QoS.AT_MOST_ONCE)};
public final static long RECONNECTION_ATTEMPT_MAX=6;
public final static long RECONNECTION_DELAY=2000;
public final static int SEND_BUFFER_SIZE=2*1024*1024;//發送最大緩衝爲2M
public static void main(String arg[]) {
MQTT mqtt = new MQTT();
try {
//設置服務端的ip
mqtt.setHost(CONNECTION_STRING);
//連接前清空會話信息
mqtt.setCleanSession(CLEAN_START);
//設置重新連接的次數
mqtt.setReconnectAttemptsMax(RECONNECTION_ATTEMPT_MAX);
//設置重連的間隔時間
mqtt.setReconnectDelay(RECONNECTION_DELAY);
//設置心跳時間
mqtt.setKeepAlive(KEEP_ALIVE);
//設置緩衝的大小
mqtt.setSendBufferSize(SEND_BUFFER_SIZE);
/* //創建連接
BlockingConnection connection = mqtt.blockingConnection();
//開始連接
connection.connect();*/
// 使用回調式API
final CallbackConnection callbackConnection = mqtt
.callbackConnection();
// 連接監聽
callbackConnection.listener(new Listener() {
// 接收訂閱話題發佈的消息
@Override
public void onPublish(UTF8Buffer topic, Buffer payload,
Runnable onComplete) {
System.out.println("=============receive msg================" + new String(payload.toByteArray()));
onComplete.run();
}
// 連接失敗
@Override
public void onFailure(Throwable value) {
System.out.println("===========connect failure===========");
callbackConnection.disconnect(null);
}
// 連接斷開
@Override
public void onDisconnected() {
System.out.println("====mqtt disconnected=====");
}
// 連接成功
@Override
public void onConnected() {
System.out.println("====mqtt connected=====");
}
});
// 連接
callbackConnection.connect(new Callback<Void>() {
// 連接失敗
public void onFailure(Throwable value) {
System.out.println("============連接失敗:" + value.getLocalizedMessage() + "============");
}
// 連接成功
public void onSuccess(Void v) {
// 訂閱主題
String topic="msg/normal";
String topic1="msg/disconnect";
Topic[] topics = { new Topic(topic, QoS.AT_LEAST_ONCE), new Topic(topic1, QoS.AT_LEAST_ONCE) };
callbackConnection.subscribe(topics, new Callback<byte[]>() {
// 訂閱主題成功
public void onSuccess(byte[] qoses) {
System.out.println("========訂閱成功=======");
}
// 訂閱主題失敗
public void onFailure(Throwable value) {
System.out.println("========訂閱失敗=======");
callbackConnection.disconnect(null);
}
});
// 發佈消息
callbackConnection.publish(topic, ("Hello,我是服務端").getBytes(), QoS.AT_LEAST_ONCE, true, new Callback<Void>() {
public void onSuccess(Void v) {
LOG.info("===========消息發佈成功============");
}
public void onFailure(Throwable value) {
LOG.info("========消息發佈失敗=======");
callbackConnection.disconnect(null);
}
});
}
});
try {
int count=0;
//getClient();
while(true){
/* count++;
//訂閱的主題
String topic="china/tianjin";
//主題的內容
String message="hello "+count+"chinese people !";
//connection.publish(topic, message.getBytes(), QoS.AT_LEAST_ONCE, false);
LOG.info("MQTTServer Message Topic="+topic+" Content :"+message);
Thread.sleep(2000);*/
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (URISyntaxException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
通過上面的代碼,我們就可以簡單的創建和實現客戶端,通過向主題發送消息,其他訂閱了該主題的客戶端就可以收到對應的消息。
客戶端在線狀態
但是在使用過程中,我們肯定是想要知道客戶端的在線情況,簡單的發送消息已經實現了,那我們就來看看如何知道客戶端在線情況吧!
- 客戶端如何知道自己是否連接上服務端?
在上面的代碼中我們使用了回調式連接,在該連接方式中
connection.listener(new Listener() {
public void onDisconnected() {
// Send a message to a topic
LOG.info("----------連接掉線-------------");
}
public void onConnected() {
LOG.info("----------連接上-------------");
}
public void onPublish(UTF8Buffer topic, Buffer payload, Runnable ack) {
// You can now process a received message from a topic.
// Once process execute the ack runnable.
System.out.println("=============receive msg================" + new String(payload.toByteArray()));
ack.run();
}
public void onFailure(Throwable value) {
LOG.info("----------a connection failure occured.-------------");
connection.disconnect(null); // a connection failure occured.
}
});
在該Listener中可以監聽到客戶端是否連接上了服務端。
- 服務端如何知道有客戶端掉線?
mqttServer.startServer(config, interceptHandlers, null, iAuthenticatorImp, authorizatorPolicy);
在剛纔,我們在Springboot中啓動了服務端,在start方法中我們傳遞了幾個參數,其中interceptHandlers就可以實現對客戶端的監聽。
package com.hyls.sb.mqtt;
import groovy.util.logging.Slf4j;
import io.moquette.interception.AbstractInterceptHandler;
import io.moquette.interception.messages.InterceptAcknowledgedMessage;
import io.moquette.interception.messages.InterceptConnectMessage;
import io.moquette.interception.messages.InterceptConnectionLostMessage;
import io.moquette.interception.messages.InterceptPublishMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Slf4j
@Component("safetyInterceptHandler")
public class SafetyInterceptHandler extends AbstractInterceptHandler {
private static final Logger LOG = LoggerFactory.getLogger(MoquetteServer.class);
@Override
public String getID() {
return SafetyInterceptHandler.class.getName();
}
@Override
public void onConnect(InterceptConnectMessage msg) {
super.onConnect(msg);
LOG.info("==啥玩意連接上了====");
}
@Override
public void onConnectionLost(InterceptConnectionLostMessage msg) {
super.onConnectionLost(msg);
LOG.info("==哦豁,有人掉了ID:===="+msg.getClientID());
LOG.info("==哦豁,有人掉了Name:===="+msg.getUsername());
}
@Override
public void onPublish(InterceptPublishMessage msg) {
super.onPublish(msg);
}
@Override
public void onMessageAcknowledged(InterceptAcknowledgedMessage msg) {
super.onMessageAcknowledged(msg);
}
}
我們自定義一個類,實現對應的監聽方法。就可以知道有客戶端掉線了。那是如何判斷掉線呢。剛纔我們新建客戶端的時候設置了心跳這一值,該值就是服務端判斷客戶端有沒有掉線的依據,超過心跳時間沒有收到客戶端發送的數據,服務端就認爲客戶端已掉線。(這裏的代碼,在上面的那個鏈接中。)
知道有客戶端掉了,是誰掉了?
在實際應用中,我們知道有客戶端掉了,但是隻有一個clientId,這個ID不是我們設置的,知道這個ID我們也不知道是那一個客戶端掉了。
package io.moquette.interception.messages;
public class InterceptConnectionLostMessage implements InterceptMessage {
private final String clientID;
private final String username;
public InterceptConnectionLostMessage(String clientID, String username) {
this.clientID = clientID;
this.username = username;
}
public String getClientID() {
return this.clientID;
}
public String getUsername() {
return this.username;
}
}
查看moquette.messages源碼,如上,我們發現有一個userName屬性,誒,我們就可以通過設置這個userName就知道是誰掉了澀。
在上面新建客戶端的時候我們
mqtt.setHost(CONNECTION_STRING);
//連接前清空會話信息
mqtt.setCleanSession(CLEAN_START);
//設置重新連接的次數
mqtt.setReconnectAttemptsMax(RECONNECTION_ATTEMPT_MAX);
//設置重連的間隔時間
mqtt.setReconnectDelay(RECONNECTION_DELAY);
//設置心跳時間
mqtt.setKeepAlive(KEEP_ALIVE);
set了一堆東西,那有沒有setName方法呢。
哎呀,果然有該方法
mqtt.setUserName("王德發");
這裏我們新增設置一個name,叫王德發(嘿嘿 滑稽)。
這下我們就知道是誰掉了。趕緊試試
就在我們翹着二郎腿,美滋滋的時候一運行
LOG.error("Authenticator has rejected the MQTT credentials CId={}, username={}", clientId, login);
控制檯就出現這個玩意,一看出現了權限問題。哦豁,咋搞
原來我們設置了運行匿名訪問服務端,所有之前設置的客戶端可以匿名訪問,現在我們設置了一個username,Mqtt就走了鑑權,但是我們沒有設置對應的鑑權規則。查看鑑權源碼
private boolean login(MqttConnectMessage msg, String clientId) {
if (msg.variableHeader().hasUserName()) {
byte[] pwd = null;
if (msg.variableHeader().hasPassword()) {
pwd = msg.payload().password().getBytes(StandardCharsets.UTF_8);
} else if (!this.brokerConfig.isAllowAnonymous()) {
LOG.error("Client didn't supply any password and MQTT anonymous mode is disabled CId={}", clientId);
return false;
}
String login = msg.payload().userName();
if (!this.authenticator.checkValid(clientId, login, pwd)) {
LOG.error("Authenticator has rejected the MQTT credentials CId={}, username={}", clientId, login);
return false;
}
NettyUtils.userName(this.channel, login);
} else if (!this.brokerConfig.isAllowAnonymous()) {
LOG.error("Client didn't supply any credentials and MQTT anonymous mode is disabled. CId={}", clientId);
return false;
}
return true;
}
這裏進行check
public interface IAuthenticator {
boolean checkValid(String var1, String var2, byte[] var3);
}
哎呀,那我們如何去自定義鑑權規則呢
回到啓動Broker的這個方法,發現參數中有一個 IAuthenticator authenticator
mqttServer.startServer(config, interceptHandlers, null, iAuthenticatorImp, authorizatorPolicy);
喔,原來這裏就是我們可以自定義傳入鑑權規則
import io.moquette.broker.security.IAuthenticator;
import org.springframework.stereotype.Component;
@Component
public class IAuthenticatorImp implements IAuthenticator {
@Override
public boolean checkValid(String s, String s1, byte[] bytes) {
return true;
}
}
我們實現鑑權接口,然後把自定義實現類作爲參數傳遞到啓動方法中。
果然現在由於我們直接返回了true,我們的客戶端設置了用戶名可以直接連接上,這樣有客戶端掉線。我們就可以通過自定義的userName知道是誰掉線了。
博主也是新手,有誤希望大家指點~