在Unix Socket的输入操作中,可以将其分为以下几个阶段:
-
等待数据就绪(内核空间):
在这个阶段,应用程序通过调用阻塞式的读取函数(如recv)或非阻塞式的读取函数(如recv、recvfrom)等待数据的到达。如果没有数据到达,阻塞式的读取函数会一直等待,而非阻塞式的读取函数会立即返回一个错误码或标识表示数据未准备好。 -
数据拷贝到内核缓冲区(内核空间):
当数据就绪后,操作系统会将数据从网络中拷贝到内核缓冲区中。这个阶段是在内核空间中进行的,应用程序无法直接访问或操作内核缓冲区。 -
数据拷贝到用户缓冲区(用户空间):
在这个阶段,从内核缓冲区将数据拷贝到应用程序提供的用户缓冲区中。这个步骤涉及了将数据从内核空间切换到用户空间的操作。 -
数据处理和应用程序操作(用户空间):
一旦数据被拷贝到用户缓冲区,应用程序就可以对数据进行处理和操作。这可能包括解析数据、执行特定的业务逻辑或对数据进行进一步的处理
在整个输入操作过程中,数据从网络到达应用程序的用户空间,经过了多次拷贝和处理。故而产生零拷贝的方案,以减少用户空间与CPU内核空间的拷贝过程,减少用户上下文与CPU内核上下文间的切换,提高系统效率
不同的IO模型可能会对输入操作的阶段和执行方式有所影响。例如,阻塞IO模型和非阻塞IO模型在等待数据就绪阶段的行为上有差异,而异步IO模型可以在数据到达后异步地通知应用程序,使应用程序可以进行其他操作而不必等待数据的拷贝和处理过程
I/O 模型
常用的为阻塞I/O 及 多路复用I/O
阻塞 I/O
在阻塞IO模型中,应用程序发起一个IO操作后,会一直阻塞等待,直到IO操作完成
- 当应用程序执行IO操作时,如果数据没有准备好或无法立即写入目标,应用程序会一直等待,直到数据就绪。
阻塞IO模型是最简单的IO模型,易于理解和使用,适用于IO操作相对较少或IO时间短暂的场景
非阻塞 I/O
在非阻塞IO模型中,应用程序发起一个IO操作后,会立即返回,而不会等待IO操作的完成
- 当应用程序执行非阻塞IO操作时,如果数据没有准备好或无法立即写入目标,应用程序会立即返回一个错误码或标识
- 应用程序可以通过轮询或使用select、poll、epoll等函数来检查IO操作的状态,以确定数据是否已经就绪
非阻塞IO模型需要应用程序自行处理IO操作的就绪状态,适用于需要同时处理多个IO操作的场景
多路复用 I/O
多路复用IO模型通过使用select、poll、epoll等函数,将多个IO操作集中在一起进行管理。应用程序将多个IO操作注册到多路复用机制(Selector)中,并在调用多路复用函数时等待就绪的IO操作
多路复用IO模型可以同时管理多个IO操作,减少了轮询的开销,提高了效率。多路复用IO模型适用于需要同时处理大量IO操作的场景
信号驱动 I/O
信号驱动IO模型中,应用程序通过注册信号处理函数,并将文件描述符设置为非阻塞模式。
- 当IO操作就绪时,操作系统会发送一个信号给应用程序,应用程序在信号处理函数中进行IO操作的处理
信号驱动IO模型可以在IO操作就绪时异步地通知应用程序,适用于需要异步IO操作的场景
异步 I/O
异步IO模型中,应用程序发起IO操作后,可以继续执行其他任务,而不需要等待IO操作的完成
- 当IO操作完成后,操作系统会通知应用程序,应用程序可以异步地获取或处理已完成的IO结果
异步IO模型不需要应用程序自行管理IO操作的状态,适用于需要高性能异步IO操作的场景
I/O 模型对比
同步 I/O 与异步 I/O
- 阻塞等待 vs 非阻塞操作:同步I/O需要应用程序阻塞等待操作完成,而异步I/O可以让应用程序在操作进行时继续执行其他任务。
- 应用程序控制 vs 操作系统控制:同步I/O模型需要应用程序主动发起和等待I/O操作的完成,而异步I/O模型中,应用程序发起I/O操作后,操作系统负责管理和执行操作,并在完成时通知应用程序。
- 阻塞模式 vs 异步通知:同步I/O模型中,应用程序需要等待数据就绪或写入目标后才能继续执行,而异步I/O模型中,应用程序可以在操作发起后立即返回,并在操作完成后得到通知
Java I/O 模型
BIO | NIO | AIO | |
---|---|---|---|
阻塞 vs 非阻塞 | 阻塞 | 非阻塞 | 非阻塞 |
同步 vs 异步 | 同步 | 同步 | 异步 |
线程资源消耗 | 需要为每个连接分配独立的线程 | 通过单线程处理多个连接 | 利用操作系统的异步通知机制,不需要额外的线程资源 |
数据处理能力 | 阻塞等待 | 使用缓冲区(Buffer)进行数据处理,提供了更高效的数据读写能力 | 通过异步I/O操作提供高性能的异步操作机制 |
BIO
同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善
public class BioExample {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("Server started, waiting for connections...");
while (true) {
Socket socket = serverSocket.accept();
System.out.println("Client connected: " + socket.getInetAddress());
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
socket.close();
System.out.println("Client disconnected.");
}
}
}
NIO
异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。当进行读写操作时,只须直接调用API的read或write方法
这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数
public class NioExample {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8888));
serverSocketChannel.configureBlocking(false);
System.out.println("Server started, waiting for connections...");
ExecutorService executorService = Executors.newFixedThreadPool(10);
while (true) {
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel != null) {
System.out.println("Client connected: " + socketChannel.getRemoteAddress());
executorService.submit(() -> {
try {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (socketChannel.read(buffer) != -1) {
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
}
socketChannel.close();
System.out.println("Client disconnected: " + socketChannel.getRemoteAddress());
} catch (IOException e) {
e.printStackTrace();
}
});
}
}
}
}
AIO
异步IO模型中,应用程序发起IO操作后,可以继续执行其他任务,而不需要等待IO操作的完成。当IO操作完成后,操作系统会通知应用程序,应用程序可以异步地获取或处理已完成的IO结果
public class AioExample {
public static void main(String[] args) throws IOException {
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8888));
System.out.println("Server started, waiting for connections...");
serverSocketChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
serverSocketChannel.accept(null, this);
try {
System.out.println("Client connected: " + socketChannel.getRemoteAddress());
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
if (result == -1) {
try {
socketChannel.close();
System.out.println("Client disconnected: " + socketChannel.getRemoteAddress());
} catch (IOException e) {
e.printStackTrace();
}
return;
}
buffer.flip();
socketChannel.write(buffer, buffer, this);
buffer.clear();
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
exc.printStackTrace();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
});
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
使用了CompletionHandler来处理异步操作的完成和错误处理
参考资料:
- 《Unix网络编程第1卷》
- Redis之IO线程、IO多路复用,BIO、NIO和AIO区别