学习一门技术,在入门使用之后。最好是知道这个框架的整体设计思想。
Netty采用了典型的三层网络架构进行设计和开发,其逻辑架构图如下所示。
它由一系列辅助类完成,包括Reactor线程NioEventLoop及其父类,NioSocketChannel/NioServerSocketChannel及其父类,Buffer组件,Unsafe组件等。
该层的主要职责就是监听网络的读写和连接操作,负责将网络层的数据读取到内存缓冲区,然后触发各种网络事件,例如连接创建、连接激活、读事件、写事件等,将这些事件触发到PipeLine中,由PipeLine管理的责任链来进行后续的处理。
它负责上述的各种网络事件在责任链中的有序传播,同时负责动态地编排责任链。
责任链可以选择监听和处理自己关心的事件,它可以拦截处理事件,以及向前向后传播事件。
不同应用的Handler节点的功能也不同,通常情况下,往往会开发编解码Hanlder用于消息的编解码,可以将外部的协议消息转换成内部的POJO对象,这样上层业务则只需要关心处理业务逻辑即可,不需要感知底层的协议差异和线程模型差异,实现了架构层面的分层隔离。
例如,CMPP协议,用于管理和中国移动短信系统的对接。
架构的不同层面,需要关心和处理的对象都不同,通常情况下,对于业务开发者,只需要关心责任链的拦截和业务Handler的编排。
因为应用层协议栈往往是开发一次,到处运行,所以实际上对于业务开发者来说,只需要关心服务层的业务逻辑开发即可。
这种分层的架构设计理念实现了NIO框架各层之间的解耦,便于上层业务协议栈的开发和业务逻辑的定制。
正是由于Netty的分层架构设计非常合理,基于Netty的各种应用服务器和协议栈开发才能够如雨后春笋般得到快速发展。
影响最终产品的性能因素非常多,其中软件因素如下。
硬件因素如下。
尽管影响产品性能的因素非常多,但是架构的性能模型合理与否对性能的影响非常大。如果一个产品的架构设计得不好,无论开发如何努力,都很难开发出一个高性能、高可用的软件产品。
“性能是设计出来的,而不是测试出来的”。
下面我们看看Netty的架构设计是如何实现高性能的。
作为一个高性能的异步通信框架,架构的可靠性是大家选择的另一个重要依据。
下面我们看一下Netty架构的可靠性设计。
由于长连接不需要每次发送消息都创建链路,也不需要在消息交互完成时关闭链路,因此相对于短连接性能更高。
对于长连接,一旦链路建立成功便一直维系双方之间的链路,直到系统退出。
为了保证长连接的链路有效性,往往需要通过心跳机制周期性地进行链路检测。
使用周期性心跳的原因是:在系统空闲时,例如凌晨,往往没有业务消息。
如果此时链路被防火墙Hang住,或者遭遇网络闪断、网络单通等,通信双方无法识别出这类链路异常。
当有业务消息时,无须心跳检测,可以由业务消息进行链路可用性检测。所以心跳消息往往是在链路空闲时发送的。为了支持心跳机制,Netty提供了如下两种链路空闲检测机制。
为了满足不同用户场景的心跳定制,Netty提供了空闲状态检测事件通知机制,用户可以订阅:空闲超时事件、读空闲超时机制、写空闲超时事件,在接收到对应的空闲事件之后,灵活地进行定制。
Netty提供多种机制对内存进行保护,包括以下几个方面。
Netty的可定制性主要体现在以下几点。
基于Netty的基本NIO框架,可以方便地进行应用层协议定制,例如,HTTP协议栈、Thrift协议栈、FTP协议栈等。
这些扩展不需要修改Netty的源码,直接基于Netty的二进制类库即可实现协议的扩展和定制。
目前,业界存在大量的基于Netty框架开发的协议,例如基于Netty的HTTP协议、Dubbo协议、RocketMQ内部私有协议等。
作为一个高性能的NIO通信框架,Netty被广泛应用于大数据处理、互联网消息中间件、游戏和金融行业等。
大多数应用场景对底层的通信框架都有很高的性能要求,作为综合性能最高的NIO框架之一,Netty可以完全满足不同领域对高性能通信的需求。
本章我们将从架构层对Netty的高性能设计和关键代码实现进行剖析,看Netty是如何支撑高性能网络通信的。
一、网络传输方式问题。
传统的RPC框架或者基于RMI等方式的远程过程调用采用了同步阻塞I/O,当客户端的并发压力或者网络时延增大之后,同步阻塞I/O会由于频繁的wait导致I/O线程经常性的阻塞,由于线程无法高效的工作,I/O处理能力自然下降。
采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,接收到客户端连接之后,为其创建一个新的线程处理请求消息,处理完成之后,返回应答消息给客户端,线程销毁,这就是典型的“一请求,一应答”模型。
该架构最大的问题就是不具备弹性伸缩能力,当并发访问量增加后,服务端的线程个数和并发访问数成线性正比,由于线程是Java虛拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能急剧下降,随着并发量的继续增加,可能会发生句柄溢出、线程堆栈溢出等问题,并导致服务器最终宕机。
二、序列化性能差。
Java序列化存在如下几个典型问题:
三、线程模型问题。
由于采用同步阻塞I/O,这会导致每个TCP连接都占用1个线程,由于线程资源是JVM虚拟机非常宝贵的资源,当I/O读写阻塞导致线程无法及时释放时,会导致系统性能急剧下降,严重的甚至会导致虚拟机无法创建新的线程。
尽管影响I/O通信性能的因素非常多,但是从架构层面看主要有三个要素。
在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。
I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。
与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。
JDK1.4提供了对非阻塞I/O的支持,JDK1.5使用epoll替代了传统的select/poll,极大地提升了NIO通信的性能。
与Socket和ServerSocket类相对应,NIO也提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式使用非常简单,但是性能和可靠性都不好,非阻塞模式则正好相反。开发人员一般可以根据自己的需要来选择合适的模式,一般来说,低负载、低并发的应用程序可以选择同步阻塞I/O以降低编程复杂度。但是对于高负载、高并发的网络应用,需要使用NIO的非阻塞模式进行开发。
Netty的I/O线程NioEventLoop由于聚合了多路复用器Selector,可以同时并发处理成百上千个客户端SocketChannel。由于读写操作都是非阻塞的,这就可以充分提升I/O线程的运行效率,避免由频繁的I/O阻塞导致的线程挂起。
另外,由于Netty采用了异步通信模式,一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O“一连接,一线程”模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
常用的Reactor线程模型有三种,分别如下。
Reactor单线程模型,指的是所有的I/O操作都在同一个NIO线程上面完成,NIO线程的职责如下:
从架构层面看,一个NIO线程确实可以完成其承担的职责。
例如,通过Acceptor接收客户端的TCP连接请求消息,链路建立成功之后,通过Dispatch将对应的ByteBuffer派发到指定的Handler上进行消息解码。用户Handler可以通过NIO线程将消息发送给客户端。
对于一些小容量应用场景,可以使用单线程模型,但是对于高负载、大并发的应用却不合适,主要原因如下。
为了解决这些问题,演进出了Reactor多线程模型,下面我们看一下Reactor多线程模型。
Rector多线程模型与单线程模型最大的区别就是有一组NIO线程处理I/O操作,它的特点如下。
在绝大多数场景下,Reactor多线程模型都可以满足性能需求,但是,在极特殊应用场景中,一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题。
例如百万客户端并发连接,或者服务端需要对客户端的握手消息进行安全认证,认证本身非常损耗性能。在这类场景下,单独一个Acceptor线程可能会存在性能不足问题,为了解决性能问题,产生了第三种Reactor线程模型——主从Reactor多线程模型。
主从Reactor线程模型的特点是,服务端用于接收客户端连接的不再是个单线程的连接处理Acceptor,而是一个独立的Acceptor线程池。
Acceptor接收到客户端TCP连接请求处理完成后(可能包含接入认证等),将新创建的SocketChannel注册到I/O处理线程池的某个I/O线程上,由它负责SocketChannel的读写和编解码工作。
利用主从Reactor线程模型,可以解决1个Acceptor线程无法有效处理所有客户端连接的性能问题。因此,Netty官方也推荐使用该线程模型。
事实上,Netty的线程模型并非固定不变,通过在启动辅助类中创建不同的EventLoopGroup实例并进行适当的参数配置,就可以支持上述三种Reactor线程模型。可以根据业务场景的性能诉求,选择不同的线程模型。
服务端代码示例如下。
EventLoopGroupreactor=newNioEventLoopGroup(1);ServerBootstrapbootstrap=newServerBootstrap();bootstrap.group(reactor,reactor).channel(NioServerSocketChannel.class)......代码示例如下。
EventLoopGroupacceptor=newNioEventLoopGroup(1);EventLoopGroupioGroup=newNioEventLoopGroup();ServerBootstrapbootstrap=newServerBootstrap();bootstrap.group(acceptor,ioGroup).channel(NioServerSocketChannel.class)......代码示例如下
EventLoopGroupacceptorGroup=newNioEventLoopGroup();EventLoopGroupioGroup=newNioEventLoopGroup();ServerBootstrapbootstrap=newServerBootstrap();bootstrap.group(acceptorGroup,ioGroup).channel(NioServerSocketChannel.class)......无锁化的串行设计在大多数场景下,并行多线程处理可以提升系统的并发性能。
但是,如果对于共享资源的并发访问处理不当,会带来严重的锁竞争,这最终会导致性能的下降。
为了尽可能地避免锁竞争带来的性能损耗,可以通过串行化设计,即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。
为了尽可能提升性能,Netty对消息的处理采用了串行无锁化设计,在I/O线程内部进行串行操作,避免多线程竞争导致的性能下降。
Netty的串行化设计工作原理图如下图所示。
Netty的NioEventLoop读取到消息之后,直接调用ChannelPipeline的fireChannelRead(Objectmsg),只要用户不主动切换线程,一直会由NioEventLoop调用到用户的Handler,期间不进行线程切换。
这种串行化处理方式避免了多线程操作导致的锁的竞争,从性能角度看是最优的。
Netty的“零拷贝”主要体现在如下三个方面。
第一种情况。Netty的接收和发送ByteBuffer采用堆外直接内存(DIRECTBUFFERS)进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAPBUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
下面我们继续看第二种“零拷贝”的实现CompositeByteBuf,它对外将多个ByteBuf封装成一个ByteBuf,对外提供统一封装后的ByteBuf接口。CompositeByteBuf实际就是个ByteBuf的装饰器,它将多个ByteBuf组合成一个集合,然后对外提供统一的ByteBuf接口,添加ByteBuf,不需要做内存拷贝。
第三种“零拷贝”就是文件传输,Netty文件传输类DefaultFileRegion通过transferTo()方法将文件发送到目标Channel中。很多操作系统直接将文件缓冲区的内容发送到目标Channel中,而不需要通过循环拷贝的方式,这是一种更加高效的传输方式,提升了传输性能,降低了CPU和内存占用,实现了文件传输的“零拷贝”。
随着JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收是个非常轻量级的工作。
但是对于缓冲区Buffer,情况却稍有不同,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。
为了尽量重用缓冲区,Netty提供了基于内存池的缓冲区重用机制。
网络由下往上分为物理层、数据链路层、网络层、传输层和应用层。
IP协议对应于网络层,TCP协议对应于传输层,而HTTP协议对应于应用层,三者从本质上来说没有可比性,Socket则是对TCP/IP协议的封装和应用(程序员层面上)。
也可以说,TPC/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。
Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API)。
通过Socket,我们才能使用TCP/IP协议。
建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为clientSocket,另一个运行于服务器端,称为serverSocket。
套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。
HTTP协议是Web联网的基础,也是手机联网常用的协议之一,HTTP协议是建立在TCP协议之上的一种应用。
HTTP连接最显著的特点是客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接。
从建立连接到关闭连接的过程称为“一次连接”。
而UDP不是面向连接的,UDP传送数据前并不与对方建立连接,对接收到的数据也不发送确认信号,发送端不知道数据是否会正确接收,当然也不用重发,所以说UDP是无连接的、不可靠的一种数据传输协议。
Socket、SocketChannel二者的实质都是一样的,都是为了实现客户端与服务器端的连接而存在的,但是在使用上却有很大的区别。
具体如下:
下面是SocketChannel方式需要用到的几个核心类:
ServerSocketChannel:ServerSocket的替代类,支持阻塞通信与非阻塞通信。
SocketChannel:Socket的替代类,支持阻塞通信与非阻塞通信。
Selector:为ServerSocketChannel监控接收客户端连接就绪事件,为SocketChannel监控连接服务器读就绪和写就绪事件。
SelectionKey:代表ServerSocketChannel及SocketChannel向Selector注册事件的句柄。
在SelectionKey类中有如下几个静态常量:
Netty作为异步事件驱动的网络,高性能之处主要来自于其I/O模型和线程处理模型,前者决定如何收发数据,后者决定如何处理数据。
用什么样的通道将数据发送给对方,BIO、NIO或者AIO,I/O模型在很大程度上决定了框架的性能。
传统阻塞型I/O(BIO)可以用下图表示:
特点如下:
在I/O复用模型中,会用到Select,这个函数也会使进程阻塞,但是和阻塞I/O所不同的是这两个函数可以同时阻塞多个I/O操作。
而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。
Netty的非阻塞I/O的实现关键是基于I/O复用模型,这里用Selector对象表示:
Netty的IO线程NioEventLoop由于聚合了多路复用器Selector,可以同时并发处理成百上千个客户端连接。
当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起。
一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
传统的I/O是面向字节流或字符流的,以流式的方式顺序地从一个Stream中读取一个或多个字节,因此也就不能随意改变读取指针的位置。
在NIO中,抛弃了传统的I/O流,而是引入了Channel和Buffer的概念。
在NIO中,只能从Channel中读取数据到Buffer中或将数据从Buffer中写入到Channel。
基于Buffer操作不像传统IO的顺序操作,NIO中可以随意地读取任意位置的数据。
数据报如何读取?
读取之后的编解码在哪个线程进行,编解码后的消息如何派发,线程模型的不同,对性能的影响也非常大。
通常,我们设计一个事件处理模型的程序有两种思路:
事件驱动方式,发生事件,主线程把事件放入事件队列,在另外线程不断循环消费事件列表中的事件,调用事件对应的处理逻辑处理事件。
事件驱动方式也被称为消息通知方式,其实是设计模式中观察者模式的思路。
以GUI的逻辑处理为例,说明两种逻辑的不同:
轮询方式,线程不断轮询是否发生按钮点击事件,如果发生,调用处理逻辑。
这里借用O’Reilly大神关于事件驱动模型解释图:
主要包括4个基本组件:
可以看出,相对传统轮询模式,事件驱动有如下优点:
Reactor是反应堆的意思,Reactor模型是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。
服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor模式也叫Dispatcher模式,即I/O多了复用统一监听事件,收到事件后分发(Dispatch给某进程),是编写高性能网络服务器的必备技术之一。
Reactor模型中有2个关键组成:
Handlers,处理程序执行I/O事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。
Reactor通过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操作。
取决于Reactor的数量和Hanndler线程数量的不同,Reactor模型有3个变种:
可以这样理解,Reactor就是一个执行while(true){selector.select();…}循环的线程,会源源不断的产生新的事件,称作反应堆很贴切。
Netty主要基于主从Reactors多线程模型(如下图)做了一定的修改,其中主从Reactor多线程模型有多个Reactor:
这里引用DougLee大神的Reactor介绍:ScalableIOinJava里面关于主从Reactor多线程模型的图:
特别说明的是:虽然Netty的线程模型基于主从Reactor多线程,借用了MainReactor和SubReactor的结构。但是实际实现上SubReactor和Worker线程在同一个线程池中:
EventLoopGroupbossGroup=newNioEventLoopGroup();EventLoopGroupworkerGroup=newNioEventLoopGroup();ServerBootstrapserver=newServerBootstrap();server.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class)上面代码中的bossGroup和workerGroup是Bootstrap构造方法中传入的两个对象,这两个group均是线程池:
bossGroup线程池则只是在Bind某个端口后,获得其中一个线程作为MainReactor,专门处理端口的Accept事件,每个端口对应一个Boss线程。
workerGroup线程池会被各个SubReactor和Worker线程充分利用。
异步的概念和同步相对。
当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
Netty中的I/O操作是异步的,包括Bind、Write、Connect等操作会简单的返回一个ChannelFuture。
调用者并不能立刻获得结果,而是通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。
当Future对象刚刚创建时,处于非完成状态,调用者可以通过返回的ChannelFuture来获取操作执行的状态,注册监听函数来执行完成后的操作。
通过isDone方法来判断当前操作是否完成。
通过isSuccess方法来判断已完成的当前操作是否成功。
通过getCause方法来获取已完成的当前操作失败的原因。
通过isCancelled方法来判断已完成的当前操作是否被取消。
通过addListener方法来注册监听器,当操作已完成(isDone方法返回完成),将会通知指定的监听器;如果Future对象已完成,则理解通知指定的监听器。
例如下面的代码中绑定端口是异步操作,当绑定操作处理完,将会调用相应的监听器处理逻辑。
serverBootstrap.bind(port).addListener(future->{if(future.isSuccess()){System.out.println(newDate()+":端口["+port+"]绑定成功!");}else{System.err.println("端口["+port+"]绑定失败!");}});相比传统阻塞I/O,执行I/O操作后线程会被阻塞住,直到操作完成;异步处理的好处是不会造成线程阻塞,线程在I/O操作期间可以执行别的程序,在高并发情形下会更稳定和更高的吞吐量。
Netty功能特性如下:
传输服务,支持BIO和NIO。
容器集成,支持OSGI、JBossMC、Spring、Guice容器。
协议支持,HTTP、Protobuf、二进制、文本、WebSocket等一系列常见协议都支持。还支持通过实行编码解码逻辑来实现自定义协议。
Core核心,可扩展事件模型、通用通信API、支持零拷贝的ByteBuf缓冲对象。
Bootstrap意思是引导,一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类。
正如前面介绍,在Netty中所有的IO操作都是异步的,不能立刻得知消息是否被正确处理。
但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过Future和ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。
Netty网络通信的组件,能够用于执行网络I/O操作。Channel为用户提供:
当前网络连接的通道的状态(例如是否打开?是否已连接?)
网络连接的配置参数(例如接收缓冲区大小)
提供异步的网络I/O操作(如建立连接,读写,绑定端口),异步调用意味着任何I/O调用都将立即返回,并且不保证在调用结束时所请求的I/O操作已完成。
调用立即返回一个ChannelFuture实例,通过注册监听器到ChannelFuture上,可以I/O操作成功、失败或取消时回调通知调用方。
支持关联I/O操作与对应的处理程序。
不同协议、不同的阻塞类型的连接都有不同的Channel类型与之对应。
下面是一些常用的Channel类型:
NioSocketChannel,异步的客户端TCPSocket连接。
NioServerSocketChannel,异步的服务器端TCPSocket连接。
NioDatagramChannel,异步的UDP连接。
NioSctpChannel,异步的客户端Sctp连接。
NioSctpServerChannel,异步的Sctp服务器端连接,这些通道涵盖了UDP和TCP网络IO以及文件IO。
Netty基于Selector对象实现I/O多路复用,通过Selector一个线程可以监听多个连接的Channel事件。
当向一个Selector中注册Channel后,Selector内部的机制就可以自动不断地查询(Select)这些注册的Channel是否有已就绪的I/O事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个Channel。
NioEventLoop中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用NioEventLoop的run方法,执行I/O任务和非I/O任务:
I/O任务,即selectionKey中ready的事件,如accept、connect、read、write等,由processSelectedKeys方法触发。
非IO任务,添加到taskQueue中的任务,如register0、bind0等任务,由runAllTasks方法触发。
NioEventLoopGroup,主要管理eventLoop的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个Channel上的事件,而一个Channel只对应于一个线程。
ChannelHandler是一个接口,处理I/O事件或拦截I/O操作,并将其转发到其ChannelPipeline(业务处理链)中的下一个处理程序。
ChannelHandler本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类:
ChannelInboundHandler用于处理入站I/O事件。
ChannelOutboundHandler用于处理出站I/O操作。
或者使用以下适配器类:
ChannelInboundHandlerAdapter用于处理入站I/O事件。
ChannelOutboundHandlerAdapter用于处理出站I/O操作。
ChannelDuplexHandler用于处理入站和出站事件。
保存ChannelHandler的List,用于处理或拦截Channel的入站事件和出站操作。
ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及Channel中各个的ChannelHandler如何相互交互。
下图引用Netty的Javadoc4.1中ChannelPipeline的说明,描述了ChannelPipeline中ChannelHandler通常如何处理I/O事件。
I/O事件由ChannelInboundHandler或ChannelOutboundHandler处理,并通过调用ChannelHandlerContext中定义的事件传播方法。
例如ChannelHandlerContext.fireChannelRead(Object)和ChannelOutboundInvoker.write(Object)转发到其最近的处理程序。
入站事件由自下而上方向的入站处理程序处理,如图左侧所示。入站Handler处理程序通常处理由图底部的I/O线程生成的入站数据。
通常通过实际输入操作(例如SocketChannel.read(ByteBuffer))从远程读取入站数据。
出站事件由上下方向处理,如图右侧所示。出站Handler处理程序通常会生成或转换出站传输,例如write请求。
I/O线程通常执行实际的输出操作,例如SocketChannel.write(ByteBuffer)。
在Netty中每个Channel都有且仅有一个ChannelPipeline与之对应,它们的组成关系如下:
一个Channel包含了一个ChannelPipeline,而ChannelPipeline中又维护了一个由ChannelHandlerContext组成的双向链表,并且每个ChannelHandlerContext中又关联着一个ChannelHandler。
入站事件和出站事件在一个双向链表中,入站事件会从链表head往后传递到最后一个入站的handler,出站事件会从链表tail往前传递到最前一个出站的handler,两种类型的handler互不干扰。
初始化并启动Netty服务端过程如下:
基于ServerBootstrap(服务端启动引导类),配置EventLoopGroup、Channel类型,连接参数、配置入站、出站事件handler。
绑定端口,开始工作。
结合上面介绍的NettyReactor模型,介绍服务端Netty的工作架构图:
Server端包含1个BossNioEventLoopGroup和1个WorkerNioEventLoopGroup。
NioEventLoopGroup相当于1个事件循环组,这个组里包含多个事件循环NioEventLoop,每个NioEventLoop包含1个Selector和1个事件循环线程。
每个BossNioEventLoop循环执行的任务包含3步:
轮询Accept事件。
处理AcceptI/O事件,与Client建立连接,生成NioSocketChannel,并将NioSocketChannel注册到某个WorkerNioEventLoop的Selector上。
处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用eventloop.execute或schedule执行的任务,或者其他线程提交到该eventloop的任务。
每个WorkerNioEventLoop循环执行的任务包含3步:
轮询Read、Write事件。
处理I/O事件,即Read、Write事件,在NioSocketChannel可读、可写事件发生时进行处理。
处理任务队列中的任务,runAllTasks。
其中任务队列中的Task有3种典型使用场景。
ctx.channel().eventLoop().execute(newRunnable(){@Overridepublicvoidrun(){//...}});2.非当前Reactor线程调用Channel的各种方法例如在推送系统的业务线程里面,根据用户的标识,找到对应的Channel引用,然后调用Write类方法向该用户推送消息,就会进入到这种场景。最终的Write会提交到任务队列中后被异步消费。
ctx.channel().eventLoop().schedule(newRunnable(){@Overridepublicvoidrun(){}},60,TimeUnit.SECONDS);小结现在稳定推荐使用的主流版本还是Netty4,Netty5中使用了ForkJoinPool,增加了代码的复杂度,但是对性能的改善却不明显,所以这个版本不推荐使用,官网也没有提供下载链接。
Netty入门门槛相对较高,是因为这方面的资料较少,并不是因为它有多难,大家其实都可以像搞透Spring一样搞透Netty。
在学习之前,建议先理解透整个框架原理结构,运行过程,可以少走很多弯路。