【EMQX实践】手撸一个MQTT-BROKER

在MQTT生态系统中,两个关键组件负责建立连接、发布和订阅消息:MQTT客户端和MQTT代理(Broker)。客户端是连接物联网设备的接口,负责发送和接收消息;而代理则作为中央枢纽,负责消息的路由和分发,确保信息能够准确、及时地传递给目标客户端。
image.png

MQTT生命周期

  1. 连接建立

客户端发送 CONNECT 报文给服务器,其中包含客户端标识符(ClientId)、协议版本、连接标志(例如是否需要遗嘱消息)以及认证信息。
服务器响应 CONNACK 报文,确认连接是否成功。

  1. 通信

订阅:客户端可以发送 SUBSCRIBE 报文以订阅感兴趣的主题,服务器会通过 SUBACK 报文确认订阅请求。
发布:客户端或服务器可以发送 PUBLISH 报文来发布消息到特定主题。消息可以设置 QoS(Quality of Service)等级,以确保消息的可靠传输。
取消订阅:客户端可以发送 UNSUBSCRIBE 报文取消对某个主题的订阅,服务器会通过 UNSUBACK 报文确认。
心跳:客户端定期发送 PINGREQ 报文,服务器会回应 PINGRESP 报文,以维持连接的活跃状态。

  1. 断开连接

正常断开:客户端发送 DISCONNECT 报文,然后关闭连接。
异常断开:如果服务器没有在最长心跳周期内收到任何活动,它会认为客户端已断开并可能执行相应的遗嘱处理。
image.png

MQTT 推送消息

  1. 发布消息
    消息发布者(可以是任意连接到 Broker 的客户端)向 Broker 发送 PUBLISH 报文,其中包含主题名和消息体。
    Broker 根据消息的主题名,查找所有订阅该主题的客户端列表。
  2. 转发消息
    Broker 将 PUBLISH 报文转发给所有订阅了相应主题的客户端。
    如果消息的 QoS 等级大于 0,则 Broker 和客户端之间会有额外的握手以确保消息被正确接收。

image.png

技术选型

Netty是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。它提供了对TCP、UDP和文件传输的支持,并且很容易扩展以支持其他传输类型。Netty的核心是基于非阻塞I/O操作的,这使得它能够处理成千上万的并发连接,非常适合用于实现MQTT协议。

代码结构

基于Netty实现MQTT BROKER主要包括4部分,核心MQTT处理模块、MQTT客户端信息模块、MQTT客户端认证模块、topic管理模块。
image.png
MQTT连接处理模块
image.png
安全认证
image.png
topic管理
image.png
客户端管理
image.png

源码

https://gitee.com/xmhzzz/emqx-practice/tree/master/mqtt-broker-service
gitee源码mqtt-broker-service
image.png

启动项目

image.png

连接

模拟两个设备(device:85211001、device:85211003)连接mqtt,分别用于推送消息及接收消息
image.png
连接成功
image.png

订阅topic

device:85211002订阅topic【sys/+/+/thing/event/property/post】
image.png
订阅成功
image.png

推送消息

device:85211001 推送消息:
Topic: sys/SMART/8445211/thing/event/property/post
QoS: 0
msg: {“id”:1716799607917,“params”:{“iccid”:142424,“temp”:“12”,“door”:1,“light”:20,“signal”:100,“lock”:0},“version”:“1.0”,“method”:“thing.event.property.post”}
image.png
推送成功
image.png
device:85211002接收成功
image.png

取消订阅

device:85211002取消订阅sys/+/+/thing/event/property/post
image.png
取消订阅成功
image.png

断开连接

device:85211001、device:85211003断开连接
image.png
断开连接成功
image.png

核心代码实现

broker

public abstract class AbstractMQTTBroker implements IMQTTBroker {

    private final AbstractMQTTBrokerBuilder builder;
    private final EventLoopGroup bossGroup;
    private final EventLoopGroup workerGroup;

    private ChannelFuture tcpChannelF;

    private final ConnectionRateLimitHandler connRateLimitHandler;


    protected AbstractMQTTBroker(AbstractMQTTBrokerBuilder builder) {
        this.builder = builder;
        connRateLimitHandler = new ConnectionRateLimitHandler(builder.connectRateLimit);
        this.bossGroup =  NettyUtil.createEventLoopGroup(this.builder.mqttBossThreads,
               newThreadFactory("mqtt-boss-thread",false, Thread.NORM_PRIORITY));
        this.workerGroup = NettyUtil.createEventLoopGroup(builder.mqttWorkerThreads,
                newThreadFactory("mqtt-worker-thread",false, Thread.NORM_PRIORITY));
    }

    protected void beforeBrokerStart() {

    }

    protected void afterBrokerStop() {

    }

    @Override
    public final void start(){
        try {
            log.info("Starting MQTT broker");
            beforeBrokerStart();
            log.debug("Starting server channel");

            tcpChannelF = this.bindTCPChannel();
            Channel channel = tcpChannelF.sync().channel();
            log.debug("Accepting mqtt connection over tcp channel at {}", channel.localAddress());


            log.info("MQTT broker started");
        } catch (InterruptedException e) {
            throw new IllegalStateException(e);
        }
    }

    @Override
    public final void shutdown() {
        log.info("Shutting down MQTT broker");
        if (tcpChannelF != null) {
            tcpChannelF.channel().close().syncUninterruptibly();
            log.debug("Stopped accepting mqtt connection over tcp channel");
        }

        bossGroup.shutdownGracefully().syncUninterruptibly();
        log.debug("Boss group shutdown");
        workerGroup.shutdownGracefully().syncUninterruptibly();
        log.debug("Worker group shutdown");
        afterBrokerStop();
        log.info("MQTT broker shutdown");
    }

    private ChannelFuture bindTCPChannel() {

        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup)
                .channel(NettyUtil.determineServerSocketChannelClass(bossGroup))
                .childHandler(  new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) {

                        ChannelPipeline pipeline = ch.pipeline();
                        pipeline.addLast("connRateLimiter", connRateLimitHandler);
                        pipeline.addLast("trafficShaper",
                                new ChannelTrafficShapingHandler(builder.writeLimit, builder.readLimit));
                        pipeline.addLast(MqttEncoder.class.getName(), MqttEncoder.INSTANCE);
                        pipeline.addLast(MqttDecoder.class.getName(), new MqttDecoder(builder.maxBytesInMessage));
                        pipeline.addLast(MQTTMessageDebounceHandler.NAME, new MQTTMessageDebounceHandler());
                        pipeline.addLast(MQTTPreludeHandler.NAME, new MQTTPreludeHandler());
                    }
                });

        // Bind and start to accept incoming connections.
        return b.bind(builder.port);

    }


    public static ThreadFactory newThreadFactory(String name, boolean daemon, int priority) {
        return new ThreadFactory() {
            private final AtomicInteger seq = new AtomicInteger();

            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                int s = seq.getAndIncrement();
                t.setName(s > 0 ? name + "-" + s : name);
                t.setDaemon(daemon);
                t.setPriority(priority);
                return t;
            }
        };
    }
}

Handler

public abstract class MQTTConnectHandler extends ChannelDuplexHandler {

    private volatile boolean connected;



    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        super.channelActive(ctx);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        connected = false;
        ApplicationContext.getContext().remove(ctx.channel());
        super.channelInactive(ctx);
    }

    @Override
    public final void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        MqttMessage mqttMessage =  (MqttMessage) msg;
        if (mqttMessage.fixedHeader() == null) {
            processDisconnectCompose(ctx);
            return;
        }

        switch (mqttMessage.fixedHeader().messageType()) {
            case CONNECT:
                processConnectCompose(ctx, (MqttConnectMessage) msg);
                break;
            case PUBLISH:
                if (checkConnected(ctx)) {
                    processPublish(ctx, (MqttPublishMessage) msg);
                }
                break;
            case SUBSCRIBE:
                if (checkConnected(ctx)) {
                    processSubscribe(ctx, (MqttSubscribeMessage) msg);
                }
                break;
            case UNSUBSCRIBE:
                if (checkConnected(ctx)) {
                    processUnsubscribe(ctx, (MqttUnsubscribeMessage) msg);
                }
                break;
            case PINGREQ:
                if (checkConnected(ctx)) {
                    ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0)));
                }
                break;
            case DISCONNECT:
                if (checkConnected(ctx)) {
                    processDisconnectCompose(ctx);
                }
                break;
            default:
                break;

        }

    }

    private Boolean processAuthSecurity(ChannelHandlerContext ctx, MqttConnectMessage msg) throws Exception {
        String username = msg.payload().userName();
        String password = "";
        if (msg.variableHeader().hasPassword()) {
            try {
                password = new String(msg.payload().passwordInBytes(), "utf-8");
            } catch (Exception e) {
                log.error("password convert error msg[{}]",e.getMessage(),e);
            }
        }
        String clientId = msg.payload().clientIdentifier();
        ChannelAttrs.setClientId(ctx.channel(),clientId);
        ChannelAttrs.setName(ctx.channel(),username);
        ChannelAttrs.setPassword(ctx.channel(),password);
        return ApplicationContext.getAuthSecurity().authSecurity(new IAuthSecurity.AuthInfo().setName(username).setPassword(password));
    }

    private MqttConnAckMessage createMqttConnAckMsg(MqttConnectReturnCode returnCode) {
        MqttFixedHeader mqttFixedHeader =
                new MqttFixedHeader(CONNACK, false, AT_MOST_ONCE, false, 0);
        MqttConnAckVariableHeader mqttConnAckVariableHeader =
                new MqttConnAckVariableHeader(returnCode, true);
        return new MqttConnAckMessage(mqttFixedHeader, mqttConnAckVariableHeader);
    }



    private void processConnectCompose(ChannelHandlerContext ctx,MqttConnectMessage msg) throws Exception {
        if (processAuthSecurity(ctx,msg)){
            connected = true;
            processConnect(ctx,msg);
            ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_ACCEPTED));
        }else {
            connected = false;
            ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USERNAME_OR_PASSWORD));
        }
    }

    protected void processDisconnectCompose(ChannelHandlerContext ctx){
        log.info("disconnect channelId[{}]",ctx.channel().id().toString());
        connected = false;
        try {
            processDisconnect(ctx);
        }catch (Exception e){
            log.error("disconnect error msg[{}]",e.getMessage(),e);
        }finally {
            ctx.channel().close();
            ctx.close();
        }
    }

    protected abstract void processDisconnect(ChannelHandlerContext ctx);

    protected abstract void processConnect(ChannelHandlerContext ctx,MqttConnectMessage msg);

    protected abstract void processPublish(ChannelHandlerContext ctx,MqttPublishMessage msg);

    protected abstract void processSubscribe(ChannelHandlerContext ctx,MqttSubscribeMessage msg);

    protected abstract void processUnsubscribe(ChannelHandlerContext ctx,MqttUnsubscribeMessage msg);

    protected  Boolean checkConnected(ChannelHandlerContext ctx){
        if (connected) {
            return true;
        } else {
            ctx.channel().close();
            ctx.close();
            return false;
        }
    }
}


public class MQTTV3ConnectHandler extends MQTTConnectHandler {

    public static final String NAME = "MQTTV3ConnectHandler";

    @Override
    protected void processDisconnect(ChannelHandlerContext ctx) {
        ApplicationContext.getContext().remove(ctx.channel());
    }

    @Override
    protected void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) {
        log.info("connect channelId[{}]",ctx.channel().id().toString());
        ApplicationContext.getContext().addClientInfo(ctx.channel());
    }

    @Override
    protected void processPublish(ChannelHandlerContext ctx, MqttPublishMessage msg) {
        log.info("publish channelId[{}]",ctx.channel().id().toString());
        String topicName = msg.variableHeader().topicName();
        ApplicationContext.getTopicManage().topicRouter(topicName).forEach(channelId -> {
            ClientInfo clientInfo = ApplicationContext.getContext().getClientInfo(channelId);
            if (ObjectUtil.isNotNull(clientInfo)) {
                log.debug("PUBLISH - clientId: {}, topic: {}, Qos: {}", clientInfo.getClientId(), topicName, msg.fixedHeader().qosLevel().value());
                Channel channel = clientInfo.getChannel();
                if (channel != null) {
                    channel.writeAndFlush(msg);
                }
            }
        });
    }

    @Override
    protected void processSubscribe(ChannelHandlerContext ctx, MqttSubscribeMessage msg) {
        log.info("subscribe channelId[{}]",ctx.channel().id().toString());
        List<Integer> grantedQoSList = Lists.newArrayList();
        for (MqttTopicSubscription subscription : msg.payload().topicSubscriptions()) {
            String topic = subscription.topicName();
            MqttQoS qoS = subscription.qualityOfService();
            ApplicationContext.getTopicManage().subscribe(ctx.channel().id().toString(),topic);
            grantedQoSList.add(qoS.value());
        }
        ctx.writeAndFlush(createSubAckMessage(msg.variableHeader().messageId(), grantedQoSList));
    }

    private static MqttSubAckMessage createSubAckMessage(Integer msgId, List<Integer> grantedQoSList) {
        MqttFixedHeader mqttFixedHeader =
                new MqttFixedHeader(SUBACK, false, AT_LEAST_ONCE, false, 0);
        MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader.from(msgId);
        MqttSubAckPayload mqttSubAckPayload = new MqttSubAckPayload(grantedQoSList);
        return new MqttSubAckMessage(mqttFixedHeader, mqttMessageIdVariableHeader, mqttSubAckPayload);
    }

    @Override
    protected void processUnsubscribe(ChannelHandlerContext ctx, MqttUnsubscribeMessage msg) {
        log.info("unsubscribe channelId[{}]",ctx.channel().id().toString());
        for (String topic : msg.payload().topics()) {
            ApplicationContext.getTopicManage().unsubscribe(ctx.channel().id().toString(),topic);
        }
        ctx.writeAndFlush(createUnSubAckMessage(msg.variableHeader().messageId()));
    }
    private MqttMessage createUnSubAckMessage(int msgId) {
        MqttFixedHeader mqttFixedHeader =
                new MqttFixedHeader(UNSUBACK, false, AT_LEAST_ONCE, false, 0);
        MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader.from(msgId);
        return new MqttMessage(mqttFixedHeader, mqttMessageIdVariableHeader);
    }
}

其它

关注公众号【 java程序猿技术】获取EMQX实践系列文章

  • 27
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

后端马农

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值