从零开始搭建属于自己的物联网平台(三)基于netty实现mqtt server网关

源码

源码仓库

往期链接

从零开始搭建属于自己的物联网平台(一)需求分析以及架构设计

从零开始搭建属于自己的物联网平台(二)实现基于订阅发布的消息总线

实现的功能及形式

  • 支持mqtt client接入
  • 支持集群化的监听接入(比如多个服务节点对外暴露端口,如果业务上关闭某个监听端口时需要统一关闭)
  • 针对消息处理,当网关接收到消息之后将消息发布到消息总线,交由后续业务进行处理

名词解释

  • 网关:这里我们特指业务上的,实现某一种协议接入的网络组件

功能设计及代码实现

首先根据一开始我们的设想,平台不应该限制链接的协议,应该支持多元化的接入,所以我们要做好接口,支持拓展,同样为了集群化部署,我们需要实现对网关存活状态的监控,网关启停的统一调度。
在这里我们利用现有的架构,对于网关存活状态的监控我们准备采用手动将网关节点注册到nacos来实现状态的检测。
而网关统一调度则采用我们之前实现的消息总线来进行,当一个节点接收到网关启停请求之后将该消息发布到消息总线,其他网关服务接收到该消息之后同步启停做到服务的统一调度。

接口定义

定义网关接口,规定网关需要实现的方法。

package com.soft863.gateway;

import com.soft863.gateway.message.codec.Transport;

/**
 * 设备网关
 */
public interface DeviceGateway {
    
    

    /**
     * @return 网关ID
     */
    String getId();

    /**
     * 
     * @return 网关端口
     */
    Integer getPort();

    /**
     * @return 支持的传输协议
     * @see com.soft863.gateway.message.codec.DefaultTransport
     */
    Transport getTransport();

    /**
     * 启动网关
     *
     * @return 启动结果
     */
    void startup();

    /**
     * 关闭网关
     *
     * @return 关闭结果
     */
    void shutdown();

    /**
     * 网关状态
     *
     * @return 网关状态
     */
    Boolean status();
}

使用netty实现mqtt server

这里使用netty来实现mqtt server。

package com.soft863.gateway.mqtt;

import com.soft863.gateway.DeviceGateway;
import com.soft863.gateway.message.codec.DefaultTransport;
import com.soft863.gateway.message.codec.Transport;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.util.ResourceLeakDetector;
import lombok.extern.slf4j.Slf4j;

/**
 * @Author: 刘华林
 * @Date: 21-3-9 下午20:07
 * @Version 2.0
 */
@Slf4j
public class MqttServerGateway implements DeviceGateway {
    
    

    /** 网关ID(为后续多实例扩展) */
    private String id;
    /** 服务端口 */
    private Integer port;
    /** netty boos线程 */
    private Integer bossGroupThreadCount;
    /** netty woreker线程数 推荐设置为核数*2 */
    private Integer workerGroupThreadCount;
    /** 网关状态 true 启动 false 关闭 */
    private Boolean status;
    /**
     * DISABLED(禁用): 不进行内存泄露的检测;
     *
     * SIMPLE(操作简单): 抽样检测,且只对部分方法调用进行记录,消耗较小,有泄漏时可能会延迟报告,默认级别;
     *
     * ADVANCED(高级): 抽样检测,记录对象最近几次的调用记录,有泄漏时可能会延迟报告;
     *
     * PARANOID(偏执): 每次创建一个对象时都进行泄露检测,且会记录对象最近的详细调用记录。是比较激进的内存泄露检测级别,消耗最大,建议只在测试时使用。
     */
    private String leakDetectorLevel;
    /**
     * 最大消息长度
     */
    private Integer maxPayloadSize;

    private ChannelFuture channelFuture;
    private EventLoopGroup bossGroup;
    private EventLoopGroup workerGroup;

    public MqttServerGateway(String id,
                             Integer port,
                             Integer bossGroupThreadCount,
                             Integer workerGroupThreadCount,
                             String leakDetectorLevel,
                             Integer maxPayloadSize) {
    
    
        this.id = id;
        this.port = port;
        this.bossGroupThreadCount = bossGroupThreadCount;
        this.workerGroupThreadCount = workerGroupThreadCount;
        this.leakDetectorLevel = leakDetectorLevel;
        this.maxPayloadSize = maxPayloadSize;
    }

    @Override
    public String getId() {
    
    
        return this.id;
    }

    @Override
    public Integer getPort() {
    
    
        return this.port;
    }

    @Override
    public Transport getTransport() {
    
    
        return DefaultTransport.MQTT;
    }

    @Override
    public void startup() {
    
    
        log.info("Setting resource leak detector level to {}", leakDetectorLevel);
        ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.valueOf(leakDetectorLevel.toUpperCase()));

        log.info("Starting Server");
        //创建boss线程组 用于服务端接受客户端的连接
        bossGroup = new NioEventLoopGroup(bossGroupThreadCount);
        // 创建 worker 线程组 用于进行 SocketChannel 的数据读写
        workerGroup = new NioEventLoopGroup(workerGroupThreadCount);
        // 创建 ServerBootstrap 对象
        ServerBootstrap b = new ServerBootstrap();
        //设置使用的EventLoopGroup
        b.group(bossGroup, workerGroup)
                //设置要被实例化的为 NioServerSocketChannel 类
                .channel(NioServerSocketChannel.class)
                // 设置 NioServerSocketChannel 的处理器
                .handler(new LoggingHandler(LogLevel.INFO))
                // 设置连入服务端的 Client 的 SocketChannel 的处理器
                .childHandler(new MqttTransportServerInitializer(maxPayloadSize));
        // 绑定端口,并同步等待成功,即启动服务端
        try {
    
    
            channelFuture = b.bind(port).sync();
            status = true;
        } catch (InterruptedException e) {
    
    
            log.error("Server starting error");
            status = false;
        }
        log.info("Server started!");
    }

    @Override
    public void shutdown() {
    
    
        log.info("Stopping Server");
        workerGroup.shutdownGracefully();
        bossGroup.shutdownGracefully();
        status = false;
        log.info("server stopped!");

    }

    @Override
    public Boolean status() {
    
    
        return this.status;
    }
}

消息处理

当netty接收到设备上行消息之后,进行解析处理,将消息广播到消息总线,后续设备订阅消息总线中的内容,再进行业务处理。

package com.soft863.gateway.mqtt;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.mqtt.MqttDecoder;
import io.netty.handler.codec.mqtt.MqttEncoder;

/**
 * @Author: 刘华林
 * @Date: 19-4-3 下午3:26
 * @Version 1.0
 */
public class MqttTransportServerInitializer  extends ChannelInitializer<SocketChannel> {
    
    

    private final int maxPayloadSize;

    public MqttTransportServerInitializer(int maxPayloadSize) {
    
    
        this.maxPayloadSize = maxPayloadSize;
    }

    @Override
    protected void initChannel(SocketChannel socketChannel) {
    
    
        ChannelPipeline pipeline = socketChannel.pipeline();
        pipeline.addLast("decoder", new MqttDecoder(maxPayloadSize));
        pipeline.addLast("encoder", MqttEncoder.INSTANCE);
        MqttTransportHandler handler = new MqttTransportHandler();
        pipeline.addLast(handler);
        socketChannel.closeFuture().addListener(handler);

    }
}

package com.soft863.gateway.mqtt;

import com.alibaba.fastjson.JSON;
import com.soft863.gateway.DeviceInstance;
import com.soft863.gateway.auth.DeviceAuthenticator;
import com.soft863.gateway.matcher.TopicMatcher;
import com.soft863.gateway.message.DefaultDeviceMsg;
import com.soft863.gateway.message.Message;
import com.soft863.gateway.message.codec.DefaultTransport;
import com.soft863.gateway.message.codec.FromDeviceMessageContext;
import com.soft863.gateway.message.codec.mqtt.SimpleMqttMessage;
import com.soft863.gateway.mqtt.adapter.JsonMqttAdaptor;
import com.soft863.gateway.protocol.DeviceMessageCodec;
import com.soft863.gateway.protocol.ProtocolSupport;
import com.soft863.gateway.registry.DeviceSession;
import com.soft863.gateway.registry.MemoryProtocolSupportRegistry;
import com.soft863.gateway.tsl.adaptor.AdaptorException;
import com.soft863.gateway.util.ApplicationUtil;
import com.soft863.stream.core.eventbus.EventBus;
import com.soft863.stream.core.eventbus.message.AdapterMessage;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.mqtt.*;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import lombok.extern.slf4j.Slf4j;

import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import static io.netty.handler.codec.mqtt.MqttMessageType.*;
import static io.netty.handler.codec.mqtt.MqttQoS.*;

/**
 * @Author: 刘华林
 * @Date: 21-3-9 下午20:22
 * @Version 2.0
 */
@Slf4j
public class MqttTransportHandler extends ChannelInboundHandlerAdapter implements GenericFutureListener<Future<? super Void>> {
    
    

    public static final MqttQoS MAX_SUPPORTED_QOS_LVL = MqttQoS.AT_LEAST_ONCE;

    private volatile boolean connected;
    private volatile InetSocketAddress address;
    private final ConcurrentMap<TopicMatcher, Integer> mqttQoSMap;
    private String clientId;
    private DeviceInstance instance;

    /**
     * 设备session
     */
    private DeviceSession deviceSession;

    /**
     * 协议解析注册器
     */
    private MemoryProtocolSupportRegistry memoryProtocolSupportRegistry;

    public MqttTransportHandler() {
    
    
        this.mqttQoSMap = new ConcurrentHashMap<>();
        this.deviceSession = ApplicationUtil.getBean(DeviceSession.class);
        this.memoryProtocolSupportRegistry = ApplicationUtil.getBean(MemoryProtocolSupportRegistry.class);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
    
    
        if (msg instanceof MqttMessage) {
    
    
            processMqttMsg(ctx, (MqttMessage) msg);
        } else {
    
    
            ctx.close();
        }
    }

    /**
     * 处理MQTT消息事件(链接、发布、订阅、取消订阅、PING、断开连接)
     *
     * @param ctx
     * @param msg
     */
    private void processMqttMsg(ChannelHandlerContext ctx, MqttMessage msg) {
    
    
        address = (InetSocketAddress) ctx.channel().remoteAddress();
        if (msg.fixedHeader() == null) {
    
    
            processDisconnect(ctx);
            return;
        }

        switch (msg.fixedHeader().messageType()) {
    
    
            case CONNECT:
                processConnect(ctx, (MqttConnectMessage) msg);
                break;
            case PUBLISH:
                processPublish(ctx, (MqttPublishMessage) msg);
                break;
            case SUBSCRIBE:
                processSubscribe(ctx, (MqttSubscribeMessage) msg);
                break;
            case UNSUBSCRIBE:
                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)) {
    
    
                    processDisconnect(ctx);
                }
                break;
            default:
                break;

        }
    }

    private void processPublish(ChannelHandlerContext ctx, MqttPublishMessage mqttMsg) {
    
    

        if (!checkConnected(ctx)) {
    
    
            return;
        }

        String topicName = mqttMsg.variableHeader().topicName();
        int msgId = mqttMsg.variableHeader().packetId();
        processDevicePublish(ctx, mqttMsg, topicName, msgId);

    }

    /**
     * 处理设备上行消息
     *
     * @param ctx
     * @param mqttMsg
     * @param topicName
     * @param msgId
     */
    private void processDevicePublish(ChannelHandlerContext ctx, MqttPublishMessage mqttMsg, String topicName, int msgId) {
    
    
        try {
    
    
            if (topicName.equals(MqttTopics.DEVICE_REGISTRY_TOPIC)) {
    
    
                // todo 设备自注册
                log.info(JsonMqttAdaptor.validatePayload(mqttMsg.payload()));
            } else {
    
    
                log.info("message arrive " + JsonMqttAdaptor.validatePayload(mqttMsg.payload()));

                // 构建标准消息
                SimpleMqttMessage simpleMqttMessage = SimpleMqttMessage.builder()
                        .topic(topicName)
                        .payload(mqttMsg.payload())
                        .clientId(clientId)
                        .build();
                FromDeviceMessageContext messageDecodeContext = FromDeviceMessageContext.of(simpleMqttMessage, deviceSession, instance.getProtocolId());
                // 调用解析器
                ProtocolSupport protocolSupport = memoryProtocolSupportRegistry.getProtocolSupport(instance.getProtocolId());
                DeviceMessageCodec deviceMessageCodec = protocolSupport.getMessageCodecSupport(DefaultTransport.MQTT.getId());
                Message message = deviceMessageCodec.decode(messageDecodeContext);
                // 取得event bus
                EventBus eventBus = ApplicationUtil.getBean(EventBus.class);
                AdapterMessage adapterMessage = new AdapterMessage();
                adapterMessage.setTopic("/device/message/" + clientId);
                adapterMessage.setPayload(JSON.toJSONString(message));
                eventBus.publish(adapterMessage);
            }
            if (msgId > 0) {
    
    
                ctx.writeAndFlush(createMqttPubAckMsg(msgId));
            }
        } catch (AdaptorException e) {
    
    
            ctx.close();
        }

    }

    private void processSubscribe(ChannelHandlerContext ctx, MqttSubscribeMessage mqttMsg) {
    
    
        if (!checkConnected(ctx)) {
    
    
            return;
        }
        List<Integer> grantedQoSList = new ArrayList<>();
        for (MqttTopicSubscription subscription : mqttMsg.payload().topicSubscriptions()) {
    
    
            String topic = subscription.topicName();
            MqttQoS reqQoS = subscription.qualityOfService();
            switch (topic) {
    
    
                default:
                    grantedQoSList.add(FAILURE.value());
                    break;
            }
        }
        ctx.writeAndFlush(createSubAckMessage(mqttMsg.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);
    }

    private void registerSubQoS(String topic, List<Integer> grantedQoSList, MqttQoS reqQoS) {
    
    
        grantedQoSList.add(getMinSupportedQos(reqQoS));
        mqttQoSMap.put(new TopicMatcher(topic), getMinSupportedQos(reqQoS));
    }

    private static int getMinSupportedQos(MqttQoS reqQoS) {
    
    
        return Math.min(reqQoS.value(), MAX_SUPPORTED_QOS_LVL.value());
    }

    private void processUnsubscribe(ChannelHandlerContext ctx, MqttUnsubscribeMessage mqttMsg) {
    
    
        if (!checkConnected(ctx)) {
    
    
            return;
        }
        for (String topicName : mqttMsg.payload().topics()) {
    
    
            mqttQoSMap.remove(new TopicMatcher(topicName));
        }
        ctx.writeAndFlush(createUnSubAckMessage(mqttMsg.variableHeader().messageId()));
    }

    private void processDisconnect(ChannelHandlerContext ctx) {
    
    
        connected = false;
        // 更新注册器状态
        instance = deviceSession.get(clientId);
        if (instance != null) {
    
    
            instance.setStatus(connected);
        }
        ctx.close();
    }

    private void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) {
    
    
        String username = msg.payload().userName();
        String password = "";
        if (msg.variableHeader().hasPassword()) {
    
    
            try {
    
    
                password = new String(msg.payload().passwordInBytes(), "utf-8");
            } catch (UnsupportedEncodingException e) {
    
    

            }
        }
        // 设备认证
        clientId = msg.payload().clientIdentifier();
        boolean authResult = ApplicationUtil.getBean(DeviceAuthenticator.class).auth(clientId, username, password);
        if (authResult) {
    
    
            ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_ACCEPTED));
            connected = true;
            // 更新注册器状态
            instance = deviceSession.get(clientId);
            if (instance != null) {
    
    
                instance.setChannel(ctx);
                instance.setStatus(connected);
            }
        } else {
    
    
            ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USERNAME_OR_PASSWORD));
            connected = false;
            // 更新注册器状态
            instance = deviceSession.get(clientId);
            if (instance != null) {
    
    
                instance.setStatus(connected);
            }
        }
    }

    private static MqttPubAckMessage createMqttPubAckMsg(int requestId) {
    
    
        MqttFixedHeader mqttFixedHeader =
                new MqttFixedHeader(PUBACK, false, AT_LEAST_ONCE, false, 0);
        MqttMessageIdVariableHeader mqttMsgIdVariableHeader =
                MqttMessageIdVariableHeader.from(requestId);
        return new MqttPubAckMessage(mqttFixedHeader, mqttMsgIdVariableHeader);
    }

    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);
    }

    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 boolean checkConnected(ChannelHandlerContext ctx) {
    
    
        if (connected) {
    
    
            return true;
        } else {
    
    
            ctx.close();
            return false;
        }
    }

    @Override
    public void operationComplete(Future<? super Void> future) throws Exception {
    
    

    }
}

网关注册器

实现网关注册,心跳,统一的状态检测等

package com.soft863.gateway.registry;

import com.soft863.gateway.DeviceGateway;
import com.soft863.gateway.registry.discovery.GatewayDiscovery;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 网关注册器
 */
@Component
@EnableScheduling
public class GatewayRegistry {
    
    

    private final GatewayDiscovery gatewayDiscovery;

    private Map<String, DeviceGateway> gateway = new ConcurrentHashMap<>(16);

    private String localHost = "127.0.0.1";

    public GatewayRegistry(GatewayDiscovery gatewayDiscovery) {
    
    
        this.gatewayDiscovery = gatewayDiscovery;
        try {
    
    
            this.localHost = Inet4Address.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
    
    
        }
    }

    public Map<String, DeviceGateway> getAllGateway() {
    
    
        return this.gateway;
    }

    public DeviceGateway register(String id, DeviceGateway gateway) {
    
    

        // 网关发现中心注册
        gatewayDiscovery.register(id, localHost, gateway.getPort());
        return this.gateway.put(id, gateway);
    }

    public DeviceGateway get(String id) {
    
    
        return this.gateway.get(id);
    }

    public Boolean has(String id) {
    
    
        return this.gateway.containsKey(id);
    }

    public DeviceGateway remove(String id) {
    
    
        return this.gateway.remove(id);
    }

    @Scheduled(fixedDelay = 5000)  //每隔1秒执行一次
    public void doHeartBeat() {
    
    
        for (Map.Entry<String, DeviceGateway> gatewayEntry : this.gateway.entrySet()) {
    
    
            gatewayDiscovery.heartbeat(gatewayEntry.getKey(), localHost, gatewayEntry.getValue().getPort(), gatewayEntry.getValue().status().toString());

        }
    }
}

猜你喜欢

转载自blog.csdn.net/baidu_29609961/article/details/131181468