关于java:Java-开发必备-IO与Netty原理精讲

2次阅读

共计 4977 个字符,预计需要花费 13 分钟才能阅读完成。

简介: I/ O 技术在零碎设计、性能优化、中间件研发中的应用越来越重要,学习和把握 I / O 相干技术曾经不仅是一个 Java 攻城狮的加分技能,而是一个必备技能。本文将带你理解 BIO/NIO/AIO 的倒退历程及实现原理,并介绍以后风行框架 Netty 的基本原理。

一 Java I/ O 模型

1 BIO(Blocking IO)

BIO 是同步阻塞模型,一个客户端连贯对应一个解决线程。在 BIO 中,accept 和 read 办法都是阻塞操作,如果没有连贯申请,accept 办法阻塞;如果无数据可读取,read 办法阻塞。

2 NIO(Non Blocking IO)

NIO 是同步非阻塞模型,服务端的一个线程能够解决多个申请,客户端发送的连贯申请注册在多路复用器 Selector 上,服务端线程通过轮询多路复用器查看是否有 IO 申请,有则进行解决。

NIO 的三大外围组件:

Buffer:用于存储数据,底层基于数组实现,针对 8 种根本类型提供了对应的缓冲区类。

Channel:用于进行数据传输,面向缓冲区进行操作,反对双向传输,数据能够从 Channel 读取到 Buffer 中,也能够从 Buffer 写到 Channel 中。

Selector:选择器,当向一个 Selector 中注册 Channel 后,Selector 外部的机制就能够主动一直地查问(Select)这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接实现等),这样程序就能够很简略地应用一个线程高效地治理多个 Channel,也能够说治理多个网络连接,因而,Selector 也被称为多路复用器。当某个 Channel 下面产生了读或者写事件,这个 Channel 就处于就绪状态,会被 Selector 监听到,而后通过 SelectionKeys 能够获取就绪 Channel 的汇合,进行后续的 I / O 操作。

Epoll 是 Linux 下多路复用 IO 接口 select/poll 的加强版本,它能显著进步程序在大量并发连贯中只有大量沉闷的状况下的零碎 CPU 利用率,获取事件的时候,它毋庸遍历整个被侦听的描述符集,只有遍历那些被内核 IO 事件异步唤醒而退出 Ready 队列的描述符汇合就行了。

3 AIO(NIO 2.0)

AIO 是异步非阻塞模型,个别用于连接数较多且连接时间较长的利用,在读写事件实现后由回调服务去告诉程序启动线程进行解决。与 NIO 不同,当进行读写操作时,只需间接调用 read 或 write 办法即可。这两种办法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入 read 办法的缓冲区,并告诉应用程序;对于写操作而言,当操作系统将 write 办法传递的流写入结束时,操作系统被动告诉应用程序。能够了解为,read/write 办法都是异步的,实现后会被动调用回调函数。

二 I/ O 模型演变

1 传统 I / O 模型

对于传统的 I / O 通信形式来说,客户端连贯到服务端,服务端接管客户端申请并响应的流程为:读取 -> 解码 -> 利用解决 -> 编码 -> 发送后果。服务端为每一个客户端连贯新建一个线程,建设通道,从而解决后续的申请,也就是 BIO 的形式。

这种形式在客户端数量一直减少的状况下,对于连贯和申请的响应会急剧下降,并且占用太多线程浪费资源,线程数量也不是没有下限的,会遇到各种瓶颈。尽管能够应用线程池进行优化,然而仍然有诸多问题,比方在线程池中所有线程都在解决申请时,无奈响应其余的客户端连贯,每个客户端仍旧须要专门的服务端线程来服务,即便此时客户端无申请,也处于阻塞状态无奈开释。基于此,提出了基于事件驱动的 Reactor 模型。

2 Reactor 模型

Reactor 模式是基于事件驱动开发的,服务端程序处理传入多路申请,并将它们同步分派给申请对应的解决线程,Reactor 模式也叫 Dispatcher 模式,即 I / O 多路复用对立监听事件,收到事件后散发(Dispatch 给某过程),这是编写高性能网络服务器的必备技术之一。

Reactor 模式以 NIO 为底层反对,外围组成部分包含 Reactor 和 Handler:

  • Reactor:Reactor 在一个独自的线程中运行,负责监听和散发事件,分发给适当的处理程序来对 I / O 事件做出反馈。它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人。
  • Handlers:处理程序执行 I / O 事件要实现的理论事件,Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。相似于客户想要与之交谈的公司中的理论员工。

依据 Reactor 的数量和 Handler 线程数量,能够将 Reactor 分为三种模型:

  • 单线程模型 (单 Reactor 单线程)
  • 多线程模型 (单 Reactor 多线程)
  • 主从多线程模型 (多 Reactor 多线程)

单线程模型

Reactor 外部通过 Selector 监控连贯事件,收到事件后通过 dispatch 进行散发,如果是连贯建设的事件,则由 Acceptor 解决,Acceptor 通过 accept 承受连贯,并创立一个 Handler 来解决连贯后续的各种事件,如果是读写事件,间接调用连贯对应的 Handler 来解决。

Handler 实现 read -> (decode -> compute -> encode) ->send 的业务流程。

这种模型益处是简略,害处却很显著,当某个 Handler 阻塞时,会导致其余客户端的 handler 和 accpetor 都得不到执行,无奈做到高性能,只实用于业务解决十分疾速的场景,如 redis 读写操作。

多线程模型

主线程中,Reactor 对象通过 Selector 监控连贯事件, 收到事件后通过 dispatch 进行散发,如果是连贯建设事件,则由 Acceptor 解决,Acceptor 通过 accept 接管连贯,并创立一个 Handler 来解决后续事件,而 Handler 只负责响应事件,不进行业务操作,也就是只进行 read 读取数据和 write 写出数据,业务解决交给一个线程池进行解决。

线程池调配一个线程实现真正的业务解决,而后将响应后果交给主过程的 Handler 解决,Handler 将后果 send 给 client。

单 Reactor 承当所有事件的监听和响应,而当咱们的服务端遇到大量的客户端同时进行连贯,或者在申请连贯时执行一些耗时操作,比方身份认证,权限查看等,这种刹时的高并发就容易成为性能瓶颈。

主从多线程模型

存在多个 Reactor,每个 Reactor 都有本人的 Selector 选择器,线程和 dispatch。

主线程中的 mainReactor 通过本人的 Selector 监控连贯建设事件,收到事件后通过 Accpetor 接管,将新的连贯调配给某个子线程。

子线程中的 subReactor 将 mainReactor 调配的连贯退出连贯队列中通过本人的 Selector 进行监听,并创立一个 Handler 用于解决后续事件。

Handler 实现 read -> 业务解决 -> send 的残缺业务流程。

对于 Reactor,最权威的材料应该是 Doug Lea 大神的 Scalable IO in Java,有趣味的同学能够看看。

三 Netty 线程模型

Netty 线程模型就是 Reactor 模式的一个实现,如下图所示:

1 线程组

Netty 形象了两组线程池 BossGroup 和 WorkerGroup,其类型都是 NioEventLoopGroup,BossGroup 用来承受客户端发来的连贯,WorkerGroup 则负责对实现 TCP 三次握手的连贯进行解决。

NioEventLoopGroup 外面蕴含了多个 NioEventLoop,治理 NioEventLoop 的生命周期。每个 NioEventLoop 中蕴含了一个 NIO Selector、一个队列、一个线程;其中线程用来做轮询注册到 Selector 上的 Channel 的读写事件和对投递到队列外面的事件进行解决。

Boss NioEventLoop 线程的执行步骤:

  • 解决 accept 事件, 与 client 建设连贯, 生成 NioSocketChannel。
  • 将 NioSocketChannel 注册到某个 worker NIOEventLoop 上的 selector。
  • 解决工作队列的工作,即 runAllTasks。

Worker NioEventLoop 线程的执行步骤:

  • 轮询注册到本人 Selector 上的所有 NioSocketChannel 的 read 和 write 事件。
  • 解决 read 和 write 事件,在对应 NioSocketChannel 解决业务。
  • runAllTasks 解决工作队列 TaskQueue 的工作,一些耗时的业务解决能够放入 TaskQueue 中缓缓解决,这样不影响数据在 pipeline 中的流动解决。

Worker NIOEventLoop 解决 NioSocketChannel 业务时,应用了 pipeline (管道),管道中保护了 handler 处理器链表,用来解决 channel 中的数据。

2 ChannelPipeline

Netty 将 Channel 的数据管道形象为 ChannelPipeline,音讯在 ChannelPipline 中流动和传递。ChannelPipeline 持有 I / O 事件拦截器 ChannelHandler 的双向链表,由 ChannelHandler 对 I / O 事件进行拦挡和解决,能够不便的新增和删除 ChannelHandler 来实现不同的业务逻辑定制,不须要对已有的 ChannelHandler 进行批改,可能实现对批改关闭和对扩大的反对。

ChannelPipeline 是一系列的 ChannelHandler 实例,流经一个 Channel 的入站和出站事件能够被 ChannelPipeline 拦挡。每当一个新的 Channel 被创立了,都会建设一个新的 ChannelPipeline 并绑定到该 Channel 上,这个关联是永久性的;Channel 既不能附上另一个 ChannelPipeline 也不能拆散以后这个。这些都由 Netty 负责实现,而无需开发人员的特地解决。

依据起源, 一个事件将由 ChannelInboundHandler 或 ChannelOutboundHandler 解决,ChannelHandlerContext 实现转发或流传到下一个 ChannelHandler。一个 ChannelHandler 处理程序能够告诉 ChannelPipeline 中的下一个 ChannelHandler 执行。Read 事件(入站事件)和 write 事件(出站事件)应用雷同的 pipeline,入站事件会从链表 head 往后传递到最初一个入站的 handler,出站事件会从链表 tail 往前传递到最前一个出站的 handler,两种类型的 handler 互不烦扰。

ChannelInboundHandler 回调办法:

ChannelOutboundHandler 回调办法:

3 异步非阻塞

写操作:通过 NioSocketChannel 的 write 办法向连贯外面写入数据时候是非阻塞的,马上会返回,即便调用写入的线程是咱们的业务线程。Netty 通过在 ChannelPipeline 中判断调用 NioSocketChannel 的 write 的调用线程是不是其对应的 NioEventLoop 中的线程,如果发现不是则会把写入申请封装为 WriteTask 投递到其对应的 NioEventLoop 中的队列外面,而后等其对应的 NioEventLoop 中的线程轮询读写事件时候,将其从队列外面取出来执行。

读操作:当从 NioSocketChannel 中读取数据时候,并不是须要业务线程阻塞期待,而是等 NioEventLoop 中的 IO 轮询线程发现 Selector 上有数据就绪时,通过事件告诉形式来告诉业务数据已就绪,能够来读取并解决了。

每个 NioSocketChannel 对应的读写事件都是在其对应的 NioEventLoop 治理的单线程内执行,对同一个 NioSocketChannel 不存在并发读写,所以无需加锁解决。

应用 Netty 框架进行网络通信时,当咱们发动 I / O 申请后会马上返回,而不会阻塞咱们的业务调用线程;如果想要获取申请的响应后果,也不须要业务调用线程应用阻塞的形式来期待,而是当响应后果进去的时候,应用 I / O 线程异步告诉业务的形式,所以在整个申请 -> 响应过程中业务线程不会因为阻塞期待而不能干其余事件。

正文完
 0