使用 Perspective Broker

基本示例

第一个要看的示例是一个完整的(尽管有点琐碎)应用程序。它在服务器端使用 PBServerFactory(),在客户端使用 PBClientFactory()

pbsimple.py

# 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()

pbsimpleclient.py

# 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 实例调用方法。在本示例中,初始根对象有一个方法,该方法返回对第二个对象的引用。

pb1server.py

#!/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()

pb1client.py

#!/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 。当对正在发送的方法调用的响应到达时,Deferredcallbackerrback 将被调用,具体取决于方法调用处理过程中是否发生了错误。

你可以使用此技术来提供对任意对象集的访问。请记住,任何可能被“通过网络传输”的对象都必须继承自 Referenceable (或其他变体)。如果你尝试传递一个不可引用对象(例如,通过从 remote_* 方法返回一个对象),你将得到一个 InsecureJelly 异常[2]

引用可以返回给你

如果你的服务器向客户端提供了一个引用,然后该客户端将该引用返回给服务器,服务器最终将获得与最初提供的相同对象。序列化层会监视返回的引用标识符,并将它们转换为实际对象。你需要始终了解对象的位置:如果它在你的这边,你进行实际的方法调用。如果它在另一边,你进行 .callRemote() [3]

pb2server.py

#!/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()

pb2client.py

#!/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 连接;之后一切都对称。

pb3server.py

#!/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()

pb3client.py

#!/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: 子句一样多的有关异常的信息,即使异常发生在过去某个未知时间点的其他人的内存空间中。

exc_server.py

#!/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()

exc_client.py

#!/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 中按顺序测试每种类型的异常。

trap_server.py

#!/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()

trap_client.py

#!/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 提供一个异常类型列表,它将返回匹配的成员。在本例中,我们正在检查的异常类型(MyExceptionMyOtherException)可能由远程端引发:它们继承自 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:解释事务语义,找到一个合适的参考。

脚注