一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持使
( 这里首先就要搞清楚异步的NIO框架是什么意思)
用更高效的socket底层,对epoll空轮询引起的cpu占用飙升在内部进行了处理,避免了直接使用NIO的陷阱,简化了NIO的处理方 式。
采用多种decoder/encoder 支持,(后面我们会举例说明)
对TCP粘包/分包进行自动化处理(后面也会演示说明)
可使用接受/处理线程池,提高连接效率,对重连、心跳检测的简单支持可配置IO线程数、TCP参数, TCP接收和发送缓冲区
使用直接内存代替堆内存,通过内存池的方式循环利用ByteBuf通过引用计数器及时申请释放不再引用的对象,降低了GC频率使用单线程串行化的方式,高效的Reactor线程模型大量使用了volitale、使用了CAS和原子类、线程安全类的使用、读写锁的使用
在NIO中通过Selector的轮询当前是否有IO事件,根据JDK NIO api描述,Selector的select方法会一直阻塞,直到IO事件达到或超时,但是在某些场景下select方法会直接返回,即使没有超时并且也没有IO事件到达,这就是著名的epoll bug,这是一个比较严重的bug,它会导致线程陷入死循环,会让CPU飙到100%,极大地影响系统的可靠性,到目前为止,JDK都没有完全解决这个问题。
Netty的解决办法:对Selector的select操作周期进行统计,每完成一次空的select操作进行一次计数,若在某个周期内连续发生N(512)次空轮询,则触发了epoll死循环bug。重建Selector,判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭。
空转的次数可以通过可以在应用层通过设置系统属性io.netty.selectorAutoRebuildThreshold传入
由于是基于reactor模型说明具有两个线程租:
所有I/O操作都由一个线程完成,即多路复用、事件分发和处理都是在一个Reactor线程上完成的。既要接收客户端的连接请求,向服务端发起连接,又要发送/读取请求或应答/响应消息。一个NIO 线程同时处理成百上千的链路,性能上无法支撑,速度慢,若线程进入死循环,整个程序不可用,对于高负载、大并发的应用场景不合适。
有一个NIO 线程(Acceptor) 只负责监听服务端,接收客户端的TCP 连接请求;NIO 线程池负责网络IO 的操作,即消息的读取、解码、编码和发送;1 个NIO 线程可以同时处理N 条链路,但是1 个链路只对应1 个NIO 线程,这是为了防止发生并发操作问题。但在并发百万客户端连接或需要安全认证时,一个Acceptor 线程可能会存在性能不足问题。
Acceptor 线程用于绑定监听端口,接收客户端连接,将SocketChannel 从主线程池的Reactor 线程的多路复用器上移除,重新注册到Sub 线程池的线程上,用于处理I/O 的读写等操作,从而保证mainReactor只负责接入认证、握手等操作;
Bootstrap
Bootstrap 是 Netty 提供的一个便利的工厂类,可以通过它来完成 Netty 的客户端或服务器端的 Netty 初始化。
当然,Netty 的官方解释说,可以不用这个启动器。但是,一点点去手动创建channel 并且完成一些的设置和启动,会非常麻烦。还是使用这个便利的工具类,会比较好。
服务器:ServerBootstrap
客户端:Bootstrap
netty的辅助启动器,netty客户端和服务器的入口,Bootstrap是创建客户端连接的启动器,ServerBootstrap是监听服务端端口的启动器,是程序的入口。
Channel(通道)
serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class);client.group(group).channel(NioSocketChannel.class)
关联jdk原生socket的组件,常用的是NioServerSocketChannel和NioSocketChannel,NioServerSocketChannel负责监听一个tcp端口,有连接进来通过boss reactor创建一个NioSocketChannel将其绑定到worker reactor,然后worker reactor负责这个NioSocketChannel的读写等io事件。
Channel是Netty的核心概念之一,它是Netty网络通信的主体,由它负责同对端进行网络通信、注册和数据操作等功能。
EventLoopGroup bossGroup = new NioEventLoopGroup(10); // 创建接收线程池 EventLoopGroup workerGroup = new NioEventLoopGroup(20); // 创建工作线程池
netty最核心的几大组件之一,就是我们常说的reactor,人为划分为boss reactor和worker reactor。
通过EventLoopGroup(Bootstrap启动时会设置EventLoopGroup)生成,最常用的是nio的NioEventLoop,就如同EventLoop的名字,EventLoop内部有一个无限循环,维护了一个selector,处理所有注册到selector上的io操作,在这里实现了一个线程维护多条连接的工作。
在Netty 中,每一个 channel 绑定了一个thread 线程。
ChannelPipeline (传递途径)
在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应,它们的组成关系ChannelPipeline是ChannelHandler实例的列表(或则说是容器),用于处理或截获通道的接收和发送数据。ChannelPipeline提供了一种高级的截取过滤器模式,让用户可以在ChannelPipeline中完全控制一个事件及如何处理ChannelHandler与ChannelPipeline的交互。
可以这样说,一个新的通道就对应一个新的ChannelPipeline并附加至通道。一旦连接,通道Channel和ChannelPipeline之间的耦合是永久性的。通道Channel不能附加其他的ChannelPipeline或从ChannelPipeline分离。
netty最核心的几大组件之一,ChannelHandler的容器,netty处理io操作的通道,与ChannelHandler组成责任链。write、read、connect等所有的io操作都会通过这个ChannelPipeline,依次通过ChannelPipeline上面的ChannelHandler处理,这就是netty事件模型的核心。ChannelPipeline内部有两个节点,head和tail,分别对应着ChannelHandler链的头和尾。
netty最核心的几大组件之一,netty处理io事件真正的处理单元,可以创建自己的ChannelHandler来处理自己的逻辑,完全控制事件的处理方式。ChannelHandler和ChannelPipeline组成责任链,使得一组ChannelHandler像一条链一样执行下去。ChannelHandler分为inBound和outBound,分别对应io的read和write的执行链。ChannelHandler用ChannelHandlerContext包裹着,有prev和next节点,可以获取前后ChannelHandler,read时从ChannelPipeline的head执行到tail,write时从tail执行到head,所以head既是read事件的起点也是write事件的终点,与io交互最紧密。
因为所有的网络通信都涉及字节序列的移动,所以高效易用的数据结构明显是必不可少的。 Netty 的 ByteBuf 实现满足并超越了这些需求。让我们首先来看看它是如何通过使用不同的索引 来简化对它所包含的数据的访问的吧。
ByteBuf提供了两个指针来支持顺序读取和写入:readerIndex()用来读操作,writerIndex()用来写操作。下图展示了一个buffer是如何通过2个指针来划分为3个区域的:
+----------------+-------------+--------------+
| 可丢弃字节 | 可读字节 | 可写字节 |
| | (内容) | |
+-----------------+--------------+------------+
| | | |
0 <= 读索引 <= 写索引 <= 容量
可丢弃的字节这个区域包含了读操作已经读过了的字节。初始化时该区域的容量为0,但当读操作进行时它的容量会逐渐达到写索引。通过调用discardReadBytes()方法来声明不用区域,如下图描述所示:
+--------------------------+------------------------+------------------+
discardable bytes | readable bytes | writable bytes |
+--------------------------+------------------------+----------------------+
| | | |
0 <= readerIndex <= writerIndex <= capacity
+-----------------------+--------------------------------------+
| readable bytes | writable bytes (got more space)
| +-----------------------+--------------------------------------+
| | |
readerIndex (0) <= writerIndex (decreased) <= capacity
清除buffer索引你可以通过调用clear()方法来设置readerIndex()和writerIndex()的值为0.clear()方法并没有清除buffer中的内容而仅仅是将两个指针的值设为0.请注意:ByteBuf的clear()方法的语法和ByteBuffer的clear()操作时完全不同的。
+----------------------------+-----------------------+----------------------+
discardable bytes | readable bytes | writable bytes |
+--------------------------+------------------------+----------------------+
| | | |
0 <= readerIndex <= writerIndex <= capacity
+------------------------------------------------------------------+
writable bytes (got more space) |
+------------------------------------------------------------------+
| |
0 = readerIndex = writerIndex <= capacity
(1)它可以被用户自定义的缓冲区类型扩展;
这是最常用的类型,ByteBuf将数据存储在JVM的堆空间,通过将数据存储在数组中实现的。
(1)堆缓冲的优点是:由于数据存储在JVM的堆中可以快速创建和快速释放,并且提供了数组的直接快速访问的方法。
(2)堆缓冲缺点是:每次读写数据都要先将数据拷贝到直接缓冲区再进行传递。
Direct Buffer在堆之外直接分配内存,直接缓冲区不会占用堆的容量。jdk1.4引入的nio的ByteBuffer类允许jvm通过本地方法调用分配内存,这样做有两个好处
因为Direct Buffer是直接在内存中,所以分配内存空间和释放内存比堆缓冲区更复杂和慢。
这个是netty特有的缓冲类型。复合缓冲区就类似于一个ByteBuf的组合视图,在这个视图里面我们可以创建不同的ByteBuf(可以是不同类型的)。 这样,复合缓冲区就类似于一个列表,我们可以动态的往里面添加和删除其中的ByteBuf,JDK里面的ByteBuffer就没有这样的功能。
Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。堆内存多了一次内存拷贝,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。ByteBuffer由ChannelConfig分配,而ChannelConfig创建ByteBufAllocator默认使用Direct Buffer。
ByteBuf buf = Unpooled.buffer(data.length) 可以点进去看一下实现的源码 CompositeByteBuf 类可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。addComponents方法将 header 与 body 合并为一个逻辑上的 ByteBuf, 这两个 ByteBuf 在CompositeByteBuf 内部都是单独存在的, CompositeByteBuf 只是逻辑上是一个整体。 通过 FileRegion 包装的FileChannel.tranferTo方法 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环write方式导致的内存拷贝问题 通过 wrap方法, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作。