一、IO的基本概念
什么是IO?
I/O就是计算机内存与外部设备之间拷贝数据的过程
什么是网络IO?
网络IO是指在计算机网络环境中进行的输入和输出操作,涉及数据在网络设备之间的传输。
网络IO操作可以是发送请求、接收响应、下载文件、传输数据等。在Java中,网络IO通过java.net
包提供的类(如Socket
、ServerSocket
等)来实现网络通信。
二、Java的IO模型
在I/O操作中有这么两组概念,同步/异步、阻塞/非阻塞
同步/异步:
什么意思?指的是数据就绪后,我收不到通知,需要自己每隔一段时间就询问尝试读一下,靠自己专门去读取数据,叫做同步;而数据就绪准备可读了之后,由某些程序回调(理解为通知)给我们的接收程序,这称为异步。
阻塞/非阻塞:
阻塞:现在有一个读取数据的操作,在没有数据传过来时,读操作会一直阻塞等待,直到有数据过来;其次,缓冲区满时,对于写操作也会一直阻塞等待直到缓冲区有空间可以写入为止。
非阻塞:非阻塞可以理解为无需等待,都是直接返回。比如读操作中,没有数据可读时,那我就不读了,直接返回,程序结束,不会阻塞等待。写操作在缓冲区已满时,我就不写了,直接程序结束。
常见的IO模型
一种很重要的IO模型叫做IO多路复用,在多路复用IO模型中,操作系统提供了一种机制(如select、poll或epoll)来监控多个IO流的状态,只有当其中某个IO操作可以执行时,程序才会进行实际的读写操作。在网络IO中呢,就是监控多个socket连接嘛,同时去定时检查多个socket,哪个要读要写了,就通知哪个线程进行IO操作
三、Java的网络IO模型
BIO
BIO是blocking I/O的简称,它是同步阻塞型IO,其相关的类和接口在java.io下
BIO模型简单来讲,就是服务端为每一个请求都分配一个线程进行处理,I/O操作都是基于流Stream的操作
示例代码,一定要仔细阅读!
import java.io.*;
import java.net.*;
public class BIOServer {
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(8080)) {
System.out.println("服务器启动,等待客户端连接...");
while (true) {
// 1. 接受客户端连接(阻塞式)
Socket socket = serverSocket.accept(); // 阻塞,直到客户端连接
System.out.println("客户端连接成功");
// 2. 为每个连接创建一个新线程处理
new Thread(new ClientHandler(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ClientHandler implements Runnable {
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 2. 获取输入流并读取客户端消息
InputStream inputStream = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String clientMessage = reader.readLine();
System.out.println("收到客户端消息: " + clientMessage);
// 3. 获取输出流并向客户端发送响应
OutputStream outputStream = socket.getOutputStream();
PrintWriter writer = new PrintWriter(outputStream, true);
writer.println("你好,客户端!服务器已收到你的消息。");
// 4. 关闭客户端连接
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
其中主要有两个缺点:
线程开销:客户端的并发数与后端的线程数成1:1的比例,线程的创建、销毁是非常消耗系统资源的,随着并发量增大,服务端性能将显著下降,甚至会发生线程堆栈溢出等错误
线程阻塞:当连接创建后,如果该线程没有操作时,会进行阻塞操作,这样极大的浪费了服务器资源(这就是同步阻塞的缺点!)
NIO
java当中的NIO就是我们的同步非阻塞模型,代码非常简单,只有一个配置项,这样就能实现非阻塞。
我们主要来讲解Java NIO当中实现的IO多路复用这种同步非阻塞IO模型
其核心就是在轮询检测中,引入Select替我们去同时检测多个连接(否则就要每一个连接上的read单独去做轮询),哪一个连接有IO事件发生,就通知哪一个进行数据读写。
接下来详细介绍NIO
NIO的三大核心组件:Buffer(缓冲区)、Channel(通道)、Selector(选择器/多路复用器)
Buffer(缓冲区):
Buffer是一个对象,包含一些要写入或者读出的数据,体现了与原I/O的一个重要区别,在面向流的I/O中,数据读写是直接进入到Stream中,而在NIO中,所有数据都是用缓冲区处理的,读数据直接从缓冲区读,写数据直接写入到缓冲区。
缓冲区的本质是一个数组,通常是一个字节数组(ByteBuffer),也可以使用其他类型,但缓冲区又不仅仅是一个数组,它还提供了对数据结构化访问以及维护读写位置等操作。
所以Buffer就是NIO当中的数据容器,网络IO中读写的数据都是存放在Buffer中,写数据是写入到Buffer,读数据是从Buffer中读。
Channel(通道):
Channel 是一个通道,管道,网络数据通过Channel读取和写入,Channel和流Stream的不同之处在于Channel是双向的,流只在一个方向上移动(InputStream/OutputStream),而Channel可以用于读写同时进行,即Channel是全双工的。
所以有了Channel之后,我们就不从Stream中读写数据,而是通过Channel从Buffer容器里读数据或写数据。
接下来要简单讲一个SocketChannel和ServerSocketChannel,可以把他们都理解为连接对象
Selector(选择器/多路复用器):
Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,即该Channel处于就绪状态,它就会被Selector轮询出来,然后通过selectedKeys可以获取就绪Channel的集合,进行后续的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;
import java.util.Iterator;
import java.util.Set;
public class NIOServerWithThread {
public static void main(String[] args) {
try {
// 1. 创建 ServerSocketChannel,绑定端口
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // 设置为非阻塞模式
// 2. 创建 Selector 并注册 ServerSocketChannel
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT); // 监听连接请求
System.out.println("服务器启动,等待客户端连接...");
// 3. 事件循环,监听 Selector
while (true) {
// 3.1 阻塞等待事件,返回事件数量
int readyChannels = selector.select();
if (readyChannels == 0) continue;
// 3.2 获取可用的通道(即有事件发生的通道)
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
// 3.3 处理每个事件
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// 处理新的客户端连接
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = server.accept();
clientChannel.configureBlocking(false);
System.out.println("客户端连接成功: " + clientChannel.getRemoteAddress());
// 注册客户端通道,监听可读事件
clientChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 创建一个新的线程来处理客户端发送的数据
new Thread(() -> handleClient(key)).start();
}
// 3.4 处理完当前事件后,需要将它从 selectedKeys 集合中移除
keyIterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 处理客户端的逻辑
private static void handleClient(SelectionKey key) {
try {
// 处理客户端发送的数据
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = clientChannel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
String clientMessage = new String(buffer.array(), 0, bytesRead).trim();
System.out.println("收到客户端消息: " + clientMessage);
// 响应客户端
buffer.clear();
buffer.put("你好,客户端!服务器已收到你的消息。".getBytes());
buffer.flip();
clientChannel.write(buffer);
} else if (bytesRead == -1) {
// 客户端关闭连接
System.out.println("客户端断开连接: " + clientChannel.getRemoteAddress());
clientChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
非阻塞性的体现:
1、在设置 ServerSocketChannel 和 SocketChannel 时,我们调用了 configureBlocking(false),这意味着在进行 I/O 操作时(如连接、读取和写入),这些操作不会阻塞线程。如果没有数据可读或连接不可用,线程不会被挂起,而是继续执行后面的代码。
2、Selector 是基于事件驱动的,主线程调用 selector.select() 方法后,会阻塞等待有事件发生(例如客户端连接或可读事件)。如果没有事件发生,主线程会继续等待,这个过程是非阻塞的,因为它不会强制执行任何 I/O 操作。
3、在上面的代码演示中,主线程在 while (true) 循环中不断调用 selector.select(),在没有事件发生时不会被阻塞(即使 selector.select() 方法是阻塞的,但它并不会影响处理其他已注册的事件的能力)。
AIO
aio就是异步模型,由于在一些操作系统上的支持不同,用的也不多,技术还不够成熟,因此不做介绍。