讲解基础概念的前面,我们先来整体构造一个结构图,这样会方便们更好地去理解RabbitMQ的基本原理。
send Message
到Receive Message
的一个大致的流程。当然上面有很多名词都相比还没有介绍到,不要着急接下来我们就开始对其进行详细的讲解。先进先出
。上图可以清晰地看到Client A和Client B是生产者,生产者生产消息最终被送到RabbitMQ的内部对象Queue中去,而消费者则是从Queue队列中取出数据。可以简化成表示为:prefetchCount
来限制每次发送给消费者消息的个数。详情见下图所示:channel.basic_qos(prefetch_count=1)
1. 切换root用户
su root
2. 安装配置epel源
rpm -ivh http://dl.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm
3. 安装erlang
yum -y install erlang
4. 安装RabbitMQ
yum -y install rabbitmq-server
5. 启动/关闭
service rabbitmq-server start/stop
6. 查看用户列表
rabbitmqctl list_users
rabbitmqctl add_user 用户名 密码
# 设置超级管理员标签
rabbitmqctl set_user_tags 用户名 administrator
# 设置权限
rabbitmqctl set_permissions -p / 用户名 ".*" ".*" ".*"
rabbitmqctl delete_user 用户名
rabbitmqctl change_password 用户名 新密码
firewall-cmd --zone=public --add-port=5672/tcp --permanent
firewall-cmd --zone=public --add-port=15672/tcp --permanent
firewall-cmd --reload
11. 插件管理
RabbitMQ的有些插件没有集成在初始的安装中,它们需要额外安装,这些文件的后缀为.ez,安装时需要将.ez文件拷贝到安装的插件目录。以下是不同系统中默认安装的插件目录路径:
插件目录 | |
---|---|
Linux | /usr/lib/rabbitmq/lib/rabbitmq_server-version/plugins |
Windows | C:\Program Files\RabbitMQ\rabbitmq_server-version\plugins(安装rabbitmq的目录) |
Homebrew | /usr/local/Cellar/rabbitmq/version/plugins |
Generic Unix | rabbitmq_server-version/plugins (安装rabbitmq的目录) |
whereis rabbitmq
rabbitmq: /usr/lib/rabbitmq /etc/rabbitmq
cd /usr/lib/rabbitmq/bin
./rabbitmq-plugins enable rabbitmq_management
service rabbitmq-server start
http://127.0.0.1:15672
http://192.168.78.128:15672/
(1)普通的消息收发
# sender.py
import pika
# 方式一: 用户名密码形式连接
# 连接rabbitmq(相当于socket链接)
# 使用密码连接
credentials = pika.PlainCredentials('admin', 'password@123')
connection = pika.BlockingConnection(pika.ConnectionParameters(host=
'localhost', credentials=credentials))
# 直接连接
# connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
# 方式二: url形式连接
# connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@[email protected]:5672'))
# 建立rabbitmq协议通道
channel = connection.channel()
# 声明一个队列
channel.queue_declare(queue='q1')
# 发送消息
channel.basic_publish(exchange='', routing_key='q1', body='hello world')
# 关闭队列
connection.close()
1. basic_publish
:方法详解
def basic_publish(self, exchange, routing_key,
body, properties=None,
mandatory=False, immediate=False):
exchange
:交换器的名称,指明消息需要发送到哪个交换器。如果设置为空字符串,则消息会被发生到 RabbitMQ 默认的交换器中,后面会讲到。routing_key
:路由键,交换器根据路由键将消息存储到相应的队列之中properties
:消息的基本属性集,其包含 14 个属性成员,分别有 contentType、deliveryMode、proiotity等。body
:消息体 payload,真正需要发送的消息。mandatory
:当参数设置为 true 时,交换器无法根据自身的类型和路由键找到一个符合条件的队列,那么 RabbitMQ 会调用 Basic.Return 命令,将消息返回给生产者。当参数设置为 false 时,出现上述情况,则消息直接丢失。immediate
:当参数设置为 true 时,如果交换器在将消息路由到队列时发现队列上并不存在任何消费者,那么这条消息将不会存入队列中。当与路由键匹配的所有队列都没有消费者时,该消息会通过 Basic.Return 返回至生产者。# reciver.py
import pika
# 建立连接
connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
# 创建channel
channel = connection.channel()
# 如果生产者先运行并创建了队列这里就可以不用声明,但是有可能消费者先运行
# 如果消费者先运行,下面的basic_consume就会因为没有队列报错。
channel.queue_declare(queue="q1")
# 定义回调函数用于取出队列中的数据
def callback(ch, method, properties, body):
"""
:param ch: channel通道
:param method: 类似于http响应头,携带各种信息
:param properties: 属性
:param body: 消息主体内容,bytes格式
"""
print(" [x] Received %r" % body)
# 基于1.01版本的 pika
channel.basic_consume(queue="q1",
on_message_callback=callback,
auto_ack=True # 不用确认消息)
# 如果是0.12版本的 pika
# channel.basic_consume(consumer_callback=callback,
# queue="q1",
# no_ack=True # 不用确认消息)
print(" [*] Waiting for messages. To exit press Ctrl + C ")
# 监听数据
channel.start_consuming()
1. basic_consume
:方法详解
def basic_consume(consumer_callback, queue='',
no_ack=False, exclusive=False,
consumer_tag=None, arguments=None):
# def basic_consume(queue='', on_message_callback=callback,
# auto_ack=False, exclusive=False,
# consumer_tag=None, arguments=None):
consumer_callback
: 设置消费者的回调函数。用来处理 RabbitMQ 推送过来的消息,比如:DefaultConsumer,使用时需要客户端重写其中的方法。queue
:队列的名称。no_ack
:设置是否自动确认。建议设置成 false,即不自动确认。exclusive
:设置是否排他。consumer_tag
:消费者标签,用来区分多个消费者。arguments
:设置消费者的其他参数。(2)安全的消息收发:主要变化在消费者
import pika
# 连接rabbitmq(相当于socket链接)
connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@[email protected]'))
# 建立rabbitmq协议通道
channel = connection.channel()
# 声明一个队列
channel.queue_declare(queue='q1')
# 发送消息
channel.basic_publish(exchange='',
routing_key='q1',
body='hello world')
# 关闭队列
connection.close()
import pika
# 建立连接
connection = pika.BlockingConnection(pika.ConnectionParameters("192.168.78.128"))
# 创建channel
channel = connection.channel()
# 如果生产者先运行并创建了队列这里就可以不用声明,但是有可能消费者先运行
# 如果消费者先运行,下面的basic_consume就会因为没有队列报错。
channel.queue_declare(queue="q1")
# 定义回调函数用于取出队列中的数据
def callback(ch, method, properties, body):
"""
:param ch: channel通道
:param method: 类似于http响应头,携带各种信息
:param properties: 属性
:param body: 消息主体内容,bytes格式
"""
print(" [x] Received %r" % body)
ch.basic_ack(delivery_tag=method.delivery_tag) # 发送确认消息
channel.basic_consume(queue="q1",
on_message_callback=callback,
auto_ack=False) # 处理完成必须手动发送确认消息
print(" [*] Waiting for messages. To exit press Ctrl + C ")
# 监听数据
channel.start_consuming()
auto_ack=False
-----> 默认行为,所以可以不用写ch.basic_ack(delivery_tag=method.delivery_tag)
(3) 队列持久化: 主要变化在生产者
rabbitmq
服务不挂掉的情况下,确保了消息不会丢失,但在生产环境中我们无法确保rabbitmq
服务能够一直运行不会挂掉,所以我们需要实现队列持久化
和消息持久化
。import pika
# 连接rabbitmq(相当于socket链接)
connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@[email protected]'))
# 建立rabbitmq协议通道
channel = connection.channel()
# 声明一个队列
# durable 声明的是一个持久化的队列
channel.queue_declare(queue='q2', durable=True)
# 发送消息
channel.basic_publish(exchange='',
routing_key='q2',
body='hello world')
# 关闭队列
connection.close()
durable=True
(4)消息持久化:主要变化在生产者
import pika
# 连接rabbitmq(相当于socket链接)
connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@[email protected]'))
# 直接连接
# connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
# 建立rabbitmq协议通道
channel = connection.channel()
# 声明一个队列
# durable 声明的是一个持久化的队列
channel.queue_declare(queue='q2', durable=True)
# 发送消息
channel.basic_publish(exchange='',
routing_key='q2',
body='hello world',
properties=pika.BasicProperties(
delivery_mode=2 # 消息持久化
)
)
# 关闭队列
connection.close()
properties=pika.BasicProperties( delivery_mode=2)
# 消息持久化(5)说明
import pika
# 建立连接
connection = pika.BlockingConnection(pika.ConnectionParameters("192.168.78.128"))
# 创建channel
channel = connection.channel()
# 因为队列实现了持久化,所以消费者端可以不用声明,如果声明了,则必须指定为持久化队列,否则会报错
# channel.queue_declare(queue="q2") 报错
# channel.queue_declare(queue="q2", durable=True)
# 定义回调函数用于取出队列中的数据
def callback(ch, method, properties, body):
"""
:param ch: channel通道
:param method: 类似于http响应头,携带各种信息
:param properties: 属性
:param body: 消息主体内容,bytes格式
"""
print('receive msg... start processing....', body)
import time
time.sleep(20)
print(" [x] Received %r" % body)
print('method.delivery_tag: ', method.delivery_tag)
ch.basic_ack(delivery_tag=method.delivery_tag)
# 基于1.01版本的 pika
channel.basic_consume(queue="q2",
on_message_callback=callback,
auto_ack=False)
print(" [*] Waiting for messages. To exit press Ctrl + C ")
# 监听数据
channel.start_consuming()
我们有没有疑问,开篇那个应用结构图里面,消费者Client A和消费者Client B是如何知道我发送的消息是给Queue1还是给Queue2,有没有过这个问题,那么我们就来解开这个面纱,看看到底是个什么构造。首先明确一点就是生产者产生的消息并不是直接发送给消息队列Queue的,而是要经过Exchange(交换器),由Exchange再将消息路由到一个或多个Queue,当然这里还会对不符合路由规则的消息进行丢弃掉,这里指的是后续要谈到的Exchange Type。那么Exchange是怎样将消息准确的推送到对应的Queue的呢?那么这里的功劳最大的当属Binding
,RabbitMQ是通过Binding将Exchange和Queue链接在一起
,这样Exchange就知道如何将消息准确的推送到Queue中去。简单示意图如下所示
广播:订阅发布模式
Routing Key
,与Exchange关联的队列会携带一个Binding Key
,当Routing Key 和 Binding Key对应上的时候,消息就会发送到对应的Queue中去。Exchange Type
(1) fanout
:fanout类型的Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中(广播)
# fanout_sender.py
import pika
connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@[email protected]'))
channel = connection.channel()
# 声明消息转发器
channel.exchange_declare(exchange='logs', # 以日志级别定义转发器(即上述提到的 Routing Key)
exchange_type='fanout' # 转发器类型
)
# 推送消息
channel.basic_publish(exchange='logs',
routing_key='', # 消息转发到的队列,因为此时是广播,所以不需要声明队列,系统会将消息转发的转发器exchange上
body='hello world', # 消息内容
)
# 关闭连接
connection.close()
# fanout_receiver.py
import pika
connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@[email protected]'))
channel = connection.channel()
channel.exchange_declare(exchange='logs', # 以日志级别定义转发器
exchange_type='fanout' # 转发器类型
)
# 声明队列 exclusive:排他(即声明不重复的队列),同时还会在此队列断开后自动将其删掉
result = channel.queue_declare(queue='', exclusive=True)
# 获取队列名称
queue_name = result.method.queue
# 将自己的队列绑定到要接收消息的消息转发器上(即上述提到的 Binding Key)
channel.queue_bind(exchange='logs', queue=queue_name)
# 消息处理函数
def callback(ch, method, properties, body):
# 此处收到的消息是bytes格式
print('receive message: %s' % body)
# 消费者
channel.basic_consume(on_message_callback=callback, queue=queue_name)
# 启动消费
channel.start_consuming()
(2)direct
:direct类型的Exchange路由规则也很简单,它会把消息路由到那些binding key与routing key完全匹配的Queue中(组播)
# direct_sender.py
import sys
import pika
connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@[email protected]'))
channel = connection.channel()
# 声明消息转发器
channel.exchange_declare(exchange='booking',
exchange_type='direct')
# 定义发送消息级别,只有绑定了该转发器对应级别的队列才能收到相应的消息
severity = sys.argv[1] if len(sys.argv) > 1 else 'create'
message = 'message: hello world from {} level'.format(severity)
# 推送消息
channel.basic_publish(exchange='booking',
routing_key=severity, # 组播,转发消息给绑定了该转发器的这四个级别的队列
body=message, # 消息内容
)
print('send {} level message: {}'.format(severity, message))
# 关闭连接
connection.close()
# direct_receiver.py
import sys
import pika
connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@[email protected]'))
channel = connection.channel()
channel.exchange_declare(exchange='booking',
exchange_type='direct')
# 声明队列 exclusive:排他(即声明不重复的队列),同时还会在此队列断开后自动将其删掉
result = channel.queue_declare(queue='', exclusive=True)
# 获取队列名称
queue_name = result.method.queue
# 定义该队列要绑定的队列级别
severities = sys.argv[1:]
for severity in severities:
# 循环将自己的队列绑定到要接收消息的消息转发器上对应级别上
# routing_key:booking转发器级别列表子集
channel.queue_bind(exchange='booking', queue=queue_name, routing_key=severity)
# 消息处理函数
def callback(ch, method, properties, body):
print('receive message: %s' % body)
# 消费者
channel.basic_consume(on_message_callback=callback, queue=queue_name)
# 启动消费
channel.start_consuming()
(3) topic
:前面提到的direct规则是严格意义上的匹配,换言之Routing Key必须与Binding Key相匹配的时候才将消息传送给Queue,那么topic这个规则就是模糊匹配,可以通过通配符满足一部分规则就可以传送。(特征播)
.
分隔的字符串(我们将被句点号.
分隔开的每一段独立的字符串称为一个单词),如stock.usd.nyse
、nyse.vmw
、quick.orange.rabbit
.
分隔的字符串*
与#
,用于做模糊匹配,其中*
用于匹配一个单词,#
用于匹配多个单词(可以是零个)# topic_sender.py
import sys
import pika
connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@[email protected]'))
channel = connection.channel()
# 声明消息转发器
channel.exchange_declare(exchange='topic_test',
exchange_type='topic')
# 定义发送消息级别,只有绑定了该转发器对应级别的队列才能收到相应的消息
severity = sys.argv[1] if len(sys.argv) > 1 else 'create'
message = 'message: hello world from {} level'.format(severity)
# 推送消息
channel.basic_publish(exchange='topic_test',
routing_key=severity, # 组播,转发消息给绑定了该转发器的这四个级别的队列
body=message, # 消息内容
)
print('send {} level message: {}'.format(severity, message))
# 关闭连接
connection.close()
# topic_receiver.py
import sys
import pika
connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@[email protected]'))
channel = connection.channel()
channel.exchange_declare(exchange='topic_test',
exchange_type='topic')
# 声明队列 exclusive:排他(即声明不重复的队列),同时还会在此队列断开后自动将其删掉
result = channel.queue_declare(queue='', exclusive=True)
# 获取队列名称
queue_name = result.method.queue
# 定义该队列要绑定的队列级别
severities = sys.argv[1:]
if not severities:
sys.stderr.write('Usage: {} [binding_key]... \n'.format(sys.argv[0]))
sys.exit(1)
for severity in severities:
# 循环将自己的队列绑定到要接收消息的消息转发器上对应级别上
# routing_key:booking转发器级别列表子集
channel.queue_bind(exchange='topic_test', queue=queue_name, routing_key=severity)
# 消息处理函数
def callback(ch, method, properties, body):
print('receive message: %s' % body)
# 消费者
channel.basic_consume(on_message_callback=callback, queue=queue_name)
# 启动消费
channel.start_consuming()
(4)headers
:headers类型的Exchange不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。(消息头播)
在绑定Queue与Exchange时指定一组键值对;当消息发送到Exchange时,RabbitMQ会取到该消息的headers(也是一个键值对的形式),对比其中的键值对是否完全匹配Queue与Exchange绑定时指定的键值对;如果完全匹配则消息会路由到该Queue,否则不会路由到该Queue。
该类型的Exchange没有用到过(不过也应该很有用武之地),所以不做介绍。
RPC
远程过程调用。# rpc_client.py
import pika
import uuid
class FibonacciRpcClient():
def __init__(self):
self.connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@[email protected]'))
self.channel = self.connection.channel()
# 生成唯一的队列
result = self.channel.queue_declare(exclusive=True, queue='')
self.callback_queue = result.method.queue
# 准备接收消息
self.channel.basic_consume(on_message_callback=self.on_response,
auto_ack=True, queue=self.callback_queue)
# 收到消息处理函数
def on_response(self, ch, method, props, body):
"""
接收任务处理结果
:param ch:
:param method:
:param props:
:param body:
:return:
"""
# 根据任务唯一标识符,判断是不是当前任务返回的结果
if self.corr_id == props.correlation_id:
# 修改任务返回结果
self.response = body
def call(self, n):
"""
发出任务
:param n:
:return:
"""
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 = FibonacciRpcClient()
print('request fib(30)')
response = fibonacci_rpc.call(30) # 发出任务
print('get response result: {}'.format(response))
# rpc_server.py
import pika
connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@[email protected]'))
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({})'.format(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_message_callback=on_request, queue='rpc_queue')
print('Awaiting RPC requests')
# 启动消费者
channel.start_consuming()
app_id
进行引导# rpc_client.py
import pika
connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@[email protected]'))
channel = connection.channel()
channel.queue_declare(queue='rpc_queues')
def python():
return 'python'
def java():
return 'java'
def on_request(ch, method, props, body):
fun_name = props.app_id
if fun_name == "p":
response = python()
elif fun_name == "j":
response = java()
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(queue='rpc_queues', on_message_callback=on_request)
print(" [x] Awaiting RPC requests")
channel.start_consuming()
body
可以作为形参,接收参数传递,body
格式必须为字符串# rpc_server.py
import pika
import uuid
class RpcClient(object):
def __init__(self):
self.connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@[email protected]'))
self.channel = self.connection.channel()
result = self.channel.queue_declare(exclusive=True, queue='')
self.callback_queue = result.method.queue
self.channel.basic_consume(on_message_callback=self.on_response, auto_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, name):
self.response = None
self.corr_id = str(uuid.uuid4())
self.channel.basic_publish(exchange='',
routing_key='rpc_queues',
properties=pika.BasicProperties(
reply_to=self.callback_queue,
correlation_id=self.corr_id,
app_id=str(name),
),
body='')
while self.response is None:
self.connection.process_data_events()
return self.response.decode()
rpc = RpcClient()
print(" [x] Requesting")
response = rpc.call("j")
print(" [.] Got %r" % response)