对于每一个http请求(client),会有一个fd去处理,v1版本的做法是,每当请求client到达时(即epoll检测到活动事件时),将此活动fd的任务转交给线程池中的线程去处理
版本一的框架如下图所示:
这里每次有新任务到达时,就将新任务转交给线程,然后把程序的控制权转交到Epoll上,但是当在高并发状态,连接数太高,就会有很多活动的事件fd, 这是频繁的转交线程也会造成很大的开销
所以Version的初步设想是,每当有一个client到达时,利用线程池中的线程去负责这个client的fd, 然后由主线程中的epoll去通知这个线程何时开始处理任务,这样单个线程就可长期监视一个fd, 主线程就不用像Version 1一样,需要频繁地分配任务
Note: 主线程应该如何通知单个线程要开始处理任务了呢?
- 也就是采用线程间的通信方式(临界区、信号量、事件信号、互斥量)
- 采用eventfd 的可读事件作为线程间的唤醒机制(类似于无名管道)
由此,初步设想已经完成,但是细想发现又不是很现实,比如,一个线程去负责一个client,那在高并发状态下,这是不可能完成的任务,
简单想一想,让一个线程负责多个文件描述符很简单,用一个map维护即可,这样就可让单个线程 负责 多个文件描述符
此时的框架图如下图所示:
但是直接这样就 单个线程负责多个文件描述符 是不现实的,比如,A线程负责了 a3和a4两个文件描述符,如果主线程应该如何通知 A线程 去处理 a3和a4两个活动fd呢,这里虽然可以想办法实现(将发生活动事件的fd 作为数据写入eventfd,然后再唤醒线程),但是有没有更好的办法呢。
进一步的思考时 如何让单个线程彻头彻尾的负责多个文件描述符,彻尾很简单,负责文件描述符的close即可。彻头就是要去负责监视文件描述符的活动事件,也就是由主线程通知A线程,你应该负责a3 这个文件描述符,A线程中也应该创建epoll对象,将a3需要监听的事件加入进来。往后,可继续添加多个文件描述符。 其实这一步相当于对之前的思路做了一个调整,之前是想让主线程去监听所有事件,现在主线程只负责accept请求,然后将accept的对象的控制权转交给其余线程,至此,版本二的设想已经基本完成
版本二的基本框架如下图所示:
为了简化处理,接下来的版本V2_1 将完成单线程的http server
和多线程的http server相比,区别主要在与单线程的Server在Acceptor后并不会转交给其他线程,而是在主线程内添加需要监视的文件描述符,所以在完成单线程server的基础上,只要加上 “转交” 的代码即可修改成多线程的server
单线程Server(v2_0)的主要代码可见v2_0 分支,接下来的 Server(v2_1)主要基于v2_0修改,v2_0已经是一个很基础的单线程http server了。
在版本v2_0中,代码太过于集中,不利于维护和修改,因此才有了版本v2_1,本版本借鉴了muduo完成
V2_1 打算新建一个Epoll类用于 IO复用,一个Acceptor用于建立新连接,TcpConnection用于管理接收到的连接,同时建立一个TcpServer,用于管理Acceptor和TcpConnection
对于每一个文件描述符,这里按照muduo的思路,新建了一个Channel类来管理 IO事件,然后用一个EventLoop对象来管理整个程序
// loop对象的主要代码
while (!quit_)
{
activeChannels_.clear();
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
for (auto it = activeChannels_.begin(); it != activeChannels_.end(); ++it)
{
(*it)->handleEvent(pollReturnTime_);
}
// 在单线程server中, 直接用上面的handleEvent 是不能处理所有的任务回调的, 比如无法处理 Channel的析构,所以析构要在下面的doPendingFunctors中处理
doPendingFunctors();
}
EventLoop是最主要的,用来管理整个IO事件,同时所有的事件回调,对象的析构都在loop中完成。
EventLoop主要提供两个接口用来处理事件的回调,runInLoop
和queueInLoop
, 这两个函数是必须的,缺一不可。首先,queueInLoop是必须的,负责对象的析构,而且其调用时间点是固定的,在上面的代码段的doPendingFunctors()
这里,然而,应该如何完成IO事件的回调呢,queueInLoop虽然可行,但是会回调却不是同步进行的,先看一下IO事件的回调的调用时间点(*it)->handleEvent(pollReturnTime_)
,由此为了让IO事件同步进行,所以添加了runInLoop这个新函数,同时,runInLoop的调用还可以发生在启用loop循环之前,可用于设置EventLoop用于管理的对象
// 这个函数可以在loop启动之前调用的,用于设置channel或者绑定IO事件等,例如套接字的监听
void EventLoop::runInLoop(const Functor &cb)
{
cb();
}
//
void EventLoop::queueInLoop(const Functor &cb)
{
pendingFunctors_.push_back(cb);
}
至此,单线程的http server已经完成
此版本在单线程server的基础上,添加线程间通信便可以实现多线程server
具体方案是:利用互斥锁管理EventLoop的functors,同时利用Eventfd实现了线程的异步唤醒,这样就可以轻松的跨线程调配任务
// 添加了下面的函数用于判断是否是同一个线程
bool isInLocalThread() const
{
return this->threadId_ = std::this_thread::get_id();
}
关于异步唤醒机制,AsyncWaker类