程序采用Reactor模式 + 固定线程池的方法来提高并发度,避免了线程频繁创建带来的开销,使用epoll来实现IO多路复用的方式。
该服务器中主要包含三类线程:
- IO线程(即主线程负责网络IO)
- 计算线程(即线程池线程负责计算)
- 第三方线程(即本程序中的Log线程)
本程序中除了Log线程,都采用One loop per thread(即EventLoop)的方式进行事件的循环。
并发模型如下图:
MainReactor只有一个,其采用非阻塞的socket监听描述符,来负责响应client的连接请求,并建立连接,之后采用Round Robin的方式分配给某个SubReactor,这里会涉及到跨线程任务分配,需要进行加锁,而锁是由每个EventLoop创建的,其只会被该线程和主线程竞争。
SubReactor可以有一个或者多个其与线程池中线程数相等,每个subReactor都会在一个独立线程中运行,并且维护一个独立的非阻塞IO Epoll事件分发器。
当主线程把新连接分配给某个SubReactor时,如何实现对该线程的告知呢?这里学习了muduo库中实现的方法,通过runInLoop和queueInLoop的方法,将需要处理的事件注入其任务队列中,然后采用eventft来进行线程间的异步唤醒,当该线程调用epoll_wait时,便可以立即醒来处理事件。这样我们便可以只需绑定好回调函数即可,大大提高了灵活性。
Epoll主要有ET与LT两种工作模式,其中ET只能应用于非阻塞IO而LT适用于阻塞与非阻塞两种IO,其中ET模式较LT模式更复杂一些,需要用户每次处理请求时,必须处理完毕。这里我采用了ET模式,对于accept请求时,一直读取到请求完毕;而对于读取数据时,采用readv进行一次读取完毕。
每个SubReactor都持有一个定时,处于处理超时请求和长时间不活跃的连接。这里我主要采用了stl中的map来实现,超时时间作为key,对应的connection连接作为值,并使用了timerfd时间文件描述符来进行超时管理,使得其可以添加到Epoll事件分发的框架中,整体框架更加一致。
程序中主要包含三个类:EventLoop、Channel、Epoll、Connection,其事件处理流程如下:
- EventLoop类:One loop per Thread则表示每个线程都有一个EventLoop对象,其一直处于时间循环中,主要负责调用Epoll进行事件收集和拿到活跃事件后将事件分发到各个Channel以及处理任务队列。
- Epoll类:其主要进行epoll_create、epoll_ctl和epoll_wait这样的系统调用,进行活跃事件的收集以及关心事件的添加与删除等操作。
- Channel类与Connection类:一个Connection对象包含一个Channel对象,而一个TCP连接由一个Connection对象负责,其中Connection类中主要实现了处理TCP连接的各种回调函数(读写事件、解析HTTP信息等),并将其注册到Channel类中;而Channel类主要与带有读写时间的对象(eventfd、listenfd、connection、timerfd)进行关联,当IO事件活跃时,会根据事件的类型(读写错误)回调相应的函数,实现事件的处理。
Log的实现了学习了muduo,Log的实现分为前端和后端,前端往后端写,后端往磁盘写。为什么要这样区分前端和后端呢?因为只要涉及到IO,无论是网络IO还是磁盘IO,肯定是慢的,慢就会影响其它操作,必须让它快才行。
这里的Log前端是前面所述的IO线程,负责产生log,后端是Log线程,设计了多个缓冲区,负责收集前端产生的log,集中往磁盘写。这样,Log写到后端是没有障碍的,把慢的动作交给后端去做好了。
后端主要是由多个缓冲区构成的,集满了或者时间到了就向文件写一次。采用了muduo介绍了“双缓冲区”的思想,实际采用4个多的缓冲区(为什么说多呢?为什么4个可能不够用啊,要有备无患)。4个缓冲区分两组,每组的两个一个主要的,另一个防止第一个写满了没地方写,写满或者时间到了就和另外两个交换指针,然后把满的往文件里写。
与Log相关的类包括FileUtil、LogFile、AsyncLogging、LogStream、Logging。 其中前4个类每一个类都含有一个append函数,Log的设计也是主要围绕这个append函数展开的。
- FileUtil是最底层的文件类,调用了底层标准IO,封装了Log文件的打开、写入并在类析构的时候关闭文件,该append函数直接向文件写。
- LogFile进一步封装了FileUtil,并设置了一个循环次数,每过这么多次就flush一次。
- AsyncLogging是核心,它负责启动一个log线程,专门用来将log写入LogFile,应用了“双缓冲技术”,其实有4个以上的缓冲区,但思想是一样的。AsyncLogging负责(定时到或被填满时)将缓冲区中的数据写入LogFile中。
- LogStream主要用来格式化输出,重载了<<运算符,同时自己也有一块缓冲区,用于存放多个<<的结果。
- Logging是对外接口,Logging类内涵一个LogStream对象,主要是为了进行格式化的信息打印,比如打log的行、文件名等信息。