1. 程式人生 > 程式設計 >Python如何實現遠端方法呼叫

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遠端方法呼叫的資料請關注我們其它相關文章!