RabbitMQ使用(下)
RabbitMQ从信息接收者角度可以看做三种模式,一对一,一对多(此一对多并不是发布订阅,而是每条信息只有一个接收者)和发布订阅。其中一对一是简单队列模式,一对多是Worker模式,而发布订阅包括发布订阅模式,路由模式和通配符模式,为什么说发布订阅模式包含三种模式呢,其实发布订阅,路由,通配符,Headers 四种模式都是使用只是交换机(Exchange)类型不一致
1 简单队列
首先,我们需要创建两个控制台项目.Send(发送者)和Receive(接收者),然后为两个项目安装RabbitMQ.Client驱动
install-package rabbitmq.client
然后在Send和Receive项目中编写我们的消息队列代码
发送者代码
1 // <summary> 2 /// 一对一 3 /// 发送 4 /// </summary> 5 /// <param name="args"></param> 6 static void Main(string[] args) 7 { 8 Console.WriteLine("Start"); 9 IConnectionFactory conFactory = new ConnectionFactory//创建连接工厂对象 10 { 11 HostName = "localhost",//IP地址 12 Port = 5672,//端口号 13 UserName = "guest",//用户账号 14 Password = "guest"//用户密码 15 }; 16 using (IConnection con = conFactory.CreateConnection())//创建连接对象 17 { 18 using (IModel channel = con.CreateModel())//创建连接会话对象 19 { 20 String queueName = "OneToOneQueue"; 21 22 //声明一个队列 23 channel.QueueDeclare( 24 queue: queueName,//消息队列名称 25 durable: false,//是否缓存 26 exclusive: false, 27 autoDelete: false, 28 arguments: null 29 ); 30 while (true) 31 { 32 Console.WriteLine("消息内容:"); 33 String message = Console.ReadLine(); 34 //消息内容 35 byte[] body = Encoding.UTF8.GetBytes(message); 36 //发送消息 37 channel.BasicPublish(exchange: "", routingKey: queueName, basicProperties: null, body: body); 38 Console.WriteLine("成功发送消息:" + message); 39 } 40 } 41 } 42 }
可以看到RabbitMQ使用了IConnectionFactory,IConnection和IModel来创建链接和通信管道,IConnection实例对象只负责与Rabbit的连接,而发送接收这些实际操作全部由会话通道进行,
而后使用QueneDeclare方法进行创建消息队列,创建完成后可以在RabbitMQ的管理工具中看到此队列,QueneDelare方法需要一个消息队列名称的必须参数.后面那些参数则代表缓存,参数等信息.
最后使用BasicPublish来发送消息,在一对一中routingKey必须和 queueName一致
接收者代码
1 class Program 2 { 3 /// <summary> 4 /// 一对一 5 /// 接受 6 /// </summary> 7 /// <param name="args"></param> 8 static void Main(string[] args) 9 { 10 Console.WriteLine("Start"); 11 IConnectionFactory connFactory = new ConnectionFactory//创建连接工厂对象 12 { 13 HostName = "localhost",//IP地址 14 Port = 5672,//端口号 15 UserName = "guest",//用户账号 16 Password = "guest"//用户密码 17 }; 18 using (IConnection conn = connFactory.CreateConnection()) 19 { 20 using (IModel channel = conn.CreateModel()) 21 { 22 String queueName = "OneToOneQueue"; 23 24 //声明一个队列 25 channel.QueueDeclare( 26 queue: queueName,//消息队列名称 27 durable: false,//是否缓存 28 exclusive: false, 29 autoDelete: false, 30 arguments: null 31 ); 32 //创建消费者对象 33 var consumer = new EventingBasicConsumer(channel); 34 consumer.Received += (model, ea) => 35 { 36 byte[] message = ea.Body;//接收到的消息 37 Console.WriteLine("接收到信息为:" + Encoding.UTF8.GetString(message)); 38 }; 39 //消费者开启监听 40 channel.BasicConsume(queue: queueName, autoAck: true, consumer: consumer); 41 Console.ReadKey(); 42 } 43 } 44 }
在接收者中是定义一个EventingBasicConsumer对象的消费者(接收者),这个消费者与会话对象关联,
然后定义接收事件,输出从消息队列中接收的数据,
最后使用会话对象的BasicConsume方法来启动消费者监听.消费者的定义也是如此简单.
不过注意一点,可以看到在接收者代码中也有声明队列的方法,其实这句代码可以去掉,但是如果去掉的话接收者在程序启动时监听队列,而此时这个队列还未存在,所以会出异常,所以往往会在消费者中也添加一个声明队列方法
此时,简单消息队列传输就算写好了,我们可以运行代码就行测试
2 Worker模式
Worker模式其实是一对多的模式,但是这个一对多并不是像发布订阅那种,而是信息以顺序的传输给每个接收者,我们可以使用上个例子来运行worker模式甚至,只需要运行多个接收者即可
可以看到运行两个接收者,然后发送者发送了1-5这五个消息,第一个接收者接收的是奇数,而第二个接收者接收的是偶数,但是现在的worker存在这很大的问题,
1.丢失数据:一旦其中一个宕机,那么另外接收者的无法接收原本这个接收者所要接收的数据
2.无法实现能者多劳:如果其中的接收者接收的较慢,那么便会极大的浪费性能,所以需要实现接收快的多接收
下面针对上面的两个问题进行处理,首先我们先来看一下所说的宕机丢失数据一说,我们在上个例子Receive接收事件中添加线程等待
1 consumer.Received += (model, ea) => 2 { 3 Thread.Sleep(1000);//等待1秒, 4 byte[] message = ea.Body;//接收到的消息 5 Console.WriteLine("接收到信息为:" + Encoding.UTF8.GetString(message)); 6 };
然后再次启动两个接收者进行测试
可以看到发送者发送了1-9的数字,第二个接收者在接收数据途中宕机,第一个接收者也并没有去接收第二个接收者宕机后的数据,有的时候我们会有当接收者宕机后,其余数据交给其它接收者进行消费,那么该怎么进行处理呢,解决这个问题得方法就是改变其消息确认模式
在Rabbit中存在两种消息确认模式,
自动确认:只要消息从队列获取,无论消费者获取到消息后是否成功消费,都认为是消息成功消费,也就是说上面第二个接收者其实已经消费了它所接收的数据
手动确认:消费从队列中获取消息后,服务器会将该消息处于不可用状态,等待消费者反馈
也就是说我们只要将消息确认模式改为手动即可,改为手动确认方式只需改两处,1.开启监听时将autoAck参数改为false,2.消息消费成功后返回确认
1 consumer.Received += (model, ea) => 2 { 3 Thread.Sleep(1000);//等待1秒, 4 byte[] message = ea.Body;//接收到的消息 5 Console.WriteLine("接收到信息为:" + Encoding.UTF8.GetString(message)); 6 //返回消息确认 7 channel.BasicAck(ea.DeliveryTag, true); 8 }; 9 //消费者开启监听 10 //将autoAck设置false 关闭自动确认 11 channel.BasicConsume(queue: queueName, autoAck: false, consumer: consumer);
然后再次测试便会出现下面结果
能者多劳是建立在手动确认基础上,下面修改一下代码中等待的时间
1 consumer.Received += (model, ea) => 2 { 3 Thread.Sleep((new Random().Next(1,6))*1000);//随机等待,实现能者多劳, 4 byte[] message = ea.Body;//接收到的消息 5 Console.WriteLine("接收到信息为:" + Encoding.UTF8.GetString(message)); 6 //返回消息确认 7 channel.BasicAck(ea.DeliveryTag, true); 8 };
然后只需要再添加BasicQos方法即可
1 //声明一个队列 2 channel.QueueDeclare( 3 queue: queueName,//消息队列名称 4 durable: false,//是否缓存 5 exclusive: false, 6 autoDelete: false, 7 arguments: null 8 ); 9 //告诉Rabbit每次只能向消费者发送一条信息,再消费者未确认之前,不再向他发送信息 10 channel.BasicQos(0, 1, false);
可以看到此时已实现能者多劳
3 Exchange模式(发布订阅模式,路由模式,通配符模式,Headers模式)
前面说过发布,路由,通配符这三种模式其实可以算为一种模式,区别仅仅是交互机类型不同.在这里出现了一个交换机的东西,发送者将消息发送发送到交换机,接收者创建各自的消息队列绑定到交换机,
发布订阅模式
路由模式
通配符模式
通过上面三幅图可以看出这三种模式本质就是一种订阅模式,路由,通配符模式只是订阅模式的变种模式。使其可以选择发送订阅者中的接收者。
注意:交换机本身并不存储数据,数据存储在消息队列中,所以如果向没有绑定消息队列的交换机中发送信息,那么信息将会丢失
下面依次来看一下这三种模式
发布订阅模式(fanout)
发送者代码
1 /// <summary> 2 /// 发布订阅 3 /// </summary> 4 /// <param name="args"></param> 5 static void Main(string[] args) 6 { 7 Console.WriteLine("Start"); 8 IConnectionFactory connFactory = new ConnectionFactory//创建连接工厂对象 9 { 10 HostName = "localhost",//IP地址 11 Port = 5672,//端口号 12 UserName = "guest",//用户账号 13 Password = "guest"//用户密码 14 }; 15 using (IConnection conn = connFactory.CreateConnection()) 16 { 17 using (IModel channel = conn.CreateModel()) 18 { 19 //交换机名称 20 String exchangeName = "pubSubFanOutExchange"; 21 //声明交换机 22 channel.ExchangeDeclare(exchange: exchangeName, type: "fanout"); 23 while (true) 24 { 25 Console.WriteLine("消息内容:"); 26 String message = Console.ReadLine(); 27 //消息内容 28 byte[] body = Encoding.UTF8.GetBytes(message); 29 //发送消息 30 channel.BasicPublish(exchange: exchangeName, routingKey: "", basicProperties: null, body: body); 31 Console.WriteLine("成功发送消息:" + message); 32 } 33 } 34 } 35 }
发送者代码与上面没有什么差异,只是由上面的消息队列声明变成了交换机声明(交换机类型为fanout),也就说发送者发送消息从原来的直接发送消息队列变成了发送到交换机
接收者代码
1 class Program 2 { 3 /// <summary> 4 /// 发布订阅 5 /// </summary> 6 /// <param name="args"></param> 7 static void Main(string[] args) 8 { 9 //创建一个随机数,以创建不同的消息队列 10 int random = new Random().Next(1, 1000); 11 Console.WriteLine("Start" + random.ToString()); 12 IConnectionFactory connFactory = new ConnectionFactory//创建连接工厂对象 13 { 14 HostName = "localhost",//IP地址 15 Port = 5672,//端口号 16 UserName = "guest",//用户账号 17 Password = "guest"//用户密码 18 }; 19 using (IConnection conn = connFactory.CreateConnection()) 20 { 21 using (IModel channel = conn.CreateModel()) 22 { 23 //交换机名称 24 String exchangeName = "pubSubFanOutExchange"; 25 //声明交换机 26 channel.ExchangeDeclare(exchange: exchangeName, type: "fanout"); 27 //消息队列名称 28 String queueName = exchangeName + "_" + random.ToString(); 29 //声明队列 30 channel.QueueDeclare(queue: queueName, durable: false, exclusive: false, autoDelete: false, arguments: null); 31 //将队列与交换机进行绑定 32 channel.QueueBind(queue: queueName, exchange: exchangeName, routingKey: ""); 33 //声明为手动确认 34 channel.BasicQos(0, 1, false); 35 //定义消费者 36 var consumer = new EventingBasicConsumer(channel); 37 //接收事件 38 consumer.Received += (model, ea) => 39 { 40 byte[] message = ea.Body;//接收到的消息 41 Console.WriteLine("接收到信息为:" + Encoding.UTF8.GetString(message)); 42 //返回消息确认 43 channel.BasicAck(ea.DeliveryTag, true); 44 }; 45 //开启监听 46 channel.BasicConsume(queue: queueName, autoAck: false, consumer: consumer); 47 Console.ReadKey(); 48 } 49 } 50 }
可以看到接收者代码与上面有些差异
首先是声明交换机(同上面一样,为了防止异常)
然后声明消息队列并对交换机进行绑定,在这里使用了随机数,目的是声明不重复的消息队列,如果是同一个消息队列,则就变成worker模式,也就是说对于发布订阅模式有多少接收者就有多少个消息队列,而这些消息队列共同从一个交换机中获取数据
然后同时开两个接收者,结果就如下
路由模式(direct)
上面说过路由模式是订阅模式的一个变种模式,以路由进行匹配发送,例如将消息1发送给A,B两个消息队列,或者将消息2发送给B,C两个消息队列,路由模式的交换机是direct
发送者代码
1 static void Main(string[] args) 2 { 3 if (args.Length == 0) throw new ArgumentException("args"); 4 Console.WriteLine("Start"); 5 IConnectionFactory connFactory = new ConnectionFactory//创建连接工厂对象 6 { 7 HostName = "localhost",//IP地址 8 Port = 5672,//端口号 9 UserName = "guest",//用户账号 10 Password = "guest"//用户密码 11 }; 12 using (IConnection conn = connFactory.CreateConnection()) 13 { 14 using (IModel channel = conn.CreateModel()) 15 { 16 //交换机名称 17 String exchangeName = "pubSubDirectExchange"; 18 //路由名称 19 String routeKey = args[0]; 20 //声明交换机 路由交换机类型direct 21 channel.ExchangeDeclare(exchange: exchangeName, type: "direct"); 22 while (true) 23 { 24 Console.WriteLine("消息内容:"); 25 String message = Console.ReadLine(); 26 //消息内容 27 byte[] body = Encoding.UTF8.GetBytes(message); 28 //发送消息 发送到路由匹配的消息队列中 29 channel.BasicPublish(exchange: exchangeName, routingKey: routeKey, basicProperties: null, body: body); 30 Console.WriteLine("成功发送消息:" + message); 31 } 32 } 33 } 34 }
发送者代码相比上面只改了两处
1.将交换机类型改为了direct类型
2.将运行时的第一个参数改成了路由名称,然后发送数据时由指定路由的消息队列进行获取数据
接收者代码
1 static void Main(string[] args) 2 { 3 if (args.Length == 0) throw new ArgumentException("args"); 4 //创建一个随机数,以创建不同的消息队列 5 int random = new Random().Next(1, 1000); 6 Console.WriteLine("Start" + random.ToString()); 7 IConnectionFactory connFactory = new ConnectionFactory//创建连接工厂对象 8 { 9 HostName = "localhost",//IP地址 10 Port = 5672,//端口号 11 UserName = "guest",//用户账号 12 Password = "guest"//用户密码 13 }; 14 using (IConnection conn = connFactory.CreateConnection()) 15 { 16 using (IModel channel = conn.CreateModel()) 17 { 18 //交换机名称 19 String exchangeName = "pubSubDirectExchange"; 20 //声明交换机 21 channel.ExchangeDeclare(exchange: exchangeName, type: "direct"); 22 //消息队列名称 23 String queueName = exchangeName + "_" + random.ToString(); 24 //声明队列 25 channel.QueueDeclare(queue: queueName, durable: false, exclusive: false, autoDelete: false, arguments: null); 26 //将队列与交换机进行绑定 27 28 foreach (var routeKey in args) 29 {//匹配多个路由 30 channel.QueueBind(queue: queueName, exchange: exchangeName, routingKey: routeKey); 31 } 32 //声明为手动确认 33 channel.BasicQos(0, 1, false); 34 //定义消费者 35 var consumer = new EventingBasicConsumer(channel); 36 //接收事件 37 consumer.Received += (model, ea) => 38 { 39 byte[] message = ea.Body;//接收到的消息 40 Console.WriteLine("接收到信息为:" + Encoding.UTF8.GetString(message)); 41 //返回消息确认 42 channel.BasicAck(ea.DeliveryTag, true); 43 }; 44 //开启监听 45 channel.BasicConsume(queue: queueName, autoAck: false, consumer: consumer); 46 Console.ReadKey(); 47 } 48 }
在接收者代码中的改动点也是与发送者一致,但是一个接收者消息队列可以声明多个路由与交换机进行绑定
运行情况如下
通配符模式(topic)
通配符模式与路由模式一致,只不过通配符模式中的路由可以声明为模糊查询,RabbitMQ拥有两个通配符
#:匹配0-n个字符语句
*:匹配一个字符语句
注意:RabbitMQ中通配符并不像正则中的单个字符,而是一个以“.”分割的字符串,如 ”topic1.*“匹配的规则以topic1开始并且"."后只有一段语句的路由 例:“topic1.aaa”,“topic1.bb”
发送者代码
1 static void Main(string[] args) 2 { 3 if (args.Length == 0) throw new ArgumentException("args"); 4 Console.WriteLine("Start"); 5 IConnectionFactory connFactory = new ConnectionFactory//创建连接工厂对象 6 { 7 HostName = "localhost",//IP地址 8 Port = 5672,//端口号 9 UserName = "guest",//用户账号 10 Password = "guest"//用户密码 11 }; 12 using (IConnection conn = connFactory.CreateConnection()) 13 { 14 using (IModel channel = conn.CreateModel()) 15 { 16 //交换机名称 17 String exchangeName = "pubSubTopicExchange"; 18 //路由名称 19 String routeKey = args[0]; 20 //声明交换机 通配符类型为topic 21 channel.ExchangeDeclare(exchange: exchangeName, type: "topic"); 22 while (true) 23 { 24 Console.WriteLine("消息内容:"); 25 String message = Console.ReadLine(); 26 //消息内容 27 byte[] body = Encoding.UTF8.GetBytes(message); 28 //发送消息 发送到路由匹配的消息队列中 29 channel.BasicPublish(exchange: exchangeName, routingKey: routeKey, basicProperties: null, body: body); 30 Console.WriteLine("成功发送消息:" + message); 31 } 32 } 33 } 34 }
修改了两点:交换机名称(每个交换机只能声明一种类型,如果还用exchang2的话就会出异常),交换机类型改为topic
接收者代码
1 static void Main(string[] args) 2 { 3 if (args.Length == 0) throw new ArgumentException("args"); 4 //创建一个随机数,以创建不同的消息队列 5 int random = new Random().Next(1, 1000); 6 Console.WriteLine("Start" + random.ToString()); 7 IConnectionFactory connFactory = new ConnectionFactory//创建连接工厂对象 8 { 9 HostName = "localhost",//IP地址 10 Port = 5672,//端口号 11 UserName = "guest",//用户账号 12 Password = "guest"//用户密码 13 }; 14 using (IConnection conn = connFactory.CreateConnection()) 15 { 16 using (IModel channel = conn.CreateModel()) 17 { 18 //交换机名称 19 String exchangeName = "pubSubTopicExchange"; 20 //声明交换机 通配符类型为topic 21 channel.ExchangeDeclare(exchange: exchangeName, type: "topic"); 22 //消息队列名称 23 String queueName = exchangeName + "_" + random.ToString(); 24 //声明队列 25 channel.QueueDeclare(queue: queueName, durable: false, exclusive: false, autoDelete: false, arguments: null); 26 //将队列与交换机进行绑定 27 foreach (var routeKey in args) 28 {//匹配多个路由 29 channel.QueueBind(queue: queueName, exchange: exchangeName, routingKey: routeKey); 30 } 31 //声明为手动确认 32 channel.BasicQos(0, 1, false); 33 //定义消费者 34 var consumer = new EventingBasicConsumer(channel); 35 //接收事件 36 consumer.Received += (model, ea) => 37 { 38 byte[] message = ea.Body;//接收到的消息 39 Console.WriteLine("接收到信息为:" + Encoding.UTF8.GetString(message)); 40 //返回消息确认 41 channel.BasicAck(ea.DeliveryTag, true); 42 }; 43 //开启监听 44 channel.BasicConsume(queue: queueName, autoAck: false, consumer: consumer); 45 Console.ReadKey(); 46 } 47 } 48 }
接收者修改与发送者一致
运行结果如下
Headers 模式
Headers 类型的Exchanges是不处理路由键的,而是根据发送的消息内容中的headers属性进行匹配。在绑定Queue与Exchange时指定一组键值对;当消息发送到RabbitMQ时会取到该消headers 与Exchange绑定时指定的键值对进行匹配;如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers属性是一个键值对,可以是Hashtable,键值对的值可以是任何类型。而fanout,direct,topic 的路由键都需要要字符串形式的。
匹配规则x-match有下列两种类型:
x-match = all :表示所有的键值对都匹配才能接受到消息
x-match = any :表示只要有键值对匹配就能接受到消息
发送者代码
1 IConnectionFactory connFactory = new ConnectionFactory//创建连接工厂对象 2 { 3 HostName = "192.168.1.107",//IP地址 4 Port = 5672,//端口号 5 UserName = "ztb",//用户账号 6 Password = "123"//用户密码 7 }; 8 using (IConnection conn = connFactory.CreateConnection()) 9 { 10 using (IModel channel = conn.CreateModel()) 11 { 12 //交换机名称 13 String exchangeName = "pubSubHeardesExchange"; 14 15 //设置headers 16 var properties = channel.CreateBasicProperties(); 17 properties.Headers = new Dictionary<string, object>(); 18 properties.Headers.Add("username", "jack"); 19 20 while (true) 21 { 22 Console.WriteLine("消息内容:"); 23 String message = Console.ReadLine(); 24 //消息内容 25 byte[] body = Encoding.UTF8.GetBytes(message); 26 //发送消息 27 channel.BasicPublish(exchange: exchangeName, routingKey: string.Empty, basicProperties: properties, body: body); 28 Console.WriteLine("成功发送消息:" + message); 29 } 30 } 31 }
接收者代码
1 IConnectionFactory connFactory = new ConnectionFactory//创建连接工厂对象 2 { 3 HostName = "192.168.1.107",//IP地址 4 Port = 5672,//端口号 5 UserName = "ztb",//用户账号 6 Password = "123"//用户密码 7 }; 8 using (IConnection conn = connFactory.CreateConnection()) 9 { 10 using (IModel channel = conn.CreateModel()) 11 { 12 //交换机名称 13 String exchangeName = "pubSubHeardesExchange"; 14 //声明交换机 15 channel.ExchangeDeclare(exchange: exchangeName, type: "headers"); 16 //消息队列名称 17 String queueName = "myHeadersQueue"; 18 //声明队列 19 channel.QueueDeclare(queue: queueName, durable: true, exclusive: false, autoDelete: false, arguments: null); 20 21 //匹配路由 22 channel.QueueBind(queue: queueName, exchange: exchangeName, routingKey: string.Empty, arguments: new Dictionary<string, object>() 23 { 24 {"x-match", "any"}, 25 {"username", "jack"}, 26 {"password", "12345" } 27 }); 28 29 30 //定义消费者 31 var consumer = new EventingBasicConsumer(channel); 32 //接收事件 33 consumer.Received += (model, ea) => 34 { 35 byte[] message = ea.Body;//接收到的消息 36 Console.WriteLine("接收到信息为:" + Encoding.UTF8.GetString(message)); 37 38 }; 39 //开启监听 40 channel.BasicConsume(queue: queueName, autoAck: false, consumer: consumer); 41 Console.ReadKey(); 42 }
运行结果如下