From 18f3062686af1286a05dd512930e139e0a10c638 Mon Sep 17 00:00:00 2001 From: msincenselee Date: Sat, 30 Nov 2019 09:50:43 +0800 Subject: [PATCH] =?UTF-8?q?[=E6=96=B0=E5=8A=9F=E8=83=BD]=20RabbitMQ=20?= =?UTF-8?q?=E9=80=9A=E4=BF=A1=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vnpy/amqp/README.md | 70 ++++++ vnpy/amqp/__init__.py | 0 vnpy/amqp/amqp_arch.jpg | Bin 0 -> 40953 bytes vnpy/amqp/base.py | 60 +++++ vnpy/amqp/consumer.py | 339 ++++++++++++++++++++++++++++ vnpy/amqp/producer.py | 391 +++++++++++++++++++++++++++++++++ vnpy/amqp/test01_receiver.py | 13 ++ vnpy/amqp/test01_sender.py | 11 + vnpy/amqp/test02_task.py | 27 +++ vnpy/amqp/test02_woker.py | 13 ++ vnpy/amqp/test03_subscriber.py | 15 ++ vnpy/amqp/test06_rpc_client.py | 37 ++++ vnpy/amqp/test06_rpc_server.py | 63 ++++++ vnpy/amqp/test07_rpc_client.py | 61 +++++ 14 files changed, 1100 insertions(+) create mode 100644 vnpy/amqp/README.md create mode 100644 vnpy/amqp/__init__.py create mode 100644 vnpy/amqp/amqp_arch.jpg create mode 100644 vnpy/amqp/base.py create mode 100644 vnpy/amqp/consumer.py create mode 100644 vnpy/amqp/producer.py create mode 100644 vnpy/amqp/test01_receiver.py create mode 100644 vnpy/amqp/test01_sender.py create mode 100644 vnpy/amqp/test02_task.py create mode 100644 vnpy/amqp/test02_woker.py create mode 100644 vnpy/amqp/test03_subscriber.py create mode 100644 vnpy/amqp/test06_rpc_client.py create mode 100644 vnpy/amqp/test06_rpc_server.py create mode 100644 vnpy/amqp/test07_rpc_client.py diff --git a/vnpy/amqp/README.md b/vnpy/amqp/README.md new file mode 100644 index 00000000..f0c05776 --- /dev/null +++ b/vnpy/amqp/README.md @@ -0,0 +1,70 @@ +代码源自余总得 https://github.com/yutiansut/QAPUBSUB/ + +**RabbitMQ and AMQP** + +RabbitMQ 是采用 Erlang 语言实现的 AMQP 协议的消息中间件, +最初起源于金融系统,用于在分布式系统中存储转发消息。 +RabbitMQ 发展到今天,被越来越多的人认可, +这和它在可靠性、可用性、扩展性、功能丰富等方面的卓越表现是分不开的。 + +消息支持多种传递模式,详细见案例https://www.rabbitmq.com/getstarted.html +RabbitMQ 是一种典型的点对点模式,也可以通过设置交换器类型来实现发布订阅模式而达到广播消费的效果 + +**1、点对点** + + 发布者:只需要定义queue的名称 + 接收者:只需要定义queue的名称。 隐含上,auto_ack=True,即收到消息后,自动确认,就会从Queue中删除 + +**2、工作队列** + + (任务)发布者:指定queue名称,durable=True。发送任务时,发送到指定的queue,设置delivery_mode=2(持久化该任务) + (执行)接收者:指定queue名称,durable=True。执行任务后,才确认. ch.basic_ack(delivery_tag=method.delivery_tag + +**3、发布 / 订阅(Pub/Sub)模式** + + 发布者:创建channel时,指定Exchange名称,类型为fanout。 + 发布时,指定Exchange名称,无routing_key。 + 订阅者:创建channel时,指定Exchagne名称,类型为fanout。 + 创建动态queue名,绑定私有。 + 绑定channel与queue,指定Exchange和queue名。 + +**4、路由模式** + + 发布者:发布者:创建channel时,指定Exchange名称,类型为direct。 + 发布时,指定Exchange名称,打上route_key作为标签 + 订阅者:创建channel时,指定Exchagne名称,类型为direct。 + 创建动态queue名,绑定私有。 + 绑定channel与queue,指定Exchange、queue名和期望获取的标签 + 如订阅多个标签,绑定多次即可。 + +**5、主题模式** + + 发布者:创建channel时,指定Exchange名称,类型为topic。 + 发布时,指定Exchange名称,打上route_key作为标签 + 订阅者:创建channel时,指定Exchagne名称,类型为topic。 + 创建动态queue名,绑定私有。 + 绑定channel与queue,指定Exchange、queue名和期望获取的标签匹配符 + 如订阅多个标签匹配符,绑定多次即可。 + 匹配有两个关键字: + * 1~多个字母 + # 0~多个字母 + +**6、远程调用模式** + + 服务端(响应):创建channel时,指定queue,定义basic_qos为只有一个执行, + 定义消息的响应执行方法。 + 消息执行方法:执行完毕后,发布结果消息,使用指定routing_key标签为回复的queue + 推送属性包含参照ID,确认ACK. + 客户端(请求):创建2个queue,一个是接收执行结果的queue,并绑定执行结果回调响应,自动ack。 + 另一个是推送请求的queue,推送时,增加reply_to 和 参照ID。 + + +**consumer.py 提供两种消费者:** + + 1. 订阅模式下得subscriber + 2. 点对点/路由/主题模式下得subscriber_routing + +**producter.py,提供两种生产者:** + + 1. 订阅模式下得publisher + 2. 点对点/路由/主题模式下得publisher_routing diff --git a/vnpy/amqp/__init__.py b/vnpy/amqp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/vnpy/amqp/amqp_arch.jpg b/vnpy/amqp/amqp_arch.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3597b3e4e53f900bc8311348b361e170a7900fc5 GIT binary patch literal 40953 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 z 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 Y1?(z);0P@uHUIzs07*qoM6N<$g8NP!2><{9 literal 0 HcmV?d00001 diff --git a/vnpy/amqp/base.py b/vnpy/amqp/base.py new file mode 100644 index 00000000..82d4ed4d --- /dev/null +++ b/vnpy/amqp/base.py @@ -0,0 +1,60 @@ +# encoding: UTF-8 + +import pika + +class base_broker(): + + def __init__(self, host='localhost', port=5672, user='guest', password='guest', + channel_number=1): + """ + + :param host: 连接rabbitmq的服务器地址(或者群集地址) + :param port: 端口 + :param user: 用户名 + :param password: 密码 + :param channel_number: 频道的数字(大于1) + """ + self.host = host + self.port = port + self.user = user + self.password = password + + self.channel_number = channel_number + + # 身份鉴权 + self.credentials = pika.PlainCredentials(self.user, self.password, erase_on_connect=True) + + # 创建连接 + self.connection = pika.BlockingConnection( + pika.ConnectionParameters(host=self.host, port=self.port, + credentials=self.credentials, + heartbeat=0, socket_timeout=5, + ) + ) + + # 创建一个频道,或者指定频段数字编号 + self.channel = self.connection.channel( + channel_number=self.channel_number) + + def reconnect(self): + """ + 重新连接 + :return: + """ + try: + self.connection.close() + except: + pass + + self.connection = pika.BlockingConnection( + pika.ConnectionParameters(host=self.host, port=self.port, + credentials=self.credentials, + heartbeat=0, socket_timeout=5,)) + + self.channel = self.connection.channel( + channel_number=self.channel_number) + return self + + def close(self): + if self.connection: + self.connection.close() diff --git a/vnpy/amqp/consumer.py b/vnpy/amqp/consumer.py new file mode 100644 index 00000000..95204de0 --- /dev/null +++ b/vnpy/amqp/consumer.py @@ -0,0 +1,339 @@ +# encoding: UTF-8 + # 消息消费者类(集合) +import json +import pika +import random +import traceback +from vnpy.amqp.base import base_broker + +from threading import Thread + +######### 模式1:接收者 ######### +class receiver(base_broker): + + def __init__(self, host='localhost', port=5672, user='admin', password='admin', exchange='x', + queue='recv.{}'.format(random.randint(0, 1000000)), routing_key='default'): + super().__init__(host=host, port=port, user=user, + password=password) + + # 唯一匹配 + self.routing_key = routing_key + + self.queue = queue + self.channel.exchange_declare(exchange=exchange, + exchange_type='direct', + passive=False, + durable=False, + auto_delete=False) + + self.queue = self.channel.queue_declare( + queue='', auto_delete=True, exclusive=True).method.queue + self.channel.queue_bind(queue=self.queue, exchange=exchange, + routing_key=self.routing_key) # 队列名采用服务端分配的临时队列 + # self.channel.basic_qos(prefetch_count=1) + + def callback(self, chan, method_frame, _header_frame, body, userdata=None): + print(1) + print(" [x] received: %r" % body) + + def subscribe(self): + # 消息接收 + self.channel.basic_consume(self.queue, self.callback, auto_ack=True) + self.channel.start_consuming() + + def start(self): + try: + self.subscribe() + except Exception as e: + print(e) + self.start() + +######### 模式2:(执行)接收者######### +class worker(base_broker): + + def __init__(self, host='localhost', port=5672, user='admin', password='admin', exchange='x_work_queue', + queue='task_queue', routing_key='default'): + super().__init__(host=host, port=port, user=user, password=password) + + # 唯一匹配 + self.routing_key = routing_key + self.exchange = exchange + + self.channel.exchange_declare(exchange=exchange, + exchange_type='direct', + durable=True) + + self.queue = self.channel.queue_declare(queue=queue,durable=True).method.queue + print('worker use exchange:{},queue:{}'.format(exchange, self.queue)) + self.channel.queue_bind(queue=self.queue, exchange=exchange, + routing_key=self.routing_key) # 队列名采用服务端分配的临时队列 + + # 每个worker只执行一个 + self.channel.basic_qos(prefetch_count=1) + + def callback(self, chan, method_frame, _header_frame, body, userdata=None): + print(1) + print(" [x] received task: %r" % body) + chan.basic_ack(delivery_tag=method_frame.delivery_tag) + print(" [x] task finished ") + + def subscribe(self): + print('worker subscribed') + # 消息接收 + self.channel.basic_consume(self.queue, self.callback, auto_ack=False) + self.channel.start_consuming() + + def start(self): + print('worker start') + try: + self.subscribe() + except Exception as e: + print(str(e)) + traceback.print_exc() + +######### 模式3:发布 / 订阅(Pub/Sub)模式, 订阅者######### +class subscriber(base_broker): + + def __init__(self, host='localhost', port=5672, user='admin', password='admin', + exchange='x_fanout', queue='sub.{}'.format(random.randint(0, 1000000)), + routing_key='default'): + super().__init__(host=host, port=port, user=user, password=password) + self.queue = queue + self.channel.exchange_declare(exchange=exchange, + exchange_type='fanout', + passive=False, + durable=False, + auto_delete=False) + self.routing_key = routing_key + + # 队列名采用服务端分配的临时队列 + self.queue = self.channel.queue_declare( + queue='', auto_delete=True, exclusive=True).method.queue + self.channel.queue_bind(queue=self.queue, exchange=exchange, + routing_key=self.routing_key) + + # 缺省回调函数地址 + self.cb_func = self.callback + + def set_callback(self,cb_func): + self.cb_func = cb_func + + def callback(self, chan, method_frame, _header_frame, body, userdata=None): + print(1) + print(" [x] %r" % body) + + def subscribe(self): + self.channel.basic_consume(self.queue, self.cb_func, auto_ack=True) + self.channel.start_consuming() + # self.channel.basic_consume( + # self.callback, queue=self.queue_name, no_ack=True) # 消息接收 + + def start(self): + try: + self.subscribe() + except Exception as ex: + print('subscriber exception:{}'.format(str(ex))) + traceback.print_exc() + #self.start() + +######### 模式4: 路由模式 ######### +class subscriber_routing(base_broker): + + def __init__(self, host='localhost', port=5672, user='admin', password='admin', + exchange='x_direct', queue='sub_r.{}'.format(random.randint(0, 1000000)), routing_keys=['default']): + super().__init__(host=host, port=port, user=user, + password=password) + self.queue = queue + self.channel.exchange_declare(exchange=exchange, + exchange_type='direct', + passive=False, + durable=False, + auto_delete=False) + # 队列名采用服务端分配的临时队列 + self.queue = self.channel.queue_declare( + queue='', auto_delete=True, exclusive=True).method.queue + + # 逐一绑定所有的routing 标签 + for routing_key in routing_keys: + self.channel.queue_bind(queue=self.queue, exchange=exchange, + routing_key=routing_key) + + def callback(self, chan, method_frame, _header_frame, body, userdata=None): + print(1) + print(" [x] %r" % body) + + def subscribe(self): + self.channel.basic_consume(self.queue, self.callback, auto_ack=True) + self.channel.start_consuming() + + def start(self): + try: + self.subscribe() + except Exception as e: + print(e) + self.start() + + +######### 模式5:主题模式 ######### +class subscriber_topic(base_broker): + + def __init__(self, host='localhost', port=5672, user='admin', password='admin', + exchange='x_topic', queue='sub_t.{}'.format(random.randint(0, 1000000)), routing_keys=['default']): + super().__init__(host=host, port=port, user=user, + password=password) + self.queue = queue + self.channel.exchange_declare(exchange=exchange, + exchange_type='topic', + passive=False, + durable=False, + auto_delete=False) + # 队列名采用服务端分配的临时队列 + self.queue = self.channel.queue_declare( + queue='', auto_delete=True, exclusive=True).method.queue + + # 逐一绑定所有的routing 标签 + for routing_key in routing_keys: + self.channel.queue_bind(queue=self.queue, exchange=exchange, + routing_key=routing_key) + + def callback(self, chan, method_frame, _header_frame, body, userdata=None): + print(1) + print(" [x] %r" % body) + + def subscribe(self): + self.channel.basic_consume(self.queue, self.callback, auto_ack=True) + self.channel.start_consuming() + + def start(self): + try: + self.subscribe() + except Exception as e: + print(e) + self.start() + +######### 模式6:RPC模式 (服务端) ######### +class rpc_server(base_broker): + # 接收: + # exchange: x_rpc + # queue: rpc_queue + # routing_key: gateway_name + # 发送执行结果: + # + def __init__(self, host='localhost', port=5672, user='admin', password='admin', exchange='x_rpc', + queue='rpc_queue', routing_key='default'): + super().__init__(host=host, port=port, user=user, + password=password) + + self.exchange = exchange + # 唯一匹配 + self.routing_key = routing_key + + self.channel.exchange_declare(exchange=exchange, + exchange_type='direct', + passive=False, + durable=False, + auto_delete=False) + + # 队列名采用指定队列( 若建立多个server方式随机分配任务,使用指定queue) + self.queue = queue + self.channel.queue_declare(queue=self.queue, auto_delete=True) + + # 绑定 exchange->queue->channel,指定routing_key + self.channel.queue_bind(queue=self.queue, exchange=exchange, + routing_key=self.routing_key) + # 支持多个订阅 + self.channel.basic_qos(prefetch_count=1) + # method + self.method_dict = {} + + def register_method(self, method_name, func): + """登记方法名称与调用函数""" + self.method_dict.update({method_name: func}) + + def on_request(self, chan, method_frame, _header_frame, body, userdata=None): + """ + 响应rpc请求得处理函数 + :param chan: + :param method_frame: + :param _header_frame: + :param body: + :param userdata: + :return: + """ + if isinstance(body, bytes): + body = body.decode('utf-8') + if isinstance(body, str): + body = json.loads(body) + print(" [RPC Server] on_request: %r" % body) + # 判断body内容类型 + if not isinstance(body, dict): + print(u'请求不是dict格式') + resp_data = {'ret_code': -1, 'err_msg': u'请求不是dict格式'} + self.reply(chan, resp_data, _header_frame.reply_to, _header_frame.correlation_id, method_frame.delivery_tag) + return + + method = body.get('method', None) + params = body.get('params', {}) + if method is None or method not in self.method_dict: + print(u'请求方法:{}不在配置中'.format(method)) + resp_data = {'ret_code': -1, 'err_msg': u'请求方法:{}不在配置中'.format(method)} + self.reply(chan, resp_data, _header_frame.reply_to, _header_frame.correlation_id, + method_frame.delivery_tag) + return + + function = self.method_dict.get(method) + try: + ret = function(**params) + resp_data = {'ret_code': 0, 'data': ret} + self.reply(chan, resp_data, _header_frame.reply_to, _header_frame.correlation_id, + method_frame.delivery_tag) + except Exception as ex: + print(u'mq rpc server exception:{}'.format(str(ex))) + traceback.print_exc() + resp_data = {'ret_code': -1, 'err_msg': '执行异常:{}'.format(str(ex))} + self.reply(chan, resp_data, _header_frame.reply_to, _header_frame.correlation_id, + method_frame.delivery_tag) + + def reply(self, chan, reply_data, reply_to, reply_id, delivery_tag): + """返回调用结果""" + # data => string + reply_msg = json.dumps(reply_data) + # 发送返回消息 + chan.basic_publish(exchange=self.exchange, + routing_key=reply_to, + properties=pika.BasicProperties(content_type='application/json', + correlation_id=reply_id), + body=reply_msg) + # 确认标识 + chan.basic_ack(delivery_tag=delivery_tag) + + def subscribe(self): + print(' consuming queue:{}'.format(self.queue)) + # 绑定消息接收,指定当前queue得处理函数为on_request + self.channel.basic_consume(self.queue, self.on_request) + + # 进入死循环,不断接收 + self.channel.start_consuming() + + def start(self): + """启动接收""" + try: + self.subscribe() + except Exception as e: + print(e) + self.start() + +if __name__ == '__main__': + + import sys + if len(sys.argv) >=2: + print(sys.argv) + from time import sleep + c = subscriber(user='admin', password='admin') + + c.subscribe() + + while True: + sleep(1) + + diff --git a/vnpy/amqp/producer.py b/vnpy/amqp/producer.py new file mode 100644 index 00000000..3726cb06 --- /dev/null +++ b/vnpy/amqp/producer.py @@ -0,0 +1,391 @@ +# encoding: UTF-8 +# 消息生产者类(集合) +import sys +import json +import pika +import random +import traceback +from threading import Thread +from uuid import uuid1 +from vnpy.amqp.base import base_broker + +######### 模式1:发送者 ######### +class sender(base_broker): + def __init__(self, host='localhost', port=5672, user='admin', password='admin', + exchange='x', queue_name='', routing_key='default'): + + # 通过基类,创建connection & channel + super().__init__(host, port, user, password, channel_number=1) + + self.exchange = exchange + self.queue_name = queue_name + self.routing_key = routing_key + + # 通过channel,创建/使用一个queue。 + # auto_delete: 当所有已绑定在queue的consumer不使用此queue时,自动删除此queue + # exclusive: private queue,它是True时,auto_delete也是True + self.channel.queue_declare(self.queue_name, auto_delete=True, exclusive=True) + + # 通过channel,创建/使用一个网关 + # exchange_type: direct 点对点 + # passive: 只是检查其是否存在 + # durable: 是否需要queue持久化消息。开启将降低性能至1/10 + # auto_delete: 当没有queue绑定时,就自动删除 + self.channel.exchange_declare(exchange=exchange, + exchange_type='direct', + passive=False, + durable=False, + auto_delete=False) + + def pub(self, text): + # channel.basic_publish向队列中发送信息 + # exchange -- 它使我们能够确切地指定消息应该到哪个队列去。 + # routing_key 指定向哪个队列中发送消息 + # body是要插入的内容, 字符串格式 + content_type = 'application/json' if isinstance(text, dict) else 'text/plain' + try: + self.channel.basic_publish(exchange=self.exchange, + routing_key=self.routing_key, + body=text, + properties=pika.BasicProperties(content_type=content_type, + delivery_mode=1)) + except Exception as e: + print(e) + # 重连一次,继续发送 + self.reconnect().channel.basic_publish(exchange=self.exchange, + routing_key=self.routing_key, + body=text, + properties=pika.BasicProperties(content_type=content_type, + delivery_mode=1)) + + def exit(self): + self.connection.close() + +######### 模式2:工作队列,任务发布者 ######### +class task_creator(base_broker): + def __init__(self, host='localhost', port=5672, user='admin', password='admin', + channel_number=1, queue_name='task_queue', routing_key='default', + exchange='x_work_queue'): + + # 通过基类,创建connection & channel + super().__init__(host, port, user, password, channel_number) + + self.queue_name = queue_name + self.exchange = exchange + self.routing_key = routing_key + + # 通过channel,创建/使用一个queue。 + queue = self.channel.queue_declare(self.queue_name, durable=True).method.queue + print(u'create/use queue:{}') + # 通过channel,创建/使用一个网关 + # exchange_type: direct + # passive: 只是检查其是否存在 + # durable: 是否需要queue持久化消息。开启将降低性能至1/10 + # auto_delete: 当没有queue绑定时,就自动删除 + self.channel.exchange_declare(exchange=exchange, + exchange_type='direct', + durable=True) + + def pub(self, text): + # channel.basic_publish向队列中发送信息 + # exchange -- 它使我们能够确切地指定消息应该到哪个队列去。 + # routing_key 指定向哪个队列中发送消息 + # body是要插入的内容, 字符串格式 + content_type = 'application/json' if isinstance(text, dict) else 'text/plain' + try: + self.channel.basic_publish(exchange=self.exchange, + routing_key=self.routing_key, + body=text, + properties=pika.BasicProperties(content_type=content_type, + delivery_mode=2)) + except Exception as e: + print(e) + # 重连一次,继续发送 + self.reconnect().channel.basic_publish(exchange=self.exchange, + routing_key=self.routing_key, + body=text, + properties=pika.BasicProperties(content_type=content_type, + delivery_mode=2)) + + def exit(self): + self.connection.close() + +######### 3、发布 / 订阅(Pub/Sub)模式,发布者 ######### +class publisher(base_broker): + + def __init__(self, host='localhost', port=5672, user='admin', password='admin', + channel_number=1, queue_name='', routing_key='default', + exchange='x_fanout'): + + # 通过基类,创建connection & channel + super().__init__(host, port, user, password, channel_number) + + self.queue_name = queue_name + self.exchange = exchange + self.routing_key = routing_key + + # 通过channel,创建/使用一个queue。 + # auto_delete: 当所有已绑定在queue的consumer不使用此queue时,自动删除此queue + # exclusive: private queue,它是True时,auto_delete也是True + self.channel.queue_declare(self.queue_name, + auto_delete=True, exclusive=True) + + # 通过channel,创建/使用一个网关 + # exchange_type: fanout,1对多的广播式, topic: 主题匹配 + # passive: 只是检查其是否存在 + # durable: 是否需要queue持久化消息。开启将降低性能至1/10 + # auto_delete: 当没有queue绑定时,就自动删除 + self.channel.exchange_declare(exchange=exchange, + exchange_type='fanout', + passive=False, + durable=False, + auto_delete=False) + + def pub(self, text, routing_key=None): + # channel.basic_publish向队列中发送信息 + # exchange -- 它使我们能够确切地指定消息应该到哪个队列去。 + # routing_key 指定向哪个队列中发送消息 + # body是要插入的内容, 字符串格式 + content_type = 'application/json' if isinstance(text, dict) else 'text/plain' + if routing_key is None: + routing_key = self.routing_key + try: + self.channel.basic_publish(exchange=self.exchange, + routing_key=routing_key, + body=text, + properties=pika.BasicProperties(content_type=content_type, + delivery_mode=1)) + except Exception as e: + print(e) + # 重连一次,继续发送 + self.reconnect().channel.basic_publish(exchange=self.exchange, + routing_key=routing_key, + body=text, + properties=pika.BasicProperties(content_type=content_type, + delivery_mode=1)) + + def exit(self): + self.connection.close() + +######### 4、路由模式:发布者 ######### +class publisher_routing(base_broker): + + def __init__(self, host='localhost', port=5672, user='admin', password='admin', + channel_number=1, queue_name='', routing_key='default', exchange='x_direct'): + super().__init__(host, port, user, password, channel_number) + + self.queue_name = queue_name + self.exchange = exchange + self.routing_key = routing_key + + self.channel.queue_declare( + self.queue_name, auto_delete=True, exclusive=True) + self.channel.exchange_declare(exchange=exchange, + exchange_type='direct', + passive=False, + durable=False, + auto_delete=False) + + def pub(self, text, routing_key): + # channel.basic_publish向队列中发送信息 + # exchange -- 它使我们能够确切地指定消息应该到哪个队列去。 + # routing_key 指定向哪个队列中发送消息 + # body是要插入的内容, 字符串格式 + content_type = 'application/json' if isinstance(text, dict) else 'text/plain' + try: + self.channel.basic_publish(exchange=self.exchange, + routing_key=routing_key, + body=text, + properties=pika.BasicProperties(content_type=content_type, + delivery_mode=1)) + except Exception as e: + print(e) + self.reconnect().channel.basic_publish(exchange=self.exchange, + routing_key=routing_key, + body=text, + properties=pika.BasicProperties(content_type=content_type, + delivery_mode=1)) + + def exit(self): + self.connection.close() + +######### 5、主题模式:发布者 ######### +class publisher_topic(base_broker): + + def __init__(self, host='localhost', port=5672, user='admin', password='admin', + channel_number=1, queue_name='', routing_key='default', exchange='x_topic'): + super().__init__(host, port, user, password, channel_number) + + self.queue_name = queue_name + self.exchange = exchange + self.routing_key = routing_key + + self.channel.queue_declare( + self.queue_name, auto_delete=True, exclusive=True) + self.channel.exchange_declare(exchange=exchange, + exchange_type='topic', + passive=False, + durable=False, + auto_delete=False) + + def pub(self, text, routing_key): + # channel.basic_publish向队列中发送信息 + # exchange -- 它使我们能够确切地指定消息应该到哪个队列去。 + # routing_key 指定向哪个队列中发送消息 + # body是要插入的内容, 字符串格式 + content_type = 'application/json' if isinstance(text, dict) else 'text/plain' + try: + self.channel.basic_publish(exchange=self.exchange, + routing_key=routing_key, + body=text, + properties=pika.BasicProperties(content_type=content_type, + delivery_mode=1)) + except Exception as e: + print(e) + self.reconnect().channel.basic_publish(exchange=self.exchange, + routing_key=routing_key, + body=text, + properties=pika.BasicProperties(content_type=content_type, + delivery_mode=1)) + + def exit(self): + self.connection.close() + + +######### 6、RPC模式(调用者) ######### +class rpc_client(base_broker): + # 发送: + # exchange: x_rpc + # queue: rpc_queue + # 接收结果: + # exchange: x_rpc + # queue: 动态生成 cb_queue_name + # 执行结果回调: + # 暂时无用得 queue_name='rpc_queue', + def __init__(self, host='localhost', port=5672, user='admin', password='admin', + exchange='x_rpc', routing_key='default'): + + # 通过基类,创建connection & channel + super().__init__(host, port, user, password, channel_number=1) + + self.exchange = exchange + #self.queue_name = queue_name + self.routing_key = routing_key + + # 通过channel,创建/使用一个网关 + # exchange_type: direct 点对点 + # passive: 只是检查其是否存在 + # durable: 是否需要queue持久化消息。开启将降低性能至1/10 + # auto_delete: 当没有queue绑定时,就自动删除 + self.channel.exchange_declare(exchange=exchange, + exchange_type='direct', + passive=False, + durable=False, + auto_delete=False) + + # 创建/声明rpc结果消息队列 + result = self.channel.queue_declare(queue='', exclusive=True) + self.cb_queue_name = result.method.queue + print('call back queue name:{}'.format(self.cb_queue_name)) + + # 绑定 回调消息队列,exchange和channnel + self.channel.queue_bind(queue=self.cb_queue_name, exchange=exchange) + + # 绑定 回调消息队列得接受处理函数为on_respone + self.channel.basic_consume(queue=self.cb_queue_name, + on_message_callback=self.on_respone, + auto_ack=True) + # 回调函数字典 + self.cb_dict = {} + + self.thread = Thread(target=self.start) + self.thread.start() + + def on_respone(self, ch, method, props, body): + """ + 相应 cb_queue返回的结果处理信息 + :param ch: + :param method: + :param props: + :param body: + :return: + """ + if isinstance(body, bytes): + body = body.decode('utf-8') + if isinstance(body, str): + body = json.loads(body) + + cb = self.cb_dict.pop(props.correlation_id, None) + if cb: + try: + cb(body) + except Exception as ex: + print('on_respone exception when call cb.{}'.format(str(ex))) + traceback.print_exc() + + def call(self, req_text, correlation_id=None, cb_func=None): + """ + 远程调用 + :param req_text: 调用指令及内容 + :param cb_func: 回调函数地址 + :return: + """ + # channel.basic_publish向队列中发送信息 + # exchange -- 它使我们能够确切地指定消息应该到哪个队列去。 + # routing_key 指定向哪个队列中发送消息 + # body是要插入的内容, 字符串格式 + # + content_type = 'application/json' if isinstance(req_text, dict) else 'text/plain' + if correlation_id is None: + correlation_id = str(uuid1()) + try: + print(u'sending request message, exchange:{},routing_key:{},body:{},reply_queue:{}' + .format(self.exchange, self.routing_key, req_text, self.cb_queue_name)) + self.channel.basic_publish(exchange=self.exchange, + routing_key=self.routing_key, + body=req_text, + properties=pika.BasicProperties(content_type=content_type, + reply_to=self.cb_queue_name, + correlation_id=correlation_id + )) + except Exception as e: + print(e) + # 重连一次,继续发送 + self.reconnect().channel.basic_publish(exchange=self.exchange, + routing_key=self.routing_key, + body=req_text, + properties=pika.BasicProperties(content_type=content_type, + reply_to=self.cb_queue_name, + correlation_id=correlation_id + )) + # 登记参照id和回调函数 + if cb_func: + self.cb_dict.update({correlation_id: cb_func}) + + def start(self): + try: + self.channel.start_consuming() + except Exception as ex: + print('rpc_client consuming exception:{}'.format(str(ex))) + traceback.print_exc() + self.channel.start_consuming() + + def exit(self): + try: + self.channel.stop_consuming() + self.channel.close() + self.connection.close() + if self.thread: + self.thread.join() + except: + pass + +if __name__ == '__main__': + import datetime + import time + p = publisher() + while True: + time.sleep(1) + msg = '{}'.format(datetime.datetime.now()) + print(msg) + p.pub(msg) diff --git a/vnpy/amqp/test01_receiver.py b/vnpy/amqp/test01_receiver.py new file mode 100644 index 00000000..e6d3f421 --- /dev/null +++ b/vnpy/amqp/test01_receiver.py @@ -0,0 +1,13 @@ +from vnpy.amqp.consumer import receiver + +if __name__ == '__main__': + + import sys + + from time import sleep + c = receiver(user='admin', password='admin') + + c.subscribe() + + while True: + sleep(1) diff --git a/vnpy/amqp/test01_sender.py b/vnpy/amqp/test01_sender.py new file mode 100644 index 00000000..66ed3504 --- /dev/null +++ b/vnpy/amqp/test01_sender.py @@ -0,0 +1,11 @@ +from vnpy.amqp.producer import sender + +if __name__ == '__main__': + import datetime + import time + p = sender() + while True: + time.sleep(1) + msg = '{}'.format(datetime.datetime.now()) + print(u'[x] send :{}'.format(msg)) + p.pub(msg) diff --git a/vnpy/amqp/test02_task.py b/vnpy/amqp/test02_task.py new file mode 100644 index 00000000..37365973 --- /dev/null +++ b/vnpy/amqp/test02_task.py @@ -0,0 +1,27 @@ +# encoding: UTF-8 + +from uuid import uuid1 +import json +from vnpy.amqp.producer import task_creator +from vnpy.trader.constant import Direction + +if __name__ == '__main__': + import datetime + import time + p = task_creator(host='192.168.0.202') + while True: + time.sleep(10) + mission = {} + mission.update({'id':str(uuid1())}) + mission.update({'templateName': u'TWAP 时间加权平均'}) + mission.update({'direction': Direction.LONG}) + mission.update({'vtSymbol': '518880'}) + mission.update({'is_stock': True}) + mission.update({'totalVolume': 300}) + mission.update({'target_price': 3.20}) + mission.update({'minVolume': 100}) + mission.update({'orderTime': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}) + msg = json.dumps(mission) + print(u'[x] create task :{}'.format(msg)) + p.pub(msg) + break diff --git a/vnpy/amqp/test02_woker.py b/vnpy/amqp/test02_woker.py new file mode 100644 index 00000000..b14c81a0 --- /dev/null +++ b/vnpy/amqp/test02_woker.py @@ -0,0 +1,13 @@ +# encoding: UTF-8 + +from vnpy.amqp.consumer import worker + +if __name__ == '__main__': + + import sys + + from time import sleep + c = worker(host='192.168.0.202', user='admin', password='admin') + c.start() + while True: + sleep(1) diff --git a/vnpy/amqp/test03_subscriber.py b/vnpy/amqp/test03_subscriber.py new file mode 100644 index 00000000..b33951fe --- /dev/null +++ b/vnpy/amqp/test03_subscriber.py @@ -0,0 +1,15 @@ +# encoding: UTF-8 + +from vnpy.amqp.consumer import subscriber + +if __name__ == '__main__': + + import sys + + from time import sleep + c = subscriber(user='admin', password='admin', exchange='x_fanout_md_tick') + + c.subscribe() + + while True: + sleep(1) diff --git a/vnpy/amqp/test06_rpc_client.py b/vnpy/amqp/test06_rpc_client.py new file mode 100644 index 00000000..432fe744 --- /dev/null +++ b/vnpy/amqp/test06_rpc_client.py @@ -0,0 +1,37 @@ +# encoding: UTF-8 + +from uuid import uuid1 +import json +import random +from vnpy.amqp.producer import rpc_client + +def cb_function(*args): + print('resp call back') + for arg in args: + print(u'{}'.format(arg)) + +if __name__ == '__main__': + import datetime + import time + c = rpc_client(host='localhost', user='admin', password='admin') + + counter = 0 + while True: + time.sleep(0.1) + mission = {'method': 'test_01'} + params = {} + params.update({'p2': random.random()}) + params.update({'p3': random.random()}) + params.update({'p1': counter}) + mission.update({'params': params}) + msg = json.dumps(mission) + print(u'[x] rpc call :{}'.format(msg)) + + c.call(msg,str(uuid1()), cb_function) + counter += 1 + + if counter > 100: + break + + print('exit') + c.exit() diff --git a/vnpy/amqp/test06_rpc_server.py b/vnpy/amqp/test06_rpc_server.py new file mode 100644 index 00000000..79863ccb --- /dev/null +++ b/vnpy/amqp/test06_rpc_server.py @@ -0,0 +1,63 @@ +import os, sys, copy +# 将repostory的目录i,作为根目录,添加到系统环境中。 +ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(ROOT_PATH) + +routing_key = 'default' + +from vnpy.amqp.consumer import rpc_server + +import argparse + +def test_func01(p1,p2,p3): + print('test_func01:', p1, p2, p3) + return p1+p2+p3 + +def test_func02(p1, p2=0): + print('test_func02:', p1, p2) + return str(p1 + p2) + +def get_strategy_names(): + print(u'{}'.format(routing_key)) + return ['stratege_name_01', 'strategy_name_02'] + + +if __name__ == '__main__': + + # 参数分析 + parser = argparse.ArgumentParser() + + parser.add_argument('-s', '--host', type=str, default='localhost', + help='rabbit mq host') + parser.add_argument('-p', '--port', type=int, default=5672, + help='rabbit mq port') + parser.add_argument('-U', '--user', type=str, default='admin', + help='rabbit mq user') + parser.add_argument('-P', '--password', type=str, default='admin', + help='rabbit mq password') + parser.add_argument('-x', '--exchange', type=str, default='exchange', + help='rabbit mq exchange') + parser.add_argument('-q', '--queue', type=str, default='queue', + help='rabbit mq queue') + parser.add_argument('-r', '--routing_key', type=str, default='default', + help='rabbit mq routing_key') + args = parser.parse_args() + + routing_key = copy.copy(args.routing_key) + + from time import sleep + s = rpc_server(host=args.host, + port=args.port, + user=args.user, + password=args.password, + exchange=args.exchange, + queue=args.queue, + routing_key=args.routing_key) + + s.register_method('test_01', test_func01) + s.register_method('test_02', test_func02) + s.register_method('get_strategy_names', get_strategy_names) + + s.start() + while True: + sleep(1) diff --git a/vnpy/amqp/test07_rpc_client.py b/vnpy/amqp/test07_rpc_client.py new file mode 100644 index 00000000..e414c192 --- /dev/null +++ b/vnpy/amqp/test07_rpc_client.py @@ -0,0 +1,61 @@ +import os, sys +# 将repostory的目录i,作为根目录,添加到系统环境中。 +ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) +sys.path.append(ROOT_PATH) + +from vnpy.amqp.producer import rpc_client +from uuid import uuid1 +import json +import random +import argparse + +def cb_function(*args): + print('resp call back') + for arg in args: + if isinstance(arg,bytes): + print(u'{}'.format(arg.decode('utf-8'))) + else: + print(u'{}'.format(arg)) + +from vnpy.trader.vtConstant import * +if __name__ == '__main__': + # 参数分析 + parser = argparse.ArgumentParser() + + parser.add_argument('-s', '--host', type=str, default='localhost', + help='rabbit mq host') + parser.add_argument('-p', '--port', type=int, default=5672, + help='rabbit mq port') + parser.add_argument('-U', '--user', type=str, default='admin', + help='rabbit mq user') + parser.add_argument('-P', '--password', type=str, default='admin', + help='rabbit mq password') + parser.add_argument('-x', '--exchange', type=str, default='exchange', + help='rabbit mq exchange') + parser.add_argument('-q', '--queue', type=str, default='queue', + help='rabbit mq queue') + parser.add_argument('-r', '--routing_key', type=str, default='default', + help='rabbit mq routing_key') + args = parser.parse_args() + + import datetime + import time + + c = rpc_client(host=args.host, port=args.port, user=args.user, password=args.password, exchange=args.exchange, queue_name=args.queue, routing_key=args.routing_key) + + counter = 0 + while True: + time.sleep(10) + mission = {'method': 'get_strategy_names'} + params = {} + mission.update({'params': params}) + msg = json.dumps(mission) + print(u'[x] rpc call :{}'.format(msg)) + c.call(msg, str(uuid1()), cb_function) + counter += 1 + + if counter > 1: + break + + print('exit') + c.exit()