開篇:每當我們將開發好的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請求
private void 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服務!】"); } private void 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; } // 生成響應頭信息 public byte[] 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.Append("</body>"); sbText.Append("</html>"); context.Response.Body = Encoding.UTF8.GetBytes(sbText.ToString()); context.Response.StateCode = "200"; context.Response.ContentType = "text/html"; context.Response.StateDescription = "OK"; } private string GetDataList() { StringBuilder sbData = new StringBuilder(); string strConn = System.Configuration.ConfigurationManager.ConnectionStrings["MyConn"].ToString(); using (SqlConnection conn = new SqlConnection(strConn)) { conn.Open(); using (SqlCommand cmd = conn.CreateCommand()) { cmd.CommandText = "SELECT * FROM UserInfo"; using(SqlDataAdapter adapter = new SqlDataAdapter(cmd)) { DataTable dt = new DataTable(); adapter.Fill(dt); if(dt != null) { foreach (DataRow row in dt.Rows) { sbData.Append("<tr>"); sbData.Append(string.Format("<td>{0}</td>",row["ID"].ToString())); sbData.Append(string.Format("<td>{0}</td>", row["UserName"].ToString())); sbData.Append("</tr>"); } } } } } return sbData.ToString(); } }
這里我們模擬一個Page頁面類,它也實現了IHttpHandler接口,在ProcessRequest方法通過ADO.NET訪問了數據庫並讀取數據作為輸出內容。這里,我們主要是通過分析ASP.NET WebForm中的aspx對象,它雖然直接繼承的類是Page類,但是Page類卻是實現了IHttpHandler接口的。在具體的處理方法中,都是通過調用這個接口的ProcessRequest方法進行處理的。
四、個人開發小結
4.1 開發效果展示
(1)開啟監聽服務
(2)請求靜態頁面
(3)請求動態頁面
4.2 開發實戰總結
本次模擬的一個超級簡單的Web服務器軟件,實現了靜態文件和動態文件(通過模擬aspx頁面對象)的處理和響應。但是,還有很多的功能並未實現,因為一個真正的Web服務器需要考慮的東西很多很多,例如:多線程的處理優化、高效的IO模型等。不過,對於一個最基本的Web服務器所需要了解的最基本的原理:Socket的監聽和連接、基於TCP協議的HTTP協議、動態文件類的反射與調用等,模擬開發本次的DEMO的過程是可以達到的。
附件下載
MyWebServer v1.0:http://pan.baidu.com/s/1mgtC1HA