RPC入門與源碼剖析

前言

RPC究竟是什麼? gRPC又是什麼? 與HTTP直接存在什麼關係?python

本文將討論一下RPC相關的概念並以Python中自帶的xmlrpc爲例,簡單剖析源碼,理解它的實現原理,理解後,本身也能夠輕鬆實現一個玩具RPC框架。json

RPC概念

RPC簡單定義:網絡

RPC(Remote Procedure Call)– 遠程過程調用,經過「網絡通訊」調用不一樣的服務,共同支撐一個軟件系統,是分佈式系統中的基石技術。app

閱讀完RPC定義,我心裏疑惑是:框架

1.服務之間經過API的方式走HTTP不也能夠實現經過網絡通訊調用不一樣的服務的目的? 2.它與HTTP有啥差異? 3.相比於API調用的方式有何優點?socket

首先,RPC 是一種技術思想而非一種規範或協議,RPC能夠基於HTTP來實現,也能夠基於其餘方式,好比常見的TCP或本身定義協議經過Socker來實現。分佈式

gRPC是Google知名的RPC框架,RPC框架是對RPC技術思想的現實實現,相似於idea和相應idea的project的關係,gRPC自己是構建於HTTP2.0上的,使用Google的Protobuf協議來傳輸信息。ide

那相比與API調用,它有什麼優點?學習

系統內部API調用在系統間交互較少,接口很少的狀況下,確實是一種有效的通訊手段,它實現簡單、直接,但一個大系統,子系統的交互可能不少,若是要使用API,就須要定義很是多的接口,難以維護。ui

此外,使用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中關鍵的那幾個部件。

那咱們下篇文章見。

最後提一下,篇篇閱讀不過百,真的很傷:(,若是你以爲有點意思,點個「在看」支持一下吧,叩謝豪恩。

相關文章
相關標籤/搜索