上一篇博客中介紹了怎樣使用socket訪問web服務器。關鍵有兩個:
- 熟悉Socket編程;
- 熟悉HTTP協議。
上一篇主要是通過socket來模擬瀏覽器向(任何)Web服務器發送(HTTP)請求,重點在瀏覽器端。本篇博客則反過來講一下怎樣使用socket來實現Web服務器,怎樣去接收、分析、處理最后回復來自瀏覽器的HTTP請求。
HTTP協議是瀏覽器和Web服務器都需要遵守的一種通信規范,如果我們編寫一個程序,正確遵守了HTTP協議,那么理論上講,這個程序可以具備瀏覽器、甚至Web服務器的功能。
圖1
如上圖1所示,Web服務器和瀏覽器之間無論是發送數據還是接收(解析)數據均遵守了HTTP協議。可以很確定地講,只要我們充分熟悉HTTP協議結構,那么無論瀏覽器的實現還是Web服務器的實現,均只是“簡單的”Socket程序的開發過程,除此之外,無其它神秘高深的東西。而Socket程序開發,稍微知道一點socket的有關知識,均能寫得出一個大概demo。
從系統架構來講,Web架構形式的系統均符合“生產者-消費者”模式(實質上,現實生活中大部分系統均屬於該模式)。瀏覽器端不斷產生數據(請求),而Web服務器端不斷處理請求,長時間持續如此。
圖2
如上圖2所示,圖中左邊部分為Web服務器中的“泵”結構,所謂泵,就是指它能夠持續長時間循環運作。圖中右邊顯示“來自瀏覽器請求”部分即為“生產者”,生產者不斷發出請求,由左邊(Web服務器)不斷進行處理,最后回復給瀏覽器。注意圖2中顯示,Web服務器中處理數據在循環體內部,換句話說,前一次HTTP請求處理結束之前,后一次HTTP請求不能開始,也就是每次請求處理均會阻塞循環的執行。這種串行處理數據的方式明顯效率不高,為了解決該問題,我們可以在接收到瀏覽器端的HTTP請求后,並不馬上在當前線程中進行處理,而是開辟獨立線程來處理請求(在.NET中可以使用異步編程實現)。這樣一來,請求處理並不會阻塞當前循環過程,見下圖3
圖3
如上圖3所示,接收到請求后,開辟其它線程來處理,這種並行處理數據的方式不會影響后續請求處理。
如果對Socket編程比較熟悉,以上所說的完全可以輕松實現(完全按照Socket編程去做)。現在難點是,Web服務器端怎樣解析來自瀏覽器的請求數據(一串字符串文本),以及應該以怎樣的格式去回復瀏覽器?答案就是必須充分了解HTTP協議格式。上一篇博客中已經提到過,有關HTTP協議格式請參見http://www.cnblogs.com/riky/archive/2007/04/09/705848.html。我們必須讀懂瀏覽器發送的請求數據,並按照正確格式發送回復。下圖4顯示瀏覽器請求數據格式:
圖4
圖中紅色部分即為數據傳輸方式(post或get)、請求路徑(url中不含主機地址部分)以及HTTP協議版本號。下面以“鍵:值”格式的文本均為瀏覽器發送給服務器的一系列數據信息(注意這些項可選),如果瀏覽器以post方式提交數據,那么數據會緊跟在下面(圖中沒顯示)。Web服務器讀懂瀏覽器發送的請求數據,並處理完畢后,必須按照圖5的格式將結果回復給瀏覽器:
圖5
如上圖5所示,最上面的以“鍵:值”的格式文本是Web服務器發送給瀏覽器的一些數據信息(這些項部分可選),緊接着,下面便是需要發送給瀏覽器的HTML文檔(如果返回的是頁面)。瀏覽器必須讀懂Web服務器發送的回復數據,然后進行渲染(顯示)。
圖6
圖6顯示了瀏覽器發起的一次HTTP請求,顯示展示了Web服務器端處理該請求的過程。我們可以看到,Web服務器在一次Socket連接過程中只處理一個HTTP請求。多次HTTP請求會伴隨着Socket不斷的連接與斷開。
文章最后上傳一個使用Socket編寫的簡單Web服務器,能夠實現以下功能:
- 運行Web服務器后,可以綁定端口,接收來自任何瀏覽器的HTTP請求;
- 能夠顯示一個默認首頁,如index.html;
- 首頁提供“登錄”功能,按照Post方式傳遞數據到處理頁面“login.zsp”(后綴名可自定義);
- Web服務器端接收接收瀏覽器發送的數據,能夠解析(解析方式很隨意)出post傳遞的參數,並模擬訪問數據庫檢查登錄情況、模擬耗時等待等;
- Web服務器生成登錄成功后的靜態頁,回復給瀏覽器。頁面顯示登錄名和當前時間。
整個demo完全就是一個Socket程序,只是增加了“HTTP協議”的環節,服務器端無論是接收(解析)數據還是發送數據,均需要遵守HTTP協議。Web服務器中最終的請求處理泵代碼如下:

1 static Socket _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //偵聽socket 2 static void Main(string[] args) 3 { 4 _socket.Bind(new IPEndPoint(IPAddress.Any, 8081)); 5 _socket.Listen(100); 6 _socket.BeginAccept(new AsyncCallback(OnAccept), _socket); //開始接收來自瀏覽器的http請求(其實是socket連接請求) 7 Console.Read(); 8 } 9 static void OnAccept(IAsyncResult ar) 10 { 11 try 12 { 13 Socket socket = ar.AsyncState as Socket; 14 Socket new_client = socket.EndAccept(ar); //接收到來自瀏覽器的代理socket 15 //NO.1 並行處理http請求 16 socket.BeginAccept(new AsyncCallback(OnAccept), socket); //開始下一次http請求接收 (此行代碼放在NO.2處時,就是串行處理http請求,前一次處理過程會阻塞下一次請求處理) 17 18 byte[] recv_buffer = new byte[1024 * 640]; 19 int real_recv = new_client.Receive(recv_buffer); //接收瀏覽器的請求數據 20 string recv_request = Encoding.UTF8.GetString(recv_buffer, 0, real_recv); 21 Console.WriteLine(recv_request); //將請求顯示到界面 22 23 Resolve(recv_request,new_client); //解析、路由、處理 24 25 //NO.2 串行處理http請求 26 } 27 catch 28 { 29 30 } 31 }
注意以上代碼中的NO.1和NO.2處,socket.BeginAccept()方法放在NO.1處時,服務器端會並行處理請求,而放在NO.2處時,服務器會串行處理請求。讀者可以每種方式都試一下,在串行處理請求時,請求處理過程會阻塞后續請求的處理(比如登錄耗時10秒鍾,其它人無法訪問網站)。
以下是demo效果圖:
圖7:Web服務器運行后,瀏覽器訪問首頁:
圖7
圖8:瀏覽器中首頁顯示(包含登錄框):
圖8
圖9:用戶點擊“登錄”按鈕,以Post方式提交數據,Web服務器解析、處理,返回新頁面:
圖9
文章有點長,部分截圖還失真了(部分圖以前整理的,沒有找到大圖,所以就湊合看:))
源碼下載:http://files.cnblogs.com/xiaozhi_5638/socket_webServer.rar