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

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 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\多路复用 区别和浅解)