本文是观看了B站的马士兵的视频后的总结:
清华大牛权威讲解nio,epoll,多路复用,更好的理解redis-netty-Kafka等热门技术
和知乎的一篇文章:
看不懂来砍我,epoll原理
理解Socket基础1—计算机基础
我们知道内存是被分为内核和用户两个部分的,内核用于运行操作系统和硬件相关的底层驱动,由于系统的保护机制,用户态的进程是无法直接访问硬件的,比如网络通信的硬件网卡;
硬件设备接收事件(网卡接收数据帧,键盘接收输入等),当有了事件后,硬件层会产生一个中断,CPU会立刻停止当前的工作(比如当前正在执行用户进程)处理这个中断,处理的工作就是内核去实现,比如调用内核中对应硬件驱动的回调;
用户态的进行想要访问硬件资源(和硬件交互)必须通过内核,内核会提供
系统调用
让用户态安全的访问计算机;
拿Socket举例,网络数据通过物理网线传给网卡,此时网卡会产生一个中断,告诉CPU有网络数据进入电脑了,这时会将数据交给内核,具体放在哪我也没研究过,反正就是放在内核里面,用户态(Java)必须通过系统调用
去拿到这个网络数据
BIO
传统的IO使用(伪代码)
// 客户端
Socket socket = new Socket("127.0.0.1",8090);
socket.getOutputStream();
socket.getInputStream();
// 服务端
ServerSocket serverSocket = new ServerSocket(8089);
Socket socket = serverSocket.accept();
socket.getOutputStream();
socket.getInputStream();
客户端:
- 创建Socket对象,传入服务端对应的ip和端口,会自动连接
- 获取IO流通信
服务端:
- 服务端创建
ServerSocket
对象,绑定ip和port -
ServerSocket
调用accept()
监听客户端连接,练连接完成会返回客户端对应的Socket
对象(这是一个阻塞方法,一般会在循环中开启线程
去执行,即一个线程一个Socket连接) - 完事儿以后通过Socket获取IO流进行数据的读写
这些是我们在java层做的事情,那么网络通信是如何发生的呢?
首先java层是用户态的一个进程,他是无法直接读取网卡的数据的,必须通过系统调用到内核中去获取;系统调用是通过native层去实现的;
BIO存在的问题:
- accept()和IO的读写是阻塞方法,必须开启多线程,每一个Socket连接建立一个线程
- 很多Socket连接建立了并没有通信,会浪费大量的系统资源;
NIO
为了解决线程浪费问题出现了NIO,将阻塞方法改为非阻塞方法,如果有连接,有数据,就去处理,没有的话继续执行下面,等待下次循环;
NIO存在的问题:
NIO虽然解决了线程浪费
的问题,可是如果在大量网络请求的情况下,当前方案下的执行效率会变得非常的低,因为Java层的循环变得非常的长,并且每次循环都需要调用系统调用
去询问内核这个请求有没有用,这个连接有没有数据,大量的无效的系统调用也会影响性能;
Select:
为了解决NIO在java层大量无效循环调用
System call
的情况,出现了一个select
系统调用,Select的作用是将10000此循环全部通过一次SC交给内核,由内核去循环,判断哪些是有效的循环,比如100次有效循环,那么我的java就可以有目的性的去调用100次有效的SC去进行数据读写,Socket连接建立;
select缺点:
- 需要将连接一次性传递给内核
- 虽然省去了大量的SC,但是内核需要去遍历循环,内核的内存压力会增大
Epoll:
等待队列红黑树
:
Epoll
将所有的Socket连接都在内核中保存了下来,就省去了Select一次性将所有的Socket连接发过来的这一步骤;
就绪列表双向链表
:
Select
效率低的原因是因为需要遍历所有的连接才能知道哪个连接有数据,而epoll
通过维护一个集合,存放所有的就绪连接,这样就避免了遍历的步骤;当有数据到达时,中断程序
会产生一个中断将有数据的Socket添加到就绪列表;
epoll将多路复用的实现拆分为三个步骤:
-
epoll_create:
内核会产生一个epoll 实例数据结构并返回一个文件描述符,这个特殊的描述符就是epoll实例的句柄,后面的两个接口都以它为中心 -
epoll_ctl:
维护等待队列将被监听的描述符添加到红黑树或从红黑树中删除,或者对监听事件进行修改 -
epoll_wait:
阻塞进程,等待数据,程序执行到这一步时,如果就绪列表
有数据,就直接返回,如果没有数据就会阻塞;
NIO
NonBlocking IO特点:
- 非阻塞IO,没有数据时不会阻塞,而是返回0
- 单线程处理多任务
核心类:
- channel
- selector
- buffer
channel:
channel通道类似流,既可以从流读取数据,也可以写入数据到流,流是单向的,通道是双向的;
channel的实现:
- FileChannel:从文件中读写数据,无法设置为非阻塞式
- DataGramChannel:从UDP读写网络数据
- SocketChannel:从TCP读写网络数据
- ServerSocketChannel:监听新进来的TCP连接,每一个新的TCP连接都会创建一个新的SocketChannel
buffer
NIO buffer 提供了一组方法,用来访问缓冲区,对于缓冲区,本质上是一块可以写入数据,可以读取数据的内存;
buffer的使用:
1.channel写入数据到buffer
2.调用buffer的flip()make buffer ready to read
3. 从buffer中读取数据
4.调用buffer的clear()`make buffer ready to write`
buffer的工作原理:
buffer的重要属性:capacity position limit
capacity:作为一个内存块,buffer有一个固定大小,capacity就是记录buffer的大小
position:当buffer写入的时候position从0开始,放入一个数据,position就后移一位;当buffer读取的时候,position从0开始,每读一个数据,后移一位;
limit:在写入的时候,limit同capacity,表示可以写入的大小;在读取时,表示当前可读取的数量;
buffer的类型:
- ByteBuffer:
- CharBuffer:
- DoubleBuffer:
- FloatBuffer:
- IntBuffer:
- LongBuffer:
- ShortBuffer:
buffer的创建(分配):
// 分配了48字节大小的字符Buffer
CharBuffer charBuffer = CharBuffer.allocate(48);
向buffer写入数据
// 1 直接用 put() 写入
charBuffer.put('1');
// 2 channel写入到buffer
channel.read(buffer);
flip():
将buffer从写模式转换成读模式
从buffer读取数据
// 1 直接使用 get() 读取
char c = charBuffer.get();
// 2 读取到channel中
channel.write(buffer);
rewind():
将position重新设置为0,可以再次读取buffer(limit保持不变)clear():
将buffer从读模式转为写模式,clear不会保存原来的数据,compact():
compact会将未读的数据拷贝到buffer的起始处,并且将position移到最后一个数后面mark() & reset() :
通过mark 记录position的值,再通过reset恢复到之前记录的positionequals() :
比较buffer内的剩余元素,如果它们类型相等,数量相等,元素值相等,那么两个buffer 就相等compareTo() :
比较元素的数量和元素值的大小;
分散和聚集(Scatter/Gather):
-
分散:
将channel的数据分散读取到多个buffer中
// 分散 , 一个channel的数据读取到多个buffer
ByteBuffer head = ByteBuffer.allocate(20);
ByteBuffer body = ByteBuffer.allocate(480);
ByteBuffer[] buffers = {head,body};
try {
// 从channel读取数据
channel.read(buffers);
} catch (IOException e) {
e.printStackTrace();
}
-
聚集:
将多个buffer数据聚集写入到一个channel中
// 聚集 , 多个buffer数据写入channel
ByteBuffer head = ByteBuffer.allocate(20);
ByteBuffer body = ByteBuffer.allocate(480);
ByteBuffer[] buffers = {head,body};
try {
// 写入数据到channel
channel.write(buffers);
} catch (IOException e) {
e.printStackTrace();
}
Selector
选择器,用于实现单线程管理多个channel
,即管理多个网络连接
1. selector的创建:
try {
Selector selector = Selector.open();
} catch (IOException e) {
e.printStackTrace();
}
2. 向selector中注册channel
// 将channel设置为非阻塞式
socketChannel.configureBlocking(false);
// 注册到selector上
SelectionKey key = socketChannel.register(selector, SelectionKey.OP_READ);
注意, 如果一个 Channel 要注册到 Selector 中, 那么这个 Channel 必须是非阻塞的, 即channel.configureBlocking(false); 因为 Channel 必须要是非阻塞的, 因此 FileChannel 是不能够使用选择器的, 因为 FileChannel 都是阻塞的
register()第二个参数用于指定selector对channel的什么事件感兴趣,常见的事件有:
- SelectionKey.OP_ACCEPT:确认事件
- SelectionKey.OP_CONNECT:连接事件,TCP连接
- SelectionKey.OP_READ:读出事件
- SelectionKey.OP_WRITE:写入事件
SelectionKey:
每次向Selector中注册一个channel都会拿到一个SelectionKey对象;通过selectionKey对绑定事件进行控制,SelectionKey重要的成员变量:
- interest Set:感兴趣事件的集合
- ready Set:已准备就绪的操作的集合
- Channel:
- Selector:
- 附加对象:
// 获取 channel
key.channel();
// 获取 selector
key.selector();
// 获取 感兴趣的事件
key.interestOps();
// 附加对象
key.attach(new Object());
Selector.select():
调用该方法后会阻塞,知道被注册的channel有事件出现,或者出现新的channel注册事件
Set keySet = selector.selectedKeys();
Iterator iterator = keySet.iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = (SelectionKey) iterator.next();
// TODO: 通过 selectionKey 获取channel 处理事件
iterator.remove(); // 删除当前元素(key)
}