1。概要
従来のメッセージキュー MQ は、主に電子商取引分野におけるトランザクションメッセージ、支払いメッセージ、物流メッセージなどのサービス (エンド) 間のメッセージ通信に使用されます。ただし、メッセージの一般的なカテゴリの下には、IoT 端末デバイスのメッセージという、別の非常に重要で共通のメッセージ フィールドがあります。近年、スマートホームや産業相互接続に起因するIoTデバイス指向のニュースが爆発的に増加していますが、10年以上開発されてきたモバイルインターネットのモバイルAPP側のニュースは依然として注目を集めています。規模が大きい。端末デバイスのメッセージの規模は、従来のサーバーのメッセージよりも桁違いに大きく、依然として急速に増加しています。
マルチシナリオ コンピューティング (ストリーム、イベントなど) とマルチシナリオ (IoT、APP) アクセスを提供するユニファイド メッセージ システム (製品) があれば、メッセージも重要なデータであるため、実際には非常に価値があります。 1 つのシステムでストレージ コストを最小限に抑え、異なるシステム間のデータ同期によって生じる一貫性の問題や課題を効果的に回避できます。
これに基づいて、IoT デバイスとサーバーのメッセージへの RocketMQ の統合アクセスを実現し、統合されたメッセージ ストレージと相互通信機能を提供する RocketMQ-MQTT 拡張プロジェクトを導入しました。
MQTTプロトコル
IoT 端末のシナリオでは、MQTT プロトコルが現在業界で広く使用されています。これは、モノのインターネットの IoT シナリオに由来し、OASIS Alliance によって定義された標準のオープン プロトコルです。IoT デバイスには多くの種類があり、動作環境も異なるため、標準のアクセス プロトコルが特に重要です。
MQTT プロトコルは、RocketMQ に似た Pub/Sub 通信モデルを定義しますが、サブスクリプションの方法がより柔軟で、マルチレベルのトピック サブスクリプション (「/t/t1/t2」など) をサポートできます。ワイルドカード サブスクリプション (「/t/t1/+」など) もサポートできます。
モデル紹介
キューストレージモデル
多次元分散用のトピック キュー モデルを設計しました。上図に示すように、メッセージはさまざまなアクセス シナリオ (サーバー側の MQ/AMQP、クライアント側の MQTT など) から送信されますが、メッセージは 1 つのコピーだけです。コミットログに書き込まれて保存され、複数の需要シナリオのキュー インデックス (ConsumerQueue) を配布します。たとえば、サーバー側シナリオ (MQ/AMQP) は、第 1 レベルのトピック キューに従って従来のサーバー側の消費を実行できます。 、クライアント側の MQTT シナリオは、MQTT マルチレベル トピックおよびワイルドカード サブスクリプション情報に従って消費できます。
このようなキュー モデルは、サーバーと端末のアクセスとメッセージの送受信のシナリオを同時にサポートし、統合の目標を達成します。
プッシュプルモデル
上図はプッシュプルモデルを示しており、図中のPノードはプロトコルゲートウェイまたはブローカープラグインであり、端末装置はMQTTプロトコルを介してゲートウェイノードに接続されます。メッセージはさまざまなシナリオ (MQ/AMQP/MQTT) から送信できます。トピック キューに保存された後、新しいメッセージの到着をリアルタイムで感知する通知ロジック モジュールがあり、メッセージ イベント (このイベントはゲートウェイ ノードにプッシュされ、ゲートウェイ ノードは接続されている端末デバイスのサブスクリプション ステータスに従って内部マッチングを実行し、どの端末デバイスがマッチングできるかを見つけ、次に、ストレージ層へのプル リクエストをトリガーして、メッセージを読み取り、端末デバイスにプッシュします。
アーキテクチャの概要
私たちの目標は、RocketMQ に基づいた統合された自己閉ループを実現することですが、ブローカーがこれ以上のシナリオ ロジックに侵入されることは望ましくなく、ゲートウェイまたはブローカー プラグインになるプロトコル コンピューティング層を抽象化します。Broker は、キューの問題を解決し、上記のコンピューティング ニーズを満たすためにキュー ストレージの適応または変換を行うことに重点を置いています。プロトコル コンピューティング層はプロトコル アクセスを担当し、プラグ可能で展開できる必要があります。
2. 開発例
システム要件
- 64 ビット オペレーティング システム、Linux/Unix/macOS を推奨
- 64 ビット JDK 1.8 以降
導入手順
RocketMQ-MQTT プロジェクトは RocketMQ の基礎となるマルチキュー ディストリビューションに依存しているため、RocketMQ はバージョン 4.9.3 からこの機能をサポートするため、RocketMQ のバージョンが 4.9.3 以降にアップグレードされていることを確認し、次の設定項目が有効になります。
enableLmq = true
enableMultiDispatch = true
RocketMQ-MQTT のデプロイメントについては、プロジェクトの説明を参照し、プロジェクトのリリース バージョンをダウンロードするか、ソース コードから直接ビルドします。
git clone https://github.com/apache/rocketmq-mqtt
cd rocketmq-mqtt
mvn -Prelease-all -DskipTests clean install -U
cd distribution/target/
cd bin
sh mqtt.sh start
設定手順
username=xxx // 权限验证账户配置
secretKey=xxx // 权限验证账户配置
NAMESRV_ADDR=xxx //namesrv接入点
eventNotifyRetryTopic=xx //notify重试topic,提前创建
clientRetryTopic=xx //客户端消息重试topic,提前创建
例
依存関係 pom.xml を導入します。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>rocketmq-mqtt</artifactId>
<groupId>org.apache.rocketmq</groupId>
<version>1.0.2-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mqtt-example</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>mqtt-common</artifactId>
</dependency>
</dependencies>
</project>
RocketMQプロデューサー
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.rocketmq.mqtt.example;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.MixAll;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageConst;
import org.apache.rocketmq.mqtt.common.util.TopicUtils;
import org.apache.rocketmq.remoting.exception.RemotingException;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
public class RocketMQProducer {
private static DefaultMQProducer producer;
private static String firstTopic = System.getenv("firstTopic");
private static String recvClientId = "recv01";
public static void main(String[] args) throws Exception {
//Instantiate with a producer group name.
producer = new DefaultMQProducer("PID_TEST");
// Specify name server addresses.
producer.setNamesrvAddr(System.getenv("namesrv"));
//Launch the instance.
producer.start();
for (int i = 0; i < 1000; i++) {
//Create a message instance, specifying topic, tag and message body.
//Call send message to deliver message to one of brokers.
try {
sendMessage(i);
Thread.sleep(1000);
sendWithWildcardMessage(i);
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
//Shut down once the producer instance is not longer in use.
producer.shutdown();
}
private static void setLmq(Message msg, Set<String> queues) {
msg.putUserProperty(MessageConst.PROPERTY_INNER_MULTI_DISPATCH,
StringUtils.join(
queues.stream().map(s -> StringUtils.replace(s, "/", "%")).map(s -> MixAll.LMQ_PREFIX + s).collect(Collectors.toSet()),
MixAll.MULTI_DISPATCH_QUEUE_SPLITTER));
}
private static void sendMessage(int i) throws MQBrokerException, RemotingException, InterruptedException, MQClientException {
Message msg = new Message(firstTopic,
"MQ2MQTT",
("MQ_" + System.currentTimeMillis() + "_" + i).getBytes(StandardCharsets.UTF_8));
String secondTopic = "/r1";
setLmq(msg, new HashSet<>(Arrays.asList(TopicUtils.wrapLmq(firstTopic, secondTopic))));
SendResult sendResult = producer.send(msg);
System.out.println(now() + "sendMessage: " + new String(msg.getBody()));
}
private static void sendWithWildcardMessage(int i) throws MQBrokerException, RemotingException, InterruptedException, MQClientException {
Message msg = new Message(firstTopic,
"MQ2MQTT",
("MQwc_" + System.currentTimeMillis() + "_" + i).getBytes(StandardCharsets.UTF_8));
String secondTopic = "/r/wc";
Set<String> lmqSet = new HashSet<>();
lmqSet.add(TopicUtils.wrapLmq(firstTopic, secondTopic));
lmqSet.addAll(mapWildCardLmq(firstTopic, secondTopic));
setLmq(msg, lmqSet);
SendResult sendResult = producer.send(msg);
System.out.println(now() + "sendWcMessage: " + new String(msg.getBody()));
}
private static Set<String> mapWildCardLmq(String firstTopic, String secondTopic) {
// todo by yourself
return new HashSet<>(Arrays.asList(TopicUtils.wrapLmq(firstTopic, "/r/+")));
}
private static String now() {
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
return sf.format(new Date()) + "\t";
}
}
RocketMQコンシューマ
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.rocketmq.mqtt.example;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.mqtt.common.model.Constants;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
public class RocketMQConsumer {
public static void main(String[] args) throws MQClientException {
// Instantiate with specified consumer group name.
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("GID_test01");
// Specify name server addresses.
consumer.setNamesrvAddr(System.getenv("namesrv"));
// Subscribe one more more topics to consume.
String firstTopic = System.getenv("firstTopic");
consumer.subscribe(firstTopic, Constants.MQTT_TAG);
// Register callback to execute on arrival of messages fetched from brokers.
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
MessageExt messageExt = msgs.get(0);
System.out.println(now() + "Receive: " + new String(messageExt.getBody()));
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//Launch the consumer instance.
consumer.start();
System.out.printf("Consumer Started.%n");
}
private static String now() {
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
return sf.format(new Date()) + "\t";
}
}
Mqttプロデューサー
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.rocketmq.mqtt.example;
import org.apache.rocketmq.mqtt.common.util.HmacSHA1Util;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MqttProducer {
public static void main(String[] args) throws InterruptedException, MqttException, NoSuchAlgorithmException, InvalidKeyException {
MemoryPersistence memoryPersistence = new MemoryPersistence();
String brokerUrl = "tcp://" + System.getenv("host") + ":1883";
String firstTopic = System.getenv("topic");
String sendClientId = "send01";
MqttConnectOptions mqttConnectOptions = buildMqttConnectOptions(sendClientId);
MqttClient mqttClient = new MqttClient(brokerUrl, sendClientId, memoryPersistence);
mqttClient.setTimeToWait(5000L);
mqttClient.setCallback(new MqttCallbackExtended() {
@Override
public void connectComplete(boolean reconnect, String serverURI) {
System.out.println(sendClientId + " connect success to " + serverURI);
}
@Override
public void connectionLost(Throwable throwable) {
throwable.printStackTrace();
}
@Override
public void messageArrived(String topic, MqttMessage mqttMessage) {
}
@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
}
});
try {
mqttClient.connect(mqttConnectOptions);
} catch (Exception e) {
e.printStackTrace();
}
long interval = 1000;
for (int i = 0; i < 1000; i++) {
String msg = "r1_" + System.currentTimeMillis() + "_" + i;
MqttMessage message = new MqttMessage(msg.getBytes(StandardCharsets.UTF_8));
message.setQos(1);
String mqttSendTopic = firstTopic + "/r1";
mqttClient.publish(mqttSendTopic, message);
System.out.println(now() + "send: " + mqttSendTopic + ", " + msg);
Thread.sleep(interval);
mqttSendTopic = firstTopic + "/r/wc";
msg = "wc_" + System.currentTimeMillis() + "_" + i;
MqttMessage messageWild = new MqttMessage(msg.getBytes(StandardCharsets.UTF_8));
messageWild.setQos(1);
mqttClient.publish(mqttSendTopic, messageWild);
System.out.println(now() + "send: " + mqttSendTopic + ", " + msg);
Thread.sleep(interval);
mqttSendTopic = firstTopic + "/r2";
msg = "msgQ2_" + System.currentTimeMillis() + "_" + i;
message = new MqttMessage(msg.getBytes(StandardCharsets.UTF_8));
message.setQos(2);
mqttClient.publish(mqttSendTopic, message);
System.out.println(now() + "send: " + mqttSendTopic + ", " + msg);
Thread.sleep(interval);
}
}
private static MqttConnectOptions buildMqttConnectOptions(String clientId) throws NoSuchAlgorithmException, InvalidKeyException {
MqttConnectOptions connOpts = new MqttConnectOptions();
connOpts.setCleanSession(true);
connOpts.setKeepAliveInterval(60);
connOpts.setAutomaticReconnect(true);
connOpts.setMaxInflight(10000);
connOpts.setUserName(System.getenv("username"));
connOpts.setPassword(HmacSHA1Util.macSignature(clientId, System.getenv("password")).toCharArray());
return connOpts;
}
private static String now() {
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
return sf.format(new Date()) + "\t";
}
}
Mqttコンシューマ
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.rocketmq.mqtt.example;
import org.apache.rocketmq.mqtt.common.util.HmacSHA1Util;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MqttConsumer {
public static void main(String[] args) throws MqttException, NoSuchAlgorithmException, InvalidKeyException {
String brokerUrl = "tcp://" + System.getenv("host") + ":1883";
String firstTopic = System.getenv("topic");
MemoryPersistence memoryPersistence = new MemoryPersistence();
String recvClientId = "recv01";
MqttConnectOptions mqttConnectOptions = buildMqttConnectOptions(recvClientId);
MqttClient mqttClient = new MqttClient(brokerUrl, recvClientId, memoryPersistence);
mqttClient.setTimeToWait(5000L);
mqttClient.setCallback(new MqttCallbackExtended() {
@Override
public void connectComplete(boolean reconnect, String serverURI) {
System.out.println(recvClientId + " connect success to " + serverURI);
try {
final String topicFilter[] = {firstTopic + "/r1", firstTopic + "/r/+", firstTopic + "/r2"};
final int[] qos = {1, 1, 2};
mqttClient.subscribe(topicFilter, qos);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void connectionLost(Throwable throwable) {
throwable.printStackTrace();
}
@Override
public void messageArrived(String topic, MqttMessage mqttMessage) throws Exception {
try {
String payload = new String(mqttMessage.getPayload());
String[] ss = payload.split("_");
System.out.println(now() + "receive:" + topic + "," + payload);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
}
});
try {
mqttClient.connect(mqttConnectOptions);
} catch (Exception e) {
e.printStackTrace();
System.out.println("connect fail");
}
}
private static MqttConnectOptions buildMqttConnectOptions(String clientId) throws NoSuchAlgorithmException, InvalidKeyException {
MqttConnectOptions connOpts = new MqttConnectOptions();
connOpts.setCleanSession(true);
connOpts.setKeepAliveInterval(60);
connOpts.setAutomaticReconnect(true);
connOpts.setMaxInflight(10000);
connOpts.setUserName(System.getenv("username"));
connOpts.setPassword(HmacSHA1Util.macSignature(clientId, System.getenv("password")).toCharArray());
return connOpts;
}
private static String now() {
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
return sf.format(new Date()) + "\t";
}
}