1. IO分类概述
1.1 阻塞与非阻塞
阻塞(Blocking)和非阻塞(Nonblocking)是在计算机编程中用于描述I/O操作的两个重要概念。阻塞与非阻塞描述的是线程在访问某个资源时,在该资源没有准备就绪期间的处理方式。
1、阻塞:阻塞是指在进行I/O操作时,当前线程会被挂起,等待数据的就绪或操作的完成。
- 在阻塞状态下,线程会一直等待,直到条件满足或超时才会继续执行
- 阻塞式I/O操作会导致调用线程无法执行其他任务,直到I/O操作完
- 例如,在读取文件时,如果没有数据可用,线程会一直等待直到有数据可读
2、非阻塞:非阻塞是指进行I/O操作时,当前线程不会被挂起,而是立即返回并继续执行其他任务。
- 在非阻塞状态下,即使I/O操作无法立即完成,线程也可以继续执行其他操作,而不需要一直等待
- 非阻塞式I/O操作通常会返回一个状态或错误码,指示操作是否完成或需要进一步处理
- 应用程序可以通过不断轮询状态来判断是否可以进行下一步操作
1.2 同步与异步
同步(synchronous)与异步(asynchronous)描述的是线程在发出请求后,是否等待结果。
1、同步(Synchronous):同步是指程序按照顺序执行,并等待某个操作完成后再继续执行下一个操作。
- 在同步操作中,程序主动发起请求并等待结果返回,直到结果返回后才能继续执行后续操作
- 同步操作通常是阻塞的,即在等待结果时当前线程会被挂起,无法执行其他任务
- 典型的同步操作包括函数调用、阻塞式I/O操作等
2、异步(Asynchronous):异步是指程序在发起某个操作后,不需要立即等待操作完成,而是继续执行后续的操作。
- 在异步操作中,程序不会阻塞等待结果的返回,而是通过回调、轮询或事件通知等机制来获取结果或处理完成的事件
- 异步操作允许程序并发执行多个任务,提高了系统的并发性和响应性。典型的异步操作包括异步函数调用、非阻塞式I/O操作、异步任务等
1.3 IO的分类
当涉及I/O操作时,可以根据是否阻塞和是否异步的角度将其分为以下四类:
1、阻塞式I/O(Blocking I/O)
- 特点:在进行I/O操作时,当前线程会被阻塞,直到数据就绪或操作完成
- 工作原理:当进行I/O操作时,线程会等待直到数据准备好或操作完成,在等待期间,线程无法执行其他任务
2、非阻塞式I/O(Non-Blocking I/O)
- 特点:进行I/O操作时,当前线程不会被阻塞,立即返回并继续执行其他任务
- 工作原理:当进行I/O操作时,如果数据尚未准备好或操作无法立即完成,操作会立即返回一个状态指示暂时不可用
3、I/O多路复用(I/O Multiplexing)
- 特点:允许一个线程同时监视多个I/O通道的就绪状态,提高了系统的并发性能
- 工作原理:通过操作系统提供的I/O复用机制,将多个I/O通道注册到一个选择器上,然后使用选择器进行轮询以确定哪些通道就绪
4、异步I/O(Asynchronous I/O)
- 特点:在进行I/O操作时,不需要立即等待操作完成,可以继续执行其他任务,通过回调或事件机制来处理操作结果
- 工作原理:应用程序提交I/O请求后立即返回,并使用回调、轮询或事件通知等方式来处理操作完成的通知
这四种I/O模型各有优劣,并适用于不同的应用场景:
- 阻塞式I/O适用于简单的同步操作
- 非阻塞式I/O适用于需要处理多个连接的场景
- I/O多路复用适用于需要监视多个连接的场景
- 异步I/O适用于需要高性能和扩展性的场景
根据具体的应用需求和系统特点,可以选择合适的I/O模型来实现高效的数据传输和处理。
2. Java NIO
2.1 Java NIO概述
在Java编程中,经常听到BIO,NIO,AIO等名词,这些名词一般指的是Java语言为实现不同类型的I/O操作所提供的API。
1、Java IO:Java IO是JDK 1.0版本其自带的用于读取和写入数据的API。因其同步、阻塞的特征,被归类为同步阻塞式IO(Blocking IO),即Java BIO。
2、Java NIO(New IO):是JDK 1.4开始提供的同步非阻塞式的IO操作API。因其同步、非阻塞的特征,开发者一般将Java NIO理解为Java Non-Blocking IO。
3、Java NIO 2.0:是JDK 1.7开始提供的异步非阻塞式的IO操作API,因其是在NIO的基础上进行了改进,称为NIO 2.0。因其异步、非阻塞的特征,开发者一般将Java NIO 2.0称为Java AIO。
下面以一个Web服务器的例子介绍Java BIO和Java NIO在实际使用中的差别。
基于BIO的场景:
基于NIO的场景:
2.2 Java BIO模型
Java BIO和NIO采用了2种不同的模型。BIO使用了面向流(Stream Oriented)的模型。流(Stream)可以理解为从源节点到目标节点的数据通道,传输的数据像水流一样从源节点流向目标节点。
面向流的特点:
- 单向的:一个流中的数据仅能从一个方向流向另一个方向
- 面向字节的:程序每次可以从流中读取1到多个字节,或写入1到多个字节
- 无缓冲的:从流中读取数据后,流中的数据消失
- 顺序访问:仅能按顺序逐个访问流中的数据,不能在流中的数据中前后移动
2.3 Java NIO模型
Java NIO使用了面向缓冲区(Buffer Oriented)的,基于通道(Channel Based)的模型。模型的不同使得BIO和NIO在功能上和操作上有着不同的特点。
在面向缓冲的模型中,数据通过数据通道(Channel)被读入/写入到一个缓冲区(Buffer)中,然后从中进行处理。
可以使用一个生活中的例子来理解:现在需要从房间A搬运一些纸质文件到房间B,房间A和房间B之间通过一个走廊相连。搬运文件时,先将文件放到一个文件箱中,再搬着文件箱从A房间移动到B房间。
在这个例子中,房间A和房间B分别是数据传输的起点和终点。起点和终点之间的走廊是Channel,临时存放纸质文件的文件箱是Buffer。
面向缓冲的特点:
- 双向的:通道可以用于读或写,也可以同时用于读写
- 面向字节块:程序每次可以从缓冲区中获取一组字节数据
- 缓冲的:缓冲中的数据可以被多次访问
- 任意访问:允许在缓冲中的数据中前后移动
2.4 两种模型的对比
Java BIO下的执行流程:
Java NIO下的执行流程:
3. Java NIO API
3.1 Buffer
Buffer(缓冲区)是Java NIO中提供的用于存储数据的容器。Buffer底层依靠数组来存储数据,并提供了对数据的结构化访问以及维护读写位置等信息的功能。
Buffer只能用于存储基本类型的数据。在NIO中,针对八种基本类型提供了7个实现类。考虑到实际过程中,数据是以字节的形式来存储和传输,所以更多的使用的是ByteBuffer。
Buffer是一个抽象类:
其中定义了4个关于底层数组信息的核心属性:
- capacity:容量位,用于标记该缓冲区的容量,缓冲区创建好之后不可变
- position:操作位,用于指向要操作的位置,实际意义类似于数组中的下标,在缓冲区刚创建的时候指向0
- limit:限制位,用于限制操作位position所能达到的最大位置。在缓冲区刚创建的时候指向容量位
- mark:标记位,用于进行标记,在缓冲区刚创建的时候指向-1,默认不启用
Buffer初始时的状态:
示例代码:
输出结果:
Buffer写入部分数据后的状态:
示例代码:
输出结果:
Buffer反转后的状态:
示例代码:
输出结果:
import java.nio.ByteBuffer;
public class BufferDemo {
public static void main(String[] args) {
//创建Buffer
ByteBuffer buf = ByteBuffer.allocate(10);
printBufferState(buf);
// 写入数据到ByteBuffer
String message = "Hello NIO";
buf.put(message.getBytes());
System.out.println("Before flip():");
printBufferState(buf);
// 反转缓冲区
buf.flip();
// 打印切换到读模式后的状态
System.out.println("After flip():");
printBufferState(buf);
// 读取数据
byte[] data = new byte[buf.limit()];
buf.get(data);
// 打印读取到的数据
System.out.println("Read data: " + new String(data));
}
public static void printBufferState(ByteBuffer buf){
//输出3个变量的值
System.out.println("position="+buf.position()+",limit="+buf.limit()+",capacity="+buf.capacity());
}
}
3.2 Channel
Channel(通道)是Java NIO中提供的用于传输数据的工具,代表了源节点和目标节点之间的数据通道。Channel与Stream相似,但是略有不同:
- Channel是双向的
- Channel默认是阻塞的,可以设置为非阻塞
- Channel是面向缓冲区操作的
Java NIO中常用的Channel实现类如下:
其中:
- FileChannel:从文件读取数据和向文件读取数据
- DatagramChannel:可以通过UDP协议在网络上读写数据
- SocketChannel:可以通过TCP协议在网络上读写数据
- ServerSocketChannel:允许您侦听传入的 TCP 连接,就像 Web 服务器一样。 对于每个传入连接,都会创建一个 SocketChannel
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelDemo {
public static void main(String[] args) throws Exception {
readDemo();
writeDemo();
}
public static void readDemo() throws Exception {
RandomAccessFile aFile = new RandomAccessFile("data/hello.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(10);
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
System.out.println("\n====>Read " + bytesRead);
buf.flip();
while(buf.hasRemaining()){
System.out.print((char) buf.get()+" ");
}
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();
}
public static void writeDemo() throws Exception {
RandomAccessFile aFile = new RandomAccessFile("data/hello2.txt", "rw");
FileChannel channel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.wrap("Hello Channel!".getBytes());
int len=channel.write(buf);
System.out.println("len="+len);
aFile.close();
}
}
3.3 Selector
Selector 是 Java NIO 提供的一个组件,它可以检查一个或多个Channel 实例,并确定哪些通道准备好用于读、写等操作。 通过这种方式,单个线程可以管理多个通道,从而管理多个网络连接。
Selector是基于事件驱动的,供提供了4类事件:connect、accept、read和write。
这四类事件定义在SelectionKey中:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
想要使用Selector来管理Channel,需要先向Selector注册该Channel实例,可以通过SelectableChannel.register()方法实现。
// 将channel设置为非阻塞模式
channel.configureBlocking(false);
// 将通道注册到选择器,并指定关注事件为读事件
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
需要注意,Channel 必须处于非阻塞模式才能与 Selector 一起使用。 也就是说,不能将 FileChannel 与 Selector 一起使用,因为 FileChannel 无法切换到非阻塞模式。SocketChannel是可以与Selector搭配使用的。
创建一个ServerSocketChannel监听8080端口,并使用Selector来处理客户端的连接和数据读取。同时,创建了多个客户端线程,模拟并发访问。每个客户端线程会连接到服务器,并发送数据,然后接收服务器的响应。
Server端程序代码:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class Server {
public static void main(String[] args) throws IOException {
// 创建Selector
Selector selector = Selector.open();
// 创建ServerSocketChannel并绑定端口
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
// 将ServerSocketChannel注册到Selector上,并指定感兴趣的事件为接收连接事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server started.");
while (true) {
// Selector进行事件轮询
selector.select();
// 获取触发的事件集合
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
// 接收连接事件
handleAccept(key);
} else if (key.isReadable()) {
// 可读事件
handleRead(key);
}
}
}
}
private static void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(key.selector(), SelectionKey.OP_READ);
System.out.println("Accepted new connection from: " + clientChannel.getRemoteAddress());
}
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
System.out.println("Connection closed by client: " + clientChannel.getRemoteAddress());
// 客户端关闭连接
clientChannel.close();
} else if (bytesRead > 0) {
// 处理接收到的数据
buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println("Received data from " + clientChannel.getRemoteAddress() + ": " + new String(data));
// 回写响应数据
String response = "Response from server";
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
clientChannel.write(responseBuffer);
}
}
}
Client端程序代码:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Client {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Thread clientThread = new Thread(new ClientRunnable());
clientThread.start();
}
}
static class ClientRunnable implements Runnable {
@Override
public void run() {
try {
// 创建客户端SocketChannel并连接到服务器
SocketChannel clientChannel = SocketChannel.open();
clientChannel.configureBlocking(false);
clientChannel.connect(new InetSocketAddress("localhost", 8080));
// 等待连接完成
while (!clientChannel.finishConnect()) {
Thread.sleep(100);
}
// 发送数据到服务器
String message = "Hello from client";
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
clientChannel.write(buffer);
// 接收服务器响应
ByteBuffer responseBuffer = ByteBuffer.allocate(1024);
while (clientChannel.read(responseBuffer) <= 0) {
// 等待服务器响应
Thread.sleep(100);
}
responseBuffer.flip();
byte[] responseData = new byte[responseBuffer.limit()];
responseBuffer.get(responseData);
System.out.println( clientChannel.getLocalAddress()+"=>Received response from server: " + new String(responseData));
// 关闭客户端连接
clientChannel.close();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
}
4. 总结
1、阻塞(Blocking)和非阻塞(Nonblocking)描述的是线程在访问某个资源时,在该资源没有准备就绪期间的处理方式
- 阻塞:阻塞是指在进行I/O操作时,当前线程会被挂起,等待数据的就绪或操作的完成
- 非阻塞:非阻塞是指进行I/O操作时,当前线程不会被挂起,而是立即返回并继续执行其他任务
2、同步(synchronous)与异步(asynchronous)描述的是线程在发出请求后,是否等待结果
- 同步是指程序按照顺序执行,并等待某个操作完成后再继续执行下一个操作
- 在异步操作中,程序不会阻塞等待结果的返回,而是通过回调、轮询或事件通知等机制来获取结果或处理完成的事件
3、当涉及I/O操作时,可以根据是否阻塞和是否异步的角度将其分为以下四类
- 阻塞式I/O(Blocking I/O)
- 非阻塞式I/O(Non-Blocking I/O)
- I/O多路复用(I/O Multiplexing)
- 异步I/O(Asynchronous I/O)
4、在Java编程中提到的BIO、NIO和AIO,一般指的是Java语言为实现不同类型的I/O操作所提供的API
- Java IO:Java IO是JDK 1.0版本其自带的用于读取和写入数据的API,因其同步、阻塞的特征,被归类为同步阻塞式IO(Blocking IO),即Java BIO
- Java NIO(New IO):是JDK 1.4开始提供的同步非阻塞式的IO操作API,因其同步、非阻塞的特征,开发者一般将Java NIO理解为Java Non-Blocking IO
- Java NIO 2.0:是JDK 1.7开始提供的异步非阻塞式的IO操作API,因其是在NIO的基础上进行了改进,称为NIO 2.0;因其异步、非阻塞的特征,开发者一般将Java NIO 2.0称为Java AIO
5、Java BIO和NIO采用了2种不同的模型
- BIO使用了面向流(Stream Oriented)的模型
- Java NIO使用了面向缓冲区(Buffer Oriented)的,基于通道(Channel Based)的模型
6、Java NIO编程中的核心API包括Buffer、Channel和Selector
- Buffer(缓冲区)是Java NIO中提供的用于存储数据的容器,底层依靠数组来存储数据,并提供了对数据的结构化访问以及维护读写位置等信息的功能
- Channel(通道)是Java NIO中提供的用于传输数据的工具,代表了源节点和目标节点之间的数据通道
- Selector 是 Java NIO 提供的一个组件,它可以检查一个或多个Channel 实例,并确定哪些通道准备好用于读、写等操作,通过这种方式,单个线程可以管理多个通道,从而管理多个网络连接