Netty-尚硅谷(5. Netty模型)学习笔记

上一篇 :4. 线程模型基本介绍

下一篇 :6. 异步模型和HTTP示例

1. Netty 工作原理示意图

  • Netty 主要基于主从 Reactors 多线程模型做了一定的改进,其中主从 Reactor 多 线程模型有多个 Reactor

1.1 简单版

在这里插入图片描述

  • 说明 :
  1. BossGroup 线程维护Selector , 只关注Accecpt
  2. 当接收到Accept事件,获取到对应的 SocketChannel, 封装成 NIOScoketChannel并注册到 Worker 线程(事件循环), 并进行维护
  3. 当Worker线程监听到 Selector 中通道发生自己感 兴趣的事件后,就进行处理(就由 Handler 处理), 注意 Handler 已经加入到通道

1.2 进阶版

在这里插入图片描述

1.3 详细版

在这里插入图片描述

  • 说明 :
  1. Netty 抽象出两组线程池 BossGroup 专门负责接收客户端的连接, WorkerGroup 专门负责网络的读写
  2. BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup
  3. NioEventLoopGroup 相当于一个事件循环组, 这个组中含有多个事件循环 ,每一个事件循环是 NioEventLoop
  4. NioEventLoop 表示一个不断循环的执行处理任务的线程, 每个 NioEventLoop 都有一个 Selector , 用于监听绑定在其上的 Socket 的网络通讯
  5. NioEventLoopGroup(BossGroup、WorkerGroup) 可以有多个线程, 即可以含有多个 NioEventLoop
  6. 每个Boss 的 NioEventLoop 循环执行的步骤有3步
    1. 轮询accept 事件
    2. 处理accept 事件 , 与client建立连接 , 生成NioScocketChannel , 并将其注册到 Worker 的 NIOEventLoop 上的 Selector
    3. 处理任务队列的任务 , 即 runAllTasks
  7. 每个 Worker 的 NIOEventLoop 循环执行的步骤
    1. 轮询read, write 事件
    2. 处理i/o事件, 即read , write 事件,在对应NioScocketChannel 处理
    3. 处理任务队列的任务 , 即 runAllTasks
  8. 每个Worker NIOEventLoop 处理业务时,会使用 Pipeline(管道), Pipeline 中包含了 Channel , 即通过 Pipeline 可以获取到对应通道, 管道中维护了很多的处理器。管道可以使用 Netty 提供的,也可以自定义

2. Netty快速入门实例-TCP服务

  • 要求
  1. Netty 服务器在 6668 端口监听,客户端能发送消息给服务器 “hello, 服务器~”
  2. 服务器可以回复消息给客户端 “hello, 客户端~”
  • 代码实现
  1. 引入依赖

    <!--netty依赖-->
    <dependency>
      <groupId>io.netty</groupId>
      <artifactId>netty-all</artifactId>
      <version>4.1.52.Final</version>
    </dependency>
    
  2. 编写 Netty 的服务端 :NettyServer

    public class NettyServer {
        public static void main(String[] args) throws InterruptedException {
            // 创建 BossGroup 和 WorkerGroup
            /*
                说明
                1. 创建两个线程组 BossGroup 和 WorkerGroup
                2. BossGroup 只处理连接请求
                3. WorkerGroup 处理真正客户端的业务
                4. 运行时,这两个都是无限循环
             */
            EventLoopGroup bossGroup = new NioEventLoopGroup();
            EventLoopGroup workerGroup = new NioEventLoopGroup();
    
            // 进行异常处理,try - catch
            try {
                // 创建 服务端 启动对象,并配置参数
                ServerBootstrap bootstrap = new ServerBootstrap();
                // 使用链式编程进行配置参数
                bootstrap.group(bossGroup, workerGroup)// 设置两个线程组
                        .channel(NioServerSocketChannel.class)// 使用 NioServerSocketChannel 作为服务器的通道实现
                        .option(ChannelOption.SO_BACKLOG, 128)// 设置线程队列等待连接的个数
                        .childOption(ChannelOption.SO_KEEPALIVE, true)// 设置连接保持活动连接状态
                        .childHandler(new ChannelInitializer<SocketChannel>() {// 给 workerGroup 的 NioEventLoop 对应的管道(Pipeline)设置处理器
                            // 创建一个通道初始化对象
                            /**
                             * 向 workerGroup 对应的 管道(Pipeline) 设置处理器
                             *
                             * @param socketChannel
                             * @throws Exception
                             */
                            @Override
                            protected void initChannel(SocketChannel socketChannel) throws Exception {
                                socketChannel.pipeline()// 获得 这个 socketChannel 对应的 Pipeline
                                        .addLast(new NettyServerHandler());// 把自定义的 Handler 添加到 管道
                            }
                        });
                System.out.println("服务器准备好了……");
                // 绑定一个端口,并且同步。生成了一个 ChannelFuture 对象
                // 这里就已经启动了服务器
                ChannelFuture channelFuture = bootstrap.bind(6668).sync();
                // 对 关闭通道 进行监听
                // 这里只是监听,只有关闭通道时才进行处理,这句话不是直接关闭了通道
                channelFuture.channel().closeFuture().sync();
            }finally {
                bossGroup.shutdownGracefully();
                workerGroup.shutdownGracefully();
            }
    
    
        }
    }
    
  3. 编写 自定义的 Netty 服务端处理器 :NettyServerHandler

    /**
     * 1. 自定义一个 Handler 需要继承 Netty 规定好的某个 处理器适配器
     * 2. 这时自定义的 Handler ,才能称为一个 Handler
     */
    public class NettyServerHandler extends ChannelInboundHandlerAdapter {
        /** 读取数据的事件(可以读取客户端发送的消息)
         *
         * @param ctx 上下文对象,包含 管道、通道、地址
         * @param msg 客户端发送的消息,默认是 Object 类型
         * @throws Exception
         */
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println("【Server】: ctx" + ctx);
            // 将 msg 转换成 ByteBuffer
            /*
                说明 :
                1. 注意这个是 ByteBuf ,是 io.netty.buffer 包下的,不是 NIO 下的 Buffer
                2. ByteBuf 比 Buffer 的性能更高一点
             */
            ByteBuf buf = (ByteBuf) msg;
            // 把 buf 转成 UTF8 格式的字符串
            System.out.println("客户端发送的 msg :" + buf.toString(CharsetUtil.UTF_8));
            System.out.println("客户端地址 :" + ctx.channel().remoteAddress());
        }
    
        /**
         * 数据读取完毕后,返回消息给客户端
         * @param ctx
         * @throws Exception
         */
        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            // 把数据写入缓冲区,并刷新缓冲区
            // 一般来说,需要对这个发送的消息进行编码
            ctx.writeAndFlush(Unpooled.copiedBuffer("Hello,客户端",CharsetUtil.UTF_8));
        }
    
        /**
         * 处理异常
         * @param ctx
         * @param cause
         * @throws Exception
         */
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            // 关闭通道
            ctx.channel().close();
        }
    }
    
  4. 编写 Netty 的客户端 :NettyClient

    public class NettyClient {
        public static void main(String[] args) throws InterruptedException {
            // 客户端需要一个事件循环组
            EventLoopGroup group = new NioEventLoopGroup();
    
            try {
    
                // 客户端启动对象 —— Bootstrap ,不是 服务端的 ServerBootstrap
                // 并且是 io.netty.bootstrap 包下的
                Bootstrap bootstrap = new Bootstrap();
    
                // 设置相关参数
                bootstrap.group(group)// 设置线程组
                        .channel(NioSocketChannel.class)// 设置客户端通道的实现类
                        .handler(new ChannelInitializer<SocketChannel>() {// 设置处理器
                            @Override
                            protected void initChannel(SocketChannel socketChannel) throws Exception {
                                socketChannel.pipeline().addLast(new NettyClientHandler());// 加入自己的处理器
                            }
                        });
                System.out.println("客户端准备好了……");
    
                // 启动客户端连接服务器端
                // 这里涉及到一个 Netty 的异步模型,后面详述
                ChannelFuture channelFuture = bootstrap.connect("localhost", 6668).sync();
                // 对关闭通道进行监听
                channelFuture.channel().closeFuture().sync();
            }finally {
                group.shutdownGracefully();
            }
    
        }
    }
    
  5. 编写 自定义的 Netty 客户端处理器 :NettyClientHandler

    public class NettyClientHandler extends ChannelInboundHandlerAdapter {
        /**
         * 当通道就绪时,就会触发该方法,就可以发信息了
         * @param ctx
         * @throws Exception
         */
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("【Client】:ctx" + ctx);
            ctx.writeAndFlush(Unpooled.copiedBuffer("Hello,server", CharsetUtil.UTF_8));
        }
    
        /**
         * 当通道有读取事件时 ,会触发
         * @param ctx
         * @param msg
         * @throws Exception
         */
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            ByteBuf buf = (ByteBuf) msg;
            System.out.println("服务器发送的 msg :" + buf.toString(CharsetUtil.UTF_8));
            System.out.println("服务器的地址 :"+ ctx.channel().remoteAddress());
        }
    
        /**
         * 异常处理
         * @param ctx
         * @param cause
         * @throws Exception
         */
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
            ctx.channel().close();
        }
    }
    
  • 启动测试

    先启动 NettyServer,在启动 NettyClient

    NettyServer 的控制台:
    在这里插入图片描述

    NettyClient 的控制台:
    在这里插入图片描述

3. 对‘Netty快速入门实例’的分析

  • 通过分析 ‘Netty快速入门实例’ 的执行流程,来熟悉并加深对 Netty模型 的理解

3.1 BossGroup 和 WorkGroup 怎么确定自己有多少个 NIOEventLoop

  1. 点进 NioEventLoopGroup
    在这里插入图片描述
    在这里插入图片描述

  2. 一直点进 this,一直进入父类
    在这里插入图片描述
    在这里插入图片描述

  3. 找到这个值的初始化代码
    在这里插入图片描述

  • 所以 BossGroup 和 WorkerGroup 含有的子线程数(NioEventLoop)默认为 CPU 核数*2

  • 在此处打一个断点,进行 Debug
    在这里插入图片描述

    在这里插入图片描述

  • 由源码中的构造方法可知 —— 想要设置线程数只要在参数中输入即可

3.2 WorkerGroup 是如何分配这些进程的

  • 设置 BossGroup 进程数为 1 ; WorkerGroup 进程数为 4 ; Client 数位 8

  • 设置进程数量
    在这里插入图片描述

  • 输出线程信息,在 NettyServerHandler 中添加
    在这里插入图片描述

  • 启动服务端、和4个客户端
    在这里插入图片描述

  • 启动第五个客户端会分配到哪个线程?
    在这里插入图片描述
    又回到了第一个线程,在默认情况下,WorkerGroup 分配的逻辑就是按顺序循环分配的

3.3 BossGroup 和 WorkerGroup 中的 Selector 和 TaskQueue

  • 打断点进行 Debug
    在这里插入图片描述
    在这里插入图片描述
  • 每个子线程都具有自己的 Selector、TaskQueue……

3.4 CTX 上下文、Channel、Pipeline 之间关系

  • 修改 NettyServerHandler ,并添加端点
    在这里插入图片描述
  • 先看 CTX 上下文中的信息
    在这里插入图片描述
    上下文中还有很多其他信息,就不一样列举了
  • Pipeline
    在这里插入图片描述
  • Channel
    在这里插入图片描述
  • CTX 上下文、Channel、Pipeline 三者关系示意图
    在这里插入图片描述

4. TaskQueue 任务队列

4.1 概述

  • 如果在某个 Handler 中有个长时间的操作,那势必会造成 Pipeline管道 的阻塞,那么这种情况就需要把这些任务提交到 TaskQueue 进行异步执行

  • TaskQueue 和 Channel 有绑定的关系

  • 任务队列中的 Task 有 3 种典型使用场景

  1. 用户程序自定义的普通任务
  2. 用户自定义定时任务
  3. 非当前 Reactor 线程调用 Channel 的各种方法

4.2 体验任务的阻塞

  1. 编写一个 服务端的 Handler :NettyServerHandlerTaskQ

    public class NettyServerHandlerTaskQ extends ChannelInboundHandlerAdapter {
        /** 读取数据的事件(可以读取客户端发送的消息)
         *
         * @param ctx 上下文对象,包含 管道、通道、地址
         * @param msg 客户端发送的消息,默认是 Object 类型
         * @throws Exception
         */
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            // 比如这里有一个非常耗时的任务,希望可以异步执行
            // 把该任务提交到 Channel 对应的 NIOEventLoop 的 TaskQueue 中
            Thread.sleep(10 * 1000);
            ctx.writeAndFlush(Unpooled.copiedBuffer("Hello,客户端,这是一个执行耗时长的任务",CharsetUtil.UTF_8));
            System.out.println("耗时长的任务执行完毕,继续");
        }
    
        /**
         * 数据读取完毕后,返回消息给客户端
         * @param ctx
         * @throws Exception
         */
        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            ctx.writeAndFlush(Unpooled.copiedBuffer("Hello,客户端",CharsetUtil.UTF_8));
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            // 关闭通道
            ctx.channel().close();
        }
    }
    
  2. 修改 NettyServer 中添加到 Pipeline 的 Handler
    在这里插入图片描述

  3. 先这样执行一个查看效果(没有添加到任务队列)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

4.3 TaskQueue 使用场景-1

  • 用户程序自定义的普通任务

  • 上面自定义了一个执行时间长的任务,该任务会阻塞这个 EventLoop 线程,可以使用该方法来解决耗时长的任务的阻塞问题

  • 修改 NettyServerHandlerTaskQ 中的 channelRead

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    // 解决方案-1:用户程序自定义的普通任务
        ctx.channel().eventLoop().execute(()->{
            try {
                Thread.sleep(10 * 1000);
                ctx.writeAndFlush(Unpooled.copiedBuffer("Hello,客户端,这是一个执行耗时长的任务,方案-1",CharsetUtil.UTF_8));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println("耗时长的任务执行完毕,继续");
    }
    
  • 运行测试
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  • 这也就能看出任务队列的异步执行了

  • 下断点,查看 TaskQueue

    在这里插入图片描述

    查看 ctx -> pipeline -> channel -> eventloop -> taskqueue
    在这里插入图片描述
    可以看到这里面有刚刚的 Handler

4.4 TaskQueue 使用场景-2

  • 用户自定义定时任务
  • 修改 NettyServerHandlerTaskQ 中的 channelRead
     @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 解决方案-2:用户自定义定时任务
            // 把任务提交到 scheduledTaskQueue
            // 在和服务端连接成功后 5s 开始异步执行 run 方法
            ctx.channel().eventLoop().schedule(()->{
                try {
                    Thread.sleep(5 * 1000);
                    ctx.writeAndFlush(Unpooled.copiedBuffer("Hello,客户端,这是一个执行耗时长的任务,方案-2",CharsetUtil.UTF_8));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, 5, TimeUnit.SECONDS);
            
        	System.out.println("耗时长的任务执行完毕,继续");
        }
    
  • 运行测试
    在这里插入图片描述
    在这里插入图片描述
  • 下断点,查看 scheduledTaskQueue 中是否多了任务
    在这里插入图片描述

查看 ctx -> pipeline -> channel -> eventloop -> scheduledTaskQueue
在这里插入图片描述

4.5 TaskQueue 使用场景-3

  • 非当前 Reactor 线程调用 Channel 的各种方法

  • 例如:在推送系统的业务线程里面,根据用户的标识,找到对应的 Channel 引用,然后调用 Write 类方法向该用户推送消息,就会进入到这种场景。最终的 Write 会提交到任务队列中后被异步消费

  • 该场景就不演示了,只简单说一下思路

  1. 在用户连接到服务端的时候,可以获取到该客户对应的 SocketChannel 的 HashCode
    在这里插入图片描述

  2. 可以在服务端维护一个 集合 ,存放所有连接的 SocketChannel

  3. 在有消息需要推送的时候,就遍历这个 SocketChannel 集合,通过 HashCode 找到 Channel

  4. 通过 Channel 找到 EventLoop

  5. 再往后就是上面提到的任务的处理了

5. 总结

  1. Netty 抽象出两组线程池,BossGroup 专门负责接收客户端连接,WorkerGroup 专门负责网络读写操作。

  2. NioEventLoop 表示一个不断循环执行处理任务的线程,每个 NioEventLoop 都有一个 selector,用于监听绑定在其上的 socket 网络通道

  3. NioEventLoop 内部采用串行化设计,从消息的 读取->解码->处理->编码->发送,始终由 IO 线程 NioEventLoop 负责

    • NioEventLoopGroup 下包含多个 NioEventLoop
    • 每个 NioEventLoop 中包含有一个 Selector,一个 taskQueue
    • 每个 NioEventLoop 的 Selector 上可以注册监听多个 NioChannel
    • 每个 NioChannel 只会绑定在唯一的 NioEventLoop 上
    • 每个 NioChannel 都绑定有一个自己的 ChannelPipeline

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yuan_404

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值