深蓝海域KMPRO

Web服务说明语言 (WSDL) 浅释

2002-09-16 10:51

Web服务说明语言 (WSDL) 浅释


Carlos C. Tapang

Infotects

2001年7月

摘要:使用 WSDL,用户能够以真正独立于语言和平台的方式,自动生成 Web 服务代理。

为什么采用 WSDL?

Internet 协议这样的标准,是由某个机构强加给人们的吗?还是因为人们认识到,遵循这些标准带来的好处远远超过付出的代价而主动接受?有很多标准提出之后最终都没有获得成功。有时候,即使通过法律或政府规定来强制执行,这些标准也没有得到推广,Ada 编程语言就是一个例子。

我认为,一项标准被广泛接受一定是因为遵循它能带来好处。以铁路服务为例,不同公司修建的铁轨要能够相互衔接,或者来自不同公司的产品要能够配合使用才行得通。有一些厂商已经联合起来,要将 SOAP 变成一种标准。Web 服务说明语言 (WSDL) 为 Web 服务提供商和用户提供了一种方便的协作方式,使 SOAP 的好处更加明显。使各个公司修建的铁轨能够衔接很简单:这些公司只需约定两根铁轨之间的距离即可。而对于 Web 服务则要复杂得多。首先,我们必须约定用于指定接口的标准格式。

有反对意见认为,SOAP 并不真正需要接口说明语言。如果 SOAP 是仅为传输内容而制定的标准,那么它需要一种语言来说明这些内容。SOAP 消息确实携带类型信息,所以 SOAP 允许动态决定类型。但是,除非知道函数的名称以及参数的数量和类型,否则我们并不能正确地调用函数。没有 WSDL,您可以根据文档(这必须有才行)或者通过检查线路消息来判断调用语法。这两种方式都涉及人工操作,因此这个过程就有可能出错。而使用 WSDL,我能够以真正独立于语言和平台的方式,为 Web 服务自动生成代理。就像 IDL 文件对于 COM 和 CORBA 一样,WSDL 文件是客户端和服务器之间的合约。

请注意,尽管 WSDL 被设计成可以表达与非 SOAP 协议的绑定,我们这里主要考虑的是其与 HTTP 上的 SOAP 相关的情况。另外,虽然 SOAP 现在主要用于进行远程过程或函数调用,但是 WSDL 还允许以 SOAP 传输文档规范。WSDL 1.1 已经作为草案提交到 W3C。请参阅 http://www.w3.org/TR/wsdl.html(英文)。

WSDL 文档结构

使用模块图有助于理解 XML 文档。下图通过显示组成 WSDL 文档(是 XML 文档)的五个小节之间的关系,展示了 WSDL 的结构。

WSDL 文档的节可分成两组。上层的组包含抽象定义,而下层的组包含具体说明。抽象各节以独立于语言和平台的方式定义 SOAP 消息,不包含特定计算机或语言专用的元素。这有助于定义一套各种网站都能实现的服务。与具体站点相关的问题(例如序列化)被放到下层各节,这些节包含具体的说明。

抽象定义

类型

独立于计算机和语言的类型定义。

消息

包含函数参数(输入与输出分开)或文档说明。

端口类型

引用消息节中的消息定义来说明函数签名(操作名称、输出参数和输出参数等)。

具体说明

绑定

指定端口类型节中每个操作的绑定。

服务

指定每个绑定的端口地址。

在下图中,箭头连接符表示文档各节之间的关系。圆点和箭头连接符表示“引用”或“使用”关系。双箭头连接符表示“修饰”关系。三维箭头连接符表示“包含”关系。可以看出,消息节使用类型节中的定义,端口类型节使用消息节中的定义,绑定节引用端口类型节,而服务节引用绑定节。端口类型和绑定节包含操作元素,而服务节包含端口元素。端口节中的操作元素由绑定节中的操作元素修饰或说明。

在本文中,我将使用标准的 XML 术语来说明 WSDL 文档。“元素”指一个 XML 元素,而“属性”指元素的属性。形式如下:

<元素 属性="属性值">内容</元素>

“内容”可以由一个或多个元素以递归方式组成。根元素是最上层的元素,文档中所有其他元素都从属于它。子元素始终从属于另一个元素(称为“父元素”)。

请注意,可以只有一个或根本就没有类型节。所有其他各节可以有零个、一个或多个父元素。例如,消息节可以有零个或多个 <message> 元素。WSDL 架构要求所有各节以规定的顺序出现:输出、类型、消息、端口类型、绑定和服务。每个抽象节都可以放在独立的文件中,并被导入到主文档中。

图 1:抽象定义和具体定义

WSDL 文件示例

让我们来观察一个 WSDL 文件示例,了解其结构和原理。请注意,这是一个非常简单的 WSDL 文档实例。这里我们的目的仅仅是用它来说明最重要的特性。后面各节将有更详细的讨论。

<?xml version="1.0" encoding="UTF-8" ?>
<definitions name="FooSample"
targetNamespace="
http://tempuri.org/wsdl/"
xmlns:wsdlns="
http://tempuri.org/wsdl/"
xmlns:typens="
http://tempuri.org/xsd"
xmlns:xsd="
http://www.w3.org/2001/XMLSchema"
xmlns:soap="
http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:stk="
http://schemas.microsoft.com/soap-toolkit/wsdl-extension"
xmlns="
http://schemas.xmlsoap.org/wsdl/">

<types>
<schema targetNamespace="
http://tempuri.org/xsd"  
xmlns="
http://www.w3.org/2001/XMLSchema"
xmlns:SOAP-ENC="
http://schemas.xmlsoap.org/soap/encoding/"
xmlns:wsdl="
http://schemas.xmlsoap.org/wsdl/"
elementFormDefault="qualified" >
</schema>
</types>

<message name="Simple.foo">
<part name="arg" type="xsd:int"/>
</message>

<message name="Simple.fooResponse">
<part name="result" type="xsd:int"/>
</message>

<portType name="SimplePortType">
<operation name="foo" parameterOrder="arg" >
<input message="wsdlns:Simple.foo"/>
<output message="wsdlns:Simple.fooResponse"/>
</operation>
</portType>

<binding name="SimpleBinding" type="wsdlns:SimplePortType">
<stk:binding preferredEncoding="UTF-8" />
<soap:binding style="rpc"
         transport="
http://schemas.xmlsoap.org/soap/http"/>
<operation name="foo">
<soap:operation
soapAction="
http://tempuri.org/action/Simple.foo"/>
<input>
<soap:body use="encoded" namespace="
http://tempuri.org/message/"
encodingStyle="
http://schemas.xmlsoap.org/soap/encoding/" />
</input>
<output>
<soap:body use="encoded" namespace="
http://tempuri.org/message/"
encodingStyle="
http://schemas.xmlsoap.org/soap/encoding/" />
</output>
</operation>
</binding>

<service name="FOOSAMPLEService">
<port name="SimplePort" binding="wsdlns:SimpleBinding">
<soap:address location="
http://carlos:8080/FooSample/FooSample.asp"/>
</port>
</service>
</definitions>

下面是示例文档的要点。稍后我会详细讨论每一节。

第一行声明该文档是 XML。虽然这不是必需的,但它可以帮助 XML 语法分析程序判断是要分析整个 WSDL 文件还是产生一个错误信号。第二行是 WSDL 文档的根元素:<definitions>。根元素附加了一些名称空间属性(名称空间声明),<types> 元素的 <schema> 子元素也附加了一些名称空间属性。

<types> 元素包含类型节。如果不需要声明数据类型,可以省略这一节。在示例 WSDL 中,没有声明应用程序专用的类型,但我还是使用了类型节,这只是为了声明文档中使用的架构名称空间。

<message> 元素包含消息节。如果我们把操作看作函数,那么 <message> 元素就定义了该函数的参数。<message> 元素中的每个 <part> 子元素都对应一个参数。输入的参数在一个单独的 <message> 元素中定义,与输出元素分开,输出元素在自己的 <message> 元素中定义。输入和输出参数在输入和输出 <message> 元素中都有各自对应的 <part> 元素。输出 <message> 元素的名称按约定以“Response”结尾,例如“fooResponse”。每个 <part> 元素都有 name 和 type 属性,就象每个函数参数都有名称和类型一样。

当用于文档交换时,WSDL 允许使用 <message> 元素来说明要交换的文档。

<part> 元素的类型可以是 XSD 基本类型、SOAP 定义类型 (soapenc)、WSDL 定义类型 (wsdl) 或类型节定义类型。

端口类型节中可以有零个、一个或多个 <portType> 元素。因为抽象的端口类型定义可以放在独立的文件中,所以 WSDL 文件中可能有零个 <portType> 元素。上例只显示了一个 <portType> 元素。正如您所看到的,<portType> 元素用 <operation> 子元素来定义一个或多个操作。示例只显示了一个 <operation> 元素,名为“foo”。这个名称相当于函数名。<operation> 元素可以有一个、两个或三个子元素:<input>、<output> 和 <fault> 元素。每个 <input> 和 <output> 元素中的 message 属性引用消息节中相关的 <message> 元素。这样,示例中的整个 <portType> 元素相当于下面的 C 函数声明:

int foo(int arg);

这个示例表明,与 C 相比,XML 是多么冗长。(包括 <message> 元素在内,示例用了 12 行 XML 来表达这个在 C 中仅一行的函数声明。)

绑定节可以有零个、一个或多个 <binding> 元素。其目的是指定如何通过线路发送每个 <operation> 的调用和响应。服务节也可以有零个、一个或多个 <service> 元素。它包含 <port> 元素。每个 <port> 元素都引用绑定节中的一个 <binding> 元素。绑定和服务节都包含对 WSDL 文档的具体说明。

名称空间

在根元素 <definitions> 和子元素 <schema> 中都有名称空间属性:

<definitions name="FooSample"
targetNamespace="
http://tempuri.org/wsdl/"
xmlns:wsdlns="
http://tempuri.org/wsdl/"
xmlns:typens="
http://tempuri.org/xsd"
xmlns:xsd="
http://www.w3.org/2001/XMLSchema"
xmlns:soap="
http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:stk="
http://schemas.microsoft.com/soap-toolkit/wsdl-extension"
xmlns="
http://schemas.xmlsoap.org/wsdl/">

<types>
<schema targetNamespace="
http://tempuri.org/xsd"  
xmlns="
http://www.w3.org/2001/XMLSchema"
xmlns:SOAP-ENC="
http://schemas.xmlsoap.org/soap/encoding/"
xmlns:wsdl="
http://schemas.xmlsoap.org/wsdl/"
elementFormDefault="qualified" >
</schema>
</types>

每个名称空间属性为文档中使用的每个名称空间都声明了一个简写。在实例“xmlns:xsd”中,为名称空间 http://www.w3.org/2001/XMLSchema(英文)定义了一个简写 (xsd)。这样,在文档的后面部分只需要给名称加上前缀(或“限定”)“xsd:”,就可以引用这个名称空间,例如限定的类型名称“xsd:int”。标准的作用域规则适用于这个简写前缀。即,在某个元素中定义的前缀只在该元素中有效。

名称空间的作用是什么?名称空间是为避免命名冲突而设立的。假设我建立了一个 Web 服务,其 WSDL 文件包含一个名为“foo”的元素,而您要将我的 Web 服务与另一个补充的服务结合使用。如果没有名称空间,那么其他 Web 服务就不能在它们的 WSDL 文件中使用名称“foo”。仅当在两个实例中它们都是指同一个对象,两个服务才能使用相同的名称。而在两个不同的名称空间中,我的 Web 服务“foo”与其他 Web 服务“foo”指的是不同的对象。在您的客户端中,可以通过限定来引用我的“foo”。例如,如果我将 carlos 声明为 http://www.infotects.com/fooService 的简写,则完全限定名 http://www.infotects.com/fooService#foo 等价于“carlos:foo”。请注意,名称空间中使用 URI 来保证其唯一性并允许在文档中使用定位符。由 URI 指向的位置不一定对应真实的 Web 位置。GUID 也可以用来代替或补充 URI。例如,GUID“335DB901-D44A-11D4-A96E-0080AD76435D”是一个有效的名称空间指示符。

targetNamespace 属性声明了一个名称空间,在该元素中声明的所有名称都从属于该名称空间。在 WSDL 文件示例中,<definitions> 的 targetNamespace 是 http://tempuri.org/wsdl。这意味着在此 WSDL 文档中声明的所有名称都从属于此名称空间。<schema> 有自己的 targetNamespace 属性,其值为 http://tempuri.org/xsd。因此,在此 <schema> 元素中定义的所有名称都从属于此名称空间,而不是主目标名称空间。

<schema> 元素中的下一行声明了一个默认的名称空间。架构中所有未限定的名称都属于此名称空间。

xmlns="http://www.w3.org/2001/XMLSchema"

SOAP 消息

看待 WSDL 文件的方式之一是:对于使用该文件的客户端和服务器而言,它决定在线路上传输什么内容。虽然 SOAP 使用了底层协议(例如 IP 和 HTTP),但是应用程序决定了它在特定的客户端和服务器之间使用的高层协议。换句话说,给定一个操作(例如仅简单地读取并返回一个输入的整数的“echoInt”),那么参数的数量、每个参数的类型以及这些参数通过线路(序列化)传输的方式就构成了应用程序专用的协议。这种协议可以按各种方式来指定,我相信最好的方式就是使用 WSDL。这样看来,WSDL 不仅是一个“接口合约”,而且还是一个协议规范语言。如果我们需要从 IP 和 HTTP 等“固定”的协议发展到应用程序专用的协议,这正是我们所需要的。

WSDL 可以指定 SOAP 消息是遵循 rpc 还是遵循文档样式。rpc 样式的消息(如示例中所用的)类似于有零个或更多参数的函数调用。文档样式的消息较平整,并且它需要的嵌套层次更少。下面的 XML 消息作为使用 MS SOAP Toolkit 2.0 (MSTK2) 中的 SoapClient 对象分析 WSDL 文件示例的结果被发送和接收。

从客户端发送时使用函数调用“foo(5131953)”:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<SOAP-ENV:Envelope
SOAP-ENV:encodingStyle="
http://schemas.xmlsoap.org/soap/encoding/"
xmlns:SOAP-ENV="
http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Body>
<m:foo xmlns:m="
http://tempuri.org/message/">
<arg>5131953</arg>
</m:foo>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

从服务器接收(响应):

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<SOAP-ENV:Envelope
SOAP-ENV:encodingStyle="
http://schemas.xmlsoap.org/soap/encoding/"
xmlns:SOAP-ENV="
http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Body>
<SOAPSDK1:fooResponse xmlns:SOAPSDK1="
http://tempuri.org/message/">
<result>5131953</result>
</SOAPSDK1:fooResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

函数调用消息及其响应都是有效的 XML。SOAP 消息包含 <Envelope> 元素,而 <Envelope> 元素包含可选的 <Header> 元素和至少一个 <Body> 元素。发送和接收的消息在主 <Envelope> 元素中都有一个 <Body> 元素。rpc 函数调用消息正文有一个以操作名称“foo”命名的元素,而响应正文有一个“fooResponse”元素。foo 元素有一个 <arg> 部分。<arg> 是一个参数,如示例 WSDL 所说明。fooResponse 同样有一个 <result> 部分。请注意 encodingStyle、envelope 和 message 名称空间在 WSDL 的绑定节中的规定方式。如下所示。

<binding name="SimpleBinding" type="wsdlns:SimplePortType">
     <stk:binding preferredEncoding="UTF-8" />
        <soap:binding style="rpc"
                  transport="
http://schemas.xmlsoap.org/soap/http"/>
        <operation name="foo">
           <soap:operation
              soapAction="
http://tempuri.org/action/Simple.foo"/>
              <input>
                   <soap:body use="encoded"
                        namespace="
http://tempuri.org/message/"
                        encodingStyle=
                              "
http://schemas.xmlsoap.org/soap/encoding/" />
              </input>
              <output>
                   <soap:body use="encoded"
                        namespace="
http://tempuri.org/message/"
                        encodingStyle=
                              "
http://schemas.xmlsoap.org/soap/encoding/" />
              </output>
        </operation>
      </binding>

WSDL 类型和消息节中的 XML 架构

WSDL 数据类型基于 W3C 推荐的“XML 架构:数据类型”(XSD)。此文档有三个不同的版本(1999、2000/10 和 2001),应该在 <definitions> 元素中的名称空间之一中声明,指定在 WSDL 文件中使用哪个版本:

xmlns:xsd="http://www.w3.org/2001/XMLSchema"

本文中我们只考虑 2001 版。WSDL 标准的支持者强烈建议使用 2001 版。

在本节和后面各节中,使用了以下前缀或名称空间简写:

前缀 等价的名称空间 说明
soapenc
http://schemas.xmlsoap.org/soap/encoding SOAP 1.1 编码
wsdl
http://schemas.xmlsoap.org/wsdl/soap WSDL 1.1
xsd
http://www.w3.org/2001/XMLSchema XML 架构

XSD 基本类型

下表摘自 MSTK2 文档,列举了 MSTK2 支持的全部 XSD 基本类型。它说明了客户端和服务器端的 WSDL 阅读器如何将 XSD 类型映射到 variant 类型以及 VB、C++ 和 IDL 中对应的类型。

XSD (Soap) 类型 variant 类型 VB C++ IDL 注释
anyURI VT_BSTR String BSTR BSTR  
base64Binary VT_ARRAY | VT_UI1 Byte() SAFEARRAY SAFEARRAY(unsigned char)  
boolean VT_BOOL Boolean VARIANT_BOOL VARIANT_BOOL  
byte VT_I2 Integer short short 转换时验证范围
date VT_DATE Date DATE DATE 时间设置为 oo:oo:oo
dateTime VT_DATE Date DATE DATE  
double VT_R8 Double double double  
duration VT_BSTR String BSTR BSTR 不执行转换或验证
ENTITIES VT_BSTR String BSTR BSTR 不执行转换或验证
ENTITY VT_BSTR String BSTR BSTR 不执行转换或验证
float VT_R4 Single float float  
gDay VT_BSTR String BSTR BSTR 不执行转换或验证
gMonth VT_BSTR String BSTR BSTR 不执行转换或验证
gMonthDay VT_BSTR String BSTR BSTR 不执行转换或验证
gYear VT_BSTR String BSTR BSTR 不执行转换或验证
gYearMonth VT_BSTR String BSTR BSTR 不执行转换或验证
ID VT_BSTR String BSTR BSTR 不执行转换或验证
IDREF VT_BSTR String BSTR BSTR 不执行转换或验证
IDREFS VT_BSTR String BSTR BSTR 不执行转换或验证
int VT_I4 Long long long  
integer VT_DECIMAL Variant DECIMAL DECIMAL 转换时验证范围
language VT_BSTR String BSTR BSTR 不执行转换或验证
long VT_DECIMAL Variant DECIMAL DECIMAL 转换时验证范围
Name VT_BSTR String BSTR BSTR 不执行转换或验证
NCName VT_BSTR String BSTR BSTR 不执行转换或验证
negativeInteger VT_DECIMAL Variant DECIMAL DECIMAL 转换时验证范围
NMTOKEN VT_BSTR String BSTR BSTR 不执行转换或验证
NMTOKENS VT_BSTR String BSTR BSTR 不执行转换或验证
nonNegativeInteger VT_DECIMAL Variant DECIMAL DECIMAL 转换时验证范围
nonPositiveInteger VT_DECIMAL Variant DECIMAL DECIMAL 转换时验证范围
normalizedString VT_BSTR String BSTR BSTR  
NOTATION VT_BSTR String BSTR BSTR 不执行转换或验证
number VT_DECIMAL Variant DECIMAL DECIMAL  
positiveInteger VT_DECIMAL Variant DECIMAL DECIMAL 转换时验证范围
QName VT_BSTR String BSTR BSTR 不执行转换或验证
short VT_I2 Integer short short  
string VT_BSTR String BSTR BSTR  
time VT_DATE Date DATE DATE 日期设置为 1899 年 12 月 30 日
token VT_BSTR String BSTR BSTR 不执行转换或验证
unsignedByte VT_UI1 Byte unsigned char unsigned char  
unsignedInt VT_DECIMAL Variant DECIMAL DECIMAL 转换时验证范围
unsignedLong VT_DECIMAL Variant DECIMAL DECIMAL 转换时验证范围
unsignedShort VT_UI4 Long Long Long 转换时验证范围

XSD 定义了两组内置的数据类型:原始型和导出型。在 http://www.w3.org/TR/2001/PR-xmlschema-2-20010330(英文)中查看内置数据类型的层次结构会有所帮助。

复杂类型

XML 架构允许定义复杂类型。在 C 中,复杂类型为结构。例如,要定义与以下 C 结构等价的数据类型:

typedef  struct {
   string  firstName;
   string  lastName;
   long    ageInYears;
   float    weightInLbs;
   float    heightInInches;
} PERSON;

我们可以在 XML 架构中写入:

<xsd:complexType name="PERSON">
   <xsd:sequence>
      <xsd:element name="firstName" type="xsd:string"/>
      <xsd:element name="lastName" type="xsd:string"/>
      <xsd:element name="ageInYears" type="xsd:int"/>
      <xsd:element name="weightInLbs" type="xsd:float"/>
      <xsd:element name="heightInInches" type="xsd:float"/>
   </xsd:sequence>
</xsd:complexType>

但是,<complexType> 能表达的不仅是结构的等价类型。它可以有除 <sequence> 以外的其他子元素。我可以使用 <all> 而不是 <sequence>:

<xsd:complexType name="PERSON">
   <xsd:all>
      <xsd:element name="firstName" type="xsd:string"/>
      <xsd:element name="lastName" type="xsd:string"/>
      <xsd:element name="ageInYears" type="xsd:int"/>
      <xsd:element name="weightInLbs" type="xsd:float"/>
      <xsd:element name="heightInInches" type="xsd:float"/>
   </xsd:all>
</xsd:complexType>

这意味着成员变量 <element> 可以以任何顺序出现,并且每一个都是可选的。这与 C 结构不完全一样。

请注意,示例中使用了内置数据类型 string、int、float。C 中的 string 与 XML 中的 string 相同,float 也相同。但是 C 中的 long 是 XML 中的 int(如上表所示)。

在 WSDL 文件中,复杂类型(如上面所示的类型)在类型节中声明。例如,我可以按以下方式声明 PERSON 类型,并在消息节中使用:

<?xml version="1.0" encoding="UTF-8" ?>
<definitions ?>
<types>
     <schema targetNamespace="someNamespace"
            xmlns:typens="someNamespace" >
     <xsd:complexType name="PERSON">
        <xsd:sequence>
           <xsd:element name="firstName" type="xsd:string"/>
           <xsd:element name="lastName" type="xsd:string"/>
           <xsd:element name="ageInYears" type="xsd:int"/>
           <xsd:element name="weightInLbs" type="xsd:float"/>
           <xsd:element name="heightInInches" type="xsd:float"/>
        </xsd:sequence>
     </xsd:complexType>
     </schema>
</types>

<message name="addPerson">
     <part name="person" type="typens:PERSON"/>
</message>

<message name="addPersonResponse">
     <part name="result" type="xsd:int"/>
</message>

</definitions>

在上例中,第一个消息名为“addPerson”,它有一个类型为“PERSON”的 <part>。PERSON 类型在类型节中声明为复杂类型。

如果初始化 MSTK2 SoapClient 时,我们在完整的 WSDL 文件中使用上述片段,将能够成功分析该文件。但是,不能发送对 <addPerson> 的函数调用。因为 SoapClient 本身不知道如何处理复杂类型,所以需要使用自定义类型映射程序来处理复杂类型。MSTK2 文档有一个示例应用程序,其中包含了自定义类型映射程序。

还有另外一种方式,可以将 <part> 元素与类型声明相关联。这种方式使用 element 属性,而不是 type 属性。在下面的另一个示例中,我在类型节中声明了两个元素(“Person”和“Gender”),然后用 element 属性在“addPerson”<message> 中引用它们。

<?xml version="1.0" encoding="UTF-8" ?>
<definitions ?>
<types>
     <schema targetNamespace="someNamespace"
            xmlns:typens="someNamespace" >
     <element name="Person">
     <xsd:complexType>
        <xsd:sequence>
           <xsd:element name="firstName" type="xsd:string"/>
           <xsd:element name="lastName" type="xsd:string"/>
           <xsd:element name="ageInYears" type="xsd:int"/>
           <xsd:element name="weightInLbs" type="xsd:float"/>
           <xsd:element name="heightInInches" type="xsd:float"/>
        </xsd:sequence>
     </xsd:complexType>
     </element>
     <element name="Gender">
     <xsd:simpleType>
        <xsd:restriction base="xsd:string">
           <xsd:enumeration value="Male" />
           <xsd:enumeration value="Female" />
        </xsd:restriction>
     </xsd:simpleType>
     </element>
     </schema>
</types>

<message name="addPerson">
     <part name="who" element="typens:Person"/>
     <part name="sex" element="typens:Gender"/>
</message>

<message name="addPersonResponse">
     <part name="result" type="xsd:int"/>
</message>
</definitions>

类型节的 Gender <element> 中嵌入了一个匿名枚举类型,这种类型可能的取值有“Male”和“Female”。我在“addPerson”<message> 中使用 element 属性而不是 type 属性来引用该元素。

在将特定的类型与 <part> 相关联时,“element”和“type”之间有什么区别呢?使用“type”属性,我们可以说明一个能够使用多种类型的部分(类似 variant);而在使用“element”属性时我们不能这样做。下面的示例说明了这一点。

<?xml version="1.0" encoding="UTF-8" ?>
<definitions ?>
<types>
     <schema targetNamespace="someNamespace"
            xmlns:typens="someNamespace">
     <xsd:complexType name="PERSON">
        <xsd:sequence>
           <xsd:element name="firstName" type="xsd:string"/>
           <xsd:element name="lastName" type="xsd:string"/>
           <xsd:element name="ageInYears" type="xsd:int"/>
           <xsd:element name="weightInLbs" type="xsd:float"/>
           <xsd:element name="heightInInches" type="xsd:float"/>
        </xsd:sequence>
     </xsd:complexType>
     <xsd:complexType name="femalePerson">
        <xsd:complexContent>
           <xsd:extension base="typens:PERSON" >
           <xsd:element name="favoriteLipstick" type="xsd:string" />
           </xsd:extension>
        </xsd:complexContent>
     </xsd:complexType>
     <xsd:complexType name="malePerson">
        <xsd:complexContent>
           <xsd:extension base="typens:PERSON" >
           <xsd:element name="favoriteShavingLotion" type="xsd:string" />
           </xsd:extension>
        </xsd:complexContent>
     </xsd:complexType>
     <xsd:complexType name="maleOrFemalePerson">
        <xsd:choice>
           <xsd:element name="fArg" type="typens:femalePerson" >
           <xsd:element name="mArg" type="typens:malePerson" />
        </xsd:choice>
     </xsd:complexType>
     </schema>
</types>

<message name="addPerson">
     <part name="person" type="typens:maleOrFemalePerson"/>
</message>

<message name="addPersonResponse">
     <part name="result" type="xsd:int"/>
</message>

</definitions>

上例还说明了扩展导出的方式。“femalePerson”和“malePerson”都是从“PERSON”导出的。每个都有一个额外的元素:“femalePerson”有“favoriteLipstick”,“malePerson”有“favoriteShavingLotion”。使用 <choice> 构造将两个导出的类型合并到一个复杂类型“maleOrFemalePerson”中。最后,在“addPerson”<message> 中,由“person”<part> 引用合并的类型。然后,这个 <part> 或参数就可以是“femalePerson”或“malePerson”。

数组

XSD 提供了 <list> 构造来声明一个项目用空格分隔的数组。但是,SOAP 不使用 XSD 列表来编码数组。而是为数组定义了自己的类型“SOAP-ENC:Array”。下例显示了如何从这个类型导出,为一维的整数数组声明一个类型:

<xsd:complexType name="ArrayOfInt">
   <xsd:complexContent>
      <xsd:restriction base="soapenc:Array">
         <attribute ref="soapenc:arrayType" wsdl:arrayType="xsd:int[]"/>
      </xsd:restriction>
   </xsd:complexContent>
</xsd:complexType>

通过使用限制导出的方式从 soapenc:Array 导出,来声明一个新的复杂类型。然后,为该复杂类型声明一个属性 arrayType。对“soapenc:arrayType”的引用实际上通过以下方式声明了 arrayType 属性:

<xsd:attribute name="arrayType" type="xsd:string"/>

然后,wsdl:arrayType 属性值决定了数组中每个成员的类型。数组项目也可以是复杂类型。

<xsd:complexType name="ArrayOfPERSON">
   <xsd:complexContent>
      <xsd:restriction base="soapenc:Array">
         <attribute ref="soapenc:arrayType"
               wsdl:arrayType="typens:PERSON[]"/>
      </xsd:restriction>
   </xsd:complexContent>
</xsd:complexType>

WSDL 要求数组的类型名称必须是“ArrayOf”加上数组中项目的类型。这样,仅从名称就可以知道“ArrayOfPERSON”是 PERSON 结构的数组。下面,我使用 ArrayOfPERSON 来声明一个 <message>,以添加多个 PERSON 而不仅仅是一个 PERSON。

<?xml version="1.0" encoding="UTF-8" ?>
<definitions ?>
<types>
     <schema targetNamespace="someNamespace"
            xmlns:typens="someNamespace" >
     <xsd:complexType name="PERSON">
        <xsd:sequence>
           <xsd:element name="firstName" type="xsd:string"/>
           <xsd:element name="lastName" type="xsd:string"/>
           <xsd:element name="ageInYears" type="xsd:int"/>
           <xsd:element name="weightInLbs" type="xsd:float"/>
           <xsd:element name="heightInInches" type="xsd:float"/>
        </xsd:sequence>
     </xsd:complexType>
     <xsd:complexType name="ArrayOfPERSON">
        <xsd:complexContent>
           <xsd:restriction base="soapenc:Array">
              <attribute ref="soapenc:arrayType"
           wsdl:arrayType="typens:PERSON[]"/>
           </xsd:restriction>
        </xsd:complexContent>
     </xsd:complexType>
     </schema>
</types>

<message name="addPersons">
     <part name="person" type="typens:ArrayOfPERSON"/>
</message>

<message name="addPersonResponse">
     <part name="result" type="xsd:int"/>
</message>

</definitions>

<portType> 和 <operation> 元素

portType 抽象定义了一系列操作。portType 中的 operation 元素定义了调用 portType 中所有方法的语法。每个 operation 元素都声明了方法的名称、参数(使用 <message> 元素)以及每个参数的类型(每个 <message> 中声明的 <part> 元素)。

一个 WSDL 文档中可以有多个 <portType> 元素。每个 <portType> 元素组合了一系列相关的操作,这与 COM 接口组合方法类似。

在一个 <operation> 元素中,最多可以有一个 <output> 元素、一个 <output> 元素和一个 <fault> 元素。这三个元素中的每一个都有 name 和 message 属性。

<output>、<output> 和 <fault> 元素中的 name 属性的作用是什么呢?它可以用来区分两个名称相同的操作(重载)。例如,考虑以下两个名称相同而参数不同的 C 函数:

void foo(int arg);
void foo(string arg);

这种重载可以在 WSDL 中表示如下:

<?xml version="1.0" encoding="UTF-8" ?>
<definitions name="fooDescription"
     targetNamespace="
http://tempuri.org/wsdl/"
     xmlns:wsdlns="
http://tempuri.org/wsdl/"
     xmlns:typens="
http://tempuri.org/xsd"
     xmlns:xsd="
http://www.w3.org/2001/XMLSchema"
     xmlns:soap="
http://schemas.xmlsoap.org/wsdl/soap/"
     xmlns:stk="
http://schemas.microsoft.com/soap-toolkit/wsdl-
            extension"
     xmlns="
http://schemas.xmlsoap.org/wsdl/">
<types>
     <schema targetNamespace="
http://tempuri.org/xsd"
              xmlns="
http://www.w3.org/2001/XMLSchema"
              xmlns:SOAP-ENC="
http://schemas.xmlsoap.org/soap/encoding/"
                    xmlns:wsdl="
http://schemas.xmlsoap.org/wsdl/"
           elementFormDefault="qualified" >
</schema>
</types>

<message name="foo1">
     <part name="arg" type="xsd:int"/>
</message>

<message name="foo2">
     <part name="arg" type="xsd:string"/>
</message>

<portType name="fooSamplePortType">
     <operation name="foo" parameterOrder="arg " >
          <input name="foo1" message="wsdlns:foo1"/>
     </operation>
     <operation name="foo" parameterOrder="arg " >
          <input name="foo2" message="wsdlns:foo2"/>
     </operation>
</portType>

<binding name="fooSampleBinding" type="wsdlns:fooSamplePortType">
<stk:binding preferredEncoding="UTF-8" />
     <soap:binding style="rpc"
            transport="
http://schemas.xmlsoap.org/soap/http"/>
     <operation name="foo">
     <soap:operation soapAction="
http://tempuri.org/action/foo1"/>
          <input name="foo1">
     <soap:body use="encoded" namespace="
http://tempuri.org/message/
      encodingStyle="
http://schemas.xmlsoap.org/soap/encoding/" />
     </input>
</operation>
<operation name="foo">
     <soap:operation soapAction="
http://tempuri.org/action/foo2"/>
          <input name="foo2">
               <soap:body use="encoded"
               namespace="
http://tempuri.org/message/
               encodingStyle="
http://schemas.xmlsoap.org/soap/encoding/"
               />
          </input>
</operation>
</binding>

     <service name="FOOService">
          <port name="fooSamplePort" binding="fooSampleBinding">
               <soap:address
                  location="
http://carlos:8080/fooService/foo.asp"/>
          </port>
     </service>
</definitions>

直到我撰写这篇文章时,还没有哪种 SOAP 实现能够重载操作名称。对基于 Java 的客户端,这很重要。因为基于 Java 的服务器使用的接口利用了 Java 的重载特性。而对基于 COM 的客户端,这并不重要,因为 COM 不支持重载。

<binding> 和 <operation> 元素
在绑定节中完全指定协议、序列化和编码。类型、消息和端口类型节处理抽象的数据内容,而绑定节处理数据传输的物理细节。绑定节将前三节中的抽象内容具体化。

绑定规范与数据和消息声明的分离意味着参与同种业务的服务提供商可以将一组操作标准化 (portType)。然后,每个提供商可以通过提供不同的自定义绑定来彼此区分。WSDL 同样有一个导入构造,这样抽象定义就能够放到自己的文件中,而与绑定和服务节分开。这些文件可以在服务提供商之间分发,并把抽象定义用作标准。例如,多家银行可以将一组银行业务操作标准化,在抽象 WSDL 文档中进行准确说明。然后,每个银行就可以自由地“自定义”底层协议、序列化优化和编码。

下面是重载示例 WSDL 的绑定节,在此重复是为了进行更详细的讨论:

<binding name="fooSampleBinding" type="wsdlns:fooSamplePortType">
     <stk:binding preferredEncoding="UTF-8" />
     <soap:binding style="rpc"
            transport="
http://schemas.xmlsoap.org/soap/http"/>
     <operation name="foo">
          <soap:operation soapAction="
http://tempuri.org/action/foo1"/>
               <input name="foo1">
                    <soap:body use="encoded"
                         namespace="
http://tempuri.org/message/
                         encodingStyle=
                        "
http://schemas.xmlsoap.org/soap/encoding/" />
               </input>
     </operation>
     <operation name="foo">
          <soap:operation soapAction="
http://tempuri.org/action/foo2"/>
               <input name="foo2">
                    <soap:body use="encoded"
                         namespace="
http://tempuri.org/message/
                         encodingStyle=
                        "
http://schemas.xmlsoap.org/soap/encoding/" />
               </input>
     </operation>
</binding>

<binding> 元素被给定一个名称(在本例中是“fooSampleBinding”),使服务节中的 <port> 元素能够引用它。它有一个“type”属性引用 <portType>,在本例中是“wsdlns:fooSamplePortType”。第二行是 MSTK2 扩展元素 <stk:binding>,它指定了 preferredEncoding“UTF-8”。

<soap:binding> 元素指定了所使用的样式 (rpc) 和传送方式。attribute 属性引用了指定使用 HTTP SOAP 协议的名称空间。

有两个 <operation> 元素具有相同的名称“foo”。区分这两个操作的是两个不同的 <input> 名称:“foo1”和“foo2”。两个 <operation> 元素中的 <soap:operation> 元素有相同的“soapAction”属性(是 URI)。soapAction 属性是在 SOAP 消息中逐字使用的 SOAP 专用 URI。产生的 SOAP 消息具有 SOAPAction 标题,<soap:operation> 元素中的 URI 成为其值。soapAction 属性是 HTTP 绑定所必需的,但不应该出现在非 HTTP 绑定中。其用法在本文中不作详细说明。在本例中,它可以用来帮助区分两个“foo”操作。SOAP 1.1 说明 soapAction 用于确定消息的“意图”。它建议服务器使用此属性来路由消息,而不必解析整个消息。在实际应用中,其用法很多。<soap:operation> 元素还可以包含另外一个属性“style”。如果对于某个操作,有必要修改在 <soap:binding> 中指定的格式,就可以使用“style”属性。

<operation> 元素可以包含 <input>、<output> 和 <fault> 元素,它们与端口类型节中相同的元素对应。上例中仅使用了 <input> 元素。这三个元素都有可选的“name”属性,用于区分同名的操作(如本例所示)。在示例的 <input> 元素中包含了 <soap:body> 元素,它指定产生的 SOAP 消息的 <body> 中包括的内容。此元素有以下属性:

Use

用于指定数据是“encoded”还是“literal”。“literal”指产生的 SOAP 消息包含的数据按照抽象定义(类型、消息和端口类型节)所指定的方式进行格式化。“encoded”指由“encodingStyle”属性(请参阅下面的说明)决定编码。

Namespace

每个 SOAP 消息正文都可以有自己的名称空间,以防止名称冲突。在此属性中指定的 URI 在产生的 SOAP 消息中被逐字使用。

EncodingStyle

对于 SOAP 编码,这应该具有 URI 值“http://schemas.xmlsoap.org/soap/encoding”。

文档样式绑定

在上一节中,<soap:binding> 元素有一个值为“rpc”的 type 属性。此属性设置为“document”时,会改变线路上消息的序列化。现在消息是文档传输,而不是函数签名。在这种绑定中,<message> 元素定义了文档格式,而不是函数签名。例如,考虑以下 WSDL 片段:

<definitions
  xmlns:stns="(SchemaTNS)"
  xmlns:wtns="(WsdlTNS)"
  targetNamespace="(WsdlTNS)">

<schema targetNamespace="(SchemaTNS)"
    elementFormDefault="qualified">
  <element name="SimpleElement" type="xsd:int"/>
  <element name="CompositElement" type="stns:CompositeType"/>
  <complexType name="CompositeType">
    <all>
      <element name='a' type="xsd:int"/>
      <element name='b' type="xsd:string"/>
    </all>
  </complexType>
</schema>

<message...>
  <part name='p1' type="stns:CompositeType"/>
  <part name='p2' type="xsd:int"/>
  <part name='p3' element="stns:SimpleElement"/>
  <part name='p4' element="stns:CompositeElement"/>
</message>
?</definitions>

该架构有两个元素(SimpleElement 和 CompositeElement)和一个声明的类型 (CompositeType)。唯一声明的 <message> 元素有四个部分:p1,是 CompositeType 类型;p2,是 int 类型;p3,是 SimpleElement;p4,是 CompositeElement。下表比较了由以下使用/类型决定的四种绑定:rpc/literal、document/literal、rpc/encoded 和 document/encoded。表格显示了对于每一种绑定会在线路上出现什么。

rpc / literal
<operation name="method1" style="rpc" ...>
<input>
<soap:body parts="p1 p2 p3 p4"

use="literal"
namespace="(MessageNS)"/>
</input>
</operation>

在线路上:
<soapenv:body... xmlns:mns="(MessageNS)"
xmlns:stns="(SchemaTNS)">
<mns:method1>
<mns:p1>
<stns:a>123</stns:a>
<stns:b>hello</stns:b>
</mns:p1>
<mns:p2>123</mns:p2>
<mns:p3>

<stns:SimpleElement>

123

</stns:SimpleElement>
</mns:p3>
<mns:p4>
<stns:CompositeElement>
<stns:a>123</stns:a>
<stns:b>hello</stns:b>
</stns:CompositeElement>
</mns:p4>
</mns:method1>
</soapenv:body>

document / literal / type=
<operation name="method1"

style="document" ...>
<input>
<soap:body parts="p1" use="literal">
</input>
</operation>

在线路上:

<soapenv:body... xmlns:stns="(SchemaTNS)">
<stns:a>123</stns:a>
<stns:b>hello</stns:b>
</soapenv:body>

rpc / encoded
<operation name="method1" style="rpc" ...>
<input>
<soap:body parts="p1 p2" use="encoded"
encoding=

"http://schemas.xmlsoap.org/soap/encoding/"
namespace="(MessageNS)"/>
</input>
</operation>

在线路上:
<soapenv:body... xmlns:mns="(MessageNS)">
<mns:method1>
<p1 TARGET="_self" HREF="#1"/>
<p2>123</p2>
</mns:method1>
<mns:CompositeType id="#1">
<a>123</a>
<b>hello</b>
</mns:CompositeType>
</soapenv:body>

document / literal / element=
<operation name="method1"

style="document" ...>
<input>
<soap:body parts="p3 p4"

use="literal">
</input>
</operation>

在线路上:

<soapenv:body... xmlns:stns="(SchemaTNS)">
<stns:SimpleElement>

123

</stns:SimpleElement>
<stns:CompositeElement>
<stns:a>123</stns:a>
<stns:b>hello</stns:b>
</stns:CompositeElement>
</soapenv:body>

  document / encoded
<operation name="method1"

style="document" ...>
<input>
<soap:body parts="p1 p2" use="encoded"
encoding=

"http://schemas.xmlsoap.org/soap/encoding/"
namespace="(MessageNS)"/>
</input>
</operation>

在线路上:
<soapenv:body... xmlns:mns="(MessageNS)">
<mns:CompositeType>
<a>123</a>
<b>hello</b>
</mns:CompositeType>
<soapenc:int>123</soapenc:int>
</soapenv:body>

 

<service> 和 <port> 元素

服务是一组 <port> 元素。每个 <port> 元素以一对一的方式将位置与 <binding> 相关联。如果有多个 <port> 元素与同一个 <binding> 相关联,那么其他 URL 位置将用作备用项。

一个 WSDL 文档中可以有多个 <service> 元素。允许多重 <service> 元素有很多用途。其中之一就是按照 URL 目标将端口组合在一起。这样,我就可以用另一个 <service> 重定向我的所有股票报价请求,而同时客户端程序还能继续工作。因为在这种服务组合中,与不同服务关联的协议都是相同的。多重 <service> 元素的另一种用途是根据底层协议对端口进行分类。例如,我可以将所有的 HTTP 端口放在一个 <service> 中,将所有 SMTP 端口放在另一个 <service> 中。然后,我的客户端就可以搜索与它能处理的协议匹配的 <service>。

<service name="FOOService">
     <port name="fooSamplePort" binding="fooSampleBinding">
          <soap:address
            location="
http://carlos:8080/fooService/foo.asp"/>
     </port>
</service>

在同一个 WSDL 文档中,<service> 的“name”属性区分了各个服务。因为一个服务中可能有多个端口,端口也可以有一个“name”属性。

总结

在本文中,我介绍了 WSDL 文档中与 SOAP 相关的最重要的特性。需要指出的是:WSDL 不仅仅能说明 HTTP 上的 SOAP。WSDL 完全能够说明使用 HTTP-POST、HTTP-GET、SMTP 等协议的 SOAP。有了 WSDL,开发者和用户都更容易使用 SOAP。我相信 WSDL 和 SOAP 结合将引入一种通过网络利用 Web 服务的新型应用。

WSDL 在其名称空间中涉及很多 XML 元素。下面的表格总结了这些元素以及它们的属性和内容:

属性 内容(子)
<definitions> name
targetNamespace
xmlns(其他名称空间)
<types>
<message>
<portType>
<binding>
<service>
<types> (无) <xsd:schema>
<message> name <part>
<portType> name <operation>
<binding> name
type
<operation>
<service> name <port>
<part> name
type
(空)
<operation> name
parameterOrder
<input>
<output>
<fault>
<input> name
message
(空)
<output> name
message
(空)
<fault> name

message
(空)
<port> name
binding
<soap:address>

资源:

  1. WSDL 1.1(英文)
  2. SOAP 1.1(英文)
  3. XML 架构入门(英文)
  4. MS SOAP Toolkit 下载网站(英文)
  5. 用于把 IDL 转换为 WSDL 的工具(英文)
  6. 免费的 Web 服务资源,包括 WSDL 到 VB 的代理生成器(英文)
  7. PocketSOAP:SOAP 相关的组件、工具和源代码(英文)

 

相关推荐