代码改变世界

一个winform带你玩转rabbitMQ

2016-11-04 09:52  地图315  阅读(399)  评论(0编辑  收藏  举报

源码已放出 https://github.com/dubing/MaoyaRabbit

本章分3部分

一、安装部署初探

二、进阶

三、api相关


安装 部署 初探

先上图


一. 安装部署

  下载 rabbitMQ :http://www.rabbitmq.com/download.html

  安装rabbitmq需要erlang,下载erlang:http://www.erlang.org/download.html

  按照官网按照步骤,例如windows http://www.rabbitmq.com/install-windows.html 

  安装完rabbitMQ可以再启动插件扩展,其中包含了一个管理后台

  

  最新版本的后台地址为 http://localhost:15672/ 

  

  用户名和密码都为guest,输入完成进入主菜单

  

  功能很丰富,可以查看当前服务器的交换机,队列,消息,连接,会话等得使用情况。

  基本上到这里服务器的安装部署环节算是ok,很简单。


 

二.  简介

  要了解rabbitMQ 首先要了解AMQP协议 百科上给的很详细 http://baike.baidu.com/view/4023136.htm?fr=aladdin

  AMQP 有四个非常重要的概念:虚拟机(virtual host),通道(exchange),队列(queue)和绑定(binding)。

  虚拟机: 通常是应用的外在边界,我们可以为不同的虚拟机分配访问权限。虚拟机可持有多个交换机、队列和绑定。
  交换机: 从连接通道(Channel)接收消息,并按照特定的路由规则发送给队列。
  队列: 消息最终的存储容器,直到消费客户端(Consumer)将其取走。
  绑定: 也就是所谓的路由规则,告诉交换机将何种类型的消息发送到某个队列中。

  这个概念很重要 不然在学习rabbitmq的地方会碰到很多困难。想要进阶学习的可以参考 https://www.rabbitmq.com/tutorials/amqp-concepts.html

  借用官方一个图来阐述AMQP

  

  RabbitMQ是一个消息代理。它的核心原理非常简单:接收和发送消息。

  你可以把它想像成一个邮局:你把信件放入邮箱,邮递员就会把信件投递到你的收件人处。在这个比喻中,RabbitMQ是一个邮箱、邮局、邮递员。RabbitMQ和邮局的主要区别是,它处理的不是纸,而是接收、存储和发送二进制的数据——消息。

  对于rabbitMQ本身的特点 参考官网 http://www.rabbitmq.com/features.html

  1、可靠性(Reliability)
  RabbitMQ提供很多特性供我们可以在性能和可靠性作出折中的选择,包括持久化、发送确认、发布者确认和高可用性等。
  2、弹性选路(Flexible Routing)
  消息在到达队列前通过交换(exchanges)来被选路。RabbitMQ为典型的选路逻辑设计了几个内置的交换类型。对于更加复杂的选路,我们可以将exchanges绑定在一起或者写属于自己的exchange类型插件。
  3、集群化(Clustering)
  在一个局域网内的几个RabbitMQ服务器可以集群起来,组成一个逻辑的代理人。
  4、联盟(Federation)
  对于那些需要比集群更加松散和非可靠连接的服务器来说,RabbitMQ提供一个联盟模型(Federation Model)
  5、高可用队列(High Available Queue)
  可以在一个集群里的几个机器里对队列做镜像,确保即时发生了硬件失效,你的消息也是安全的。
  6、多客户端(Many Clients)
  有各种语言的RabbitMQ客户端
  7、管理UI(Management UI)
  RabbitMQ提供一个易用的管理UI来监控和控制消息代理人的各个方面。
  8、跟踪(Tracing)
  如果你的消息系统行为异常,RabbitMQ提供跟踪支持来找出错误的根源。
  9、插件系统(Plugin System)
  RabbitMQ提供各种方式的插件扩展,我们可以实现自己的插件。

  使用任务队列一个优点是能够轻易地并行处理任务。当处理大量积压的任务,只要增加工作队列,通过这个方式,能够实现轻易的缩放。


 

三. 初探

  文中的winform所采取的client为官方的.net版本 https://github.com/rabbitmq/rabbitmq-dotnet-client

  首先是Connection和Channel的概念

  Connection 建立与rabbitmq server的一个连接,由ConnectionFactory创建,Channel建立在connection基础上的一个频道,相对于connection来说,它是轻量级的。可以理解成一次会话。

  代码示例 本机环境

1
2
3
4
5
6
7
    using (IConnection connection = factory.CreateConnection())
    {
        using (IModel channel = connection.CreateModel())
        {
//do something
        }
    }

  exchange常用有三种类型:

  Direct :处理路由键。需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。这是一个完整的匹配。
  Fanout :不处理路由键。你只需要简单的将队列绑定到交换机上。一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。
  Topic : 将路由键和某模式进行匹配。此时队列需要绑定要一个模式上。符号“#”匹配一个或多个词,符号“*”匹配不多不少一个词。

  还有一种多重属性的类型headers,我们在下一章节讨论。

  我们用winform分别造成三种类型的exchange来实际体验一下

  

  这里所谓的限定exchange是在我们安装rabbitmq server的时候自动生成的一些 我们的测试不使用这些exchange。

  然后我们新建3个Queue,这里我们会发现一个有趣的现象,rabbitmq server对于新生成的队列都会默认绑定在一个名称为“”的默认exchange上。

  先试试direct类型,下面我们分别把Q1,Q2,Q3根据路由key为空,k1,k.#绑定在dEx上(direct exchange)。
    

  然后我们根据路由key为空,k,k1,k2,k3来发送消息m1,m2,m3,m4,m5
    

  再用3个队列接收消息试一下结果
  

  因为发送确认标记ack,所以队列上读取过的消息会被删除,为了进一步认证,我在结尾又添加了一个routingkey为k.#的消息(对应绑定Q3),由图可见direct 模式下队列之收取他们完全对应的routingkey消息。

  下面我们再试一下fanout类型,把Q1,Q2,Q3根据路由key为空,k1,k.#绑定在fEx上(fanout exchange)。

  
  同上步骤建立绑定关系

  

  生产消息,然后看下队列接受消息的情况
  

  效果很明显,fanout为广播模式。

  再试试topic类型 把Q1,Q2,Q3根据路由key为空,k1,k.#绑定在tEx上(topic exchange)。

  

  推送消息
  

  接收消息
  
  通过3种模式 3个队列的消息读取 大家应该了解了这3中模式的区别。


进阶

一.  exchange属性

  Type

  前一章我们说了exchange的类型分为fanout,direct,topic.还有一种不常用的headers。
  headers这种类型的exchange绑定的时候会忽略掉routingkey,Headers是一个键值对,可以定义成成字典等。发送者在发送的时候定义一些键值对,接收者也可以再绑定时候传入一些键值对,两者匹配的话,则对应的队列就可以收到消息。匹配有两种方式all和any。这两种方式是在接收端必须要用键值"x-mactch"来定义。all代表定义的多个键值对都要满足,而any则代码只要满足一个就可以了。之前的几种exchange的routingKey都需要要字符串形式的,而headers exchange则没有这个要求,因为键值对的值可以是任何类型
  举个例子,发送端定义2个键值{k1,1},{k2,2},接收端绑定队列的时候定义{"x-match", "any"},那么接收端的键值属性里只要存在{k1,1}或{k2,2}都可以获取到消息。
  这样的类型扩展的程度很大,适合非常复杂的业务场景。

  Durability

  持久性,这是exchange的可选属性,如果你Durability设置为false,那些当前会话结束的时候,该exchange也会被销毁。 
  新建一个transient exchange 
  
  关闭当前连接再查看一下
  

  

  刚才我们新建的transient已经销毁了。

  Auto delete

  当没有队列或者其他exchange绑定到此exchange的时候,该exchange被销毁。这个很简单就不示例了。

  Internal (比较简单 也不展示了)

  表示这个exchange不可以被client用来推送消息,仅用来进行exchange和exchange之间的绑定。

  PS: 无法声明2个名称相同 但是类型却不同的exchange

  


二.  Queue属性  

  Durability 和exchange相同,未持久化的队列,服务重启后销毁。

  Auto delete 当没有消费者连接到该队列的时候,队列自动销毁。

  Exclusive 使队列成为私有队列,只有当前应用程序可用,当你需要限制队列只有一个消费者,这是很有用的。

  扩展属性如下对应源程序 RabbitMQ.Client.IModel.QueueDeclare(string, bool, bool, bool, System.Collections.Generic.IDictionary<string,object>)最后的参数

  Message TTL 当一个消息被推送在该队列的时候 可以存在的时间 单位为ms,(对应扩展参数argument "x-message-ttl" )

  Auto expire 在队列自动删除之前可以保留多长时间(对应扩展参数argument "x-expires")

  Max length 一个队列可以容纳的已准备消息的数量(对应扩展参数argument "x-max-length")

  ... 更多参考 http://www.rabbitmq.com/extensions.html

  ps:一旦创建了队列和交换机,就不能修改其标志了。例如,如果创建了一个non-durable的队列,然后想把它改变成durable的,唯一的办法就是删除这个队列然后重现创建。


三.  Message属性

  Durability 

  消息的持久在代码中设置的方法与exchange和queue不同,有2种方法

  1.

1
2
3
4
IBasicProperties properties = channel.CreateBasicProperties();
properties.SetPersistent(true);
byte[] payload = Encoding.ASCII.GetBytes(message);
channel.BasicPublish(exchange.name, txtMessageRoutingKey.Text.Trim(), properties, payload);

  2.

1
2
3
4
IBasicProperties properties = channel.CreateBasicProperties();
properties.DeliveryMode = 2;
byte[] payload = Encoding.ASCII.GetBytes(message);
channel.BasicPublish(exchange.name, txtMessageRoutingKey.Text.Trim(), properties, payload);

  contentType: 标识消息内容的MIME,例如JSON用application/json

  replayTo: 标识回调的queue的地址

  correlationId:用于request和response的关联,确保消息的请求和响应的同一性

  Message的2种状态:

  Ready

  此状态的消息存在于队列中待处理。

  Unacknowledged

  此状态的消息表示已经在处理未确认。

  说到Unacknowledged,这里需要了解一个ack的概念。当Consumer接收到消息、处理任务完成之后,会发送带有这个消息标示符的ack,来告诉server这个消息接收到并处理完成。RabbitMQ会一直等到处理某个消息的Consumer的链接失去之后,才确定这个消息没有正确处理,从而RabbitMQ重发这个消息。
  Message acknowledgment是默认关闭的。初始化Consumer时有个noAck参数,如果设置为true,这个Consumer在收到消息之后会马上返回ack。

  string BasicConsume(string queue, bool noAck, RabbitMQ.Client.IBasicConsumer consumer)

  一般来说,常用的场景noack一般就是设置成true,但是对于风险要求比较高的项目,例如支付。对于每一条消息我们都需要保证他的完整性和正确性。就需要获取消息后确认执行完正确的业务逻辑后再主动返回一个ack给server。可以通过rabbitmqctl list_queues name message_rady message_unacknowleded 命令来查看队列中的消息情况,也可以通过后台管理界面。

  我们先hold住一条消息

  

  然后我们再关闭链接或者重启服务

  
  数据还是完整的。 

  ps:message的消费还分为consume和baseget 下面讲到集群的时候再介绍。


四.  binding相关

  如果你绑定了一个durable的队列和一个durable的交换机,RabbitMQ会自动保留这个绑定。类似的,如果删除了某个队列或交换机(无论是不是 durable),依赖它的绑定都会自动删除。

  在声明一个队列的同时,server会默认让此队列绑定在默认的exchange上,这个exchange的名称为空。

   


 五.  发布订阅

  我们上一章的demo中实际上已经使用了发布订阅模式。

  RabbitMQ消息模型的核心理念是:发布者(producer)不会直接发送任何消息给队列。事实上,发布者(producer)甚至不知道消息是否已经被投递到队列。发布者(producer)只需要把消息发送给一个exchange。exchange非常简单,它一边从发布者方接收消息,一边把消息推入队列。exchange必须知道如何处理它接收到的消息,是应该推送到指定的队列还是是多个队列,或者是直接忽略消息。这些规则是通过exchange type来定义的。

  

  发布订阅其实很简单,例如上章我所示例,假设我们一开始没有任何消息,现在有一个生产者P1,他是一个天气预报播放者。然后我们有2个消费者来订阅他的消息。
  P1通过广播类型的交换机fEx来发布他的天气消息,c1,c2分别建立一个队列为Q1,Q2. 并且订阅P1的fEx.

  基本可以如图所示
  
  我们P1利用fEx生成一条消息的时候,c1,c2通过Q1,Q2都可以获取到p1所发布的消息

  我们发布3条消息
  
  查看队列情况
  Q1:
  
  Q2:

  

  Q1,Q2都拿到了广播的消息,至于C1,C2如何消费这些消息,互相之间完全没有干扰。

  ps:简单一句话 发布订阅中发布者所产生的消息通过exchange对所有绑定他的队列队形消息推送,每个队列获取绑定所对应的消息


六.  WorkQueue (可用于消费者集群)

  区分于发布订阅,消费者集群主要解决横向服务器扩展问题,如果一个队列积压太多,如何均与的让不同的消费者来承担。

  

  默认来说,RabbitMQ会按顺序得把消息发送给每个消费者(consumer)。平均每个消费者都会收到同等数量得消息。这种发送消息得方式叫做——轮询(round-robin)。

  我们开3个程序,1个生产 2个消费。

  如图所示绑定关系如下

  

  2个消费者用同样的程序,这里记录进程pid以区分,实际项目中可以用不同服务器来区分

  

   启动消息消费,使消费者处理work状态

  

  然后我们不停的通过生产者这发布消息

  

  然后我们看下2个消费者的消费情况

  1.

  

  2.
  

  3.
  

  4.
  

  5.
  
  

  默认地,RabbitMQ会逐一地向下一个Consumer发放消息,每一个Consumer会得到数目相同的消息。文中所示之所以是按照1条一条的轮询,是因为程序中控制了一个队列单次消费的数量。

  void BasicQos(uint prefetchSize, ushort prefetchCount, bool global)


API CommandLine 以及其他功能

RabbitMQ API

  RabbitMQ Server提供了丰富的http api。

  举个列子

  

  需要HTTP基本身份验证。默认的用户名/密码为guest/guest。

  这些返回值得意义我从官网搬来解释,为了避免翻译的问题导致大家理解的误差这里直接给出原文

cluster_name The name of the entire cluster, as set with rabbitmqctl set_cluster_name.
erlang_full_version A string with extended detail about the Erlang VM and how it was compiled, for the node connected to.
erlang_version A string with the Erlang version of the node connected to. As clusters should all run the same version this can be taken as representing the cluster.
exchange_types A list of all exchange types available.
listeners All (non-HTTP) network listeners for all nodes in the cluster. (See contexts in /api/nodes for HTTP).
management_version Version of the management plugin in use.
message_stats A message_stats object for everything the user can see - for all vhosts regardless of permissions in the case of monitoring and administrator users, and for all vhosts the user has access to for other users.
node The name of the cluster node this management plugin instance is running on.
object_totals An object containing global counts of all connections, channels, exchanges, queues and consumers, subject to the same visibility rules as for message_stats.
queue_totals An object containing sums of the messagesmessages_ready and messages_unacknowledged fields for all queues, again subject to the same visibility rules as for message_stats.
rabbitmq_version Version of RabbitMQ on the node which processed this request.
statistics_db_node Name of the cluster node hosting the management statistics database.
statistics_level Whether the node is running fine or coarse statistics.

  又或者通过api查询虚拟主机
  
  许多api的URI需要一个虚拟主机路径的一部分的名字,因为名字只有唯一在一个虚拟主机识别物体。作为默认的虚拟主机称为“/”,这​​将需要被编码为“%2F”。

  在我的demo程序中对应的api功能可以通过这里的功能来实现

  

  其更丰富的功能可以参考官网说明文档 http://hg.rabbitmq.com/rabbitmq-management/raw-file/3646dee55e02/priv/www-api/help.html

  以及 http://hg.rabbitmq.com/rabbitmq-management/raw-file/rabbitmq_v3_3_5/priv/www/api/index.html

  一般来说我们常用的我在应用程序中已经给出 例如查看所有队列等

  


 RabbitMQ CommandLine

  除了丰富的http api,rabbitmq server自然也有其很全面命令行。

  例如查询所有exchange。

  

  查询所有队列以及他们包含的消息数目

   

  rabbitmqctl更多的命令说明参考 http://www.rabbitmq.com/man/rabbitmqctl.1.man.html


Message的BasicGet于consume的区别

   consume的功能上一张介绍过,basicget更偏向于我们平时用过的其他类型的MessageQueue,它就是最基本的接受消息,consume的消费针对basicget来说属于一个长连接于短连接的区别。

消费者关系一旦确定,基本上默认它就是在侦听通道的消息是否在生产。而basicget则是由客户端手动来控制。

  在demo中在下图所示处区分

  

  如果你选择了消费消息,那么基本上代码层面是这样来完成的

1
2
3
4
5
6
7
8
9
var consumer = new QueueingBasicConsumer(channel);
channel.BasicQos(0, 1, false);
channel.BasicConsume(queue.name, rbAckTrue.Checked, consumer);
while (true)
{
    var e = consumer.Queue.Dequeue();
    MessageBox.Show(string.Format("队列{0}获取消息{1},线程id为{2}", queue.name, Encoding.ASCII.GetString(e.Body), Process.GetCurrentProcess().Id));
    Thread.Sleep(1000);
}

本篇先到此,希望对大家有帮助