ASP.net本質論之用控制檯應用程式建立Asp.net伺服器

駑馬農夫發表於2017-09-26
主題 概要
Asp.net 應用程式域、HttpRunTime
編輯 時間
新建 20170925
序號 參考資料
1 Asp.net本質論
2 C#高階程式設計(第七版)
3 http://blog.csdn.net/sh524555685/article/details/7454244(應用程式域解釋)
4 http://blog.csdn.net/lhc1105/article/details/47815971(程式集加到GAC方法)

Web應用程式,歸根結底是一種網路處理程式,網路處理程式主要的關鍵就是監聽與處理。

監聽程式

Socket是最原始的網路監聽程式,下面的示例顯示了監聽本機9152埠並返回hello,world的html頁面。

Socket監聽程式

namespace SocketWeb
{
    public class Socket
    {
        public void run()
        {
            IPAddress address = IPAddress.Loopback;

            IPEndPoint endPoint = new IPEndPoint(address, 9152);

            System.Net.Sockets.Socket socket = new System.Net.Sockets.Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

            socket.Bind(endPoint);

            socket.Listen(10);

            Console.WriteLine("開始監聽,埠號為{0}", endPoint.Port);

            while (true)
            {
                System.Net.Sockets.Socket client = socket.Accept();
                try
                {

                    Console.WriteLine(client.RemoteEndPoint);

                    byte[] buffer = new byte[4096];

                    int length = client.Receive(buffer, 4096, SocketFlags.None);

                    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";
                    //string statusLine = "200 OK\r\n";

                    byte[] statusLineByte = utf8.GetBytes(statusLine);

                    string responseBody = @"<!DOCTYPE html><html lang = 'en'><head><meta charset = 'UTF-8'>" +
                        "<title> From Socket Server</title></head><body><h1>hello,world </h1></body></html> ";

                    //string responseBody = @"123456";

                    byte[] responseBodyByte = utf8.GetBytes(responseBody);

                    string responseHead = string.Format("Content-Type:text/html; charset=utf-8\r\nContent-Length:{0}\r\n",responseBodyByte.Length);

                    byte[] responseHeadByte = utf8.GetBytes(responseHead);

                    client.Send(statusLineByte);

                    client.Send(responseHeadByte);

                    //--頭部與內容的分隔行
                    client.Send(new byte[] { 13,10});

                    client.Send(responseBodyByte);

                    client.Close();

                    if (Console.KeyAvailable)
                    {
                        break;
                    }
                }
                catch (Exception ex)
                {
                    var msg = ex.Message;
                    client.Close();
                }

            }

            socket.Close();
        }
    }
}

在main函式中執行這個run方法進行監聽,在瀏覽器中輸入localhost:9152,能返回響應:
這裡寫圖片描述

需要注意的是傳送響應內容時一定要把頭部和內容分隔開:
這裡寫圖片描述

不然會被中止連線:
這裡寫圖片描述

TCP監聽程式

.net為了簡化TCP協議的監聽程式,提供了一個TcpListener類,下面的程式碼與socket程式的效果相同:

namespace Aspnet.TcpListener
{
    public class WebTcp
    {
        public void run()
        {
            IPAddress address = IPAddress.Loopback;

            IPEndPoint endPoint = new IPEndPoint(address, 9152);

            System.Net.Sockets.TcpListener tcpListener = new System.Net.Sockets.TcpListener(endPoint);

            tcpListener.Start();

            Console.WriteLine("開始監聽,埠號為{0}", endPoint.Port);

            while (true)
            {
                TcpClient tcpClient = tcpListener.AcceptTcpClient();
                Console.WriteLine("已經建立聯接");

                //--得到一個網路流
                NetworkStream ns = tcpClient.GetStream();                 

                try
                {

                    byte[] buffer = new byte[4096];

                    int length = ns.Read(buffer,0,4096);

                    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";
                    //string statusLine = "200 OK\r\n";

                    byte[] statusLineByte = utf8.GetBytes(statusLine);

                    string responseBody = @"<!DOCTYPE html><html lang = 'en'><head><meta charset = 'UTF-8'>" +
                        "<title> From Socket Server</title></head><body><h1>hello,world </h1></body></html> ";


                    byte[] responseBodyByte = utf8.GetBytes(responseBody);

                    string responseHead = string.Format("Content-Type:text/html; charset=utf-8\r\nContent-Length:{0}\r\n", responseBodyByte.Length);

                    byte[] responseHeadByte = utf8.GetBytes(responseHead);


                    ns.Write(statusLineByte,0,statusLineByte.Length);
                    ns.Write(responseHeadByte, 0, responseHeadByte.Length);
                    ns.Write(new byte[] { 13, 10 }, 0, 2);
                    ns.Write(responseBodyByte, 0, responseBodyByte.Length);

                    ns.Close();

                    if (Console.KeyAvailable)
                    {
                        break;
                    }
                }
                catch (Exception ex)
                {
                    var msg = ex.Message;
                    ns.Close();
                }
            }

            tcpListener.Stop();
        }

    }
}

HTTP監聽程式

為了進一步簡化HTTP協議的監聽程式,.net提供了一個HttpListener類,通過字串的形式提供地址和監聽埠號。

  public void run()
        {
            string prex = "http://localhost:9152/";
            System.Net.HttpListener httpListener = new System.Net.HttpListener();

            httpListener.Prefixes.Add(prex);
            httpListener.Start();

            Console.WriteLine("開始監聽.....");

            while (true)
            {
                HttpListenerContext context = httpListener.GetContext();
                Console.WriteLine("已經建立聯接");

                try
                {
                    HttpListenerRequest request = context.Request;
                    Console.WriteLine(request.ToString());

                    HttpListenerResponse response = context.Response;   
                    string responseBody = @"<!DOCTYPE html><html lang = 'en'><head><meta charset = 'UTF-8'>" +
                        "<title> From Socket Server</title></head><body><h1>hello,world </h1></body></html> ";

                    response.ContentLength64 = System.Text.Encoding.UTF8.GetByteCount(responseBody);
                    response.ContentType = "text/html; charset=utf-8";

                    Stream output = response.OutputStream;
                    StreamWriter streamWriter = new StreamWriter(output);
                    streamWriter.Write(responseBody);

                    streamWriter.Close();
                    if (Console.KeyAvailable)
                    {
                        break;
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);                  
                }
            }

            httpListener.Stop();
        }

可以看到,上面的監聽和處理程式實際上是在一起的,只是通過監聽程式回發一些固定的內容。下面通過建立自定義的應用程式域,自定義請求處理函式來建立自己的Web伺服器。

自定義Web伺服器

所謂自定義Web伺服器,就是複用前面的http監聽程式,並呼叫自己的請求處理程式,完全拋棄IIS伺服器。

在建立之前,首先需要了解一個概念。

Web應用程式域

在.net之前的技術中,程式作為獨立的邊界來使用,每個程式都有其私有的虛擬記憶體。在.net體系結構中,應用程式有一個新的邊界:應用程式域。多個應用程式可以執行在一個程式的多個應用程式域中。
這裡寫圖片描述

那為什麼要引入應用程式域?
我們知道所有.net應用程式都執行在託管環境中,但作業系統只提供程式供程式執行,而程式只是提供了基本的記憶體管理,它不瞭解什麼是託管程式碼。所以託管程式碼,也可以說是我們建立的.Net程式,是無法直接執行在作業系統程式中的。為了使託管程式碼能夠執行在非託管的程式之上,就需要有一箇中介者,這個中介者可以執行於非託管的程式之上,同時向託管程式碼提供執行的環境。這個中介者就是應用程式域(Application Domain,簡寫為App Domain)。所以我們的.Net程式,不管是Windows窗體、Web窗體、控制檯應用程式,又或者是一個程式集,總是執行在一個App Domain中

如果只有一個類庫程式集(.dll檔案),是無法啟動一個程式的(它並非可執行檔案)。所以,建立程式需要載入一個可執行程式集(Windows 窗體、控制檯應用程式等.exe檔案)。當可執行程式集載入完畢,.Net會在當前程式中建立一個新的應用程式域,稱為預設應用程式域。一個程式中只會建立一個預設應用程式域,這個應用程式域的名稱與程式集名稱相同。預設應用程式域不能被解除安裝,並且與其所在的程式同生共滅。

那為什麼引入了應用程式域,就能執行託管程式碼呢?也就是,應用程式域是如何提供託管環境的?
簡單來說,是因為應用程式域允許它所載入的程式集訪問由.net Runtime所提供的服務。這些服務包括託管堆(Managed Heap),垃圾回收器(Garbage collector),JIT 編譯器等.Net底層機制,這些服務本身(它們構成了.Net Runtime)是由非託管C++實現的。

可以看到,一個程式可以包含多個應用程式域,每個應用程式域都有自己的執行時環境。

建立自定義Web伺服器的應用程式域

一個程式可以包含多個應用程式域,我們建立一個控制檯exe程式來啟動程式,我們稱啟動程式建立的預設應用程式域叫控制檯應用程式域,把監聽以及資料回發的功能放在這個應用程式域中。另外建立一個應用程式域來進行請求的處理,假設叫作Web伺服器應用程式域。兩個應用程式域之間的通訊涉及到一個跨域訪問的問題,跨域訪問的類必須派生自System.MarshalByRefObject。

請求處理類

新增一個程式集Aspnet.WebServer,並定義一個WebServer類:

namespace Aspnet.WebServer
{
    public class WebServer:System.MarshalByRefObject
    {
        public void ProcessRequest(string page,string query,System.IO.TextWriter writer)
        {

            Console.WriteLine("Web伺服器應用程式域ID={0}",AppDomain.CurrentDomain.Id);
            System.Web.Hosting.SimpleWorkerRequest simpleRequest = new System.Web.Hosting.SimpleWorkerRequest(page,query,writer);
            System.Web.HttpRuntime.ProcessRequest(simpleRequest);
        }

    }
}

這個類封裝了一個SimpleWorkerRequest物件,並呼叫該應執行時ProcessRequest方法。這就是我們自定義的Web處理程式,列印出當前應用程式域ID,並轉給執行時處理。

重寫HttpLinstener監聽程式

監聽程式,就使用上面的HttpLinstener,為它加上處理程式的委託,並把請求處理轉給此委託。

  public delegate void ProcessRequestHandler(string page, string query, System.IO.TextWriter writer);

        public ProcessRequestHandler processRequestHandler;

        public void setProcessRequestHandler(ProcessRequestHandler processRequestHandler)
        {
            this.processRequestHandler = processRequestHandler;
        }   

新的監聽程式:

public void run()
        {
            string prex = "http://localhost:9152/";
            System.Net.HttpListener httpListener = new System.Net.HttpListener();

            httpListener.Prefixes.Add(prex);
            httpListener.Start();

            Console.WriteLine("控制檯應用程式域ID={0}",AppDomain.CurrentDomain.Id);
            Console.WriteLine("開始監聽.....");

            while (true)
            {
                HttpListenerContext context = httpListener.GetContext();
                Console.WriteLine("已經建立聯接");

                try
                {
                    HttpListenerRequest request = context.Request;
                    Console.WriteLine(request.ToString());

                    HttpListenerResponse response = context.Response;


                    using (TextWriter streamWriter = new StreamWriter(response.OutputStream))
                    {
                        string path = Path.GetFileName(request.Url.AbsolutePath);

                        StringWriter sw = new StringWriter();

                        this.processRequestHandler(path, request.Url.Query, sw);

                        var code = sw.Encoding;

                        //--獲取處理結果
                        string content = sw.ToString();                      
                        sw.Close();

                        Console.WriteLine(content);

                        response.ContentLength64 = System.Text.Encoding.UTF8.GetByteCount(content);
                        response.ContentType = "text/html; charset=utf-8";
                        streamWriter.Write(content);

                        Console.WriteLine("Process OK");

                    }

                    if (Console.KeyAvailable)
                    {
                        break;
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
            }
            httpListener.Stop();
        }

自定義應用程式域

現在監聽程式和處理程式都準備完畢,需要自定義我們的Web應用程式域。使用CreateApplicationHost方法建立:

namespace Aspnet.SelfAppDomain
{
    public class SelfAppDomain
    {
        public void build()
        {

            System.Type hostType = typeof(WebServer.WebServer);
            WebServer.WebServer sefWebServer = System.Web.Hosting.ApplicationHost.CreateApplicationHost(hostType, "/",
                System.Environment.CurrentDirectory) as WebServer.WebServer;

            Console.WriteLine("CurrentDomain ID:{0}", AppDomain.CurrentDomain.Id);

            WebHttp httpListener = new WebHttp();
            httpListener.setProcessRequestHandler(sefWebServer.ProcessRequest);

            httpListener.run();            
        }
    }
}

要特別注意的是,使用CreateApplicationHost建立新的應用程式域,這個應用程式將重新載入hostType,按照:
1) GAC
2) 網站物理檔案目錄下的bin資料夾。
的順序尋找,不然會報未找到檔案錯誤。

但比較奇詭的是,我把程式集拷到bin目錄下,總是不成功。因此,為了順利執行,只好把它載入到GAC中。載入方法:
執行VS2015命令提示符
1) 通過sn –k 命令生成公鑰
這裡寫圖片描述

2) 程式集屬性中使用此公鑰簽名
這裡寫圖片描述

3)使用gacutil.exe –i 命令新增到GAC
這裡寫圖片描述

如果一切順利完畢,執行此控制檯應用程式,就完成了我們自定義的Web伺服器:
在瀏覽器中輸入http://localhost:9152,控制檯上顯示確實有了兩個應用程式域:
這裡寫圖片描述

相關文章