zoukankan      html  css  js  c++  java
  • 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。但是selectpoll两种实现方式差不多。

    使用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中开辟空间,创建一个红黑树,用来存放fd
    • epoll_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的方式快的多。

  • 相关阅读:
    python(内置高阶函数)
    cms 环境搭建
    cookie、session 和 token 区别
    接口用例设计
    python(字符编码与转码)
    从北斗卫星时钟(北斗校时器)发展纵论世界卫星导航新格局
    北斗授时系统(GPS授时设备)错一秒会怎样?京准电子科技
    北斗校时服务器(GPS时钟服务器)在电力调度系统应用
    GPS卫星时钟(北斗授时设备)在监狱管理系统方案
    NTP校时(网络对时服务器)IPC网络摄像机时钟同步
  • 原文地址:https://www.cnblogs.com/Zs-book1/p/14889529.html
Copyright © 2011-2022 走看看