RabbitMQ

Remote Procedure Call  (RPC)

用RabbitMQ实现RPC还是比较简单,一个客户端发送一个请求,服务端返回一个响应。为了实现响应我们需要发送一个"callback" 指定某个请求的队列,也就是下面主要介绍的Correlation Id

Correlation Id

实现RPC最简单的方式是为每个RPC请求创建一个callback回调函数,但这样是无效的或者说是冗余繁杂的。

这也会产生一个问题,就是返回的response不能清晰的知道属于哪个request,这就会用到CorrelationId,我们将会为每次请求创建唯一的值,然后,我们收到这个callback response,查找这个值通过匹配的方式能够知道response属于哪个RPC request,如果我们收到一个未知的CorrelationId 值,我们完全可以忽略它,不属于我们的请求。

这里可能会有所迷惑,为什么我们要忽略这个未知的值而不是直接报错,这个是由于RPC Server的竞争,虽然不太可能,这个可能是当RPC server发送一个reponse,但未发送确认(acknowledgment)消息给request之前宕机了,如果发生了,RPC Server重启之后会再次发送请求。这就是为什么在client端需要处理duplicate response。

PRC工作原理

  • 当Clientk开始,将会创建匿名唯一的callback队列
  • 对于RPC请求,client会发送一个消息附带2个属性:ReplyTo通常是发送callback请求队列名,CorrelationId指定每次请求的唯一值。
  • 请求发送到队列
  • RPC Server等待请求对于指定的queue,当接收到请求,处理job并且会发送一个response消息给client,response的队列是ReplyTo request的队列。(就是说request和response的queue name和CorrelationId是一样的)
  • Client等待callback queue的数据,当收到请求消息,判断CorrelcationId是否匹配,如果匹配了,返回response给application 应用程序。

下面以Fibonacci数列为例,实现简单的RPC Server / Client

RPC Server的逻辑比较简单:

  • 建立连接connectionchannel,声明queue
  • 我们可能会运行多余一个的RPC Server,为了在多个Server之间实现负载均衡,需要设置prefetchCount channel.BasicQos settings。(解释一下:prefetchCount = 1是告诉RabbitMQ不要同时给一个worker分发多个消息,或者换句话说不要分发一个新消息给这个worker直到worker之前的消息已经处理并且被确认。而是分发新消息给下一个不繁忙的worker)
  • 用BasiConsume访问queue,然后注册一个delivery handler处理和发送response back。

示例Code:

  public static void RunRPCDemo()
        {
            var factory = new ConnectionFactory() { HostName = "10.2.87.39" };
            using (var connection = factory.CreateConnection())
            {
                using (var channel = connection.CreateModel())
                {
                    channel.QueueDeclare(queue: "rpc_queue",
                        durable: false,
                        exclusive: false,
                        autoDelete: false,
                        arguments: null);
                    channel.BasicQos(0, 1, false);
                    var consumer = new EventingBasicConsumer(channel);
                    channel.BasicConsume(queue: "rpc_queue",
                        autoAck: false,
                        consumer: consumer);
                    Console.WriteLine("[x] waiting RPC request");
                    consumer.Received += (model, ea) =>
                    {
                        string response = null;
                        var body = ea.Body.ToArray();
                        var props = ea.BasicProperties;
                        var replyProps = channel.CreateBasicProperties();
                        replyProps.CorrelationId = props.CorrelationId;

                        try
                        {
                            var message = Encoding.UTF8.GetString(body);
                            int n = int.Parse(message);
                            Console.WriteLine($"[.] fib{message}");
                            response = fib(n).ToString();
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine($"[.] {e.Message}");
                        }
                        finally
                        {
                            var reponseBytes = Encoding.UTF8.GetBytes(response);
                            channel.BasicPublish(exchange: "",
                                routingKey: props.ReplyTo,
                                basicProperties: replyProps,
                                body: reponseBytes);
                            channel.BasicAck(deliveryTag: ea.DeliveryTag,
                                multiple: false);

                        }
                    };
                    Console.WriteLine("Press any key to exist");
                    Console.ReadLine();
                }
            }
        }

        private static int fib(int n)
        {
            if (n == 0 || n == 1)
            {
                return n;
            }
            return fib(n - 1) + fib(n - 2);
        }
View Code

RPC Client 逻辑稍微复杂一些:

  • 建立connection和channel,声明唯一的callback queue
  • 订阅callback queue,以至于可以收到RPC 响应
  • Call 方法是实际RPC 请求
  • 创建一个唯一的CorrelationId number
  • 发布request message附带ReplyTo和CorrelationId两个属性
  • 接下来等待RPC Server响应
  • 对于每个响应,判断是否是我们所期待的
  • 最终返回response给user

示例Code

 public class RPCClient
    {
        private readonly IConnection connection;
        private readonly IModel channel;
        private readonly string replyQueueName;
        private readonly EventingBasicConsumer consumer;
        private readonly BlockingCollection<string> respQueue = new BlockingCollection<string>();
        private readonly IBasicProperties props;

        public RPCClient()
        {
            var factory = new ConnectionFactory() { HostName = "10.2.87.39" };
            connection = factory.CreateConnection();
            channel = connection.CreateModel();

            replyQueueName = channel.QueueDeclare().QueueName;
            consumer = new EventingBasicConsumer(channel);

            props = channel.CreateBasicProperties();
            var correlationId = $"{Guid.NewGuid()}";
            props.CorrelationId = correlationId;
            props.ReplyTo = replyQueueName;

            consumer.Received += (model, ea) =>
            {
                var body = ea.Body.ToArray();
                var response = Encoding.UTF8.GetString(body);
                if (ea.BasicProperties.CorrelationId == correlationId)
                {
                    respQueue.Add(response);
                }
            };
        }

        public string Call(string message)
        {
            var messageBytes = Encoding.UTF8.GetBytes(message);
            channel.BasicPublish(exchange: "",
                routingKey: "rpc_queue",
                basicProperties: props,
                body: messageBytes);
            channel.BasicConsume(consumer: consumer,
                queue: replyQueueName,
                autoAck: true);
            return respQueue.Take();
        }

        public void Close()
        {
            connection.Close();
        }
    }
View Code

客户端Code:

 public static void RunRPCDemo()
 {
            var rpcClient = new RPCClient();
            Console.WriteLine($"[x] requesting fib(30)");
            var response = rpcClient.Call("30");
            Console.WriteLine($"[.] get {response}");
            rpcClient.Close();
  }

运行RPC Server和Client,结果如下:

简单的RPC已经实现了。

待解决问题:

  • 没有server运行时,Client如何处理
  • RPC Client长时间没有收到response,过期时间怎么处理
  • 如果server 异常了,是否需要向client发送response
  • 无效的incoming message,如何处理

 

posted @ 2021-03-25 10:57  云霄宇霁  阅读(49)  评论(0编辑  收藏  举报