RPC究竟是什麼? gRPC又是什麼? 與HTTP直接存在什麼關係?python
本文將討論一下RPC相關的概念並以Python中自帶的xmlrpc爲例,簡單剖析源碼,理解它的實現原理,理解後,本身也能夠輕鬆實現一個玩具RPC框架。json
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框架經常使用的通訊協議:RMI、Socket、SOAP(HTTP XML)、REST(HTTP JSON)
通訊框架:MINA 和 Netty
知名開源RPC框架代碼比較複雜,具備不少功能,但RPC最核心的功能很是簡單,因此這裏去繁從簡,只瞭解RPC框架最核心的功能,而後經過Python自帶的xmlrpc來學習一下RPC代碼層面的實現。
一個RPC框架其核心功能能夠分紅5個主要部分,分別是:客戶端、客戶端 Stub、網絡傳輸模塊、服務端 Stub、服務端等,之間的關係以下圖。
經過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做爲網絡傳輸協議。
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中關鍵的那幾個部件。
那咱們下篇文章見。
最後提一下,篇篇閱讀不過百,真的很傷:(,若是你以爲有點意思,點個「在看」支持一下吧,叩謝豪恩。