相比于传统的阻塞IO,NIO提供了一种更灵活和高效的 I/O 操作方式,NIO 提供的非阻塞式的 I/O 操作,使得一个单独的线程可以管理多个通道(Channel),从而更好地处理并发连接和大量的 I/O 操作。
1. 核心组件
NIO 的核心组件包括通道(Channel)、缓冲区(Buffer)和选择器(Selector)。
(1)通道(Channel)
- 通道是 NIO 中用于读取和写入数据的抽象。它可以连接到文件、网络套接字等输入/输出设备。
- Java NIO 提供了不同类型的通道,包括文件通道(FileChannel)、套接字通道(SocketChannel 和 ServerSocketChannel)、Datagram 通道(DatagramChannel)等。
- 通道与传统的流(Stream)不同,通道可以双向传输数据,而流是单向的。
(2) 缓冲区(Buffer)
- 缓冲区是用于暂存数据的对象,它是 NIO 操作数据的基本单位。所有的数据读取和写入都是通过缓冲区进行的。
- 缓冲区具有固定的容量,可以通过 put() 和 get() 等方法向其中写入或读取数据。
- Java NIO 提供了不同类型的缓冲区,包括 ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer 和 DoubleBuffer,每种缓冲区用于不同类型的数据。
(3) 选择器(Selector)
- 选择器是 NIO 中用于多路复用的关键组件。它可以监听多个通道的事件,当一个或多个通道准备好进行读取或写入时,选择器将这些通道放入就绪集合(SelectionKey)中。
- 选择器使得一个单独的线程可以有效地管理多个通道,提高了系统的性能和资源利用率。
- 选择器是事件驱动模型的核心,它使得 NIO 可以实现非阻塞式的 I/O 操作。
2. 与传统 IO 的区别
(1)阻塞与非阻塞:
- 传统的 IO 是阻塞式的,当一个 IO 操作开始后,线程会被阻塞,直到该操作完成。这意味着如果线程在进行 IO 操作时没有其他任务可执行,系统资源会被浪费。
- NIO 使用非阻塞式 IO,意味着线程在进行 IO 操作时可以同时执行其他任务,从而提高了系统资源的利用率。
(2)通道与流:
- 传统的 IO 使用字节流和字符流进行数据传输,而 NIO 使用通道(Channel)和缓冲区(Buffer)。
- 通道可以双向传输数据,而流是单向的。通道提供了更多的控制和灵活性。
(3)多路复用:
- NIO 引入了选择器(Selector)的概念,可以同时监听多个通道的事件,当一个或多个通道准备好进行读取或写入时,选择器会通知程序。
- 这使得一个线程可以有效地管理多个通道,处理并发连接和大量的 IO 操作,提高了系统的性能和可扩展性。
3. NIO 的优势
-
高并发处理:NIO 可以通过较少的线程处理更多的连接,提高了系统的性能和资源利用率。
-
灵活的数据操作:NIO 提供了更灵活和高效的数据操作方式,可以更好地处理大量的 IO 操作。
使用NIO传输图片
客户端代码
import java.io.FileInputStream; // 导入文件输入流类,用于读取文件内容
import java.io.IOException; // 导入I/O异常类,用于处理可能的文件读取和网络通信异常
import java.net.InetSocketAddress; // 导入网络地址类,用于指定服务器的IP地址和端口号
import java.nio.ByteBuffer; // 导入ByteBuffer类,用于在通道中传输数据
import java.nio.channels.FileChannel; // 导入文件通道类,用于从文件中读取数据
import java.nio.channels.SocketChannel; // 导入Socket通道类,用于网络通信
public class ImageClient {
// 主函数,程序的入口点
public static void main(String[] args) throws IOException {
// 创建一个SocketChannel实例,并准备连接到服务器
// SocketChannel是NIO(非阻塞I/O)中用于网络通信的通道
SocketChannel socketChannel = SocketChannel.open();
// 连接到指定的服务器地址和端口,这里连接到本地机器(localhost)的8080端口
// InetSocketAddress是一个网络地址的封装,包含了IP地址和端口号
socketChannel.connect(new InetSocketAddress("localhost", 8080));
// 读取图片文件到ByteBuffer中
// 使用FileInputStream来读取文件,然后通过getChannel()方法获取到文件的通道(FileChannel)
FileInputStream fis = new FileInputStream("src/main/java/images/userAvatar.jpg");
FileChannel fileChannel = fis.getChannel();
// 分配一个ByteBuffer,大小为1024字节,用于临时存储从文件中读取的数据
// ByteBuffer是一个字节容器,可以通过它读取或写入数据到通道
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 循环读取文件内容,直到文件结束
while (fileChannel.read(buffer) > 0) {
// 调用flip()方法,将ByteBuffer从写模式切换到读模式
// 在调用write()方法之前,必须先调用flip()方法
buffer.flip();
// 将ByteBuffer中的数据写入到SocketChannel中,发送到服务器
socketChannel.write(buffer);
// 调用clear()方法,清空ByteBuffer,准备下一次读取
// clear()方法会将position设置为0,limit设置为capacity,为下一次读取或写入做准备
buffer.clear();
}
// 关闭FileInputStream和SocketChannel,释放资源
fis.close();
socketChannel.close();
}
}
服务端代码
import java.io.FileOutputStream; // 导入文件输出流类,用于将接收到的数据写入文件
import java.io.IOException; // 导入I/O异常类,用于处理可能的文件写入和网络通信异常
import java.net.InetSocketAddress; // 导入网络地址类,用于指定服务器的IP地址和端口号
import java.nio.ByteBuffer; // 导入ByteBuffer类,用于在通道中传输数据
import java.nio.channels.FileChannel; // 导入文件通道类,用于向文件中写入数据
import java.nio.channels.ServerSocketChannel; // 导入服务器套接字通道类,用于监听和接受连接
import java.nio.channels.SocketChannel; // 导入Socket通道类,用于网络通信
public class ImageServer {
public static void main(String[] args) throws IOException {
// 创建一个ServerSocketChannel实例,用于监听连接请求
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 将ServerSocketChannel绑定到特定的端口,这里使用8080端口
serverSocketChannel.bind(new InetSocketAddress(8080));
// 配置ServerSocketChannel为非阻塞模式,这允许服务器同时处理多个连接
serverSocketChannel.configureBlocking(false);
// 无限循环,持续监听新的连接请求
while (true) {
// 尝试接受一个新的连接请求
// 因为设置了非阻塞模式,如果当前没有连接请求,accept()会立即返回null
SocketChannel socketChannel = serverSocketChannel.accept();
// 如果成功接受了一个连接请求,则进行以下操作
if (socketChannel != null) {
// 分配一个ByteBuffer,大小为1024字节,用于临时存储从SocketChannel中读取的数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 创建一个FileOutputStream,用于将接收到的数据写入到文件中
// 这里假设文件名为"D:\\ltsServer\\text.jpg"
FileOutputStream fos = new FileOutputStream("D:\\ltsServer\\text.jpg");
// 获取FileOutputStream对应的FileChannel
FileChannel fileChannel = fos.getChannel();
// 读取SocketChannel中的数据,并写入到文件中,直到SocketChannel中没有更多数据可读
int bytesRead;
while ((bytesRead = socketChannel.read(buffer)) > 0) {
// 打印从SocketChannel中读取的字节数
System.out.println(bytesRead);
// 调用flip()方法,将ByteBuffer从读模式切换到写模式
buffer.flip();
// 将ByteBuffer中的数据写入到FileChannel中,即将数据写入到文件中
fileChannel.write(buffer);
// 调用clear()方法,清空ByteBuffer,准备下一次读取
buffer.clear();
}
// 关闭FileOutputStream和SocketChannel,释放资源
fos.close();
socketChannel.close();
}
// 在实际应用中,对于非阻塞模式,通常需要使用选择器(Selector)来同时处理多个通道
// 这里的代码只是简单示例,没有使用选择器
}
}
}
这里再补充一段在服务端使用 选择器(Selector)监听通道的代码
private static void start() throws IOException {
// 创建ServerSocketChannel,并设置为非阻塞模式
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
// 创建Selector,用于监听通道事件
Selector selector = Selector.open();
// 将ServerSocketChannel注册到Selector上,监听连接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 不断循环,处理通道事件
while (true) {
selector.select(); // 阻塞,等待通道事件发生
for (SelectionKey key : selector.selectedKeys()) {
if (key.isAcceptable()) { // 客户端发起连接事件
acceptClient(selector, serverSocketChannel);
} else if (key.isReadable()) { // 客户端发送数据事件
readMessageFromClient(key);
}
}
selector.selectedKeys().clear(); // 清空已处理的事件集合
}
}