開篇:每當我們將開發好的ASP.NET網站部署到IIS伺服器中,在瀏覽器正常瀏覽頁面時,可曾想過Web伺服器是怎麼工作的,其原理是什麼?“紙上得來終覺淺,絕知此事要躬行”,於是我們自己模擬一個簡單的Web伺服器來體會一下。

一、請求-處理-響應模型

1.1 基本過程介紹

每一個HTTP請求都會經歷三個步湊: 請求-處理-響應 :每當我們在瀏覽器中輸入一個URL時都會被封裝為一個HTTP請求報文傳送到Web伺服器,而Web伺服器則接收並解析HTTP請求報文,然後針對請求進行處理(返回指定的HTML頁面、CSS樣式表、JS指令碼檔案亦或是載入動態頁面生成HTML並返回)。最後將要返回的內容轉為輸出流並封裝為HTTP響應報文傳送回瀏覽器。

當然,瀏覽器接收到響應報文後會載入HTML、CSS與JS並顯示在頁面中,最後成為我們看到的最終效果。

1.2 通訊過程介紹

Web伺服器本質上來說就是一個 Socket服務端 ,在不停地接受著客戶端的請求,然後針對每一個客戶端的請求進行處理,處理完畢就 即時關閉 連線。而我們的瀏覽器則是一個 Socket客戶端 ,通過 TCP協議 向服務端傳送 HTTP請求報文 。

About:Socket非常類似於電話插座,以一個電話網為例:電話的通話雙方相當於相互通訊的2個程式,電話號碼就是IP地址。任何使用者在通話之前,首先要佔有一部電話機,相當於申請一個Socket;同時要知道對方的號碼,相當於對方有一個固定的Socket。然後向對方撥號呼叫,相當於發出連線請求。對方假如在場並空閒,拿起電話話筒,雙方就可以正式通話,相當於連線成功。雙方通話的過程,是一方向電話機發出訊號和對方從電話機接收訊號的過程,相當於向Socket傳送資料和從Socket接收資料。通話結束後,一方掛起電話機相當於關閉socket,撤消連線。

1.3 HTTP協議基礎

Internet的基本協議是 TCP/IP協議 (傳輸控制協議和網際協議),目前廣泛使用的 FTP、HTTP(超文字傳輸協議)、Archie Gopher都是建立在TCP/IP上面的應用層協議,不同的協議對應不同的應用。而 HTTP協議是Web應用所使用的主要協議 。

HTTP協議是 基於請求響應模式 的。客戶端向伺服器傳送一個請求,請求頭包含請求的方法、 URI、協議版本、以及包含請求修飾符、客戶端資訊和內容的類似MIME的訊息結果。伺服器則以一個狀態行作為響應,相應的內容包括訊息協議的版本、成功或者錯誤編碼加上包含伺服器資訊、實體元資訊以及可能的實體內容。

HTTP是 無狀態協議 ,依賴於瞬間或者近乎瞬間的請求處理。請求資訊被立即傳送,理想的情況是 沒有延時的進行處理,不過,延時還是客觀存在的。HTTP有一種內建的機制,在訊息的傳遞時間上由一定的靈活性:超時機制。一個超時就是客戶機等待請求 訊息的返回資訊的最長時間。

(1)HTTP請求報文示例

(2)HTTP響應報文示例

TIP:關於HTTP協議的詳細介紹,可以瀏覽一下小坦克大神的這篇:HTTP協議詳解

二、關鍵設計思路

2.1 要實現的功能

(1)處理使用者的靜態檔案請求:主要是指html/css/js檔案的請求;

(2)處理使用者的動態檔案請求:這裡只處理ASP.NET請求,即ashx與aspx檔案的請求;

2.2 要封裝的類

(1)HttpRequest、HttpResponse與HttpContext類

根據我們對ASP.NET請求處理機制的分析,我們知道在HttpRuntime的ProcessRequest方法中構造了一個 HttpContext 物件。在 HttpContext 的建構函式中,根據 HttpWorkerRequest 物件建立了 HttpContext 物件,這是一個重要的Http上下文物件,兩個重要型別的欄位也隨之被初始化: HttpRequest 物件和 HttpResponse 物件。因此,我們在設計時也可以設計一個 HttpContext 類將 HttpRequest 和 HttpResponse 兩個例項進行封裝。

TIP:有關ASP.NET請求處理機制的分析,可以瀏覽我的另外一篇文章: ASP.NET請求處理機制探索之二-核心

(2)IHttpHandler介面與實現IHttpHandler介面的HttpApplication類

針對每個Http請求都有一個抽象的HttpApplication物件來進行處理,而為了考慮擴充套件性(可以是ashx,也可以是aspx),封裝了一個IHttpHandler介面,讓不同的處理物件實現這個介面即可。IHttpHandler介面很簡單,就聲明瞭一個ProcessRequest方法,每個實現的類只需要實現這個方法即可。

2.3 總體設計流程

三、關鍵程式碼實現

3.1 開啟Socket服務監聽瀏覽器端的HTTP請求

privatevoid btnStart_Click(object sender, EventArgs e)
		{
			// 建立Socket->繫結IP與埠->設定監聽佇列的長度->開啟監聽連線
			socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
			socketWatch.Bind(new IPEndPoint(IPAddress.Parse(txtIPAddress.Text), int.Parse(txtPort.Text)));
			socketWatch.Listen(10);
			// 建立Thread->後臺執行
			threadWatch = new Thread(ListenClientConnect);
			threadWatch.IsBackground = true;
			threadWatch.Start(socketWatch);
			isEndService = false;
			txtIPAddress.ReadOnly = true;
			txtPort.ReadOnly = true;
			btnStart.Enabled = false;
			ShowMessage("~_~訊息:【您已成功啟動Web服務!】");
		}
		privatevoid ListenClientConnect(object obj)
		{
			Socket socketListen = obj as Socket;
			while (!isEndService)
			{
				Socket proxSocket = socketListen.Accept();
				byte[] data = new byte[1024 * 1024 * 2];
				int length = proxSocket.Receive(data, 0, data.Length, SocketFlags.None);
				// Step1:接收HTTP請求
				string requestText = Encoding.Default.GetString(data, 0, length);
				HttpContext context = new HttpContext(requestText);
				// Step2:處理HTTP請求
				HttpApplication application = new HttpApplication();
				application.ProcessRequest(context);
				ShowMessage(string.Format("{0} {1} from {2}", context.Request.HttpMethod, context.Request.Url, proxSocket.RemoteEndPoint.ToString()));
				// Step3:響應HTTP請求
				proxSocket.Send(context.Response.GetResponseHeader());
				proxSocket.Send(context.Response.Body);
				// Step4:即時關閉Socket連線
				proxSocket.Shutdown(SocketShutdown.Both);
				proxSocket.Close();
			}
		}

在監聽執行緒中,通過HttpApplication類物件呼叫其ProcessRequest方法進行具體的處理。最重要的,處理完畢後立即通過Socket傳送響應資訊,並及時關閉Socket連線。

3.2 設計HttpConext類封裝HttpRequest與HttpResponse

(1)HttpContext

public class HttpContext
	{
		public HttpRequest Request { get; set; }
		public HttpResponse Response { get; set; }
		public HttpContext(string requestText)
		{
			Request = new HttpRequest(requestText);
			Response = new HttpResponse();
		}
	}

(2)HttpRequest

public class HttpRequest
	{
		public HttpRequest(string requestText)
		{
			string[] lines = requestText.Replace("\r\n", "\r").Split('\r');
			string[] requestLines = lines[0].Split('');
			// 獲取HTTP請求方式、請求的URL地址、HTTP協議版本
			HttpMethod = requestLines[0];
			Url = requestLines[1];
			HttpVersion = requestLines[2];
		}
		// 請求方式:GET or POST?
		public string HttpMethod { get; set; }
		// 請求URL
		public string Url { get; set; }
		// Http協議版本
		public string HttpVersion { get; set; }
		// 請求頭
		public Dictionary<string, string> HeaderDictionary { get; set; }
		// 請求體
		public Dictionary<string, string> BodyDictionary { get; set; }
	}

(3)HttpResponse

public class HttpResponse
	{
		// 響應狀態碼
		public string StateCode { get; set; }
		// 響應狀態描述
		public string StateDescription { get; set; }
		// 響應內容型別
		public string ContentType { get; set; }
		//響應報文的正文內容
		public byte[] Body { get; set; }
		// 生成響應頭資訊
		publicbyte[] GetResponseHeader()
		{
			string strRequestHeader = string.Format(@"HTTP/1.1 {0} {1}
Content-Type: {2}
Accept-Ranges: bytes
Server: Microsoft-IIS/7.5
X-Powered-By: ASP.NET
Date: {3} 
Content-Length: {4}
", StateCode, StateDescription, ContentType, string.Format("{0:R}", DateTime.Now), Body.Length);
			return Encoding.UTF8.GetBytes(strRequestHeader);
		}
	}

這裡需要注意的是在HttpResponse類中,為了生成響應頭資訊,需要格式化一個固定格式的資訊,並且在最後保留兩個 CRLF(即換行符) 作為頭部結束標誌,可以看看下面的格式說明:

3.3 設計IHttpHandler介面

public interface IHttpHandler
    {
        void ProcessRequest(HttpContext context);
    }

仿照ASP.NET內部實現,我們也設計一個IHttpHandler介面,只定義了一個方法:ProcessRequest;

3.4 設計實現IHttpHandler介面的HttpApplication類

public class HttpApplication : IHttpHandler
	{
		// 對請求上下文進行處理
		public void ProcessRequest(HttpContext context)
		{
			// 1.獲取網站根路徑
			string bastPath = AppDomain.CurrentDomain.BaseDirectory;
			string fileName = Path.Combine(bastPath+"\\MyWebSite", context.Request.Url.TrimStart('/'));
			string fileExtension = Path.GetExtension(context.Request.Url);
			// 2.處理動態檔案請求
			if(fileExtension.Equals(".aspx") || fileExtension.Equals(".ashx"))
			{
				string className = Path.GetFileNameWithoutExtension(context.Request.Url);
				IHttpHandler handler = Assembly.Load("MyWebServer").CreateInstance("MyWebServer.Page." + className) as IHttpHandler;
				handler.ProcessRequest(context);
				return;
			}
			// 3.處理靜態檔案請求
			if (!File.Exists(fileName))
			{
				context.Response.StateCode = "404";
				context.Response.StateDescription = "Not Found";
				context.Response.ContentType = "text/html";
				string notExistHtml = Path.Combine(bastPath, @"MyWebSite\notfound.html");
				context.Response.Body = File.ReadAllBytes(notExistHtml);
			}
			else
			{
				context.Response.StateCode = "200";
				context.Response.StateDescription = "OK";
				context.Response.ContentType = GetContenType(Path.GetExtension(context.Request.Url));
				context.Response.Body = File.ReadAllBytes(fileName);
			} 
		}
		// 根據副檔名獲取內容型別
		public string GetContenType(string fileExtension)
		{
			string type = "text/html; charset=UTF-8";
			switch (fileExtension)
			{
				case ".aspx":
				case ".html":
				case ".htm":
					type = "text/html; charset=UTF-8";
					break;
				case ".png":
					type = "image/png";
					break;
				case ".gif":
					type = "image/gif";
					break;
				case ".jpg":
				case ".jpeg":
					type = "image/jpeg";
					break;
				case ".css":
					type = "text/css";
					break;
				case ".js":
					type = "application/x-javascript";
					break;
				default:
					type = "text/plain; charset=gbk";
					break;
			}
			return type;
		}
	}

這裡,我們封裝一個抽象的HttpApplication類,它實現了IHttpHandler介面,對一般的請求做一個通用的處理操作。如果是靜態檔案請求,則直接讀取檔案並生成響應流,如果是動態檔案請求,則通過反射方式生成對應的Page物件例項,將HttpContext物件傳入其ProcessRequest方法中進行處理,最後的響應內容都封裝到了HttpConext中的HttpResponse物件的Body屬性中。

3.5 設計實現IHttpHandler介面的模擬Page類

public class DemoPage : IHttpHandler
	{
		public void ProcessRequest(HttpContext context)
		{
			StringBuilder sbText = new StringBuilder();
			sbText.Append("<html>");
			sbText.Append("<head><meta http-equiv='Content-Type' content='text/html; charset=utf-8'/><title>DemoPage</title></head>");
			sbText.Append("<body style='margin:10px auto;text-align:center;'>");
			sbText.Append("<h1>使用者資訊列表</h1>");
			sbText.Append("<table align='center' cellpadding='1' cellspacing='1'><thead><tr><td>ID</td><td>使用者名稱</td></tr></thead>");
			sbText.Append("<tbody>");
			sbText.Append(GetDataList());
			sbText.Append("</tbody></table>");
			sbText.Append(string.Format("<h3>更新時間:{0}</h3>", DateTime.Now.ToString()));
			sbText.相關文章