RabbitMQ介绍

一,RabbitMQ功能

    RabbitMQ就是消息队列,当两个进程需要进行通信的时候,RabbitMQ为一个代理服务器,其中一个进程先将消息发送给服务器,然后服务器再发送给另一个服务器,这样就实现两个不同的进程之间的通信了。RabbitMQ是一个消息代理:它接受和转发消息。你可以把它想象成一个邮局:当你把你想要发布的邮件放在邮箱中时,你可以确定邮差先生最终将邮件发送给你的收件人。在这个比喻中,RabbitMQ是邮政信箱,邮局和邮递员。

二,实现简单的通信:

原理图:

produce端:

import pika
#向队列发送一条消息

#第一件事是与RabbitMQ服务器建立连接
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

#接下来,在发送之前,我们需要确保收件人队列存在,如果我们发送消息到不存在的位置,
#RabbitMQ将只删除该消息。我们来创建一个将传递消息的hello队列:
channel.queue_declare(queue='hello')

#在RabbitMQ中,消息永远不会直接发送到队列中,它总是需要经过交换。我们现在需要知道
#的是如何使用同空字符串标识的默认交换。这种交换是特殊的,它允许我们准确地指定消息
#应该到达哪个队列。队列名称需要在routing_key参数中指定。
channel.basic_publish(exchange='',
                      routing_key='hello',
                      body='Hello Word'
)

print("[x]已经发送消息'Hello Word'")

#在退出程序之前,我们需要确保网络缓冲区被刷新,并且我们的消息被实际传递送到RabbitMQ。
#们可以通过轻轻关闭连接来完成。
connection.close()

consume端:

import pikae
#接收

#首先连接到RabbitMQ服务器。
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

#下一步,就像以前一样,要确保队列存在,使用queue_declare创建一个队列是相等,我们
#可以根据需要多次运行该命令,并且只会创建一个。你可能会问什么我们再次声明队列是为什么,
#我们已经在之前的代码中声明了它。如果我们确信队列已经存在,我们可以避免这种情况。
#例如,如果在《produce》程序之前运行过。但我们还不确定首先运行哪个程序。在这种情况下,
#重复在两程序中重复声明是一种很好的做法。
channel.queue_declare(queue='hello')

#从队列接收消息它通过向队列订阅回调函数来工作。每当我们收到一条消息,这个回调函数
# 就被pika库调用。在我们的例子中,这个函数会在屏幕上打印消息的内容。
def callback(ch,method,properties,body):
    #ch:是声明的管道内存对象
    print("[x] Received%s"%body)

#接下来,我们需要告诉RabbitMQ这个特定的回调函数应该从我们的hello队列接收消息
#为了让这个命令成功,我们必须确保我们想要订阅的队列存在。幸运的是,我们对此
# 有信心 ,我们已经使用queue_declare创建了一个队列。
channel.basic_consume(callback,#如果接到消息,就会调用回调函数
                      queue='hello',
                      no_ack=True
)

print('[*]正在等待消息。要退出,请按CTRL + C')
#最后,我们进入一个永无止境的循环,等待数据并在必要时运行回调,
#会一直运行,当没有消息,就会卡住
channel.start_consuming()

注意:这里的produce,consume,RabbitMQ服务器都在同一台主机上运行的,如果要连接远程的主机,需要提供用户和密码。

三,循环调度

介绍:是指当有多个消费者的时候,例如,按顺序分别打开了c1, c2, c3三个消费者,当生产者发送一条消息时,此时是c1接到了这条消息;当生产者再发一条消息时,此时是c2接到了这条消息;当生产者再次发一条消息时,此时是c3接到了这条消息;当生生者再发一条消息时,此时是c1接到了这条消息....,就这样,c1,c2,c3按顺序依次接收消息。

代码:produce端和consume端的代码与上面一样。

优点可以轻松地平行工作。如果我们正在积累积压的工作,我们可以增加更多的工作人员,并且这种方式很容易扩展。


四,消息确认

之前的代码都是消息者接受消息后不用发确定接受的信息给服务器的,生产者发送了消息,就会在队列中删除它,在no_ack=True中已经说明了。但是在实际的应用场景中,是需要发送确认信息给服务器的,然后服务器再在队列中删除消息,例如当消息者在接收一个任务时,而接收处理完这个任务需要10秒种,此时,很不幸,这台主机挂掉了,那么这条任务就不应该在服务器中删除,而应该等待另一台消费者主机来接收处理它。要实现这个,需要一点,消息者端接收消息处理后发送一个确认信息给服务器,服务器再从队列中删除它,否则就不删。实现代码如下:

produce端:

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.queue_declare(queue='hello')

channel.basic_publish(exchange='',
                      routing_key='hello',
                      body='Hello Word'
)

print("[x]已经发送消息'Hello Word'")

connection.close()

consume端:

import pika
import time

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.queue_declare(queue='hello')

def callback(ch,method,properties,body):
    '''ch:是声明的管道内存对象
	ch.basic_ack(delivery_tag=method.delivery_tag)用来确认接收处理完这条消息
	,发送确认信息给了服务器,服务器就会从队列中删除这个任务
    '''
    print('-->',ch,method,properties)
    time.sleep(10)
    print("[x] Received%s"%body)
    ch.basic_ack(delivery_tag=method.delivery_tag)

channel.basic_consume(callback,
                      queue='hello',
 #no_ack是指生产者发送消息到队列后, 不会确定消费者是否从队列取出消息后并处理完,
 #不会发一个确认信息给服务器,服务器发出去了就会删除它,这样是不保险的。
)

print('[*]正在等待消息。要退出,请按CTRL + C')
channel.start_consuming()


五,消息持久性

    消息确认是为了防止consume端突然宕机,但在实际考虑中,是不是也应该想到服务器会宕机,例如,当我们正在向服务器接收任务时,突然服务器停止了,那么里面的队列和消息则会全部清空,为了防止这种情况发生,我们应该怎么办?这时候就要用到消息持久性了,即在服务器停掉的情况下,消息也不会丢失。为了保证消息持久性,我们应该想到队列在服务器里,消息在队列里,所以我们应该在代码中做到两点,一是保证队列持久性,二是保证消息持久性。

produce端:

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

#加durable持久化队列,当服务器断开了,队列还存在。若只持久化队列,则队列在,消息不在
channel.queue_declare(queue='hello2',durable=True)

#加properties持久化消息,当服务器断开了,若已经持久化队列,则消息和队列都在。若只持久化消息,没有持久化队列,则消息也不会存在。
channel.basic_publish(exchange='',
                      routing_key='hello2',
                      body='Hello Word',
                      properties=pika.BasicProperties(delivery_mode=2)
)

print("[x]已经发送消息'Hello Word'")
connection.close()

consume端:

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.queue_declare(queue='hello2',durable=True)

def callback(ch,method,properties,body):
    print("[x] Received%s" % body)
    ch.basic_ack(delivery_tag=method.delivery_tag) #消息确认

channel.basic_consume(callback,
                      queue='hello2',
)

print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()

六,消息公平派遣

    前面说了循环调度可以帮助减轻工作压力,比如,本来是1台consume端在接收处理消息,现在可以分配到两台机子上来处理。但考虑到实际情况,这样的分配还可不可以公平一点。比如,在一个工厂,有3个工人在处理消息,循环调度是指每个工人轮流接收处理消息。假如有这样一种情况,其中一个工人的工作能力很强,其他工人接收处理一条消息所花的时间等于他接收处理4条消息的时间。那么循环轮流分配任务就显得效率低了一点。因为假如工厂分别来了三条消息,三个工人(a,  b, c)分别领到了一条消息,其中c的工作能力很强,c需要10秒完成,a, b需要40秒完成。突然在11秒的时候又来了一条消息,本来按循环轮流调度的原则,这条消息应该给a,但是a上一条消息还没处理完,而c却已经处理完消息在那里休息。所以了为提高效率,我们把这条消息给c来处理。总的来说,怎么做到效率更高的分配任务,就是使自己有消息在处理的时候不能再接收其他消息。


原理图:


注:其中的prefetch=1是指当c有一条消息在处理的时候不能接收其他消息,直到他处理完成。


produce端:

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

#加durable持久化队列
channel.queue_declare(queue='hello2',durable=True)

#加properties持久化消息
channel.basic_publish(exchange='',
                      routing_key='hello2',
                      body='Hello Word',
                      properties=pika.BasicProperties(delivery_mode=2)
)

print("[x]已经发送消息'Hello Word'")
connection.close()

consume端:

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.queue_declare(queue='hello2',durable=True)

def callback(ch,method,properties,body):
    print("[x] Received%s" % body)
    ch.basic_ack(delivery_tag=method.delivery_tag)#消息确认

channel.basic_qos(prefetch_count=1) #为了公平派遣

channel.basic_consume(callback,
                      queue='hello2',
)

print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()


七,fanout广播

    在前面的介绍中,我们创建了一个工作队列。工作队列背后的假设是,每个任务只被传递给一个工作人员。在这一部分,我们将做一些完全不同的事情 - 我们会向多个消费者传递信息。这种模式被称为“发布/订阅”,它类似一种广播,发布方在发布一条消息后,许多订阅方会同时收到消息。那么怎么才能做到呢?

原理图:


注:前面的部分是交换机是设置为默认,这部分交换机就起作用了。为了实现广播模式,我们首先肯定要在发布方一端声明一个交换机,订阅方一端是从一个队列中取消息,还是各自的队列中取消息呢?答案就在图中,每个订阅方都会暂时的有一个队列,从里面取出消息后就可以删除了。交换机只要邦定每个交换机就可以实现了。

发布方:

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

#声明一个交换机,并指定交换机的类型为'fanout'
channel.exchange_declare(exchange='logs',exchange_type='fanout')

message = 'hello word!'

#这里的线路值routing_key为空值,是代表了一种广泛的广播,指发送到所有的队列。
channel.basic_publish(exchange='logs',
                      routing_key='',
                      body=message
)

print('[x] send %r'%message)

connection.close()

订阅方:

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

#声明交换机以及类型,为什么要重新声明一次,理由和前面所讲为什么要重复声明一次队列一样
channel.exchange_declare(exchange='logs',exchange_type='fanout')

#每个订阅方都要声明一个消息队列,再从里面接收消息
#exclusive(唯一的)表示了这个队列是针对这一个订阅方是唯一的,这个订阅方退出了,那么队列就会自动删除
#需要给这个特别的队列取一个名字为result.method.queue
result = channel.queue_declare(exclusive=True)
queueName = result.method.queue

#订阅方需邦定这个唯一的队列,里面写好参数
channel.queue_bind(exchange='logs',queue=queueName)

print('[*] waiting for logs,to exit press CTRL+C ')

def callback(ch, method, properties, body):
    print('received: %r'%body)

channel.basic_consume(callback,
                      queue=queueName,
                      no_ack=True
)

channel.start_consuming()


八,direct广播

    前面我们已经利用fanout交换类型实现了广播模式,为了使规划合理,我们可能使它变成更加人性化,我们可以将系统日志全部发送给这一部分的订阅方,将系统错误消息发送给那一部分的订阅方。而前面是不分什么类型的订阅方,只要是订阅方统统都要接收消息。为了实现这种过滤式的发送消息,那么怎么实现呢?

原理图:


注:我们可以通过邦定发送路线的方式来实现,就好像,有三条马路,其中一条是专门给人走路的,其中一条是专门自行车骑行的,另一条是专门汽车开的。这里,我们只要给出一个路线邦定值routing_key,谁邦定上它才能在这一条线上发送消息,这样就可以了。在图中,c1邦定了routing_key=erro,那么发布方发送系统错误日志的时候,只需要指定那条错误路线就可以发送到那里了。

发布方:

import pika
import sys

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

#声明交换机以及类型为direct
channel.exchange_declare(exchange='direct_log',exchange_type='direct')

#发布方在这里可以声明要发送到哪种路线上去,默认是info路线
#比如你要发送错误到订阅方去,python xxx.py erro
severity = sys.argv[1] if len(sys.argv) > 1 else 'info'
print('sys.argv',len(sys.argv))
print('severity:',severity)

message = ' '.join(sys.argv[1:]) or 'hello word !'
print('message',message)

#用severity邦定routing_key
channel.basic_publish(exchange='direct_log',
                      routing_key=severity,
                      body=message
)

print('[x] send %r:%r'%(severity,message))

connection.close()

订阅方:

import pika
import sys

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

#声明交换以及类型为direct
channel.exchange_declare(exchange='direct_log',
                         exchange_type='direct'
                         )

#声明队列
result = channel.queue_declare(exclusive=True)
queueName = result.method.queue

#指定routing_key为什么类型,例如我要专门接收错误信息,则severities输入为erro即可
#也可以多邦定几条,例如,我可以专门接收erro,info的信息.
severities = sys.argv[1:]

if not severities:
    sys.stderr.write("Usage: %s [info] [warning] [error]\n" % sys.argv[0])
    sys.exit(1)

#循环这个列表,订阅方可以多邦定几条线路,这是没有问题的
for severity in severities:
    channel.queue_bind(exchange='direct_log',
                       queue=queueName,
                       routing_key=severity)

def callback(ch,method,properities,body):
    print(" [x] %r:%r" % (method.routing_key, body))

channel.basic_consume(callback,
                      queue=queueName,
                      no_ack=True)

channel.start_consuming()


九,topic广播

    为了加强direct类型的功能,在实际应用中,假如我们要分别发送给mysql和qq一个info,在上面的例子中,我们是需要两个订阅方的,分别为,mysql.info 和 qq.info。为了提高效率,我们提出了一种新的类型——topic.我们只要在订阅方邦定的路线值为  *.info ,那么不管是mysql.info还是qq.info都可以让同一个订阅接收到消息。实现原理图为:


注:c1接收*.orange.*的消息,如 a.orange.b

c2接收*.*.rabbit的类型消息,如a.b.rabbit

c3接收lazy开头的消息。如果只是#,那么是指可以接收所有类型的消息。这样就相当于fanout了


发布方:

import pika
import sys

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='topic_log',exchange_type='topic')

routing_key = sys.argv[1] if len(sys.argv) > 1 else 'anonymous.info'
message = ' '.join(sys.argv[1:]) or 'hello word'

channel.basic_publish(exchange='topic_log',
                      routing_key=routing_key,
                      body=message)

print(" [x] Sent %r:%r" % (routing_key, message))
connection.close()

订阅方:

import pika
import sys

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='topic_log',exchange_type='topic')

result = channel.queue_declare(exclusive=True)
queueName = result.method.queue

#这里可以为*.info,*.erro 或 mysql.*
binding_keys = sys.argv[1:]

if not binding_keys:
    sys.stderr.write("Usage: %s [binding_key]...\n" % sys.argv[0])
    sys.exit(1)

for binding_key in binding_keys:
    channel.queue_bind(exchange='topic_log',
                       routing_key=binding_key,
                       queue=queueName)

print(' [*] Waiting for logs. To exit press CTRL+C')

def callback(ch,method,properties,body):
    print(" [x] %r:%r" % (method.routing_key, body))

channel.basic_consume(callback,
                      queue=queueName,
                      no_ack=True)

channel.start_consuming()


十,RPC远程过程调用

    之前的内容,我们学习了如何使用工作在多个工作人员之间分配耗时的任务。但是如果我们需要在远程计算机上运行某个功能并等待结果呢?那么,这是一个不同的故事。这种模式通常称为远程过程调用RPC在这里,我们将使用RabbitMQ构建一个RPC系统:一个客户端和一个可扩展的RPC服务器。由于我们没有任何值得分发的耗时任务,我们将创建一个返回斐波那契数字的虚拟RPC服务。通俗的讲:客户端首先发一条信息给服务端执行,然后服务端将结果再返回给客户端。

原理图:



注:要实现这个功能,我们首先要做的是发一条指令给server,通过队列rpc_queue发送,然后server发送结果给client,通过一个特定的队列发送。在client发送指令给server的过程中,可以附带两个属性,一个是reply_to=queueName:client告诉server返回结果时,通过queueName这个队列发送过来。一个是correlation_id=abc:client先随机生成一个验证码,然后发指令给server的时候将这个验证码当属性发送过去。然后server返回结果的同时带上这个验证码,client再次检查这个验证码是否一致,若一致,就接收这个结果。为什么需要一个验证呢?这是因为当我们将很多指令发给server,server还来不及处理时,而在cient中收到回复结果后,不清楚回复属于哪个请求。那是就要使用correlation_id属性。我们将把它设置为每个请求的唯一值。稍后,当我们在回调队列中收到消息时,我们将查看此属性,并基于此属性,我们将能够将响应与请求进行匹配。如果我们看到未知的correlation_id值,我们可以放心地丢弃该消息 - 它不属于我们的请求。


rpc_client:

import pika
import uuid
import time

class FibonacciRpcClient(object):

	def __init__(self):
		self.connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))

		self.channel = connection.channel()

		#声明要接收返回消息的队列
		result = self.channel.queue_declare(exclusive=True)

		#声明服务端返回时所要接收的队列
		self.callback_queue = result.method.queue_declare

		self.channel.basic_consume(self.on_respond,#只要接收到消息就执行这个函数
								   no_ack = True,
								   queue = self.callback_queue
			)

	def on_response(self,ch,method,props,body):
		'''回调函数'''
		if self.corr_id == props.correlation_id:
			self.response = body

	def call(self,n):
		self.response = None

		self.corr_id = str(uuid.uuid4()) #随机生成的字符串
		print(self.corr_id)

		#发送要执行的消息给服务器
		self.channel.basic_publish(exchange='',
								   routing_key='rpc_queue',
								   #消息可以附带属性:服务器返回发送消息时的队列名称,随机验证字符串
								   properties=pika.BasicProperties(
								   		reply_to = self.callback_queue,
								   		correlation_id = self.corr_id
								   	),
								   body=str(n)
			)

		while self.response is None:
			self.connection.process_data_events() #非阻塞版的start_consume
			print('no messages')
			time.sleep(0.5)

		return int(self.response)



fibonacci_rpc = FibonacciRpcClient()
print('[x] Requesting fib(8)')

response = fibonacci_rpc.call(8)
print('[.] Got %r'%respond)

rpc_server:

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

#声明要在队列rpc_queue中接收消息
channel.queue_declare(queue='rpc_queue')

def fib(n):
    if n==0:
        return 1
    elif n==1:
        return 1
    else:
        return fib(n-1)+fib(n-2)

def on_request(ch,method,props,body):
    n = int(body)
    result = fib(n)

    #已经接收到消息,现在要将消息返回发送给客户端
    ch.basic_publish(exchange='',
                     routing_key=props.reply_to, #指明要发送的队列名称
                     properties=pika.BasicProperties(correlation_id=props.correlation_id),
                     body=str(result)

    )
    
    #一旦消息确认,就会从队列中删除消息
    ch.basic_ack(delivery_tag=method.delivery_tag)


#接收来自客户端的消息
channel.basic_consume(on_request, #指明回函数
                      queue='rpc_queue' #指明要接收的队列
                      )

print(" [x] Awaiting RPC requests")
channel.start_consuming()

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值