簡介
MQTT(消息隊列遙測傳輸)是ISO 標準(ISO/IEC PRF 20922)下基於發佈/訂閱範式的消息協議。
它工作在TCP/IP協議族上,是爲硬件性能低下的遠程設備以及網絡狀況糟糕的情況下而設計的發佈/訂閱型消息協議,爲此,它需要一個消息中間件。
實現方式
實現MQTT協議需要客戶端和服務器端通訊完成。
在通訊過程中,MQTT協議中有三種身份:發佈者(Publish)、代理(Broker)(服務器)、訂閱者(Subscribe)。
其中,消息的發佈者和訂閱者都是客戶端,消息代理是服務器,消息發佈者可以同時是訂閱者。
MQTT傳輸的消息分爲:主題(Topic)和負載(payload)兩部分:
(1)Topic,可以理解爲消息的類型,訂閱者訂閱(Subscribe)後,就會收到該主題的消息內容(payload);
(2)payload,可以理解爲消息的內容,是指訂閱者具體要使用的內容。
訂閱(Subscription)
訂閱包含主題篩選器(Topic Filter)和最大服務質量(QoS)。訂閱會與一個會話(Session)關聯。一個會話可以包含多個訂閱。每一個會話中的每個訂閱都有一個不同的主題篩選器。
消息發佈質量(Qos):
qos=0:“至多一次”,這一級別會發生消息丟失或重複,消息發佈依賴於TCP/IP網絡
qos=1:“至少一次”,確保消息到達,但消息重複可能會發生
qos=2:“只有一次”,確保消息到達一次
訂閱普通主題。例如:訂閱 hello 主題。
訂閱通配符主題。例如:訂閱通配符主題 testtopic/#,並給 testtopic/1 主題發送消息,會接收到該消息。
會話(Session)
每個客戶端與服務器建立連接後就是一個會話,客戶端和服務器之間有狀態交互。會話存在於一個網絡之間,也可能在客戶端和服務器之間跨越多個連續的網絡連接。
主題名(Topic Name)
連接到一個應用程序消息的標籤,該標籤與服務器的訂閱相匹配。服務器會將消息發送給訂閱所匹配標籤的每個客戶端。
主題篩選器(Topic Filter)
一個對主題名通配符篩選器,在訂閱表達式中使用,表示訂閱所匹配到的多個主題。
- 所有的主題名和主題過濾器必須至少包含一個字符
- 主題名或主題過濾器以前置或後置斜槓 “/” 區分
- 只包含斜槓 “/” 的主題名或主題過濾器是合法的
- 主題名和主題過濾器是 UTF-8 編碼字符串, 它們不能超過 65535 字節
- 主題名和主題過濾器是區分大小寫的
參考:https://blog.csdn.net/amwha/article/details/74364175
負載(Payload)
消息訂閱者所具體接收的內容。
包含CONNECT、SUBSCRIBE、SUBACK、UNSUBSCRIBE四種類型的消息:
(1)CONNECT,消息體內容主要是:客戶端的ClientID、訂閱的Topic、Message以及用戶名和密碼。
(2)SUBSCRIBE,消息體內容是一系列的要訂閱的主題以及QoS。
(3)SUBACK,消息體內容是服務器對於SUBSCRIBE所申請的主題及QoS進行確認和回覆。
(4)UNSUBSCRIBE,消息體內容是要訂閱的主題。
服務器(Broker)
也稱爲 MQTT 消息服務器,它可以是運行了 MQTT 消息服務器軟件的一臺服務器或一個服務器集羣。MQTT Broker 負責接收來自客戶端的網絡連接,並處理客戶端的訂閱/取消訂閱(Subscribe/Unsubscribe)、消息發佈(Publish)請求,同時也會將客戶端發佈的消息轉發給其他訂閱者。
服務器選型
1、基本需求
1)支持 mqtt3.1 / mqtt3.1.1協議(可選 mqtt5.0)
3.1和3.1.1是最常見的協議版本,幾乎目前生產的IoT設備都支持,所以Broker也必須支持。至於5.0版本,目前各大Broker都在努力支持,不過還需要一些時間纔會普及。
2)支持QoS0、QoS1(可選QoS2)
各大廠商都至少支持了QoS1,保證消息到達。一般的場景下不會用到QoS2,所以可以選擇性地考慮支持QoS2
3)支持遺囑消息
這是必須支持的功能,通常設備斷開都不是主動斷開的,而是沒有電了才斷開,屬於異常斷開,需要設置遺囑消息來通知後端服務或者其他設備進行後續處理。
4)支持持久化
一些數據如QoS1消息、持久Session,需要支持持久化,這是MQTT協議規定的。
5)支持多種連接方式
MQTT over TCP:最基礎的連接方式
MQTT over Websocket:在Websocket之上做MQTT封裝,對APP這種客戶端來說很友好
MQTT over TCP/SSL:基礎連接方式做通信加密,通常SSL採用TLS
MQTT over Websocket/SSL:Websocket做通信加密,通常SSL採用TLS
6)保留消息(可選)
保留消息的利用場景幾乎可以忽略,而帶來的查詢成本會很高(每次訂閱主題都要查一遍有沒有保留消息,再加上通配符匹配,時延很高),所以不一定需要支持,具體應用具體分析。
7)支持集羣
Broker要支持保持海量MQTT連接,需要做集羣。集羣的難點在於Session的持久化和集羣通信。
8)支持自定義驗證方式
驗證客戶端的合法性有三點:CONNECT階段驗證是否允許連接、PUBLISH階段驗證是否允許發佈、SUBSCRIBE階段驗證是否允許訂閱。
CONNECT階段需要驗證ClientID、Username、Password、IP四項,不過大部分開源Broker都只支持Username和Password的驗證。
PUBLISH、SUBSCRIBE的驗證的目的是防止非法客戶端訂閱別人的主題,向別人的主題發佈消息。但每臺設備每次訂閱、發佈都要驗證一次頻率巨高,所以需要設計Cache和高效查詢機制。
2、高級功能:支持共享訂閱
共享訂閱的具體含義是,多個客戶端訂閱同一個主題,消息只會被分發給其中的一個客戶端。
共享訂閱主要針對的是需要客戶端負載均衡的場景,比如後端服務多個Worker,需要共享訂閱來只讓一個Worker得到數據。但仔細地想一想,後端服務一定有大量消息扇入,在Broker端用共享訂閱可能會導致內存爆炸,還不如直接發到Kafka,利用Kafka的負載均衡來做。不過現在的Broker都在逐漸支持共享訂閱,所以也是一個趨勢吧。
參考:https://www.jianshu.com/p/cf91f4bea071
MQTT客戶端
Java客戶端
org.eclipse.paho.client.mqttv3.jar 下載地址
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.2</version>
</dependency>
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
/**
* 創建一個MQTT客戶端
*
*/
@Slf4j
public class MqttPushClient {
public static String MQTT_HOST = "";
public static String MQTT_CLIENTID = "";
public static String MQTT_USERNAME = "";
public static String MQTT_PASSWORD = "";
public static int MQTT_TIMEOUT = 10;
public static int MQTT_KEEPALIVE = 10;
private MqttClient client;
private static volatile MqttPushClient mqttClient = null;
public static MqttPushClient getInstance() {
if (mqttClient == null) {
synchronized (MqttPushClient.class) {
if (mqttClient == null) {
mqttClient = new MqttPushClient();
}
}
}
return mqttClient;
}
private MqttPushClient() {
log.info("Connect MQTT: " + this);
connect();
}
/**
* 創建連接
*/
private void connect() {
try {
client = new MqttClient(MQTT_HOST, MQTT_CLIENTID, new MemoryPersistence());
MqttConnectOptions option = new MqttConnectOptions();
option.setCleanSession(true);//設置是否清空session,false表示服務器會保留客戶端的連接記錄,true表示每次連接到服務器都以新的身份連接
option.setUserName(MQTT_USERNAME);//設置連接的用戶名
option.setPassword(MQTT_PASSWORD.toCharArray());//設置連接的密碼
option.setConnectionTimeout(MQTT_TIMEOUT);// 設置超時時間
option.setKeepAliveInterval(MQTT_KEEPALIVE);// 設置會話心跳時間
option.setAutomaticReconnect(true);// 自動重連
try {
client.setCallback(new MqttPushCallback());
client.connect(option);
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 發佈主題,用於通知<br>
* 默認qos爲1 非持久化
*
* @param topic
* @param data
*/
public void publish(String topic, String data) {
publish(topic, data, 1, false);
}
/**
* 發佈
*
* @param topic
* @param data
* @param qos
* @param retained
*/
public void publish(String topic, String data, int qos, boolean retained) {
MqttMessage message = new MqttMessage();
message.setQos(qos);//設置qos,決定消息到達次數。
message.setRetained(retained);// 服務器是否保存消息
message.setPayload(data.getBytes());//設置消息內容
MqttTopic mqttTopic = client.getTopic(topic);// 設置消息主題
if (null == mqttTopic) {
log.error("Topic Not Exist");
}
MqttDeliveryToken token;
try {
token = mqttTopic.publish(message);
token.waitForCompletion();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 訂閱某個主題 qos默認爲1
*
* @param topic
*/
public void subscribe(String topic) {
subscribe(topic, 1);
}
/**
* 訂閱某個主題
*
* @param topic
* @param qos
*/
public void subscribe(String topic, int qos) {
try {
client.subscribe(topic, qos);
} catch (Exception e) {
e.printStackTrace();
}
}
}
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttMessage;
/**
* MQTT 推送回調
*/
public class MqttPushCallback implements MqttCallback {
private static final Logger log = LoggerFactory.getLogger(MqttPushCallback.class);
//連接斷開時的回調
@Override
public void connectionLost(Throwable cause) {
log.info("斷開連接,建議重連" + this);
//斷開連接,建議重連
}
//消息發送成功時的回調
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
//log.info(token.isComplete() + "");
}
//收到下推消息時的回調
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
log.info("Topic: " + topic);
log.info("Message: " + new String(message.getPayload()));
}
}
參考:https://www.cnblogs.com/shidian/p/11778270.html
https://www.cnblogs.com/wunaozai/p/11147841.html
整合springboot
pom.xml
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mqtt</artifactId>
</dependency>
配置文件
#MQTT配置信息
#MQTT-用戶名
spring.mqtt.username=xxxx
#MQTT-密碼
spring.mqtt.password=xxxx
#MQTT-服務器連接地址
spring.mqtt.url=tcp://xxx.xxx.xxx:18831
#MQTT-連接服務器默認客戶端ID
spring.mqtt.client.id=xxxxxx
#MQTT-默認的消息推送主題,實際可在調用接口時指定
spring.mqtt.default.topic=/public/TEST/#
#連接超時
spring.mqtt.completionTimeout=3000
接收配置類
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.IntegrationComponentScan;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import java.text.SimpleDateFormat;
//接收配置類
@Configuration
@IntegrationComponentScan
@Slf4j
public class MqttReceiveConfig {
@Value("${spring.mqtt.username}")
private String username;
@Value("${spring.mqtt.password}")
private String password;
@Value("${spring.mqtt.url}")
private String hostUrl;
@Value("${spring.mqtt.client.id}")
private String clientId;
@Value("${spring.mqtt.default.topic}")
private String defaultTopic;
@Value("${spring.mqtt.completionTimeout}")
private int completionTimeout; //連接超時
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz");
@Bean
public MqttConnectOptions getMqttConnectOptions() {
// MQTT的連接設置
MqttConnectOptions mqttConnectOptions = new MqttConnectOptions();
// 設置連接的用戶名
mqttConnectOptions.setUserName(username);
// 設置連接的密碼
mqttConnectOptions.setPassword(password.toCharArray());
// 設置是否清空session,這裏如果設置爲false表示服務器會保留客戶端的連接記錄,
// 把配置裏的 cleanSession 設爲false,客戶端掉線後 服務器端不會清除session,
// 當重連後可以接收之前訂閱主題的消息。當客戶端上線後會接受到它離線的這段時間的消息
mqttConnectOptions.setCleanSession(false);
// 設置發佈端地址
mqttConnectOptions.setServerURIs(new String[]{hostUrl});
// 設置會話心跳時間 單位爲秒 服務器會每隔1.5*20秒的時間向客戶端發送個消息判斷客戶端是否在線,但這個方法並沒有重連的機制
mqttConnectOptions.setKeepAliveInterval(20);
return mqttConnectOptions;
}
@Bean
public MqttPahoClientFactory mqttClientFactory() {
// 客戶端工廠類,根據配置的選項(用戶名、密碼、服務器集羣地址等)創建一個默認的客戶端。
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
factory.setConnectionOptions(getMqttConnectOptions());
return factory;
}
//接收通道
@Bean
public MessageChannel mqttInputChannel() {
return new DirectChannel();
}
//配置client,監聽的topic
@Bean
public MessageProducer inbound() {
MqttPahoMessageDrivenChannelAdapter adapter =
new MqttPahoMessageDrivenChannelAdapter(clientId + "_inbound",
mqttClientFactory(), defaultTopic
// "elevators/cms/#"//加上這個爲了匹配node red的topic
);
adapter.setCompletionTimeout(completionTimeout);
adapter.setConverter(new DefaultPahoMessageConverter());
adapter.setQos(1);
adapter.setOutputChannel(mqttInputChannel());
return adapter;
}
//通過通道獲取數據
@Bean
@ServiceActivator(inputChannel = "mqttInputChannel")
public MessageHandler handler() {
return new MessageHandler() {
@Override
public void handleMessage(Message<?> message) throws MessagingException {
String topic = message.getHeaders().get("mqtt_receivedTopic").toString();
System.out.println("Topic: " + topic);
System.out.println("Message: " + topic);
}
};
}
}
消息推送配置類
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.IntegrationComponentScan;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
//MQTT消息推送配置類
@Configuration
@IntegrationComponentScan
public class MqttSenderConfig {
@Value("${spring.mqtt.username}")
private String username;
@Value("${spring.mqtt.password}")
private String password;
@Value("${spring.mqtt.url}")
private String hostUrl;
@Value("${spring.mqtt.client.id}")
private String clientId;
@Value("${spring.mqtt.default.topic}")
private String defaultTopic;
@Bean
public MqttConnectOptions getMqttConnectOptions() {
MqttConnectOptions mqttConnectOptions = new MqttConnectOptions();
mqttConnectOptions.setUserName(username);
mqttConnectOptions.setPassword(password.toCharArray());
mqttConnectOptions.setServerURIs(new String[]{hostUrl});
mqttConnectOptions.setKeepAliveInterval(2);
return mqttConnectOptions;
}
@Bean
public MqttPahoClientFactory mqttClientFactory() {
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
factory.setConnectionOptions(getMqttConnectOptions());
return factory;
}
@Bean
@ServiceActivator(inputChannel = "mqttOutboundChannel")
public MessageHandler mqttOutbound() {
// MQTT出站通道適配器的抽象類的實現,用於推送消息。
MqttPahoMessageHandler messageHandler = new MqttPahoMessageHandler(clientId, mqttClientFactory());
messageHandler.setAsync(true);
messageHandler.setDefaultTopic(defaultTopic);
return messageHandler;
}
@Bean
public MessageChannel mqttOutboundChannel() {
return new DirectChannel();
}
}
消息推送接口類
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.handler.annotation.Header;
//配置MqttGateway消息推送接口類
@MessagingGateway(defaultRequestChannel = "mqttOutboundChannel")
public interface MqttGateway {
void sendToMqtt(String data, @Header(MqttHeaders.TOPIC) String topic);
}
@Autowired
private MqttGateway mqttGateway;
mqttGateway.sendToMqtt("121212121210099", "/public/TEST/windows");
異常問題
mqtt客戶端頻繁中斷:Lost connection: 已斷開連接; retry
問題原因:
原因1:造成這種情況的原因是ClientId相同。2個服務端使用相同的ClientId連接mqtt服務器。
解決方案:讓消費端的ClientId爲隨機字符串。這樣ClientId就不會重複。
原因2:在回調函數內進行業務處理遇到異常並沒有捕獲
解決方案:在可能出現異常的語句塊,進行try-catch捕獲
MQTT 客戶端工具
Mosquito CLI
MQTTX
MQTT.fx
MQTT Box
mqtt-spy
MQTT Lens