级别: 初级 David Mertz,博士 (mertz@gnosis.cx), 归属者, Gnosis Software, Inc.
2001 年 12 月 01 日 确实,XML-RPC 有其自身的缺点,但这个函数调用协议也非常适合多种任务。在本文中,专栏作家 David Mertz 将 XML-RPC 作为建立对象数据模型的方法进行研究,并且响应读者的反馈,将 XML-RPC 作为序列化对象的方法与他早期专栏文章中讨论的
xml_pickle 模块进行了比较。代码样本详细说明了这个比较。
XML-RPC 是一个具有很大价值的远程函数调用协议:它比所有竞争对手都差。与 Java RMI 或 CORBA 或 COM 相比,XML-RPC 可以发送的数据类型很少,而其消息的大小却很庞大。XML-RPC 滥用 HTTP 协议以避开完全有必要存在的防火墙,因此会发送无状态消息并造成通道瓶颈。与 SOAP 相比,XML-RPC 缺少了重要的安全性机制和健壮的对象模型。作为数据表示法,与许多本机编程语言机制(如 Java 的
serialize 、Python 的
pickle 、Perl 的
Data::Dumper 或 Ruby、Lisp、PHP 和许多其它语言的类似模块)相比,XML-RPC 显得缓慢、笨拙且不完整。
换句话说,XML-RPC 是 Richard Gabriel 关于软件设计的“更坏就是更好”理念(请参阅
参考资料)的完美体现。对于 XML-RPC,我几乎不能写出比上一段中更生动的描述,而且我认为这个协议非常适合许多大型的任务。要理解其中原因,值得引用 Gabriel 的“更坏就是更好”理念原则:
-
简单性:在实现和接口中,设计必须简单。相对于接口而言,实现的简单性更为重要。简单性是设计中最重要的注意事项。
-
正确性:在所有可观察的方面,设计都必须正确。正确性的重要程度仅次于简单性。
-
一致性:设计不能过于不一致。在某些情况下,为了实现简单性可以牺牲一致性,但放弃设计中处理非常见情况的那些部分比引入实现复杂性或不一致性更好。
-
完整性:实际上,设计必须覆盖许多重要的情况。应该覆盖可能出现所有情况。为保证其它质量,可以牺牲完整性。实际上,只要危害到实现简单性,就必须牺牲完整性。如果保证了简单性,可以牺牲一致性以实现完整性;尤其是在接口的一致性没有价值的情况下。
Gabriel 在 XML-RPC 特殊技术出现几年之前,他就在文章中非常好地指出了它的优点。
xml_pickle 还是 XML-RPC?
我已经为 Python 编写了一个比较流行的模块,叫作
xml_pickle 。这个模块(在本专栏中以前的文章中讨论过,请参阅
参考资料)旨在通过使用与那些标准
cPickle 和
pickle 模块非常相似的接口来序列化 Python 对象。唯一的区别是在我的模块中,表示法是 XML 格式的。我编写
xml_pickle 的意图是创建一种还可以从其它编程语言(以及不同的 Python 版本)读取的非常轻量型的格式。该模块还附带有一个 DTD,以供要验证 XML pickle 的用户使用,但来自用户的反馈却表示正式验证很少引起关注。
我不断从用户那里收到关于
xml_pickle 的一个问题是考虑到 XML-RPC 有更广泛的用途并且在很多编程语言中都有其实现,它是否是一个更好的选择。虽然这个狭隘问题的答案可能偏向于
xml_pickle ,但还是值得进行比较 ― 而且它会引出关于数据类型丰富性的问题。
乍看起来,XML-RPC 似乎做着与
xml_pickle 不同的事情:XML-RPC 调用远程过程并取回结果。清单 1 中的典型用法示例出现在 XML-RPC 网站上以及
Programming Web Services with XML-RPC一书(请参阅
参考资料)中:
清单 1. XML-RPC 用法的 Python shell 示例
>>>
import
xmlrpclib
>>> betty = xmlrpclib.Server(
"http://betty.userland.com"
)
>>>
print
betty.examples.getStateName(41)
South Dakota
|
相反,
xml_pickle 创建了本地内存中的对象的字符串表示。这些看起来也许不同,但为了调用远程过程,XML-RPC 首先需要将其参数转换成合适的 XML 表示法(换句话说,pickle/序列化这些参数)。同样,XML-RPC 调用的返回值可以包含嵌套的数据结构。而且,
xmlrpclib 的
.dumps() 方法与一个
xml_pickle 模块的名称相同(都受到几个标准模块的启发),并且做相同的事 ― 编写 XML 序列化,而不执行实际调用。
乍看起来,
xml_pickle 和
xmlrpclib 在功能上是可以互换的,至少在您只关心序列化问题时是这样的。但我们将会看到,更细致的研究会揭露一些差异。
表示对象
让我们创建一个对象,然后使用两种不同的方法来对它进行序列化。将会出现一些对比:
清单 2. XML-RPC 序列化的 Python shell 示例
>>>
import
xmlrpclib
>>>
class
C:
pass
..
>>> c = C()
>>> c.bool, c.int, c.tup = (xmlrpclib.True, 37, (11.2,
'spam'
) )
>>>
print
xmlrpclib.dumps((c,),
'PyObject'
)
<
?
xml version=
'1.0'
?
>
<methodCall>
<methodName>PyObject</methodName>
<params>
<param>
<value><struct>
<member>
<name>tup</name>
<value><array><data>
<value><double>
11.2</double></value>
<value><string>spam</string></value>
</data></array></value>
</member>
<member>
<name>bool</name>
<value><boolean>
1</boolean></value>
</member>
<member>
<name>int</name>
<value><int>
37</int></value>
</member>
</struct></value>
</param>
</params>
</methodCall>
|
您应该已经注意到了一些事。首先,整个 XML 文档有一个根
<methodCall> 元素,它与我们的当前目的无关。然而,除了多一些字节外,附加的包含元素并不重要。同样,
<methodName> 也是多余的,但该示例给出了表明文档角色的名称。而且,对
xmlrpclib.dumps() 的调用接受一组对象,但我们只对“pickle”一个对象感兴趣(如果有其它对象,它们会有自己的
<param> 元素)。但与某些封装不同,我们的对象的属性都很好地包含在
<struct> 元素的
<member> 元素中。
现在,让我们看一下
xml_pickle 做了什么(对象同上):
清单 3. XML-RPC 序列化的 Python shell 示例
>>>
from
xml_pickle
import
XML_Pickler
>>>
print
XML_Pickler(c).dumps()
<
?
xml version=
"1.0"
?
>
<
!
DOCTYPE PyObject SYSTEM
"PyObjects.dtd"
>
<PyObject
class
=
"C"
id=
"1840428"
>
<attr name=
"bool"
type=
"PyObject"
class
=
"Boolean"
id=
"1320396"
>
<attr name=
"value"
type=
"numeric"
value=
"1"
/>
</attr>
<attr name=
"int"
type=
"numeric"
value=
"37"
/>
<attr name=
"tup"
type=
"tuple"
id=
"1130924"
>
<item type=
"numeric"
value=
"11.199999999999999"
/>
<item type=
"string"
value=
"spam"
/>
</attr>
</PyObject>
|
与 XML-RPC 相比,
xml_pickle 版本有优势也有劣势(这两者的实际大小差不多)。请注意,尽管 Python 没有内置 Boolean 类型,但在使用类表示新的类型时,
xml_pickle 会迅速进行调整(虽然会更累赘)。相反,XML-RPC 仅限于序列化其 8 种数据类型,而不做其它事情。当然,其中的两种类型
<array> 和
<struct> 本身就是集合,而且可以是复合类型。另外,
xml_pickle 可以将多个集合成员指向同一个底层对象;这是 XML-RPC 的设计所不具备的(也已引入到
xml_pickle 的以后版本中)。还有件小事情,
xml_pickle 仅包含一个
numeric 类型属性,但
value 属性的实际模式允许对整数、浮点数、复杂类型等进行译码。虽然 XML-RPC 样式会从审美角度吸引程序员使用静态类型语言,但这些策略并没有失去也没有得到真正的普遍性。
XML-RPC 的缺点
XML-RPC 作为对象序列化格式的问题是它太简单了,没有足够的类型来处理大多数高级编程语言中的对象。清单 4 说明了这个缺点。
清单 4. XML-RPC 重载的 Python shell 示例
>>> c = C()
>>> c.foo =
'bar'
>>> d = {
'foo'
:
'bar'
}
>>> print xmlrpclib.dumps((c,d),
'PyObjects'
)
<
?
xml version=
'1.0'
?
>
<methodCall>
<methodName>PyObjects</methodName>
<params>
<param>
<value><struct>
<member>
<name>foo</name>
<value><string>bar</string></value>
</member>
</struct></value>
</param>
<param>
<value><struct>
<member>
<name>foo</name>
<value><string>bar</string></value>
</member>
</struct></value>
</param>
</params>
</methodCall>
|
在清单 4 中,对两样事物进行了序列化 ― 对象实例和字典。虽然公平地说 Python 对象特别类似于字典,但以
完全相同的方式表示字典和对象会丢失许多信息。此外,XML-RPC 中
<struct> 的含义过于普通,这对任何 OOP 语言(或至少是任何拥有自身散列/字典构造的语言)的影响非常大;它并不是 Python 特有的。另一方面,无法分辨 XML-RPC 的
<array> 类型中的 Python 元组和列表完全是 Python 特有的限制。
xml_pickle 能更好地处理所有 Python 类型(如我们所看到的,包括由用户类定义的数据类型)。实际上,在
xml_pickle 中没有字典的直接 pickle 处理,基本上是因为没有人要求这样做(要添加它很容易)。但作为对象属性的字典经过了 pickle 处理,如清单 5 所示。
清单 5. xml_pickle 字典的 Python shell 示例
>>> c, c2 = C(), C()
>>> c2.foo =
'bar'
>>> d = {
'foo'
:
'bar'
}
>>> c.c, c.d = c2, d
>>>
print
XML_Pickler(c).dumps()
<
?
xml version=
"1.0"
?
>
<
!
DOCTYPE PyObject SYSTEM
"PyObjects.dtd"
>
<PyObject
class
=
"C"
id=
"1917836"
>
<attr name=
"c"
type=
"PyObject"
class
=
"C"
id=
"1981484"
>
<attr name=
"foo"
type=
"string"
value=
"bar"
/>
</attr>
<attr name=
"d"
type=
"dict"
id=
"1917900"
>
<entry>
<key type=
"string"
value=
"foo"
/>
<val type=
"string"
value=
"bar"
/>
</entry>
</attr>
</PyObject>
|
该示例中隐含的
xml_pickle 方法的另一个优点是字典键不必是字符串。在 XML-RPC
<struct> 元素中,
<name> 键总是字符串。但是,在这方面,Perl、PHP 和大多数语言都更接近于 XML-RPC 模型。
xml_pickle 的缺点
可惜,
xml_pickle 缺少许多编程语言都拥有的一些类型。如果我们的目标不只是保存和恢复
Python 对象,而是跨语言交换对象,那么
xml_pickle 目前并不能完全胜任。在原则上,浮点数和整数的问题并不重要;但是,如果 XML 解析器在分析
value 属性的格式之前就能够确定需要的类型,那么为(比如) Java 设计 unpickle 程序就会更简单。
跨语言 pickle 要更加关注的是 XML-RPC 所拥有的
<boolean> 和
<dateTime.iso8601> 标记,但 Python 却没有这些内置类型。即使我声称
xml_pickle 可以轻松且很好地处理定义定制数据类型的用户类,但在它处理跨语言情况时,这并不完全正确。例如,清单 6 中的
xml_pickle 表示片段描述了 iso8601 日期/时间:
清单 6. iso8601 日期/时间的 xml_pickle 版本
<attr name="dte" type="PyObject" class="DateTime" id="1984076">
<attr name="value" type="string" value="20011122T17:28:55" />
</attr> |
有两个问题使得在譬如 Perl 或 REBOL 或 PHP 中使用该数据变得困难。其中一个是所恢复的类的名称空间。在 Python 中,缺省情况下,所恢复的
xmlrpclib.DateTime 的名称空间变成了
xml_pickle.DateTime (但在“unpickle”之前可以手工操作名称空间)。Python 的实例化和名称空间的工作方式很少依赖这个事实,至少在我们感兴趣的是实例属性而不是其方法的情况下不依赖于这个事实。但各种语言处理作用域问题的方法也各不相同。
第二个也是重要得多的问题是如果有些语言自身有这个类型,就不能轻易地将这个定制类识别成其内部类型。Perl 和 PHP 本身都没有
DateTime 类型,因此只要那些语言中的 unpickle 程序恢复了
value 实例属性,就不会丢失什么。与此相反,REBOL 本身有更多数据类型 ― 不仅有日期,而且有外来类型,如电子邮件地址和 URL。在
xml_pickle 过程中会丢失这些类型。当然,XML-RPC 也会丢失那些数据类型。不管采用哪种方式,我们都能使用简单的字符串类型来表示某些更特殊的东西(或 XML-RPC 中的
<base64> ,
xml_pickle 通过转义高位值 ― 例如,“\xff”― 来处理它们)。
结束语:往何处发展?
作为表示流行编程语言的对象实例的方法,XML-RPC 和
xml_pickle 都不能完全满足要求。但它们离这个要求都不远。让我推荐一些方法来弥补这些协议之间的微小差距,并提供一种通用的对象序列化格式。
“修复”
xml_pickle 实际上非常简单 ― 只要将更多类型添加到格式中即可。例如,由于先开发了
xml_pickle ,因此已经将
UnicodeType 添加到 Python 中。添加对它的完整支持只需要四行新代码(虽然由于 XML 本身使用 Unicode 而使它略微得到简化)。或者,再次应用户的要求稍微多做些工作,添加
numeric 模块的
ArrayType 。即使 Python 中没有某个类型,也可以在
xml_pickle 中定义一个定制类来添加该类型的行为 ― 例如,可以用以下的代码段支持 REBOL 的“电子邮件地址(e-mail address)”类型:
<attr name="my_address" type="email" value="mertz@gnosis.cx" /> |
“unpickle”之后,
xml_pickle 可以将“email”只看作是“string”的一个同义词,或者我们可以用一些有用的行为实现
EmailAddress 类。如果我们采用后一种方法,这样的一种行为就会被 pickle 到以上
xml_pickle 代码段中。
“修复”XML-RPC 要困难一些。建议只添加一些新数据类型可能很简单,而且从纯技术观点看,这不会有什么特别的问题。但作为一个社会问题,XML-RPC 的成功使引入不兼容的更改变得很困难:假设的“数据增强型”XML-RPC 不会与所有现有的实现和安装都兼容。实际上,一些实现者由于缺少“nil”类型而感到十分烦恼,因此他们添加了一种非标准(或充其量不过是半标准)类型以对应 Java
null 、Python
None 、Perl
undef 和 SQL
NONE 等类型。但添加更多只有某些编程语言才使用的类型的行动还不会开始。
将 XML-RPC 增强成对象序列化器的一种方法是采用
<struct> 元素来执行双重任务。可以使用一个
<member> 将没有完全被标准 XML-RPC 定义为某种类型的所有东西封装到
<struct> 中,其中
<name> 表明特殊类型。虽然现有的 XML-RPC 库不会这样做,但 XML-RPC 协议和 DTD 非常简单,因此添加这个行为相当容易(但在大多数情况下都要求修改这个库,而不只是封装)。
例如,XML-RPC 本身不能描述 Python 列表和元组之间的区别。因此,作为 Python 对象的描述,清单 7 中的代码段是不完整的。
清单 7. 列表或元组的 XML-RPC 片段
<array>
<data>
<value>
<double>11.2</double>
</value>
<value>
<string>spam</string>
</value>
</data>
</array> |
我们可以将它替代成有效的 XML-RPC 表示法,而且可以用合适的实现将它恢复成特定的 Python 对象:
清单 8. 元组的 XML-RPC 片段
<struct>
<member>
<name>NEWTYPE:tuple</name>
<value>
<array>
<data>
<value>
<double>11.2</double>
</value>
<value>
<string>spam</string>
</value>
</data>
</array>
</value>
</member>
</struct> |
真正的
<struct> 能够以两种(或更多)方法表示。首先,每个
<struct> 都可以被封装到另一个
<struct> 中(也许具有
<name> OLDTYPE:struct 或类似名称)。对于 Python,这可能是最好的,因为字典和对象实例都是
NEWTYPE 。其次,可以为这种特殊用法保留类似于名称空间的前缀
NEWTYPE: (不太可能有意外冲突)。
在表示新数据类型这一方面,
xml_pickle 的设计比 XML-RPC 更具有可扩展性。而且,
xml_pickle 的扩展还保持了很好的向后跨版本兼容性。作为它的设计者,我对
xml_pickle 所包含的灵活性感到很高兴。但是,事实是 XML-RPC 得到了更广泛的的使用和实现。幸好,只要略微进行额外的分层 ― 不必破坏底层的 DTD ― 就可以改进 XML-RPC 以用于表示任意的数据类型。该机制不太优雅,但 XML-RPC 已经过精心设计,可以在改进之后与这些现有的实现兼容。
参考资料
XML 问题专栏前面的部分讨论了许多主题:
-
XML 问题 #1 介绍 Python
xml_pickle 对象 。
-
XML 问题 #2 描述如何使用 Python 的
xml_objectify 。
-
XML 问题 #3介绍 DocBook。
-
XML 问题 #4继续介绍如何使用 DocBook 构建旧的文档压缩文件。
-
XML 问题 #5说明如何通过 XSLT 将 XML 文档转换成 HTML。
-
XML 问题 #6比较了一些 XML 编辑器,同时特别着眼于那些适合于有大量文本的文档工具。
-
XML 问题 #7将 DTD 与“XML 模式”进行了权衡,并建议无论 W3C XML Schema 规范有多么成熟,开发人员在什么情况下需要坚持使用 DTD。
-
XML 问题 #8讨论了由计算机科学家概念化的数据
模型的抽象理论是如何帮助我们开发特定的多表示数据流的。
-
XML 问题 #9讨论了公共域 sql2dtd 和 sql2xml 实用程序;这些实用程序可以不依赖 RDBMS 生成可移植的 XML 结果集。
-
XML 问题 #10扩展了 David 的“可爱的 Python” #15 专栏中的通用文本索引器,包含了特定于 XML 的查找和索引特点,并讨论了索引器是如何利用 XML 的层次节点结构的优势的。
-
XML 问题 #11 重温了本系列第一篇专栏文章中介绍的模块
xml_pickle 和
xml_objectify 。
-
XML 问题 #12讨论在一致和可逆的方式下,启动一个 XML 文档时,生成 SQL 语句的公共域实用程序,用此 SQL 语句来创建和填充数据库。
-
XML 问题 #13探索压缩 XML 文档的几种方法。
-
XML 问题 #14揭露了使用 Haskell(和库 HaXml)处理 XML 数据的优点。
关于作者
对本文的评价
|