Springboot集成Netty实现WebSocket

927 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的9天,点击查看活动详情

背景

在 SpringBoot 项目中,SpringBoot 内置的 Tomcat 服务器一般占有的端口是8080端口,而要在该项目中运行 netty 服务器肯定不能占用8080端口,需要在该项目中再开启另外一个端口,作为 netty 服务器的访问端口。

而netty内部使用的是 nio,通过 Selector 管理多个 Channel 实现多路复用,但如果在开启 SpringBoot 项目的过程中,netty服务器先于Tomcat服务器开启同时没有产生事件让selector去执行,就会造成 netty 服务器卡住 Tomcat服务器即Tomcat服务器不能被启动。

整合

环境

Netty的依赖

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

SpringBoot版本

<spring-boot.version>2.3.7.RELEASE</spring-boot.version>

启动Netty服务器

  1. 在项目中创建一个 Netty 包,实现 WebSocketServer
@Slf4j
@Component
public class WebSocketServer {
    /**
     * 端口号
     */
    @Value("${netty.websocketPort}")
    private int port;

    /**
     * 线程组1 接收 accept事件
     */
    private EventLoopGroup bossGroup = new NioEventLoopGroup();

    /**
     * 线程组 2 接收 读写事件
     */
    private EventLoopGroup workerGroup = new NioEventLoopGroup();

    /**
     * 通信channel
     */
    private Channel channel;

    /**
     * 异步开启 netty 服务器
     */
    @Async
    public void start() {
        ServerBootstrap serverBootstrap = new ServerBootstrap();

        serverBootstrap.group(bossGroup, workerGroup)
                .handler(new LoggingHandler(LogLevel.DEBUG))
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {

                        // http编解码器
                        nioSocketChannel.pipeline().addLast(new HttpServerCodec());
                        // 添加块处理器
                        nioSocketChannel.pipeline().addLast(new ChunkedWriteHandler());

                        /**
                         * http 数据在传输过程中是分段的,HttpObjectAggregator 可以将多段进行聚合
                         * 这就是为什么,当浏览器发送大量请求的时候,会产生多次 http 请求
                         */
                        nioSocketChannel.pipeline().addLast(new HttpObjectAggregator(8192));

                        /**
                         * 1. 对应 websocket,它的数据是以帧的形式传递
                         * 2. 浏览器请求时,ws://localhost:7000/hello 表示请求的uri
                         * 3. WebSocketServerProtocolHandler 将 http协议升级为 ws 协议
                         */
                        nioSocketChannel.pipeline().addLast(new WebSocketServerProtocolHandler("/hello"));

                        // 自定义的handler,处理业务逻辑
                        nioSocketChannel.pipeline().addLast(new WebSocketServerHandler());
                    }
                });

        try {

            // 开启服务器
            ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
            // 获取到channel
            channel = channelFuture.channel();
            log.info("服务器已经启动,{}", channel.localAddress());
            // 等待关闭服务器
            channel.closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 优雅关闭服务器
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public void stop() {
        log.info("Netty WebSocket服务器已关闭");
        // 优雅的关闭EventLoopGroup,释放所有的资源
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }
}

基础的 netty 配置这里就不详细说了,具体要注意的是 start 方法要加上 @Async 注解,表示异步开启netty服务器, 这样就不会有netty服务器阻塞Tomcat服务器的情况

使用@Async要同时在启动类中加入@EnableAsync 才会生效

  1. 在config包下创建 WebSocketConfig 文件
@Component
public class WebSocketConfig {

    @Value("${netty.startWithSpring}")
    private boolean autoStartWebsocket;

    @Resource
    private WebSocketServer webSocketServer;


    /**
     * 在创建完所有的 bean 后自动调用
     */
    @PostConstruct
    public void start() {
        if (autoStartWebsocket) {
            webSocketServer.start();
        }
    }

    /**
     * 在销毁所有的 bean 之前调用
     */
    @PreDestroy
    public void close() {
        if (autoStartWebsocket) {
            webSocketServer.stop();
        }
    }
}

这里的 start 方法加上了 @PostConstruct 注解,表示在创建完所有 bean 之后调用该方法,因为要先创建 WebSocketServer 然后再调用 WebSocketServer 的 start 方法,就可以异步创建 Netty 服务器

基于Netty 实现 WebSocket

还是之前的代码WebSocketServer中的代码

  1. 要加入 http 编解码器,因为 websocket 协议是基于 http

nioSocketChannel.pipeline().addLast(new HttpServerCodec());

  1. 添加 块处理器

nioSocketChannel.pipeline().addLast(new ChunkedWriteHandler());

  1. 添加 HttpObjectAggregator
  • http 数据在传输过程中是分段的,HttpObjectAggregator 可以将多段进行聚合

nioSocketChannel.pipeline().addLast(new HttpObjectAggregator(8192));

  1. 添加 WebSocketServerProtocolHandler 处理器
  • 对应 websocket,它的数据是以帧的形式传递
  • 浏览器请求时,ws://localhost:7000/hello 表示请求的uri
  • WebSocketServerProtocolHandler 将 http协议升级为 ws 协议

nioSocketChannel.pipeline().addLast(new WebSocketServerProtocolHandler("/hello"));

  1. 最后添加自己的业务处理 handler 即可

最后就可以在SpringBoot项目中启动Netty服务器