# JAVA IO 模型
# BIO
Blocking IO,同步并阻塞方式,应用程序向 OS 请求网络 IO 操作,然后会等待 IO 操作完成。客户端与服务器端连接,一个连接创建一个线程。
适用于连接数目比较小且固定的架构,对服务器要求比较高
简单流程:
- 服务端启动一个 ServerSocket。
- 客户端启动 Socket 对服务端通讯,默认情况下服务端对每个客户建立一个线程。
- 客户端发送请求后,先检测服务端是否有线程响应,没有则等待或被拒绝。
- 有响应,客户端线程等待请求结束返回响应,再继续执行。
# NIO
Non-Blocking IO,同步非阻塞,一个线程处理多个请求,客户端发送的连接请求会注册到多路复用器上,多路复用器轮询到 IO 请求会处理
适用于连接数目多且连接比较短的架构,比如聊天服务器,弹幕系统,服务器间通讯。
# Selector、Channel 和 Buffer 的关系
- 每个 Channel 都会对应一个 Buffer
- Selector 对应一个线程,一个线程对应多个 Channel
- 程序切换到哪个 channel 是由事件决定的。
- Selector 会根据不同的事件在各个通道切换
- Buffer 就是一个内存块,底层有个数组
- 数据的读取写入是通过 Buffer,可以双向读写,需要 filp 方法切换
- channel 是双向的,可以返回底层操作系统的情况,比如 Linux,底层操作系统的通道就是双向的
# Buffer
Buffer 是一个抽象类,子类有:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
在子类中有一个数组 hb 用于缓冲的实现。
1 | // Invariants: mark <= position <= limit <= capacity |
# Channel
- 通道可以同时进行读写
- 通道可以实现异步读写数据
- 通道可以从缓冲读,可以写到缓冲
Channel 是一个接口
1 | public interface Channel extends Closeable { |
常用的 Channel 类有 FileChannel(文件读写)、DatagramChannel(UDP 数据读写)、ServerSocketChannel 和 SocketChannel(这俩 TCP 数据读写)
在各种 channel 所依托的流如果关闭其 read()
方法会返回 - 1,其余返回 0
# Selector
用一个线程处理多个客户端连接,就会用到选择器,Selector 可以检测多个注册的通道上是否有事件发生(多个 Channel 以事件的方式可以注册到同一个 Selector)
只有在 连接 / 通道 真正有读写事件发生时,才会进行读写,大大减少了系统开销,避免了多线程上下文切换导致的开销。
Selector 是一个抽象类,常用方法有
open( )
:得到一个选择器对象selectedKeys( )
:获得内部集合所有等待 IO 操作的 selectionKeyselect(long timeout)
:监控所有注册通道,当有 IO 操作发生时将对应的 SelectionKey 加入到内部集合并且返回,参数超时时间,超时返回有事件发生的 SelectionKeyselect()
:阻塞直到注册的通道有事件到达,返回有事件发生的 SelectionKeyselcetNow()
:不阻塞,立马返回wakeuo()
:唤醒 selectorkeys()
:返回当前所有注册在 selector 中 channel 的 selectionKey
一个 SelectionKey 表示了一个特定的 channel 通道对象和一个特定的 selector 选择器对象之间的注册关系。
🛑SelectionKey 在被轮询后需要 remove (),selector 不会自己删除 selectedKeys () 集合中的 selectionKey,如果不人工 remove (),将导致下次 select () 的时候 selectedKeys () 中仍有上次轮询留下来的信息,这样必然会出现错误。
🛑注册过的 channel 信息会以 SelectionKey 的形式存储在 selector.keys () 中。keys () 中的成员是不需要被删除的 (以此来记录 channel 信息)。
# SelectionKey
SelectionKey 表示 Selector 与网络通道是注册关系
# NIO 非阻塞网络编程
实现:
客户端连接时,会通过 ServerSocketChannel 得到 SocketChannel。
将 SocketChannel 注册到 Selector 上,
register(Selector sel, int ops)
,一个 Selector 可以注册多个 SocketChannel。注册后返回一个 SelectionKey。
1
2
3
4
5
6
7
8// 读事件
public static final int OP_READ = 1 << 0;
// 写事件
public static final int OP_WRITE = 1 << 2;
// 连接事件
public static final int OP_CONNECT = 1 << 3;
// 接受连接事件
public static final int OP_ACCEPT = 1 << 4;Selector 进行监听,
select( )
返回有事件发生的通道个数。进一步得到各个 SelectionKey。
在通过 SelectionKey 反向获取 SocketChannel。
通过得到的 Channel 完成处理。
服务端实现
1 |
|
客户端实现
1 |
|
# NIO 与零拷贝
# 零拷贝
这要从 Linux 说起:Linux 系统中一切皆文件,很多活动本质上都是读写操作。
一般的数据拷贝过程:
- 当应用程序读取磁盘数据时,调用 read ( ) 从用户态到内核态,该过程由 cpu 完成
- 之后 CPU 发送 I/O 请求,磁盘收到请求后开始准备数据
- 磁盘将数据传送到磁盘的缓冲区中,然后发送 I/O 中断
- CPU 收到中断后开始拷贝数据,然后由 read () 返回,再从内核态转换成用户态
直接内存访问(Direct Memory Access)方式是一种硬件直接访问内存的一种方式:
读数据;
- 调用 read () 函数,用户态切换内核态,状态切换一次;
- DMA 控制器将数据从磁盘拷贝到内核缓冲区,1 次 DMA 拷贝;
- CPU 将数据从内核缓冲区复制到用户缓冲区,1 次 CPU 拷贝;
- read () 函数返回,用户态切换回用户态,2 次状态切换;
写数据;
- 调用 write () 函数,用户态切换内核态,1 次切换;
- CPU 将用户缓冲区数据拷贝到内核缓冲区,1 次 CPU 拷贝;
- DMA 将数据从内核缓冲区复制到套接字的缓冲区,1 次 DMA 拷贝;
- write () 函数返回,内核态切换回用户态,2 次切换;
零拷贝是网络编程的关键,很多性能优化都离不开零拷贝
在 java 程序中,常用的零拷贝有 mmap(内存映射)和 sendFile
# mmap 内存映射
内存映射文件是 一种读写数据的方法,比常规的流或者通道读写要快,但是会有一些安全问题。
内存映射文件是一个文件到一块内存的映射。使用内存映射文件处理存储于磁盘的文件时,不比对文件执行 IO 操作。在 Linux 中,mmap 实现了内核中读缓冲区域用户空间缓冲区的映射,从而实现二者的缓冲区共享。这样就减少了一次 cpu 拷贝。
- 用户进程通过
mmap()
向操作系统内核发起 IO 调用,用户态切换为内核态。 - CPU 利用 DMA 控制器,把数据从硬盘中拷贝到内核缓冲区。
- 内核态切换回用户态,
write()
返回。 - 用户进程通过
write()
向操作系统内核发起 IO 调用,用户态切换为内核态。 - CPU 将内核缓冲区的数据拷贝到的 socket 缓冲区。
- CPU 利用 DMA 控制器,把数据从 socket 缓冲区拷贝到网卡,内核态切换回用户态,write 调用返回。
一次读 + 写 4 次上下文切换,3 次数据拷贝
MappedByteBuffer 类继承自 ByteBuffer,子类 DirectByteBuffer 内部维护了一个缓存数组偏移量 arrayBaseOffset
FileChannel 提供了 map()
方法把文件映射到虚拟内存,可以整个文件映射,也可以分段映射
1 |
|
# sendFile 系统调用
建立两个文件之间的传输通道
- 用户进程发起
sendfile系统调用
,用户态到内核态 - DMA 控制器,把数据从硬盘中拷贝到内核缓冲区。
- CPU 将读缓冲区中数据拷贝到 socket 缓冲区
- DMA 控制器,异步把数据从 socket 缓冲区拷贝到网卡,
- 内核态到回用户态,
sendfile调用
返回。
2 次上下文切换,最少 3 次数据拷贝
# AIO
Asynchronous I/O,异步非阻塞,无论是客户端的连接请求还是读写请求都会异步执行, 由操作系统完成后回调通知服务端程序启动线程去处理
适用于连接数目多且连接比较长的架构,相册服务器
BIO | NIO | AIO | |
---|---|---|---|
IO 模型 | 同步阻塞 | 同步非阻塞(多路复用) | 异步非阻塞 |
编程难度 | 简单 | 复杂 | 复杂 |
可靠性 | 差 | 好 | 好 |
吞吐量 | 低 | 高 | 高 |