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中关键的那几个部件。
那咱们下篇文章见。
最后提一下,篇篇阅读不过百,真的很伤:(,若是你以为有点意思,点个「在看」支持一下吧,叩谢豪恩。