java BIO、NIO、多路复用

BIO操作系统实现

1.调用socket 返回int(fd文件描述符)
2.bind(fd,8090)
3.listen(fd)
4.accept(fd,) = 5 (5代表客户端的连接),这个方法会阻塞
5.recv(5,)从5读取数据,会阻塞

任何语言,BIO,NIO和组件(nginx)开启socket都是这个流程

主线程可以clone出一个新的thread来处理读取请求.recv(5,)

BIO:

每线程,每连接,
优势:可以接受很多的连接,
问题:线程内存浪费;cpu调度消耗
根源:blocking 阻塞,accept和recv
解决方案:NonBlocking 非阻塞

NIO:

java中是New IO (selector,channel,byteBuffer)
操作系统是NonBlocking IO

NIO优势:规避多线程问题
弊端:假设有1w个连接,只有一个发来数据,每循环一次,其实你必须向内核发送1W次的recv的系统调用,那么这里有9999次是无意义的,消耗时间和资源,

如果程序自己读取I/O,那么这个I/O模型,无论BIO,NIO,多路复用器(select,poll,epoll):同步IO模型


多路复用器:


优势:通过一次系统调用,把fds传递给内核,内核进行遍历,这种遍历减少了系统调用的次数

select:

使用fd_set结构体告诉内核同时监控那些文件句柄,使用逐个排查方式去检查是否有文件句柄就绪或者超时。该方式有以下缺点:文件句柄数量是有上线的,逐个检查吞吐量低,
每次调用都要重复初始化fd_set。
 select的几大缺点:

(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大(解决方案:内核开辟空间保存fds)

(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

(3)select支持的文件描述符数量太小了,默认是1024

poll:

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
@对于select 这种方式,需要进行 2 次「遍历」文件描述符集合,一次是在内核态里,一个次是在用户态里 ,而且还会发生 2 次「拷贝」文件描述符集合,先从用户空间传入内核空间,
由内核修改后,再传出到用户空间中
select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024,
只能监听 0~1023 的文件描述符
poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。


epoll:


epoll 提供了三个函数:

int epoll_create(int size);
建立一个epoll 对象,并传回它的fd

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
@事件注册函数,将需要监听的事件和需要监听的fd交给epoll对象,epoll对象内部维护了一棵红黑树,经过网卡的数据会产生网卡中断,放数据到内核的缓冲区,此时还做了一个延申
@处理:
@根据socket得到四元组,然后可以得到fd,再去红黑树里去找,并把找到的有状态的fd copy到链表里
@select和poll没有这个延申处理,所以每次调用select和poll都会循环查一遍内核缓冲区,看看传递的fds有没有相匹配的数据到达

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
等待注册的事件被触发或者timeout发生,这里实际一直去上一步链表中取

socket->3
bind(3,8090)
listen(3)
epoll_create() = 7
epoll_ctl(7,ADD,3,EPOLLIN) ≈ accept
epoll_wait()  这个方法是阻塞的
accept(3) = 8
epoll_ctl(7,ADD,8,EPOLLIN) ≈ read
epoll_wait() 这个方法可以返回多个事件

@epoll支持两种事件触发模式,分别是边缘触发(edge-triggered,ET)和水平触发(level-triggered,LT)。

这两个术语还挺抽象的,其实它们的区别还是很好理解的。
使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,
因此我们程序要保证一次性将内核缓冲区的数据读取完;
使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;
举个例子,你的快递被放到了一个快递箱里,如果快递箱只会通过短信通知你一次,即使你一直没有去取,它也不会再发送第二条短信提醒你,这个方式就是边缘触发;
如果快递箱发现你的快递没有被取出,它就会不停地发短信通知你,直到你取出了快递,它才消停,这个就是水平触发的方式。

### BIONIO、AIO 和 IO 多路复用的概念及使用场景 #### 同步与异步、阻塞与非阻塞的基础理解 在网络通信中,同步(Synchronous)意味着当前线程会一直等待直到操作完成;而异步(Asynchronous)则表示不会阻塞当前线程,而是通过回调函数或其他机制通知操作已完成。同样,阻塞(Blocking)是指程序会在某些情况下暂停运行,而非阻塞(Non-blocking)则是指即使没有数据可读或写入也不会让程序停止。 #### BIO (Blocking I/O) BIO 是一种传统的输入输出模型,其中每个客户端连接都会创建一个新的线程来处理请求。这种方式简单易懂,但在高并发环境下表现不佳。由于每次 I/O 操作都需要等待资源可用,因此当有大量的客户端连接时,服务器可能因为线程过多而导致性能下降甚至崩溃[^1]。 ```java // BIO 示例代码 ServerSocket serverSocket = new ServerSocket(port); while (true) { Socket socket = serverSocket.accept(); // 阻塞在这里 new Thread(() -> handleRequest(socket)).start(); } ``` #### NIO (Non-blocking I/O) 相比 BIO 的每连接一线程模型,NIO 使用少量线程配合选择器(Selector)来管理多个通道(Channel)。这使得它可以高效地处理大量的并发连接。NIO 中的核心组件包括缓冲区(Buffer)、通道以及选择器。选择器能监听多个 Channel 上的状态变化,并只在有事件发生时才触发相应逻辑[^5]。 ```java // NIO 示例代码 Selector selector = Selector.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { int readyChannels = selector.select(); if (readyChannels == 0) continue; Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isAcceptable()) { /* Handle accept */ } if (key.isReadable()) { /* Handle read */ } keyIterator.remove(); } } ``` #### AIO (Asynchronous I/O) AIO 提供了一种完全异步的方式来进行文件和套接字访问。在这种模式下,应用程序无需主动查询或者轮询即可得知某项任务已经结束——操作系统负责告知应用何时完成了指定的操作。这种设计非常适合那些需要极高吞吐量的应用场合[^3]。 然而值得注意的是,在实际开发过程中,JDK 对于 AIO 支持并不像 NIO 那样成熟稳定,尤其是在跨平台兼容性和功能完备度方面还存在一定局限性。 ```java // AIO 示例代码 AsynchronousServerSocketChannel listener = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(port)); listener.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() { @Override public void completed(AsynchronousSocketChannel ch, Void att) {} @Override public void failed(Throwable exc, Void att) {} }); ``` #### IO 多路复用 IO 多路复用利用单一进程/线程同时监视多个文件描述符上的活动情况,只有当至少有一个就绪状态改变才会唤醒处理器进一步动作。常见的实现方法包括但不限于 `select`、`poll` 及 Linux 特有的高性能解决方案 `epoll` 等[^4]^。 虽然从表面上看,IO 多路复用似乎与 NIO 很接近,但实际上前者更多依赖底层 OS API 来达到目的,而后者的抽象层次更高一些,封装得更好一点,更适合面向对象语言如 Java 进行高层次编程[^2]. --- ### 总结对比表 | **特性** | **BIO** | **NIO** | **AIO** | **IO 多路复用** | |----------------|----------------------------------|--------------------------------------|---------------------------------------|-------------------------------------| | 并发能力 | 较低 | 高 | 极高 | 高 | | 实现复杂度 | 简单 | 中等 | 高 | 中等到高等 | | 资源消耗 | 显著增加 | 减少 | 最优 | 减少 | | 主要适用场景 | 小规模低并发 | 中大规模 | 需求极致性能的大规模 | 高效处理大量短时间活跃的连接 | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值