PB 可复制:传递复杂类型

概述

本章重点介绍如何使用 PB 将复杂类型(特别是类实例)传递到远程进程或从远程进程传递回来。第一部分介绍如何简单地将对象的內容复制到远程进程 (pb.Copyable )。第二部分介绍如何只复制一次內容,然后在內容发生变化时更新它们 (Cacheable )。

动机

上一章 中,你已经了解了如何通过在 callRemote 函数的参数或返回值中使用它们来将基本类型传递到远程进程。但是,如果你尝试过,你可能会发现当尝试传递比基本类型 int/list/dict/string 或其他 pb.Referenceable 对象更复杂的东西时会遇到问题。在某些时候,你希望在进程之间传递整个对象,而不是必须将它们简化为字典,然后在另一端重新实例化它们。

传递对象

将对象发送到远程进程的最明显和最直接的方法类似于以下代码。碰巧的是,这段代码不起作用,原因将在下面解释。

class LilyPond:
  def __init__(self, frogs):
    self.frogs = frogs

pond = LilyPond(12)
ref.callRemote("sendPond", pond)

如果你尝试运行这段代码,你可能会希望实现 remote_sendPond 方法的合适远程端会看到该方法被 LilyPond 类中的实例调用。但相反,你会遇到可怕的 InsecureJelly 异常。这是 Twisted 告诉你你违反了安全限制,接收端拒绝接受你的对象。

安全选项

有什么大不了的?将类简单地复制到另一个进程的命名空间有什么问题?

反过来问这个问题可能更容易理解问题所在:接受一个陌生人的请求,在你的本地命名空间中创建任意对象,有什么问题?真正的问题是你赋予了他们多少权力:他们可以基于通过远程连接发送给你的字节,说服你采取哪些行动。

对象通常比字符串和字典等基本类型代表更多的权力,因为它们还包含(或引用)代码,这些代码可以在执行时修改其他数据结构。一旦以前可信的数据被破坏,程序的其余部分就会受到损害。

内置的 Python “自带电池” 类相对温和,但你仍然不希望让外部程序使用它们在你的命名空间或你的计算机上创建任意对象。想象一下一个协议,该协议涉及发送一个带有 read() 方法的文件类对象,该方法应该稍后用于检索文档。然后想象一下,如果该对象是用 os.fdopen("~/.gnupg/secring.gpg") 创建的。或者 telnetlib.Telnet("localhost", "chargen") 的实例。

你为自己的程序编写的类可能具有更大的权力。它们可能会在 __init__ 期间运行代码,甚至仅仅因为它们的存在而具有特殊意义。一个程序可能具有 User 对象来表示用户帐户,并有一条规则说系统中的所有 User 对象在授权登录会话时都会被引用。(在这个系统中,User.__init__ 可能会将对象添加到已知用户的全局列表中)。创建对象的简单行为会授予某人访问权限。如果你被诱骗创建了一个错误的对象,未经授权的用户将获得访问权限。

因此,对象创建需要成为系统安全设计的一部分。“可信内部”和“不可信外部”之间的界线需要描述可以对外部事件做出哪些反应。其中一项事件是通过 PB 远程过程调用接收对象,这是一种在你的“内部”命名空间中创建对象的请求。问题是,如何对它做出反应。出于这个原因,你必须明确指定将接受哪些远程类,以及如何创建它们的本地代表。

使用哪个类?

在我们能够对传入的序列化对象做任何有用的事情之前,另一个需要回答的基本问题是:我们应该创建哪个类?简单的答案是创建与发送端序列化时“相同类型”的类,但这并不像你想象的那么容易或那么直接。请记住,请求来自不同的程序,使用可能不同的类库集。事实上,由于 PB 也在 Java、Emacs-Lisp 和其他语言中实现,因此无法保证发送方甚至在运行 Python!我们在接收端只知道两个描述他们试图发送给我们的实例的东西:类的名称,以及对象的內容的表示。

PB 允许你使用 setUnjellyableForClass 函数 [1] 指定从远程类名到本地类的映射。

此函数接受一个远程/发送方类引用(发送端使用的完全限定名称,或可以从中提取名称的类对象),以及一个本地/接收方类(用于为传入的序列化对象创建本地表示)。每当远程端发送对象时,它们传输的类名都会在由此函数控制的表中查找。如果找到匹配的类,则使用它来创建本地对象。如果没有找到,你会收到 InsecureJelly 异常。

通常,你希望两端共享相同的代码库:要么你控制在连接两端运行的程序,要么两个程序共享某种共同语言,该语言在两端都存在的代码中实现。你不会期望他们发送一个 MyFooziWhatZit 类的对象,除非你也有该类的定义。因此,Jelly 层拒绝所有传入的类,除了你使用 setUnjellyableForClass 明确标记的类是合理的。但请记住,发送方对 User 对象的理解可能与接收方的不同,这可能是由于无关包之间的命名空间冲突、尚未以相同速度更新的节点之间的版本偏差,或者恶意入侵者试图以某种有趣或可能存在漏洞的方式导致你的代码失败。

pb.Copyable

好了,理论就讲到这里。如何从一端发送一个完整的对象到另一端?

copy_sender.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 LilyPond:
    def setStuff(self, color, numFrogs):
        self.color = color
        self.numFrogs = numFrogs

    def countFrogs(self):
        print("%d frogs" % self.numFrogs)


class CopyPond(LilyPond, pb.Copyable):
    pass


class Sender:
    def __init__(self, pond):
        self.pond = pond

    def got_obj(self, remote):
        self.remote = remote
        d = remote.callRemote("takePond", self.pond)
        d.addCallback(self.ok).addErrback(self.notOk)

    def ok(self, response):
        print("pond arrived", response)
        reactor.stop()

    def notOk(self, failure):
        print("error during takePond:")
        if failure.type == jelly.InsecureJelly:
            print(" InsecureJelly")
        else:
            print(failure)
        reactor.stop()
        return None


def main():
    from copy_sender import CopyPond  # so it's not __main__.CopyPond

    pond = CopyPond()
    pond.setStuff("green", 7)
    pond.countFrogs()
    # class name:
    print(".".join([pond.__class__.__module__, pond.__class__.__name__]))

    sender = Sender(pond)
    factory = pb.PBClientFactory()
    reactor.connectTCP("localhost", 8800, factory)
    deferred = factory.getRootObject()
    deferred.addCallback(sender.got_obj)
    reactor.run()


if __name__ == "__main__":
    main()

copy_receiver.tac

# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

"""
PB copy receiver example.

This is a Twisted Application Configuration (tac) file.  Run with e.g.
   twistd -ny copy_receiver.tac

See the twistd(1) man page or
http://twistedmatrix.com/documents/current/howto/application for details.
"""


import sys

if __name__ == "__main__":
    print(__doc__)
    sys.exit(1)

from copy_sender import CopyPond, LilyPond

from twisted.application import internet, service
from twisted.internet import reactor
from twisted.python import log
from twisted.spread import pb

# log.startLogging(sys.stdout)


class ReceiverPond(pb.RemoteCopy, LilyPond):
    pass


pb.setUnjellyableForClass(CopyPond, ReceiverPond)


class Receiver(pb.Root):
    def remote_takePond(self, pond):
        print(" got pond:", pond)
        pond.countFrogs()
        return "safe and sound"  # positive acknowledgement

    def remote_shutdown(self):
        reactor.stop()


application = service.Application("copy_receiver")
internet.TCPServer(8800, pb.PBServerFactory(Receiver())).setServiceParent(
    service.IServiceCollection(application)
)

发送方有一个名为 LilyPond 的类。为了使该类能够通过 callRemote 传输(作为参数、返回值或两者引用的内容 [例如字典值]),它必须继承自四个 Serializable 类之一。在本节中,我们将重点关注 CopyableLilyPond 的可复制子类称为 CopyPond 。我们创建了它的一个实例,并将其作为参数通过 callRemote 发送到接收方的 remote_takePond 方法。Jelly 层将序列化(“jelly”)该对象,使其成为一个类名为“copy_sender.CopyPond”的实例,以及一些代表对象状态的数据块。 pond.__class__.__module__pond.__class__.__name__ 用于推导出类名字符串。对象的 getStateToCopy 方法用于获取状态:这是由 pb.Copyable 提供的,默认情况下只是检索 self.__dict__ 。这就像 pickle 使用的可选 __getstate__ 方法一样。名称和状态对将通过网络发送到接收方。

接收端定义了一个名为 ReceiverPond 的本地类来表示传入的 LilyPond 实例。该类继承自发送方的 LilyPond 类(完全限定名为 copy_sender.LilyPond ),它指定了我们期望它如何表现。我们相信这是与发送方使用的相同的 LilyPond 类。(至少,我们希望我们的类能够接受由发送方创建的状态)。它还继承自 pb.RemoteCopy ,这是所有充当本地代表角色的类的要求(这些类被赋予 setUnjellyableForClass 的第二个参数)。RemoteCopy 提供了告诉 Jelly 层如何从传入的序列化状态创建本地对象的方法。

然后使用 setUnjellyableForClass 注册这两个类。这有两个效果:远程类的实例(第一个参数)将被允许通过安全层,而本地类的实例(第二个参数)将用于包含在发送方序列化远程对象时传输的状态。

当接收方反序列化(“unjellies”)对象时,它将创建一个本地 ReceiverPond 类的实例,并将传输的状态(通常以字典的形式)传递给该对象的 setCopyableState 方法。这就像 pickle 在反序列化对象时使用的 __setstate__ 方法一样。 getStateToCopy /setCopyableState__getstate__ /__setstate__ 不同,允许对象以不同于它们传输的方式(跨 [内存] 空间)持久化(跨时间)。

当运行此代码时,它会生成以下输出

[-] twisted.spread.pb.PBServerFactory starting on 8800
[-] Starting factory <twisted.spread.pb.PBServerFactory instance at
0x406159cc>
[Broker,0,127.0.0.1]  got pond: <__builtin__.ReceiverPond instance at
0x406ec5ec>
[Broker,0,127.0.0.1] 7 frogs
$ ./copy_sender.py
7 frogs
copy_sender.CopyPond
pond arrived safe and sound
Main loop terminated.
$

控制复制的状态

通过覆盖 getStateToCopysetCopyableState ,您可以控制对象如何通过网络传输。例如,您可能希望执行一些数据缩减:预先计算一些结果,而不是将所有原始数据通过网络发送。或者,您可以在发送之前用标记替换对发送方本地对象的引用,然后在接收时用对接收方代理的引用替换这些标记,该代理可以对本地数据缓存执行相同的操作。

getStateToCopy 的另一个用途是实现“本地专用”属性:仅本地进程可访问的数据,远程用户不可访问。例如,可以在发送到远程系统之前从对象状态中删除 .password 属性。结合 Copyable 对象从往返中返回不变的事实,这可以用来构建一个挑战-响应系统(实际上 PB 使用 pb.Referenceable 对象来实现授权,如 这里 所述)。

发送对象从 getStateToCopy 返回的任何内容都将被序列化并通过网络发送;setCopyableState 获取通过网络传输的任何内容,并负责设置其所在对象的 state。

copy2_classes.py

#!/usr/bin/env python

# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

from twisted.spread import pb


class FrogPond:
    def __init__(self, numFrogs, numToads):
        self.numFrogs = numFrogs
        self.numToads = numToads

    def count(self):
        return self.numFrogs + self.numToads


class SenderPond(FrogPond, pb.Copyable):
    def getStateToCopy(self):
        d = self.__dict__.copy()
        d["frogsAndToads"] = d["numFrogs"] + d["numToads"]
        del d["numFrogs"]
        del d["numToads"]
        return d


class ReceiverPond(pb.RemoteCopy):
    def setCopyableState(self, state):
        self.__dict__ = state

    def count(self):
        return self.frogsAndToads


pb.setUnjellyableForClass(SenderPond, ReceiverPond)

copy2_sender.py

#!/usr/bin/env python

# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.


from copy2_classes import SenderPond

from twisted.internet import reactor
from twisted.python import log
from twisted.spread import jelly, pb


class Sender:
    def __init__(self, pond):
        self.pond = pond

    def got_obj(self, obj):
        d = obj.callRemote("takePond", self.pond)
        d.addCallback(self.ok).addErrback(self.notOk)

    def ok(self, response):
        print("pond arrived", response)
        reactor.stop()

    def notOk(self, failure):
        print("error during takePond:")
        if failure.type == jelly.InsecureJelly:
            print(" InsecureJelly")
        else:
            print(failure)
        reactor.stop()
        return None


def main():
    pond = SenderPond(3, 4)
    print("count %d" % pond.count())

    sender = Sender(pond)
    factory = pb.PBClientFactory()
    reactor.connectTCP("localhost", 8800, factory)
    deferred = factory.getRootObject()
    deferred.addCallback(sender.got_obj)
    reactor.run()


if __name__ == "__main__":
    main()

copy2_receiver.py

#!/usr/bin/env python

# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.


import copy2_classes  # needed to get ReceiverPond registered with Jelly

from twisted.application import internet, service
from twisted.internet import reactor
from twisted.spread import pb


class Receiver(pb.Root):
    def remote_takePond(self, pond):
        print(" got pond:", pond)
        print(" count %d" % pond.count())
        return "safe and sound"  # positive acknowledgement

    def remote_shutdown(self):
        reactor.stop()


application = service.Application("copy_receiver")
internet.TCPServer(8800, pb.PBServerFactory(Receiver())).setServiceParent(
    service.IServiceCollection(application)
)

在此示例中,类在单独的源文件中定义,该文件还设置了它们之间的绑定。 SenderPondReceiverPond 除此绑定之外没有其他关系:它们恰好实现了相同的方法,但使用不同的内部实例变量来实现它们。

对象的接收者甚至不必将类定义导入其命名空间。他们导入类定义(从而执行 setUnjellyableForClass 语句)就足够了。Jelly 层会记住类定义,直到收到匹配的对象。当然,对象的发送者需要定义来首先创建对象。

当运行时, copy2 示例会发出以下内容

$ twistd -n -y copy2_receiver.py
[-] twisted.spread.pb.PBServerFactory starting on 8800
[-] Starting factory <twisted.spread.pb.PBServerFactory instance at
0x40604b4c>
[Broker,0,127.0.0.1]  got pond: <copy2_classes.ReceiverPond instance at
0x406eb2ac>
[Broker,0,127.0.0.1]  count 7
$ ./copy2_sender.py
count 7
pond arrived safe and sound
Main loop terminated.

需要注意的事项

  • setUnjellyableForClass 的第一个参数必须引用 发送方已知的 类。发送方无法知道您的本地 import 语句是如何设置的,而 Python 的灵活命名空间语义允许您通过各种不同的名称访问同一个类。您必须与发送方所做的匹配。让两端都从一个单独的文件中导入类,使用规范的模块名称(没有“同级导入”),是确保正确性的好方法,尤其是在发送类和接收类一起定义时, setUnjellyableForClass 紧随其后。

  • 发送的类必须继承自 pb.Copyable 。注册接收该类的类必须继承自 pb.RemoteCopy [2]

  • 同一个类可以用于发送和接收。只需让它同时继承自 pb.Copyablepb.RemoteCopy 。这将使它能够以对称的方式在网络上发送和接收同一个类。但不要混淆它何时到达(并使用 setCopyableState )以及何时发送(使用 getStateToCopy )。

  • InsecureJelly 异常由接收端引发。它们将被异步地传递给 errback 处理程序。如果你没有在 callRemote 返回的 Deferred 中添加一个,那么你将永远不会收到问题的通知。

  • pb.RemoteCopy 派生的类将使用一个不带参数的构造函数 __init__ 方法创建。所有设置必须在 setCopyableState 方法中执行。正如 RemoteCopy 上的文档字符串所说,不要在 RemoteCopy 的子类中实现需要参数的构造函数。

更多信息

  • pb.Copyable 主要在 twisted.spread.flavors 中实现,那里的文档字符串是获取更多信息的最佳来源。

  • Copyable 也用于 twisted.web.distrib 将 HTTP 请求传递给其他程序以进行渲染,允许将 URL 空间的子树委托给多个程序(在多台机器上)。

pb.Cacheable

有时,你想要发送到远程进程的对象很大且很慢。“大”意味着表示它的状态需要大量数据(存储、网络带宽、处理)。“慢”意味着状态不经常改变。将完整状态只发送一次(第一次需要时)可能更有效,然后在每次修改时只发送状态的差异或更改。 pb.Cacheable 类提供了一个框架来实现这一点。

pb.Cacheable 继承自 pb.Copyable ,因此它基于在发送端捕获对象状态,然后在接收端将其转换为新对象的思想。这被扩展为让对象在发送端“发布”(从 pb.Cacheable 派生),与接收端的一个“观察者”匹配(从 pb.RemoteCache 派生)。

为了有效地使用 pb.Cacheable ,你需要将对对象的更改隔离到访问器函数(特别是“设置器”函数)中。你的对象需要在每次更改某个属性时获得控制权 [3]

你从 pb.Cacheable 派生你的发送端类,并添加两个方法: getStateToCacheAndObserveForstoppedObserving 。第一个方法在第一次创建远程缓存引用时被调用,并检索用于第一次填充缓存的数据。它还提供了一个名为“观察者”的对象 [4] ,它指向接收端缓存。每次对象的状态发生变化时,你都会向观察者发送一条消息,通知他们更改。另一个方法 stoppedObserving 在远程缓存消失时被调用,以便你可以停止发送更新。

在接收端,你让你的缓存类继承自 pb.RemoteCache ,并实现 setCopyableState ,就像你对 pb.RemoteCopy 对象一样。此外,你必须实现方法来接收由 pb.Cacheable 发送给观察者的更新:这些方法的名称应该以 observe_ 开头,并与发送端的 callRemote 调用匹配,就像通常的 remote_*perspective_* 方法与正常的 callRemote 调用匹配一样。

第一次将对 pb.Cacheable 对象的引用发送到任何特定接收者时,将为其创建一个发送端观察者,并将调用 getStateToCacheAndObserveFor 方法来获取当前状态并注册观察者。该方法返回的状态将被发送到远程端,并使用 setCopyableState 转换为本地表示,就像上面描述的 pb.RemoteCopy 一样(实际上它继承自该类)。

之后,发送端的“设置器”函数应该在观察者上调用 callRemote ,这会导致接收端上的 observe_* 方法运行,然后这些方法应该更新接收端本地(缓存)状态。

当接收者停止跟踪缓存的对象,最后一个引用消失时,可以释放 pb.RemoteCache 对象。在它死亡之前,它会告诉发送端它不再关心原始对象。当引用计数变为零时,观察者消失,pb.Cacheable 对象可以停止宣布发生的每个更改。 stoppedObserving 方法用于告诉 pb.Cacheable 观察者已经消失。

有了 pb.Cacheablepb.RemoteCache 类,通过调用 pb.setUnjellyableForClass 将它们绑定在一起,剩下的就是将对你的 pb.Cacheable 的引用通过网络传递到远程端。相应的 pb.RemoteCache 对象将自动创建,并且匹配的方法将用于使接收端从属对象与发送端主对象同步。

示例

这是一个完整的示例,其中 MasterDuckPond 由发送端控制,而 SlaveDuckPond 是一个跟踪主对象更改的缓存

cache_classes.py

#!/usr/bin/env python

# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.


from twisted.spread import pb


class MasterDuckPond(pb.Cacheable):
    def __init__(self, ducks):
        self.observers = []
        self.ducks = ducks

    def count(self):
        print("I have [%d] ducks" % len(self.ducks))

    def addDuck(self, duck):
        self.ducks.append(duck)
        for o in self.observers:
            o.callRemote("addDuck", duck)

    def removeDuck(self, duck):
        self.ducks.remove(duck)
        for o in self.observers:
            o.callRemote("removeDuck", duck)

    def getStateToCacheAndObserveFor(self, perspective, observer):
        self.observers.append(observer)
        # you should ignore pb.Cacheable-specific state, like self.observers
        return self.ducks  # in this case, just a list of ducks

    def stoppedObserving(self, perspective, observer):
        self.observers.remove(observer)


class SlaveDuckPond(pb.RemoteCache):
    # This is a cache of a remote MasterDuckPond
    def count(self):
        return len(self.cacheducks)

    def getDucks(self):
        return self.cacheducks

    def setCopyableState(self, state):
        print(" cache - sitting, er, setting ducks")
        self.cacheducks = state

    def observe_addDuck(self, newDuck):
        print(" cache - addDuck")
        self.cacheducks.append(newDuck)

    def observe_removeDuck(self, deadDuck):
        print(" cache - removeDuck")
        self.cacheducks.remove(deadDuck)


pb.setUnjellyableForClass(MasterDuckPond, SlaveDuckPond)

cache_sender.py

#!/usr/bin/env python

# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

from cache_classes import MasterDuckPond

from twisted.internet import reactor
from twisted.python import log
from twisted.spread import jelly, pb


class Sender:
    def __init__(self, pond):
        self.pond = pond

    def phase1(self, remote):
        self.remote = remote
        d = remote.callRemote("takePond", self.pond)
        d.addCallback(self.phase2).addErrback(log.err)

    def phase2(self, response):
        self.pond.addDuck("ugly duckling")
        self.pond.count()
        reactor.callLater(1, self.phase3)

    def phase3(self):
        d = self.remote.callRemote("checkDucks")
        d.addCallback(self.phase4).addErrback(log.err)

    def phase4(self, dummy):
        self.pond.removeDuck("one duck")
        self.pond.count()
        self.remote.callRemote("checkDucks")
        d = self.remote.callRemote("ignorePond")
        d.addCallback(self.phase5)

    def phase5(self, dummy):
        d = self.remote.callRemote("shutdown")
        d.addCallback(self.phase6)

    def phase6(self, dummy):
        reactor.stop()


def main():
    master = MasterDuckPond(["one duck", "two duck"])
    master.count()

    sender = Sender(master)
    factory = pb.PBClientFactory()
    reactor.connectTCP("localhost", 8800, factory)
    deferred = factory.getRootObject()
    deferred.addCallback(sender.phase1)
    reactor.run()


if __name__ == "__main__":
    main()

cache_receiver.py

#!/usr/bin/env python

# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.


import cache_classes

from twisted.application import internet, service
from twisted.internet import reactor
from twisted.spread import pb


class Receiver(pb.Root):
    def remote_takePond(self, pond):
        self.pond = pond
        print("got pond:", pond)  # a DuckPondCache
        self.remote_checkDucks()

    def remote_checkDucks(self):
        print("[%d] ducks: " % self.pond.count(), self.pond.getDucks())

    def remote_ignorePond(self):
        # stop watching the pond
        print("dropping pond")
        # gc causes __del__ causes 'decache' msg causes stoppedObserving
        self.pond = None

    def remote_shutdown(self):
        reactor.stop()


application = service.Application("copy_receiver")
internet.TCPServer(8800, pb.PBServerFactory(Receiver())).setServiceParent(
    service.IServiceCollection(application)
)

运行时,此示例将输出以下内容

$ twistd -n -y cache_receiver.py
[-] twisted.spread.pb.PBServerFactory starting on 8800
[-] Starting factory <twisted.spread.pb.PBServerFactory instance at
0x40615acc>
[Broker,0,127.0.0.1]  cache - sitting, er, setting ducks
[Broker,0,127.0.0.1] got pond: <cache_classes.SlaveDuckPond instance at
0x406eb5ec>
[Broker,0,127.0.0.1] [2] ducks:  ['one duck', 'two duck']
[Broker,0,127.0.0.1]  cache - addDuck
[Broker,0,127.0.0.1] [3] ducks:  ['one duck', 'two duck', 'ugly duckling']
[Broker,0,127.0.0.1]  cache - removeDuck
[Broker,0,127.0.0.1] [2] ducks:  ['two duck', 'ugly duckling']
[Broker,0,127.0.0.1] dropping pond
$ ./cache_sender.py
I have [2] ducks
I have [3] ducks
I have [2] ducks
Main loop terminated.

需要注意的要点

  • 对于每个持有活动引用的远程程序,都有一个 Observer。同一程序内的多个引用无关紧要:序列化层会注意到重复项并进行相应的引用计数 [5]

  • 多个 Observer 需要保存在一个列表中,并且当发生变化时,所有 Observer 都需要更新。通过在将 Observer 添加到列表的同时发送初始状态,在一个无法被状态更改中断的原子操作中,您可以确保可以向所有 Observer 发送相同的状态更新。

  • observer.callRemote 调用仍然可能失败。如果远程端最近断开连接并且 stoppedObserving 尚未被调用,您可能会收到 DeadReferenceError 。最好在这些 callRemote 中添加一个 errback 来丢弃此类错误。这是一个有用的习惯用法。

    observer.callRemote('foo', arg).addErrback(lambda f: None)
    
  • getStateToCacheAndObserverFor 必须返回一些表示对象当前状态的对象。这可能只是对象的 __dict__ 属性。最好在将其发送到远程端之前删除其 pb.Cacheable 特定成员。特别是,Observer 列表应该被排除在外,以避免令人眼花缭乱的递归 Cacheable 引用。如果保留这样的项目,其潜在后果令人难以置信。

  • getStateToCacheAndObserveForstoppedObserving 都可以使用 perspective 参数。我认为这样做是为了允许查看器特定更改缓存更新方式。如果所有远程查看器都应该看到相同的数据,则可以忽略它。

更多信息

  • 有关信息的最佳来源来自 twisted.spread.flavors 中的文档字符串,其中实现了 pb.Cacheable

  • spread.publish 模块也使用 Cacheable ,并且可能是进一步信息的来源。

脚注