用C++實現HTTP伺服器 - Windows平台
軟體名: Que's HTTP Server (點擊下載最新版含原始碼)
作者: 闕榮文 - Que's C++ Studio
著作權說明: 免費,開放原始碼,禁止用作商業用途.
日期: 2011.7.8
1. 目的和用途
用C++實現一個Windows下的,簡單的HTTP伺服器模型,用作區域網路內的檔案分享權限設定工具.
我以前在公司裡,同事之間經常要共用一些大檔案(超過2G),如果直接共用Windows目錄,非常不安全不說,經常還因為各種各樣的問題導致無法訪問:比如guest使用者被禁用,防火牆攔截等.現在,我只要開啟這個程式(一個非常簡單的程式,只有一個幾百K大小的EXE檔案),對方就可以用瀏覽器(IE,Firefox等都可以,還有比瀏覽器安裝範圍更廣的用戶端嗎?)訪問到我共用的檔案,還支援斷點續傳,多麼愜意.
另外,這是一個符合協議要求的HTTP伺服器,理論上說,放到公網上也是完全可以的,只是功能沒有成熟的Apache, IIS等豐富,安全效能也不足,但是作為一個學習的模型,提供檔案下載服務還是沒問題的.
2. 原理說明
(1) 關於HTTP協議.
我個人非常非常喜歡HTTP協議,這是一個簡潔的,基於文本的,應答式的協議.
基於文本的一大好處是調試特別方便,因為所有的資料都是可以直接讀出的文本,程式結果正確與否一目瞭然,另外還有很多其它好處,可以參考<<UNIX編程藝術>>,裡面有一章專門講基於文本的協議的好處,深以為然.
HTTP協議的交流過程可以簡單的描述為:雙方建立TCP串連,用戶端(通常是瀏覽器)發送一段固定格式的文本(所謂的請求),伺服器根據用戶端請求的內容,產生並回應一段固定格式的文本(所謂的回應頭)和檔案內容(如果用戶端請求的是檔案下載的話),資料發送完畢後,中斷連線.
這就是我們今天網路世界的基礎,沒錯,就這麼簡單.
當然,用戶端的請求和伺服器的回應頭都有格式要求,可以讀一讀W3C關於HTTP協議的文檔.(Que's HTTP Server發行包內也包含了一份,請點擊文章開頭的連結下載.)
(2) Windows的網路模型.
Windows系統中,當屬完成連接埠模型的網路效能最好.作為可能面臨巨大網路壓力的HTTP伺服器,完成連接埠模型是唯一的選擇.
完成連接埠模型用一種類似於回調的方式處理網路操作,所有的網路操作都不會阻塞,而是排隊後直接返回,等到操作完成,系統會觸發一個完成連接埠事件以通知應用程式.所以應用程式只要用一個線程一直檢查對應的完成連接埠就可以得到網路操作的結果.和回調不同的是,系統允許多個線程同時等待同一個完成連接埠事件,Windows系統確保對於一個完成連接埠時間,只有一個等待線程被喚醒.這樣,對於有多個CPU的機器,我們可以建立多個線程(一個CPU對應一個線程)等待同一個完成連接埠事件以並行的方式處理網路事件以充分發揮機器的效能.
關於完成連接埠編程方面的指導請參考<<Windows網路編程技術>>(微軟出版,Anthony Jones著)第八章.
3. 程式結構介紹
CHTTPServer對象
CHTTPServer對象代表了整個HTTP伺服器在記憶體中的存在,負責建立完成連接埠模型,建立所需的線程,監控完成連接埠事件並且維持URL和伺服器上的真實檔案的對應關係.除了維護網路模組外,CHTTPServer對象還要根據接收到的用戶端請求產生正確的伺服器響應.它的角色和ASP.NET或者JSP編程中用到的預定義Server對象是類似的.
CHTTPRequest對象
CHTTPRequest對象是對用戶端請求的封裝,由於用戶端請求是一段帶格式的文本,用程式設計語言說就是一個字串,為了編程方便,我建立了CHTTPRequest對象管理這個字串,CHTTPRequest對象提供了若干方法,可以很方便的訪問到用戶端請求的各個參數.它的角色和ASP.NET或者JSP編程中用到的預定義Request對象是類似的.
CHTTPResponse對象
CHTTPResponse對象是對伺服器響應的封裝,CHTTPServer對象根據CHTTPRequest對象的內容,設定CHTTPResponse對象的參數,最後由CHTTPResponse對象把這些參數輸出為符合HTTP協議要求的文本.它的角色和ASP.NET或者JSP編程中用到的預定義Response對象是類似的.
CHTTPContent對象
CHTTPContent對象代表了用戶端所請求的資源(URL).它可能是一個檔案,也可能就是一段伺服器即時產生的HTML/TEXT文本,比如某個目錄的檔案清單,或者是一個出錯資訊,如HTTP404檔案未找到的提示.
程式啟動並執行流程如下:
CHTTPServer對象啟動並監控網路模組,當一個新的用戶端串連建立以後,CHTTPServer對象開始讀取這個新串連,直到接收到一個完整的要求標頭為止.
當CHTTPServer對象收到一個完整的要求標頭後,它就以之為參數,建立一個新的CHTTPRequest對象,然後分析這個CHTTPRequest對象,產生正確的CHTTPResponse對象和CHTTPContent對象,然後把CHTTPResponse對象輸出的文本和CHTTPContent對象的內容按順序發送給用戶端,並在資料發送完成後關閉串連,回收上述的各個對象.
4. 核心程式碼分析
下面貼一段CHTTPServer對象根據CHTTPRequest對象的內容,產生CHTTPResponse對象和CHTTPContent對象的關鍵代碼.
// 已經完整接收到了一個要求標頭,處理之// 目的: 通過分析Request對象,準備好Response對象,並在可能的情況下產生一個Content對象並關聯到Response對象中.void CHTTPServer::OnRequest(PCLIENTINF pSockInf){ASSERT(pSockInf); ASSERT(pSockInf->pRequest); ASSERT(pSockInf->pResponse);std::wstring strUrlObject(L"");std::wstring strServerFilePath(L"");// 是否是有效要求標頭if(!pSockInf->pRequest->Verify()){// 要求標頭格式不正確,返回HTTP 400和一段關於400的預定義說明文本pSockInf->pResponse->SetServerCode(SC_BADREQUEST); // HTTP 400CHTTPContent *pContent = new CHTTPContent;pContent->OpenText(g_HTTP_Bad_Request, strlen(g_HTTP_Bad_Request));pSockInf->pResponse->AttachContent(pContent);goto exit;}// 請求的方法是否是 GET 或者 HEADHTTP_METHOD method = pSockInf->pRequest->GetMethod();pSockInf->pResponse->SetMethod(method);if(method != METHOD_GET && method != METHOD_HEAD){// 目前只支援兩種HTTP方法pSockInf->pResponse->SetServerCode(SC_BADMETHOD); // HTTP 405CHTTPContent *pContent = new CHTTPContent;pContent->OpenText(g_HTTP_Bad_Method, strlen(g_HTTP_Bad_Method));pSockInf->pResponse->AttachContent(pContent);goto exit;}// 擷取用戶端請求的對象strUrlObject = pSockInf->pRequest->GetUrlObject();if(strUrlObject.size() <= 0){// URL Object 為空白,說明用戶端的請求有問題.pSockInf->pResponse->SetServerCode(SC_BADREQUEST); // 要求標頭格式錯誤 HTTP 400CHTTPContent *pContent = new CHTTPContent;pContent->OpenText(g_HTTP_Bad_Request, strlen(g_HTTP_Bad_Request));pSockInf->pResponse->AttachContent(pContent);goto exit;}// 映射為伺服器檔案名稱.MapServerFile(strUrlObject, strServerFilePath);// 如果 URL 的最後一個字元是 '/' 說明請求檔案清單,否則是請求一個具體的檔案.if(strUrlObject.back() == L'/'){// 瀏覽目錄建立目錄列表的內容對象,並關聯給Response對象CHTTPContent *pContent = new CHTTPContent;if(m_bNavDir){if(pContent->OpenDir(strUrlObject, strServerFilePath)){pSockInf->pResponse->SetServerCode(SC_OK); // HTTP 200}else{// 無法列出目錄的檔案清單.pContent->OpenText(g_HTTP_Server_Error, strlen(g_HTTP_Server_Error));pSockInf->pResponse->SetServerCode(SC_SERVERERROR); // HTTP 500}}else{// 禁止瀏覽目錄pContent->OpenText(g_HTTP_Forbidden, strlen(g_HTTP_Forbidden));pSockInf->pResponse->SetServerCode(SC_FORBIDDEN); // HTTP 403}pSockInf->pResponse->AttachContent(pContent);}else{// 用戶端請求了伺服器上的一個檔案// 1.用戶端是否請求了斷點續傳的內容// 2.建立檔案內容對象並關聯給Response對象__int64 lFrom = 0;__int64 lTo = -1;if(pSockInf->pRequest->GetRange(lFrom, lTo)){pSockInf->pResponse->SetServerCode(SC_PARTIAL); //HTTP 206}else{pSockInf->pResponse->SetServerCode(SC_OK); // HTTP 200}CHTTPContent *pContent = new CHTTPContent;if(pContent->OpenFile(WtoA(strServerFilePath.c_str()).c_str(), lFrom, lTo)){// 檔案開啟成功.}else{// 檔案不存在或者其它什麼原因,開啟失敗.pContent->OpenHtml(g_HTTP_Content_NotFound, strlen(g_HTTP_Content_NotFound));pSockInf->pResponse->SetServerCode(SC_NOTFOUND); // HTTP 404}pSockInf->pResponse->AttachContent(pContent);}exit://///////// 準備回應標頭pSockInf->pResponse->CookResponse(); // 把上面設定的參數輸出為一段符合HTTP協議的文本. // 寫日誌.LOGGER_CINFO(theLogger, _T("串連[%s:%d]請求資源[%s],回應[HTTP %d].\r\n"), AtoW(pSockInf->pRequest->GetIP().c_str()).c_str(), pSockInf->pRequest->GetPort(), strUrlObject.c_str(),pSockInf->pResponse->GetServerCode()); return;}
5. 使用說明
這是一個非常輕量級的程式,不需要安裝,只有一個EXE檔案,運行後可以設定作為HTTP伺服器的根目錄的位置和服務連接埠.如果選中了"允許瀏覽目錄"的複選框,則允許用戶端瀏覽器列出伺服器上某個目錄下的所有檔案,比如客戶瀏覽器輸入 "http://xxxxx/汽車圖片/" 那麼將會看到伺服器根目錄下"汽車圖片"子目錄下的所有檔案的列表.如果不選中,則顯示"403 Forbidden"的提示.
程式啟動並執行效果如所示:
用Firefox瀏覽目錄的效果如下:
=====================================================================
更新日誌
說明:V1.51之前的代碼結構不好,很雜亂,介面也很簡陋,只是在公司內部作為一個方便檔案分享權限設定的小工具使用,沒有發布.
V1.51 Beta
1. 啟用新介面.
2. 重新編寫核心,開始出現 CHTTPContent對象.
3. 啟用新的日誌系統.
V1.52 Beta
現在伺服器支援超過2G的大檔案的下載了, 內部使用64位長的整數(之前使用32位整數,只能支援到2G的檔案)來記錄檔案長度.
V1.52 Beta Build 15279
瀏覽目錄時,現在不再列出系統檔案和隱藏檔案,並且所有的子目錄都在前面出現.
V1.52 Beta Build 152710
新增: 在狀態列顯示當前使用的頻寬(每2秒重新整理一次).
新增: 在狀態列顯示當前一共有多少個串連(即時).
修正: 在瀏覽目錄時,現在超過2G的檔案可以正確顯示大小.
修改: 現在,超出的日誌每次只移除前面100行(原來移除200行).
新增: 每個串連在關閉時,在日誌裡有體現伺服器為處理該串連所用的時間.
新增: 現在允許設定伺服器的最大串連數.
修改: 核心做了一小部分修改,使程式邏輯更清晰明了.
V1.52 Beta Build 152711
修正: 改正了一個內部同步錯誤,現在頻寬計算結果更精確了.
V1.52 Beta Build 152712
修正: 當一個串連發送資料超過2G時,現在在日誌中可以正確顯示了.
V1.52 Beta Build 152713
修正: 修正了下行頻寬統計的一個錯誤.
V1.52 Beta Build 152714
新增: 現在可以設定會話逾時了.
新增: 如果一個串連在指定時間內沒有發送或者接收任何資料,則會被判定為死串連而被移除.這個時間可以在設定介面設定.
修改: 瀏覽目錄時,如果一個檔案超過1G,則會顯示為"XXX GB" 而不是原來的 "XXXX MB".
修改: 一些介面提示性文本作了小修改.
V1.52 Beta Build 152715
修改: 重新繪製工具條表徵圖.
新增: 現在可以設定每個客戶(以IP為準)最多可以有多少個串連.
新增: 現在可以設定每個串連的最大頻寬. 注意:為保持串連的活躍性,每個串連的最小頻寬不會小於 250B/s.
修改: 不再為偵聽通訊端單獨建立一個線程,而是使用AcceptEx調用,以節省伺服器系統資源並提高效率.
新增: 串連關閉時,會在日誌中顯示該串連所用的平均頻寬.
修正: 當一個要求標頭被分為多次接收時,會導致串連被關閉.
新增: 支援檔案日誌,並可以在設定介面中禁用/啟用.記錄檔和EXE檔案在同一個檔案夾內,檔案名稱為 HTTPServerLOG(年月日).TXT,每個記錄檔最大5MB.
新增: 在禁止瀏覽目錄的情況下,允許設定預設檔案名稱(多個檔案名稱以逗號分隔,最長260個字元).
修改: 現在核心使用3個定時器隊列分別對應3中應用,以提高效能.
注: V1.5X版本將不再增加新功能,而繼續測試以發現和修複BUG為主.
V1.52 Build 152716
修正: 停止伺服器時,如果還有串連,不再提示定時器刪除錯誤.
V1.52 Build 152717
修改: 串連結束時,日誌中的平均速度顯示為 "XXX MB/s"等
v1.52 Build 152718
修改: 調整了一些日誌文本的格式,使之更有條理.
開放原始碼.