Summary
ASP.NET Web Service 方法( WebMethods )怎样为创建 Web 服务提供一种高效的解决方案呢。 WebMethods 使传统的 Microsoft.NET 方法成为 Web 服务操作,它支持 HTTP 、 XML 、 XML Schema 、 SOAP 和 WSDL 。 WebMethods( .asmx )句柄将到来的 SOAP 消息派送给适当的方法,并将到来的 XML 元素串行化为对应的 .NET 对象。
Introduction
当今在 Microsoft.NET 中实现基于 HTTP 的 Web 服务有两种根本不同的方法。第一种也是较低级的一种技术是编写一个定制的 IhttpHandler 类并把它嵌入到 HTTP 管道中。这种方法要求你使用 System.web API 处理到来的 HTTP 消息,用 System.Xml API 处理 HTTP 消息体中的 SOAP 封装( envelope )。编写一个定制的句柄同样也需要你手工编写WSDL 文档,准确的描述实现过程。做到这一切要求对 XML 、 XSD 、 SOAP 和 WSDL 规范有深入的了解,但这对大多数人来讲都是让人望而却步的先决条件。
实现 Web 服务的一种更高效的方法是使用 Microsoft ASP.NET 的 WebMethods 框架。 ASP.NET 为 .asmx 终节点(叫作 WebServiceHandler )装载了一个专门的 IhttpHandler 类,它为你的需要提供了 XML 、 XSD 、 SOAP 和 WSDL的功能性样板。因为 WebMethod 框架使你从底层 XML 技术的复杂性中解脱出来,可以将精力集中到一些紧要的业务问题。
选择一种实现技术涉及到在灵活性和高效性之间的权衡,如图 1 所示。一个定制的 IhttpHandler 有很大的灵活性,但却要花费大量的时间来编写、测试和调试代码。 WebMethods 框架使构建和运行 Web 服务变得异常轻松,不过很明显你将被限制在框架的界线里。不过,如果 WebMethods 框架不能正确的满足你的需要,也可通过填加你自己的功能来扩展框架。
总的来讲,除非你已经掌握了 XML 、 XSD 、 SOAP 和 WSDL 并且愿意承受直接处理它们的负担,最好还是使用WebMethods 框架来实现你的 Web 服务需要。它提供了大多数 Web 服务终点需要的基本服务,还有一些扩展属性使构架更适合你的具体需要。基于此假设,文章的余下部分我们将讨论 WebMethods 的内部工作机制。
WebMethods 框架
WebMethods 框架通过在方法开始处标记 [WebMethods] 属性,将 SOAP 消息映射到一个 .NET 类的方法,[WebMethods] 可以在 System.Web.Service 名称空间中找到。比如下面的 .NET 类包括四个方法,其中的两个被标注了[WebMethods] 属性。
using System.Web.Services;
public class MathService
{
[WebMethod]
public double Add(double x, double y) {
return x + y;
}
[WebMethod]
public double Subtract(double x, double y) {
return x - y;
}
public double Multiply(double x, double y) {
return x * y;
}
public double Divide(double x, double y) {
return x / y;
}
}
要在 WebMethods 框架中使用这个类,需要把它编译成一个 assembly 并拷贝到虚拟目录的 bin 子目录下。在这个例子中, Add 和 Subtract 方法被作为 Web 服务操作,而 Multiply 和 Divide 却不能。(因为它们没有被标记为[WebMethods] )
你可以通过一个 .asmx 终节点来访问 Add 和 Subtract Web 服务操作:创建一个文本文件 Math.asmx ,它包含下面的简单声明,然后把它放到包含 assembly 的同一个虚拟目录下(注这里是虚拟目录本身,而不是它的 bin 子目录)
<%@ WebService class="MathService"%>
这个声明告诉 .asmx 句柄去哪个类中查找 WebMethods ,余下的就由句柄全全处理。比如,假设虚拟目录叫作“math ”,它包含了 Math.asmx ,它的 bin 子目录下包含了 assembly ,浏览 http://localhost/math/math.asmx 时 .asmx句柄将生成 文档。
关于 .asmx 句柄如何工作有一个主要的变化。 .asmx 文件通常只包含了 WebService 的声明,根据名字引用相应的Web 服务类。因此,在这种情况下, assembly 必须已经被编译并且部署到虚拟目录的 bin 子目录中。 .asmx 句柄也提供了对 .asmx 文件源代码的即时编译( just-in-time compilation ),比如下面的文件就既包括了 WebService 声明,也包括了引用类的源代码。
<@% WebService class="MathServiceJit" language="C#"%>using System.Web.Services;
public class MathServiceJit
{
[WebMethod]
public double Add(double x, double y) {
return x + y;
}
[WebMethod]
public double Subtract(double x, double y) {
return x - y;
}
public double Multiply(double x, double y) {
return x * y;
}
public double Divide(double x, double y) {
return x / y;
}
}
当此文件通过 HTTP 被第一次访问时, .asmx 句柄编译源码并将 assembly 部署到相应位置。注意 WebService 声明必须提供语言以使 .asmx 句柄在运行时能选择正确的编译器。这种方法明显的劣势就是直到第一次访问这个文件的时候你才会发现它的编译错误。
当你在 Visual Studio.NET 中创建一个新的 Web Service 项目时,通常使用“双文件”技术,即类的源文件和引用它的 .asmx 文件是分开的。 IDE 会尽量屏蔽这些,如果你在 Solution Explorer 工具栏中点击 Show All Files ,你会注意到项目中 Web Service 类都有两个文件。事实上 Visual Studio.NET 不支持 .asmx 文件的 syntax highlighting or IntelliSense® 。对于 Web 项目, Visual Studio.NET 也负责创建一个虚拟目录,自动地将编译好的 assembly 放到虚拟目录的 bin 子目录下。
在我们详细讨论 .asmx 句柄如何工作之前,先来简单的看一下消息是怎样从 IIS 传递到 .asmx 句柄的。当一个HTTP 消息到达 80 端口后, IIS 用在 IIS 元数据库中找到的信息决定由哪个 ISAPI.DLL 来处理消息。 .NET 安装时将.asmx 扩展名映射到 Aspnet_isapi.dll 。
Aspnet_isapi.dll 是 .NET 框架提供的标准的 ISAPI 扩展名,它只是简单的将 HTTP 请求传递到一个单独的工作者进程 Aspnet_wp.exe 。 Aspnet_wp.exe hosts CLR (通用语言运行时)和 HTTP 管道。消息一旦进入了 HTTP 管道,管道就查找配置文件看哪个 IhttpHandler 类用来处理给定的扩展名。如果你查看你的 machine.config 文件,会看到它包含了一个映射到 .asmx 文件的 httphandler ,如下所示:
<configuration><system.web>
<httpHandlers>
<add verb="*" path="*.asmx" type="System.Web.Services.Protocols.WebServiceHandlerFactory,
System.Web.Services, Version=1.0.3300.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a" validate="false"/>
当一个访问 .asmx 文件的消息进入 .NET HTTP 管道时,管道就会调用 WebServiceHandlerFactory 类来实例化一个新的 WebServiceHandler 对象,用它来处理请求(通过调用 IhttpHandlerProcessRequest 方法)。 WebServiceHandler 对象打开物理的 .asmx 文件来决定包含你的 WebMethods 的类名。
HTTP管道一旦调用了.asmx句柄,便开始了XML、XSD、SOAP和WSDL的处理。.asmx句柄提供的余下的功能被分为三个领域:
消息分派
当.asmx句柄被HTTP管道调用时,通过查看.asmx文件中的WebService声明,确定检查哪个.NET类。然后它观察到来的HTTP消息中的信息,确定调用引用类中的哪个方法。为了调用前面例子中的Add方法,HTTP请求消息应像下面一样:
POST /math/math.asmx HTTP/1.1
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "http://tempuri.org/Add"
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<Add xmlns="http://tempuri.org/">
<x>33</x>
<y>66</y>
</Add>
</soap:Body>
</soap:Envelope>
上面的HTTP请求消息中有两条信息可以用来确定调用类中的哪个方法:SOAPAction头或soap体中请求元素的名字。在这个例子中,每种方法都指出了发送者想调用的方法名。
.asmx句柄使用SOAPAction头的值来实现消息的分派。因此,.asmx句柄查看消息中的SOAPAction头,使用.NET映射检查引用类中的方法。它只考虑标记了[WebMethod]属性的方法,但通过查看每种方法的SOAPAction值再具体确定调用哪个方法。因为我们在类中并没有明确的指定SOAPAction的值,.asmx句柄认为SOAPAction的值是Web服务的名称空间加上方法名。而且我们也没有指定名称空间,所以句柄就把http://tempuri.org作为默认值。这样Add方法的默认SOAPAction值就是http://tempuri.org/Add。
可以按如下方法定制Web服务的名称空间。把类标记上[WebService]属性,用[SoapDocumentMethod]属性标记WebMethods来指定具体的SOAPAction值。示例如下:
using System.Web.Services;
using System.Web.Services.Protocols;
[WebService(Namespace="http://example.org/math")]
public class MathService
{
[WebMethod]
public double Add(double x, double y) {
return x + y;
}
[WebMethod]
[SoapDocumentMethod(Action="urn:math:subtract")]
public double Subtract(double x, double y) {
return x - y;
}
...
}
现在.asmx句柄认为Add方法的SOAPAction值是http://example.org/math/Add,SubTract的值是urn:math:subtract(因为我们在类中明确定义了)。比如下面的HTTP请求消息调用Subtract:
POST /math/math.asmx HTTP/1.1
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "urn:math:subtract"
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<Subtract xmlns="http://example.org/math">
<x>33</x>
<y>66</y>
</Subtract>
</soap:Body>
</soap:Envelope>
如果.asmx 句柄没为HTTP请求消息找到一个SOAPAction匹配,将会抛出一个异常。如果你不想依赖SOAPAction头来分派消息,可以引导.asmx句柄使用请求元素名称。采用这种方法需要为类标记上[SoapDocumentService]属性的RoutingStyle特性,同时也应该指出 WebMethods不需要SOAPAction值(在类中设定其值为空)。如下所示:
using System.Web.Services;
using System.Web.Services.Protocols;
[WebService(Namespace="http://example.org/math")]
[SoapDocumentService(
RoutingStyle=SoapServiceRoutingStyle.RequestElement)]
public class MathService
{
[WebMethod]
[SoapDocumentMethod(Action="")]
public double Add(double x, double y) {
return x + y;
}
[WebMethod]
[SoapDocumentMethod(Action="")]
public double Subtract(double x, double y) {
return x - y;
}
...
}
在这种情况下,句柄甚至不关心SOAPAction的值,它使用请求元素的名字确定调用方法。比如,在下面的HTTP请求消息中,它希望调用Add方法的请求元素的名字是Add:
POST /math/math.asmx HTTP/1.1
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: ""
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<Add xmlns="http://example.org/math">
<x>33</x>
<y>66</y>
</Add>
</soap:Body>
</soap:Envelope>
所以当.asmx句柄接收到HTTP消息时它要做的第一件事情就是决定如何分派消息到对应的WebMethod。在它真正调用方法之前,还需要将到来的XML映射到.NET对象。
将XML映射到对象
一旦WebMethod句柄决定了调用哪个方法,它就会将XML消息反串行化为.NET对象。随着消息分派,句柄通过reflection检查类,然后决定怎样处理XML消息。XmlSerializer类自动完成XML和System.Xml.Serialization名称空间中类的映射。
XmlSerializer能实现任何.NET公共类型到XML Schema类型的映射,有了这个适当的映射,它能自动的实现.NET对象和XML实例文档的映射(见图4)。XmlSerializer受XML Schema所支持功能的限制,虽不能处理所有复杂的现代对象模型(如非树型的对象图),却能处理开发者常用的复杂类型。
再看前面Add的例子,XmlSerializer将把x和y元素映射为.NET的double值(调用Add方法时必须提供的)。Add方法返回一个double类型值给调用者,这也需要被串行化为SOAP应答消息中的一个XML元素。
Figure 4. Mapping XML to objects
XmlSerializer也能自动处理一些复杂类型(除了上面说到的一些限制)。比如,下面的WebMethod计算两个点结构之间的距离。
using System;
using System.Web.Services;
public class Point {
public double x;
public double y;
}
[WebService(Namespace="urn:geometry")]
public class Geometry {
[WebMethod]
public double Distance(Point orig, Point dest) {
return Math.Sqrt(Math.Pow(orig.x-dest.x, 2) +
Math.Pow(orig.y-dest.y, 2));
}
}
请求此操作的SOAP消息将包含一个Distance元素,它包含了两个子元素,一个称作orig,另一个是dest,每一个都包括了x和y元素,如下所示:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<Distance xmlns="urn:geometry">
<orig>
<x>0</x>
<y>0</y>
</orig>
<dest>
<x>3</x>
<y>4</y>
</dest>
</Distance>
</soap:Body>
</soap:Envelope>
这种情况下SOAP应答消息将包含一个DistanceResponse元素,它包含一个double 类型的DistanceResult子元素。
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<DistanceResponse
xmlns="urn:geometry">
<DistanceResult>5</DistanceResult>
</DistanceResponse>
</soap:Body>
</soap:Envelope>
缺省的XML映射使用方法名作为请求元素名,参数名作为子元素名。每个参数的结构依赖于类型的结构。公共字段和属性的名字简单映射为子元素,如Point类中的x和y。应答元素的名字缺省为请求元素的名字后面附加上“Response”,应答元素也包含一个子元素,是请求元素名字后面附加“Result”。也有可能使用一些固定的映射属性来打破标准的XML映射。比如,你可以使用[XmlType]属性来定制类型的名字和名称空间,使用[XmlElement]和[XmlAttribute]属性来控制如何将参数或类成员分别映射为元素或属性,也可以使用[SoapDocumentMethod]属性控制怎样把方法本身映射为请求/响应消息中的元素名。比如,检查下面重新定义的Distance。
using System;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml.Serialization;
public class Point {
[XmlAttribute]
public double x;
[XmlAttribute]
public double y;
}
[WebService(Namespace="urn:geometry")]
public class Geometry {
[WebMethod]
[SoapDocumentMethod(RequestElementName="CalcDistance",
ResponseElementName="CalculatedDistance")]
[return: XmlElement("result")]
public double Distance(
[XmlElement("o")]Point orig, [XmlElement("d")]Point dest) {
return Math.Sqrt(Math.Pow(orig.x-dest.x, 2) +
Math.Pow(orig.y-dest.y, 2));
}
}
它所期望的SOAP请求消息如下:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<CalcDistance xmlns="urn:geometry">
<o x="0" y="0" />
<d x="3" y="4" />
</CalcDistance>
</soap:Body>
</soap:Envelope>
并将产生下面的应答消息:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<CalculatedDistance xmlns="urn:geometry">
<result>5</result>
</CalculatedDistance>
</soap:Body>
</soap:Envelope>
.asmx句柄使用SOAP document/literal风格来实现和描述上面显示的默认映射。意思上说WSDL定义将包含literal XML schema定义,它描述了SOAP消息中用到的请求和响应元素。
.asmx句柄也能使用SOAP rpc/encoded风格。这意味着SOAP体中包含一个RPC调用的XML代表(representation),参数用SOAP编码规则来串行化。实现这些仅需将[SoapDocumentService] and [SoapDocumentMethod]替换为[SoapRpcService] and [SoapRpcMethod]属性。
正如你所看到的,我们可能完全定制一个从给定方法到SOAP消息的映射。XmlSerializer提供了一个强大的串行化引擎。
除了处理参数的反串行化,.asmx句柄也能串行化/反串行化SOAP头。SOAP头的处理不同于参数,因为它们被认为是典型的无法控制的信息,和具体的方法没有直接的联系。由于这些,头处理主要是通过中间层(interception layers),完全为WebMethods屏蔽了头处理。
然而如果想涉足于WebMethod中的头信息,你必须提供一个.NET类,从SoapHeader派生而来,它代表了头的XML schema类型。然后定义一个此类型的成员变量作为每一个头实例的占位符。最后标记每个要访问头的WebMethod,指定你想要到达的域名。
比如,下面的SOAP请求包括一个用来进行身份验证的UsernameToken头。
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Header>
<x:UsernameToken xmlns:x="http://example.org/security">
<username>Mary</username>
<password>yraM</password>
</x:UsernameToken>
</soap:Header>
<soap:Body>
<CalcDistance xmlns="urn:geometry">
为了使.asmx句柄有可能反串行化SOAP头,首先你需要定义一个.NET类,它代表了暗含的XML Schema类。在此例中相应的类如下:
[XmlType(Namespace="http://example.org/security")]
[XmlRoot(Namespace="http://example.org/security")]
public class UsernameToken : SoapHeader {
public string username;
public string password;
}
然后你需要在WebMethod类中定义一个成员变量来控制一个头类的实例,同样要为WebMethods标记[SoapHeader]属性。见如下:
using System;
using System.Web.Services;
using System.Web.Services.Protocols;
[WebService(Namespace="urn:geometry")]
public class Geometry {
public UsernameToken Token;
[WebMethod]
[SoapHeader("Token")]
public double Distance(Point orig, Point dest) {
if (!Token.username.Equals(Reverse(Token.password)))
throw new Exception("access denied");
return Math.Sqrt(Math.Pow(orig.x-dest.x, 2) +
Math.Pow(orig.y-dest.y, 2));
}
}
这样,在WebMethod中你可以访问Token域,并提取SOAP头提供的信息。你也可以使用同样的技术将头信息送回客户端——你需要在[SoapHeader]属性声明中指定头的方向。
.asmx句柄也提供了.NET异常的自动串行化。任何被.asmx句柄劫获的未处理的异常都会被自动串行化为应答消息中的SOAP Fault元素。比如,在前面的例子中,假如用户名与反转的口令不匹配,我们的代码将会抛出一个.NET异常。.asmx句柄劫获这个异常,将它串行化为下面的SOAP应答:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault>
<faultcode>soap:Server</faultcode>
<faultstring>Server was unable to process request. --> access denied</faultstring>
<detail />
</soap:Fault>
</soap:Body>
</soap:Envelope>
如果你想更多的控制SOAP Fault元素,也可以通过指定所有SOAP Fault元素细节来明确抛出一个SOAP异常对象。比如:faultcode, faulstring, faultactor, detail元素。
领会WebMethods如何工作需要理解根本的串行化引擎和它的各种选项。串行化引擎的好处就是它隐藏了定制句柄中底层的XML API代码。然而尽管许多开发人员认为这样很好,一些人也认为这也是它的缺陷,因为他们想亲手来处理WebMethod中未经加工的SOAP消息。
自动生成WSDL文档
写好并部署了一个WebMethod后,客户需要明确知道和它成功通信SOAP消息应该是个什么样子。提供Web服务描述的标准方法是通过WSDL(和嵌入的XSD定义)。.asmx句柄自动的生成了人性化的可读文档和WSDL定义,它准确的反映了WebMethod接口。如果在你的WebMethods中应用了一些映射属性,它们将都被反映在生成的文档中。
假如你浏览这个.asmx文件,你将看到一个人性化的可读文档页面就像图2中显示的那样。这个文档页是由DefaultWsdlHelpGenerator.aspx (在 C:/windows/Microsoft.NET/Framework/v1.0.3705/config中)生成的。打开这个文件你会看到它就是一个标准的ASP.NET页面,它使用.NET reflection生成那个文档。这个特点使文档一直与代码同步,可以通过简单的修改这个文件来定制你的生成文档。
也可以在Web.config文件中指定一个不同的文档文件来绕过基于虚拟目录文档生成器:
<configuration>
<system.web>
<webServices>
<wsdlHelpGenerator href="MyDocumentation.aspx"/>
</webServices>
...
如果客户端发出请求.asmx终点,且请求字符串后加“?wsdl”,.asmx句柄将生成一个WSDL定义而不是一个人性化的可读文档。客户端可以使用WSDL定义生成代理类,它自动的知道怎样和Web服务通信。
要定制WSDL生成过程,你可以写一个SoapExtensionReflector类并在你的Web.config文件中注册到WebMethods框架。然后当.asmx句柄生成WSDL定义时,它将会调用你的反映类(reflector)使你有机会定制提供给用户的最后定义。
同样你也可以采用两种技术来绕过WSDL生成过程。首先,可以在你的虚拟目录下提供一个静态的WSDL文档供客户访问,然后从Web.config文件中除去文档生成器(如下所示)。
<configuration>
<system.web>
<webServices>
<protocols>
<remove name="Documentation"/>
</protocols>
...
另外一个比较自动化的技术是使用[WebServiceBinding]属性来指定WebMethod类实现的静态WSDL文档在虚拟目录中的位置,也要用[SoapDocumentMethod]属性为每一个实现的WebMethod指定WSDL绑定的名字。这样做以后,自动WSDL生成过程将会导入你的静态WSDL文件,生成一个新的服务描述。
手工编写WSDL是极端困难的,因为现在没有很多WSDL编辑器。因此自动文档/WSDL生成是WebMethod框架中有价值的一部分。没有它,许多开发者的日子会很难过的。
总结
ASP.NET WebMethods框架为创建WEB服务提供了高效的方法。WebMethods使得传统的.NET方法能够成为支持HTTP、XML、XML Schema和WSDL的Web服务操作。WebMethod(.asmx)句柄自动决定怎样将到来的SOAP请求消息分派到适当的方法,然后又将请求消息中的XML元素串行化为相应的.NET对象。为简化集成客户端,.asmx句柄也支持可读文档和WSDL的生成。
和自己定制的IhttpHandlers相比,尽管WebMethods框架有点受限,它还是提供了称为SOAP扩展框架的强大扩展模型。SOAP扩展允许你引入我们上面没有讨论到的额外功能来满足你的具体需要。比如,微软发布了Web Serivices Enhancements1.0,它提供了SoapExtension类,为WebMethods框架引入了几个GXA规范的支持。