Description:
C++編寫的web服務器,借鑑了《muduo網絡庫》的思想;使用了Reactor併發模型,非阻塞IO+線程池;解析了get、head請求;並實現了異步日誌,記錄服務器運行狀態。
詳細代碼可見: https://github.com/whjkm/Web_Server
Architecture:
I/O
多路複用(事件分配器) + 非阻塞I/O
+ 主線程(處理請求)+ 工作線程(讀、計算、寫) + eventloop
,即Reactor
反應堆模式。
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
: Channel
是Reactor
結構中的“事件”,它自始至終都屬於一個EventLoop
,因此每個Chanenl
對象都只屬於某一個IO線程;負責一個文件描述符的IO事件的分發,但它並不擁有這個文件描述符;在Channel
類中保存IO事件的類型對應的回調函數,當IO事件發生時,最終會調用到Channel
類中的回調函數。因此,程序中所有帶有讀寫時間的對象都會和一個Channel
關聯,包括loop
中的eventfd
,listenfd
,HttpData
等。
EventLoop
: One loop per thread
顧名思義每個線程只能有一個EventLoop
對象;EventLoop
即是時間循環,每次從poller
裏拿活躍事件,並給到Channel
裏分發處理。EventLoop
中的loop
含糊是會在最底層(Thread
)中被真正調用,開始無限的循環,直到某一輪的檢查到退出狀態後從底層一層一層的退出。
EventLoopThread
: IO線程不一定是主線程,我們可以在任何一個線程創建並運行EventLoop
。一個程序也可以有不止一個IO線程,我們可以按優先級將不同的socket分給不同的IO線程,避免優先級反轉。EventLoopThread
會啓動自己的線程。
EventLoopThreadPool
: EventLoop
線程池,每一個EventLoopThread
就是一個SubReactor
,按照輪詢的方式分發請求。
Epoll
: Epoll
是IO 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
處理超時的請求並關閉清除。可以根據不同的情況優雅的關閉連接。