什么是发布订阅

发布订阅是一种设计模式定义了一对多的依赖关系,让多个订阅者对象同时监听某一个主题对象。这个主题对象在自身状态变化时,会通知所有的订阅者对象,使他们能够自动更新自己的状态。

为了描述这种模式,我们将会构建一个简单的日志系统。它包括两个程序——第一个程序负责发送日志消息,第二个程序负责获取消息并输出内容。在我们的这个日志系统中,所有正在运行的接收方程序都会接受消息。我们用其中一个接收者(receiver)把日志写入硬盘中,另外一个接受者(receiver)把日志输出到屏幕上。最终,日志消息被广播给所有的接受者(receivers)。

Exchanges

RabbitMQ消息模型的核心理念是生产者永远不会直接发送任何消息给队列,生产者只能发送消息给到exchange,exchange比较简单,一边从生产者就收消息,一边把消息推送到队列中。exchange必须清楚的知道消息应该按照什么规则路由到对应的队列中,而具体使用那种路由算法是由exchange type决定的。AMQP协议提供了四种交换机类型:

Name(交换机类型)

Default pre-declared names(预声明的默认名称)

Direct exchange(直连交换机)

(Empty string) and amq.direct

Fanout exchange(扇型交换机)

amq.fanout

Topic exchange(主题交换机)

amq.topic

Headers exchange(头交换机)

amq.match (and amq.headers in RabbitMQ)

除交换机类型外,在声明交换机时还可以附带许多其他的属性,其中最重要的几个分别是:

  • Name
  • Durability (消息代理重启后,交换机是否还存在)
  • Auto-delete (当所有与之绑定的消息队列都完成了对此交换机的使用后,删掉它)
  • Arguments(依赖代理本身)

    交换机可以有两个状态:持久(durable)、暂存(transient)。持久化的交换机会在消息代理(broker)重启后依旧存在,而暂存的交换机则不会(它们需要在代理再次上线后重新被声明)。然而并不是所有的应用场景都需要持久化的交换机。

    本文中具体讲解下以下两种交换机:直连交换机(前面几个例子中使用的交换机类型),扇形交换机(本文中要使用的交换机类型)

    直连交换机

    直连交换机(direct exchange)可以使用消息携带的路由键(routing key)将消息投递给对应的队列中。用来处理消息的单播路由(unicast routing),也可以处理多播路由。

    那么它具体是如何工作的呢

    • 将一个队列绑定到某个交换机上,同时给该绑定指定一个路由键(routing key)
    • 当一个携带路由键为R的消息被发送到直连交换机时,交换机会把它路由给绑定值同样为R的队列。

    直连交换机经常用来循环分发任务给多个工作者,当这样做时,一定要明白,这时消息的负载均衡是发生在消费者(consumer)之间的,而不是队列(queue)中。

    直连交换机图例:

    扇形交换机

    扇形交换机(funout exchange)将消息路由给绑定到它身上的所有队列,不关心所绑定的路由键(routing key)。扇形交换机用来处理消息的广播路由(broadcast routing)。

    由于扇形交换机投递消息到所有绑定他的队列,以下几个场景比较适合使用扇形交换机:

    • 大规模多用户在线(MMO)游戏可以使用它来处理排行榜更新等全局事件
    • 体育新闻网站可以用它来近乎实时地将比分更新分发给移动客户端
    • 分发系统使用它来广播各种状态和配置更新
    • 在群聊的时候,它被用来分发消息给参与群聊的用户。(AMQP没有内置presence的概念,因此XMPP可能会是个更好的选择)

    扇形交换机图例

    创建exchange

     

                        channel.ExchangeDeclare(exchange: "log_exchange", //exchange 名称

                            type: ExchangeType.Fanout, //exchange 类型

                            durable: false,

                            autoDelete: false,

                            arguments: null);

     

    临时队列

    之前的几个示例中我们在为每一个声名的队列都指定了一个名字,因为我们希望consumer指向正确的队列。当我们希望在生产者和消费者之间共享队列时,为队列命名就非常的重要了。

    不过我们要实现的日志系统只是想要得到所有的消息,而且只对当前正在传递的消息感兴趣,并不关心队列的名称,所以为了满足我们的需求,要做两件事情:

    无论什么时间连接到RabbitMQ我们都需要一个新的空的队列。为了达到目的我们可以使用随机数创建队列,或让服务器给我们提供一个随机的名称。

    一旦消费者与RabbitMQ断开,消费者所接受的队列都应该被自动删除。

    创建临时队列

     

                        //创建一个未命名的新的消息队列,

                        QueueDeclareOk queue = channel.QueueDeclare(queue: "", //队列名称,为空时有系统自动分配

                            durable: false,

                            exclusive: false,

                            autoDelete: true,//自动删除,如果该队列没有任何订阅的消费者的话,该队列会被自动删除。这种队列适用于临时队列。

                            arguments: null);

                        //或

                        //queue = channel.QueueDeclare();

     

    绑定

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

    创建交换机与队列的关系

     

    //扇形交换机(funout exchange)将消息路由给绑定到它身上的所有队列,不关心所绑定的路由键(routing key)

                        //fanout exchange不需要指定routing key 指定了也没用

                        //通过绑定告诉exchange 需要发送消息到哪些消息队列

                        channel.QueueBind(queue: queueName, exchange: EXCHANGE_NAME, routingKey: ROUTING_KEY, arguments: null);

     

    完整代码:

    生产者  Pub_SubProducer.cs

     

    using System;

    using System.Collections.Generic;

    using System.Linq;

    using System.Text;

    using System.Threading.Tasks;

    using RabbitMQ.Client;

     

    namespace RabbitMQProducer

    {

        public class Pub_SubProducer

        {

            const string EXCHANGE_NAME = "log_exchange";

            const string ROUTING_KEY = "";

     

            //直接发送消息到交换机

            public static void Publish()

            {

                var factory = new ConnectionFactory()

                {

                    HostName = "127.0.0.1"

                };

                using (var connection = factory.CreateConnection())

                {

                    using (IModel channel = connection.CreateModel())

                    {

                        channel.ExchangeDeclare(exchange: EXCHANGE_NAME, //exchange 名称

                            type: ExchangeType.Fanout, //exchange 类型

                            durable: false,

                            autoDelete: false,

                            arguments: null);

     

                        Parallel.For(1, 100, item =>

                        {

                            string message = $"日志内容{DateTime.Now.ToString()}";

                            channel.BasicPublish(exchange: EXCHANGE_NAME, routingKey: ROUTING_KEY, basicProperties: null, body: Encoding.UTF8.GetBytes(message));

                            Console.WriteLine(message);

                        });

     

                        Console.WriteLine(" Press [enter] to exit.");

                        Console.ReadLine();

                    }

                }

            }

        }

    }

     

    消费者 Pub_SubConsumer.cs

     

    using RabbitMQ.Client;

    using System;

    using System.Collections.Generic;

    using System.Linq;

    using System.Text;

    using System.Threading.Tasks;

    using RabbitMQ.Client.Events;

    using System.IO;

     

    namespace RabbitMQConsumer

    {

        public class Pub_SubConsumer

        {

            const string EXCHANGE_NAME = "log_exchange";

            const string ROUTING_KEY = "";

            //输出到屏幕

            public static void Subscribe()

            {

                var factory = new ConnectionFactory()

                {

                    HostName = "127.0.0.1"

                };

                using (var connection = factory.CreateConnection())

                {

                    using (IModel channel = connection.CreateModel())

                    {

                        channel.ExchangeDeclare(exchange: EXCHANGE_NAME, //exchange 名称

                            type: ExchangeType.Fanout, //exchange 类型

                            durable: false,

                            autoDelete: false,

                            arguments: null);

     

                        //创建一个未命名的新的消息队列,

                        QueueDeclareOk queue = channel.QueueDeclare(queue: "", //队列名称,为空时有系统自动分配

                            durable: false,

                            exclusive: false,

                            autoDelete: true,//自动删除,如果该队列没有任何订阅的消费者的话,该队列会被自动删除。这种队列适用于临时队列。

                            arguments: null);

                        //或

                        //queue = channel.QueueDeclare();

     

                        string queueName = queue.QueueName;

                        //扇形交换机(funout exchange)将消息路由给绑定到它身上的所有队列,不关心所绑定的路由键(routing key)

                        //fanout exchange不需要指定routing key 指定了也没用

                        //通过绑定告诉exchange 需要发送消息到哪些消息队列

                        channel.QueueBind(queue: queueName, exchange: EXCHANGE_NAME, routingKey: ROUTING_KEY, arguments: null);

     

                        EventingBasicConsumer consumer = new EventingBasicConsumer(channel);

                        consumer.Received += (sender, args) =>

                        {

                            string message = Encoding.UTF8.GetString(args.Body);

                            Console.WriteLine(message);

                        };

     

                        channel.BasicConsume(queue: queueName, noAck: true, consumer: consumer);

     

                        Console.WriteLine(" Press [enter] to exit.");

                        Console.ReadLine();

                    }

                }

            }

     

            /// <summary>

            /// 输出到文件

            /// </summary>

            public static void SubscribeFile()

            {

                var factory = new ConnectionFactory()

                {

                    HostName = "127.0.0.1"

                };

                using (var connection = factory.CreateConnection())

                {

                    using (IModel channel = connection.CreateModel())

                    {

                        channel.ExchangeDeclare(exchange: EXCHANGE_NAME, //exchange 名称

                            type: ExchangeType.Fanout, //exchange 类型

                            durable: false,

                            autoDelete: false,

                            arguments: null);

     

                        //创建一个未命名的新的消息队列,

                        QueueDeclareOk queue = channel.QueueDeclare(queue: "", //队列名称,为空时有系统自动分配

                            durable: false,

                            exclusive: false,

                            autoDelete: true,//自动删除,如果该队列没有任何订阅的消费者的话,该队列会被自动删除。这种队列适用于临时队列。

                            arguments: null);

                        //或

                        //queue = channel.QueueDeclare();

     

                        string queueName = queue.QueueName;

                        //扇形交换机(funout exchange)将消息路由给绑定到它身上的所有队列,不关心所绑定的路由键(routing key)

                        //fanout exchange不需要指定routing key 指定了也没用

                        //通过绑定告诉exchange 需要发送消息到哪些消息队列

                        channel.QueueBind(queue: queueName, exchange: EXCHANGE_NAME, routingKey: ROUTING_KEY, arguments: null);

     

                        EventingBasicConsumer consumer = new EventingBasicConsumer(channel);

                        consumer.Received += (sender, args) =>

                        {

                            string message = Encoding.UTF8.GetString(args.Body);

     

                            //写入日志到txt文件

                            using (StreamWriter writer = new StreamWriter(@"c:\log\log.txt", true, Encoding.UTF8))

                            {

                                writer.WriteLine(message);

                                writer.Close();

                            }

     

                            Console.WriteLine(message);

                        };

     

                        channel.BasicConsume(queue: queueName, noAck: true, consumer: consumer);

     

                        Console.WriteLine(" Press [enter] to exit.");

                        Console.ReadLine();

                    }

                }

            }

        }

    }

     

    运行以上实例代码发现,每个订阅者实例 都能得到相同的内容。

RabbitMQ入门教程——发布/订阅的更多相关文章

  1. RabbitMQ入门:发布/订阅(Publish/Subscribe)

    在前面的两篇博客中 RabbitMQ入门:Hello RabbitMQ 代码实例 RabbitMQ入门:工作队列(Work Queue) 遇到的实例都是一个消息只发送给一个消费者(工作者),他们的消息 ...

  2. RabbitMQ入门(3)——发布/订阅(Publish/Subscribe)

    在上一篇RabbitMQ入门(2)--工作队列中,有一个默认的前提:每个任务都只发送到一个工作人员.这一篇将介绍发送一个消息到多个消费者.这种模式称为发布/订阅(Publish/Subscribe). ...

  3. RabbitMQ入门教程(五):扇形交换机发布/订阅(Publish/Subscribe)

    原文:RabbitMQ入门教程(五):扇形交换机发布/订阅(Publish/Subscribe) 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. ...

  4. RabbitMQ入门教程(十七):消息队列的应用场景和常见的消息队列之间的比较

    原文:RabbitMQ入门教程(十七):消息队列的应用场景和常见的消息队列之间的比较 分享一个朋友的人工智能教程.比较通俗易懂,风趣幽默,感兴趣的朋友可以去看看. 这是网上的一篇教程写的很好,不知原作 ...

  5. RabbitMQ入门教程(八):远程过程调用RPC

    原文:RabbitMQ入门教程(八):远程过程调用RPC 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.cs ...

  6. RabbitMQ入门教程(四):工作队列(Work Queues)

    原文:RabbitMQ入门教程(四):工作队列(Work Queues) 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https:/ ...

  7. RabbitMQ入门教程(二):简介和基本概念

    原文:RabbitMQ入门教程(二):简介和基本概念 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn ...

  8. RabbitMQ入门教程(十四):RabbitMQ单机集群搭建

    原文:RabbitMQ入门教程(十四):RabbitMQ单机集群搭建 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://b ...

  9. RabbitMQ入门教程(十二):消息确认Ack

    原文:RabbitMQ入门教程(十二):消息确认Ack 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csd ...

随机推荐

  1. 探求网页同步提交、ajax和comet不为人知的秘密(上篇)

    标题里的技术都是web开发里最常见的技术,但是我想这些常用的技术有很多细节是很多朋友不太清楚的,理解这些细节是我们深入掌握这些技术的一把钥匙,今天我就讲讲我使用这些技术时体会到的这些细节. 同步提交是 ...

  2. swift 加载 storyboard 里的UIViewController

    let storyBoard:UIStoryboard! = UIStoryboard(name: "Main", bundle: nil) let deskVC:DeskView ...

  3. 解决服务器每次都要输入Enter PEM pass phrase

    今天架设好Python的HTTPS云服务器, 发现每次连接都要Enter PEM pass phrase 把服务器端的key里面的key剥离掉就好了 openssl rsa -in server.ke ...

  4. Atitit 多继承实现解决方案 java c#

    Atitit 多继承实现解决方案 java c# Java c#都没有提供多继承的解决方案..默认从语言级别以及没办法多继承了. 只可以崽类库的级别实现拉.. 继承的原理就是,使用一个内部super指 ...

  5. Mybatis中SqlMapper配置的扩展与应用(1)

    奋斗了好几个晚上调试程序,写了好几篇博客,终于建立起了Mybatis配置的扩展机制.虽然扩展机制是重要的,然而如果没有真正实用的扩展功能,那也至少是不那么鼓舞人心的,这篇博客就来举几个扩展的例子. 这 ...

  6. Java EE开发平台随手记1

    过完春节以来,一直在负责搭建公司的新Java EE开发平台,所谓新平台,其实并不是什么新技术,不过是将目前业界较为流行的框架整合在一起,做一些简单的封装和扩展,让开发人员更加易用. 和之前负责具体的项 ...

  7. SharePoint Server 2013开发之旅(四):配置工作流开发和测试环境

    工作流这个功能,在SharePoint Server 2013中做了很大的改动.我们可以从微软官方的文档中了解一下大概的情况 http://technet.microsoft.com/zh-cn/li ...

  8. 修改NLS_DATE_FORMAT的四种方式

    一. 在用户环境变量中指定(LINUX) 在用户的.bash_profile中增加两句: export NLS_LANG=AMERICAN ---这一句必须指定,否则下一句不生效.export NLS ...

  9. SVN代码回滚

    取消对代码的修改分为两种情况:   第一种情况:改动没有被提交(commit). 这种情况下,使用svn revert就能取消之前的修改. svn revert用法如下: # svn revert [ ...

  10. 启动Genymotion时报错Failed to initialize backend EGL display

    在启动Genymotion的时候报错: video card说的是显卡,你的显卡可能不支持  OpenGL2.0,或者你装的驱动有问题. 解决办法:将驱动重新安装一下. 可直接下载一个如“驱动人生“一 ...