了解Java NIO (Channel/Buffer/Selec
Java NIO
在 Java 编程中,Java NIO(New Input/Output)是一种高性能的 I/O 操作方式,它提供了一套不同于传统 Java I/O 的 API,能够更高效地处理大量并发连接和数据传输。本文将详细介绍 Java NIO 中的核心类,包括 Channel、ByteBuffer 和 Selector 的使用方法和注意点。
一、Java NIO 概述
Java NIO 由以下几个核心部分组成:
Channels(通道):用于在字节缓冲区和 I/O 源(如文件、网络套接字)之间进行数据传输。
Buffers(缓冲区):用于存储数据的容器,可以是字节缓冲区、字符缓冲区等。
Selectors(选择器):用于监控多个通道的事件,实现单线程处理多个连接。
二、Channel(通道)
(一)Channel 简介
Channel 是对传统 I/O 中输入流和输出流的抽象,它代表一个到实体(如文件、网络套接字)的开放连接,可以进行读写操作。
(二)Channel 的类型
FileChannel:用于文件的读写操作。
SocketChannel:用于 TCP 网络连接的客户端。
ServerSocketChannel:用于 TCP 网络连接的服务器端,用于监听新的连接请求。
DatagramChannel:用于 UDP 网络通信。
(三)Channel 的创建和使用
创建 FileChannel
通过FileInputStream、FileOutputStream或RandomAccessFile获取FileChannel。
FileInputStream fis = new FileInputStream("file.txt");
FileChannel fileChannel = fis.getChannel();
创建 SocketChannel 和 ServerSocketChannel
创建客户端SocketChannel:
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8080));
创建服务器端ServerSocketChannel:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
使用 Channel 进行读写操作
从通道读取数据到缓冲区:
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);
将缓冲区的数据写入通道:
buffer.flip();
int bytesWritten = channel.write(buffer);
(四)Channel 注意点
非阻塞模式:可以设置通道为非阻塞模式,在非阻塞模式下,通道的读写操作不会阻塞线程,而是立即返回。这对于处理大量并发连接非常有用。
channel.configureBlocking(false);
通道之间的数据传输:可以直接在两个通道之间传输数据,而无需通过中间缓冲区。
FileChannel sourceChannel =...;
FileChannel destinationChannel =...;
sourceChannel.transferTo(0, sourceChannel.size(), destinationChannel);
三、ByteBuffer(字节缓冲区)
(一)ByteBuffer 简介
ByteBuffer 是 Java NIO 中用于处理字节数据的缓冲区。它提供了一种高效的方式来存储和操作字节数据,可以与通道一起使用进行数据传输。
(二)ByteBuffer 的创建
分配指定大小的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
包装现有的字节数组
byte[] array = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(array);
(三)ByteBuffer 的基本操作
写入数据
buffer.put((byte)1);
buffer.put((byte)2);
buffer.put((byte)3);
读取数据
byte firstByte = buffer.get();
byte secondByte = buffer.get();
切换缓冲区模式
写入模式切换到读取模式:
buffer.flip();
读取模式切换到写入模式:
buffer.clear();
标记和重置
设置标记:
buffer.mark();
重置到标记位置:
buffer.reset();
获取缓冲区信息
获取当前位置:
int position = buffer.position();
获取容量:
int capacity = buffer.capacity();
获取限制:
int limit = buffer.limit();
(四)ByteBuffer 注意点
缓冲区的状态管理
位置(position):表示下一个要读取或写入的字节位置。
限制(limit):在读取模式下,限制表示可以读取的最大字节数;在写入模式下,限制通常等于缓冲区的容量。
容量(capacity):缓冲区的固定大小,一旦创建就不能改变。
字节顺序
ByteBuffer可以使用大端字节序(Big Endian)或小端字节序(Little Endian)。可以通过ByteBuffer.order()方法设置字节序。
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.order(ByteOrder.BIG_ENDIAN);
buffer.putInt(12345);
直接缓冲区和非直接缓冲区
直接缓冲区:通过ByteBuffer.allocateDirect()方法创建。直接缓冲区直接与操作系统的底层内存交互,通常在进行大量数据传输时性能更好。
非直接缓冲区:通过ByteBuffer.allocate()方法创建。非直接缓冲区在 Java 堆内存中分配,可能在与通道进行数据传输时需要额外的复制操作。
缓冲区的复用
可以重复使用同一个缓冲区来处理不同的数据块,通过clear()或compact()方法来准备缓冲区进行新的数据操作。
四、Selector(选择器)
(一)Selector 简介
Selector 是 Java NIO 中用于实现多路复用的关键组件。它可以同时监控多个通道的事件,当有事件发生时,选择器会通知相应的线程进行处理。
(二)Selector 的创建和使用
创建 Selector
Selector selector = Selector.open();
注册通道到 Selector
对于ServerSocketChannel:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
对于SocketChannel:
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
使用 Selector 进行事件处理
选择就绪的通道:
int readyChannels = selector.select();
遍历就绪的通道:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// 处理连接接受事件
} else if (key.isReadable()) {
// 处理可读事件
} else if (key.isWritable()) {
// 处理可写事件
}
keyIterator.remove();
}
(三)Selector 注意点
事件类型
SelectionKey.OP_ACCEPT:连接接受事件,表示有新的连接请求。
SelectionKey.OP_CONNECT:连接完成事件,表示客户端连接已经建立。
SelectionKey.OP_READ:可读事件,表示通道中有数据可读。
SelectionKey.OP_WRITE:可写事件,表示通道可以写入数据。
非阻塞模式:与通道一样,选择器也在非阻塞模式下工作。这意味着select()方法不会阻塞线程,而是立即返回就绪的通道数量。
优化和性能考虑
避免频繁的select()调用,可以设置一个超时时间来控制等待事件的时间。
合理地处理事件,避免在事件处理中进行耗时的操作,以免影响其他通道的响应时间。
五、Java NIO 的应用场景
高并发网络服务器:可以使用 Java NIO 实现高性能的网络服务器,处理大量并发连接。
文件传输:利用通道和缓冲区可以高效地进行文件的读写和传输。
异步 I/O 操作:通过非阻塞模式和选择器,可以实现异步的 I/O 操作,提高程序的响应性。
目前Netty 等网络框架封装了复杂API的使用,开箱即用。