本文基于Netty4.1展开介绍相关理论模型,使用场景,基本组件、总体架构,知其然且知其因此然,但愿给读者提供学习实践参考。html
Netty是 一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。java
JDK原生也有一套网络应用程序API,可是存在一系列问题,主要以下:react
Netty的对JDK自带的NIO的API进行封装,解决上述问题,主要特色有:编程
Netty常见的使用场景以下:缓存
有兴趣的读者能够了解一下目前有哪些开源项目使用了 Netty:Related projects安全
Netty做为异步事件驱动的网络,高性能之处主要来自于其I/O模型和线程处理模型,前者决定如何收发数据,后者决定如何处理数据bash
用什么样的通道将数据发送给对方,BIO、NIO或者AIO,I/O模型在很大程度上决定了框架的性能服务器
传统阻塞型I/O(BIO)能够用下图表示: 微信
特色markdown
问题
在I/O复用模型中,会用到select,这个函数也会使进程阻塞,与阻塞I/O所不一样的,这个函数能够同时阻塞多个I/O操做,可同时对多个读操写操做的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操做函数
Netty的非阻塞I/O的实现关键是基于I/O复用模型,这里用Selector对象表示:
Netty的IO线程NioEventLoop因为聚合了多路复用器Selector,能够同时并发处理成百上千个客户端链接。当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程能够进行其余任务。线程一般将非阻塞 IO 的空闲时间用于在其余通道上执行 IO 操做,因此单独的线程能够管理多个输入和输出通道。
因为读写操做都是非阻塞的,这就能够充分提高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 大神关于事件驱动模型解释图
能够看出,相对传统轮询模式,事件驱动有以下优势:
Reactor是反应堆的意思,Reactor模型,是指经过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。 服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor模式也叫Dispatcher模式,即I/O多路复用统一监听事件,收到事件后分发(Dispatch给某进程),是编写高性能网络服务器的必备技术之一。
Reactor模型中有2个关键组成:
Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件作出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人
Handlers 处理程序执行I/O事件要完成的实际事件,相似于客户想要与之交谈的公司中的实际官员。Reactor经过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操做
取决于Reactor的数量和Handler线程数量的不一样,Reactor模型有3个变种
能够这样理解,Reactor就是一个执行while (true) { selector.select(); ...}循环的线程,会源源不断的产生新的事件,称做反应堆很贴切。
篇幅关系,这里再也不具体展开Reactor特性、优缺点比较,有兴趣的读者能够参考我以前另一篇文章:《理解高性能网络模型》
Netty主要基于主从Reactors多线程模型(以下图)作了必定的修改,其中主从Reactor多线程模型有多个Reactor:MainReactor和SubReactor:
这里引用Doug Lee大神的Reactor介绍:Scalable IO in Java里面关于主从Reactor多线程模型的图
特别说明的是: 虽然Netty的线程模型基于主从Reactor多线程,借用了MainReactor和SubReactor的结构,可是实际实现上,SubReactor和Worker线程在同一个线程池中:
EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap server = new ServerBootstrap(); server.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) 复制代码
上面代码中的bossGroup 和workerGroup是Bootstrap构造方法中传入的两个对象,这两个group均是线程池
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能马上获得结果。实际处理这个调用的部件在完成后,经过状态、通知和回调来通知调用者。
Netty中的I/O操做是异步的,包括bind、write、connect等操做会简单的返回一个ChannelFuture,调用者并不能马上得到结果,经过Future-Listener机制,用户能够方便的主动获取或者经过通知机制得到IO操做结果。
当future对象刚刚建立时,处于非完成状态,调用者能够经过返回的ChannelFuture来获取操做执行的状态,注册监听函数来执行完成后的操做,常见有以下:
例以下面的代码中绑定端口是异步操做,当绑定操做处理完,将会调用相应的监听器处理逻辑
serverBootstrap.bind(port).addListener(future -> { if (future.isSuccess()) { System.out.println(new Date() + ": 端口[" + port + "]绑定成功!"); } else { System.err.println("端口[" + port + "]绑定失败!"); } }); 复制代码
相比传统阻塞I/O,执行I/O操做后线程会被阻塞住, 直到操做完成;异步处理的好处是不会形成线程阻塞,线程在I/O操做期间能够执行别的程序,在高并发情形下会更稳定和更高的吞吐量。
前面介绍完Netty相关一些理论介绍,下面从功能特性、模块组件、运做过程来介绍Netty的架构设计
Bootstrap意思是引导,一个Netty应用一般由一个Bootstrap开始,主要做用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类。
正如前面介绍,在Netty中全部的IO操做都是异步的,不能马上得知消息是否被正确处理,可是能够过一会等它执行完成或者直接注册一个监听,具体的实现就是经过Future和ChannelFutures,他们能够注册一个监听,当操做执行成功或失败时监听会自动触发注册的监听事件。
Netty网络通讯的组件,可以用于执行网络I/O操做。 Channel为用户提供:
不一样协议、不一样的阻塞类型的链接都有不一样的 Channel 类型与之对应,下面是一些经常使用的 Channel 类型
Netty基于Selector对象实现I/O多路复用,经过 Selector, 一个线程能够监听多个链接的Channel事件, 当向一个Selector中注册Channel 后,Selector 内部的机制就能够自动不断地查询(select) 这些注册的Channel是否有已就绪的I/O事件(例如可读, 可写, 网络链接完成等),这样程序就能够很简单地使用一个线程高效地管理多个 Channel 。
NioEventLoop中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用NioEventLoop的run方法,执行I/O任务和非I/O任务:
两种任务的执行时间比由变量ioRatio控制,默认为50,则表示容许非IO任务执行的时间与IO任务的执行时间相等。
NioEventLoopGroup,主要管理eventLoop的生命周期,能够理解为一个线程池,内部维护了一组线程,每一个线程(NioEventLoop)负责处理多个Channel上的事件,而一个Channel只对应于一个线程。
ChannelHandler是一个接口,处理I/O事件或拦截I/O操做,并将其转发到其ChannelPipeline(业务处理链)中的下一个处理程序。
ChannelHandler自己并无提供不少方法,由于这个接口有许多的方法须要实现,方便使用期间,能够继承它的子类:
或者使用如下适配器类:
保存Channel相关的全部上下文信息,同时关联一个ChannelHandler对象
保存ChannelHandler的List,用于处理或拦截Channel的入站事件和出站操做。 ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户能够彻底控制事件的处理方式,以及Channel中各个的ChannelHandler如何相互交互。
下图引用Netty的Javadoc4.1中ChannelPipline的说明,描述了ChannelPipeline中ChannelHandler一般如何处理I/O事件。 I/O事件由ChannelInboundHandler或ChannelOutboundHandler处理,并经过调用ChannelHandlerContext中定义的事件传播方法(例如ChannelHandlerContext.fireChannelRead(Object)和ChannelOutboundInvoker.write(Object))转发到其最近的处理程序。
I/O Request
via Channel or
ChannelHandlerContext
|
+---------------------------------------------------+---------------+
| ChannelPipeline | |
| \|/ |
| +---------------------+ +-----------+----------+ |
| | Inbound Handler N | | Outbound Handler 1 | |
| +----------+----------+ +-----------+----------+ |
| /|\ | |
| | \|/ |
| +----------+----------+ +-----------+----------+ |
| | Inbound Handler N-1 | | Outbound Handler 2 | |
| +----------+----------+ +-----------+----------+ |
| /|\ . |
| . . |
| ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
| [ method call] [method call] |
| . . |
| . \|/ |
| +----------+----------+ +-----------+----------+ |
| | Inbound Handler 2 | | Outbound Handler M-1 | |
| +----------+----------+ +-----------+----------+ |
| /|\ | |
| | \|/ |
| +----------+----------+ +-----------+----------+ |
| | Inbound Handler 1 | | Outbound Handler M | |
| +----------+----------+ +-----------+----------+ |
| /|\ | |
+---------------+-----------------------------------+---------------+
| \|/
+---------------+-----------------------------------+---------------+
| | | |
| [ Socket.read() ] [ Socket.write() ] |
| |
| Netty Internal I/O Threads (Transport Implementation) |
+-------------------------------------------------------------------+
复制代码
入站事件由自下而上方向的入站处理程序处理,如图左侧所示。 入站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服务端过程以下:
public static void main(String[] args) { // 建立mainReactor NioEventLoopGroup boosGroup = new NioEventLoopGroup(); // 建立工做线程组 NioEventLoopGroup workerGroup = new NioEventLoopGroup(); final ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap // 组装NioEventLoopGroup .group(boosGroup, workerGroup) // 设置channel类型为NIO类型 .channel(NioServerSocketChannel.class) // 设置链接配置参数 .option(ChannelOption.SO_BACKLOG, 1024) .childOption(ChannelOption.SO_KEEPALIVE, true) .childOption(ChannelOption.TCP_NODELAY, true) // 配置入站、出站事件handler .childHandler(new ChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) { // 配置入站、出站事件channel ch.pipeline().addLast(...); ch.pipeline().addLast(...); } }); // 绑定端口 int port = 8080; serverBootstrap.bind(port).addListener(future -> { if (future.isSuccess()) { System.out.println(new Date() + ": 端口[" + port + "]绑定成功!"); } else { System.err.println("端口[" + port + "]绑定失败!"); } }); } 复制代码
结合上面的介绍的Netty Reactor模型,介绍服务端Netty的工做架构图:
server端包含1个Boss NioEventLoopGroup和1个Worker NioEventLoopGroup,NioEventLoopGroup至关于1个事件循环组,这个组里包含多个事件循环NioEventLoop,每一个NioEventLoop包含1个selector和1个事件循环线程。
每一个Boss NioEventLoop循环执行的任务包含3步:
每一个Worker NioEventLoop循环执行的任务包含3步:
其中任务队列中的task有3种典型使用场景
ctx.channel().eventLoop().execute(new Runnable() { @Override public void run() { //... } }); 复制代码
2 非当前reactor线程调用channel的各类方法 例如在推送系统的业务线程里面,根据用户的标识,找到对应的channel引用,而后调用write类方法向该用户推送消息,就会进入到这种场景。最终的write会提交到任务队列中后被异步消费。
3 用户自定义定时任务
ctx.channel().eventLoop().schedule(new Runnable() { @Override public void run() { } }, 60, TimeUnit.SECONDS); 复制代码
如今稳定推荐使用的主流版本仍是Netty4,Netty5 中使用了 ForkJoinPool,增长了代码的复杂度,可是对性能的改善却不明显,因此这个版本不推荐使用,官网也没有提供下载连接。
Netty 入门门槛相对较高,实际上是由于这方面的资料较少,并非由于他有多难,你们其实均可以像搞透 Spring 同样搞透 Netty。在学习以前,建议先理解透整个框架原理结构,运行过程,能够少走不少弯路。
software-architecture-patterns.pdf
《Netty In Action》
《Netty权威指南》