JAVA中的I/O模型-多路复用|技术点评

2,583 阅读6分钟

微信公众号:码上就有
公众号的文章名称:JAVA中的I/O模型-多路复用

背景

  上一章节中讲解了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限制)
  2. poll函数 - > 同步多路复用IO方法
    返回值中返回对应有响应的fds集合。
  3. epoll_create函数 - > 打开epoll文件描述符
    该方法将会返回一个epoll实例(该实例用于接收IO事件通知)。
  4. epoll_ctl函数 - > epoll描述符的控制接口
    接收fd绑定对应事件到epoll实例上。
  5. 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这个函数就是将对应创建的serverfd添加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,传递给内核,内核进行遍历,减少了系统调用的次数!!
弊端:

  1. 重复传递fd
  2. 每次都要重新遍历全量fd

epoll

优势:

  1. 客户端连接的fds内核存放空间。
  2. 不同的事件注册到不同的epoll实例上,性能得到了极大的提升。

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

总结

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

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

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

本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情