首页 > 代码库 > RabbitMQ学习(六).NET Client之RPC

RabbitMQ学习(六).NET Client之RPC

Remote procedure call (RPC)

(using the .NET client)

在第二个教程second tutorial 中我们已经了解到了工作队列如何将耗时任务分配给多个workers。

但是假如我们需要在远端机器上面运行一个函数并且等待结果返回呢?这通常叫做RPC,即远端过程调用。

这里我们将用RabbitMQ构造一个RPC系统,客户端请求调用服务端的计算斐波纳契数列值得一个函数,并等待计算结果。

Client interface(客户端调用接口)

首先看一下客户端接口,我们定义一个RPC调用类,其中提供了一个叫做Call的接口,这个接口内部所做的事情就是将调用服务端计算斐波那契数列的请求(包含参数)发送到指定的消息队列,然后再另一个临时队列阻塞等待服务端将计算结果放入到这个临时队列。

var rpcClient = new RPCClient();

Console.WriteLine(" [x] Requesting fib(30)");
var response = rpcClient.Call("30");
Console.WriteLine(" [.] Got ‘{0}‘", response);

rpcClient.Close();

Callback queue(回调队列)

下面代码是Call接口内部实现的一部分,为了等待服务端RPC调用的结果,我们需要告诉服务端将计算结果放到哪个队列中,这里props参数就已经制定了计算结果的存放队列名称,同时还附上了每个RPC请求的ID,方便读取response的时候能够知道对应于哪个请求:

var corrId = Guid.NewGuid().ToString();
var props = channel.CreateBasicProperties();
props.ReplyTo = replyQueueName;
props.CorrelationId = corrId;

var messageBytes = Encoding.UTF8.GetBytes(message);
channel.BasicPublish("", "rpc_queue", props, messageBytes);

// ... then code to read a response message from the callback_queue ...

Correlation Id(RPC请求ID)

很显然我们不可能为每个RPC调用都创建一个存放调用结果的回调队列,我们可以为每个client端都创建一个。

至于每个RPC请求发出去之后,收到回应时如何知道这个response是对应于哪个RPC请求,就需要用到 correlationId 属性. 

这个ID值可以有多重方法生成,比如客户端IP+计数值,或者一个唯一的GUID等都可以。

Summary(总结)

技术分享

RPC调用工作流程:

  • 客户端启动时候创建一个匿名独占的回调队列。
  • 进行RPC调用时,发送带有两个属性的消息(RPC请求)到指定队列,这两个属性是指定回调队列名称的 replyTo 属性,以及唯一标识一个RPC请求的 correlationId 属性。
  • 将RPC请求发送到约定好的 rpc_queue 队列。
  • 服务端的RPC worker收到RPC请求之后开始调用函数,执行完成之后将执行结果放到 replyTo 指定的回调队列中。
  • 客户端在回调队列上等待调用结果,当收到消息之后查看 correlationId 属性时候是刚才的RPC请求的response,如果是就返回。

Putting it all together(代码总览)

斐波那契数列计算函数:

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

 

RPC服务端代码 RPCServer.cs :

class RPCServer
{
    public static void Main()
    {
        var factory = new ConnectionFactory() { HostName = "localhost" };
        using (var connection = factory.CreateConnection())
        {
            using (var channel = connection.CreateModel())
            {
                channel.QueueDeclare("rpc_queue", false, false, false, null);
                channel.BasicQos(0, 1, false);
                var consumer = new QueueingBasicConsumer(channel);
                channel.BasicConsume("rpc_queue", false, consumer);
                Console.WriteLine(" [x] Awaiting RPC requests");

                while (true)
                {
                    string response = null;
                    var ea = (BasicDeliverEventArgs)consumer.Queue.Dequeue();

                    var body = ea.Body;
                    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({0})", message);
                        response = fib(n).ToString();
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine(" [.] " + e.Message);
                        response = "";
                    }
                    finally
                    {
                        var responseBytes = Encoding.UTF8.GetBytes(response);
                        channel.BasicPublish("", props.ReplyTo, replyProps, responseBytes);
                        channel.BasicAck(ea.DeliveryTag, false);
                    }
                }
            }
        }
    }

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

The server code is rather straightforward:

  • 和往常一样建立连接,channel,以及申明队列。
  • 你可能想要运行多个server进程,为了让请求均匀地负载到多个servers上面,我们需要调用 channel.basicQos. 告诉RabbitMQ不要将超过一个的消息同时分配给同一个worker,详见:http://blog.csdn.net/jiyiqinlovexx/article/details/38946955
  • 调用basicConsume 访问队列. 
  • 然后进入循环,等待消息,执行函数调用,返回response。.

 

RPC客户端代码 RPCClient.cs:

class RPCClient
{
    private IConnection connection;
    private IModel channel;
    private string replyQueueName;
    private QueueingBasicConsumer consumer;

    public RPCClient()
    {
        var factory = new ConnectionFactory() { HostName = "localhost" };
        connection = factory.CreateConnection();
        channel = connection.CreateModel();
        replyQueueName = channel.QueueDeclare();
        consumer = new QueueingBasicConsumer(channel);
        channel.BasicConsume(replyQueueName, true, consumer);
    }

    public string Call(string message)
    {
        var corrId = Guid.NewGuid().ToString();
        var props = channel.CreateBasicProperties();
        props.ReplyTo = replyQueueName;
        props.CorrelationId = corrId;

        var messageBytes = Encoding.UTF8.GetBytes(message);
        channel.BasicPublish("", "rpc_queue", props, messageBytes);

        while (true)
        {
            var ea = (BasicDeliverEventArgs)consumer.Queue.Dequeue();
            if (ea.BasicProperties.CorrelationId == corrId)
            {
                return Encoding.UTF8.GetString(ea.Body);
            }
        }
    }

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

class RPC
{
    public static void Main()
    {
        var rpcClient = new RPCClient();

        Console.WriteLine(" [x] Requesting fib(30)");
        var response = rpcClient.Call("30");
        Console.WriteLine(" [.] Got ‘{0}‘", response);

        rpcClient.Close();
    }
}

The client code is slightly more involved:

  • 建立connection和channel,申明一个独占的临时回调队列。
  • 订阅回调队列,以便能够接收RPC请求的response。
  • 实现Call方法,用于真正发送RPC请求
  • 首先生成唯一的 correlationId 值保存下来,while循环中将会用这个值来匹配response。
  • 建立RPC请求消息,包含两个属性: replyTo and correlationId.
  • 发出RPC请求,等待回应.
  • while循环对于每一个response都会检查 thecorrelationId 是否匹配,如果匹配就保存下来这个response.
  • 最后返回response给用户.

 

测试程序,开始进行RPC调用:

RPCClient fibonacciRpc = new RPCClient();

System.out.println(" [x] Requesting fib(30)");
String response = fibonacciRpc.call("30");
System.out.println(" [.] Got ‘" + response + "‘");

fibonacciRpc.close();

Now is a good time to take a look at our full example source code (which includes basic exception handling) for RPCClient.cs and RPCServer.cs.

Compile as usual (see tutorial one):

$ csc /r:"RabbitMQ.Client.dll" RPCClient.cs
$ csc /r:"RabbitMQ.Client.dll" RPCServer.cs

Our RPC service is now ready. We can start the server:

$ RPCServer.exe
 [x] Awaiting RPC requests

To request a fibonacci number run the client:

$ RPCClient.exe
 [x] Requesting fib(30)

The design presented here is not the only possible implementation of a RPC service, but it has some important advantages:

  • If the RPC server is too slow, you can scale up by just running another one. Try running a second RPCServer in a new console.
  • On the client side, the RPC requires sending and receiving only one message. No synchronous calls like queueDeclare are required. As a result the RPC client needs only one network round trip for a single RPC request.

Our code is still pretty simplistic and doesn‘t try to solve more complex (but important) problems, like:

  • How should the client react if there are no servers running?
  • Should a client have some kind of timeout for the RPC?
  • If the server malfunctions and raises an exception, should it be forwarded to the client?
  • Protecting against invalid incoming messages (eg checking bounds, type) before processing.

If you want to experiment, you may find the rabbitmq-management plugin useful for viewing the queues.

RabbitMQ学习(六).NET Client之RPC