C#中使用Socket實現簡單Web伺服器

周見智發表於2014-08-17

上一篇部落格中介紹了怎樣使用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         }
View Code

注意以上程式碼中的NO.1和NO.2處,socket.BeginAccept()方法放在NO.1處時,伺服器端會並行處理請求,而放在NO.2處時,伺服器會序列處理請求。讀者可以每種方式都試一下,在序列處理請求時,請求處理過程會阻塞後續請求的處理(比如登入耗時10秒鐘,其它人無法訪問網站)。

以下是demo效果圖:

圖7:Web伺服器執行後,瀏覽器訪問首頁:

圖7

圖8:瀏覽器中首頁顯示(包含登入框):

圖8

圖9:使用者點選“登入”按鈕,以Post方式提交資料,Web伺服器解析、處理,返回新頁面:

圖9

文章有點長,部分截圖還失真了(部分圖以前整理的,沒有找到大圖,所以就湊合看:))

原始碼下載:https://files.cnblogs.com/xiaozhi_5638/socket_webServer.rar

相關文章