使用 Perspective Broker¶
基本示例¶
第一个要看的示例是一个完整的(尽管有点琐碎)应用程序。它在服务器端使用 PBServerFactory()
,在客户端使用 PBClientFactory()
。
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
from twisted.internet import reactor
from twisted.spread import pb
class Echoer(pb.Root):
def remote_echo(self, st):
print("echoing:", st)
return st
if __name__ == "__main__":
reactor.listenTCP(8789, pb.PBServerFactory(Echoer()))
reactor.run()
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
from twisted.internet import reactor
from twisted.python import util
from twisted.spread import pb
factory = pb.PBClientFactory()
reactor.connectTCP("localhost", 8789, factory)
d = factory.getRootObject()
d.addCallback(lambda object: object.callRemote("echo", "hello network"))
d.addCallback(lambda echo: "server echoed: " + echo)
d.addErrback(lambda reason: "error: " + str(reason.value))
d.addCallback(util.println)
d.addCallback(lambda _: reactor.stop())
reactor.run()
首先我们看一下服务器。这定义了一个 Echoer 类(从 pb.Root
派生),它有一个名为 remote_echo()
的方法。 pb.Root
对象(因为它们继承了 pb.Referenceable
,稍后描述)可以定义名称为 remote_*
的方法;获取该 pb.Root
对象的远程引用的客户端将能够调用这些方法。
该 pb.Root
类对象被传递给 pb.PBServerFactory
()
。这是一个与其他任何 Factory
对象类似的对象:它为新连接创建的 Protocol
对象知道如何使用 PB 协议。传递给 pb.PBServerFactory()
的对象成为“根对象”,这使得客户端可以检索它。客户端只能请求对您想要提供的对象的引用:这有助于您实现安全模型。由于导出单个对象非常常见(并且因为该对象上的 remote_*
方法可以返回对您可能想要提供的任何其他对象的引用),因此最简单的示例是 PBServerFactory
被传递了根对象,并且客户端检索了它。
客户端使用 pb.PBClientFactory
建立到指定端口的连接。这是一个两步过程,包括打开到指定主机和端口的 TCP 连接,并使用 .getRootObject()
请求根对象。
由于 .getRootObject()
必须等到网络连接建立并交换一些数据,因此可能需要一段时间,所以它返回一个 Deferred,将 gotObject() 回调附加到它。(有关 Deferred
的完整解释,请参阅 延迟执行 的文档)。如果连接成功并且获得了对远程根对象的引用,则会运行此回调。传递给回调的第一个参数是对远程根对象的远程引用。(您也可以向回调传递其他参数,请参阅 .addCallback()
和 .addCallbacks()
的其他参数)。
回调执行以下操作
object.callRemote("echo", "hello network")
这会导致服务器的 .remote_echo()
方法被调用。(运行 .callRemote("boom")
会导致 .remote_boom()
运行,等等)。同样由于涉及的延迟,callRemote()
返回一个 Deferred
。假设远程方法在没有引发异常的情况下运行(包括尝试调用未知方法),则附加到该 Deferred
的回调将使用远程方法调用返回的任何对象进行调用。
在这个示例中,服务器的 Echoer
对象有一个方法被调用,完全就像服务器端的一些代码执行了以下操作
echoer_object.remote_echo("hello network")
从 remote_echo()
的定义中我们可以看到,这只是返回了它接收到的相同字符串:“hello network”。
从客户端的角度来看,远程调用得到的是另一个Deferred
对象,而不是那个字符串。 callRemote()
总是返回一个Deferred
。这就是为什么 PB 被描述为一个用于“半透明”远程方法调用的系统,而不是“透明”的系统:你不能假装远程对象实际上是本地的。尝试这样做(就像其他一些 RPC 机制所做的那样,coughCORBAcough)在面对网络的异步性质时就会崩溃。使用 Deferreds 证明是一种非常干净的方式来处理整个事情。
远程引用对象(传递给 getRootObject()
的成功回调)是 RemoteReference
类的实例。这意味着你可以使用它来调用它所引用的远程对象上的方法。只有 RemoteReference
的实例才有资格使用 .callRemote()
。 RemoteReference
对象是存在于远程端(在本例中为客户端)的对象,而不是本地端(实际对象定义的地方)。
在我们的示例中,本地对象是那个 Echoer()
实例,它继承自 pb.Root
,它继承自 pb.Referenceable
。正是 Referenceable
类使该对象有资格用于远程方法调用[1] 。如果你有一个可引用的对象,那么任何设法获取其引用的客户端都可以调用他们喜欢的任何 remote_*
方法。
注意
他们唯一能做的事情就是调用这些方法。特别是,他们不能访问属性。从安全角度来看,你可以通过限制 remote_*
方法可以做什么来控制他们可以做什么。
还要注意:其他类(如 Referenceable
)允许访问其他方法,特别是 perspective_*
和 view_*
可以被访问。不要用这些名称编写仅限本地的方法,因为这样远程调用者将能够做超出你预期的事情。
还要注意:其他类(如 pb.Copyable
)确实允许访问属性,但你可以控制他们可以看到哪些属性。
你不必是 pb.Root
才能被远程调用,但你必须是 pb.Referenceable
。(继承自 pb.Referenceable
但不是从 pb.Root
继承的对象可以被远程调用,但只有 pb.Root
类对象可以传递给 PBServerFactory
。)
完整示例¶
这是一个使用 pb.Referenceable
作为根对象和远程公开方法的结果的客户端和服务器示例。在每个上下文中,都可以对公开的 Referenceable
实例调用方法。在本示例中,初始根对象有一个方法,该方法返回对第二个对象的引用。
#!/usr/bin/env python
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
from twisted.spread import pb
class Two(pb.Referenceable):
def remote_three(self, arg):
print("Two.three was given", arg)
class One(pb.Root):
def remote_getTwo(self):
two = Two()
print("returning a Two called", two)
return two
from twisted.internet import reactor
reactor.listenTCP(8800, pb.PBServerFactory(One()))
reactor.run()
#!/usr/bin/env python
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
from twisted.internet import reactor
from twisted.spread import pb
def main():
factory = pb.PBClientFactory()
reactor.connectTCP("localhost", 8800, factory)
def1 = factory.getRootObject()
def1.addCallbacks(got_obj1, err_obj1)
reactor.run()
def err_obj1(reason):
print("error getting first object", reason)
reactor.stop()
def got_obj1(obj1):
print("got first object:", obj1)
print("asking it to getTwo")
def2 = obj1.callRemote("getTwo")
def2.addCallbacks(got_obj2)
def got_obj2(obj2):
print("got second object:", obj2)
print("telling it to do three(12)")
obj2.callRemote("three", 12)
main()
pb.PBClientFactory.getRootObject
将处理连接创建的所有细节。它返回一个Deferred
,当反应器连接到远程服务器并且 pb.PBClientFactory
获取根时,它的回调将被调用,并且当对象连接由于任何原因(无论是主机查找失败、连接拒绝还是服务器端错误)失败时,它的 errback
将被调用。
根对象有一个名为 remote_getTwo
的方法,它返回 Two()
实例。在客户端端,回调得到该实例的 RemoteReference
。然后客户端可以调用 two 的 .remote_three()
方法。
RemoteReference
对象有一个方法,这是它们存在的目的: callRemote
。此方法允许你调用引用所引用的对象上的远程方法。 RemoteReference.callRemote
,就像 pb.PBClientFactory.getRootObject
一样,返回一个Deferred
。当对正在发送的方法调用的响应到达时,Deferred
的 callback
或 errback
将被调用,具体取决于方法调用处理过程中是否发生了错误。
你可以使用此技术来提供对任意对象集的访问。请记住,任何可能被“通过网络传输”的对象都必须继承自 Referenceable
(或其他变体)。如果你尝试传递一个不可引用对象(例如,通过从 remote_*
方法返回一个对象),你将得到一个 InsecureJelly
异常[2] 。
引用可以返回给你¶
如果你的服务器向客户端提供了一个引用,然后该客户端将该引用返回给服务器,服务器最终将获得与最初提供的相同对象。序列化层会监视返回的引用标识符,并将它们转换为实际对象。你需要始终了解对象的位置:如果它在你的这边,你进行实际的方法调用。如果它在另一边,你进行 .callRemote()
[3] 。
#!/usr/bin/env python
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
from twisted.internet import reactor
from twisted.spread import pb
class Two(pb.Referenceable):
def remote_print(self, arg):
print("two.print was given", arg)
class One(pb.Root):
def __init__(self, two):
# pb.Root.__init__(self) # pb.Root doesn't implement __init__
self.two = two
def remote_getTwo(self):
print("One.getTwo(), returning my two called", self.two)
return self.two
def remote_checkTwo(self, newtwo):
print("One.checkTwo(): comparing my two", self.two)
print("One.checkTwo(): against your two", newtwo)
if self.two == newtwo:
print("One.checkTwo(): our twos are the same")
two = Two()
root_obj = One(two)
reactor.listenTCP(8800, pb.PBServerFactory(root_obj))
reactor.run()
#!/usr/bin/env python
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
from twisted.internet import reactor
from twisted.spread import pb
def main():
foo = Foo()
factory = pb.PBClientFactory()
reactor.connectTCP("localhost", 8800, factory)
factory.getRootObject().addCallback(foo.step1)
reactor.run()
# keeping globals around is starting to get ugly, so we use a simple class
# instead. Instead of hooking one function to the next, we hook one method
# to the next.
class Foo:
def __init__(self):
self.oneRef = None
def step1(self, obj):
print("got one object:", obj)
self.oneRef = obj
print("asking it to getTwo")
self.oneRef.callRemote("getTwo").addCallback(self.step2)
def step2(self, two):
print("got two object:", two)
print("giving it back to one")
print("one is", self.oneRef)
self.oneRef.callRemote("checkTwo", two)
main()
服务器向客户端提供一个 Two()
实例,客户端然后将该引用返回给服务器。服务器比较给定的“two”和接收到的“two”,并显示它们是相同的,并且两者都是真实对象,而不是远程引用。
pb2client.py
中演示了其他一些技术。其中之一是使用 .addCallback
而不是 .addCallbacks
添加回调。正如你从 Deferred 文档中可以看出,.addCallback
是一种简化的形式,它只添加一个成功回调。另一个是,为了跟踪从一个回调到下一个回调的状态(对主 One() 对象的远程引用),我们创建一个简单的类,将引用存储在该类的实例中,并将回调指向一系列绑定方法。这是一种封装状态机的便捷方式。每个响应都会启动下一个方法,任何需要从一个状态传递到下一个状态的数据都可以简单地保存为该对象的属性。
请记住,客户端可以将您提供的任何远程引用返回给您。不要将您的价值数十亿美元的股票交易清算服务器建立在您信任客户端会将正确的引用返回给您的想法之上。PB 中固有的安全模型意味着他们只能将您为当前连接提供的引用返回给您(而不是您提供给其他人的引用,也不是您上次在 TCP 会话断开之前提供的引用,也不是您尚未提供的引用给客户端),但就像 URL 和 HTTP cookie 一样,他们提供给您的特定引用完全由他们控制。
对客户端对象的引用¶
任何可引用的东西都可以通过网络传递,无论哪个方向。 “客户端”可以将对“服务器”的引用传递给“服务器”,然后服务器可以使用 .callRemote() 来调用客户端端的函数。这模糊了“客户端”和“服务器”之间的区别:唯一的真正区别是谁发起了原始的 TCP 连接;之后一切都对称。
#!/usr/bin/env python
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
from twisted.internet import reactor
from twisted.spread import pb
class One(pb.Root):
def remote_takeTwo(self, two):
print("received a Two called", two)
print("telling it to print(12)")
two.callRemote("print", 12)
reactor.listenTCP(8800, pb.PBServerFactory(One()))
reactor.run()
#!/usr/bin/env python
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
from twisted.internet import reactor
from twisted.spread import pb
class Two(pb.Referenceable):
def remote_print(self, arg):
print("Two.print() called with", arg)
def main():
two = Two()
factory = pb.PBClientFactory()
reactor.connectTCP("localhost", 8800, factory)
def1 = factory.getRootObject()
def1.addCallback(got_obj, two) # hands our 'two' to the callback
reactor.run()
def got_obj(obj, two):
print("got One:", obj)
print("giving it our two")
obj.callRemote("takeTwo", two)
main()
在这个例子中,客户端将对自身对象的引用传递给服务器。然后服务器在客户端对象上调用一个远程函数。
引发远程异常¶
到目前为止,我们已经介绍了事情顺利进行时会发生什么。如果出现问题怎么办?Python 的方法是引发某种异常。Twisted 的方法也是如此。
您唯一需要做的特殊事情是通过从 pb.Error
派生来定义您的 Exception
子类。当任何远程可调用函数(如 remote_*
或 perspective_*
)引发 pb.Error
派生的异常时,该异常对象的序列化形式将通过网络发送回来 [4]。另一端(执行了 callRemote
)将运行“errback
”回调,并使用包含异常对象副本的 Failure
对象。此 Failure
对象可以被查询以检索错误消息和堆栈跟踪。
Failure
是一个特殊的类,定义在 twisted/python/failure.py
中,旨在简化异步异常的处理。就像异常处理程序可以嵌套一样,errback
函数也可以链接。如果一个 errback 无法处理特定类型的故障,它可以“传递”给链中更下游的 errback 处理程序。
为了简单起见,将 Failure
视为远程抛出的 Exception
对象的容器。要提取放入异常中的字符串,请使用其 .getErrorMessage()
方法。要获取异常的类型(作为字符串),请查看其 .type
属性。堆栈跟踪也可用。目的是让 errback 函数获得与 Python 的正常 try:
子句一样多的有关异常的信息,即使异常发生在过去某个未知时间点的其他人的内存空间中。
#!/usr/bin/env python
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
from twisted.internet import reactor
from twisted.spread import pb
class MyError(pb.Error):
"""This is an Expected Exception. Something bad happened."""
pass
class MyError2(Exception):
"""This is an Unexpected Exception. Something really bad happened."""
pass
class One(pb.Root):
def remote_broken(self):
msg = "fall down go boom"
print("raising a MyError exception with data '%s'" % msg)
raise MyError(msg)
def remote_broken2(self):
msg = "hadda owie"
print("raising a MyError2 exception with data '%s'" % msg)
raise MyError2(msg)
def main():
reactor.listenTCP(8800, pb.PBServerFactory(One()))
reactor.run()
if __name__ == "__main__":
main()
#!/usr/bin/env python
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
from twisted.internet import reactor
from twisted.spread import pb
def main():
factory = pb.PBClientFactory()
reactor.connectTCP("localhost", 8800, factory)
d = factory.getRootObject()
d.addCallbacks(got_obj)
reactor.run()
def got_obj(obj):
# change "broken" into "broken2" to demonstrate an unhandled exception
d2 = obj.callRemote("broken")
d2.addCallback(working)
d2.addErrback(broken)
def working():
print("erm, it wasn't *supposed* to work..")
def broken(reason):
print("got remote Exception")
# reason should be a Failure (or subclass) holding the MyError exception
print(" .__class__ =", reason.__class__)
print(" .getErrorMessage() =", reason.getErrorMessage())
print(" .type =", reason.type)
reactor.stop()
main()
$ ./exc_client.py
got remote Exception
.__class__ = twisted.spread.pb.CopiedFailure
.getErrorMessage() = fall down go boom
.type = __main__.MyError
Main loop terminated.
哦,如果你引发了其他类型的异常怎么办?不是从 pb.Error
派生的东西?好吧,这些被称为“意外异常”,这使得 Twisted 认为出了真正的问题。这些将在服务器端引发异常。这不会破坏连接(异常被捕获,就像大多数响应网络流量发生的异常一样),但它会在服务器的 stderr 上打印出一个难看的堆栈跟踪,并显示一条消息,说“对等方将收到 PB 追溯”,就像异常发生在远程可调用函数之外一样。(此消息将转到当前日志目标,如果使用 log.startLogging
重定向它)。客户端将在两种情况下都获得相同的 Failure
对象,但将您的异常从 pb.Error
派生是告诉 Twisted 您期望这种异常,并且让客户端处理它而不是让服务器也抱怨是可以的。查看 exc_client.py
并将其更改为调用 broken2()
而不是 broken()
以查看服务器行为的变化。
如果您没有在 Deferred
中添加 errback
函数,那么远程异常仍然会发送一个 Failure
对象,但它将被放置在 Deferred
中,无处可去。当该 Deferred
最终超出范围时,执行了 callRemote
的一方将发出有关“Deferred 中的未处理错误”的消息,以及一个难看的堆栈跟踪。它不能在此时引发异常(毕竟,触发问题的 callRemote
早已消失),但它会发出一个追溯。因此,做一个好程序员,始终添加 errback
处理程序,即使它们只是对 log.err
的调用。
Try/Except 块和 Failure.trap
¶
要实现 Python try/except 块的等效项(它可以捕获特定类型的异常并将其他异常“向上”传递到更高级别的 try/except
块),您可以使用 .trap()
方法以及 Deferred
上的多个 errback
处理程序。在 errback
处理程序中重新引发异常的作用是将该新异常传递给链中的下一个处理程序。 trap
方法被赋予一个要查找的异常列表,并将重新引发不在列表中的任何异常。这不会将未处理的异常“向上”传递到封闭的 try
块,而是将异常“传递”给同一 Deferred
上的后续 errback
处理程序。 trap
调用用于在链接的 errback 中按顺序测试每种类型的异常。
#!/usr/bin/env python
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
from twisted.internet import reactor
from twisted.spread import pb
class MyException(pb.Error):
pass
class One(pb.Root):
def remote_fooMethod(self, arg):
if arg == "panic!":
raise MyException
return "response"
def remote_shutdown(self):
reactor.stop()
reactor.listenTCP(8800, pb.PBServerFactory(One()))
reactor.run()
#!/usr/bin/env python
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
from twisted.internet import reactor
from twisted.python import log
from twisted.spread import jelly, pb
class MyException(pb.Error):
pass
class MyOtherException(pb.Error):
pass
class ScaryObject:
# not safe for serialization
pass
def worksLike(obj):
# the callback/errback sequence in class One works just like an
# asynchronous version of the following:
try:
response = obj.callMethod(name, arg)
except pb.DeadReferenceError:
print(" stale reference: the client disconnected or crashed")
except jelly.InsecureJelly:
print(" InsecureJelly: you tried to send something unsafe to them")
except (MyException, MyOtherException):
print(" remote raised a MyException") # or MyOtherException
except BaseException:
print(" something else happened")
else:
print(" method successful, response:", response)
class One:
def worked(self, response):
print(" method successful, response:", response)
def check_InsecureJelly(self, failure):
failure.trap(jelly.InsecureJelly)
print(" InsecureJelly: you tried to send something unsafe to them")
return None
def check_MyException(self, failure):
which = failure.trap(MyException, MyOtherException)
if which == MyException:
print(" remote raised a MyException")
else:
print(" remote raised a MyOtherException")
return None
def catch_everythingElse(self, failure):
print(" something else happened")
log.err(failure)
return None
def doCall(self, explanation, arg):
print(explanation)
try:
deferred = self.remote.callRemote("fooMethod", arg)
deferred.addCallback(self.worked)
deferred.addErrback(self.check_InsecureJelly)
deferred.addErrback(self.check_MyException)
deferred.addErrback(self.catch_everythingElse)
except pb.DeadReferenceError:
print(" stale reference: the client disconnected or crashed")
def callOne(self):
self.doCall("callOne: call with safe object", "safe string")
def callTwo(self):
self.doCall("callTwo: call with dangerous object", ScaryObject())
def callThree(self):
self.doCall("callThree: call that raises remote exception", "panic!")
def callShutdown(self):
print("telling them to shut down")
self.remote.callRemote("shutdown")
def callFour(self):
self.doCall("callFour: call on stale reference", "dummy")
def got_obj(self, obj):
self.remote = obj
reactor.callLater(1, self.callOne)
reactor.callLater(2, self.callTwo)
reactor.callLater(3, self.callThree)
reactor.callLater(4, self.callShutdown)
reactor.callLater(5, self.callFour)
reactor.callLater(6, reactor.stop)
factory = pb.PBClientFactory()
reactor.connectTCP("localhost", 8800, factory)
deferred = factory.getRootObject()
deferred.addCallback(One().got_obj)
reactor.run()
$ ./trap_client.py
callOne: call with safe object
method successful, response: response
callTwo: call with dangerous object
InsecureJelly: you tried to send something unsafe to them
callThree: call that raises remote exception
remote raised a MyException
telling them to shut down
callFour: call on stale reference
stale reference: the client disconnected or crashed
在这个例子中,callTwo
尝试通过 callRemote
发送一个本地定义类的实例。由 jelly
在远程端实现的默认安全模型不允许对未知类进行反序列化(即从网络中获取字节流并将其转换回对象:某个类的活动实例):一个原因是它不知道应该使用哪个本地类来创建一个与远程对象相对应的实例 [5]。
连接的接收端可以决定接受什么和拒绝什么。它通过引发 jelly.InsecureJelly
异常来表明其不同意。由于它发生在远程端,因此异常会异步返回给调用者,因此会运行与关联的 Deferred
相关的 errback
处理程序。该 errback 接收一个 Failure
,它包装了 InsecureJelly
。
请记住,trap
会重新引发它没有被要求查找的异常。您每个 errback 处理程序只能检查一组异常:所有其他异常必须在后续处理程序中检查。 check_MyException
显示如何在单个 errback 中检查多种类型的异常:向 trap
提供一个异常类型列表,它将返回匹配的成员。在本例中,我们正在检查的异常类型(MyException
和 MyOtherException
)可能由远程端引发:它们继承自 pb.Error
。
处理程序可以返回 None
来终止对 errback 链的处理(准确地说,它切换到 errback 之后的回调;如果没有回调,则处理终止)。最好在链的末尾放置一个可以捕获所有异常的 errback(没有 trap
测试,没有可能引发更多异常的可能性,始终返回 None
)。就像使用常规的 try: except:
处理程序一样,您需要仔细考虑 errback 处理程序本身可能会引发异常的方式。在异步环境中,额外重要的是,从 Deferred
末尾掉落的异常不会被发出信号,直到该 Deferred
超出范围,并且在那时可能只会导致日志消息(如果 log.startLogging
未用于将其指向 stdout 或日志文件,则该日志消息甚至可能被丢弃)。相反,未被任何其他 except:
块处理的同步异常将非常明显地立即终止程序,并伴随一个嘈杂的堆栈跟踪。
callFour
展示了在使用 callRemote
时可能发生的另一种异常:pb.DeadReferenceError
。当远程端断开连接或崩溃时,就会发生这种情况,导致本地端保留一个陈旧的引用。这种异常恰好会立即报告(XXX:这是保证的吗?可能不是),因此必须在传统的同步 try: except pb.DeadReferenceError
块中捕获。
另一种可能发生的异常是 pb.PBConnectionLost
异常。如果在您等待 callRemote
调用完成时连接丢失,就会发生这种情况(异步)。当线路断开时,所有挂起的请求都将使用此异常终止。请注意,您无法知道请求是否已到达另一端,也无法知道在连接丢失之前它们在处理过程中进行到了什么程度。XXX:解释事务语义,找到一个合适的参考。
脚注