当前位置: 首页 > 后端技术 > Java

Netty源码分析系列(二)Netty架构设计

时间:2023-04-02 10:19:25 Java

前言上一篇我们对Netty做了一个基本的概述,知道了Netty是什么以及Netty的简单应用。Netty源码分析系列(一)Netty概述本篇文章,我们将谈谈Netty的架构设计。在学习一个框架之前,我们首先要了解它的设计原理,然后再进行深入的分析。接下来,我们从三个方面分析Netty的架构设计。Selector模型JavaNIO是基于Selector模型来实现非阻塞I/O的。Netty底层是基于JavaNIO实现的,所以也使用了Selector模型。Selector模型解决了每个客户端一个线程的传统阻塞I/O编程问题。Selector提供了一种机制,用于监视一个或多个NIO通道并识别何时可以使用一个或多个NIO通道进行数据传输。这样,一个线程可以管理多个通道,从而管理多个网络连接。选择器提供了选择准备好执行的任务的能力。从底层的角度来看,Selector会轮询Channel是否准备好执行每个I/O操作。Selector允许单线程处理多个Channel。选择器是一种复用技术。SelectableChannel并不是所有的Channel都能被Selector复用,只有抽象类SelectableChannel的子类才能被Selector复用。例如,FileChannel不能被选择器重用,因为FileChannel不是SelectableChannel的子类。为了与Selector一起使用,SelectableChannel必须首先通过register方法注册此类的实例。该方法返回一个新的SelectionKey对象,表示Channel已经注册到Selector。使用Selector注册后,Channel将保持注册状态,直到取消注册。一个Channel最多只能向任何特定的Selector注册一次,但同一个Channel可以向多个Selector注册。您可以通过调用isRegistered方法来确定一个Channel是否注册了一个或多个Selector。SelectableChannel可以安全地供多个并发线程使用。将Channel注册到Selector使用SelectableChannel的register方法将Channel注册到Selector。方法接口源码如下:publicfinalSelectionKeyregister(Selectorsel,intops)throwsClosedChannelException{returnregister(sel,ops,null);}publicabstractSelectionKeyregister(Selectorsel,intops,Objectatt)throwsClosedChannelException;说明如下:sel:指定Channel要注册的Selector。ops:指定Selector需要查询的通道的操作。在Selector中注册的Channel代表一个SelectionKey事件。SelectionKey的类型包括:OP_READ:可读事件;值:1<<0OP_WRITE:可写事件;value:1<<2OP_CONNECT:客户端连接服务器的事件(tcp连接),一般是创建SocketChannel客户端通道;value:1<<3OP_ACCEPT:服务端接收客户端连接的事件,一般是创建一个ServerSocketChannel服务端通道;value:1<<4具体注册代码如下://1.创建通道管理器(Selector)Selectorselector=Selector.open();//2.创建通道ServerSocketChannelServerSocketChannelserverSocketChannel=ServerSocketChannel.open();//3.channel必须是非注册到SelectorBlocking,所以FileChannel不能使用Selector,因为FileChannel是阻塞的serverSocketChannel.configureBlocking(false);//4.第二个参数指定我们感兴趣的Channel事件类型SelectionKeykey=serverSocketChannel.register(selector,SelectionKey.OP_READ);//你也可以使用OR运算符|组合多个事件,例如SelectionKeykey=serverSocketChannel.register(selector,SelectionKey.OP_READ|SelectionKey.OP_WRITE);值得注意的是,一个Channel只能注册到一个Selector一次,如果Channel多次注册到Selector,实际上相当于更新了SelectionKey的兴趣集。SelectionKeyChannel和Selector的关系确定后,一旦Channel处于某种就绪状态,就可以被selector查询。然后通过调用Selector的select方法完成这项工作。select方法的作用是查询感兴趣的通道操作的就绪状态。//当注册事件到达时,该方法返回,否则该方法将一直阻塞selector.select();SelectionKey包含兴趣集合,代表选中的感兴趣的事件集合。兴趣集合可以通过SelectionKey进行读写,例如://返回当前感兴趣的事件列表intinterestSet=key.interestOps();//也可以通过interestSetboolean来判断其中包含的事件isInterestedInAccept=interestSet&SelectionKey.OP_ACCEPT;booleanisInterestedInConnect=interestSet&SelectionKey.OP_CONNECT;booleanisInterestedInRead=interestSet&SelectionKey.OP_READ;booleanisInterestedInWrite=interestSet&SelectionKey.OP_WRITE;//可以通过方法interestOps(intops)key.interestOps(interestSet|SelectionKey.OP_WRITE)修改事件列表;请看,通过对兴趣集合和给定的SelectionKey常量进行位与运算,您可以确定某个事件是否在兴趣集合中。SelectionKey包含现成的集合。就绪集是通道准备就绪的操作集。选择后,首先访问就绪集合。可以这样访问就绪集合:intreadySet=key.readyOps();//也可以用四种方法判断不同的事件是否就绪key.isReadable();//读取事件是否就绪key.isWritable();//写入事件是否就绪key.isConnectable();//客户端连接事件是否就绪key.isAcceptable();//服务器连接事件是否就绪我们可以通过SelectionKey获取当前通道和选择器//返回当前事件关联的Channel,可转换的选项包括:`ServerSocketChannel`和`SocketChannel`Channelchannel=key.channel();//返回与当前事件关联的Selector对象Selectorselector=key.selector();可以将对象或其他信息附加到SelectionKey,以便可以轻松识别特定频道。key.attach(theObject);对象attachedObj=key.attachment();使用register()方法向Selector注册Channel时,也可以附加对象。SelectionKeykey=channel.register(selector,SelectionKey.OP_READ,theObject);遍历SelectionKey一旦select方法被调用,返回值表示一个或多个通道就绪,那么就可以调用选择器的selectedKey()方法来访问SelectionKey集合中就绪的通道如下:SetselectionKeys=selector.selectedKeys();可以遍历选中的key集合访问就绪通道,代码如下://获取监听事件SetselectionKeys=selector.selectedKeys();Iteratoriterator=selectionKeys.iterator();//迭代处理while(iterator.hasNext()){//获取事件SelectionKeykey=iterator.next();//移除事件,避免重复处理iterator.remove();//可连接if(key.isAcceptable()){...}//可读if(key.isReadable()){...}//可写if(key.isWritable()){...}}事件-drivenNetty是一个异步事件驱动的网络应用程序框架。在Netty中,事件是某些动作感兴趣的东西。比如在一个Channel中注册OP_READ,表示该Channel有读取兴趣,当Channel中有可读数据时,会通知一个事件。Netty事件驱动模型包含以下核心组件。ChannelChannel(管道)是JavaNIO的一个基本抽象,它代表了一个开放的连接到一个实体,例如硬件设备、文件、网络套接字,或者可以执行一个或多个不同I/O操作的程序。回调回调只是一种方法,是对已提供给另一个方法的方法的引用。这允许后者在适当的时候调用前者。Netty内部使用回调来处理事件;当触发回调时,相关事件可以由ChannelHandler接口处理。例如:在上一篇文章中,Netty开发的服务端的管道处理器代码中,当Channel中有可读消息时,会调用NettyServerHandler的回调方法channelRead。publicclassNettyServerHandlerextendsChannelInboundHandlerAdapter{//实际读取数据(这里我们可以读取客户端发送的消息)@OverridepublicvoidchannelRead(ChannelHandlerContextctx,Objectmsg)throwsException{System.out.println("serverctx="+CTX);频道channel=ctx.channel();//将msg转换成ByteBuf//ByteBuf是Netty提供的,不是NIO的ByteBuffer。ByteBufbuf=(ByteBuf)消息;System.out.println("客户端发送的消息为:"+buf.toString(CharsetUtil.UTF_8));System.out.println("客户端地址:"+channel.remoteAddress());}//处理异常,一般需要关闭通道@OverridepublicvoidexceptionCaught(ChannelHandlerContextctx,Throwablecause)throwsException{ctx.close();}}FutureFuture可以被认为是异步操作结果的占位符;它会在未来某个时刻完成,提供对结果的访问,Netty提供ChannelFuture用于异步操作,每个Netty出站I/O操作都会返回一个ChannelFuture(完全异步和事件驱动)。以下是ChannelFutureListener的用法示例。@OverridepublicvoidchannelRead(ChannelHandlerContextctx,Objectmsg)throwsException{ChannelFuturefuture=ctx.channel().close();future.addListener(newChannelFutureListener(){@OverridepublicvoidoperationComplete(ChannelFutureExchannelFuture)prows{/..}});}EventsandhandlersNetty中的事件按照出站/入站数据流分类:入站数据或相关状态变化触发的事件包括:连接已激活或去激活。数据读取。用户事件。错误事件、出站事件是将来会触发的操作的结果:打开或关闭与远程节点的连接。将数据写入或刷新到套接字。每个事件都可以分派给ChannelHandler类中的用户实现方法。下图显示了这样一个ChannelHandler链是如何处理事件的。ChannelHandler为处理器提供了一个基本的抽象,可以理解为响应特定事件而执行的回调。责任链模式(ChainofResponsibilityPattern)是一种行为设计模式,它为一个请求创建一个处理对象链。链中的每个节点都被视为一个对象,每个节点处理不同的请求,内部自动维护下一个节点对象。当一个请求从链的头端发出时,它将沿着链的路径依次传递给每个节点对象,直到有一个对象处理该请求。责任链模式的重点就在这个“链”上。一个链处理相似的请求,决定链中谁来处理这个请求,并返回相应的结果。在Netty中,定义了ChannelPipeline接口来抽象责任链。责任链模式定义了一个抽象处理程序(Handler)角色,该角色将请求抽象化,并定义了一个方法来设置和返回对下一个处理程序的引用。在Netty中,定义了ChannelHandler接口来承担这个角色。责任链模式的优缺点优点:发送方不需要知道自己发送的请求会被哪个对象处理,实现了发送方和接收方的解耦。发送者对象的简化设计。可以动态添加和删除节点。缺点:所有的请求都是从链头开始遍历,不利于性能。不方便调试。由于这种模式采用了类似递归的方式,调试时逻辑比较复杂。使用场景:一个请求需要一系列的处理任务。业务流程处理,如文件审批。扩展系统。ChannelPipelineNetty的ChannelPipeline设计采用责任链设计模式,底层采用双向链表的数据结构将链上的处理器串联起来。当客户端的每一个请求到达时,Netty认为ChannelPipeline中的所有处理器都有机会处理它。因此,对于入栈的请求,都是从头节点传播到尾节点(到尾节点的msg会被释放)。Inbound事件:通常是指IO线程产生的入站数据(通俗理解:从socket底部上来的事件都是inbound)。比如EventLoop接收到selector的OP_READ事件,inboundprocessor调用socketChannel.read(ByteBuffer)接收数据后,这会导致channel的ChannelPipeline中包含的下一个channelRead方法被调用。Outbound事件:通常是指IO线程实际执行的输出操作(通俗理解:凡是要主动操作socket底层的事件都是outbound)。例如,bind方法旨在请求将服务器套接字绑定到给定的SocketAddress,这将导致通道的ChannelPipeline中包含的下一个出站处理程序中的bind方法被调用。将事件传递给下一个处理器处理器必须调用ChannelHandlerContext中的事件传播方法将事件传递给下一个处理器。入站和出站事件的传播方法如下图所示:下面的例子说明了事件传播的典型方式:“连接的!”);ctx.fireChannelActive();}}publicclassMyOutboundHandlerextendsChannelOutboundHandlerAdapter{@Overridepublicvoidclose(ChannelHandlerContextctx,ChannelPromisepromise)throwsException{System.out.println("Closing...");ctx.close(承诺);}}总结正是由于Netty的分层架构设计合理,基于Netty的各种应用服务器和协议栈的开发才能如雨后春笋般迅速发展。说到底,我是一个被打击了还在努力的码农。如果文章对你有帮助,记得点赞关注,谢谢!