AIO,BIO 与 NIO 的基础概念
IO 读写的底层流程
用户程序进行IO的读写,基本上会用到系统调用read&write
- read 把数据从内核缓冲区复制到进程缓冲区
- write把 数据从进程缓冲区复制到内核缓冲区,它们不等价于数据在内核缓冲区和磁盘之间的交换。
- (1)客户端请求:Linux通过网卡,读取客户断的请求数据,将数据读取到内核缓冲区。
- (2)获取请求数据:服务器从内核缓冲区读取数据到Java进程缓冲区。
- (3)服务器端业务处理:Java服务端在自己的用户空间中,处理客户端的请求。
- (4)服务器端返回数据:Java服务端已构建好的响应,从用户缓冲区写入系统缓冲区。
- (5)发送给客户端:Linux内核通过网络 I/O ,将内核缓冲区中的数据,写入网卡,网卡通过底层的通讯协议,会将数据发送给目标客户端。
AIO,BIO 与 NIO 的定义
- 同步阻塞IO, BIO(Block-IO):数据的读取写入必须阻塞在一个线程内等待其完成。
这里假设一个烧开水的场景,有一排水壶在烧开水,BIO的工作模式就是, 叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。实际上线程在等待水壶烧开的时间段什么都没有做。 保持一个连接,不能做其他事。 - 同步非阻塞IO,non-blocking-IO,
同时支持阻塞与非阻塞模式。同步非阻塞 IO 的定义为,如果还拿烧开水来说,NIO的做法是叫 一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作。 可以保持多个连接,不能做其他事。 - 异步非阻塞I/O模型: AIO
异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。 可以保持多个连接,可以做其他事。
以socket.read()
为例子:
- 传统的BIO里面
socket.read()
,如果 TCP RecvBuffe r里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。 - 对于NIO,如果
TCP RecvBuffe
r有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。 - 最新的 AIO(Async I/O) 里面会更进一步:不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的。
IO 与 NIO 的区别
- IO 面向流的意思是,必须用一个持久化来接受。流是一种数据存储的方式,数据流必须有文件来接;而NIO 使用缓冲区,可以将数据存储的缓冲区中,然后选择行读或者一个一个字段,效率跟高;
- NIO 由于会出现轮询所有水壶的操作,因此需要一个多路选择器。
同步与异步的区别
- 同步: 发送一个请求,等待返回,再发送下一个请求,同步可以避免出现死锁,脏读的发生。
异步: 发送一个请求,不等待返回,随时可以再发送下一个请求,可以提高效率,保证并发。
阻塞和非阻塞
- 阻塞:
- 传统的IO流都是阻塞式的。也就是说,当一个线程调用
read()
或者write()
方法时,该线程将被阻塞,直到有一些数据读取或者被写入,在此期间,该线程不能执行其他任何任务。 - 在完成网络通信进行IO操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量的客户端时,性能急剧下降。
- 传统的IO流都是阻塞式的。也就是说,当一个线程调用
- 非阻塞:
- JavaNIO 是非阻塞式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程会去执行其他任务。
- 线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。
- 因此 NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
可以将是否同步与是否阻塞理解为横向维度与纵向维度
缓冲区 Buffer
- Buffer 是一个对象。它包含一些要写入或者读出的数据。在面向流的 I/O 中,可以将数据写入或者将数据直接读到Stream 对象中。
- 在NIO中,所有的数据都是用缓冲区处理。这也就本文上面谈到的 IO 是面向流的,NIO 是面向缓冲区的。
缓冲区实质是一个数组,通常它是一个字节数组( ByteBuffer),也可以使用其他类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的 结构化访问以及维护读写位置(limit) 等信息。
最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能于操作byte数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean)都对应一种缓冲区,具体如下:
- ByteBuffer:字节缓冲区
- CharBuffer: 字符缓冲区
- ShortBuffer:短整型缓冲区
- IntBuffer:整型缓冲区
- LongBuffer: 长整型缓冲区
- FloatBuffer:浮点型缓冲区
- DoubleBuffer:双精度浮点型缓冲区
通道Channel
- Channel 是一个通道,可以通过它读取和写入数据,他就像自来水管一样,网络数据通过 Channel 读取和写入。
- 通道和流不同之处在于通道是双向的,流只是在一个方向移动,而且通道可以用于读,写或者同时用于读写。
因为Channel是全双工的,所以它比流更好地映射底层操作系统的API,特别是在UNIX网络编程中,底层操作系统的通道都是全双工的,同时支持读和写。
Channel 有四种实现:
- FileChannel: 是从文件中读取数据。
- DatagramChannel: 从 UDP 网络中读取或者写入数据。
- SocketChannel :从 TCP 网络中读取或者写入数据。
- ServerSocketChannel:允许你监听来自TCP的连接,就像服务器一样。每一个连接都会有一个SocketChannel产生。
多路复用器 Selector
Selector 选择器可以监听多个Channel(比如 read、write、accep、connect),实现一个线程管理多个Channel,节省线程切换上下文的资源消耗。
- Selector 只能管理非阻塞的通道,FileChannel 是阻塞的,无法管理。
关键对象
- Selector:选择器对象,通道注册、通道监听对象和 Selector 相关。
- SelectorKey:通道监听关键字,通过它来监听通道状态。
监听注册
- 监听注册在 Selector:
socketChannel.register(selector, SelectionKey.OP_READ);
监听的事件有
- OP_ACCEPT: 接收就绪,serviceSocketChannel 使用的
- OP_READ: 读取就绪,socketChannel使用
- OP_WRITE: 写入就绪,socketChannel使用
- OP_CONNECT: 连接就绪,socketChannel使用
NIO
NIO 框架
定义: Java NIO 成功的应用在了各种分布式、即时通信和中间件 Java 系统中,充分的证明了基于 NIO 构建的通信基础,是一种高效,且扩展性很强的通信架构。例如:Dubbo(服务框架),就默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信。
java NIO的工作原理:
- 由一个专门的线程来处理所有的 IO 事件,并负责分发。
- 事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
- 线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。
(注:每个线程的处理流程大概都是读取数据、解码、计算处理、编码、发送响应。)
代码演示
BIO
-
传统IO,即BIO,同步阻塞处理的模型如下:
{ ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//线程池 ServerSocket serverSocket = new ServerSocket(); serverSocket.bind(8088); while(!Thread.currentThread.isInturrupted()){ //主线程死循环等待新连接到来 Socket