本身動手模擬開發一個簡單的Web服務器

開篇:每當咱們將開發好的ASP.NET網站部署到IIS服務器中,在瀏覽器正常瀏覽頁面時,可曾想過Web服務器是怎麼工做的,其原理是什麼?「紙上得來終覺淺,絕知此事要躬行」,因而咱們本身模擬一個簡單的Web服務器來體會一下。javascript

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

1.1 基本過程介紹

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

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

1.2 通訊過程介紹

socket model

  Web服務器本質上來講就是一個Socket服務端,在不停地接受着客戶端的請求,而後針對每個客戶端的請求進行處理,處理完畢就即時關閉鏈接。而咱們的瀏覽器則是一個Socket客戶端,經過TCP協議向服務端發送HTTP請求報文java

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請求報文示例app

  (2)HTTP響應報文示例socket

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

2、關鍵設計思路

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類將HttpRequestHttpResponse兩個實例進行封裝。

TIP:有關ASP.NET請求處理機制的分析,能夠瀏覽個人另一篇文章:ASP.NET請求處理機制探索之二-核心

  (2)IHttpHandler接口與實現IHttpHandler接口的HttpApplication類

  針對每一個Http請求都有一個抽象的HttpApplication對象來進行處理,而爲了考慮擴展性(能夠是ashx,也能夠是aspx),封裝了一個IHttpHandler接口,讓不一樣的處理對象實現這個接口便可。IHttpHandler接口很簡單,就聲明瞭一個ProcessRequest方法,每一個實現的類只須要實現這個方法便可。

2.3 整體設計流程

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、我的開發小結

4.1 開發效果展現

  (1)開啓監聽服務

  (2)請求靜態頁面

  (3)請求動態頁面

4.2 開發實戰總結

  本次模擬的一個超級簡單的Web服務器軟件,實現了靜態文件和動態文件(經過模擬aspx頁面對象)的處理和響應。可是,還有不少的功能並未實現,由於一個真正的Web服務器須要考慮的東西不少不少,例如:多線程的處理優化、高效的IO模型等。不過,對於一個最基本的Web服務器所須要瞭解的最基本的原理:Socket的監聽和鏈接、基於TCP協議的HTTP協議、動態文件類的反射與調用等,模擬開發本次的DEMO的過程是能夠達到的。

附件下載

  MyWebServer v1.0http://pan.baidu.com/s/1mgtC1HA

 

相關文章
相關標籤/搜索