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头的。这个头只在请求消息中需要,在响应消息中并不需要。
让许多SOAP新手困惑的是SOAP中没有关于SOAP服务器怎样使用请求头来分发请求的要求;这被留为一个实现上的细节。一些SOAP服务器将映射请求-URIs到类名,并分派调用到静态方法或到在请求持续期内存活的类的实例。其它SOAP服务器则将请求-URIs映射到始终存活的对象,经常是用查询字符串来编码一个用来定位在服务器进程中的对象关键字。还有一些其它的SOAP服务器用HTTP cookies来编码一个对象关键字,这个关键字可被用来在每次方法请求中恢复对象的状态。重要的是客户对这些区别并不知道。客户软件只是简单遵循HTTP和XML的规则来形成SOAP请求,让服务器自由以它认为最合适的方式来为请求服务。
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)分解到正确的实例是反序列化程序的工作。
前面的讨论解释了多引用存取元素怎样与它的目标实例相关联。下面要讨论的是目标实例在哪里被序列化。这就关系到独立元素和包的概念。