下载本文代码: CuttingEdge0301.exe (107KB)
| 宿主 ADO.NET 运行库 | |
| ApplicationHost 类 | |
| 示例 ADO.NET 主机 | |
| Cassini 个人 Web 服务器 | |
| CD 上的 Web 站点 | |
| 小结 |
早在 2000 年 9 月和 10 月发行的 MSDN Magazine 中,我讲述了如何构建 ASP 应用程序的客户端环境;也就是运行 ASP 网页的无服务器环境(参见 前沿技术:A Client-side Environment for ASP Pages 和 前沿技术:A Client-side Environment for ASP Pages art 2)。这些专栏的灵感源自下面的情况。
假设您的一个客户需要利用一张 CD 来在线发布某些内容,例如,百科全书、黄页或文档集。客户需要在 CD 内包含一个查看器应用程序和一种灵活的软件体系结构来传送内容。另外,除了有处理器最低性能和使用最新版本的 Windows 要求外,客户希望 CD 没有什么特殊的系统要求,从而方便用户使用。这意味着最终的应用程序不应依赖于国内版本 Windows 中包括的 Microsoft Internet 信息服务 (IIS) 或个人 Web 服务器。它应在纯粹的、无服务器的环境下离线运行。
在很多情况下,客户有现成的在线内容 Web 站点。在其它情况下,作为项目的一部分,她计划为在线内容创建 Web 站点。在这种情况下,利用 ASP 或 ADO.NET 来做这项工作是很明智的选择,因为它们非常灵活并且功能强大,可使您快速、有效地构建查看器应用程序。但是自定义应用中果真能够宿主 ASP 或 ADO.NET 吗?
大约三年前当我第一次探讨这个问题时,ADO.NET 还没有发布,只是谣传有一种暂定被称为 ASP+ 的东西,这种东西很炫,并且不久即将发行。那时,没有可用来在 IIS 之外以离线方式呈现 ASP 页的工具。更糟糕的是,设计 ASP 并不能轻松地完成这样的宿主操作。因此我提出一种由两部分构成的 ASP 模拟器:一种专用的浏览器以及一个 ASP 服务器模块。构建的浏览器用来截取任何形式的提交与 URL 请求,并且将它们重定向到我自己的 ASP 服务器模块。反过来,ASP 服务器将从磁盘加载资源,解析其内容,从而动态生成 HTML 代码。该浏览器还负责利用与各种响应、请求和服务器 ASP 对象几乎完全相同的编程接口来实例化与初始化伪对象。图 1 概括了总体体系结构。

图 1 离线服务器
虽然不是理想的解决方案,但它还是满足了客户的期待,并整合成一种较大型的产品,今天仍然能够使很多专业人士使用在线和离线的内容。根据从这些专栏发布起我获得的反馈信息可以判断出,很多开发人员都面临过类似的挑战。
几个月后,Microsoft 发布了第一个 beta 版的 ADO.NET,我思考着利用该新产品重新访问我的解决方案。ADO.NET 的设计是模块化的,所以很适合于宿主在外部应用中,包括 IIS 自身。但是,能够在自定义应用程序中宿主 ADO.NET 并不等于构建了一种由 CD 提供的离线 Web 浏览器的现成解决方案。在自定义应用程序中宿主 ADO.NET 运行库引擎仅是离线提供动态内容的第一步。如果看看图 1 中所示的体系结构您就会发现,它基于两个不同的组件 — 一个还接受用户输入的请求处理器和一个生成实际 HTML 代码的 ASP 源处理器。宿主 ADO.NET 引擎只是取代了图 1 中的 ASP 服务器模块。实际上,您需要更多的东西 — 在理想情况下,需要的是一个浏览器和一个嵌入的 Web 服务器。
ADO.NET Cassini 示例 Web 服务器(参见http://www.asp.net/Projects/Cassini/Download)是一种可以集成、部署解决方案的压缩、本地 Web 服务器。Cassini 采用 ADO.NET 宿主 API(System.Web.Hosting 命名空间)来创建简单的托管 Web 服务器。套接字连接是通过 System.Net API 来处理的。可从 Microsoft 获得 Cassini 的源代码。图 2 显示了基于 Cassini 的离线 Web 应用程序的典型体系结构。您可以看到,整个方案就像是一种基于 Internet 的典型 Web 应用程序,但是更为简单。很显然,Cassini 既不是 IIS 的完全代替物,也不是 Microsoft 版的开放源代码 Web 服务器。Cassini 是本地的 Web 服务器,用来处理对本地文件夹的本地调用。我将回顾一下 Cassini 组件,然后为您说明如何在 CD 上部署 Web 站点,并以此作为结束。

图 2 Cassini Web 应用程序
ADO.NET 应用程序并不要求将 IIS 作为主机模块。事实上,ADO.NET 甚至不要求用 Web 服务器来运行。它公开了一个任何调用方都能使用的尽人皆知的接口,连接并要求内部的 HTTP 管道处理请求。
宿主 ADO.NET 引擎时,两个类起着重要的作用 — ttpRuntime 和 ApplicationHost。前者是对象的管道的入口点,它更像一条装配链,可以将 .aspx 资源的原始 HTTP 请求转变为全新的 HTML 文本。后者使得客户端应用程序宿主 ADO.NET 引擎成为可能。ApplicationHost 类负责创建主机进程中的 AppDomain,该进程将处理新应用程序的所有传入请求。
Tim Ewald 和 Keith Brown 在他们的文章“HTTP Pipelines: Securely Implement Processing, Filtering, and Content Redirection with HTTP Pipelines in ADO.NET”(MSDN 杂志 2002 年 9 月刊)中全面讲述了 HttpRuntime 类的内部组成。只在应用程序主机收到并预处理请求时,才使用 HttpRuntime 类。应用程序主机将所有请求信息打包到一个请求类中,该请求类派生于 HttpWorkerRequest 抽象类,或更可能派生于其名为 SimpleWorkerRequest 的标准实现类。在准备好使用请求类实例后,主机将处理权移交给 HttpRuntime,调用其 ProcessRequest 静态方法,如下列代码所示:
SimpleWorkerRequest req; req = new SimpleWorkerRequest(aspx, null, Console.Out); HttpRuntime.ProcessRequest(req);
前面的代码片断显示了启动对 ADO.NET 网页进行处理的核心代码。该代码的执行由通过 ApplicationHost 创建的主机类的某个特殊方法来控制。稍后我将回到该主题。现在,绝对可以说,SimpleWorkerRequest 的构造函数根据 ASPX 资源的虚拟路径进行处理、采用一个可选的查询字符串,并采用文本编写器对象作为输出。您可以使用流编写器对象(而不是标准输出控制台)将 HTML 代码保存到磁盘。
主机和 ADO.NET HTTP 运行库之间的交互是由名为 ApplicationHost 的特定 Microsoft .NET Framework 类来控制的。从 ADO.NET HTTP 运行库的角度来看,主机只是调用方 — 即创建了当前的 AppDomain 并且通过调用 ProcessRequest 方法为特定的请求提供服务的模块。ADO.NET HTTP 运行库和主机之间的接口都由 ApplicationHost 类的操作来完成。HTTP 运行库全然没有调用方的特性 — 而完全是一个像 IIS 一样的 Web 服务器、一个像 Cassini 一样的的本地 Web 服务器,或者甚至就是一个片刻就可以创建的简单应用程序。ADO.NET 可以为调用 HttpRuntimeProcessRequest 并传递正确信息的任何模块提供服务。图 3 说明了 ADO.NET HTTP 运行库和其它部分的关系。

图 3 ADO.NET 与整个系统
将 ADO.NET 宿主在应用程序中的第一步就是创建新的应用程序主机。这可以通过调用 ApplicationHost 类的 CreateApplicationHost 静态方法来完成。CreateApplicationHost 在调用方进程中创建新的 AppDomain。之所以需要新的 AppDomain 是因为 ADO.NET 要依靠一些设置,这些设置只能在 AppDomain 级进行设置,并且某些设置只能在创建 AppDomain 之前进行。这些设置的一部分是投影复制缓存位置的应用程序基本路径和目录。CreateApplicationHost 需要一个虚拟文件夹才能工作,这意味着在第一次访问某个新的虚拟文件夹时就会创建一个新主机并随后创建新的 AppDomain。(注意像 Cassini 这样的简单 Web 服务器一次只需要一个虚拟文件夹,但这只是一种特殊情况。)
在创建主机接口对象后,典型的主机应用程序就开始监听请求。如果主机的工作方式与 Web 服务器的相同,它就要通过端口 80 开始监听传入的消息;否则它可以是您指定的任何端口。然后将请求打包到请求类中,并传递给 ADO.NET 运行库。
HttpRuntime.ProcessRequest 方法通过对象的管道路由 Request 对象。在通道末端会出现一个新对象 — 就是动态创建类的一个实例,该类是 Page 类的继承类,该实例表示被请求的 .aspx 页。要结束该请求,HTTP 运行库要调用 Page 类的ProcessRequest 方法。该页的 ProcessRequest 方法执行 Page 对象的大量任务,每项任务都以事件作为信号。通过利用 runat=server 属性集来为该页的每个组成元素创建服务器控件实例,对该页进行首次初始化。接着,ADO.NET 代码加载该页的视图状态,并将它与发送的数据(如文本框和复选框的值)合并。最后,运行库执行客户请求的任何服务器代码(大部分为回发事件),保存视图状态,并将 HTML 写到输出编写器中。
CreateApplicationHost 静态方法是 ApplicationHost 类的唯一成员。它的 C# 原型如下所示:
public static object CreateApplicationHost( Type hostType, string virtualDir, string physicalDir );
以上代码片断所示的 virtualDir 参数表示所创建的应用域的虚拟目录,而 physicalDir 参数表示此虚拟路径后的文件系统路径 — 被请求的 .aspx 文件必须从该磁盘文件夹为该 Web 应用程序进行加载。这些信息都与域相关,并由 ADO.NET 工厂对象用来创建 HttpApplication 对象 (global.asax) 和网页对象 (.aspx)。
CreateApplicationHost 的第一个参数是类型对象,它的赋值是应用程序定义的主机类的类型。这种方法返回用户提供的类的实例,该类用来连接主机程序的默认 AppDomain 和新近创建的 AppDomain(参见图 4)。主机类型对象是类似服务器应用程序的核心代码和目标 AppDomain(CreateApplicationHost 先前创建)中 ADO.NET HTTP 运行库之间的一种代理。主机可执行程序可能需要配置、启动与终止特定的 Web 应用,因此在 AppDomain 中需要有一个专门的对应部分。反过来,主机类型包括监听 HTTP 请求端口的方法。当请求到达时,将创建 SimpleWorkerRequest 对象并传送给 HttpRuntime。

图 4 默认 AppDomain
请求对象是在同一 AppDomain 中创建的,实际上,该 AppDomain 将为请求提供服务。HttpWorkerRequest 是抽象基类,它定义了 ADO.NET 用来进行处理请求的所有属性和方法。SimpleWorkerRequest 是基类的简单实现,可以为 ADO.NET 运行库提供诸如被请求的 URL 和查询字符串等最低限度的信息。它还可以将 ADO.NET 输出接收到流编写器对象。如果您认为需要更多的预处理功能(如像解析报头和发送的数据),那么您可以扩展 SimpleWorkerRequest 并适当覆盖 HttpWorkerRequest 方法。(查看 Cassini 源代码获得扩展 SimpleWorkerRequest 的类的具体示例。)
有许多东西支持 ADO.NET ËÞÖ÷。我们来编写一些代码以说明如何将 ADO.NET 宿主在一个 Windows 窗体应用程序中。思路是利用 ADO.NET 来为该程序的各种功能生成帮助页。示例应用程序的用户输入关键字,并可以迅速在 HTML 页中显示出来。HTML 页由 ADO.NET 页动态生成,该页对应所需的帮助。
图 5 显示了 Windows 窗体应用程序的核心代码。加载时,窗体初始化了应用程序主机。虚拟根文件夹称为 /dino;其文件位于当前文件夹的 Help 子目录中。注意 /dino 只是一个名称,所以不需要为之创建文件系统目录或 IIS 虚拟文件夹。物理路径必须存在,否则将会抛出“找不到文件”的异常。
主机类型是一个名为 MyAppHost 的类,其源代码如图 6 所示。MyHostClass 只有一个方法,即 CreateHtmlPage。主机类成员的数量和原型完全由您决定。最要紧的是您要设计一个可以反映您想要做的事情的编程接口。在这种情况下,最终目标相当简单 — 只是接受 ASPX 文件,然后转化成 HTML。SimpleWorkerRequest 对象合乎所愿,其构造函数的原型正是所需要的。
第一个参数是要处理的 ASPX 文件的名称。第二个参数是可选的查询字符串。注意,如果您使用命令行参数的 URL,那么真实的文件名将是错误的。(在预处理该请求时,IIS 将 URL 与查询字符串分离开来。)SimpleWorkerRequest 类希望将 URL 和查询作为两个不同的实体进行接收。另外还要注意,必须删除初始的查询字符串字符。
最后,构造函数的第三个参数是可用来缓冲输出文本的流编写器对象。ASPX 页通过 Response(更高级的方法,如 Page.Render)发出的所有内容都会收集在该编写器内。我使得编写器在某个临时 HTML 文件上工作,因此当关闭编写器时就会创建 HTML 文件。最后,嵌入的 WebBrowser ActiveX? 控件显示该页。
您可使用的 ADO.NET 功能没有限制 — ADO.NET HTTP 运行库是驱动力。可以随意地使用配置文件、ADO.NET 适配器、视图状态、输出缓存、XML、HTTP 模块以及任何其它 ADO.NET 和 .NET Framework 所涉及的东西。另外,应用程序与 IIS 的状态无关。您可以保持 IIS 为运行状态或者甚至终止它 — 由示例应用程序宿主的 ADO.NET 运行库将不受影响。(IIS 只是另一种应用程序主机,尽管是一种特别丰富和复杂的主机。)
由于 ADO.NET 受到了保护,因此规定所需的程序集与 ADO.NET 中的完全相同。这意味着必须将包含主机类的程序集同时复制到 ADO.NET 主机应用程序目录和 ADO.NET 应用程序文件夹的 Bin 子目录中。另一种选择是将该程序集放入全局程序集缓存 (Global Assembly Cache,GAC) 中,就像在 Cassini 中一样。原因是两个模块都需要加载该主机类:该主机应用程序模拟 IIS 和被调用的 ADO.NET 辅助进程。两个可执行文件都要在各自的路径中为程序集找到主机类。对于主机应用程序来说,路径就是当前的文件夹;对于 ADO.NET 辅助进程来说,路径是包含 .aspx 页或 GAC 的文件夹的 Bin 子文件夹。正因为要在两个不同的地方才能找到主机类,所以强烈建议您在独立的程序集中对其进行编码。但是,在同一可执行文件中嵌入应用程序和主机类是合法的,并且工作正常。例如,对于示例应用程序,您必须在与可执行文件相同的文件夹 (Folder\Bin\Release directory) 中备份 MyAppHost,第二个备份应该位于 Folder\Bin\Release\Help\Bin 文件夹(Bin 在 ASPX 文件之下,一般的 ADO.NET 应用程序需要这样做)。
能够在用户应用程序中宿主 ADO.NET 可能并不像听起来那么令人兴奋。当然,您可以构建智能帮助系统,设计离线工具来将 Web 站点的网页预编译成 HTML。但是在无 IIS 的情况下,只通过 ADO.NET 宿主可以让您构建运行在本地机上的类似 Web 应用程序的无服务器应用程序吗?
如果您还不相信,那么想一想:如果 ADO.NET 页包含交互控件和回发功能,则将发生什么事情?我们回顾一下示例主机应用程序的这种情况。HTML 页是通过 WebBrowser 控件来显示的 — Microsoft Internet Explorer 的浏览引擎的实例。浏览器总是通过端口 80 转发 HTTP POST 命令来处理链接上的点击。因为应用程序主机在端口 80 上没有激活的监听程序,所以将返回一个“page not found”的错误。您需要在客户端顶部构建一个自定义层来截取并将回发信息重新定向到应用程序主机专用的 Request 对象。换句话说,实际利用 ADO.NET 宿主仅可以呈现静态的、只读网页。顺便说一句,这方面也被证明是最难克服的障碍,我以前在 2000 年 9 月的专栏中提到过。因为这是该难题的主要部分,因此仅通过 ADO.NET 宿主不足以构建真正的、无服务器环境以离线使用 Web 站点。这需要配备一个小型 Web 服务器,服务器能够理解浏览器需要什么。Cassini 示例 Web 服务器是完成这项任务的理想工具。

图 7 启动 Cassini
Cassini Web 服务器是一种名为 cassiniwebserver.exe 的 Windows 窗体应用程序。通过指定要监听的端口号、要监控的虚拟路径以及其背后的物理路径来启动 Web 服务器。通过图 7 中所示的窗体启动 Cassini。注意,Cassini 一次只能处理一个虚拟文件夹,并且只能接收通过 localhost 产生的请求。这正是您在诸如 CD 或 DVD 等离线媒体上部署现有 Web 站点所需要的。Cassini 是一个托管应用程序,它的可执行文件占用不超过 70KB。除了程序 cassiniwebserver.exe,您还需要考虑应用程序主机 DLL — assini.dll。必须要利用下面的命令行指令,将应用程序主机程序集保存在 GAC 中:
gacutil /i Cassini.dll
图 8 列出了构成 Cassini Web 服务器的应用程序主机的所有公共类。所有类都在 cassini.dll 程序集中编译。Server 类是主机可执行文件的入口点。其构造函数创建应用程序主机,将 Host 类指定为主机类型。
void CreateHost() {
m_host = (Host) ApplicationHost.CreateApplicationHost(
typeof(Host), vdirPath, filesPath);
m_host.Configure(this, port, vdirPath, filesPath,_aspnetPath);
}
Server 类的实例在 Cassini 的 AppDomain 中创建,但 Host 对象却属于 AppDomain£¬创建它是为了处理 HTTP 请求。Server 对象还提供了一种公共 API 来启动、停止和配置 Web 服务器。Web 服务器 (cassiniwebserver.exe) 将该对象看作为它与 ADO.NET 后端的唯一接触点。
Host 类表示一个远程对象,并且继承了 MarshalByRefObject。远程对象是一个可从不同的 AppDomain 内调用的对象。Host 类负责打开指定端口上的套接字,并监听传入的数据包。当请求到达并被接收后,该类将创建一个新的 Connection 对象来处理该请求:
Connection conn = new Connection(this, (Socket) acceptedSocket); conn.ProcessOneRequest();
Connection 类收到套接字,开始处理 HTTP 数据包。Connection 对象是主机和实际辅助请求间的主要媒介。它首先创建 Request 对象,然后请求该对象处理负载:
Request req = new Request(host, this); req.Process();
Request 对象派生于 SimpleWorkerRequest。它检查 HTTP 数据包,提取并预处理消息标题,并准备响应缓冲区。完成后,它只是将该请求发送给 ADO.NET HTTP 管道,以实际生成相应的 HTML 响应文本。ADO.NET 管道中的入口点是 HttpRuntime 对象的 ProcessRequest 方法。执行该请求时,ADO.NET 回调 Request 对象的几种方法。特别是,所有通过 Response 对象发送出去的数据都会引起对主机辅助请求的某个公共方法的调用。Cassini 的实际实现是利用 Request 类将这个响应数据传送给 Connection 对象的方法,反过来,该方法将通过打开的套接字和端口路由数据。正是因为这个原因,Connection 对象的实例将被传送给 Request 类的构造函数。
如果您考虑一下 Cassini 的体系结构并将它与图 4 进行比较,那么您一定会认为它是一种常用模式。Cassini 是构建无服务器 ADO.NET 应用程序的合适工具,因为它结合了两个重要功能:能够宿主 ADO.NET 来处理 ASPX 请求与一种简单的、本地 Web 服务器基础结构。利用 Cassini,您不需要自定义查看器顶部运行的代码,从而处理所有网页的回发信息。Cassini 就是看不见的链接,它现在使得在无 IIS 的情况下传送 CD 上的整个 Web 站点成为可能。
当第一次检查 Cassini 时,我采取了下列步骤来检验其有效性。首先,终止 IIS,启动 Cassini 可执行文件,如图 7 所示。注意:为了测试 Cassini,必须首先终止 IIS,否则将发生端口冲突。另外,Cassini 只能处理通过 http://localhost 的调用;您不能利用机器名(如 http://msdnmag)访问本地网页,也不能从远程计算机访问装有 Cassini 的机器上的 Web 页。
启动 Cassini 并终止 IIS 后,我打开了 Internet Explorer 并输入 http://localhost。出乎意料的是,我竟在没有 IIS 的情况下运行了基于 ADO.NET 的 localnet!不言而喻,如果网页包含到 Internet Web 站点的链接,那么,只要 Internet 连接可用,它们就将照常工作。
但是,要成功地将 Web 站点打包到 CD 上,必须执行几项初步的检查。首先,确保网页内的所有链接或者与本地主机相关,或者明确的位于本地主机上。否则,会出现 HTTP 403 错误(禁止访问)。为了离线部署 Web 站点,将所有文件与 Cassini 可执行文件以及 .NET Framework 可重新发布的文件一同复制到存储媒介上。配置安装程序以复制 Web 站点树,并将 cassini.dll 安装到客户机的 GAC 上。另外,应启动 Cassini Web 服务器。还应检查 IIS 的安装程序,如果正在运行,则终止它。
如果您将 Cassini 用于离线的 Web 应用程序,最好完全禁用 IIS。附带说一句,这正是您试图构建 ADO.NET 页时 Web Matrix 所做的事情。Web Matrix 是支持社区的、免费 IDE,是专门为 ADO.NET 应用程序设计的(从 Microsoft 的 http://www.asp.net/webmatrix/download.aspx 处下载)。Web Matrix 带有 Cassini,但是,如果您能提供 IIS 虚拟文件夹,也可以使用 IIS。如果您选择使用 Cassini 作为 Web 服务器,那么 Web Matrix 将发出警告:它正准备终止 IIS。从这点来看,Web Matrix 的行为正如 ADO.NET 主机应该采取的行为。图 9 显示了可以在安装程序中用以终止 IIS、启动 Cassini,并打开本地主机根目录的代码。
从第一个 beta 版的 .NET Framework 开始,System.Web.Hosting 命名空间就可以使用了。但要获得完整的文档还需要一段时间。ADO.NET 可由任一托管可执行文件宿主的事实是一个巨大的进步。只要非托管应用程序可以宿主 CLR,它们也就可以宿主 ADO.NET 运行库。这正是 IIS 用来为 ASPX 请求提供服务的 ADO.NET 辅助进程 spnet_wp.exe 所带来的一切,spnet_wp.exe 是一个 Win32 进程,但是它宿主公共语言运行库,随后又宿主 ADO.NET。另一方面,Cassini 是一种决定性的工具,它允许 ADO.NET 应用程序在无服务器的情况下离线运行。
将您的问题和评论发送给 Dino cutting@microsoft.com。
Dino Esposito 是意大利罗马本部的讲师兼顾问。他是 Building Web Solutions with ASP.NET and ADO.NET 和 Applied XML Programming for .NET 两本书的作者,这两本书均由 Microsoft Press 出版。Dino 大部分时间教授 ADO.NET、ADO.NET 和 XML 方面的课程。可以给 Dino 发送电子邮件 dinoe@wintellect.com。