На этом шаге мы рассмотрим реализацию этой задачи.
Вы хотите реализовать простую систему удаленного вызова процедур (remote procedure call (RPC)) поверх слоя передачи сообщений, такого как сокеты,
соединения модуля multiprocessing или ZeroMO.
RPC легко реализовать путем упаковки с помощью pickle-запросов, аргументов и возвращаемых значений функций, а также передачи упакованной байтовой строки между интерпретаторами. Вот пример простого RPC-обработчика, который можно встроить в сервер:
# 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: # Получаем сообщение func_name, args, kwargs = pickle.loads(connection.recv()) # Запускаем RPC и посылаем ответ 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() # Удаленные функции def add(x, y): return x + y def sub(x, y): return x - y # Регистрируем с обработчиком handler = RPCHandler() handler.register_function(add) handler.register_function(sub) # Запускаем сервер 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, args, 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', 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' >>>
Стоит отметить, что многие слои передачи сообщений (такие как multiprocessing) уже сериализуют данные с помощью pickle. В этом случае
вызовы pickle.dumps() и pickle.loads() могут быть удалены.
Главная идея классов RPCHandler и RPCProxy относительно проста. Если клиент хочет вызывать удаленную функцию, такую как foo(1, 2, z=3), прокси-класс создает кортеж (foo, (1, 2), {z: 3}), который содержит имя функции и аргументы. Данный кортеж упаковывается и отсылается через соединение. Это выполняется в замыкании do_rpc(), которое возвращается методом __getattr__() класса RPCProxy. Сервер получает и распаковывает сообщение, проверяет, зарегистрировано ли имя функции, а затем выполняет ее с переданными аргументами. Результат (или исключение) упаковывается и отсылается обратно.
Как показано выше, данный пример использует для коммуникации модуль multiprocessing. Однако этот подход может быть применен практически для любой системы передачи сообщений. Например, если вы захотите реализовать RPC через ZeroMO, просто замените объекты соединений подходящими объектами сокетов ZeroMO.
Поскольку здесь применяется pickle, безопасность под угрозой (умный хакер может создать сообщения, которые выполнят произвольные функции во время распаковки). В частности, вы никогда не должны разрешать RPC от недоверенных или неаутентифицированных клиентов. И вы ни в коем случае не должны давать доступ с любого компьютера, подключенного к интернету: RPC нужно использовать внутри сети, за файерволом.
В качестве альтернативы pickle вы можете попробовать JSON, XML или какой-то другой способ кодирования данных для сериализации. Например, этот рецепт легко адаптировать к JSON - просто замените pickle.loads() и pickle.dumps() на json.loads() и json.dumps(). Например:
# rpcserver.py import json 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: # Получаем сообщение func_name, args, kwargs = json.loads(connection.recv()) # Запускаем RPC и посылаем ответ 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 # Пример 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() # Удаленные функции def add(x, y): return x + y def sub(x, y): return x - y # Регистрируем с обработчиком handler = RPCHandler() handler.register_function(add) handler.register_function(sub) # Запускаем сервер rpc_server(handler, ('localhost', 17000), authkey=b'peekaboo')
Сложный момент в реализации RPC - обработка исключений. По меньшей мере, сервер не должен падать, если метод возбуждает исключение. Однако средства отправки сообщений об исключениях обратно клиенту требуют изучения. Если вы используете pickle, экземпляры исключений часто сериализуются и заново возбуждаются уже на клиенте. Если вы используете какой-либо другой протокол, вам, вероятно, придется подумать об альтернативном подходе. Как минимум, вы, вероятно, захотите возвращать строку с исключением в ответе. Это подход, которому мы следовали в примере с JSON.
Еще один пример реализации RPC вы найдете в 209 шаге, где обсуждаются классы SimpleXMLRPCServer и ServerProxy.
На следующем шаге мы рассмотрим простую аутентификацию клиентов.