In the second tutorial, we learned how to use work queues to allocate time-consuming tasks among multiple workers.
But what if we need to run features on a remote computer and wait for the results. That's a different pattern. This pattern is often referred to as a remote procedure call or RPC.
In this tutorial, we will use RABBITMQ to build an RPC system: a client and an extensible RPC server. Since we do not have any time-consuming tasks to distribute, we will create a virtual RPC service that returns a Fibonacci number. Client
To illustrate how to use RPC services, we will create a simple client class. It exposes a method called Call, which sends an RPC request and blocks until the answer is received:
var rpcclient = new Rpcclient ();
Console.WriteLine ("[X] requesting fib");
var response = Rpcclient.call ("a");
Console.WriteLine ("[.] Got ' {0} ' ", response);
Rpcclient.close ();
about RPC points of attention
Although RPC is a very common computing model, it is often criticized. The problem occurs when a programmer does not know whether a function call is a local or a slow RPC. Such confusion leads to an unpredictable system and increases the unnecessary complexity of debugging. Misuse of RPC can result in unsustainable spaghetti code rather than simplifying software.
with this in mind, consider the following recommendations:
Make sure that the function call is obvious and which is remote. Keep
track of your system. Makes the dependencies between components clear.
handle the error condition. How the client should react when the RPC server shuts down for a long time.
when there is doubt to avoid RPC. If you can, you should use asynchronous piping-rather than RPC-like blocking, and the results will be pushed asynchronously to the next computational phase.
Callback Queue
In general, RPC is easy on RABBITMQ. The client sends a request message and the server replies to the response message. In order to receive a response, we need to send a request with a callback queue address:
var Corrid = Guid.NewGuid (). ToString ();
var props = Channel. Createbasicproperties ();
Props. ReplyTo = Replyqueuename;
Props. Correlationid = Corrid;
var messagebytes = Encoding.UTF8.GetBytes (message);
Channel. Basicpublish (Exchange: "",
Routingkey: "Rpc_queue",
basicproperties:props,
body:messagebytes);
... then code to read a response the callback_queue ...
Message Property
The AMQP 0-9-1 protocol defines a set of 14 properties of the accompanying message. Most properties are rarely used except for the following:
DeliveryMode: Marks a message as persistent (a value of 2) or transient (any other value). You may remember this attribute from the second tutorial.
ContentType: An encoding used to describe MIME types. For example, for JSON encodings that are often used, setting this property to: Application/json is a good practice.
ReplyTo: Typically used to name a callback queue.
Correlationid: Used to associate an RPC response with a request.
Correlation Id
In the method presented above, we recommend that you create a callback queue for each RPC request. This is very inefficient, but fortunately there is a better way-let's create a callback queue for each client.
This raises a new issue in which a response is received and the request is not clear. That is using the Correlationid attribute. We will set a unique value for each request. Then, when we receive a message in the callback queue, we will look at this property and, based on that, we will be able to match the response to the request. If we see an unknown Correlationid value, we may safely discard the message-it does not belong to our request.
You might ask why we should ignore the unknown message in the callback queue, not the error. This is due to the possibility of competitive conditions on the server side. Although it is unlikely that the RPC server will die before it sends us the answer, but before sending the confirmation message for the request. If this occurs, the restarted RPC server will process the request again. That's why on the client side, we have to handle these repetitive responses gracefully, and RPC is supposed to be idempotent. Summary
Our RPC will work like this:
When the client starts, it creates an anonymous exclusive callback queue.
for RPC requests, the client sends a message with two attributes: ReplyTo, which is set to the callback queue and the Correlationid,correlationid is set to the unique value for each request.
the request is sent to the Rpc_queue queue.
RPC Worker (Aka:server) is waiting for a request on the queue. When the request appears, it executes the job and sends the result back to the client using the queue in the ReplyTo field.
the client waits for data in the callback queue. When the information appears, it checks the Correlationid property. If it matches the value in the request, the response to the application is returned.
Complete Example
Fibonacci tasks:
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 only assumes a valid positive integer input. (Do not expect this worker to work for large numbers, which may be the slowest recursive implementation possible).
The code for our RPC server RPCServer.cs is as follows:
Using System;
Using Rabbitmq.client;
Using RabbitMQ.Client.Events;
Using System.Text; Class Rpcserver {public static void Main () {var factory = new ConnectionFactory () {HostName = "Localhos
T "}; 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", Noack:false, Consumer:consumer);
Console.WriteLine ("[x] awaiting RPC requests"); Consumer.
Received + = (model, ea) => {string response = null; 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 (Exchange: "", Routingkey:props.)
ReplyTo, Basicproperties:replyprops, body:responsebytes); Channel. Basicack (Deliverytag:ea.
Deliverytag, Multiple:false);
}
};
Console.WriteLine ("Press [Enter] to exit"); Console.ReadLine ();
}//////assumes only valid positive integer input. Don ' t expect this one to work for big numbers, and it ' s///probably the slowest recursive implementation
.
private static int fib (int n) {if (n = = 0 | | n = = 1) {return n;
return fib (n-1) + fib (n-2); }
}
The server code is fairly simple:
As usual, we started building connections, channels and declaring queues.
we may want to run multiple server processes. In order to distribute the load evenly across multiple servers, we need to set the Prefetchcount setting in Channel.basicqos.
we use Basicconsume to access queues. Then we register a delivery handler in which we work and respond back.
Code RPCClient.cs for our RPC client:
Using System;
Using System.Collections.Generic;
Using System.Linq;
Using System.Text;
Using System.Threading.Tasks;
Using Rabbitmq.client;
Using RabbitMQ.Client.Events;
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 ().
QueueName;
Consumer = new Queueingbasicconsumer (channel); Channel. Basicconsume (Queue:replyqueuename, Noack:true, Consumer:consu
MER); 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 (Exchange: "", Routingkey: "Rpc_queue", Basicpropert
Ies:props, body: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 (); }
}
Client code involves:
We establish a connection and channel and declare an exclusive ' callback ' queue as a reply.
we subscribe to the ' callback ' queue so 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 uses this value to capture the appropriate response.
Next, we publish the request message, which contains two properties: ReplyTo and Correlationid.
at this point, we can sit down and wait for the appropriate response to arrive.
while loops are doing a very simple job, for each response message, it checks to see if Correlationid is what we are looking for. If so, it saves the response.
Finally, we return the response to the user.
To make a client request:
var rpcclient = new Rpcclient ();
Console.WriteLine ("[X] requesting fib");
var response = Rpcclient.call ("a");
Console.WriteLine ("[.] Got ' {0} ' ", response);
Rpcclient.close ();
Our RPC service is now ready. We can start the server:
CD rpcserver
dotnet run
# => [x] awaiting RPC requests
Run Client request Fibonacci number:
CD rpcclient
dotnet run
# => [x] requesting FIB (30)
The design presented here is not the only possible implementation of RPC services, but has some important advantages:
If the RPC server is too slow, it can be extended by running another RPC server. Try running the second rpcserver in the new console.
on the client side, RPC needs to send and receive a message. There is no need to synchronize calls, such as Queuedeclare. As a result, RPC clients need only one network round trip to a single RPC request.
Our code is still very simple and does not attempt to solve more complex (but important) problems, such as:
How the client should react if no server is running.
whether the client needs some kind of timeout for RPC.
if the server fails and throws an exception, it should be forwarded to the client.
prevents invalid incoming messages (such as checking boundaries, types) before processing.