• Netty高性能架构设计


    一、Netty概述

    (一)Netty是什么

        Netty 是由 JBOSS 提供的⼀个 Java 开源框架,现为 Github 上的独⽴项⽬。
        Netty 是⼀个异步的、基于事件驱动的⽹络应⽤框架,⽤以快速开发⾼性能、⾼可靠性的⽹络 IO程序。
        Netty 主要针对在 TCP 协议下,⾯向 Client 端的⾼并发应⽤,或者 Peer-to-Peer 场景下的⼤量数据持续传输的应⽤。
        Netty 本质是⼀个 NIO 框架,适⽤于服务器通讯相关的多种应⽤场景。

    (二)为什么要用Netty

      1、原⽣NIO存在的问题

      (1)NIO跨平台和兼容性问题:NIO是底层API,它的实现依赖于操作系统针对IO操作的APIs.所以java能在所有操作系统上实现统⼀的接⼝,并⽤⼀致的⾏为来操作IO是很伟⼤的。使⽤NIO会经常发现代码在Linux上正常运⾏,但在Windows上就会出现问题。所以编写程序,特别是NIO程序,需要在程序⽀持的所有操作系统上进⾏功能测试,否则你可能会碰到⼀些莫明的问题。NIO2看起来很理想,但是NIO2只⽀持Jdk1.7+,若你的程序在Java1.6上运⾏,则⽆法使⽤NIO2。另外,Java7的NIO2中没有提供DatagramSocket的⽀持,所以NIO2只⽀持TCP程序,不⽀持UDP程序

      (2)NIO对缓冲区的聚合和分散操作可能会导致内存泄露分散Scattering对应缓冲区写⼊:通道(Channel)向缓冲区数组中写出数据,按照索引从第0个缓冲区(Buffer)开始,依次写⼊数据;聚合Gathering对应缓冲区读取:通道(Channel)从缓冲区数组中读取数据,按照索引从第0个缓冲区(Buffer)开始,依次读取数据;分散Scattering与聚合Gathering都是通道(Channel)对缓冲区数组(Buffer[])进⾏读写的操作;

      (3)NIO的类库和API繁杂,使⽤麻烦。需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。

      (4)需要具备其他的额外技能:要熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和⽹络编程⾮常熟悉,才能编写出⾼质量的NIO程序。

      (5)开发⼯作量和难度都⾮常⼤:例如客户端⾯临断连重连、⽹络闪断、半包读写、失败缓存、⽹络拥塞和异常流的处理等等。

      (6)JDK中NIO的Bug:著名的epoll-bug也可能会导致⽆效的状态选择和100%的CPU利⽤率。(例如使用selector.select();时,就不会阻塞,以下面的代码为例,就会一直打印输出,直到耗尽CPU资源)

           while (true) {
                try {
                    // 这里发生的是,不管有没有已选择的SelectionKey,Selector.select()方法总是不会阻塞并且会立刻返回。
                    // 这违反了Javadoc中对Selector.select()方法的描述,
                    // Javadoc中的描述:Selector.select() must not unblock if nothing is selected.
                    // (Selector.select()方法若未选中任何事件将会阻塞。)
                    System.out.println(".............");
                    selector.select();
                }
                catch (IOException ex) {
                    ex.printStackTrace();
                    // handle in a proper way
                    break;
                }

      2、为什么⽤Netty

        (1)快,Netty正是基于NIO实现了这种Reactor模型,Boss线程⽤来专⻔处理连接的建⽴,SubReactor专⻔⽤来处理IO的读写以及任务的处理。这种线程模型在充分利⽤CPU性能的情况下⽀撑⼤量的并发连接

        (2)内存使⽤少,⽹络数据传输⾯临着⼤量的对象创建和销毁,Netty主要从两个⽅⾯缓解JVM的压⼒

          ByteBufAllocator对象池。池化ByteBuf实例以提⾼性能并最⼩化内存碎⽚,后者每次调⽤时都返回⼀个新的实例

          零拷⻉。⽀持DirectBuffer的使⽤,通过JVM的本地调⽤分配内存,这可避免每次调⽤本地I /O操作之前(或之后)将缓冲区的内容复制到(或从)中间缓冲区

        (3)API简单,⽹络编程⼀般都⽐较复杂,更⾯临着IO读写以及线程安全问题问题要处理,Netty针对这些问题做了⼤量封装,使API更简单易⽤。基于事件模式,对⽹络事件进⾏串⾏化处理,在保证⾼效的同时,⼜降低了编程的复杂度

        (4)Netty⾮常稳定,⼀般我们遇到的NIO的select空转,TCP断线重连,keep-alive检测等问题,Neety都已解决

    (三)Netty版本和应用场景

      1、Netty 版本说明

        Netty 版本分为 Netty 3.x 和 Netty 4.x、Netty 5.x,因为 Netty 5 出现重⼤ bug ,已经被官⽹废弃了,⽬前推荐使⽤的是 Netty 4.x 的稳定版本。⽬前在官⽹可下载的版本 Netty 3.x 、 Netty 4.0.x 和 Netty 4.1.x。Netty 下载地址:https://bintray.com/netty/downloads/netty/

      2、Netty的应⽤场景

      (1)互联⽹⾏业:在分布式系统中,各个节点之间需要远程服务调⽤,⾼性能的 RPC 框架必不可少, Netty 作为异步⾼性能的通信框架,往往作为基础通信组件被这些 RPC 框架使⽤。典型的应⽤有:阿⾥分布式服务框架 Dubbo 的 RPC 框架使⽤ Dubbo 协议进⾏节点间通信,Dubbo 协议默认使⽤ Netty 作为基础通信组件,⽤于实现各进程节点之间的内部通信。

      (2)游戏⾏业:⽆论是⼿游服务端还是⼤型的⽹络游戏,Java 语⾔得到了越来越⼴泛的应⽤。Netty 作为⾼性能的基础通信组件,提供了 TCP/UDP 和 HTTP 协议栈,⽅便定制和开发私有协议栈,账号登录服务器。同时地图服务器之间可以⽅便的通过 Netty 进⾏⾼性能的通信。
      (3)⼤数据领域:经典的 Hadoop 的⾼性能通信和序列化组件 Avro 的 RPC 框架,默认采⽤ Netty 进⾏跨界点通信。它的 NettyService 基于 Netty 框架⼆次封装实现。

    二、传统IO服务模型

      详细的可以看下这篇文章:IO(BIO、NIO、AIO)

            

       采⽤阻塞IO模型获取输⼊的数据。 每个连接需要独⽴的完成数据的输⼊,业务的处理,数据返回。当并发数⼤的时候,会创建⼤量的线程,占⽤系统资源,如果连接创建后,当前线程没有数据可读,会阻塞,造成线程资源浪费。最最原始的⽹络编程思路就是服务器⽤⼀个while循环,不断监听端⼝是否有新的套接字连接,如果有,那么就调⽤⼀个处理函数处理,类似:

    while(true){
        socket = accept();
        handle(socket) 
    }

      这种⽅法的最⼤问题是⽆法并发,效率太低,如果当前的请求没有处理完,那么后⾯的请求只能被阻塞,服务器的吞吐量太低。之后,想到了使⽤多线程,也就是很经典的connection per thread,每⼀个连接⽤⼀个线程处理,类似:

    public class BasicModel implements Runnable {
        @Override
        public void run() {
            try {
                ServerSocket ss = new ServerSocket(7007);
                while (!Thread.interrupted())
                    new Thread(new Handler(ss.accept())).start(); //创建新线程来handle,or, single-threaded, or a thread pool
            } catch (IOException ex) { /* ... */ }
        }
    
        static class Handler implements Runnable {
            final Socket socket;
            Handler(Socket s) { socket = s; }
            public void run() {
                try {
                    byte[] input = new byte[1024];
                    socket.getInputStream().read(input);
                    byte[] output = process(input);
                    socket.getOutputStream().write(output);
                } catch (IOException ex) { /* ... */ }
            }
            private byte[] process(byte[] input) {
                byte[] output=null;
                /* ... */
                return output;
            }
        }
    }

      对于每⼀个请求都分发给⼀个线程,每个线程中都独⾃处理上⾯的流程。tomcat服务器的早期版本确实是这样实现的。

      多线程并发模式,⼀个连接⼀个线程的优点是:

        ⼀定程度上极⼤地提⾼了服务器的吞吐量,因为之前的请求在read阻塞以后,不会影响到后续的请求,因为他们在不同的线程中。这也是为什么通常会讲“⼀个线程只能对应⼀个socket”的原因。另外有个问题,如果⼀个线程中对应多个socket连接不⾏吗?语法上确实可以,但是实际上没有⽤,每⼀个socket都是阻塞的,所以在⼀个线程⾥只能处理⼀个socket,就算accept了多个也没⽤,前⼀个socket被阻塞了,后⾯的是⽆法被执⾏到的。

      多线程并发模式,⼀个连接⼀个线程的缺点是:

        缺点在于资源要求太⾼,系统中创建线程是需要⽐较⾼的系统资源的,如果连接数太⾼,系统⽆法承受,⽽且,线程的反复创建-销毁也需要代价。

        同时连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在 read 操作,造成线程资源浪费

    三、Reactor模式

      ⽬前存在的线程模型有:传统阻塞IO服务模型、Reactor模型、Proactor模型。传统阻塞的IO模型上面已经讲到。

      Netty的整体架构,基于了⼀个著名的模式——Reactor模式。Reactor模式,是⾼性能⽹络编程的必知必会模式(Netty 主要基于主从 Reactor 多线程模型做了⼀定的改进,其中主从 Reactor 多线程模型有多个 Reactor) 

    (一)Reactor模型

      针对传统阻塞 I/O 服务模型的 2 个缺点,解决⽅案:

        1、基于 I/O 复⽤模型:多个连接共⽤⼀个阻塞对象,应⽤程序只需要在⼀个阻塞对象等待,⽆需阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应⽤程序,线程从阻塞状态返回,开始进⾏业务处理。Reactor 对应的叫法:反应器模式、分发者模式(Dispatcher)、3. 通知者模式(notifier)

        2、基于线程池复⽤线程资源:不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进⾏处理,⼀个线程可以处理多个连接的业务。

      一句话概括就是:IO多路复⽤ + 线程池 = Reactor模型:

          

       对上图说明:

        Reactor 模式,通过⼀个或多个输⼊同时传递给服务处理器的模式(基于事件驱动)

        服务器端程序处理传⼊的多个请求,并将它们同步分派到相应的处理线程,因此 Reactor 模式也叫Dispatcher 模式

        Reactor 模式使⽤ IO 复⽤监听事件,收到事件后,分发给某个线程(进程),这点就是⽹络服务器⾼并发处理关键

      Reactor模式中核⼼组成:

        1、Reactor:Reactor 在⼀个单独的线程中运⾏,负责监听和分发事件,分发给适当的处理程序来对IO 事件做出反应。

        2、Handlers:处理程序执⾏ I/O 事件要完成的实际事件,也就是实际的业务处理逻辑。Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执⾏⾮阻塞操作。

      Reactor模式分类:

        根据 Reactor 的数量和处理资源池线程的数量不同,有 3 种典型的实现:单 Reactor 单线程、单 Reactor 多线程、主从 Reactor 多线程

    (二)单Reactor单线程

      原理图:

          

      整体流程:  

        1、Select 是前⾯ I/O 复⽤模型介绍的标准⽹络编程 API,可以实现应⽤程序通过⼀个阻塞对象监听多路连接请求

        2、Reactor 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进⾏分发

        3、如果是建⽴连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建⼀个Handler 对象处理连接完成后的后续业务处理

        4、如果不是建⽴连接事件,则 Reactor 会分发调⽤连接对应的 Handler 来响应

        5、Handler 会完成 Read → 业务处理 → Send 的完整业务流程

      服务器端⽤⼀个线程通过多路复⽤搞定所有的 IO 操作(包括连接,读、写等),编码简单,清晰明了,但是如果客户端连接数量较多,将⽆法⽀撑,前⾯的 NIO 案例就属于这种模型。

      java的NIO模式的Selector⽹络通讯,其实就是⼀个简单的Reactor模型。

      实际上的Reactor模式,是基于java nio的,在他的基础上,抽象出来两个组件——Reactor和Handler两个组件:

        1、Reactor:负责响应IO事件,当检测到⼀个新的事件,将其发送给相应的Handler去处理;新的事件包含连接建⽴就绪、读就绪、写就绪等。

        2、Handler:将⾃身(handler)与事件绑定,负责事件的处理,完成channel的读⼊,完成处理业务逻辑后,负责将结果写出channel。

      ⽅案优缺点分析:

        1、优点:模型简单,没有多线程、进程通信、竞争的问题,全部都在⼀个线程中完成

        2、缺点:性能问题,只有⼀个线程,⽆法完全发挥多核 CPU 的性能。 Handler 在处理某个连接上的业务时,整个进程⽆法处理其他连接事件,很容易导致性能瓶颈

        3、缺点:可靠性问题,线程意外终⽌,或者进⼊死循环,会导致整个系统通信模块不可⽤,不能接收和处理外部消息,造成节点故障

        4、使⽤场景:客户端的数量有限,业务处理⾮常快速,⽐如 Redis 在业务处理的时间复杂度 O(1) 的情况Redis内部就是这种模型

      单Reactor单线程代码样例:

    public class SimpleReactor {
    
        public static void main(String[] args) throws IOException {
            testServer();
        }
        public static void testServer() throws IOException {
            // 1、获取Selector选择器
            Selector selector = Selector.open();
            // 2、获取通道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            // 3.设置为非阻塞
            serverSocketChannel.configureBlocking(false);
            // 4、绑定连接
            serverSocketChannel.bind(new InetSocketAddress(7007));
            // 5、将通道注册到选择器上,并注册的操作为:“接收”操作
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            // 6、采用轮询的方式,查询获取“准备就绪”的注册过的操作
            while (selector.select() > 0)
            {
                // 7、获取当前选择器中所有注册的选择键(“已经准备就绪的操作”)
                Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
                while (selectedKeys.hasNext())
                {
                    // 8、获取“准备就绪”的时间
                    SelectionKey selectedKey = selectedKeys.next();
                    // 9、判断key是具体的什么事件
                    if (selectedKey.isAcceptable())
                    {
                        // 10、若接受的事件是“接收就绪” 操作,就获取客户端连接
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        // 11、切换为非阻塞模式
                        socketChannel.configureBlocking(false);
                        // 12、将该通道注册到selector选择器上
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    }
                    else if (selectedKey.isReadable())
                    {
                        // 13、获取该选择器上的“读就绪”状态的通道
                        SocketChannel socketChannel = (SocketChannel) selectedKey.channel();
                        // 14、读取数据
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        int length = 0;
                        while ((length = socketChannel.read(byteBuffer)) != -1)
                        {
                            byteBuffer.flip();
                            System.out.println(new String(byteBuffer.array(), 0, length));
                            byteBuffer.clear();
                        }
                        socketChannel.close();
                    }
                    // 15、移除选择键
                    selectedKeys.remove();
                }
            }
            // 7、关闭连接
            serverSocketChannel.close();
        }
    }

    (三)单Reactor多线程

      原理图:

          

      图中多线程体现在两个部分:

        1、Reactor主线程

        Reactor通过select监听客户请求,如果是连接请求事件,则由Acceptor处理连接,如果是其他请求,则由dispatch找到对应的Handler,这⾥的Handler只负责响应事件,读取和响应,会将具体的业务处理交由Worker线程池处理。

        2、Worker线程池

        Worker线程池会分配独⽴线程完成真正的业务,并将结果返回给Handler,Handler收到响应后,通过send将结果返回给客户端。

      这⾥Reactor处理所有的事件监听和响应,⾼并发情景下容易出现性能瓶颈。

      ⽅案优缺点分析:

        1、优点:可以充分的利⽤多核 cpu 的处理能⼒

        2、缺点:多线程数据共享和访问⽐较复杂,Reactor 处理所有的事件的监听和响应,在单线程运⾏,在⾼并发场景容易出现性能瓶颈。

    (四)主从Reactor多线程

      原理图:

          

       这种模式是对单Reactor的改进,由原来单Reactor改成了Reactor主线程与Reactor⼦线程。

        1、Reactor 主线程 MainReactor 对象通过 select 监听连接事件,收到事件后,通过 Acceptor 处理连接事件

        2、当 Acceptor 处理连接事件后,MainReactor 将连接分配给 SubReactor

        3、subreactor 将连接加⼊到连接队列进⾏监听,并创建 handler 进⾏各种事件处理

        4、当有新事件read、send发⽣时, subreactor 就会调⽤对应的 handler 处理

        5、handler 通过 read 读取数据,分发给后⾯的 worker 线程处理

        6、worker 线程池分配独⽴的 worker 线程进⾏业务处理,并返回结果

        7、handler 收到响应的结果后,再通过 send 将结果返回给 client

        8、Reactor 主线程可以对应多个 Reactor ⼦线程,即 MainRecator 可以关联多个 SubReactor

      ⽅案优缺点说明:

        1、优点:⽗线程与⼦线程的数据交互简单职责明确,⽗线程只需要接收新连接,⼦线程完成后续的业务处理。

        2、优点:⽗线程与⼦线程的数据交互简单, Reactor 主线程只需要把新连接传给⼦线程,⼦线程⽆需返回数据。

        3、缺点:编程复杂度较⾼

        4、结合实例:这种模型在许多项⽬中⼴泛使⽤,包括 Nginx 主从 Reactor 多进程模型,Memcached主从多线程,Netty 主从多线程模型的⽀持

    (五)Reactor小结

      Reactor 模式具有如下的优点:

        1、响应快,不必为单个同步时间所阻塞,虽然 Reactor 本身依然是同步的

        2、可以最⼤程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销

        3、扩展性好,可以⽅便的通过增加 Reactor 实例个数来充分利⽤ CPU 资源

        4、复⽤性好,Reactor 模型本身与具体事件处理逻辑⽆关,具有很⾼的复⽤性

    四、Proactor模式

      Proactor⽤于异步IO,⽽Reactor⽤于同步IO;也就是我们不必等待I/O数据准备好也就是内核缓存已经读数据到⽤户空间。这⼀切都有内核来帮我们搞定,数据准备好了之后就通知Proactor,然后Proactor就调⽤相应的Handler进⾏业务处理。相对于Reactor省去了遍历事件通知队列selector 的代价。

          

       Proactor模型在理论上是⽐Reactor模型性能更好,但是因为依赖于操作系统的⾮阻塞异步模型,⽽linux的⾮阻塞异步模型还不完善,并没有真正的实现Proactor模型,⽽是epoll模拟出Proactor模型,所以还是以Reactor为主。

    五、Netty模型

    (一)工作原理

      1、简版:

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

          

          (1)BossGroup 线程维护 Selector,只关注 Accecpt

        (2)当接收到 Accept 事件,获取到对应的 SocketChannel,封装成 NIOScoketChannel 并注册到Worker 线程(事件循环),并进⾏维护

        (3)当 Worker 线程监听到 Selector 中通道发⽣⾃⼰感兴趣的事件后,就进⾏处理(由 handler),注意 handler 已经加⼊到通道

      2、详细版

          

        (1)Netty 抽象出两组线程池,BossGroup 专⻔负责接收客户端的连接,WorkerGroup 专⻔负责⽹络的读写

        (2)BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup(threads)

        (3)NioEventLoopGroup 相当于⼀个事件循环组,这个组中含有多个事件循环,每⼀个事件循环是NioEventLoop(thread)

        (4)NioEventLoop 表示⼀个不断循环的执⾏处理任务的线程,每个 NioEventLoop 都有⼀个Selector,⽤于监听绑定在其上的 socket 的⽹络通讯,NioEventLoop 内部采⽤串⾏化设计,从消息的读取->解码->处理->编码->发送,始终由 IO 线程 NioEventLoop 负责。

        (5)NioEventLoopGroup 可以有多个线程,即可以含有多个 NioEventLoop

        (6)每个 BossNioEventLoop 循环执⾏的步骤有 3 步:

            a、轮询 accept 事件

            b、处理 accept 事件,与 client 建⽴连接,⽣成 NioScocketChannel ,并将其注册到某个worker NIOEventLoop 上的 Selector

            c、处理任务队列的任务,即 runAllTasks

        (7)每个 Worker NIOEventLoop 循环执⾏的步骤:

            a、轮询 read , write 事件

            b、处理 I/O 事件,即 read , write 事件,在对应 NioScocketChannel 处理

            c、处理任务队列的任务,即 runAllTasks

        (8)每个 Worker NIOEventLoop 处理业务时,会使⽤ pipeline(管道),pipeline 中包含了channel,即通过 pipeline 可以获取到对应通道,管道中维护了很多的处理器

      说明:NioEventLoopGroup 下包含多个 NioEventLoop

        (1)每个 NioEventLoop 中包含有⼀个 Selector,⼀个 taskQueue

        (2)每个 NioEventLoop 的 Selector 上可以注册监听多个 NioChannel

        (3)每个 NioChannel 只会绑定在唯⼀的 NioEventLoop 上

        (4)每个 NioChannel 都绑定有⼀个⾃⼰的 ChannelPipeline

    (二)NioEventLoop

      NioEventLoop 是 Netty 的 Reactor 线程,它的职责如下:

        1、作为服务端 Acceptor 线程,负责处理客户端的请求接⼊;

        2、作为客户端 Connecor 线程,负责注册监听连接操作位,⽤于判断异步连接结果;

        3、作为 IO 线程,监听⽹络读操作位,负责从 SocketChannel 中读取报⽂;

        4、作为 IO 线程,负责向 SocketChannel 写⼊报⽂发送给对⽅,如果发⽣写半包,会⾃动注册监听写事件,⽤于后续继续发送半包数据,直到数据全部发送完成;

        5、为定时任务线程,可以执⾏定时任务,例如链路空闲检测和发送⼼跳消息等;

        6、作为线程执⾏器可以执⾏普通的任务线程(Runnable)。

      NioEventLoop 继承 SingleThreadEventExecutor,这就意味着它实际上是⼀个线程个数为 1 的线程池。

      对于⽤户⽽⾔,直接调⽤ NioEventLoop 的 execute(Runnable task) ⽅法即可执⾏⾃定义的 Task。

      NioEventLoop(thread) 设计原理:

        当系统在运⾏过程中,如果频繁的进⾏线程上下⽂切换,会带来额外的性能损耗。多线程并发执⾏某个业务流程,业务开发者还需要时刻对线程安全保持警惕,哪些数据可能会被并发修改,如何保护?这不仅降低了开发效率,也会带来额外的性能损耗。

        串⾏执⾏ Handler 链:为了解决上述问题,Netty 采⽤了串⾏化设计理念,从消息的读取、编码以及后续 Handler 的执⾏,始终都由 IO 线程 NioEventLoop 负责,这就意外着整个流程不会进⾏线程上下⽂的切换,数据也不会⾯临被并发修改的⻛险,对于⽤户⽽⾔,甚⾄不需要了解 Netty 的线程细节,这确实是个⾮常好的设计理念,它的⼯作原理图如下: 

          

       ⼀个 NioEventLoop 聚合了⼀个多路复⽤器 Selector,因此可以处理成百上千的客户端连接,Netty 的处理策略是每当有⼀个新的客户端接⼊,则从 NioEventLoopGroup线程组中顺序获取⼀个可⽤的 NioEventLoop,当到达数组上限之后,重新返回到 0,通过这种⽅式,可以基本保证各个NioEventLoop 的负载均衡。⼀个客户端连接只注册到⼀个 NioEventLoop 上,这样就避免了多个 IO线程去并发操作它。

      Netty 通过串⾏化设计理念降低了⽤户的开发难度,提升了处理性能。利⽤线程组实现了多个串⾏化线程⽔平并⾏执⾏,线程之间并没有交集,这样既可以充分利⽤多核提升并⾏处理能⼒,同时避免了线程上下⽂的切换和并发保护带来的额外性能损耗。

    (三)代码示例

      1、server示例

    public class NettyServer {
        public static void main(String[] args) {
            //创建BossGroup 和 WorkerGroup
            //说明
            //1. 创建两个线程组 bossGroup 和 workerGroup
            //2. bossGroup 只是处理连接请求 , 真正的和客户端业务处理,会交给 workerGroup完成
            //3. 两个都是无限循环
            //4. bossGroup 和 workerGroup 含有的子线程(NioEventLoop)的个数
            //   默认实际 cpu核数 * 2
            EventLoopGroup bossGroup = new NioEventLoopGroup(1);
            EventLoopGroup workerGroup = new NioEventLoopGroup(); //8
    
            try {
                //创建服务器端的启动对象,配置参数
                ServerBootstrap bootstrap = new ServerBootstrap();
                //使用链式编程来进行设置
                bootstrap.group(bossGroup, workerGroup) //设置两个线程组
                        .channel(NioServerSocketChannel.class) //使用NioSocketChannel 作为服务器的通道实现
                        .option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列得到连接个数
                        .childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持活动连接状态
                        //          .handler(null) // 该 handler对应 bossGroup , childHandler 对应 workerGroup
                        .childHandler(new ChannelInitializer<SocketChannel>() {//创建一个通道初始化对象(匿名对象)
                            //给pipeline 设置处理器
                            @Override
                            protected void initChannel(SocketChannel ch) throws Exception {
                                System.out.println("客户socketchannel hashcode=" + ch.hashCode()); //可以使用一个集合管理 SocketChannel, 再推送消息时,可以将业务加入到各个channel 对应的 NIOEventLoop 的 taskQueue 或者 scheduleTaskQueue
                                ch.pipeline().addLast(new NettyServerHandler());
                            }
                        }); // 给我们的workerGroup 的 EventLoop 对应的管道设置处理器
    
                System.out.println(".....服务器 is ready...");
    
    
                //绑定一个端口并且同步, 生成了一个 ChannelFuture 对象
                //启动服务器(并绑定端口)
                ChannelFuture cf = bootstrap.bind(6668).sync();
    
                //给cf 注册监听器,监控我们关心的事件
    
                cf.addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future) throws Exception {
                        if (cf.isSuccess()) {
                            System.out.println("监听端口 6668 成功");
                        } else {
                            System.out.println("监听端口 6668 失败");
                        }
                    }
                });
    
            //对关闭通道进行监听
    
                cf.channel().closeFuture().sync();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                bossGroup.shutdownGracefully();
                workerGroup.shutdownGracefully();
            }
    
        }
    }

      2、NettyServerHandler示例

    public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    
    
        //读取数据实际(这里我们可以读取客户端发送的消息)
    
        /**
         * 1. ChannelHandlerContext ctx:上下文对象, 含有 管道pipeline , 通道channel, 地址
         * 2. Object msg: 就是客户端发送的数据 默认Object
         */
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
            // 比如这里我们有一个非常耗时长的业务-> 异步执行 -> 提交该channel 对应的
            // NIOEventLoop 的 taskQueue中,
    
            // 解决方案1 用户程序自定义的普通任务
    
            ctx.channel().eventLoop().execute(new Runnable() {
                @Override
                public void run() {
    
                    try {
                        Thread.sleep(5 * 1000);
                        ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵2", CharsetUtil.UTF_8));
                        System.out.println("channel code=" + ctx.channel().hashCode());
                    } catch (Exception ex) {
                        System.out.println("发生异常" + ex.getMessage());
                    }
                }
            });
    
            ctx.channel().eventLoop().execute(new Runnable() {
                @Override
                public void run() {
    
                    try {
                        Thread.sleep(5 * 1000);
                        ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵3", CharsetUtil.UTF_8));
                        System.out.println("channel code=" + ctx.channel().hashCode());
                    } catch (Exception ex) {
                        System.out.println("发生异常" + ex.getMessage());
                    }
                }
            });
    
            //解决方案2 : 用户自定义定时任务 -》 该任务是提交到 scheduleTaskQueue中
    
            ctx.channel().eventLoop().schedule(new Runnable() {
                @Override
                public void run() {
    
                    try {
                        Thread.sleep(5 * 1000);
                        ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵4", CharsetUtil.UTF_8));
                        System.out.println("channel code=" + ctx.channel().hashCode());
                    } catch (Exception ex) {
                        System.out.println("发生异常" + ex.getMessage());
                    }
                }
            }, 5, TimeUnit.SECONDS);
    
            System.out.println("go on ...");
    
    //        System.out.println("服务器读取线程 " + Thread.currentThread().getName() + " channle =" + ctx.channel());
    //        System.out.println("server ctx =" + ctx);
    //        System.out.println("看看channel 和 pipeline的关系");
    //        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("客户端地址:" + channel.remoteAddress());
        }
    
    
        public void channelRead2(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println("服务器读取线程 " + Thread.currentThread().getName() + " channle =" + ctx.channel());
            System.out.println("server ctx =" + ctx);
            System.out.println("看看channel 和 pipeline的关系");
            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("客户端地址:" + channel.remoteAddress());
        }
    
        //数据读取完毕
        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            //writeAndFlush 是 write + flush
            //将数据写入到缓存,并刷新
            //一般讲,我们对这个发送的数据进行编码
            ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵1", CharsetUtil.UTF_8));
        }
    
        //处理异常, 一般是需要关闭通道
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            ctx.close();
        }
    }

      3、客户端示例

    public class NettyClient {
        public static void main(String[] args) {
            //客户端需要一个事件循环组
            EventLoopGroup group = new NioEventLoopGroup();
    
            //创建客户端启动对象
            //注意客户端使用的不是 ServerBootstrap 而是 Bootstrap
            Bootstrap bootstrap = new Bootstrap();
    
            try{
                //设置相关参数
                bootstrap.group(group) //设置线程组
                        .channel(NioSocketChannel.class) // 设置客户端通道的实现类(反射)
                        .handler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel ch) throws Exception {
                                ch.pipeline().addLast(new NettyClientHandler()); //加入自己的处理器
                            }
                        });
    
                System.out.println("客户端 ok..");
    
                //启动客户端去连接服务器端
                //关于 ChannelFuture 要分析,涉及到netty的异步模型
                ChannelFuture channelFuture = null;
                try {
                    channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //给关闭通道进行监听
                channelFuture.channel().closeFuture().sync();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                group.shutdownGracefully();
            }
    
        }
    }

      4、NettyClientHandler示例

    public class NettyClientHandler extends ChannelInboundHandlerAdapter {
        //当通道就绪就会触发该方法
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("client " + ctx);
            ctx.writeAndFlush(Unpooled.copiedBuffer("hello, server: (>^ω^<)喵", CharsetUtil.UTF_8));
        }
    
        //当通道有读取事件时,会触发
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            ByteBuf buf = (ByteBuf) msg;
            System.out.println("服务器回复的消息:" + buf.toString(CharsetUtil.UTF_8));
            System.out.println("服务器的地址: " + ctx.channel().remoteAddress());
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
            ctx.close();
        }
    } 

    六、异步模型

      上面的代码示例中,在使用BootStrap启动项目时,使用到了ChannelFuture,这就是Netty中使用异步模型的一种体现。

    (一)基本介绍

      异步操作概念 :

        ⽤者调⽤⼀个异步操作后 , 并不能⻢上知道该操作的返回值 , 该操作也不会⻢上执⾏完成 , 该操作完成后 , 会通过回调机制 , 如通知 , 注册的回调函数等机制通知调⽤者 ;

      Netty 中的异步操作与 ChannelFuture 返回值 :

        1、异步操作 : Netty 模型中凡是关于 IO 的操作 , 如绑定端⼝ ( Bind ) , 远程连接 ( Connect ) , 读取数据 (Read ) , 写出数据 ( Write ) 等操作都是异步操作 ;

        2、异步操作返回值 : 上述 IO 操作返回值都是 ChannelFuture 类型实例 , ChannelFuture 是异步 IO 操作的返回结果 ;

        3、在服务器端绑定端⼝号时 , 调⽤ Bootstrap 的 bind ⽅法 , 会返回 ChannelFuture 对象 ;

        4、在客户端调⽤ Bootstrap 的 connect ⽅法 , 也会返回 ChannelFuture 对象 ;

      Netty 中的异步操作机制 :

        1、Future-Listener 机制 : Future 表示当前不知道结果 , 在未来的某个时刻才知道结果 , Listener 表示监听操作 , 监听返回的结果 ;

        2、Netty 异步模型的两个基础 : Future ( ChannelFuture 未来知道结果 ) , Callback ( 监听回调 ) ;

      以客户端写出数据到服务器端为例 :

        客户端写出数据 : 客户端调⽤写出数据⽅法 ChannelFuture writeAndFlush(Object msg) , 向服务器写出数据 ;

        操作耗时 : 假设在服务器中接收到该数据后 , 要执⾏⼀个⾮常耗时的操作才能返回结果 , 就是操作⾮常耗时 ;

        客户端不等待 : 客户端这⾥写出了数据 , 肯定不能阻塞等待写出操作的结果 , 需要⽴刻执⾏下⾯的操作 ,因此该⽅法是异步的 ;

        客户端监听 : writeAndFlush ⽅法返回⼀个 ChannelFuture 对象 , 如果客户端需要该操作的返回结果 ,那么通过 ChannelFuture 可以监听该写出⽅法是否成功 ;

      异步操作返回结果 :

        1、返回结果 : Future 表示异步 IO 操作执⾏结果 , 通过该 Future 提供的 检索 , 计算 等⽅法检查异步操作是否执⾏完成 ;

        2、常⽤接⼝ : ChannelFuture 继承了 Future , 也是⼀个接⼝ , 可以为该接⼝对象注册监听器 , 当异步任务完成后会回调该监听器⽅法 ;

      Future 链式操作 : 这⾥以读取数据 , 处理后返回结果为例 ;

        1、数据读取操作 ;

        2、对读取的数据进⾏解码处理 ;

        3、执⾏业务逻辑

        4、将数据编码 ;

        5、将编码后的数据写出 ;

        上述5 个步骤 , 每个数据处理操作 , 都有与之对应的 Handler 处理器 ;

    (二)Future-Listener 机制

      1、Future 返回值 : 在 Netty 中执⾏ IO 操作 , 如 bind , read , write , connect 等⽅法 , 会⽴刻返回ChannelFuture 对象 ;

      2、ChannelFuture 返回时状态 : 调⽤ IO ⽅法后 , ⽴刻返回 ChannelFuture 对象 , 此时该操作未完成 ;

      3、注册监听器 : ChannelFuture 可以设置 ChannelFutureListener 监听器 , 监听该 IO 操作完成状态 ,如果 IO 操作完成 , 那么会回调其 public void operationComplete(ChannelFuture future) throwsException 接⼝实现⽅法 ;

      4、IO 操作执⾏状态判定 : 在 operationComplete ⽅法中通过 调⽤ ChannelFuture future 参数的如下⽅法 , 判定当前 IO 操作完成状态 ;

        future.isDone() : IO 操作是否完成 ;

        future.isSuccess() : IO 操作是否成功 ; ( 常⽤ )

        future.isCancelled() : IO 操作是否被取消 ;

        future.cause() : IO 操作的失败原因 ;

      代码示例:

    // 监听绑定操作的结果
    // 添加 ChannelFutureListener 监听器, 监听 bind 操作的结果
    channelFuture.addListener(new ChannelFutureListener() {
      @Override
      public void operationComplete(ChannelFuture future) throws Exception {
        if(future.isDone()){
          System.out.println("绑定端⼝完成");
         }
        if(future.isSuccess()){
          System.out.println("绑定端⼝成功");
         }else{
          System.out.println("绑定端⼝失败");
         }
        if(future.isCancelled()){
          System.out.println("绑定端⼝取消");
         }
        System.out.println("失败原因 : " + future.cause());
      }
    });
    ------------------------------------------------------------------
    -----------------------------------------------------------
    ---------------------------------------------
    朦胧的夜 留笔~~
  • 相关阅读:
    Hdu3022 Sum of Digits
    bzoj3864 Hero meet devil
    bzoj2448 挖油
    poj3783 Balls
    bzoj3802 Vocabulary
    Hdu5181 numbers
    Hdu5693 D Game
    图形填充之边标志算法
    图形填充之栅栏填充算法
    图形填充之种子填充算法
  • 原文地址:https://www.cnblogs.com/liconglong/p/15216455.html
Copyright © 2020-2023  润新知