微信公众号: 房东的小黑黑
路途随遥远,将来更美好
学海无涯,大家一起加油!
这篇文章主要是阅读了一些关于NIO的文章,对一些重要的部分进行了摘取总结。
BIO:同步阻塞IO模式,线程发起IO请求后,一直阻塞IO,直到缓冲区数据就绪后,再进行下一步操作。
NIO:是同步非阻塞IO,线程发起IO请求后,立即返回。同步体现在selector仍然要去轮询判断channel是否准备好,非阻塞体现在这个过程中处理用户线程不会一直等待,可以去做其他的事情,但是要定时轮询IO缓存区数据是否准备好。
NIO主要有buffer、channel、selector三个组件,通过零拷贝的buffer获取数
AIO是异步非阻塞IO模型。在上述NIO实现中,需要用户线程定时轮询,去检查IO缓冲区数据是否准备就绪,占用应用程序线程资源,其实轮询也是阻塞的,它需要查询哪些IO就绪了。而真正的理想的异步非阻塞IO应该让内核系统完成,用户线程只需告诉内核,当缓冲区就绪后,通知我或者执行回调函数。
在BIO模式中, socket.accept()
、 socket.read()
、 socket.write()
三个主要函数都是同步阻塞的。当一个连接处理IO的时候,系统是阻塞的,要想处理多个连接,就要使用多线程。但是,当面对数万级别的连接时,传统的BIO模型就不行了,太消耗资源了。
NIO以块的方式处理数据,但是IO是以最基础的字节流的形式进行写入和读出。
NIO的通道是双向的,但是IO中的流是单向的。
NIO采用的是多路复用的IO模型,普通的IO用的就是阻塞的IO模型。
1) 首先先创建 ServerSocketChannel
对象和真正处理数据的线程池。
2)然后给刚刚创建的 ServerSocketChannel
对象进行绑定一个对用的端口,然后设置为非阻塞。
3)然后创建 Selector
对象并打开,然后把这个 Selector
对象注册到 ServerSocketChannel
中,并设置好监听的事件,监听 SelectionKey.OP_ACCEPT
。
4) Selector
对象死循环监听每一个Channel通道的事件,循环执行 Selector.select()
方法,轮询就绪的方法。
5)从 Selector
中获取所有的SelectorKey,如果SelectorKey是处于OP_ACCEPT状态,说明有新的客户端接入,调用 ServerSocketChannel.accept
接收新的客户端。
6)然后把这个接收的新客户端的Channel通道注册到 ServerSocketChannel
上,并且把之前的OP_ACCEPT状态改为 SelectionKey.OP_READ
读取事件状态,并且设置为非阻塞,然后把当前的这个 SelectorKey
给移除掉,说明这个事件完成了。
7)如果第五步的事件不是OP_ACCEPT,那就是OP_READ读取数据的事件状态。然后调用对应的机制。
NIO的类库和API繁琐,使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
需要具备其他的额外技能做铺垫,例如要熟悉Java多线程编程。
可靠性能力补齐,工作量和难度都非常大。比如说拆包闭包问题。
JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。
API简单,开发门槛低。
定制能力强,可以通过ChannelHandler对通信框架进行灵活的扩展。
成熟、稳定,Netty修复了已经发现的所有JDK NIO。
社区活跃,并且经历过大规模的商业应用考验。
高并发。 Netty是一款基于NIO开发的网络通信框架,对比于BIO,它的并发性得到了很大的提高。
传输快。Netty的传输快是依赖于NIO的一个特性 零拷贝
封装好。Netty封装了NIO操作的很多细节,提供易于使用的API,开发门槛低。原生的NIO的API繁琐,使用麻烦。
可靠性。解决了一些原生NIO存在的问题,比如说空轮序,封包闭包问题。
对Selector的select操作周期进行统计,每完成一次空的Select操作就进行一次计数。
若在某个周期内连续发生N次空轮询,则触发epoll死循环bug。
重建Selecto,判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭。
select操作的返回值不是已准备好的通道的总数,而是从上一个select()调用之后进入就绪态的通道的数量。之前的调用就绪的,并且在本次调用中仍就绪的通道不会被计入,而那些在前一次调用中已经就绪但是已经不再处于就绪状态的通道也不会被计入。
在传统的I/O操作中,每次都需要把内核空间的数据拷贝到用户空间中,这样挺浪费空间的,所以零拷贝的出现就是为了解决这个问题。
主要有两种方法: mmap+write
和 Sendfile
使用mmap+write方式替换原来的read+write方式,mmap是一种内存映射文件的方法,即将一个文件或者其他对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对应关系。
这样就可以省略掉原来内核Read缓冲区Copy数据到用户缓冲区,但是还是需要内核Read缓冲区将数据Copy到内核Socket缓冲区。
Sendfile是为了简化通过网络在两个通道内进行的数据传输过程。
它不仅减少了数据复制,还减少了上下文次数的切换。数据传送只发生在内核空间里,所以减少了一次上下文切换,但是还是存在一次Copy。
后来进行了改进,将Kernel Buffer中对应的数据描述信息(内存地址,偏移量)记录到相应的Socket缓冲区中,这样连内核空间中的一次CPU Copy也省掉了。
Netty零拷贝主要体现在三个方面。
Netty的接收和发送ByteBuffer采用DirtectByteBuffer,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。
Netty在操作多个Channel时,使用CompositeChannelBuffer,它并不会开辟新的内存并复制所有ChannelBuffer内容,而是直接保存了所有ChannelBuffer的引用,并在子ChannelBuffer里进行读写,实现了零拷贝。
Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel。
1) 创建ServerBootStrap实例。
2)设置并绑定Reactor线程池:EventLoopGroup boss 和 worker,EventLoop就是处理所有注册到本线程Selector上面的Channel。
3)设置并绑定服务端的NioServerSocketChannel。
4)创建处理网络事件的ChannelPipeline和handler。
5)绑定并启动监听端口。
6)当轮询到准备就绪的channel后,由Reactor线程:NioEventLoop执行pipline中的方法,最终调度并执行channelHander。
1) 创建Bootstrap实例。
2)创建处理客户端连接Reactor线程组NioEventLoopGroup。
3)创建客户端连接的NioSocketChannel。
4)创建pipeline的channelHandler。
5)异步发起TCP连接并判断是否成功。