Java网络编程--Netty框架NIO(三)

一、Netty简介

  Netty是一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持,作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。
  作为当前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,一些业界著名的开源组件也基于Netty的NIO框架构建。
主要的优点有:

  1. 框架设计优雅,底层模型随意切换适应不同的网络协议要求
  2. 提供很多标准的协议、安全、编码解码的支持
  3. 解决了很多NIO不易用的问题
  4. 社区更为活跃,在很多开源框架中使用,如Dubbo、RocketMQ、Spark等。

二、Reactor模型

  Netty中的Reactor模型主要由多路复用器(Acceptor)、事件分发器(Dispatcher)、事件处理器(Handler)组成,可以分为三种。
1、单线程模型:所有I/O操作都由一个线程完成,即多路复用、事件分发和处理都是在一个Reactor线程上完成的。


Java网络编程--Netty框架NIO(三)_第1张图片
单线程模型

  对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发的应用却不合适,主要原因如下:
  一个线程同时处理成百上千的链路,性能上无法支撑,即便CPU负荷达到100%,也无法满足海量消息的编码、解码、读取和发送;
  当负载过重后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈;
  一旦单线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障,可靠性不高。

2、多线程模型:为了解决单线程模型存在的一些问题,演化而来的Reactor线程模型。


Java网络编程--Netty框架NIO(三)_第2张图片
多线程模型

多线程模型的特点:
  有专门一个Acceptor线程用于监听服务端,接收客户端的TCP连接请求;
  网络IO的读写操作由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送;
  一个NIO线程可以同时处理多条链路,但是一个链路只能对应一个NIO线程,防止发生并发操作问题。
  在绝大多数场景下,Reactor多线程模型都可以满足性能需求;但是,在极特殊应用场景中,一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题。例如百万客户端并发连接,或者服务端需要对客户端的握手消息进行安全认证,认证本身非常损耗性能。在这类场景下,单独一个Acceptor线程可能会存在性能不足问题,为了解决性能问题,产生了第三种Reactor线程模型-主从Reactor多线程模型。

3、主从多线程模型:采用多个reactor,每个reactor都在自己单独的线程里执行。如果是多核,则可以同时响应多个客户端的请求,一旦链路建立成功就将链路注册到负责I/O读写的SubReactor线程池上。


Java网络编程--Netty框架NIO(三)_第3张图片
主从多线程模型

  事实上,Netty的线程模型并非固定不变,在启动辅助类中创建不同的EventLoopGroup实例并通过适当的参数配置,就可以支持上述三种Reactor线程模型。正是因为Netty对Reactor线程模型的支持提供了灵活的定制能力,所以可以满足不同业务场景的性能需求。

三、Maven依赖


    io.netty
    netty-all
    4.1.6.Final

四、Netty模块组件

  1. Bootstrap、ServerBootstrap
    Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类,ServerBootstrap 是服务端启动引导类。
分类 Bootstrap ServerBootstrap
网络功能 连接到远程主机和端口 绑定本地端口
EventLoopGroup数量 1 2
  1. Future、ChannelFuture
    在 Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。
    但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过 Future 和 ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。

  2. Channel
    Netty 网络通信的组件,能够用于执行网络 I/O 操作。Channel 为用户提供:

1)当前网络连接的通道的状态(例如是否打开?是否已连接?)

网络连接的配置参数 (例如接收缓冲区大小)

2)提供异步的网络 I/O 操作(如建立连接,读写,绑定端口),异步调用意味着任何 I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成。 调用立即返回一个 ChannelFuture 实例,通过注册监听器到 ChannelFuture 上,可以 I/O 操作成功、失败或取消时回调通知调用方。

3)支持关联 I/O 操作与对应的处理程序。

4)不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应。

5)常用的 Channel 类型:

NioSocketChannel,异步的客户端 TCP Socket 连接。
NioServerSocketChannel,异步的服务器端 TCP Socket 连接。
NioDatagramChannel,异步的 UDP 连接。
NioSctpChannel,异步的客户端 Sctp 连接。
NioSctpServerChannel,异步的 Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO。

  1. Selector
    Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件。
    当向一个 Selector 中注册 Channel 后,Selector 内部的机制就可以自动不断地查询(Select) 这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel 。

  2. NioEventLoop
    NioEventLoop 中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务:
    I/O 任务,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法触发。
    非 IO 任务,添加到 taskQueue 中的任务,如 register0、bind0 等任务,由 runAllTasks 方法触发。
    两种任务的执行时间比由变量 ioRatio 控制,默认为 50,则表示允许非 IO 任务执行的时间与 IO 任务的执行时间相等。

  3. NioEventLoopGroup
    NioEventLoopGroup,主要管理 eventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。

  4. ChannelHandler
    ChannelHandler 是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序。
    ChannelHandler 本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类:
    ChannelInboundHandler 用于处理入站 I/O 事件。
    ChannelOutboundHandler 用于处理出站 I/O 操作。
    或者使用以下适配器类:
    ChannelInboundHandlerAdapter 用于处理入站 I/O 事件。
    ChannelOutboundHandlerAdapter 用于处理出站 I/O 操作。
    ChannelDuplexHandler 用于处理入站和出站事件。

  5. ChannelHandlerContext
    保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象。

  6. ChannelPipline
    保存 ChannelHandler 的 List,用于处理或拦截 Channel 的入站事件和出站操作。
    ChannelPipeline 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel 中各个的 ChannelHandler 如何相互交互。
    一个 Channel 包含了一个 ChannelPipeline,而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。
    入站事件和出站事件在一个双向链表中,入站事件会从链表 head 往后传递到最后一个入站的 handler,出站事件会从链表 tail 往前传递到最前一个出站的 handler,两种类型的 handler 互不干扰。
    在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应,它们的组成关系如下:


    Java网络编程--Netty框架NIO(三)_第4张图片
    ChannelPipeline

五、简单示例

  1. 服务器代码
public class NettyServer {
    public static void main(String[] args) {
        /**
         * TODO 1.创建ServerBootstrap对象,netty的辅助启动器,netty客户端和服务器的入口,
         * Bootstrap是创建客户端连接的启动器,
         * ServerBootstrap是监听服务端端口的启动器,跟tomcat的Bootstrap类似,程序的入口。
         */
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        /**
         * TODO 2. EventLoop:netty最核心的几大组件之一,就是我们常说的reactor,
         * 人为划分为boss reactor和worker reactor。
         * 通过EventLoopGroup(Bootstrap启动时会设置EventLoopGroup)生成,
         * 最常用的是nio的NioEventLoop,就如同EventLoop的名字,EventLoop内部有一个无限循环,维护了一个selector,
         * 处理所有注册到selector上的io操作,在这里实现了一个线程维护多条连接的工作。
         */
        //  创建mainReactor
        NioEventLoopGroup boss = new NioEventLoopGroup();
        // 创建工作线程组
        NioEventLoopGroup worker = new NioEventLoopGroup();
        serverBootstrap
                .group(boss, worker)
                /**
                 *  TODO 3. 通道 Channel:关联jdk原生socket的组件,
                 *  常用的是NioServerSocketChannel和NioSocketChannel,
                 *  NioServerSocketChannel负责监听一个tcp端口,
                 *  有连接进来通过boss reactor创建一个NioSocketChannel将其绑定到worker reactor,
                 *  然后worker reactor负责这个NioSocketChannel的读写等io事件。
                 */
                .channel(NioServerSocketChannel.class)  // 指定所使用的NIO传输Channel
                // 添加一个EchoServer-Handler到子Channel的ChannelPipeline
                .childHandler(new ChannelInitializer() {
                    protected void initChannel(NioSocketChannel ch) {
                        // 配置入站、出站事件channel
                        ch.pipeline().addLast(new StringDecoder());
                        // 读取数据
                        ch.pipeline().addLast(new SimpleChannelInboundHandler() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                                System.out.println(msg);
                            }
                        });
                    }
                })
                .bind(8000);    // 设置端口值
    }
}
Java网络编程--Netty框架NIO(三)_第5张图片
服务端 Netty Reactor 工作架构图

Server 端包含 1 个 Boss NioEventLoopGroup 和 1 个 Worker NioEventLoopGroup。

NioEventLoopGroup 相当于 1 个事件循环组,这个组里包含多个事件循环 NioEventLoop,每个 NioEventLoop 包含 1 个 Selector 和 1 个事件循环线程。
每个 Boss NioEventLoop 循环执行的任务包含 3 步:
轮询 Accept 事件。
处理 Accept I/O 事件,与 Client 建立连接,生成 NioSocketChannel,并将 NioSocketChannel 注册到某个 Worker NioEventLoop 的 Selector 上。
处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用 eventloop.execute 或 schedule 执行的任务,或者其他线程提交到该 eventloop 的任务。
每个 Worker NioEventLoop 循环执行的任务包含 3 步:
轮询 Read、Write 事件。
处理 I/O 事件,即 Read、Write 事件,在 NioSocketChannel 可读、可写事件发生时进行处理。
处理任务队列中的任务,runAllTasks。

  1. 客户端代码
public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        // TODO 1.创建Bootstrap对象
        Bootstrap bootstrap = new Bootstrap();
        // TODO 2.创建事件组对象
        NioEventLoopGroup group = new NioEventLoopGroup();
        // TODO 3. 指定EventLoopGroup以处理客户端事件;需要适用于NIO的实现
        bootstrap.group(group)
                // TODO 4. 适用于NIO传输的Channel类型
                .channel(NioSocketChannel.class)
                // TODO 5.在创建Channel时,向ChannelPipeline中添加一个StringEncoder实例
                .handler(new ChannelInitializer() {
                    @Override
                    protected void initChannel(Channel ch) {
                        ch.pipeline().addLast(new StringEncoder());
                    }
                });
        // TODO 6.设定Channel参数
        Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();
        while (true) {
            // TODO 7.使用通道输出数据
            channel.writeAndFlush(new Date() + ": hello world!");
            Thread.sleep(2000);
        }
    }
}
   

六、NIO类库

NIO到底是什么的简称?有人称之为New IO,因为它相对于之前的IO类库是新增的,所以被称为New IO,这是它的官方叫法。但是,由于之前老的IO类库是阻塞IO,New IO类库的目标就是要让JAVA支持非阻塞IO,所以,更多的人喜欢称之为非阻塞IO(Non-block IO)。
与Socket类和ServerSocket类相对应,NIO也提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式使用非常简单,但是性能和可靠性都不好,非阻塞模式正好相反。开发人员一般可以根据自己的需要来选择合适的模式,一般来说,低负载、低并发的应用程序可以选择同步阻塞IO以降低编程复杂度。但是对于高负载、高并发的网络应用,需要使用NIO的非阻塞模式进行开发。

  1. 缓冲区Buffer
    每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区:
    ByteBuffer:字节缓冲区
    CharBuffer:字符缓冲区
    ShortBuffer:短整型缓冲区
    IntBuffer:整形缓冲区
    LongBuffer:长整形缓冲区
    FloatBuffer:浮点型缓冲区
    DoubleBuffer:双精度浮点型缓冲区

  2. 通道Channel
    Channel是一个通道,可以通过它读取和写入数据,它就像自来水管一样,网络数据通过Channel读取和写入。通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道可以用于读、写或者同时用于读写。
    因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的API。特别是在UNIX网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。

  3. 多路复用器Selector
    多路复用器提供选择已经就绪的任务的能力。简单来讲,Selector会不断的轮询注册在其上的Channel,如果某个Channel上面有新的TCP连接接入、读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合进行后续的IO操作。
    一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端,这的确是一个巨大的改进。

你可能感兴趣的:(Java网络编程--Netty框架NIO(三))