总结:
(本文假设读者对COM和XML技术已经很熟悉。)
SOAP(Simple Object Access Protocal) 技术有助于实现大量异构程序和平台之间的互操作性,从而使存在的应用能够被广泛的用户所访问。SOAP是把成熟的基于HTTP的WEB技术与XML的灵活性和可扩展性组合在了一起。
这篇文章带你全面回顾对象远程进程调用(ORPC)技术的历程,以帮助你理解SOAP技术的基础,以及它克服存在技术(如CORBA和DCOM)的许多缺陷的方法。随后讲述详细的SOAP编码规则,并把焦点放在SOAP是怎样映射到存在的ORPC概念上的。
引言:
当我在1984年开始把计算作为我的职业的时候,大多数程序员并不关心网络协议。但是在九十年代网络变得无所不在,现在如果有谁使用计算机却不使用某种形式网络连接是很难以想象的。今天,一般的程序员对建立可扩展的分布式应用表现出更大的兴趣,而不再只是关注于用MFC实现个性化的可浮动半透明非矩形的Coolbars了。
程序员通常喜欢用编程模型来思考问题,而很少考虑网络协议。尽管这样做通常是很好的,但在这篇文章中我将讨论的SOAP是一个没有明显的编程模型的网络协议。这并不意味着SOAP的体系结构从根本上会改变你编程的方式。相反,SOAP的一个主要目标是使存在的应用能被更广泛的用户所使用。为了实现这个目的,没有任何SOAP API或SOAP 对象请求代理(SOAP ORB),SOAP是假设你将使用尽可能多的存在的技术。几个主要的CORBA厂商已经承诺在他们的ORB产品中支持SOAP协议。微软也承诺在将来的COM版本中支持SOAP。DevelopMentor已经开发了参考实现,它使得在任何平台上的任何Java或Perl程序员都可以使用SOAP。
在SOAP后面的指导理念是“它是第一个没有发明任何新技术的技术”。SOAP采用了已经广泛使用的两个协议:HTTP和XML。HTTP用于实现SOAP的RPC风格的传输,而XML是它的编码模式。采用几行代码和一个XML解析器,HTTP服务器(如MS的IIS或Apache)立刻成为了SOAP的ORBs。 因为目前超过一半的Web服务器采用IIS或Apache, SOAP将会从这两个产品的广泛而可靠的使用中获取利益。这并不意味着所有的SOAP请求必须通过Web服务器来路由,传统的Web 服务器只是分派SOAP请求的一种方式。因此Web服务如IIS或Apache对建立SOAP使能的应用是充分的,但决不是必要的。
正如这篇文章将要描述的,SOAP简单地用XML来编码HTTP的传输内容。SOAP最常用的应用是作为一个RPC协议。为了理解SOAP怎样工作,有必要简要回顾一下RPC协议的历史。
RPCs的历史
建立分布式应用的两个主要通信模型是消息传送(经常与队列组合在一起)和请求/响应。消息传递系统允许通信任何一方在任何时间发送消息。请求/响应协议把通信模式限制在请求/响应的双方。基于消息的应用强烈地意识到它们正在与外部的并行进程进行通信,并且需要一个显式的设计风格。基于请求/响应的应用更象一个单进程的应用,因为发送请求的应用或多或少被阻塞直至收到来自另一个进程的响应。这使得请求/响应通信自然地适合于RPC应用。
尽管消息通信和请求/响应各有他们的优点,他们都是可以用对方来实现的。消息系统可以用较底层的请求/响应协议来建立。如微软的Message Queue Server (MSMQ)内部采用了DCE RPC来建立大多数的控制逻辑。RPC系统也可以采用较底层的消息系统来建立。MSMQ提供的关联 ID正是为了这个目的。不管评价如何,大多数的应用仍趋向于使用RPC协议,因为它们广泛的使用,它们更简单的设计,以及更自然的到传统的编程技术的映射。
在八十年代,两个主要的RPC协议是Sun RPC 和DCE RPC。最流行的Sun RPC应用是大多数UNIX系统所使用的Network File System (NFS)。最流行的DCE RPC应用则是Windows NT?,它采用DCE RPC 协议来实现许多系统服务。这两个协议被证明适用于很大范围的应用。但是,在八十年代末期,面向对象技术的风靡使软件界沉迷于在面向对象语言和基于RPC的通信之间建立一个纽带。
在九十年代产生的对象RPC (ORPC) 协议正是试图把面向对象和网络协议联系起来。ORPC 和 RPC 协议的主要不同是ORPC代码化了从通信终端到语言级对象的映射。在每个ORPC请求的头中都有一个cookie,服务器端的程序能用它来定位在服务器进程中的目标对象。通常这个cookie只是一个对数组的索引,但其它技术也经常被使用,如用符号名作为Hash表的键。
图1 ORPC请求与响应
图1表示一个典型的ORPC请求和响应。有几个请求头组件被服务器端的处理程序用于分发调用。对象端点ID被用于定位在服务器进程中目标对象。接口标识符和方法标识符用于决定在目标对象中哪一个方法被调用。传输体用于传递请求中的[in]和[in,out]参数的值(在响应中是[out]和[in,out])。要注意的是任选的协议扩展可以出现在头文件和传输体之间。这是在协议设计中的惯例,因为它允许新的服务搭载在ORPC的请求和服务上。大多数ORPC系统用这个区域传递附加的上下文信息(如事务信息和因果关系标识符)。
目前两个主要的OPRC协议是DCOM 和 CORBA的 Internet Inter-ORB Protocol (IIOP) 或更一般的General Inter-ORB Protocol (GIOP)。DCOM和IIOP/GIOP的请求格式非常相似。两个协议都用一个对象端点ID来确定目标对象,用方法标识符来决定调用哪个方法。
这两个协议主要有两点不同:主要的一点不同是采用IIOP/GIOP时,接口标识符是隐含的,因为一个给定的CORBA对象只实现一个接口(尽管OMG当前正在进行每个对象有多个接口支持的标准化工作)。DCOM与IIOP/GIOP请求的另一个细微差别是在传输体中参数值的格式。在DCOM中,传输体用网络数据表达(NDR)的格式来写,在IIOP/GIOP中,传输体用公共数据表达(CDR)的格式来写。NDR和 CDR分别处理在各种平台上的不同的数据表达。但是在这两种格式之间有一些小的差别,这使它们相互之间并不兼容。
在ORPC与RPC协议之间的另一个重要的不同是通信端点的命名方式。在ORPC协议中,对于ORPC端点的一些可传递的表达方式被要求在网络之间传递对象引用。在CORBA/IIOP,这个表达方式被称为可交互的对象引用(IOR)。IORs包含用紧凑格式表达的寻址信息,使用了它任何CORBA产品都可以决定一个对象端点。在DCOM中,这种表达方式被称为OBJREF,它组合了分布的引用计算和端点/对象标识。CORBA和DCOM都提供了在网络上寻找对象端点的高级机制,但最终这些机制都映射回到了IORs或OBJREFs。图3是表示一个 IOR/OBJREF 怎样与在IIOP/DCOM请求消息中的寻址信息关联起来的。
目前的技术存在的问题?
尽管DCOM和IIOP都是固定的协议,业界还没有完全转向其中任何一个协议。没有融合的部分原因是文化的问题所致。而且在当一些组织试图标准化一个或另一个协议的时候,两个协议的技术适用性就被提出质疑。传统上认为DCOM和CORBA都是合理服务器到服务器端的通信协议。但是,二者对客户到服务器端的通信都存在明显的弱点,尤其是客户机被散布在Internet上的时候。
DCOM 和 CORBA/IIOP都是依赖于单个厂商的解决方案来最大优势地使用协议。尽管两个协议都在各种平台和产品上被实现了,但现实是选定的发布需要采用单一厂商的实现。在DCOM的情况下,这意味着每个机器要运行在Windows NT。(尽管DCOM已经被转移到其它平台,但它只在Windows?上获得了广泛的延伸)。在CORBA情况下,这意味着每个机器要运行同样的ORB产品。的确让两个CORBA产品用IIOP相互调用是有可能的,但是许多高级的服务(如安全和事务)此时通常不是可交互的。而且,任何专门厂商为同样的机器的通信所作的优化很难起作用,除非所有的应用被建立在同一个ORB产品上。
DCOM 和CORBA/IIOP都依赖于周密管理的环境。两个任意的计算机使得DCOM或IIOP 在环境之外被成功调用(calls out of the box)的几率是很低的。特别是在考虑安全性的时候尤其是这样。尽管写一个能成功地运用DCOM或IIOP的紧缩包(shrink-wrap)应用是可能的,但这样做要比基于socket的应用要更多地关注细节。这对于乏味但必需的配置和安装管理任务特别适用。
DCOM 和 CORBA/IIOP都依赖于相当高技术的运行环境。尽管进程内的COM似乎特别简单,但COM/DCOM远程处理程序绝对不只是几天就解决的事情。IIOP 是一个比DCOM更容易实现的协议,但两个协议都有相当多的深奥的规则来处理数据排列、类型信息和位操作。这使得一般的程序员在没有领会ORB产品或OLE32.DLL的情况下去构造一个简单的CORBA或DCOM调用也变得很困难。
也许对DCOM和CORBA/IIOP来说,最令人难以忍受的一点是它们不能在Internet 上发挥作用。对DCOM来说,一般用户的iMac 或廉价的运行Windows 95的PC 兼容机要想使用你的服务器执行基于领域认证几乎是不可能的。更糟的是,如果防火墙或代理服务器分隔开了客户和服务器的机器,任何IIOP或DCOM包要通过的可能性是很低的,主要是由于大多数Internet连接技术对HTTP协议的偏爱所致。尽管一些厂商如Microsoft, Iona和Visigenic都已经建立了通道技术,但这些产品很容易对配置错误敏感而且它们是不可交互的。
在一个服务器群落中这些问题并不能影响DCOM或IIOP的使用。因为在服务器群落中主机的数量很少(一般是成百上千,而不是成千上万),这就抵消了DCOM基于ping的生命周期管理的成本。在服务器群落中,所有主机被一个公共管理域管理的机率很大,使得统一的配置变得可能。相对少量的机器也能保持商业ORB产品可控制使用的成本,因为只需要更少量的ORB许可权。如果只有IIOP在服务器群落中被使用,就只需要少量的ORB许可权。最后,在服务器群落中所有主机有直接的IP连接也是可能的,这就消除了与防火墙相关的DCOM和 IIOP问题。
HTTP作为一个更好的RPC
在服务器群落中使用DCOM 和CORBA 是通用的做法,但客户机则使用HTTP进入服务器群落。HTTP与RPC的协议很相似,它简单、配置广泛,并且对防火墙比其它协议更容易发挥作用。HTTP请求一般由Web服务器软件(如IIS和Apache)来处理,但越来越多的应用服务器产品正在支持HTTP作为除DCOM和IIOP外的又一个协议。
象DCOM和IIOP一样,HTTP层通过TCP/IP进行请求/响应通信。一个HTTP的客户端用TCP连接到HTTP服务器。在HTTP中使用的标准端口号是80,但任何其它端口也能被使用。在建立TCP连接后,客户端可以发送一个请求消息到服务器端。服务器在处理请求后发回一个HTTP响应消息到客户端。请求和响应消息都可以包含任意的传输体的信息,通常用Content-Length和Content-Type的 HTTP 头来标记。下面是一个合法的HTTP请求消息:
POST /foobar HTTP/1.1
Host: 209.110.197.12
Content-Type: text/plain
Content-Length: 12
Hello, World
你可能已经注意到HTTP头只是一般文本。这使得用包检查程序或基于文本的Internet工具(如telnet)来诊断HTTP问题变得更容易。HTTP基于文本的属性也使得HTTP更容易适用于在Web开发中流行的低技术水平的编程环境。
HTTP请求的第一行包含三个组件:HTTP方法,请求-URI,协议版本。在前面的例子中,这些分别对应于POST, /foobar, 和 HTTP/1.1。Internet工程任务组(IETF)已经标准化了数量固定的HTTP方法。GET是HTTP用来访问Web的方法。 POST是建立应用程序的最常用的HTTP方法。和GET不一样,POST允许任意数据从客户端发送到服务器端。请求URI (Uniform Resource Identifier)是一个HTTP服务器端软件,它用来识别请求的目标的简单的标识符(它更象一个IIOP/GIOP object_key 或一个DCOM IPID)。关于URIs更多的信息请参照"URIs, URLs, and URNs"。在这个例子中协议的版本是HTTP/1.1, 它表示遵守RFC 2616的规则。HTTP/1.1比HTTP/1.0多增加了几个特性,包括对大块数据传输的支持以及对在几个HTTP请求之间保持TCP连接的支持。
请求的第三行和第四行指定了请求体的尺寸和类型。Content-Length 头指定了体信息的比特数。Content-Type类型标识符指定MIME类型为体信息的语法。HTTP (象 DCE一样) 允许服务器和客户端协商用于编制信息的传输语法。大多数DCE应用采用NDR.。大多数Web应用采用text/html 或其它基于文本的语法。
注意在上面样例中Content-Length头与请求体之间的空行。不同的HTTP头被carriage-return/行码序列划定界限。这些头与体之间用另外的carriage-return/行码序列来划定界限。请求接着包括原始字节,这些字节的语法和长度由Content-Length和Content-Type HTTP 头来识别。在这个例子中,内容是十二字节的普通文本字符串"Hello, World"。
在处理了请求之后,HTTP服务器被期望发回一个HTTP响应到客户端。响应必须包括一个状态代码来表示请求的结果。响应也可以包含任意的体信息。下面是一个HTTP响应消息:
200 OK
Content-Type: text/plain
Content-Length: 12
dlroW ,olleH
在这个例子中,服务器返回状态代码200,它是HTTP中标准的成功代码。如果服务器端不能破解请求代码,它将返回下列的响应:
400 Bad Request
Content-Length: 0
如果HTTP服务器决定到目标URI的请求应该临时转向另外的一个不同的URI,下列响将被返回:
307 Temporarily Moved
Location: http://209.110.197.44/foobar
Content-Length: 0
这个响应告知客户,请求将能够通过重新传递它到在Location头中指定的地址来被满足。
所有的标准状态码和头都在RFC 2616中被描述。它们中很少的内容与SOAP用户直接相关,但有一个明显的例外。在HTTP/1.1,底层的TCP连接在多个请求/响应对之间重用。HTTP Connection头允许客户端或服务器中任何一方关闭底层的连接。通过增加下列HTTP头到请求或响应中,双方都会要求在处理请求后关闭它们的TCP连接:
Connection: close
当与HTTP/1.0软件交互时,为了保持TCP连接,建议发送方加入下列HTTP头到每个请求或响应中:
Connection: Keep-Alive
这个头使缺省的HTTP/1.0协议在每次响应后重新开始TCP连接的行为无法使用。
HTTP的一个优点是它正被广泛的使用和接受。图4表示了一个简单的Java程序,它发送前面表示的请求并从响应中解析出结果字符串。
下面则是一个简单的C程序用CGI来读取来自HTTP请求的字符串并通过HTTP响应把它的逆序串返回。
#include <stdio.h>
int main(int argc, char **argv) {
char buf[4096];
int cb = read(0, buf, sizeof(buf));
buf[cb] = 0;
strrev(buf);
printf("200 OK\r\n");p>
printf("Content-Type: text/plain\r\n");
printf("Content-Length: %d\r\n", cb);
printf("\r\n");
printf(buf);
return 0;
图5表示了一个更流行的版本,服务器的实现是用Java servlet,以避免CGI的每个请求一个进程的开销。
一般来说CGI是花费代价最小的写HTTP服务器端代码的方法。实际上,每一个HTTP服务器端产品都提供了一个更有效的机制来让你的代码处理一个HTTP请求。IIS提供了ASP和ISAPI作为写HTTP代码的机制。Apache允许你用运行在Apache后台程序中的 C或Perl来写模块。大多数应用服务器软件允许你写Java servlet,COM组件,EJB Session beans或基于可携带对象适配器(POA)接口的CORBA servants。
XML 作为一个更好的网络数据表达方式(NDR)
HTTP是一个相当有用的RPC协议,它提供了IIOP或DCOM在组帧、连接管理以及序列化对象应用等方面大部分功能的支持。( 而且URLs与IORs和OBJREFs在功能上令人惊叹的接近)。HTTP所缺少的是用单一的标准格式来表达一个RPC调用中的参数。这则正是XML的用武之地。
象NDR和CDR,XML是一个与平台无关的中性的数据表达协议。XML允许数据被序列化成一个可以传递的形式,使得它容易地在任何平台上被解码。XML有以下不同于NDR和CDR的特点:
有大量XML编码和解码软件存在于每个编程环境和平台上
XML基于文本,相当容易用低技术水平的编程环境来处理
XML是特别灵活的格式,它容易用一致的方式来被扩展
为支持可扩展性,在XML中每一个元素和属性有一个名域URI与它相联系,这个URI用xmlns属性来指定。
考虑下面的XML文档:
<reverse_string xmlns="urn:schemas-develop-com:StringProcs">
<string1>Hello, World</string1>
<comment xmlns='http://foo.com/documentation'>
This is a comment!!
</comment>
</reverse_string>
元素<reverse_string>和<string1>的名域URI是urn:schemas-develop-com:StringProcs。元素<comment>的名域URI是http://foo.com/documentation。第二个URI也是一个URL的事实是不重要的。在这两种情况下,URI简单地被用来消除元素<reverse_string>,<string1>,<comment>和任何碰巧有同样标记名的其它元素间的歧义。
为了方便,XML允许名域URIs被映射为局部唯一的前缀。这意味着下面的XML文档在语义上等同于上面的文档:
<sp:reverse_string
xmlns:sp="urn:schemas-develop-com:StringProcs"
xmlns:doc='http://foo.com/documentation'
>
<sp:string1>Hello, World</sp:string1>
<doc:comment>
This is a comment!!
</doc:comment>
</sp:reverse_string>
后面的形式对作者来说更容易,尤其是如果有许多名域URIs在使用时。
XML也支持带类型的数据表达。正在推出的XML Schema规范为描述XML数据类型标准化了一个词汇集。下面是一个元素<reverse_string>的XML Schema的描述:
<schema
xmlns='http://www.w3.org/1999/XMLSchema'
targetNamespace='urn:schemas-develop-com:StringProcs'
>
<element name='reverse_string'>
<type>
<element name='string1' type='string' />
<any minOccurs='0' maxOccurs='*'/>
</type>
</element>
</schema>
这个XML Schema定义阐述了XML名域urn:schemas-develop-com:StringProcs包含了一个名为<reverse_string>的元素,这个元素包含了一个名为string1的子元素(类型为string),它被0个或更多没有指定的元素所遵守。
XML Schema 规范还定义了一组内置的原始数据类型和建立一个XML文档中元素的类型的机制。下面的XML文档用XML Schema类型属性来把元素和类型名联系在一起:
<customer
xmlns='http://customer.is.king.com'
xmlns:xsd='http://www.w3.org/1999/XMLSchema'
>
<name xsd:type='string'>Don Box</name>
<age xsd:type='float'>23.5</name>
</customer>
连接XML文档事例到XML Schema描述的新的一个机制在本文写作的时候正在标准化过程中。
HTTP + XML = SOAP
SOAP把XML的使用代码化为请求和响应参数编码模式,并用HTTP作传输。这似乎有点抽象。具体地讲,一个SOAP方法可以简单地看作遵循SOAP编码规则的HTTP请求和响应。一个SOAP终端则可以看作一个基于HTTP的URL,它用来识别方法调用的目标。象CORBA/IIOP一样,SOAP不需要具体的对象被绑定到一个给定的终端,而是由具体实现程序来决定怎样把对象终端标识符映射到服务器端的对象。
SOAP请求是一个HTTP POST请求。SOAP请求的content-type必须用text/xml。而且它必须包含一个请求-URI。服务器怎样解释这个请求-URI是与实现相关的,但是许多实现中可能用它来映射到一个类或者一个对象。一个SOAP请求也必须用SOAPMethodName HTTP头来指明将被调用的方法。简单地讲,SOAPMethodName头是被URI指定范围的应用相关的方法名,它是用#符作为分隔符将方法名与URI分割开:
SOAPMethodName: urn:strings-com:IString#reverse
这个头表明方法名是reverse,范围URI是urn:strings-com:Istring。 在SOAP中,规定方法名范围的名域URI在功能上等同于在DCOM 或 IIOP中规定方法名范围的接口ID。
简单的说,一个SOAP请求的HTTP体是一个XML文档,它包含方法中[in]和[in,out]参数的值。这些值被编码成为一个显著的调用元素的子元素,这个调用元素具有SOAPMethodName HTTP头的方法名和名域URI。调用元素必须出现在标准的SOAP <Envelope>和<Body>元素内(后面会更多讨论这两个元素)。下面是一个最简单的SOAP方法请求:
POST /string_server/Object17 HTTP/1.1
Host: 209.110.197.2
Content-Type: text/xml
Content-Length: 152
SOAPMethodName: urn:strings-com:IString#reverse
<Envelope>
<Body>
<m:reverse xmlns:m='urn:strings-com:IString'>
<theString>Hello, World</theString>
</m:reverse>
</Body>
</Envelope>
SOAPMethodName头必须与<Body>下的第一个子元素相匹配,否则调用将被拒绝。这允许防火墙管理员在不解析XML的情况下有效地过滤对一个具体方法的调用。
SOAP响应的格式类似于请求格式。响应体包含方法的[out]和 [in,out]参数,这个方法被编码为一个显著的响应元素的子元素。这个元素的名字与请求的调用元素的名字相同,但以Response后缀来连接。下面是对前面的SOAP请求的SOAP响应:
200 OK
Content-Type: text/xml
Content-Length: 162
<Envelope>
<Body>
<m:reverseResponse xmlns:m='urn:strings-com:IString'>
<result>dlroW ,olleH</result>
</m:reverseResponse>
</Body>
</Envelope>
这里响应元素被命名为reverseResponse,它是方法名紧跟Response后缀。要注意的是这里是没有SOAPMethodName HTTP头的。这个头只在请求消息中需要,在响应消息中并不需要。
图6和图7表明SOAP是怎样与以前讨论的ORPC协议相互对应的。让许多SOAP新手困惑的是SOAP中没有关于SOAP服务器怎样使用请求头来分发请求的要求;这被留为一个实现上的细节。一些SOAP服务器将映射请求-URIs到类名,并分派调用到静态方法或到在请求持续期内存活的类的实例。其它SOAP服务器则将请求-URIs映射到始终存活的对象,经常是用查询字符串来编码一个用来定位在服务器进程中的对象关键字。还有一些其它的SOAP服务器用HTTP cookies来编码一个对象关键字,这个关键字可被用来在每次方法请求中恢复对象的状态。重要的是客户对这些区别并不知道。客户软件只是简单遵循HTTP和XML的规则来形成SOAP请求,让服务器自由以它认为最合适的方式来为请求服务。
SOAP体的核心
SOAP的XML特性是为把数据类型的实例序列化成XML的编码模式。为了达到这个目的,SOAP不要求使用传统的RPC风格的代理。而是一个SOAP方法调用包含至少两个数据类型:请求和响应。考虑这下面个COM IDL代码:
[ uuid(DEADF00D-BEAD-BEAD-BEAD-BAABAABAABAA) ]
interface IBank : IUnknown {
HRESULT withdraw([in] long account,
[out] float *newBalance,
[in, out] float *amount
[out, retval] VARIANT_BOOL *overdrawn);
}
在任何RPC协议下,account和amount参数的值将出现在请求消息中,newBalance,overdrawn参数的值,还有amount参数的更新值将出现在响应消息中。
SOAP把方法请求和方法响应提升到了一流状态。在SOAP中,请求和响应实际上类型的实例。为了理解一个方法比如IBank::withdraw怎样映射一个SOAP请求和响应类型,考虑下列的数据类型:
struct withdraw {
long account;
float amount;
};
这是一个所有的请求参数被打包成为一个单一的数据类型。同样下面的数据表示打包所有响应参数到一个单一的数据类型。
struct withdrawResponse {
float newBalance;
float amount;
VARIANT_BOOL overdrawn;
};
再给出下面的简单的Visual Basic程序,它使用了以前定义的Ibank接口:
Dim bank as IBank
Dim amount as Single
Dim newBal as Single
Dim overdrawn as Boolean
amount = 100
Set bank = GetObject("soap:http://bofsoap.com/am")
overdrawn = bank.withdraw(3512, amount, newBal)
你能够想象底层的代理(可能是一个SOAP,DCOM,或IIOP代理)看上去象图8中所表示的那样。这里,在发送请求消息之前,参数被序列化成为一个请求对象。同样被响应消息接收到的响应对象被反序列化为参数。一个类似的转变同样发生在调用的服务器端。
当通过SOAP调用方法时,请求对象和响应对象被序列化成一种已知的格式。每个SOAP体是一个XML文档,它具有一个显著的称为<Envelope>的根元素。标记名<Envelope>由SOAP URI (urn:schemas-xmlsoap-org:soap.v1)来划定范围,所有SOAP专用的元素和属性都是由这个URI来划定范围的。SOAP Envelope包含一个可选的<Header>元素,紧跟一个必须的<Body>元素。<Body>元素也有一个显著的根元素,它或者是一个请求对象或者是一个响应对象。下面是一个IBank::withdraw请求的编码:
<soap:Envelope
xmlns:soap='urn:schemas-xmlsoap-org:soap.v1'>
<soap:Body>
<IBank:withdraw xmlns:IBank=
'urn:uuid:DEADF00D-BEAD-BEAD-BEAD-BAABAABAABAA'>
<account>3512</account>
<amount>100</amount>
</IBank:withdraw>
</soap:Body>
</soap:Envelope>
下列响应消息被编码为:
<soap:Envelope
xmlns:soap='urn:schemas-xmlsoap-org:soap.v1'>
<soap:Body>
<IBank:withdrawResponse xmlns:IBank=
'urn:uuid:DEADF00D-BEAD-BEAD-BEAD-BAABAABAABAA'>
<newBalance>0</newBalance>
<amount>5</amount>
<overdrawn>true</overdrawn>
</IBank:withdrawResponse>
</soap:Body>
</soap:Envelope>
注意[in, out]参数出现在两个消息中。
在检查了请求和响应对象的格式后,你可能已经注意到序列化格式通常是:
<t:typename xmlns:t='namespaceuri'> ;
<fieldname1>field1value</fieldname1>
<fieldname2>field2value</fieldname2>
</t:typename>
在请求的情况下,类型是隐式的C风格的结构,它由对应方法中的[in]和[in, out]参数组成。对响应来说,类型也是隐式的C风格的结构,它由对应方法中的[out]和[in, out]参数组成。这种每个域对应一个子元素的风格有时被称为元素正规格式(ENF)。一般情况下,SOAP只用XML特性来传达描述包含在元素内容中信息的注释。
象DCOM和IIOP一样,SOAP支持协议头扩展。SOAP用可选的<Header>元素来传载被协议扩展所使用的信息。如果客户端的SOAP软件包含要发送头信息,原始的请求将可能如图9所示。在这种情况下命名causality的头将与请求一起序列化。收到请求后,服务器端软件能查看头的名域URI,并处理它识别出的头扩展。这个头扩展被http://comstuff.com URI识别,并期待一个如下的对象:
struct causality {
UUID id;
};
在这种情况下的请求,如果头元素的URI不能被识别,头元素可以被安全地忽略。
但你不能安全的忽略所有的SOAP体中的头元素。如果一个特定的SOAP头对正确处理消息是很关键的,这个头元素能被用SOAP属性mustUnderstand=’true’标记为必须的。这个属性告诉接收者头元素必须被识别并被处理以确保正确的使用。为了强迫前面causality头成为一个必须的头,消息将被写成如下形式:
<soap:Envelope
xmlns:soap='urn:schemas-xmlsoap-org:soap.v1'>
<soap:Header>
<causality
soap:mustUnderstand='true'
xmlns="http://comstuff.com">
<id>362099cc-aa46-bae2-5110-99aac9823bff</id>
</causality>
</soap:Header>
<!— soap:Body element elided for clarity —>
</soap:Envelope>
SOAP软件遇到不能识别必须的头元素情况时,必须拒绝这个消息并出示一个错误。如果服务器在一个SOAP请求中发现一个不能识别的必须的头元素,它必须返回一个错误响应并且不发送任何调用到目标对象。如果客户端在一个SOAP请求中发现一个不能识别出的必须的头元素,它必须向调用者返回一个运行时错误。(在COM情况下,这将映射为一个明显的HRESULT)
SOAP数据类型
在SOAP消息中,每个元素可能是一个SOAP结构元素,一个根元素,一个存取元素或一个独立的元素。在SOAP中,soap:Envelope, soap:Body和 soap:Header 是唯一的三个结构元素。它们的基本关系由下列XML Schema所描述:
<schema
targetNamespace='urn:schemas-xmlsoap-org:soap.v1'>
<element name='Envelope'>
<type>
<element name='Header' type='Header'
minOccurs='0' />
<element name='Body' type='Body'
minOccurs='1' />
</type>
</element>
</schema>
在SOAP元素的四种类型中,除了结构元素外都被用作表达类型的实例或对一个类型实例的引用。
根元素是显著的元素,它是soap:Body 或是 soap:Header的直接的子元素。其中soap: Body只有一个根元素,它表达调用、响应或错误对象。这个根元素必须是soap:Body的第一个子元素,它的标记名和域名URI必须与HTTP SOAPMethodName头或在错误消息情况下的soap:Fault相对应。而soap:Header元素有多个根元素,与消息相联系的每个头扩展对应一个。这些根元素必须是soap:Header的直接子元素,它们的标记名和名域URI表示当前存在扩展数据的类型。
存取元素被用作表达类型的域、属性或数据成员。一个给定类型的域在它的SOAP表达将只有一个存取元素。存取元素的标记名对应于类型的域名。考虑下列Java 类定义:
package com.bofsoap.IBank;
public class adjustment {
public int account ;
public float amount ;
}
在一个SOAP消息中被序列化的实例如下所示:
<t:adjustment
xmlns:t='urn:develop-com:java:com.bofsoap.IBank'>
<account>3514</account>
<amount>100.0</amount>
</t:adjustment>
在这个例子中,存取元素account和amount被称着简单存取元素,因为它们访问对应于在W3C XML Schema规范 (见 http://www.w3.org/TR/XMLSchema-2) 的Part 2中定义的原始数据类型的值。这个规范指定了字符串,数值,日期等数据类型的名字和表达方式以及使用一个新的模式定义中的<datatype>结构来定义新的原始类型的机制。
对引用简单类型的存取元素,元素值被简单地编码为直接在存取元素下的字符数据,如上所示。对引用组合类型的存取元素(就是那些自身用子存取元素来构造的存取元素),有两个技术来对存取元素进行编码。最简单的方法是把被结构化的值直接嵌入在存取元素下。考虑下面的Java类定义:
package com.bofsoap.IBank;
public class transfer {
public adjustment from;
public adjustment to;
}
如果用嵌入值编码存取元素,在SOAP中一个序列化的transfer对象如下所示:
<t:transfer
xmlns:t='urn:develop-com:java:com.bofsoap.IBank'
>
<from>
<account>3514</account>
<amount>-100.0</amount>
</from>
<to>
<account>3518</account>
<amount>100.0</amount>
</to>
</t:transfer>
在这种情况下,adjustment对象的值被直接编码在它们的存取元素下。
在考虑组合存取元素时,需要说明几个问题。先考虑上面的transfer类。类的from和to的域是对象引用,它可能为空。SOAP用XML Schemas的null属性来表示空值或引用。下面例子表示一个序列化的transfer对象,它的from域是空的:
<t:transfer
xmlns:t='urn:develop-com:java:com.bofsoap.IBank'
xmlns:xsd='http://www.w3.org/1999/XMLSchema/instance'
>
<from xsd:null='true' />
<to>
<account>3518</account>
<amount>100.0</amount>
</to>
</t:transfer>
在不存在的情况下, xsd:null属性的隐含值是false。给定元素的能否为空的属性是由XML Schema定义来控制的。例如下列XML Schema将只允许from存取元素为空:
<type name='transfer' >
<element
name='from'
type='adjustment'
nullable='true'
/>
<element
name='to'
type='adjustment'
nullable='false' <!— false is the default —>
/>
</type>
在一个元素的Schema声明中如果没有nullable属性,就意味着在一个XML文档中的元素是不能为空的。Null存取元素的精确格式当前还在修订中�要了解用更多信息参考最新版本的SOAP规范。
与存取元素相关的另一个问题是由于类型关系引起的可代换性。由于前面的adjustment类不是一个final类型的类,transfer对象的from和to域实际引用继承类型的实例是可能的。为了支持这种类型兼容的替换,SOAP使用一个名域限定的类型属性的XML Schema约定。这种类型属性的值是一个对元素具体的类型的限制的名字。考虑下面的adjustment扩展类:
package com.bofsoap.IBank;
public class auditedadjustment extends adjustment {
public int auditlevel;
}
给出下面Java语言:
transfer xfer = new transfer();
xfer.from = new auditedadjustment();
xfer.from.account = 3514; xfer.from.amount = -100;
xfer.from.auditlevel = 3;
xfer.to = new adjustment();
xfer.to.account = 3518; xfer.from.amount = 100;
在SOAP中transfer对象的序列化形式如下所示:
<t:transfer
xmlns:xsd='http://www.w3.org/1999/XMLSchema'
xmlns:t='urn:develop-com:java:com.bofsoap.IBank'
>
<from xsd:type='t:auditedadjustment' >
<account>3514</account>
<amount>-100.0</amount>
<auditlevel>3</auditlevel >
</from>
<to>
<account>3518</account>
<amount>100.0</amount>
</to>
</t:transfer>
在这里xsd:type属性引用一个名域限定的类型名,它能被反序列化程序用于实例化对象的正确类型。因为to存取元素引用到一个被预料的类型的实例(而不是一个可代替的继承类型),xsd:type属性是不需要的。
刚才的transfer类设法回避了一个关键问题。如果正被序列化的transfer对象用下面这种方式初始化将会发生什么情况:
transfer xfer = new transfer();
xfer.from = new adjustment();
xfer.from.account = 3514; xfer.from.amount = -100;
xfer.to = xfer.from;
基于以前的议论,在SOAP 中transfer对象的序列化形式如下所示:
<t:transfer
xmlns:t='urn:develop-com:java:com.bofsoap.IBank'>
<from>
<account>3514</account>
<amount>-100.0</amount>
</from>
<to>
<account>3514</account>
<amount>-100.0</amount>
</to>
</t:transfer>
这个表达有两个问题。首先最容易理解的问题是同样的信息被发送了两次,这导致了一个比实际所需要消息的更大的消息。一个更微妙的但是更重要的问题是由于反序列化程序不能分辨两个带有同样值的adjustment对象与在两个地方被引用的一个单一的adjustment对象的区别,两个存取元素间的身份关系就被丢失。如果这个消息接收者已经在结果对象上执行了下面的测试,(xfer.to == xfer.from)将不会返回true。
void processTransfer(transfer xfer) {
if (xfer.to == xfer.from)
handleDoubleAdjustment(xfer.to);
else
handleAdjustments(xfer.to, xfer.from);
}
(xfer.to.equals(xfer.from))可能返回true的事实只是比较了两个存取元素的值而不是它们身份。
为了支持必须保持身份关系的类型的序列化,SOAP支持多引用存取元素。目前我们接触到的存取元素是单引用存取元素,也就是说,元素值是嵌入在存取元素下面的,而且其它存取元素被允许引用那个值(这很类似于在NDR中的[unique]的概念)。多引用存取元素总是被编码为只包含已知的soap:href属性的空元素。soap:href属性总是包含一个代码片段标识符,它对应于存取元素引用到的实例。如果to和from存取元素已经被编码为多引用存取元素,序列化的transfer对象如下所示:
<t:transfer
xmlns:t='urn:develop-com:java:com.bofsoap.IBank'>
<from soap:href='#id1' />
<to soap:href='#id1' />
</t:transfer>
这个编码假设与adjustment类兼容的一个类型的实例已经在envelope中的其它地方被序列化,而且这个实例已经被用soap:id属性标记,如下所示:
<t:adjustment soap:id='id1'
xmlns:t='urn:develop-com:java:com.bofsoap.IBank'>
<account>3514</account>
<amount>-100.0</amount>
</t:adjustment>
对多引用存取元素,把代码段的标识符(例如#id1)分解到正确的实例是反序列化程序的工作。
前面的讨论解释了多引用存取元素怎样与它的目标实例相关联。下面要讨论的是目标实例在哪里被序列化。这就关系到独立元素和包的概念。
独立元素
在SOAP中,一个独立元素表示至少被一个多引用存取元素引用的类型的实例。所有的独立元素用soap:id属性作标记,而且这个属性的值在整个SOAP envelope中必须是唯一的。独立的元素被编码就好象是它们被一个存取元素打包,这个存取元素的标记名是实例的名域限制的类型名。在上面的例子中,实例的名域限制的类型名是t:adjustment。
SOAP限制独立元素能被编码的场所。SOAP定义了一个能适用于任何元素的属性:(soap:Package)。这个属性被用于控制独立元素能在哪里被解码。SOAP序列化规则指出独立元素必须编码为soap:Header元素或soap:Body元素的直接子元素,或者是任何其它标记为soap:Package=‘true’的元素。通过把一个元素注释为包,你能保证编码那个实例的XML元素是完全自包含的,并且在这个包以外没有任何引用到这个元素的多引用存取元素。
假设transfer 类对应于一个方法请求。如果transfer类型不是一个包,被to和from存取元素引用的独立元素将作为soap:Body元素的直接子元素出现,如图10所示。如果transfer类型是一个合法的SOAP包类型,编码可能象图11所示。注意,因为transfer元素是一个包,所有多引用存取器元素都引用被包含的元素。这使得把transfer元素看成一个能从它的父辈元素中分离出的独立的XML代码段变得更为容易。
多引用存取元素总是引用独立元素的模型是有一个例外的。SOAP允许包含字符串和二进值数据的存取元素是多引用存取元素的目标。这意味着下面的代码是合法的:
<t:mytype>
<field1 soap:href="#id1" />
<field2 soap:id="id1">Hello, SOAP</field2>
</t:mytype>
尽管事实是存取元素2有一个soap:id属性,它实际上是一个存取元素而不是独立元素。
SOAP数组
数组被编码为组合类型的一个特殊的例子。在SOAP中,一个数组必须有一个秩(维数)和一个容量。一个数组被编码为一个组合类型,其中每一个数组元素被编码为一个子元素,这个子元素的名字是元素的名域限制的类型名。
假设有下面的COM IDL类型定义:
struct POINTLIST {
long cElems;
[size_is(cElems)] POINT points[];
};
这个类型的实例将被序列化为:
<t:POINTLIST xmlns:t='uri for POINTLIST'>
<cElems>3</cElems>
<points xsd:type='t:POINT[3]' >
<POINT><x>3</x><y>4</y></POINT>
<POINT><x>7</x><y>5</y></POINT>
<POINT><x>1</x><y>9</y></POINT>
</points>
<t:POINTLIST>
如果points域被标记为[ptr]属性,这个编码将用一个多引用存取元素,如下所示:
<t:POINTLIST xmlns:t='uri for POINTLIST'>
<cElems>3</cElems>
<points soap:href="#x9" />
</t:POINTLIST>
<t:ArrayOfPOINT soap:id='x9' xsd:type='t:POINT[3]'>
<POINT><x>3</x><y>4</y></POINT>
<POINT><x>7</x><y>5</y></POINT>
<POINT><x>1</x><y>9</y></POINT>
</t:ArrayOfPOINT>
当把一个数组编码为一个独立元素时,标记名是带前缀ArrayOf的类型名。
象NDR和CDR一样,SOAP支持部分转换的数组。如果子元素的数量少于所声明的容量,这些元素被假设正从数组的末尾丢失。这能够通过在正包含的数组元素上使用soap:offset属性来被忽略。
<t:ArrayOfPOINT soap:id='x9' xsd:type='t:POINT[5]'
soap:offset='[1]'>
<POINT><x>1</x><y>9</y></POINT>
</t:ArrayOfPOINT>
soap:offset属性表示出现在数组中的第一个元素的索引。在上面的例子中,元素0,2到4都是不被转换的。SOAP也支持稀疏数组,这是通过使用soap:position属性来把每个元素用它的绝对索引来注释而实现的:
<t:ArrayOfPOINT soap:id='x9' xsd:type='t:POINT[9]'>
<POINT soap:position='[3]'><x>3</x><y>4</y></POINT>
<POINT soap:position='[7]'><x>4</x><y>5</y></POINT>
</t:ArrayOfPOINT>
在这个例子中,元素0到2,4到6,以及8到9都不是被转换的。
请注意,在SOAP中数组的精确语法在这篇文章写作时还在被重新审查以调整到即将推出的W3C XML Schema规范中。要不断了解SOAP规范的最新版本来获得更多的细节。
错误处理
一个服务器有时将不能正确地为一个方法请求提供服务。这可能是由于一般的HTTP错误造成的(如请求-URI不能被映射到本地的资源或一个HTTP级的安全违反)。也可能是在SOAP翻译软件中的问题,如马歇尔打包错误或一个必须的头不能被认出。其它可能的原因包括一个请求不能正确地被服务,或者应用/对象代码决定要返回一个应用级的错误给调用者。这些情况在SOAP规范中都被清楚地加以处理。
如果在分发对任何SOAP代码的调用之前一个错误发生在HTTP层,一个纯HTTP响应必须被返回。标准的HTTP状态代码编号将被采用,400级的代码表示一个客户引发的错误,500级的代码表示服务器引发的错误。这通常在代码执行前由Web服务器软件自动处理。
假设在HTTP层一切正常,错误发生的下一个地方是在那些翻译和分发对应用代码(如COM对象和CORBA伺服对象)的SOAP调用。如果错误发生在这一层,服务器必须返回一个错误消息来代替一个标准的响应消息。一个错误消息是下列被编码为soap:Body的根元素的类型的实例。
<schema
targetNamespace='urn:schemas-xmlsoap-org:soap.v1'
>
<element name='Fault'>
<type>
<element name='faultcode' type='string' />
<element name='faultstring' type='string' />
<element name='runcode' type='string' />
<element name='detail' />
</type>
</element>
</schema>
faultcode存取元素必须包含一个用已知的整数表示的SOAP错误代码或者一个专门应用的名域限制的值。当前的SOAP 错误代码如图12所示。Faultstring存取元素包含对发生的错误的可读性的描述。runcode 存取元素包含一个字符串,它的值必须是Yes, No或 Maybe,表明被请求的操作实际上是否在错误产生之前被执行。Detail存取元素是可选的,用于包含一个专门应用的异常对象。
下面是一个对应于一个包含无法识别的必须的头元素的请求的SOAP错误的例子:
<soap:Envelope
xmlns:soap='urn:schemas-xmlsoap-org:soap.v1'
>
<soap:Body>
<soap:Fault> ;
<faultcode>200</faultcode>
<faultstring>
Unrecognized 'causality' header
</faultstring>
<runcode>No</runcode>
</soap:Fault>
</soap:Body>
</soap:Envelope>
假设具体应用的错误需要被返回,你可能看到如图13所示的代码。在应用定义的错误的情况下,考虑应用的异常/错误对象时detail存取元素起到了soap:Body 元素的作用。
奥秘
一个遗留的HTTP问题还需要进一步阐明。SOAP支持(但不需要)HTTP扩展框架约定来指定必须的HTTP头扩展。这些约定主要有两个目的。首先,它们允许任意的URI被用于限定给定的HTTP头的范围(象XML名域一样)。第二,这些约定允许把必须的头与可选的头区分开来(象soap:mustUnderstand)。下面是一个使用HTTP扩展框架来把SOAPMethodName头定义成为一个必须的头扩展:
M-POST /foobar HTTP/1.1
Host: 209.110.197.2
Man: "urn:schemas-xmlsoap-org:soap.v1; ns=42"
42-SOAPMethodName: urn:bobnsid:IFoo#DoIt
Man头映射SOAP URI到前缀为42的头,并表示没有认出SOAP的服务器必须返回一个HTTP错误,状态代码为501 (没有被实现) 或 510 (没有被扩展)。HTTP方法必须是M-POST,表明目前是必须的头扩展。
结论
SOAP是一个被类型化的序列化格式,它恰巧用HTTP 作为请求/响应消息传输协议。SOAP被设计为与正将出现的XML Schema规范密切配合,并支持在Internet的任何地方运行的COM, CORBA, Perl, Tcl, 和 Java-language, C, Python, 或 PHP 等程序间的互操作性。
希望本文给了你一个对这个协议具体细节的更清晰的理解。我鼓励你用SOAP进行实验,或者试着使用SOAP使能的系统之一(列在http://www.develop.com/soap/),或者自己做一些工作。我本人发现采用脚本语言(Jscript),使一个基本的SOAP客户与服务器建立并运行只花费了不到一个小时。针对你对HTTP和XML的熟悉程度,以及你的目标平台的成熟度,你所花费的时间会有所不同。
关键词:容易对象访问协议(SOAP)初级向导