当前位置: 首页 > 科技观察

Java开发必备!I-O与Netty原理精讲

时间:2023-03-13 15:07:30 科技观察

I/O技术在系统设计、性能优化、中间件研发等方面越来越重要。学习和掌握I/O相关技术,不仅仅是Java攻城狮。奖励技能,但必须具备的技能。本文将带你了解BIO/NIO/AIO的发展历史和实现原理,并介绍当前流行的框架Netty的基本原理。文末福利:古今独家解读《Java开发手册(嵩山版)》。一、JavaI/O模型1BIO(BlockingIO)BIO是一种同步阻塞模型,一个客户端连接对应一个处理线程。在BIO中,accept和read方法都是阻塞操作。如果没有连接请求,accept方法阻塞;如果没有要读取的数据,则read方法会阻塞。2NIO(NonBlockingIO)NIO是一种同步非阻塞模型。服务器上的一个线程可以处理多个请求。客户端发送的连接请求被注册到多路复用器Selector上,服务器线程通过轮询的方式进行多路复用。服务器检查是否有IO请求,有则处理。NIO的三个核心组件:Buffer:用于存储数据,底层基于数组实现,为8种基本类型提供了相应的缓冲类。Channel:用于数据传输,对buffer进行操作,支持双向传输,数据可以从Channel读取到Buffer,也可以从Buffer写入到Channel。Selector:选择器,当一个Channel注册到一个Selector时,Selector的内部机制可以自动不断查询(Select)这些注册的Channel是否有就绪的I/O事件(如可读、可写、网络连接Completion等)),这样程序就可以很方便的使用一个线程来高效的管理多个Channel,也可以说是管理多个网络连接。因此,Selector也被称为多路复用器。当Channel发生读写事件时,Channel处于就绪状态,会被Selector监听,然后通过SelectionKeys获取就绪Channel的集合,用于后续的I/O操作。epoll是Linux下多路复用IO接口select/poll的增强版。在大量并发连接中只有少数活动程序时,可以显着提高系统的CPU利用率。获取事件时,不需要遍历整个被检测到的描述符集合,只需要遍历被内核IO事件异步唤醒并加入Ready队列的描述符集合即可。3AIO(NIO2.0)AIO是一种异步非阻塞模型。一般用于连接数多,连接时间长的应用。读写事件完成后,回调服务会通知程序启动线程进行处理。与NIO不同的是,在进行读写操作时,只需要直接调用read或write方法即可。这两个方法是异步的。对于读操作,当有流可读时,操作系统会将可读流传入read方法的缓冲区,并通知应用程序;对于写操作,当操作系统将write方法传递的流写完时,操作系统会主动通知应用程序。可以这样理解,read/write方法都是异步的,完成后会主动调用回调函数。2I/O模型演进1传统I/O模型对于传统的I/O通信方式,客户端连接到服务器,服务器接收客户端的请求并响应的过程是:读取->解码->应用过程->编码->发送结果。服务端为每个客户端连接创建一个新的线程,并建立一个通道来处理后续的请求,这就是BIO的方式。这样,当客户端数量不断增加时,连接和请求的响应会急剧下降,使用过多的线程会浪费资源。线程数不是无限的,会遇到各种瓶颈。虽然可以利用线程池进行优化,但是还是存在很多问题。例如,当线程池中的所有线程都在处理请求时,它们无法响应其他客户端连接。每个客户端仍然需要一个专门的服务器线程来服务,即使此时客户端没有请求,处于阻塞状态,无法释放。在此基础上,提出了事件驱动的Reactor模型。2Reactor模型Reactor模式是基于事件驱动开发的。服务器程序处理传入的多通道请求,并将它们同步分派给与请求对应的处理线程。Reactor模式也叫Dispatcher模式,即I/O多路复用统一监听事件并在接收到事件后进行调度(Dispatchtoaprocess),是编写高性能Web服务器的必备技术之一。Reactor模式由NIO作为底层支持。核心组件包括Reactor和Handler:Reactor:Reactor运行在一个单独的线程中,负责监听和分发事件,并将事件分发给合适的处理程序来响应I/O事件。它就像公司的电话接线员一样,接听客户的电话并将线路转接到适当的联系人。处理程序:处理程序执行要由I/O事件完成的实际事件。Reactor通过调度适当的处理程序来响应I/O事件。处理程序执行非阻塞操作。类似于客户想要与之交谈的公司中的实际员工。根据Reactor的数量和Handler线程的数量,Reactors可以分为三种模型:单线程模型(singleReactorsinglethread)多线程模型(singleReactormulti-threaded)主从多线程模型(multi-Reactormulti-threaded)单线程模型Reactor内部通过Selector监听连接事件,收到事件后通过dispatch进行分发。如果是连接建立事件,则由Acceptor处理。Acceptor通过accept接受连接,并创建一个Handler来处理连接的各种后续事件。读写事件,直接调用连接对应的Handler进行处理。Handler完成读取->(解码->计算->编码)->发送的业务流程。这种模式的优点是简单,缺点也很明显。当一个Handler被阻塞时,其他clients的handlers和accpetors将不会被执行,也就无法达到高性能。只适用于业务处理速度非常快的场景,比如Redis的读写操作。在多线程模型的主线程中,Reactor对象通过Selector监听连接事件,收到事件后通过dispatch进行分发。如果是连接建立事件,则由Acceptor处理。Acceptor通过accept接收连接,创建Handler处理后续事件。Handler只负责响应事件,不进行业务操作,即只读取数据和写入写入数据,业务处理交给线程池处理。线程池分配一个线程完成真正的业务处理,然后将响应结果发送给主进程的Handler进行处理,Handler将结果发送给客户端。单个Reactor负责监听和响应所有事件,当我们的服务器遇到大量客户端同时连接,或者请求连接时进行一些耗时的操作,比如身份认证,权限校验等.,这种瞬时高并发很容易成为性能瓶颈。主从多线程模型中有多个Reactor,每个Reactor都有自己的Selector、线程和dispatch。主线程中的mainReactor通过自己的Selector监听连接建立事件,通过Accpetor接收事件,将新的连接分配给某个子线程。子线程中的subReactor通过自己的Selector将mainReactor分配的连接添加到连接队列中进行监听,并创建Handler处理后续事件。Handler完成读取->业务处理->发送的完整业务流程。关于Reactor,最权威的资料应该是DougLea的ScalableIOinJava,有兴趣的同学可以看看。三种Netty线程模型Netty线程模型是Reactor模型的一种实现,如下图所示:1线程组Netty抽象了两组线程池,BossGroup和WorkerGroup,它们都是NioEventLoopGroup。BossGroup用于接受来自客户端的连接。WorkerGroup负责处理完成TCP三次握手的连接。NioEventLoopGroup包含多个NioEventLoop来管理NioEventLoop的生命周期。每个NioEventLoop包含一个NIOSelector、一个队列和一个线程;该线程用于轮询Selector上注册的Channel的读写事件,并处理投递到队列中的事件。BossNioEventLoop线程的执行步骤:处理accept事件,与客户端建立连接,生成NioSocketChannel。将NioSocketChannel注册到workerNIOEventLoop上的选择器。处理任务队列中的任务,即runAllTask??s。WorkerNioEventLoop线程的执行步骤:轮询所有在自己的Selector上注册的NioSocketChannels的读写事件。处理读写事件,在对应的NioSocketChannel中处理业务。runAllTask??s处理任务队列TaskQueue的任务,一些比较耗时的业务处理可以放到TaskQueue中进行慢速处理,以免影响流水线中数据的流动处理。WorkerNIOEventLoop在处理NioSocketChannel业务时,使用了一个管道(pipeline),管道维护了一个handler处理器链表来处理通道中的数据。2ChannelPipelineNetty将Channel的数据管道抽象为ChannelPipeline,消息在ChannelPipline中流动和传递。ChannelPipeline持有I/O事件拦截器ChannelHandler的双向链表。ChannelHandler拦截并处理I/O事件。方便增删ChannelHandler,实现不同的业务逻辑定制。它不需要修改现有的ChannelHandler。修改可以实现对修改闭包和扩展的支持。ChannelPipeline是一系列的ChannelHandler实例,流经Channel的入站和出站事件可以被ChannelPipeline拦截。每当创建新的Channel时,都会创建一个新的ChannelPipeline并将其绑定到Channel。该协会是永久性的;Channel既不能附加另一个ChannelPipeline,也不能分离当前的ChannelPipeline。这些都是Netty完成的,开发者没有特殊处理。根据来源,事件将由ChannelInboundHandler或ChannelOutboundHandler处理,ChannelHandlerContext实现转发或传播到下一个ChannelHandler。一个ChannelHandlerhandler可以通知ChannelPipeline中的下一个ChannelHandler执行。读取事件(入站事件)和写入事件(出站事件)使用相同的管道。入站事件将从链表头传递到最后一个入站处理程序,出站事件将从链表尾部转发到前面。一个outboundhandler,两类handler互不干扰。ChannelInboundHandler回调方法:ChannelOutboundHandler回调方法:3异步非阻塞写操作:通过NioSocketChannel的write方法向连接写入数据时,是非阻塞的,会立即返回,即使调用write的线程是我们的业务线程.Netty在ChannelPipeline中判断调用NioSocketChannel的write的调用线程是否是其对应的NioEventLoop中的线程。如果没有找到,则将写请求封装为WriteTask投递到其对应的NioEventLoop中的队列中,然后等待其对应的NioEventLoop中的线程轮询读写事件时,将其取出队列并执行。读操作:从NioSocketChannel读取数据时,不需要业务线程阻塞等待。而是当NioEventLoop中的IO轮询线程发现Selector上的数据准备好后,通过事件通知的方式通知业务数据准备好了。阅读和处理。每个NioSocketChannel对应的读写事件都在其对应的NioEventLoop管理的单线程中执行。同一个NioSocketChannel没有并发读写,所以不需要加锁处理。在使用Netty框架进行网络通信时,当我们发起一个I/O请求时,我们会立即返回,不会阻塞我们的业务调用线程;如果我们要获取请求的响应结果,不需要业务调用线程使用阻塞方式Waiting,而是在响应结果出来时,使用I/O线程异步通知业务,所以在整个请求->响应过程中,业务线程不会因为阻塞和等待而无法做其他事情。