如何通过 RPC 访问属性

How to access properties via RPC

我参考 Python 的 xmlrpc 模块实现了 Pickle-RPC。

从客户端,我可以调用注册到 RPC 服务器的实例的方法。

s = ServerProxy(('127.0.0.1', 49152), DatagramRequestSender)
s.foo('bar')

我也想访问属性。

s = ServerProxy(('127.0.0.1', 49152), DatagramRequestSender)
s.foobar

我应该如何实施?

client.py

import pickle
import socket
from io import BytesIO


class StreamRequestSender:
    max_packet_size = 8192
    address_family = socket.AF_INET
    socket_type = socket.SOCK_STREAM

    def send_request(self, address, request):
        with socket.socket(self.address_family, self.socket_type) as client_socket:
            with client_socket.makefile('wb') as wfile:
                pickle.dump(request, wfile)
            with client_socket.makefile('rb') as rfile:
                response = pickle.load(rfile)
                return response


class DatagramRequestSender(StreamRequestSender):
    socket_type = socket.SOCK_DGRAM

    def send_request(self, address, request):
        with socket.socket(self.address_family, self.socket_type) as client_socket:
            with BytesIO() as wfile:
                pickle.dump(request, wfile)
                client_socket.sendto(wfile.getvalue(), address)
            data = client_socket.recv(self.max_packet_size)
            with BytesIO(data) as rfile:
                response = pickle.load(rfile)
                return response


class ServerProxy:
    def __init__(self, address, RequestSenderClass):
        self.__address = address
        self.__request_sender = RequestSenderClass()

    def __send(self, method, args):
        request = (method, args)
        response = self.__request_sender.send_request(self.__address, request)
        return response

    def __getattr__(self, name):
        return _Method(self.__send, name)


class _Method:
    def __init__(self, send, name):
        self.__send = send
        self.__name = name

    def __getattr__(self, name):
        return _Method(self.__send, "{}.{}".format(self.__name, name))

    def __call__(self, *args):
        return self.__send(self.__name, args)


if __name__ == '__main__':
    s = ServerProxy(('127.0.0.1', 49152), DatagramRequestSender)
    print(s.pow(2, 160))

server.py

import pickle
import sys
from socketserver import StreamRequestHandler, DatagramRequestHandler, ThreadingTCPServer, ThreadingUDPServer


class RPCRequestHandler:
    def handle(self):
        method, args = pickle.load(self.rfile)
        response = self.server.dispatch(method, args)
        pickle.dump(response, self.wfile)


class StreamRPCRequestHandler(RPCRequestHandler, StreamRequestHandler):
    pass


class DatagramRPCRequestHandler(RPCRequestHandler, DatagramRequestHandler):
    pass


class RPCDispatcher:
    def __init__(self, instance=None):
        self.__instance = instance

    def register_instance(self, instance):
        self.__instance = instance

    def dispatch(self, method, args):
        _method = None
        if self.__instance is not None:
            try:
                _method = self._resolve_dotted_attribute(self.__instance, method)
            except AttributeError:
                pass
        if _method is not None:
            return _method(*args)
        else:
            raise Exception('method "{}" is not supported'.format(method))

    @staticmethod
    def _resolve_dotted_attribute(obj, dotted_attribute):
        attributes = dotted_attribute.split('.')
        for attribute in attributes:
            if attribute.startswith('_'):
                raise AttributeError('attempt to access private attribute "{}"'.format(attribute))
            else:
                obj = getattr(obj, attribute)
        return obj


class RPCServer(ThreadingUDPServer, RPCDispatcher):
    def __init__(self, server_address, RPCRequestHandlerClass, instance=None):
        ThreadingUDPServer.__init__(self, server_address, RPCRequestHandlerClass)
        RPCDispatcher.__init__(self, instance)


if __name__ == '__main__':
    class ExampleService:
        def pow(self, base, exp):
            return base ** exp


    s = RPCServer(('127.0.0.1', 49152), DatagramRPCRequestHandler, ExampleService())
    print('Serving Pickle-RPC on localhost port 49152')
    print('It is advisable to run this example server within a secure, closed network.')
    try:
        s.serve_forever()
    except KeyboardInterrupt:
        print("\nKeyboard interrupt received, exiting.")
        s.server_close()
        sys.exit(0)

您必须通过覆盖对象的 __getattr__ 方法来拦截对象的属性访问,弄清楚它只是一个属性访问而不是方法调用,然后向您的服务器发送特定消息它不调用该方法,而是直接 returns 引用属性的值。这正是what Pyro4 does. Code: https://github.com/irmen/Pyro4/blob/master/src/Pyro4/core.py#L255

另外:使用 pickle 作为序列化格式是一个巨大的安全问题。它允许任意远程代码执行。因此,为了所有神圣的爱,请确保您绝对信任客户,并且永远不要接受来自互联网的随机连接。