美文网首页
BIO\NIO\多路复用 区别和浅解

BIO\NIO\多路复用 区别和浅解

作者: YYYYYY25 | 来源:发表于2020-11-11 16:38 被阅读0次

    Title:BIO\NIO\多路复用 区别和浅解

    技术博客已迁移至个人页,欢迎查看 yloopdaed.icu

    您也可以关注 JPP - 这是一个Java养成计划,需要您的加入。


    前言

    网络通信中BIO、NIO、多路复用相关的知识点非常多,网络上也有很多相关的技术文章。但是每个人的角度和切入点不同,知识涵盖的内容太多了,很难整理出一条清晰的思路。

    我这两天尝试着脱离代码(因为加入代码Demo之后,篇幅增长,很多API也不是很熟悉,增加阅读的难度),单纯从操作系统网络通信的流程去梳理BIO、NIO,再到多路复用的优缺点。最终达到了解这些技术的联系和发展的目的。

    BIO

    此流程基于Java示例代码,代码可以查看 JPP /IOTest 类

    BIO

    主线程:

    1 服务端等待客户端连接,阻塞进程

    2 检测到客户端连接,返回对应的文件描述符fd,并将连接移动到子线程

    3 开始下一次循环,跳回第1步

    子线程:

    1 子线程中服务端等待客户端响应,阻塞进程

    2 接收到客户端响应,子线程处理

    3 开始下一次循环,跳回第1步

    优势:可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多考虑系统的过载、限流等问题。

    问题:

    1 线程的创建和销毁成本很高

    2 线程本身占用较大内存

    3 线程的切换成本是很高的

    4 容易造成锯齿状的系统负载

    为了解决BIO的缺点,进入NIO

    NIO

    此流程基于Java示例代码,代码可以查看 JPP /NIOTest 类

    NIO

    本文介绍的NIO是操作系统网络通信中的NON-BLOCKING IO,是Java NIO的基础。
    上图中的两个 主线程 循环 指的是没有开辟子线程。意思是服务端接收客户端连接和处理客户端请求都在同一个线程中。

    流程:

    1 服务端请求客户端连接,非阻塞。如果没有直接返回null并向下执行

    2 如果有客户端连接,请求消息响应,非阻塞。如果没有响应直接返回null并继续遍历客户端

    3 遍历完所有客户端后,会重新开始循环,跳回第1步

    优势:

    1 规避多线程的问题

    2 单线程解决多任务

    问题:

    客户端循环遍历时,不断进行用户态和内核态的切换,系统调用开销非常大

    什么是用户态和内核态?
    我的理解就是权限不同。用户态的进程能够访问的资源受操作系统的控制,而运行在内核态的进程才可以访问系统中的硬件设别,例如网卡。

    所以上面客户端循环遍历询问消息响应,会不断进行用户态和内核态的切换。

    开销大在哪?

    • 1 保护现场
    • 2 恢复现场
    • 3 软中断
    • 4 寻找中断向量表
    • 5 找回调函数

    多路复用

    在NIO的基础上,通过一次系统调用将连接客户端响应询问移动到内核处理,而不是反复进行用户态和内核态的切换。

    如果把每次系统调用理解成一条通路,那么这种把多次系统调用合并成一次的方式,就叫做多路复用

    例如:美团 Java NIO浅析

    interface ChannelHandler{
        void channelReadable(Channel channel);
        void channelWritable(Channel channel);
    }
    class Channel{
        Socket socket;
        Event event;//读,写或者连接
    }
    
    //IO线程主循环:
    class IoThread extends Thread{
        public void run(){
            Channel channel;
            while(channel=Selector.select()){//选择就绪的事件和对应的连接
                if(channel.event==accept){
                    registerNewChannelHandler(channel);//如果是新连接,则注册一个新的读写处理器
                }
                if(channel.event==write){
                    getChannelHandler(channel).channelWritable(channel);//如果可以写,则执行写事件
                }
                if(channel.event==read){
                    getChannelHandler(channel).channelReadable(channel);//如果可以读,则执行读事件
                }
            }
        }
        Map<Channel,ChannelHandler> handlerMap;//所有channel的对应事件处理器
    }
    

    Selector中的select函数会执行系统内核的调用:Linux 2.6之前是select、poll,2.6之后是epoll。

    此流程基于Java示例代码,代码可以查看 JPP /NIOEpollTest 类

    多路复用

    多路复用的实现方式

    select

    流程:

    1 客户端建立连接后返回fd

    2 用二进制位表bitmap标记fds对应的位置,并将这个bitmap从用户态拷贝到内核态

    3 收到客户端响应,将对应的bitmap位标记为1

    4 程序处理消息,重新创建bitmap,循环下一次

    缺点:

    1 bitmap默认最大限制1024位

    2 bitmap不可重用,每次循环重新创建

    3 用户态和内核态切换开销

    4 轮询所有的客户端处理消息 O(n)

    poll

    流程:

    与select相似,select中用bitmap标记fds,在poll中自己声明了结构体

    struct pollfd{
        int fd;
        short events;
        short revents
    }
    

    传输pollfd数组,解决了select中fds限制1024位的问题

    其次,内核将响应客户端对应的pollfd结构体中的revnets标记为1,说明这个客户端有消息响应

    处理消息时将这个pollfd的revnets重置即可,不用在重新创建数组,所以select中每次循环重新创建bitmap的问题也被解决了

    遗留问题:

    1 由于fds文件描述符存储在用户空间,拷贝到内存空间处理一定会涉及到用户态和内核态的切换。这个系统调用有一定开销

    2 每次内核态返回的信息是全量信息,要轮询处理,时间复杂度是 O(n),如果连接数过多,也会有很多无意义的开销

    这两个问题留给 epoll

    epoll

    流程:

    1 建立epoll对象时在内核分配资源,其数据结构是红黑树。添加和检索的时间复杂度 O(lgn)

    int epoll_create(int size); 
    

    2 建立连接时,在红黑树中存储 epoll_event结构体,其中包含fd和events等信息

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);  
    

    3 调用epoll_wait收集响应的连接,放入一个单向链表

    int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);  
    

    到此为止,上面提到的多路复用的所有缺点都得以解决。

    最后

    网络通信

    相关文章

      网友评论

          本文标题:BIO\NIO\多路复用 区别和浅解

          本文链接:https://www.haomeiwen.com/subject/mwzebktx.html