网络中的阻塞与非阻塞以及reactor模型

文章目录

  • 一、网络`IO`的职责
    • 操作`IO`
      • `IO`的操作方式
      • **阻塞与非阻塞`IO`的具体差别:**
        • 阻塞`IO`在系统调用中的流程
        • 非阻塞`IO`在系统调用中的流程
      • 网络编程系统调用具备检测和操作的功能
        • `accept`:
        • `read`:
        • `write`:
  • 二、系统调用在调用非阻塞`IO`的具体处理
    • `connect`
    • `accept`
    • `read`
    • `write`
  • 三、网络中连接断开的情况
    • 主动关闭
    • 被动关闭
  • 四、分离检测与操作
    • 以`epoll`为例介绍`io`多路复用
      • 建立连接
      • 连接断开
      • 消息到达
      • 消息发送
    • `epoll`详解
      • 介绍`epoll_wait`的四个参数接口
  • 五、引入`Reactor`
    • 什么是`reactor`?
    • 为什么要有`reactor`
    • `reactor`为什么搭配非阻塞?
  • 六、`reactor`在中间件的运用
    • 1、`redis`
    • 2、`memcache`
    • 3、`nginx`

一、网络IO的职责

操作IO

IO的操作方式

  • 阻塞IO
  • 非阻塞IO

通过fcntl修改fd是阻塞还是非阻塞,之后所有使用这个fd的系统调用都会表现为非阻塞

阻塞与非阻塞IO的具体差别:

数据在未就绪的处理

阻塞IO在系统调用中的流程

read阻塞IO调用流程:先去检测内核缓冲区有无数据,如果没有数据则阻塞等待,当客户端给我们发送数据了,然后我们的网卡驱动往里面内核缓冲区填充数据,然后阻塞的系统调用才会把阻塞去掉,然后就拷贝,然后系统调用就返回

write阻塞IO调用流程:先去检测内核缓冲区是否可以写数据(是否是满的),如果是满的,则就阻塞,一直等到网络协议栈把内核缓冲区数据发送出去了之后他有剩余空间了才会停止阻塞,将数据写进内核缓冲区,返回实际写入内核空间的字节数

**accept阻塞IO调用流程:**检查全连接队列中是否有节点,如果没有则一直阻塞等待,如果有则取出返回分配的客户端fd

非阻塞IO在系统调用中的流程

不管内核缓冲区是否可写可读,都直接返回。在必要情况会设置errno

网络编程系统调用具备检测和操作的功能

accept

  • 检测:全连接是否还有未处理的连接信息
  • 操作:从全连接队列中取出连接去获取并生成客户的fd以及ip端口信息

read

  • 检测:内核缓冲区是或否有数据
  • 操作:将内核缓冲区数据拷贝到用户态缓冲区来

write

  • 检测:内核缓冲区是否可以写数据(满了吗)
  • 操作:将用户态缓冲区拷贝到内核态缓冲区

二、系统调用在调用非阻塞IO的具体处理

connect

  • 多次调用connect,其返回值和**设置的errno**可能是不一样的,当errnoEISC-ONN时,就告诉我们连接已经建立成功了,如果是第一次调用connect的时候会返回EINPROGRESS,告诉我们正在建立连接

accept

  • 如果全连接队列中为空(客户端没有与服务器进行连接),那么accept会返回-1,并且errno会设置为EWOULDBLOCK,告诉我们全连接队列为空

read

  • read调用查看errno
    • read = -1 && errno = EWOULDBLOCK:代表读缓冲区还没有数据
    • read = -1 && errno = EINTR:代表可以读的时候被中断了

write

  • write调用查看errno
    • 同上 不过是代表缓冲区满了

三、网络中连接断开的情况

主动关闭

  • shutdown关闭一端
  • close是关闭两个端(首先是回收资源,然后读写端相应的会关闭)
  • 客户端写端关联服务端读端服务端读端关联客户端写端
    • 所以客户端调用shutdown关闭读端时,服务端对应的写端就会关闭(需要自己去判断)

被动关闭

  • read 返回 0:告诉我们连接断开了,更准确的说是服务端的读端关闭了

  • write返回-1 && errno = EPIPE:告诉我们服务端的写端关闭了

  • 为什么要区分读端关闭还是写端关闭

    • 因为有的服务器框架需要支持半关闭状态,
      • 如果是读端关闭了,服务器还可以将这个连接未发送出去的数据调用write发送出去,对应四次回收中对端发送fin包之前的时间段。

四、分离检测与操作

accept亲自检测全连接队列中是否有节点,而是使用io多路复技术来检测io是否就绪,保证当执行到accept时一定是io就绪的状态

并且io多路复用可以同时检测多个io的就绪状态

epoll为例介绍io多路复用

建立连接

  • io检测接收连接的处理流程

    • socket
    • bind&listen
    • 监听读事件(epollin
      • epoll如何监听的!epoll会监听listenfd的读事件,当我们连接监听成功的时候,全连接队列上会多一个节点,这个节点就会发送一个信号EPOLLIN)来告诉epoll(也可以是select、poll),就会触发读事件,就说明接收连接的io已经就绪,于是后续就调用accept,而现在accept则只会进行操作而不会进行检测,因为io多路复用已经做好了检测
  • io检测主动连接的处理流程

    • 服务器作为客户端 连接一些服务比如mysql

    • 监听写事件(epollout

      • 服务器调用conncet发送SYN包,此时状态是EINPROGRESS,如何由io多路复用来检测这个连接是否建立成功,io多路复用会检测三次握手的最后一次握手是否发送,当三次握手的最后一次握手发出时会发送一个信号,告诉epoll写事件触发了(所以当epoll监听到写事件触发的时候就说明我们的连接建立成功了)

      • 下面是一个epoll负责connect的检测的小demo

        // 设置sockfd非阻塞
        connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
        
        epollfd = epoll_create(EPOLL_SIZE);  // 10
        
        event.events = EPOLLOUT;
        event.data.fd = sockfd;
        ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &event);
        
        epoll_wait(epollfd, events, EPOLL_SIZE, -1);  // -1代表阻塞
        
        if (events[0].events & EPOLLERR || events[0].events & EPOLLHUP) {
            printf("connect failed\n");
            exit(EXIT_FAILURE);
        }
        
        else if (events[0].events & EPOLLOUT) {
            printf("connect success\n");
        
            // 发送一条消息
            ret = write(sockfd, send_buf, strlen(send_buf));
            if (ret == -1) {
                perror("write");
                exit(EXIT_FAILURE);
            }
        }
        

连接断开

通过判断event[i].events来对客户单断开连接进行检测

  • EPOLLRDHUP:表示服务器读端关闭
  • EPOLLHUP:表示服务器读写端都关闭

消息到达

  • 客户端fd触发EPOLLIN(检测),然后调用read(操作)

消息发送

  • 客户 端fd触发EPOLLOUT(检测),然后调用write/send(操作)

epoll详解

epoll_create会创建一个红黑树和一个双端队列

epoll_ctl是对红黑树增删改,同时会跟网卡驱动建立回调关系,响应事件触发时会调用回调函数,这个回调函数会将触发的事件拷贝到rdlist双向链表中;

调用epoll_wait将会把rdlist中就绪事件拷贝到用户态中(拷贝出来后会清空内核态就绪队列的事件)

介绍epoll_wait的四个参数接口

  • 参数

    • epoll文件描述符

    • 用户空间的数组,用来接收内核空间中就绪队列的东西

    • 预期要拷贝多少事件

    • 定时(以毫秒为单位)

      • epoll是一种同步的io,没有阻塞与非阻塞之说,但是可以通过timeout来控制函数的阻塞,

        设置为-1则会表现为阻塞(就绪队列没有事件就一直阻塞),

        设置为0会表现为非阻塞(就绪队列)

  • 返回

    • 返回实际取出来多少事件

五、引入Reactor

是将对io的操作转化为对事件的处理

什么是reactor?

一个服务器有很多io,我们将他们递交给epoll内核管理,一旦io上面有事件(可读或者可写),就触发事件(可读或者可写)对应的回调函数处理业务。每个fd都有一个对应的结构体,里面保存了一些必要信息(回调函数,独立的读写缓冲区)

为什么要有reactor

reactorepoll的基础上面增加了什么,有了哪些好处

  • epoll:他是对io的管理
  • reactor:是对事件的管理,不同事件对应于不同的回调函数
  • 由于sock_item封装,对未处理完的事件,放到一个独立(独立于其他的fd(客户端))的buffer里面
    • 好处?
      • 比如实现了 一个httpserver,我们的recv只能收1024,但是get请求有2000个字符,每次接收都是放在fd自己的buffer里面,不会被其他的fd的输入所影响

reactor为什么搭配非阻塞?

为什么io多路复用帮我们检测好了io就绪,为什么还要使用非阻塞io,考虑一下三种情况

  • 多线程环境

    • 多个线程使用同一epoll监听同一fd

      当这个fd上有io的时候,就绪队列的节点会向每一个线程的epoll通知这里有新的连接建立。

      如果使用阻塞的io,则当一个线程的epoll_wait将它取出来后其他线程的epoll_wait就会阻塞在那儿

      **惊群!:**有一个海王,有很多个女朋友,每一个都叫老婆,有一次他同时碰到了这些女朋友,他交了医生老婆。结果他们都答应了

  • 边缘触发情况下

    • 调用epollwait,在客户端fd发生io事件的时候取出rdlist中的对应节点,后续不管readbuf还有没有数据都不会自动的把该fd放入rdlist中(而lt是会在readbuf还有数据时自动的放入rdlist,再让epollwait触发)

      在边缘触发下,readbuf必须一次性将数据读空,不然会出现类似tcp的粘包,如果fd设置为阻塞,则readbuf在已经读空的情况下不会返回而是继续阻塞,所以要设置为非阻塞来判断read读完

  • select bug

    • 当某个socket接收缓冲区有新数据分节到达,然后select报告这个socket描述符可读,但随后,协议栈检查到这个新分节检验和错误后丢弃了这个分节,这时候调用read无数据可读,如果socket没有设置非阻塞,那么read就会阻塞当前线程

六、reactor在中间件的运用

1、redis

使用单reactor

  • 具体环境

    • kv、数据结构、内存数据库

      通过key来查valuevalue可以支持种数据结构,这些数据的操作都是在内存上操作

    • 命令处理是单线程的

  • redis为什么要使用单reactor

    • 只能采用单reactor,因为核心业务逻辑是单线程的。
    • 去操作数据结构的时候很简单,操作命令时间复杂度比较低
  • redis是怎么处理的reactor

    大致流程

    • 创建listenfd
    • bind listen
    • listenfd注册读时间
    • 读事件触发回调accept
    • clientfd注册读事件
    • 读事件触发
  • redis针对reactor做了哪些优化

    • 开启多线程,将读数据和解协议绑定在一起放在另一个线程,将组包和写数据放在另一个线程处理,中间的业务逻辑还是放在主线程操作

2、memcache

reactor

  • 针对reactor做了哪些优化

主线程中有一个专门用于接收连接的reactor,用多少个连接也就有多少个子reactor,来一个连接创建一个线程,线程与线程的交互是使用pipe来交流的

3、nginx

reactor

多进程,每个进程有自己的epfd,监听同一listenfd,用户层处理惊群是为了可以自定义负载均衡

本专栏知识点是通过<零声教育>的系统学习,进行梳理总结写下文章,对c/c++linux课程感兴趣的读者,可以点击链接,详细查看详细的服务:

你可能感兴趣的:(服务器,网络,运维,linux)