NIO多路复用

阻塞与非阻塞IO

 Java IO和NIO之间第一个最大的区别是,IO是面向流的NIO是面向缓冲区的。 

Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。

Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取立马返回,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 

 IO多路复用

多路复用就是通过复用同一个线程可以监听多个文件描述符fd,一旦某个文件描述符fd就绪(可以读/写),就通过(selector、poll、epoll)等方式通知用户进程进行相应read、write操作。最大优势是减少系统开销小,不必创建过多的进程/线程

1、select

客户端操作服务器时会产生这三种文件描述符(简称fd):writefds(写)、readfds(读)、和exceptfds(异常)。select会阻塞住监视3类文件描述符,等有数据、可读、可写、出异常 或超时、就会返回,返回后通过遍历fdset整个集合来找到就绪的描述符fd,然后进行对应的IO操作。

优点:
  几乎在所有的平台上支持,跨平台支持性好
缺点:
  1、采用轮询方式扫描所有fd_set集合,会随着文件描述符FD数量增多而性能下降。
  2、每次调用 select(),需要把fd_set集合从用户态拷贝到内核态
  3、默认linux打开的fd文件描述符有限1024个,可修改宏定义,但是效率仍然慢。

时间复杂度: O(N) 

数据结构 存储结构bitmap

Java中使用Selector 的图解如下:

Seletor通过同时监听多个Channel,在系统内核没有响应的时候,Seletor通过select()方法把当前线程进入死循环当中,当有一个或多个Chanel就绪(如连接、接受、读或写)的时候,就开始进行处理。

注意,如果一个 Channel 要注册到 Selector 中,那么这个 Channel 必须是非阻塞的,即channel.configureBlocking(false);
因为 Channel 必须要是非阻塞的,因此 FileChannel 是不能够使用选择器的,因为 FileChannel 都是阻塞的.

当像Selector注册Channel时,Channel.register()方法会返回一个SelectionKey 对象。这个对象代表了注册到该Selector的通道。可以通过selector.selectedKeys()方法访问这些SelectionKey对象。

可以遍历这个已选择的SelectionKey集合来访问就绪的通道

 2、poll

基本原理与select一致,也是轮询+遍历;唯一的区别就是poll没有最大文件描述符限制,poll只有一个pollfd数组,数组中的每个元素都表示一个需要监听IO操作事件的文件描述符

           struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */
               short revents;    /* returned events */
           };

 具体参考链接:https://man7.org/linux/man-pages/man2/poll.2.html

优点:
   1、poll没有最大文件描述符限制  

        2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
缺点:
  1、采用轮询方式扫描所有pollfd集合,会随着文件描述符FD数量增多而性能下降。
  2、每次调用 poll(),需要把pollfd集合从用户态拷贝到内核态

时间复杂度: O(N) 

数据结构 数组

 3、epoll 

没有fd个数限制,用户态拷贝到内核态只需要一次,使用事件通知机制来触发。通过epoll_ctl注册fd,一旦fd就绪就会通过callback回调机制来激活对应fd,进行相关的io操作。1G内存大概支持10万个句柄,这个数量和内存大小有关

epoll之所以高性能是得益于它的三个函数
  1)epoll_create()系统启动时,在Linux内核里面申请一个B+树结构文件系统,返回epoll对象,也是一个fd,以及就绪链表(该链表存储已经就绪的文件描述符)
  2)epoll_ctl() 每新建一个连接,都通过该函数操作epoll对象,在这个对象里面修改添加删除对应的链接fd, 绑定一个callback函数
  3)epoll_wait() 轮训所有的callback集合,并完成对应的IO操作 

优点:
  1、没有文件描述符fd大小限制

        2、通过事件驱动callback回调实现fd通知,而不是轮询的方式

时间复杂度: O(1) 

数据结构 红黑树

epoll有水平触发LT(level trigger)和边缘触发ET(edge trigger)两种触发模式,LT是默认的模式,ET是“高速”模式。

水平触发LT模式:当epoll_wait检测到描述符事件发生并将此事件通知用户应用程序,用户应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

在边缘触发ET模式中,只会提示一次,当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

IO多路复用的三种实现方式

selectpollepoll
数据结构bitmap数组红黑树
最大连接数1024无限制无限制
FD拷贝每次调用select拷贝每次调用poll拷贝fd首次调用epoll_ctl拷贝,每次调用epoll_wait不拷贝

工作效率

(时间复杂度)

轮询:O(n)轮询:O(n)回调:O(1)

参考:https://blog.csdn.net/weixin_44164489/article/details/108930663
 

### nIO多路复用的定义 NIO多路复用是一种高效的I/O处理方式,允许单个线程管理多个网络连接。通过这种方式,程序可以在一个线程中监听多个通道的状态变化(如可读、可写),从而避免了传统同步阻塞I/O模式下需要为每个连接创建独立线程所带来的开销[^1]。 --- ### NIO多路复用的工作原理 NIO多路复用的核心依赖于`Selector`类。以下是其主要工作机制: 1. **注册Channel到Selector** 将多个`SelectableChannel`对象(如`ServerSocketChannel`或`SocketChannel`)注册到同一个`Selector`实例上,并指定感兴趣的事件类型(如`SelectionKey.OP_READ`表示关注读操作)。这一步使得`Selector`能够感知这些通道上的状态变化[^4]。 2. **轮询准备好的Channel** 调用`select()`方法让当前线程进入等待状态,直到某些已注册的通道发生了所关心的操作为止。此时返回的结果集包含了所有已经准备好对应操作的通道列表[^3]。 3. **遍历并处理事件** 对于每一个处于就绪状态下的通道,逐一取出它们关联的数据流或者发起新的通信动作;完成之后再继续回到第二步循环监测新到来的变化情况[^5]。 这种设计极大地减少了因频繁创建销毁大量短生命周期轻量级单元而带来的性能损耗问题,在应对大规模并发访问请求时表现出显著优越性。 --- ### 应用场景分析 #### 高并发环境 由于能够在单一进程中同时维护成千上万个活跃链接的能力,因此特别适合应用于Web服务器、即时通讯工具以及其他任何可能面临极高频率交互需求的服务端架构之中[^1]。 例如在一个典型的聊天室应用里,如果采用传统的BIO模型,则每新增一位参与者都需要额外分配一个新的服务进程/线程来单独为其提供支持——随着人数增加不仅消耗内存空间还会加剧调度压力。然而借助NIO多路复用方案则可以通过少量固定数目的工作者线程池即可满足相同规模甚至更大范围内的客户需求。 另外值得注意的是尽管如此高效但也并非毫无代价:持续不断地扫描各个文件描述符是否具备活动迹象本身就会耗费一定计算资源;而且当实际存在的有效连接数目过多时还可能导致上下文切换成本上升等问题出现[^3]。 为了缓解这些问题,在具体工程实践中往往会结合诸如Reactor模式这样的高级编程范型进一步优化整体表现效果。比如Netty框架内部正是采用了类似的双层结构设计理念:其中Boss组专门负责接受外部来访者的接入请求并将成功建立起来的新会话分发给Worker组成员分别承担后续具体的事务逻辑处理职责[^4]。 --- ### 示例代码展示 下面给出一段简单的基于NIO Selector实现的TCP回显服务器示例: ```java 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; public class EchoServer { private final int port; public EchoServer(int port) { this.port = port; } public void start() throws IOException { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(port)); serverSocketChannel.configureBlocking(false); Selector selector = Selector.open(); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { selector.select(); Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); if (!key.isValid()) continue; if (key.isAcceptable()) handleAccept(key); if (key.isReadable()) handleRead(key); } } } private void handleAccept(SelectionKey key) throws IOException { ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel(); SocketChannel clientChannel = serverSocketChannel.accept(); System.out.println("Accepted connection from " + clientChannel.getRemoteAddress()); clientChannel.configureBlocking(false); clientChannel.register(key.selector(), SelectionKey.OP_READ); } private void handleRead(SelectionKey key) throws IOException { SocketChannel channel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); int bytesRead = channel.read(buffer); if (bytesRead == -1) { channel.close(); return; } String message = new String(buffer.array()).trim(); System.out.println("Received: " + message); channel.write(ByteBuffer.wrap(("Echo: " + message).getBytes())); } public static void main(String[] args) throws IOException { new EchoServer(8080).start(); } } ``` 此段代码展示了如何利用NIO中的`Selector`机制构建一个多路复用的简单服务器应用程序。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值