一、环境搭建
第一步:软件安装
1、 如果安装RabbitMQ首先安装基于erlang语言支持的OTP软件
2、 安装完成OTP之后再安装RabbitMQ
OTP下载地址:http://www.erlang.org/downloads
RabbitMQ下载地址:http://www.rabbitmq.com/news.html
第二步:环境变量配置
以上步骤都完成之后配置环境变量
首先配置ERLANG_HOME如下图(变量值就是你按照otp软件的路径)
然后在配置RABBITMQ_SERVER如下图(变量值是rabbitMQ的安装路径)
最后进行path的配置如下图(path的值为;%ERLANG_HOME%\bin;%RABBITMQ_SERVER%\sbin;注意是追加)
第三步:启动监控管理器
找到你安装rabbitMQ的路径,然后切换到sbin的文件夹,输入rabbitmq-plugins enable rabbitmq_management命令来启动监控管理器,然后在浏览器输入http:localhost:15672 用户名和密码默认都为guest。
二、RabbitMQ常用的命令
启动监控管理器:rabbitmq-plugins enable rabbitmq_management
关闭监控管理器:rabbitmq-plugins disable rabbitmq_management
启动rabbitmq:rabbitmq-service start
关闭rabbitmq:rabbitmq-service stop
查看所有的队列:rabbitmqctl list_queues
清除所有的队列:rabbitmqctl reset
关闭应用:rabbitmqctl stop_app
启动应用:rabbitmqctl start_app
用户和权限设置
添加用户:rabbitmqctl add_user username password
分配角色:rabbitmqctl set_user_tags username administrator
新增虚拟主机:rabbitmqctl add_vhost vhost_name
将新虚拟主机授权给新用户:rabbitmqctl set_permissions -p vhost_name username '.*' '.*' '.*'
角色说明
none 最小权限角色
management 管理员角色
policymaker 决策者
monitoring 监控
administrator 超级管理员
三、RabbitMQ Java教程
1、简介
AMQP,即Advanced MessageQueuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,反之亦然。
AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。
RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。
下面将重点介绍RabbitMQ中的一些基础概念,了解了这些概念,是使用好RabbitMQ的基础。
2、引入依赖架包
3、 DEMO
1."HelloWorld!"
发送端代码:
package com.my.demo1;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 生产者
* <p>
* Created by YJH on 2017/11/8 21:59.
*/
public class Send {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置RabbitMQ相关信息
factory.setHost("localhost");
//factory.setUsername("");
//factory.setPassword("");
//factory.setPort(8888);
//创建一个新的连接
Connection connection = factory.newConnection();
//创建一个通道
Channel channel = connection.createChannel();
//声明一个队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//发送消息到队列中
for (int i = 0; i < 10; i++) {
String msg = "这是生产者发送的消息 ---> " + i;
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
System.out.println(" [x] 发送: '" + msg + "'");
Thread.sleep(5500);
}
//关闭通道和连接
channel.close();
connection.close();
}
}
接收端代码:
package com.my.demo1;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 消费者
* <p>
* Created by YJH on 2017/11/8 22:18.
*/
public class Recv {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务器地址
factory.setHost("localhost");
//创建一个新的连接
Connection connection = factory.newConnection();
//创建一个通道
Channel channel = connection.createChannel();
//声明要关注的队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages...");
//DefaultConsumer类实现了Consumer接口,通过传入一个通道
//告诉服务器我们需要那个频道的消息,如果频道中有消息,就会执行回调函数handleDelivery
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
super.handleDelivery(consumerTag, envelope, properties, body);
String msg = new String(body, "UTF-8");
System.out.println(" [x] 接收: '" + msg + "'");
}
};
//自动回复队列应答 -- RabbitMQ中的消息确认机制
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
|
注释:
ConnectionFactory、Connection、Channel都是RabbitMQ对外提供的API中最基本的对象。Connection是RabbitMQ的socket链接,它封装了socket协议相关部分逻辑。ConnectionFactory为Connection的制造工厂。
Channel是我们与RabbitMQ打交道的最重要的一个接口,我们大部分的业务操作是在Channel这个接口中完成的,包括定义Queue、定义Exchange、绑定Queue与Exchange、发布消息等。
Queue(队列)是RabbitMQ的内部对象,用于存储消息,用下图表示。
RabbitMQ中的消息都只能存储在Queue中,生产者(下图中的P)生产消息并最终投递到Queue中,消费者(下图中的C)可以从Queue中获取消息并消费。
queueDeclare()方法:
第一个参数表示队列名称、
第二个参数为是否持久化(true表示是,队列将在服务器重启时生存)、
第三个参数为是否是独占队列(创建者可以使用的私有队列,断开后自动删除)、
第四个参数为当所有消费者客户端连接断开时是否自动删除队列、
第五个参数为队列的其他参数
basicPublish()方法:
第一个参数为交换机名称、
第二个参数为队列映射的路由key、
第三个参数为消息的其他属性、
第四个参数为发送信息的主体
2. Work Queues
发送端代码:
package com.my.demo2;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* Created by YJH on 2017/11/8 23:02.
*/
public class NewTask {
private static final String TASK_QUEUE_NAME = "task_queue";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//队列——队列的名称
//持久——如果我们声明一个持久队列(队列将在服务器重启后幸存)
//互斥——如果我们声明一个排它队列(仅限于此连接),则为true。
//自动删除——如果我们说有一个自动删除”队列(服务器将删除它时,不再使用)
//参数——队列的其他属性(构造参数)
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
for (int i = 1; i <= 12; i++) {
String msg = "这是第-" + i + "-条消息.";
//exchange - 将消息发布到哪个exchange
//routingKey - 路由key(队列的名称)
//props - 消息的其他属性 - 路由头等
//body - 消息体
channel.basicPublish("", TASK_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes("UTF-8"));
System.out.println(" [x] 发送 '" + msg + "'");
}
channel.close();
connection.close();
}
}
|
接收端代码一:
package com.my.demo2;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* Created by YJH on 2017/11/8 23:17.
*/
public class Worker {
private static final String TASK_QUEUE_NAME = "task_queue";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
System.out.println(" [*] Waiting for messages...");
//设置一次分发的条数
channel.basicQos(10);
final 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(" [x] 接收: '" + message + "'");
try {
doWork(message);
} finally {
System.out.println(" [x] 完成");
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
//autoAck是否自动回复,如果为true的话,每次生产者只要发送信息就会从内存中删除,
//那么如果消费者程序异常退出,那么就无法获取数据,我们当然是不希望出现这样的情况,所以才去手动回复,
//每当消费者收到并处理信息然后在通知生成者。最后从队列中删除这条信息。如果消费者异常退出,如果还有其他消费者,
//那么就会把队列中的消息发送给其他消费者,如果没有,等消费者启动时候再次发送
boolean autoAck = false;
channel.basicConsume(TASK_QUEUE_NAME, autoAck, consumer);
}
private static void doWork(String task) {
for (char ch : task.toCharArray()) {
if (ch == '.') {
try {
Thread.sleep(2000);
} catch (InterruptedException _ignored) {
Thread.currentThread().interrupt();
}
}
}
}
}
|
接收端代码二:
package com.my.demo2;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* Created by YJH on 2017/11/8 23:17.
*/
public class Worker2 {
private static final String TASK_QUEUE_NAME = "task_queue";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
final Connection connection = factory.newConnection();
final Channel channel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
System.out.println(" [*] Waiting for messages...");
//设置一次分发的条数
channel.basicQos(1);
final 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(" [x] 接收 '" + message + "'");
try {
doWork(message);
} finally {
System.out.println(" [x] 完成");
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
//消息消费完成确认
channel.basicConsume(TASK_QUEUE_NAME, false, consumer);
}
private static void doWork(String task) {
for (char ch : task.toCharArray()) {
if (ch == '.') {
try {
Thread.sleep(2000);
} catch (InterruptedException _ignored) {
Thread.currentThread().interrupt();
}
}
}
}
}
|
注释:
channel.basicQos(1);保证一次只分发一个 。autoAck是否自动回复,如果为true的话,每次生产者只要发送信息就会从内存中删除,那么如果消费者程序异常退出,那么就无法获取数据,我们当然是不希望出现这样的情况,所以才去手动回复,每当消费者收到并处理信息然后在通知生成者。最后从队列中删除这条信息。如果消费者异常退出,如果还有其他消费者,那么就会把队列中的消息发送给其他消费者,如果没有,等消费者启动时候再次发送。
Message durability
如果我们希望即使在RabbitMQ服务重启的情况下,也不会丢失消息,我们可以将Queue与Message都设置为可持久化的(durable),这样可以保证绝大部分情况下我们的RabbitMQ消息不会丢失。但依然解决不了小概率丢失事件的发生(比如RabbitMQ服务器已经接收到生产者的消息,但还没来得及持久化该消息时RabbitMQ服务器就断电了),如果我们需要对这种小概率事件也要管理起来,那么我们要用到事务。由于这里仅为RabbitMQ的简单介绍,所以这里将不讲解RabbitMQ相关的事务。
Prefetch count
前面我们讲到如果有多个消费者同时订阅同一个Queue中的消息,Queue中的消息会被平摊给多个消费者。这时如果每个消息的处理时间不同,就有可能会导致某些消费者一直在忙,而另外一些消费者很快就处理完手头工作并一直空闲的情况。我们可以通过设置prefetchCount来限制Queue每次发送给每个消费者的消息数,比如我们设置prefetchCount=1,则Queue每次给每个消费者发送一条消息;消费者处理完这条消息后Queue会再给该消费者发送一条消息。
3. Publish/Subscribe
发送端代码:
package com.my.demo3;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 群发所有人
* <p>
* 发布者
* <p>
* Created by YJH on 2017/11/9 16:12.
*/
public class EmitLog {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT, true);
String msg = "info: 这是EmitLog消息";
channel.basicPublish(EXCHANGE_NAME, "", null, msg.getBytes("UTF-8"));
System.out.println(" [x] 发送 '" + msg + "'");
channel.close();
connection.close();
}
}
|
接收端代码一:
package com.my.demo3;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 订阅者
* <p>
* Created by YJH on 2017/11/9 16:21.
*/
public class ReceiveLogs {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//BuiltinExchangeType.FANOUT标识分发,所有的消费者得到同样的队列信息
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT, true);
//创建一个自动删除,非持久队列,并且获取它们名称
String queueName = channel.queueDeclare().getQueue();
//将队列绑定到Exchange,不需要额外的参数。
//queue —— 队列的名称
//exchange —— exchange的名称
//routingKey —— 路由主要用于绑定
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println(" [*] 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(" [x] 接收 '" + message + "' queueName = " + queueName);
}
};
channel.basicConsume(queueName, true, consumer);
}
}
|
接收端代码二:
package com.my.demo3;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 订阅者
* <p>
* Created by YJH on 2017/11/9 16:21.
*/
public class ReceiveLogs2 {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT, true);
//创建一个自动删除,非持久队列,并且获取它们名称
String queueName = channel.queueDeclare().getQueue();
//将队列绑定到Exchange,不需要额外的参数。
//queue —— 队列的名称
//exchange —— exchange的名称
//routingKey —— 路由主要用于绑定
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println(" [*] 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(" [x] 接收 '" + message + "' queueName = " + queueName);
}
};
channel.basicConsume(queueName, true, consumer);
}
}
|
注释:
在上两个demo的队列都指定了名称,但是现在我们不需要这么做,我们需要接收所有的信息,而不只是其中的一个。如果要做这样的队列,我们需要2件事,一个就是获取一个新的空的队列,这样我就需要创建一个随机名称的队列,最好让服务器帮我们做出选择,第二个就是我们断开用户的队列,应该自动进行删除。
Exchange
生产者将消息投递到Queue中,实际上这在RabbitMQ中这种事情永远都不会发生。实际的情况是,生产者将消息发送到Exchange(交换器,下图中的X),由Exchange将消息路由到一个或多个Queue中(或者丢弃)。
Binding
RabbitMQ中通过Binding将Exchange与Queue关联起来,这样RabbitMQ就知道如何正确地将消息路由到指定的Queue了
4. Routing
发送端代码:
package com.my.demo4;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* 对指定人群发
* <p>
* 发布者(发送"info", "warning", "error")
* <p>
* Created by YJH on 2017/11/9 22:06.
*/
public class EmitLogDirect {
private static final String EXCHANGE_NAME = "direct_logs";
/**
* 路由关键字
*/
private static final String[] routingKeys = new String[]{"info", "warning", "error"};
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
for (String key : routingKeys) {
String message = "你好!擎天柱";
channel.basicPublish(EXCHANGE_NAME, key, null, message.getBytes("UTF-8"));
System.out.println(" [x] 发送 '" + key + "':'" + message + "'");
}
channel.close();
connection.close();
}
}
|
接收端代码一:
package com.my.demo4;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.Arrays;
/**
* 订阅者(接收"error")
*
* Created by YJH on 2017/11/9 22:07.
*/
public class ReceiveLogsDirect {
//交换机名称
private static final String EXCHANGE_NAME = "direct_logs";
//路由key
private static final String[] routingKeys = new String[]{"info", "warning"};
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//获取匿名队列名称
String queueName = channel.queueDeclare().getQueue();
System.out.println(" [*] 绑定:"+ Arrays.toString(routingKeys));
//根据路由key进行绑定
for (String key : routingKeys) {
channel.queueBind(queueName, EXCHANGE_NAME, key);
}
System.out.println(" [*] 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(" [x] 接收 '" + envelope.getRoutingKey() + "':'" + message + "'");
}
};
channel.basicConsume(queueName, true, consumer);
}
}
|
接收端代码二:
package com.my.demo4;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.Arrays;
/**
* 订阅者(接收"error")
* <p>
* Created by YJH on 2017/11/9 22:07.
*/
public class ReceiveLogsDirect2 {
private static final String EXCHANGE_NAME = "direct_logs";
/**
* 路由关键字
*/
private static final String[] routingKeys = new String[]{"error"};
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//获取匿名队列名称
String queueName = channel.queueDeclare().getQueue();
System.out.println(" [*] 绑定:" + Arrays.toString(routingKeys));
//根据路由key进行绑定
for (String key : routingKeys) {
channel.queueBind(queueName, EXCHANGE_NAME, key);
}
System.out.println(" [*] 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(" [x] 接收 '" + envelope.getRoutingKey() + "':'" + message + "'");
}
};
channel.basicConsume(queueName, true, consumer);
}
}
|
注释:
采用路由的方式对不同的消息进行过滤,可以实现类似于群聊和单聊都可以
Binding key
在绑定(Binding)Exchange与Queue的同时,一般会指定一个binding key;消费者将消息发送给Exchange时,一般会指定一个routing key;当binding key与routing key相匹配时,消息将会被路由到对应的Queue中。在绑定多个Queue到同一个Exchange的时候,这些Binding允许使用相同的binding key。
binding key 并不是在所有情况下都生效,它依赖于Exchange Type,比如fanout类型的Exchange就会无视binding key,而是将消息路由到所有绑定到该Exchange的Queue。
Exchange Types
RabbitMQ常用的Exchange Type有fanout、direct、topic、headers这四种(AMQP规范里还提到两种ExchangeType,分别为system与自定义),下面分别进行介绍。
fanout
fanout类型的Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中。
上图中,生产者(P)发送到Exchange(X)的所有消息都会路由到图中的两个Queue,并最终被两个消费者(C1与C2)消费。(如demo3)
direct
direct类型的Exchange路由规则也很简单,它会把消息路由到那些binding key与routing key完全匹配的Queue中。
以上图的配置为例,我们以routingKey=”error”发送消息到Exchange,则消息会路由到Queue1(amqp.gen-S9b…,这是由RabbitMQ自动生成的Queue名称)和Queue2(amqp.gen-Agl…);如果我们以routingKey=”info”或routingKey=”warning”来发送消息,则消息只会路由到Queue2。如果我们以其他routingKey发送消息,则消息不会路由到这两个Queue中。
topic
前面讲到direct类型的Exchange路由规则是完全匹配binding key与routing key,但这种严格的匹配方式在很多情况下不能满足实际业务需求。topic类型的Exchange在匹配规则上进行了扩展,它与direct类型的Exchage相似,也是将消息路由到binding key与routing key相匹配的Queue中,但这里的匹配规则有些不同,它约定:
l routing key为一个句点号“. ”分隔的字符串(我们将被句点号“. ”分隔开的每一段独立的字符串称为一个单词),如“stock.usd.nyse”、“nyse.vmw”、“quick.orange.rabbit”
l binding key与routing key一样也是句点号“. ”分隔的字符串
l binding key中可以存在两种特殊字符“*”与“#”,用于做模糊匹配,其中“*”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)
以上图中的配置为例,routingKey=”quick.orange.rabbit”的消息会同时路由到Q1与Q2,routingKey=”lazy.orange.fox”的消息会路由到Q1与Q2,routingKey=”lazy.brown.fox”的消息会路由到Q2,routingKey=”lazy.pink.rabbit”的消息会路由到Q2(只会投递给Q2一次,虽然这个routingKey与Q2的两个bindingKey都匹配);routingKey=”quick.brown.fox”、routingKey=”orange”、routingKey=”quick.orange.male.rabbit”的消息将会被丢弃,因为它们没有匹配任何bindingKey。
headers
headers类型的Exchange不依赖于routingkey与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。
在绑定Queue与Exchange时指定一组键值对;当消息发送到Exchange时,RabbitMQ会取到该消息的headers(也是一个键值对的形式),对比其中的键值对是否完全匹配Queue与Exchange绑定时指定的键值对;如果完全匹配则消息会路由到该Queue,否则不会路由到该Queue。
该类型的Exchange没有用到过(不过也应该很有用武之地),所以不做介绍。
5. Topics
发送端代码:
package com.my.demo5;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.util.Arrays;
/**
* 提供者
* <p>
* Created by YJH on 2017/11/9 22:56.
*/
public class EmitLogTopic {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) {
Connection connection = null;
Channel channel = null;
try {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
connection = factory.newConnection();
channel = connection.createChannel();
//声明一个BuiltinExchangeType.TOPIC模式的交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
String[] routingKey = getRouting();
//发送消息
for (String key : routingKey) {
String msg = "这是使用TOPIC -- " + key;
channel.basicPublish(EXCHANGE_NAME, key, null, msg.getBytes("UTF-8"));
System.out.println(" [x] 发送 '" + key + "':'" + msg + "'");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (connection != null) {
try {
connection.close();
} catch (Exception ignore) {
ignore.printStackTrace();
}
}
}
}
private static String[] getRouting() {
//待发送的消息
String[] routingKeys = new String[]{
"quick.orange.rabbit",
"lazy.orange.elephant",
"quick.orange.fox",
"lazy.brown.fox",
"quick.brown.fox",
"quick.orange.male.rabbit",
"lazy.orange.male.rabbit"
};
System.out.println("routingKeys = " + Arrays.toString(routingKeys));
return routingKeys;
}
}
|
接收端代码一:
package com.my.demo5;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.Arrays;
/**
* 消费者(只接收 *.orange.*)
*
* "*" :可以替代一个词
* "#":可以替代0或者更多的词
* <p>
* Created by YJH on 2017/11/9 22:59.
*/
public class ReceiveLogsTopic {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//声明一个BuiltinExchangeType.TOPIC模式的交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
String queueName = channel.queueDeclare().getQueue();
String[] bindingKey = getRouting();
//绑定路由key
for (String key : bindingKey) {
channel.queueBind(queueName, EXCHANGE_NAME, key);
}
System.out.println(" [*] 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(" [x] 接收 '" + envelope.getRoutingKey() + "':'" + message + "'");
}
};
channel.basicConsume(queueName, true, consumer);
}
private static String[] getRouting() {
//待发送的消息
String[] routingKeys = new String[]{"*.orange.*"};
System.out.println("routingKeys = " + Arrays.toString(routingKeys));
return routingKeys;
}
}
|
接收端代码二:
package com.my.demo5;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.Arrays;
/**
* 消费者(只接收 "*.*.rabbit", "lazy.#")
* <p>
* "*" :可以替代一个词
* "#":可以替代0或者更多的词
* <p>
* Created by YJH on 2017/11/9 22:59.
*/
public class ReceiveLogsTopic2 {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//声明一个BuiltinExchangeType.TOPIC模式的交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
String queueName = channel.queueDeclare().getQueue();
String[] bindingKey = getRouting();
//绑定路由key
for (String key : bindingKey) {
channel.queueBind(queueName, EXCHANGE_NAME, key);
}
System.out.println(" [*] 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(" [x] 接收 '" + envelope.getRoutingKey() + "':'" + message + "'");
}
};
channel.basicConsume(queueName, true, consumer);
}
private static String[] getRouting() {
//待发送的消息
String[] routingKeys = new String[]{"*.*.rabbit", "lazy.#"};
System.out.println("routingKeys = " + Arrays.toString(routingKeys));
return routingKeys;
}
}
|
注释:
这种应该属于模糊匹配
* :可以替代一个词
#:可以替代0或者更多的词
6. Remote procedure call (RPC)
服务点代码:
public class RPCServer {
private static final String RPC_QUEUE_NAME = "rpc_queue";
private static int fib(int n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
return fib(n - 1) + fib(n - 1);
}
public static void main(String[] args) throws IOException, InterruptedException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
channel.basicQos(1);
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(RPC_QUEUE_NAME, false, consumer);
System.out.println("RPCServer Awating RPC request");
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
BasicProperties props = delivery.getProperties();
BasicProperties replyProps = new AMQP.BasicProperties.Builder().
correlationId(props.getCorrelationId()).build();
String message = new String(delivery.getBody(), "UTF-8");
int n = Integer.parseInt(message);
System.out.println("RPCServer fib(" + message + ")");
String response = "" + fib(n);
channel.basicPublish( "", props.getReplyTo(), replyProps, response.getBytes());
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
|
客户端代码:
public class RPCClient {
private Connection connection;
private Channel channel;
private String requestQueueName = "rpc_queue";
private String replyQueueName;
private QueueingConsumer consumer;
public RPCClient() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
connection = factory.newConnection();
channel = connection.createChannel();
replyQueueName = channel.queueDeclare().getQueue();
consumer = new QueueingConsumer(channel);
channel.basicConsume(replyQueueName, true, consumer);
}
public String call(String message) throws IOException, InterruptedException {
String response;
String corrID = UUID.randomUUID().toString();
AMQP.BasicProperties props = new AMQP.BasicProperties().builder()
.correlationId(corrID).replyTo(replyQueueName).build();
channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
if (delivery.getProperties().getCorrelationId().equals(corrID)) {
response = new String(delivery.getBody(), "UTF-8");
break;
}
}
return response;
}
public void close() throws Exception {
connection.close();
}
public static void main(String[] args) throws Exception {
RPCClient rpcClient = null;
String response;
try {
rpcClient = new RPCClient();
System.out.println("RPCClient Requesting fib(20)");
response = rpcClient.call("20");
System.out.println("RPCClient Got '" + response + "'");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (rpcClient != null) {
rpcClient.close();
}
}
}
}
|
注释:
如果需要远程计算机上运行一个函数,等待结果。这就是一个不同的故事了,这种模式通常被称为远程过程调用或者RPC。
服务器代码解读
1:建立连接,通道,队列
2:我们可能运行多个服务器进程,为了分散负载服务器压力,我们设置channel.basicQos(1);
3:我们用basicconsume访问队列。然后进入循环,在其中我们等待请求消息并处理消息然后发送响应。
客户端代码解读
1:建立一个连接和通道,并声明了一个唯一的“回调”队列的答复
2:我们订阅回调队列,这样就可以得到RPC的响应
3:定义一个call方法用于发送当前的回调请求
4:生成一个唯一的correlationid,然后通过while循环来捕获合适的回应
5:我们请求信息,发送2个属性,replyTo 和correlationId
6:然后就是等待直到有合适的回应到达
7:while循环是做一个非常简单的工作,对于每一个响应消息,它检查是否有correlationid然后进行匹配。然后是就进行响应。
8:最后把响应返回到客户端。
Message属性:
AMQP协议一共预定义了14个属性,但是大多数属性很少使用,下面几个可能用的比较多
deliveryMode:有2个值,一个是持久,另一个表示短暂(第二篇说过)
contentType:内容类型:用来描述编码的MIME类型。例如,经常使用JSON编码是将此属性设置为一个很好的做法:application/json。
replyTo:经常使用的是回调队列的名字
correlationid:RPC响应请求的相关应用
Correlation Id
在队列上接收到一个响应,但它并不清楚响应属于哪一个,当我们使用CorrelationId属性的时候,我们就可以将它设置为每个请求的唯一值,稍后当我们在回调队列中接收消息的时候,我们会看到这个属性,如果我们看到一个未知的CorrelationId,我们就可以安全地忽略信息-它不属于我们的请求。为什么我们应该忽略未知的消息在回调队列中,而不是失败的错误?这是由于服务器端的一个竞争条件的可能性。比如还未发送了一个确认信息给请求,但是此时RPC服务器挂了。如果这种情况发生,将再次重启RPC服务器处理请求。这就是为什么在客户端必须处理重复的反应。
rpc工作方式:
1:当客户端启动时,它创建一个匿名的独占回调队列。
2:对于rpc请求,客户端发送2个属性,一个是replyTo设置回调队列,另一是correlationId为每个队列设置唯一值
3:请求被发送到一个rpc_queue队列中
4:rpc服务器是等待队列的请求,当收到一个请求的时候,他就把消息返回的结果返回给客户端,使请求结束。
5:客户端等待回调队列上的数据,当消息出现的时候,他检查correlationId,如果它和从请求返回的值匹配,就进行响应。
四、HTML页面与RabbitMQ进行交互
1、安装插件
在控制台内输入:
启动stomp有关的一系列插件
rabbitmq-plugins enable rabbitmq_management rabbitmq_web_stomp rabbitmq_stomp rabbitmq_web_stomp_examples |
查看已启动了哪些RabbitMQ插件
重启rabbitmq
services restart rabbitmq |
可以在15670端口访问web-stomp-examples,RabbitMQ运行在15672端口,stomp服务运行在15674端口。
2、引入js文件
3、HTML代码
queue_1.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>queue_1.html</title>
<style>
.box {
width: 440px;
float: left;
margin: 0 20px 0 20px;
}
.box div, .box input {
border: 1px solid;
-moz-border-radius: 4px;
border-radius: 4px;
width: 100%;
padding: 5px;
margin: 3px 0 10px 0;
}
.box div {
border-color: grey;
height: 300px;
overflow: auto;
}
div code {
display: block;
}
#first div code {
-moz-border-radius: 2px;
border-radius: 2px;
border: 1px solid #eee;
margin-bottom: 5px;
}
#second div {
font-size: 0.8em;
}
body {
font-family: "Arial";
color: #444;
}
h1, h2 {
color: #f60;
font-weight: normal;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
margin: 0;
}
a {
color: #f60;
border: 1px solid #fda;
background: #fff0e0;
border-radius: 3px;
-moz-border-radius: 3px;
padding: 2px;
text-decoration: none;
/* font-weight: bold; */
}
ul.menu {
list-style-type: none;
padding: 0;
margin: 0;
}
ul.menu li {
padding: 5px 0;
}
</style>
</head>
<body>
<div id="first" class="box">
<h2>接收</h2>
<div></div>
<form><input autocomplete="off" value="" placeholder="输入要发送的消息"/></form>
</div>
<div id="second" class="box">
<h2>日志</h2>
<div></div>
</div>
<script type="text/javascript" src="../js/jquery-1.8.3.js"></script>
<script type="text/javascript" src="../js/sockjs-0.3.min.js"></script>
<script type="text/javascript" src="../js/stomp.js"></script>
<script type="text/javascript">
$(document).ready(function () {
var has_had_focus = false;
var pipe = function (el_name, send) {
var div = $(el_name + ' div');
var inp = $(el_name + ' input');
var form = $(el_name + ' form');
var print = function (m, p) {
p = (p === undefined) ? '' : JSON.stringify(p);
div.append($("<code>").text(m + ' ' + p));
div.scrollTop(div.scrollTop() + 10000);
};
if (send) {
form.submit(function () {
send(inp.val());
inp.val('');
return false;
});
}
return print;
};
// Stomp.js boilerplate
var client = Stomp.client('ws://192.168.1.191:15674/ws');
client.debug = pipe('#second');
var print_first = pipe('#first', function (data) {
client.send('/exchange/ex_test/Q.3', {}, data);
});
var on_connect = function (x) {
console.log(x.toString());
var id = client.subscribe("/exchange/ex_test/Q.*", function (d) {
print_first(d.body);
});
console.log("\n" + id.toString());
};
var on_error = function (c) {
alert("error" + c.toString());
};
client.connect('admin', 'admin', on_connect, on_error, '/');
$('#first input').focus(function () {
if (!has_had_focus) {
has_had_focus = true;
$(this).val("");
}
});
});
</script>
</body>
</html>
|
queue_2.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>queue_2.html</title>
<style>
.box {
width: 440px;
float: left;
margin: 0 20px 0 20px;
}
.box div, .box input {
border: 1px solid;
-moz-border-radius: 4px;
border-radius: 4px;
width: 100%;
padding: 5px;
margin: 3px 0 10px 0;
}
.box div {
border-color: grey;
height: 300px;
overflow: auto;
}
div code {
display: block;
}
#first div code {
-moz-border-radius: 2px;
border-radius: 2px;
border: 1px solid #eee;
margin-bottom: 5px;
}
#second div {
font-size: 0.8em;
}
body {
font-family: "Arial";
color: #444;
}
h1, h2 {
color: #f60;
font-weight: normal;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
margin: 0;
}
a {
color: #f60;
border: 1px solid #fda;
background: #fff0e0;
border-radius: 3px;
-moz-border-radius: 3px;
padding: 2px;
text-decoration: none;
/* font-weight: bold; */
}
ul.menu {
list-style-type: none;
padding: 0;
margin: 0;
}
ul.menu li {
padding: 5px 0;
}
</style>
</head>
<body>
<div id="first" class="box">
<h2>接收</h2>
<div></div>
<form><input autocomplete="off" value="" placeholder="输入要发送的消息"/></form>
</div>
<div id="second" class="box">
<h2>日志</h2>
<div></div>
</div>
<script type="text/javascript" src="../js/jquery-1.8.3.js"></script>
<script type="text/javascript" src="../js/sockjs-0.3.min.js"></script>
<script type="text/javascript" src="../js/stomp.js"></script>
<script type="text/javascript">
$(document).ready(function () {
var has_had_focus = false;
var pipe = function (el_name, send) {
var div = $(el_name + ' div');
var inp = $(el_name + ' input');
var form = $(el_name + ' form');
var print = function (m, p) {
p = (p === undefined) ? '' : JSON.stringify(p);
div.append($("<code>").text(m + ' ' + p));
div.scrollTop(div.scrollTop() + 10000);
};
if (send) {
form.submit(function () {
send(inp.val());
inp.val('');
return false;
});
}
return print;
};
// Stomp.js boilerplate
var client = Stomp.client('ws://192.168.1.191:15674/ws');
client.debug = pipe('#second');
var h1 = {'userId': 'queue_2'};
var print_first = pipe('#first', function (data) {
client.send('/exchange/ex_test/P2', {}, data);
});
var on_connect = function (x) {
console.log(x.toString());
var id = client.subscribe("/exchange/ex_test/P2", function (d) {
print_first(d.body);
});
console.log("\n" + id.toString());
};
var on_error = function (c) {
alert("error" + c.toString());
};
client.connect('admin', 'admin', on_connect, on_error, '/');
$('#first input').focus(function () {
if (!has_had_focus) {
has_had_focus = true;
$(this).val("");
}
});
});
</script>
</body>
</html>
|
queue_3.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>queue_3.html</title>
<style>
.box {
width: 440px;
float: left;
margin: 0 20px 0 20px;
}
.box div, .box input {
border: 1px solid;
-moz-border-radius: 4px;
border-radius: 4px;
width: 100%;
padding: 5px;
margin: 3px 0 10px 0;
}
.box div {
border-color: grey;
height: 300px;
overflow: auto;
}
div code {
display: block;
}
#first div code {
-moz-border-radius: 2px;
border-radius: 2px;
border: 1px solid #eee;
margin-bottom: 5px;
}
#second div {
font-size: 0.8em;
}
body {
font-family: "Arial";
color: #444;
}
h1, h2 {
color: #f60;
font-weight: normal;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
margin: 0;
}
a {
color: #f60;
border: 1px solid #fda;
background: #fff0e0;
border-radius: 3px;
-moz-border-radius: 3px;
padding: 2px;
text-decoration: none;
/* font-weight: bold; */
}
ul.menu {
list-style-type: none;
padding: 0;
margin: 0;
}
ul.menu li {
padding: 5px 0;
}
</style>
</head>
<body>
<div id="first" class="box">
<h2>接收</h2>
<div></div>
<form><input autocomplete="off" value="" placeholder="输入要发送的消息"/></form>
</div>
<div id="second" class="box">
<h2>日志</h2>
<div></div>
</div>
<script type="text/javascript" src="../js/jquery-1.8.3.js"></script>
<script type="text/javascript" src="../js/sockjs-0.3.min.js"></script>
<script type="text/javascript" src="../js/stomp.js"></script>
<script type="text/javascript">
$(document).ready(function () {
var has_had_focus = false;
var pipe = function (el_name, send) {
var div = $(el_name + ' div');
var inp = $(el_name + ' input');
var form = $(el_name + ' form');
var print = function (m, p) {
p = (p === undefined) ? '' : JSON.stringify(p);
div.append($("<code>").text(m + ' ' + p));
div.scrollTop(div.scrollTop() + 10000);
};
if (send) {
form.submit(function () {
send(inp.val());
inp.val('');
return false;
});
}
return print;
};
// Stomp.js boilerplate
var client = Stomp.client('ws://192.168.1.191:15674/ws');
client.debug = pipe('#second');
var h1 = {'userId': 'queue_3'};
var print_first = pipe('#first', function (data) {
client.send('/exchange/ex_test/Q.1', {}, data);
});
var on_connect = function (x) {
console.log(x.toString());
var id = client.subscribe("/exchange/ex_test/Q.*", function (d) {
print_first(d.body);
});
console.log("\n" + id.toString());
};
var on_error = function (c) {
alert("error" + c.toString());
};
client.connect('admin', 'admin', on_connect, on_error, '/');
$('#first input').focus(function () {
if (!has_had_focus) {
has_had_focus = true;
$(this).val("");
}
});
});
</script>
</body>
</html>
|
注释
queue_1和queue_2私聊,queue_3收不到消息
js订阅/发送总结
/queue/queuename:使用默认转发器订阅/发布消息,默认由stomp自动创建一个持久化队列
/amq/queue/queuename:与/queue/queuename的区别在于队列不由stomp自动进行创建,队列不存在失败
/topic/routing_key:通过amq.topic转发器订阅/发布消息,订阅时默认创建一个临时队列,通过routing_key与topic进行绑定
/temp-queue/xxx:创建一个临时队列(只能在headers中的属性reply-to中使用),可用于发送消息后通过临时队列接收回复消息,接收通过client.onreceive
/exchange/exchangename/[routing_key]:通过转发器订阅/发布消息,转发器需要手动创建
client.subscribe(destination,callback,headers):订阅消息
client.send(destination,headers,body):发布消息
client.unsubscribe(id):取消订阅,id为订阅时返回的编号
client.onreceive:默认接收回调从临时队列获取消息
五、参考内容
官网:http://www.rabbitmq.com/
教程:http://www.rabbitmq.com/getstarted.html
一些好的中文教程:
RabbitMQ安装和各种模式介绍:http://www.cnblogs.com/LipeiNet/p/5973061.html
js连接RabbitMQ达到实时消息推送:https://www.cnblogs.com/puyangsky/p/6666624.html
RabbitMQ基础概念介绍:http://blog.csdn.net/whycold/article/details/41119807
RabbitMQ使用场景练习:STOMP plugin:http://blog.csdn.net/azhegps/article/details/53815283
通过WebSocket STOMP(js连接RabbitMQ)英文文档:http://jmesnil.net/stomp-websocket/doc/
中文翻译:https://segmentfault.com/a/1190000006617344
RabbitMQ STOMP Adapter(js连接RabbitMQ):http://www.rabbitmq.com/stomp.html