python下rabbitmq的使用

python下rabbitmq的使用

http://www.rabbitmq.com/

注:命令行字符串一定使用""

首先rabbitmq基于erlang语言,所以要先装erlang的解释器
http://www.erlang.org/downloads
之后在安装rabbitmq

安装完之后
rabbitmq-plugins enable rabbitmq_management

可以通过访问:http://localhost:15672进行测试,默认的登陆账号为:guest,密码为:guest。

其他配置

  1. 安装完以后erlang需要手动设置ERLANG_HOME 的系统变量。

set ERLANG_HOME=F:\Program Files\erl9.0
#环境变量path里加入:%ERLANG_HOME%\bin
#环境变量path里加入: 安装路径\RabbitMQ Server\rabbitmq_server-3.6.10\sbin

2.激活Rabbit MQ’s Management Plugin

使用Rabbit MQ 管理插件,可以更好的可视化方式查看Rabbit MQ 服务器实例的状态,你可以在命令行中使用下面的命令激活。

rabbitmq-plugins.bat enable rabbitmq_management

3.创建管理用户

rabbitmqctl.bat add_user sa 123456

  1. 设置管理员

rabbitmqctl.bat set_user_tags sa administrator

5.设置权限

rabbitmqctl.bat set_permissions -p / sa “." ".” “.*”

  1. 其他命令

#查询用户:
rabbitmqctl.bat list_users
#查询vhosts:
rabbitmqctl.bat list_vhosts
#启动RabbitMQ服务:
net stop RabbitMQ && net start RabbitMQ

以上这些,账号、vhost、权限、作用域等基本就设置完了。


介绍

RabbitMQ是一个消息代理:它接受和转发消息。你可以把它想象成一个邮局:当你把你想要发布的邮件放在邮箱里时,你可以确定邮差先生最终会把邮件发送给你的收件人。在这个比喻中,RabbitMQ是邮政信箱,邮局和邮递员。

先决条件
本教程假定RabbitMQ 在标准端口(5672)上的本地主机上安装并运行。如果您使用不同的主机,端口或凭证,则连接设置需要进行调整。

我们的第一个程序send.py会向队列发送一条消息。我们需要做的第一件事是与RabbitMQ服务器建立连接。

#!/usr/bin/env python
import pika

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

我们现在连接到本地计算机上的代理 - 因此是 本地主机。如果我们想连接到另一台机器上的代理,我们只需在此指定其名称或IP地址。

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

channel.queue_declare(queue = 'hello')

此时我们准备发送消息。我们的第一条消息将只包含一个字符串Hello World!我们想把它发送给我们的 hello队列。

在RabbitMQ中,消息永远不会直接发送到队列,它总是需要经过交换。但是让我们不要被细节拖累 - 你可以在本教程的第三部分阅读更多关于交换的内容。我们现在需要知道的是如何使用由空字符串标识的默认交换。这种交换是特殊的 - 它允许我们准确地指定消息应该到达哪个队列。队列名称需要在routing_key参数中指定:

channel.basic_publish(exchange='',
                      routing_key='hello',
                      body='Hello World!')
print(" [x] Sent 'Hello World!'")

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

connection.close()

如果这是您第一次使用RabbitMQ,并且您没有看到“已发送”消息,那么您可能会抓住您的头脑,想知道会出现什么问题。也许代理启动时没有足够的可用磁盘空间(默认情况下它至少需要200 MB空闲空间),因此拒绝接受消息。检查代理日志文件以确认并在必要时减少限制。该配置文件文档会告诉你如何设置disk_free_limit。

我们的第二个程序receive.py将接收队列中的消息并将它们打印在屏幕上。

再次,我们首先需要连接到RabbitMQ服务器。负责连接到Rabbit的代码与以前相同。

下一步,就像以前一样,要确保队列存在。使用queue_declare创建一个队列是幂等的 - 我们可以根据需要多次运行该命令,并且只会创建一个。

channel.queue_declare(queue = 'hello')

您可能会问为什么我们再次声明队列 - 我们已经在之前的代码中声明了它。如果我们确信队列已经存在,我们可以避免这种情况。例如,如果send.py程序之前运行过。但我们还不确定首先运行哪个程序。在这种情况下,重复在两个程序中重复声明队列是一种很好的做法。

列出队列

您可能希望看到RabbitMQ有什么队列以及它们中有多少条消息。您可以使用rabbitmqctl工具(作为特权用户)执行此操作:

sudo rabbitmqctl list_queues

==在Windows上,省略sudo ==

从队列接收消息更为复杂。它通过向队列订阅回调函数来工作。每当我们收到一条消息,这个回调函数就被皮卡库调用。在我们的例子中,这个函数会在屏幕上打印消息的内容。

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

接下来,我们需要告诉RabbitMQ这个特定的回调函数应该从我们的hello队列接收消息:

channel.basic_consume(callback,
                      queue='hello',
                      no_ack=True)

为了让这个命令成功,我们必须确保我们想要订阅的队列存在。幸运的是,我们对此有信心 - 我们已经使用queue_declare创建了一个队列。

该NO_ACK参数将被描述以后。

最后,我们进入一个永无止境的循环,等待数据并在必要时运行回调。

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

将他们合并起来

# send.py

#!/usr/bin/env python
import pika
# 建立一个实例
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))  # # 默认端口5672,可不写
# 声明一个管道,在管道里发消息
channel = connection.channel()

# 在管道里声明queue
channel.queue_declare(queue='hello')

channel.basic_publish(exchange='',
                      routing_key='hello',  # queue名字
                      body='Hello World!')  # 消息内容
print(" [x] Sent 'Hello World!'")
connection.close() # 队列关闭
# receive.py
#!/usr/bin/env python
import pika
# 建立实例
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
# 声明管道
channel = connection.channel()

# 为什么又声明了一个‘hello’队列?
# 如果确定已经声明了,可以不声明。但是你不知道那个机器先运行,所以要声明两次。
channel.queue_declare(queue='hello')

def callback(ch, method, properties, body):  #  四个参数为标准格式
    # 管道内存对象  内容相关信息  后面讲
    print(" [x] Received %r" % body)


channel.basic_consume( # 消费信息
                        callback, # 如果收到消息,就调用callback函数来处理消息
                      queue='hello',  # 你要从那个队列里收消息
                      no_ack=True)  # 写的话,如果接收消息,机器宕机消息就丢了
                      # 一般不写。宕机则生产者检测到发给其他消费者

print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()  # 开始消费消息

在第一篇教程中,我们编写了用于从命名队列发送和接收消息的程序。在这一个中,我们将创建一个工作队列,用于在多个工作人员之间分配耗时的任务。

工作队列(又名:任务队列)背后的主要思想是避免立即执行资源密集型任务,并且必须等待它完成。相反,我们安排稍后完成任务。我们将任务封装 为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当你运行许多工人时,任务将在他们之间共享。

这个概念在Web应用程序中特别有用,因为在短的HTTP请求窗口中无法处理复杂的任务。

在本教程的前一部分中,我们发送了一条包含“Hello World!”的消息。现在我们将发送代表复杂任务的字符串。我们没有真实世界的任务,比如要调整大小的图像或要渲染的PDF文件,所以让我们假装我们很忙 - 使用time.sleep()函数来伪装它。我们将把字符串中的点数作为它的复杂度; 每一个点都会占用一秒的“工作”。例如,Hello …描述的假任务 将需要三秒钟。

我们稍微修改前面例子中的send.py代码,以允许从命令行发送任意消息。这个程序将把任务安排到我们的工作队列中,所以让我们把它命名为 new_task.py:

import sys

message = ' '.join(sys.argv[1:]) or "Hello World!"
channel.basic_publish(exchange='',
                      routing_key='hello',
                      body=message)
print(" [x] Sent %r" % message)

我们的旧版receive.py脚本也需要进行一些更改:它需要为邮件正文中的每个点伪造第二个工作。它会弹出队列中的消息并执行任务,所以我们称之为worker.py

import time

def callback(ch, method, properties, body):
    print(" [x] Received %r" % body)
    time.sleep(body.count(b'.'))
    print(" [x] Done")

循环调度
使用任务队列的优点之一是可以轻松地平行工作。如果我们正在积累积压的工作,我们可以增加更多的工作人员并以这种方式轻松扩展。

首先,我们试着同时运行两个worker.py脚本。他们都会从队列中获取消息,但具体到底是什么?让我们来看看。

您需要打开三个控制台。两个将运行worker.py 脚本。这些控制台将成为我们的两个消费者 - C1和C2。

# shell 1
python worker.py
# => [*] Waiting for messages. To exit press CTRL+C
# shell 2
python worker.py
# => [*] Waiting for messages. To exit press CTRL+C

在第三个我们将发布新的任务。一旦你开始了消费者,你可以发布几条消息:

# shell 3
python new_task.py First message.
python new_task.py Second message..
python new_task.py Third message...
python new_task.py Fourth message....
python new_task.py Fifth message.....

让我们看看交付给我们工人的东西:

# shell 1
python worker.py
# => [*] Waiting for messages. To exit press CTRL+C
# => [x] Received 'First message.'
# => [x] Received 'Third message...'
# => [x] Received 'Fifth message.....'
# shell 2
python worker.py
# => [*] Waiting for messages. To exit press CTRL+C
# => [x] Received 'Second message..'
# => [x] Received 'Fourth message....'

默认情况下,RabbitMQ将按顺序将每条消息发送给下一个使用者。平均而言,每个消费者将获得相同数量的消息。这种分配消息的方式称为循环法。尝试与三名或更多的工人。

消息确认

做任务可能需要几秒钟的时间。你可能想知道如果其中一个消费者开始一项长期任务并且只是部分完成而死亡会发生什么。用我们目前的代码,一旦RabbitMQ将消息传递给客户,它立即将其标记为删除。在这种情况下,如果你杀了一个工人,我们将失去刚刚处理的信息。我们也会失去所有派发给这个特定工作人员但尚未处理的消息。

但我们不想失去任何任务。如果一名工人死亡,我们希望将任务交付给另一名工人。

为了确保消息永不丢失,RabbitMQ支持 消息确认。消费者发回ack(请求)告诉RabbitMQ已经收到,处理了特定的消息,并且RabbitMQ可以自由删除它。

如果消费者模具(其信道被关闭时,关闭连接,或TCP连接丢失),而不发送ACK,RabbitMQ的将理解,没有被完全处理的消息,并且将重新排队它。如果有其他消费者同时在线,它会迅速将其重新发送给另一位消费者。这样,即使工作人员偶尔死亡,也可以确保没有任何信息丢失。

没有任何消息超时; > 当消费者死亡时,RabbitMQ将重新传递消息。即使处理消息需要非常很长的时间也没关系。

手动消息确认默认打开。在前面的例子中,我们通过no_ack = True 标志明确地将它们关闭。一旦我们完成了一项任务,现在是时候清除这个标志并且发送工人的正确确认。

def callback(ch, method, properties, body):
    print " [x] Received %r" % (body,)
    time.sleep( body.count('.') )
    print " [x] Done"
    ch.basic_ack(delivery_tag = method.delivery_tag)

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

使用这段代码,我们可以确定,即使在处理消息时使用CTRL + C来杀死一个工作者,也不会丢失任何东西。工人死后不久,所有未确认的消息将被重新发送。

忘记确认

错过basic_ack是一个常见的错误。这是一个很容易的错误,但后果是严重的。当你的客户退出时(这可能看起来像随机的重新传送),消息将被重新传递,但是RabbitMQ将会消耗越来越多的内存,因为它将不能释放任何未消息的消息。

为了调试这种错误,您可以使用rabbitmqctl 来打印messages_unacknowledged字段:

sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged

在Windows上,删除sudo

消息持久性

我们已经学会了如何确保即使消费者死亡,任务也不会丢失。但是如果RabbitMQ服务器停止,我们的任务仍然会丢失。

当RabbitMQ退出或崩溃时,它会忘记队列和消息,除非您告诉它不要。需要做两件事来确保消息不会丢失:我们需要将队列和消息标记为持久。

首先,我们需要确保RabbitMQ永远不会失去我们的队列。为了做到这一点,我们需要宣布它是持久的:

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

虽然这个命令本身是正确的,但它在我们的设置中不起作用。那是因为我们已经定义了一个名为hello的队列 ,这个队列并不耐用。RabbitMQ不允许您使用不同的参数重新定义现有的队列,并会向任何试图执行该操作的程序返回错误。但是有一个快速的解决方法 - 让我们声明一个具有不同名称的队列,例如task_queue:

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

此queue_declare更改需要应用于生产者和消费者代码。

此时我们确信,即使RabbitMQ重新启动,task_queue队列也不会丢失。现在我们需要将消息标记为持久 - 通过提供值为2的delivery_mode属性。

channel.basic_publish(exchange='',
                      routing_key="task_queue",
                      body=message,
                      properties=pika.BasicProperties(
                         delivery_mode = 2, # make message persistent
                      ))

关于消息持久性的说明
将邮件标记为永久邮件并不能完全保证邮件不会丢失。尽管它告诉RabbitMQ将消息保存到磁盘,但RabbitMQ接收到消息并且尚未保存消息时仍有一段时间窗口。此外,RabbitMQ不会为每条消息执行fsync(2) - 它可能只是保存到缓存中,并没有真正写入磁盘。持久性保证不强,但对我们简单的任务队列来说已经足够了。如果您需要更强大的保证,那么您可以使用 发布商确认。

公平派遣

您可能已经注意到调度仍然无法完全按照我们的要求工作。例如,在有两名工人的情况下,当所有奇怪的信息都很重,甚至信息很少时,一名工作人员会一直很忙,另一名工作人员几乎不会做任何工作。那么,RabbitMQ不知道任何有关这一点,并仍将均匀地发送消息。

发生这种情况是因为RabbitMQ只在消息进入队列时调度消息。它没有考虑消费者未确认消息的数量。它只是盲目地将第n条消息分发给第n位消费者。

为了解决这个问题,我们可以使用basic.qos方法和 prefetch_count = 1设置。这告诉RabbitMQ一次不要向工作人员发送多个消息。或者换句话说,不要向工作人员发送新消息,直到它处理并确认了前一个消息。相反,它会将其分派给不是仍然忙碌的下一个工作人员。

channel.basic_qos(prefetch_count=1)

有关队列大小的说明
如果所有的工作人员都很忙,你的队伍可以填满。您将需要密切关注,也许需要添加更多的工作人员,或使用消息TTL。

# new_task.py

#!/usr/bin/env python
import pika
import sys

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

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

message = ' '.join(sys.argv[1:]) or "Hello World!"
channel.basic_publish(exchange='',
                      routing_key='task_queue',
                      body=message,
                      properties=pika.BasicProperties(
                         delivery_mode = 2, # 消息持久化 persistent
                      ))
print(" [x] Sent %r" % message)
connection.close()
# worker.py

#!/usr/bin/env python
import pika
import time

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

# 每次声明队列的时候,都加上durable,队列持久化
channel.queue_declare(queue='task_queue', durable=True)
print(' [*] Waiting for messages. To exit press CTRL+C')

def callback(ch, method, properties, body):
    print(" [x] Received %r" % body)
    time.sleep(body.count(b'.'))
    print(" [x] Done")
    ch.basic_ack(delivery_tag = method.delivery_tag) # 告诉生成者,消息处理完成

channel.basic_qos(prefetch_count=1)  # 类似权重,按能力分发,如果有一个消息,就不在给你发
channel.basic_consume(callback,
                      queue='task_queue')

channel.start_consuming()

为了说明这种模式,我们将建立一个简单的日志系统。它将包含两个程序 - 第一个将发射日志消息,第二个将接收并打印它们。

在我们的日志系统中,接收程序的每个运行副本都会收到消息。这样我们就可以运行一个接收器并将日志指向磁盘; 同时我们将能够运行另一个接收器并在屏幕上查看日志。

基本上,发布的日志消息将被广播给所有的接收者。

交换机有4种类型

  1. direct

    直连交换机是一种带路由功能的交换机,一个队列会和一个交换机绑定,除此之外再绑定一个routing_key,当消息被发送的时候,需要指定一个binding_key,这个消息被送达交换机的时候,就会被这个交换机送到指定的队列里面去。同样的一个binding_key也是支持应用到多个队列中的。

  2. topic

    直连交换机的routing_key方案非常简单,如果我们希望一条消息发送给多个队列,那么这个交换机需要绑定上非常多的routing_key,假设每个交换机上都绑定一堆的routing_key连接到各个队列上。那么消息的管理就会异常地困难。

    所以RabbitMQ提供了一种主题交换机,发送到主题交换机上的消息需要携带指定规则的routing_key,主题交换机会根据这个规则将数据发送到对应的(多个)队列上。

    主题交换机的routing_key需要有一定的规则,交换机和队列的binding_key需要采用*.#.*…的格式,每个部分用.分开,其中:

     *表示一个单词
     #表示任意数量(零个或多个)单词。
    
  3. headers

    首部交换机是忽略routing_key的一种路由方式。路由器和交换机路由的规则是通过Headers信息来交换的,这个有点像HTTP的Headers。将一个交换机声明成首部交换机,绑定一个队列的时候,定义一个Hash的数据结构,消息发送的时候,会携带一组hash数据结构的信息,当Hash的内容匹配上的时候,消息就会被写入队列。

    绑定交换机和队列的时候,Hash结构中要求携带一个键“x-match”,这个键的Value可以是any或者all,这代表消息携带的Hash是需要全部匹配(all),还是仅匹配一个键(any)就可以了。相比直连交换机,首部交换机的优势是匹配的规则不被限定为字符串(string)。

  4. fanout

    扇形交换机是最基本的交换机类型,它所能做的事情非常简单———广播消息。扇形交换机会把能接收到的消息全部发送给绑定在自己身上的队列。因为广播不需要“思考”,所以扇形交换机处理消息的速度也是所有的交换机类型里面最快的。

匹配规则x-match有下列两种类型:

x-match = all :表示所有的键值对都匹配才能接受到消息

x-match = any :表示只要有键值对匹配就能接受到消息

channel.exchange_declare(exchange='logs',
                         exchange_type='fanout')

列出所有交换

sudo rabbitmqctl list_exchanges

指定交换方式

channel.basic_publish(exchange = 'logs',
                      routing_key = '',
                      体=消息)

直接创建一个空队列

result = channel.queue_declare()

此时,result.method.queue包含一个随机队列名称。例如,它可能看起来像amq.gen-JzTY20BRgKO-HjmUJj0wLg。

其次,一旦消费者连接关闭,队列应该被删除。这是一个专有标志:

result = channel.queue_declare(exclusive = True)

有关队列的一些说明

http://www.rabbitmq.com/queues.html

交换与队列必须是绑定关系

channel.queue_bind(exchange='logs', queue=result.method.queue)

从现在起,日志交换会将消息附加到我们的队列中。

列出绑定

rabbitmqctl list_bindings

发出日志消息的生产者程序与之前的教程没有多大区别。最重要的变化是我们现在想发布消息到我们的日志交换,而不是无名的消息。发送时我们需要提供一个routing_key,但是对于扇出交换,它的值将被忽略。这里是emit_log.py脚本的代码 :

#!/ usr / bin / env python 
import pika
 import sys

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

channel.exchange_declare(exchange = 'logs',
                         exchange_type = 'fanout')

message = ''. join(sys.argv [ 1:])或 “info:Hello World!” 
channel.basic_publish(exchange = 'logs',
                      routing_key = '',
                      体=消息)
打印(“[x]发送%r”%消息)
connection.close()时

如果没有队列绑定到交换机上,这些消息将会丢失,但这对我们来说没问题; 如果没有消费者正在收听,我们可以放心地丢弃消息。
receive_logs.py的代码:

#!/ usr / bin / env python 
import pika

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

channel.exchange_declare(exchange = 'logs',
                         exchange_type = 'fanout')

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

channel.queue_bind(exchange = 'logs',
                   队列= queue_name)

print('[*]正在等待日志。要退出,请按CTRL + C')

def  callback (ch,method,properties,body): 
    print( “[x]%r”%body)

channel.basic_consume(回调,
                      队列= queue_name,
                      no_ack = True)

channel.start_consuming()

如果您想将日志保存到文件中,只需打开一个控制台并输入:

python receive_logs.py > logs_from_rabbit.log

如果你想在屏幕上看到日志,产生一个新的终端并运行:

python receive_logs.py

当然,要发射日志类型:

python emit_log.py

使用rabbitmqctl list_bindings,你可以验证代码是否真正创建了绑定和队列。有两个receive_logs.py 程序在运行,你应该看到如下所示:

sudo rabbitmqctl list_bindings
# => Listing bindings ...
# => logs    exchange        amq.gen-JzTY20BRgKO-HjmUJj0wLg  queue           []
# => logs    exchange        amq.gen-vso0PVvyiRIL2WoV3i48Yg  queue           []
# => ...done.

结果的解释很简单:来自交换日志的数据转到两个带有服务器分配名称的队列。这正是我们的意图。


在本教程中,我们将添加一个功能 - 我们将只能订阅一部分消息。例如,我们只能将重要的错误消息引导到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。

绑定

在前面的例子中,我们已经创建了绑定。您可能会回想一下代码:

channel.queue_bind(exchange=exchange_name,
                   queue=queue_name)

绑定是交换和队列之间的关系。这可以简单地理解为:队列对来自该交换机的消息感兴趣。

绑定可以使用额外的routing_key参数。为了避免与basic_publish参数混淆,我们将其称为 绑定键。这就是我们如何使用一个键创建一个绑定:

channel.queue_bind(exchange=exchange_name,
                   queue=queue_name,
                   routing_key='black')

绑定键的含义取决于交换类型。我们之前使用的 fanout交易所简单地忽略了它的价值。

直接交换

我们之前教程的日志记录系统将所有消息广播给所有消费者。我们希望将其扩展为允许根据其严重性过滤消息。例如,我们可能希望将日志消息写入磁盘的脚本仅接收严重错误,而不会在警告或信息日志消息中浪费磁盘空间。

我们正在使用fanout交换机,这不会给我们太多的灵活性 - 它只能无意识地播放。

我们将使用direct交换。direct交换背后的路由算法很简单 - 消息进入队列,其 绑定密钥与消息的路由密钥完全匹配。

发射日志

我们将使用这个模型用于我们的日志系统。取而代之的扇出,我们将消息发送到直接交流。我们将提供日志严重性作为路由键。这样接收脚本将能够选择想要接收的严重性。我们先关注发射日志。

像往常一样,我们需要首先创建一个交换:

channel.exchange_declare(exchange='direct_logs',
                         exchange_type='direct')

我们准备发送一条消息:

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

为了简化事情,我们将假定“严重性”可以是’信息’,‘警告’,'错误’之一。

订阅

接收邮件的方式与上一个教程中的一样,只有一个例外 - 我们将为每个我们感兴趣的严重程度创建一个新绑定。

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

for severity in severities:
    channel.queue_bind(exchange='direct_logs',
                       queue=queue_name,
                       routing_key=severity)

代码合并

# emit_log_direct.py:

#!/usr/bin/env python
import pika
import sys

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

channel.exchange_declare(exchange='direct_logs',
                         exchange_type='direct')

severity = sys.argv[1] if len(sys.argv) > 2 else 'info'
message = ' '.join(sys.argv[2:]) or 'Hello World!'
channel.basic_publish(exchange='direct_logs',
                      routing_key=severity,
                      body=message)
print(" [x] Sent %r:%r" % (severity, message))
connection.close()
# receive_logs_direct.py:

#!/usr/bin/env python
import pika
import sys

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

channel.exchange_declare(exchange='direct_logs',
                         exchange_type='direct')

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

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_logs',
                       queue=queue_name,
                       routing_key=severity)

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=queue_name,
                      no_ack=True)

channel.start_consuming()

如果只想保存’警告’和’错误’(而不是’信息’)将消息记录到文件中,只需打开一个控制台并输入:

python receive_logs_direct.py warning error > logs_from_rabbit.log

如果您希望在屏幕上看到所有日志消息,请打开一个新终端并执行以下操作:

python receive_logs_direct.py info warning error
# => [*] Waiting for logs. To exit press CTRL+C

例如,要输出错误日志消息,只需输入:

python emit_log_direct.py error "Run. Run. Or it will explode."
# => [x] Sent 'error':'Run. Run. Or it will explode.'

在我们的日志系统中,我们可能不仅需要根据严重性来订阅日志,还要根据发布日志的来源进行订阅。您可能从syslog unix工具知道这个概念,该 工具根据严重性(信息/警告/暴击…)和工具(auth / cron / kern …)来路由日志。

这会给我们很大的灵活性 - 我们可能想听取来自’cron’的严重错误,而且还听取来自’kern’的所有日志。

为了在我们的日志系统中实现这一点,我们需要了解更复杂的topic交换。

topic交换

发送到话题交换的消息不能有任意的 routing_key - 它必须是由点分隔的单词列表。单词可以是任何东西,但通常它们指定了与该消息相关的一些功能。一些有效的路由键例子:“ stock.usd.nyse ”,“ nyse.vmw ”,“ quick.orange.rabbit ”。只要您愿意,路由键中可以有多少个字,最多255个字节。

绑定键也必须是相同的形式。话题交换背后的逻辑 类似于直接话题 - 使用特定路由键发送的消息将被传递到与匹配绑定键绑定的所有队列。但是绑定键有两个重要的特殊情况:

*(星号)可以代替一个字。
#(散列)可以替代零个或多个单词。

在这个例子中,我们将发送所有描述动物的消息。消息将使用由三个字(两个点)组成的路由键发送。路由关键字中的第一个单词将描述速度,第二个颜色和第三个物种:

 "<celerity>.<colour>.<species>".

我们创建了三个绑定:Q1绑定了绑定键 “.orange.” Q2 绑定了"..rabbit" and “lazy.#”.
这些绑定可以概括为:

Q1对所有的橙色动物都感兴趣。
Q2希望听到关于兔子的一切,以及关于懒惰动物的一切。

将路由键设置为“ quick.orange.rabbit ”的消息将传递到两个队列。消息“ lazy.orange.elephant ”也会去他们两个。另一方面,“ quick.orange.fox ”只会进入第一个队列,而“ lazy.brown.fox ”只会进入第二个队列。“ lazy.pink.rabbit ”只会传递到第二个队列一次,即使它匹配了两个绑定。“ quick.brown.fox ”不匹配任何绑定,因此将被丢弃。

如果我们违反我们的合同并发送带有一个或四个单词的消息,如“ orange ”或“ quick.orange.male.rabbit ” ,会发生什么情况?那么,这些消息将不匹配任何绑定,并会丢失。

另一方面,“ lazy.orange.male.rabbit”即使有四个单词,也会匹配最后一个绑定,并将传递到第二个队列。

Topic交换
Topic交换功能强大,可以像其他交流一样行事。

当使用“ # ”(散列)绑定键绑定队列时,它将接收所有消息,而不管路由密钥如何 - 就像在扇出交换中一样。

当在绑定中没有使用特殊字符“ * ”(星号)和“ # ”(散列)时,主题交换将像直接交换一样。

把它放在一起
我们将在我们的日志系统中使用Topic交换。我们首先假定日志的路由键有两个单词:“ . ”。

# emit_log_topic.py
# 发送几乎与之前一样
#!/usr/bin/env python
import pika
import sys

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

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

routing_key = sys.argv[1] if len(sys.argv) > 2 else 'anonymous.info'
message = ' '.join(sys.argv[2:]) or 'Hello World!'
channel.basic_publish(exchange='topic_logs',
                      routing_key=routing_key,
                      body=message)
print(" [x] Sent %r:%r" % (routing_key, message))
connection.close()
# receive_logs_topic.py:

#!/usr/bin/env python
import pika
import sys

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

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

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

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_logs',
                       queue=queue_name,
                       routing_key=binding_key)

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=queue_name,
                      no_ack=True)

channel.start_consuming()
  • 要接收所有日志运行:
    python receive_logs_topic.py “#”

  • 要从设施“ kern ” 接收所有日志:
    python receive_logs_topic.py “kern.*”

  • 或者,如果您只想听到关于“ 关键 ”日志的信息:
    python receive_logs_topic.py “*.critical”

  • 您可以创建多个绑定:
    python receive_logs_topic.py “kern." ".critical”

  • 并发布带有路由键“ kern.critical ”类型的日志:
    python emit_log_topic.py “kern.critical” “A critical kernel error”

请注意,代码没有对路由或绑定键作任何假设,您可能需要使用两个以上的路由键参数。


在本教程中,我们将使用RabbitMQ构建一个RPC系统:一个客户端和一个可扩展的RPC服务器。由于我们没有任何值得分发的耗时任务,我们将创建一个返回斐波那契数字的虚拟RPC服务。

客户端界面

为了说明如何使用RPC服务,我们将创建一个简单的客户端类。它将公开一个名为call的方法 ,它发送一个RPC请求并阻塞,直到收到答案:

fibonacci_rpc = FibonacciRpcClient()
result = fibonacci_rpc.call(4)
print("fib(4) is %r" % result)
有关RPC的说明
虽然RPC是计算中很常见的模式,但它经常受到批评。当程序员不知道函数调用是本地的还是慢速的RPC时会出现这些问题。像这样的混乱导致不可预知的系统,并增加了调试的不必要的复杂性。而不是简化软件,滥用RPC会导致不可维护的意大利面代码。

铭记这一点,请考虑以下建议:

确保显而易见哪个函数调用是本地的,哪个是远程的。
记录您的系统。清楚组件之间的依赖关系。
处理错误情况。当RPC服务器长时间关闭时,客户端应该如何反应?
有疑问时避免RPC。如果可以的话,你应该使用异步管道 - 而不是类似于RPC的阻塞,结果被异步推送到下一个计算阶段。
回调队列

一般来说,通过RabbitMQ来执行RPC是很容易的。客户端发送请求消息,服务器回复响应消息。为了接收响应,客户端需要发送一个“回叫”队列地址和请求。让我们试试看:

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

channel.basic_publish(exchange='',
                      routing_key='rpc_queue',
                      properties=pika.BasicProperties(
                            reply_to = callback_queue,
                            ),
                      body=request)

# ... and some code to read a response message from the callback_queue ...
消息属性
AMQP 0-9-1协议预定义了一组包含14个属性的消息。大多数属性很少使用,但以下情况除外:

delivery_mode:将消息标记为持久(值为2)或瞬态(任何其他值)。你可能会记得第二篇教程中的这个属性。
content_type:用于描述编码的MIME类型。例如,对于经常使用的JSON编码,将此属性设置为application / json是一种很好的做法。
reply_to:通常用于命名回调队列。
correlation_id:用于将RPC响应与请求关联起来。
相关ID

在上面介绍的方法中,我们建议为每个RPC请求创建一个回调队列。这是非常低效的,但幸运的是有一个更好的方法 - 让我们为每个客户端创建一个回调队列。

这引发了一个新问题,在该队列中收到回复后,不清楚回复属于哪个请求。那是什么时候使用 correlation_id属性。我们将把它设置为每个请求的唯一值。稍后,当我们在回调队列中收到消息时,我们将查看此属性,并基于此属性,我们将能够将响应与请求进行匹配。如果我们看到未知的 correlation_id值,我们可以放心地丢弃该消息 - 它不属于我们的请求。

您可能会问,为什么我们应该忽略回调队列中的未知消息,而不是因为错误而失败?这是由于服务器端可能出现竞争状况。尽管不太可能,但在发送给我们答案之后,但在发送请求的确认消息之前,RPC服务器可能会死亡。如果发生这种情况,重新启动的RPC服务器将再次处理该请求。这就是为什么在客户端,我们必须优雅地处理重复的响应,理想情况下RPC应该是幂等的。

我们的RPC会像这样工作:

  • 当客户端启动时,它创建一个匿名独占回叫队列。
  • 对于RPC请求,客户端将发送具有两个属性的消息:reply_to,该消息 设置为回调队列和correlation_id,该值设置为每个请求的唯一值。
  • 该请求被发送到rpc_queue队列。
  • RPC worker(又名:服务器)正在等待该队列上的请求。当出现请求时,它执行该作业,并使用reply_to字段中的队列将结果发送回客户端。
  • 客户端在回调队列中等待数据。当出现消息时,它会检查correlation_id属性。如果它匹配来自请求的值,则返回对应用程序的响应。

把它放在一起

# rpc_server.py

#!/usr/bin/env python
import pika

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

channel = connection.channel()

channel.queue_declare(queue='rpc_queue')

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

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

    print(" [.] fib(%s)" % n)
    response = fib(n)

    ch.basic_publish(exchange='',  # 把执行结果发回给客户端
                     routing_key=props.reply_to,   # 客户端要求返回想用的queue
                    # 返回客户端发过来的correction_id 为了让客户端验证消息一致性 
                    properties=pika.BasicProperties(correlation_id = \
                                                         props.correlation_id),
                     body=str(response))
    ch.basic_ack(delivery_tag = method.delivery_tag)

channel.basic_qos(prefetch_count=1)
channel.basic_consume(on_request, queue='rpc_queue')

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

服务器代码非常简单:

(4)像往常一样,我们首先建立连接并声明队列。
(11)我们声明我们的斐波那契函数。它只假定有效的正整数输入。(不要指望这个版本适用于大数字,它可能是最慢的递归实现)。
(19)我们声明了basic_consume的回调,它是RPC服务器的核心。它在收到请求时执行。它完成工作并将响应发回。
(32)我们可能想运行多个服务器进程。为了在多台服务器上平均分配负载,我们需要设置 prefetch_count设置。
# rpc_client.py

#!/usr/bin/env python
import pika
import uuid

class FibonacciRpcClient(object):
    def __init__(self):
        self.connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))

        self.channel = self.connection.channel()

        result = self.channel.queue_declare(exclusive=True)
        self.callback_queue = result.method.queue

        self.channel.basic_consume(self.on_response, no_ack=True,
                                   queue=self.callback_queue)

    def on_response(self, ch, method, props, body):
        # 如果收到的ID和本机生成的相同,则返回的结果就是我想要的指令返回的结果
        if self.corr_id == props.correlation_id:
            self.response = body

    def call(self, n):
        self.response = None
        self.corr_id = str(uuid.uuid4())
        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:
            # 非阻塞版的start_consuming()
            self.connection.process_data_events()
        return int(self.response)

fibonacci_rpc = FibonacciRpcClient()

print(" [x] Requesting fib(30)")
response = fibonacci_rpc.call(30)
print(" [.] Got %r" % response)

客户端代码稍有涉及:

(7)我们建立连接,通道并为回复声明独占的“回叫”队列。
(16)我们订阅'回调'队列,以便我们可以接收RPC响应。
(18)对每个响应执行的'on_response'回调函数做了一个非常简单的工作,对于每个响应消息它检查correlation_id是否是我们正在寻找的。如果是这样,它将保存self.response中的响应并打破消费循环。
(23)接下来,我们定义我们的主要调用方法 - 它执行实际的RPC请求。
(24)在这个方法中,首先我们生成一个唯一的correlation_id 数并保存 - 'on_response'回调函数将使用这个值来捕获适当的响应。
(25)接下来,我们发布具有两个属性的请求消息:  reply_to和correlation_id。
(32)在这一点上,我们可以坐下来等待,直到适当的回应到达。
(33)最后,我们将回复返回给用户。

我们的RPC服务已经准备就绪。我们可以启动服务器:

python rpc_server.py
# => [x] Awaiting RPC requests

要申请斐波那契数字运行客户端:

python rpc_client.py
# => [x] Requesting fib(30)

所提出的设计不是RPC服务的唯一可能实现,但它具有一些重要的优点:

如果RPC服务器速度太慢,可以通过运行另一个来扩展。尝试在新的控制台中运行第二个rpc_server.py。
在客户端,RPC需要发送和接收一条消息。不需要像queue_declare这样的同步调用 。因此,RPC客户端仅需要一次网络往返即可获得单个RPC请求。
我们的代码仍然非常简单,不会尝试解决更复杂(但重要)的问题,如:

如果没有服务器在运行,客户应该如何应对?
客户端是否应该对RPC有某种超时?
如果服务器发生故障并引发异常,是否应将其转发给客户端?
在处理之前防止无效的传入消息(例如检查边界)。

http://www.rabbitmq.com/tutorials/tutorial-six-python.html

wget http://mirrors.ustc.edu.cn/fedora/epel//6/x86_64/epel-release-6-8.noarch.rpm
rpm -ivh epel-release-6-8.noarch.rpm
yum install rabbitmq-server -y
yum install erlang
/usr/lib/rabbitmq/bin/rabbitmq-plugins list
/usr/lib/rabbitmq/bin/rabbitmq-plugins enable rabbitmq_management
whereis rabbitmq
cd /usr/lib/rabbitmq /etc/rabbitmq
rabbitmq-server start  
rabbitmqctl list_queues   //显示所有队列信息
rabbitmqctl add_vhost order//创建Vhost
rabbitmqctl delete_vhost vhostname //删除vhost
rabbitmqctl list_vhosts  //遍历所有虚拟主机信息
rabbitmqctl add_user username password 添加用户及密码
rabbitmqctl list_users 
rabbitmqctl set_permissions -p order rabbit ".*" ".*" ".*"  //绑定权限并且具备读写的权限
rabbitmqctl change_password username newpassword 修改用户密码
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python可以使用RabbitMQ作为面向消息的中间件来进行消息的生产和消费。我们可以通过连接RabbitMQ服务器并使用Python程序来发送消息和获取消息。可以使用封装了RabbitMQ类的代码来实现这些操作。这个类中封装了连接RabbitMQ服务器的方法、发送消息的方法和获取消息的方法。通过实例化这个类的对象并调用相关的方法,就可以实现消息的生产和消费操作。在正常情况下,消息队列中的消息会一直保存,但如果RabbitMQ挂掉,则消息队列中的消息会全部消失。可以通过使用rabbitmqctl命令的list_queues参数来查询RabbitMQ中的队列以及对应的消息个数。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [python程序使用RabbitMQ](https://blog.csdn.net/qq_60695343/article/details/126362800)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [pythonrabbitMQ使用](https://blog.csdn.net/jack_laoliu/article/details/98765173)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值