上两篇中我们介绍了rabbitmq中的收发模型和工作队列模型,它们都一个共同的特点——exchange是透明的,似乎不存在,收、发双方都是通过queue名称产生关联。
本篇中我们来着重介绍下rabbitmq中的exchange ,rabbitmq中涉及exchange(非透明)的收发模型我们称之为发布-订阅模型。在发布-订阅模型中,消息生产者和消费者不再是直接面对queue(队列名称),而是直面exchange,都需要经过exchange来进行消息的发送和接收。
发布-订阅模型又可以分为以下几种:
fanout: 广播交换机,广播模型,不关心queue名称,不关心路由key
direct: 直接交换机,直接路由模型,根据路由key字符串精确匹配, 不关心queue名称,关心路由key
topic:主题交换机,主题路由模型,根据路由key字符串模糊(通配符)匹配, 不关心queue名称,关心路由key
headers:头交换机,头路由模型(使用较少),不关心queue名称,不关心路由key,关心消息头中key-value对。
本篇,我们来介绍下第一种发布订阅模型——fanout广播模型。
广播模型的特点是:所有发往同一个fanout交换机的消息都会被所有监听这个交换机的消费者接收到,而不关心具体的路由key。广播交换机就如同一个广播地址,只要有消费者监听这个广播地址,则一定都会收到来自这个广播地址收到的所有消息。如图:
示例代码如下
日志生产者:
package com.tingcream.rabbitmq.broadcast;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class LogProducer {
private static String EXCHANGE_NAME="logs";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
//主机 端口 vhost 用户名 密码
factory.setHost("192.168.9.102");
factory.setUsername("rabbitmq");
factory.setPassword("rabbitmq123");
factory.setPort(AMQP.PROTOCOL.PORT);
factory.setVirtualHost("/");
Connection connection=factory.newConnection();
Channel channel=connection.createChannel();
// 声明一个fanout 广播交换机,名称为logs , 声明的fanout队列 autoDelete为true, exclude 为true
channel.exchangeDeclare(EXCHANGE_NAME,"fanout"); //amq.gen-xxxxxxxxxx AD EXCL
// channel.exchangeDeclare(exchange, type, durable, autoDelete, arguments)
for (int i=0;i<10;i++){
String message="你好 World "+i;
// channel.basicPublish(exchange, routingKey, props, body);
//发布消息时指定routingKey为""
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes());
System.out.println("LogProducer Send :" + message );
}
channel.close();
connection.close();
}
}
日志生产者代码中声明了一个名叫logs的fanout类型交换机,并使用这个交换机来发送消息,并且发送消息时设置路由key为"",因为在广播的场景中根本就不关心路由key。
日志消费者A:
package com.tingcream.rabbitmq.broadcast;
import java.io.IOException;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
public class LogReceiverA {
private static String EXCHANGE_NAME="logs";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
//主机 端口 vhost 用户名 密码
factory.setHost("192.168.9.102");
factory.setUsername("rabbitmq");
factory.setPassword("rabbitmq123");
factory.setPort(AMQP.PROTOCOL.PORT);
factory.setVirtualHost("/");
Connection connection=factory.newConnection();
Channel channel=connection.createChannel();
//重要 与生产者使用同一个交换机
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//获取一个随机的队列名称
String queueName = channel.queueDeclare().getQueue();
//关联 exchange 和 queue ,因为是广播无需指定routekey,routingKey设置为空字符串
// channel.queueBind(queue, exchange, routingKey)
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println("LogReceiverA Waiting for messages");
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println( "LogReceiverA接收到消息:" + message );
}
};
channel.basicConsume(queueName, true, consumer);
}
}
日志消费者B:
package com.tingcream.rabbitmq.broadcast;
import java.io.IOException;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
public class LogReceiverB {
private static String EXCHANGE_NAME="logs";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
//主机 端口 vhost 用户名 密码
factory.setHost("192.168.9.102");
factory.setUsername("rabbitmq");
factory.setPassword("rabbitmq123");
factory.setPort(AMQP.PROTOCOL.PORT);
factory.setVirtualHost("/");
Connection connection=factory.newConnection();
Channel channel=connection.createChannel();
//重要 与生产者使用同一个交换机
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//获取一个随机的队列名称
String queueName = channel.queueDeclare().getQueue();
//关联 exchange 和 queue ,因为是广播无需指定routekey,routingKey设置为空字符串
// channel.queueBind(queue, exchange, routingKey)
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println("LogReceiverB Waiting for messages");
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println( "LogReceiverA接收到消息:" + message );
}
};
//true 自动回复ack
channel.basicConsume(queueName, true, consumer);
}
}
注意:
日志消费者A、B代码中使用了随机生成的queue名称(在本地随机产生一个名称),然后绑定这个queue名称到logs交换机上了。这是因为所有发布-订阅模型对queue名称都不关心,只要queue名称唯一、不重复即可。绑定queue到交换机是一种惯例的做法,实际上就是从交换机上取出一条消息并关联上一个指定的queue,然后我们就可以对这个queue进行监听等等,当然本例中queue的名称是随机生成。
先启动消费者A、B, 再启动生产者,运行下,登录管理后台。
登录管理后台后,我们发现exchange标签项中最后一行多出了一个名叫logs交换机,它的类型为fanout(广播),这就是我们代码中自定义的一个交换机。其上的7个交换机都是rabbitmq中内置的交换机。(最上面一个我们已经很熟悉了,收-发模型和工作队列模型就是采用的这种exchange)
queue标签项中多出了两条随机生成的队列名称。