五、NIO
# 五、NIO
# 1、操作系统级别下的IO模型有哪些?
阻塞式 IO (Blocking IO):
当应用程序发起 IO 操作时,如果数据还没有准备好或者无法立即处理,IO 操作会阻塞程序的执行,直到数据准备就绪或者操作完成为止。
非阻塞式 IO (Non-blocking IO):
在非阻塞 IO 模型中,应用程序发起 IO 操作后,会立即返回,无论数据是否就绪或者能否立即处理。
这样程序可以继续执行其他任务,而不必等待 IO 操作完成。需要通过轮询或者事件通知等方式来检查 IO 操作的状态。
IO 复用 (IO Multiplexing):
IO 复用模型通过操作系统提供的多路复用机制,如 select、poll 或 epoll,在一个线程中同时监视多个 IO 通道的状态。
当其中任意一个 IO 通道就绪时,程序可以进行相应的处理。常见于网络编程中,由单个线程管理多个客户端连接。
信号驱动 IO (Signal-driven IO):
在信号驱动 IO 模型中,应用程序会将 IO 操作请求发送给操作系统,并注册一个信号处理函数。
当 IO 操作完成时,操作系统会发送一个信号给应用程序,通知其 IO 操作已完成,然后应用程序可以调用相应的处理函数来处理数据。
异步 IO (Asynchronous IO):
异步 IO 模型中,应用程序发起 IO 操作后立即返回,但是会指定一个回调函数或者事件处理器。
当 IO 操作完成时,操作系统会通知应用程序,然后调用指定的回调函数来处理数据。相比非阻塞 IO,异步 IO 不需要程序通过轮询来检查 IO 状态,因此效率更高。
# 2、Java语言下的IO模型有哪些?
BIO (Blocking I/O):
BIO 属于同步阻塞 IO 模型,该模型中,应用程序发起IO操作后,会一直阻塞,直到操作系统内核把数据拷贝到用户空间。
NIO (Non-blocking I/O 或者叫 New IO):
Java 中的 NIO 于 JDK 1.4 中引入,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。
它是支持面向缓冲的,基于通道的 I/O 操作方法。NIO适用于 高负载、高并发的(网络)请求。
Java 中的 NIO 主要对应于 操作系统级别的IO 复用模型
在Linux等操作系统中,IO 复用模型通常通过select、poll或者更高效的epoll系统调用来实现。I/O多路复用允许一个线程同时监控多个输入输出通道,包括Socket连接,等待其中的一个或多个通道准备好进行读写操作,从而实现了单线程管理多个连接的能力,提高了系统的并发性和效率。
Java NIO的核心组件如Selector(选择器)、Channel(通道)和Buffer(缓冲区)的设计,正是为了利用这种操作系统提供的能力。Selector可以看作是I/O多路复用的具体实现点,它能够监控注册在其上的多个Channel,当某个Channel上有读写事件发生时,通过选择器的通知机制,应用程序就可以进行相应的读写操作,而无需为每个连接创建单独的线程,从而避免了线程上下文切换的开销。
AIO (Asynchronous I/O) :
JDK 1.7 中引入,它是异步 IO 模型。
异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在调用处,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
# 3、Java的NIO应用场景?相比于IO的优势在哪?
多路复用:
NIO可以使用单个线程管理多个通道,这种多路复用的特性使得它非常适合处理大量的并发连接,例如网络服务器。
NIO提供了选择器(Selector)机制,可以通过一个线程管理多个通道的IO操作。
当某个通道有IO事件发生时,可以通过选择器进行通知,从而实现高效的事件驱动模型。
非阻塞 I/O
NIO 支持非阻塞 I/O,这意味着在执行 I/O 操作时,线程不会被阻塞。这使得在网络传输中可以有效地管理大量并发连接。
NIO的非阻塞主要体现在以下方面:
1.通道(Channels):NIO引入了通道(Channel)的概念,它是对传统流(Stream)的升级。通道是双向的,既可以进行读操作也可以进行写操作,而且支持非阻塞模式。例如,SocketChannel和ServerSocketChannel分别用于客户端和服务端的网络通信,它们可以在非阻塞模式下工作。
2.缓冲区(Buffers):NIO中数据的读取和写入都是通过缓冲区(Buffer)完成的。缓冲区是一个可以保存多个类型数据的容器,它独立于通道存在,使得数据可以被多次读取或写入,这在非阻塞模式下尤为重要,因为数据可能没有立即准备好。
3.选择器(Selectors):这是NIO非阻塞I/O的核心机制。选择器允许单个线程管理多个通道,可以同时监听多个通道的事件(如读、写、连接等)。当一个或多个通道准备好进行I/O操作时,选择器会通知应用程序,而不需要每个通道都阻塞等待。这样,一个线程就能高效地处理多个并发连接,减少了线程上下文切换的开销。
4.非阻塞模式操作:通过调用通道的.configureBlocking(false)方法,可以使通道进入非阻塞模式。在非阻塞模式下,读写操作不会阻塞线程,如果操作不能立即完成(比如没有数据可读或缓冲区已满无法写入),这些操作会立即返回,而不是等待,返回值会指示操作的状态(如读取到的字节数或是否成功写入)。
5.事件驱动编程:在非阻塞模式下,开发者通常采用事件驱动的方式编写代码,即基于选择器通知的事件来决定下一步的操作。这种方式更加灵活高效,特别是在处理大量并发连接时,能够显著提高应用程序的可伸缩性和性能。
缓冲
NIO 读写数据都是通过缓冲区进行操作的。读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。
NIO利用 Buffer 和Channel可以高效的管理网络IO中的字节流数据。 这点类似于传统IO中的 BufferedInputStream中的缓冲区。
总结:
NIO性能优势主要体现在处理高并发的网络IO场景。
传统 I/O 在网络通信中主要使用阻塞式 I/O,为每个连接分配一个线程。当连接数量增加时,系统性能将受到严重影响,线程资源成为系统的性能瓶颈。
而 NIO 提供了非阻塞 I/O 和 I/O 多路复用,可以在单个线程中处理多个并发连接,从而在网络传输中显著提高性能。
对于处理请求数较少或者少量连接读写大文件的场景其优势相对于传统IO并不明显。
# 上面讲了很多理论性的知识,下面来点代码,去体验下NIO的基本用法
# 4、Java的IO、NIO、AIO 操作文件读写
public static void main(String[] args) {
/*==========传统IO==========*/
try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter("123.txt"))
){
bufferedWriter.write("测试传统IO");
} catch (IOException e) {
e.printStackTrace();
}
try (BufferedReader bufferedReader = new BufferedReader(new FileReader("123.txt"));){
String line;
while ((line = bufferedReader.readLine()) != null){
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}
/*==========NIO==========*/
Path path = Paths.get("456.txt");
try ( FileChannel fileChannel = FileChannel.open(path, EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE))) {
ByteBuffer buffer = StandardCharsets.UTF_8.encode("测试NIO");
fileChannel.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ)){
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = fileChannel.read(buffer);
while (bytesRead != -1) {
buffer.flip();
System.out.println(StandardCharsets.UTF_8.decode(buffer));
buffer.clear();
bytesRead = fileChannel.read(buffer);
}
} catch (IOException e) {
e.printStackTrace();
}
/*==========AIO==========*/
// 使用 Paths.get() 获取文件路径
Path pathAIO = Paths.get("789.txt");
try {
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(pathAIO, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
ByteBuffer buffer = StandardCharsets.UTF_8.encode("测试AIO");
Future<Integer> result = fileChannel.write(buffer, 0);
result.get();
fileChannel.close();
} catch (Exception e) {
e.printStackTrace();
}
try (AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(pathAIO, StandardOpenOption.READ)){
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 异步读取 主线程会继续往下执行 为了防止读取完成之前 程序运行结束 使用线程同步器来处理同步问题
CountDownLatch countDownLatch = new CountDownLatch(1);
fileChannel.read(buffer, 0, buffer, new CompletionHandler<>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
attachment.flip();
System.out.println(StandardCharsets.UTF_8.decode(attachment));
attachment.clear();
countDownLatch.countDown();
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
System.out.println("读取失败");
exc.printStackTrace();
countDownLatch.countDown();
}
});
// 等待异步操作完成
countDownLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
}
# 5、NIO的核心类 :Buffer(缓冲区)、Channel(通道)、Selector(选择器)
在传统的BIO(Blocking IO)中
IO 是面向流的处理,比如 InputStream和 OutputStream,面向流的 I/O 系统一次一个字节地处理数据。
NIO(Non-blocking IO) 是面向块(缓冲区)的处理,面向块(缓冲区)的 I/O 系统以缓存块的形式处理数据。
有点类似于BIO中的 缓冲流 BufferedInputStream和BufferedOutputStream。
在NIO体系中是以 Buffer 缓冲区和 Channel 通道配合来处理数据的。
Buffer(缓冲区):
Buffer是抽象类:
其中最常用的之类是 ByteBuffer 字节缓冲区。
Buffer中维护了4个重要的变量用来描述缓冲区的功能特性。
capacity: 缓冲区能够容纳的数据元素的最大数量。容量在缓冲区创建时被设定,不能被改变,不能为负数。
limit: 缓冲区的限制 是第一个不应该读取或写入的元素的索引。缓冲区的限制不能为负,并且不能大于其容量。
可以理解为Buffer 中可以读/写数据的边界。在写模式下,limit 代表最多能写入的数据,一般等于 capacity(可以通过limit(int newLimit)方法设置);
读模式下,limit 等于 Buffer 中实际写入的数据大小。position: 下一个要被读或写的元素的索引。position 会自动由相应的 get()和 put()函数更新。缓冲区的位置不能为负,并且不能大于其限制。
从写操作模式到读操作模式切换的时候(调用flip方法),position 都会归零,这样就可以从头开始读写了。mark: 标记,Buffer允许将位置直接定位到该标记处,这是一个可选属性;
JDK文档中说明
标记、位置、限制和容量值遵守以下关系:
0 <= 标记 <= 位置 <= 限制 <= 容量
Channel
Channel 通道只负责传输数据、不直接操作数据。操作数据都是通过 Buffer 缓冲区来进行操作!通常,通道可以分为两大类:
(FileChannel)文件通道和(SocketChannel)套接字通道。
BIO 中的流是单向的,分为各种 InputStream(输入流)和 OutputStream(输出流),数据只是在一个方向上传输(类似于通信信道的单工通信方式)。
通道与流的不同之处在于通道是双向的,它可以用于读、写或者同时用于读写(类似于通信信道的全双工通信方式)。
常用的Channel实现:
FileChannel: 用于文件 I/O 的通道,支持文件的读、写和追加操作。FileChannel 允许在文件的任意位置进行数据传输,
支持文件锁定以及内存映射(涉及零拷贝优化相关技术)文件等高级功能。FileChannel 无法设置为非阻塞模式,因此它只适用于阻塞式文件操作。SocketChannel: 用于 TCP 套接字 I/O 的通道。SocketChannel 支持非阻塞模式,可以与 Selector一起使用,实现高效的网络通信。
SocketChannel 允许连接到远程主机,进行数据传输。ServerSocketChannel: 用于监听 TCP 套接字连接的通道。与 SocketChannel 类似,ServerSocketChannel 也支持非阻塞模式,
并可以与 Selector 一起使用。ServerSocketChannel 负责监听新的连接请求,接收到连接请求后,可以创建一个新的 SocketChannel 以处理数据传输。DatagramChannel: 用于 UDP 套接字 I/O 的通道。DatagramChannel 支持非阻塞模式,可以发送和接收数据报包,适用于无连接的、不可靠的网络通信。
AsynchronousFileChannel: AsynchronousFileChannel 是 Java 7 引入的一个异步文件通道类,提供了对文件的异步读、写、打开和关闭等操作。
FileChannel的代码示例:
public static void main(String[] args) {
try (FileChannel sourceChannel = FileChannel.open(Paths.get("C:\\123.txt"), StandardOpenOption.READ);
FileChannel destinationChannel = FileChannel.open(Paths.get("C:\\123_copy.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
ByteBuffer buffer = ByteBuffer.allocate(8*1024);
// 如果还有数据就循环读取写入
while (sourceChannel.read(buffer) != -1) {
// 转换写模式
buffer.flip();
// 写入
destinationChannel.write(buffer);
// 写入后重置缓冲区
buffer.clear();
}
}catch (Exception e){
e.printStackTrace();
}
}
AsynchronousFileChannel的代码示例
public static void main(String[] args){
Path path = Paths.get("123.txt");
// 使用 Future 方式
try (AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
while (true) {
Future<Integer> result = fileChannel.read(buffer, position);
int bytesRead = result.get();
if (bytesRead <= 0) {
break;
}
position += bytesRead;
// 转换成读模式
buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.println(new String(data));
buffer.clear();
}
}catch (Exception e){
e.printStackTrace();
}
// 实现 CompletionHandler 接口方式
try (AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);){
ByteBuffer buffer = ByteBuffer.allocate(1024);
AtomicLong position = new AtomicLong(0);
CountDownLatch latch = new CountDownLatch(1);
fileChannel.read(buffer, position.get(), null, new CompletionHandler<Integer, Object>() {
@Override
public void completed(Integer bytesRead, Object attachment) {
if (bytesRead > 0) {
position.addAndGet(bytesRead);
buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
System.out.print(new String(data));
buffer.clear();
fileChannel.read(buffer, position.get(), attachment, this);
} else {
latch.countDown();
}
}
@Override
public void failed(Throwable exc, Object attachment) {
System.out.println("Error: " + exc.getMessage());
latch.countDown();
}
});
latch.await();
} catch (Exception e) {
e.printStackTrace();
}
}
Selector
Selector(选择器): 是基于事件驱动的 I/O 多路复用模型,它允许一个线程处理多个 Channel(这点非常重要)。
Selector 模型通过将 I/O 操作转化为事件驱动的方式,实现了高效的多路复用,来提高系统的并发处理能力和效率。
Selector的重要特性:
①、事件注册: 在 Selector 模型中,程序会向 Selector 对象注册感兴趣的事件,这些事件可以是连接Socket建立、读数据、写数据等。
也就是一个Selector 可以注册 多个Channel,我们只需要使用一个线程管理Selector就能够处理Selector 上的多个通道(Channel)。
NIO处理高并发的关键所在。②、事件监听: Selector 会不断地轮询注册在其上的通道(Channel),检查这些通道是否有已经就绪的事件发生。
③、事件处理: 当 Selector 发现某个通道上发生了感兴趣的事件时,它会通知程序,并且程序可以根据事件类型执行相应的操作。
例如,如果一个通道的数据可读,程序可以读取数据并进行处理;如果一个通道的数据可写,程序可以将数据写入通道。④、非阻塞式调用: 在 Selector 模型中,通常会使用非阻塞式调用(Non-blocking I/O),这意味着程序可以在等待事件发生时继续执行其他任务,
而不会被阻塞。⑤、多路复用: Selector 能够同时监听多个通道,当任何一个通道上发生感兴趣的事件时,Selector 都能及时地通知程序,因此能够有效地实现多路复用,
提高系统的并发处理能力。
SelectionKey
SelectionKey抽象类表示 Channel通道 在 Selector 中的注册信息以及与该通道相关联的状态和操作。
可以通过Selector 抽象类的 open方法 创建 Selector 实例:
try {
Selector selector = Selector.open();
} catch (IOException e) {
e.printStackTrace();
}
在一个Selector 实例中 有三种 SelectionKey 集合分别用来返回不同状态的 Channel通道信息。
分别对应Selector 中的三个方法:
- keys方法: 返回的 Set<SelectionKey> 代表了注册在该 Selector 上的 Channel
- selectedKeys方法: 返回的 Set<SelectionKey> 代表了所有可通过 select() 方法获取的、需要进行 IO 处理的 Channel
- 已取消键集: 是已被取消但其通道尚未注销的键的集合。不可直接访问此集合。在下一次执行 select() 方法时,
这些 Channel 对应的 SelectionKey 会被彻底删除
SelectionKey 中定义了四种事件注册类型:
public static final int OP_READ = 1;
public static final int OP_WRITE = 4;
public static final int OP_CONNECT = 8;
public static final int OP_ACCEPT = 16;
OP_READ(值为1):表示通道已经准备好进行读取操作。当通道中有数据可读时,将触发该事件。
OP_WRITE(值为4):表示通道已经准备好进行写入操作。当通道可写入数据时,将触发该事件。
OP_CONNECT(值为8):表示通道已经完成连接操作。该事件通常在客户端套接字进行连接时使用。
OP_ACCEPT(值为16):表示服务器套接字已经准备好接受新的连接。该事件通常在服务器端套接字接受新连接时使用。
这些常量可以在 SelectionKey 的 interestOps() 方法中使用,用于指定注册感兴趣的事件类型。当注册的事件发生时,将触发相应的操作。
Selector的简单代码示例:
通过 ServerSocketChannel 实现群聊功能
服务端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class TestA {
public static void main(String[] args) {
try {
// 创建ServerSocketChannel实例 接受客户端连接请求
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 设置为非阻塞模式
// 当调用 accept() 方法时,如果没有新的连接请求到达,该方法将立即返回null,而不是阻塞等待新的连接。
// 这样可以使服务器同时处理多个连接而不必为每个连接创建一个新的线程
serverSocketChannel.configureBlocking(false);
// 绑定 18848端口
serverSocketChannel.socket().bind(new InetSocketAddress(18848));
// 创建Selector实例
Selector selector = Selector.open();
// 注册ServerSocketChannel 到 Selector ,监听 OP_ACCEPT 事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 通过死循环持续监听 Selector 内通道的事件
while (true) {
// 阻塞地监听通道是否有事件发生。如果有通道已经准备好的事件,则 select() 方法会返回已经就绪的通道数
int readyCounts = selector.select();
// 如果没有就绪状态的通道就继续下一轮循环
if (readyCounts == 0) {
continue;
}
// 获取 selectedKeys 需要进行 IO 处理的 Channel
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
// 如果有就绪的通道 就循环处理
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// 处理连接事件
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
// 将客户端通道注册到 Selector 并监听 OP_READ 事件
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 处理读事件
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = client.read(buffer);
if (bytesRead > 0) {
buffer.flip();
System.out.println(new String(buffer.array(), 0, bytesRead));
// 将客户端通道注册到 Selector 并监听 OP_WRITE 事件
client.register(selector, SelectionKey.OP_WRITE);
} else if (bytesRead < 0) {
// 客户端断开连接
client.close();
}
} else if (key.isWritable()) {
// 处理写事件
SocketChannel client = (SocketChannel) key.channel();
// 接收到客户端的数据后 反馈给客户端 发送成功
ByteBuffer buffer = ByteBuffer.wrap("发送成功".getBytes());
client.write(buffer);
// 将客户端通道注册到 Selector 并监听 OP_READ 事件
client.register(selector, SelectionKey.OP_READ);
}
keyIterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端1
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
public class TestB {
private Selector selector;
private SocketChannel socketChannel;
private static final String HOST = "localhost";
private static final int PORT = 18848;
public TestB() {
try {
selector = Selector.open();
socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("连接到" + HOST + ":" + PORT + "群聊了");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
new Thread(() -> {
try {
while (true) {
if (selector.select() > 0) {
for (SelectionKey key : selector.selectedKeys()) {
selector.selectedKeys().remove(key);
if (key.isReadable()) {
readMessage();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
sendMessage("我是TestB");
String input;
while ((input = reader.readLine()) != null) {
sendMessage("TestB:"+input);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void sendMessage(String message) throws IOException {
if (message != null && !message.trim().isEmpty()) {
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
socketChannel.write(buffer);
}
}
private void readMessage() throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = socketChannel.read(buffer);
if (read > 0) {
buffer.flip();
String msg = new String(buffer.array(), 0, read);
System.out.println(msg);
}
}
public static void main(String[] args) {
new TestB().start();
}
}
客户端2
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
public class TestC {
private Selector selector;
private SocketChannel socketChannel;
private static final String HOST = "localhost";
private static final int PORT = 18848;
public TestC() {
try {
selector = Selector.open();
socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("连接到" + HOST + ":" + PORT + "群聊了");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
new Thread(() -> {
try {
while (true) {
if (selector.select() > 0) {
for (SelectionKey key : selector.selectedKeys()) {
selector.selectedKeys().remove(key);
if (key.isReadable()) {
readMessage();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
sendMessage("我是TestC");
String input;
while ((input = reader.readLine()) != null) {
sendMessage("TestC:"+input);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void sendMessage(String message) throws IOException {
if (message != null && !message.trim().isEmpty()) {
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
socketChannel.write(buffer);
}
}
private void readMessage() throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = socketChannel.read(buffer);
if (read > 0) {
buffer.flip();
String msg = new String(buffer.array(), 0, read);
System.out.println(msg);
}
}
public static void main(String[] args) {
new TestC().start();
}
}
java.nio.file.Files类
Files 类是 Java NIO(New I/O)包的一部分,提供了对文件操作的高级支持,包括对文件通道、文件锁等的操作。
相比于BIO的 File 类 支持更复杂和更高级的文件操作。 JDK1.7引入。
下面介绍一些常用的方法:
①、判断文件是否存在
// Path 为路径相关的接口抽象 ,
// LinkOption是一个枚举类型,用于指定在处理文件时如何处理符号链接(symbolic links)。
//符号链接是指向另一个文件或目录的特殊文件,类似于Unix系统中的软链接或Windows系统中的快捷方式。
// LinkOption提供了两个常量:
// NOFOLLOW_LINKS:表示在处理文件时不要跟踪符号链接。如果指定了此选项,在对文件进行操作时,将不会解析符号链接所指向的实际文件,而是直接操作符号链接本身。
// FOLLOW_LINKS:表示在处理文件时要跟踪符号链接。如果指定了此选项,在对文件进行操作时,会自动解析符号链接,然后操作符号链接所指向的实际文件。
public static boolean exists(Path path, LinkOption... options)
public static void main(String[] args) {
Path path = Paths.get("D:\\123.txt");
// 默认 FOLLOW_LINKS
boolean exists = Files.exists(path);
System.out.println(exists);
}
②、创建文件
// FileAttribute是文件属性或目录属性的抽象 常用实现有 PosixFileAttributes
// 例如可以使用 PosixFileAttributes 来创建具有特定权限的文件
public static Path createFile(Path path,
FileAttribute<?>... attrs)
throws IOException
public static void main(String[] args) {
Path path = Paths.get("D:\\1234.txt");
try {
Files.createFile(path);
} catch (IOException e) {
e.printStackTrace();
}
}
③、创建目录
public static void main(String[] args) {
Path path = Paths.get("D:\\12345");
try {
Files.createDirectory(path);
} catch (IOException e) {
e.printStackTrace();
}
}
④、删除文件
public static void main(String[] args) {
Path path = Paths.get("D:\\1234.txt");
try {
Files.delete(path);
} catch (IOException e) {
e.printStackTrace();
}
}
⑤、复制文件
public static void main(String[] args) {
Path source = Paths.get("D:\\123.txt");
Path target = Paths.get("D:\\1234.txt");
try {
// StandardCopyOption 有三个属性
// REPLACE_EXISTING:表示如果目标文件已经存在,则用源文件替换目标文件。
//COPY_ATTRIBUTES:表示在复制文件时也复制文件的属性。这些属性包括文件的元数据,例如文件权限、最后修改时间等。如果不使用此选项,目标文件将会获得系统默认的属性。
//ATOMIC_MOVE:表示使用原子性操作来移动文件。原子性操作是指在一个步骤内完成的操作,要么全部成功,要么全部失败,没有中间状态。这个选项通常用于将文件从一个位置原子性地移动到另一个位置,确保移动操作的完整性。
Files.copy(source,target, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
e.printStackTrace();
}
}
⑥、移动文件
public static void main(String[] args) {
Path source = Paths.get("D:\\123.txt");
Path target = Paths.get("D:\\1234.txt");
try {
Files.move(source,target, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
e.printStackTrace();
}
}
⑦、读取文本文件
public static void main(String[] args) {
Path path = Paths.get("D:\\1234.txt");
try {
List<String> list = Files.readAllLines(path, StandardCharsets.UTF_8);
list.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
}
⑧、写入文本文件
public static void main(String[] args) {
Path path = Paths.get("D:\\1234.txt");
try {
// 追加模式
// Files.write(path,list,StandardCharsets.UTF_8, StandardOpenOption.APPEND);
List<String> list = Arrays.asList("123", "456");
Files.write(path,list,StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
}
}
⑨、遍历
public class TestC {
public static void main(String[] args) {
Path path = Paths.get("D:\\");
try {
Files.walkFileTree(path,new MyFileVisitor());
} catch (IOException e) {
e.printStackTrace();
}
}
}
class MyFileVisitor extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
// 准备访问目录
return super.preVisitDirectory(dir, attrs);
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
// 访问文件
System.out.println(file);
return super.visitFile(file, attrs);
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
// 访问文件失败
return super.visitFileFailed(file, exc);
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
// 正在访问目录
System.out.println(dir);
return super.postVisitDirectory(dir, exc);
}
}
⑩、利用重写visitFile方法查找文件
public class TestC {
public static void main(String[] args) {
// 查找D盘有没有 123.txt 文件
Path path = Paths.get("D:\\");
try {
MyFileVisitor myFileVisitor = new MyFileVisitor("123.txt");
Files.walkFileTree(path,myFileVisitor);
System.out.println(myFileVisitor.searchFilePath);
} catch (IOException e) {
e.printStackTrace();
}
}
}
class MyFileVisitor extends SimpleFileVisitor<Path> {
private String searchFileName;
public Path searchFilePath;
public MyFileVisitor() {
}
public MyFileVisitor(String searchFileName) {
this.searchFileName = searchFileName;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String fileName = file.getFileName().toString();
if (fileName.equals(searchFileName)) {
searchFilePath = file;
// 如果找到了 就终止
return FileVisitResult.TERMINATE;
}
// 如果没找到继续查找
return FileVisitResult.CONTINUE;
}
}
# 6、Java NIO中的零拷贝优化支持
什么是零拷贝优化?
推荐看几篇博客大致了解下https://zhuanlan.zhihu.com/p/83398714 (opens new window)
https://blog.csdn.net/a745233700/article/details/122660332?spm=1001.2014.3001.5506 (opens new window)
零拷贝(Zero-copy)是一种计算机程序设计领域的优化技术,旨在减少数据在内存之间复制的次数,
从而提升系统性能,特别是对于大量数据的传输尤为重要。在传统的数据处理流程中,数据从一个位置(如磁盘)读取到操作系统内核缓冲区,
再从内核缓冲区复制到用户空间的应用程序缓冲区,然后在某些场景下(如网络传输),数据可能还需要从用户空间复制回内核空间的网络缓冲区以便发送。
零拷贝技术试图消除或最小化这些不必要的数据复制步骤,具体方法有以下几种:
用户空间直接访问(User-Space Direct Access): 允许应用程序直接访问内核管理的内存,例如通过内存映射(mmap)文件,
这样数据可以从磁盘直接加载到用户空间并用于网络传输,无需先复制到内核空间。写时复制(Copy-on-Write): 在数据实际被修改前,多个进程可以共享同一份数据的内存映射,只有当数据需要修改时才会创建数据的副本。
共享内存(Shared Memory): 多个进程可以直接访问同一块内存区域,避免数据在进程间复制。
DMA(Direct Memory Access : 在硬件级别实现零拷贝,允许外围设备(如网卡)直接读写内存,而不需要CPU介入数据搬运,常用于高速网络传输。
通过这些技术,零拷贝能够显著减少CPU使用率,降低内存带宽消耗,提升数据处理速度,尤其是在高负载的网络服务器、数据库和文件系统中效果明显。
JDK中对于零拷贝的支持
MappedByteBuffer 和 FileChannel 的transferTo()/transferFrom()方法
MappedByteBuffer的使用
MappedByteBuffer 用于表示一个内存映射文件,即将文件的一部分或全部映射到内存中,以便通过直接操作内存来实现对文件的读写。
这种方式可以提高文件 I/O 的性能,因为操作系统可以直接在内存和磁盘之间传输数据,无需通过 Java 应用程序进行额外的数据拷贝。
MappedByteBuffer 是 NIO 基于内存映射(mmap)这种零拷⻉⽅式的提供的⼀种实现,底层实际是调用了 Linux 内核的 mmap 系统调用。
它可以将一个文件或者文件的一部分映射到内存中,形成一个虚拟内存文件,这样就可以直接操作内存中的数据,而不需要通过系统调用来读写文件。
代码示例:
public static void main(String[] args) throws IOException {
try (FileChannel sourceChannel = FileChannel.open(Paths.get("D:\\123.txt"), StandardOpenOption.READ);
FileChannel destinationChannel = FileChannel.open(Paths.get("D:\\123.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.READ)) {
long fileSize = sourceChannel.size();
MappedByteBuffer sourceMappedBuffer = sourceChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
MappedByteBuffer destinationMappedBuffer = destinationChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileSize);
for (int i = 0; i < fileSize; i++) {
// 一个字节一个字节写
byte b = sourceMappedBuffer.get(i);
destinationMappedBuffer.put(i, b);
}
}
}
transferTo()和transferFrom()方法
transferTo()和transferFrom()是 NIO 基于发送文件(sendfile)这种零拷贝方式提供的一种实现,底层实际是调用了 Linux 内核的 sendfile系统调用。
它可以直接将文件数据从磁盘发送到网络,而不需要经过用户空间的缓冲区。
代码示例:
public static void main(String[] args) {
Path sourcePath = Paths.get("123.txt");
Path destinationPath = Paths.get("123_copy.txt");
try (FileChannel sourceChannel = FileChannel.open(sourcePath, StandardOpenOption.READ);
FileChannel destinationChannel = FileChannel.open(destinationPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
long position = 0;
long count = sourceChannel.size();
// 循环传输,直到所有字节都被传输
// 因为缓冲区大小限制、通道的限制、网络限制、文件系统限制、操作系统限制等因素可能会导致 transferTo 无法传输count 大小的数据
while (position < count) {
long transferred = sourceChannel.transferTo(position, count - position, destinationChannel);
position += transferred;
}
} catch (IOException e) {
e.printStackTrace();
}
}