Remote procedure call (RPC)

(using the .NET client)

Prerequisites

This tutorial assumes RabbitMQ isinstalled and running on localhoston standard port (5672). In case you use a different host, port or credentials, connections settings would require adjusting.

Where to get help

If you're having trouble going through this tutorial you can contact usthrough the mailing list.

In the second tutorial we learned how to use Work Queues to distribute time-consuming tasks among multiple workers.

But what if we need to run a function on a remote computer and wait for the result? Well, that's a different story. This pattern is commonly known asRemote Procedure Call or RPC.

In this tutorial we're going to use RabbitMQ to build an RPC system: a client and a scalable RPC server. As we don't have any time-consuming tasks that are worth distributing, we're going to create a dummy RPC service that returns Fibonacci numbers.

Client interface

To illustrate how an RPC service could be used we're going to create a simple client class. It's going to expose a method named call which sends an RPC request and blocks until the answer is received:

var rpcClient = new RPCClient();

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

A note on RPC

Although RPC is a pretty common pattern in computing, it's often criticised. The problems arise when a programmer is not aware whether a function call is local or if it's a slow RPC. Confusions like that result in an unpredictable system and adds unnecessary complexity to debugging. Instead of simplifying software, misused RPC can result in unmaintainable spaghetti code.

Bearing that in mind, consider the following advice:

  • Make sure it's obvious which function call is local and which is remote.
  • Document your system. Make the dependencies between components clear.
  • Handle error cases. How should the client react when the RPC server is down for a long time?

When in doubt avoid RPC. If you can, you should use an asynchronous pipeline - instead of RPC-like blocking, results are asynchronously pushed to a next computation stage.

Callback queue

In general doing RPC over RabbitMQ is easy. A client sends a request message and a server replies with a response message. In order to receive a response we need to send a 'callback' queue address with the request:

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 ...

Message properties

The AMQP protocol predefines a set of 14 properties that go with a message. Most of the properties are rarely used, with the exception of the following:

  • deliveryMode: Marks a message as persistent (with a value of 2) or transient (any other value). You may remember this property from the second tutorial.
  • contentType: Used to describe the mime-type of the encoding. For example for the often used JSON encoding it is a good practice to set this property to: application/json.
  • replyTo: Commonly used to name a callback queue.
  • correlationId: Useful to correlate RPC responses with requests.

Correlation Id

In the method presented above we suggest creating a callback queue for every RPC request. That's pretty inefficient, but fortunately there is a better way - let's create a single callback queue per client.

That raises a new issue, having received a response in that queue it's not clear to which request the response belongs. That's when the correlationId property is used. We're going to set it to a unique value for every request. Later, when we receive a message in the callback queue we'll look at this property, and based on that we'll be able to match a response with a request. If we see an unknown correlationId value, we may safely discard the message - it doesn't belong to our requests.

You may ask, why should we ignore unknown messages in the callback queue, rather than failing with an error? It's due to a possibility of a race condition on the server side. Although unlikely, it is possible that the RPC server will die just after sending us the answer, but before sending an acknowledgment message for the request. If that happens, the restarted RPC server will process the request again. That's why on the client we must handle the duplicate responses gracefully, and the RPC should ideally be idempotent.

Summary

Our RPC will work like this:

  • When the Client starts up, it creates an anonymous exclusive callback queue.
  • For an RPC request, the Client sends a message with two properties: replyTo, which is set to the callback queue and correlationId, which is set to a unique value for every request.
  • The request is sent to an rpc_queue queue.
  • The RPC worker (aka: server) is waiting for requests on that queue. When a request appears, it does the job and sends a message with the result back to the Client, using the queue from thereplyTo field.
  • The client waits for data on the callback queue. When a message appears, it checks thecorrelationId property. If it matches the value from the request it returns the response to the application.

Putting it all together

The Fibonacci task:

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

We declare our fibonacci function. It assumes only valid positive integer input. (Don't expect this one to work for big numbers, and it's probably the slowest recursive implementation possible).

The code for our RPC server RPCServer.cs looks like this:

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:

  • As usual we start by establishing the connection, channel and declaring the queue.
  • We might want to run more than one server process. In order to spread the load equally over multiple servers we need to set the prefetchCount setting in channel.basicQos.
  • We use basicConsume to access the queue. Then we enter the while loop in which we wait for request messages, do the work and send the response back.

The code for our RPC client 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:

  • We establish a connection and channel and declare an exclusive 'callback' queue for replies.
  • We subscribe to the 'callback' queue, so that we can receive RPC responses.
  • Our call method makes the actual RPC request.
  • Here, we first generate a unique correlationId number and save it - the while loop will use this value to catch the appropriate response.
  • Next, we publish the request message, with two properties: replyTo and correlationId.
  • At this point we can sit back and wait until the proper response arrives.
  • The while loop is doing a very simple job, for every response message it checks if thecorrelationId is the one we're looking for. If so, it saves the response.
  • Finally we return the response back to the user.

Making the Client request:

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.

Remote procedure call (RPC)的更多相关文章

  1. 译:6.RabbitMQ Java Client 之 Remote procedure call (RPC,远程过程调用)

    在  译:2. RabbitMQ 之Work Queues (工作队列)  我们学习了如何使用工作队列在多个工作人员之间分配耗时的任务. 但是如果我们需要在远程计算机上运行一个函数并等待结果呢?嗯,这 ...

  2. 官网英文版学习——RabbitMQ学习笔记(八)Remote procedure call (RPC)

    在第四篇学习笔记中,我们学习了如何使用工作队列在多个工作者之间分配耗时的任务.   但是,如果我们需要在远程计算机上运行一个函数并等待结果呢?这是另一回事.这种模式通常称为远程过程调用或RPC.   ...

  3. 分布式计算 要不要把写日志独立成一个Server Remote Procedure Call Protocol

    w https://en.wikipedia.org/wiki/Remote_procedure_call In distributed computing a remote procedure ca ...

  4. RPC(Remote Procedure Call Protocol)——远程过程调用协议

    RPC(Remote Procedure Call Protocol)--远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议.RPC协议假定某些传输协议的存在 ...

  5. RPC(Remote Procedure Call Protocol)远程过程调用协议

    RPC(Remote Procedure Call Protocol)——远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议.RPC协议假定某些传输协议的存在 ...

  6. RPC远程过程调用(Remote Procedure Call)

    RPC,就是Remote Procedure Call,远程过程调用 远程过程调用,自然是相对于本地过程调用 本地过程调用,就好比你现在在家里,你要想洗碗,那你直接把碗放进洗碗机,打开洗碗机开关就可以 ...

  7. RPC(Remote Procedure Call Protocol)——远程过程调用协议 学习总结

        首先了解什么叫RPC,为什么要RPC,RPC是指远程过程调用,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需 ...

  8. 每日扫盲(五):RPC(Remote Procedure Call)

    作者:洪春涛链接:https://www.zhihu.com/question/25536695/answer/221638079来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注 ...

  9. RPC(Remote Procedure Call Protocol)

    远程过程调用协议: 1.调用客户端句柄:执行传送参数 2.调用本地系统内核发送网络消息 3.消息传送到远程主机 4.服务器句柄得到消息并取得参数 5.执行远程过程 6.执行的过程将结果返回服务器句柄 ...

随机推荐

  1. xampp命令

    XAMPP命令安装 XAMPPtar xvfz xampp-linux-1.6.4.tar.gz -C /opt启动 XAMPP/opt/lampp/lampp start停止 XAMPP/opt/l ...

  2. bzoj 2005 NOI 2010 能量采集

    我们发现对于一个点(x,y),与(0,0)连线上的点数是gcd(x,y)-1 那么这个点的答案就是2*gcd(x,y)-1,那么最后的答案就是所有点 的gcd值*2-n*m,那么问题转化成了求每个点的 ...

  3. 白话TCP三次握手

    在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接. 第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认: 第二次握 ...

  4. spring 声明式事务中try catch捕获异常

    原文:http://heroliuxun.iteye.com/blog/848122 今天遇到了一个这个问题 最近遇到这样的问题,使用spring时,在业务层需要捕获异常(特殊需要),当前一般情况下不 ...

  5. 《Java编程思想》笔记 第十三章 字符串

    1.String对象不可变 String对象不可变,只读.任何指向它的引用都不能改变它的内容.改变String内容意味着创建了一个新的String对象. String 对象作为方法参数时都会复制一份引 ...

  6. Selenium2+python自动化49-判断文本(text_to_be_present_in_element)【转载】

    前言 在做结果判断的时候,经常想判断某个元素中是否存在指定的文本,如登录后判断页面中是账号是否是该用户的用户名. 在前面的登录案例中,写了一个简单的方法,但不是公用的,在EC模块有个方法是可以专门用来 ...

  7. python高阶函数,map,filter,reduce,ord,以及lambda表达式

    为什么我突然扯出这么几个函数,是因为我今天在看流畅的python这本书的时候,里面有一部分内容看的有点懵逼. >>> symbols = '$¢£¥€¤' >>> ...

  8. 如何在windows平台下使用hsdis与jitwatch查看JIT后的汇编码

    1. 安装hsids 这一步比较麻烦,需要提前安装cygwin,以及下载openjdk的源码 具体步骤请参考下面的两篇文章 How to build hsdis-amd64.dll and hsdis ...

  9. Docker发布镜像至Docker Hub

    第一步:Docker生成镜像 docker@default:~$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE metal-workbench- ...

  10. 构建ASP.NET MVC4+EF5+EasyUI+Unity2.x注入的后台管理系统

    http://www.tuicool.com/articles/NfyqQr 本节主要知识点是easyui 的手风琴加树结构做菜单导航 有园友抱怨原来菜单非常难看,但是基于原有树形无限级别的设计,没有 ...