Bootstrap

Python RPyC库 官方文档 之三 教程

欢迎大家扫码关注我的微信公众号:
数据之恋

第一部分: 经典 RPyC 简介

我们将从经典的 RPyC 来开始我们的教程, 即 RPyC 2.60 的方法学。 因为 RPyC 3 版本对整个库做了重新设计, 所以有了些改变, 但是如果您对 RPyC 2.60 很熟悉, 您会有宾至如归的感觉。 如果您不熟悉, 我们保证您过一会也会有宾至如归的感觉。

1、运行一个服务器

让我们从基础开始 ———— 运行一个服务器。 在本教程中, 我们会将服务器和客户端运行在同一个机器上(localhost)。 经典的服务器可以使用下面的命令开启:

$ python bin/rpyc_classic.py
INFO:SLAVE/18812:server started on [127.0.0.1]:18812

下面展示了服务器正在运行时的参数:

  • “SLAVE” 表明了 “slaveService” (您稍后将会学到更多关于服务的知识), “[127.0.0.1]:18812” 是服务器绑定的地址, 在这个例子中, 服务器只接受来自于本机的连接。 如果您使用 “–host 0.0.0.0” 来启动一个服务器, 您将可以从任何地方来执行任何代码。

2、运行一个客户端

下一步是运行一个连接到服务器的一个客户端。 被用来创建一个连接到服务器的代码非常简单, 您会同意的:

import rpyc

conn = rpyc.classic.connect("localhost")

如果您的服务器没有在默认的端口上运行(TCP 18812), 您需要传入 “port=” 参数给 “classic.connect()”。

3、modules 的命名空间

连接对象的 modules 属性暴露的服务器的 module-space, 即它允许您进入远程模块。 下面是如何去做的方式:

rsys = conn.modules.sys # 服务器上的远程模块

这个点符号仅适用于顶层的模块。 每当您要对一个包中包含的模块进行嵌套导入时, 您必须使用括号符号来导入远程模块, 比如:

minidom = conn.modules["xml.dom.minidom"]

有了这个, 您几乎可以做任何事情。 举个例子, 这是您如何看服务器的命令行:

>>> rsys.argv
["bin/rpyc_classic.py"]

为服务器的导入机制添加模块搜索路径:

>>> rsys.path.append("/tmp/totally-secure-package-location")

改变服务器进程的当前工作目录:

>>> conn.modules.os.chdir("..")

或者甚至在服务器的标准输出中打印一些东西:

>>> print("Hello World", file=conn.modules.sys.stdout())

4、builtins 的命名空间

经典连接的 “builtins” 属性暴露了所有在服务器的 Python 环境中可使用的内建函数。 举个例子, 您可以用它来进入服务器上的一个文件:

>>> f = conn.builtins.open("/home/oblivious/.ssh/id_rsa")
>>> f.read()
'-----BEGIN RSA PRIVATE KEY-----\nMIIJKQIBAAKCAgEA0...XuVmz/ywq+5m\n-----END RSA PRIVATE KEY-----\n'

哎呀, 我刚刚泄露了我的私钥…

5、eval 和 execute 方法

如果您还没有满足, 接下来还有更多的: 经典连接也具有 eval 和 execute 属性, 他们允许您在服务器上求出任意表达式的值甚至执行任意的语句。 举个例子:

>>> conn.execute('import math')
>>> conn.eval('2 * math.pi')
6.283185307179586

但是, 这需要 RPyC 经典连接具有一些全局变量的概念, 您如何能看到它们? 它们可以通过 “namespace” 属性访问 ———— 每个新连接都会初始化该属性为一个空字典。 所以, 当我们导入之后, 我们现在可以:

>>> conn.namespace
{'__builtins__': <...>, 'math': <...>}

一些敏锐的读者可能会注意到上面的诡计都不是严格需要的, 因为使用 “conn.builtins.compile()” 方法 ———— 可以通过 “conn.modules.builtins.compile()” 方法访问到, 利用远程创建的字典手动提供该函数可以实现同样的功能。

这是真的, 但是有时我们需要一点糖。

6、teleport 方法

这有一个很有意思的方法, 它允许您传输一个方法到另一边, 并且在那里执行它们:

>>> def square(x):
...    return x ** 2
>>> fn = conn.teleport(square)
>>> fn(2)

这个期望计算 2 的平方, 当时这个计算是在远程执行的!

而且, 这种传输的方法是在远程命名空间中自动定义的:

>>> conn.eval('square(3)')
9

>>> conn.namespace['square'] is fn
True

并且传输的代码同样可以访问命名空间:

>>> con.execute('import sys')
>>> conn.teleport(lambda: print(sys.version_info))

在远程的终端中打印版本信息。

请注意, 当前它不可能支持传输任意的函数, 特别是闭包对象可能会出问题。 当出现问题时, 查看一下外部库可能是值得的, 比如 dill 库。

第二部分: 网络参考和异常

在第一部分中, 我们了解了如何使用 RPyC 经典连接来远程执行几乎所有事情。

目前一切看起来都很正常。 现在是时候弄脏我们的手, 来进一步理解引擎盖下到底发生了什么!

1. 设置

开启一个经典服务器使用:

python bin/rpyc_classic.py

连接好您的客户端:

>>> import rpyc
>>> conn = rpyc.classic.connect("localhost")

2. 网络引用(Netrefs)

我们知道我们可以使用 “conn.modules.sys” 来访问服务器上的 “sys” 模块。 但是那到底是什么神奇的东西呢?

>>> type(conn.modules.sys)
<netref class "builtins.module">
>>> type(conn.mudules.sys.path)
<netref class "builtins.list">
>>> type(conn.modules.os.path.abspath)
<netref class "builtins.function">

瞧, Netrefs(network reference, 网络引用, 也被成为透明对象代理)是一种特殊的对象, 它将应该在本地执行的所有事委托给相应的远程对象。 Netrefs 也许不是真正的函数和模块的列表, 但是它们 “尽全力” 去做到看起来和感觉起来像它们所指向的对象。 实际上, 它们甚至欺骗了 Python 的自省机制!

>>> isinstance(conn.modules.sys.path, list)
True

>>> import inspect
>>> inspect.isbuiltin(conn.modules.os.listdir)
True
>>> inspect.isfunction(conn.modules.os.path.abspath)
True
>>> inspect.ismethod(conn.modules.os.path.abspath)
False
>>> inspect.ismethod(conn.modules.sys.stdout.write)
True

很酷, 是不是?

我们都知道理解一件事的最好的方式就是粉碎它、 切开它并把它的内容洒向世界! 所以我们这么做:

>>> dir(conn.modules.sys.path)
['____conn__', '____oid__', '__add__', '__class__', '__contains__', '__delattr__',
'__delitem__', '__delslice__', '__doc__', '__eq__', '__ge__', '__getattribute__',
'__getitem__', '__getslice__', '__gt__', '__hash__', '__iadd__', '__imul__',
'__init__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__',
'__setitem__', '__setslice__', '__str__', 'append', 'count', 'extend', 'index', 'insert',
'pop', 'remove', 'reverse', 'sort']

除了一些期望的方法和属性, 您可能会注意到 ‘__conn’ 和 ‘__oid’。 这些属性保存了对象应该去处理的连接, 还有允许服务器从一个字典中去查找对象的标识符。

3. 异常

让我们继续这条令人愉快的毁灭之路。 毕竟, 事情并不总是光明的, 问题必须去解决。 当客户端发起了一个失败的请求(在服务器端抛出了异常), 异常会透明的传输到客户端。 看一下这个片段:

>>> conn.modules.sys.path[300]         # 在列表中只有 12 个元素
======= Remote traceback =======
Traceback (most recent call last):
  File "D:\projects\rpyc\core\protocol.py", line 164, in _dispatch_request
    res = self._handlers[handler](self, *args)
  File "D:\projects\rpyc\core\protocol.py", line 321, in _handle_callattr
    return attr(*args, **dict(kwargs))
IndexError: list index out of range

======= Local exception ========
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\projects\rpyc\core\netref.py", line 86, in method
    return self.____sync_req__(consts.HANDLE_CALLATTR, name, args, kwargs)
  File "D:\projects\rpyc\core\netref.py", line 53, in ____sync_req__
    return self.____conn__.sync_request(handler, self.____oid__, *args)
  File "D:\projects\rpyc\core\protocol.py", line 224, in sync_request
    self.serve()
  File "D:\projects\rpyc\core\protocol.py", line 196, in serve
    self._serve(msg, seq, args)
  File "D:\projects\rpyc\core\protocol.py", line 189, in _serve
    self._dispatch_exception(seq, args)
  File "D:\projects\rpyc\core\protocol.py", line 182, in _dispatch_exception
    raise obj
IndexError: list index out of range
>>>

就像您看到的, 我们得到了两个 traceback: 远程一个, 显示的是服务器端抛出的错误, 本地一个, 显示的是我们做了什么才导致出错。

第三部分: 服务和新形式的 RPyC

截止目前, 我们介绍了经典的 RPyC 的特性。 然而, RPyC 编程的新模型(从 RPyC 3.00 开始)是以服务为基础的。 就像您可能已经注意到经典模式中的客户端基本上完全控制了服务器, 这就是为什么我们(用来)调用 RPyC 从(slave)服务器的原因。 幸运的是, 情况不再是这样了。 新的模型是以服务为导向的: 服务向另一方提供了一组定义良好的功能的方法, 使得 RPyC 成为一个通用的 RPC 平台。 事实上, 目前为止您看到的经典的 RPyC 是一个简单的 “另一个” 服务。

服务其实很简单。 为了证明这个, “SlaveServie” (实现了经典的 RPyC 服务)才只有 30 行, 还包括了注释。 基本上, 一个服务具有以下的样板文件:

import rpyc

class MyService(rpyc.Service):
    def on_connect(self, conn):
        # code that runs when a connection is created
        # (to init the service, if needed)
        pass

    def on_disconnect(self, conn):
        # code that runs after the connection has already closed
        # (to finalize the service, if needed)
        pass

    def exposed_get_answer(self): # this is an exposed method
        return 42

    exposed_the_real_answer_though = 43     # an exposed attribute

    def get_question(self):  # while this method is not exposed
        return "what is the airspeed velocity of an unladen swallow?"

小贴士:

  • “on_connect” 和 “on_disconnect” 的 “conn” 参数是在 RPyC 4.0 中添加的。 这与以前的版本不向后兼容, 以前的版本中使用连接参数来调用服务的构造函数, 并把它存储到 “self._conn”。

就像您看到的, 除了一些特殊的初始化或终止化的方法之外, 您可以像定义其他类一样自由的定义类。 然而, 与常规类不同的是, 您可以选择那些属性将暴露给另一方: 如果名字是以 “exposed_” 开始的, 这个属性将可以被远程访问, 否则只能在本地进行访问。 在这个例子中, 客户端能够调用 “get_answer” 方法, 但是不能调用 “get_question” 方法, 我们马上就能看到。

向全世界暴露(或者说公开也行)出您的服务, 您需要开启一个服务器。 有很多方式可以做到这一点, 但是最简单的是:

# ... 从上面的代码片段继续 ...

if __name__ == "__main__":
	from rpyc.utils.server import ThreadedServer
	t = ThreadedServer(MyService, port=18861)
	t.start()

在远程的这一方, 服务作为连接的根对象(root object)被暴露, 例如 “conn.root”。 现在您知道了理解这个简短的 Demo 的全部:

>>> import rpyc
>>> c = rpyc.connect("localhost", 18861)
>>> c.root
<__main__.MyService objct at 0x834e1ac>

这个 “根对象” 是对服务器进程中的服务实例的一个引用(Netrefs)。 它可以用来访问和调用暴露出来的属性和方法。

>>> c.root.get_answer()
42
>>> c.root.the_real_answer_though
43

与此同时, 问题并没有被暴露出来:

>>> c.root.get_question()
======= Remote traceback =======
...
  File "/home/tomer/workspace/rpyc/core/protocol.py", line 298, in sync_request
    raise obj
AttributeError: cannot access 'get_question'

1. 访问策略

默认情况下, 属性和方法只有当使用了 “exposed_” 前缀后才可见。 这也意味着内建对象(如列表或字典)的属性默认情况下也不能访问。 如果需要的话, 您可以在创建一个服务器的时候通过传入一个适当的选项来设置它。 举个例子:

from rpyc.utils.server import ThreadedServer

server = ThreadedServer(MyService, port=18861, protocol_config={
	"allow_public_attrs": True, 
})
server.start()

有关所有可用的设置的描述, 参见 “DEFAULT_CONFIG”。

2. 共享服务实例

注意, 我们已经将 “MyService” 类传给了服务器, 效果是每个进来的连接都可以使用它自己的、 独立的 “MyService” 实例作为根对象。

如果您传入的不是类而是一个实例, 所有进来的连接将使用这个实例作为他们共享的根对象, 如:

t = ThreadedServer(MyService(), port=18861)

请注意上面例子中的微妙的区别(括号!)。

小贴士:

可以传输实例的功能是从 RPyC 4.0 版本开始提供的。 在早期的版本中, 您只能传一个类, 这样每个连接都将接收到一个独立的实例。

3. 向服务传入参数

在第二个示例中, 您传入一个完整构造的服务实例, 将额外的参数传递给 init 方法是简单的。 然而, 如果您想要为每个具有独立的根对象连接传递参数, 这种情形会麻烦一些。 在本例中, 向这样使用 “classpartial()”:

from rpyc.utils.helpers import classpartial

service = classpartial(MyService, 1, 2, pi=3)
t = ThreadedServer(service, port=18861)

小贴士:

classpartial 方法在 4.0 版本中添加。

4. 但是请等一下, 这还有更多的!

所有的服务都有一个名字, 通常是类的名称去掉 “Service” 后缀。 在我们的示例中, 服务的名称是 “MY”(服务的名称不区分大小写)。 如果您想要一个定制化的名字, 或多个名字(别名), 您可以通过设置 “ALTASES” 列表来实现。 第一个别名被认为是正式名称, 其他的被认为是别名:

class SomeOtherService(rpyc.Service):
	ALIASES = ["floop", "bloop"]
	...

在原始代码片段中, 客户端将得到:

>>> c.root.get_service_name()
"MY"
>>> c.root.get_service_aliases()
("MY",)

服务有名字是因为需要进行服务注册: 通常情况下, 一个服务器会广播它的详细信息到附近的注册服务器以供发现。 要使用服务发现, 请确保您已经启动了 “bin/rpyc_registry.py”。 服务器会监听 UDP 广播的 socket, 并且将会对关于哪些服务在何处运行的查询进行应答。

一旦一个注册的服务器在网络上 “可广播” 的位置运行, 并且该服务器已经设置了自动注册(默认), 客户端可自动发现服务。 想找到一个运行着的服务器, 可以传一个服务名字:

>>> rpyc.discover("MY")
(("192.168.1.101", 18861), )

如果您并不关心您连接的是哪个服务器, 您可以使用 “connect_by_service”:

>>> c2 = rpyc.connect_by_service("MY")
>>> c2.root.get_answer()
42

5. 服务的解耦合

截止目前为止, 我们仅仅讨论了服务器暴露出来(公开出来)的服务, 但是客户端呢? 客户端也要暴露服务么? 毕竟, RPyC 是一个对称的协议 ———— 客户端和服务器之间并没有区别。 好吧, 您可能已经想到了, 答案是 “对的”: 客户端和服务器都需要暴露服务。 然而, 由两方暴露的服务并不是相同的 ———— 他们是解耦的。

默认情况下, 客户端(使用 “connect()” 方法连接一个服务器)暴露 “VoidService” 方法。 就像名字表达的意思那样, 这个服务并不向另一方暴露功能, 意味着服务器不能向该客户端发出请求(除了显示的传递的功能, 比如回调函数)。 您可以在客户端通过向某个 “connect()” 方法传入 “service=” 参数来设置这个暴露的服务。

事实上, 连接两端的服务是解耦合的, 并不意味着他们是随意的。 举个例子, 服务 A 可能期望连接到服务 B ———— 如果不是这样将会报出运行时异常(大部分是 “ArributeError”)。 很多时候, 两端的服务可以是不同的, 但是请记住, 如果您需要在两方之间进行交互, 两个服务必须是 “兼容的”。

小贴士:

经典模式: 当使用了任何一个 “connect()” 方法, 客户端方面的服务也被设置为 “SlaveService”(与服务器端相同)。

第四部分: 回调与对称

在我们深入讨论异步调用之前, 我们必须讨论最后一个主体: 回调。 传入一个回调函数意味着将函数(或者在我们示例中的任何其他可调用对象)作为第一类对象, 即, 就像语言的其他值一样。 在 C 和 C++ 中是使用函数指针来实现的, 但是在 Python 中, 并没有什么特殊的机制。 当然了, 您肯定见过回调:

>>> def f(x):
...     return x**2
...
>>> map(f, range(10))   # f 作为一个参数传给了 map 方法
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

因为在 Python 中方法(其他值也一样)是对象, 还因为 RPyC 是对称的, 本地方法可以作为一个参数传递给远程对象, 反之亦然。 这有一个例子:

>>> import rpyc
>>> c = rpyc.classic.connect("localhost")
>>> rlist = c.modules.__builtin__.range(10)  # 这是一个远程列表
>>> rlist
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>>
>>> def f(x):
...     return x**3
...
>>> c.modules.__builtin__.map(f, rlist)  # 将本地方法 f 作为一个参数调用远程 map 方法
[0, 1, 8, 27, 64, 125, 216, 343, 512, 729]
>>>

# 更好的理解一下前面的例子:
>>> def g(x):
...     print "hi, this is g, executing locally", x
...     return x**3
...
>>> c.modules.__builtin__.map(g, rlist)
hi, this is g, executing locally 0
hi, this is g, executing locally 1
hi, this is g, executing locally 2
hi, this is g, executing locally 3
hi, this is g, executing locally 4
hi, this is g, executing locally 5
hi, this is g, executing locally 6
hi, this is g, executing locally 7
hi, this is g, executing locally 8
hi, this is g, executing locally 9
[0, 1, 8, 27, 64, 125, 216, 343, 512, 729]
>>>

要解释 RPyC 的对称性意味着什么, 请考虑下图:

RPyC的对称性说明

就像您看到的这样, 当客户端等待结果(同步请求)时, 它将服务于所有传入的请求, 这意味着服务器可以调用它从客户端处接收到的回调。 换句话说, RPyC 的对称性意味着客户端和服务器最终都是 “服务器”, 并且 “角色” 更多的是语义上的而不是程序上的。

第五部分: 异步操作与事件

1. 异步

本教程的最后一部分讨论 RPyC 程序更 “高级” 一点的: 异步操作 ———— RPyC 的一个关键特性。 您目前看到的代码都是同步的 ———— 可能跟您平时编写的代码很像: 当您调用一个函数时, 您将阻塞, 直到得到结果。 另一方面, 异步调用, 允许您开启请求并继续, 而不是等待。 您得到一个 “AsyncResult” (也被称为 “future” 或 “promise”)对象而不是得到该次调用的结果, 它最终将保存结果。

请注意, 执行异步请求的顺序并没有任何保证!

为了使一个远程方法的调用变成异步方式, 您要做的全部只是使用 “aysnc_” 来包装它, 它会创建一个包装器函数并返回一个 “AsyncResult” 而不是阻塞。 “AsyncResult” 对象有些属性和方法:

  • ready: 表示结果是否到达;

  • error: 表示结果是一个值还是一个异常;

  • expired: 表示 “AsyncResult” 对象是否过期(它的 “time-to-wait” 在结果回来之前就结束了)。 除非设置了 “set_expiry”, 否则对象将永不过期;

  • value: “AsyncResult” 对象包含的值。 如果结果并没有返回来, 访问这个属性将阻塞。 如果结果是一个异常, 访问这个属性将抛出该异常。 如果该对象已经过期, 将会抛出一个异常。 否则, 返回结果;

  • wait(): 等待结果返回, 或者直到对象过期;

  • add_callback(func): 添加一个当结果返回后的回调函数;

  • set_expiry(seconds): 设置 “AsyncResult” 对象的过期时间。 默认情况下, 没有过期时间;

这听起来有些复杂, 所以让我们看一下真实的代码, 让您相信它真的并不可怕:

>>> import rpyc
>>> c=rpyc.classic.connect("localhost")
>>> c.modules.time.sleep
<built-in function sleep>
>>> c.modules.time.sleep(2)  # 阻塞 2 秒, 直到调用结果返回

 # 使用 "async_" 包装远程方法, 使其转为异步调用
>>> asleep = rpyc.async_(c.modules.time.sleep)
>>> asleep
async_(<built-in function sleep>)

# 调用一个一步方法得到一个 "AsyncResult", 而不是一个结果
>>> res = asleep(15)
>>> res
<AsyncResult object (pending) at 0x0842c6bc>
>>> res.ready
False
>>> res.ready
False

# ... 15 秒后 ...
>>> res.ready
True
>>> print res.value
None
>>> res
<AsyncResult object (ready) at 0x0842c6bc>

这有一个更有趣的代码段:

>>> aint = rpyc.async_(c.modules.__builtin__.int)  # 异步包装远程 int() 方法

# 一个合法的调用
>>> x = aint("8")
>>> x
<AsyncResult object (pending) at 0x0844992c>
>>> x.ready
True
>>> x.error
False
>>> x.value
8

# 这个将引发异常
>>> x = aint("this is not a valid number")
>>> x
<AsyncResult object (pending) at 0x0847cb0c>
>>> x.ready
True
>>> x.error
True
>>> x.value
Traceback (most recent call last):
...
  File "/home/tomer/workspace/rpyc/core/async_.py", line 102, in value
    raise self._obj
ValueError: invalid literal for int() with base 10: 'this is not a valid number'
>>>

2. 事件

将 “async_” 和回调绑定会得到一个非常有趣的结果: 异步回调, 也被称为事件。 一般来说事件是由事件的生产者发出来通知事件的消费者相关的改变, 并且这个流是单向的(从生产者到消费者)。 换句话说, 在 RPC 术语中, 事件可以作为异步回调来实现, 其中忽略返回值。 想要更好的去阐释这种情况, 考虑下面的 “FileMonitor” 例子 ———— 它监视一个文件(使用 os.stat())的改变, 并且当发生改变(使用新旧状态结果)时通知客户端。

import rpyc
import os
import time
from threading import Thread

class FileMonitorService(rpyc.SlaveService):
    class exposed_FileMonitor(object):   # 暴露名字的方式不止局限于在方法上使用
        def __init__(self, filename, callback, interval = 1):
            self.filename = filename
            self.interval = interval
            self.last_stat = None
            self.callback = rpyc.async_(callback)   # 创建一个异步回调
            self.active = True
            self.thread = Thread(target = self.work)
            self.thread.start()
        def exposed_stop(self):   # 这个方法也必须被暴露出来
            self.active = False
            self.thread.join()
        def work(self):
            while self.active:
                stat = os.stat(self.filename)
                if self.last_stat is not None and self.last_stat != stat:
                    self.callback(self.last_stat, stat)   # 将这个改变通知给客户端
                self.last_stat = stat
                time.sleep(self.interval)

if __name__ == "__main__":
    from rpyc.utils.server import ThreadedServer
    ThreadedServer(FileMonitorService, port = 18871).start()

下面是事件的现场演示:

>>> import rpyc
>>>
>>> f = open("/tmp/floop.bloop", "w")
>>> conn = rpyc.connect("localhost", 18871)
>>> bgsrv = rpyc.BgServingThread(conn)  # 创建一个后台线程来处理进来的事件
>>>
>>> def on_file_changed(oldstat, newstat):
...     print "file changed"
...     print "    old stat: %s" % (oldstat,)
...     print "    new stat: %s" % (newstat,)
...
>>> mon = conn.root.FileMonitor("/tmp/floop.bloop", on_file_changed)  # 创建一个 FileMonitor

# 稍等一会, 以便于 FileMonitor 能够查看原始的文件

>>> f.write("shmoop")  # 改变大小
>>> f.flush()
 
# 过一会另一个线程会打印出
file changed
    old stat: (33188, 1564681L, 2051L, 1, 1011, 1011, 0L, 1225204483, 1225204483, 1225204483)
    new stat: (33188, 1564681L, 2051L, 1, 1011, 1011, 6L, 1225204483, 1225204556, 1225204556)

>>>
>>> f.write("groop")  # 改变大小
>>> f.flush()
file changed
    old stat: (33188, 1564681L, 2051L, 1, 1011, 1011, 6L, 1225204483, 1225204556, 1225204556)
    new stat: (33188, 1564681L, 2051L, 1, 1011, 1011, 11L, 1225204483, 1225204566, 1225204566)

>>> f.close()
>>> f = open(filename, "w")
file changed
    old stat: (33188, 1564681L, 2051L, 1, 1011, 1011, 11L, 1225204483, 1225204566, 1225204566)
    new stat: (33188, 1564681L, 2051L, 1, 1011, 1011, 0L, 1225204483, 1225204583, 1225204583)

>>> mon.stop()
>>> bgsrv.stop()
>>> conn.close()

请注意, 在这个示例中我使用了 “BgServingThread”, 它主要开启一个后台线程去为进来的所有请求服务, 这样主线程就可以自由地做它想做的。 您无须为它开启第二个线程, 如果您的应用程序有一个反应器(像 “gtk” 的 “gobject.io_add_watch”): 简单的将连接注册到反应器以便 “read”, 调用 “conn.serve”。 如果您没有反应器并且不希望去开启一个线程, 您应该意识到这些通知不会被处理, 除非您与该连接进行一个交互(它提取所有传入的请求)。 这有一个例子:

>>> f = open("/tmp/floop.bloop", "w")
>>> conn = rpyc.connect("localhost", 18871)
>>> mon = conn.root.FileMonitor("/tmp/floop.bloop", on_file_changed)
>>>

# 改变大小 ...
>>> f.write("shmoop")
>>> f.flush()

# ... 几秒过去了并没有打印任何消息 ...
# 直到我们与该连接进行一些交互后: 打印了一个远程对象调用
# 对象的远程 __str__, 这样所有等待的请求将突然间被处理
>>> print mon
file changed
    old stat: (33188, 1564681L, 2051L, 1, 1011, 1011, 0L, 1225205197, 1225205197, 1225205197)
    new stat: (33188, 1564681L, 2051L, 1, 1011, 1011, 6L, 1225205197, 1225205218, 1225205218)
<__main__.exposed_FileMonitor object at 0xb7a7a52c>
>>>
;