【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處理超時的請求並關閉清除。可以根據不同的情況優雅的關閉連接。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章