基础
1. Java 中的网络 IO 模型
1.1. BIO
:同步的、阻塞式 IO
在这种模型中,服务器上一个线程处理一次连接,即客户端每发起一个请求,服务端都要开启一个线程专门处理该请求。
构建 C/S 系统的 Java 编程组件是 ServerSocket
和 Socket
。
1.2. NIO
:同步的、非阻塞式 IO
在这种模型中,服务器上一个线程处理多个连接,即多个客户端请求都会被注册到多路复用器(Selector
)上,多路复用器会轮训这些连接,轮训到连接上有 IO 活动就进行处理。这种模式也叫做 Reactor
模式。
构建 C/S 系统的 Java 编程组件是 Channel
、Buffer
、Selector
。
1.3. AIO
:异步非阻塞式 IO
在这种模型中,由操作系统完成与客户端之间的read/write,之后再由操作系统主动通知服务器线程去处理后面的工作,在这个过程中服务器线程不必同步等待 read/write 完成。这种模式也叫做 Proactor
模式。
2. 网路 IO 术语
- 阻塞:如果线程调用 read/write 过程,但 read/write 过程没有就绪或没有完成,则调用 read/write 过程的线程会一直等待,这个过程叫做阻塞式读写。
- 非阻塞:如果线程调用 read/write 过程,但 read/write 过程没有就绪或没有完成,调用 read/write 过程的线程并不会一直等待,而是去处理其他工作,等到 read/write 过程就绪或完成后再回来处理,这个过程叫做非阻塞式读写。
- 异步:read/write 过程托管给操作系统来完成,完成后操作系统会通知(通过回调或者事件)应用网络 IO 程序(其中的线程)来进行后续的处理。
- 同步:read/write 过程由网络 IO 程序(其中的线程)来完成。
NIO 组件
1. 缓冲区(Buffer
)
缓冲区(Buffer
)本质上是一个可读可写的内存块,可以理解成一个容器对象,Channel
读写文件或者网络都要经由 Buffer
。
1.1. 常用子类
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
DoubleBuffer
FloatBuffer
MappedByteBuffer
2. 通道(Channel
)
通道(Channel
)是双向的,可读可写。
2.1. 常用子类
FileChannel
:用于文件读写DatagramChannel
:用于 UDP 数据包收发ServerSocketChannel
:用于服务端 TCP 数据包收发SocketChannel
:用于客户端 TCP 数据包收发
3. 选择器(Selector
)
选择器(Selector
)是实现 IO 多路复用的关键,多个 Channel
注册到某个 Selector
上,当 Channel
上有事件发生时,Selector
就会取得事件然后调用线程去处理事件。
3.1. 常用子类
Selector
是一个抽象类,常用方法:
1 | public abstract class Selector implements Closeable { |
服务端的工作流程
- 当客户端发起连接时,会通过
ServerSocketChannel
创建对应的SocketChannel
。 - 调用
SocketChannel
的注册方法将SocketChannel
注册到Selector
上,注册方法返回一个SelectionKey
,该SelectionKey
会被放入Selector
内部的SelectionKey
集合中。该SelectionKey
和Selector
关联(即通过SelectionKey
可以找到对应的Selector
),也和SocketChannel
关联(即通过SelectionKey
可以找到对应的SocketChannel
)。 Selector
会调用select()/select(timeout)/selectNow()
方法对内部的SelectionKey
集合关联的SocketChannel
集合进行监听,找到有事件发生的SocketChannel
对应的SelectionKey
。- 通过
SelectionKey
找到有事件发生的SocketChannel
,完成数据处理。
相关源码为:
1 | /** |
Linux 下零拷贝技术
1. 直接 IO 技术
内核缓冲区是 Linux 系统的 Page Cahe
。整个过程有四次数据拷贝,读进来两次,写回去又两次:磁盘–>内核缓冲区–>Socket 缓冲区–>网络。
直接 IO 过程使用的 Linux 系统 API 为:
1 | ssize_t read(int filedes, void *buf, size_t nbytes); |
2. 内存映射文件技术
整个过程有三次数据拷贝,不再经过应用程序内存,直接在内核空间中从内核缓冲区拷贝到 Socket 缓冲区。
内存映射文件过程使用的 Linux 系统 API 为:
1 | void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); |
3. 零拷贝技术
内核缓冲区到 Socket 缓冲区之间并没有做数据的拷贝,只是一个地址的映射。底层的网卡驱动程序要读取数据并发送到网络上的时候,看似读取的是 Socket 的缓冲区中的数据,其实直接读的是内核缓冲区中的数据。内存中数据拷贝的次数为 0。
零拷贝过程使用的 Linux 系统 API 为:
1 | ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); |
JDK中提供的是:
1 | FileChannel.transderTo(long position, long count, WritableByteChannel target); |
具体可参考:
1 | public static void main(String[] args) throws IOException { |