IO(Input/Output)模型指的是计算机系统进行输入输出操作时采用的不同策略和机制,主要涉及操作系统如何处理应用程序与外部设备(如磁盘、网络接口、键盘、显示器等)之间的数据交换。在现代计算环境中,尤其是网络编程和高性能服务器设计中,IO模型的选择对系统的性能、并发能力以及资源利用率有着重要影响。
一、同步与异步,阻塞与非阻塞的区别
同步,一个任务的完成之前不能做其他操作,必须等待(等于在打电话)
异步,一个任务的完成之前,可以进行其他操作(等于在聊VX)
阻塞,是相对于CPU来说的, 挂起当前线程,不能做其他操作只能等待
非阻塞,,无须挂起当前线程,可以去执行其他操作
1、同步(Synchronous)
关注点:同步关注的是操作的执行顺序和依赖关系。
执行模式:在同步操作中,一个任务的执行需要等待另一个任务的完成作为前提条件。
调用方行为:调用方发起一个操作后,会一直等待该操作完成并获取结果,期间不能执行其他任务。
通信机制:同步通常涉及到直接的控制流传递,即调用方主动等待返回值或通知。
2、异步(Asynchronous)
关注点:异步关注的是任务的独立性和并发执行。
执行模式:异步操作允许调用方发起一个操作后立即返回,无需等待该操作完成,可以继续执行其他任务。
调用方行为:调用方发起操作后,操作在后台执行,调用方可以继续处理其他逻辑,待操作完成后通过回调函数、事件通知、Future/Promise等方式得到结果。
通信机制:异步通常基于消息传递、事件驱动等机制,调用方与被调用方之间不存在直接的控制流依赖。
3、阻塞(Blocking)
关注点:阻塞关注的是调用者在等待资源状态变化时的状态。
线程状态:当调用者发起一个操作后,若所需资源(如数据、文件句柄、网络连接等)尚未就绪,调用者会被挂起(阻塞),无法执行任何其他操作,直到资源就绪或操作完成。
CPU使用:阻塞期间,调用线程不占用CPU资源,通常由操作系统调度器将其置于等待状态。
4、非阻塞(Non-blocking)
关注点:非阻塞关注的是调用者在等待资源时的即时响应能力。
线程状态:当调用者发起一个操作后,无论资源是否就绪,调用者都不会被挂起,而是立即获得一个状态信息(如“操作未完成”、“资源不可用”等),并继续执行后续代码。
CPU使用:非阻塞情况下,调用线程不会因等待资源而放弃CPU,可能需要通过轮询或其他机制检测资源状态,可能导致CPU使用率较高。
二、阻塞IO(BIO):同步并阻塞
A去钓鱼,但是A只能钓鱼,但是不能去做别的事情。
应用程序调用一个IO函数,导致应用程序阻塞,如果数据已经准备好,从内核拷贝到用户空间,否则一直等待下去。一个典型的读操作流程大致如下图,当用户进程调用recvfrom这个系统调用时,kernel就开始了IO的第一个阶段:准备数据,就是数据被拷贝到内核缓冲区中的一个过程(很多网络IO数据不会那么快到达,如没收一个完整的UDP包),等数据到操作系统内核缓冲区了,就到了第二阶段:将数据从内核缓冲区拷贝到用户内存,然后kernel返回结果,用户进程才会解除block状态,重新运行起来。
BIO的特点就是在IO执行的两个阶段用户进程都会block住;
以单个线程为单位,期间整个过程不能去做其他事情
1、特点
(1)同步:应用程序发起 IO 操作(如 java 中的 socket.read() 或 socket.write() )后,必须等待该操作完成才能继续执行后续代码。
(2)阻塞:在数据未准备好或传输未完成时,调用线程会被操作系统挂起(阻塞),直到IO操作完成。
(3)BIO 中的阻塞,就是阻塞在 2 个地方:
-
OS 等待数据报通过网络发送过来,如果建立连接后数据一直没过来,就会白白浪费线程的资源;
-
将数据从内核空间拷贝到用户空间。
2、工作流程
应用程序发起read()请求,如果数据尚未到达缓冲区或缓冲区为空,请求线程被阻塞。
数据到达并填充到缓冲区后,操作系统唤醒阻塞的线程,将数据从内核缓冲区复制到用户空间缓冲区,read()返回成功。
同理,write()请求也会阻塞,直到所有数据被写入内核缓冲区并确认可以发送到网络,然后返回。

@RestController
@RequestMapping("/bio")
public class BioController {
@Operation(summary = "BIO阻塞操作")
@GetMapping("/hello")
public Void hello() throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
System.out.println("等待连接...");
//阻塞方法,有客户端链接才不会阻塞
Socket clientSocket = serverSocket.accept();
//进行telnet连接 输入telnet localhost 8080
System.out.println("连接成功...");
handle(clientSocket);
}
}
private void handle(Socket clientSocket) throws IOException {
byte[] bytes = new byte[1024];
System.out.println("准备read...");
int read = clientSocket.getInputStream().read(bytes);
System.out.println("read完毕...");
//输出获取到的信息
if (read != -1) {
System.out.println("接收到客户端消息:" + new String(bytes, 0, read));
}
System.out.println("end...");
}
}
三、非阻塞IO(NIO):同步非阻塞
A去钓鱼,在钓鱼的期间,A可以喝喝茶,聊聊微信等,只需要每隔一段固定时间检查有没有上钩(轮询),假设有鱼上钩,A就停下手里的事情把鱼钓上来。
非阻塞I/O模型,我们把一个套接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要把进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。当用户进程发出read操作时,如果kernel中数据还没准备好,那么并不会block用户进程,而是立即返回error,用户进程判断结果是error,就知道数据还没准备好,用户可以再次发read,直到kernel中数据准备好,并且用户再一次发read操作,产生system call,那么kernel 马上将数据拷贝到用户内存,然后返回;
NIO的特点是以单个线程为单位,期间可以去做其他事情,但是需要用户进程不断主动询问kernel数据准备好没,造成cpu资源浪费
为了避免CPU空转,引进代理(select、poll),代理可以观察多个流I/O事件,空闲时会把当前线程阻塞掉,当有一个或多个I/O事件时,就从阻塞态醒过来,把所有IO流都轮询一遍,于是没有IO事件我们的程序就阻塞在select方法处,即便这样依然存在问题,我们从select出只是知道有IO事件发生,却不知道是哪几个流,还是只能轮询所有流,epoll这样的代理就可以把哪个流发生怎样的IO事件通知我们;
1、特点
(1)同步:应用程序需要不断轮询检查IO操作的状态。
(2)非阻塞:当数据未准备好时,调用read()或write()不会阻塞,而是立即返回一个错误码(如EWOULDBLOCK或EAGAIN)。
2、工作流程
应用程序设置套接字(Socket)为非阻塞模式。
发起read()请求,即使数据未准备好,也不会阻塞,而是立即返回。
应用程序通过循环或定时器反复尝试read(),直至数据可用并被复制到用户空间。
write()同样会立即返回,应用程序需自行管理何时再次尝试写入未完成的数据。

@RestController
@RequestMapping("/nio")
public class NioController {
//不设置非阻塞NIO会退化为BIO
@Operation(summary = "NIO非阻塞操作")
@GetMapping("/hello")
public Void hello() throws IOException {
//创建一个ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//创建一个ServerSocket
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(8080));
//设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
//打开selector处理channel
Selector selector = Selector.open();
//注册socket channel的连接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务启动成功...");
while (true) {
//阻塞等待需要处理的事件
selector.select();
//获取需要处理的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
//处理每个事件
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
//如果是OP_ACCEPT事件,则进行连接获取和事件注册
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = server.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("连接成功...");
} else if (key.isReadable()) {//如果是OP_READ事件,则进行数据读取
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = socketChannel.read(buffer);
if (read > 0) {
System.out.println("收到客户端数据:" + new String(buffer.array()));
} else if (read == -1) {
System.out.println("客户端断开连接...");
socketChannel.close();
}
}
}
}
}
}
四、多路复用IO(Multiplexing IO)
A去钓鱼,但是A带了很多钓鱼竿,A不断的查看哪个鱼竿钓到鱼了。提高了钓鱼的效率,同等情况下A钓到鱼也更多。
I/O多路复用就在于单个进程可以同时处理多个网络连接IO,基本原理就是select,poll,epoll这些个函数会不断轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程,这三个functon会阻塞进程,但和IO阻塞不同,这些函数可以同时阻塞多个IO操作,而且可以同时对多个读操作,写操作IO进行检验,直到有数据到达,才真正调用IO操作函数,调用过程如下图;
IO多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中任意一个进入就绪状态,select函数就可以返回。
IO多路复用的优势在于并发数比较高的IO操作情况,可以同时处理多个连接,和bloking IO一样socket是被阻塞的,只不过在多路复用中socket是被select阻塞,而在阻塞IO中是被socket IO给阻塞。
1、代表模型
-
选择器(Selector)模型(如Java NIO中的
Selector) -
epoll(Linux)、kqueue(BSD)等高效事件通知机制
2、特点
-
同步:应用程序在一个线程中监控多个IO通道的状态。
-
非阻塞:每个通道设置为非阻塞模式,通过多路复用器等待多个通道中的任一通道变为可读/可写状态。
3、工作流程
-
应用程序注册多个非阻塞套接字到多路复用器(如
epoll_ctl())。 -
调用多路复用API(如
epoll_wait())阻塞等待,当任一通道有IO事件(如可读或可写)时返回。 -
根据返回的事件集合,应用程序对相应通道进行
read()或write()操作,此时由于数据已准备好,这些操作通常不会阻塞。

五、异步IO(AIO):异步非阻塞
异步IO告知内核启动某个操作,并让内核在整个操作(包括将内核数据复制到我们自己的缓冲区)完成后通知我们,调用aio_read(Posix异步I/O函数以aio或lio开头)函数,给内核传递描述字、缓冲区指针、缓冲区大小(与read相同的3个参数)、文件偏移以及通知的方式,然后系统立即返回。我们的进程不阻塞于等待I/0操作的完成。当内核将数据拷贝到缓冲区后,再通知应用程序。
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了
1、特点
-
异步:应用程序发起IO请求后,立即返回,无需等待IO完成,也不必轮询或使用多路复用机制。
-
回调:内核在IO操作完成后,通过回调函数通知应用程序。
2、工作流程
-
应用程序发起
aio_read()或aio_write()请求,并提供一个完成回调函数。 -
请求立即返回,应用程序继续执行其他任务。
-
当内核完成数据的读取或写入(包括数据从内核到用户空间的复制)后,自动调用指定的回调函数通知应用程序。

六、信号驱动IO(IO)
可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们,通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已经准备好待处理。
等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取
1、特点
-
异步:应用程序注册一个信号处理器,当特定IO操作就绪时,内核通过发送信号通知应用程序。
-
非阻塞:应用程序不阻塞等待IO完成,而是继续执行其他任务。
2、工作流程
-
应用程序使用
fcntl()设置套接字为信号驱动模式,并注册一个信号处理器。 -
发起
read()请求后,立即返回,不等待数据。 -
当数据到达时,内核向应用程序发送一个SIGIO信号。
-
信号处理器或主程序捕获信号后,执行实际的
read()操作,此时数据已准备就绪,操作通常不阻塞。

七、IO、NIO 区别
1、 面向流与面向缓冲区
-
IO(传统):面向流(Stream-oriented)。在传统IO中,数据以流的形式进行读写,一次读取或写入一个或多个字节,直到流的结束。流是单向的,分为输入流和输出流,且操作过程中没有内置的缓冲区。
-
NIO:面向缓冲区(Buffer-oriented)。NIO引入了缓冲区的概念,数据读写都发生在缓冲区内。应用程序可以一次性读取大量数据到缓冲区,随后根据需要逐步处理;或者先将数据写入缓冲区,再一次性或分批将缓冲区内容刷新到外部设备。缓冲区支持随机访问,可以在其中前后移动指针进行读写。
2、阻塞与非阻塞
-
IO(传统):阻塞式(Blocking)。在进行IO操作时,如果数据尚未准备好或传输未完成,调用线程会被阻塞,直到操作完成。这意味着在等待数据的过程中,线程无法执行其他任务。
-
NIO:非阻塞式(Non-blocking)。NIO提供了非阻塞的IO操作选项。当试图读取数据但尚未就绪时,调用不会阻塞,而是立即返回一个标志表示数据未准备好。应用程序可以通过轮询或事件通知机制(如选择器)得知何时数据就绪,从而避免线程长时间阻塞。
3、选择器(Selector)与多路复用
-
IO(传统):无选择器。每个连接或文件描述符通常需要一个单独的线程来处理其IO操作。
-
NIO:引入了选择器(Selector)机制。一个单独的线程可以使用选择器同时监控多个通道(Channel)的IO状况,如监听多个Socket的读写事件。当某个或某些通道准备好进行读写操作时,选择器会返回相应的就绪事件,应用程序可以根据这些事件决定下一步操作哪个通道,实现单线程管理多个连接,提高系统并发性能。
4、编程模型
-
IO(传统):基于流的API设计,如
InputStream、OutputStream、Reader、Writer及其子类,通常采用简单的读写操作方法。 -
NIO:基于通道(Channel)和缓冲区(Buffer)的API设计,提供了如
SocketChannel、ServerSocketChannel、FileChannel等通道类,以及各种类型的缓冲区类如ByteBuffer。NIO编程涉及到注册通道到选择器、轮询选择器以获取就绪事件、对缓冲区进行读写操作等步骤,逻辑相对复杂。
5、性能与适用场景
-
IO(传统):适用于简单、低并发的场景,编程模型直观易用。但在高并发、高性能需求下,由于每个连接都需要独立线程,可能导致大量线程上下文切换,系统资源消耗大。
-
NIO:适合于高并发、大数据量、网络通信密集型应用,如服务器端编程。通过复用线程和非阻塞IO,能够更有效地利用系统资源,减少上下文切换开销。但NIO编程相对复杂,需要处理更多的回调和事件驱动逻辑。
| IO | NIO |
| 面向流 | 面向缓冲 |
| 阻塞IO | 非阻塞IO |
| 无 | 选择器 |
八、Java中NIO核心组件
1、channel
Channel 它就像自来水管一样,网络数据通过 Channel 读取和写入。一个Channel(通道)代表和某一实体的连接,这个实体可以是文件、网络套接字等。也就是说,通道是Java NIO提供的一座桥梁,用于我们的程序和操作系统底层I/O服务进行交互。

通道与流的不同之处在于通道是双向的,可以用于读、写,或者二者同时进行;流是单向的,要么是 InputStream,要么是 OutputStream。因为 Channel 是全双工的,所以它可以比流更好地映射底层操作系统的 API。特别是在 UNIX 网络编程模型 中,底层操作系统的通道都是全双工的,同时支持读写操作。
(1)channel与stream区别
一个通道,既可以读又可以写,而一个Stream是单向的(所以分 InputStream 和 OutputStream)
通道有非阻塞I/O模式
(2)channel可以监听哪几个事件
OP_READ(读就绪)含义:当通道上有新的数据可供读取时触发。这意味着连接到该通道的远程对端已经发送了数据,且数据已经被操作系统接收并放入了通道的缓冲区中,等待应用程序读取。
OP_WRITE(写就绪)含义:当通道有足够的缓冲空间可以写入数据时触发。这意味着之前由于缓冲区满而未能完全写出的数据现在可以继续写入,或者通道刚刚建立连接,可以开始写入数据。
OP_CONNECT(连接完成)含义:对于
SocketChannel在发起非阻塞连接操作后,当连接建立成功时触发。这表明远程服务器已经接受了连接请求,双方的TCP握手过程已完成,通道已准备好进行读写操作。
OP_ACCEPT(接受就绪)含义:对于
ServerSocketChannel,当有新的连接请求到达且可以被接受时触发。这意味着一个新的客户端连接请求已经被服务器端的网络层接收,ServerSocketChannel可以调用accept()方法接受这个连接请求,创建一个新的SocketChannel来处理这个客户端连接。
(3)Java的NIO中最常用的通道实现主要包括以下几个类
FileChannel功能:
FileChannel用于与本地文件系统进行交互,实现对文件的读取、写入、映射到内存以及位置跳转等操作。它可以高效地批量读写文件数据,支持文件的随机访问(相对于流式访问而言)。
SocketChannel功能:
SocketChannel用于实现基于TCP协议的网络通信。它可以建立客户端与服务器之间的连接,进行双向的数据传输。由于TCP提供可靠的数据传输保证(如数据包排序、重传等),SocketChannel适用于需要保证数据完整性和顺序性的网络应用。
ServerSocketChannel功能:
ServerSocketChannel用于在服务器端监听和接受TCP连接请求。一旦有新的连接请求到达,ServerSocketChannel会创建一个新的SocketChannel来处理该连接。它是构建TCP服务器的基础,如Web服务器、邮件服务器等。
DatagramChannel功能:
DatagramChannel用于处理基于UDP协议的无连接、不可靠的数据报通信。它既可以发送单播、多播数据包,也可以接收来自任意源的数据包。由于UDP不保证数据的顺序和可靠性,DatagramChannel适用于实时性要求高、容忍一定程度数据丢失的应用,如在线游戏、实时视频流、网络广播等
2、buffer
Buffer 对象 包含了一些要写入或者要读出的数据。在 NIO 类库 中加入 Buffer 对象,是其与 原 IO 类库 的一个重要区别。在面向流的 IO 中,可以将数据直接写入或者将数据直接读到 Stream 对象 中。在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,写入到缓冲区中。任何时候访问 NIO 中的数据,都是通过缓冲区进行操作。
缓冲区实质上是一个数组。通常它是一个字节数组(ByteBuffer),也可以使用其他种类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置(limit)等信息。最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了 ByteBuffer,还有其他的一些缓冲区,事实上,每一种 Java 基本类型(除了 boolean)都对应有一种与之对应的缓冲区
NIO中所使用的缓冲区不是一个简单的byte数组,而是封装过的Buffer类,通过它提供的API,我们可以灵活的操纵数据。与Java基本类型相对应,NIO提供了多种 Buffer 类型,如ByteBuffer、CharBuffer、IntBuffer等,区别就是读写缓冲区时的单位长度不一样(以对应类型的变量为单位进行读写)。
(1)java核心Buffer实现
ByteBuffer功能:
ByteBuffer用于存储字节数据,是最基础也是最常用的Buffer类型。它可以用于处理任何基于字节的数据,如文本、图片、音频、视频等二进制数据。在网络通信中,ByteBuffer常用于封装和解析TCP/UDP数据包。
CharBuffer功能:
CharBuffer专门用于存储Unicode字符数据,适用于处理文本内容。它可以与字符集编码解码器(如CharsetDecoder和CharsetEncoder)配合,实现字符串的编码和解码。
ShortBuffer功能:
ShortBuffer用于存储短整型(short)数值,适合于需要高效处理大量16位整数的场景。
IntBuffer功能:
IntBuffer用于存储整型(int)数值,适用于需要处理大量32位整数的情况,如图像像素数据、索引列表等。
LongBuffer功能:
LongBuffer用于存储长整型(long)数值,对于需要高效处理大量64位整数的场景非常有用。
FloatBuffer功能:
FloatBuffer用于存储浮点型(float)数值,常用于处理科学计算、图形渲染、音频处理等需要大量浮点数运算的领域。
DoubleBuffer功能:
DoubleBuffer用于存储双精度浮点型(double)数值,适用于对精度要求更高或需要处理更大范围浮点数的场景。这些Buffer类都继承自抽象基类
Buffer,共享一套通用的Buffer API,包括容量(capacity)、限制(limit)、位置(position)、标记(mark)等关键属性的管理方法,以及基本的读写操作。它们之间的主要区别在于内部存储的数据类型和相关的数据访问、转换方法。
MappedByteBuffer功能:
MappedByteBuffer是一个特殊的ByteBuffer实现,用于内存映射文件(Memory-Mapped Files)。它可以直接将文件的部分或全部内容映射到内存地址空间,使得对文件的操作如同访问内存一样高效。这种Buffer特别适用于大规模文件的快速、随机访问,尤其是在需要频繁读写大文件且希望降低I/O开销的场景。
(2)buffer读写数据的基本操作
1、创建Buffer实例
根据需要处理的数据类型创建对应的Buffer实例。例如,处理字节数据时创建ByteBuffer,处理字符数据时创建CharBuffer等。
// 创建一个容量为1024字节的ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 创建一个容量为1024个字符的CharBuffer
CharBuffer charBuffer = CharBuffer.allocate(1024);
2、写入数据到Buffer
Buffer提供了多种写入数据的方法,如put()、putInt()、putFloat()等。这些方法会将数据写入到当前位置(position),并将position向前移动相应的字节数。
// 将字节数组数据写入ByteBuffer
byte[] data = ...;
byteBuffer.put(data);
// 直接将整数值写入ByteBuffer
int value = 12345;
byteBuffer.putInt(value);
// 将字符序列写入CharBuffer
String text = "Hello, World!";
charBuffer.put(text.toCharArray());
3、调用flip()方法
在写入数据后,为了准备从Buffer中读取数据,需要调用flip()方法。此方法会将limit设置为当前position,然后将position重置为0。这样,后续的读取操作只会在已写入的数据范围内进行。
byteBuffer.flip();
charBuffer.flip();
4、从Buffer中读取数据
使用诸如get()、getInt()、getFloat()等读取方法从Buffer中提取数据。读取操作会从当前position开始,读取后position会向前移动相应的字节数。
// 从ByteBuffer中读取字节数据到字节数组
byte[] readData = new byte[byteBuffer.remaining()];
byteBuffer.get(readData);
// 从ByteBuffer中直接读取整数值
int readValue = byteBuffer.getInt();
// 从CharBuffer中读取字符数据到字符串
StringBuilder sb = new StringBuilder(charBuffer.remaining());
for (int i = 0; i < charBuffer.remaining(); i++) {
sb.append(charBuffer.get());
}
5、清理Buffer以备重用
读取完数据后,如果打算重复使用同一个Buffer对象,需要将其清理至初始状态。可以选择:
//clear()方法:将position设回0,limit设回容量(capacity),同时丢弃所有已有的标记(mark)。这会清空整个Buffer,使其可以重新用于写入数据。
byteBuffer.clear();
charBuffer.clear();
//compact()方法:保留尚未读取的数据(从position到limit之间的部分),将其移动到Buffer起始处,并更新position和limit。已读取的数据(position之前的)被丢弃。此方法适用于仍有部分数据未被读取,但需要腾出空间继续写入新数据的情况。
if (byteBuffer.hasRemaining()) {
byteBuffer.compact();
}
3、selector
简单来讲,Selector 会不断地轮询 “注册在其上的 Channel”,如果某个 Channel 上面发生读或者写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来,然后通过 SelectionKey 可以获取 “就绪 Channel 的集合”,进行后续的 IO 操作。一个 多路复用器 Selector 可以同时轮询多个 Channel,由于 JDK 使用了 epoll() 代替传统的 select 的实现,所以它并没有最大连接句柄的限制。这也就意味着,只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端。
(1)为什么要使用Selector
如果用阻塞I/O,需要多线程(浪费内存),如果用非阻塞I/O,需要不断重试(耗费CPU)。Selector的出现解决了这尴尬的问题,非阻塞模式下,通过Selector,我们的线程只为已就绪的通道工作,不用盲目的重试了。比如,当所有通道都没有数据到达时也就没有Read事件发生,我们的线程会在select()方法处被挂起,从而让出CPU资源。
(2) NIO服务端的主要创建过程
要使用一个Selector,你要先注册这个Selector的Channels。然后你调用Selector的select()方法。这个方法会阻塞,直到它注册的Channels当中有一个准备好了的事件发生了。当select()方法返回的时候,线程可以处理这些事件,如新的连接的到来,数据收到了等。
NIO服务端序列图

NIO客户端序列图

详细请参考:读尽天下源码,心中自然无码
九、Selector和Epoll的区别
1、所属体系
Selector:是Java NIO(New Input/Output)库的一部分,是Java语言层面提供的一个多路复用API,适用于Java应用程序。
epoll:是Linux操作系统内核级别的I/O多路复用机制,直接作为系统调用提供给C/C++等编程语言使用,是Linux特有的(尽管有其他类Unix系统如FreeBSD有类似的kqueue机制)
2、工作原理与数据结构
Selector:基于select()系统调用或者其更现代的替代品(如poll())实现。它通常维护一个Selector对象,该对象内部跟踪感兴趣的通道(Channel)列表以及每个通道上关注的事件类型(如读就绪、写就绪)。当调用select()方法时,它会阻塞直到至少有一个通道上的事件发生或超时。select()系统调用在内部可能会涉及从用户空间到内核空间的数据复制(如fd_set结构),并且可能需要线性扫描所有关注的文件描述符以检查其状态。、
epoll:提供了epoll_create(), epoll_ctl(), epoll_wait()等系统调用来管理事件。epoll利用了更先进的数据结构(如红黑树和就绪事件链表)来高效地跟踪关注的文件描述符及其状态。当调用epoll_wait()时,它仅返回已就绪的文件描述符,避免了不必要的扫描。而且,epoll使用的是事件驱动模型,即只有当关联的文件描述符状态发生变化时,才触发回调函数,减少了上下文切换。
3、性能与扩展性
Selector:由于其基于传统的select()或poll(),通常存在以下问题:
(1)效率:每次调用select()时都需要将整个文件描述符集合复制到内核空间,当文件描述符数量增大时,这种操作的开销会显著增加,导致线性时间复杂度的性能下降。
(2)固定数量限制:如select()有默认的最大并发文件描述符数量限制(通常是1024个)。
epoll:
(1)效率与扩展性:epoll只关心已就绪的文件描述符,因此其性能不随文件描述符数量增长而显著下降,具有更好的可扩展性。此外,由于使用了高效的内核数据结构,epoll在大量连接场景下的性能优于Selector基于的传统多路复用机制。
(2)无数量限制:epoll没有预设的最大并发限制,仅受限于系统能打开的最大文件数
4、编程接口与使用方式
Selector:面向Java开发者,通过Java API(如java.nio.channels.Selector类)进行编程,易于在Java环境中集成和使用。
epoll:面向使用C/C++或其他可以调用Linux系统调用的语言的开发者,需要直接调用相应的系统函数并处理底层细节。
十、Netty
Netty 是业界最流行的 NIO 框架 之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,已经得到成百上千的商用项目验证,例如 Hadoop 的 RPC 框架 Avro ,阿里的 RPC 框架 Dubbo 就使用了 Netty 作为底层通信框架。通过对 Netty 的分析,我们将它的优点总结如下:
- API 使用简单,开发门槛低;
- 功能强大,预置了多种编解码功能,支持多种主流协议;
- 定制能力强,可以通过 ChannelHandler 对通信框架进行灵活地扩展;
- 性能高,通过与其他业界主流的 NIO 框架 对比,Netty 的综合性能最优;
- 成熟、稳定,Netty 修复了已经发现的所有 JDK NIO BUG,业务开发人员不需要再为 NIO 的 BUG 而烦恼;
- 社区活跃,版本迭代周期短,发现的 BUG 可以被及时修复,同时,更多的新功能会加入;
- 经历了大规模的商业应用考验,质量得到验证。Netty 在互联网、大数据、网络游戏、企业应用、电信软件等众多行业已经得到了成功商用,证明它已经完全能够满足不同行业的商业应用了。
正是因为这些优点,Netty 逐渐成为了 Java NIO 编程 的首选框架。
Netty原理详细参考:这些年背过的面试题 —— Netty篇-CSDN博客
本文介绍了计算机系统的IO模型,包括同步与异步、阻塞与非阻塞的区别,详细阐述了阻塞IO、非阻塞IO、多路复用IO、异步IO和信号驱动IO的特点与工作流程,对比了IO和NIO的差异,分析了Java中NIO核心组件,探讨了Selector和Epoll的区别,最后介绍了Netty框架的优点。

1573

被折叠的 条评论
为什么被折叠?



