【C++】一个基于Reactor的多线程Web服务器

Description:

C++编写的web服务器,借鉴了《muduo网络库》的思想;使用了Reactor并发模型,非阻塞IO+线程池;解析了get、head请求;并实现了异步日志,记录服务器运行状态。
详细代码可见: https://github.com/whjkm/Web_Server

Architecture:

I/O 多路复用(事件分配器) + 非阻塞I/O + 主线程(处理请求)+ 工作线程(读、计算、写) + eventloop,即Reactor反应堆模式。
[外链图片转存失败(img-KzNVQvZq-1564494938165)(./images/Architecture.png)]

Reactor:

Reactor设计模式是event-driven architecture的一种实现方式,处理多个客户端向服务端请求服务的场景。每种服务在服务端可能由多个方法组成。Reactor会解耦并发请求的服务并分发给对应的事件处理器来处理。
在这里插入图片描述

MainReactor只有一个,负责响应client的连接请求,并建立连接,它使用一个NIO Selector。在建立连接后用Round Robin的方式分配给某个SubReactor,因为涉及到跨线程任务分配,需要加锁,这里的锁由某个特定线程中的loop创建,只会被该线程和主线程竞争。

SubReactor可以有一个或多个,每个SubReactor都会在一个独立线程中运行,并且维护一个独立的NIO Selector。当主线程把新连接分配给了某个SubReactor,该线程此时可能正阻塞在多路选择器(epoll)的等待中,怎么得知新连接的到来呢?这里使用了eventfd进行异步唤醒,线程会从epoll_wait中醒来,得到活跃事件,进行处理。

本项目中的Reactor主要由以下几个部分构成:

  • Channel {Channel.h,Channel.cpp}
  • Epoll {Epoll.h,Epoll.cpp}
  • EventLoop{EventLoop.h,EventLoop.cpp,EventLoopThread.h,EventLoopThread.cpp,EventLoopThreadPoll.h,EventLoopThreadPool.cpp}

Channel: ChannelReactor结构中的“事件”,它自始至终都属于一个EventLoop,因此每个Chanenl对象都只属于某一个IO线程;负责一个文件描述符的IO事件的分发,但它并不拥有这个文件描述符;在Channel类中保存IO事件的类型对应的回调函数,当IO事件发生时,最终会调用到Channel类中的回调函数。因此,程序中所有带有读写时间的对象都会和一个Channel关联,包括loop中的eventfdlistenfdHttpData等。

EventLoop: One loop per thread 顾名思义每个线程只能有一个EventLoop对象;EventLoop即是时间循环,每次从poller里拿活跃事件,并给到Channel里分发处理。EventLoop中的loop含糊是会在最底层(Thread)中被真正调用,开始无限的循环,直到某一轮的检查到退出状态后从底层一层一层的退出。

EventLoopThread: IO线程不一定是主线程,我们可以在任何一个线程创建并运行EventLoop。一个程序也可以有不止一个IO线程,我们可以按优先级将不同的socket分给不同的IO线程,避免优先级反转。EventLoopThread会启动自己的线程。

EventLoopThreadPoolEventLoop线程池,每一个EventLoopThread就是一个SubReactor,按照轮询的方式分发请求。

Epoll: EpollIO mutilplexing的封装。

Log:

多线程异步日志库:log的实现分为前端和后端,前端往后端写,后端往磁盘写。为什么要这样区分前端和后端呢?因为只要涉及到IO,无论是网络IO还是磁盘IO,肯定是慢的,慢就会影响其他操作。

这里的Log前端就是前所述的IO线程,负责产生log,后端是Log线程,设计了多个缓冲区,负责收集前端产生的log,集中往磁盘写。这样Log写到后端是没有障碍的,把慢的动作交给后端去做好了。

后端主要是由多个缓冲区构成的,缓冲区满了或者时间到了就向文件写一次。采用了muduo介绍的“双缓冲区”思想,实际采用4个多的缓冲区。4个缓冲区分两组,每组的两个一个为主要的,另一个防止第一个写满了没地方写,写满或者时间到了就和另外两个交换指针,然后把满的往文件里写。

日志库包括以下的几个部分:

  • FileUtil {FileUtil.h, FileUtil.cpp}
  • LogFile {LogFile.h, LogFile.cpp}
  • AsyncLogging {AsyncLogging.h, AsyncLogging.cpp}
  • LogStream { LogStream.h, LogStream.cpp}
  • Logging {Logging.h, Logging.cpp}

前4个类每个类中都含有一个append函数,Log的设计也主要是围绕这个append函数展开的。

FileUtil是最底层的文件类,封装了Log文件的打开,写入并在类析构的时候关闭文件,底层使用了标准IO,该append函数直接向文件写。

LogFile进一步封装了FileUtil,并设置了一个循环次数,每过多少次就flush一次。

AsyncLogging是核心,它负责启动一个log线程,专门用来将log写入LogFile,应用了“双缓冲技术”,其实有4个以上的缓冲区。AsyncLogging负责(定时或被填满时)将缓冲区中的数据写入LogFile中。

LogStream主要用来格式化输出,重载了<<运算符,同时也有自己的一块缓冲区,这里缓冲区的存在是为了缓存一行,把多个<<的结果连成一块。

Logging是对外接口,Logging类内涵一个LogStream对象,主要是为了每次打log的时候,在log之前和之后加上固定的格式化信息,比如输出打log的行号,文件名等信息。

Other:

其他文件:

  • base:存放的是一些基础代码,封装了pthread的常用功能(互斥器,条件变量,线程),并仿照java concurrent编写了CountDownLatch
  • tests:存放的是服务器的测试代码,客户端测试和日志测试。

处理流程

在这里插入图片描述

  • 创建主线程(主线程注册/IO事件)监听请求并维持eventloop,创建工作线程池处理后续事件并维持eventloop
  • 监听到请求,主线程从阻塞的eventloop唤醒,处理连接请求并以IO事件封装给工作线程池(轮询的方式分配)的任务队列,每次都会通过TimeManager处理超时的请求并关闭清除。
  • 工作线程从eventloop唤醒,工作线程处理后续操作,读,计算解析http报文(状态机);写:根据解析的结果返回http应答(如果出现错误可选择关闭连接),服务器可选择关闭连接(长连接或短连接)每次都会通过TimeManager处理超时的请求并关闭清除。可以根据不同的情况优雅的关闭连接。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章