微信公众号:码上就有
公众号的文章名称:JAVA中的I/O模型-多路复用
背景
上一章节中讲解了
NIO
相关的知识点,知道了代码中通过配置configureBlocking
配置不阻塞,让程序能够一直运行下去(底下也是内核系统进行支持,监听是否有FD
发生状态变化)。
环境相关介绍:
1.8 - JDK (1.6前后有版本变化)
CentOS Linux release 7.8.2003 (Core)
多路复用 及 Reactor模式
为何需要
在上一节中我们讲解到NIO
中如何解决阻塞以及更好的进行客户端数据的读取数据。
但是同样也依旧会存在以下问题:
- 代码中维护客户端连接。
- 服务器在不断的将客户端
FD
传递进行轮询判断是否有事件(涉及线程上下文切换)。
Demo
11 public class Multiplexing {
12
13 private ServerSocketChannel server = null;
14 private Selector selector = null;
15 int port = 9090;
16
17 public void initServer(){
18 try {
19 server = ServerSocketChannel.open();
20 server.configureBlocking(false);
21 server.bind(new InetSocketAddress(port));
22 selector = Selector.open();
23 server.register(selector, SelectionKey.OP_ACCEPT);
24 } catch (IOException e) {
25 e.printStackTrace();
26 }
27 }
28
29 public void start(){
30 initServer();
31 System.out.println("服务器启动了.......");
32 try {
33 while (true){
34 Set<SelectionKey> keys = selector.keys();
35 while (selector.select(500)>0){
36 Set<SelectionKey> selectionKeys = selector.selectedKeys();
37 Iterator<SelectionKey> iterator = selectionKeys.iterator();
38 while (iterator.hasNext()) {
39 SelectionKey selectionKey = iterator.next();
40 iterator.remove();
41 if (selectionKey.isAcceptable()) {
42 acceptHandler(selectionKey);
43 } else if (selectionKey.isReadable()) {
44 readHandler(selectionKey);
45 } else if (selectionKey.isWritable()) {
46
47 }
48 }
49 }
50 }
51 }catch (Exception e){
52 e.printStackTrace();
53 }finally {
54 try {
55 selector.close();
56 server.close();
57 } catch (Exception ex) {
58
59 }
60 }
61 }
62
63 public static void main(String[] args) {
64 Multiplexing multiplexing = new Multiplexing();
65 multiplexing.start();
66 }
67 }
多路复用 - 预先需知
在之前讲解需要了解到的知识点,后期的任何网络
IO
的变化其实大部分都是依赖于底层操作系统的支持,多路复用也是基于底层操作系统函数的支持。
select
函数 - > 同步多路复用IO
方法
返回值中会返回三个集合数据包含readfds
,writefds
以及exceptfds
文件描述符集合。(fds
有1024
限制)poll
函数 - > 同步多路复用IO
方法
返回值中返回对应有响应的fds
集合。epoll_create
函数 - > 打开epoll
文件描述符
该方法将会返回一个epoll
实例(该实例用于接收IO
事件通知)。epoll_ctl
函数 - >epoll
描述符的控制接口
接收fd
绑定对应事件到epoll
实例上。epoll_wait
函数 - > 等待epoll
文件描述符上IO
事件
返回对应有IO
事件的fd
。
上述方法中,前两个都是基于多路复用进行的,下面三个方法则完全归属于epoll
方式(个人觉得他也使用到了多路复用,但是更偏向于Reactor
模型)。
不知道有没有小伙伴对于多路复用与Reactor
模型这两个概念有没有疑问(我起初看这两个词经常在一起,以为是指的是一个意思),后来经过学习查阅才明白这两个是有一定的差距的:
前两个方法是多路复用的主要调用方法,而
epoll
则是Reactor
模型的代表了。
多路复用的过程即使将产生的fd
全部传递至方法中。即抽象可以理解为select(int[] fds)
。每次不停的进行轮询判断是否有事件产生,产生之后再进行client
的非阻塞事件操作,但是服务端的socket
依旧会进行遍历集合。
epoll
也是有多路复用的一个概念在其中,但是为什么会叫Reactor
模式呢?那是因为通过划分事件进行分别注册到对应的epoll
实例上(如下图)。将不同的事件交给不同的epoll
实例,最后会交给对应的业务线程去进行处理
代码运行 - 过程详解
项目启动:
.......
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 6
bind(6, {sa_family=AF_INET, sin_port=htons(9090), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(6, 50)
.......
epoll_create(256) = 9
epoll_ctl(9, EPOLL_CTL_ADD, 6, {EPOLLIN, {u32=6, u64=5031441452962414598}}) = 0
epoll_wait(9, [], 8192, 500) = 0
.......
上面最开始的三板斧都是固定不变的(针对于server
端)。之后我们就可以看到他是采用了epoll
方式。epoll_ctl
这个函数就是将对应创建的server
的fd
添加epoll
实例中。接下来就是epoll_wait
不断的对其实例上注册的fd
进行循环。
启动客户端进行连接并进行数据传输:
......
epoll_wait(9, [{EPOLLIN, {u32=6, u64=15747492883499843590}}], 8192, 500) = 1
accept(6, {sa_family=AF_INET, sin_port=htons(40872), sin_addr=inet_addr("127.0.0.1")}, [16]) = 10
epoll_ctl(9, EPOLL_CTL_ADD, 10, {EPOLLIN, {u32=10, u64=15747492883499843594}}) = 0
......
epoll_wait(9, [{EPOLLIN, {u32=10, u64=11426042750433230858}}], 8192, 500) = 1
read(10, "111111\n", 1024) = 7
write(1, "client port: 41412, data: 111111"..., 33) = 33
write(1, "\n", 1) = 1
epoll_wait(9, [], 8192, 500) = 0
......
可以看到同样进行的client
连接后的fd
同样也放到epoll
实例中(这里可以理解为单Reactor
模型)。传输数据的时候监听到fd=10
上有事件发生,即产生对应的读写事件。
实验结果
上面的讲解主要是讲了epoll
,关于多路复用的也是涉及到一部分。底层还是由系统层面进行支撑的,当然,也并不是多路复用就是完美的解决方案,不然后面也不会有一主N从的Reactor
模型出现。
多路复用器:
优势:
通过一次系统调用,把fds,传递给内核,内核进行遍历,减少了系统调用的次数!!
弊端:
- 重复传递
fd
。 - 每次都要重新遍历全量
fd
。
epoll
:
优势:
- 客户端连接的
fds
内核存放空间。 - 不同的事件注册到不同的
epoll
实例上,性能得到了极大的提升。
弊端:
服务资源开销增加(多Reactor
情况下)
总结
上面讲述了多路复用以及
Reactor
模型。二者皆都谈及了一些,当然对于其中的组成部分还有许多没有提及(例如channel
,buffer
等)。
不知道小伙伴们有没有想过epoll
单个实例的时候其实跟select
与poll
一样的效果,那为什么多个实例就能得到极大的提升?
其实道理也很简单,就是在针对一个事件监听的数组要查询上面某个fd状态发生改变,其实底层就是遍历操作。只有当前数组元素较少,遍历的时长则会减少,对应的响应就更能及时(这个时候就能很好的理解下面这张图了)。
本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情