Build your own IoT platform from scratch (3) Implement mqtt server gateway based on netty

source code

source code repository

Past links

Build your own IoT platform from scratch (1) Demand analysis and architecture design

Build your own IoT platform from scratch (2) Realize a message bus based on subscription publishing

Function and form realized

  • Support mqtt client access
  • Support clustered listening access (for example, multiple service nodes expose ports to the outside world, if a listening port is closed in business, it needs to be closed uniformly)
  • For message processing, when the gateway receives the message, it publishes the message to the message bus and hands it over to the subsequent business for processing

Glossary

  • Gateway: Here we specifically refer to network components that implement a certain protocol access in business

Function design and code implementation

First of all, according to our vision at the beginning, the platform should not limit the protocol of the link, but should support diversified access, so we need to make a good interface and support expansion. Also for cluster deployment, we need to monitor the survival status of the gateway. Unified scheduling of gateway startup and shutdown.
Here we use the existing architecture. For the monitoring of the gateway survival status, we plan to manually register the gateway node with nacos to realize the status detection.
The unified scheduling of the gateway is carried out using the message bus we implemented before. When a node receives the gateway start and stop request, it publishes the message to the message bus. After receiving the message, other gateway services start and stop synchronously to achieve unified service scheduling. .

Interface definition

Define the gateway interface and specify the methods that the gateway needs to implement.

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

Use netty to implement mqtt server

Here netty is used to implement 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;
    }
}

message processing

After netty receives the uplink message from the device, it parses and processes it, broadcasts the message to the message bus, and the subsequent device subscribes to the content in the message bus, and then performs business processing.

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 {
    
    

    }
}

Gateway Registrar

Realize gateway registration, heartbeat, unified state detection, etc.

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

        }
    }
}

Guess you like

Origin blog.csdn.net/baidu_29609961/article/details/131181468