定义
Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,其包含以下优点:
- 封装JDK自带NIO的API
- 可靠性能力补齐
- 高性能,高吞吐量,低资源消耗
特点
NIO
传统BIO模型如下:
一个请求Socket请求都有对应一个Thread来承接,由于Thread数量有限,所以该模式不支持高并发,且Thread执行过程中会阻塞请求。
而NIO模型如下:
Selector处理所有请求,断开请求和处理线程的耦合,达到多路复用的目的,从而提高并发。
Linux提供select、poll、epoll三种方式来实现IO多路复用,Linux 2.6内核正式引入epoll,其采用Reactor模式实现,完全基于事件驱动,性能相比select和poll有较大提升,Netty、Nginx、Redis等组件中对于网络IO的处理,均使用epoll。
Netty的线程模型
Netty内部实现了两个线程池,boss线程池和work线程池,其中boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求时,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责将请求的read和write事件分发给对应的Handler处理。
零拷贝
一般我们的数据如果需要从IO读取到堆内存,中间需要经过Socket缓冲区,也就是说一个数据会被拷贝两次才能到达他的的终点。
针对这种情况,当Netty需要接收数据的时候,他会在堆内存之外开辟一块内存,数据就直接从IO读到了那块内存中去,在Netty里面通过ByteBuf可以直接对这些数据进行直接操作,从而加快了传输速度。
核心设计
1 Channel
Channel是Java NIO的一个基本构造,它代表一个实体的开放连接,包括读写操作。
可以把Channel看作是数据传输的载体。
2 Callback
回调是常用的被调用方触发调用方的编程方式之一,Netty内部使用回调的方式来处理事件。
3 Future
Future提供了另一种在操作完成时通知应用程序的方式。
这个对象可以看作是一个异步操作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。
比如ChannelFuture.connect()
方法,作用是异步连接远程节点,方法直接返回,不会阻塞。
4 Event和ChannelHandler
Netty是基于事件驱动的,所以内部定义了非常多的事件。
事件按照入站或出站数据流的相关性进行分类的,其中入站数据或者相关的状态更改而触发的事件包括:
-
连接已被激活或者连接失活
-
数据读取
-
用户事件
-
错误事件
出站事件是未来将会触发的某个动作的操作结果,这些动作包括:
-
打开或者关闭到远程节点的连接
-
将数据写到或者冲刷到套接字
这些事件都可以被分发给ChannelHandler类中的某个用户实现的方法,你可以把ChannelHandler当作为处理特定事件而执行的回调。
核心组件
ByteBuf
网络传输主要靠字节,Java NIO中使用ByteBuffer作为其字节容器,Netty中则提供了更简单易使用的ByteBuf。
工作方式
ByteBuf具有两个索引:一个用来读、一个用来写。
读索引记录已经读到的位置,写索引记录已经写到的位置。read和write开头的方法,都将移动这两个索引。set和get开头的方法则不会。
读索引肯定小于写索引,否则同数组越界类似,抛出异常IndexOutOfBoundsException
。分别有isReadable()
和isWritable()
方法来判定是否可以读取或写入索引。
使用模式
1 堆缓冲区
最常用的模式是将数据存储在JVM堆空间中。其内部数据是一个Java数组,所以这种模式被称为**“支撑数组”**。
可以通过hasArray()
方法来判定是否有一个支撑数组。
2 直接缓冲区
对于直接缓冲区,其数据都存储在操作系统的物理内存上。
由于本地I/O操作前,都会先从堆缓冲区复制到直接缓冲区上。所以使用直接缓冲区可以省去一次数据复制的代价。
缺点如下:
-
创建和销毁它需要调用操作系统的方法,非常昂贵,通常建议池化使用。
-
在Java中操作直接缓冲区上的数据时,需要先复制到堆上。
3 复合缓冲区
Netty通过ByteBuf的子类CompositeByteBuf
来实现符合缓冲区,特点是可以根据需要添加或者删除ByteBuf实例。
比如http协议传输消息包含头部和主体,如果发送多个消息均适用同一个主体,而仅仅是头部不同,那么就很适合使用CompositeByteBuf
。
CompositeByteBuf
不支持直接访问其支撑数组,需要先拷贝到一个Byte数组后再进行操作。
清理ByteBuf
清理ByteBuf有两种方式:一个是clean()
,另一个是discardReadBytes()
。
clean()
方法会直接重置两个索引,而discardReadBytes()
方法则是对数据进行了移动。
读索引左侧的空间被称为“可丢弃空间”,discardReadBytes()
方法的原理就是将读索引右侧的数据,覆盖到索引位置为0的地方,简单的说就是将数据“整体左移归零”。
派生缓冲区
调用duplicate()
、slice()
等方法,将建立一个新的ByteBuf实例,具有独立的读写索引和标记索引,但和原ByteBuf实例共享存储空间,这就意味着改变其中一个,另一个的值也会发生变化。
ByteBufHolder
Netty提供了ByteBufHolder
接口,用来操作ByteBuf和一些其他属性值,起到了包装ByteBuf的作用。
Netty提供了一个默认的实现DefaultByteBufHolder
。
ByteBuf的分配
Netty通过ByteBufAllocator
接口实现了ByteBuf的分配。
Netty对其有两种实现PooledByteBufAllocator
和UnpooledByteBufAllocator
。
前者默认池化对象,后者则每次访问都返回一个新对象。
引用计数
Netty通过引用计数来计算池化对象的使用情况,如果引用计数为0,所以该对象可以被释放了。
试图访问一个已经被释放的引用计数的对象,将会导致一个IllegalReferenceCountException
。
ChannelHandler和ChannelPipeline
ChannelHandler
每一个Channel代表了一个网络连接和IO处理之间的桥梁,Netty中通过一连串的Channelhandler来处理Channel。
Netty定义了ChannelInBoundHandler和ChannelOutBoundHandler两个接口来分别定义入站和出站的处理行为。
ByteBuf内存泄漏
其中入站时,需要在ChannelInBoundHandler的channelRead()
方法中手动释放池化的ByteBuf实例相关的内存。
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ReferenceCountUtil.release(msg);
}
Netty提供了一个简单的入站处理器实现:SimpleChannelInboundHandler
,它会自动释放资源。
如果想要将消息传递给后续处理器而不能释放,则需要增加引用计数,然后通过ChannelhandlerContext传递:
ctx.fireChannelRead(msg.retain());
出站时,如果你处理了write()操作并丢弃了一个消息,那么你也应该负责释放它。释放资源后需要通知ChannelPromise,否则可能会出现ChannelFutureListener收不到某个消息已经被处理了的通知的情况。
ChannelPipeline
ChannelPipiline是可以动态调整的。
每一个ChannelHandler都可以通过添加、删除、替换ChannelHandler来实时修改ChannelPipeline的布局。
ChannelHandlerContext
每一个ChannelHandler都对应了一个ChannelHandlerContext,那么当在 Channel 建立或 ChannelHandler 处理完一个事件后需要传递给下一个 ChannelHandler 时,就会创建一个ChannelHandlerContext。
在一条ChannelPipeline上,从一个ChannelHandler移动到另一个ChannelHandler,是通过ChannelHandlerContext的调用来完成的。
也可以直接操作ChannelHandlerContext,来从ChannelPipeline中某个特定的ChannelHandler来开始调用,而这是一个常见场景。
其他用法
可以通过调用ChannelHandlerContext.pipelines()
方法来获取ChannelPipeline的引用,从而通过对其中的ChannelHandler进行操作来完成一些复杂设计。
异常处理
入站异常
如果需要处理异常情况需要重写如下方法:
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception
异常会按照入站顺序继续在ChannelPipeline中移动,所以后面所有的ChannelHandler中的exceptionCaught()
方法仍然会被执行。
出站异常
每个出站操作都将返回一个ChannelFuture,包括异常,所以可通过ChannelFuture的cause()
方法来获取异常:
ChannelFuture future = channel.write(someMessage);
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture f) {
if (!f.isSuccess()) {
f.cause().printStackTrace();
f.channel().close();
}
}
});
EventLoop
主要作用是处理异步事件、管理I/O、管理线程。
当Channel建立时,Netty中的EventLoopGroup会为其分配一个EventLoop,其中包含一个Thread。多个Channel可以共享同一个EventLoop,这有利于提高系统的并发能力。
Channel执行具体的I/O操作时,由EventLoop中的Thread来处理其产生的I/O事件,并将事件分发给对应的ChannelHandler来处理。
Bootstrap
引导类的作用是设置配置并引导启动。
对于服务端和客户端来说有所不同:
服务端运行时需要一个Channel来接收来自客户端的连接,并创建子Channel用于通信。
客户端运行时则只需要一个单独的Channel来用于所有的网络交互。
服务端引导类是ServerBootstrap
,客户端引导类是Bootstrap
,二者均继承于AbstractBootstrap
。