大概半年前曾写过一个在 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 将变成空字符串,而 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; } }
|