2011年10月6日

進入ASP.NET - ASP.NET的起點

最近開始深入ASP.NET的核心,花了很多時間來了解,也翻了很多書還有找了很多很多網站,同樣的如果不寫這篇,我想大概沒幾天我就忘記了,就像是委派一樣,看過四次,但還是只剩一些殘餘的印象XDD ( 所以下次一定要把委派整理起來- - ),所以我決定還是稱現在比較清楚的時候,趕快整理起來,並且寫到Blog來做個紀錄。

ASP.NET 核心

這系列是ASP.NET核心篇的起點,也就是ASP.NET底層是怎樣運作的,從一個HTTP Request近來的時候,會怎樣處理,Web應用程式又是怎樣吐回到Client的Browser,老實說,看完後,我覺得還滿有趣的 ( 但是看的時候一點也不有趣= = ),而這系列也是比較硬派一點的文章,我也試著用我了解的方式且比較多的例子來詮釋一遍,當然,如果有神人發現有誤,請務必和我說喔!

來寫一個Web Server吧!

當個使用者其實是最開心的,甚麼都可以不用管,出問題了還可罵說,怎麼寫得那麼爛…,但是身為程式開發人員就倒楣了,不但要被罵,還要找出哪裡有問題…,所以我們也必須去了解底層是怎樣運作,所以我們這邊簡單的從Browser開始講起;其實當一個Browser要求一個網頁的時候,其實是一堆文字的來來回回,也就是透過協定來進行Client和Server的傳輸 ( 細節就不說了,如果有機會,我在另外一篇補完 ); 而我們平常使用的IIS或是Apache等等Web Server,提供的網頁服務,就是再處理Browser的請求;當Browser需要一個東西,Web Server就吐回去一個東西,ok,我相信講到這邊,大家應該都沒有問題,那既然我們也知道協定、傳輸等等方式,是不是可以用C#等等語言寫一個Web Server!?,答案當然是可以,但我想應該沒有人會那麼無聊,寫出像IIS或是Apache那樣強大的Web Server…( 或是說,也很難寫出來,還是乖乖地用人家寫的吧 ),雖然無法寫出那麼強大的Web Server,但是簡單的還是可以吧!?,是的,就像下面的程式碼;不過不用急著去把它看完 ( 我想看到這麼長的程式,大概就會想直接按"上一頁"了吧… );它的原理其實很簡單,利用TcpListener這個類別來監聽,聽到請求後就將資料吐回,當然你也可以用Socket這個類別或是HttpListener來實作;總之,我想強調的是,基本上Client和Server端通訊內容,其實也就是一堆文字,也就是利用這些文字來處理要做甚麼事情。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.IO;
using System.Threading;
using System.Web;
using System.Web.Hosting;
using System.Net;

namespace AspNetSimulator
{
    class WebHandle
    {
        private TcpListener _tcpListener;    //監聽用
        private FileStream _fileStream;      //利用串流處理要求。
        private string _virtualDir;
        private string _realDir;

        public WebHandle(string virtualDir, string realDir)
        {
            this._virtualDir = virtualDir;
            this._realDir = realDir;
        }

        //啟動服務
        public void StartService()
        {
            try
            {
                IPEndPoint address = new IPEndPoint(IPAddress.Loopback, 8080);
                _tcpListener = new TcpListener(address);
                //啟動監聽
                _tcpListener.Start();   
                Console.WriteLine("啟動服務...");
                
                WebServiceStart();
            }
            catch (NullReferenceException)
            {
                Console.WriteLine("NullReferenceException throwed!");
            }
        }

        //主要服務
        public void WebServiceStart()
        {
            while (true)
            {
                //建立連線
                TcpClient tcpClient = _tcpListener.AcceptTcpClient();
                Console.WriteLine("連線建立...");
                //取得網路串流
                NetworkStream networkStream = tcpClient.GetStream();
                //設定使用UTF-8
                Encoding utf8 = Encoding.UTF8;
                byte[] request = new byte[4096];
                //讀取請求,並塞到request裡面,此方法會傳回長度。
                int length = networkStream.Read(request, 0, 4096);
                //轉換成人類能看得懂的字串。
                string requestString = utf8.GetString(request, 0, length);

                string output = "";
                //中間略,簡單的說就是將網頁輸出到output...

                //以下只是簡單的將訊息送出。
                string statusLine = "HTTP/1.1 200 OK\r\n";
                byte[] outputStatusLineBs = utf8.GetBytes(statusLine);
                byte[] outputBodyBs = utf8.GetBytes(output);
                string responseHeader = string.Format("Content-Type: text/html;charset=UTF-8\r\nContent-Length:{0}\r\n",outputBodyBs.Length);
                byte[] outputResponseHeaderBs = utf8.GetBytes(responseHeader);
                networkStream.Write(outputStatusLineBs, 0, outputStatusLineBs.Length);
                networkStream.Write(outputResponseHeaderBs, 0, outputResponseHeaderBs.Length);
                networkStream.Write(new byte[]{13,10},0,2);
                networkStream.Write(outputBodyBs, 0, outputBodyBs.Length);
                tcpClient.Close();
            }
        }
    }
}

.NET的Application Domain

在談論ASP.NET之前,先說一個觀念,那就是Application Domain,他是.NET裡面管理的最小單位,舉例來說,每個Web應用程式會有一個Application Domain ( 和 Application Pool不同…),而Web Server也會有一個Application Domain;而Application Domain會有一些特性,例如說,兩個Application Domain不能直接互通、執行時,以Application Domian為邊界、卸載的時候也以Application Domain為單位 ( 但是可以隨時動態載入組件 )等等。

為何會提到Application Domain?

其實是因為Application Domain有一個關鍵性的特點,發現了嗎?就是兩個Application Domain不能直接互通!,簡單的說,當一個Web Server收到Client的請求時,Web Server沒辦法跨Application Domain到Web應用程式的Application Domain來讓Web應用程式處理這個請求 ( 也就是說Web應用程式沒辦法得知這個Client的請求,所以也沒辦法處理,然後吐回處理過的html網頁… )。

MarshalByRefObject是救星!

沒錯,解決辦法就是使用MarshalByRefObject這個類別,繼承了這個類別的物件,可以讓Web Server透過他來將資料封裝起來並傳遞到Web應用程式裡面去,然後再吐回Web Server。

來看看MarshalByRefObject如何寫

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Web.Hosting;
using System.Web;

namespace AspNetSimulator
{
    //繼承MarshalByRefObject來讓MyAspNetHost跨越兩個Application Domain 
    class MyAspNetHost:MarshalByRefObject
    {
         public void  ProcessRequest( string fileName ,ref string output)
         {
             //利用記憶體當串流。
             MemoryStream memoryStream=new MemoryStream();
             //設定輸出,來將網頁透過這個Stream送到這裡。
             StreamWriter streamWriter = new StreamWriter(memoryStream);
             //自動更新 
             streamWriter.AutoFlush = true;      
             //利用HttpWorkRequest類別,來設定要給Web應用程式的請求。
             //第一個參數是檔案名稱,第二個參數是query,第三個參數是將產生出來的html要透過哪個Stream做寫入至Memory
             HttpWorkerRequest worker = new SimpleWorkerRequest(fileName, "", streamWriter);   
             //正式派發出去執行。
             HttpRuntime.ProcessRequest(worker) ;
             //準備讀取的Stream
             StreamReader streamReader = new StreamReader(memoryStream);
             //移動指標
             memoryStream.Position = 0;
             //從頭讀到尾,並且塞到output裡面去。
             output = streamReader.ReadToEnd();
         }
    }
}

這裡要特別注意,此物件並不是用來傳遞,而是定義了這個物件以後,到時候我們的Web Server就可以來呼叫這個物件的方法,如上所示,我們就可以在Web Server這個類別使用ProcessRequest()這個方法;接下來,我們稍微解釋一下程式碼,基本上前面幾行設定串流模式,並利用記憶體當儲存體;比較需要注意的是這兩行。

 //利用HttpWorkRequest類別,來設定要給Web應用程式的請求。
 //第一個參數是檔案名稱,第二個參數是query,第三個參數是將產生出來的html要透過哪個Stream做寫入至Memory
 HttpWorkerRequest worker = new SimpleWorkerRequest(fileName, "", streamWriter);   
 //正式派發出去執行。
 HttpRuntime.ProcessRequest(worker) ;

我們利用SimpleWorkerRequest類別來產生HttpWorkerRequest這個抽象類別的實體 ( 也就是woker ),這裡有三個參數,第一個參數其實就是檔案名稱,例如Test.aspx,而第二個參數則是query,已就是檔案後面的?xxx=xxxx這種URL的query,第三個則表示未來Web應用程式產出的HTML要利用哪個串流來進行寫入,( 而這邊因為是範例,第二個參數就直接給空白),最後再將這個實體( worker ) 送到HttpRuntime.ProcessRequest來執行,送進去後,就是Web應用程式的事情了,經過一連串的執行運算,最後會產生HTML,並且透過串流寫到記憶體,所以就可以用以下程式碼進行取出。

 //準備讀取的Stream
 StreamReader streamReader = new StreamReader(memoryStream);
 //移動指標
 memoryStream.Position = 0;
 //從頭讀到尾,並且塞到output裡面去。
 output = streamReader.ReadToEnd();

要注意的是output是利用ref的方式,也就是說,到時候Web Server會定義一個output的變數,未來這裡會直接寫回到Web Server的output變數裡面,這樣,就完成了中介的MarshalByRefObject物件。

ApplicationHost.CreateApplicationHost方法

開始撰寫Web Server之前,還有一個東西需要講,那就是ApplicationHost.CreateApplicationHost這個方法,CreateApplicationHost是ApplicationHost的一個靜態方法,他可以幫助我們建立Web應用程式的Application Domain;我們執行程式時,當然會先將Web Server執行起來,也會建立起Web Server的Application Domain,但是Web應用程式呢?,所以我們會在Web Server裡面去建立Web應用程式的Application Domain,而建立的方法就是使用CreateApplicationHost。

//利用CreateApplicationHost方法來建立一個ASP.NET的Application Domain,
//以便執行ASP.NET。
_myAspNetHost = (MyAspNetHost)ApplicationHost.CreateApplicationHost
    (typeof(MyAspNetHost), _virtualDir, _realDir);

如上程式碼,就是建立Web應用程式的Application Domain的寫法,他會帶三個參數,第一個是最難解釋的,先跳過,第二個參數其實就是Web應用程式的根所對應的虛擬路徑,第三個則是實體路徑;接下來我們回來講第一個參數,通常我們會把剛剛寫好的MarshalByRefObject物件放到第一個參數,並且使用typeof來取得MarshalByRefObject的Type這個型別,老實說,這塊我看了很久,最後還去翻Source來查,才知道比較細的細節,以下我們先看一下CreateApplicationHost這個程式碼。

[SecurityPermission(SecurityAction.Demand, Unrestricted=true)]
public static object CreateApplicationHost(Type hostType, string virtualDir, string physicalDir)
{
    if (Environment.OSVersion.Platform != PlatformID.Win32NT)
    {
        throw new PlatformNotSupportedException(SR.GetString("RequiresNT"));
    }
    if (!StringUtil.StringEndsWith(physicalDir, Path.DirectorySeparatorChar))
    {
        physicalDir = physicalDir + Path.DirectorySeparatorChar;
    }
    ApplicationManager applicationManager = ApplicationManager.GetApplicationManager();
    string appId = (virtualDir + physicalDir).GetHashCode().ToString("x");
    return applicationManager.CreateInstanceInNewWorkerAppDomain(hostType, appId, VirtualPath.CreateNonRelative(virtualDir), physicalDir).Unwrap();
}

上面在敘說CreateApplicationHost這個方法,我們其實可以看到,傳進來的Type型別,又傳入到CreateInstanceInNewWorkerAppDomain裡面去,所以再往下看吧。

internal ObjectHandle CreateInstanceInNewWorkerAppDomain(Type type, string appId, VirtualPath virtualPath, string physicalPath)
{
    IApplicationHost appHost = new SimpleApplicationHost(virtualPath, physicalPath);
    HostingEnvironmentParameters hostingParameters = new HostingEnvironmentParameters {
        HostingFlags = HostingEnvironmentFlags.HideFromAppManager
    };
    return this.CreateAppDomainWithHostingEnvironmentAndReportErrors(appId, appHost, hostingParameters).CreateInstance(type.AssemblyQualifiedName);
}

到這邊,直接跳到最後一行,我們可以看到type.AssmblyQualifiedName,其實這是取出type的詳細資訊,也就是MarshalByRefObject的NameSpace、版本、等等的詳細資訊;這邊如果再繼續查下去,還有非常多層,我就跳過不講了,但至少我們發現了關鍵,也就是說,這邊轉成Type是為了讓Web應用程式的Application Domain在runtime時,動態載入MarshalByRefObject這個物件,所以在runtime的時候,程式會去尋找GAC和網站底下的bin目錄,並將之載入,所以需要知道MarshalByRefObject這個物件的詳細資訊,所以使用Type來取得詳細資訊;到這邊,我們知道了CreateApplicatinHost如何使用,我們就可以開始寫Web Server了。

撰寫Web Server

接下來就是整段的Web Server程式碼,下面比較簡單的都註解了,基本上看一下應該都可以看得懂。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.IO;
using System.Threading;
using System.Web;
using System.Web.Hosting;
using System.Net;

namespace AspNetSimulator
{
    class WebHandle
    {
        //繼承了MarshalByRefObject的MyAspNetHost,
        //負責處理We Server 和 Asp.net Application Domain之間的通訊
        private MyAspNetHost _myAspNetHost;
        private TcpListener _tcpListener;    //監聽用
        private FileStream _fileStream;      //利用串流處理要求。
        private string _virtualDir;
        private string _realDir;

        public WebHandle(string virtualDir, string realDir)
        {
            this._virtualDir = virtualDir;
            this._realDir = realDir;
        }

        //啟動服務
        public void StartService()
        {
            try
            {
                IPEndPoint address = new IPEndPoint(IPAddress.Loopback, 8080);
                _tcpListener = new TcpListener(address);
                //啟動監聽
                _tcpListener.Start();   
                Console.WriteLine("啟動服務...");
                //利用CreateApplicationHost方法來建立一個ASP.NET的Application Domain,
                //以便執行ASP.NET。
                _myAspNetHost = (MyAspNetHost)ApplicationHost.CreateApplicationHost
                    (typeof(MyAspNetHost), _virtualDir, _realDir);
              //啟動服務
                WebServiceStart();
            }
            catch (NullReferenceException)
            {
                Console.WriteLine("NullReferenceException throwed!");
            }
        }

        //主要服務
        public void WebServiceStart()
        {
            while (true)
            {
                //建立連線
                TcpClient tcpClient = _tcpListener.AcceptTcpClient();
                Console.WriteLine("連線建立...");
                //取得網路串流
                NetworkStream networkStream = tcpClient.GetStream();
                //設定使用UTF-8
                Encoding utf8 = Encoding.UTF8;
                byte[] request = new byte[4096];
                //讀取請求,並塞到request裡面,此方法會傳回長度。
                int length = networkStream.Read(request, 0, 4096);
                //轉換成人類能看得懂的字串。
                string requestString = utf8.GetString(request, 0, length);

                string output = "";
                //這裡是關鍵,
                //簡單的說,就是WebHandle這個Application Domain 透過 _myAspNetHost 來和
                //ASP.NET Application Domain傳遞訊息。
                //( 這裡因為是Demo,所以就直接指定為Test.aspx網頁了 )
                //而使用ref來將ASP.NET Application Domain的資料塞回這裡。
                _myAspNetHost.ProcessRequest("Test.aspx", ref output);
                //以下只是簡單的將訊息送出。
                string statusLine = "HTTP/1.1 200 OK\r\n";
                byte[] outputStatusLineBs = utf8.GetBytes(statusLine);
                byte[] outputBodyBs = utf8.GetBytes(output);
                string responseHeader = string.Format("Content-Type: text/html;charset=UTF-8\r\nContent-Length:{0}\r\n",outputBodyBs.Length);
                byte[] outputResponseHeaderBs = utf8.GetBytes(responseHeader);
                networkStream.Write(outputStatusLineBs, 0, outputStatusLineBs.Length);
                networkStream.Write(outputResponseHeaderBs, 0, outputResponseHeaderBs.Length);
                networkStream.Write(new byte[]{13,10},0,2);
                networkStream.Write(outputBodyBs, 0, outputBodyBs.Length);
                tcpClient.Close();
            }
        }
    }
}

我們會在_tcpListerer.start()後,建立Web應用程式的Application Domain,來讓Web應用程式準備運作,然後進入WebServiceStart方法,利用無窮迴圈來讓服務不中斷,而當程式讀到這行時,就會暫停,直到有請求進來,並繼續執行下去;另外WebServiceStart可以使用執行序來進行多執行序的處理,這裡是簡單的Demo,就直接這樣寫了。

TcpClient tcpClient = _tcpListener.AcceptTcpClient();

然後就進行串流的讀取,將請求的部分進行處理,如果寫的複雜一點,可能也還需去判斷要求的是哪個檔案,或是有沒有query等等的處理。( 所以沒事不要自己寫Web Server… )最後到下面這行,這邊就是透過繼承了MarshalByRefObject的物件,並呼叫此物件的方法,如上所說,經過一連串的執行,就會把html資料吐回到output裡面。

//這裡是關鍵,
//簡單的說,就是WebHandle這個Application Domain 透過 _myAspNetHost 來和
//ASP.NET Application Domain傳遞訊息。
//( 這裡因為是Demo,所以就直接指定為Test.aspx網頁了 )
//而使用ref來將ASP.NET Application Domain的資料塞回這裡。
_myAspNetHost.ProcessRequest("Test.aspx", ref output);

最後,我們去把這些資料輸出回瀏覽器,就完成了這個平常簡單的一次請求…

還有一小段進入點!

另外,有沒有發現上面全部都是類別,完全沒有進入點,其實我是使用了Console專案來撰寫,所以下面補上程式的進入點,於是Web Server (偽) 就完成了=w=。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace AspNetSimulator
{
    class ServerSimulator
    {
        static void Main(string[] args)
        {
            WebHandle webHandle = new WebHandle("/", "C:\\My\\TestWeb");
            webHandle.StartService();
        }
    }
}

寫了那麼多,其實也只是ASP.NET的開頭,也希望能讓大家對ASP.NET的一些基礎能更加瞭解!

沒有留言:

張貼留言