Netty学习笔记_9(Netty概述)

一、原生NIO存在的问题

  1. NIO的类库与API繁杂,需要熟练掌握Selector、ServerSocketChannel、SocketChannel、Bytebuffer等
  2. 要求熟悉Java多线程编程和网络编程
  3. 开发工作量和难度大,例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等
  4. JDK NIO的BUG:例如Epoll Bug,它会导致Selector空轮询,最终导致CPU占用100%,到JDK1.7还未被有效解决

二、Netty说明

Netty学习笔记_9(Netty概述)_第1张图片

 

  1. Netty是由JBOSS提供的一个Java开源框架。Netty提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠的网络IO程序
  2. Netty可以帮助你快速、简单的开发一个网络应用,相当于简化和流程化了NIO的开发过程
  3. Netty是目前最流行的NIO框架,在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,Elasticsearch、Dubbo框架内部都采用了Netty

三、Netty线程模型介绍

       1、线程模型分类:【1】传统阻塞I/O服务模型;【2】Reactor模式(反应器模型)

       2、3种典型的Reactor(根据Reactor数量和处理资源线程池数量不同):

              1)单Reactor单线程

             2)单Reactor多线程

             3)主从Reactor多线程

      3、Netty线程模式——主要基于主从Reactor(有多个Reactor)多线程模型做了一定的改进

3-1、传统阻塞IO服务模型

Netty学习笔记_9(Netty概述)_第2张图片

    1、模型特点

  1. 采用阻塞IO获取输入的数据
  2. 每个连接都需要独立的线程完成数据的输入、业务处理、数据返回

    2、问题分析

  1. 当并发数很大时,会创建大量的线程,占用很大的系统资源
  2. 连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在read操作上,造成线程资源浪费

    3、解决方案

  1. 基于IO复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞所有连接,当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理
  2. 基于线程池复用线程资源:不必为每一个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务

3-2、Reactor模式(反应器、分发者、通知者)

1、工作原理及说明

Netty学习笔记_9(Netty概述)_第3张图片

 

  1. Reactor模式,通过一个或多个输入同时传递给服务器处理的模式(基于事件驱动)
  2. 服务器端程序处理传入的多个请求,并将它们同步分派到相应的处理线程
  3. Reactor模式使用了IO复用监听事件,受到事件后分发给某个线程(进程),网络服务高并发处理的关键

2、核心组成

  1. Reactor:在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序对IO事件作出反应
  2. Handlers:处理程序执行IO事件要完成的实际事件。Reactor通过调用适当的处理程序来响应IO事件,处理程序非阻塞操作

3-2-1、单Reactor单线程

1、工作原理及说明

Netty学习笔记_9(Netty概述)_第4张图片

 

  • Select是前面IO复用模型介绍的标准网络编程API,可以实现应用程序通过一个阻塞对象监听多路连接请求;
  • Reactor对象通过Select监控客户端请求事件,收到事件后通过Dispatch进行分发;
  • 如果建立连接请求事件,则由Acceptor通过Accept处理连接请求,然后创建一个Handler对象处理连接完成后的后续业务处理;
  • 如果不是建立连接事件,则Reactor会分发给调用连接对应的Handler来响应;
  • Handler会完成Read—>业务处理—>Send的完整业务流程;

2、优缺点分析

  • 优点:模型简单,无多线程、进程通信、竞争的问题,全部由一个线程完成;
  • 缺点:性能问题,只有一个线程无法发挥出多核CPU的性能,Handler在处理某连接业务时,整个进程无法处理其他连接事件,容易导致性能瓶颈;
  • 缺点:可靠性问题,线程意外中止,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部信息,节点故障;
  • 使用场景:客户端数量有限,业务处理快捷(例如 Redis在业务处理的时间复杂度为O(1)的情况)

3-2-2、单Reactor多线程

1、工作原理及说明

  • Reactor通过Select监控客户端请求事件,收到事件后,通过dispatch进行分发;
  • 如果是建立连接的请求,则由Acceptor通过accept处理连接请求,同时创建一个handler处理完成连接后的后续请求;
  • 如果不是连接请求,则由Reactor分发调用连接对应的handler来处理;
  • Handler只负责响应事件,不做具体的业务处理,通过read读取数据后,会分发给后面的worker线程池中的某个线程处理业务
  • Worker线程池会分配独立的线程处理真正的业务,并将结果返回给Handler
  • Handler收到响应后,通过send方法将结果反馈给client

2、优缺点分析

  • 优点:可以充分的利用多核CPU的处理能力;
  • 缺点:多线程数据共享、访问操作比较复杂,reactor处理所有的事件的监听和响应,在单线程运行,在高并发场景容易出现性能瓶颈

3-2-3、主从Reactor多线程

1、工作原理及说明

Netty学习笔记_9(Netty概述)_第5张图片

  • Reactor主线程MainReactor对象通过select监听连接事件,收到事件后,通过Acceptor处理连接事件;
  • 当Acceptor处理连接事件后,MainReactor将创建好的连接分配给SubReactor;
  • SubReactor将连接加入到连接队列进行监听,并创建Handler进行各种事件处理;
  • 当有新事件发生时,SubReactor调用对应的Handler进行处理
  • Handler通过read读取数据,分发给后面的Worker线程池处理
  • Worker线程池会分配独立的Worker线程进行业务处理,并将结果返回
  • Handler收到响应结果后,通过send方法将结果返回给client

2、优缺点分析

  • 优点:父线程和子线程的职责明确,父线程只需要接收新连接,子线程完成后续业务处理;
  • 优点:父线程与子线程的数据交互简单,Reactor主线程是需要把新连接传给子线程,子线程无需返回数据
  • 缺点:编程复杂度较高

3-2-4、Reactor模式小结

  1. 响应快,虽然Reactor本身是同步的,但不必为单个同步事件所阻塞
  2. 最大程度的避免了复杂的多线程及同步问题,避免了多线程/进程的切换开销
  3. 扩展性好,可以方便的通过增加Reactor势力个数充分利用CPU资源
  4. 复用性好,Reactor模型本身与具体事件处理逻辑无关,具有很高的复用性

3-3、Netty模型

3-3-1、工作原理及说明

Netty学习笔记_9(Netty概述)_第6张图片

  1. Netty抽象出两组线程池:BossGroup专门负责接收客户端的连接;WorkerGroup专门负责网络的读写
  2. BossGroup和WorkerGroup的类型都是NioEventLoopGroup
  3. NioEventLoopGroup相当于一个事件循环祖,组中含有多个事件循环,每一个事件循环是NioEventLoop
  4. NioEventLoop表示一个不断循环的执行任务的过程,每个NioEventLoop都有一个selector,用于监听绑定在其上的socket的网络通信
  5. NioEventLoopGroup可以含有多个线程,即可以含有多个NioEventLoop
  6. 每个boss NioEventLoop执行的步骤有3步

    【1】轮询accept事件

    【2】处理accept事件,与client建立连接,生成NioSocketChannel,并将其注册到某个worker NioEventLoop上的selector

    【3】处理任务队列的任务,即runAllTasks

  7. 每个worker的NioEventLoop循环执行的步骤:

    【1】轮询read,write事件

    【2】处理IO事件,在对应的NioSocketChannel处理

    【3】处理任务队列的任务,即runAllTasks

  8. 每个worker NioEventLoop处理业务时,会使用PipeLine(管道),pipeline中包含了channel,即通过pipeline可以获取对应通道,通道中维护了很多处理器

3-3-2、案例分析

1、要求:

Netty服务器在6668端口监听,客户端能发送消息给服务器“hello,服务器”|| 服务器可以回复消息给客户端“hello,客户端”

2、设计步骤:

  1. 在项目中导入Netty开发需要的包:netty-all-xxx.Final.jar
  2. 编写服务器端及服务器业务处理器
    import com.SF.NIO.NIOServer;
    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.ChannelOption;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.channel.socket.nio.NioServerSocketChannel;
    import io.netty.channel.socket.nio.NioSocketChannel;
    
    public class NettyServer {
        public static void main(String[] args) throws InterruptedException {
    
            //创建BossGroup 和 WorkerGroup
            /**
             * 说明
             * 1、创建两个线程组 bossGroup 和 workerGroup
             * 2、bossGroup 只是处理连接请求,真正与客户端的业务处理交给 workerGroup 完成
             * 3、两个都是无限循环
             * 4、bossGroup 和 workerGroup 含有的子线程(NioEventLoop)的个数
             *    默认以 CPU 内核数*2
             * */
            EventLoopGroup bossGroup = new NioEventLoopGroup(1);
            EventLoopGroup workerGroup = new NioEventLoopGroup();
    
    
            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() { //创建一个通道初始化对象
                            //给 pipeline 设置处理器
                            @Override
                            protected void initChannel(SocketChannel ch) throws Exception {
                                ch.pipeline().addLast(new NettyServerHandler());
                            }
                        }); //给我们的 workerGroup 的EventLoop 对应的管道设置处理器
    
                System.out.println("服务器 is ready !!!");
    
                //绑定一个端口并同步,生成一个ChannelFuture对象
                //启动服务器并绑定端口
                ChannelFuture cf = bootstrap.bind(6668).sync();
    
                //对关闭通道进行监听
                cf.channel().closeFuture().sync();
            }finally {
                bossGroup.shutdownGracefully();
                workerGroup.shutdownGracefully();
            }
        }
    }
    
    import io.netty.buffer.ByteBuf;
    import io.netty.buffer.Unpooled;
    import io.netty.channel.Channel;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.ChannelInboundHandlerAdapter;
    import io.netty.channel.ChannelPipeline;
    import io.netty.util.CharsetUtil;
    
    import java.nio.charset.Charset;
    
    /**
     * 说明
     * 1、我们自定义的 handler 需要继承 netty 规定的某一个 HandlerAdapter
     * 2、这时我们自定义的一个 handler 才能称为一个 handler
     * */
    public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    
        //读取数据的事件(这里我们可以读取客户端发送的消息)
        /**
         * 1、ChannelHandlerContext ctx:上下文对象,含有管道pipeline,通道channel,地址
         * 2、Object msg:即客户端发送的数据,默认是Object
         * */
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    //        super.channelRead(ctx, msg);
            System.out.println("服务器读取线程 "+Thread.currentThread().getName());
            System.out.println("server ctx = "+ctx);
            System.out.println("**********************************************************");
    
            Channel channel = ctx.channel();
            ChannelPipeline pipeline = ctx.pipeline(); //本质是一个双向连表
    
            //将msg转成一个byteBuf
            //ByteBuf 是由 Netty 提供的,不是 NIO 的 ByteBuffer
            ByteBuf buf = (ByteBuf) msg;
            System.out.println("客户端发送消息是:"+buf.toString(CharsetUtil.UTF_8));
            System.out.println("客户端地址:"+ctx.channel().remoteAddress());
        }
    
        //数据读取完毕
        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
    //        super.channelReadComplete(ctx);
            //将数据写入到缓冲并刷新
            //一般将发送的数据进行编码
            ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端~",CharsetUtil.UTF_8));
        }
    
        //处理异常,一般需要关闭通道
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    //        super.exceptionCaught(ctx, cause);
            ctx.close();
        }
    }
    

     

3-3-3、Netty模型小结

  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

你可能感兴趣的:(Netty,笔记)