Springboot项目集成Netty组件

系列文章目录

Springboot项目集成Netty组件
Netty新增解析数据包指定长度功能


文章目录

  • 系列文章目录
  • 前言
  • 一、Netty是什么?
  • 二、使用步骤
    • 1. 项目引入依赖
      • 1.1 项目基础版本信息:
      • 1.2 Netty依赖
    • 2. 项目配置
      • 2.1 在 yml 配置文件中配置以下:
      • 2.2 创建Netty配置类
    • 3. Netty 服务端
      • 3.1 Netty 服务端
      • 3.2 Netty channle注册类
      • 3.3 Netty 协议初始化解码器
      • 3.4 Netty 全局通道池
      • 3.5 WebSocket 协议通道数据处理
        • 3.5.1 WebSocket 处理URI参数
        • 3.5.2 WebSocket 数据处理类
      • 3.6 Socket 协议通道数据处理
      • 3.7 Netty 公共工具类
      • 3.8 Netty 启动类
  • 总结


前言

项目中有时候会需与其他客户端/系统建立Sokcet连接 或WebSocket连接,满足业务需求。Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。我们可以便捷的利用此组件搭建自己的客户端/服务器框架,进行二次开发,满足自己的业务需求。

本文介绍了Springboot集成Netty的配置以及使用方式,项目是基于ruoyi分离版作为基础项目进行二次开发,如果需要完整项目,可以私聊我,如果有时间会整理出来。


一、Netty是什么?

Netty是由JBOSS提供的一个java开源框架,现为 Github上的独立项目。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
也就是说,Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。
“快速”和“简单”并不用产生维护性或性能上的问题。Netty 是一个吸收了多种协议(包括FTP、SMTP、HTTP等各种二进制文本协议)的实现经验,并经过相当精心设计的项目。最终,Netty 成功的找到了一种方式,在保证易于开发的同时还保证了其应用的性能,稳定性和伸缩性。

简单说: Netty是一个高性能、高可靠性的基于NIO封装的网络应用框架

二、使用步骤

1. 项目引入依赖

1.1 项目基础版本信息:

Java 版本1.8
SpringBoot 版本 2.2.13.RELEASE

1.2 Netty依赖

        <dependency>
            <groupId>io.nettygroupId>
            <artifactId>netty-allartifactId>
            <version>4.1.68.Finalversion>
        dependency>

2. 项目配置

2.1 在 yml 配置文件中配置以下:

# netty-server 配置
netty:
  # 监听端口
  port: 8180
  # 队列池 - 最大队列数 200
  queue-max: 200
  # 线程队列容量 10
  pool-queue-init: 10
  # netty 请求路径
  path: /ws

2.2 创建Netty配置类

@Data
@Component
//application.yml此文件在ruoyi-admin模块中
@PropertySource(name="NettyProperties",value = {"classpath:application.yml"},ignoreResourceNotFound=false, encoding="UTF-8")
@ConfigurationProperties(prefix = "netty")
public class NettyProperties {
    private Integer port;
    private Integer queueMax;
    private String path;
}

3. Netty 服务端

3.1 Netty 服务端

import com.ruoyi.entrust.constant.NettyProperties;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @Author: ldddd
 * @Date: 2021/12/26 18:18
 */
@Slf4j
@Component
public class NettySocketServer {

    @Autowired
    private NettyProperties properties;

    public void start() throws Exception {
        // Boss线程:由这个线程池提供的线程是boss种类的,用于创建、连接、绑定socket, (有点像门卫)然后把这些socket传给worker线程池。
        // 在服务器端每个监听的socket都有一个boss线程来处理。在客户端,只有一个boss线程来处理所有的socket。
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        // Worker线程:Worker线程执行所有的异步I/O,即处理操作
        EventLoopGroup workgroup = new NioEventLoopGroup();

        try {
            // ServerBootstrap 启动NIO服务的辅助启动类,负责初始话netty服务器,并且开始监听端口的socket请求
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workgroup)
                    // 设置非阻塞,用它来建立新accept的连接,用于构造serversocketchannel的工厂类
                    .channel(NioServerSocketChannel.class)
                    // ServerChannelInitializer 对出入的数据进行的业务操作,其继承ChannelInitializer
                    .childHandler(new ServerChannelInitializer())
                    //设置队列大小
                    .option(ChannelOption.SO_BACKLOG, properties.getQueueMax())
                    // 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture channelFuture = serverBootstrap.bind(properties.getPort()).sync();
            log.info("Netty服务器开启等待客户端连接, 监听端口:{}", properties.getPort());
            channelFuture.channel().closeFuture().sync();

        } catch (Exception e) {
            e.printStackTrace();

        } finally {
            // 关闭主线程组
            bossGroup.shutdownGracefully();
            // 关闭工作线程
            workgroup.shutdownGracefully();
        }
    }
}

3.2 Netty channle注册类

对出入的数据进行的业务操作进行初始化

目前通道支持连接协议 WebSocketSocket

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

/**
 * @Author: LiuZedi
 * @Date: 2021/12/26 18:32
 *
 * channle注册类
 */
public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch)  {
        ChannelPipeline pipeline = ch.pipeline();

        //添加日志
        pipeline.addLast("logging",new LoggingHandler(LogLevel.WARN));
        // 判断 连接类型 此处 用来支持多种连接 目前支持 WebSocket 和 Socket
        pipeline.addLast("socketChoose",new SocketChooseHandler());
    }
}

3.3 Netty 协议初始化解码器

此类用于 解析多种协议,调用对应协议的处理器

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketFrameAggregator;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;

import java.nio.ByteOrder;
import java.util.List;

/**
 * @Author: ldddd
 * @Date: 2022/1/18 10:34
 *
 *
 * 协议初始化解码器.
 * 用来判定实际使用什么协议.
 */
@Slf4j
public class SocketChooseHandler extends ByteToMessageDecoder {

    /** 默认暗号长度为23 */
    private static final int MAX_LENGTH = 23;
    /** WebSocket握手的协议前缀 */
    private static final String WEBSOCKET_PREFIX = "GET /";

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        String protocol = getBufStart(in);
        in.resetReaderIndex();
        // 输出数据
        log.debug("获取的数据为: {}",protocol);
        if (protocol.startsWith(WEBSOCKET_PREFIX)) {
            log.info("解析 webSocket");
            //  websocket连接时,执行以下处理
            // HttpServerCodec:将请求和应答消息解码为HTTP消息
            ctx.pipeline().addLast("http-codec", new HttpServerCodec());
            // HttpObjectAggregator:将HTTP消息的多个部分合成一条完整的HTTP消息
            ctx.pipeline().addLast("aggregator", new HttpObjectAggregator(65535));
            // ChunkedWriteHandler:向客户端发送HTML5文件,文件过大会将内存撑爆
            ctx.pipeline().addLast("http-chunked", new ChunkedWriteHandler());
            ctx.pipeline().addLast("WebSocketAggregator", new WebSocketFrameAggregator(65535));

            //  若超过6000秒未收到约定心跳,则主动断开channel释放资源
            ctx.pipeline().addLast(new IdleStateHandler(6000,0,0));

            // 用于处理websocket URI参数
            ctx.pipeline().addLast(new WebSocketURIHandler());
            //用于处理websocket, /ws为访问websocket时的uri
            ctx.pipeline().addLast("ProtocolHandler", new WebSocketServerProtocolHandler("/ws"));
            ctx.pipeline().addLast(new WebSocketServerHandler());
        } else {
            log.info("解析 socket");

            //  常规TCP连接时,执行以下处理
            ctx.pipeline().addLast(new IdleStateHandler(60,0,0));
            // 数据读取使用小端模式
            ctx.pipeline().addLast("length-decoder",new LengthFieldBasedFrameDecoder(ByteOrder.LITTLE_ENDIAN,Integer.MAX_VALUE, 0, 4, 0, 4,true));
            ctx.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));
            ctx.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
            ctx.pipeline().addLast(new SocketServerHandler());
        }


        ctx.pipeline().remove(this.getClass());
    }

    private String getBufStart(ByteBuf in){

        int length = in.readableBytes();
        if (length > MAX_LENGTH) {
            length = MAX_LENGTH;
        }

        // 标记读位置
        in.markReaderIndex();
        byte[] content = new byte[length];
        in.readBytes(content);
        return new String(content);
    }
}

在处理 Socket协议的处理中,代码中使用了小端处理方案,具体原因可见我的另外一篇博客Netty新增解析数据包指定长度功能

3.4 Netty 全局通道池

由于作为Netty 服务器端,实际业务中会创建很多通道,有可能查找指定已有通道并进行数据交互。因此,开发此全局通道池,业务中可以进行保存通道,复用,以及查询通道进行交互等功能。

以下注释是 本人实际项目中的注释,理解后,根据实际项目进行对应修改,但是此类是通用

import io.netty.channel.Channel;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @Author: ldddd
 * @Date: 2022/1/15 16:07
 */
public class GlobalChannelPool {

    // 临时存放连接客户端
    public static ChannelGroup temporaryChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    // 存放已绑定用户信息连接客户端
    public static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * 在集合中获取对应的通道
     *
     * @param module  模块名 exam 和 entrust  可根据实际业务进行修改
     * @param protocol 连接类型 webSocket 和 socket  可根据实际业务进行修改
     * @param keyName  数据类型  exam模块key是mac | entrust模块key是uid   可根据实际业务进行修改
     * @param key 数据类型值 可根据实际业务进行修改
     * @return
     */
    public static List<Channel> getChannelByKey(String module, String protocol ,String keyName, String key){
        if (StringUtils.isEmpty(module) && StringUtils.isEmpty(protocol) && StringUtils.isEmpty(keyName) && StringUtils.isEmpty(key)){
            return null;
        }
        List<Channel> channelList = null;
        String moduleName = "module";
        String protocolName = "protocol";

        try{
            // 先过滤 模块  examination | detection | entrust
            AttributeKey<String> moduleKey = AttributeKey.valueOf(moduleName);
            channelList = channels.stream().filter(channel -> channel.attr(moduleKey).get().equals(module)).collect(Collectors.toList());

            // 过滤 socket类型  webSocket 和 socket
            AttributeKey<String> protocolKey = AttributeKey.valueOf(protocolName);
            channelList = channelList.stream().filter(channel -> channel.attr(protocolKey).get().equals(protocol)).collect(Collectors.toList());

            // 过滤 数据类型  examination模块 身份证读卡器 key是mac | detection模块 身份证读卡器 key是mac | entrust模块 签名功能 key是signature
            AttributeKey<String> dataKey = AttributeKey.valueOf(keyName);
            channelList = channelList.stream().filter(channel -> channel.attr(dataKey).get().equals(key)).collect(Collectors.toList());
        }catch (Exception e){
            return null;
        }

        return channelList;
    }

    /**
     * 通道添加属性 区分通道
     *
     * @param channel 通道
     * @param module  模块名 exam 和 entrust
     * @param protocol 连接协议 webSocket 和 socket
     * @param keyName  数据类型  exam模块key是mac | entrust模块key是uid
     * @param key 数据类型值
     */
    public static void addChannelAttrKey(Channel channel, String module, String protocol ,String keyName, String key){
        if (channel != null){
            // 存储 模块信息  examination | detection | entrust
            AttributeKey<String> moduleKey = AttributeKey.valueOf("module");
            channel.attr(moduleKey).set(module);
            // 存储 连接协议  webSocket 和 socket
            AttributeKey<String> protocolKey = AttributeKey.valueOf("protocol");
            channel.attr(protocolKey).set(protocol);
            // 存储 数据类型  examination模块 身份证读卡器 key是mac | detection模块 身份证读卡器 key是mac | entrust模块 签名功能 key是signature
            AttributeKey<String> dataKey = AttributeKey.valueOf(keyName);
            channel.attr(dataKey).set(key);
        }
    }
}

3.5 WebSocket 协议通道数据处理

由于项目中,WebSocket请求链接可以携带参数,因此对于此参数需要提前处理,才可以进一步进行处理WebSocket链接中交互的数据。

3.5.1 WebSocket 处理URI参数

此函数用于处理 WebSocket中参数

import com.ruoyi.entrust.constant.NettyProperties;
import com.ruoyi.examination.utils.SpringContextUtil;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;

/**
 * @Author: ldddd
 * @Date: 2022/1/19 14:53
 */
@Slf4j
public class WebSocketURIHandler extends SimpleChannelInboundHandler<WebSocketFrame> {

    private NettyProperties nettyProperties;

    public WebSocketURIHandler(){
        nettyProperties = (NettyProperties) SpringContextUtil.getBean("nettyProperties");
    }

    /**
     * 建立连接
     *
     * channel 通道 action 活跃的 当客户端主动链接服务端的链接后,这个通道就是活跃的了。
     * 也就是客户端与服务端建立了通信通道并且可以传输数据
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // 把新连接保存到临时连接表中
        GlobalChannelPool.temporaryChannels.add(ctx.channel());
        log.info("客户端与服务端连接开启:{},客户端名字:{}", ctx.channel().remoteAddress().toString(),ctx.name());
    }

    /**
     * 断开连接
     *
     * channel 通道 Inactive 不活跃的 当客户端主动断开服务端的链接后,这个通道就是不活跃的。
     * 也就是说客户端与服务端关闭了通信通道并且不可以传输数据
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        // 从连接列表中移除该连接
        GlobalChannelPool.channels.remove(ctx.channel());
        log.info("客户端与服务端连接关闭:{},客户端名字:{}", ctx.channel().remoteAddress().toString(),ctx.name());
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof FullHttpRequest) {
            FullHttpRequest request = (FullHttpRequest) msg;
            String uri = request.uri();
            String origin = request.headers().get("Origin");

            log.info("uri:{},Origin:{}",uri,origin);
            Map<String, String> params = NettyUtil.getParams(uri);

            if (nettyProperties.getPath().equals(NettyUtil.getBasePath(uri))) {
                // 因为有可能携带了参数,导致客户端一直无法返回握手包,因此在校验通过后,重置请求路径
                Channel channel = ctx.channel();
                GlobalChannelPool.channels.add(channel);
                // 添加 AttributeKey
                GlobalChannelPool.addChannelAttrKey(channel,params.get("module"),"webSocket",params.get("keyName"),params.get("key"));
                // 连接已绑定完成,把该链接移除临时存储区,避免积累过多,检索慢
                GlobalChannelPool.temporaryChannels.remove(channel);
                request.setUri(nettyProperties.getPath());
                log.info("webSocket处理完成,重新生成路径!!!");
            }
            else {
                log.warn("webSocket连接不正确。请求链接URI是:{}",uri);
                ctx.close();
            }
        }
        super.channelRead(ctx, msg);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame msg) throws Exception {
        ctx.channel().writeAndFlush(msg.retain());
    }
}

3.5.2 WebSocket 数据处理类

package com.ruoyi.entrust.utils.netty;

import io.netty.channel.*;
import io.netty.handler.codec.http.websocketx.*;
import lombok.extern.slf4j.Slf4j;


/**
 * @Author: ldddd
 * @Date: 2022/1/18 14:41
 */

@Slf4j
@ChannelHandler.Sharable
public class WebSocketServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    /**
     * 建立连接
     *
     * channel 通道 action 活跃的 当客户端主动链接服务端的链接后,这个通道就是活跃的了。
     * 也就是客户端与服务端建立了通信通道并且可以传输数据
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // 把新连接保存到临时连接表中
        GlobalChannelPool.temporaryChannels.add(ctx.channel());
        log.info("客户端与服务端连接开启:{},客户端名字:{}", ctx.channel().remoteAddress().toString(),ctx.name());
    }

    /**
     * 断开连接
     *
     * channel 通道 Inactive 不活跃的 当客户端主动断开服务端的链接后,这个通道就是不活跃的。
     * 也就是说客户端与服务端关闭了通信通道并且不可以传输数据
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {

        // 从连接列表中移除该连接
        GlobalChannelPool.channels.remove(ctx.channel());
        log.info("客户端与服务端连接关闭:{},客户端名字:{}", ctx.channel().remoteAddress().toString(),ctx.name());
    }

    /**
     * 接收客户端发送的消息 channel 通道 Read 读 简而言之就是从通道中读取数据,
     * 也就是服务端接收客户端发来的数据。但是这个数据在不进行解码时它是ByteBuf类型的
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        log.info("接收到来自客户端的信息:{}", ctx.channel().remoteAddress().toString());


        if (msg != null) {
            // WebSocket消息处理
            String webSocketInfo = ((TextWebSocketFrame) msg).text().trim();
            log.info("收到webSocket消息:" + webSocketInfo);
        }else {  //数据处理为空
            log.warn("接收到来自客户端的信息:{}   warn:消息为空", ctx.channel().remoteAddress().toString());
        }
    }

    /**
     *
     * channel 通道 Read 读取 Complete 完成 在通道读取完成后会在这个方法里通知,
     * 对应可以做刷新操作 ctx.flush()
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    /**
     * exception 异常 Caught 抓住 抓住异常,当发生异常的时候,可以做一些相应的处理,比如打印日志、关闭链接
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("发生异常的连接是" + ctx.channel().id());
        log.error("netty服务端发送异常" + cause);
        cause.printStackTrace();
        ctx.close();
    }
}

3.6 Socket 协议通道数据处理

package com.ruoyi.entrust.utils.netty;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.entrust.constant.NettyDataParam;
import com.ruoyi.entrust.utils.JsonUtil;
import com.ruoyi.examination.domain.DetectionOrder;
import com.ruoyi.examination.domain.Order;
import com.ruoyi.examination.service.impl.DetectionOrderServiceImpl;
import com.ruoyi.examination.service.impl.ExaminationOrderServiceImpl;
import com.ruoyi.examination.utils.SpringContextUtil;
import io.netty.channel.*;
import com.ruoyi.examination.domain.Message;
import io.netty.handler.codec.http.websocketx.*;
import lombok.extern.slf4j.Slf4j;

/**
 * @Author: ldddd
 * @Date: 2022/1/17 21:09
 */


@Slf4j
@ChannelHandler.Sharable
public class SocketServerHandler extends SimpleChannelInboundHandler<Object> {

    /**
     * 建立连接
     *
     * channel 通道 action 活跃的 当客户端主动链接服务端的链接后,这个通道就是活跃的了。
     * 也就是客户端与服务端建立了通信通道并且可以传输数据
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // 把新连接保存到临时连接表中
        GlobalChannelPool.temporaryChannels.add(ctx.channel());
        log.info("客户端与服务端连接开启:{},客户端名字:{}", ctx.channel().remoteAddress().toString(),ctx.name());
    }

    /**
     * 断开连接
     *
     * channel 通道 Inactive 不活跃的 当客户端主动断开服务端的链接后,这个通道就是不活跃的。
     * 也就是说客户端与服务端关闭了通信通道并且不可以传输数据
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        // 从连接列表中移除该连接
        GlobalChannelPool.channels.remove(ctx.channel());
        log.info("客户端与服务端连接关闭:{},客户端名字:{}", ctx.channel().remoteAddress().toString(),ctx.name());
    }

    /**
     * 接收客户端发送的消息 channel 通道 Read 读 简而言之就是从通道中读取数据,
     * 也就是服务端接收客户端发来的数据。但是这个数据在不进行解码时它是ByteBuf类型的
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        log.info("接收到来自客户端的信息:{}", ctx.channel().remoteAddress().toString());

        // WebSocket消息处理
        if (msg instanceof WebSocketFrame) {
            log.warn("Socket Handler 接收到WebSocket信息: 请检查!!!");
        }
        // Socket消息处理
        else{
            String requestData = (String)msg;
            log.info("收到socket消息:"+ requestData);
        }
    }

    /**
     *
     * channel 通道 Read 读取 Complete 完成 在通道读取完成后会在这个方法里通知,
     * 对应可以做刷新操作 ctx.flush()
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }


    /**
     * exception 异常 Caught 抓住 抓住异常,当发生异常的时候,可以做一些相应的处理,比如打印日志、关闭链接
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("发生异常的连接是" + ctx.channel().id());
        log.error("netty服务端发送异常" + cause);
        cause.printStackTrace();
        ctx.close();
    }
}

3.7 Netty 公共工具类

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.domain.AjaxResult;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.extern.slf4j.Slf4j;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @Author: ldddd
 * @Date: 2022/1/18 16:59
 */
@Slf4j
public class NettyUtil {

    /**
     * 将路径参数转换成Map对象,如果路径参数出现重复参数名,将以最后的参数值为准
     * @param uri 传入的携带参数的路径
     * @return
     */
    public static Map<String, String> getParams(String uri) {
        Map<String, String> params = new HashMap<>(10);
        int idx = uri.indexOf("?");
        if (idx != -1) {
            String[] paramsArr = uri.substring(idx + 1).split("&");

            for (String param : paramsArr) {
                idx = param.indexOf("=");
                params.put(param.substring(0, idx), param.substring(idx + 1));
            }
        }
        return params;
    }

    /**
     * 获取URI中参数以外部分路径
     * @param uri
     * @return
     */
    public static String getBasePath(String uri) {
        if (uri == null || uri.isEmpty())
            return null;

        int idx = uri.indexOf("?");
        if (idx == -1)
            return uri;
        return uri.substring(0, idx);
    }


    /**
     * 发布消息到对应 通道
     *
     * @param channel 当前连接通道
     * @param message 所需发布消息
     * @param requestJsonObject 查询通道的参数
     * @param protocol 通道连接协议
     * @return 是否有对应通道
     */
    public static boolean pushInfoToChannels(Channel channel, String message, JSONObject requestJsonObject, String protocol){

        boolean haveChannels = false;

        // 查询对应 连接通道
        List<Channel> channels = GlobalChannelPool.getChannelByKey(requestJsonObject.getString("module"),protocol,requestJsonObject.getString("keyName"),requestJsonObject.getString("key"));
        if (null == channels || channels.size() == 0){
            log.warn("没有发现对应{}客户端,模块为:{},请求连接模式为:{},KeyName为:{},Key为:{}", protocol,requestJsonObject.getString("module"),protocol,requestJsonObject.getString("keyName"),requestJsonObject.getString("key"));
            channel.writeAndFlush(JSON.toJSONString(AjaxResult.error(HttpStatus.NO_WEBSOCKET,"没有发现对应webSocket客户端")));
        }else {
            log.info("发现多个对应{}客户端,数量为:{},模块为:{},请求连接模式为:{},KeyName为:{},Key为:{}", protocol,channels.size(), requestJsonObject.getString("module"), protocol, requestJsonObject.getString("keyName"),requestJsonObject.getString("key"));
            haveChannels = true;
        }

        // 发送数据到对应的通道
        if (haveChannels){
            for (Channel ch: channels){
                ch.writeAndFlush(new TextWebSocketFrame(message));
            }
        }

        return haveChannels;
    }


    /**
     * 发布消息到对应 通道
     *
     * @param message 所需发布消息
     * @param requestJsonObject 查询通道的参数
     * @param protocol 通道连接协议
     * @return 是否有对应通道
     */
    public static boolean pushInfoToChannels(String message, JSONObject requestJsonObject, String protocol){

        boolean haveChannels = false;

        // 查询对应 连接通道
        List<Channel> channels = GlobalChannelPool.getChannelByKey(requestJsonObject.getString("module"),protocol,requestJsonObject.getString("keyName"),requestJsonObject.getString("key"));

        if (null == channels || channels.size() == 0){
            log.warn("没有发现对应{}客户端,模块为:{},请求连接模式为:{},KeyName为:{},Key为:{}", protocol,requestJsonObject.getString("module"),protocol,requestJsonObject.getString("keyName"),requestJsonObject.getString("key"));
        }else {
            log.info("发现多个对应{}客户端,数量为:{},模块为:{},请求连接模式为:{},KeyName为:{},Key为:{}", protocol,channels.size(), requestJsonObject.getString("module"), protocol, requestJsonObject.getString("keyName"),requestJsonObject.getString("key"));
            haveChannels = true;
        }

        // 发送数据到对应的通道
        if (haveChannels){
            for (Channel ch: channels){
                ch.writeAndFlush(new TextWebSocketFrame(message));
            }
        }
        return haveChannels;
    }
}

3.8 Netty 启动类

此处 Netty作为单独服务器启动,与SpringBoot使用端口并不一致

通过实现CommandLineRunner接口和添加@Component注解,使启动Netty服务器在项目启动之后执行

Spring boot的CommandLineRunner接口主要用于实现在应用初始化后,去执行一段代码块逻辑,这段初始化代码在整个应用生命周期内只会执行一次。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * @Author: ldddd
 * @Date: 2022/1/15 14:55
 */
@Order(1)
@Component
public class NettyStart implements CommandLineRunner {
    @Autowired
    private NettySocketServer nettySocketServer;
    @Override
    public void run(String... args) throws Exception {
        nettySocketServer.start();
    }
}

总结

以上就是今天要讲的内容,本文介绍了Netty的使用,Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。我们可以快速使用此类方法进行使用开发自己的网络服务器。

之后我将写一篇文章,介绍如何使用Netty 实现 通过微信小程序 在网页上进行电子签字的功能。

你可能感兴趣的:(Spring,Netty,netty,spring,boot,java,websocket,socket)