11.3.3 RPC调用的实现

来源:互联网 发布:知误会前番书语 编辑:程序博客网 时间:2024/06/05 11:01

本章的代码来源于 <openstack王者归来>

Openstack中共有两种远程调用方式:rpc.call异步和rpc.cast同步。rpc.call需要用到主题交换和直接交换两种交换方式。主题交换用于客户端向服务器发送RPC请求,而直接交换主要用于服务器向客户端返回RPC调用的结果。  本小节实现这两种交换方式,并通过这两种交换方式完成一个简单的rpc.call调用。  后面可自行分析rpc.cast调用流程。

RPC调用示例中python文件:

文件名

描   述

client.py

RPC调用请求客户端的启动脚本

dispatcher.py

将客户端发布的消息分发给相应的方法处理

impl_kombu.py

示例的核心代码。实现了主题消费者、主题生产者、直接消费者和直接生产者等。

manager.py

定义了处理RPC调用请求的方法

rpc_amqp.py

用于处理RPC请求并发生RPC响应

rpc.py

为外界提供API

server.py

服务器端的启动脚本

service.py

主要用于创建和管理RPC服务

注意:(1)、本小节的示例文件结构和代码逻辑是仿造Nova中的rpc模块。为了帮助快速理清脉络,示例删去了一些可选的配置以及多线程方面的代码。

      (2)、许多工具包实现和RabbitMQ的交互,使用的工具包是kombu包,这是openstack使用的默认工具包。

1、服务器端:定义和启动RPC服务。  server.py和service.py:

RPC服务的启动脚本:server.py

import servicesrv = service.Service()srv.start()while True:srv.drain_events()

首先创建service对象,分别调用该对象的start方法和drain_events方法。

Service类的service.py文件,其初始化方法、start方法和drain_events方法定义如下:

import rpcimport managerimport dispatcherTOPIC = 'sendout_request'class Service(object):    def __init__(self):        self.topic = TOPIC        self.manager = manager.Manager()    def start(self):        self.conn = rpc.create_connection()        rpc_dispatcher = dispatcher.RpcDispatcher(self.manager)        self.conn.create_consumer(self.topic, rpc_dispatcher)        self.conn.consume()    def drain_events(self):        self.conn.drain_events()

(1)、初始化方法

TOPIC ='sendout_request'class Service(object):    def __init__(self):        self.topic = TOPIC        self.manager = manager.Manager()

定义了topic和manager成员变量(处理RPC请求的方法)。

topic用于定义主题交换机的主题,设置的主题为sendout_request。manager对象用于定义处理rpc请求的方法。 定义一个add方法,用于计算两个书的和。

manager.py:class Manager(object):    def add(self, v1=0, v2=0):        rval = v1 + v2        print("%(v1)d + %(v2)d is%(rval)d\n" % locals())        return rval

(2)、start方法

def start(self):        self.conn = rpc.create_connection()  #创建rabbitmq连接        rpc_dispatcher =dispatcher.RpcDispatcher(self.manager) #创建分发器        self.conn.create_consumer(self.topic,rpc_dispatcher)   #创建主题消费者        self.conn.consume()   #激活主题消费者

比较重要的是create_consumer方法,两个参数:主题和分发器。一个消费者激活以后才能处理交换器队列中的消息。

(3)、drain_events方法

接收和处理RPC请求,最终会调用BrokerConnection对象的drain_events方法。

2、服务器端:建立与RabbitMQ的连接

Service类会调用rpc.create_connection方法建立连接,返回一个Connection对象。Connection类中涉及建立RabbitMQ服务器连接的方法有初始化reconnect方法和_connect方法。

(1)、初始化方法     调用该类的reconnect方法

(2)、reconnect方法

classConnection(object):    def reconnect(self):        # 初次重连的等待时间        sleep_time = conf.get('interval_start',1)        # 每次连接失败后增加的等待时间        stepping =conf.get('interval_stepping', 2)        # 重连的最大等待时间        interval_max = conf.get('interval_max',30)        sleep_time -= stepping         while True:            try:                self._connect()  # 尝试连接RabbitMQ服务器                return            except Exception, e:                if 'timeout' not in str(e): # 如果不是超时异常,则抛出                    raise            # 设置下次连接的等待时间            sleep_time += stepping            sleep_time = min(sleep_time,interval_max)            print("AMQP Server isunreachable,"                  "trying to connect %dseconds later\n" % sleep_time)            time.sleep(sleep_time)

reconnect方法会不断尝试与Rabbitmq服务器连接,直至连接成功。失败的次数越多,reconnect方法在执行下次连接前的等待时间也会增加。

(3)、_connect方法 : 实现了一次与Rabbitmq服务器的连接。

rabbit_params = {    'hostname':'10.239.131.181',  # rambitmq服务器所在节点的地址    'port':5672,                  # rabbitmq服务器监听的端口    'userid': 'guest',            # 连接rabbitmq服务器的用户名    'password': 'guest',          # 连接rabbitmq服务器的密码    'virtual_host': '/',          # 虚拟目录}# 类内部方法    def _connect(self):        hostname =rabbit_params.get('hostname')        port = rabbit_params.get('port')    # socket addr信息 (IP, port)         if self.connection: #如果已经建立连接            print("Reconnecting to AMQPServer on "                  "%(hostname)s:%(port)d\n"% locals())            self.connection.release() #释放原来的连接            self.connection = None        #创建BrokerConnection对象        self.connection =kombu.connection.BrokerConnection(**rabbit_params)        self.consumer_num = itertools.count(1)# 重置消费者迭代器        # 与rabbitmq服务器连接        self.connection.connect()        self.channel =self.connection.channel() #获取连接的信道        for consumer in self.consumers:            consumer.reconnect(self.channel) # 重置消费者的信道

①  创建BrokerConnection对象,其中rabbit_params定义与RABBITMQ服务器的连接。rabbit_params变量为impl_kombu.py文件的全局变量。可根据实际情况修改。

②  定义连接的虚拟目录,在rabbitmq服务器中,不同虚拟目录的交换器和队列是相互独立的。

③  迭代计数器。Rabbitmq中,同一个连接下的消费者需要具有一个唯一的tag,迭代器用于产生消费者的tag。

④  将消费者与信道相关联,一个消费者只有与信道相关联才能才能接收和处理RPC请求。


3、服务器端:创建和激活主题消费者

(1)、创建主题消费者 :

① Service类通过调用Connection类的create_consumer方法来创建主题消费者。

classConnection(object):defcreate_consumer(self, topic, proxy):        proxy_cb =rpc_amqp.ProxyCallback(proxy)        self.declare_topic_consumer(topic,proxy_cb)

创建ProxyCallback可调用对象。当消费者收到消息时,会调用ProxyCallback对象来处理消息。      通过declare_topic_consumer方法创建主题消费者。    proxy是一个RpcDispatcher对象,对于rpc请求分发处理。

② declare_topic_consumer方法调用了declare_consumer方法

self.declare_consumer(TopicConsumer,topic, callback) class Connection(object):defdeclare_consumer(self, consumer_cls, topic, callback):        def _declare_consumer():  #内部方法            consumer =consumer_cls(self.channel,  #创建consumer对象                                    topic,callback,                                   self.consumer_num.next())            self.consumers.append(consumer) #添加consumer对象            print('Succed declaring consumerfor topic %s\n' % topic)            return consumer        #不断执行_declare_consumer方法,直至执行成功        return self.ensure(_declare_consumer,topic)

declare_consumer 方法调用ensure方法。ensure方法会不断执行_declare_consumer方法。如果_declare_consumer执行失败,重新建立rabbitmq连接,再次执行_declare_consumer方法,直至_declare_consumer方法执行成功返回创建的主题消费者。

_declare_consumer内部方法,主要功能是创建consumer_cls对象。

③ _declare_consumer方法的主要功能是创建TopicConsumer对象。TopicConsumer类定义在impl_kombu.py文件中。

class TopicConsumer(ConsumerBase):    def __init__(self, channel, topic,callback, tag, **kwargs):        self.topic = topic #消费者主题        #设置交换器和交换队列属性options = {'durable': False, 'auto_delete': False,                  'exclusive': False}        options.update(kwargs)        #创建主题交换器        exchange = kombu.entity.Exchange(name=topic,type='topic',             durable=options['durable'], auto_delete=options['auto_delete'])        #初始化父类,创建交换队列super(TopicConsumer, self).__init__(channel,        callback,tag,name=topic,exchange=exchange,routing_key=topic,**options)

options变量是用来设置交换器和队列的属性。

TopicConsumer类继承自ConsumerBase类。分析ConsumerBase类的定义。

class ConsumerBase(object):    def __init__(self, channel, callback, tag,**kwargs):        self.callback = callback    #处理rpc请求的回调函数        self.tag = str(tag)         #消费者的tag        self.kwargs = kwargs        #交换器和队列属性        self.queue = None           #队列初始为空        self.reconnect(channel)     #创建并声明队列初始化方法中设置的callback成员变量是一个ProxyCallback对象,分发和处理RPC请求的回调函数。调用reconnect方法来创建和声明队列。def reconnect(self,channel):        self.channel = channel        self.kwargs['channel'] = channel  #设置信道属性        self.queue =kombu.entity.Queue(**self.kwargs)#创建队列        self.queue.declare() #声明队列

ConsumerBase的reconnect方法首先创建一个Queue对象,然后调用queue对象的declare方法声明该队列。 创建消费者的代码分析完成。

注意:以上代码只是创建和声明一个消息队列,并没有创建消费者。  在nova中,同一条连接下,一条队列只与一个消费者相关联。存在一一对应的关系。 一个队列声明了才能工作。一一对应关系只是nova内部的逻辑。实际上一个消费者可以对应多个消息队列。

(2)、激活消费者

消费者创建完成后,还需激活,才能处理RPC请求。Service类的consume方法最终会调用ConsumerBase的consume方法。

class ConsumerBase(object):def consume(self,*args, **kwargs):        options = {'consumer_tag': self.tag} #设置消费者的tag属性        options['nowait'] = False   #是否等待响应        #内部函数,用于处理一条消息        def _callback(raw_message):            message =self.channel.message_to_python(raw_message)            try:                msg = message.payload #获取消息体                self.callback(msg)    #处理消息                message.ack()         #通知交换器消息处理完毕            except Exception:                print("Failed to processmessage... skipping it.\n")        #激活消费者        self.queue.consume(*args,callback=_callback, **options)

_callback内部方法,是分发和处理RPC请求的方法。_callback方法首先是对消息进行解析,获取消息体。然后调用self.callback方法处理消息体。最后向Rabbitmq服务器报告消息处理成功。

4、客户端:向主题服务器发送RPC请求

主题消费者创建好之后,客户端便可向RabbitMQ服务器发送RPC请求了。

(1)、client.py:

import rpcTOPIC = 'sendout_request'  #主题#消息体msg = {'method':'add',   #RPC调用的方法名       'args':{'v1':2, 'v2':3}} #参数列表rval = rpc.call(TOPIC,msg)     #发送rpc.call请求print('Succeedimplementing RPC call. the return value is %d.\n' % rval)client.py会调用rpc.call方法,rpc.call方法最终会调用impl_kombu.call方法。def call(topic, msg,timeout):    print('Making synchronous call on %s ...\n'% topic)    msg_id = DIRECT + str(uuid.uuid4()) #构造消息ID    msg.update({'msg_id': msg_id})      #将消息ID添加到消息体中    print('MSG_ID is %s\n' % msg_id)      conn= rpc.create_connection()      #连接rabbitmq服务器       #CallWaiter对象是直接消费者的处理方法    wait_msg = CallWaiter(conn)    conn.declare_direct_consumer(msg_id,wait_msg) #声明直接交换器    conn.topic_send(topic, msg)  #向主题交换器发送消息    return wait_msg.wait_reply() #等待RPC响应

两个功能:①创建和声明直接消费者  ②向主题交换器发送RPC请求。

(2)、向主题交换器发送RPC请求。

发送RPC请求的功能主要是Connection对象的topic_send方法实现。

def topic_send(self,topic, msg):        self.publisher_send(TopicPublisher,topic, msg)调用publisher_send方法。def publisher_send(self,cls, topic, msg, **kwargs):        def _publish():            #创建主题生产者对象            publisher = cls(self.channel,topic, **kwargs)            publisher.send(msg) #向主题交换器发送消息        #调用内部方法        self.ensure(_publish, topic)

publisher_send方法调用_publish内部方法。_publish首先创建一个publisher生产者对象,然后调用publisher对象的send方法发送RPC请求。

(3)、publisher对象是TopicPublisher对象。分别查看TopicPublisher类的初始化方法和send方法。

classTopicPublisher(Publisher):    def __init__(self, channel, topic,**kwargs):        #设置主题交换器属性        options = {'durable': False,                   'auto_delete': False,                   'exclusive': False}        options.update(kwargs)        #初始化父类        super(TopicPublisher,self).__init__(channel, topic, topic,                                             type='topic',**options)

可以看到,TopicPublisher类的工作都是在父类中完成的。

(4)、TopicPublisher类的父类Publisher初始化方法。

classPublisher(object):    def __init__(self, channel, exchange_name,routing_key, **kwargs):        self.exchange_name = exchange_name #交换器名        self.routing_key = routing_key #消息绑定路由键,将消息发送到指定的交换器        self.type = kwargs.pop('type') #交换器类型        self.kwargs = kwargs    #其他属性        self.reconnect(channel) #创建Producer     def reconnect(self, channel):        #创建交换器        self.exchange = kombu.entity.Exchange(self.exchange_name,                                             self.type,                                             **self.kwargs)        #创建生产者        self.producer =kombu.messaging.Producer(channel,                                                 exchange=self.exchange)     def send(self, msg):        self.producer.publish(msg, routing_key=self.routing_key)

reconnect方法创建exchange和producer对象。其中生产者对象定义一个publish方法,用于发送RPC消息。

TopicPublisher类的send方法定义在Publisher类中,调用了producer对象的publish方法。

注意:RabbitMQ中,是通过交换器名来区分交换器的。相同名字的交换器被认为是同一个交换器。主题发布者中声明的交换器属性必须和主题消费者中声明的属性完全一致。 直接发布者和直接消费者满足同样的要求。

5、服务器端:接收和处理RPC请求

处理RPC请求的方法做了逐层的封装,下图描绘了各层封装的调用关系。

RPC请求回调函数的调用关系图

(1)、ConsumerBase.consume方法的_callback内部方法

     最终会调用ProxyCallback对象来处理消息。

(2)、ProxyCallback对象

ProxyCallback对象是可调用对象。 ProxyCallback类定义在 rpc_amqp.py文件中。

classProxyCallback(object):    def __call__(self, message_data):        method = message_data.get('method') #获取RPC调用的方法        args = message_data.get('args', {}) #获取参数列表        msg_id = message_data.get('msg_id') #获取消息的ID        print('Receive RPC request. method is%s.\n' % method)         self._process_data(msg_id, method,args) #处理消息

message_data是客户端传过来的消息。消息中共包含方法名method、参数列表和消息ID msg_id 3个属性。

ProxyCallback类的__call__方法会调用_process_data方法来处理消息。

def_process_data(self, msg_id, method, args):        #调用RpcDispatcher的dispatch方法处理消息        rval = self.proxy.dispatch(method,**args)        msg_reply(msg_id, rval)  #返回处理结果

_process_data方法首先调用RpcDispatcher的dispatch方法处理消息,然后调用msg_reply方法来发送消息。

msg_reply方法定义在rpc_amqp.py文件中,查看其定义:

def msg_reply(msg_id,reply):    msg = {'result': reply}   #构造消息体    conn = impl_kombu.Connection() #建立连接    conn.direct_send(msg_id, msg)  #向直接交换器发送消息

msg_reply方法主要是调用Conection对象的direct_send方法来返回RPC调用结果。

在direct_send方法中会创建DirectPublisher对象,该对象声明一个直接交换器,交换器名字为msg_id。

(3)、RpcDispatcher对象

ProxyCallback对象的_process_data方法会调用RpcDispatcher的dispatch方法处理RPC请求。RpcDispatcher类定义在dispatcher.py文件中,dispatch方法定义:

classRpcDispatcher(object):    def dispatch(self, method, **kwargs):        if hasattr(self.callback, method):#manager对象定义method方法,执行            return getattr(self.callback,method)(**kwargs)        print('No such RPC method: %s\n' %method) #否则,报错

RpcDispatcher对象的dispatch方法最终会调用Manager对象的add方法。功能是实现计算两个数的和。

总结:

①  ConsumerBase对象consume方法的_callback内部方法的功能是获取RPC消息中的消息体。然后将消息交给ProxyCallback对象处理

②  ProxyCallback对象的功能是解析消息体中的调用方法和参数列表,然后交给RpcDispatcher对象处理

③  RpcDispatcher对象的功能是调用manager对象的相应方法处理RPC消息

④  Manager对象定义了处理RPC请求的各种方法。

6、客户端:创建和激活直接消费者

rpc.call方法调用impl_kombu.call方法。impl_kombu.call方法主要实现两个功能:

(1)、创建和声明直接消费者

(2)、向主题交换器发送RPC请求。(前面已介绍)   

介绍直接消费者的创建和声明。

查看impl_kombu.call方法的定义:

def call(topic, msg,timeout):    print('Making synchronous call on %s ...\n'% topic)    msg_id = DIRECT + str(uuid.uuid4()) #构造消息ID    msg.update({'msg_id': msg_id})     #将消息ID添加到消息体中    print('MSG_ID is %s\n' % msg_id)    conn = rpc.create_connection() #连接rabbitmq服务器    # CallWaiter对象是直接消费者的处理方法wait_msg= CallWaiter(conn)    conn.declare_direct_consumer(msg_id,wait_msg) #声明直接消费者    conn.topic_send(topic, msg)  #向直接交换器发送消息    return wait_msg.wait_reply() #等待RPC响应

Connection类的declare_direct_consumer方法创建和声明了直接消费者。直接消费者的回调函数是一个Callwaiter对象。查看Callwaiter类的__call__方法的定义:

classCallWaiter(object):def __call__(self,data):        if data['result']:            self._result = data['result']

结果保存为_result成员变量。data是通过rpc_amqp.msg_reply方法返回的消息。由于__call__方法是由kombu调用,客户端无法直接获得返回值,只能暂时把结果保存为成员变量。

在impl_kombu.call方法的最后,调用CallWaiter对象的wait_reply方法等待RPC响应。

classCallWaiter(object):    def wait_reply(self):        self._connection.consume()   #激活消费者        self._connection.drain_events() #等待响应        return self._result

7、示例程序的测试和运行

(1)、执行代码前需保证Rabbitmq-server和kombu包已正确安装。

(2)、修改impl_kombu.py文件下的rabbit_params变量中的相应字段。


0 0
原创粉丝点击