APP下载
报价宝  ›  科技  › 

从 0 到 1:全面理解 RPC 远端呼叫 _协议

报价宝 来源:baojiabao.com 发布时间:2019-09-02 20:53:00 10月29日更新
报价宝综合消息从 0 到 1:全面理解 RPC 远端呼叫 _协议

作者 | Python程式设计时光

责编 | 胡巍巍

什么是RPC呢?百度百科给出的解释是这样的:“RPC(Remote Procedure Call Protocol)——远端过程呼叫协议,它是一种通过网络从远端计算机程式上请求服务,而不需要了解底层网络技术的协议”。

这个概念听起来还是比较抽象,没关系,继续往后看,后面概念性的东西,我会讲得足够清楚,让你完全掌握 RPC 的基础内容。

既 REST ,何 RPC ?

在 OpenStack 里的程式间通讯方式主要有两种,一种是基于HTTP协议的RESTFul API方式,另一种则是RPC呼叫。

那么这两种方式在应用场景上有何区别呢?

首先,给你提两个问题,带着这两个问题再往下看:

1、RPC 和 REST 区别是什么?

2、为什么要采用RPC呢?

首先,第一个问题:RPC 和 REST 区别是什么?

你一定会觉得这个问题很奇怪,是的,包括我,但是你在网络上一搜,会发现类似对比的文章比比皆是,我在想可能很多初学者由于基础不牢固,才会将不相干的二者拿出来对比吧。既然是这样,那为了让你更加了解陌生的RPC,就从你熟悉得不能再熟悉的 REST 入手吧。

01、所属类别不同

REST,是Representational State Transfer 的简写,中文描述表述性状态传递(是指某个瞬间状态的资源资料的快照,包括资源资料的内容、表述格式(XML、JSON)等资讯。)

REST 是一种软件架构风格。这种风格的典型应用,就是HTTP。其因为简单、扩充套件性强的特点而广受开发者的青睐。

而RPC 呢,是 Remote Procedure Call Protocol 的简写,中文描述是远端过程呼叫,它可以实现客户端像呼叫本地服务(方法)一样呼叫服务器的服务(方法)。

而 RPC 可以基于 TCP/UDP,也可以基于 HTTP 协议进行传输的,按理说它和REST不是一个层面意义上的东西,不应该放在一起讨论,但是谁让REST这么流行呢,它是目前最流行的一套互联网应用程序的API设计标准,某种意义下,我们说 REST 可以其实就是指代 HTTP 协议。

02、使用方式不同

从使用上来看,HTTP 界面只关注服务提供方,对于客户端怎么呼叫并不关心。界面只要保证有客户端呼叫时,返回对应的资料就行了。而RPC则要求客户端界面保持和服务端的一致。

03、面向物件不同

从设计上来看,RPC,所谓的远端过程呼叫 ,是面向方法的 ,REST:所谓的 Representational state transfer ,是面向资源的,除此之外,还有一种叫做 SOA,所谓的面向服务的架构,它是面向讯息的,这个接触不多,就不多说了。

04、序列化协议不同

界面呼叫通常包含两个部分,序列化和通讯协议。

通讯协议,上面已经提及了,REST 是 基于 HTTP 协议,而 RPC 可以基于 TCP/UDP,也可以基于 HTTP 协议进行传输的。

常见的序列化协议,有:json、xml、hession、protobuf、thrift、text、bytes等,REST 通常使用的是 JSON或者XML,而 RPC 使用的是 JSON-RPC,或者 XML-RPC。

通过以上几点,我们知道了 REST 和 RPC 之间有很明显的差异。

然后第二个问题:为什么要采用RPC呢?

那到底为何要使用 RPC,单纯的依靠RESTful API不可以吗?为什么要搞这么多复杂的协议,渣渣表示真的学不过来了。

关于这一点,以下几点仅是我的个人猜想,仅供交流哈:

说了这么多,我们该如何选择这两者呢?我总结了如下两点,供你参考:

“远端呼叫”意思就是:被呼叫方法的具体实现不在程式执行本地,而是在别的某个地方(分布到各个服务器),呼叫者只想要函式运算的结果,却不需要实现函式的具体细节。

光说不练嘴把式,接下来,我将分别用三种不同的方式全面地让你搞明白 rpc 远端呼叫是如何实现的。

01、基于 xml-rpc

Python实现 rpc,可以使用标准库里的 SimpleXMLRPCServer,它是基于XML-RPC 协议的。

有了这个模组,开启一个 rpc server,就变得相当简单了。执行以下程式码:

import SimpleXMLRPCServer

classcalculate:

defadd(self, x, y):

returnx + y

defmultiply(self, x, y):

returnx * y

defsubtract(self, x, y):

returnabs(x-y)

defdivide(self, x, y):

returnx/y

obj = calculate

server = SimpleXMLRPCServer.SimpleXMLRPCServer(("localhost", 8088))

# 将例项注册给rpc server

server.register_instance(obj)

print "Listening on port 8088"

server.serve_forever

有了 rpc server,接下来就是 rpc client,由于我们上面使用的是 XML-RPC,所以 rpc clinet 需要使用xmlrpclib 这个库。

import xmlrpclib

server = xmlrpclib.ServerProxy("http://localhost:8088")

然后,我们通过 server_proxy 物件就可以远端呼叫之前的rpc server的函式了。

>>server.add(2, 3)

5

>>> server.multiply(2, 3)

6

>>> server.subtract(2, 3)

1

>>> server.divide(2, 3)

0

SimpleXMLRPCServer是一个单执行绪的服务器。这意味着,如果几个客户端同时发出多个请求,其它的请求就必须等待第一个请求完成以后才能继续。

若非要使用 SimpleXMLRPCServer 实现多执行绪并发,其实也不难。只要将程式码改成如下即可。

fromSimpleXMLRPCServer importSimpleXMLRPCServer

fromSocketServer importThreadingMixIn

classThreadXMLRPCServer(ThreadingMixIn, SimpleXMLRPCServer):pass

classMyObject:

defhello(self):

return"hello xmlprc"

obj = MyObject

server = ThreadXMLRPCServer(("localhost", 8088), allow_none=True)

server.register_instance(obj)

print"Listening on port 8088"

server.serve_forever

02、基于json-rpc

SimpleXMLRPCServer 是基于 xml-rpc 实现的远端呼叫,上面我们也提到 除了 xml-rpc 之外,还有 json-rpc 协议。

那 python 如何实现基于 json-rpc 协议呢?

答案是很多,很多web框架其自身都自己实现了json-rpc,但我们要独立这些框架之外,要寻求一种较为干净的解决方案,我查询到的选择有两种

第一种是 jsonrpclib

pipinstall jsonrpclib -i https://pypi.douban.com/simple

第二种是 python-jsonrpc

pipinstall python-jsonrpc -i https://pypi.douban.com/simple

先来看第一种 jsonrpclib

它与 Python 标准库的 SimpleXMLRPCServer 很类似(因为它的类名就叫做 SimpleJSONRPCServer ,不明真相的人真以为它们是亲兄弟)。或许可以说,jsonrpclib 就是仿照 SimpleXMLRPCServer 标准库来进行编写的。

它的汇入与 SimpleXMLRPCServer 略有不同,因为SimpleJSONRPCServer分布在jsonrpclib库中。

服务端

fromjsonrpclib.SimpleJSONRPCServer importSimpleJSONRPCServer

server = SimpleJSONRPCServer(('localhost', 8080))

server.register_function(lambdax,y: x+y, 'add')

server.serve_forever

客户端

import jsonrpclib

server = jsonrpclib.Server("http://localhost:8080")

再来看第二种python-jsonrpc,写起来貌似有些复杂。

服务端

importpyjsonrpc

classRequestHandler(pyjsonrpc.HttpRequestHandler):

@pyjsonrpc.rpcmethod

defadd(self, a, b):

"""Test method"""

returna + b

http_server = pyjsonrpc.ThreadingHttpServer(

server_address=('localhost', 8080),

RequestHandlerClass=RequestHandler

)

print"Starting HTTP server ..."

print"URL: http://localhost:8080"

http_server.serve_forever

客户端

import pyjsonrpc

http_client = pyjsonrpc.HttpClient(

url="http://localhost:8080/jsonrpc"

)

还记得上面我提到过的 zabbix API,因为我有接触过,所以也拎出来讲讲。zabbix API 也是基于 json-rpc 2.0协议实现的。

因为内容较多,这里只带大家打个,zabbix 是如何呼叫的:直接指明要呼叫 zabbix server 的哪个方法,要传给这个方法的引数有哪些。

03、基于 zerorpc

以上介绍的两种rpc远端呼叫方式,如果你足够细心,可以发现他们都是http+rpc 两种协议结合实现的。

接下来,我们要介绍的这种(zerorpc),就不再使用走 http 了。

zerorpc 这个第三方库,它是基于TCP协议、 ZeroMQ 和 MessagePack的,速度相对快,响应时间短,并发高。zerorpc 和 pyjsonrpc 一样,需要额外安装,虽然SimpleXMLRPCServer不需要额外安装,但是SimpleXMLRPCServer效能相对差一些。

pipinstall zerorpc -i https://pypi.douban.com/simple

import zerorpc

classcaculate(object):

defhello(self, name):

return'hello, {}'.format(name)

defadd(self, x, y):

returnx + y

defmultiply(self, x, y):

returnx * y

defsubtract(self, x, y):

returnabs(x-y)

defdivide(self, x, y):

returnx/y

s = zerorpc.Server(caculate)

s.bind("tcp://0.0.0.0:4242")

s.run

客户端

import zerorpc

c = zerorpc.Client

c.connect("tcp://127.0.0.1:4242")

客户端除了可以使用zerorpc框架实现程式码呼叫之外,它还支援使用“命令列”的方式呼叫。

是的,通过 Github 上的档案几个 demo 可以体验到这个第三方库做真的是优秀。

比如我们可以用下面这个命令,建立一个rpc server,后面这个 time Python 标准库中的 time 模组,zerorpc 会将 time 注册系结以供client呼叫。

zerorpc--server --bind tcp://127.0.0.1:1234 time

在客户端,就可以用这条命令来远端呼叫这个 time 函式。

zerorpc --client --connecttcp://127.0.0.1:1234strftime %Y/%m/%d

往rpc中引入讯息中介软件

经过了上面的学习,我们已经学会了如何使用多种方式实现rpc远端呼叫。

通过对比,zerorpc 可以说是脱颖而出,一支独秀。

但为何在 OpenStack 中,rpc client 不直接 rpc 呼叫 rpc server ,而是先把 rpc 呼叫请求发给 RabbitMQ ,再由订阅者(rpc server)来取讯息,最终实现远端呼叫呢?

为此,我也做了一番思考:

OpenStack 元件繁多,在一个较大的丛集内部每个元件内部通过rpc通讯频繁,如果都采用rpc直连呼叫的方式,连线数会非常地多,开销大,若有些 server 是单执行绪的模式,超时会非常的严重。

OpenStack 是复杂的分散式丛集架构,会有多个 rpc server 同时工作,假设有 server01,server02,server03 三个server,当 rpc client 要发出rpc请求时,发给哪个好呢?这是问题一。

你可能会说轮循或者随机,这样对大家都公平。这样的话还会引出另一个问题,倘若请求刚好发到server01,而server01刚好不凑巧,可能由于机器或者其他因为导致服务没在工作,那这个rpc讯息可就直接失败了呀。要知道做为一个丛集,高可用是基本要求,如果出现刚刚那样的情况其实是很尴尬的。这是问题二。

丛集有可能根据实际需要扩充节点数量,如果使用直接呼叫,耦合度太高,不利于部署和生产。这是问题三。

解决问题一:讯息只有一份,接收者由AMQP的负载演算法决定,预设为在所有Receiver中均匀传送(round robin)。

解决问题二:有了讯息中介软件做缓冲站,client 可以任性随意的发,server 都挂掉了?没有关系,等 server 正常工作后,自己来讯息中介软件取就行了。

解决问题三:无论有多少节点,它们只要认识讯息中介软件这一个中介就足够了。

既然讲到了讯息伫列,如果你之前没有接触过这块内容,最好花几分钟的时间跟我好好过下关于讯息伫列的几个基础概念。

首先,RPC只是定义了一个通讯界面,其底层的实现可以各不相同,可以是 socket,也可以是今天要讲的 AMQP。

AMQP(Advanced Message Queuing Protocol)是一种基于伫列的可靠讯息服务协议,作为一种通讯协议,AMQP同样存在多个实现,如Apache Qpid,RabbitMQ等。

以下是 AMQP 中的几个必知的概念:

Publisher:讯息释出者

Receiver:讯息接收者,在RabbitMQ中叫订阅者:Subscriber。

Queue:用来储存讯息的储存空间,讯息没有被receiver前,储存在伫列中。

Exchange:用来接收Publisher发出的讯息,根据Routing key 转发讯息到对应的Message Queue中,至于转到哪个伫列里,这个路由演算法又由exchange type决定的。

Exchange type:主要四种描述exchange的型别。

direct:讯息路由到满足此条件的伫列中(queue,可以有多个):routing key = binding key

topic:讯息路由到满足此条件的伫列中(queue,可以有多个):routing key 匹配 binding pattern. binding pattern是类似正则表示式的字串,可以满足复杂的路由条件。

fanout:讯息路由到多有系结到该exchange的伫列中。

binding :binding是用来描述exchange和queue之间的关系的概念,一个exchang可以系结多个伫列,这些关系由binding建立。前面说的binding key /binding pattern也是在binding中给出。

为了让你明白这几者的关系,我画了一张模型图。

关于AMQP,有几下几点值得注意:

生产中是如何使用RPC的?

前面铺垫了那么久,终于到了讲真实应用的场景。在生产中RPC是如何应用的呢?

其他模型我不太清楚,在 OpenStack 中的应用模型是这样的

接下来,就是源代码解读 OpenStack ,看看其是如何通过rpc进行远端呼叫的。如若你对此没有兴趣(我知道很多人对此都没有兴趣,所以不浪费大家时间),可以直接跳过这一节,进入下一节。

目前Openstack中有两种RPC实现,一种是在oslo messaging,一种是在openstack.common.rpc。

openstack.common.rpc是旧的实现,oslo messaging是对openstack.common.rpc的重构。openstack.common.rpc在每个专案中都存在一份拷贝,oslo messaging即将这些公共程式码抽取出来,形成一个新的专案。oslo messaging也对RPC API 进行了重新设计,对多种 transport 做了进一步封装,底层也是用到了kombu这个AMQP库。(注:Kombu 是Python中的messaging库。Kombu旨在通过为AMQ协议提供惯用的高阶界面,使Python中的讯息传递尽可能简单,并为常见的讯息传递问题提供经过验证和测试的解决方案。)

关于oslo_messaging库,主要提供了两种独立的API:

因为 notify 实现是太简单了,所以这里我就不多说了,如果有人想要看这方面内容,可以收藏我的部落格(http://python-online.cn) ,我会更新补充 notify 的内容。

OpenStack RPC 模组提供了 rpc.call,rpc.cast, rpc.fanout_cast 三种 RPC 呼叫方法,传送和接收 RPC 请求。

rpc.call 和 .rpc.cast 从实现程式码上看,他们的区别很小,就是call呼叫时候会带有wait_for_reply=True引数,而cast不带。

要了解 rpc 的呼叫机制呢,首先要知道 oslo_messaging 的几个概念主要方法有四个:

transport:RPC功能的底层实现方法,这里是rabbitmq的讯息伫列的访问路径

transport 就是定义你如何访连线讯息中介软件,比如你使用的是 Rabbitmq,那在 nova.conf中应该有一行transport_url的配置,可以很清楚地看出指定了 rabbitmq 为讯息中介软件,并配置了连线rabbitmq的user,passwd,主机,埠。

transport_url=rabbit://user:[email protected]:5672

defget_transport(conf, url=None, allowed_remote_exmods=None):

return_get_transport(conf, url, allowed_remote_exmods,

transport_cls=RPCTransport)

target:指定RPC topic交换机的匹配资讯和系结主机。

target用来表述 RPC 服务器监听topic,server名称和server监听的exchange,是否广播fanout。

classTarget(object):

def__init__(self, exchange=None, topic=None, namespace=None,

version=None, server=None, fanout=None,

legacy_namespaces=None):

self.exchange = exchange

self.topic = topic

self.namespace = namespace

self.version = version

self.server = server

self.fanout = fanout

self.accepted_namespaces = [namespace] + (legacy_namespaces or[])

rpc server 要获取讯息,需要定义target,就像一个门牌号一样。

rpc client 要传送讯息,也需要有target,说明讯息要发到哪去。

endpoints:是可供别人远端呼叫的物件

RPC服务器暴露出endpoint,每个 endpoint 包涵一系列的可被远端客户端通过 transport 呼叫的方法。直观理解,可以参考nova-conductor建立rpc server的程式码,这边的endpoints就是 nova/manager.py:ConductorManager

dispatcher:分发器,这是 rpc server 才有的概念

只有通过它 server 端才知道接收到的rpc请求,要交给谁处理,怎么处理?

在client端,是这样指定要呼叫哪个方法的。

而在server端,是如何知道要执行这个方法的呢?这就是dispatcher 要干的事,它从 endpoint 里找到这个方法,然后执行,最后返回。

Serializer:在 python 物件和message(notification) 之间资料做序列化或是反序列化的基类。

1. deserialize_context(ctxt) :对字典变成 request contenxt.

2. deserialize_entity(ctxt, entity) :对entity做反序列化,其中ctxt是已经deserialize过的,entity是要处理的。

3. serialize_context(ctxt) :将Request context变成字典型别

4. serialize_entity(ctxt, entity) :对entity做序列化,其中ctxt是已经deserialize过的,entity是要处理的。

executor:服务的执行方式,单执行绪或者多执行绪

每个notification listener都和一个executor系结,来控制收到的notification如何分配。预设情况下,使用的是blocking executor(具体特性参加executor一节)

oslo_messaging.get_notification_listener(transport, targets, endpoints, executor=’blocking’, serializer=None, allow_requeue=False, pool=None)

rpc server 和rpc client 的四个重要方法

reset:Reset service.

start:该方法呼叫后,server开始poll,从transport中接收message,然后转发给dispatcher.该message处理过程一直进行,直到stop方法被呼叫。executor决定server的IO处理策略。可能会是用一个新程式、新协程来做poll操作,或是直接简单的在一个循环中注册一个回拨。同样,executor也决定分配message的方式,是在一个新执行绪中dispatch或是…..

stop:当呼叫stop之后,新的message不会被处理。但是,server可能还在处理一些之前没有处理完的message,并且底层driver资源也还一直没有释放。

wait:在stop呼叫之后,可能还有message正在被处理,使用wait方法来阻塞当前程式,直到所有的message都处理完成。之后,底层的driver资源会释放。

模仿OpenStack写 rpc 呼叫

模仿是一种很高效的学习方法,我这里根据 OpenStack 的呼叫方式,抽取出核心内容,写成一个简单的 demo,有对 OpenStack 感兴趣的可以了解一下,大部分人也可以直接跳过这章节。

注意以下程式码不能直接执行,你还需要配置 rabbitmq 的连线方式,你可以写在配置档案中,通过 get_transport 从cfg.CONF 中读取,也可以直接将其写成url的格式做成引数,传给 get_transport 。而且还要nova或者其他openstack元件的环境中执行(因为需要有ctxt这个环境变数)

简单的 rpc client

#coding=utf-8

importoslo_messaging

fromoslo_config importcfg

# 建立 rpc client

transport = oslo_messaging.get_transport(cfg.CONF, url="")

target = oslo_messaging.Target(topic='test', version='2.0')

client = oslo_messaging.RPCClient(transport, target)

# rpc同步呼叫

client.call(ctxt, 'test', arg=arg)

简单的 rpc server

#coding=utf-8

fromoslo_config importcfg

importoslo_messaging

importtime

# 定义endpoint类

classServerControlEndpoint(object):

target = oslo_messaging.Target(namespace='control',

version='2.0')

def__init__(self, server):

self.server = server

defstop(self, ctx):

ifself.server:

self.server.stop

classTestEndpoint(object):

deftest(self, ctx, arg):

returnarg

# 建立rpc server

transport = oslo_messaging.get_transport(cfg.CONF, url="")

target = oslo_messaging.Target(topic='test', server='server1')

endpoints = [

ServerControlEndpoint(None),

TestEndpoint,

]

server = oslo_messaging.get_rpc_server(transport, target,endpoints,executor='blocking')

try:

server.start

whileTrue:

time.sleep(1)

exceptKeyboardInterrupt:

print("Stopping server")

server.stop

server.wait

【End】

文章标签: 报价宝 降噪耳机价格 耳机价格 红米手机价格 华为手机价格 小米手机价格 电视机价格 笔记本电脑价格 笔记本价格 汽车价格 手机价格 笔记本电脑价格 华为手机价格 耳机价格 降噪耳机价格