写点什么

JAVA 中的 I/O 模型 - 多路复用

用户头像
云流
关注
发布于: 2021 年 03 月 10 日

背景

  上一章节中讲解了NIO相关的知识点,知道了代码中通过配置configureBlocking配置不阻塞,让程序能够一直运行下去(底下也是内核系统进行支持,监听是否有FD发生状态变化)。

环境相关介绍:

1.8 - JDK (1.6 前后有版本变化)

CentOS Linux release 7.8.2003 (Core)


多路复用 及 Reactor 模式

为何需要

  在上一节中我们讲解到NIO中如何解决阻塞以及更好的进行客户端数据的读取数据。

  但是同样也依旧会存在以下问题:

  1. 代码中维护客户端连接。

  2. 服务器在不断的将客户端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的变化其实大部分都是依赖于底层操作系统的支持,多路复用也是基于底层操作系统函数的支持。

  1. select函数 - > 同步多路复用IO方法

返回值中会返回三个集合数据包含 readfds,writefds以及exceptfds文件描述符集合。(fds1024限制)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) = 6bind(6, {sa_family=AF_INET, sin_port=htons(9090), sin_addr=inet_addr("0.0.0.0")}, 16) = 0listen(6, 50)  .......epoll_create(256)                       = 9epoll_ctl(9, EPOLL_CTL_ADD, 6, {EPOLLIN, {u32=6, u64=5031441452962414598}}) = 0epoll_wait(9, [], 8192, 500)            = 0.......复制代码
复制代码

  上面最开始的三板斧都是固定不变的(针对于server端)。之后我们就可以看到他是采用了epoll方式。epoll_ctl这个函数就是将对应创建的serverfd添加epoll实例中。接下来就是epoll_wait不断的对其实例上注册的fd进行循环。

启动客户端进行连接并进行数据传输:

......epoll_wait(9, [{EPOLLIN, {u32=6, u64=15747492883499843590}}], 8192, 500) = 1accept(6, {sa_family=AF_INET, sin_port=htons(40872), sin_addr=inet_addr("127.0.0.1")}, [16]) = 10epoll_ctl(9, EPOLL_CTL_ADD, 10, {EPOLLIN, {u32=10, u64=15747492883499843594}}) = 0......epoll_wait(9, [{EPOLLIN, {u32=10, u64=11426042750433230858}}], 8192, 500) = 1read(10, "111111\n", 1024)              = 7write(1, "client port: 41412, data: 111111"..., 33) = 33write(1, "\n", 1)                       = 1epoll_wait(9, [], 8192, 500)            = 0......复制代码
复制代码

  可以看到同样进行的client连接后的fd同样也放到epoll实例中(这里可以理解为单Reactor模型)。传输数据的时候监听到fd=10上有事件发生,即产生对应的读写事件。

实验结果

  上面的讲解主要是讲了epoll,关于多路复用的也是涉及到一部分。底层还是由系统层面进行支撑的,当然,也并不是多路复用就是完美的解决方案,不然后面也不会有一主 N 从的Reactor模型出现。

多路复用器:

优势:

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

弊端:


  1. 重复传递fd

  2. 每次都要重新遍历全量fd

epoll

优势:


  1. 客户端连接的fds内核存放空间。

  2. 不同的事件注册到不同的epoll实例上,性能得到了极大的提升。

弊端:

服务资源开销增加(多Reactor情况下)


总结

  上面讲述了多路复用以及Reactor模型。二者皆都谈及了一些,当然对于其中的组成部分还有许多没有提及(例如channel,buffer等)。

  不知道小伙伴们有没有想过epoll单个实例的时候其实跟selectpoll一样的效果,那为什么多个实例就能得到极大的提升?

  其实道理也很简单,就是在针对一个事件监听的数组要查询上面某个 fd 状态发生改变,其实底层就是遍历操作。只有当前数组元素较少,遍历的时长则会减少,对应的响应就更能及时(这个时候就能很好的理解下面这张图了)。



作者:Montos

链接:https://juejin.cn/post/6937913439801212959

来源:掘金


用户头像

云流

关注

还未添加个人签名 2020.09.02 加入

还未添加个人简介

评论

发布
暂无评论
JAVA中的I/O模型-多路复用