項(xiàng)目介紹
本項(xiàng)目實(shí)現(xiàn)的是一個(gè)HTTP服務(wù)器,項(xiàng)目中將會通過基本的網(wǎng)絡(luò)套接字讀取客戶端發(fā)來的HTTP請求并進(jìn)行分析,最終構(gòu)建HTTP響應(yīng)并返回給客戶端。
HTTP在網(wǎng)絡(luò)應(yīng)用層中的地位是不可撼動(dòng)的,無論是移動(dòng)端還是PC端瀏覽器,HTTP無疑是打開互聯(lián)網(wǎng)應(yīng)用窗口的重要協(xié)議。
該項(xiàng)目將會把HTTP中最核心的模塊抽取出來,采用CS模型實(shí)現(xiàn)一個(gè)小型的HTTP服務(wù)器,目的在于理解HTTP協(xié)議的處理過程。
該項(xiàng)目主要涉及C/C++、HTTP協(xié)議、網(wǎng)絡(luò)套接字編程、CGI、單例模式、多線程、線程池等方面的技術(shù)。
網(wǎng)絡(luò)協(xié)議棧介紹
協(xié)議分層
協(xié)議分層
網(wǎng)絡(luò)協(xié)議棧的分層情況如下:
網(wǎng)絡(luò)協(xié)議棧中各層的功能如下:
- 應(yīng)用層:根據(jù)特定的通信目的,對數(shù)據(jù)進(jìn)行分析處理,以達(dá)到某種業(yè)務(wù)性的目的。
- 傳輸層:處理傳輸時(shí)遇到的問題,主要是保證數(shù)據(jù)傳輸?shù)目煽啃浴?/li>
- 網(wǎng)絡(luò)層:完成數(shù)據(jù)的轉(zhuǎn)發(fā),解決數(shù)據(jù)去哪里的問題。
- 鏈路層:負(fù)責(zé)數(shù)據(jù)真正的發(fā)生過程。
數(shù)據(jù)的封裝與分用
數(shù)據(jù)的封裝與分用
數(shù)據(jù)封裝與分用的過程如下:
也就是說,發(fā)送端在發(fā)生數(shù)據(jù)前,該數(shù)據(jù)需要先自頂向下貫穿網(wǎng)絡(luò)協(xié)議棧完成數(shù)據(jù)的封裝,在這個(gè)過程中,每一層協(xié)議都會為該數(shù)據(jù)添加上對應(yīng)的報(bào)頭信息。接收端在收到數(shù)據(jù)后,該數(shù)據(jù)需要先自底向上貫穿網(wǎng)絡(luò)協(xié)議棧完成數(shù)據(jù)的解包和分用,在這個(gè)過程中,每一層協(xié)議都會將對應(yīng)的報(bào)頭信息提取出來。
而本項(xiàng)目要做的就是,在接收到客戶端發(fā)來的HTTP請求后,將HTTP的報(bào)頭信息提取出來,然后對數(shù)據(jù)進(jìn)行分析處理,最終將處理結(jié)果添加上HTTP報(bào)頭再發(fā)送給客戶端。
需要注意的是,該項(xiàng)目中我們所處的位置是應(yīng)用層,因此我們讀取的HTTP請求實(shí)際是從傳輸層讀取上來的,而我們發(fā)送的HTTP響應(yīng)實(shí)際也只是交給了傳輸層,數(shù)據(jù)真正的發(fā)送還得靠網(wǎng)絡(luò)協(xié)議棧中的下三層來完成,這里直接說“接收到客戶端的HTTP請求”以及“發(fā)送HTTP響應(yīng)給客戶端”,只是為了方便大家理解,此外,同層協(xié)議之間本身也是可以理解成是在直接通信的。
HTTP相關(guān)知識介紹
HTTP的特點(diǎn)
HTTP的五大特點(diǎn)
HTTP的五大特點(diǎn)如下:
- 客戶端服務(wù)器模式(CS,BS): 在一條通信線路上必定有一端是客戶端,另一端是服務(wù)器端,請求從客戶端發(fā)出,服務(wù)器響應(yīng)請求并返回。
- 簡單快速: 客戶端向服務(wù)器請求服務(wù)時(shí),只需傳送請求方法和請求資源路徑,不需要發(fā)送額外過多的數(shù)據(jù),并且由于HTTP協(xié)議結(jié)構(gòu)較為簡單,使得HTTP服務(wù)器的程序規(guī)模小,因此通信速度很快。
- 靈活: HTTP協(xié)議對數(shù)據(jù)對象沒有要求,允許傳輸任意類型的數(shù)據(jù)對象,對于正在傳輸?shù)臄?shù)據(jù)類型,HTTP協(xié)議將通過報(bào)頭中的Content-Type屬性加以標(biāo)記。
- 無連接: 每次連接都只會對一個(gè)請求進(jìn)行處理,當(dāng)服務(wù)器對客戶端的請求處理完畢并收到客戶端的應(yīng)答后,就會直接斷開連接。HTTP協(xié)議采用這種方式可以大大節(jié)省傳輸時(shí)間,提高傳輸效率。
- 無狀態(tài): HTTP協(xié)議自身不對請求和響應(yīng)之間的通信狀態(tài)進(jìn)行保存,每個(gè)請求都是獨(dú)立的,這是為了讓HTTP能更快地處理大量事務(wù),確保協(xié)議的可伸縮性而特意設(shè)計(jì)的。
說明一下:
- 隨著HTTP的普及,文檔中包含大量圖片的情況多了起來,每次請求都要斷開連接,無疑增加了通信量的開銷,因此HTTP1.1支持了長連接Keey-Alive,就是任意一端只要沒有明確提出斷開連接,則保持連接狀態(tài)。(當(dāng)前項(xiàng)目實(shí)現(xiàn)的是1.0版本的HTTP服務(wù)器,因此不涉及長連接)
- HTTP無狀態(tài)的特點(diǎn)無疑可以減少服務(wù)器內(nèi)存資源的消耗,但是問題也是顯而易見的。比如某個(gè)網(wǎng)站需要登錄后才能訪問,由于無狀態(tài)的特點(diǎn),那么每次跳轉(zhuǎn)頁面的時(shí)候都需要重新登錄。為了解決無狀態(tài)的問題,于是引入了Cookie技術(shù),通過在請求和響應(yīng)報(bào)文中寫入Cookie信息來控制客戶端的狀態(tài),同時(shí)為了保護(hù)用戶數(shù)據(jù)的安全,又引入了Session技術(shù),因此現(xiàn)在主流的HTTP服務(wù)器都是通過Cookie+Session的方式來控制客戶端的狀態(tài)的。
URL格式
URL(Uniform Resource Lacator)叫做統(tǒng)一資源定位符,也就是我們通常所說的網(wǎng)址,是因特網(wǎng)的萬維網(wǎng)服務(wù)程序上用于指定信息位置的表示方法。
一個(gè)URL大致由如下幾部分構(gòu)成:
簡單說明:
- http://表示的是協(xié)議名稱,表示請求時(shí)需要使用的協(xié)議,通常使用的是HTTP協(xié)議或安全協(xié)議HTTPS。
- user:pass表示的是登錄認(rèn)證信息,包括登錄用戶的用戶名和密碼。(可省略)
- www.example.jp表示的是服務(wù)器地址,通常以域名的形式表示。
- 80表示的是服務(wù)器的端口號。(可省略)
- /dir/index.html表示的是要訪問的資源所在的路徑(/表示的是web根目錄)。
- uid=1表示的是請求時(shí)通過URL傳遞的參數(shù),這些參數(shù)以鍵值對的形式通過&符號分隔開。(可省略)
- ch1表示的是片段標(biāo)識符,是對資源的部分補(bǔ)充。(可省略)
注意:
- 如果訪問服務(wù)器時(shí)沒有指定要訪問的資源路徑,那么瀏覽器會自動(dòng)幫我們添加/,但此時(shí)仍然沒有指明要訪問web根目錄下的哪一個(gè)資源文件,這時(shí)默認(rèn)訪問的是目標(biāo)服務(wù)的首頁。
- 大部分URL中的端口號都是省略的,因?yàn)槌R妳f(xié)議對應(yīng)的端口號都是固定的,比如HTTP、HTTPS和SSH對應(yīng)的端口號分別是80、443和22,在使用這些常見協(xié)議時(shí)不必指明協(xié)議對應(yīng)的端口號,瀏覽器會自動(dòng)幫我們進(jìn)行填充。
URI、URL、URN
URI、URL、URN的定義
URI、URL、URN的定義如下:
- URI(Uniform Resource Indentifier)統(tǒng)一資源標(biāo)識符:用來唯一標(biāo)識資源。
- URL(Uniform Resource Locator)統(tǒng)一資源定位符:用來定位唯一的資源。
- URN(Uniform Resource Name)統(tǒng)一資源名稱:通過名字來標(biāo)識資源,比如mailto:java-net@java.sun.com。
URI、URL、URN三者的關(guān)系
URL是URI的一種,URL不僅能唯一標(biāo)識資源,還定義了該如何訪問或定位該資源,URN也是URI的一種,URN通過名字來標(biāo)識資源,因此URL和URN都是URI的子集。
URI、URL、URN三者的關(guān)系如下:
絕對的URI和相對的URI
URI有絕對和相對之分:
- 絕對的URI: 對標(biāo)識符出現(xiàn)的環(huán)境沒有依賴,比如URL就是一種絕對的URI,同一個(gè)URL無論出現(xiàn)在什么地方都能唯一標(biāo)識同一個(gè)資源。
- 相對的URI: 對標(biāo)識符出現(xiàn)的環(huán)境有依賴,比如HTTP請求行中的請求資源路徑就是一種相對的URI,這個(gè)資源路徑出現(xiàn)在不同的主機(jī)上標(biāo)識的就是不同的資源。
HTTP的協(xié)議格式
HTTP請求協(xié)議格式
HTTP請求協(xié)議格式如下:
HTTP請求由以下四部分組成:
- 請求行:[請求方法] + [URI] + [HTTP版本]。
- 請求報(bào)頭:請求的屬性,這些屬性都是以key: value的形式按行陳列的。
- 空行:遇到空行表示請求報(bào)頭結(jié)束。
- 請求正文:請求正文允許為空字符串,如果請求正文存在,則在請求報(bào)頭中會有一個(gè)Content-Length屬性來標(biāo)識請求正文的長度。
HTTP響應(yīng)協(xié)議格式
HTTP響應(yīng)協(xié)議格式如下:
HTTP響應(yīng)由以下四部分組成:
- 狀態(tài)行:[HTTP版本] + [狀態(tài)碼] + [狀態(tài)碼描述]。
- 響應(yīng)報(bào)頭:響應(yīng)的屬性,這些屬性都是以key: value的形式按行陳列的。
- 空行:遇到空行表示響應(yīng)報(bào)頭結(jié)束。
- 響應(yīng)正文:響應(yīng)正文允許為空字符串,如果響應(yīng)正文存在,則在響應(yīng)報(bào)頭中會有一個(gè)Content-Length屬性來標(biāo)識響應(yīng)正文的長度。
HTTP的請求方法
HTTP的請求方法
HTTP常見的請求方法如下:
GET方法和POST方法
HTTP的請求方法中最常用的就是GET方法和POST方法,其中GET方法一般用于獲取某種資源信息,而POST方法一般用于將數(shù)據(jù)上傳給服務(wù)器,但實(shí)際GET方法也可以用來上傳數(shù)據(jù),比如百度搜索框中的數(shù)據(jù)就是使用GET方法提交的。
GET方法和POST方法都可以帶參,其中GET方法通過URL傳參,POST方法通過請求正文傳參。由于URL的長度是有限制的,因此GET方法攜帶的參數(shù)不能太長,而POST方法通過請求正文傳參,一般參數(shù)長度沒有限制。
HTTP的狀態(tài)碼
HTTP的狀態(tài)碼
HTTP狀態(tài)碼是用來表示服務(wù)器HTTP響應(yīng)狀態(tài)的3位數(shù)字代碼,通過狀態(tài)碼可以知道服務(wù)器端是否正確的處理了請求,以及請求處理錯(cuò)誤的原因。
HTTP的狀態(tài)碼如下:
常見狀態(tài)碼
常見的狀態(tài)碼如下:
HTTP常見的Header
HTTP常見的Header
HTTP常見的Header如下:
- Content-Type:數(shù)據(jù)類型(text/html等)。
- Content-Length:正文的長度。
- Host:客戶端告知服務(wù)器,所請求的資源是在哪個(gè)主機(jī)的哪個(gè)端口上。
- User-Agent:聲明用戶的操作系統(tǒng)和瀏覽器的版本信息。
- Referer:當(dāng)前頁面是哪個(gè)頁面跳轉(zhuǎn)過來的。
- Location:搭配3XX狀態(tài)碼使用,告訴客戶端接下來要去哪里訪問。
- Cookie:用戶在客戶端存儲少量信息,通常用于實(shí)現(xiàn)會話(session)的功能。
簡歷沒項(xiàng)目可寫?加入學(xué)習(xí)更多實(shí)戰(zhàn)項(xiàng)目(完整視頻教程+源碼+難點(diǎn)答疑)
CGI機(jī)制介紹
CGI機(jī)制的概念
CGI(Common Gateway Interface,通用網(wǎng)關(guān)接口)是一種重要的互聯(lián)網(wǎng)技術(shù),可以讓一個(gè)客戶端,從網(wǎng)頁瀏覽器向執(zhí)行在網(wǎng)絡(luò)服務(wù)器上的程序請求數(shù)據(jù)。CGI描述了服務(wù)器和請求處理程序之間傳輸數(shù)據(jù)的一種標(biāo)準(zhǔn)。
實(shí)際我們在進(jìn)行網(wǎng)絡(luò)請求時(shí),無非就兩種情況:
通常從服務(wù)器上獲取資源對應(yīng)的請求方法就是GET方法,而將數(shù)據(jù)上傳至服務(wù)器對應(yīng)的請求方法就是POST方法,但實(shí)際GET方法有時(shí)也會用于上傳數(shù)據(jù),只不過POST方法是通過請求正文傳參的,而GET方法是通過URL傳參的。
而用戶將自己的數(shù)據(jù)上傳至服務(wù)器并不僅僅是為了上傳,用戶上傳數(shù)據(jù)的目的是為了讓HTTP或相關(guān)程序?qū)υ摂?shù)據(jù)進(jìn)行處理,比如用戶提交的是搜索關(guān)鍵字,那么服務(wù)器就需要在后端進(jìn)行搜索,然后將搜索結(jié)果返回給瀏覽器,再由瀏覽器對HTML文件進(jìn)行渲染刷新展示給用戶。
但實(shí)際對數(shù)據(jù)的處理與HTTP的關(guān)系并不大,而是取決于上層具體的業(yè)務(wù)場景的,因此HTTP不對這些數(shù)據(jù)做處理。但HTTP提供了CGI機(jī)制,上層可以在服務(wù)器中部署若干個(gè)CGI程序,這些CGI程序可以用任何程序設(shè)計(jì)語言編寫,當(dāng)HTTP獲取到數(shù)據(jù)后會將其提交給對應(yīng)CGI程序進(jìn)行處理,然后再用CGI程序的處理結(jié)果構(gòu)建HTTP響應(yīng)返回給瀏覽器。
其中HTTP獲取到數(shù)據(jù)后,如何調(diào)用目標(biāo)CGI程序、如何傳遞數(shù)據(jù)給CGI程序、如何拿到CGI程序的處理結(jié)果,這些都屬于CGI機(jī)制的通信細(xì)節(jié),而本項(xiàng)目就是要實(shí)現(xiàn)一個(gè)HTTP服務(wù)器,因此CGI的所有交互細(xì)節(jié)都需要由我們來完成。
何時(shí)需要使用CGI模式
只要用戶請求服務(wù)器時(shí)上傳了數(shù)據(jù),那么服務(wù)器就需要使用CGI模式對用戶上傳的數(shù)據(jù)進(jìn)行處理,而如果用戶只是單純的想請求服務(wù)器上的某個(gè)資源文件則不需要使用CGI模式,此時(shí)直接將用戶請求的資源文件返回給用戶即可。
此外,如果用戶請求的是服務(wù)器上的一個(gè)可執(zhí)行程序,說明用戶想讓服務(wù)器運(yùn)行這個(gè)可執(zhí)行程序,此時(shí)也需要使用CGI模式。
CGI機(jī)制的實(shí)現(xiàn)步驟
一、創(chuàng)建子進(jìn)程進(jìn)行程序替換
服務(wù)器獲取到新連接后一般會創(chuàng)建一個(gè)新線程為其提供服務(wù),而要執(zhí)行CGI程序一定需要調(diào)用exec系列函數(shù)進(jìn)行進(jìn)程程序替換,但服務(wù)器創(chuàng)建的新線程與服務(wù)器進(jìn)程使用的是同一個(gè)進(jìn)程地址空間,如果直接讓新線程調(diào)用exec系列函數(shù)進(jìn)行進(jìn)程程序替換,此時(shí)服務(wù)器進(jìn)程的代碼和數(shù)據(jù)就會直接被替換掉,相當(dāng)于HTTP服務(wù)器在執(zhí)行一次CGI程序后就直接退出了,這肯定是不合理的。因此新線程需要先調(diào)用fork函數(shù)創(chuàng)建子進(jìn)程,然后讓子進(jìn)程調(diào)用exec系列函數(shù)進(jìn)行進(jìn)程程序替換。
二、完成管道通信信道的建立
調(diào)用CGI程序的目的是為了讓其進(jìn)行數(shù)據(jù)處理,因此我們需要通過某種方式將數(shù)據(jù)交給CGI程序,并且還要能夠獲取到CGI程序處理數(shù)據(jù)后的結(jié)果,也就是需要進(jìn)行進(jìn)程間通信。因?yàn)檫@里的服務(wù)器進(jìn)程和CGI進(jìn)程是父子進(jìn)程,因此優(yōu)先選擇使用匿名管道。
由于父進(jìn)程不僅需要將數(shù)據(jù)交給子進(jìn)程,還需要從子進(jìn)程那里獲取數(shù)據(jù)處理的結(jié)果,而管道是半雙工通信的,為了實(shí)現(xiàn)雙向通信于是需要借助兩個(gè)匿名管道,因此在創(chuàng)建調(diào)用fork子進(jìn)程之前需要先創(chuàng)建兩個(gè)匿名管道,在創(chuàng)建子進(jìn)程后還需要父子進(jìn)程分別關(guān)閉兩個(gè)管道對應(yīng)的讀寫端。
三、完成重定向相關(guān)的設(shè)置
創(chuàng)建用于父子進(jìn)程間通信的兩個(gè)匿名管道時(shí),父子進(jìn)程都是各自用兩個(gè)變量來記錄管道對應(yīng)讀寫端的文件描述符的,但是對于子進(jìn)程來說,當(dāng)子進(jìn)程調(diào)用exec系列函數(shù)進(jìn)行程序替換后,子進(jìn)程的代碼和數(shù)據(jù)就被替換成了目標(biāo)CGI程序的代碼和數(shù)據(jù),這也就意味著被替換后的CGI程序無法得知管道對應(yīng)的讀寫端,這樣父子進(jìn)程之間也就無法進(jìn)行通信了。
需要注意的是,進(jìn)程程序替換只替換對應(yīng)進(jìn)程的代碼和數(shù)據(jù),而對于進(jìn)程的進(jìn)程控制塊、頁表、打開的文件等內(nèi)核數(shù)據(jù)結(jié)構(gòu)是不做任何替換的。因此子進(jìn)程進(jìn)行進(jìn)程程序替換后,底層創(chuàng)建的兩個(gè)匿名管道仍然存在,只不過被替換后的CGI程序不知道這兩個(gè)管道對應(yīng)的文件描述符罷了。
這時(shí)我們可以做一個(gè)約定:被替換后的CGI程序,從標(biāo)準(zhǔn)輸入讀取數(shù)據(jù)等價(jià)于從管道讀取數(shù)據(jù),向標(biāo)準(zhǔn)輸出寫入數(shù)據(jù)等價(jià)于向管道寫入數(shù)據(jù)。這樣一來,所有的CGI程序都不需要得知管道對應(yīng)的文件描述符了,當(dāng)需要讀取數(shù)據(jù)時(shí)直接從標(biāo)準(zhǔn)輸入中進(jìn)行讀取,而數(shù)據(jù)處理的結(jié)果就直接寫入標(biāo)準(zhǔn)輸出就行了。
當(dāng)然,這個(gè)約定并不是你說有就有的,要實(shí)現(xiàn)這個(gè)約定需要在子進(jìn)程被替換之前進(jìn)行重定向,將0號文件描述符重定向到對應(yīng)管道的讀端,將1號文件描述符重定向到對應(yīng)管道的寫端。
四、父子進(jìn)程交付數(shù)據(jù)
這時(shí)父子進(jìn)程已經(jīng)能夠通過兩個(gè)匿名管道進(jìn)行通信了,接下來就應(yīng)該討論父進(jìn)程如何將數(shù)據(jù)交給CGI程序,以及CGI程序如何將數(shù)據(jù)處理結(jié)果交給父進(jìn)程了。
父進(jìn)程將數(shù)據(jù)交給CGI程序:
- 如果請求方法為GET方法,那么用戶是通過URL傳遞參數(shù)的,此時(shí)可以在子進(jìn)程進(jìn)行進(jìn)程程序替換之前,通過putenv函數(shù)將參數(shù)導(dǎo)入環(huán)境變量,由于環(huán)境變量也不受進(jìn)程程序替換的影響,因此被替換后的CGI程序就可以通過getenv函數(shù)來獲取對應(yīng)的參數(shù)。
- 如果請求方法為POST方法,那么用戶是通過請求正文傳參的,此時(shí)父進(jìn)程直接將請求正文中的數(shù)據(jù)寫入管道傳遞給CGI程序即可,但是為了讓CGI程序知道應(yīng)該從管道讀取多少個(gè)參數(shù),父進(jìn)程還需要通過putenv函數(shù)將請求正文的長度導(dǎo)入環(huán)境變量。
說明一下:請求正文長度、URL傳遞的參數(shù)以及請求方法都比較短,通過寫入管道來傳遞會導(dǎo)致效率降低,因此選擇通過導(dǎo)入環(huán)境變量的方式來傳遞。
也就是說,使用CGI模式時(shí)如果請求方法為POST方法,那么CGI程序需要從管道讀取父進(jìn)程傳遞過來的數(shù)據(jù),如果請求方法為GET方法,那么CGI程序需要從環(huán)境變量中獲取父進(jìn)程傳遞過來的數(shù)據(jù)。
但被替換后的CGI程序?qū)嶋H并不知道本次HTTP請求所對應(yīng)的請求方法,因此在子進(jìn)程在進(jìn)行進(jìn)程程序替換之前,還需要通過putenv函數(shù)將本次HTTP請求所對應(yīng)的請求方法也導(dǎo)入環(huán)境變量。因此CGI程序啟動(dòng)后,首先需要先通過環(huán)境變量得知本次HTTP請求所對應(yīng)的請求方法,然后再根據(jù)請求方法對應(yīng)從管道或環(huán)境變量中獲取父進(jìn)程傳遞過來的數(shù)據(jù)。
CGI程序讀取到父進(jìn)程傳遞過來的數(shù)據(jù)后,就可以進(jìn)行對應(yīng)的數(shù)據(jù)處理了,最終將數(shù)據(jù)處理結(jié)果寫入到管道中,此時(shí)父進(jìn)程就可以從管道中讀取CGI程序的處理結(jié)果了。
CGI機(jī)制的意義
CGI機(jī)制的處理流程
CGI機(jī)制的處理流程如下:
處理HTTP請求的步驟如下:
- 判斷請求方法是GET方法還是POST方法,如果是GET方法帶參或POST方法則進(jìn)行CGI處理,如果是GET方法不帶參則進(jìn)行非CGI處理。
- 非CGI處理就是直接根據(jù)用戶請求的資源構(gòu)建HTTP響應(yīng)返回給瀏覽器。
- CGI處理就是通過創(chuàng)建子進(jìn)程進(jìn)行程序替換的方式來調(diào)用CGI程序,通過創(chuàng)建匿名管道、重定向、導(dǎo)入環(huán)境變量的方式來與CGI程序進(jìn)行數(shù)據(jù)通信,最終根據(jù)CGI程序的處理結(jié)果構(gòu)建HTTP響應(yīng)返回給瀏覽器。
CGI機(jī)制的意義
- CGI機(jī)制就是讓服務(wù)器將獲取到的數(shù)據(jù)交給對應(yīng)的CGI程序進(jìn)行處理,然后將CGI程序的處理結(jié)果返回給客戶端,這顯然讓服務(wù)器邏輯和業(yè)務(wù)邏輯進(jìn)行了解耦,讓服務(wù)器和業(yè)務(wù)程序可以各司其職。
- CGI機(jī)制使得瀏覽器輸入的數(shù)據(jù)最終交給了CGI程序,而CGI程序輸出的結(jié)果最終交給了瀏覽器。這也就意味著CGI程序的開發(fā)者,可以完全忽略中間服務(wù)器的處理邏輯,相當(dāng)于CGI程序從標(biāo)準(zhǔn)輸入就能讀取到瀏覽器輸入的內(nèi)容,CGI程序?qū)懭霕?biāo)準(zhǔn)輸出的數(shù)據(jù)最終就能輸出到瀏覽器。
日志編寫
服務(wù)器在運(yùn)作時(shí)會產(chǎn)生一些日志,這些日志會記錄下服務(wù)器運(yùn)行過程中產(chǎn)生的一些事件。
日志格式
本項(xiàng)目中的日志格式如下:
日志說明:
- 日志級別: 分為四個(gè)等級,從低到高依次是INFO、WARNING、ERROR、FATAL。
- 時(shí)間戳: 事件產(chǎn)生的時(shí)間。
- 日志信息: 事件產(chǎn)生的日志信息。
- 錯(cuò)誤文件名稱: 事件在哪一個(gè)文件產(chǎn)生。
- 行數(shù): 事件在對應(yīng)文件的哪一行產(chǎn)生。
日志級別說明:
- INFO: 表示正常的日志輸出,一切按預(yù)期運(yùn)行。
- WARNING: 表示警告,該事件不影響服務(wù)器運(yùn)行,但存在風(fēng)險(xiǎn)。
- ERROR: 表示發(fā)生了某種錯(cuò)誤,但該事件不影響服務(wù)器繼續(xù)運(yùn)行。
- FATAL: 表示發(fā)生了致命的錯(cuò)誤,該事件將導(dǎo)致服務(wù)器停止運(yùn)行。
日志函數(shù)編寫
我們可以針對日志編寫一個(gè)輸出日志的Log函數(shù),該函數(shù)的參數(shù)就包括日志級別、日志信息、錯(cuò)誤文件名稱、錯(cuò)誤的行數(shù)。如下:
{
std::cout<<"["<}<<"]["<
說明一下: 調(diào)用time函數(shù)時(shí)傳入nullptr即可獲取當(dāng)前的時(shí)間戳,因此調(diào)用Log函數(shù)時(shí)不必傳入時(shí)間戳。
文件名稱和行數(shù)的問題
通過C語言中的預(yù)定義符號__FILE__和__LINE__,分別可以獲取當(dāng)前文件的名稱和當(dāng)前的行數(shù),但最好在調(diào)用Log函數(shù)時(shí)不用調(diào)用者顯示的傳入__FILE__和__LINE__,因?yàn)槊看握{(diào)用Log函數(shù)時(shí)傳入的這兩個(gè)參數(shù)都是固定的。
需要注意的是,不能將__FILE__和__LINE__設(shè)置為參數(shù)的缺省值,因?yàn)檫@樣每次獲取到的都是Log函數(shù)所在的文件名稱和所在的行數(shù)。而宏可以在預(yù)處理期間將代碼插入到目標(biāo)地點(diǎn),因此我們可以定義如下宏:
后續(xù)需要打印日志的時(shí)候就直接調(diào)用LOG,調(diào)用時(shí)只需要傳入日志級別和日志信息,在預(yù)處理期間__FILE__和__LINE__就會被插入到目標(biāo)地點(diǎn),這時(shí)就能獲取到日志產(chǎn)生的文件名稱和對應(yīng)的行數(shù)了。
日志級別傳入問題
我們后續(xù)調(diào)用LOG傳入日志級別時(shí),肯定希望以INFO、WARNING這樣的方式傳入,而不是以"INFO"、"WARNING"這樣的形式傳入,這時(shí)我們可以將這四個(gè)日志級別定義為宏,然后通過#將宏參數(shù)level變成對應(yīng)的字符串。如下:
#define WARNING 2
#define ERROR 3
#define FATAL 4
#define LOG(level, message) Log(#level, message, __FILE__, __LINE__)
此時(shí)以INFO、WARNING的方式傳入LOG的宏參數(shù),就會被轉(zhuǎn)換成對應(yīng)的字符串傳遞給Log函數(shù)的level參數(shù),后續(xù)我們就可以以如下方式輸出日志了:
套接字相關(guān)代碼編寫
套接字相關(guān)代碼編寫
我們可以將套接字相關(guān)的代碼封裝到TcpServer類中,在初始化TcpServer對象時(shí)完成套接字的創(chuàng)建、綁定和監(jiān)聽動(dòng)作,并向外提供一個(gè)Sock接口用于獲取監(jiān)聽套接字。
此外,可以將TcpServer設(shè)置成單例模式:
- 將TcpServer類的構(gòu)造函數(shù)設(shè)置為私有,并將拷貝構(gòu)造和拷貝賦值函數(shù)設(shè)置為私有或刪除,防止外部創(chuàng)建或拷貝對象。
- 提供一個(gè)指向單例對象的static指針,并在類外將其初始化為nullptr。
- 提供一個(gè)全局訪問點(diǎn)獲取單例對象,在單例對象第一次被獲取的時(shí)候就創(chuàng)建這個(gè)單例對象并進(jìn)行初始化。
代碼如下:
//TCP服務(wù)器
class TcpServer{
private:
int _port; //端口號
int _listen_sock; //監(jiān)聽套接字
static TcpServer* _svr; //指向單例對象的static指針
private:
//構(gòu)造函數(shù)私有
TcpServer(int port)
:_port(port)
,_listen_sock(-1)
{}
//將拷貝構(gòu)造函數(shù)和拷貝賦值函數(shù)私有或刪除(防拷貝)
TcpServer(const TcpServer&)=delete;
TcpServer* operator=(const TcpServer&)=delete;
public:
//獲取單例對象
static TcpServer* GetInstance(int port)
{
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; //定義靜態(tài)的互斥鎖
if(_svr == nullptr){
pthread_mutex_lock(&mtx); //加鎖
if(_svr == nullptr){
//創(chuàng)建單例TCP服務(wù)器對象并初始化
_svr = new TcpServer(port);
_svr->InitServer();
}
pthread_mutex_unlock(&mtx); //解鎖
}
return _svr; //返回單例對象
}
//初始化服務(wù)器
void InitServer()
{
Socket(); //創(chuàng)建套接字
Bind(); //綁定
Listen(); //監(jiān)聽
LOG(INFO, "tcp_server init ... success");
}
//創(chuàng)建套接字
void Socket()
{
_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_listen_sock < 0){ //創(chuàng)建套接字失敗
LOG(FATAL, "socket error!");
exit(1);
}
//設(shè)置端口復(fù)用
int opt = 1;
setsockopt(_listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
LOG(INFO, "create socket ... success");
}
//綁定
void Bind()
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if(bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){ //綁定失敗
LOG(FATAL, "bind error!");
exit(2);
}
LOG(INFO, "bind socket ... success");
}
//監(jiān)聽
void Listen()
{
if(listen(_listen_sock, BACKLOG) < 0){ //監(jiān)聽失敗
LOG(FATAL, "listen error!");
exit(3);
}
LOG(INFO, "listen socket ... success");
}
//獲取監(jiān)聽套接字
int Sock()
{
return _listen_sock;
}
~TcpServer()
{
if(_listen_sock >= 0){ //關(guān)閉監(jiān)聽套接字
close(_listen_sock);
}
}
};
//單例對象指針初始化為nullptr
TcpServer* TcpServer::_svr = nullptr;
說明一下:
- 如果使用的是云服務(wù)器,那么在設(shè)置服務(wù)器的IP地址時(shí),不需要顯式綁定IP地址,直接將IP地址設(shè)置為INADDR_ANY即可,此時(shí)服務(wù)器就可以從本地任何一張網(wǎng)卡當(dāng)中讀取數(shù)據(jù)。此外,由于INADDR_ANY本質(zhì)就是0,因此在設(shè)置時(shí)不需要進(jìn)行網(wǎng)絡(luò)字節(jié)序列的轉(zhuǎn)換。
- 在第一次調(diào)用GetInstance獲取單例對象時(shí)需要?jiǎng)?chuàng)建單例對象,這時(shí)需要定義一個(gè)鎖來保證線程安全,代碼中以PTHREAD_MUTEX_INITIALIZER的方式定義的靜態(tài)的鎖是不需要釋放的,同時(shí)為了保證后續(xù)調(diào)用GetInstance獲取單例對象時(shí)不會頻繁的加鎖解鎖,因此代碼中以雙檢查的方式進(jìn)行加鎖。
HTTP服務(wù)器主體邏輯
HTTP服務(wù)器主體邏輯
我們可以將HTTP服務(wù)器封裝成一個(gè)HttpServer類,在構(gòu)造HttpServer對象時(shí)傳入一個(gè)端口號,之后就可以調(diào)用Loop讓服務(wù)器運(yùn)行起來了。服務(wù)器運(yùn)行起來后要做的就是,先獲取單例對象TcpServer中的監(jiān)聽套接字,然后不斷從監(jiān)聽套接字中獲取新連接,每當(dāng)獲取到一個(gè)新連接后就創(chuàng)建一個(gè)新線程為該連接提供服務(wù)。
代碼如下:
//HTTP服務(wù)器
class HttpServer{
private:
int _port; //端口號
public:
HttpServer(int port)
:_port(port)
{}
//啟動(dòng)服務(wù)器
void Loop()
{
LOG(INFO, "loop begin");
TcpServer* tsvr = TcpServer::GetInstance(_port); //獲取TCP服務(wù)器單例對象
int listen_sock = tsvr->Sock(); //獲取監(jiān)聽套接字
while(true){
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len); //獲取新連接
if(sock < 0){
continue; //獲取失敗,繼續(xù)獲取
}
//打印客戶端相關(guān)信息
std::string client_ip = inet_ntoa(peer.sin_addr);
int client_port = ntohs(peer.sin_port);
LOG(INFO, "get a new link: ["+client_ip+":"+std::to_string(client_port)+"]");
//創(chuàng)建新線程處理新連接發(fā)起的HTTP請求
int* p = new int(sock);
pthread_t tid;
pthread_create(&tid, nullptr, CallBack::HandlerRequest, (void*)p);
pthread_detach(tid); //線程分離
}
}
~HttpServer()
{}
};
說明一下:
- 服務(wù)器需要將新連接對應(yīng)的套接字作為參數(shù)傳遞給新線程,為了避免該套接字在新線程讀取之前被下一次獲取到的套接字覆蓋,因此在傳遞套接字時(shí)最好重新new一塊空間來存儲套接字的值。
- 新線程創(chuàng)建后可以將新線程分離,分離后主線程繼續(xù)獲取新連接,而新線程則處理新連接發(fā)來的HTTP請求,代碼中的HandlerRequest函數(shù)就是新線程處理新連接時(shí)需要執(zhí)行的回調(diào)函數(shù)。
主函數(shù)邏輯
運(yùn)行服務(wù)器時(shí)要求指定服務(wù)器的端口號,我們用這個(gè)端口號創(chuàng)建一個(gè)HttpServer對象,然后調(diào)用Loop函數(shù)運(yùn)行服務(wù)器,此時(shí)服務(wù)器就會不斷獲取新連接并創(chuàng)建新線程來處理連接。
代碼如下:
{
std::cout<<"Usage:nt"<}
int main(int argc, char* argv[])
{
if(argc != 2){
Usage(argv[0]);
exit(4);
}
int port = atoi(argv[1]); //端口號
std::shared_ptr svr(new HttpServer(port)); //創(chuàng)建HTTP服務(wù)器對象
svr->Loop(); //啟動(dòng)服務(wù)器
return 0;
}<<">
HTTP請求結(jié)構(gòu)設(shè)計(jì)
HTTP請求類
我們可以將HTTP請求封裝成一個(gè)類,這個(gè)類當(dāng)中包括HTTP請求的內(nèi)容、HTTP請求的解析結(jié)果以及是否需要使用CGI模式的標(biāo)志位。后續(xù)處理請求時(shí)就可以定義一個(gè)HTTP請求類,讀取到的HTTP請求的數(shù)據(jù)就存儲在這個(gè)類當(dāng)中,解析HTTP請求后得到的數(shù)據(jù)也存儲在這個(gè)類當(dāng)中。
代碼如下:
class HttpRequest{
public:
//HTTP請求內(nèi)容
std::string _request_line; //請求行
std::vector _request_header; //請求報(bào)頭
std::string _blank; //空行
std::string _request_body; //請求正文
//解析結(jié)果
std::string _method; //請求方法
std::string _uri; //URI
std::string _version; //版本號
std::unordered_map _header_kv; //請求報(bào)頭中的鍵值對
int _content_length; //正文長度
std::string _path; //請求資源的路徑
std::string _query_string; //uri中攜帶的參數(shù)
//CGI相關(guān)
bool _cgi; //是否需要使用CGI模式
public:
HttpRequest()
:_content_length(0) //默認(rèn)請求正文長度為0
,_cgi(false) //默認(rèn)不使用CGI模式
{}
~HttpRequest()
{}
};,>
HTTP響應(yīng)結(jié)構(gòu)設(shè)計(jì)
HTTP響應(yīng)類
HTTP響應(yīng)也可以封裝成一個(gè)類,這個(gè)類當(dāng)中包括HTTP響應(yīng)的內(nèi)容以及構(gòu)建HTTP響應(yīng)所需要的數(shù)據(jù)。后續(xù)構(gòu)建響應(yīng)時(shí)就可以定義一個(gè)HTTP響應(yīng)類,構(gòu)建響應(yīng)需要使用的數(shù)據(jù)就存儲在這個(gè)類當(dāng)中,構(gòu)建后得到的響應(yīng)內(nèi)容也存儲在這個(gè)類當(dāng)中。
代碼如下:
class HttpResponse{
public:
//HTTP響應(yīng)內(nèi)容
std::string _status_line; //狀態(tài)行
std::vector _response_header; //響應(yīng)報(bào)頭
std::string _blank; //空行
std::string _response_body; //響應(yīng)正文(CGI相關(guān))
//所需數(shù)據(jù)
int _status_code; //狀態(tài)碼
int _fd; //響應(yīng)文件的fd (非CGI相關(guān))
int _size; //響應(yīng)文件的大?。ǚ荂GI相關(guān))
std::string _suffix; //響應(yīng)文件的后綴(非CGI相關(guān))
public:
HttpResponse()
:_blank(LINE_END) //設(shè)置空行
,_status_code(OK) //狀態(tài)碼默認(rèn)為200
,_fd(-1) //響應(yīng)文件的fd初始化為-1
,_size(0) //響應(yīng)文件的大小默認(rèn)為0
{}
~HttpResponse()
{}
};
EndPoint類編寫
EndPoint結(jié)構(gòu)設(shè)計(jì)
EndPoint結(jié)構(gòu)設(shè)計(jì)
EndPoint這個(gè)詞經(jīng)常用來描述進(jìn)程間通信,比如在客戶端和服務(wù)器通信時(shí),客戶端是一個(gè)EndPoint,服務(wù)器則是另一個(gè)EndPoint,因此這里將處理請求的類取名為EndPoint。
EndPoint類中包含三個(gè)成員變量:
- sock:表示與客戶端進(jìn)行通信的套接字。
- http_request:表示客戶端發(fā)來的HTTP請求。
- http_response:表示將會發(fā)送給客戶端的HTTP響應(yīng)。
EndPoint類中主要包含四個(gè)成員函數(shù):
- RecvHttpRequest:讀取客戶端發(fā)來的HTTP請求。
- HandlerHttpRequest:處理客戶端發(fā)來的HTTP請求。
- BuildHttpResponse:構(gòu)建將要發(fā)送給客戶端的HTTP響應(yīng)。
- SendHttpResponse:發(fā)送HTTP響應(yīng)給客戶端。
代碼如下:
class EndPoint{
private:
int _sock; //通信的套接字
HttpRequest _http_request; //HTTP請求
HttpResponse _http_response; //HTTP響應(yīng)
public:
EndPoint(int sock)
:_sock(sock)
{}
//讀取請求
void RecvHttpRequest();
//處理請求
void HandlerHttpRequest();
//構(gòu)建響應(yīng)
void BuildHttpResponse();
//發(fā)送響應(yīng)
void SendHttpResponse();
~EndPoint()
{}
};
設(shè)計(jì)線程回調(diào)
設(shè)計(jì)線程回調(diào)
服務(wù)器每獲取到一個(gè)新連接就會創(chuàng)建一個(gè)新線程來進(jìn)行處理,而這個(gè)線程要做的實(shí)際就是定義一個(gè)EndPoint對象,然后依次進(jìn)行讀取請求、處理請求、構(gòu)建響應(yīng)、發(fā)送響應(yīng),處理完畢后將與客戶端建立的套接字關(guān)閉即可。
代碼如下:
public:
static void* HandlerRequest(void* arg)
{
LOG(INFO, "handler request begin");
int sock = *(int*)arg;
EndPoint* ep = new EndPoint(sock);
ep->RecvHttpRequest(); //讀取請求
ep->HandlerHttpRequest(); //處理請求
ep->BuildHttpResponse(); //構(gòu)建響應(yīng)
ep->SendHttpResponse(); //發(fā)送響應(yīng)
close(sock); //關(guān)閉與該客戶端建立的套接字
delete ep;
LOG(INFO, "handler request end");
return nullptr;
}
};
讀取HTTP請求
讀取HTTP請求
讀取HTTP請求的同時(shí)可以對HTTP請求進(jìn)行解析,這里我們分為五個(gè)步驟,分別是讀取請求行、讀取請求報(bào)頭和空行、解析請求行、解析請求報(bào)頭、讀取請求正文。
代碼如下:
class EndPoint{
private:
int _sock; //通信的套接字
HttpRequest _http_request; //HTTP請求
HttpResponse _http_response; //HTTP響應(yīng)
public:
//讀取請求
void RecvHttpRequest()
{
RecvHttpRequestLine(); //讀取請求行
RecvHttpRequestHeader(); //讀取請求報(bào)頭和空行
ParseHttpRequestLine(); //解析請求行
ParseHttpRequestHeader(); //解析請求報(bào)頭
RecvHttpRequestBody(); //讀取請求正文
}
};
一、讀取請求行
讀取請求行很簡單,就是從套接字中讀取一行內(nèi)容存儲到HTTP請求類中的request_line中即可。
代碼如下:
class EndPoint{
private:
int _sock; //通信的套接字
HttpRequest _http_request; //HTTP請求
HttpResponse _http_response; //HTTP響應(yīng)
private:
//讀取請求行
void RecvHttpRequestLine()
{
auto& line = _http_request._request_line;
if(Util::ReadLine(_sock, line) > 0){
line.resize(line.size() - 1); //去掉讀取上來的n
}
}
};
需要注意的是,這里在按行讀取HTTP請求時(shí),不能直接使用C/C++提供的gets或getline函數(shù)進(jìn)行讀取,因?yàn)椴煌脚_下的行分隔符可能是不一樣的,可能是r、n或者rn。
比如下面是用WFetch請求百度首頁時(shí)得到的HTTP響應(yīng),可以看到其中使用的行分隔符就是rn:
因此我們這里需要自己寫一個(gè)ReadLine函數(shù),以確保能夠兼容這三種行分隔符。我們可以把這個(gè)函數(shù)寫到一個(gè)工具類當(dāng)中,后續(xù)編寫的處理字符串的函數(shù)也都寫到這個(gè)類當(dāng)中。
ReadLine函數(shù)的處理邏輯如下:
- 從指定套接字中讀取一個(gè)個(gè)字符。
- 如果讀取到的字符既不是n也不是r,則將讀取到的字符push到用戶提供的緩沖區(qū)后繼續(xù)讀取下一個(gè)字符。
- 如果讀取到的字符是n,則說明行分隔符是n,此時(shí)將npush到用戶提供的緩沖區(qū)后停止讀取。
- 如果讀取到的字符是r,則需要繼續(xù)窺探下一個(gè)字符是否是n,如果窺探成功則說明行分隔符為rn,此時(shí)將未讀取的n讀取上來后,將npush到用戶提供的緩沖區(qū)后停止讀?。蝗绻Q探失敗則說明行分隔符是r,此時(shí)也將npush到用戶提供的緩沖區(qū)后停止讀取。
也就是說,無論是哪一種行分隔符,最終讀取完一行后我們都把npush到了用戶提供的緩沖區(qū)當(dāng)中,相當(dāng)于將這三種行分隔符統(tǒng)一轉(zhuǎn)換成了以n為行分隔符,只不過最終我們把n一同讀取到了用戶提供的緩沖區(qū)中罷了,因此如果調(diào)用者不需要讀取上來的n,需要后續(xù)自行將其去掉。
代碼如下:
class Util{
public:
//讀取一行
static int ReadLine(int sock, std::string& out)
{
char ch = 'X'; //ch只要不初始化為n即可(保證能夠進(jìn)入while循環(huán))
while(ch != 'n'){
ssize_t size = recv(sock, &ch, 1, 0);
if(size > 0){
if(ch == 'r'){
//窺探下一個(gè)字符是否為n
recv(sock, &ch, 1, MSG_PEEK);
if(ch == 'n'){ //下一個(gè)字符是n
//rn->n
recv(sock, &ch, 1, 0); //將這個(gè)n讀走
}
else{ //下一個(gè)字符不是n
//r->n
ch = 'n'; //將ch設(shè)置為n
}
}
//普通字符或n
out.push_back(ch);
}
else if(size == 0){ //對方關(guān)閉連接
return 0;
}
else{ //讀取失敗
return -1;
}
}
return out.size(); //返回讀取到的字符個(gè)數(shù)
}
};
說明一下:recv函數(shù)的最后一個(gè)參數(shù)如果設(shè)置為MSG_PEEK,那么recv函數(shù)將返回TCP接收緩沖區(qū)頭部指定字節(jié)個(gè)數(shù)的數(shù)據(jù),但是并不把這些數(shù)據(jù)從TCP接收緩沖區(qū)中取走,這個(gè)叫做數(shù)據(jù)的窺探功能。
二、讀取請求報(bào)頭和空行
由于HTTP的請求報(bào)頭和空行都是按行陳列的,因此可以循環(huán)調(diào)用ReadLine函數(shù)進(jìn)行讀取,并將讀取到的每行數(shù)據(jù)都存儲到HTTP請求類的request_header中,直到讀取到空行為止。
代碼如下:
class EndPoint{
private:
int _sock; //通信的套接字
HttpRequest _http_request; //HTTP請求
HttpResponse _http_response; //HTTP響應(yīng)
private:
//讀取請求報(bào)頭和空行
void RecvHttpRequestHeader()
{
std::string line;
while(true){
line.clear(); //每次讀取之前清空line
Util::ReadLine(_sock, line);
if(line == "n"){ //讀取到了空行
_http_request._blank = line;
break;
}
//讀取到一行請求報(bào)頭
line.resize(line.size() - 1); //去掉讀取上來的n
_http_request._request_header.push_back(line);
}
}
};
說明一下:
- 由于ReadLine函數(shù)是將讀取到的數(shù)據(jù)直接push_back到用戶提供的緩沖區(qū)中的,因此每次調(diào)用ReadLine函數(shù)進(jìn)行讀取之前需要將緩沖區(qū)清空。
- ReadLine函數(shù)會將行分隔符n一同讀取上來,但對于我們來說n并不是有效數(shù)據(jù),因此在將讀取到的行存儲到HTTP請求類的request_header中之前,需要先將n去掉。
三、解析請求行
解析請求行要做的就是將請求行中的請求方法、URI和HTTP版本號拆分出來,依次存儲到HTTP請求類的method、uri和version中,由于請求行中的這些數(shù)據(jù)都是以空格作為分隔符的,因此可以借助一個(gè)stringstream對象來進(jìn)行拆分。此外,為了后續(xù)能夠正確判斷用戶的請求方法,這里需要通過transform函數(shù)統(tǒng)一將請求方法轉(zhuǎn)換為全大寫。
代碼如下:
class EndPoint{
private:
int _sock; //通信的套接字
HttpRequest _http_request; //HTTP請求
HttpResponse _http_response; //HTTP響應(yīng)
private:
//解析請求行
void ParseHttpRequestLine()
{
auto& line = _http_request._request_line;
//通過stringstream拆分請求行
std::stringstream ss(line);
ss>>_http_request._method>>_http_request._uri>>_http_request._version;
//將請求方法統(tǒng)一轉(zhuǎn)換為全大寫
auto& method = _http_request._method;
std::transform(method.begin(), method.end(), method.begin(), toupper);
}
};
四、解析請求報(bào)頭
解析請求報(bào)頭要做的就是將讀取到的一行一行的請求報(bào)頭,以: 為分隔符拆分成一個(gè)個(gè)的鍵值對存儲到HTTP請求的header_kv中,后續(xù)就可以直接通過屬性名獲取到對應(yīng)的值了。
代碼如下:
//服務(wù)端EndPoint
class EndPoint{
private:
int _sock; //通信的套接字
HttpRequest _http_request; //HTTP請求
HttpResponse _http_response; //HTTP響應(yīng)
private:
//解析請求報(bào)頭
void ParseHttpRequestHeader()
{
std::string key;
std::string value;
for(auto& iter : _http_request._request_header){
//將每行請求報(bào)頭打散成kv鍵值對,插入到unordered_map中
if(Util::CutString(iter, key, value, SEP)){
_http_request._header_kv.insert({key, value});
}
}
}
};
此處用于切割字符串的CutString函數(shù)也可以寫到工具類中,切割字符串時(shí)先通過find方法找到指定的分隔符,然后通過substr提取切割后的子字符串即可。
代碼如下:
class Util{
public:
//切割字符串
static bool CutString(std::string& target, std::string& sub1_out, std::string& sub2_out, std::string sep)
{
size_t pos = target.find(sep, 0);
if(pos != std::string::npos){
sub1_out = target.substr(0, pos);
sub2_out = target.substr(pos + sep.size());
return true;
}
return false;
}
};
五、讀取請求正文
在讀取請求正文之前,首先需要通過本次的請求方法來判斷是否需要讀取請求正文,因?yàn)橹挥姓埱蠓椒ㄊ荘OST方法才可能會有請求正文,此外,如果請求方法為POST,我們還需要通過請求報(bào)頭中的Content-Length屬性來得知請求正文的長度。
在得知需要讀取請求正文以及請求正文的長度后,就可以將請求正文讀取到HTTP請求類的request_body中了。
代碼如下:
class EndPoint{
private:
int _sock; //通信的套接字
HttpRequest _http_request; //HTTP請求
HttpResponse _http_response; //HTTP響應(yīng)
private:
//判斷是否需要讀取請求正文
bool IsNeedRecvHttpRequestBody()
{
auto& method = _http_request._method;
if(method == "POST"){ //請求方法為POST則需要讀取正文
auto& header_kv = _http_request._header_kv;
//通過Content-Length獲取請求正文長度
auto iter = header_kv.find("Content-Length");
if(iter != header_kv.end()){
_http_request._content_length = atoi(iter->second.c_str());
return true;
}
}
return false;
}
//讀取請求正文
void RecvHttpRequestBody()
{
if(IsNeedRecvHttpRequestBody()){ //先判斷是否需要讀取正文
int content_length = _http_request._content_length;
auto& body = _http_request._request_body;
//讀取請求正文
char ch = 0;
while(content_length){
ssize_t size = recv(_sock, &ch, 1, 0);
if(size > 0){
body.push_back(ch);
content_length--;
}
else{
break;
}
}
}
}
};
說明一下:
- 由于后續(xù)還會用到請求正文的長度,因此代碼中將其存儲到了HTTP請求類的content_length中。
- 在通過Content-Length獲取到請求正文的長度后,需要將請求正文長度從字符串類型轉(zhuǎn)換為整型。
處理HTTP請求
定義狀態(tài)碼
在處理請求的過程中可能會因?yàn)槟承┰蚨苯油V固幚?,比如請求方法不正確、請求資源不存在或服務(wù)器處理請求時(shí)出錯(cuò)等等。為了告知客戶端本次HTTP請求的處理情況,服務(wù)器需要定義不同的狀態(tài)碼,當(dāng)處理請求被終止時(shí)就可以設(shè)置對應(yīng)的狀態(tài)碼,后續(xù)構(gòu)建HTTP響應(yīng)的時(shí)候就可以根據(jù)狀態(tài)碼返回對應(yīng)的錯(cuò)誤頁面。
狀態(tài)碼定義如下:
#define BAD_REQUEST 400
#define NOT_FOUND 404
#define INTERNAL_SERVER_ERROR 500
處理HTTP請求
處理HTTP請求的步驟如下:
- 判斷請求方法是否是正確,如果不正確則設(shè)置狀態(tài)碼為BAD_REQUEST后停止處理。
- 如果請求方法為GET方法,則需要判斷URI中是否帶參。如果URI不帶參,則說明URI即為客戶端請求的資源路徑;如果URI帶參,則需要以?為分隔符對URI進(jìn)行字符串切分,切分后?左邊的內(nèi)容就是客戶端請求的資源路徑,而?右邊的內(nèi)容則是GET方法攜帶的參數(shù),由于此時(shí)GET方法攜帶了參數(shù),因此后續(xù)處理需要使用CGI模式,于是需要將HTTP請求類中的cgi設(shè)置為true。
- 如果請求方法為POST方法,則說明URI即為客戶端請求的資源路徑,由于POST方法會通過請求正文上傳參數(shù),因此后續(xù)處理需要使用CGI模式,于是需要將HTTP請求類中的cgi設(shè)置為true。
- 接下來需要對客戶端請求的資源路徑進(jìn)行處理,首先需要在請求的資源路徑前拼接上web根目錄,然后需要判斷請求資源路徑的最后一個(gè)字符是否是/,如果是則說明客戶端請求的是一個(gè)目錄,這時(shí)服務(wù)器不會將該目錄下全部的資源都返回給客戶端,而是默認(rèn)將該目錄下的index.html返回給客戶端,因此這時(shí)還需要在請求資源路徑的后面拼接上index.html。
- 對請求資源的路徑進(jìn)行處理后,需要通過stat函數(shù)獲取客戶端請求資源文件的屬性信息。如果客戶端請求的是一個(gè)目錄,則需要在請求資源路徑的后面拼接上/index.html并重新獲取資源文件的屬性信息;如果客戶端請求的是一個(gè)可執(zhí)行程序,則說明后續(xù)處理需要使用CGI模式,于是需要將HTTP請求類中的cgi設(shè)置為true。
- 根據(jù)HTTP請求類中的cgi分別進(jìn)行CGI或非CGI處理。
代碼如下:
#define HOME_PAGE "index.html"
//服務(wù)端EndPoint
class EndPoint{
private:
int _sock; //通信的套接字
HttpRequest _http_request; //HTTP請求
HttpResponse _http_response; //HTTP響應(yīng)
public:
//處理請求
void HandlerHttpRequest()
{
auto& code = _http_response._status_code;
if(_http_request._method != "GET"&&_http_request._method != "POST"){ //非法請求
LOG(WARNING, "method is not right");
code = BAD_REQUEST; //設(shè)置對應(yīng)的狀態(tài)碼,并直接返回
return;
}
if(_http_request._method == "GET"){
size_t pos = _http_request._uri.find('?');
if(pos != std::string::npos){ //uri中攜帶參數(shù)
//切割uri,得到客戶端請求資源的路徑和uri中攜帶的參數(shù)
Util::CutString(_http_request._uri, _http_request._path, _http_request._query_string, "?");
_http_request._cgi = true; //上傳了參數(shù),需要使用CGI模式
}
else{ //uri中沒有攜帶參數(shù)
_http_request._path = _http_request._uri; //uri即是客戶端請求資源的路徑
}
}
else if(_http_request._method == "POST"){
_http_request._path = _http_request._uri; //uri即是客戶端請求資源的路徑
_http_request._cgi = true; //上傳了參數(shù),需要使用CGI模式
}
else{
//Do Nothing
}
//給請求資源路徑拼接web根目錄
std::string path = _http_request._path;
_http_request._path = WEB_ROOT;
_http_request._path += path;
//請求資源路徑以/結(jié)尾,說明請求的是一個(gè)目錄
if(_http_request._path[_http_request._path.size() - 1] == '/'){
//拼接上該目錄下的index.html
_http_request._path += HOME_PAGE;
}
//獲取請求資源文件的屬性信息
struct stat st;
if(stat(_http_request._path.c_str(), &st) == 0){ //屬性信息獲取成功,說明該資源存在
if(S_ISDIR(st.st_mode)){ //該資源是一個(gè)目錄
_http_request._path += "/"; //需要拼接/,以/結(jié)尾的目錄前面已經(jīng)處理過了
_http_request._path += HOME_PAGE; //拼接上該目錄下的index.html
stat(_http_request._path.c_str(), &st); //需要重新資源文件的屬性信息
}
else if(st.st_mode&S_IXUSR||st.st_mode&S_IXGRP||st.st_mode&S_IXOTH){ //該資源是一個(gè)可執(zhí)行程序
_http_request._cgi = true; //需要使用CGI模式
}
_http_response._size = st.st_size; //設(shè)置請求資源文件的大小
}
else{ //屬性信息獲取失敗,可以認(rèn)為該資源不存在
LOG(WARNING, _http_request._path + " NOT_FOUND");
code = NOT_FOUND; //設(shè)置對應(yīng)的狀態(tài)碼,并直接返回
return;
}
//獲取請求資源文件的后綴
size_t pos = _http_request._path.rfind('.');
if(pos == std::string::npos){
_http_response._suffix = ".html"; //默認(rèn)設(shè)置
}
else{
_http_response._suffix = _http_request._path.substr(pos);
}
//進(jìn)行CGI或非CGI處理
if(_http_request._cgi == true){
code = ProcessCgi(); //以CGI的方式進(jìn)行處理
}
else{
code = ProcessNonCgi(); //簡單的網(wǎng)頁返回,返回靜態(tài)網(wǎng)頁
}
}
};
說明一下:
- 本項(xiàng)目實(shí)現(xiàn)的HTTP服務(wù)器只支持GET方法和POST方法,因此如果客戶端發(fā)來的HTTP請求中不是這兩種方法則認(rèn)為請求方法錯(cuò)誤,如果想讓服務(wù)器支持其他的請求方法則直接增加對應(yīng)的邏輯即可。
- 服務(wù)器向外提供的資源都會放在web根目錄下,比如網(wǎng)頁、圖片、視頻等資源,本項(xiàng)目中的web根目錄取名為wwwroot。web根目錄下的所有子目錄下都會有一個(gè)首頁文件,當(dāng)用戶請求的資源是一個(gè)目錄時(shí),就會默認(rèn)返回該目錄下的首頁文件,本項(xiàng)目中的首頁文件取名為index.html。
- stat是一個(gè)系統(tǒng)調(diào)用函數(shù),它可以獲取指定文件的屬性信息,包括文件的inode編號、文件的權(quán)限、文件的大小等。如果調(diào)用stat函數(shù)獲取文件的屬性信息失敗,則可以認(rèn)為客戶端請求的這個(gè)資源文件不存在,此時(shí)直接設(shè)置狀態(tài)碼為NOT_FOUND后停止處理即可。
- 當(dāng)獲取文件的屬性信息后發(fā)現(xiàn)該文件是一個(gè)目錄,此時(shí)請求資源路徑一定不是以/結(jié)尾的,因?yàn)樵诖酥耙呀?jīng)對/結(jié)尾的請求資源路徑進(jìn)行過處理了,因此這時(shí)需要給請求資源路徑拼接上/index.html。
- 只要一個(gè)文件的擁有者、所屬組、other其中一個(gè)具有可執(zhí)行權(quán)限,則說明這是一個(gè)可執(zhí)行文件,此時(shí)就需要將HTTP請求類中的cgi設(shè)置為true。
- 由于后續(xù)構(gòu)建HTTP響應(yīng)時(shí)需要用到請求資源文件的后綴,因此代碼中對請求資源路徑通過從后往前找.的方式,來獲取請求資源文件的后綴,如果沒有找到.則默認(rèn)請求資源的后綴為.html。
- 由于請求資源文件的大小后續(xù)可能會用到,因此在獲取到請求資源文件的屬性后,可以將請求資源文件的大小保存到HTTP響應(yīng)類的size中。
CGI處理
CGI處理時(shí)需要?jiǎng)?chuàng)建子進(jìn)程進(jìn)行進(jìn)程程序替換,但是在創(chuàng)建子進(jìn)程之前需要先創(chuàng)建兩個(gè)匿名管道。這里站在父進(jìn)程角度對這兩個(gè)管道進(jìn)行命名,父進(jìn)程用于讀取數(shù)據(jù)的管道叫做input,父進(jìn)程用于寫入數(shù)據(jù)的管道叫做output。
示意圖如下:
創(chuàng)建匿名管道并創(chuàng)建子進(jìn)程后,需要父子進(jìn)程各自關(guān)閉兩個(gè)管道對應(yīng)的讀寫端:
- 對于父進(jìn)程來說,input管道是用來讀數(shù)據(jù)的,因此父進(jìn)程需要保留input[0]關(guān)閉input[1],而output管道是用來寫數(shù)據(jù)的,因此父進(jìn)程需要保留output[1]關(guān)閉output[0]。
- 對于子進(jìn)程來說,input管道是用來寫數(shù)據(jù)的,因此子進(jìn)程需要保留input[1]關(guān)閉input[0],而output管道是用來讀數(shù)據(jù)的,因此子進(jìn)程需要保留output[0]關(guān)閉output[1]。
此時(shí)父子進(jìn)程之間的通信信道已經(jīng)建立好了,但為了讓替換后的CGI程序從標(biāo)準(zhǔn)輸入讀取數(shù)據(jù)等價(jià)于從管道讀取數(shù)據(jù),向標(biāo)準(zhǔn)輸出寫入數(shù)據(jù)等價(jià)于向管道寫入數(shù)據(jù),因此在子進(jìn)程進(jìn)行進(jìn)程程序替換之前,還需要對子進(jìn)程進(jìn)行重定向。
假設(shè)子進(jìn)程保留的input[1]和output[0]對應(yīng)的文件描述符分別是3和4,那么子進(jìn)程對應(yīng)的文件描述符表的指向大致如下:
現(xiàn)在我們要做的就是將子進(jìn)程的標(biāo)準(zhǔn)輸入重定向到output管道,將子進(jìn)程的標(biāo)準(zhǔn)輸出重定向到input管道,也就是讓子進(jìn)程的0號文件描述符指向output管道,讓子進(jìn)程的1號文件描述符指向input管道。
示意圖如下:
此外,在子進(jìn)程進(jìn)行進(jìn)程程序替換之前,還需要進(jìn)行各種參數(shù)的傳遞:
- 首先需要將請求方法通過putenv函數(shù)導(dǎo)入環(huán)境變量,以供CGI程序判斷應(yīng)該以哪種方式讀取父進(jìn)程傳遞過來的參數(shù)。
- 如果請求方法為GET方法,則需要將URL中攜帶的參數(shù)通過導(dǎo)入環(huán)境變量的方式傳遞給CGI程序。
- 如果請求方法為POST方法,則需要將請求正文的長度通過導(dǎo)入環(huán)境變量的方式傳遞給CGI程序,以供CGI程序判斷應(yīng)該從管道讀取多少個(gè)參數(shù)。
此時(shí)子進(jìn)程就可以進(jìn)行進(jìn)程程序替換了,而父進(jìn)程需要做如下工作:
- 如果請求方法為POST方法,則父進(jìn)程需要將請求正文中的參數(shù)寫入管道中,以供被替換后的CGI程序進(jìn)行讀取。
- 然后父進(jìn)程要做的就是不斷調(diào)用read函數(shù),從管道中讀取CGI程序?qū)懭氲奶幚斫Y(jié)果,并將其保存到HTTP響應(yīng)類的response_body當(dāng)中。
- 管道中的數(shù)據(jù)讀取完畢后,父進(jìn)程需要調(diào)用waitpid函數(shù)等待CGI程序退出,并關(guān)閉兩個(gè)管道對應(yīng)的文件描述符,防止文件描述符泄露。
代碼如下:
class EndPoint{
private:
int _sock; //通信的套接字
HttpRequest _http_request; //HTTP請求
HttpResponse _http_response; //HTTP響應(yīng)
private:
//CGI處理
int ProcessCgi()
{
int code = OK; //要返回的狀態(tài)碼,默認(rèn)設(shè)置為200
auto& bin = _http_request._path; //需要執(zhí)行的CGI程序
auto& method = _http_request._method; //請求方法
//需要傳遞給CGI程序的參數(shù)
auto& query_string = _http_request._query_string; //GET
auto& request_body = _http_request._request_body; //POST
int content_length = _http_request._content_length; //請求正文的長度
auto& response_body = _http_response._response_body; //CGI程序的處理結(jié)果放到響應(yīng)正文當(dāng)中
//1、創(chuàng)建兩個(gè)匿名管道(管道命名站在父進(jìn)程角度)
//創(chuàng)建從子進(jìn)程到父進(jìn)程的通信信道
int input[2];
if(pipe(input) < 0){ //管道創(chuàng)建失敗,則返回對應(yīng)的狀態(tài)碼
LOG(ERROR, "pipe input error!");
code = INTERNAL_SERVER_ERROR;
return code;
}
//創(chuàng)建從父進(jìn)程到子進(jìn)程的通信信道
int output[2];
if(pipe(output) < 0){ //管道創(chuàng)建失敗,則返回對應(yīng)的狀態(tài)碼
LOG(ERROR, "pipe output error!");
code = INTERNAL_SERVER_ERROR;
return code;
}
//2、創(chuàng)建子進(jìn)程
pid_t pid = fork();
if(pid == 0){ //child
//子進(jìn)程關(guān)閉兩個(gè)管道對應(yīng)的讀寫端
close(input[0]);
close(output[1]);
//將請求方法通過環(huán)境變量傳參
std::string method_env = "METHOD=";
method_env += method;
putenv((char*)method_env.c_str());
if(method == "GET"){ //將query_string通過環(huán)境變量傳參
std::string query_env = "QUERY_STRING=";
query_env += query_string;
putenv((char*)query_env.c_str());
LOG(INFO, "GET Method, Add Query_String env");
}
else if(method == "POST"){ //將正文長度通過環(huán)境變量傳參
std::string content_length_env = "CONTENT_LENGTH=";
content_length_env += std::to_string(content_length);
putenv((char*)content_length_env.c_str());
LOG(INFO, "POST Method, Add Content_Length env");
}
else{
//Do Nothing
}
//3、將子進(jìn)程的標(biāo)準(zhǔn)輸入輸出進(jìn)行重定向
dup2(output[0], 0); //標(biāo)準(zhǔn)輸入重定向到管道的輸入
dup2(input[1], 1); //標(biāo)準(zhǔn)輸出重定向到管道的輸出
//4、將子進(jìn)程替換為對應(yīng)的CGI程序
execl(bin.c_str(), bin.c_str(), nullptr);
exit(1); //替換失敗
}
else if(pid < 0){ //創(chuàng)建子進(jìn)程失敗,則返回對應(yīng)的錯(cuò)誤碼
LOG(ERROR, "fork error!");
code = INTERNAL_SERVER_ERROR;
return code;
}
else{ //father
//父進(jìn)程關(guān)閉兩個(gè)管道對應(yīng)的讀寫端
close(input[1]);
close(output[0]);
if(method == "POST"){ //將正文中的參數(shù)通過管道傳遞給CGI程序
const char* start = request_body.c_str();
int total = 0;
int size = 0;
while(total < content_length && (size = write(output[1], start + total, request_body.size() - total)) > 0){
total += size;
}
}
//讀取CGI程序的處理結(jié)果
char ch = 0;
while(read(input[0], &ch, 1) > 0){
response_body.push_back(ch);
} //不會一直讀,當(dāng)另一端關(guān)閉后會繼續(xù)執(zhí)行下面的代碼
//等待子進(jìn)程(CGI程序)退出
int status = 0;
pid_t ret = waitpid(pid, &status, 0);
if(ret == pid){
if(WIFEXITED(status)){ //正常退出
if(WEXITSTATUS(status) == 0){ //結(jié)果正確
LOG(INFO, "CGI program exits normally with correct results");
code = OK;
}
else{
LOG(INFO, "CGI program exits normally with incorrect results");
code = BAD_REQUEST;
}
}
else{
LOG(INFO, "CGI program exits abnormally");
code = INTERNAL_SERVER_ERROR;
}
}
//關(guān)閉兩個(gè)管道對應(yīng)的文件描述符
close(input[0]);
close(output[1]);
}
return code; //返回狀態(tài)碼
}
};
說明一下:
- 在CGI處理過程中,如果管道創(chuàng)建失敗或者子進(jìn)程創(chuàng)建失敗,則屬于服務(wù)器端處理請求時(shí)出錯(cuò),此時(shí)返回INTERNAL_SERVER_ERROR狀態(tài)碼后停止處理即可。
- 環(huán)境變量是key=value形式的,因此在調(diào)用putenv函數(shù)導(dǎo)入環(huán)境變量前需要先正確構(gòu)建環(huán)境變量,此后被替換的CGI程序在調(diào)用getenv函數(shù)時(shí),就可以通過key獲取到對應(yīng)的value。
- 子進(jìn)程傳遞參數(shù)的代碼最好放在重定向之前,否則服務(wù)器運(yùn)行后無法看到傳遞參數(shù)對應(yīng)的日志信息,因?yàn)槿罩臼且詂out的方式打印到標(biāo)準(zhǔn)輸出的,而dup2函數(shù)調(diào)用后標(biāo)準(zhǔn)輸出已經(jīng)被重定向到了管道,此時(shí)打印的日志信息將會被寫入管道。
- 父進(jìn)程循環(huán)調(diào)用read函數(shù)從管道中讀取CGI程序的處理結(jié)果,當(dāng)CGI程序執(zhí)行結(jié)束時(shí)相當(dāng)于寫端進(jìn)程將寫端關(guān)閉了(文件描述符的生命周期隨進(jìn)程),此時(shí)讀端進(jìn)程將管道當(dāng)中的數(shù)據(jù)讀完后,就會繼續(xù)執(zhí)行后續(xù)代碼,而不會被阻塞。
- 父進(jìn)程在等待子進(jìn)程退出后,可以通過WIFEXITED判斷子進(jìn)程是否是正常退出,如果是正常退出再通過WEXITSTATUS判斷處理結(jié)果是否正確,然后根據(jù)不同情況設(shè)置對應(yīng)的狀態(tài)碼(此時(shí)就算子進(jìn)程異常退出或處理結(jié)果不正確也不能立即返回,需要讓父進(jìn)程繼續(xù)向后執(zhí)行,關(guān)閉兩個(gè)管道對應(yīng)的文件描述符,防止文件描述符泄露)。
非CGI處理
非CGI處理時(shí)只需要將客戶端請求的資源構(gòu)建成HTTP響應(yīng)發(fā)送給客戶端即可,理論上這里要做的就是打開目標(biāo)文件,將文件中的內(nèi)容讀取到HTTP響應(yīng)類的response_body中,以供后續(xù)發(fā)送HTTP響應(yīng)時(shí)進(jìn)行發(fā)送即可,但我們并不推薦這種做法。
因?yàn)镠TTP響應(yīng)類的response_body屬于用戶層的緩沖區(qū),而目標(biāo)文件是存儲在服務(wù)器的磁盤上的,按照這種方式需要先將文件內(nèi)容讀取到內(nèi)核層緩沖區(qū),再由操作系統(tǒng)將其拷貝到用戶層緩沖區(qū),發(fā)送響應(yīng)正文的時(shí)候又需要先將其拷貝到內(nèi)核層緩沖區(qū),再由操作系統(tǒng)將其發(fā)送給對應(yīng)的網(wǎng)卡進(jìn)行發(fā)送。
示意圖如下:
可以看到上述過程涉及數(shù)據(jù)在用戶層和內(nèi)核層的來回拷貝,但實(shí)際這個(gè)拷貝操作是不需要的,我們完全可以直接將磁盤當(dāng)中的目標(biāo)文件內(nèi)容讀取到內(nèi)核,再由內(nèi)核將其發(fā)送給對應(yīng)的網(wǎng)卡進(jìn)行發(fā)送。
示意圖如下:
要達(dá)到上述效果就需要使用sendfile函數(shù),該函數(shù)的功能就是將數(shù)據(jù)從一個(gè)文件描述符拷貝到另一個(gè)文件描述符,并且這個(gè)拷貝操作是在內(nèi)核中完成的,因此sendfile比單純的調(diào)用read和write更加高效。
但是需要注意的是,這里還不能直接調(diào)用sendfile函數(shù),因?yàn)閟endfile函數(shù)調(diào)用后文件內(nèi)容就發(fā)送出去了,而我們應(yīng)該構(gòu)建HTTP響應(yīng)后再進(jìn)行發(fā)送,因此我們這里要做的僅僅是將要發(fā)送的目標(biāo)文件打開即可,將打開文件對應(yīng)的文件描述符保存到HTTP響應(yīng)的fd當(dāng)中。
代碼如下:
class EndPoint{
private:
int _sock; //通信的套接字
HttpRequest _http_request; //HTTP請求
HttpResponse _http_response; //HTTP響應(yīng)
private:
//非CGI處理
int ProcessNonCgi()
{
//打開客戶端請求的資源文件,以供后續(xù)發(fā)送
_http_response._fd = open(_http_request._path.c_str(), O_RDONLY);
if(_http_response._fd >= 0){ //打開文件成功
return OK;
}
return INTERNAL_SERVER_ERROR; //打開文件失敗
}
};
說明一下: 如果打開文件失敗,則返回INTERNAL_SERVER_ERROR狀態(tài)碼表示服務(wù)器處理請求時(shí)出錯(cuò),而不能返回NOT_FOUND,因?yàn)橹罢{(diào)用stat獲取過客戶端請求資源的屬性信息,說明該資源文件是一定存在的。
構(gòu)建HTTP響應(yīng)
構(gòu)建HTTP響應(yīng)
構(gòu)建HTTP響應(yīng)首先需要構(gòu)建的就是狀態(tài)行,狀態(tài)行由狀態(tài)碼、狀態(tài)碼描述、HTTP版本構(gòu)成,并以空格作為分隔符,將狀態(tài)行構(gòu)建好后保存到HTTP響應(yīng)的status_line當(dāng)中即可,而響應(yīng)報(bào)頭需要根據(jù)請求是否正常處理完畢分別進(jìn)行構(gòu)建。
代碼如下:
#define LINE_END "rn"
#define PAGE_400 "400.html"
#define PAGE_404 "404.html"
#define PAGE_500 "500.html"
//服務(wù)端EndPoint
class EndPoint{
private:
int _sock; //通信的套接字
HttpRequest _http_request; //HTTP請求
HttpResponse _http_response; //HTTP響應(yīng)
public:
//構(gòu)建響應(yīng)
void BuildHttpResponse()
{
int code = _http_response._status_code;
//構(gòu)建狀態(tài)行
auto& status_line = _http_response._status_line;
status_line += HTTP_VERSION;
status_line += " ";
status_line += std::to_string(code);
status_line += " ";
status_line += CodeToDesc(code);
status_line += LINE_END;
//構(gòu)建響應(yīng)報(bào)頭
std::string path = WEB_ROOT;
path += "/";
switch(code){
case OK:
BuildOkResponse();
break;
case NOT_FOUND:
path += PAGE_404;
HandlerError(path);
break;
case BAD_REQUEST:
path += PAGE_400;
HandlerError(path);
break;
case INTERNAL_SERVER_ERROR:
path += PAGE_500;
HandlerError(path);
break;
default:
break;
}
}
};
注意:本項(xiàng)目中將服務(wù)器的行分隔符設(shè)置為rn,在構(gòu)建完?duì)顟B(tài)行以及每行響應(yīng)報(bào)頭之后都需要加上對應(yīng)的行分隔符,而在HTTP響應(yīng)類的構(gòu)造函數(shù)中已經(jīng)將空行初始化為了LINE_END,因此在構(gòu)建HTTP響應(yīng)時(shí)不用處理空行。
對于狀態(tài)行中的狀態(tài)碼描述,我們可以編寫一個(gè)函數(shù),該函數(shù)能夠根據(jù)狀態(tài)碼返回對應(yīng)的狀態(tài)碼描述。
代碼如下:
static std::string CodeToDesc(int code)
{
std::string desc;
switch(code){
case 200:
desc = "OK";
break;
case 400:
desc = "Bad Request";
break;
case 404:
desc = "Not Found";
break;
case 500:
desc = "Internal Server Error";
break;
default:
break;
}
return desc;
}
構(gòu)建響應(yīng)報(bào)頭(請求正常處理完畢)
構(gòu)建HTTP的響應(yīng)報(bào)頭時(shí),我們至少需要構(gòu)建Content-Type和Content-Length這兩個(gè)響應(yīng)報(bào)頭,分別用于告知對方響應(yīng)資源的類型和響應(yīng)資源的長度。
對于請求正常處理完畢的HTTP請求,需要根據(jù)客戶端請求資源的后綴來得知返回資源的類型。而返回資源的大小需要根據(jù)該請求被處理的方式來得知,如果該請求是以非CGI方式進(jìn)行處理的,那么返回資源的大小早已在獲取請求資源屬性時(shí)被保存到了HTTP響應(yīng)類中的size當(dāng)中,如果該請求是以CGI方式進(jìn)行處理的,那么返回資源的大小應(yīng)該是HTTP響應(yīng)類中的response_body的大小。
代碼如下:
class EndPoint{
private:
int _sock; //通信的套接字
HttpRequest _http_request; //HTTP請求
HttpResponse _http_response; //HTTP響應(yīng)
private:
void BuildOkResponse()
{
//構(gòu)建響應(yīng)報(bào)頭
std::string content_type = "Content-Type: ";
content_type += SuffixToDesc(_http_response._suffix);
content_type += LINE_END;
_http_response._response_header.push_back(content_type);
std::string content_length = "Content-Length: ";
if(_http_request._cgi){ //以CGI方式請求
content_length += std::to_string(_http_response._response_body.size());
}
else{ //以非CGI方式請求
content_length += std::to_string(_http_response._size);
}
content_length += LINE_END;
_http_response._response_header.push_back(content_length);
}
};
對于返回資源的類型,我們可以編寫一個(gè)函數(shù),該函數(shù)能夠根據(jù)文件后綴返回對應(yīng)的文件類型。查看Content-Type轉(zhuǎn)化表可以得知后綴與文件類型的對應(yīng)關(guān)系,將這個(gè)對應(yīng)關(guān)系存儲一個(gè)unordered_map容器中,當(dāng)需要根據(jù)后綴得知文件類型時(shí)直接在這個(gè)unordered_map容器中進(jìn)行查找,如果找到了則返回對應(yīng)的文件類型,如果沒有找到則默認(rèn)該文件類型為text/html。
代碼如下:
static std::string SuffixToDesc(const std::string& suffix)
{
static std::unordered_map suffix_to_desc = {
{".html", "text/html"},
{".css", "text/css"},
{".js", "application/x-javascript"},
{".jpg", "application/x-jpg"},
{".xml", "text/xml"}
};
auto iter = suffix_to_desc.find(suffix);
if(iter != suffix_to_desc.end()){
return iter->second;
}
return "text/html"; //所給后綴未找到則默認(rèn)該資源為html文件
},>
構(gòu)建響應(yīng)報(bào)頭(請求處理出現(xiàn)錯(cuò)誤)
對于請求處理過程中出現(xiàn)錯(cuò)誤的HTTP請求,服務(wù)器將會為其返回對應(yīng)的錯(cuò)誤頁面,因此返回的資源類型就是text/html,而返回資源的大小可以通過獲取錯(cuò)誤頁面對應(yīng)的文件屬性信息來得知。此外,為了后續(xù)發(fā)送響應(yīng)時(shí)可以直接調(diào)用sendfile進(jìn)行發(fā)送,這里需要將錯(cuò)誤頁面對應(yīng)的文件打開,并將對應(yīng)的文件描述符保存在HTTP響應(yīng)類的fd當(dāng)中。
代碼如下:
class EndPoint{
private:
int _sock; //通信的套接字
HttpRequest _http_request; //HTTP請求
HttpResponse _http_response; //HTTP響應(yīng)
private:
void HandlerError(std::string page)
{
_http_request._cgi = false; //需要返回對應(yīng)的錯(cuò)誤頁面(非CGI返回)
//打開對應(yīng)的錯(cuò)誤頁面文件,以供后續(xù)發(fā)送
_http_response._fd = open(page.c_str(), O_RDONLY);
if(_http_response._fd > 0){ //打開文件成功
//構(gòu)建響應(yīng)報(bào)頭
struct stat st;
stat(page.c_str(), &st); //獲取錯(cuò)誤頁面文件的屬性信息
std::string content_type = "Content-Type: text/html";
content_type += LINE_END;
_http_response._response_header.push_back(content_type);
std::string content_length = "Content-Length: ";
content_length += std::to_string(st.st_size);
content_length += LINE_END;
_http_response._response_header.push_back(content_length);
_http_response._size = st.st_size; //重新設(shè)置響應(yīng)文件的大小
}
}
};
特別注意:對于處理請求時(shí)出錯(cuò)的HTTP請求,需要將其HTTP請求類中的cgi重新設(shè)置為false,因?yàn)楹罄m(xù)發(fā)送HTTP響應(yīng)時(shí),需要根據(jù)HTTP請求類中的cgi來進(jìn)行響應(yīng)正文的發(fā)送,當(dāng)請求處理出錯(cuò)后要返回給客戶端的本質(zhì)就是一個(gè)錯(cuò)誤頁面文件,相當(dāng)于是以非CGI方式進(jìn)行處理的。
發(fā)送HTTP響應(yīng)
發(fā)送HTTP響應(yīng)
發(fā)送HTTP響應(yīng)的步驟如下:
- 調(diào)用send函數(shù),依次發(fā)送狀態(tài)行、響應(yīng)報(bào)頭和空行。
- 發(fā)送響應(yīng)正文時(shí)需要判斷本次請求的處理方式,如果本次請求是以CGI方式成功處理的,那么待發(fā)送的響應(yīng)正文是保存在HTTP響應(yīng)類的response_body中的,此時(shí)調(diào)用send函數(shù)進(jìn)行發(fā)送即可。
- 如果本次請求是以非CGI方式處理或在處理過程中出錯(cuò)的,那么待發(fā)送的資源文件或錯(cuò)誤頁面文件對應(yīng)的文件描述符是保存在HTTP響應(yīng)類的fd中的,此時(shí)調(diào)用sendfile進(jìn)行發(fā)送即可,發(fā)送后關(guān)閉對應(yīng)的文件描述符。
代碼如下:
class EndPoint{
private:
int _sock; //通信的套接字
HttpRequest _http_request; //HTTP請求
HttpResponse _http_response; //HTTP響應(yīng)
public:
//發(fā)送響應(yīng)
void SendHttpResponse()
{
//發(fā)送狀態(tài)行
send(_sock, _http_response._status_line.c_str(), _http_response._status_line.size(), 0);
//發(fā)送響應(yīng)報(bào)頭
for(auto& iter : _http_response._response_header){
send(_sock, iter.c_str(), iter.size(), 0);
}
//發(fā)送空行
send(_sock, _http_response._blank.c_str(), _http_response._blank.size(), 0);
//發(fā)送響應(yīng)正文
if(_http_request._cgi){
auto& response_body = _http_response._response_body;
const char* start = response_body.c_str();
size_t size = 0;
size_t total = 0;
while(total < response_body.size()&&(size = send(_sock, start + total, response_body.size() - total, 0)) > 0){
total += size;
}
}
else{
sendfile(_sock, _http_response._fd, nullptr, _http_response._size);
//關(guān)閉請求的資源文件
close(_http_response._fd);
}
}
};
差錯(cuò)處理
至此服務(wù)器邏輯其實(shí)已經(jīng)已經(jīng)走通了,但你會發(fā)現(xiàn)服務(wù)器在處理請求的過程中有時(shí)會莫名其妙的崩潰,根本原因就是當(dāng)前服務(wù)器的錯(cuò)誤處理還沒有完全處理完畢。
邏輯錯(cuò)誤
邏輯錯(cuò)誤
邏輯錯(cuò)誤主要是服務(wù)器在處理請求的過程中出現(xiàn)的一些錯(cuò)誤,比如請求方法不正確、請求資源不存在或服務(wù)器處理請求時(shí)出錯(cuò)等等。邏輯錯(cuò)誤其實(shí)我們已經(jīng)處理過了,當(dāng)出現(xiàn)這類錯(cuò)誤時(shí)服務(wù)器會將對應(yīng)的錯(cuò)誤頁面返回給客戶端。
讀取錯(cuò)誤
讀取錯(cuò)誤
邏輯錯(cuò)誤是在服務(wù)器處理請求時(shí)可能出現(xiàn)的錯(cuò)誤,而在服務(wù)器處理請求之前首先要做的是讀取請求,在讀取請求的過程中出現(xiàn)的錯(cuò)誤就叫做讀取錯(cuò)誤,比如調(diào)用recv讀取請求時(shí)出錯(cuò)或讀取請求時(shí)對方連接關(guān)閉等。
出現(xiàn)讀取錯(cuò)誤時(shí),意味著服務(wù)器都沒有成功讀取完客戶端發(fā)來的HTTP請求,因此服務(wù)器也沒有必要進(jìn)行后續(xù)的處理請求、構(gòu)建響應(yīng)以及發(fā)送響應(yīng)的相關(guān)操作了。
可以在EndPoint類中新增一個(gè)bool類型的stop成員,表示是否停止本次處理,stop的值默認(rèn)設(shè)置為false,當(dāng)讀取請求出錯(cuò)時(shí)就直接設(shè)置stop為true并不再進(jìn)行后續(xù)的讀取操作,因此讀取HTTP請求的代碼需要稍作修改。
代碼如下:
class EndPoint{
private:
int _sock; //通信的套接字
HttpRequest _http_request; //HTTP請求
HttpResponse _http_response; //HTTP響應(yīng)
bool _stop; //是否停止本次處理
private:
//讀取請求行
bool RecvHttpRequestLine()
{
auto& line = _http_request._request_line;
if(Util::ReadLine(_sock, line) > 0){
line.resize(line.size() - 1); //去掉讀取上來的n
}
else{ //讀取出錯(cuò),則停止本次處理
_stop = true;
}
return _stop;
}
//讀取請求報(bào)頭和空行
bool RecvHttpRequestHeader()
{
std::string line;
while(true){
line.clear(); //每次讀取之前清空line
if(Util::ReadLine(_sock, line) <= 0){ //讀取出錯(cuò),則停止本次處理
_stop = true;
break;
}
if(line == "n"){ //讀取到了空行
_http_request._blank = line;
break;
}
//讀取到一行請求報(bào)頭
line.resize(line.size() - 1); //去掉讀取上來的n
_http_request._request_header.push_back(line);
}
return _stop;
}
//讀取請求正文
bool RecvHttpRequestBody()
{
if(IsNeedRecvHttpRequestBody()){ //先判斷是否需要讀取正文
int content_length = _http_request._content_length;
auto& body = _http_request._request_body;
//讀取請求正文
char ch = 0;
while(content_length){
ssize_t size = recv(_sock, &ch, 1, 0);
if(size > 0){
body.push_back(ch);
content_length--;
}
else{ //讀取出錯(cuò)或?qū)Χ岁P(guān)閉,則停止本次處理
_stop = true;
break;
}
}
}
return _stop;
}
public:
EndPoint(int sock)
:_sock(sock)
,_stop(false)
{}
//本次處理是否停止
bool IsStop()
{
return _stop;
}
//讀取請求
void RecvHttpRequest()
{
if(!RecvHttpRequestLine()&&!RecvHttpRequestHeader()){ //短路求值
ParseHttpRequestLine();
ParseHttpRequestHeader();
RecvHttpRequestBody();
}
}
};
說明一下:
- 可以將讀取請求行、讀取請求報(bào)頭和空行、讀取請求正文對應(yīng)函數(shù)的返回值改為bool類型,當(dāng)讀取請求行成功后再讀取請求報(bào)頭和空行,而當(dāng)讀取請求報(bào)頭和空行成功后才需要進(jìn)行后續(xù)的解析請求行、解析請求報(bào)頭以及讀取請求正文操作,這里利用到了邏輯運(yùn)算符的短路求值策略。
- EndPoint類當(dāng)中提供了IsStop函數(shù),用于讓外部處理線程得知是否應(yīng)該停止本次處理。
此時(shí)服務(wù)器創(chuàng)建的新線程在讀取請求后,就需要判斷是否應(yīng)該停止本次處理,如果需要?jiǎng)t不再進(jìn)行處理請求、構(gòu)建響應(yīng)以及發(fā)送響應(yīng)操作,而直接關(guān)閉于客戶端建立的套接字即可。
代碼如下:
public:
static void* HandlerRequest(void* arg)
{
LOG(INFO, "handler request begin");
int sock = *(int*)arg;
EndPoint* ep = new EndPoint(sock);
ep->RecvHttpRequest(); //讀取請求
if(!ep->IsStop()){
LOG(INFO, "Recv No Error, Begin Handler Request");
ep->HandlerHttpRequest(); //處理請求
ep->BuildHttpResponse(); //構(gòu)建響應(yīng)
ep->SendHttpResponse(); //發(fā)送響應(yīng)
}
else{
LOG(WARNING, "Recv Error, Stop Handler Request");
}
close(sock); //關(guān)閉與該客戶端建立的套接字
delete ep;
LOG(INFO, "handler request end");
return nullptr;
}
};
寫入錯(cuò)誤
寫入錯(cuò)誤
除了讀取請求時(shí)可能出現(xiàn)讀取錯(cuò)誤,處理請求時(shí)可能出現(xiàn)邏輯錯(cuò)誤,在響應(yīng)構(gòu)建完畢發(fā)送響應(yīng)時(shí)同樣可能會出現(xiàn)寫入錯(cuò)誤,比如調(diào)用send發(fā)送響應(yīng)時(shí)出錯(cuò)或發(fā)送響應(yīng)時(shí)對方連接關(guān)閉等。
出現(xiàn)寫入錯(cuò)誤時(shí),服務(wù)器也沒有必要繼續(xù)進(jìn)行發(fā)送了,這時(shí)需要直接設(shè)置stop為true并不再進(jìn)行后續(xù)的發(fā)送操作,因此發(fā)送HTTP響應(yīng)的代碼也需要進(jìn)行修改。
代碼如下:
class EndPoint{
private:
int _sock; //通信的套接字
HttpRequest _http_request; //HTTP請求
HttpResponse _http_response; //HTTP響應(yīng)
public:
//發(fā)送響應(yīng)
bool SendHttpResponse()
{
//發(fā)送狀態(tài)行
if(send(_sock, _http_response._status_line.c_str(), _http_response._status_line.size(), 0) <= 0){
_stop = true; //發(fā)送失敗,設(shè)置_stop
}
//發(fā)送響應(yīng)報(bào)頭
if(!_stop){
for(auto& iter : _http_response._response_header){
if(send(_sock, iter.c_str(), iter.size(), 0) <= 0){
_stop = true; //發(fā)送失敗,設(shè)置_stop
break;
}
}
}
//發(fā)送空行
if(!_stop){
if(send(_sock, _http_response._blank.c_str(), _http_response._blank.size(), 0) <= 0){
_stop = true; //發(fā)送失敗,設(shè)置_stop
}
}
//發(fā)送響應(yīng)正文
if(_http_request._cgi){
if(!_stop){
auto& response_body = _http_response._response_body;
const char* start = response_body.c_str();
size_t size = 0;
size_t total = 0;
while(total < response_body.size()&&(size = send(_sock, start + total, response_body.size() - total, 0)) > 0){
total += size;
}
}
}
else{
if(!_stop){
if(sendfile(_sock, _http_response._fd, nullptr, _http_response._size) <= 0){
_stop = true; //發(fā)送失敗,設(shè)置_stop
}
}
//關(guān)閉請求的資源文件
close(_http_response._fd);
}
return _stop;
}
};
此外,當(dāng)服務(wù)器發(fā)送響應(yīng)出錯(cuò)時(shí)會收到SIGPIPE信號,而該信號的默認(rèn)處理動(dòng)作是終止當(dāng)前進(jìn)程,為了防止服務(wù)器因?yàn)閷懭氤鲥e(cuò)而被終止,需要在初始化HTTP服務(wù)器時(shí)調(diào)用signal函數(shù)忽略SIGPIPE信號。
代碼如下:
class HttpServer{
private:
int _port; //端口號
public:
//初始化服務(wù)器
void InitServer()
{
signal(SIGPIPE, SIG_IGN); //忽略SIGPIPE信號,防止寫入時(shí)崩潰
}
};
接入線程池
當(dāng)前多線程版服務(wù)器存在的問題:
- 每當(dāng)獲取到新連接時(shí),服務(wù)器主線程都會重新為該客戶端創(chuàng)建為其提供服務(wù)的新線程,而當(dāng)服務(wù)結(jié)束后又會將該新線程銷毀,這樣做不僅麻煩,而且效率低下。
- 如果同時(shí)有大量的客戶端連接請求,此時(shí)服務(wù)器就要為每一個(gè)客戶端創(chuàng)建對應(yīng)的服務(wù)線程,而計(jì)算機(jī)中的線程越多,CPU壓力就越大,因?yàn)镃PU要不斷在這些線程之間來回切換。此外,一旦線程過多,每一個(gè)線程再次被調(diào)度的周期就變長了,而線程是為客戶端提供服務(wù)的,線程被調(diào)度的周期變長,客戶端也就遲遲得不到應(yīng)答。
這時(shí)可以在服務(wù)器端引入線程池:
- 在服務(wù)器端預(yù)先創(chuàng)建一批線程和一個(gè)任務(wù)隊(duì)列,每當(dāng)獲取到一個(gè)新連接時(shí)就將其封裝成一個(gè)任務(wù)對象放到任務(wù)隊(duì)列當(dāng)中。
- 線程池中的若干線程就不斷從任務(wù)隊(duì)列中獲取任務(wù)進(jìn)行處理,如果任務(wù)隊(duì)列當(dāng)中沒有任務(wù)則線程進(jìn)入休眠狀態(tài),當(dāng)有新任務(wù)時(shí)再喚醒線程進(jìn)行任務(wù)處理。
示意圖如下:
設(shè)計(jì)任務(wù)
設(shè)計(jì)任務(wù)
當(dāng)服務(wù)器獲取到一個(gè)新連接后,需要將其封裝成一個(gè)任務(wù)對象放到任務(wù)隊(duì)列當(dāng)中。任務(wù)類中首先需要有一個(gè)套接字,也就是與客戶端進(jìn)行通信的套接字,此外還需要有一個(gè)回調(diào)函數(shù),當(dāng)線程池中的線程獲取到任務(wù)后就可以調(diào)用這個(gè)回調(diào)函數(shù)進(jìn)行任務(wù)處理。
代碼如下:
class Task{
private:
int _sock; //通信的套接字
CallBack _handler; //回調(diào)函數(shù)
public:
Task()
{}
Task(int sock)
:_sock(sock)
{}
//處理任務(wù)
void ProcessOn()
{
_handler(_sock); //調(diào)用回調(diào)
}
~Task()
{}
};
說明一下: 任務(wù)類需要提供一個(gè)無參的構(gòu)造函數(shù),因?yàn)楹罄m(xù)從任務(wù)隊(duì)列中獲取任務(wù)時(shí),需要先以無參的方式定義一個(gè)任務(wù)對象,然后再以輸出型參數(shù)的方式來獲取任務(wù)。
編寫任務(wù)回調(diào)
任務(wù)類中處理任務(wù)時(shí)需要調(diào)用的回調(diào)函數(shù),實(shí)際就是之前創(chuàng)建新線程時(shí)傳入的執(zhí)行例程CallBack::HandlerRequest,我們可以將CallBack類的()運(yùn)算符重載為調(diào)用HandlerRequest函數(shù),這時(shí)CallBack對象就變成了一個(gè)仿函數(shù)對象,這個(gè)仿函數(shù)對象被調(diào)用時(shí)實(shí)際就是在調(diào)用HandlerRequest函數(shù)。
代碼如下:
public:
CallBack()
{}
void operator()(int sock)
{
HandlerRequest(sock);
}
void HandlerRequest(int sock)
{
LOG(INFO, "handler request begin");
EndPoint* ep = new EndPoint(sock);
ep->RecvHttpRequest(); //讀取請求
if(!ep->IsStop()){
LOG(INFO, "Recv No Error, Begin Handler Request");
ep->HandlerHttpRequest(); //處理請求
ep->BuildHttpResponse(); //構(gòu)建響應(yīng)
ep->SendHttpResponse(); //發(fā)送響應(yīng)
if(ep->IsStop()){
LOG(WARNING, "Send Error, Stop Send Response");
}
}
else{
LOG(WARNING, "Recv Error, Stop Handler Request");
}
close(sock); //關(guān)閉與該客戶端建立的套接字
delete ep;
LOG(INFO, "handler request end");
}
~CallBack()
{}
};
編寫線程池
設(shè)計(jì)線程池結(jié)構(gòu)
可以將線程池設(shè)計(jì)成單例模式:
- 將ThreadPool類的構(gòu)造函數(shù)設(shè)置為私有,并將拷貝構(gòu)造和拷貝賦值函數(shù)設(shè)置為私有或刪除,防止外部創(chuàng)建或拷貝對象。
- 提供一個(gè)指向單例對象的static指針,并在類外將其初始化為nullptr。
- 提供一個(gè)全局訪問點(diǎn)獲取單例對象,在單例對象第一次被獲取時(shí)就創(chuàng)建這個(gè)單例對象并進(jìn)行初始化。
ThreadPool類中的成員變量包括:
- 任務(wù)隊(duì)列:用于暫時(shí)存儲未被處理的任務(wù)對象。
- num:表示線程池中線程的個(gè)數(shù)。
- 互斥鎖:用于保證任務(wù)隊(duì)列在多線程環(huán)境下的線程安全。
- 條件變量:當(dāng)任務(wù)隊(duì)列中沒有任務(wù)時(shí),讓線程在該條件變量下進(jìn)行等等,當(dāng)任務(wù)隊(duì)列中新增任務(wù)時(shí),喚醒在該條件變量下進(jìn)行等待的線程。
- 指向單例對象的指針:用于指向唯一的單例線程池對象。
ThreadPool類中的成員函數(shù)主要包括:
- 構(gòu)造函數(shù):完成互斥鎖和條件變量的初始化操作。
- 析構(gòu)函數(shù):完成互斥鎖和條件變量的釋放操作。
- InitThreadPool:初始化線程池時(shí)調(diào)用,完成線程池中若干線程的創(chuàng)建。
- PushTask:生產(chǎn)任務(wù)時(shí)調(diào)用,將任務(wù)對象放入任務(wù)隊(duì)列,并喚醒在條件變量下等待的一個(gè)線程進(jìn)行處理。
- PopTask:消費(fèi)任務(wù)時(shí)調(diào)用,從任務(wù)隊(duì)列中獲取一個(gè)任務(wù)對象。
- ThreadRoutine:線程池中每個(gè)線程的執(zhí)行例程,完成線程分離后不斷檢測任務(wù)隊(duì)列中是否有任務(wù),如果有則調(diào)用PopTask獲取任務(wù)進(jìn)行處理,如果沒有則進(jìn)行休眠直到被喚醒。
- GetInstance:獲取單例線程池對象時(shí)調(diào)用,如果單例對象未創(chuàng)建則創(chuàng)建并初始化后返回,如果單例對象已經(jīng)創(chuàng)建則直接返回單例對象。
代碼如下:
//線程池
class ThreadPool{
private:
std::queue _task_queue; //任務(wù)隊(duì)列
int _num; //線程池中線程的個(gè)數(shù)
pthread_mutex_t _mutex; //互斥鎖
pthread_cond_t _cond; //條件變量
static ThreadPool* _inst; //指向單例對象的static指針
private:
//構(gòu)造函數(shù)私有
ThreadPool(int num = NUM)
:_num(num)
{
//初始化互斥鎖和條件變量
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
}
//將拷貝構(gòu)造函數(shù)和拷貝賦值函數(shù)私有或刪除(防拷貝)
ThreadPool(const ThreadPool&)=delete;
ThreadPool* operator=(const ThreadPool&)=delete;
//判斷任務(wù)隊(duì)列是否為空
bool IsEmpty()
{
return _task_queue.empty();
}
//任務(wù)隊(duì)列加鎖
void LockQueue()
{
pthread_mutex_lock(&_mutex);
}
//任務(wù)隊(duì)列解鎖
void UnLockQueue()
{
pthread_mutex_unlock(&_mutex);
}
//讓線程在條件變量下進(jìn)行等待
void ThreadWait()
{
pthread_cond_wait(&_cond, &_mutex);
}
//喚醒在條件變量下等待的一個(gè)線程
void ThreadWakeUp()
{
pthread_cond_signal(&_cond);
}
public:
//獲取單例對象
static ThreadPool* GetInstance()
{
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; //定義靜態(tài)的互斥鎖
//雙檢查加鎖
if(_inst == nullptr){
pthread_mutex_lock(&mtx); //加鎖
if(_inst == nullptr){
//創(chuàng)建單例線程池對象并初始化
_inst = new ThreadPool();
_inst->InitThreadPool();
}
pthread_mutex_unlock(&mtx); //解鎖
}
return _inst; //返回單例對象
}
//線程的執(zhí)行例程
static void* ThreadRoutine(void* arg)
{
pthread_detach(pthread_self()); //線程分離
ThreadPool* tp = (ThreadPool*)arg;
while(true){
tp->LockQueue(); //加鎖
while(tp->IsEmpty()){
//任務(wù)隊(duì)列為空,線程進(jìn)行wait
tp->ThreadWait();
}
Task task;
tp->PopTask(task); //獲取任務(wù)
tp->UnLockQueue(); //解鎖
task.ProcessOn(); //處理任務(wù)
}
}
//初始化線程池
bool InitThreadPool()
{
//創(chuàng)建線程池中的若干線程
pthread_t tid;
for(int i = 0;i < _num;i++){
if(pthread_create(&tid, nullptr, ThreadRoutine, this) != 0){
LOG(FATAL, "create thread pool error!");
return false;
}
}
LOG(INFO, "create thread pool success");
return true;
}
//將任務(wù)放入任務(wù)隊(duì)列
void PushTask(const Task& task)
{
LockQueue(); //加鎖
_task_queue.push(task); //將任務(wù)推入任務(wù)隊(duì)列
UnLockQueue(); //解鎖
ThreadWakeUp(); //喚醒一個(gè)線程進(jìn)行任務(wù)處理
}
//從任務(wù)隊(duì)列中拿任務(wù)
void PopTask(Task& task)
{
//獲取任務(wù)
task = _task_queue.front();
_task_queue.pop();
}
~ThreadPool()
{
//釋放互斥鎖和條件變量
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
};
//單例對象指針初始化為nullptr
ThreadPool* ThreadPool::_inst = nullptr;
說明一下:
- 由于線程的執(zhí)行例程的參數(shù)只能有一個(gè)void*類型的參數(shù),因此線程的執(zhí)行例程必須定義成靜態(tài)成員函數(shù),而線程執(zhí)行例程中又需要訪問任務(wù)隊(duì)列,因此需要將this指針作為參數(shù)傳遞給線程的執(zhí)行例程,這樣線程才能夠通過this指針訪問任務(wù)隊(duì)列。
- 在向任務(wù)隊(duì)列中放任務(wù)以及從任務(wù)隊(duì)列中獲取任務(wù)時(shí),都需要通過加鎖的方式來保證線程安全,而線程在調(diào)用PopTask之前已經(jīng)進(jìn)行過加鎖了,因此在PopTask函數(shù)中不必再加鎖。
- 當(dāng)任務(wù)隊(duì)列中有任務(wù)時(shí)會喚醒線程進(jìn)行任務(wù)處理,為了防止被偽喚醒的線程調(diào)用PopTask時(shí)無法獲取到任務(wù),因此需要以while的方式判斷任務(wù)隊(duì)列是否為空。
引入線程池后服務(wù)器要做的就是,每當(dāng)獲取到一個(gè)新連接時(shí)就構(gòu)建一個(gè)任務(wù),然后調(diào)用PushTask將其放入任務(wù)隊(duì)列即可。
代碼如下:
class HttpServer{
private:
int _port; //端口號
public:
//啟動(dòng)服務(wù)器
void Loop()
{
LOG(INFO, "loop begin");
TcpServer* tsvr = TcpServer::GetInstance(_port); //獲取TCP服務(wù)器單例對象
int listen_sock = tsvr->Sock(); //獲取監(jiān)聽套接字
while(true){
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len); //獲取新連接
if(sock < 0){
continue; //獲取失敗,繼續(xù)獲取
}
//打印客戶端相關(guān)信息
std::string client_ip = inet_ntoa(peer.sin_addr);
int client_port = ntohs(peer.sin_port);
LOG(INFO, "get a new link: ["+client_ip+":"+std::to_string(client_port)+"]");
//構(gòu)建任務(wù)并放入任務(wù)隊(duì)列中
Task task(sock);
ThreadPool::GetInstance()->PushTask(task);
}
}
};
項(xiàng)目測試
服務(wù)器結(jié)構(gòu)
至此HTTP服務(wù)器后端邏輯已經(jīng)全部編寫完畢,此時(shí)我們要做的就是將對外提供的資源文件放在一個(gè)名為wwwroot的目錄下,然后將生成的HTTP服務(wù)器可執(zhí)行程序與wwwroot放在同級目錄下。比如:
由于當(dāng)前HTTP服務(wù)器沒有任何業(yè)務(wù)邏輯,因此向外提供的資源文件只有三個(gè)錯(cuò)誤頁面文件,這些錯(cuò)誤頁面文件中的內(nèi)容大致如下:
404 Not Found
對不起,你所要訪問的資源不存在!
首頁請求測試
服務(wù)器首頁編寫
服務(wù)器的web根目錄下的資源文件主要有兩種,一種就是用于處理客戶端上傳上來的數(shù)據(jù)的CGI程序,另一種就是供客戶端請求的各種網(wǎng)頁文件了,而網(wǎng)頁的制作實(shí)際是前端工程師要做的,但現(xiàn)在我們要對服務(wù)器進(jìn)行測試,至少需要編寫一個(gè)首頁,首頁文件需要放在web根目錄下,取名為index.html。
以演示為主,首頁的代碼如下:
首頁請求測試
指定端口號運(yùn)行服務(wù)器后可以看到一系列日志信息被打印出來,包括套接字創(chuàng)建成功、綁定成功、監(jiān)聽成功,這時(shí)底層用于通信的TCP服務(wù)器已經(jīng)初始化成功了。
此時(shí)在瀏覽器上指定IP和端口訪問我們的HTTP服務(wù)器,由于我們沒有指定要訪問服務(wù)器web根目錄下的那個(gè)資源,此時(shí)服務(wù)器就會默認(rèn)將web根目錄下的index.html文件進(jìn)行返回,瀏覽器收到index.html文件后經(jīng)過刷新渲染就顯示出了對應(yīng)的首頁頁面。
同時(shí)服務(wù)器端也打印出了本次請求的一些日志信息。如下:
此時(shí)通過ps -aL命令可以看到線程池中的線程已經(jīng)被創(chuàng)建好了,其中PID和LWP相同的就是主線程,剩下的就是線程池中處理任務(wù)的若干新線程。如下:
錯(cuò)誤請求測試
錯(cuò)誤請求測試
如果我們請求的資源服務(wù)器并沒有提供,那么服務(wù)器就會在獲取請求資源屬性信息時(shí)失敗,這時(shí)服務(wù)器會停止本次請求處理,而直接將web根目錄下的404.html文件返回瀏覽器,瀏覽器收到后經(jīng)過刷新渲染就顯示出了對應(yīng)的404頁面。
這時(shí)在服務(wù)器端就能看到一條日志級別為WARNING的日志信息,這條日志信息中說明了客戶端請求的哪一個(gè)資源是不存在的。
GET方法上傳數(shù)據(jù)測試
編寫CGI程序
如果用戶請求服務(wù)器時(shí)上傳了數(shù)據(jù),那么服務(wù)器就需要將該數(shù)據(jù)后交給對應(yīng)的CGI程序進(jìn)行處理,因此在測試GET方法上傳數(shù)據(jù)之前,我們需要先編寫一個(gè)簡單的CGI程序。
首先,CGI程序啟動(dòng)后需要先獲取父進(jìn)程傳遞過來的數(shù)據(jù):
- 先通過getenv函數(shù)獲取環(huán)境變量中的請求方法。
- 如果請求方法為GET方法,則繼續(xù)通過getenv函數(shù)獲取父進(jìn)程傳遞過來的數(shù)據(jù)。
- 如果請求方法為POST方法,則先通過getenv函數(shù)獲取父進(jìn)程傳遞過來的數(shù)據(jù)的長度,然后再從0號文件描述符中讀取指定長度的數(shù)據(jù)即可。
代碼如下:
bool GetQueryString(std::string& query_string)
{
bool result = false;
std::string method = getenv("METHOD"); //獲取請求方法
if(method == "GET"){ //GET方法通過環(huán)境變量獲取參數(shù)
query_string = getenv("QUERY_STRING");
result = true;
}
else if(method == "POST"){ //POST方法通過管道獲取參數(shù)
int content_length = atoi(getenv("CONTENT_LENGTH"));
//從管道中讀取content_length個(gè)參數(shù)
char ch = 0;
while(content_length){
read(0, &ch, 1);
query_string += ch;
content_length--;
}
result = true;
}
else{
//Do Nothing
result = false;
}
return result;
}
CGI程序在獲取到父進(jìn)程傳遞過來的數(shù)據(jù)后,就可以根據(jù)具體的業(yè)務(wù)場景進(jìn)行數(shù)據(jù)處理了,比如用戶上傳的如果是一個(gè)關(guān)鍵字則需要CGI程序做搜索處理。我們這里以演示為目的,認(rèn)為用戶上傳的是形如a=10&b=20的兩個(gè)參數(shù),需要CGI程序進(jìn)行加減乘除運(yùn)算。
因此我們的CGI程序要做的就是,先以&為分隔符切割數(shù)據(jù)將兩個(gè)操作數(shù)分開,再以=為分隔符切割數(shù)據(jù)分別獲取到兩個(gè)操作數(shù)的值,最后對兩個(gè)操作數(shù)進(jìn)行加減乘除運(yùn)算,并將計(jì)算結(jié)果打印到標(biāo)準(zhǔn)輸出即可(標(biāo)準(zhǔn)輸出已經(jīng)被重定向到了管道)。
代碼如下:
bool CutString(std::string& in, const std::string& sep, std::string& out1, std::string& out2)
{
size_t pos = in.find(sep);
if(pos != std::string::npos){
out1 = in.substr(0, pos);
out2 = in.substr(pos + sep.size());
return true;
}
return false;
}
int main()
{
std::string query_string;
GetQueryString(query_string); //獲取參數(shù)
//以&為分隔符將兩個(gè)操作數(shù)分開
std::string str1;
std::string str2;
CutString(query_string, "&", str1, str2);
//以=為分隔符分別獲取兩個(gè)操作數(shù)的值
std::string name1;
std::string value1;
CutString(str1, "=", name1, value1);
std::string name2;
std::string value2;
CutString(str2, "=", name2, value2);
//處理數(shù)據(jù)
int x = atoi(value1.c_str());
int y = atoi(value2.c_str());
std::cout<<"";
std::cout<<"";
std::cout<<"";
std::cout<<"
"<";
std::cout<<"
"<";
std::cout<<"
"<";
std::cout<<"
"<"; //除0后cgi程序崩潰,屬于異常退出
std::cout<<"";
std::cout<<"";
return 0;
}<<">
<<"><<"><<">說明一下:
- CGI程序輸出的結(jié)果最終會交給瀏覽器,因此CGI程序輸出的最好是一個(gè)HTML文件,這樣瀏覽器收到后就可以其渲染到頁面上,讓用戶看起來更美觀。
- 可以看到,使用C/C++以HTML的格式進(jìn)行輸出是很費(fèi)勁的,因此這部分操作一般是由Python等語言來完成的,而在此之前對數(shù)據(jù)進(jìn)行業(yè)務(wù)處理的動(dòng)作一般才用C/C++等語言來完成。
- 在編寫CGI程序時(shí)如果要進(jìn)行調(diào)試,debug內(nèi)容應(yīng)該通過標(biāo)準(zhǔn)錯(cuò)誤流進(jìn)行輸出,因?yàn)樽舆M(jìn)程在被替換成CGI程序之前,已經(jīng)將標(biāo)準(zhǔn)輸出重定向到管道了。
URL上傳數(shù)據(jù)測試
CGI程序編寫編寫完畢并生成可執(zhí)行程序后,將這個(gè)可執(zhí)行程序放到web根目錄下,這時(shí)在請求服務(wù)器時(shí)就可以指定請求這個(gè)CGI程序,并通過URL上傳參數(shù)讓其進(jìn)行處理,最終我們就能得到計(jì)算結(jié)果。
此外,如果請求CGI程序時(shí)指定的第二個(gè)操作數(shù)為0,那么CGI程序在進(jìn)行除法運(yùn)算時(shí)就會崩潰,這時(shí)父進(jìn)程等待子進(jìn)程后就會發(fā)現(xiàn)子進(jìn)程是異常退出的,進(jìn)而設(shè)置狀態(tài)碼為INTERNAL_SERVER_ERROR,最終服務(wù)器就會構(gòu)建對應(yīng)的錯(cuò)誤頁面返回給瀏覽器。
表單上傳數(shù)據(jù)測試
當(dāng)然,讓用戶通過更改URL的方式來向服務(wù)器上傳參數(shù)是不現(xiàn)實(shí)的,服務(wù)器一般會讓用戶通過表單來上傳參數(shù)。
HTML中的表單用于搜集用戶的輸入,我們可以通過設(shè)置表單的method屬性來指定表單提交的方法,通過設(shè)置表單的action屬性來指定表單需要提交給服務(wù)器上的哪一個(gè)CGI程序。
比如現(xiàn)在將服務(wù)器的首頁改成以下HTML代碼,指定將表單中的數(shù)據(jù)以GET方法提交給web根目錄下的test_cgi程序:
操作數(shù)1:
操作數(shù)2:
此時(shí)我們直接訪問服務(wù)器看到的就是一個(gè)表單,向表單中輸入兩個(gè)操作數(shù)并點(diǎn)擊“計(jì)算”后,表單中的數(shù)據(jù)就會以GET方法提交給web根目錄下的test_cgi程序,此時(shí)CGI程序進(jìn)行數(shù)據(jù)計(jì)算后同樣將結(jié)果返回給了瀏覽器。
同時(shí)在提交表單的一瞬間可以看到,通過表單上傳的數(shù)據(jù)也回顯到了瀏覽器上方的URL中,并且請求的資源也變成了web根目錄下的test_cgi。實(shí)際就是我們在點(diǎn)擊“計(jì)算”后,瀏覽器檢測到表單method為“get”后,將把表單中數(shù)據(jù)添加到了URL中,并將請求資源路徑替換成了表單action指定的路徑,然后再次向服務(wù)器發(fā)起HTTP請求。
理解百度搜索
當(dāng)我們在百度的搜索框輸入關(guān)鍵字并回車后,可以看到上方的URL發(fā)生了變化,URL中的請求資源路徑為/s,并且URL后面攜帶了很多參數(shù)。
實(shí)際這里的/s就可以理解成是百度web根目錄下的一個(gè)CGI程序,而URL中攜帶的各種參數(shù)就是交給這個(gè)CGI程序做搜索處理的,可以看到攜帶的參數(shù)中有一個(gè)名為wd的參數(shù),這個(gè)參數(shù)正是用戶的搜索關(guān)鍵字。
POST方法上傳數(shù)據(jù)測試
表單上傳數(shù)據(jù)測試
測試表單通過POST方法上傳數(shù)據(jù)時(shí),只需要將表單中的method屬性改為“post”即可,此時(shí)點(diǎn)擊“計(jì)算”提交表單時(shí),瀏覽器檢測到表單的提交方法為POST后,就會將表單中的數(shù)據(jù)添加到請求正文中,并將請求資源路徑替換成表單action指定的路徑,然后再次向服務(wù)器發(fā)起HTTP請求。
可以看到,由于POST方法是通過請求正文上傳的數(shù)據(jù),因此表單提交后瀏覽器上方的URL中只有請求資源路徑發(fā)生了改變,而并沒有在URL后面添加任何參數(shù)。同時(shí)觀察服務(wù)器端輸出的日志信息,也可以確認(rèn)瀏覽器本次的請求方法為POST方法。
項(xiàng)目擴(kuò)展
當(dāng)前項(xiàng)目的重點(diǎn)在于HTTP服務(wù)器后端的處理邏輯,主要完成的是GET和POST請求方法,以及CGI機(jī)制的搭建。如果想對當(dāng)前項(xiàng)目進(jìn)行擴(kuò)展,可以選擇在技術(shù)層面或應(yīng)用層面進(jìn)行擴(kuò)展。
技術(shù)層面的擴(kuò)展
技術(shù)層面可以選擇進(jìn)行如下擴(kuò)展:
- 當(dāng)前項(xiàng)目編寫的是HTTP1.0版本的服務(wù)器,每次連接都只會對一個(gè)請求進(jìn)行處理,當(dāng)服務(wù)器對客戶端的請求處理完畢并收到客戶端的應(yīng)答后,就會直接斷開連接??梢詫⑵鋽U(kuò)展為HTTP1.1版本,讓服務(wù)器支持長連接,即通過一條連接可以對多個(gè)請求進(jìn)行處理,避免重復(fù)建立連接(涉及連接管理)。
- 當(dāng)前項(xiàng)目雖然在后端接入了線程池,但也只能滿足中小型應(yīng)用,可以考慮將服務(wù)器改寫成epoll版本,讓服務(wù)器的IO變得更高效。
- 可以給當(dāng)前的HTTP服務(wù)器新增代理功能,也就是可以替代客戶端去訪問某種服務(wù),然后將訪問結(jié)果再返回給客戶端。
應(yīng)用層面的擴(kuò)展
應(yīng)用層面可以選擇進(jìn)行如下擴(kuò)展:
- 基于當(dāng)前HTTP服務(wù)器,搭建在線博客。
- 基于當(dāng)前HTTP服務(wù)器,編寫在線畫圖板。
- 基于當(dāng)前HTTP服務(wù)器,編寫一個(gè)搜索引擎。
-
互聯(lián)網(wǎng)
+關(guān)注
關(guān)注
54文章
11073瀏覽量
102614 -
服務(wù)器
+關(guān)注
關(guān)注
12文章
8958瀏覽量
85085 -
網(wǎng)絡(luò)
+關(guān)注
關(guān)注
14文章
7485瀏覽量
88541 -
編程
+關(guān)注
關(guān)注
88文章
3565瀏覽量
93536 -
HTTP
+關(guān)注
關(guān)注
0文章
499瀏覽量
30980
發(fā)布評論請先 登錄
相關(guān)推薦
評論