Python如何實現遠端方法呼叫
問題
你想在一個訊息傳輸層如 sockets 、multiprocessing connections 或 ZeroMQ 的基礎之上實現一個簡單的遠端過程呼叫(RPC)。
解決方案
將函式請求、引數和返回值使用pickle編碼後,在不同的直譯器直接傳送pickle位元組字串,可以很容易的實現RPC。 下面是一個簡單的PRC處理器,可以被整合到一個伺服器中去:
# rpcserver.py import pickle class RPCHandler: def __init__(self): self._functions = { } def register_function(self,func): self._functions[func.__name__] = func def handle_connection(self,connection): try: while True: # Receive a message func_name,args,kwargs = pickle.loads(connection.recv()) # Run the RPC and send a response try: r = self._functions[func_name](*args,**kwargs) connection.send(pickle.dumps(r)) except Exception as e: connection.send(pickle.dumps(e)) except EOFError: pass
要使用這個處理器,你需要將它加入到一個訊息伺服器中。你有很多種選擇, 但是使用 multiprocessing 庫是最簡單的。下面是一個RPC伺服器例子:
from multiprocessing.connection import Listener from threading import Thread def rpc_server(handler,address,authkey): sock = Listener(address,authkey=authkey) while True: client = sock.accept() t = Thread(target=handler.handle_connection,args=(client,)) t.daemon = True t.start() # Some remote functions def add(x,y): return x + y def sub(x,y): return x - y # Register with a handler handler = RPCHandler() handler.register_function(add) handler.register_function(sub) # Run the server rpc_server(handler,('localhost',17000),authkey=b'peekaboo')
為了從一個遠端客戶端訪問伺服器,你需要建立一個對應的用來傳送請求的RPC代理類。例如
import pickle class RPCProxy: def __init__(self,connection): self._connection = connection def __getattr__(self,name): def do_rpc(*args,**kwargs): self._connection.send(pickle.dumps((name,kwargs))) result = pickle.loads(self._connection.recv()) if isinstance(result,Exception): raise result return result return do_rpc
要使用這個代理類,你需要將其包裝到一個伺服器的連線上面,例如:
>>> from multiprocessing.connection import Client >>> c = Client(('localhost',authkey=b'peekaboo') >>> proxy = RPCProxy(c) >>> proxy.add(2,3) 5 >>> proxy.sub(2,3) -1 >>> proxy.sub([1,2],4) Traceback (most recent call last): File "<stdin>",line 1,in <module> File "rpcserver.py",line 37,in do_rpc raise result TypeError: unsupported operand type(s) for -: 'list' and 'int' >>>
要注意的是很多訊息層(比如 multiprocessing )已經使用pickle序列化了資料。 如果是這樣的話,對 pickle.dumps() 和 pickle.loads() 的呼叫要去掉。
討論
RPCHandler 和 RPCProxy 的基本思路是很比較簡單的。 如果一個客戶端想要呼叫一個遠端函式,比如 foo(1,2,z=3),代理類建立一個包含了函式名和引數的元組 ('foo',(1,2),{'z': 3}) 。 這個元組被pickle序列化後通過網路連線發生出去。 這一步在 RPCProxy 的 __getattr__() 方法返回的 do_rpc() 閉包中完成。 伺服器接收後通過pickle反序列化訊息,查詢函式名看看是否已經註冊過,然後執行相應的函式。 執行結果(或異常)被pickle序列化後返回傳送給客戶端。我們的例項需要依賴 multiprocessing 進行通訊。 不過,這種方式可以適用於其他任何訊息系統。例如,如果你想在ZeroMQ之上實習RPC, 僅僅只需要將連線物件換成合適的ZeroMQ的socket物件即可。
由於底層需要依賴pickle,那麼安全問題就需要考慮了 (因為一個聰明的黑客可以建立特定的訊息,能夠讓任意函式通過pickle反序列化後被執行)。 因此你永遠不要允許來自不信任或未認證的客戶端的RPC。特別是你絕對不要允許來自Internet的任意機器的訪問, 這種只能在內部被使用,位於防火牆後面並且不要對外暴露。
作為pickle的替代,你也許可以考慮使用JSON、XML或一些其他的編碼格式來序列化訊息。 例如,本機例項可以很容易的改寫成JSON編碼方案。還需要將 pickle.loads() 和 pickle.dumps() 替換成 json.loads() 和 json.dumps() 即可:
# jsonrpcserver.py import json class RPCHandler: def __init__(self): self._functions = { } def register_function(self,kwargs = json.loads(connection.recv()) # Run the RPC and send a response try: r = self._functions[func_name](*args,**kwargs) connection.send(json.dumps(r)) except Exception as e: connection.send(json.dumps(str(e))) except EOFError: pass # jsonrpcclient.py import json class RPCProxy: def __init__(self,**kwargs): self._connection.send(json.dumps((name,kwargs))) result = json.loads(self._connection.recv()) return result return do_rpc
實現RPC的一個比較複雜的問題是如何去處理異常。至少,當方法產生異常時伺服器不應該奔潰。 因此,返回給客戶端的異常所代表的含義就要好好設計了。 如果你使用pickle,異常物件例項在客戶端能被反序列化並丟擲。如果你使用其他的協議,那得想想另外的方法了。 不過至少,你應該在響應中返回異常字串。我們在JSON的例子中就是使用的這種方式。
以上就是Python如何實現遠端方法呼叫的詳細內容,更多關於Python遠端方法呼叫的資料請關注我們其它相關文章!