asp.net 大概半年前曾寫過一個在 WinForm 程式中嵌入 ASP.NET 的簡單例子,《在WinForm程式中嵌入ASP.NET》。因為是實驗性質的工作,所以當時偷懶直接使用系統內建的 SimpleWorkerRequest 完成 ASP.NET 頁面請求的處理工作。使用內建工具類在實現上雖然簡單,但受到系統的諸多功能限制,如後面有朋友提到無法直接處理多級子目錄的問題等等。(如虛擬目錄為 "/" 時無法處理 "/help/about.aspx" 類型的頁面請求)
對於此類需求,一個最好的實現執行個體就是 www.asp.net 提供的 Cassini。這個例子完整地示範了如何?一個支援 ASP.NET 的簡單 Web 服務器功能,並被 Borland 的 Delphi.NET 等許多開源項目,當作調試用 Web 服務器。雖然只有幾十 K 的原始碼,但麻雀雖小五髒俱全,還是非常值得一看的。但因為 Cassini 是為處理 Web 服務而設計,因此需要在瞭解其結構的基礎上,做一些定製來滿足我們的需求。
首先來看看 Cassini 的程式結構。
與我前文例子中採用的結構類似,Cassini 包括介面(CassiniForm)、伺服器(Server)、宿主(Host)和要求處理常式(Request)等幾個主要部分,並通過 Connection 等幾個工具類,完成 Web 請求的解析與應答功能。
[1] Cassini 的管理者(Admin)首先通過 CassiniForm 的介面,設定 Web 服務器連接埠、頁面物理目錄和虛擬目錄等配置資訊;
[2] 然後以配置資訊構造 Server 對象,並調用 Server.Start 方法啟動 Web 服務器;
以下內容為程式碼:
public class CassiniForm : Form
{
private void Start()
{
// ...
try {
_server = new Cassini.Server(portNumber, _virtRoot, _appPath);
_server.Start();
}
catch {
// 顯示錯誤資訊
}
// ...
}
}
[3] Server 對象在建立時,將擷取或自動初始化 ASP.NET 的註冊表配置。這個工作是通過 Server.GetInstallPathAndConfigureAspNetIfNeeded 方法完成的。工作原理是通過 HttpRuntime 所在 Assembly (System.Web.dll) 的版本獲得合適的 ASP.NET 版本;然後從註冊表中查詢 HKEY_LOCAL_MACHINESOFTWAREMicrosoftASP.NET 下是否有正確的 ASP.NET 的安裝路徑;如果有則返回之;否則會根據 System.Web.dll 的版本,以及 HKEY_LOCAL_MACHINESOFTWAREMicrosoft.NETFramework 下 .NET Framework 按照目錄等資訊,動態構造一個合適的 ASP.NET 註冊表配置。進行這個工作的原因是 ASP.NET 可以在按照 .NET Framework 後,使用 aspnet_regiis.exe 手工登出掉,而運行支援 ASP.NET 的 Web 服務器,又必須有合適的設定。
在完成配置和 ASP.NET 安裝路徑後,Server 將建立並配置 Host 對象作為 ASP.NET 的宿主。
以下內容為程式碼:
public class Server : MarshalByRefObject
{
private void CreateHost() {
_host = (Host)ApplicationHost.CreateApplicationHost(typeof(Host), _virtualPath, _physicalPath);
_host.Configure(this, _port, _virtualPath, _physicalPath, _installPath);
}
public void Start() {
if (_host != null)
_host.Start();
}
}
[4] Host 類作為 ASP.NET 的宿主類,主要完成三部分工作:配置 ASP.NET 的運行時環境、響應用戶端(Client)發起的 Web 頁面請求、以及判斷用戶端請求的有效性。
配置 ASP.NET 的運行時環境主要工作是,為 ASP.NET 的執行和後面請求有效性的判斷擷取足夠的配置資訊。例如 Server 能夠提供的 Web 服務連接埠、頁面虛擬路徑、頁面實體路徑以及 ASP.NET 程式安裝路徑等等,以及 Host 根據這些資訊計算出的 ASP.NET 用戶端指令碼的虛擬和實體路徑等等。此外還會接管線程所在 AppDomain 的卸載事件 AppDomain.DomainUnload,在 Web 服務器停止的時候自動終止 Web 服務。
響應用戶端(Client)發起的 Web 頁面請求功能,是通過建立 Socket 監聽 Server 對象指定的 Web 服務 TCP 通訊埠來完成的。Host.Start 方法建立 Socket,並通過線程池非同步呼叫 Host.OnStart 方法在後台監聽請求;Host.OnStart 方法則在 接收到 Web 請求後,通過線程池非同步呼叫 Host.OnSocketAccept 方法完成請求的響應工作;Host.OnSocketAccept 則負責在處理 Web 請求的時候,建立 Connection 對象,並進一步調用 Connection.ProcessOneRequest 方法處理 Web 請求。雖然 Host 沒有使用複雜的請求分配演算法,但因為線程池的靈活使用,使得其效能完全不受處理瓶頸的限制,也是線程池使用的良好範例。
以下內容為程式碼:
internal class Host : MarshalByRefObject
{
public void Start() {
if (_started)
throw new InvalidOperationException();
// 建立 Socket 監聽 Web 服務連接埠
_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
_socket.Bind(new IPEndPoint(IPAddress.Any, _port));
_socket.Listen((int)SocketOptionName.MaxConnections);
internal class Connection {
public void ProcessOneRequest() {
// wait for at least some input
if (WaitForRequestBytes() == 0) { // 等待用戶端請求資料
WriteErrorAndClose(400); // 發送 HTTP 400 錯誤給用戶端
return;
}
Request request = new Request(_host, this);
request.Process();
}
private int WaitForRequestBytes() {
int availBytes = 0;
try {
if (_socket.Available == 0) {
// poll until there is data
_socket.Poll(100000 /* 100ms */, SelectMode.SelectRead); // 等待用戶端資料 100ms 時間
if (_socket.Available == 0 && _socket.Connected)
_socket.Poll(10000000 /* 10sec */, SelectMode.SelectRead);
}
availBytes = _socket.Available;
}
catch {
}
return availBytes;
}
[6] Request 在接收到 Connection 的請求後,將從用戶端讀取請求內容,並按照 HTTP 協議進行分析。因為本文不是做 HTTP 協議的分析工作,所以這部分代碼就不詳細討論了。
在 Request.ParseRequestLine 函數分析 HTTP 要求獲得請求頁面路徑後,會調用前面提到過的 Host.IsVirtualPathInApp 函數判斷此路徑是否在 Web 服務器提供的虛擬路徑下級,並且返回此虛擬路徑是否指向 ASP.NET 的用戶端指令碼。如果 Web 請求的虛擬路徑以 "/" 結尾,則調用 Request.ProcessDirectoryListingRequest 方法返回列目錄的響應;否則調用 HttpRuntime.ProcessRequest 方法完成實際的 ASP.NET 請求處理工作。
HttpRuntime 通過 Request 的基類 HttpWorkerRequest 提供的統一介面,採用 IoC 的策略擷取最終頁面的所在。與我前面文章中使用的 SimpleWorkerRequest 實現最大不同在於 Request.MapPath 完成了一個較為完整的虛擬目錄到物理目錄映射機制。
SimpleWorkerRequest.MapPath 實現相對簡陋:
以下內容為程式碼:
public override string SimpleWorkerRequest.MapPath(string path)
{
if (!this._hasRuntimeInfo)
{
return null;
}