基于Netty实现Mqtt客户端(五)-订阅/取消订阅

引言

我们在完成连接相关实现后,就可以接收其他客户端发送的消息了。客户端之间的消息通讯通过以下方式实现:

  • 客户端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” 是不同的

如果订阅的主题过滤器与消息的主题名匹配,应用消息会被发送给每一个匹配的客户端订阅。主题可能是管理员在服务端预先定义好的,也可能是服务端收到第一个订阅或使用那个主题名的应用消息时动态添加的。服务端也可以使用一个安全组件有选择地授权客户端使用某个主题资源。

项目源码

netty-mqtt-client

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是使用Netty-Mqtt-Client实现Mqtt客户发布消息和订阅消息的核心Java代码,带注释说明: ```java import io.netty.buffer.Unpooled; import io.netty.handler.codec.mqtt.*; import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.codec.mqtt.MqttPublishMessage; import io.netty.handler.codec.mqtt.MqttSubscribeMessage; import io.netty.handler.codec.mqtt.MqttUnsubscribeMessage; import io.netty.handler.codec.mqtt.MqttMessageBuilders.*; // 创建一个Mqtt客户类 public class MqttClient { private final String clientId; // 客户ID private final String serverHost; // 服务器主机名 private final int serverPort; // 服务器口号 private final String username; // 用户名 private final String password; // 密码 private final int keepAlive; // 心跳间隔时间 private EventLoopGroup group; // Netty线程组 private MqttClientInitializer initializer; // Netty客户初始化器 private Channel channel; // Netty通道 // 构造方法,初始化Mqtt客户配置 public MqttClient(String clientId, String serverHost, int serverPort, String username, String password, int keepAlive) { this.clientId = clientId; this.serverHost = serverHost; this.serverPort = serverPort; this.username = username; this.password = password; this.keepAlive = keepAlive; } // 连接服务器 public void connect() { group = new NioEventLoopGroup(); // 创建Netty线程组 initializer = new MqttClientInitializer(clientId, username, password, keepAlive); // 创建Netty客户初始化器 Bootstrap bootstrap = new Bootstrap(); // 创建Netty客户启动器 bootstrap.group(group) .channel(NioSocketChannel.class) .remoteAddress(serverHost, serverPort) .handler(initializer); try { ChannelFuture future = bootstrap.connect().sync(); // 连接服务器,同步等待连接完成 if (future.isSuccess()) { // 连接成功 channel = future.channel(); // 获取Netty通道 } } catch (InterruptedException e) { e.printStackTrace(); } } // 断开连接 public void disconnect() { if (channel != null && channel.isActive()) { channel.close(); // 关闭Netty通道 } if (group != null) { group.shutdownGracefully(); // 关闭Netty线程组 } } // 发布消息 public void publish(String topic, String message, MqttQoS qos) { MqttFixedHeader header = new MqttFixedHeader(MqttMessageType.PUBLISH, false, qos, false, 0); MqttPublishVariableHeader variableHeader = new MqttPublishVariableHeader(topic, 0); ByteBuf payload = Unpooled.buffer(); payload.writeBytes(message.getBytes()); MqttPublishMessage publishMessage = new MqttPublishMessage(header, variableHeader, payload); channel.writeAndFlush(publishMessage); // 发送Mqtt PUBLISH消息 } // 订阅主题 public void subscribe(String topic, MqttQoS qos) { MqttFixedHeader header = new MqttFixedHeader(MqttMessageType.SUBSCRIBE, false, MqttQoS.AT_LEAST_ONCE, false, 0); MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(1); MqttTopicSubscription topicSubscription = new MqttTopicSubscription(topic, qos); MqttSubscribePayload payload = new MqttSubscribePayload(Arrays.asList(topicSubscription)); MqttSubscribeMessage subscribeMessage = new MqttSubscribeMessage(header, variableHeader, payload); channel.writeAndFlush(subscribeMessage); // 发送Mqtt SUBSCRIBE消息 } // 取消订阅主题 public void unsubscribe(String topic) { MqttFixedHeader header = new MqttFixedHeader(MqttMessageType.UNSUBSCRIBE, false, MqttQoS.AT_LEAST_ONCE, false, 0); MqttMessageIdVariableHeader variableHeader = MqttMessageIdVariableHeader.from(1); MqttUnsubscribePayload payload = new MqttUnsubscribePayload(Arrays.asList(topic)); MqttUnsubscribeMessage unsubscribeMessage = new MqttUnsubscribeMessage(header, variableHeader, payload); channel.writeAndFlush(unsubscribeMessage); // 发送Mqtt UNSUBSCRIBE消息 } } ``` 以上代码中,我们创建了一个MqttClient类,该类通过Netty-Mqtt-Client实现Mqtt客户发布消息和订阅消息的功能。具体实现细节如下: - connect()方法:连接Mqtt服务器,其中我们通过Netty创建了一个NioEventLoopGroup线程组、一个MqttClientInitializer客户初始化器和一个Bootstrap客户启动器,并将它们配置好后发起连接请求; - disconnect()方法:断开Mqtt服务器连接,关闭Netty通道和线程组; - publish()方法:发布Mqtt消息,其中我们使用了MqttFixedHeader、MqttPublishVariableHeader、ByteBuf和MqttPublishMessage等Netty-Mqtt-Client提供的类来构建Mqtt PUBLISH消息,并通过Netty通道将其发送给服务器; - subscribe()方法:订阅Mqtt主题,其中我们使用了MqttFixedHeader、MqttMessageIdVariableHeader、MqttTopicSubscription、MqttSubscribePayload和MqttSubscribeMessage等Netty-Mqtt-Client提供的类来构建Mqtt SUBSCRIBE消息,并通过Netty通道将其发送给服务器; - unsubscribe()方法:取消订阅Mqtt主题,其中我们使用了MqttFixedHeader、MqttMessageIdVariableHeader、MqttUnsubscribePayload和MqttUnsubscribeMessage等Netty-Mqtt-Client提供的类来构建Mqtt UNSUBSCRIBE消息,并通过Netty通道将其发送给服务器。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值