使用Springboot集成Netty进行网关通讯

场景

使用netty和设备进行tcp通讯,设备通过配置ip地址和端口,写到设备里面,设备就会通过这个地址发送注册、心跳以及一些应答数据,首先得去看设备那边的文档,看它使用的消息格式是什么

首先引入netty依赖

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.100.Final</version>
        </dependency>

编写服务类

需要在springboot启动成功后启动,那么需要实现ApplicationRunner或者CommandLineRunner接口,进行启动,在销毁时需要关闭服务

/**
 * @Description
 * @Date 2024/4/22 15:58
 * @Author tom
 **/
@Component
@Slf4j
public class LoraServer implements ApplicationRunner {

    private final EventLoopGroup boss = new NioEventLoopGroup();
    private final EventLoopGroup worker = new NioEventLoopGroup();


    private Channel channel;


    @Value("${sf.netty.host}")
    private String host;

    @Value("${sf.netty.port}")
    private int port;


    @Override
    public void run(ApplicationArguments args) throws Exception {
        try {
            // 启动类
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            // 设置参数,组配置
            serverBootstrap.group(boss, worker)
                    // 指定channel
                    .channel(NioServerSocketChannel.class)
                    // 初始化服务端可连接队列
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    // 允许重复使用本地地址和端口,连接关闭后,可以立即重用端口
                    .option(ChannelOption.SO_REUSEADDR, true)
                    // 设置TCP长连接,TCP会主动探测空闲连接的有效性
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    // 禁用Nagle算法,小数据时可以即时传输
                    .childOption(ChannelOption.TCP_NODELAY, true)
                    // 发送缓冲区大小
                    .childOption(ChannelOption.SO_SNDBUF, 256 * 1024)
                    // 接收缓冲区大小
                    .childOption(ChannelOption.SO_RCVBUF, 256 * 1024)
                    // Netty服务端channel初始化
                    .childHandler(new ChannelInitializer<Channel>() {
                        @Override
                        protected void initChannel(Channel channel) throws Exception {
                            // 添加心跳检测,如果60s不在认定为下线
                            channel.pipeline().addLast(new IdleStateHandler(60,-1,-1, TimeUnit.SECONDS));
                            // 根据长度进行分包
        					channel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.wrappedBuffer(new byte[]{0x0D})));
                            channel.pipeline().addLast("logging",new LoggingHandler("INFO"));
//                            channel.pipeline().addLast("loraServerDecoder",new LoraServerDecoder());
                            //自定义编码解码器
                            channel.pipeline().addLast("loraServerEncoder",new LoraServerEncoder());

                            channel.pipeline().addLast("loraServerHandler",new LoraServerHandler());
                        }
                    });
            // 绑定端口,开始接收进来的连接
            ChannelFuture future = serverBootstrap.bind(port).sync();

            if (future.isSuccess()) {
                log.info("Netty服务端启动!! 端口:[{}]", port);
            }
            channel = future.channel();
        } catch (Exception e) {
            log.error("Netty服务端启动异常!! error:{}", e.getMessage());
        }
    }


    @PreDestroy
    public void destroy() {
        if (channel != null) {
            channel.close();
        }
        worker.shutdownGracefully();
        boss.shutdownGracefully();
        log.info("Netty服務端关闭成功");
    }

}

发送时需要对数据进行编码,这里是根据满足对方的格式需要去维护

@Component
@Slf4j
public class LoraServerEncoder extends MessageToByteEncoder<LoraSendPacket> {

    @Override
    protected void encode(ChannelHandlerContext ctx, LoraSendPacket packet, ByteBuf byteBuf) throws Exception {

        // 发送
        if (packet.getCommandCode() == COMMAND_CODE_SEND) {
            // 固定协议头
            byteBuf.writeByte(packet.getHead());
            // 包序号
            byteBuf.writeByte((byte)packet.getSortNo());

            byte[] ipaddr = getAddressHex(packet.getAddress());
            // ip
            byteBuf.writeBytes(ipaddr);
            // 命令码
            byteBuf.writeByte(packet.getCommandCode());
            // 发送数据
            if (StrUtil.isNotBlank(packet.getData())) {
                byte[] bytes = packet.getData().getBytes("GBK");
                byteBuf.writeByte(bytes.length);
                byteBuf.writeBytes(bytes);
                // crc
                byteBuf.writeByte(calcCrc(packet.getHead(),packet.getSortNo(),ipaddr,packet.getCommandCode(),bytes.length,bytes));
            }

            byte[] response = new byte[byteBuf.readableBytes()];
            int i = 0;
            while (byteBuf.readableBytes() > 0) {
                response[i]= byteBuf.readByte();
                i++;
            }
//            log.info("发送数据:{}", HexUtil.encodeHexStr(response));
            log.info("channelId:【{}】 发送数据 数据 [{}]", ctx.channel().id(), HexUtil.encodeHexStr(response));
            byteBuf.resetReaderIndex();
        }

        // 发送清除命令
        if (packet.getCommandCode() == COMMAND_CODE_CLEAR) {
            //5A 00 FF FF FF FF E6 01 00 3D;
            byteBuf.writeByte(0x5A);
            byteBuf.writeByte(0x00);
            byteBuf.writeByte(0xFF);
            byteBuf.writeByte(0xFF);
            byteBuf.writeByte(0xFF);
            byteBuf.writeByte(0xFF);
            byteBuf.writeByte(0xE6);
            byteBuf.writeByte(0x01);
            byteBuf.writeByte(0x00);
            byteBuf.writeByte(0x3D);
            log.info("channelId:【{}】 发送清除命令", ctx.channel().id());
        }
    }
    /**
     * 前面所有数字和 % 256 的 余数
     * @return
     */
    private int calcCrc(int head, int sortNo, byte[] ipaddr, int commandCode, int length, byte[] bytes) {
        int i = 0;

        for (byte b : ipaddr) {
            i += b;
        }
        i += commandCode;
        i += length;
        for (byte aByte : bytes) {
            i += aByte;
        }
        return (head + sortNo + i) % 256;
    }

    private byte[] getAddressHex(String address) {
        String[] split = address.split("\\.");
        byte[] bytes = new byte[4];

        for (int i = 0; i < split.length; i++) {
            bytes[i] = (byte) Integer.parseInt(split[i]);
        }
        return bytes;
    }

}

下面就是处理具体的逻辑,当它进行TCP连接后把连接存起来,方便业务上去发送数据

@Slf4j
@Component
@ChannelHandler.Sharable
public class LoraServerHandler extends ChannelInboundHandlerAdapter {

    // 管理一个全局map,保存连接进服务端的通道
    public static final Map<ChannelId, ChannelHandlerContext> CHANNEL_MAP = new ConcurrentHashMap<>();

    @Resource
    private ApplicationEventPublisher applicationEventPublisher;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 当客户端主动连接服务端,通道活跃后触发
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        InetSocketAddress inetSocketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
        String clientIp = inetSocketAddress.getAddress().getHostAddress();
        int clientPort = inetSocketAddress.getPort();
        // 获取连接通道唯一标识
        ChannelId channelId = ctx.channel().id();
        // 如果map中不包含此连接,就保存连接
        if (CHANNEL_MAP.containsKey(channelId)) {
            log.info("客户端【{}】是连接状态,连接通道数量:{}", channelId, CHANNEL_MAP.size());
        } else {
            // 保存连接
            CHANNEL_MAP.put(channelId, ctx);
            log.info("客户端【{}】连接Netty服务端!![clientIp:{} clientPort:{}]", channelId, clientIp, clientPort);
            log.info("连接通道数量:{}", CHANNEL_MAP.size());

//            CLIENT_MAP.put(clientIp + clientPort,new LoraClient(clientIp,clientPort));
        }
    }

    /**
     * 当客户端主动断开连接,通道不活跃触发
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        InetSocketAddress inetSocketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
        String clientIp = inetSocketAddress.getAddress().getHostAddress();
        int clientPort = inetSocketAddress.getPort();
        // 获取终止连接的客户端ID
        ChannelId channelId = ctx.channel().id();
        // 包含此客户端才去删除
        if (CHANNEL_MAP.containsKey(channelId)) {
            // 删除连接
            CHANNEL_MAP.remove(channelId);
            log.warn("客户端【{}】断开Netty连接!![clientIp:{} clientPort:{}]", channelId, clientIp, clientPort);
            log.info("连接通道数量:{}", CHANNEL_MAP.size());
        }
    }

    /**
     * 通道有消息触发
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        try {
            // 报文解析处理
            ByteBuf byteBuf = (ByteBuf) msg;
            byte head = byteBuf.getByte(0);
            if (head == BEAT) {
                InetSocketAddress inetSocketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
                log.info("channelId:【{}】 收到心跳[clientIp:{} clientPort:{}]", ctx.channel().id(), inetSocketAddress.getAddress(),inetSocketAddress.getPort());
            }
            if (head == HEAD) {
                byte[] response = new byte[byteBuf.readableBytes()];
                int i = 0;
                while (byteBuf.readableBytes() > 0) {
                    response[i]= byteBuf.readByte();
                    i++;
                }
                log.info("channelId:【{}】 收到应答 数据 [{}]", ctx.channel().id(), HexUtil.encodeHexStr(response));
                byteBuf.resetReaderIndex();
                handleReceive(ctx,byteBuf);
            }
//            ctx.writeAndFlush(Unpooled.copiedBuffer("服务端响应: " + msg + "\n", CharsetUtil.UTF_8));
            // 发布自定义Netty数据包处理事件
//            applicationEventPublisher.publishEvent();
        } catch (Exception e) {
            log.error("channelId:【{}】 报文解析失败!! msg:{} error:{}", ctx.channel().id(), msg.toString(), e.getMessage());
        }


    }



    /**
     * 包头:5A 固定
     * 包序号:是应答包的序号,也就是当前显示数据的包序号。
     * 地址 ID:手表地址 ID
     * 命令码:0XF1 固定
     * 数据长度:0X01
     * 键值:下翻键:0X31,上翻键:0X32,确认键:0X33
     * 检验:校验和,CRC 前面所有数据之和除 256 的余数。
     * 结束符:0X0D
     * @param ctx
     * @param byteBuf
     */
    private void handleReceive(ChannelHandlerContext ctx, ByteBuf byteBuf) {
        byte head = byteBuf.getByte(0); // 应答头
        byte pkgNo = byteBuf.getByte(1); // 应答包号
        byte ip1 = byteBuf.getByte(2);
        byte ip2 = byteBuf.getByte(3);
        byte ip3 = byteBuf.getByte(4);
        byte ip4 = byteBuf.getByte(5);
        byte cmd = byteBuf.getByte(6); // 命令
        byte len = byteBuf.getByte(7); // 长度
        byte data = byteBuf.getByte(8); // 数据
        byte end = byteBuf.getByte(9); // 结束符
        if (data == PRESS_CONFIRM) {
            // 处理应答消息的事件
            applicationEventPublisher.publishEvent(new LoraReceiveEvent(ctx, pkgNo, ip1, ip2, ip3, ip4, cmd, len, data, end));
        }
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
        String socketString = ctx.channel().remoteAddress().toString();
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.READER_IDLE) {
                log.warn("Client: 【{}】 READER_IDLE 读超时", socketString);
                CHANNEL_MAP.remove(ctx.channel().id());
                ctx.close();
            } else if (event.state() == IdleState.WRITER_IDLE) {
                log.warn("Client: 【{}】 WRITER_IDLE 写超时", socketString);
                CHANNEL_MAP.remove(ctx.channel().id());
                ctx.close();
            } else if (event.state() == IdleState.ALL_IDLE) {
                log.warn("Client: 【{}】 ALL_IDLE 读/写超时", socketString);
                CHANNEL_MAP.remove(ctx.channel().id());
                ctx.close();
            }
        }
    }



    /**
     * 当连接发生异常时触发
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.error("channelId:【{}】 发生异常!! error:{}", ctx.channel().id(), cause.getMessage());
        CHANNEL_MAP.remove(ctx.channel().id());
        // 当出现异常就关闭连接
        ctx.close();
    }


    @PreDestroy
    public void destroy() {
        if (!CHANNEL_MAP.isEmpty()) {
            CHANNEL_MAP.forEach((k,v)->{
                v.close();
                log.info("Netty服务端销毁 {}",k);
            });
        }
    }


    public void sendMessage(LoraSendPacket loraSendPacket) {
        if (loraSendPacket.getCommandCode() == COMMAND_CODE_SEND) {
            Long increment = stringRedisTemplate.opsForValue().increment("iot:lora:sortNo");
            assert increment != null;
            if (increment.intValue() > 0xFF) {
                increment = 0L;
                stringRedisTemplate.opsForValue().set("iot:lora:sortNo", "0");
            }
            loraSendPacket.setSortNo(increment.intValue());
            CHANNEL_MAP.forEach((k,v)->{
                v.channel().writeAndFlush(loraSendPacket);
            });
        }
        if (loraSendPacket.getCommandCode() == COMMAND_CODE_CLEAR) {
            stringRedisTemplate.opsForValue().set("iot:lora:sortNo", "0");
            CHANNEL_MAP.forEach((k,v)->{
                v.channel().writeAndFlush(loraSendPacket);
            });
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值