Reactor网络模型下的部分编程要点

36 阅读4分钟

Ractor

  • 一种同步网络模型,以IO多路复用和非阻塞IO实现。将对IO的操作转为为各类事件的操作(多数为读就绪事件和写就绪事件)
  • 多路复用实现对IO的监测
  • 非阻塞IO实现对IO的操作

封装过程

  • 关联事件及其对应的处理器
    • 1.对新到连接的处理
    • 2.对被动断开的处理
    • 3.对读事件的处理
    • 4.对写事件的处理
  • 事件控制接口
    • 1.注册事件
    • 2.注销事件
    • 3.改变事件
  • 事件循环

编程过程

为什么使用非阻塞IO?

非阻塞IO的特点是在数据未准备好(未从内核拷贝到协议栈)时,就可以返回当前操作的结果。而阻塞IO会直到数据准备好才返回。在多路复用中如果出现了阻塞的现象,那势必影响到其他fd的事件循环处理。

IO函数在操作IO的时候会返回错误码,可以根据错误码对网络状态进行分析,来选择对应的处理方式。所以说IO函数本身也可以用来监测IO的状态,也可以用来操作IO读写

  • connect
    • errno = EINPROGRESS 表示正在建立连接
    • errno = EISCONN 表示连接建立成功
  • write = -1
    • error = EPIPE 表示写端被关闭,写失败
    • errno = EWOULDBLOCK 表示当前写缓存区写不下这么多
  • read = -1
    • errno = EWOULDBLOCK 表示当前读缓存区为空,数据未准备好
    • errno = EINRT 表示系统发生了中断,正在处理其他事务
    • errno = other 都属于非良性的故障码,直接关闭连接

连接的建立

  • 处理来自客户端的链接
    int epollfd = epoll_create(1); 
    struct epoll_event events[POLL_SIZE] = {0};

    // 注册监听listenfd的可读事件
    struct epoll_event ev;
    ev.events = EPOLLIN;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, listen, &ev);
    
    // 触发可读事件时,通过accept进行接收
    int client = accept(listenfd, addr, sz);
    struct epoll_event ev;
    ev.events = EPOLLIN;  // 给新的链接绑定可读事件,如果绑定可写事件,则必然会触发
    epoll_ctl(epollfd, EPOLL_CTL_ADD,clientfd, &ev);
    
    // 进入事件循环 timeout = 0表示非阻塞, timeout = -1表示永久阻塞
    epoll_wait(fd, events, maxevents, timeout);
  • 处理连接到外部的请求
int confd = socket(AF_INET, SOCK_STREAM, 0 );
connect(confd, &remoteAddr, sz);

// 注册当前fd的写事件
struct epoll_event ev;
ev.event = EPOLLOUT;
epoll_ctl(epollfd, EPOLL_CTL_ADD, confd, &ev);

//事件循环如果触发可写,则说明可以发送请求,即连接成功过

连接断开

需要处理的场景,必然是对端主动请求关闭链接,调用close或者shutdown。shutdown可以选择关闭读端或写端,触发EPOLLRHUP(读端被关闭事件),EPOLLHUP(读写端都关闭);close的调用会引发四次挥手。

  • 对close的处理过程,在被动方进入close_wait的时候协议栈会向读缓冲区写入文件结束符EOF,此时可以通过read=0去感知close请求
if(read == 0)
{
    before_close(); // 关闭连接前的一些操作
    close(fd);
}
  • 对shutdown的处理
if(events[i].event  == EPOLLRHUP)
{
     // 读端关闭时,还可以写入
     close_read();
     close(fd);
}
if(events[i].event == EPOLLHUP)
{
    before_close(); // 关闭连接前的一些操作
    close(fd);
}

数据到达

即触发了可读事件

if(events[i] == EPOLLIN)
{
    while(1)
    {
        // 这里使用非阻塞IO进行IO操作
        int n = read(fd, buf, sz);
        if(n < 0)
        {
        
        }
        else if( n == 0)
        {
            close(fd);
        }
    }
}

数据发送

触发了可写事件

if(events[i] == EPOLLOUT)
{
    int n = write(fd, buf, sz);
    if(n == -1)   // 写入失败
    {
        if(errno == EINTR)
            continue;
        if(errno == EWOULDBLOCK)
        {
            // 写缓存空间不足,此时需要给当前fd重新注册写事件,下次事件循环时再尝试写
            struct epoll_wait ev;
            ev.events = EPOLLOUT;
            epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev);
        }
        else
        {
            close(fd);
        }
    }
    else if(n > 0)   // 触发写事件,且写入成功
    {
        // 写成功之后删除当前写事件,减少cpu符合
        // 是否需要写入根据实际请求判断即可,没必要一直对事件进行监听
        epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);
    }
}

reactor的应用

  • 单线程应用reactor---redis
    • redis对单reactor模型进行了对事件响应进行了优化。采用单线程read多线程write,提高了事件循环的效率
    • 为什么read单线程,write多线程?跟这两个函数的效果有关:read是读取预期数量的字节数,返回实际读取到的字节数,一次read可能读不全,如果采用多线程,可能单个线程取不到完整的请求包,影响业务逻辑;write是尝试写入指定字节数,如果成功,则全部写入,如果失败,则一个都不写入,所以不存在多线程写入错乱的问题。
    • 事件响应的分配采用round robin的方式。保证负载均衡
  • 多线程应用reactor---memcached
    • CPU有N核就使用N个reactor对象(one eventloop per thread,每个线程都有一个事件循环),其中专门分配一个reactor去处理accept连接,提高了新到连接的响应速度,也避免了accept的惊群现象(新版本的linux内核中已经实现了惊群避免?)
    • 由于是多线程操作,适用于加锁粒度小的场景,不影响效率
  • 多进程应用reactor---nginx
    • 主进程创建listenfd
    • fork子进程,子进程可以共享主进程创建的listenfd
    • 在用户层通过共享队列加锁实现线程同步,保证只有一个线程成功accept,避免了惊群现象
    • 取得锁的线程在成功accept之后,对clientfd进行事件绑定
    • nginx主要实现的反向代理,没有解析请求包的需求,只需要实现转发功能,所以在这里采用的是epoll的边缘触发模式,可以更快的响应到新到的请求