在 WinForm 中完整支援在多級目錄中儲存的 ASP.NET

來源:互聯網
上載者:User
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] +-------------+ [2] +--------+
| Admin |---->| CassiniForm |---->| Server |
+-------+ +-------------+ +--------+
| [3]
V
+--------+ [4] +------+
| Client |---->| Host |
+--------+ +------+
^ | [5]
| V
| +------------+ [6] +---------+
[7]| | Connection |---->| Request |--+
| +------------+ +---------+ | [7]
+----------------------------------------+



[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);

_started = true;
ThreadPool.QueueUserWorkItem(_onStart); // 通過線程池非同步呼叫
}

private void OnStart(Object unused) {
while (_started) {
try {
Socket socket = _socket.Accept(); // 響應用戶端請求
ThreadPool.QueueUserWorkItem(_onSocketAccept, socket); // 通過線程池非同步呼叫
}
catch {
Thread.Sleep(100);
}
}
_stopped = true;
}

private void OnSocketAccept(Object acceptedSocket) {
Connection conn = new Connection(this, (Socket)acceptedSocket);
conn.ProcessOneRequest(); // 處理用戶端請求
}
}



最後,判斷用戶端請求的有效性的功能,是通過三個重載的 Host.IsVirtualPathInApp 方法,提供給 Connection 在具體處理用戶端請求時調用,來判斷請求的有效性,下面討論 Connection 時再詳細解釋。

[5] Host 在建立 Connection 對象並調用其 ProcessOneRequest 方法處理使用者請求時,Connection 對象會首先等待用戶端請求資料(WaitForRequestBytes),然後建立 Request 對象,並調用 Request.Process 方法處理請求。而其自身,則通過一堆 WaitXXX 函數,為 Request 類提供支援。
以下內容為程式碼:

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;
}

string physPath = null;
string appPhysPath = this._appPhysPath.Substring(0, (this._appPhysPath.Length - 1)); // 去掉末尾斜杠

if (((path == null) || (path.Length == 0)) || path.Equals("/"))
{
physPath = appPhysPath;
}

if (path.StartsWith(this._appVirtPath))
{
physPath = appPhysPath + path.Substring(this._appVirtPath.Length).Replace('/', '\');
}

InternalSecurityPermissions.PathDiscovery(physPath).Demand();

return physPath;
}



Request.MapPath 的實現則相對要完善許多,考慮了很多 SimpleWorkerRequest 無法處理的情況,使得 Request 的適應性更強。
以下內容為程式碼:

public override String Request.MapPath(String path) {
String mappedPath = String.Empty;

if (path == null || path.Length == 0 || path.Equals("/")) {
// asking for the site root
if (_host.VirtualPath == "/") {
// app at the site root
mappedPath = _host.PhysicalPath;
}
else {
// unknown site root - don't point to app root to avoid double config inclusion
mappedPath = Environment.SystemDirectory;
}
}
else if (_host.IsVirtualPathAppPath(path)) {
// application path
mappedPath = _host.PhysicalPath;
}
else if (_host.IsVirtualPathInApp(path)) {
// inside app but not the app path itself
mappedPath = _host.PhysicalPath + path.Substring(_host.NormalizedVirtualPath.Length);
}
else {
// outside of app -- make relative to app path
if (path.StartsWith("/"))
mappedPath = _host.PhysicalPath + path.Substring(1);
else
mappedPath = _host.PhysicalPath + path;
}

mappedPath = mappedPath.Replace('/', '\');

if (mappedPath.EndsWith("\") && !mappedPath.EndsWith(":\"))
mappedPath = mappedPath.Substring(0, mappedPath.Length-1);

return mappedPath;
}




關於 Cassini 的進一步討論,可以參考 www.asp.net 上的討論專版。

[7] 在 HttRuntime 完成具體的 ASP.NET 頁面處理工作後,會通過 Request.SendResponseFromXXX 系列函數,將頁面結果返回給用戶端。

雖然 SimpleWorkerRequest.MapPath 方法實現簡單,但理論上完全可以處理多級目錄的情況。之所以在使用 SimpleWorkerRequest 時,無法處理嵌套目錄,是因為 SimpleWorkerRequest 在建構函式中錯誤地分解了請求的頁面所在虛擬目錄等資訊。
SimpleWorkerRequest 的兩個建構函式,在將請求頁面虛擬路徑(如"/help/about.aspx")儲存後,都調用了 ExtractPagePathInfo 方法對頁面路徑做進一步的分解工作。
以下內容為程式碼:

private void SimpleWorkerRequest.ExtractPagePathInfo()
{
int idx = this._page.IndexOf('/');
if (idx >= 0)
{
this._pathInfo = this._page.Substring(idx);
this._page = this._page.Substring(0, idx);
}
}



this._pathInfo 是為實現 HttpWorkerRequest.GetPathInfo 提供的儲存欄位。而 GetPathInfo 將返回 URL 中在頁面後的路徑資訊,例如對 "path/virdir/page.html/tail" 將返回 "/tail"。早期的許多 HTTP 用戶端程式,如 Delphi 中 WebAction 的分發,都利用了這個路徑資訊的特性,在 Web 頁面或 ISAPI 一級之後,再次進行請求分發。但因為 SimpleWorkerRequest 實現上或者設計上的限制,導致在處理 PathInfo 時會將 "/help/about.aspx" 類似的多級 url 錯誤切斷。最終返回給 HttpRuntime 的 this._path 將變成Null 字元串,而 this._pathInfo 被設定為 "/help/about.aspx",而單級路徑如 "about.aspx" 不受影響。
知道了這個原理後,就可以對 SimpleWorkerRequest 稍作修改,重載受到 ExtractPagePathInfo 影響的幾個方法,即可完成對多級目錄結構下頁面的支援。如果需要進一步的映射支援,如同時支援多個虛擬子目錄,可以參照 Cassini 的 Request 實現 MapPath 等方法。
以下內容為程式碼:

public class Request : SimpleWorkerRequest
{
private string _appPhysPath;
private string _appVirtPath;

private string _page;
private string _pathInfo;

public Request(string page, string query, TextWriter output) : base(page, query, output)
{
this._appPhysPath = Thread.GetDomain().GetData(".appPath").ToString();
this._appVirtPath = Thread.GetDomain().GetData(".hostingVirtualPath").ToString();

this._page = page;

// TODO: 從 page 中進一步解析 Path Info
}

public override string GetPathInfo()
{
if (this._pathInfo == null)
{
return string.Empty;
}
return this._pathInfo;
}

private string GetPathInternal(bool includePathInfo)
{
string path = (_appVirtPath.Equals("/") ? _page : _appVirtPath + _page);

if (includePathInfo && (_pathInfo != null))
{
return path + this._pathInfo;
}
else
{
return path;
}
}

public override string GetUriPath()
{
return GetPathInternal(true);
}

public override string GetFilePath()
{
return GetPathInternal(false);
}

public override string GetRawUrl()
{
string query = this.GetQueryString();

if ((query != null) && (query.Length > 0))
{
return GetPathInternal(true) + "?" + query;
}
else
{
return GetPathInternal(true);
}
}

public override string GetFilePathTranslated()
{
return _appPhysPath + _page.Replace('/', '\');
}

public override string MapPath(string path)
{
string physPath = null;

if (((path == null) || (path.Length == 0)) || path.Equals("/"))
{
physPath = this._appPhysPath;
}

if (path.StartsWith(this._appVirtPath))
{
physPath = this._appPhysPath + path.Substring(this._appVirtPath.Length).Replace('/', '\');
}

return physPath;
}
}




聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.