IO学习笔记6
2.3 多路复用
但是NIO仍有它的缺陷,因为服务端和客户端都在一个线程中,主线程遍历客户端集合去每一个客户端都问一遍:你有没有数据,这样的话,如果有10K个客户端,只有最后一个客户端才收到了信息,但是取数据时还是要去将前面的99999个客户端问一遍,但是前面的遍历其实都是无用功。所以还要继续优化,就有了多路复用
回顾NIO的缺陷就是遍历。每次需要遍历所有的客户端,调用客户端的read()方法来取数据,没有数据继续调用下一个客户端的read()方法。每一次read()调用就对应一次系统调用----系统调用会产生中断,牵扯到保护现场恢复现场耗时增加。而当客户端非常多但是真正发送数据的客户端较少,频繁的调用进行read系统调用,但是真正 有效的调用(能够拿到客户端发来的数据)却很少,造成的后果就是:
- 频繁系统调用耗时,资源利用率低(有效调用少)
- 有效客户端如果位置靠后,则需要遍历前面所有客户端之后才会轮到它,时间复杂度是O(n)
那么能不能每次调用read时,只调用真正发来数据的客户端。
此时的问题就是如何才能知道哪儿些客户端是有数据的呢?
多路复用就是解决这个问题的。我们通过上面的学习知道了每一个客户端连接都会对应一个fd文件描述符。多路复用会通过在内核观察这些文件描述符的状态,然后返回给服务端到底哪儿些fd是有数据的,服务端读取时只需要读取这些有数据的fd就行了。
根据操作系统的不通,多路复用有多种实现。
2.3.1 select/poll
基本所有操作系统都会遵循POSIX协议--支持select,有的会支持poll。但是select和poll两种实现方式差不多。
使用man命令查看select:
int
select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout);
查看poll:
int
poll(struct pollfd fds[], nfds_t nfds, int timeout);
select和poll的区别
select和poll其实很相似,两个都是需要传一个fd的集合,然后交由操作系统内核kernel,内核对fds进行遍历,筛选出可以读到数据的fds,返回给服务端,然后服务端就可以只对这些有数据的客户端进行read系统调用。这样时间复杂度就由O(n)降低到了O(m)
不同的是select可以穿的fds大小是有限制的,而poll取消了这个限制。
2.3.2 epoll
select和poll两种方式将遍历fds这件事提到了kernel级别,在kernel中进行fds的筛选,减少了系统调用。
但是它的弊端就是:
- 每次都需要传全量的fds
- 每次对全量的fds进行遍历,最终只筛选出可读的fds,时间复杂度仍然是O(n)
为了解决这两个问题,出现了epoll。
在计算机中存在中断,中断分为软中断和硬中断。硬中断就是时钟中断,软中断就是系统调用等。IO中断也是软中断。软中断即int 80会触发回调函数callback,发生中断时会保护现场,callback就是用来恢复现场的。中断完成后的操作。
IO中断会触发callback
而在计算机组成中,有网卡设备。网卡是有自己的buffer的。在内存中,除了kernel程序之外,网卡也有对应的DMA网卡驱动程序。当数据到达网卡后,会触发IO中断,通知cpu/或通知DMA网卡驱动 接收数据。有三种方式:
- package: 每到达一个数据包就触发一次IO中断,通知cpu接收数据到内存中
- buffer(平时常使用):每一个数据包就中断通知一次cpu这种方式效率太慢了,cpu频繁中断切换太忙了,因此可以使用DMA,DMA可以绑定网卡,产生一个buffer,数据达到网卡后先存到buffer中,然后通过dma直接将数据放入内存中,dma将数据放入内存后会触发IO中断。
- 轮询:如果数据发送非常频繁,而且很快就满了,那么不如就不发生中断了,就直接一直轮询接受网卡的数据。
当触发IO中断,接收网卡的数据后,就会触发callback。之前的处理方式就是将网卡数据经过网络协议栈,最终绑定到fd的buffer。所以当应用程序某一时刻询问kernel某个或某些fd是否可读或可写,就会有状态返回。
epoll原理
epoll与select和poll的区别就是epoll在callback层做了其他处理:
epoll在内核中开辟了两块空间,一块是一个红黑树,用来存放全量fd,另一块用来存放状态被修改的fds链表(fds也就是fd的集合),也就是可读/可写的fds。
在以往的select/poll,都是需要传fds,然后内核对fds进行遍历筛选,它并没有将fd进行保存,因此每次都要传全量的fds。
而epoll在kernel中开辟了两块内存空间,一块是一个红黑树,用来存放交给它的fd,另一块用来存放有状态的fd返回给用户程序。
epoll分为三个系统调用:
epoll_create: 在kernel中开辟空间,创建一个红黑树,用来存放fdepoll_ctl: 对红黑树中的fd进行操作epoll_wait: 等待回调事件的产生,获取有状态的fd链表
当client向网卡发送一段数据后,那么整个数据处理流程如下:

简单实用代码实现多路复用
package com.gouxiazhi.io;
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;
/**
* @author shuai.zhao@going-link.com
* @date 2021/6/15
*/
public class SingletonMultiplexingIO {
private Selector selector;
public void init() {
try {
// 创建服务端
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(9090), 20);
// 设置非阻塞io
ssc.configureBlocking(false);
// 创建多路复用器
selector = Selector.open();
// 向多路复用器注册fd
ssc.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
init();
System.out.println("服务端启动了");
while (true) {
try {
while (selector.select() > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isAcceptable()) {
acceptHandler(selectionKey);
} else if (selectionKey.isReadable()) {
readHandler(selectionKey);
}
}
}
} catch (Exception ignore) {
}
}
}
public void acceptHandler(SelectionKey selectionKey) throws IOException {
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
SocketChannel client = ssc.accept();
client.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
client.register(selector, SelectionKey.OP_READ, buffer);
System.out.println("新客户端:" + client.getRemoteAddress());
}
public void readHandler(SelectionKey selectionKey) throws IOException {
SocketChannel client = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
buffer.clear();
while (true) {
int read = client.read(buffer);
System.out.println("read = " + read);
if (read > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
client.write(buffer);
}
buffer.clear();
} else if (read == 0) {
break;
} else {
client.close();
}
}
}
public static void main(String[] args) {
SingletonMultiplexingIO smio = new SingletonMultiplexingIO();
smio.start();
}
}
启动上面服务端,再次使用C10K客户端去链接:
// 服务端
新客户端:/127.0.0.1:20208
新客户端:/127.0.0.1:20209
Exception in thread "main" java.lang.ExceptionInInitializerError
at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:39)
at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223)
at sun.nio.ch.IOUtil.read(IOUtil.java:192)
at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:378)
at com.gouxiazhi.io.SingletonMultiplexingIO.readHandler(SingletonMultiplexingIO.java:77)
at com.gouxiazhi.io.SingletonMultiplexingIO.start(SingletonMultiplexingIO.java:51)
at com.gouxiazhi.io.SingletonMultiplexingIO.main(SingletonMultiplexingIO.java:95)
Caused by: java.io.IOException: Too many open files
at sun.nio.ch.FileDispatcherImpl.init(Native Method)
// 客户端
connection time consuming:1589
clients = 10214
可以看到同样是10214个连接,使用多路复用的方式,1589毫秒就完成了,效率要比NIO的方式快的多。
IO学习笔记6的更多相关文章
- Java IO学习笔记:概念与原理
Java IO学习笔记:概念与原理 一.概念 Java中对文件的操作是以流的方式进行的.流是Java内存中的一组有序数据序列.Java将数据从源(文件.内存.键盘.网络)读入到内存 中,形成了 ...
- Java IO学习笔记总结
Java IO学习笔记总结 前言 前面的八篇文章详细的讲述了Java IO的操作方法,文章列表如下 基本的文件操作 字符流和字节流的操作 InputStreamReader和OutputStreamW ...
- Java IO学习笔记三
Java IO学习笔记三 在整个IO包中,实际上就是分为字节流和字符流,但是除了这两个流之外,还存在了一组字节流-字符流的转换类. OutputStreamWriter:是Writer的子类,将输出的 ...
- Java IO学习笔记二
Java IO学习笔记二 流的概念 在程序中所有的数据都是以流的方式进行传输或保存的,程序需要数据的时候要使用输入流读取数据,而当程序需要将一些数据保存起来的时候,就要使用输出流完成. 程序中的输入输 ...
- Java IO学习笔记一
Java IO学习笔记一 File File是文件和目录路径名的抽象表示形式,总的来说就是java创建删除文件目录的一个类库,但是作用不仅仅于此,详细见官方文档 构造函数 File(File pare ...
- nodejs的socket.io学习笔记
socket.io学习笔记 1.服务器信息传输: 2.不分组,数据传输: 3.分组数据传输: 4.Socket.io难点大放送(暂时没有搞定): 服务器信息传输 1. // send to curre ...
- 阻塞 io 非阻塞 io 学习笔记
阻塞 io 非阻塞 io 学习笔记
- Java IO学习笔记一:为什么带Buffer的比不带Buffer的快
作者:Grey 原文地址:Java IO学习笔记一:为什么带Buffer的比不带Buffer的快 Java中为什么BufferedReader,BufferedWriter要比FileReader 和 ...
- Java IO学习笔记二:DirectByteBuffer与HeapByteBuffer
作者:Grey 原文地址:Java IO学习笔记二:DirectByteBuffer与HeapByteBuffer ByteBuffer.allocate()与ByteBuffer.allocateD ...
- Java IO学习笔记三:MMAP与RandomAccessFile
作者:Grey 原文地址:Java IO学习笔记三:MMAP与RandomAccessFile 关于RandomAccessFile 相较于前面提到的BufferedReader/Writer和Fil ...
随机推荐
- 【爬虫+数据清洗+可视化分析】舆情分析哔哩哔哩"狂飙"的评论
目录 一.背景介绍 二.爬虫代码 2.1 展示爬取结果 2.2 爬虫代码讲解 三.可视化代码 3.1 读取数据 3.2 数据清洗 3.3 可视化 3.3.1 IP属地分析-柱形图 3.3.2 评论时间 ...
- 学习Java Day29
今天学习了类的设计技巧: 1.一定要保证数据私有 2.一定要对数据初始化 3.不要在类中使用过多的基本类型 4.不是所有的字段都需要单独的字段访问器和字段更改器 5.分解有过多职责的类 6.类名和方法 ...
- LG P3809 【模板】后缀排序
贴模板 注意:\(\text{id}\) 表示第二关键字排序后(其实无需排序,利用上轮的 \(\text{sa}\) 值即可)相应的第一关键字的位置 计数排序为了稳定性最后确定位置时要倒着开始 复制的 ...
- CF1742G Orray
题目传送门 思路 大抵算是一道位运算入门题? 首先为了使 \(b_i\) 的字典序最大,我们注意到 \(b_1=a_1\),所以 \(a_1\) 必然是序列中最大的那个数. 接下来考虑贪心,设当前已经 ...
- 读论文SRCNN:Learning a Deep Convolutional Network for Image Super-Resolution
Learning a Deep Convolutional Network for Image Super-Resolution SRCNN是深度学习应用于SR领域的开山之作. 论文 2014 ECC ...
- jsHtml解码
//将HTML代码转换为文本 function HTMLDecode(text) { var temp = document.createElement("div"); temp. ...
- pytorch学习笔记二之自动差分引擎
自动差分引擎¶ torch.autograd是 PyTorch 的自动差分引擎,可为神经网络训练提供支持. 1. 背景¶ 神经网络(NN)是在某些输入数据上执行的嵌套函数的集合. 这些函数 ...
- LeetCode-1765 地图中的最高点
来源:力扣(LeetCode)链接:https://leetcode-cn.com/problems/map-of-highest-peak 题目描述 给你一个大小为 m x n 的整数矩阵 isWa ...
- 时钟切换电路(无毛刺)——clock switching glitch free
0.引言 随着越来越多的多频时钟被应用在今天的芯片中,尤其是在通信领域中,经常需要在芯片运行时切换时钟线的源时钟.这通常是通过在硬件中复用两个不同的频率时钟源,并通过内部逻辑控制多路选择器选择线来实现 ...
- 【补题】第 46 届 ICPC EC Final
比赛 题目:第 46 届 ICPC EC Final(正式赛) 榜单 A. DFS Order 签到题 容易发现对于一个点,它的最小位置就是从根走一条链直接到它,最大位置就是除了它的子树,其它全已经走 ...