RPC入门与源码剖析

1,101 阅读10分钟

前言

RPC到底是什么? gRPC又是什么? 与HTTP直接存在什么关系?

本文将讨论一下RPC相关的概念并以Python中自带的xmlrpc为例,简单剖析源码,理解它的实现原理,理解后,自己也可以轻松实现一个玩具RPC框架。

RPC概念

RPC简单定义:

RPC(Remote Procedure Call)– 远程过程调用,通过「网络通信」调用不同的服务,共同支撑一个软件系统,是分布式系统中的基石技术。

阅读完RPC定义,我内心疑惑是:

1.服务之间通过API的方式走HTTP不也可以实现通过网络通信调用不同的服务的目的? 2.它与HTTP有啥差别? 3.相比于API调用的方式有何优势?

首先,RPC 是一种技术思想而非一种规范或协议,RPC可以基于HTTP来实现,也可以基于其他方式,比如常见的TCP或自己定义协议通过Socker来实现。

gRPC是Google知名的RPC框架,RPC框架是对RPC技术思想的现实实现,类似于idea和相应idea的project的关系,gRPC本身是构建于HTTP2.0上的,使用Google的Protobuf协议来传输信息。

那相比与API调用,它有什么优势?

系统内部API调用在系统间交互较少,接口不多的情况下,确实是一种有效的通信手段,它实现简单、直接,但一个大系统,子系统的交互可能很多,如果要使用API,就需要定义非常多的接口,难以维护。

此外,使用RPC框架后,远程调用会与本地调用一样简单,底层网络传输的过程对用户而言是透明的。RPC框架会自动进行数据序列化、协议编码和网络传输等过程。

题外话:很多介绍RPC的博文会聊到系统间API调用走HTTP要进行3次握手、4次挥手,请求量大起来是很耗资源的,而RPC框架调用会维持长连接,从而减少网络开销。

这其实存在概念性错误,HTTP协议可以维持长连接,只需要在包头添加上Connection:keep-alive,HTTP/1.0默认使用短连接,但HTTP/1.1起,默认使用的就是长连接了,HTTP是应用层协议,它的长连接和短连接,实质上是 TCP 协议的长连接和短连接。

gRPC使用HTTP/2.0默认就会使用长连接来传输数据。

常见RPC框架

  • gPRC:Google开源的RPC框架,基于HTTP/2.0实现,可支持常见的多种语言,底层使用了Netty框架。
  • Thrift:Facebook开源的RPC框架,一个跨语言服务开发框架。
  • Dubbo:阿里巴巴开源的RPC框架,协议和序列化框架都可以插拔。

RPC框架常用的通信协议:RMI、Socket、SOAP(HTTP XML)、REST(HTTP JSON)

通信框架:MINA 和 Netty

RPC核心功能

知名开源RPC框架代码比较复杂,具有很多功能,但RPC最核心的功能非常简单,所以这里去繁从简,只了解RPC框架最核心的功能,然后通过Python自带的xmlrpc来学习一下RPC代码层面的实现。

一个RPC框架其核心功能可以分成5个主要部分,分别是:客户端、客户端 Stub、网络传输模块、服务端 Stub、服务端等,之间的关系如下图。

  • 客户端(Client):服务调用方。
  • 客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端。
  • 服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理。
  • 服务端(Server):服务的真正提供者。
  • Network Service:底层传输,可以是 TCP 或 HTTP。

通过Python的xmlrpc简单使用一下,加深对上面概念的理解。

首先构建一个Server端。

from xmlrpc.server import SimpleXMLRPCServer

def is_even(n):
    return n % 2 == 0

server = SimpleXMLRPCServer(('127.0.0.1', 5000))
print('Listening on port 5000....')
server.register_function(is_even, 'is_even') # 注册服务
server.serve_forever()

上述代码中,使用了SimpleXMLRPCServer类构建了RPC Server实例,然后将Server端可以被远程调用的方法通过register_function方法进行注册,这里将is_even方法注册,客户端可以无感调用is_even方法,最后调用serve_forever方法让Server端一直轮训监听5000端口。

除了使用register_function方法注册方法,你还可以通过register_instance方法注册类实例,此时该类下的所有方法都可以被远程Client端调用,更多用法参考文档。

接着构建一个Client端。

import xmlrpc.client

with xmlrpc.client.ServerProxy('http://127.0.0.1:5000/') as proxy:
    res1 = proxy.is_even(3) # 调用服务端的is_even方法
    print('res1: ', res1)
    res2 = proxy.is_even(100)
    print('res2: ', res2)

Cliet端的代码更简洁,通过with上下文机制管理 xmlrpc.client.ServerProxy,然后直接调用Server端的方法,看上去很神奇。

仔细观察Server端的输出,可以发现Client端的两次调用,其实就是两次POST请求,走的是HTTP/1.1,这说明Python的xmlrpc基于HTTP/1.1作为网络传输协议。

xmlrpc源码剖析

Client端如同调用本地方法的形式实现对Server端方法的调用,这效果非常棒,那它是怎么实现的呢?我自己可不可以写一个呢?

剖析源码前,要思考一下从哪里入手,其实从Client端看更易理解(我一开始看Server端,绕了半天...)。

仔细观察下面的代码。

with xmlrpc.client.ServerProxy('http://127.0.0.1:5000/') as proxy:
    res1 = proxy.is_even(3) # 调用服务端的is_even方法
    print('res1: ', res1)
    res2 = proxy.is_even(100)
    print('res2: ', res2)

with来操作一个类,那么这个类肯定要实现__enter____exit__(因为with就是对它两进行调用)。

# Lib/xmlrpc/Client/ServerProxy

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.__close()

没啥东西,观察到proxy.is_even(3),proxy就是self即ServerProxy类实例,该类本身是不存在is_even,但代码运行没有报错,那肯定重写了__getattr__

# Lib/xmlrpc/Client/ServerProxy

    # 调用属性
    def __getattr__(self, name):
        # magic method dispatcher
        return _Method(self.__request, name)

_Method类代码如下。

# Lib/xmlrpc/Client

class _Method:
    # some magic to bind an XML-RPC method to an RPC server.
    # supports "nested" methods (e.g. examples.getStateName)
    def __init__(self, send, name):
        self.__send = send
        self.__name = name
    def __getattr__(self, name):
        return _Method(self.__send, "%s.%s" % (self.__name, name))
    def __call__(self, *args):
        return self.__send(self.__name, args)

看到_Method类的__call__方法,它的最终作用就是self.__request(name, args),对应到proxy.is_even(3),就是self.__request(is_even, 3),重点在__request

# Lib/xmlrpc/Client/ServerProxy

    def __request(self, methodname, params):
        # call a method on the remote server
        
        # dumps方法最终会返回一个具有XML格式的字符串
        request = dumps(params, methodname, encoding=self.__encoding,
                        allow_none=self.__allow_none).encode(self.__encoding, 'xmlcharrefreplace')
        # 请求
        response = self.__transport.request(
            self.__host, # self.__host, self.__handler = urllib.parse.splithost(uri),在ServerProxy类实例化时获取了对应的值
            self.__handler,
            request, # xml格式数据
            verbose=self.__verbose # 是否显示详细的debug信息,默认为False
            )

        if len(response) == 1:
            response = response[0]
        # 返回相应结果
        return response

__request方法的关键就是调用了self.__transport.request方法进行请求,该方法的调用过程__transport.request --> __transport.single_request --> __transport.send_request

# Lib/xmlrpc/Client/ServerProxy

    # 构建请求头与请求体
    def send_request(self, host, handler, request_body, debug):
        connection = self.make_connection(host)
        headers = self._extra_headers[:]
        if debug:
            connection.set_debuglevel(1)
        if self.accept_gzip_encoding and gzip:
            connection.putrequest("POST", handler, skip_accept_encoding=True)
            headers.append(("Accept-Encoding", "gzip"))
        else:
            connection.putrequest("POST", handler)
        headers.append(("Content-Type", "text/xml"))
        headers.append(("User-Agent", self.user_agent))
        self.send_headers(connection, headers)
        self.send_content(connection, request_body)
        return connection

最终的最终,会调用python中http库下client.py中的HTTPConnection.send方法,将数据以socket的形式发送出去。

阶段性总结一下,xmlrpc的Client端,其实就是将方法名与方法参数编码转为xml格式的数据通过socket传递给Server端(走HTTP),并在同一次HTTP链接中获取Server端对应方法返回的结果。

接着来看Server端的代码。

server = SimpleXMLRPCServer(('127.0.0.1', 5000))
print('Listening on port 5000....')
server.register_function(is_even, 'is_even') # 注册服务
server.serve_forever()

SimpleXMLRPCServer类继承了socketserver.TCPServer与SimpleXMLRPCDispatcher,采用了Mixin形式,TCPServer主要负责建立链接,而这里主要关注的是SimpleXMLRPCDispatcher,该类负责XMLRPC的调度,还有一个很重要的就是SimpleXMLRPCRequestHandler类,后面会提及。

class SimpleXMLRPCServer(socketserver.TCPServer,
                         SimpleXMLRPCDispatcher):
        def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler,
                 logRequests=True, allow_none=False, encoding=None,
                 bind_and_activate=True, use_builtin_types=False):
        # ... 省略

在使用SimpleXMLRPCServer时,调用了register_function方法进行Server端方法的注册,它的本质就是将注册的方法放到dict中。

# Lib/xmlrpc
class SimpleXMLRPCDispatcher:
    def __init__(self, allow_none=False, encoding=None,
                 use_builtin_types=False):
        self.funcs = {} # 注册的方法存在在dict中
        self.instance = None

    def register_function(self, function, name=None):
        """Registers a function to respond to XML-RPC requests.

        The optional name argument can be used to set a Unicode name
        for the function.
        """

        if name is None:
            name = function.__name__
        self.funcs[name] = function

到这里都很简单,而要理清server.serve_forever方法的调用过程就很绕了,该方法中涉及了监听Client端传递信息到解析调用本地相关方法并将相关方法结果返回的逻辑,为了降低阅读难度以及方便你证实,我会给出该方法的完整调用链,而重点只分析其中比较重要的调用方法。

SimpleXMLRPCServer.serve_forever方法 --> SimpleXMLRPCServer._handle_request_noblock方法 --> SimpleXMLRPCServer.process_request方法 --> SimpleXMLRPCServer.finish_request方法

finish_request方法代码如下。

    def finish_request(self, request, client_address):
        """Finish one request by instantiating RequestHandlerClass."""
        self.RequestHandlerClass(request, client_address, self)

self.RequestHandlerClass在SimpleXMLRPCServer过程中被定义,默认值为SimpleXMLRPCRequestHandler类,该类没有__init__方法,但继承的BaseRequestHandler类有,继续给出调用链。

BaseRequestHandler.__init__方法 --> BaseHTTPRequestHandler.handle方法 --> BaseHTTPRequestHandler.handle_one_request方法

handle_one_request方法很关键,其部分代码如下。

# Lib/http/server.py/BaseHTTPRequestHandler

def handle_one_request(self):

        try:
            # ... 省略了很多条件判断
            
            mname = 'do_' + self.command
            if not hasattr(self, mname):
                self.send_error(
                    HTTPStatus.NOT_IMPLEMENTED,
                    "Unsupported method (%r)" % self.command)
                return
            method = getattr(self, mname)
            method() # 调用 do_POST() 方法
            # 将获取的数据从缓冲器强制推给Client端(相当于返回数据)
            self.wfile.flush() #actually send the response if not already done.
        except socket.timeout as e:
            #a read or a write timed out.  Discard this connection
            self.log_error("Request timed out: %r", e)
            self.close_connection = True
            return

其中最关键的就是调用了method方法,它的本质其实是调用了SimpleXMLRPCRequestHandler类的do_POST方法,该方法部分代码如下

    def do_POST(self):
        '''处理了POST请求'''
        try:
            #... 省略
            
            # 解析请求的xml数据,其中就包含这 is_even 方法名和 3 这个参数
            data = self.decode_request_content(data)
            if data is None:
                return #response has been sent

            # 调用最关键的方法,该方法会调用Client端请求的方法并将结果返回
            response = self.server._marshaled_dispatch(
                    data, getattr(self, '_dispatch', None), self.path
                )
        except Exception as e: # This should only happen if the module is buggy
           
            self.send_header("Content-length", "0")
            self.end_headers()
            
        # ... 省略代码

最终回到了simplexmlRPCServer类的_marshaled_dispatch方法进行调度

  def _marshaled_dispatch(self, data, dispatch_method = None, path = None):
        
        try:
            params, method = loads(data, use_builtin_types=self.use_builtin_types)

            # generate response
            if dispatch_method is not None:
                response = dispatch_method(method, params)
            else:
                # 调用方法,即调用 is_even 方法
                response = self._dispatch(method, params)
            # wrap response in a singleton tuple
            response = (response,)
            response = dumps(response, methodresponse=1,
                             allow_none=self.allow_none, encoding=self.encoding)
        except Fault as fault:
            response = dumps(fault, allow_none=self.allow_none,
                             encoding=self.encoding)
        except:
            # ... 省略代码
        return response.encode(self.encoding, 'xmlcharrefreplace')

对is_even方法的调用发送在SimpleXMLRPCDispatcher._dispatch方法中,该方法部分代码如下。

# Lib/xmlrpc/server.py/SimpleXMLRPCDispatcher

def _dispatch(self, method, params):
        try:
            # call the matching registered function
            func = self.funcs[method]
        except KeyError:
            pass
        else:
            if func is not None:
                return func(*params)
            raise Exception('method "%s" is not supported' % method)

调用完Server端的is_even方法后,再将结果层层返回,最终将结果通过socket回传。

同样总结一下,Server端会以Poll机制轮训监听5000端口,有数据后,会解析到对应类的do_POST方法,然后再执行相应的调度逻辑,最终从funcs字典中找到对应的方法,然后执行获得结果,获得的结果会通过socket返回。

结尾

本来我以为Python的xmlrpc应该很简单,看它代码中的注释,第一版代码写于1999年,但后面看着看着,还是有点复杂度的,rpc核心本身不复杂,但xmlrpc中涉及了很多http相关的内容,让代码整体看起来比较繁杂。

下一篇文章,我将通过socket+json的形式来实现一个特别简单的rpc,让你特别直观的理解rpc中关键的那几个部件。

那我们下篇文章见。

最后提一下,篇篇阅读不过百,真的很伤:(,如果你觉得有点意思,点个「在看」支持一下吧,叩谢豪恩。