One Loop Per One Thread服务器通信逻辑梳理

Reactor、Connection、Accept、EventLoop模块关系梳理

总体逻辑总结

  • 主Reactor负责监听新连接,监听到的新连接分发给子Reactor
  • 子Reactor则通过EventLoopPoller处理具体的读写异常事件
  • Channel负责将套接字的事件封装并注册到EventLoop
  • BufferConnection负责处理数据,负责数据的接收发送以及协议解析
  • TimerQueue管理定时任务,确保EventLoop可以执行定时任务,如果连接超时则进行释放

主Reactor与子Reactor之间交互(主Reactor接收新连接并通知子Reactor)

  • Acceptor使用监听套接字等待新的连接请求
  • 新连接到来后Acceptor创建一个新的Socket来处理该连接
  • Acceptor为新的Socket创建一个Channel,同时将其注册到EventLoop 中 &
  • EventLoop(主Reactor)将新连接的处理任务,分发到子Reactor中,并向子Reactor的eventfd中写入数据唤醒子Reactor处理新连接的到来 

子Reactor处理连接逻辑

  • 子ReactorEventLoop通过poller等待关心的事件发生
  • poller监控的事件发生时,通知该连接对应的Channel
  • Channel则执行注册的回调函数,处理具体的读写操作
  • Connection模块则处理数据的接收、发送以及协议解析 

定时任务处理逻辑

  • EventLoop中包含有TimerQueue,专门负责管理定时任务
  • 定时任务到期后,TimerQueue通知EventLoop模块执行任务,处理掉超时连接 

各个模块主要作用总结

  • 主Reactor:运行在主线程中,负责监听新连接,通过eventfd来通知子Reactor处理新连接
  • 从Reactor:独立线程中运行,通过EventLoop和Poller处理具体事件
  • EventLoop:Reactor的核心,通过Poller监控事件,Channnel则负责具体处理事件
  • Poller:负责管理某个Reactor中所有的CHannel
  • Channel:封装文件描述符以及对应处理方法,通过回调函数来处理I/O操作 

主从Reactor服务器通信逻辑分析

总体概述(下面展开说明具体步骤)

  • 初始化TcpServer:设置通信端口创建Acceptor LoopThreadPoll
  • 启动TcpServer:创建从Reactor线程池,同时启动主Reactor的事件循环
  • 接收新连接:Accept接收新连接,然后调用TcpServer::NewConnection函数
  • 分发新连接:Tcpserver::NewConnection创建新的Connection对象,并将新创建的连接分发给从Reactor中
  • 初始化:通过连接模块中的Established函数设置连接状态,并启动读事件监控
  • 处理读事件:当有数据可读时,epoll通知相应的EventLoop,然后调用Connection::HandleRead处理数据

  1. 初始化TcpServer

  • 构造函数接收到服务器设置的端口号后
    • 初始化Acceptor 和 LoopThreadPool对象
  • 设置主Reactor接收客户端连接后的执行逻辑,通过_accept.SetAcceptCallback设置连接回调函数为TcpServer::NewConnection(也就是把Accept类中的_accept_callback回调函数设置成了TCPServer对象的新连接处理函数
  • 调用_acceptor.Listen()函数启动监听套接字的读事件监控

 Acceptor::Acceptor

  • Acceptor构造函数中,会创建一个服务器套接字,然后初始化一个Channel对象
  • 然后利用_channel.SetReadCallback设置读事件回调函数

 

2. 启动TcpServer(创建主从Reactor) 

  • _poll.Create():创建从Reactor线程池
  • _baseloop.Start():启动主Reactor的事件循环机制

从属Reactor线程池:创建线程池后,为每一个线程创建LoopThread对象后,然后启动线程

LoopThread中利用ThreadEntry方法创建EventLoop对象,然后启动事件循环,该方法也是从Reactor线程的入口函数(为防止多线程争夺资源,此处需要用锁)

 

 EventLoop::Start:启动事件循环,通过poller获取就绪事件,然后调Channel::HandleEvent函数处理这些事件,然后执行任务队列的所有任务

 

 Poll方法则是利用epoll_wait系统调用,获取就绪事件,然后将这些事件存储在active列表中

 

 3. 接收新连接

  • 客户端发起对服务端的连接,新连接到达后,Accept则触发读事件,然后调用之前设置的回调函数HandleRead方法处理新连接
  • HandleRead则通过accept获取新的连接,同时调用_accept_callback回调函数处理新连接(也就是执行了新连接到来后的处理逻辑

 

accept系统调用获取新连接

4. 分发新连接 

  • NewConnection方法创建新Connection对象,然后通过LoopThreadPool::NextLoop函数,从池子里选择一个从Reactor的EventLoop对该连接进行管理
  • 设置该连接的各种回调函数

 

5. 选择一个从Reactor处理连接 

  • NextLoop方法实现轮询调度,从线程池的中选择下一个EventLoop

 

6. 初始化连接Connection 

(注意,初始化连接操作是在分发连接之前完成的,单独分析)

  •  初始化各种变量过程不解释,重点分析Established函数的运行逻辑
  • Established函数,将EstablishedInLoop方法包装秤一个任务,同时通过EventLoop::RunInLoop方法执行

 

RunInLoop函数,检查当前是否在EventLoop线程中,如果在当前线程中,则直接执行回调函数cb,如果不在线程中,则将回调函数放入任务队列中

 

 QueueInLoop函数将回调函数添加到任务队列_tasks中,然后通过WeakUpEventFd方法唤醒EventLoop(也就是唤醒其他线程处理任务)

 

7. 处理连接 

(也是在初始化连接步骤完成,同时结合第6步一同分析)

 EnableRead函数将读事件添加到_events中,然后调用updata方法进行监控。首先是利用updata函数将channel对象更新到EventLoop中,然后将channel对象添加到epoll中进行管理

 

8. 事件循环处理连接的读事件 

  • 新连接有数据可读的时候epoll会通知对应的EventLoop
  • 然后调用CHannel的HandleEvent方法进行处理 

Channel类作用分析

Channel类与Reactor中其他模块关系梳理

Reactor中EventLoop、Channel、Poller三者关系总结

  • EventLoop类通过Poller类管理Channel的事件监控,当事件触发的时候调用Channel设置的回到函数
  • Poller通过epoll监控文件描述符事件,也就是需要关心的读写事件,有事件响应后则就将事件分发给Channel进行处理

 新连接到来后Channel创建过程分析

接收新连接:通过accept获取一个新的文件描述符

 EventLoop将CHannel注册到Poller中,监控对应的事件

 

Poller中如果发现监控的事件发生时,则通知对应的Channel来处理事件 

 

CHannel根据触发事件类型,调用相应回调函数进行处理 

 

就绪事件处理逻辑

poller类检测到事件发生后,并将其设置为就绪事件 

 

EventLoop获取并处理就绪事件 

 

channel 处理已就绪事件,与上面逻辑相同 

eventfd系统调用(线程通知)

  • 参数
    • initval:文件描述符初始值,通常都是设置为0
    • flags:下列参数支持 按位或操作
      • EFD_SEMAPHORE:启动信号量
      • EFD_NONBLOCK:文件描述符非阻塞状态
      • EFD_CLOEXEC:在执行exec()的时候关闭文件描述符
  • 返回值
    • 成功:新的文件描述符
    • 失败:返回-1 

eventfd实现线程通知实验代码

#include <sys/eventfd.h>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
#include <pthread.h>

int efd;

void *thread_func(void *arg) {
    uint64_t u;
    ssize_t s;

    printf("Thread: Waiting for event...\n");
    s = read(efd, &u, sizeof(uint64_t));
    if (s != sizeof(uint64_t)) {
        perror("read");
        return NULL;
    }
    printf("Thread: Received event, counter = %llu\n", (unsigned long long) u);
    return NULL;
}

int main() {
    pthread_t t;

    efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
    if (efd == -1) {
        perror("eventfd");
        return -1;
    }

    if (pthread_create(&t, NULL, thread_func, NULL) != 0) {
        perror("pthread_create");
        return -1;
    }

    sleep(1);  // Simulate work
    printf("Main: Triggering event\n");

    uint64_t u = 1;
    ssize_t s = write(efd, &u, sizeof(uint64_t));
    if (s != sizeof(uint64_t)) {
        perror("write");
        return -1;
    }

    pthread_join(t, NULL);
    close(efd);
    return 0;
}

内部实现了解

  • eventfd内部有一个计数器,当调用write的时候,计数器增加,当调用read的时候,计数器则会减少。如果计数器为0,则读操作会阻塞,直到有数据写入。
  • 一个线程可以在eventfd上等待事件,当另外一个线程向eventfd中写入数据的时候,等待的线程会被唤醒,从而实现线程之间通信,也可以用于一个Reactor唤醒另一个Reactor

eventfd与Reactor模式

含义

  • Linux内部提供的一种机制,主要用于进程或者线程之间的事件通知

主要作用总结

  • 事件通知:在事件循环机制中,eventfd会创建一个文件描述符,然后向该文件描述符写入数据,触发事件通知
  • 唤醒事件循环:当EventLoop在等待某件事件发生的时候,例如等待I/O或者定时器事件,如果需要从其他线程中向EventLoop中添加任务或者事件。此时就可通过eventfd创建的文件描述符中写入数据来唤醒EventLoop,从而可以及时处理新的任务。
  • 线程间通信:通过该系统调用返回的文件描述,一个线程向该文件描述符中写入消息,通知运行在另一个线程的EventLoop执行某些操作

主从Reactor服务器中eventfd作用:确保EventLoop可以及时响应新连接或者其他需要立即处理的任务,通过eventfd的唤醒机制。(可以简单理解为主Reactor唤醒从Reactor的一种方式,主要还是怕从Reactor偷懒) 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值