例子程序例子
当 Microsoft® .NET Framework 第一次发布时,它引入了一个有突破性的 Web 服务框架,那就是 ASMX。设计 ASMX 的目的在于尽可能地简化 Web 服务的开发过程,这样即使您不是 XML 专家,也可以创建并运行 Web 服务。ASMX 是通过隐藏大多数基础 XML 和 Web 服务细节来实现这一点的。与强制开发人员直接处理 SOAP 信封和 Web 服务描述语言 (WSDL) 文件不同,ASMX 引入了自动映射层,从而实现了与传统 .NET 代码的连接。
ASMX 也和流行的 ASP.NET HTTP 管线紧密集成。因此,它具有传统 ASP.NET Web 应用程序的优点,例如,高级的宿主环境和进程模型、可靠的配置和部署选项,以及灵活的扩展性点。结果,ASMX 通常是大多数 Web 服务开发人员的首选。大多数开发人员错误地认为 ASMX 需要 IIS;毕竟,他们所见过的都是这种情况。但事实上,ASMX 在技术上与 IIS 并没有任何依赖关系。
在没有 IIS 的条件下宿主 Web 服务的需要是非常实际的。在某些环境下,可能有各种原因导致无法在必须宿主 Web 服务的计算机上运行 IIS。幸运的是,在没有 IIS 的条件下,您可以在您的进程中宿主 ASMX。自从 .NET Framework 1.0 发布以来,就可以实现这一点,但是您必须提供您的 Web 服务器来接收 HTTP 请求。Cassini 是由 ASP.NET 团队开发的一个示例 Web 服务器,它可以满足这种需要,并允许您在没有 IIS 的条件下运行 ASP 页。然而,对于大多数开发人员来说,编写他们自己的 Web 服务器或者使用诸如 Cassini 的示例 Web 服务器都是不合理的。
自从 Windows Server™ 2003 和 Windows® XP SP2 发布以后,出现了一个新的 HTTP 协议栈,名为 http.sys。通过 http.sys 和 .NET Framework 2.0 中的一些新托管类(特别是 HttpListener),您就可以轻松地为您的应用程序构建 Web 服务器,而无需在计算机上安装 IIS。这些进展使得在任何环境中运行 ASMX 成为可能。请注意,.NET Framework 2.0 当前只是测试版,因此还会有所改动。
ASP.NET HTTP 体系结构
ASP.NET 专门设计为避免依赖于 IIS。基础体系结构是由共同处理传入的 HTTP 消息的 .NET 类构成的一条管线。它被看作管线的原因是每个 HTTP 请求都要经过一系列对象,每个对象执行一些处理。
HttpRuntime 类位于管线的前端,负责启动进程。当调用 HttpRuntime 类的静态 ProcessRequest 方法时,管线开始执行。ProcessRequest 带有一个 HttpWorkerRequest 对象,该对象包含当前请求的所有信息。HttpRuntime 使用 HttpWorkerRequest 中的信息来填充 HttpContext 对象。然后它实例化适当的 HttpApplication 类,这个类会调用注册到应用程序的任何 IHttpModule 实现以用于预处理或后期处理。此时会识别、实例化和调用适当的 IHttpHandler 实现。
每个进入管线的 HTTP 请求都会发生这个过程。所有 ASP.NET 功能(包括 ASMX 的功能)都包含在这些管线类中。例如,当请求到达 System.Web.Services.Protocols.WebServiceHandlerFactory 类时,就开始支持对 ASMX 终结点的处理,该类负责识别、编译(如果需要)和实例化标识的 ASMX 类,以及调用传入的 SOAP 消息的目标 WebMethod。
图 1 HTTP管线和 Web 服务器
管线是完全自治的,与 IIS 相互独立。甚至当与 IIS 一起使用时,也是在与 inetinfo.exe 独立的进程中运行的。这个进程的名称取决于主机 OS(在 Windows XP 上为 aspnet_wp.exe,在 Windows Server 2003 上为 w3wp.exe)。除了有自己的进程模型外,管线也有独立的配置方式,与 IIS 元数据库是分开的。管线唯一没有的就是可用来接收传入的 HTTP 请求的 Web 服务器。您仍需要一些能够侦听传入的 HTTP 消息的组件,如 IIS 5.0 或 http.sys。即使是这样,这些组件也只是负责接收 HTTP 请求并将它们交给 ASP.NET 管线,这以后的任何事情都要由它来处理(请参见图 1)。
一旦该请求使其进入辅助进程,辅助进程就会创建 HttpWorkerRequest 对象(表示传入的请求)并调用 HttpRuntime.ProcessRequest 来启动管线。由于有了这样合理的设计,您就可以直接在自己的应用程序中调用 HttpRuntime。
宿主 HTTP 管线
宿主 ASP.NET 所需要的类可以在 System.Web 和 System.Web.Hosting 命名空间中找到。开始时需要用到的类主要有 ApplicationHost、HttpRuntime 和一个从 HttpWorkerRequest 派生的类。首先调用 ApplicationHost.CreateApplicationHost。这个方法新创建一个可以处理 ASP.NET 请求的应用程序域 (AppDomain)。由于您要显式创建 AppDomain,因此在调用时必须指定虚拟目录和相应的物理目录。
除了创建新的 AppDomain 以外,CreateApplicationHost 还在这个新的 AppDomain 中实例化了一个对象,您可以通过这个对象进行通讯。当进行该方法调用时,您要指定要让它实例化的类型。由于该对象将跨 AppDomain 边界使用,因此它必须从 MarshalByRefObject 派生。您可能想要使用自己的类,它具有与 AppDomain 交互所需要的方法。例如,至少您想要一个 ProcessRequest 方法,它可以提交新的 ASP.NET 请求以进行处理。
这里有一个类可用来实现该目的:
public class MySimpleHost : MarshalByRefObject { public void ProcessRequest(string file) { ... // use the ASP.NET HTTP pipeline to process request } }
在本例中,ProcessRequest 接受要处理的页面的文件名。在 ProcessRequest 中,您可以使用 HttpRuntime 来启动管线处理。HttpRuntime 有一个静态方法,名称也叫做 ProcessRequest,它带有一个 HttpWorkerRequest 类型的参数。
HttpWorkerRequest 是一个抽象类,但幸运的是,.NET 附带了一个简单的、名为 SimpleWorkerRequest 的派生类,它旨在处理简单的 HTTP GET 请求。当您实例化 SimpleWorkerRequest 时,必须指定要处理的页面的名称、一个可选的查询字符串和一个 TextWriter(管线将输出写入其中)。一旦您拥有 HttpWorkerRequest 对象,您就可以通过调用 ProcessRequest 来调用管线,如下所示:
... // MySimpleHost.ProcessRequest SimpleWorkerRequest swr = new SimpleWorkerRequest(page, null, Console.Out); HttpRuntime.ProcessRequest(swr);
对于 MySimpleHost,您需要在宿主应用程序中调用 ApplicationHost.CreateApplicationHost 来实例化这个对象。然后,可以使用 MySimpleHost.ProcessRequest 将请求发送给 HTTP 管线进行处理,如以下代码片段所示:
... // console host application MySimpleHost msh = (MySimpleHost) ApplicationHost.CreateApplicationHost(typeof(MySimpleHost), "/", Directory.GetCurrentDirectory()); foreach (string page in args) msh.ProcessRequest(page);
ApplicationHost.CreateApplicationHost 的实现期望在以下两个位置之一找到指定类型的程序集:在全局程序集缓存 (GAC) 中,或者在指定物理目录的 bin 目录中。由于没有文档化的方式可以更改这种重新实现 CreateApplicationHost 的行为缺陷,因此根据您的项目配置和部署方案,可能需要在其中的一个位置安装程序集。
图 2 包含宿主 ASP.NET 的整个控制台应用程序的代码。该示例可以下载。您在命令行中指定 ASP.NET 文件的名称以及一个可选的查询字符串。然后该程序会通过调用 MySimpleHost.ProcessRequest 将它们传递给管线。
在本专栏的下载中,我提供了几个 ASP.NET 文件以供您试用,其中包括一个名为 math.asmx 的文件。当您运行应用程序时,请在命令行中指定“math.asmx WSDL”,您会看见打印在控制台窗口中的、由 ASMX 生成的 WSDL 定义(等价于通过宿主在 IIS 中的 math.asmx 浏览 http://host/math.asmx?WSDL)。如果您只在命令行中指定“math.asmx”,则它会打印由 ASMX 生成的可读的文档页面。
这明显是个不实际的例子,因为您必须在命令行中指定 ASP.NET 页。在实际应用中,这个信息会通过 HTTP 请求传入。为了支持 HTTP,您需要在应用程序中集成一个 Web 服务器。
别了,IIS
Http.sys 是一个新的低级 HTTP 协议栈,可在 Windows Server 2003 和 Windows XP SP2 中使用。Http.sys 是一个内核模式组件,它为计算机中的所有应用程序提供它的 HTTP 服务。这意味着 HTTP 支持深深依赖于 OS。甚至 IIS 6.0 也进行了重新架构,以便可以使用 http.sys(请参见图 3)。
图 3 Http.sys 体系结构
在 6.0 版之前,IIS 依靠 TCP/IP 内核和 Windows Sockets API (Winsock) 接收 HTTP 请求。由于 Winsock 是一个用户模式组件,因此每个接收操作都需要在内核模式和用户模式之间进行切换。现在 Http.sys 可以直接在内核中缓存响应。当处理缓存的响应时,将 HTTP 栈放在内核中可以使得移除代价昂贵的上下文切换成为可能,从而提高效率和整体吞吐量。
当 http.sys 接收到请求时,它可以直接将该请求转发到正确的辅助进程中。另外,如果辅助进程无法接受该请求,http.sys 会存储该请求,直到辅助进程启动并可以接受它为止。这意味着辅助进程失败不会中断服务。当 IIS 6.0 启动时,WWW 服务会与 http.sys 进行通讯,并为配置的每个 IIS 应用程序注册路由信息。无论您何时在 IIS 中创建应用程序或移除应用程序时,WWW 服务都会与 http.sys 进行通讯以更新它的路由信息。
正如您在图 3 中所看到的,http.sys 为 IIS 6.0 Web 体系结构奠定了基础,但它没有以任何方式与 IIS 产生联系。运行在计算机中的任何应用程序都可以利用 http.sys 来接收 HTTP 请求。与 WWW 服务相似,您可以用 http.sys 注册应用程序,并开始侦听传入的 HTTP 请求。.NET Framework 2.0 引入了一套托管类,使得这些托管类可以很容易地实现该操作。
HttpListener:实现您自己的 Web 服务器
System.Net 包含几个用来与 http.sys 进行交互的新类。HttpListener 是这些类中的关键一个。可以使用它来创建简单的 Web 服务器(或侦听器),用于响应传入的 HTTP 请求。这个侦听器在 HttpListener 对象的生存期内都保持活动状态,不过您可以通过命令通知它开始和停止侦听。
要使用 HttpListener,必须先对它进行实例化。然后通过向 Prefixes 属性中添加 URL 前缀,来指示侦听器应该处理哪些 HTTP URL。每个 URI 必须包含一个方案(“http”或“https”)、主机、端口(可选)和路径(可选)。每个前缀都必须以正斜杠结尾:
HttpListener listener = new HttpListener(); listener.Prefixes.Add("http://localhost:8081/foo/"); listener.Prefixes.Add("http://127.0.0.1:8081/foo/"); listener.Start();
对于一个特定的 URL 前缀,只能有一个 HttpListener 在侦听它。如果您试图添加副本,就会得到 Win32Exception 异常。当您指定一个端口时,可以将主机名替换为“*”,以指示侦听器应该处理具有该端口的所有 URI,除非另一个 HttpListener 与它们匹配。或者可以将主机名替换为“+”,以指示侦听器接受对指定端口的所有请求,如下所示:
HttpListener listener = new HttpListener(); listener.Prefixes.Add("http://+:8081/"); listener.Start();
您也可以通过 AuthenticationScheme 属性指定侦听器所使用的身份验证方案。HttpListener 支持匿名、基本、简要和 Windows 身份验证。它也支持安全套接字层 (SSL) 连接,因此可以通过 HTTPS 安全地使用基本身份验证,如下所示:
HttpListener listener = new HttpListener(); listener.AuthenticationSchemes = AuthenticationSchemes.Basic; listener.Prefixes.Add("https://+:8081/"); listener.Start();
正如您刚刚看到的,一旦指定了侦听器要处理的 URI 前缀,就必须调用 Start 方法(请注意:在调用 Start 之前必须至少添加一个前缀)。Start 并没有真正做什么重要的事情 — 它只是简单地准备侦听器对象以开始接收请求。
要接收请求,就必须调用 GetContext,如下所示:
HttpListenerContext ctx = listener.GetContext();
GetContext 是一个同步调用,它在等待传入的请求到达时阻塞。直到接收到一个请求时,它才会返回。HttpListener 也通过 BeginGetContext 和 EndGetContext 提供异步接收请求机制。GetContext 和 EndGetContext 返回一个 HttpListenerContext 对象,该对象表示接收到的 HTTP 请求。
您可以使用 HttpListener 将自己的 Web 服务器与如图 2 所示的示例应用程序相集成。您需要做的只是在您的代码中将请求从 HttpListener 转发到 HTTP 管线。您可以添加一个循环,该循环不断地调用 GetContext,并使用返回的 HttpListenerContext 对象中的信息来调用 ProcessRequest。然后使用 HttpListenerContext 类来查找请求的文件名(使用 Request.Url.LocalPath 属性,如“/math.asmx”)和查询字符串(使用 Request.Url.Query 属性,如“?WSDL”)。使用该代码,您就可以通过 HTTP 请求 ASP.NET 页,如图 4 所示。
做出这些更改并运行程序之后,控制台应用程序就开始等待 GetContext 返回。现在您可以打开一个 Web 浏览器,并定位到注册的 URI(例如,http://localhost:8081/math.asmx)。这个操作完成后,GetContext 就会返回并将请求递交给 HTTP 管线。然后应该查看一下写入控制台窗口的响应,如前面所做的。
HttpListenerContext与 HttpContext 类似,它提供 Request、Response 和 User 属性,使得与 HTTP 消息的所有方面进行交互变得很容易。Request 属性的类型为 HttpListenerRequest,而 Response 属性的类型为 HttpListenerResponse。HttpListenerRequest 用于访问请求的 HTTP 方法、URL、标头、用户代理、主体及其他部分。HttpListenerResponse 用于将 HTTP 响应写回到客户端。User 属性返回 IPrincipal,它带有关于通过身份验证的用户的信息。
您可以使用这些属性进一步扩展这个示例,以便它能够直接写入 HTTP 响应流。可以这样来实现:修改 MySimpleHost.ProcessRequest 的签名,以使其接受一个 TextWriter。完成后,可以将 HttpListenerContext.Response.OutputStream 包装在 StreamWriter 对象中并将它传入。
图 6 浏览器输出
图 5 提供了修改后的示例的完整代码,它集成了一个 HttpListener。现在运行示例时,就可以使用浏览器来读取并定位由 ASMX 生成的文档页面(请参见图 6)。每次应用程序接收到一个请求时,控制台窗口中都会打印出一条消息(请参见图 7)。
图 7 控制台应用程序输出
这个示例演示了一个可以处理 HTTP GET 请求和查询字符串的宿主模型。SimpleWorkerRequest 只是为处理这种简单情况而设计的,它不能处理更加高级的 POST 操作。因此,这个示例无法完全宿主 ASMX 终结点,ASMX 终结点需要有 POST 支持来处理传入的 SOAP 请求。
宿主ASMX
要完全支持 ASMX 终结点,您需要一个知道如何处理请求/响应流的自定义 HttpWorkerRequest。它应该基于从 GetContext 获得的 HttpListenerContext 对象。这是个很棘手的任务,因为 HttpWorkerRequest 非常庞大,而且文档没有完全标准化。因此,我提供了一个名为 HttpListenerWorkerRequest 的示例实现(如图 8 所示)。
此时,您可能会尝试回到前面的示例,将 SimpleWorkerRequest 的所有实例替换为 HttpListenerWorkerRequest。然而,这样做需要您将 HttpListenerContext 对象传入 ProcessRequest。遗憾的是,HttpListenerContext 不是从 MarshalByRefObject 派生的,从而阻止它跨 AppDomain 边界传递。要实现此目的,就需要重新设计这个示例。
首先,需要一个包装 HttpListener 对象的类,并使它可以跨 AppDomain 进行控制。我在图 9 中提供了一个名为 HttpListenerWrapper 的类。这就是从现在起要在调用 CreateApplicationHost 时指定的类型。它有一个 Configure 方法,该方法可以实例化包含的 HttpListener 对象,并注册所提供的 URI 前缀。它具有 Start 和 Stop 方法,这两个方法简单委托给侦听器。它还有一个 ProcessRequest 方法来处理其他任何事情 — 调用 GetContext、实例化新的 HttpListenerWorkerRequest、并将其传入处理该请求的 HttpRuntime.ProcessRequest。您可以在宿主应用程序中使用以下类:
HttpListenerWrapper listener = (HttpListenerWrapper)ApplicationHost.CreateApplicationHost( typeof(HttpListenerWrapper), "/", Directory.GetCurrentDirectory()); listener.Configure(prefixes, "/", Directory.GetCurrentDirectory()); listener.Start(); while (true) listener.ProcessRequest();
现在您就有了一个用于 ASMX 终结点的全功能宿主。提供的下载中还有另外一个完整的示例,它演示了这段代码的操作。现在,当控制台应用程序运行时,就应该能够使用 SOAP 或者通过 HTML 文档页面提供的窗体调用 ASMX WebMethods。
宿主模型:桌面上的 Web 服务
既然您知道了如何在选择的进程中宿主 ASMX,现在您大概最想知道应该在什么时候、什么地方使用这个技术。在一台特殊的计算机上,由于各种情况而无法运行 IIS,不过您可能仍然想使用 ASMX 编程模型,以便在该节点上宿主 Web 服务。如果是这种情况,本专栏所讨论的技术就是一个很好的选择。
与 inetinfo.exe 不同(它提供许多用于不同通信类型的服务),http.sys 是一个简化的内核,只对 HTTP 流量进行处理。因此它的攻击面减少了。假如您在计算机上安装了 Windows XP SP2 或 Windows Server 2003,您就可以在没有 IIS 的条件下,在自己的进程中宿主 ASMX。这意味着您可以根据自己的具体需要,在控制台应用程序、Windows 窗体应用程序或 Windows NT® 服务中宿主 ASMX。
然而,我应该指出,当您采用这种方法时,您就放弃了 IIS 通过 w3wp.exe(或 aspnet_wp.exe)辅助进程提供的高级进程模型。这意味着您失去了进程管理(启动、失败检测、回收)、线程池管理和 ISAPI 支持等一些功能。当您宿主 ASMX 时,您就提供了这个进程,因此您要负责提供进程模型和相关的服务。
图 10 桌面上的 Web 服务
在自己的进程中宿主 ASMX 时,最引人注意的情况也许就是需要在桌面上运行 Web 服务的情况。例如,您可能有一个 Windows 窗体应用程序,它需要从一个 Web 服务器或其他某个内部企业范围的 Windows 服务接收通知(请参见图 10)。在桌面上,您不需要像在服务器上那样有一个高级的进程模型,但是利用 ASMX 的有效编程模型还是有好处的。Http.sys 和 ASMX 宿主很适合于这种情况。我在下载中提供了其他一些示例(宿主 ASMX 的一个 Windows 窗体应用程序和一个 Windows 服务)来阐明这些概念。
我们所处的位置
我介绍了在自己选择的进程中宿主 ASP.NET HTTP 管线的基本知识。同时还讨论了如何利用 http.sys 和新的 HttpListener 托管类(和相关的类)来为您的应用程序构建 Web 服务器。HttpListener 使得接收 HTTP 消息并将它们转发给 ASP.NET 页面以进行处理变得很容易。使用这些技术,您可以很灵活地在任何地方运行 ASMX。
Figure 2 Simple ASP.NET Host
Using System; using System.Web; using System.Web.Hosting; using System.IO; ... class Program { static void Main(string[] args) { if (args.Length == 0) { Console.WriteLine( "Usage: simplehost filename [querystring]"); return; } string file = args[0]; string query = (args.Length > 1) ? args[1] : ""; MySimpleHost msh = (MySimpleHost) ApplicationHost.CreateApplicationHost( typeof(MySimpleHost), "/", Directory.GetCurrentDirectory()); msh.ProcessRequest(file, query); } } public class MySimpleHost : MarshalByRefObject { public void ProcessRequest(string file, string query) { SimpleWorkerRequest swr = new SimpleWorkerRequest(file, query, Console.Out); HttpRuntime.ProcessRequest(swr); } }
MySimpleHost msh = (MySimpleHost) ApplicationHost.CreateApplicationHost( typeof(MySimpleHost), "/", Directory.GetCurrentDirectory()); HttpListener listener = new HttpListener(); listener.Prefixes.Add("http://localhost:8081/"); listener.Prefixes.Add("http://127.0.0.1:8081/"); listener.Start(); while (true) { HttpListenerContext ctx = listener.GetContext(); string file = ctx.Request.Url.LocalPath.Replace("/", ""); string query = ctx.Request.Url.Query.Replace("?", ""); msh.ProcessRequest(file, query); }
using System; using System.Web; using System.Web.Hosting; using System.Net; using System.IO; ... class Program { static void Main(string[] args) { MySimpleHost msh = (MySimpleHost) ApplicationHost.CreateApplicationHost( typeof(MySimpleHost), "/", Directory.GetCurrentDirectory()); HttpListener listener = new HttpListener(); listener.Prefixes.Add("http://localhost:8081/"); listener.Prefixes.Add("http://127.0.0.1:8081/"); listener.Start(); Console.WriteLine( "Listening for requests on http://localhost:8081/"); while (true) { HttpListenerContext ctx = listener.GetContext(); string page = ctx.Request.Url.LocalPath.Replace("/", ""); string query = ctx.Request.Url.Query.Replace("?", ""); Console.WriteLine("Received request for {0}?{1}", page, query); StreamWriter sw = new StreamWriter(ctx.Response.OutputStream); msh.ProcessRequest(page, query, sw); sw.Flush(); ctx.Response.Close(); } } } public class MySimpleHost : MarshalByRefObject { public void ProcessRequest(string p, string q, TextWriter tw) { SimpleWorkerRequest swr = new SimpleWorkerRequest(p, q, tw); HttpRuntime.ProcessRequest(swr); } }
public class HttpListenerWorkerRequest : HttpWorkerRequest { private HttpListenerContext _context; private string _virtualDir; private string _physicalDir; public HttpListenerWorkerRequest( HttpListenerContext context, string vdir, string pdir) { if (null == context) throw new ArgumentNullException("context"); if (null == vdir || vdir.Equals("")) throw new ArgumentException("vdir"); if (null == pdir || pdir.Equals("")) throw new ArgumentException("pdir"); _context = context; _virtualDir = vdir; _physicalDir = pdir; } // required overrides (abstract) public override void EndOfRequest() { _context.Response.OutputStream.Close(); _context.Response.Close(); _context.Close(); } public override void FlushResponse(bool finalFlush) { _context.Response.OutputStream.Flush(); } public override string GetHttpVerbName() { return _context.Request.HttpMethod; } public override string GetHttpVersion() { return string.Format("HTTP/{0}.{1}", _context.Request.ProtocolVersion.Major, _context.Request.ProtocolVersion.Minor); } public override string GetLocalAddress() { return _context.Request.LocalEndPoint.Address.ToString(); } public override int GetLocalPort() { return _context.Request.LocalEndPoint.Port; } public override string GetQueryString() { string queryString = ""; string rawUrl = _context.Request.RawUrl; int index = rawUrl.IndexOf('?'); if (index != -1) queryString = rawUrl.Substring(index + 1); return queryString; } public override string GetRawUrl() { return _context.Request.RawUrl; } public override string GetRemoteAddress() { return _context.Request.RemoteEndPoint.Address.ToString(); } public override int GetRemotePort() { return _context.Request.RemoteEndPoint.Port; } public override string GetUriPath() { return _context.Request.Url.LocalPath; } // remaining methods omitted // download sample for more details ... }
public class HttpListenerWrapper : MarshalByRefObject { private HttpListener _listener; private string _virtualDir; private string _physicalDir; public void Configure(string[] prefixes, string v, string p) { _virtualDir = v; _physicalDir = p; _listener = new HttpListener(); foreach (string prefix in prefixes) _listener.Prefixes.Add(prefix); } public void Start() { _listener.Start(); } public void Stop() { _listener.Stop(); } public void ProcessRequest() { HttpListenerContext ctx = _listener.GetContext(); HttpListenerWorkerRequest workerRequest = new HttpListenerWorkerRequest(ctx, _virtualDir, _physicalDir); HttpRuntime.ProcessRequest(workerRequest); } }