 | 级别: 初级 Mike Olson (mike.olson@fourthought.com), 首席顾问, Fourthought, Inc.
2002 年 10 月 01 日
在
Python Web 服务开发者的这一部分中,Mike Olson 回到了编写一些 Python 代码上。这篇专栏文章将再次讨论同一系列专栏文章的第五篇和第六篇中的示例代码,Mike 和 Uche Ogbuji 分别在这两篇文章中讨论了 SOAP.py 和 ZSI,它们是可以在 Python 中使用的 SOAP 实现。Mike 将继续研究这两种库,看看它们如何相互作用。
Web 服务的承诺之一是互操作性。正如我们在先前的专栏文章中所阐述的,用不同的编程语言、在不同的平台上构建的 Web 服务应该就象由同一种语言所提供的服务一样一起工作。在本专栏先前的文章中我们实现了日历 Web 服务,在
Python Web 服务开发者的这一部分中,我将使用这个服务的客户机和服务器,看看它们如何互操作。
为了要使用本文中的样本,请您按照同一系列专栏文章的第五部分和第六部分中的安装步骤安装 SOAP.py 和 ZSI。(您应该把这两篇文章再看一遍以获得关于 SOAP.py 和 ZSI 的背景知识;您可以在
参考资料部分找到它们的链接。)顺便提一下,最近 ZSI 已经有了更新的版本:版本 1.2 于今年 3 月发布了。在本专栏中出现过的样本对于 ZSI 1.1 和 1.2 都能用。对于那些有兴趣升级的开发者,请参阅下面的
参考资料部分以得到您可以去下载最新版本的 URL。安装使用标准的
distutils 命令。如果您安装了 ZSI 以前的版本,请先将其删除以避免冲突。要安装新版本,请将分发包解包,将位置改为
ZSI-1.2 目录。按照下面的样子执行安装命令:
[molson@penny ZSI-1.2]# python setup.py install
|
ZSI 客户机与 SOAP.py 服务器
在第一部分的分析中,您可以运行 ZSI 客户机,对应的服务器是 SOAP.py 服务器。您首先需要做的事情就是更改客户机和服务器在哪个端口上通信。SOAP.py 示例侦听端口 8888,而 ZSI 客户机侦听端口 8080。由于两个端口都可以用,所以您可以将所有示例都设置为端口 8888。
起初,您可以试着完全按脚本原先的样子运行脚本(当然除了端口的更改)。记住,要启动服务器,请在一个窗口中运行脚本
soapy-server.py :
在另外一个窗口中,运行脚本
zsi-client.py :
在客户机窗口中,您会得到一条出错消息,它的内容是:
Traceback (most recent call last):
File "zsi-client.py", line 21, in ?
print b.Receive(TC.Any(aslist = b.aslist))
File "/usr/local/lib/python2.1/site-packages/ZSI/client.py", line 227, in Receive
raise TypeError, "Unexpected SOAP fault: " + msg.string
TypeError: Unexpected SOAP fault: No method getMonth found
|
用前几篇专栏文章(请参阅
参考资料看看关于 SOAP.py 的第五篇专栏文章)中所展示的技巧分析您得到的调试消息,您马上就明白这个问题了。所调用的方法的名称空间不正确。在 SOAP 体内引用方法时方法不带前缀,也没有定义缺省的名称空间。SOAP.py 服务器定义
getMonth 在
http://uche.ogbuji.net/eg/ws/simple-cal 名称空间中,因此您需要调整 ZSI 客户机,以使它能请求在正确的名称空间中的那个方法。
只在 ZSI
Binding 对象上调用
getMonth 解决不了这个问题;您需要使用一些较低级别的接口以便在需要的地方添加关键参数。您需要指定所调用的方法的名称空间。要指定它的名称空间,您需要更改给方法起的名称(给名称添加前缀)并在
nsdict 参数中指定前缀到名称空间的映射。
清单 1展示了经过这些修改的 ZSI 客户机:
清单 1. 更新过的 ZSI 客户机
#zsi-client.py
import sys
#Import the ZSI client
from ZSI.client import Binding
from ZSI import TC
u = ''
n = 'http://uche.ogbuji.net/eg/ws/simple-cal'
b = Binding(url=u, ns=n, host='localhost', port=8888)
b.Send(None,
"ns1:getMonth",
(2002,2),
requesttypecode=TC.Any('ns1:getMonth', aslist=b.aslist),
nsdict={'ns1':n},
)
res = b.Receive(TC.Any(aslist = b.aslist))
print res[0]
|
做了这些更改之后,ZSI 客户机输出 SOAP.py 服务器返回的 2002 年 2 月的日历就不会有问题了。看起来,您已经成功的完成了前面一半的分析。现在,我们要看一下在另外一个方向上如何进行通信。
对调一下:ZSI 服务器与 SOAP.py 客户机
要测试用 ZSI 服务器作为服务器的 SOAP.py 客户机,您首先要做的事情就是编写 SOAP.py 客户机。还记得吗,我们曾使用
httplib 编写一个非常低级的客户机来测试 SOAP.py 服务器(请参阅
参考资料看看有关 ZSI 的第六篇专栏文章)。由于您想知道 SOAP.py 对 ZSI 的反应如何,这样做就不管用了。为了使用您的日历 Web 服务,可以编写一个速度较快的客户机,如
清单 2中所示。
清单 2. 更新的 SOAP.py 客户机
#!/usr/bin/env python
import sys
#Import the SOAP.py machinery
from WebServices import SOAP
CAL_NS = "http://uche.ogbuji.net/eg/ws/simple-cal"
remote = SOAP.SOAPProxy(
"localhost:8888",
#namespace=CAL_NS,
soapaction=""
)
print remote.getMonth(2002,2)
|
这个客户机的实现与第五篇专栏文章中的
curses 客户机的类似。需要注意一点,
SOAPProxy 的 namespace(名称空间)参数被注释掉了。正如 ZSI 客户机的缺省实现无法知道名称空间一样,服务器的同样也无法知道。
要运行样本脚本,请在一个窗口中再次启动 ZSI 服务器:
在另一个窗口中启动 SOAP.py 客户机:
如您所愿(因为这个客户机是为了与 ZSI 服务器通信而特地编写的),返回给您的正是 2002 年 2 月的日历。
ZSI 和名称空间
在接下去的测试中,您要扩展 ZSI 客户机以便它能支持名称空间内的多个方法。要实现这一目的需要克服一些困难;但是 ZSI 包的作者 Rich Salz 说,很快就可以支持方法的名称空间了。他说,我的设想是通过 Python 2.2 中的一个新功能,这个功能允许您任意给函数定义属性。如果您在函数定义上添加了
namespace 属性,那么 ZSI 指派代码就会查询这个属性以判定在哪个名称空间中定义了方法。为了将来引用,您可以通过将属性设置在函数对象上给函数添加任何属性。下面是一个很短的示例:
>>> def foo(a):
... pass
...
>>> foo.namespace = "http://foo.com"
>>> print foo.namespace
http://foo.com
>>>
|
Salz 还提到,他会添加对除名称空间信息以外的关于函数的其它属性(比如参数类型信息)的支持。这样 ZSI 就可以在函数被调用之前对传入的参数进行类型检查。
不管在任何情况下,要知道 ZSI 服务器的名称空间,您需要给
getMonth 方法(以及其它任何我们希望放在名称空间中的方法)添加一点逻辑。对于当前的的调用,您可以先添加一个
ClientBinding 试试。它包含有关调用的所有相关信息,包括其名称空间在内。(缺省情况下,ZSI 将忽略名称空间。)然后,您可以将
ClientBinding 的名称空间同您所期望的名称空间进行比较。如果匹配,那么您可以返回正确的结果;反之,您可以抛出异常,异常内容为:方法未定义。
清单 3 展示重构的
getMonth 方法的一个示例。
清单 3. 重构的 getMonth 方法
def getMonth(year, month):
cb = dispatch.GetClientBinding()
if cb.GetNS() != CAL_NS:
raise TypeError, "Unimplemented method %s %s" % (cb.GetNS(),name)
return calendar.month(year, month)
|
不幸的是,要利用这一技术,您需要给在模块中定义的每个函数都添加这一逻辑。
清单 4 采用了一种更为通用的方法,对于您在一个模块内定义的所有函数,这种方法允许您轻松的将其定义在一个名称空间内。您可以相当轻松的在多个名称空间中定义同一个方法,或者定义名称空间各不相同的多个方法(只要方法名是相同的)。为了达到这一目的,叫做
_functionMap 的函数一般会利用映射字典映射请求。字典的主键是名称空间,每个条目指向一个函数,当接收到与函数相对应的名称空间里的请求时就调用这个函数。然后,为了能看到模块内最上层函数(按照 ZSI 的要求),您可以使用简单的 lambda 来代表真正的函数定义。对于
getMonth ,
getMonth lambda 就会被调用;它再调用
_functionMap 查看调用的名称空间以及可用的函数映射。如果发现匹配的情况,就调用函数;反之,则发出异常。现在,要给模块添加新的函数,您要定义函数和对应的 lambda。
注意一点:您需要在每个 lambda 定义的缺省参数中都放上一个对
_functionMap 的引用。这是因为,您在创建好 lambda 时希望删除模块对
_functionMap 函数的引用;如果不删除的话,那么这个函数将会被公开在您的 Web 服务接口上,因为对于任何在最上层定义的方法,ZSI 都允许它被调用。
清单 4 展示文件
zsi-ns-server.py , 它用 ZSI 实现了可以知道名称空间的服务器。
清单 4. zsi-ns-server.py:更为通用的方法
#!/usr/bin/env python
import sys, calendar
#Import the ZSI machinery
from ZSI import dispatch
CAL_NS = "http://uche.ogbuji.net/eg/ws/simple-cal"
#The actual implementations
def getMonth(year, month):
return calendar.month(year, month)
def getYear(year):
return calendar.calendar(year)
#Generic function to check the namespace
def _functionMap(name,mapping,*args):
cb = dispatch.GetClientBinding()
func = mapping.get(cb.GetNS())
if func is None:
raise TypeError, "Unimplemented method %s %s" % (cb.GetNS(),name)
return apply(func,args)
#Publicly defined methods
getMonthMap = {CAL_NS:getMonth,
}
getMonth = lambda year,month,_functionMap=_functionMap,map=getMonthMap:_functionMap
("getMonth",map,year,month)
getYearMap = {CAL_NS:getYear,
}
getYear = lambda year,_functionMap=_functionMap,map=getYearMap:_functionMap("getYear",map,year)
#Delete this so it is not available as a service.
del _functionMap
print "Starting server..."
dispatch.AsServer(port=8888)
|
在不久的将来
在这一专栏的下一部分中,我们将对一些不同的分布式编程技术进行比较,并分析每种技术具有的一些性能特征。我们将比较 SOAP、CORBA(Common Object Request Broker Architecture,通用对象请求代理体系结构)和 XML-RPC,另外还有一个用
pickle 库写成的自助实现。我们还要看一下消息的开销、应用程序的内存占用量、消息传送次数及实现这些方法中的每一种所需要的有关源代码的大小。
参考资料
关于作者  | 
|  | Mike Olson 是
Fourthought, Inc的顾问和合伙创办人,这是一家软件供应商和专做企业知识管理应用的 XML 解决方案的咨询公司。Fourthought 开发了
4Suite及
4Suite Server,这是 XML 中间件的开放源代码平台。您可以通过
mike.olson@fourthought.com与 Mr. Olson 联络。
|
对本文的评价
|  |