I/O 模型可以简单理解为决定了数据如何在程序和外部环境(例如磁盘、网络等)之间进行发送和接收的方式,从而直接影响了程序通信的性能。不同的 I/O 模型可以以不同的方式管理数据的传输和处理,这些方式可以在性能、并发性和可维护性等方面产生重要影响。
BIO 是传统的阻塞式 I/O 模型,它的特点是当一个线程执行一个 I/O 操作时,它会被阻塞,直到操作完成。这种模型在编程上比较简单,但在高并发环境下性能较差,因为每个连接都需要一个独立的线程,会导致资源消耗过大。
NIO 是新的非阻塞 I/O 模型,它使用了 Java NIO 包中的通道(Channel)和缓冲区(Buffer)来实现。NIO 支持多路复用技术,允许一个线程监视多个通道的状态,从而更高效地处理多个连接。NIO 在高并发环境中表现更出色,但编程复杂度相对较高。
AIO 是异步 I/O 模型,它通过 Java NIO 2 中的异步通道(AsynchronousChannel)来实现。AIO 允许程序发起 I/O 操作后继续执行其他任务,而不需要轮询操作状态,当操作完成时会通知程序。这种模型适用于需要高度并发和异步操作的场景,但编程复杂性也相对较高。
1, BIO(Blocking I/O):
2, NIO(Non-blocking I/O):
3, AIO(Asynchronous I/O):
Java BIO(Blocking I/O,阻塞式I/O)是Java传统的I/O模型,它的工作方式基于阻塞操作。在Java BIO中,I/O操作会导致程序被阻塞,直到操作完成。以下是关于Java BIO的基本介绍:
阻塞模型:在Java BIO中,当程序执行I/O操作时,它会被阻塞,直到操作完成。这意味着程序无法执行其他任务,直到I/O操作完成或发生超时。
同步操作:BIO的I/O操作是同步的,这意味着程序在发出I/O请求后需要等待直到数据准备就绪或写入完成。这通常涉及到使用Java的输入流和输出流来进行读取和写入操作。
适用性:BIO适用于相对简单的I/O操作,特别是在单线程或低并发环境中。例如,它可以用于文件I/O操作,网络通信,或者处理少量并发连接的服务器。
资源消耗:BIO的一个主要问题是资源消耗。每个连接都需要一个独立的线程,这在高并发情况下会导致线程资源的浪费和性能下降。大量线程的创建和管理也可能导致系统开销增加。
ServerSocket serverSocket = new ServerSocket(port);
Socket clientSocket = serverSocket.accept();
InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream();
byte[] buffer = new byte[1024];
int bytesRead = inputStream.read(buffer);
outputStream.write(buffer, 0, bytesRead);
inputStream.close();
outputStream.close();
clientSocket.close();
serverSocket.close();
这是一个简单的Java BIO编程流程的示例,它展示了如何创建服务器、等待客户端连接、进行数据传输,最后关闭连接。需要注意的是,该示例是单线程的,对于多个客户端连接,你需要创建多个线程来处理每个连接,这也是BIO模型在高并发环境下性能较差的原因之一。
创建了一个服务器,监听6666端口,并使用线程池处理多个客户端连接。每当有客户端连接时,会创建一个新的ClientHandler线程来处理连接。这种方式可以同时处理多个客户端连接,提高了服务器的并发性能。
public class BioTest {
public static void main(String[] args) {
final int port = 6666;
final int poolSize = 10; // 线程池大小
// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(poolSize);
try {
// 创建ServerSocket并绑定到指定端口
ServerSocket serverSocket = new ServerSocket(port);
System.out.println("Server listening on port " + port);
while (true) {
// 等待客户端连接请求,当有连接请求时,accept方法返回一个Socket对象
Socket clientSocket = serverSocket.accept();
System.out.println("Accepted connection from " + clientSocket.getRemoteSocketAddress());
// 使用线程池处理客户端连接
executorService.execute(new ClientHandler(clientSocket));
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 客户端处理线程
static class ClientHandler implements Runnable {
private Socket clientSocket;
public ClientHandler(Socket clientSocket) {
this.clientSocket = clientSocket;
}
@Override
public void run() {
try {
// 获取输入流和输出流
InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream();
// 读取客户端发送的数据
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
// 将数据原样写回客户端
outputStream.write(buffer, 0, bytesRead);
System.out.println("get message:"+new String(buffer, 0, bytesRead
));
}
// 关闭连接
clientSocket.close();
System.out.println("Connection closed");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
telnet 127.0.0.1 6666
ctrl+]
send hello
Java NIO(New I/O,新I/O)是Java编程语言中用于非阻塞I/O操作的一组API。它提供了一种更灵活、高效的I/O处理方式,适用于高并发和高性能的网络通信和文件操作。以下是Java NIO的基本介绍:
非阻塞模型:Java NIO采用了非阻塞I/O模型,与传统的阻塞I/O(BIO)不同,它不会导致线程在I/O操作中阻塞,而是可以继续执行其他任务,从而提高并发性能。
通道和缓冲区:NIO引入了通道(Channel)和缓冲区(Buffer)的概念。通道是数据的源或目标,缓冲区用于在通道和应用程序之间传输数据。通道可以是文件、套接字、管道等。
选择器(Selector):Java NIO还引入了选择器,用于多路复用I/O操作。选择器可以同时管理多个通道,监测它们的状态,一旦有通道准备好执行读或写操作,选择器就会通知应用程序进行处理,而不需要轮询。
多线程:Java NIO适用于多线程环境。多个线程可以同时监听和处理多个通道,从而实现高并发的I/O操作。
适用性:Java NIO适用于需要高并发、高性能的网络通信应用,例如Web服务器、代理服务器、聊天服务器等。它还适用于需要高效文件操作的应用。
高性能:由于非阻塞模型和多路复用技术,Java NIO通常具有较高的性能,可以处理大量并发连接。
编程复杂性:相对于传统的BIO,Java NIO的编程复杂性较高,需要更多的编码工作。但它提供了更灵活的控制和更高的性能。
关系图的说明:
1,每个channel 都会对应一个Buffer
2,Selector 对应一个线程, 一个线程对应多个channel(连接)
3,该图反应了有三个channel 注册到 该selector //程序
4,程序切换到哪个channel 是有事件决定的, Event 就是一个重要的概念
5,Selector 会根据不同的事件,在各个通道上切换
6,Buffer 就是一个内存块 , 底层是有一个数组
7,数据的读取写入是通过Buffer, 这个和BIO , BIO 中要么是输入流,或者是输出流, 不能双向,但是NIO的Buffer 是可以读也可以写, 需要 flip 方法切换
8,channel 是双向的, 可以返回底层操作系统的情况, 比如Linux , 底层的操作系统通道就是双向的.
缓冲区(Buffer)是Java NIO 中的核心组件之一,用于在通道(Channel)和应用程序之间传输数据。缓冲区是一个固定大小的内存区域,它可以保存各种类型的数据,如字节、字符、整数等。缓冲区具有一些重要的属性和方法,用于管理数据的读取和写入。
Java NIO 提供了不同类型的缓冲区,每种类型适用于不同数据类型的读写。常见的缓冲区类型包括:
ByteBuffer buffer = ByteBuffer.allocate(1024); // 创建一个1KB的字节缓冲区
缓冲区有一些重要属性,包括容量(capacity)、位置(position)、限制(limit)和标记(mark)。
缓冲区提供了一系列读取和写入数据的方法,通常包括put()用于写入数据,get()用于读取数据,flip()用于切换读写模式,rewind()用于重新读取,以及clear()和compact()等用于清空缓冲区或重新组织数据的方法。
缓冲区有两种主要模式:读模式和写模式。在读模式下,你可以从缓冲区读取数据;在写模式下,你可以向缓冲区写入数据。使用flip()方法可以在读写模式之间切换。
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 写入数据到缓冲区
buffer.put("Hello, World!".getBytes());
// 切换到读模式,准备从缓冲区读取数据
buffer.flip();
// 从缓冲区读取数据
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
// 清空缓冲区,为下一次写入数据做准备
buffer.clear();
通道(Channel)是Java NIO(New I/O)中的核心概念之一,它用于在数据源和数据目标之间进行双向数据传输。通道通常与缓冲区(Buffer)结合使用,以实现高效的数据读取和写入。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
// 创建FileChannel
FileChannel fileChannel = new FileInputStream("source.txt").getChannel();
// 创建SocketChannel
SocketChannel socketChannel = SocketChannel.open();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 从FileChannel读取数据到缓冲区
int bytesRead = fileChannel.read(buffer);
// 将数据从缓冲区写入FileChannel
buffer.flip(); // 切换到读模式
while (buffer.hasRemaining()) {
fileChannel.write(buffer);
}
fileChannel.close();
long currentPosition = fileChannel.position(); // 查询当前位置
fileChannel.position(1024); // 设置新位置
通道支持直接数据传输,通过transferTo()和transferFrom()方法在通道之间传输数据。
FileChannel sourceChannel = new FileInputStream("source.txt").getChannel();
FileChannel destChannel = new FileOutputStream("destination.txt").getChannel();
sourceChannel.transferTo(0, sourceChannel.size(), destChannel);
实例要求:
使用 FileChannel(通道) 和 方法 transferFrom ,完成文件的拷贝
拷贝一张图片
代码演示:
public class NIOFileChannel04 {
public static void main(String[] args) throws Exception {
//创建相关流
FileInputStream fileInputStream = new FileInputStream("d:\\a.png");
FileOutputStream fileOutputStream = new FileOutputStream("d:\\a2.png");
//获取各个流对应的filechannel
FileChannel sourceCh = fileInputStream.getChannel();
FileChannel destCh = fileOutputStream.getChannel();
//使用transferForm完成拷贝
destCh.transferFrom(sourceCh,0,sourceCh.size());
//关闭相关通道和流
sourceCh.close();
destCh.close();
fileInputStream.close();
fileOutputStream.close();
}
}
选择器(Selector)是Java NIO中的一个关键组件,它允许单个线程来监视多个通道的I/O事件,从而实现高效的非阻塞I/O操作。选择器的主要作用是管理多个通道的事件,如读就绪、写就绪、连接就绪等,并且能够提供一种有效的方式,以便在这些通道之间切换。
选择器(Selector)的主要原理是使用单个线程来管理多个客户端连接的I/O事件。这种机制称为多路复用(Multiplexing)。以下是多路复用选择器的基本原理:
单线程管理多个通道:选择器创建一个线程,该线程负责管理多个通道,监视它们的I/O事件。这些通道可以是SocketChannel、ServerSocketChannel、DatagramChannel等。线程使用选择器来注册并监视这些通道。
事件通知:当一个或多个通道上的I/O事件发生时(如有数据可读、连接已建立、可写等),选择器会通知线程,而不需要线程轮询检查每个通道。
事件处理:线程接收到事件通知后,可以使用选择器提供的方法获取就绪的通道和事件类型(如可读、可写),然后执行相应的处理逻辑。这允许一个线程有效地处理多个通道的事件。
非阻塞操作:选择器的通道通常被设置为非阻塞模式,因此即使没有数据可读或可写,线程也不会阻塞在通道上。这使得线程可以同时处理多个通道,而不必等待每个通道的I/O操作完成。
多路复用:选择器通过在一个线程中管理多个通道,实现了多路复用,使得一个线程可以高效地处理多个客户端连接,而不需要为每个连接都创建一个新线程。
选择器通常使用Selector.open()来创建。一个选择器可以同时管理多个通道。
import java.nio.channels.Selector;
Selector selector = Selector.open();
import java.nio.channels.SelectionKey;
SelectableChannel channel = ...; // 获取要注册的通道
channel.configureBlocking(false); // 设置通道为非阻塞模式
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
选择器可以通过select()方法来检查就绪的通道,这些通道有特定类型的事件等待处理。
int readyChannels = selector.select();
通过遍历选择键(SelectionKey)来处理就绪的通道。选择键中包含了通道和其对应的就绪事件。
Set<SelectionKey> selectedKeys = selector.selectedKeys();
for (SelectionKey key : selectedKeys) {
if (key.isReadable()) {
// 处理可读事件
}
if (key.isWritable()) {
// 处理可写事件
}
if (key.isConnectable()) {
// 处理连接就绪事件
}
if (key.isAcceptable()) {
// 处理接受连接事件
}
}
selectedKeys.clear(); // 清空已处理的选择键
当通道不再需要被选择器管理时,可以取消注册,释放资源。
key.cancel();
以下是一个完整的示例,演示了如何使用选择器和通道进行非阻塞I/O操作:
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;
/**
* @Author: srf
* @Date: 2023/10/23 17:33
* @description:
*/
public class NioTest {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
int readyChannels = selector.select();
if (readyChannels == 0) {
continue;
}
for (SelectionKey key : selector.selectedKeys()) {
if (key.isAcceptable()) {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
clientChannel.close();
} else if (bytesRead > 0) {
buffer.flip();
// 处理从通道读取的数据
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
System.out.println();
}
}
selector.selectedKeys().clear();
}
}
}
}
NIO非阻塞聊天服务器示例:
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class ChatServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(8080));
serverSocket.configureBlocking(false);
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
ByteBuffer buffer = ByteBuffer.allocate(1024);
System.out.println("Chat server is running...");
while (true) {
selector.select();
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);
client.register(selector, SelectionKey.OP_READ);
System.out.println("Client connected: " + client.getRemoteAddress());
} else if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
int bytesRead = client.read(buffer);
if (bytesRead == -1) {
client.close();
} else if (bytesRead > 0) {
buffer.flip();
String message = new String(buffer.array(), 0, bytesRead);
System.out.println("Received: " + message);
buffer.clear();
// Broadcast the message to all clients
for (SelectionKey broadcastKey : selector.keys()) {
if (broadcastKey.isValid() && broadcastKey.channel() instanceof SocketChannel) {
SocketChannel channel = (SocketChannel) broadcastKey.channel();
if (channel != client) {
buffer.put(message.getBytes());
buffer.flip();
channel.write(buffer);
buffer.clear();
}
}
}
}
}
keyIterator.remove();
}
}
}
}
服务器端接受多个客户端连接,并允许客户端之间进行聊天。服务器接收客户端消息并将其广播给所有连接的客户端。客户端连接到服务器,并可以发送和接收消息。
NIO非阻塞聊天客户端示例:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;
public class ChatClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080));
socketChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
Thread receiveThread = new Thread(() -> {
try {
while (true) {
buffer.clear();
int bytesRead = socketChannel.read(buffer);
if (bytesRead == -1) {
socketChannel.close();
break;
} else if (bytesRead > 0) {
buffer.flip();
String receivedMessage = new String(buffer.array(), 0, buffer.limit());
System.out.println("Received: " + receivedMessage);
}
}
} catch (IOException e) {
e.printStackTrace();
}
});
receiveThread.start();
Scanner scanner = new Scanner(System.in);
while (true) {
String message = scanner.nextLine();
if ("exit".equalsIgnoreCase(message)) {
socketChannel.close();
receiveThread.interrupt();
break;
}
buffer.clear();
buffer.put(message.getBytes());
buffer.flip();
socketChannel.write(buffer);
}
}
}
客户端示例中,我们使用了两个线程:一个用于接收消息,另一个用于发送消息。用户可以输入消息,并在输入"exit"时退出客户端。
NIO(New I/O)与零拷贝是相关但不同的概念,它们都与高性能I/O操作有关,但在不同的层面和场景下发挥作用。
虽然NIO可以减少数据在Java应用程序中的内存复制,但它并不等同于零拷贝。零拷贝是更低层次的操作,通常需要操作系统或硬件支持,而NIO主要是一种I/O编程模型,侧重于提供更高级别的I/O操作接口,以实现非阻塞、高并发的I/O操作。
在实际应用中,可以将NIO与零拷贝技术结合使用,以最大程度地提高I/O性能。例如,可以使用NIO来管理非阻塞I/O通道,并结合零拷贝技术来执行实际的数据传输,从而在性能和效率上获得双重优势。但零拷贝技术通常需要更深入的系统编程和对底层硬件的了解,因此通常由高级编程语言的底层库或操作系统来实现。
“零拷贝” 这个术语源于它的工作原理:它旨在消除或最小化数据传输中的额外数据拷贝操作,从而减少了对内存和CPU的开销。零拷贝的主要优势在于它避免了在数据传输过程中的以下拷贝操作:
用户空间到内核空间的拷贝: 在传统I/O操作中,数据通常首先从应用程序的用户空间复制到内核空间的缓冲区,然后再从内核空间传输到目标位置(如网络卡、磁盘等)。零拷贝技术避免了这个用户空间到内核空间的复制操作,将数据直接从应用程序的用户空间传输到目标位置。
内核空间到用户空间的拷贝: 在某些情况下,内核可能需要将数据从内核空间的缓冲区复制到用户空间,以便应用程序可以访问它。零拷贝也避免了这种额外的内核到用户空间的复制操作。
中间数据缓冲的拷贝: 零拷贝技术通常避免了将数据从一个缓冲区复制到另一个缓冲区的操作,因为数据可以直接在内存和设备之间传输。