RabbitMQ 实践及使用

目录


- 1. RabbitMQ的安装
    - 1.1 配置好 epel
    - 1.2 安装 RPM包
    - 1.3 创建用户设置权限
- 2. RabbitMQ组件
- 3. RabbitMQ-Server 六种消息类型
    - 3.1 "Hello World"
        - 3.1.1 代码整合
    - 3.2 Work queues (工作队列模式)
        - 3.2.1 代码整合
        - 3.2.2 循环调度
        - 3.2.3 消息确认
        - 3.2.4 忘记确认
        - 3.2.5 消息持久化
        - 3.2.6 公平调度
        - 3.2.7 代码整合
    - 3.3 Publish/Subscribe(发布/订阅)
        - 3.3.1 交换机(Exchange)
        - 3.3.2 交换器列表
        - 3.3.3 匿名的交换器
        - 3.3.4 临时队列
        - 3.3.5 绑定(Bindings)
        - 3.3.6 代码整合
    - 3.4 Routing (路由)
        - 3.4.1 直连交换机(Direct exchange)
        - 3.4.2 多个绑定(Multiple bindings)
        - 3.4.3 代码整合
    - 3.5 Topics(主题交换机)
        - 3.5.1 主题交换机
        - 3.5.2 代码整合
    - 3.6 RPC (远程过程调试模式)
        - 3.6.1 回调队列
        - 3.6.2 消息属性
        - 3.6.3 关联标识
        - 3.6.4 整合代码

 

1. RabbitMQ的安装


 

1.1 配置好 epel

# For EL5: rpm -Uvh http://download.fedoraproject.org/pub/epel/5/i386/epel-release-5-4.noarch.rpm 
# For EL6: rpm -Uvh http://download.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm 
# For EL7: rpm -Uvh http://download.fedoraproject.org/pub/epel/7/x86_64/e/epel-release-7-8.noarch.rpm

 

 

1.2 安装 RPM包

[root@192.168.118.15 /opt]# ls
erlang-19.0.4-1.el6.x86_64.rpm  rabbitmq-server-3.6.13-1.el7.noarch.rpm
[root@192.168.118.15 /opt]# yum install *.rpm -y
[root@192.168.118.15 /opt]#systemctl start rabbitmq-server 
[root@192.168.118.15 /opt]#netstat -ntplu | egrep beam
tcp        0      0 0.0.0.0:5672            0.0.0.0:*               LISTEN      2130/beam.smp       
tcp        0      0 0.0.0.0:25672           0.0.0.0:*               LISTEN      2130/beam.smp

服务启动成功。

 

 

1.3 创建用户设置权限

[root@192.168.118.15 /opt]#rabbitmqctl add_user admin admin
Creating user "admin"

设置 admin 为管理员角色

[root@192.168.118.15 /opt]#rabbitmqctl set_user_tags admin administrator
Setting tags for user "admin" to [administrator]

设置权限

[root@192.168.118.15 /opt]#rabbitmqctl add_vhost /admin
Creating vhost "/admin"
[root@192.168.118.15 /opt]#rabbitmqctl set_permissions -p '/admin' admin '.*' '.*' '.*'
Setting permissions for user "admin" in vhost "/admin"

开启web管理后台
[root@192.168.118.15 /opt]#rabbitmq-plugins enable rabbitmq_management  
The following plugins have been enabled:
  amqp_client
  cowlib
  cowboy
  rabbitmq_web_dispatch
  rabbitmq_management_agent
  rabbitmq_management

Applying plugin configuration to rabbit@localhost... started 6 plugins.

后台监听在 15672 端口,开启成功
[root@192.168.118.15 /opt]#netstat -ntplu | egrep beam
tcp        0      0 0.0.0.0:5672            0.0.0.0:*               LISTEN      2130/beam.smp       
tcp        0      0 0.0.0.0:25672           0.0.0.0:*               LISTEN      2130/beam.smp       
tcp        0      0 0.0.0.0:15672           0.0.0.0:*               LISTEN      2130/beam.smp

 

 

通过web页面登录查看用户配置是否正确。


ok,用户配置完成。至此,RabbitMQ 安装完毕。

 

2. RabbitMQ组件

AMQP协议是一个高级抽象层消息通信协议,RabbitMQ是AMQP协议的实现。它主要包括以下组件:

        (1) Server(broker):接受客户端连接,实现AMQP消息队列和路由功能的进程
        (2) Virtual Host:其实是一个虚拟概念,类似于权限控制组,一个Virtual Host里面可以有若干个Exchange和Queue,但是权限控制的最小粒度是Virtual Host
        (3) Exchange:接受生产者发送的消息,并根据Binding规则将消息路由给服务器中的队列。ExchangeType决定了Exchange路由消息的行为,例如,在RabbitMQ中,ExchangeType有direct、Fanout和Topic三种,不同类型的Exchange路由的行为是不一样的。
        (4) Message Queue:消息队列,用于存储还未被消费者消费的消息。
        (5) Message: 由Header和Body组成,Header是由生产者添加的各种属性的集合,包括Message是否被持久化、由哪个Message Queue接受、优先级是多少等。而Body是真正需要传输的APP数据。
        (6) Binding:Binding联系了Exchange与Message Queue。Exchange在与多个Message Queue发生Binding后会生成一张路由表,路由表中存储着Message Queue所需消息的限制条件即Binding Key。当Exchange收到Message时会解析其Header得到Routing Key,Exchange根据Routing Key与Exchange Type将Message路由到Message Queue。Binding Key由Consumer在Binding Exchange与Message Queue时指定,而Routing Key由Producer发送Message时指定,两者的匹配方式由Exchange Type决定。
        (7) Connection:连接,对于RabbitMQ而言,其实就是一个位于客户端和Broker之间的TCP连接。
        (8) Channel:信道,仅仅创建了客户端到Broker之间的连接后,客户端还是不能发送消息的。需要为每一个Connection创建Channel,AMQP协议规定只有通过Channel才能执行AMQP的命令。一个Connection可以包含多个Channel。之所以需要Channel,是因为TCP连接的建立和释放都是十分昂贵的,如果一个客户端每一个线程都需要与Broker交互,如果每一个线程都建立一个TCP连接,暂且不考虑TCP连接是否浪费,就算操作系统也无法承受每秒建立如此多的TCP连接。RabbitMQ建议客户端线程之间不要共用Channel,至少要保证共用Channel的线程发送消息必须是串行的,但是建议尽量共用Connection。
        (9) Command:AMQP的命令,客户端通过Command完成与AMQP服务器的交互来实现自身的逻辑。例如在RabbitMQ中,客户端可以通过publish命令发送消息,txSelect开启一个事务,txCommit提交一个事务。

 

3. RabbitMQ-Server 六种消息类型

 

以上 6 种类型通过 python 连接并测试。

 

3.1 "Hello World"

功能:一个生产者 P 发送消息到队列Q,一个消费者 C 接收

 

 3.1.1 代码整合

import pika # 首先要安装 pika 模块
credentials = pika.PlainCredentials('admin', 'admin')   # 远程rabbitmq的用户名密码
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.118.15', 5672, '/admin', credentials))  # 连接 RabbitMQ信息
channel = connection.channel()  # 定义 channel 对象
channel.queue_declare(queue='hello')    # 声明一个队列 hello
channel.basic_publish(exchange='',
                      routing_key='hello',  # 路由键,写明将消息发往哪个队列,本例是将消息发往队列hello
                      body='Hello World!')  # 消息内容
print('[x]Sent "Hello World!"')
connection.close()  # 连接关闭
producer.py

多运行几次后,在RabbitMQ服务器上查看

[root@192.168.118.15 ~]#rabbitmqctl list_queues -p '/admin'
Listing queues
hello   5

后台管理查看:

 

接下来,创建一个 consumer 来消费队列中的这些消息。

 

import pika
import time

credentials = pika.PlainCredentials('admin', 'admin')  # 创建认证证书
connection = pika.BlockingConnection(
    pika.ConnectionParameters('192.168.118.15', 5672, '/admin', credentials))  # 连接 RabbitMQ信息
channel = connection.channel()  # 定义 channel 对象
channel.queue_declare(queue='hello')  # 声明一个队列 hello

def callback(ch, method, properties, body):  # 四个参数为标准格式
    print('>>>>>>> %s' % body)
    time.sleep(5)


channel.basic_consume(  # 消费消息
    callback,  # 如果收到消息,就调用callback函数来处理消息
    queue='hello',  # 你要从那个队列里收消息
    no_ack=True  # 收到消息立即返回ack确认信息
)

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

 

 运行结果:

 

3.2 Work queues (工作队列模式)

功能:一个生产者,多个消费者,每个消费者获取到的消息唯一,多个消费者只有一个队列。

 

 3.2.1 代码整合

import pika # 首先要安装 pika 模块
credentials = pika.PlainCredentials('admin', 'admin')   # 远程rabbitmq的用户名密码
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.118.15', 5672, '/admin', credentials))  # 连接 RabbitMQ信息
channel = connection.channel()  # 定义 channel 对象
channel.queue_declare(queue='hello')    # 声明一个队列 hello
channel.basic_publish(exchange='',
                      routing_key='hello',  # 路由键,写明将消息发往哪个队列,本例是将消息发往队列hello
                      body='Hello World!')  # 消息内容
print('[x]Sent "Hello World!"')
connection.close()  # 队列关闭
producer.py
import pika
import time

credentials = pika.PlainCredentials('admin', 'admin')   # 创建认证证书
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.118.15', 5672, '/admin', credentials))  # 连接 RabbitMQ信息
channel = connection.channel()  # 定义 channel 对象
channel.queue_declare(queue='hello')    # 声明一个队列 hello

def callback(ch, method, properties, body): # 四个参数为标准格式
    # print(ch)
    # print(method)
    # print(properties)
    print('>>>>>>> %s' % body)
    time.sleep(5)
    # ch.basic_ack(delivery_tag=method.delivery_tag)    # 告诉生成者,消息处理完成

channel.basic_consume(  # 消费消息
    callback,   # 如果收到消息,就调用callback函数来处理消息
    queue='hello'   # 你要从那个队列里收消息
    # no_ack = True
)

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

 

 运行8次producer.py 后,生成8条消息进入队列,再次开启2个consumer.py 进行消费,如图:

 

默认来说,RabbitMQ会按顺序得把消息发送给每个消费者,平均每个消费者都会收到同等数量得消息。这种发送消息得方式叫做——轮询(round-robin)。

 

3.2.2 循环调度

 通过上面的测试,我们发现每个消费者默认是平均分配所有消息,因此这种方式扩容起来很简单,如果队列中堆积了很多任务,我们只需要添加更多的消费者就可以了。

3.2.3 消息确认
当处理一个比较耗时得任务的时候,你也许想知道消费者(consumers)是否运行到一半就挂掉。当前的代码中,当消息被RabbitMQ发送给消费者(consumers)之后,马上就会在内存中移除。这种情况,你只要把一个工作者(worker)停止,正在处理的消息就会丢失。同时,所有发送到这个工作者的还没有处理的消息都会丢失。
我们不想丢失任何任务消息。如果一个工作者(worker)挂掉了,我们希望任务会重新发送给其他的工作者(worker)。
为了防止消息丢失,RabbitMQ提供了消息响应(acknowledgments)。消费者会通过一个ack(响应),告诉RabbitMQ已经收到并处理了某条消息,然后RabbitMQ就会释放并删除这条消息。
如果消费者(consumer)挂掉了,没有发送响应,RabbitMQ就会认为消息没有被完全处理,然后重新发送给其他消费者(consumer)。这样,即使工作者(workers)偶尔的挂掉,也不会丢失消息。
消息是没有超时这个概念的;当工作者与它断开连的时候,RabbitMQ会重新发送消息。这样在处理一个耗时非常长的消息任务的时候就不会出问题了。
消息响应默认是开启的。是时候移除这个标识了,当工作者(worker)完成了任务,就发送一个响应。

import pika
import time

credentials = pika.PlainCredentials('admin', 'admin')   # 创建认证证书
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.118.15', 5672, '/admin', credentials))  # 连接 RabbitMQ信息
channel = connection.channel()  # 定义 channel 对象
channel.queue_declare(queue='hello')    # 声明一个队列 hello

def callback(ch, method, properties, body): # 四个参数为标准格式
    # print(ch)
    # print(method)
    # print(properties)
    print('>>>>>>> %s' % body)
    time.sleep(1)
    ch.basic_ack(delivery_tag=method.delivery_tag)  # 当消息处理完毕后,告诉生成者,消息处理完成

channel.basic_consume(  # 消费消息
    callback,   # 如果收到消息,就调用callback函数来处理消息
    queue='hello'   # 你要从那个队列里收消息
)

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

 

3.2.4 忘记确认

 运行上面的代码,我们发现即使消费者中断,消息也不会消失。当工作者(worker)挂掉后,所有没有响应的消息都会被重新发送,如果它不能够释放没响应的消息,RabbitMQ就会占用越来越多的内存。

用了排除这种错误,可以使用 rabbitmqctl 命令,输出 messages_unacknowledged字段:

[root@192.168.118.15 ~]#rabbitmqctl list_queues -p '/admin' messages_ready messages_unacknowledged
Listing queues
0   20

综上,在编写消费者程序的时候建议使用第三种方式。

 

3.2.5 消息持久化

如果你没有特意告诉RabbitMQ,那么在它退出或者崩溃的时候,将会丢失所有队列和消息。为了确保信息不会丢失,有两个事情是需要注意的:我们必须把“队列”和“消息”设为持久化。

 首先,为了不让队列消失,需要把队列声明为持久化(durable):

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

尽管这行代码本身是正确的,但是仍然不会正确运行。因为我们已经定义过一个叫hello的非持久化队列。RabbitMq不允许你使用不同的参数重新定义一个队列,它会返回一个错误。但我们现在使用一个快捷的解决方法——用不同的名字,例如task_queue。

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

 

 这个queue_declare必须在生产者(producer)和消费者(consumer)对应的代码中修改。

import pika # 首先要安装 pika 模块
credentials = pika.PlainCredentials('admin', 'admin')   # 远程rabbitmq的用户名密码
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.118.15', 5672, '/admin', credentials))  # 连接 RabbitMQ信息
channel = connection.channel()  # 定义 channel 对象
channel.queue_declare(queue='task_queue', durable=True) # 声明一个队列 hello
for i in range(1, 21):
    channel.basic_publish(exchange='',
                          routing_key='hello',  # 路由键,写明将消息发往哪个队列,本例是将消息发往队列hello
                          body='%s' % i,
                          properties=pika.BasicProperties(
                              delivery_mode=2,  # 让消息持久化
                          ))    # 消息内容
    print('[x]Sent "Hello World!"')
connection.close()  # 队列关闭
producer.py

修改完成,运行 producer.py

import pika
import time

credentials = pika.PlainCredentials('admin', 'admin')   # 创建认证证书
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.118.15', 5672, '/admin', credentials))  # 连接 RabbitMQ信息
channel = connection.channel()  # 定义 channel 对象
channel.queue_declare(queue='task_queue', durable=True) # 声明一个队列 hello
def callback(ch, method, properties, body): # 四个参数为标准格式
    # print(ch)
    # print(method)
    # print(properties)
    print('>>>>>>> %s' % body)
    time.sleep(1)
    ch.basic_ack(delivery_tag=method.delivery_tag)  # 告诉生成者,消息处理完成

channel.basic_consume(  # 消费消息
    callback,   # 如果收到消息,就调用callback函数来处理消息
    queue='task_queue'  # 你要从那个队列里收消息
    # no_ack=True
)

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

使用 consumer.py 就可以消费消息。

 

持久化注意:

   将消息设为持久化并不能完全保证不会丢失。以上代码只是告诉了RabbitMq要把消息存到硬盘,但从RabbitMq收到消息到保存之间还是有一个很小的间隔时间。因为RabbitMq并不是所有的消息都使用fsync(2)——它有可能只是保存到缓存中,并不一定会写到硬盘中。并不能保证真正的持久化,但已经足够应付我们的简单工作队列。如果你一定要保证持久化,你需要改写你的代码来支持事务(transaction)。

 

3.2.6 公平调度

    通过之前的验证,我们知道每个消费者都是平均消费消息。比如有两个工作者(workers),处理奇数消息的比较繁忙,处理偶数消息的比较轻松。然而RabbitMQ并不知道这些,它仍然一如既往的派发消息。
    这是因为RabbitMQ只管分发进入队列的消息,不会关心有多少消费者(consumer)没有作出响应。它盲目的把第n-th条消息发给第n-th个消费者。

 

我们可以使用basic.qos方法,并设置prefetch_count=1。这样是告诉RabbitMQ,再同一时刻,不要发送超过1条消息给一个工作者(worker),直到它已经处理了上一条消息并且作出了响应。这样,RabbitMQ就会把消息分发给下一个空闲的工作者(worker)。

channel.basic_qos(prefetch_count=1)

 

3.2.7 代码整合

import pika # 首先要安装 pika 模块
credentials = pika.PlainCredentials('admin', 'admin')   # 远程rabbitmq的用户名密码
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.118.15', 5672, '/admin', credentials))  # 连接 RabbitMQ信息
channel = connection.channel()  # 定义 channel 对象
channel.queue_declare(queue='task_queue', durable=True) # 声明一个队列 hello
queue_list = ['1...............', '2....', '3.', '4.', '5.']
for i in queue_list:
    channel.basic_publish(exchange='',
                          routing_key='task_queue', # 路由键,写明将消息发往哪个队列,本例是将消息发往队列hello
                          body='%s' % i,
                          properties=pika.BasicProperties(
                              delivery_mode=2,  # 让消息持久化
                          ))    # 消息内容
    print('[x]Sent "Hello World!"')
connection.close()  # 队列关闭
producer.py
import pika
import time

credentials = pika.PlainCredentials('admin', 'admin')   # 创建认证证书
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.118.15', 5672, '/admin', credentials))  # 连接 RabbitMQ信息
channel = connection.channel()  # 定义 channel 对象
channel.queue_declare(queue='task_queue', durable=True) # 声明一个队列 hello

def callback(ch, method, properties, body): # 四个参数为标准格式
    # print(ch)
    # print(method)
    # print(properties)
    print('>>>>>>> %s' % body)
    # print(body.count(b'.'))
    time.sleep(body.count(b'.'))  # 通过 '.' 来代表消费消息时长
    ch.basic_ack(delivery_tag=method.delivery_tag)  # 告诉生成者,消息处理完成
channel.basic_qos(prefetch_count=1)
channel.basic_consume(  # 消费消息
    callback,   # 如果收到消息,就调用callback函数来处理消息
    queue='task_queue'  # 你要从那个队列里收消息
    # no_ack=True
)

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

通过运行以上代码,开启两个consumer.py 的时候,一个消费一个消息,另一个消费了4个消息,这样就达到了我们预期效果。

 

3.3 Publish/Subscribe(发布/订阅)

 说明:分发一个消息给多个消费者(consumer),这种模式被称为“发布/订阅”。

3.3.1 交换机(Exchange)

在前面的模式中,我们发送消息到队列并从队列中取出消息。现在是时候介绍RabbitMQ中完整的消息模型了。
简单的概括一下之前的模式:

  • 发布者(producer)是发布消息的应用程序
  • 队列(queue)用于消息存储的缓冲
  • 消费者(consumer)是接收消息的应用程序

   RabbitMQ消息模型的核心概念是:发布者(producer)不会直接发送任何消息给队列。事实上,发布者(producer)甚至不知道消息是否已经被投递到队列。
发布者(producer)只需要把消息发送给一个交换机(exchange)。交换机非常简单,它一边从发布者方接收消息,一边把消息推送到队列。交换机必须知道如何处理它接收到的消息,是应该推送到指定的队列还是是多个队列,或者是直接忽略消息。这些规则是通过交换机类型(exchange type)来定义的。

     

有几个可供选择的交换机类型:直连交换机(direct), 主题交换机(topic), (头交换机)headers和 扇型交换机(fanout)。我们在这里主要说明最后一个 —— 扇型交换机(fanout)。先创建一个fanout类型的交换机,命名为logs:

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

 

 扇型交换机(fanout)很简单,你可能从名字上就能猜测出来,它把消息发送给它所知道的所有队列。这正是我们的日志系统所需要的。

 

3.3.2 交换器列表

Rabbitmqctl能够列出服务器上所有的交换器:

[root@192.168.118.15 ~]#rabbitmqctl list_exchanges
Listing exchanges
amq.direct  direct
amq.fanout  fanout
amq.headers headers
amq.rabbitmq.log    topic
amq.match   headers
amq.topic   topic
    direct
amq.rabbitmq.trace  topic

这个列表中有一些叫做amq.*的交换器。这些都是默认创建的,不过这时候你还不需要使用他们。

 

3.3.3 匿名的交换器
前面的教程中我们对交换机一无所知,但仍然能够发送消息到队列中。因为我们使用了命名为空字符串(“”)默认的交换机。
回想我们之前是如何发布一则消息:

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

 


exchange参数就是交换机的名称。空字符串代表默认或者匿名交换机:消息将会根据指定的routing_key分发到指定的队列。

现在,我们就可以发送消息到一个具名交换机了:

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



3.3.4 临时队列

你还记得之前我们使用的队列名吗( hello和task_queue)?给一个队列命名是很重要的——我们需要把工作者(workers)指向正确的队列。如果你打算在发布者(producers)和消费者(consumers)之间共享同队列的话,给队列命名是十分重要的。
但是这并不适用于我们的日志系统。我们关心的是最新的消息而不是旧的。为了解决这个问题,我们需要做两件事情。
首先,当我们连接上RabbitMQ的时候,我们需要一个全新的、空的队列。我们可以手动创建一个随机的队列名,或者让服务器为我们选择一个随机的队列名(推荐)。我们只需要在调用queue_declare方法的时候,不提供queue参数就可以了:

result = channel.queue_declare()

 


这时候我们可以通过result.method.queue获得已经生成的随机队列名。
第二步,当与消费者(consumer)断开连接的时候,这个队列应当被立即删除。exclusive标识符即可达到此目的。

result = channel.queue_declare()

 


3.3.5 绑定(Bindings)




我们已经创建了一个扇型交换机(fanout)和一个队列。现在我们需要告诉交换机如何发送消息给我们的队列。交换器和队列之间的联系我们称之为绑定(binding)。

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

 


现在,logs交换机将会把消息添加到我们的队列中。

绑定(binding)列表
你可以使用 rabbitmqctl list_bindings 列出所有现存的绑定。

 


3.3.6 代码整合
            

 

import pika # 首先要安装 pika 模块
import time

credentials = pika.PlainCredentials('admin', 'admin')   # 远程rabbitmq的用户名密码
connection = pika.BlockingConnection(pika.ConnectionParameters('192.168.118.15', 5672, '/admin', credentials))  # 连接 RabbitMQ信息
channel = connection.channel()  # 定义 channel 对象
channel.exchange_declare(exchange='logs', exchange_type='fanout')   # 声明一个交换器 logs exchange类型:fanout
for i in range(1000):
    channel.basic_publish(exchange='logs',
                          routing_key='',   # 路由键,写明将消息发往哪个队列,本例是将消息发往队列hello
                          body='%s' % i,
                          ) # 消息内容
    print('[x]Sent "%s"' % i)
    time.sleep(1)
connection.close()  # 队列关闭
Producer.py
import pika

credentials = pika.PlainCredentials('admin', 'admin')  # 创建认证证书
connection = pika.BlockingConnection(
    pika.ConnectionParameters('192.168.118.15', 5672, '/admin', credentials))  # 连接 RabbitMQ信息
channel = connection.channel()  # 定义 channel 对象
channel.exchange_declare(exchange='task_queue', exchange_type='fanout')  # 声明一个队列 hello
result = channel.queue_declare(exclusive=True)  # 随机创建一个队列,当消费者断开时,删除队列
queue_name = result.method.queue
channel.queue_bind(exchange='logs', queue=queue_name)


def callback(ch, method, properties, body):  # 四个参数为标准格式
    print('>>>>>>> %s' % body)
    ch.basic_ack(delivery_tag=method.delivery_tag)  # 告诉生成者,消息处理完成

channel.basic_consume(  # 消费消息
    callback,  # 如果收到消息,就调用callback函数来处理消息
    queue=queue_name  # 你要从那个队列里收消息
    # no_ack=True
)

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

 



执行效果:


生产者不断发送消息到Exchange,只有当消费者连接的时候才会建立队列并接收消息,否则消息全部丢弃,只需要接收最新的消息。

 

3.4 Routing (路由)

上面的模式中,我们实现了一个简单的日志系统。可以把日志消息广播给多个接收者。
在Routing模式中,新增一个功能——使得RabbitMQ只订阅消息的一个子集。例如:我们只需要把严重的错误日志信息写入日志文件(存储到磁盘),但同时仍然把所有的日志信息输出到控制台中。

绑定(Bindings)
前面的例子,我们已经创建过绑定(bindings),代码如下:

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

 


绑定(binding)是指交换机(exchange)和队列(queue)的关系。可以简单理解为:这个队列(queue)对这个交换机(exchange)的消息感兴趣。

绑定的时候可以带上一个额外的routing_key参数。为了避免与basic_publish的参数混淆,我们把它叫做绑定键(binding key)。以下是如何创建一个带绑定键的绑定。

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

 


绑定键的意义取决于交换机(exchange)的类型。我们之前使用过的扇型交换机(fanout exchanges)会忽略这个值。

 


3.4.1 直连交换机(Direct exchange)
我们的日志系统广播所有的消息给所有的消费者(consumers)。我们打算扩展它,使其基于日志的严重程度进行消息过滤。例如我们也许只是希望将比较严重的错误(error)日志写入磁盘,以免在警告(warning)或者信息(info)日志上浪费磁盘空间。

我们使用的扇型交换机(fanout exchange)没有足够的灵活性 —— 它能做的仅仅是广播。

我们将会使用直连交换机(direct exchange)来代替。路由的算法很简单 —— 交换机将会对绑定键(binding key)和路由键(routing key)进行精确匹配(值相等),从而确定消息该分发到哪个队列。
下图能够很好的描述这个场景:
    


在这个场景中,我们可以看到直连交换机 X和两个队列进行了绑定。第一个队列使用orange作为绑定键,第二个队列有两个绑定,一个使用black作为绑定键,另外一个使用green。
这样以来,当路由键为orange的消息发布到交换机,就会被路由到队列Q1。路由键为black或者green的消息就会路由到Q2。其他的所有消息都将会被丢弃。


3.4.2 多个绑定(Multiple bindings)
    
多个队列使用相同的绑定键是合法的。这个例子中,我们可以添加一个X和Q1之间的绑定,使用black绑定键。这样一来,直连交换机就和扇型交换机的行为一样,会将消息广播到所有匹配的队列。带有black路由键的消息会同时发送到Q1和Q2。


发送日志

我们将会发送消息到一个直连交换机,把日志级别作为路由键。这样接收日志的脚本就可以根据严重级别来选择它想要处理的日志。我们先看看发送日志。

我们需要创建一个交换机(exchange):

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

 


然后我们发送一则消息:

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

 


我们先假设“severity”的值是info、warning、error中的一个。

订阅

处理接收消息的方式和之前差不多,只有一个例外,我们将会为我们感兴趣的每个严重级别分别创建一个新的绑定。

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)

 



3.4.3 代码整合

 

import pika  # 首先要安装 pika 模块
import sys

credentials = pika.PlainCredentials('admin', 'admin')  # 远程rabbitmq的用户名密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters('192.168.118.15', 5672, '/admin', credentials))  # 连接 RabbitMQ信息
channel = connection.channel()  # 定义 channel 对象
channel.exchange_declare(exchange='direct_logs', exchange_type='direct')  # 声明一个交换器 logs exchange类型:fanout
severity = sys.argv[1] if len(sys.argv) > 1 else 'info'
print(severity)
message = ' '.join(sys.argv[2:]) or 'Hello World!'
channel.basic_publish(exchange='direct_logs',
                      routing_key=severity,  # 路由键,写明将消息发往哪个队列,本例是将消息发往队列hello
                      body=message,
                      )  # 消息内容
print(" [x] Sent %r:%r" % (severity, message))
connection.close()  # 队列关闭
Producer.py
import pika
import sys

credentials = pika.PlainCredentials('admin', 'admin')  # 创建认证证书
connection = pika.BlockingConnection(
    pika.ConnectionParameters('192.168.118.15', 5672, '/admin', credentials))  # 连接 RabbitMQ信息
channel = connection.channel()  # 定义 channel 对象
channel.exchange_declare(exchange='direct_logs', exchange_type='direct')  # 声明一个队列 hello
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])  # 定义了三种接收消息方式info,warning,error
    sys.exit(1)

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

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


def callback(ch, method, properties, body):  # 四个参数为标准格式
    print(" [x] %r:%r" % (method.routing_key, body))
    # ch.basic_ack(delivery_tag=method.delivery_tag)  # 告诉生成者,消息处理完成

channel.basic_consume(  # 消费消息
    callback,  # 如果收到消息,就调用callback函数来处理消息
    queue=queue_name,  # 你要从那个队列里收消息
    no_ack=True
)

channel.start_consuming()  # 开始消费消息
Consumer.py

 
共开启三个窗口:
Producer.py 生产消息
Consumer.py 打印包括 info / warning / error
Consumer.py 只打印error信息

 
运行结果:

 
通过RabbitMQ管理查看,绑定了两个队列。

 

3.5 Topics(主题交换机)


为什么需要主题交换机?

在上面的模式中,我们改进了日志系统。使用直连交换机(direct)代替了扇型交换机(fanout),从只能盲目的广播消息改进为有可能选择性的接收日志。
尽管直连交换机能够改善我们的系统,但是它也有它的限制 —— 没办法基于多个标准执行路由操作。
在我们的日志系统中,我们不只希望订阅基于严重程度的日志,同时还希望订阅基于发送来源的日志。Unix工具syslog就是同时基于严重程度-severity (info/warn/crit…) 和 设备-facility (auth/cron/kern…)来路由日志的。
如果这样的话,将会给予我们非常大的灵活性,我们既可以监听来源于“cron”的严重程度为“critical errors”的日志,也可以监听来源于“kern”的所有日志。
为了实现这个目的,接下来我们学习如何使用另一种更复杂的交换机 —— 主题交换机。

3.5.1 主题交换机

发送到主题交换机(topic exchange)的消息,它的路由键(routing_key)必须是一个由 . 分隔开的词语列表。这些单词随便是什么都可以,但是最好是跟携带它们的消息有关系的词汇。以下是几个推荐的例子:”stock.usd.nyse”, “nyse.vmw”, “quick.orange.rabbit”。词语的个数可以随意,但是不要超过255字节。
绑定键也必须拥有同样的格式。主题交换机背后的逻辑跟直连交换机很相似 —— 一个携带着特定路由键的消息会被主题交换机投递给绑定键与之想匹配的队列。但是它的绑定键和路由键有两个特殊应用方式:

        ○ '*'(星号)用来表示一个单词;
        ○ '#'(井号)用来表示任意数量(零个或多个)单词。

下边用图说明:
        
这个例子里,我们发送的所有消息都是用来描述小动物的。发送的消息所携带的路由键是由三个单词所组成的,这三个单词被两个.分割开。路由键里的第一个单词描述的是动物的手脚的利索程度,第二个单词是动物的颜色,第三个是动物的种类。所以它看起来是这样的: <celerity>.<colour>.<species>。

我们创建了三个绑定:Q1的绑定键为 *.orange.*,Q2的绑定键为 *.*.rabbit 和 lazy.# 。

这三个绑定键被可以总结为:

        ○ Q1 对所有的桔黄色动物都感兴趣。
        ○ Q2 则是对所有的兔子和所有懒惰的动物感兴趣。

携带有 quick.orange.rabbit 的消息将会被分别投递给这两个队列。
携带着 lazy.orange.elephant 的消息同样也会给两个队列都投递过去。
携带有 quick.orange.fox 的消息会投递给第一个队列,携带有 lazy.brown.fox 的消息会投递给第二个队列。
携带有 lazy.pink.rabbit 的消息只会被投递给第二个队列一次,即使它同时匹配第二个队列的两个绑定。
携带着 quick.brown.fox 的消息不会投递给任何一个队列。

如果我们违反约定,发送了一个携带有一个单词或者四个单词("orange" or "quick.orange.male.rabbit")的消息时,发送的消息不会投递给任何一个队列,而且会丢失掉。
但是另一方面,即使 "lazy.orange.male.rabbit" 有四个单词,他还是会匹配最后一个绑定,并且被投递到第二个队列中。

主题交换机
主题交换机是很强大的,它可以表现出跟其他交换机类似的行为。
当一个队列的绑定键为 “#”(井号) 的时候,这个队列将会无视消息的路由键,接收所有的消息(扇型交换机模式)。
当 * (星号) 和 # (井号) 这两个特殊字符都未在绑定键中出现的时候,此时主题交换机就拥有的直连交换机的行为(直连交换机模式)。

3.5.2 代码整合

接下来我们会将主题交换机应用到我们的日志系统中。在开始工作前,我们假设日志的路由键由两个单词组成,路由键看起来是这样的:<facility>.<severity>

import pika  # 首先要安装 pika 模块
import sys

credentials = pika.PlainCredentials('admin', 'admin')  # 远程rabbitmq的用户名密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters('192.168.118.15', 5672, '/admin', credentials))  # 连接 RabbitMQ信息
channel = connection.channel()  # 定义 channel 对象
channel.exchange_declare(exchange='topic_logs', exchange_type='topic')  # 声明一个交换器 logs exchange类型:fanout
routing_key = sys.argv[1] if len(sys.argv) > 1 else 'anonymous.info'
message = ' '.join(sys.argv[2:]) or 'Hello World!'
channel.basic_publish(exchange='topic_logs',
                      routing_key=routing_key,  # 路由键,写明将消息发往哪个队列,本例是将消息发往队列hello
                      body=message,
                      )  # 消息内容
print(" [x] Sent %r:%r" % (routing_key, message))
connection.close()  # 队列关闭
Producer.py
import pika
import sys

credentials = pika.PlainCredentials('admin', 'admin')  # 创建认证证书
connection = pika.BlockingConnection(
    pika.ConnectionParameters('192.168.118.15', 5672, '/admin', credentials))  # 连接 RabbitMQ信息
channel = connection.channel()  # 定义 channel 对象
channel.exchange_declare(exchange='topic_logs', exchange_type='topic')  # 声明一个队列 hello
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 [info] [warning] [error]\n" % sys.argv[0])  # 定义了三种接收消息方式info,warning,error
    sys.exit(1)

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

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


def callback(ch, method, properties, body):  # 四个参数为标准格式
    print(" [x] %r:%r" % (method.routing_key, body))
    # ch.basic_ack(delivery_tag=method.delivery_tag)  # 告诉生成者,消息处理完成

channel.basic_consume(  # 消费消息
    callback,  # 如果收到消息,就调用callback函数来处理消息
    queue=queue_name,  # 你要从那个队列里收消息
    no_ack=True
)

channel.start_consuming()  # 开始消费消息
Consumer.py

 



执行下边命令 接收所有日志:

python consumer.py "#"

 


执行下边命令 接收来自”kern“设备的日志:

python consumer.py "kern.*"

 


执行下边命令 只接收严重程度为”critical“的日志:

python consumer.py "*.critical"

 


执行下边命令 建立多个绑定:

python consumer.py "kern.*" "*.critical"

 


执行下边命令 发送路由键为 “kern.critical” 的日志:

python producer.py "kern.critical" "A critical kernel error"

 


执行上边命令试试看效果吧。另外,上边代码不会对路由键和绑定键做任何假设,所以你可以在命令中使用超过两个路由键参数。

• 绑定键为 * 的队列会取到一个路由键为空的消息吗?
答:不会接收为空的消息,匹配所有消息。


• 绑定键为 #.* 的队列会获取到一个名为..的路由键的消息吗?它会取到一个路由键为单个单词的消息吗?
答:绑定键为 #.* 的队列会获取到一个名为..的路由键,会取到一个路由键为单个单词的消息


• a.*.# 和 a.#的区别在哪儿?
答:这里的 '#' 可以理解为正则中的 '?' 问号,意思是0到无穷大。
    a.*.# 表示首先必须满足 a.* 也就是 a.单词,满足这个条件以后都会匹配到
    a.# 表示只要以a开头的routing_key 都会被匹配到

3.6 RPC (远程过程调试模式)


如果我们需要将一个函数运行在远程计算机上并且等待从那儿获取结果时,该怎么办呢?这就是另外的故事了。这种模式通常被称为远程过程调用(Remote Procedure Call)或者RPC。
我们使用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类的阻塞。结果被异步地推送到下一个计算场景。

 


3.6.1 回调队列
一般来说通过RabbitMQ来实现RPC是很容易的。一个客户端发送请求信息,服务器端将其应用到一个回复信息中。为了接收到回复信息,客户端需要在发送请求的时候同时发送一个回调队列(callback queue)的地址。我们试试看:

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)

 



3.6.2 消息属性

AMQP协议给消息预定义了一系列的14个属性。大多数属性很少会用到,除了以下几个:
    • delivery_mode(投递模式):将消息标记为持久的(值为2)或暂存的(除了2之外的其他任何值)。第二篇教程里接触过这个属性,记得吧?
    • content_type(内容类型):用来描述编码的mime-type。例如在实际使用中常常使用application/json来描述JOSN编码类型。
    • reply_to(回复目标):通常用来命名回调队列。
    • correlation_id(关联标识):用来将RPC的响应和请求关联起来。

 


3.6.3 关联标识

上边介绍的方法中,我们建议给每一个RPC请求新建一个回调队列。这不是一个高效的做法,幸好这儿有一个更好的办法 —— 我们可以为每个客户端只建立一个独立的回调队列。
这就带来一个新问题,当此队列接收到一个响应的时候它无法辨别出这个响应是属于哪个请求的。correlation_id 就是为了解决这个问题而来的。我们给每个请求设置一个独一无二的值。稍后,当我们从回调队列中接收到一个消息的时候,我们就可以查看这条属性从而将响应和请求匹配起来。如果我们接手到的消息的correlation_id是未知的,那就直接销毁掉它,因为它不属于我们的任何一条请求。
你也许会问,为什么我们接收到未知消息的时候不抛出一个错误,而是要将它忽略掉?这是为了解决服务器端有可能发生的竞争情况。尽管可能性不大,但RPC服务器还是有可能在已将应答发送给我们但还未将确认消息发送给请求的情况下死掉。如果这种情况发生,RPC在重启后会重新处理请求。这就是为什么我们必须在客户端优雅的处理重复响应,同时RPC也需要尽可能保持幂等性。

总结:
    


我们的RPC如此工作:
    • 当客户端启动的时候,它创建一个匿名独享的回调队列。
    • 在RPC请求中,客户端发送带有两个属性的消息:一个是设置回调队列的 reply_to 属性,另一个是设置唯一值的 correlation_id 属性。
    • 将请求发送到一个 rpc_queue 队列中。
    • RPC工作者(又名:服务器)等待请求发送到这个队列中来。当请求出现的时候,它执行他的工作并且将带有执行结果的消息发送给reply_to字段指定的队列。
    • 客户端等待回调队列里的数据。当有消息出现的时候,它会检查correlation_id属性。如果此属性的值与请求匹配,将它返回给应用。


3.6.4 整合代码

import pika  # 首先要安装 pika 模块
import sys

credentials = pika.PlainCredentials('admin', 'admin')  # 远程rabbitmq的用户名密码
connection = pika.BlockingConnection(
    pika.ConnectionParameters('192.168.118.15', 5672, '/admin', credentials))  # 连接 RabbitMQ信息
channel = connection.channel()  # 定义 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,
                     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()
rpc_server.py

 


服务器端代码相当简单:
    • (4)像往常一样,我们建立连接,声明队列
    • (11)我们声明我们的fib函数,它假设只有合法的正整数当作输入。(别指望这个函数能处理很大的数值,函数递归你们都懂得…)
    • (19)我们为 basic_consume 声明了一个回调函数,这是RPC服务器端的核心。它执行实际的操作并且作出响应。
    • (32)或许我们希望能在服务器上多开几个线程。为了能将负载平均地分摊到多个服务器,我们需要将 prefetch_count 设置好。


rpc_client.py

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# Author: hkey
import pika
import uuid


class FibbonacciRpcClient:
    def __init__(self):
        self.connection = pika.BlockingConnection(
            # 连接 RabbitMQ信息
            pika.ConnectionParameters('192.168.118.15', 5672, '/admin', pika.PlainCredentials('admin', 'admin')))
        self.channel = self.connection.channel()  # 定义 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):
        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:
            self.connection.process_data_events()
        return int(self.response)


fibonacci_rpc = FibbonacciRpcClient()

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

 


客户端代码稍微有点难懂:
    • (7)建立连接、通道并且为回复(replies)声明独享的回调队列。
    • (16)我们订阅这个回调队列,以便接收RPC的响应。
    • (18)“on_response”回调函数对每一个响应执行一个非常简单的操作,检查每一个响应消息的correlation_id属性是否与我们期待的一致,如果一致,将响应结果赋给self.response,然后跳出consuming循环。
    • (23)接下来,我们定义我们的主要方法 call 方法。它执行真正的RPC请求。
    • (24)在这个方法中,首先我们生成一个唯一的 correlation_id 值并且保存起来,’on_response’回调函数会用它来获取符合要求的响应。
    • (25)接下来,我们将带有 reply_to 和 correlation_id 属性的消息发布出去。
    • (32)现在我们可以坐下来,等待正确的响应到来。
    • (33)最后,我们将响应返回给用户。


我们的RPC服务已经准备就绪了,现在启动服务器端:


此处呈现的设计并不是实现RPC服务的唯一方式,但是他有一些重要的优势:
    • 如果RPC服务器运行的过慢的时候,你可以通过运行另外一个服务器端轻松扩展它。试试在控制台中运行第二个 rpc_server.py 。
    • 在客户端,RPC请求只发送或接收一条消息。不需要像 queue_declare 这样的异步调用。所以RPC客户端的单个请求只需要一个网络往返。


我们的代码依旧非常简单,而且没有试图去解决一些复杂(但是重要)的问题,如:
    • 当没有服务器运行时,客户端如何作出反映。
    • 客户端是否需要实现类似RPC超时的东西。
    • 如果服务器发生故障,并且抛出异常,应该被转发到客户端吗?
    • 在处理前,防止混入无效的信息(例如检查边界)

 

posted @ 2019-01-05 17:33  hukey  阅读(355)  评论(0编辑  收藏  举报