NIO
Java中的NIO代表"New I/O",即新的I/O(输入/输出)系统。它是Java提供的一种用于处理I/O操作的API,相对于传统的I/O系统,NIO提供了更为灵活和高效的方式来进行I/O操作。
以下是Java中NIO的主要组成部分和特点:
-
通道(Channel):通道是NIO中的核心概念,它代表了一个连接到数据源或数据目标的对象,可以是文件、套接字(Socket)或其他I/O资源。通道可以进行读取和写入操作,并且支持非阻塞(non-blocking)模式,允许一个线程管理多个通道。
-
缓冲区(Buffer):缓冲区是用于在内存中存储数据的区域。NIO操作通常涉及到数据的读取和写入,这些数据都需要通过缓冲区进行传递。Java提供了不同类型的缓冲区,如ByteBuffer、CharBuffer、IntBuffer等,用于存储不同类型的数据。
-
选择器(Selector):选择器是用于多路复用的关键组件。它允许一个线程同时管理多个通道,监视它们是否准备好进行读取或写入操作。这使得在单个线程中处理多个通道变得高效,特别适用于网络编程,其中需要同时处理多个客户端连接。
-
非阻塞模式(Non-blocking Mode):NIO允许通道在非阻塞模式下工作。在非阻塞模式下,当没有数据可用时,通道的读取和写入操作不会立即阻塞线程,而是返回一个状态,允许线程继续执行其他任务,从而提高了应用程序的并发性能。
-
多路复用(Multiplexing):NIO通过选择器实现了多路复用,使得一个线程可以有效地管理多个通道,监听它们的事件状态,从而减少了线程的开销。
Java NIO主要用于开发需要高性能的网络应用程序,如Web服务器、聊天服务器、游戏服务器等,它可以处理大量并发连接而不会导致系统资源的枯竭。相比传统的阻塞I/O,NIO提供了更多的控制和性能优势,但它也需要更复杂的编程模型。
在Java中,BIO(Blocking I/O,阻塞式I/O)是一种传统的I/O模型,也被称为同步I/O。在BIO中,I/O操作会导致线程被阻塞,直到操作完成或者出现异常。这意味着每个I/O操作都需要分配一个独立的线程,因此在高并发环境下,BIO往往会导致线程资源的浪费和性能问题。
以下是Java中BIO的主要特点和组成部分:
-
阻塞模式:在BIO中,I/O操作是阻塞的,这意味着当程序试图读取或写入数据时,它会一直等待,直到操作完成或发生错误。这会导致程序的响应时间不稳定,尤其在高负载情况下容易出现性能瓶颈。
-
一线程一连接:在BIO模型中,通常需要为每个客户端连接创建一个独立的线程。这会导致线程资源的消耗,如果有大量并发连接,会导致线程耗尽和系统资源的浪费。
-
适用于低并发:BIO适用于低并发的应用场景,例如传统的客户端/服务器应用程序,其中并发连接数较少且响应时间不是关键问题。
尽管BIO模型简单易用,但它的性能限制使得它在高并发的网络应用程序中不太适用。为了解决这些性能问题,Java引入了NIO(New I/O)模型,它提供了非阻塞I/O和多路复用等特性,使得在高并发环境下能够更高效地处理多个连接。因此,在开发需要高性能和并发处理的网络应用程序时,通常会选择使用NIO而不是BIO。
特性 | NIO (New I/O) | BIO (Blocking I/O) |
---|---|---|
I/O模型 | 非阻塞式(Non-blocking) | 阻塞式(Blocking) |
多路复用 | 支持多路复用,一个线程管理多个通道 | 不支持多路复用,一个线程管理一个通道 |
缓冲区 | 使用缓冲区来读取和写入数据 | 通常没有明确的缓冲区概念,直接读写流 |
线程资源消耗 | 较少线程资源消耗,适合高并发环境 | 需要为每个连接分配一个独立线程,资源占用较高 |
非阻塞模式 | 支持非阻塞模式,I/O操作不会阻塞线程 | 阻塞模式,I/O操作会阻塞线程 |
适用性 | 适用于高并发环境,如网络服务器,聊天服务器,游戏服务器等 | 适用于低并发环境,如传统客户端/服务器应用程序 |
性能 | 提供更高的性能,减少了线程切换和资源浪费 | 性能较低,容易出现线程资源耗尽和性能瓶颈 |
编程复杂性 | 编程模型相对复杂,需要处理缓冲区和选择器等概念 | 编程模型相对简单,直接读写流即可 |
读取数据的区别
Java中的BIO(Blocking I/O)和NIO(Non-blocking I/O)在读取数据时有明显的区别:
BIO(Blocking I/O):
-
阻塞模式:BIO是阻塞I/O模式的代表。在使用BIO进行数据读取时,如果没有数据可用,读取操作会一直阻塞当前线程,直到数据到达或发生超时。
-
同步操作:BIO是同步的,这意味着读取操作会等待直到数据准备就绪,而在等待期间,线程会被阻塞,无法执行其他任务。
-
线程开销:每个客户端连接通常需要一个单独的线程来处理,这会导致服务器需要维护大量线程,占用较多的内存和系统资源,限制了服务器的并发性能。
NIO(Non-blocking I/O):
-
非阻塞模式:NIO是非阻塞I/O模式的代表。在使用NIO进行数据读取时,如果没有数据可用,读取操作不会阻塞当前线程,而是立即返回,允许线程继续处理其他任务。
-
异步操作:NIO是异步的,它允许注册事件和处理器,可以在数据准备就绪时通知。这使得一个线程可以同时管理多个连接,而不需要为每个连接分配一个线程。
-
选择器(Selector):NIO引入了选择器,允许一个线程同时管理多个通道,监视它们的可读、可写等事件状态。这提高了服务器的并发性能,减少了线程开销。
总的来说,BIO适用于低并发、连接数有限的场景,因为每个连接都需要一个独立的线程,会造成线程资源浪费。NIO适用于高并发、连接数较多的场景,因为它可以更高效地处理多个连接,减少线程开销。但是,NIO编程模型相对复杂,需要更多的代码来处理事件驱动和缓冲区管理,因此也更具挑战性。选择使用哪种I/O模型应根据具体的应用需求和性能考虑来决定。
组成
- 通道(Channel):通道是NIO中的核心概念之一。通道表示一个连接到输入源或输出目标的对象,可以是文件、套接字(Socket)或其他I/O资源。通道支持非阻塞模式,允许同时管理多个通道,从而提高了并发性。
- 缓冲区(Buffer):缓冲区是用于在内存中存储数据的区域。在NIO中,数据的读取和写入通常需要通过缓冲区来进行。Java提供了不同类型的缓冲区,如ByteBuffer、CharBuffer、IntBuffer等,用于存储不同类型的数据。
- 选择器(Selector):选择器是NIO的核心组件之一,它允许一个线程同时监视多个通道的事件状态。通过选择器,可以实现单线程处理多个通道的I/O操作,从而提高了系统的性能和效率。选择器用于多路复用,可以监控通道是否准备好读取或写入数据。
- 通道的非阻塞模式:通道可以设置为非阻塞模式,这意味着当没有数据可用时,通道的读取和写入操作不会阻塞线程,而是立即返回,让线程可以继续处理其他任务。
- 每个
channel
对应一个buffer
- 每个
selector
对应一个线程- 一个
selector
对应多个channel
selector
切换channel
是由事件event
决定的Buffer
底层就是一个数组Buffer
既可以读又可以写,但是需要通过flip()
方法切换
工作方式
在Java NIO中,Channel、Buffer和Selector是三个关键的组成部分
-
Channel(通道):通道是用于读取和写入数据的抽象概念,可以连接到不同的数据源或数据目标,如文件、套接字等。通道可以处于非阻塞模式,这意味着当没有数据可用时,通道的读取和写入操作不会阻塞当前线程,而是立即返回。不同类型的通道包括FileChannel、SocketChannel、ServerSocketChannel等。通道通过
read()
方法来读取数据到缓冲区,通过write()
方法将数据从缓冲区写入通道。 -
Buffer(缓冲区):缓冲区是用于在内存中存储数据的区域,通常用于在通道和应用程序之间传递数据。不同类型的数据需要不同类型的缓冲区,例如ByteBuffer用于字节数据,CharBuffer用于字符数据。缓冲区提供了读取数据和写入数据的方法,同时也跟踪了数据的位置、界限和容量等信息,以便有效地管理数据的传输。
-
Selector(选择器):选择器是NIO的核心组件之一,用于多路复用,允许一个线程同时管理多个通道。选择器可以监视多个通道的事件状态,如通道是否可读、可写等,并且只有在通道准备好执行相应的操作时才会唤醒线程。这样,一个线程可以高效地处理多个通道,减少了线程的开销,提高了应用程序的性能。选择器通过
select()
方法来检测就绪通道,然后可以使用通道进行读取或写入操作。
相互协作:
-
应用程序首先创建一个或多个通道,然后为每个通道分配一个缓冲区,通道和缓冲区之间建立了连接。
-
选择器被创建并用于监视这些通道。应用程序可以向选择器注册通道,以便选择器可以在通道准备好进行读取或写入操作时得到通知。
-
应用程序开始循环,调用选择器的
select()
方法,该方法会阻塞直到有通道准备好进行I/O操作。一旦某个通道准备好,select()
方法返回,并返回一个已准备好的通道集合。 -
应用程序迭代已准备好的通道集合,并使用通道的读取和写入操作来传输数据,同时使用缓冲区来存储数据。
-
此过程循环执行,允许应用程序高效地管理多个通道,而无需为每个通道创建一个线程。
Channel
流和Channel的对比:
-
阻塞 vs 非阻塞:
- 流(Stream):流是传统I/O模型的一部分,它是阻塞的。这意味着当你从流中读取数据或将数据写入流时,程序会被阻塞,直到操作完成或者出现异常。
- Channel:Channel是Java NIO的一部分,它支持非阻塞模式。在非阻塞模式下,当没有数据可用时,通道的读取和写入操作不会阻塞线程,而是立即返回,允许线程继续处理其他任务。
-
单向 vs 双向:
- 流(Stream):流通常是单向的,意味着一个流可以是输入流(用于读取数据)或输出流(用于写入数据),但不能同时用于读取和写入。
- Channel:通道是双向的,它可以用于读取和写入数据,同一个通道可以执行输入和输出操作。
-
多路复用:
- 流(Stream):流不支持多路复用,每个流通常需要一个单独的线程来处理。
- Channel:通道支持多路复用,一个线程可以管理多个通道,通过选择器(Selector)来监视它们的事件状态。
-
缓冲区:
- 流(Stream):流通常不涉及缓冲区,数据通过流直接传递。
- Channel:通常需要使用缓冲区来进行数据的读取和写入,通道与缓冲区协同工作,使得数据的传输更加灵活。
-
适用性:
- 流(Stream):适用于传统的I/O操作,如文件读写,文本处理等。在简单的I/O场景下使用较为方便。
- Channel:适用于需要高性能、高并发处理的网络应用程序,如Web服务器、聊天服务器、游戏服务器等,以及需要更灵活控制的文件I/O操作。
常见的实现类
Java NIO提供了多个不同类型的Channel实现类,每个实现类用于不同类型的I/O操作。以下是一些常见的Channel实现类以及它们的主要特点:
- FileChannel:
java.nio.channels.FileChannel
用于文件I/O操作。它可以读取和写入文件数据,支持文件的随机访问,以及文件锁定。FileChannel通常用于读写本地文件。 - SocketChannel:
java.nio.channels.SocketChannel
用于TCP协议的网络套接字通信。它可以连接到远程服务器并进行读取和写入操作。SocketChannel通常用于客户端和服务器之间的通信。 - ServerSocketChannel:
java.nio.channels.ServerSocketChannel
也用于网络套接字通信,但它是服务器端的套接字通道。它可以监听传入的客户端连接请求,并创建SocketChannel来处理客户端连接。 - DatagramChannel:
java.nio.channels.DatagramChannel
用于UDP协议的数据报套接字通信。它支持数据报的发送和接收,通常用于需要快速数据传输的应用,如实时多媒体流。
FileChannel
FileChannel对象实例无法直接创建,需要首先获得一个已经打开的文件或者创建一个新的文件,然后使用该文件创建一个FileChannel。
读文件
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class BIOFileReadChannel {
public static void main(String[] args) {
try (
RandomAccessFile randomAccessFile = new RandomAccessFile("D:\\blog\\1.txt", "rw");
FileChannel channel = randomAccessFile.getChannel()
) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
buffer.flip();
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
String s = new String(bytes);
System.out.println("s = " + s);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
flip
中文为翻转,读音为/flɪp/
remain
中文为剩余、保持,读音为rəˈmān
写文件
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class BIOFileChannel {
public static void main(String[] args) {
try (
RandomAccessFile randomAccessFile = new RandomAccessFile("D:\\blog\\1.txt", "rw");
FileChannel channel = randomAccessFile.getChannel()
) {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("hello, 世界".getBytes());
byteBuffer.flip();
int write = channel.write(byteBuffer);
System.out.println("write = " + write);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
使用一个Buffer进行读写
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class BIOFileReadWriteChannel {
public static void main(String[] args) {
try (
RandomAccessFile randomAccessFile = new RandomAccessFile("D:\\blog\\单机版\\前端\\package-lock.json", "rw");
RandomAccessFile randomAccessFile2 = new RandomAccessFile("D:\\blog\\单机版\\前端\\package-lock3.json", "rw");
FileChannel c1 = randomAccessFile.getChannel();
FileChannel c2 = randomAccessFile2.getChannel()
) {
ByteBuffer buffer = ByteBuffer.allocate(128);
while (c1.read(buffer) != -1) {
buffer.flip();
c2.write(buffer);
buffer.clear();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
复制文件
从一个通道传递到另一个通道
public static void main(String[] args) throws IOException {
RandomAccessFile r1 = new RandomAccessFile("D:\\Java IO\\PetStore-2021-12-02.zip", "rw");
RandomAccessFile r2 = new RandomAccessFile("D:\\Java IO\\PetStore3.zip", "rw");
FileChannel c1 = r1.getChannel();
FileChannel c2 = r2.getChannel();
try (r1; r2; c1; c2) {
// c1.transferTo(0L, c1.size(), c2);
c2.transferFrom(c1, 0, c1.size());
}
}
常用方法
FileChannel类提供了许多常用的方法,用于执行文件I/O操作。以下是一些常用的FileChannel方法:
-
读取数据:
-
int read(ByteBuffer dst)
:从通道读取数据到给定的ByteBuffer中,并返回读取的字节数。该方法会一直读取,直到数据完全填满缓冲区或达到文件末尾。 -
long read(ByteBuffer[] dsts)
:从通道读取数据到多个ByteBuffer数组中,并返回读取的总字节数。它允许你一次读取多个缓冲区。
-
-
写入数据:
-
int write(ByteBuffer src)
:将ByteBuffer中的数据写入通道,并返回写入的字节数。该方法会一直写入,直到缓冲区中的数据全部写入或达到文件末尾。 -
long write(ByteBuffer[] srcs)
:将多个ByteBuffer数组中的数据写入通道,并返回写入的总字节数。可以一次写入多个缓冲区。
-
-
文件位置操作:
-
long position()
:获取当前文件的位置,即下一个读写操作将从该位置开始。 -
FileChannel position(long newPosition)
:设置文件的位置,使下一个读写操作从指定位置开始。
-
-
文件截取:
FileChannel truncate(long size)
:将文件截取为指定大小。如果文件大于指定大小,将会截取文件;如果文件小于指定大小,将会扩展文件并用零字节填充。
-
强制刷新到磁盘:
void force(boolean metaData)
:将通道的内容强制刷新到磁盘上的文件。如果metaData参数为true,还会强制刷新文件的元数据信息(如文件的修改时间)。
-
关闭通道:
void close()
:关闭FileChannel,释放相关资源。一旦通道关闭,就不能再进行读写操作。
ServerSocketChannel
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.channels.ServerSocketChannel;
public class Main {
public static void main(String[] args) throws IOException {
ServerSocketChannel channel = ServerSocketChannel.open();
// 设置为非阻塞模式
channel.configureBlocking(false);
try (channel) {
ServerSocket socket = channel.socket();
socket.bind(new InetSocketAddress(8888));
while (true) {
Socket accept = socket.accept();
if (accept != null) {
System.out.println("accept = " + accept);
accept.getOutputStream().write("""
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
<html>
<head></head>
<body>
<h1>你好,世界</h1>
</body>
</html>
""".getBytes());
}
}
}
}
}
BIO - Socket
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BIOChat {
public static void main(String[] args) {
ExecutorService pool = Executors.newCachedThreadPool();
try (ServerSocket serverSocket = new ServerSocket(6666)) {
while (true) {
Socket socket = serverSocket.accept();
if (socket != null) {
pool.submit(() -> handle(socket));
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void handle(Socket socket) {
String name = "线程名称:" + Thread.currentThread().getName();
System.out.println(name);
byte[] bytes = new byte[1024];
try (socket) {
while (true) {
int read = socket.getInputStream().read(bytes);
if (read == -1) {
System.out.println(name + ", 退出");
break;
}
System.out.println("------");
String s = new String(bytes, 0, read);
System.out.println(name + ", s = " + s);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
class Send {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
CountDownLatch countDownLatch = new CountDownLatch(10);
try {
for (int i = 0; i < 10; i++) {
pool.submit(() -> {
final Socket socket = new Socket();
try (socket) {
socket.connect(new InetSocketAddress("localhost", 6666));
Thread.sleep(10000);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(UUID.randomUUID().toString().getBytes());
outputStream.flush();
outputStream.write("66666".getBytes());
outputStream.flush();
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
pool.shutdown();
}
}
- 以上代码中,
Socket
的accept()
、read()
将会被阻塞
Buffer
在Java NIO(New I/O)中,Buffer是一个重要的概念,用于缓存数据的临时存储和传输。它是NIO操作的核心之一,通常用于读取和写入数据,如文件、网络通信等。Buffer提供了对数据的有序、可追踪的操作,有助于提高I/O操作的性能和效率。
以下是关于Buffer的主要特点和概念:
-
容量(Capacity):Buffer有一个固定的容量,它表示可以存储的最大数据量。一旦分配了Buffer的容量,它不能更改。
-
位置(Position):Buffer中的位置表示下一个要读取或写入的数据元素的索引。初始时,位置为0,当读取或写入数据时,位置会自动移动。
-
限制(Limit):限制是一个指示在Buffer中有效数据结束的索引,它通常小于或等于容量。在写入数据时,限制用于控制写入的数据量;在读取数据时,限制用于限制读取的数据范围。
-
标记(Mark):标记是一个可以通过
mark()
方法设置的索引,它允许你记录某个特定位置,然后稍后通过reset()
方法将位置重置为标记的位置。 -
清空(Clear):在将Buffer从写模式切换到读模式时,可以使用
clear()
方法来清除限制和位置,使Buffer可以重新用于读取操作。数据仍然存在于Buffer中,但位置被设置为0,限制被设置为容量。 -
翻转(Flip):在将Buffer从读模式切换到写模式时,可以使用
flip()
方法来翻转Buffer。这将限制设置为当前位置,位置设置为0,以准备开始写入操作。 -
倒带(Rewind):
rewind()
方法将位置设置为0,但保留限制的值,使得Buffer可以重新读取之前的数据。 -
压缩(Compact):
compact()
方法用于将未读取的数据复制到Buffer的起始位置,以便继续写入数据,同时更新位置和限制。
Buffer类的具体实现有很多,例如ByteBuffer
、CharBuffer
、IntBuffer
等,每种Buffer都适用于不同的数据类型。
基本用法:
import java.nio.IntBuffer;
public class BIOBuffer {
public static void main(String[] args) {
IntBuffer buffer = IntBuffer.allocate(10);
for (int i = 0; i < buffer.capacity(); i++) {
buffer.put(i * 2);
}
buffer.flip();
while (buffer.hasRemaining()) {
System.out.println(buffer.get());
}
}
}
四大重要属性
在Java NIO中,Buffer类具有四个主要属性,这些属性用于管理缓冲区中的数据和位置。这些属性是:
-
容量(Capacity):容量表示Buffer的最大数据存储能力,即可以存放多少字节或元素。容量是在创建Buffer时确定的,一旦分配,就不能更改。容量决定了Buffer可以存储的数据的上限。
-
位置(Position):位置表示当前读取或写入操作的位置索引。初始时,位置为0,当你读取或写入数据时,位置会自动移动。读取数据会增加位置,写入数据也会增加位置。位置不能超过容量,否则会抛出异常,调用
flip()
后,position
将会回到0的位置。 -
限制(Limit):限制是一个指示在Buffer中有效数据结束的索引,它通常小于或等于容量。在写入数据时,限制用于控制写入的数据量;在读取数据时,限制用于限制读取的数据范围。限制的初始值通常等于容量,可以通过
limit()
方法来设置。 -
标记(Mark):标记是一个临时索引,你可以使用
mark()
方法设置标记,然后稍后通过reset()
方法将位置重置为标记的位置。标记用于在某个位置之后进行读取操作后返回到之前的位置。
这些属性一起协同工作,以确保Buffer的正确读写操作。通常的读写流程包括移动位置、读取或写入数据,并根据需要设置和使用标记和限制。
以下是这些属性在Buffer中的典型使用方式:
-
容量(Capacity):通常通过
capacity()
方法获取容量,表示Buffer的最大存储能力。 -
位置(Position):通过
position()
方法获取当前位置,通过position(int newPosition)
方法设置位置。 -
限制(Limit):通过
limit()
方法获取当前限制,通过limit(int newLimit)
方法设置限制,每次读写数据都会到limit
结束 -
标记(Mark):通过
mark()
方法设置标记,通过reset()
方法将位置重置为标记的位置。- 在
Buffer
类内部有一个成员变量mark
,默认值-1
,每调用一次mark()
方法,会把position
的值记录下来 - 每调用一次
reset()
方法,则会将position
的值设置为mark
的值
- 在
public Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
- 会清除标记、将
limit
设置为初始的容量、重置position
的位置
public Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
- 会清除标记、将
limit
设置为position
、重置position
的位置
public Buffer rewind() {
position = 0;
mark = -1;
return this;
}
- 会清除标记、重置
position
的位置
Buffer转换
只读的buffer
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
MappedByteBuffer
MappedByteBuffer
是 Java NIO 中的一种特殊类型的 ByteBuffer
,用于**将文件的一部分或整个文件映射到内存中,**在内存中改变了文件内容,将会实时的修改磁盘上的文件 】。它主要用于处理大文件,可以将文件内容映射到内存,从而允许直接在内存中读写文件数据,而无需频繁地进行磁盘 I/O 操作。
以下是关于 MappedByteBuffer
的主要特点和用法:
-
文件映射:
MappedByteBuffer
允许将文件的一部分或整个文件映射到内存中。这样,文件的内容会被映射到一个ByteBuffer
对象中,你可以直接在内存中对这个ByteBuffer
进行读写操作。 -
内存映射文件:这种映射方式称为内存映射文件(Memory-Mapped File),它将文件的内容和内存中的
ByteBuffer
关联起来。任何对ByteBuffer
的操作都会反映在底层文件中,这使得读写文件变得非常高效。 -
文件通道:通常,你会使用
FileChannel
对象来创建MappedByteBuffer
,然后通过FileChannel
的map()
方法将文件映射到内存中。这需要先打开文件并获取其通道。 -
直接内存:
MappedByteBuffer
通常使用直接内存(Direct Memory)来存储映射的文件数据,这意味着数据不会占用堆内存,而是存在于 JVM 外部的直接内存区域。 -
缓存同步:在对映射的文件进行写入操作后,你可以通过强制刷新缓冲区或等待缓冲区自动同步到文件中,以确保数据被持久化到磁盘。
-
适用场景:
MappedByteBuffer
适用于需要处理大型文件的场景,如日志文件、数据库文件、大型图像文件等。它可以提供高效的随机访问和修改文件数据的能力。
以下是一个简单的示例,演示了如何创建和使用 MappedByteBuffer
:
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class BIOMappedBuffer {
public static void main(String[] args) {
try (
RandomAccessFile randomAccessFile = new RandomAccessFile("D:\\blog\\单机版\\1.txt", "rw");
FileChannel channel = randomAccessFile.getChannel();
) {
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, channel.size());
buffer.put(0, (byte) 'h');
buffer.put(1, (byte) 'E');
buffer.put(2, (byte) 'L');
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
总之,MappedByteBuffer
是一种用于将文件映射到内存中的高效方式,可以加速对大文件的读写操作。然而,它需要谨慎使用,特别是在处理大文件时,以确保文件映射的正确管理和资源释放。
分散和聚集
Buffer 的分散(Scattering)和聚集(Gathering)是 Java NIO 中用于进行数据传输的一种高级特性,通常用于同时读取或写入多个缓冲区的数据。这两种操作使得可以更有效地处理复杂的数据结构,如协议头和数据体分开存储的情况。
分散(Scattering):
分散操作是指将一个通道中的数据按顺序读取到多个缓冲区中。通常,这在处理数据报文或消息时非常有用,其中数据的不同部分需要存储在不同的缓冲区中。
使用 ScatteringByteChannel
接口(如 FileChannel
)和 read()
方法来实现分散操作。read()
方法会将数据按照给定的顺序填充到一组缓冲区中,每个缓冲区接收的数据量由其容量决定。当一个缓冲区被填满后,数据会继续写入下一个缓冲区,直到所有缓冲区都被填充或数据传输完成。
ByteBuffer buffer1 = ByteBuffer.allocate(256);
ByteBuffer buffer2 = ByteBuffer.allocate(128);
ByteBuffer[] buffers = { buffer1, buffer2 };
channel.read(buffers); // 将数据按顺序分散到缓冲区
聚集(Gathering):
聚集操作是指将多个缓冲区中的数据按顺序写入到一个通道中。通常,这在构建数据报文或消息时非常有用,其中数据的不同部分存储在不同的缓冲区中。
使用 GatheringByteChannel
接口(如 FileChannel
)和 write()
方法来实现聚集操作。write()
方法会按照给定的顺序从一组缓冲区中读取数据,并将其写入通道。与分散操作类似,数据会按顺序从一个缓冲区读取,然后继续下一个缓冲区,直到所有缓冲区的数据都被写入通道或数据传输完成。
ByteBuffer buffer1 = ByteBuffer.allocate(256);
ByteBuffer buffer2 = ByteBuffer.allocate(128);
ByteBuffer[] buffers = { buffer1, buffer2 };
channel.write(buffers); // 将数据按顺序聚集写入通道
读取、写入文件:
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;
public class BIO {
public static void main(String[] args) {
ByteBuffer[] byteBuffers = new ByteBuffer[10];
for (int i = 0; i < 10; i++) {
byteBuffers[i] = ByteBuffer.allocate(128);
}
try (RandomAccessFile r1 = new RandomAccessFile("D:\\temp\\Java\\abc\\1.pdf", "rw");
RandomAccessFile r2 = new RandomAccessFile("D:\\temp\\Java\\abc\\2.pdf", "rw");
FileChannel c1 = r1.getChannel();
FileChannel c2 = r2.getChannel()
) {
while (c1.read(byteBuffers) != -1) {
Arrays.stream(byteBuffers).forEach(ByteBuffer::flip);
c2.write(byteBuffers);
Arrays.stream(byteBuffers).forEach(ByteBuffer::clear);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Selector
Selector
是一个重要的多路复用器(Multiplexer),用于监控多个通道的状态,使得一个单独的线程可以管理多个通道的 I/O 操作。Selector
可以帮助你编写高性能的网络服务器或客户端,特别适用于处理大量并发连接。
以下是关于 Selector
的主要特点和用法:
- 多路复用:
Selector
允许一个线程同时管理多个通道,监视它们的状态变化(如可读、可写、连接就绪等)。这减少了线程数量,降低了系统资源开销,使得处理大规模并发连接变得更加高效。 - 通道注册:通过将通道注册到
Selector
上,可以告诉Selector
哪些通道应该由它来管理。通道可以是SelectableChannel
的子类,如SocketChannel
、ServerSocketChannel
、FileChannel
等。 - 事件监听:一旦某个通道上发生了感兴趣的事件(如可读、可写等),
Selector
会通知你的应用程序,从而触发相应的操作。 - 非阻塞模式:与传统的阻塞式 I/O 不同,
Selector
配合非阻塞式通道,使得通道可以在没有数据可读写时立即返回,不会阻塞线程。 - 事件集合:
Selector
使用SelectionKey
对象来表示感兴趣的事件和关联的通道。通过SelectionKey
,你可以获取事件类型、通道等信息。 - 轮询机制:
Selector
使用轮询(polling)机制,不断检查注册的通道是否有感兴趣的事件发生,一旦事件发生,就通知应用程序。
例子:用selector
管理socket
服务端
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 NIOSelector {
public static void main(String[] args) {
try (
ServerSocketChannel channel = ServerSocketChannel.open();
Selector selector = Selector.open()
) {
channel.configureBlocking(false);
channel.bind(new InetSocketAddress(6666));
channel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
System.out.println("selectionKey = " + selectionKey);
if (selectionKey.isAcceptable()) {
System.out.println("接收到了连接...");
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
System.out.println("接收到了消息...");
try (SocketChannel socketChannel = (SocketChannel) selectionKey.channel()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer);
buffer.flip();
String s = new String(buffer.array(), 0, buffer.limit());
System.out.println("s = " + s);
}
}
iterator.remove();
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
接收端:
class NIOSocketChannel {
public static void main(String[] args) {
try (SocketChannel socketChannel = SocketChannel.open()) {
socketChannel.configureBlocking(false);
if (!socketChannel.connect(new InetSocketAddress("127.0.0.1", 6666))) {
while (!socketChannel.finishConnect()) {
System.out.println("没有完成连接");
}
}
socketChannel.write(ByteBuffer.wrap("hello, world".getBytes()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
服务器端 (NIOSelector
类):
-
首先,服务器端创建了一个
ServerSocketChannel
和一个Selector
。ServerSocketChannel
用于监听客户端连接,而Selector
用于管理多个通道的事件。 -
设置
ServerSocketChannel
为非阻塞模式,以确保不会阻塞在accept
操作上。 -
绑定服务器端口 6666,并将
ServerSocketChannel
注册到Selector
上,监听OP_ACCEPT
事件,表示接受客户端连接。 -
进入无限循环,使用
selector.select()
阻塞等待事件发生。当有事件发生时,会返回一个SelectionKey
集合,其中包含了触发事件的通道。 -
遍历
SelectionKey
集合,针对每个事件进行处理:- 如果事件是
OP_ACCEPT
,表示有客户端连接请求,会接受连接并将客户端的SocketChannel
注册到Selector
上,监听OP_READ
事件,以后可以读取客户端发送的数据。 - 如果事件是
OP_READ
,表示有数据可读,服务器会读取客户端发送的数据并在控制台上输出。
- 如果事件是
-
使用
iterator.remove()
移除已经处理过的SelectionKey
。
客户端 (NIOSocketChannel
类):
-
客户端创建了一个
SocketChannel
对象,并设置为非阻塞模式。 -
尝试连接到服务器的地址(127.0.0.1:6666),如果连接没有立即完成,就进入循环等待连接完成。
-
一旦连接完成,客户端会通过
socketChannel.write()
方法发送 “hello, world” 消息给服务器。
常用方法
SelectionKey
是 Java NIO 中用于表示通道和事件之间关联的对象,它包含了通道的状态和关联的事件。以下是 SelectionKey
常见的方法和用法:
-
channel()
方法:channel()
方法返回与此键关联的通道,可以通过它获取操作通道的引用。
SelectableChannel channel = selectionKey.channel();
-
selector()
方法:selector()
方法返回与此键关联的选择器。
Selector selector = selectionKey.selector();
-
interestOps()
和interestOps(int ops)
方法:interestOps()
方法返回键的兴趣操作集合,即该键所关注的事件。interestOps(int ops)
方法用于设置键的兴趣操作集合,修改关注的事件。
int ops = selectionKey.interestOps(); // 获取关注的事件 selectionKey.interestOps(SelectionKey.OP_READ); // 设置关注的事件
-
readyOps()
方法:readyOps()
方法返回键的就绪操作集合,即当前通道上已经准备好的事件。
int ops = selectionKey.readyOps();
-
isValid()
方法:isValid()
方法用于检查键是否有效。如果键已经被取消或关闭,则返回false
,否则返回true
。
boolean valid = selectionKey.isValid();
-
cancel()
方法:cancel()
方法用于取消键的注册,将其从选择器中移除。这通常在通道关闭时使用。
selectionKey.cancel();
-
attach(Object obj)
和attachment()
方法:attach(Object obj)
方法用于将一个对象附加到键上,以便在需要时存储与键相关的信息。attachment()
方法用于获取附加到键上的对象。
selectionKey.attach(myObject); // 附加一个对象到键上 Object attachedObj = selectionKey.attachment(); // 获取附加的对象
-
事件操作方法:
isAcceptable()
、isConnectable()
、isReadable()
、isWritable()
等方法用于检查键是否对应于特定类型的事件。这些方法通常在处理就绪事件时使用。
if (selectionKey.isReadable()) { // 处理可读事件 }
群聊
服务器:
package org.example;
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 NIOChatServer {
public Selector selector;
public ServerSocketChannel channel;
public void run() throws IOException {
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("客户端连接了," + serverSocketChannel);
socketChannel.write(ByteBuffer.wrap("服务器收到你的连接了".getBytes()));
} else if (key.isReadable()) {
read(key, selector.keys());
}
}
}
}
private void read(SelectionKey key, Set<SelectionKey> keys) throws IOException {
try {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
channel.read(byteBuffer);
byteBuffer.flip();
String message = new String(byteBuffer.array(), 0, byteBuffer.limit());
System.out.println("客户端发消息了:" + message);
forward(keys, channel, message);
} catch (IOException e) {
key.cancel();
System.out.println("客户端下线了....");
}
}
private void forward(Set<SelectionKey> keys, SocketChannel current, String message) {
Iterator<SelectionKey> iterator = keys.iterator();
ByteBuffer buffer = ByteBuffer.wrap(("转发的消息-" + message).getBytes());
while (iterator.hasNext()) {
try {
SelectionKey item = iterator.next();
SelectableChannel channel = item.channel();
if (channel instanceof SocketChannel socketChannel && socketChannel != current) {
socketChannel.write(buffer);
}
} catch (IOException e) {
System.out.println("发送失败,疑似下线:" + channel);
iterator.remove();
e.printStackTrace();
}
}
}
public NIOChatServer() throws IOException {
this.selector = Selector.open();
this.channel = ServerSocketChannel.open();
this.channel.bind(new InetSocketAddress(6666));
this.channel.configureBlocking(false);
this.channel.register(this.selector, SelectionKey.OP_ACCEPT);
}
public static void main(String[] args) throws IOException {
NIOChatServer server = new NIOChatServer();
server.run();
}
}
-
在
main
方法中,首先创建了一个NIOChatServer
实例,该实例初始化了一个Selector
和一个ServerSocketChannel
,然后将服务器绑定到端口 6666,并将ServerSocketChannel
注册到Selector
上,监听连接事件 (OP_ACCEPT
)。 -
在
run
方法中,服务器进入一个无限循环,等待事件发生。当有事件发生时,服务器使用selector.select()
阻塞等待事件,一旦有事件就绪,它会遍历SelectionKey
集合来处理事件。 -
在事件处理部分:
- 如果事件是
OP_ACCEPT
,表示有客户端连接请求,服务器会接受连接并将客户端的SocketChannel
注册到Selector
上,监听读事件 (OP_READ
)。然后,服务器向客户端发送一条消息以确认连接。 - 如果事件是
OP_READ
,表示有数据可读,服务器会读取客户端发送的消息,然后通过forward
方法将消息转发给所有其他连接到服务器的客户端。
- 如果事件是
-
read
方法用于读取客户端发送的消息,它首先将消息读入一个ByteBuffer
中,然后将其转换为字符串并处理。 -
forward
方法遍历所有注册在Selector
上的SocketChannel
,并将收到的消息转发给除当前客户端之外的所有其他客户端。这个方法通过ByteBuffer
来发送消息。
客户端:
package org.example;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
public class NIOChatClient {
public SocketChannel socketChannel;
public Selector selector;
public void run() throws IOException {
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isReadable()) {
read(key);
}
iterator.remove();
}
}
}
private void read(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
buffer.flip();
String message = new String(buffer.array(), 0, buffer.limit());
System.out.println("收到消息了,消息内容:" + message);
}
public NIOChatClient() throws IOException {
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_READ);
socketChannel.connect(new InetSocketAddress(6666));
while (!socketChannel.finishConnect()) {
System.out.println("正在连接服务器");
}
socketChannel.write(ByteBuffer.wrap("你好,服务器".getBytes()));
}
public static void main(String[] args) throws IOException {
final NIOChatClient client = new NIOChatClient();
final Scanner input = new Scanner(System.in);
new Thread(() -> {
while (input.hasNext()) {
String s = input.nextLine();
try {
client.socketChannel.write(ByteBuffer.wrap(s.getBytes()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
client.run();
}
}
-
在
main
方法中,首先创建了一个NIOChatClient
实例,该实例初始化了一个非阻塞的SocketChannel
和一个Selector
,然后将SocketChannel
注册到Selector
上,关注读事件 (OP_READ
)。 -
在构造函数中,客户端会尝试连接到服务器,如果连接还没有完成 (
finishConnect()
返回false
),它会不断尝试连接,直到连接完成。一旦连接完成,客户端向服务器发送一条 “你好,服务器” 的欢迎消息。 -
在
run
方法中,客户端进入一个无限循环,等待事件发生。当有事件发生时,客户端使用selector.select()
阻塞等待事件,一旦有事件就绪,它会遍历SelectionKey
集合来处理事件。 -
在事件处理部分:
- 如果事件是
OP_READ
,表示有数据可读,客户端会读取服务器发送的消息,并将其显示在控制台上。
- 如果事件是
-
客户端使用
Scanner
来接收用户输入的消息,并通过一个单独的线程将用户输入的消息发送给服务器。
Q.E.D.