摘要:絕大多數的Web開發,都是構建在HTTP協議之上的Web應用。為了分析基于HTTP協議服務器程序以及瀏覽器與服務器的交互過程,以一個簡單的基于HTTP協議中服務器程序為實例,探討怎樣使用多線程與異步操作的理論,運用WinSock編程來逐步解析HTTP協議的服務器程序的核心部分,這樣既可達到避免調用線程阻塞的目的,又可提高服務器程序的可響應性。
關鍵詞:HTTP; HTTP協議; 服務器; WinSock
中圖分類號:TN91934文獻標識碼:A文章編號:1004373X(2012)04011703
Analysis of server program based on HTTP protocol
ZHU Rui
(Department of Electronic Science, Xi’an Air Forces Engineering University, Xi’an 710043, China)
Abstract: The overwhelming majority of Web developments are built based on Web application of HTTP protocol. To analyze the server program based on the HTTP protocol and the process of interaction between the browser and server, a simple HTTPbased server program is taken as an example to describe how to use the theories of multithreading and asynchronous operation, and the core section of HTTPbased server program is gradually resolved with Winsock programming. This method can achieve the purpose to avoid blocking the calling thread, and improve the responsiveness of a server program.
Keywords: HTTP; HTTP protocol; server; WinSock
收稿日期:20110913
基金項目:陜西省電子信息系統和綜合集成重點試驗室基金項目資助項目(200904B)0引言
Web的應用層協議HTTP是Web的核心。HTTP在Web的客戶程序和服務器程序中得以實現。運行在不同端系統上的客戶程序和服務器程序通過交換HTTP消息彼此交流。HTTP定義這些消息的結構以及客戶和服務器如何交換這些消息。Web頁面(Web Page)也稱為文檔,由多個對象構成。對象(Object)僅僅是可由單個URL尋址的文件,例如HTML文件、JPG圖像、GIF圖像、JAVA小應用程序、語音片段等。大多數Web頁面由單個基本HIML文件和若干個所引用的對象構成。HTTP定義Web客戶(即瀏覽器)如何從Web服務器請求Web頁面,以及服務器如何把Web頁面傳送給客戶。
1HTTP協議
超文本傳輸協議(HTTP)是一個基于請求與響應模式的、無狀態的、應用層的協議,常基于TCP的連接方式。絕大多數的Web開發,都是構建在HTTP協議之上的Web應用。本程序實現的是一個輕量級的Web服務器\\[1\\]。
(1) HTTP請求由3部分組成,分別是:請求行、消息報頭、請求正文。
請求行以一個方法符號開頭,以空格分開,后面跟著請求的URI和協議的版本,格式如下:Method RequestURI HTTPVersion CRLF 。其中 Method表示請求方法;RequestURI是一個統一資源標識符;HTTPVersion表示請求的HTTP協議版本;CRLF表示回車和換行(除了作為結尾的CRLF外,不允許出現單獨的CR或LF字符)\\[2\\]。
(2) HTTP響應由3個部分組成,分別是:狀態行、消息報頭、響應正文。
狀態行格式是:HTTPVersion StatusCode ReasonPhrase CRLF,其中HTTPVersion表示服務器HTTP協議的版本;StatusCode表示服務器發回的響應狀態代碼;ReasonPhrase表示狀態代碼的文本描述。狀態代碼由3位數字組成,第1個數字定義了響應的類別,且有五種可能取值\\[3\\]:
1xx:指示信息,表示請求已接收,繼續處理;
2xx:成功,表示請求已被成功接收、理解、接受;
3xx:重定向,要完成請求必須進行更進一步的操作;
4xx:客戶端錯誤,請求有語法錯誤或請求無法實現;
5xx:服務器端錯誤,服務器未能實現合法的請求。
常見狀態代碼、狀態描述、說明\\[4\\]:
200 OK//客戶端請求成功
400 Bad Request//客戶端請求有語法錯誤,不能被服務器所理解
HTTP消息由客戶端到服務器的請求和服務器到客戶端的響應組成。請求消息和響應消息都是由開始行(對于請求消息,開始行就是請求行,對于響應消息,開始行就是狀態行)、消息報頭(可選)、空行(只有CRLF的行)、消息正文(可選)組成。
(3) 請求正文。
2HTTPSVR程序分析
HTTPSVR是一個簡單的Web服務器,具有圖形界面,可以指定端口和Web主目錄,且可以生成Web訪問的日志,它由一個無限循環構成:接收來自客戶端的請求,根據HTTP協議解析并處理請求,然后發送應答至客戶端。
2.1HTTPSVR主要工作流程圖
HTTPSVR功能強大,具有豐富的圖形界面,主要的工作流程如圖1所示\\[5\\]。
圖1HTTPSVR工作流程圖首先通過CListenSocket::OnAccept( int nErrorCode )函數生成CRequestSocket類,監聽到連接請求時,Accept函數創建新的套接字pRequest并返回句柄,AsyncSelect函數監聽80端口的FD_READ和FD_CLOSE兩個事件,當傳入FD_READ事件時,準備接收,并且觸發OnReceive()函數,如果傳入FD_WRITE事件,發送數據的時候,OnSend()函數就會觸發。
當有數據到達時,執行ReqSock.cpp中的void CRequestSocket::OnReceive(int nErrorCode)函數。代碼int nBytes = Receive( m_buf.GetData(),m_buf.GetSize() )將tcp層上傳的數據包存放在請求和應答報文的緩沖區m_buf中。
接下來使用swich語句對3種請求狀態(REQ_REQUEST,REQ_HEADER,REQ_BODY)進行處理,當請求狀態m_reqStatus == REQ_DONE,調用StartResponse()開始構造應答報文。
當StartResponse()構造應答報文結束后,利用AsyncSelect( FD_WRITE | FD_CLOSE );調用void CRequestSocket::OnSend(int nErrorCode)函數發送緩沖區m_buf中的數據\\[6\\]。
2.2異步非阻塞類CAsyncSocket
CAsyncSocket的Create()函數,除了創建了一個Socket以外,使用WSAAsyncSelect()將這個Socket與該窗口對象關聯,以讓該窗口對象處理來自Socket的事件(消息),然而CSocketWnd收到Socket事件之后,只是簡單地回調CAsyncSocket::OnReceive() CAsyncSocket::OnSend(),CAsyncSocket::OnAccept(),CAsyncSocket::OnConnect()等虛函數。所以CAsyncSocket的派生類,只需要在這些虛函數里添加發送和接收的代碼。使用CAsyncSocket時,如果使用Create缺省創建socket,則所有網絡I/O都是異步操作,進行有關網絡數據傳輸時需要用到以下函數:OnAccept,OnClose,OnConnect,OnOutOfBandData,OnReceive,OnSend\\[7\\]。
2.3HTTPSVR關鍵類分析
void CListenSocket::OnAccept(int nErrorCode) 創建新的CRequestSocket,并監聽80端口,請求到達時返回套接字,并通過AsyncSelect(FD_READ|FD_CLOSE)調用void CRequestSocket::OnReceive(int nErrorCode)函數接收數據。
void CRequestSocket::OnReceive(int nErrorCode) \\[8\\]實現httpsvr的所有主要功能,包括接受瀏覽器的請求,根據Http協議解析請求報文,構造應答報文,將處理后的數據發送回瀏覽器。Receive(m_buf.GetData(),m_buf.GetSize())函數接收來自瀏覽器的請求數據包,switch語句根據不同的狀態處理。瀏覽器的請求狀態m_reqStatus為REQ_REQUEST時,使用while循環進行處理。ProcessLine()函數對接收到報文的每一行進行解析,然后初始化m_pRequest類。Swich語句結束后調用StartResponse()根據m_pRequest中的解析結果構造應答報文,最后函數AsyncSelect( FD_WRITE | FD_CLOSE )調用OnSend函數進行發送。
void CRequestSocket::OnSend(int nErrorCode)調用int nBytes = Send(m_buf.GetData(),m_cbOut)函數發送緩存中的數據,并對可能出現的錯誤進行處理,或者關閉或者重新進行發送。
void CRequestSocket::ProcessLine(void)根據請求狀態m_reqStatus的不同作出處理,完成對CRequest*m_pRequest的初始化。
BOOL CRequestSocket::StartResponse(void)\\[9\\],該函數用于構造應答報文(將構造好的應答報文緩存在m_buf中,用void CRequestSocket::OnSend(int nErrorCode)發送m_buf),并根據不同的Method(GET,HEAD,POST)構造應答報文。例如當Method為GET時,表示瀏覽器需要瀏覽網頁,就用FindTarget(strFile);在默認目錄底下進行查找,如果存在則打開報文,并使用 StuffHeading()構造http協議要求的頭部,然后使用StartTargetStuff();將剛才打開的網頁內容填充到m_buf中,等待發送,構造完畢后再返回BOOL CRequestSocket::StartResponse(void),然后調用AsyncSelect(FD_WRITE |FD_CLOSE);該函數調度void CRequestSocket::OnSend(int nErrorCode)對緩存m_buf中的數據進行發送。
BOOL CRequestSocket::FindTarget( CString strFile ),由于瀏覽器發送過來的請求都是網絡地址格式的,所以該函數將網絡地址格式轉為windows文件系統的路徑格式。例如發送:GET /default.html,經過本函數轉換之后變為c:\WebPages\default.html(httpsvr默認根目錄為c:\WebPages)。
void CRequestSocket::StartTargetStuff( void ),該函數讀取客戶端請求瀏覽的網頁文件內容到m_buf中,等待進一步發送。
UINT CGIThread( LPVOID pvParam )\\[10\\],該函數以線程的方式運行,調用相應的GCI函數處理,并輸出結果到一個臨時文件。該線程處理完畢后,再由BOOL CRequestSocket::StartResponse( void )將臨時文件發給瀏覽器。
3HTTPSRV程序執行過程
3.1運行函數
程序由MFC中的文件APPMODULE.CPP的_tWinMain函數開始執行,執行該文件的return AfxWinMain函數。
3.2鏈接服務器
由WINMAIN.CPP文件中的int AFXAPI AfxWinMain函數來創建工作線程pThread對httpsvr進行初始化工作,調用HttpSvr.cpp文件中的BOOL CHttpSvrApp::InitInstance方法對HTTP服務器進行初始化,然后運行線程的主函數,最后在THRDCORE.CPP文件中運行int CWinThread::Run函數,開始服務器的循環。
在循環中,首先調用Listen.cpp中的void CListenSocket::OnAccept方法,生成CRequestSocket,將其設置用于監聽8080端口。設置端口為8080,將Web服務文件夾地址Root Dir指向root所在地址。此時顯示結果如圖2所示。
圖2鏈接服務器3.3請求客戶
在ReqSock.cpp文件中,根據響應狀態m_reqStatus的不同,對接收到的數據包進行不同的響應處理。當browser發送第一個數據包時,響應狀態m_reqStatus被設置為REQ_REQUEST,之后,對REQ_REQUEST數據包的每一行進行處理,根據http的協議使用ProcessLine方法對m_pRequest進行初始化,完成以上操作之后,調用判斷StartResponse方法來構造應答報文,運行AsyncSelect函數,之后其調用void CRequestSocket::OnSend方法將緩存m_buf的應答報文發送給客戶端,如圖3所示。