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
- 監聽到請求,主線程從阻塞的
喚醒,處理連接配接請求并以IO事件封裝給工作線程池(輪詢的方式配置設定)的任務隊列,每次都會通過eventloop
處理逾時的請求并關閉清除。TimeManager
- 工作線程從
喚醒,工作線程處理後續操作,讀,計算解析eventloop
封包(狀态機);寫:根據解析的結果傳回http
應答(如果出現錯誤可選擇關閉連接配接),伺服器可選擇關閉連接配接(長連接配接或短連接配接)每次都會通過http
處理逾時的請求并關閉清除。可以根據不同的情況優雅的關閉連接配接。TimeManager