RabbitMQ总结


一、环境搭建

第一步:软件安装

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-plugins list

重启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

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值