I/O模型就是用什么样的通道进行数据的发送与接收,很大程度上决定了程序通信的性能
Java共支持3种网络IO模型:BIO、NIO、AIO
BIO:同步阻塞模型,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情(没有数据需要读取时)会进行阻塞造成不必要的线程开销
NIO:同步非阻塞模型,服务器实现模式为一个线程处理多个请求,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求就进行处理
AIO:异步非阻塞模型,AIO引入了异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程。它的特点是先由操作系统完成后才通知服务器程序启动线程处理,一般适用于连接数较多且连接时间较长的应用
个人理解,阻塞与非阻塞本质上的区别就是一个线程(协程)能处理多少个请求(这里指的是无障碍正常处理)。
如果一个线程只能处理一个请求,那这就是阻塞模型。如果能处理多个请求,那就是非阻塞模型。
NIO虽然是非阻塞的,但仍然是个同步模型,那么怎么去理解这个同步呢?
实际上,假设我们没有用任何的Selector,在一个循环内获取连接accpet,在非阻塞的情况下,当没有获取到accpet时就不会阻塞,而是在循环内再次去获取。
而当我们获取到了accept,在我们拿到这个数据的过程,又或者是复制数据的过程,我们必须得等到数据返回了以后才能进行下一步,这个就是NIO是同步的原因
那么异步也很显然了,就是在我们拿这个数据的过程中不进行等待,而是在这个数据返回后通过一个**回调函数(线程)**来获取,这个就叫做异步。
- Java NIO全称Java Non-Blocking IO,是指jdk提供的新api。从jdk1.4开始,java提供了一系列改进的IO的新特性,被统称为NIO(New IO),是同步非阻塞的
- NIO相关的类都被放在java.nio包下,并且对原java.io包中的很多类都进行了改写
- NIO有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)
- NIO是面向缓存区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞性的高伸缩性网络
- NIO的非阻塞模式是一个线程从某通道发送或读取数据时,仅得到当前可用的数据,如果没有可用的数据就什么都不会获取,而不是保持线程阻塞。所以直到数据可用之前,线程可以继续做其他的事情。
- HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级
缓冲区本质上是一个可以读写数据的内存块,可以理解成一个容器对象(含数组),该容器提供了一组方法,可以更轻松的使用内存块。
缓冲区对象内置了一些机制,能够追踪和记录缓冲区的状态变化情况。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer
在NIO中,Buffer是一个顶层父类,它是一个抽象类,层级图如下
- ByteBuffer,存储字节数据到缓冲区
- ShortBuffer,存储字符串数据到缓冲区
- CharBuffer,存储字符数据到缓冲区
- ...
Buffer类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素的信息,例如:缓冲区的容量,终点,当前位置,标记等
NIO的通道类似于流,有以下区别
- 通道可以进行读写,而流只能读或写
- 通道可以实现异步读写数据
- 通道可以从缓冲读数据,也可以写数据到缓冲
Channel是一个接口,常用的实现类有:FileChannel
, DatagramChannel
, ServerSocketChannel
和SocketChannel
。其中FIleChannel
用于文件的数据读写,DatagramChannel用于UDP的数据读写,ServerSocketChannel和SocketChannel用于TCP的数据读写
注意:FileChannel只能工作在阻塞模式中
FileChannel
不能直接打开,必须用过FileInputStream
、FileOutputStream
、RandomAccessFile
来获取FileChannel
,他们都有getChannel
方法
- 通过
FileInputStream
获取的Channel只能读 - 通过
FileOutputStream
获取的Channel只能写 - 通过
RandomAccessFile
获取的channel根据构造RandomAccessFile
时的读写模式决定是否可读可写
Selector负责监听事件,然后分发给Channel
Channel注册到Selector时,需要保证是非阻塞的,即调用方法configureBlocking(false)
从accpet中获得的Channel注册进Selector之后,即可读取数据
read需要注意错误处理,有如下两种情况
- 客户端异常关闭,则会抛出异常,如果没有处理则会直接中断线程,所以要捕获异常然后把key给
cancel
掉 - 客户端正常关闭,会返回-1,如果没有处理,因为key没有被删掉所以会一直循环下去,所以需要判断返回值
- 一个Thread对应一个Selector
- 一个Selector对应多个Channel
- 一个Channel对应一个Buffer
- 程序切换到哪个Channel是由事件(Event)决定的,Selector会根据不同的事件,在各个通道上切换
- Buffer就是一个内存块,低层维护了一个数组
- 数据的读写是通过Buffer,与BIO通过流不同,NIO的Buffer可以读也可以写,用Flip方法切换
- Channel是双向的,可以返回低层操作系统的情况
现在都是多核cpu,设计时要充分考虑cpu多核的利用
NIO如果只用一个选择器,就不能充分的利用多核cpu,该如何改进?
分两组选择器
- 单线程配一个选择器,专门处理accept事件
- 创建cpu核心数的线程,每个线程分配一个选择器,轮流处理read事件
需要注意的细节问题
- Channel注册到Selector时,应该让read的线程来注册,而不是让accept的线程来注册
- 线程之间通信可以用一个队列来进行
- Selector的select方法会阻塞当前线程,如果这时候注册进Selector将无法实时反映,可以用wakeup方法来唤醒一次select
- 多线程的线程数设置为当前服务器的cpu核心数即可
Netty是由JBOSS提供的一个java开源框架,现为Github上的独立项目
Netty是一个异步的,基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络IO程序
Netty主要针对TCP协议下,面向Client端的高并发应用,或者Peer-to-Peer场景下的大量数据持续传输应用
Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景
- Netty VS NIO,工作量大,bug多
- 需要自己构建协议
- 需要自己解决TCP传输问题,例如黏包,半包
- epoll空轮询导致cpu占用
- 对API进行增强,使其更易用
- Netty VS 其他网络应用框架
- Mina由apache维护,将来的3.x版本可能会有较大的重构,破坏API向下兼容性,Netty的开发迭代更迅速,API更简洁,文档更优秀
ServerBootStrap
类为Netty的启动器,负责组装Netty的组件,启动服务器等Channel
可以理解为数据的通道msg
为流动的数据,最开始输入的是ByteBuf
,经过pipeline(流水线)
的加工后会变成其他对象(例如字符串),最后输出又变成ByeBuf
handler
为数据的处理工序- 工序有多道,合在一起就是一个
pipeline
,pipeline
负责发布事件传播给每个handler
,handler
对自己可以负责的事件进行处理(重写相应事件的方法) handler
又分为Inbound
和Outbound
两类
- 工序有多道,合在一起就是一个
eventLoop
为处理数据的Worker
Worker
可以管理多个Channel
的IO操作,并且一旦Worker
绑定了某个Channel
就无法解除Worker
既可以执行IO操作,也可以进行任务处理,每个工人都有任务队列,可以堆放多个待处理任务,任务分为普通任务和定时任务(类似线程池的Worker?)Worker
按照pipeline
顺序,依次按照handler
的规划处理数据,可以理解为每道工序指定不同的Worker
EventLoop
本质是一个单线程执行器(同时维护一个Selector
),里面有run方法处理Channel
上的IO事件
它的继承关系比较复杂,有两条线路
- 一条线继承于
juc.ScheduledExecutorService
,因此EventLoop
实际上也是个线程池 - 另一条继承于netty自己的
OrderedEventExecutor
- 提供了
inEventLoop(Thread)
判断一个线程是否属于此EventLoop
- 提供了parent方法也查找自己属于哪个
EventLoopGroup
- 提供了
EventLoopGroup
是一组EventLoop
,Channel
一般会调用EventLoopGroup
的register方法来绑定其中一个EventLoop
,后续这个Channel
上的IO事件都由此EventLoop
来处理
EventLoopGroup
继承于netty的EventExecutorGroup
- 实现了Iterable接口,提供遍历
EventLoop
的能力 - 另有next方法获取集合中下一个
EventLoop
- 实现了Iterable接口,提供遍历
Channel
的主要作用
close()
可以用来关闭Channel
closeFuture()
可以用来处理Channel
的关闭sync()
方法的作用是同步等待Channel
的关闭addListener
方法是异步等待Channel
关闭
pipeline()
方法添加handler
write()
方法写入数据但不刷出,需要配合flush()
writeAndFlush()
方法将数据写入并刷出
ChannelFuture
,主要用于获取和关闭Channel
,有同步和异步两种
- 获取,需要注意
Bootstrap.connect(SocketAddress)
方法是异步的,所以获取时也要注意- 同步获取:调用
sync()
阻塞,等待连接完毕 - 异步获取:调用
addListener(CallBack)
把当前future当做回调对象放入参数的接口中
- 同步获取:调用
- 关闭
- 同步关闭:
sync()
- 异步关闭:
addListener
- 同步关闭:
在异步处理时,经常用到这两个接口
Netty的Future
和Java的Future
同名,但是是两个接口。Netty的Future
继承自jdk的Future
,而Promise
又对Netty的Future
进行了扩展
- jdk的
Future
只能同步等待任务结束才能得到结果 - Netty的
Future
可以同步等待任务结束得到结果,也可以异步得到结果,当然都得等到任务结束 - Netty的
Promise
不仅有Netty的Future
的功能,而且脱离了任务独立存在,只作为线程间传递结果的容器
ChannelHandler
用来处理Channel
上的各种事件,分为入站和出站两种。所有ChannelHandler
被连成一串,就是Pipeline
- 入站处理器通常为
ChannelInboundHandlerAdapter
的子类,主要用来读取客户端数据,写回结果 - 出站处理器通常为
ChannelOutboundHandlerAdapter
的子类,主要对写回的结果加工
Channel
就像一个加工车间,Pipeline
为流水线,ChannelHandler
就是流水线上的工序,而ByteBuf
是原材料,经过了很多入站工序和出站工序的加工最终变成产品
ChannelPipeline
类ChannelHandler
实例对象的链表容器,用于处理或截获通道的接受的发送,为责任链模式的核心组件,会按顺序的组织各个ChannelHandler
,并在他们之间转发事件,其中有 Inbound 和 OutBound 事件流模型
- Inbound:负责为读取的数据进行加工,会从链表的头部开始遍历,然后由super方法进行责任链数据的传递
- Outbound:负责为写出的数据进行加工,会从链表的尾部开始遍历,然后由super方法进行责任链数据的传递
对字节数据的封装,可以动态扩容
ByteBuf
由四个部分组成
- readerIndex: 读指针
- writerIndex: 写指针
- capacity: 容量
- maxCapacity: 最大容量
最开始,读写指针都在0的位置。
写入数据后,写指针会向后移动,如果超过了容量的大小,就会触发扩容(不能超过最大容量)。而写入的部分就可以通过读指针来读取。
读取数据后,读指针会向后移动,已经读过的数据的部分就叫做废弃部分。
这样就不需要手动进行读写切换了,并且容量可以动态伸缩
ByteBuf
的扩容规则是
- 如果写入后数据大小未超过512,则选择下一个16的整数倍。例如写入后大小为12,则扩容后capacity为16
- 如果写入后数据大小超过512,则选择下一个2^n,例如写入后大小为513,则扩容后的capacity为1024
- 扩容容量超过maxCapacity会报异常
由于 Netty 中有堆外内存的ByteBuf
实现,堆外内存最好的手动释放,而不是等待GC
UnpooledHeapByteBuf
使用堆内内存,只需要等待GC即可UnpooledDirectByteBuf
使用堆外内存,需要用特殊方法来回收PooledByteBuf
和它的子类使用了池化机制,需要更复杂的规则来回收
回收内存的源码实现可以参照下面方法的不同实现
protected abstract void deallocate()
Netty 采用引用计数法来控制回收内存,每个ByteBuf
都实现了ReferenceCounted
接口
- 每个
ByteBuf
对象的引用计数都为1 - 调用 release 方法计数减1
- 调用 retain 方法计数加1
- 当计数为0时,低层内存就会被回收
由谁去处理 release
由于 pipeline 的存在,如果让每个ByteBuf
都去处理一遍 release ,那么就会造成我们的 pipeline 链还未传递处理完成就导致内存被回收了,这样就失去了传递性。
所以,Netty 采用的基本规则是,谁是最后使用者,谁就负责处理 release
- 池化,可以重用池中的 ByteBuf 实例,更节约内存以及提高效率
- 读写指针分离,不需要像 ByteBuffer 一样切换读写模式
- 容量动态伸缩
- 支持链式调用,可以使用的更流畅
- 很多地方都体现了零拷贝,例如 slice, duplicate, CompositeByteBuf