引言
我们在完成连接相关实现后,就可以接收其他客户端发送的消息了。客户端之间的消息通讯通过以下方式实现:
- 客户端A向服务端订阅主题T;
- 客户端B向服务端发送消息,并指定该消息是发给主题T的;
- 服务端查看谁订阅了主题T,转发该消息给所有订阅主题T的客户端。
订阅主题
/**
* 订阅主题
*
* @param qos 0-至多发1次
* 1-至少送达1次
* 2-完全送达并回应
* @param topics 主题集
* @throws Exception 失败异常
*/
public void subscribe(int qos, String... topics) throws Exception {
if (channel == null)
return;
SubscribeProcessor sp = new SubscribeProcessor();
subscribeProcessorList.add(sp);
try {
String result = sp.subscribe(channel, qos, topics, actionTimeout);
if (ProcessorResult.RESULT_SUCCESS.equals(result)) {
Log.i("-->订阅成功:" + Arrays.toString(topics));
} else {
throw new CancellationException();
}
} catch (Exception e) {
if (e instanceof CancellationException) {
Log.i("-->订阅取消:" + Arrays.toString(topics));
} else {
Log.i("-->订阅异常:" + Arrays.toString(topics) + " " + e);
throw e;
}
} finally {
subscribeProcessorList.remove(sp);
}
}
class MqttHandler extends SimpleChannelInboundHandler<Object> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msgx) throws Exception {
if (msgx == null) {
return;
}
MqttMessage msg = (MqttMessage) msgx;
MqttFixedHeader mqttFixedHeader = msg.fixedHeader();
if (mqttFixedHeader.messageType() == MqttMessageType.PINGRESP) {
Log.i("[ping]-->channelRead0 : " + msgx);
} else {
Log.i("-->channelRead0 : " + msgx);
}
switch (mqttFixedHeader.messageType()) {
case CONNACK:
if (connectProcessor != null)
connectProcessor.processAck(ctx.channel(), (MqttConnAckMessage) msg);
break;
case SUBACK:
if (subscribeProcessorList.size() > 0) {
for (SubscribeProcessor subscribeProcessor : subscribeProcessorList) {
subscribeProcessor.processAck(ctx.channel(), (MqttSubAckMessage) msg);
}
}
break;
case UNSUBACK:
if (unsubscribeProcessorList.size() > 0) {
for (UnsubscribeProcessor unsubscribeProcessor : unsubscribeProcessorList) {
unsubscribeProcessor.processAck(ctx.channel(), (MqttUnsubAckMessage) msg);
}
}
break;
case PUBLISH:
break;
case PUBACK:
break;
case PUBREC:
// qos = 2的发布才参与
break;
case PUBREL:
// qos = 2的发布才参与
break;
case PUBCOMP:
// qos = 2的发布才参与
break;
case PINGRESP:
// 心跳请求响应
if (pingProcessor != null) {
pingProcessor.processAck(ctx.channel(), msg);
}
break;
default:
break;
}
}
}
SubscribeProcessor.java
package io.x2ge.mqtt.core;
import io.netty.channel.Channel;
import io.netty.handler.codec.mqtt.MqttMessageIdAndPropertiesVariableHeader;
import io.netty.handler.codec.mqtt.MqttSubAckMessage;
import io.netty.handler.codec.mqtt.MqttSubscribeMessage;
import io.x2ge.mqtt.utils.AsyncTask;
import io.x2ge.mqtt.utils.Log;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class SubscribeProcessor extends AsyncTask<String> {
private long timeout;
private int msgId;
private final AtomicBoolean receivedAck = new AtomicBoolean(false);
@Override
public String call() throws Exception {
if (!isCancelled() && !receivedAck.get()) {
synchronized (receivedAck) {
receivedAck.wait(timeout);
}
}
return receivedAck.get() ? ProcessorResult.RESULT_SUCCESS : ProcessorResult.RESULT_FAIL;
}
public String subscribe(Channel channel, String[] topics, long timeout) throws Exception {
return subscribe(channel, 0, topics, timeout);
}
public String subscribe(Channel channel, int qos, String[] topics, long timeout) throws Exception {
this.timeout = timeout;
int id = 0;
String s;
try {
id = MessageIdFactory.get();
this.msgId = id;
MqttSubscribeMessage msg = ProtocolUtils.subscribeMessage(id, qos, topics);
Log.i("-->发起订阅:" + msg);
channel.writeAndFlush(msg);
s = execute().get(timeout, TimeUnit.MILLISECONDS);
} finally {
MessageIdFactory.release(id);
}
return s;
}
public void processAck(Channel channel, MqttSubAckMessage msg) {
MqttMessageIdAndPropertiesVariableHeader variableHeader = msg.idAndPropertiesVariableHeader();
if (variableHeader.messageId() == msgId) {
synchronized (receivedAck) {
receivedAck.set(true);
receivedAck.notify();
}
}
}
}
生成订阅请求报文:
public static List<String> getTopics(SubscriptionTopic[] subscriptionTopics) {
if (subscriptionTopics != null) {
List<String> topics = new LinkedList<>();
for (SubscriptionTopic sb : subscriptionTopics) {
topics.add(sb.getTopic());
}
return topics;
} else {
return null;
}
}
public static List<MqttTopicSubscription> getTopicSubscriptions(SubscriptionTopic[] subscriptionTopics) {
if (subscriptionTopics != null && subscriptionTopics.length > 0) {
List<MqttTopicSubscription> list = new LinkedList<>();
for (SubscriptionTopic sm : subscriptionTopics) {
list.add(new MqttTopicSubscription(sm.getTopic(), MqttQoS.valueOf(sm.getQos())));
}
return list;
}
return null;
}
public static MqttSubscribeMessage subscribeMessage(int messageId, String... topics) {
return subscribeMessage(messageId, 0, topics);
}
public static MqttSubscribeMessage subscribeMessage(int messageId, int qos, String... topics) {
List<SubscriptionTopic> list = new ArrayList<>();
for (String topic : topics) {
SubscriptionTopic sb = new SubscriptionTopic();
sb.setQos(qos);
sb.setTopic(topic);
list.add(sb);
}
return subscribeMessage(messageId, list.toArray(new SubscriptionTopic[0]));
}
public static MqttSubscribeMessage subscribeMessage(int messageId, SubscriptionTopic... subscriptionTopics) {
return subscribeMessage(messageId, getTopicSubscriptions(subscriptionTopics));
}
public static MqttSubscribeMessage subscribeMessage(int messageId, List<MqttTopicSubscription> mqttTopicSubscriptions) {
MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(
MqttMessageType.SUBSCRIBE,
false,
MqttQoS.AT_LEAST_ONCE,
false,
0);
MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader.from(messageId);
MqttSubscribePayload mqttSubscribePayload = new MqttSubscribePayload(mqttTopicSubscriptions);
return new MqttSubscribeMessage(
mqttFixedHeader,
mqttMessageIdVariableHeader,
mqttSubscribePayload);
}
取消订阅主题
public void unsubscribe(String... topics) throws Exception {
if (channel == null)
return;
UnsubscribeProcessor usp = new UnsubscribeProcessor();
unsubscribeProcessorList.add(usp);
try {
String result = usp.unsubscribe(channel, topics, actionTimeout);
if (ProcessorResult.RESULT_SUCCESS.equals(result)) {
Log.i("-->取消订阅成功:" + Arrays.toString(topics));
} else {
throw new CancellationException();
}
} catch (Exception e) {
if (e instanceof CancellationException) {
Log.i("-->取消订阅取消:" + Arrays.toString(topics));
} else {
Log.i("-->取消订阅异常:" + Arrays.toString(topics) + " " + e);
throw e;
}
} finally {
unsubscribeProcessorList.remove(usp);
}
}
class MqttHandler extends SimpleChannelInboundHandler<Object> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msgx) throws Exception {
if (msgx == null) {
return;
}
MqttMessage msg = (MqttMessage) msgx;
MqttFixedHeader mqttFixedHeader = msg.fixedHeader();
if (mqttFixedHeader.messageType() == MqttMessageType.PINGRESP) {
Log.i("[ping]-->channelRead0 : " + msgx);
} else {
Log.i("-->channelRead0 : " + msgx);
}
switch (mqttFixedHeader.messageType()) {
case CONNACK:
if (connectProcessor != null)
connectProcessor.processAck(ctx.channel(), (MqttConnAckMessage) msg);
break;
case SUBACK:
if (subscribeProcessorList.size() > 0) {
for (SubscribeProcessor subscribeProcessor : subscribeProcessorList) {
subscribeProcessor.processAck(ctx.channel(), (MqttSubAckMessage) msg);
}
}
break;
case UNSUBACK:
if (unsubscribeProcessorList.size() > 0) {
for (UnsubscribeProcessor unsubscribeProcessor : unsubscribeProcessorList) {
unsubscribeProcessor.processAck(ctx.channel(), (MqttUnsubAckMessage) msg);
}
}
break;
case PUBLISH:
break;
case PUBACK:
break;
case PUBREC:
// qos = 2的发布才参与
break;
case PUBREL:
// qos = 2的发布才参与
break;
case PUBCOMP:
// qos = 2的发布才参与
break;
case PINGRESP:
// 心跳请求响应
if (pingProcessor != null) {
pingProcessor.processAck(ctx.channel(), msg);
}
break;
default:
break;
}
}
}
UnsubscribeProcessor.java
package io.x2ge.mqtt.core;
import io.netty.channel.Channel;
import io.netty.handler.codec.mqtt.MqttMessageIdAndPropertiesVariableHeader;
import io.netty.handler.codec.mqtt.MqttUnsubAckMessage;
import io.netty.handler.codec.mqtt.MqttUnsubscribeMessage;
import io.x2ge.mqtt.utils.AsyncTask;
import io.x2ge.mqtt.utils.Log;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class UnsubscribeProcessor extends AsyncTask<String> {
private long timeout;
private int msgId;
private final AtomicBoolean receivedAck = new AtomicBoolean(false);
@Override
public String call() throws Exception {
if (!isCancelled() && !receivedAck.get()) {
synchronized (receivedAck) {
receivedAck.wait(timeout);
}
}
return receivedAck.get() ? ProcessorResult.RESULT_SUCCESS : ProcessorResult.RESULT_FAIL;
}
public String unsubscribe(Channel channel, String[] topics, long timeout) throws Exception {
this.timeout = timeout;
int id = 0;
String s;
try {
id = MessageIdFactory.get();
this.msgId = id;
MqttUnsubscribeMessage msg = ProtocolUtils.unsubscribeMessage(id, Arrays.asList(topics));
Log.i("-->发起取消订阅:" + msg);
channel.writeAndFlush(msg);
s = execute().get(timeout, TimeUnit.MILLISECONDS);
} finally {
MessageIdFactory.release(id);
}
return s;
}
public void processAck(Channel channel, MqttUnsubAckMessage msg) {
MqttMessageIdAndPropertiesVariableHeader variableHeader = msg.idAndPropertiesVariableHeader();
if (variableHeader.messageId() == msgId) {
synchronized (receivedAck) {
receivedAck.set(true);
receivedAck.notify();
}
}
}
}
生产取消订阅请求报文:
public static MqttUnsubscribeMessage unsubscribeMessage(int messageId, List<String> topicList) {
MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(
MqttMessageType.UNSUBSCRIBE,
false,
MqttQoS.AT_MOST_ONCE,
false,
0x02);
MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(messageId);
MqttUnsubscribePayload mqttUnsubscribeMessage = new MqttUnsubscribePayload(topicList);
return new MqttUnsubscribeMessage(
mqttFixedHeader,
variableHeader,
mqttUnsubscribeMessage);
}
主题通配符(摘自MQTT协议)
主题层级(topic level)分隔符用于将结构化引入主题名。如果存在分隔符,它将主题名分割为多个主题层级 topic level 。
订阅的主题过滤器可以包含特殊的通配符,允许你一次订阅多个主题。
主题过滤器中可以使用通配符,但是主题名不能使用通配符 [MQTT-4.7.1-1]。
主题层级分隔符
斜杠(‘/’ U+002F)用于分割主题的每个层级,为主题名提供一个分层结构。当客户端订阅指定的主题过滤器包含两种通配符时,主题层级分隔符就很有用了。主题层级分隔符可以出现在主题过滤器或主题名字的任何位置。相邻的主题层次分隔符表示一个零长度的主题层级。
多层通配符
数字标志(‘#’ U+0023)是用于匹配主题中任意层级的通配符。多层通配符表示它的父级和任 意数量的子层级。多层通配符必须位于它自己的层级或者跟在主题层级分隔符后面。不管哪 种情况,它都必须是主题过滤器的最后一个字符 [MQTT-4.7.1-2]。
例如,如果客户端订阅主题 “sport/tennis/player1/#”,它会收到使用下列主题名发布的消息:
- “sport/tennis/player1”
- “sport/tennis/player1/ranking”
- “sport/tennis/player1/score/wimbledon”
注意:
- “sport/#”也匹配单独的 “sport” ,因为 # 包括它的父级。
- “#”是有效的,会收到所有的应用消息。
- “sport/tennis/#”也是有效的。
- “sport/tennis#”是无效的。
- “sport/tennis/#/ranking”是无效的。
单层通配符
加号 (‘+’ U+002B) 是只能用于单个主题层级匹配的通配符。
在主题过滤器的任意层级都可以使用单层通配符,包括第一个和最后一个层级。然而它必须 占据过滤器的整个层级 [MQTT-4.7.1-3]。可以在主题过滤器中的多个层级中使用它,也可以 和多层通配符一起使用。
例如, “sport/tennis/+” 匹配 “sport/tennis/player1” 和 “sport/tennis/player2” ,但是不匹 配 “sport/tennis/player1/ranking” 。同时,由于单层通配符只能匹配一个层级, “sport/+” 不匹配 “sport” 但是却匹配 “sport/”。
注意:
- “+” 是有效的。
- “+/tennis/#” 是有效的。
- “sport+” 是无效的。
- “sport/+/player1” 也是有效的。
- “/finance” 匹配 “+/+” 和 “/+” ,但是不匹配 “+”。
以$开头的主题
服务端不能将 $ 字符开头的主题名匹配通配符 (#或+) 开头的主题过滤器 [MQTT-4.7.2-1]。服 务端应该阻止客户端使用这种主题名与其它客户端交换消息。服务端实现可以将 $ 开头的主 题名用作其他目的。 注意:
- $SYS/ 被广泛用作包含服务器特定信息或控制接口的主题的前缀。
- 应用不能使用$字符开头的主题。
- 订阅 “#” 的客户端不会收到任何发布到以 “$” 开头主题的消息。
- 订阅 “+/monitor/Clients” 的客户端不会收到任何发布到 “$SYS/monitor/Clients” 的消息。
- 订阅 “KaTeX parse error: Expected 'EOF', got '#' at position 5: SYS/#̲” 的客户端会收到发布到以 “SYS/” 开头主题的消息。
- 订阅 “ S Y S / m o n i t o r / + ” 的 客 户 端 会 收 到 发 布 到 “ SYS/monitor/+” 的客户端会收到发布到 “ SYS/monitor/+”的客户端会收到发布到“SYS/monitor/Clients” 主题的消息。
- 如果客户端想同时接受以 “$SYS/” 开头主题的消息和不以 $ 开头主题的消息,它需要同 时订阅 “#” 和 “$SYS/#”。
主题语义和用法
主题名和主题过滤器必须符合下列规则:
- 所有的主题名和主题过滤器必须至少包含一个字符 [MQTT-4.7.3-1]。
- 主题名和主题过滤器是区分大小写的。 主题名和主题过滤器可以包含空格。
- 主题名或主题过滤器以前置或后置斜杠 “/” 区分。 只包含斜杠 “/” 的主题名或主题过滤器是合法的。
- 主题名和主题过滤器不能包含空字符 (Unicode U+0000) [Unicode] [MQTT-4.7.3-2]。
- 主题名和主题过滤器是UTF-8编码字符串,它们不能超过65535字节 [MQTT-4.7.3-3]。见 1.5.3节。
除了不能超过UTF-编码字符串的长度限制之外,主题名或主题过滤器的层级数量没有其它限制。
匹配订阅时,服务端不能对主题名或主题过滤器执行任何规范化(normalization)处理,不能修改或替换任何未识别的字符 [MQTT-4.7.3-4]。主题过滤器中的每个非通配符层级需要逐 字符匹配主题名中对应的层级才算匹配成功。
注意:
使用UTF-8编码规则意味着,主题过滤器和主题名的比较可以通过比较编码后的UTF-8字 节或解码后的Unicode字符。
注意:
- “ACCOUNTS” 和 “Accounts” 是不同的主题名
- “Accounts payable” 是合法的主题名
- “/finance” 和 “finance” 是不同的
如果订阅的主题过滤器与消息的主题名匹配,应用消息会被发送给每一个匹配的客户端订阅。主题可能是管理员在服务端预先定义好的,也可能是服务端收到第一个订阅或使用那个主题名的应用消息时动态添加的。服务端也可以使用一个安全组件有选择地授权客户端使用某个主题资源。