目录
本篇文章内容的前置知识为 NIO Buffer类,如果不了解,可点击链接学习
什么是 NIO Channel?
在 Java NIO 中,Channel(通道)是一种广义的 I/O 抽象,用于表示与数据源或数据目的地之间的连接。一个 Channel 可以代表文件、套接字(Socket)、网络连接等多种底层资源。相比传统的流式 I/O,Channel 提供了更高效的读写机制,尤其适合非阻塞式数据传输。
为了适应不同的协议类型和传输方式,Java NIO 提供了多个 Channel 实现,如 FileChannel、SocketChannel、ServerSocketChannel 和 DatagramChannel 等。每种 Channel 实现都针对特定的使用场景进行了优化,能够在文件操作、TCP/UDP 网络通信等方面提供高效的 I/O 支持。通过这些 Channel,开发者可以在 Java 中实现统一且灵活的数据通道管理机制。
常见 Channel 类型包括:
FileChannel:用于文件的读写操作
SocketChannel:用于 TCP 网络通信中的客户端
ServerSocketChannel:用于 TCP 网络通信中的服务器端
DatagramChannel:用于 UDP 网络通信
为什么要学习NIO Channel?
在 Java 中,学习 NIO 的 Channel 类具有重要的现实意义。Channel 是 Java NIO 中用于数据传输的核心组件,它代表了与数据源之间的连接,如文件、网络套接字等资源。相比传统的阻塞式 I/O,Channel 提供了一种基于缓冲区的非阻塞 I/O 操作方式,能够大幅提升系统在处理高并发、大数据量传输时的性能表现。
通过 Channel,开发者可以以统一的方式操作不同类型的底层资源,如通过 FileChannel 操作文件,通过 SocketChannel 进行 TCP 网络通信,通过 DatagramChannel 实现 UDP 数据传输等。这种统一的抽象不仅提升了代码的可维护性,也简化了对各种 I/O 模型的学习和使用。
此外,Channel 通常配合 ByteBuffer 使用,具备灵活的缓冲区控制能力,支持批量数据处理、状态标记、重复读取等操作,适用于需要精细控制数据读写过程的应用场景。
更重要的是,NIO 的 Channel 是构建高性能服务器和分布式系统的重要基础。许多高性能框架(如 Netty)和中间件组件,其底层通信机制都基于 Channel 和 Selector 实现。掌握 Channel 的使用方法和工作原理,有助于开发者深入理解 Java 与操作系统底层 I/O 机制的关系,为开发高并发、高吞吐的系统奠定坚实的技术基础。
学习 Channel 类不仅能够提升程序的 I/O 处理效率,也有助于开发者构建面向性能优化的现代应用程序,是 Java 高级开发中不可或缺的重要内容。
FileChannel
FileChannel用于文件读写
1. 获取 FileChannel 的方式
(1) 通过输入/输出流方式:
FileInputStream fis = new FileInputStream(srcFile);
FileChannel inChannel = fis.getChannel();
FileOutputStream fos = new FileOutputStream(destFile);
FileChannel outChannel = fos.getChannel();
FileInputStream获取读通道:
FileInputStream是 Java 标准 IO 中的输入流,用于从文件读取数据。
getChannel()方法返回与该流关联的FileChannel,该通道处于只读模式(READ)
示例中的srcFile是一个File对象,表示要读取的源文件。
FileOutputStream获取写通道:
FileOutputStream用于向文件写入数据。
getChannel()方法返回的FileChannel处于只写模式(WRITE)
示例中的destFile是目标文件,若不存在会自动创建;若已存在则会被覆盖。
适用场景
读取文件:通过FileInputStream获取通道,配合ByteBuffer读取文件内容。
写入文件:通过FileOutputStream获取通道,将数据写入文件。
(2) 通过 RandomAccessFile(随机访问):
RandomAccessFile rFile = new RandomAccessFile("filename.txt", "rw");
FileChannel channel = rFile.getChannel();
RandomAccessFile简介:
RandomAccessFile支持随机访问文件,可在文件的任意位置读写数据。
构造函数参数:
第一个参数是文件名或File对象。
第二个参数是访问模式:
"r":只读模式
"rw":读写模式(若文件不存在会创建)
"rws"/"rwd":强制同步到磁盘(涉及文件元数据或内容)
获取通道:
getChannel()方法返回的FileChannel模式与RandomAccessFile的打开模式一致。
例如,使用"rw"模式打开时,通道支持读写操作。
适用场景
随机访问文件:通过channel.position(offset)方法定位到文件的指定位置进行读写。
读写模式切换:同一通道可同时执行读写操作,不需要像流一样分开处理。
2. 读取 FileChannel
使用 read(ByteBuffer buf) 方法将通道中的数据读入 buf。
返回值是读取的字节数,若返回 -1,表示读取完毕。
ByteBuffer buf = ByteBuffer.allocate(CAPACITY);
int length = -1;
while ((length = inChannel.read(buf)) != -1) {
}
inChannel.read(buf) 是“通道读 -> 缓冲区写”,此时 ByteBuffer 处于写模式。
3. 写入 FileChannel
使用 write(ByteBuffer buf) 将 buf 中的数据写入通道。
ByteBuffer 需先通过 flip() 切换到读模式,才能写入通道。
buf.flip(); // 切换为读模式
while ((outlength = outChannel.write(buf)) != 0) {
System.out.println("写入的字节数: " + outlength);
}
outChannel.write(buf) 是“缓冲区读 -> 通道写”,所以 ByteBuffer 必须是读模式。
4. 关闭通道
使用 channel.close() 方法释放资源。
channel.close();
5. 强制刷新到磁盘
使用 force(true) 方法,将缓冲区中写入通道的数据强制写入磁盘,确保数据落地。
channel.force(true);
使用 FileChannel 完成文件复制
主要流程:
使用 FileInputStream/FileOutputStream 获取 FileChannel
创建 ByteBuffer 缓冲区
从输入通道读取 -> 写入缓冲区
缓冲区 flip() -> 输出通道写入
每轮循环都需要两次模式切换:flip() 切换为读、clear() 切换为写
ByteBuffer buf = ByteBuffer.allocate(1024);
while ((length = inChannel.read(buf)) != -1) {
buf.flip(); // 写 -> 读模式
while ((outlength = outChannel.write(buf)) != 0) {
System.out.println("写入的字节数: " + outlength);
}
buf.clear(); // 读 -> 写模式,准备下一轮
}
outChannel.force(true); // 强制刷新
代码本质是将数据从inChannel(输入通道)复制到outChannel(输出通道)
inChannel.read(buf) 返回实际读取的字节数
outChannel.write(buf) 返回实际写入的字节
在代码逻辑中,length最终会等于-1,而outlength在最后一轮循环中会等于0
SocketChannel
在 Java NIO 中,网络通信主要依赖两种通道类型:SocketChannel 和 ServerSocketChannel。其中,SocketChannel 用于实际的数据传输,既可以在客户端使用,也可以在服务器端使用;而 ServerSocketChannel 用于服务器端监听来自客户端的连接请求。
这两种通道在功能上分别对应于传统阻塞式 I/O 中的 Socket 和 ServerSocket 类,但 NIO 提供了非阻塞操作模式,使得程序在处理高并发连接时能够更高效地利用系统资源。非阻塞模式下,通道的连接、读取和写入操作都是异步的,线程不会因为等待数据就被阻塞,从而大大提升了系统的并发处理能力。因此 NIO 中的 SocketChannel 与 ServerSocketChannel 是构建高性能网络应用的重要基础。
阻塞与非阻塞模式
Java NIO 中所有 Channel 都支持阻塞模式和非阻塞模式,通过 configureBlocking(boolean) 方法设置:
socketChannel.configureBlocking(false):非阻塞模式;
socketChannel.configureBlocking(true):阻塞模式。
非阻塞模式下,连接、读取和写入操作都是异步的,这种模式是 NIO 的核心优势。
SocketChannel 的基本操作
1. 客户端:获取并连接 SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false); // 设置非阻塞模式
socketChannel.connect(new InetSocketAddress("127.0.0.1", 80));
建立一个非阻塞的 TCP 连接到服务器(127.0.0.1:80),并确保连接完全建立后再进行后续操作。
非阻塞模式下,连接可能未立即建立完毕,此时需要:
while (!socketChannel.finishConnect()) {
}
finishConnect()的作用:
返回true:连接已完全建立,可以进行数据读写。
返回false:连接仍在进行中,需继续等待。
TCP(Transmission Control Protocol,传输控制协议)
是一种面向连接的、可靠的传输层协议。
TCP 建立连接需要完成三次握手:
第一次握手(SYN):
客户端发送 SYN 包(同步序列号)到服务器,请求建立连接。
第二次握手(SYN+ACK):
服务器收到 SYN 后,发送 SYN+ACK 包(确认客户端 SYN 并请求自己的同步)
第三次握手(ACK):
客户端收到 SYN+ACK 后,发送 ACK 包(确认服务器 SYN),连接正式建立。
2. 服务端:接收连接(需 ServerSocketChannel)
服务端使用 ServerSocketChannel 接收连接:
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(80));
serverChannel.configureBlocking(false);
当有连接到来时:
SocketChannel clientChannel = serverChannel.accept(); // 获取客户端连接
clientChannel.configureBlocking(false); // 客户端通道也设为非阻塞
3. 读取 SocketChannel 数据
ByteBuffer buf = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buf); // 通道读入缓冲区(buf处于写模式)
bytesRead > 0:读取到 bytesRead 字节数据。
bytesRead == -1:客户端关闭连接(类似传统 IO 的 InputStream.read() == -1)
4. 写入 SocketChannel 数据
buffer.flip(); // 切换为读模式
socketChannel.write(buffer); // 将缓冲区数据写入通道
注意 ByteBuffer 必须处于读模式。
5. 关闭 SocketChannel
关闭前,如果写过数据建议先发送一个结束标志,通知对方数据已传输完:
socketChannel.shutdownOutput(); // 发送输出结束标志
socketChannel.close(); // 关闭连接
使用 SocketChannel 发送文件
主要流程:
(1) 客户端通过 FileChannel 读取文件;
(2) 使用 SocketChannel 向服务器发送:
文件名(不含路径)
文件长度
文件内容
(3) 每次发送使用 ByteBuffer,需注意 flip/clear 模式切换。
文件内容发送核心代码:
while ((length = fileChannel.read(buffer)) > 0) {
buffer.flip(); // 写 -> 读
socketChannel.write(buffer); // 发出数据
buffer.clear(); // 读 -> 写(准备下一轮)
}
文件名 + 文件长度发送代码:
ByteBuffer nameBuffer = charset.encode(destFile); // 编码文件名
ByteBuffer buffer = ByteBuffer.allocate(SEND_BUFFER_SIZE);
// 发送文件名长度
buffer.putInt(nameBuffer.capacity());
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
// 发送文件名内容
socketChannel.write(nameBuffer);
// 发送文件长度(long 类型)
buffer.putLong(file.length());
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
charset.encode(destFile) 是将 destFile(文件名)编码成一个字节序列,并返回一个 ByteBuffer。
charset 是一个字符集编码对象,通常使用 Charset.forName("UTF-8") 来进行编码。
destFile 是文件名,这一行的目的是将文件名转换为字节缓冲区,以便通过通道传输。nameBuffer.capacity() 返回 nameBuffer(即文件名)的容量,即文件名占用的字节数。
buffer.putInt(nameBuffer.capacity()) 将文件名的长度(以字节为单位)写入 buffer。file.length() 返回文件的大小(以字节为单位)
buffer.putLong(file.length()) 将文件的大小(long 类型)写入 buffer 中。
DatagramChannel
在 Java 中,DatagramChannel 是 NIO 提供的用于进行 UDP 数据传输 的通道类。与 TCP 不同,UDP 是一种无连接的协议,它不需要建立连接,只要知道目标 IP 和端口即可直接发送数据。
使用传统 Socket 编程时,UDP 通信基于 DatagramSocket,而在 Java NIO 中,对应的就是 DatagramChannel。
DatagramChannel 的使用步骤
1. 获取 DatagramChannel
使用 open() 方法创建通道,并设置为非阻塞模式:
DatagramChannel channel = DatagramChannel.open();
channel.configureBlocking(false); // 设置非阻塞模式
如果是服务端或需要接收数据的客户端,还需要绑定本地监听端口:
channel.socket().bind(new InetSocketAddress(18080)); // 绑定端口
2. 读取数据(接收 UDP 数据包)
UDP 接收方式不同于 TCP 的 read() 方法,DatagramChannel 使用的是 receive() 方法
SocketAddress clientAddr = channel.receive(buffer);
参数是 ByteBuffer,表示读取到的数据存储的位置;
返回值是 SocketAddress,代表数据来源的地址(发送端 IP + 端口)
若无数据可读,非阻塞模式下立即返回 null;
buffer作为参数的含义是告诉receive()方法将接收到的 UDP 数据包内容,存储到这个buffer所代表的内存位置中。
UDP 是无连接的协议,每次接收数据都需要获取发送方的地址信息。
receive()方法的作用是:
读取 UDP 数据包内容:操作系统内核将 UDP 数据包内容复制到buffer中。
获取发送方地址:返回一个SocketAddress对象(包含 IP 和端口),标识数据来源。
3. 写入数据(发送 UDP 数据包)
发送数据使用 send() 方法而不是 write() 方法:
buffer.flip(); // 写 -> 读模式
channel.send(buffer, new InetSocketAddress("127.0.0.1", 18899));
// 通过 UDP 协议向指定目标地址发送数据包
buffer.clear(); // 清空准备写入
必须指定目标地址,因为 UDP 是无连接的。
4. 关闭 DatagramChannel
关闭通道时调用:
channel.close();
使用 DatagramChannel 的案例
客户端:UDP 数据发送器
功能:读取用户输入,将数据通过 UDP 发送到服务器。
DatagramChannel dChannel = DatagramChannel.open(); // 打开通道
dChannel.configureBlocking(false); // 设置非阻塞
ByteBuffer buffer = ByteBuffer.allocate(...); // 创建缓冲区
Scanner scanner = new Scanner(System.in); // 控制台输入
while (scanner.hasNext()) {
String next = scanner.next();
buffer.put((时间戳 + 用户输入).getBytes()); // 写入数据
buffer.flip();
dChannel.send(buffer, new InetSocketAddress("127.0.0.1", 18899)); // 发送
buffer.clear(); // 清空
}
dChannel.close(); // 关闭通道
特点:
使用简单,不需要建立连接;
数据是“发了就走”,可能会丢失(UDP特性)
不需要调用 connect();
每次发送时都需指定目标地址。
服务端:UDP 接收器 + Selector
功能:接收客户端发来的 UDP 数据,并打印输出。
DatagramChannel channel = DatagramChannel.open(); // 创建通道
channel.configureBlocking(false); // 非阻塞模式
channel.bind(new InetSocketAddress("127.0.0.1", 18899)); // 绑定监听端口
Selector selector = Selector.open(); // 开启选择器
channel.register(selector, SelectionKey.OP_READ); // 注册读事件
while (selector.select() > 0) { // 有事件时处理
for (SelectionKey key : selector.selectedKeys()) {
if (key.isReadable()) {
ByteBuffer buffer = ByteBuffer.allocate(...);
SocketAddress client = channel.receive(buffer); // 接收数据
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.limit()));
buffer.clear();
}
}
selector.selectedKeys().clear(); // 清除已处理事件
}
selector.close(); channel.close(); // 关闭资源
特点:
使用 Selector 实现非阻塞监听,检查通道是否有数据到来;
channel.receive() 是读取入口;
使用 SelectionKey.OP_READ 注册读事件;
每次接收都能获取发送者的地址(SocketAddress)
使用 flip() 切换读模式,clear() 准备再次写入。
尚未完结,可点击链接跳转下一篇文章