11.8 实现远程方法调用
最后更新于:2022-04-01 15:38:48
## 问题
You want to implement simple remote procedure call (RPC) on top of a message passinglayer, such as sockets, multiprocessing connections, or ZeroMQ.
## 解决方案
RPC is easy to implement by encoding function requests, arguments, and return valuesusing pickle, and passing the pickled byte strings between interpreters. Here is anexample of a simple RPC handler that could be incorporated into a server:
# rpcserver.py
import pickleclass RPCHandler:
> def __init__(self):self._functions = { }def register_function(self, func):self._functions[func.__name__] = funcdef handle_connection(self, connection):try:while True:> # Receive a messagefunc_name, args, kwargs = pickle.loads(connection.recv())# Run the RPC and send a responsetry:
> > r = self._functions[func_name]([*](#)args,**kwargs)connection.send(pickle.dumps(r))
except Exception as e:connection.send(pickle.dumps(e))except EOFError:pass
To use this handler, you need to add it into a messaging server. There are many possiblechoices, but the multiprocessing library provides a simple option. Here is an exampleRPC server:
from multiprocessing.connection import Listenerfrom 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 = Truet.start()
# Some remote functionsdef add(x, y):
> return x + y
def sub(x, y):return x - y
# Register with a handlerhandler = RPCHandler()handler.register_function(add)handler.register_function(sub)
# Run the serverrpc_server(handler, (‘localhost', 17000), authkey=b'peekaboo')
To access the server from a remote client, you need to create a corresponding RPC proxyclass that forwards requests. For example:
import pickle
class RPCProxy:def __init__(self, connection):self._connection = connectiondef __getattr__(self, name):def do_rpc([*](#)args, [**](#)kwargs):
self._connection.send(pickle.dumps((name, args, kwargs)))result = pickle.loads(self._connection.recv())if isinstance(result, Exception):
> raise result
return result
return do_rpc
To use the proxy, you wrap it around a connection to the server. For example:
>>> from multiprocessing.connection import Client
>>> c = Client(('localhost', 17000), 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'>>>
It should be noted that many messaging layers (such as multiprocessing) already se‐rialize data using pickle. If this is the case, the pickle.dumps() and pickle.loads()calls can be eliminated.
## 讨论
The general idea of the RPCHandler and RPCProxy classes is relatively simple. If a clientwants to call a remote function, such as foo(1, 2, z=3), the proxy class creates a tuple(‘foo', (1, 2), {‘z': 3}) that contains the function name and arguments. Thistuple is pickled and sent over the connection. This is performed in the do_rpc() closurethat’s returned by the __getattr__() method of RPCProxy. The server receives andunpickles the message, looks up the function name to see if it’s registered, and executesit with the given arguments. The result (or exception) is then pickled and sent back.As shown, the example relies on multiprocessing for communication. However, thisapproach could be made to work with just about any other messaging system. For ex‐ample, if you want to implement RPC over ZeroMQ, just replace the connection objectswith an appropriate ZeroMQ socket object.Given the reliance on pickle, security is a major concern (because a clever hacker cancreate messages that make arbitrary functions execute during unpickling). In particular,you should never allow RPC from untrusted or unauthenticated clients. In particular,you definitely don’t want to allow access from just any machine on the Internet—thisshould really only be used internally, behind a firewall, and not exposed to the rest ofthe world.As an alternative to pickle, you might consider the use of JSON, XML, or some otherdata encoding for serialization. For example, this recipe is fairly easy to adapt to JSONencodingif you simply replace pickle.loads() and pickle.dumps() withjson.loads() and json.dumps(). For example:
# jsonrpcserver.pyimport json
class RPCHandler:def __init__(self):self._functions = { }def register_function(self, func):self._functions[func.__name__] = funcdef handle_connection(self, connection):try:while True:
# Receive a messagefunc_name, args, kwargs = json.loads(connection.recv())# Run the RPC and send a responsetry:
> 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.pyimport json
class RPCProxy:def __init__(self, connection):self._connection = connectiondef __getattr__(self, name):def do_rpc([*](#)args, [**](#)kwargs):self._connection.send(json.dumps((name, args, kwargs)))result = json.loads(self._connection.recv())return result
return do_rpc
One complicated factor in implementing RPC is how to handle exceptions. At the veryleast, the server shouldn’t crash if an exception is raised by a method. However, themeans by which the exception gets reported back to the client requires some study. Ifyou’re using pickle, exception instances can often be serialized and reraised in theclient. If you’re using some other protocol, you might have to think of an alternativeapproach. At the very least, you would probably want to return the exception string inthe response. This is the approach taken in the JSON example.For another example of an RPC implementation, it can be useful to look at the imple‐mentation of the SimpleXMLRPCServer and ServerProxy classes used in XML-RPC, asdescribed in Recipe 11.6.