1、IO模型
①、IO模型基本说明
- IO模型简单的理解:就是用什么样的通信进行数据的发送和接收,很大程度上决定了程序通信的性能
- java支持3种网络编程IO模式:BIO、NIO、AIO
- BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有链接请求时服务器就需要启动一个线程进行处理,如果这个链接不做任何事情就会造成不必要的线程开销
- NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的链接请求都会注册到多路复用器上,多路复用器轮询到链接IO请求就会进行处理
- AIO【目前还没有得到广泛的使用】:异步非阻塞,AIO引入异步通道的概念,采用了Proactor模式,简化了程序便携,有效的请求才启动线程,他的特点是现有操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用
②、BIO、NIO、AIO使用场景
- BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4之前的唯一选择,单程序简单易于理解。
- NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务期间通讯等。编程比较复杂,JDK1.4开始支持。
- AIO方式适用于连接数目多且谅解比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
2、Java NIO
①、NIO基本介绍
- 同步非阻塞
- 三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)
- NIO是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可以在换种去前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
- NIO是非阻塞模式,是一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变得可以读取之前,该线程可以做其他的事情。非阻塞写也如此,一个线程请求写入一些数据到某一通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
- NIO时可以做到用同一个线程来处理多个操作。假设10000个请求过来,根据实际情况,可以分配50或100个线程来处理。不想BIO一定要分配10000个。
- HTTP2.0使用了多路复用技术,做到同一个连接并发处理多个请求,而且并发请求数量比HTTP1.1大好几个数量级。
②、NIO和BIO的比较
- BIO以流的方式处理数据,而NIO以块的方式处理数据,块IO效率比流IO高很多
- BIO是阻塞的,NIO时非阻塞的
- BIO基于字节流和字符流进行操作,而NIO基于Channel和buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中,Selector用于监听多个通信的事件,因此使用单个线程就可以监听多个客户端通道。
③、NIO三大核心组件(selector、channel、buffer)关系
- 每个channel对应一个buffer
- 一个selector对应一个线程;一个线程对应多个channel
- selector对channel的切换使用事件决定的
- selector会根据不同的事件,在channel中切换
- buffer本质是一个内存块,底层是一个数组
- 数据的读取和写入是通过buffer是双向的交互(flip方法切换)的。(BIO中是流的,是单向的)
- channel也是双向的,可以反应底层操作系统情况,比如linux,linux底层操作系统通道就是双向的。
3、缓冲区Buffer
①、基本介绍
- 缓冲区:本质是一个可以读写数据的内存块,可以理解成为一个容器对象(含数组),该对象提供了一组方法,可以更轻松的使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供从文件、网络读取数据的通道,但是读取和写入的数据必须经由Buffer。
②、Buffer类
-
在NIO中,Buffer是一个顶层父类,是一个抽象类【相关子类:ByteBuffer、ShortBuffer、CharBuffer、IntBuffer、LongBuffer、DoubleBuffer、FloatBuffer】
-
Buffer类定义了所有的缓冲区都具有的是个属性来提供关于其所包含的数据元素信息:
属性 描述 Capacity 容量,即可以容纳的最大数据量;在缓冲区创建时被设定且不被改变 Limit 表示缓冲区的当前重点,不饿能对缓冲区超过极限的位置进行读写操作。且极限位置可以修改 Position 位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变该值,为下次读写做准备 Mark 标记
4、通道Channel
①、基本介绍
- NIO的通道类似于流,但有区别(通道可以读写,而流只能写;通道可以实现异步读写,通道可以从缓冲区读数据,也可以写数据到缓冲区)。
- BIO中stream时单向的。而NIO中的通道channel是双向的。
- Channel是NIO中的一个接口(public interface Channel extends Closeable{})。
②、Channel实现的常用类
-
FileChannel:用于本地文件数据的读写
方法 说明 read 从通道读取数据并放入缓冲区中 write 把缓冲区数据写入到通道中 transferFrom 从目标通道中复制数据到当前通道 transferTo 把数据从当前通道复制给目标通道 -
DatagramChannel:用于UDP的数据的读写。
-
ServerSoceketChannelImpl、SocketChannelImpl:用于TCP的数据读写。
【Buffer和Channel的使用细节】
- ByteBuffer支持类型化的put和get,put放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则可能BufferUnderflowException异常。
- 可以将一个普通Buffer转成只读Buffer。
- NIO还提供了MappedByteBuffer,可以让文件直接在内存(堆外内存)中进行修改,而如何同步到文件由NIO来完成。
- NIO读写操作都是通过Buffer完成,NIO还支持通过多个Buffer(即Buffer数组)完成读写操作,即分散Scattering和聚合Gatering。
5、选择器Selector
①、基本介绍
- NIO可以使用一个线程处理多个客户端连接,就会使用Selector选择器。
- Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然,然后针对每个事件进行相应的处理。这样就可以只用一个线程去管理多个通道,也就是管理多个连接和请求。
- 只有在连接/通道(channel)真正有读写事件发生时,才会进行读写,就大大地减少了系统的开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
- 避免了多线程之间的上下文切换导致的开销。
②、Selector相关类
-
Selector是一个抽象类
public abstract class Selector implements Closeable{}
方法 说明 open 得到一个选择器对象 select 监控所有注册的通道,当其中有IO操作可以进行时,将对应的SelectionKey加入到内部集合中并返回,参数用来设置超时事件 selectedKeys 从内部集合中得到所有的SelectionKey
③、NIO非阻塞网络编程(Selector、SelectionKey、ServerSocketChannel和SocketChannel)原理
- 当客户端连接时,会通过ServerSocketChannel得到SocketChannel。
- 将socketChannel注册到Selector上,register(Selector sel,int ops),一个selector上可以注册多个SocketChannel。
- 注册后返回一个SelectionKey,会和该Selector关联(集合)。
- Selector进行监听select方法,返回有事件发生的通道个数。
- 进一步得到各个SelectionKey(有事件发生)。
- 再通过SelectionKey反向获取SocketChannel,方法channel()。
- 可以通过得到的channel,完成业务处理。
6、NIO与零拷贝
①、零拷贝基本介绍
- 零拷贝时网络编程的关键,很多性能优化都离不开
- 在Java程序中,常用的零拷贝有mmap(内存映射)和sendFile。
②、mmap优化
- mmap通过内存映射,将文件映射到内核缓冲区,同时,用户可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。
③、sendFile优化
- Linux2.1版本提供了sendFile函数,其基本原理:数据不经过用户态,直接从内核缓冲区进入到SocketBuffer,同时,由于和用户态完全无关,就减少一次上下文切换。
- Linux2.4版本中,避免了从内核缓冲区copy到Socketbuffer的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。
④、mmap和sendFile的区别
- mmap适合小数据量的读写,sendFile适合大文件的传输;
- mmap需要四次以上的上下文切换,三次数据拷贝;
- sendFile需要三次上下文切换,最少两次数据拷贝;
- sendFile可以利用DMA方式,减少CPU拷贝,mmap这不能(必须从内核copy到Socket缓存区)。
7、原生NIO存在的问题
- NIO的类库和API繁杂,使用麻烦:需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等
- 需要具备其他的额外技能:需要熟悉java多线程编程,因为NIO编程设计Reactor模式,必须对多线程和网络编程非常熟悉才能编写出高质量的NIO程序
- 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等
- JDK NIO的bug:例如Epoll Bug,他会导致Selector空轮询,最终导致CPU 100%问题。知道JDK7该问题仍然存在,没有被根本解决。