python AMQP 客户端连接

设计

  • 该文章主要是花了比较长的时间解决AMQP 数据收发异步的问题。该问题主要在于PIKA包不支持异步的问题。收发数据使用同一个连接和同一个通道会在某些环境异常(Windows 和部分Linux没有出现,在生产环境的Linux下会产生异常) 。而且问题很多,其中一个就是 Stream connection lost: AssertionError((’_AsyncTransportBase._produce() tx buffer size underflow’, -275, 1),)。

设计一个AMQPClientUtil类 用户AMQPClient管理

  • AMQMClient管理 这里会创建两个AMQP对象,一个是用户定义的比如 hello_amqp,主要用户其他服务来请求数据,另一个有系统定义hello_amqprep,在用户定义的queue末尾增加req主要用户向其他服务请求数据。这样设计的目的在于将主动请求和主动接受分开。避免一个数据队列数据量过多
  • 外部通过AMQPClientUntil的对象调用 只存在启动时,调用run函数,已初始化AMQP连接。发送数据时调用 send

设计一个AMQPClient类

  • 该类的主要作用的是连接AMQP,并进行异步收发数据
  • 详情见源码内说明

源码

# coding=UTF-8
import pika
import json
import threading
import time
import zlib
import datetime
import sys

KEEP_ACTIVE = "keep_active"

def getlocaltime():
	'''
	格式化 输出时间 这一块主要用于日志输出
	'''
    return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

class AMQPClientUtil():
	'''
	AMQMClient管理 这里会创建两个AMQP对象,一个是用户定义的比如 hello_amqp,主要用户其他服务来请求数据,另一个有系统定义hello_amqprep,在用户定义的queue末尾增加req主要用户向其他服务请求数据。这样设计的目的在于将主动请求和主动接受分开。避免一个数据队列数据量过多
	'''
    def __init__(self, queue, exchange, cb_func, username, userpasswd, host, port):
        self.recvChannel = AMQPClient(queue, exchange, cb_func, username, userpasswd, host, port)
        req_queue = queue + "req"
        self.sendChannel = AMQPClient(req_queue, exchange, cb_func, username, userpasswd, host, port)
        pass

    def run(self):
        self.recvChannel.run()
        self.sendChannel.run()

    def Send(self, routing_key, type, strMsg, szip="false"):
        self.sendChannel.Send(routing_key, type, strMsg, szip)


class AMQPClient():
    def __init__(self, queue, exchange, cb_func, username, userpasswd, host, port):
        '''
        :param queue: 队列和RoutineKey
        :param exchange:  交换机
        :param cb_func:   接收消息的回调函数 函数原型 (  DealMesIn(self,  msgType, msg, routineKey)  )
        '''
        print(getlocaltime(), "正在初始化AMQP---- " + queue)
        self.queue = queue
        self.exchange = exchange
        self.pid = 1
        self.cb_func = cb_func
        self.timmer = None
        self.cur_reciveDateTime = datetime.datetime.now()
        self.channel = None
        self.connection = None
        self.channel_send = None
        self.connection_send = None
        # 全局化 PIKA 参数 , 因为PIKA不支持异步,所以后面会有多个连接
        self.properties = pika.BasicProperties(headers={})
        credentials = pika.PlainCredentials(username, userpasswd)
        self.parameters = pika.ConnectionParameters(host=host, port=port, credentials=credentials, heartbeat=0)
        
        # 起一个线程监控接收数据的情况
        thread = threading.Thread(target=self.__checkReciveTime)
        thread.start()
     
    def __checkReciveTime(self):
        while 1:
            cur_time = datetime.datetime.now()
            if (cur_time - self.cur_reciveDateTime).seconds > 600:
                # 表示数据已经断了
                print ('AMQP断了正在重新连接......')                
                self.__ConnectAMQP()
                pass
            time.sleep(61 * 5)
            
    def __getPid(self):
	    # 与服务端程序设计有关,服务端收到数据之后会将PID返回,让客户端实现同步操作
        self.pid += 1
        return self.pid

    def run(self):
    	# 启动线程连接AMQP
        thread = threading.Thread(target=self.__ConnectAMQP)
        thread.start()
        self.__keep_active()
        pass

    def __ConnectAMQP(self):
        try:
            print(getlocaltime(), "ConnectAMQP---- " + self.queue)
            self.createAMQP = False
            
            # 关闭该关闭的数据
            self.__reset()
            # 创建一个接收数据的 连接 和 通道
            self.connection, self.channel = self.__getAMQPConeAndChannel()
            # 创建一个发送数据的 连接 和 通道
            self.connection_send, self.channel_send = self.__getAMQPConeAndChannel()
			# 注: 上面两组连接与通道产生的原因是 PIKA 是不支持异步,那如果只有一个连接和通道用于接收和发送数据会造成start_consuming异常
		
            print(getlocaltime(), "初始化AMQP成功---- " + self.queue)
            self.createAMQP = True
            self.channel.start_consuming()
        except BaseException as e:
        	# 关闭之前创建的连接
            self.__reset()
            if isinstance(e, KeyboardInterrupt):
                return
            
            exc_type, exc_value, exc_obj = sys.exc_info()
            traceback.print_exception(exc_type,exc_value,exc_obj,limit=2,file=sys.stdout)
            print(getlocaltime(), "{}:{}".format(sys._getframe().f_code.co_name, sys._getframe().f_lineno), "AMQP异常, ", e)
            # AMQP断连之后的重连机制
            timmer = threading.Timer(10, AMQPClient.__ConnectAMQP, args=(self, ))
            timmer.start()
    
    def __reset(self):
        try:
            if self.connection:
                self.connection.close()
            if self.connection_send:
                self.connection_send.close()
            if self.channel:
                self.channel.stop_consuming()
            if self.channel:
                self.channel_send.stop_consuming()
        except Exception as e:
            pass
        finally:
            pass
            
    def __getAMQPConeAndChannel(self):
    	# 创建连接
        connection = pika.BlockingConnection(self.parameters)
        # 创建通道
        channel = connection.channel()
        # 声明一个交换机  durable 持久化数据 auto_delete 绑定数据
        channel.exchange_declare(
            exchange=self.exchange,
            exchange_type='direct',
            passive=True,
            durable=False,
            auto_delete=True)
        # 绑定队列和路由
        self.bind_queue(self.queue, channel)
        return connection, channel

    def bind_queue(self, queue,channel):
    	# 这个参数主要适用于超时断连自动删除队列和路由键
        dic_args = {"x-expires":60000, "x-message-ttl":30000}
        channel.queue_declare(queue=queue, arguments=dic_args)
        channel.queue_bind(queue=queue, exchange=self.exchange, routing_key=queue)
        # prefetch_count 当队列中有最大多个数据没有被确认接收 ,不再接收其他数据,如果想实现同步调用可以将这个值设置为1
        channel.basic_qos(prefetch_count=54)
        # auto_ack=False 这个参数 是收到消息自动确认,如果设置为True则表示自动确认收到消息。想实现同步消息 需要 prefetch_count设置为1 并且 auto_ack=True
        channel.basic_consume(queue=queue, on_message_callback=self.DealMesIn, auto_ack=False)

    def __keep_active(self):
    	# 定时发送消息 以保证没有收发消息的时候,队列和路由键不会被销毁
        self.Send(self.queue, KEEP_ACTIVE, KEEP_ACTIVE)
        if self.timmer != None:
            self.timmer.cancel()
        self.timmer = threading.Timer(15, AMQPClient.__keep_active, args=(self, ))
        self.timmer.start()

    def DealMesIn(self, ch, method, properties, body):	    
    	# 内部的一个消息接收函数,主要是内部处理掉 keep_active
    	self.cur_reciveDateTime = datetime.datetime.now()
        dic_args = {"x-expires": 60000, "x-message-ttl": 30000}
        queue_declare = self.channel.queue_declare(queue=self.queue, arguments=dic_args)
        headers = properties.headers
        try:
	        if headers["zip"] == 'false' and (body.decode() == KEEP_ACTIVE):
	            print(getlocaltime(), body.decode())
	        else:
				msg = ""
                if headers["zip"] == "true":
					decompress = zlib.decompressobj()
                    msg = decompress.decompress(body)
                else:
                    msg = body.decode()
  	        	# 调用用户注册的消息回调
                self.cb_func(headers["type"], msg, headers["from"])
	    except Exception as e:
	    	exc_type, exc_value, exc_obj = sys.exc_info()
            traceback.print_exception(exc_type,exc_value,exc_obj,limit=2,file=sys.stdout)
            print (getlocaltime(), "{}:{}".format(sys._getframe().f_code.co_name, sys._getframe().f_lineno), "recive...error:",e)
        finally:
	    	# 确认消息 如果auto_ack=True,则这个不是需要的,如果 auto_ack=False,是需要设置的,不然消息数量达到 prefetch_count之后,不再接收消息
            ch.basic_ack(delivery_tag=method.delivery_tag)

    def Send(self, routing_key, type, strMsg, szip="false"):
        if not self.createAMQP:
            return
        if szip:
            # 压缩系统还没做好
            szip = "false"
        dict_header = {}
        dict_header["type"] = type
        dict_header["from"] = self.queue
        dict_header["pid"] = str(self.__getPid())
        dict_header["structlen"] = str(len(strMsg))
        dict_header["zip"] = szip
        try:
        	# 构建消息的头,用于服务端解析消息的来源
            self.properties.headers = dict_header
            # 发送消息
            self.channel_send.basic_publish(exchange=self.exchange, routing_key=routing_key, properties=self.properties, body=strMsg)
        except:
            print (getlocaltime(), "{}:{}".format(sys._getframe().f_code.co_name, sys._getframe().f_lineno), "send...error")
        finally:
            # connection.close()
            pass

# 以下是测试案例
def OnMessage(ch, method, properties, body):

    headers = properties.headers
    if headers["zip"] == 'false':
        print(datetime.datetime.now().strftime("%H:%M:%S"), "nozip", body)
    else:
        print(zlib.decompress(body))
        pass

def aaa():
    print (getlocaltime(), "{}:{}".format(sys._getframe().f_code.co_name, sys._getframe().f_lineno))

if __name__ == '__main__':
    aaa()

    a = None
    try:
        a = AMQPClientUtil("hello_test2", "exchange", OnMessage, "username", "password", "amqp地址", "port")
        a.run()
        dict_data = {}
        dict_data["SubscribeMarketData"] = ["AP101"]
        time.sleep(3)
        while 1:
            a.Send("hello_test1", "SubscribeMarketData", json.dumps(dict_data))
            time.sleep(60)

            pass
    finally:
        if a != None:
            pass
  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值