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头的。这个头只在请求消息中需要,在响应消息中并不需要。
让许多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规范。