目錄
第一部分: 經典 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 的對稱性意味着客戶端和服務器最終都是 “服務器”, 並且 “角色” 更多的是語義上的而不是程序上的。
第五部分: 異步操作與事件
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>
>>>