RabbitMQ学习笔记(六) RPC
什么RPC?
这一段是从度娘摘抄的。
RPC(Remote Procedure Call Protocol)——远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。
RPC采用客户机/服务器模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,客户机调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息,最后,客户端调用进程接收答复信息,获得进程结果,然后调用执行继续进行。
Callback Queue
之前在做学习笔记(二)工作队列的例子的时候,我们只是发送了任务给消息消费者实例去完成,但是对于消息生产者,它并不知道任务是否完成,所以这里缺少一个回调方法。
既然我们使用RabbitMQ, 所以很容易想到的就是我们可以互换一下消息消费者和消息生产者的角色,当消费生产者完成任务之后,通过创建一个新的消息队列,将完成任务的消息,发送给消息生产者。
那么如何让消息消费者,知道消费生产者关注的消息队列呢?
在我们发布消息的时候,会调用channel对象的BasicPublish
方法,这个方法中有一个IBasicProperties
的参数basicProperties
。
在这对象中,有一个ReplyTo
属性,我们可以将生产者监听的消息队列名称存放在里面。当消费者程序接收到这条消息的时候,就可以在Receive事件的ea对象中获取ReplyTo属性的值
var props = channel.CreateBasicProperties(); props.ReplyTo = replyQueueName; var messageBytes = Encoding.UTF8.GetBytes(message); channel.BasicPublish(exchange: "", routingKey: "rpc_queue", basicProperties: props, body: messageBytes);
Correlation Id
那么当消息生产者接收到消息消费者任务完成的消息之后,该如何确定完的是哪一个任务呢?
在现实情况,消息生产者通常会发出多个任务,多个消息消费者分别进行不同的任务,这时候我们就需要知道是哪个消息消费者完成了任务。
当消息生产者调用channel对象的BasicPublish
方法发送消息时,IBasicProperties
对象除了可以帮助我们传递消息生产者监听的消息队列名,还可以帮我们传递一个CorrelationId
(相关Id),当发送任务消息的时候,我们给每个任务消息定义一个唯一的相关Id, 并存储在IBasicProperties
对象的CorrelationId
属性中。
var properties = channel.CreateBasicProperties(); properties.ReplyTo = replyQueueName; properties.CorrelationId = Guid.NewGuid().ToString();
这样消息消费者在接收到任务消息时,可以从Receive的ea参数中获取CorrelationId
。当任务完成时,再将保存有这个CorrelationId
的任务完成消息发送到消息生产者关注的消息队列中, 消息生产者就可以知道是哪个任务完成了
手动实现RabbitMQ的RPC
Send
static void Main(string[] args) { var factory = new ConnectionFactory() { HostName = "localhost" }; using (var connection = factory.CreateConnection()) { using (var channel = connection.CreateModel()) { var replyQueueName = channel.QueueDeclare().QueueName; var consumer = new EventingBasicConsumer(channel); channel.BasicConsume(queue: replyQueueName, autoAck: true, consumer: consumer); channel.QueueDeclare(queue: "manual_rpc_queue", durable: true, exclusive: false, autoDelete: false, arguments: null); string message = args[0]; var body = Encoding.UTF8.GetBytes(message); var properties = channel.CreateBasicProperties(); properties.ReplyTo = replyQueueName; properties.CorrelationId = Guid.NewGuid().ToString(); consumer.Received += (m, ea) => { if (ea.BasicProperties.CorrelationId == properties.CorrelationId) { Console.WriteLine($"Task {properties.CorrelationId} completed."); Console.WriteLine($"{Encoding.UTF8.GetString(ea.Body)}"); Environment.Exit(1); } }; channel.BasicPublish(exchange: "", routingKey: "manual_rpc_queue", basicProperties: properties, body: body ); Console.WriteLine("[x] Sent {0}", message); Console.Read(); } } }
Receive
static void Main(string[] args) { var factory = new ConnectionFactory() { HostName = "localhost" }; using (var connection = factory.CreateConnection()) { using (var channel = connection.CreateModel()) { channel.QueueDeclare(queue: "manual_rpc_queue", durable: true, exclusive: false, autoDelete: false, arguments: null); channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false); var consumer = new EventingBasicConsumer(channel); consumer.Received += (model, ea) => { var body = ea.Body; var message = Encoding.UTF8.GetString(body); Thread.Sleep(4000); Console.WriteLine("[x] Received {0}", message); var prop = channel.CreateBasicProperties(); prop.CorrelationId = ea.BasicProperties.CorrelationId; channel.BasicPublish( exchange: "", routingKey: ea.BasicProperties.ReplyTo, mandatory: false, basicProperties: prop, body: Encoding.UTF8.GetBytes($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} Task {prop.CorrelationId} Completed.")); channel.BasicAck(ea.DeliveryTag, false); }; channel.BasicConsume(queue: "manual_rpc_queue", autoAck: false, consumer: consumer); Console.Read(); } } }
最终效果
RabbitMQ提供的RPC实现
RabbitMQ提供了一个SimpleRpcClient类和SimpleRpcServer类,来简化我们的代码,代码不难理解
Send
static void Main(string[] args) { var factory = new ConnectionFactory() { HostName = "localhost" }; using (var connection = factory.CreateConnection()) { using (var channel = connection.CreateModel()) { SimpleRpcClient client = new SimpleRpcClient(channel, new PublicationAddress(exchangeType: ExchangeType.Direct, exchangeName: string.Empty, routingKey: "RpcQueue")); var prop = channel.CreateBasicProperties(); prop.CorrelationId = Guid.NewGuid().ToString(); IBasicProperties outProp; var msg = client.Call(prop, Encoding.UTF8.GetBytes(args[0]), out outProp); if (prop.CorrelationId == outProp.CorrelationId) { Console.WriteLine($"Task {prop.CorrelationId} completed."); Console.WriteLine(Encoding.UTF8.GetString(msg)); } } } }
Receive
- 创建MySimpleRpcServer类,继承自SimpleRpcServer类
- HandleSimpleCall方法里添加回调返回值
- ProcessRequest方法为任务处理方法
MySimpleRpcServer.cs
public class MySimpleRpcServer : SimpleRpcServer { public MySimpleRpcServer(Subscription subscription) : base(subscription) { } public override byte[] HandleSimpleCall(bool isRedelivered, IBasicProperties requestProperties, byte[] body, out IBasicProperties replyProperties) { replyProperties = null; return Encoding.UTF8.GetBytes($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} Task {requestProperties.CorrelationId} Completed."); } /// <summary> /// 进行处理 /// </summary> /// <param name="evt"></param> public override void ProcessRequest(BasicDeliverEventArgs evt) { Console.WriteLine("[x] Received {0}", Encoding.UTF8.GetString(evt.Body)); Thread.Sleep(4000); base.ProcessRequest(evt); } }
Program.cs
static void Main(string[] args) { var factory = new ConnectionFactory() { HostName = "localhost" }; using (var connection = factory.CreateConnection()) { using (var channel = connection.CreateModel()) { channel.QueueDeclare("RpcQueue", true, false, false, null); SimpleRpcServer rpc = new MySimpleRpcServer(new Subscription(channel, "RpcQueue")); rpc.MainLoop(); Console.ReadKey(); } } }