《ASP.NET本質》創建簡單的Web服務器

      在遙遠的Unix時代,爲了解決傳輸層的編程問題,從 4.2BSD Unix開始,Unix提供了類似於文件操作的網絡操作方式——Socket。通過Socket,程序員就可以像操作文件一樣通過打開、寫入、讀取、關閉等操作完成網絡編程。這使得網絡編程可以統一到文件操作之下。通過Socket幫助程序員解決網絡傳輸層的問題,而系統中的網絡系統負責處理網絡內部的複雜操作,這樣程序員就可以比較容易地編寫網絡應用程序。需要注意的是,應用層的協議需要針對網絡程序專門處理,Socket不負責應用層的協議,僅僅負責傳輸層的協議。
      當然,網絡畢竟不是簡單的文件,所以,在使用Socket的時候,程序員還是需要設置一些網絡相關的細節問題參數。
     當通過Socket開發網絡應用程序的時候,首先需要考慮所使用的網絡類型,注意包括以下三個方面:
   1)Socket類型,使用網絡協議的類別,IPv4的類型爲PF_INET。
   2)數據通信的類型,常見的數據包(SOCK_DGRAM)、數據流(SOCK_STREAM)。
   3)使用的網絡協議,比如:TCP協議
    在同一個網絡地址上,爲了區分使用相同協議的不同應用程序,可以爲不同的應用程序分配一個數字編號,這個編號成爲網絡端口號(port)。端口號是一個兩字節的證書,取值範圍從0~65535.IANA(Internet Assgned Number Authority,互聯網地址分配機構)維護了一個有端口分配列表,這些端口分爲三類,第一類範圍是0~1023,稱爲總所周知的端口,有IANA進行控制和分配,有特定的網絡程序使用,例如TCP協議使用80號端口來完成HTTP協議的傳輸。第二類的範圍是1024~49151,稱爲登記端口,這些端口不由IANA控制,但是IANA維護了一個登記的列表,如果沒有在IANA登記的話,也不應該在程序中使用。但是,大多數的系統中,在沒有衝突的情況下,也可以由用戶程序使用。第三輪的範圍是49152~65535,稱爲動態或者私有端口,這些端口可以由普通用戶使用。
       對於一個網絡應用程序來說,通過地址、協議和端口號可以唯一地確定網絡上的一個應用程序。其中地址和端口的組合成爲端點(EndPoint)。每個Socket需要綁定到一個端點上與其他端點進行通信。
      在.NET中,System.Net命名空間提供了網絡編程的大多數數據類型以及常用操作,其中常用的類型如下:
     IPAddress類用來表示一個IP地址
     IPEndPoint類用來表示一個IP地址和一個端口的組合,成爲網絡的端點
     System.Net.Sockets命名空間中提供了基於Socket編程的數據類型
     Socket類封裝了Socket的操作。
     常用操作如下:
     Listen:設置基於連接通信的Socket進入監聽狀態,並設置等待隊列的長度
     Accept:等待一個新的連接,當連接到達的時候,返回一個針對新連接的Socket對象。通過這個新的Socket對象,可以與新連接通信
     Receive:通過Socket接受字節數據,保存到一個字節數組中,返回實際接受的字節數。
     Send:通過Socket發送預先保存在字節數組中的數據。

下面的代碼演示瞭如何通過Socket編程創建一個簡單的Web服務器。這個服務器通過49152端口提供訪問,向瀏覽器返回一個固定的靜態網頁。在這個示例中,請求的消息由瀏覽器生成,併發送到服務器,這個程序將簡單地顯示請求的信息。迴應的消息由服務器程序生成,通過Socket傳輸層返回給瀏覽器。


static void Main(string[] args)
        {
            //獲得本機的 loopback 網絡地址
            IPAddress address = IPAddress.Loopback;
            //創建可以訪問的端點
            IPEndPoint endPoint = new IPEndPoint(address, 49152);
            //創建Socket,使用IPv4地址,傳輸控制協議,雙向、可靠、基於鏈接的字節流
            Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //將socket綁定到端點上
            socket.Bind(endPoint);
            //設置連接隊列的長度
            socket.Listen(10);

            Console.WriteLine("開始監聽,端口號:{0}",endPoint.Port);
            while (true)
            {
                //開始監聽,這個方法會阻塞線程的執行,直到接受到一個客戶端的連接請求
                Socket client = socket.Accept();
                //輸出客戶端地址
                Console.WriteLine(client.RemoteEndPoint);
                //準備讀取客戶端請求的數據讀取的數據將保存在一個數組中
                byte[] buffer=new byte[4096];
                //接受數據並獲取數據長度
                int length = client.Receive(buffer, 4096, SocketFlags.None);
                //將請求的數據翻譯爲UTF-8
                System.Text.Encoding utf8 = System.Text.Encoding.UTF8;
                string requestString = utf8.GetString(buffer, 0, length);
                //顯示請求的消息
                Console.WriteLine(requestString);
                //迴應的狀態行
                string statusLine = "HTTP/1.1 200 OK \r\n";
                byte[] statusLineBytes = utf8.GetBytes(statusLine);
                //準備發送到客戶端的網頁
                string responseBody = "<html><head><title>From Socket Server</title></head><body><h1>Hello,world</h1></body></html>";
                byte[] responseBodyBytes = utf8.GetBytes(responseBody);
                //迴應頭部
                string responseHeader = string.Format("Content-Type:text/html;charset=UTF-8\r\nContent-Length:{0}\r\n",responseBody.Length);
                byte[] responseHeaderBytes = utf8.GetBytes(responseHeader);
                //向客戶端發送狀態信息
                client.Send(statusLineBytes);
                //向客戶端發送迴應頭
                client.Send(responseHeaderBytes);
                //頭部與內容的分隔行
                client.Send(new byte[] { 13, 10 });
                //向客戶端發送內容部分
                client.Send(responseBodyBytes);
                //端口與客戶端的連接
                client.Close();
                if (Console.KeyAvailable)
                    break;
            }
            //關閉服務器
            socket.Close();
        }



開啓服務,請求localhost:49152 模擬成功

-----------------------------------------------------------------
基於TcpListener的Web服務器

       爲了簡化基於TCP協議的監聽程序,.NET在System.Net.Sockets命名空間中提供了TcpListener類,使用它,在構造函數中傳遞一組網絡端點信息就可以準備好監聽參數,而不再需要設置使用的網絡協議等細節,調用Start方法之後,監聽工作就開始了。AcceptTcpClient方法將阻塞進程,直到一個客戶端的連接到達監聽器,這個方法將返回一個代表客戶端連接的代理對象,它的類型爲TcpClient,我們可以通過對它與客戶端進行通信。
      在輸入輸出部分,通過TcpClient對象可以得到一個用戶輸入和輸出的網絡流對性NetworkStream,這是一個派生自Stream對象的字節流對象,對Socket的輸入和輸出進行了封裝,這樣,我們可以通過常用的字節流操作來完成網絡的輸入和輸出。

static void Main(string[] args)
        {
            //取得本機的loopback網絡地址
            IPAddress address = IPAddress.Loopback;
            //創建可以訪問的端點
            IPEndPoint endPoint = new IPEndPoint(address, 49152);
            //創建TCP監聽器
            TcpListener newserver = new TcpListener(endPoint);
            //啓動監聽
            newserver.Start();
            Console.WriteLine("開始監聽");
            while (true)
            {
                //等待客戶端連接
                TcpClient client = newserver.AcceptTcpClient();
                Console.WriteLine("已經建立連接");
                //得到一個網絡流
                NetworkStream ns = client.GetStream();
                System.Text.Encoding utf8 = System.Text.Encoding.UTF8;
                byte[] request=new byte[4096];
                int length = ns.Read(request, 0, 4096);
                //請求信息
                string requestString = utf8.GetString(request, 0, length);
                Console.WriteLine(requestString);
                //狀態行
                string statusLine = "HTTP/1.1 200 OK\r\n";
                byte[] statusLineBytes = utf8.GetBytes(statusLine);
                //網頁內容
                string responseBody = "<html><head><title></title></head><body>Hello<body></html>";
                byte[] responseBodyBytes = utf8.GetBytes(responseBody);
                //迴應的頭部信息
                string responseHeader = string.Format("Content-Type:text/html;charset=UTF-8\r\nContent-Length:{0}\r\n",responseBody.Length);
                byte[] responseHeaderBytes = utf8.GetBytes(responseHeader);
                //輸入狀態行
                ns.Write(statusLineBytes, 0, statusLineBytes.Length);
                //輸出迴應頭部
                ns.Write(responseHeaderBytes, 0, responseHeaderBytes.Length);
                //迴應頭部與內容之間的空行
                ns.Write(new byte[]{13,10},0,2);
                //輸出內容
                ns.Write(responseBodyBytes, 0, responseBodyBytes.Length);
                //關閉客戶端連接
                client.Close();
                if (Console.KeyAvailable)
                    break;
            }
            //關閉服務器
            newserver.Stop();
        }



----------------------------------------------

基於HttpListener的Web服務器
        爲了進一步簡化HTTP協議的監聽器,.NET在命名空間System.NET中提供了HttpListener類。伴隨這個對象,.NET提供了一系列相關對象封裝了HTTP的處理工作。注意,這個類使用Http.sys系統組件完成工作,所以,只有在Windows XP SP2 或者 Server 2003以上的操作系統中才能使用。
       HttpListener類進一步簡化了監聽工作,僅需通過字符串的方法提供監聽的地址、端口號以及虛擬路徑,就可以開始監聽工作。
      開始監聽後,GetContext方法將阻塞線程,當客戶端的請求到達之後,HttpListener返回一個HttpListenerContext對性愛那個最爲處理客戶端請求的總代理,通過代理對象的Request屬性,我們可以得到一個類型爲HttpListenerRequest的代表請求參數的對象,這個對象將大多數請求參數進行了對象化,所以,我們可以通過它提供的一系列屬性來獲取請求參數。例如HttpListenerRequest的HttpMethod屬性就提供了請求的方法類型。通過代理的Response屬性,可以得到一個類型爲HttpListenerResponse的迴應處理對象,這個對象將回應的數據和操作進行了封裝,使得我們大幅度簡化了迴應的編程工作了。

static void Main(string[] args)
        {
            //檢查系統是否支持
            if (!HttpListener.IsSupported)
            {
                throw new System.InvalidOperationException("使用HttpListener必須爲Windows XP SP2 或 Server2003 以上系統");
            }
            //注意前綴必須以  / 正斜槓結尾
            string[] prefixes = new string[] {"http://localhost:49152/" };
            //創建監聽器
            HttpListener listener = new HttpListener();
            //增加監聽的前綴
            foreach (string s in prefixes)
            {
                listener.Prefixes.Add(s);
            }
            //開始監聽
            listener.Start();
            Console.WriteLine("監聽中");
            while (true)
            {
                //注意:GetContext方法將阻塞線程,知道請求到達
                HttpListenerContext context = listener.GetContext();
                //獲取對象
                HttpListenerRequest request = context.Request;
                Console.WriteLine("{0}{1}HTTP/1.1",request.HttpMethod,request.RawUrl);
                Console.WriteLine("Accept:{0}",string.Join(",",request.AcceptTypes));
                Console.WriteLine("Accept-Language:{0}",string.Join(",",request.UserLanguages));
                Console.WriteLine("User-Agent:{0}",request.UserAgent);
                Console.WriteLine("Accept-Encoding:{0}",request.Headers["Accept-Encoding"]);
                Console.WriteLine("Connection:{0}",request.KeepAlive?"Keep-Alive":"close");
                Console.WriteLine("Host:{0}",request.UserHostName);
                Console.WriteLine("Pragma:{0}",request.Headers["Pragma"]);
                //取得迴應對象
                HttpListenerResponse response = context.Response;
                //構造 迴應內容
                string responseString = @"<html><head><title>aaa</title></head><body>Hello</body></html>";
                response.ContentLength64 = System.Text.Encoding.UTF8.GetByteCount(responseString);
                response.ContentType = "text/html;charset=UTF-8";

                System.Text.Encoding utf8 = System.Text.Encoding.UTF8;
                System.IO.Stream output = response.OutputStream;
                byte[] responseStringBytes=utf8.GetBytes(responseString);
                //輸出迴應
                output.Write(responseStringBytes, 0, responseStringBytes.Length);
                if (Console.KeyAvailable)
                    break;
            }
            listener.Stop();
        }

       在使用HttpListener的時候,常用的請求和迴應參數都變成了對象的屬性,大幅度降低了變成的工作量。但是,大多數的參數還是需要通過Headers索引器來訪問的,就像上例中的Accept-Encoding請求參數,我們就不能直接通過屬性訪問。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章