進入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的一些基礎能更加瞭解!