If you have been a developer for at least a couple of years, chances are pretty high that you heard about message brokers. Message Brokers are software applications that enable services/systems to communicate between each other and exchange information reliably. RabbitMq is probably one of the most widely used message broker systems used across the globe.
In this article, we will use RabbitMq to implement a RPC (remote procedure call) call. The architecture of the system would revolve around a request Queue and a response queue (each client would have its own client queue). We will use the CorrelationId to distinguish between individual calls made by the client. The idea is illustrated in the diagram below.

To illustrate the idea further to develop a client and a server, each of its functionalities could be summarized as the following.
- Client
Displays a list of User Names (just some strings) received from Server, Should be able to send a number to the server, which would indicate a number of usernames to return.
- Server
Listens to the probable requests from different clients. Generates and returns a collection of strings denoting usernames based on the count indicated by the client in the request message.
The Server
We will begin by creating the Server. The Server would receive a message in the request queue. The message would also contain meta-information which would help the server to understand the response queue to use. Each of the requests would contain
- The actual message
- Response Queue Name
- Correlation Id (uniquely identify each request)
Let us begin defining and initializing our queue and listeners.
// Rpc Client on Server
internal class RpcClient: IDisposable {
private
const string QueueName = "UserRpcQueue";
private IConnection _connection;
private IModel _channel;
private bool _isDisposed;
public void InitializeAndRun() {
var factory = new ConnectionFactory {
HostName = "localhost",
};
_connection = factory.CreateConnection();
_channel = _connection.CreateModel();
_channel.QueueDeclare(queue: QueueName, durable: false, exclusive: false, autoDelete: false, arguments: null);
_channel.BasicQos(0, 1, false);
var consumer = new EventingBasicConsumer(_channel);
_channel.BasicConsume(queue: QueueName, autoAck: false, consumer: consumer);
consumer.Received += Consumer_Received; // we will come to this in a bit
}
}
In the above code, we are initializing a Queue with the name "UserRpcQueue". Note that we have set the Auto Acknowledgement to false. This is important because we need to send the acknowledgment only after executing the method and sending the response back.
Let us now expand the Consumer_Recieved method and decide what we need to do with each incoming request and how we respond to it. Remember, we will need to send the acknowledgment for the incoming request.
private void Consumer_Received(object ? sender, BasicDeliverEventArgs ea) {
LogMessage?.Invoke("Recieved Request from client..");
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
var replyProps = _channel.CreateBasicProperties();
replyProps.CorrelationId = ea.BasicProperties.CorrelationId;
replyProps.ReplyTo = ea.BasicProperties.ReplyTo;
var value = int.Parse(message);
LogMessage?.Invoke($ "Preparing to generate {value} User Names");
var userNames = GenerateUserNames(value); // the Worker method, which needs to be executed by the Server.
foreach(var user in userNames) {
LogMessage?.Invoke(user);
}
var response = JsonSerializer.Serialize < IEnumerable < string >> (userNames);
var responseBody = Encoding.UTF8.GetBytes(response);
_channel.BasicPublish(exchange: "", routingKey: replyProps.ReplyTo, basicProperties: replyProps, body: responseBody);
_channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
}
What the Server does is actually pretty simple ( yeah, the real trick is in the client). It receives the request from one of the clients in a specific queue, in this case, named "UserRpcQueue". It then unwraps the message to retrieve the queue to use for response and the correlation id which would be used by the client to uniquely identify each request (and subsequent response).
Once it has this information it would now process the actual message and execute the operation required. In this case, it is the generation of a few strings (usernames), where the number of strings to be generated is the message sent by the client.
The response of the operation is then sent back to the client through the reply queue name supplied by the client. It also remembers to pack the CorrelationId which it received in the request as a part of response too. The server would use the correlation id to identify the individual request (we will get to it in a moment). At this point, the server is also ready to acknowledge the client for receipt and processing of the request.
The full source code for RpcClient on server is as follows.
// Rpc Client on Server
internal class RpcClient: IDisposable {
private
const string QueueName = "UserRpcQueue";
private IConnection _connection;
private IModel _channel;
private bool _isDisposed;
public void InitializeAndRun() {
var factory = new ConnectionFactory {
HostName = "localhost",
};
_connection = factory.CreateConnection();
_channel = _connection.CreateModel();
_channel.QueueDeclare(queue: QueueName, durable: false, exclusive: false, autoDelete: false, arguments: null);
_channel.BasicQos(0, 1, false);
var consumer = new EventingBasicConsumer(_channel);
_channel.BasicConsume(queue: QueueName, autoAck: false, consumer: consumer);
consumer.Received += Consumer_Received;
}
private void Consumer_Received(object ? sender, BasicDeliverEventArgs ea) {
LogMessage?.Invoke("Recieved Request from client..");
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
var replyProps = _channel.CreateBasicProperties();
replyProps.CorrelationId = ea.BasicProperties.CorrelationId;
replyProps.ReplyTo = ea.BasicProperties.ReplyTo;
var value = int.Parse(message);
LogMessage?.Invoke($ "Preparing to generate {value} User Names");
var userNames = GenerateUserNames(value);
foreach(var user in userNames) {
LogMessage?.Invoke(user);
}
var response = JsonSerializer.Serialize < IEnumerable < string >> (userNames);
var responseBody = Encoding.UTF8.GetBytes(response);
_channel.BasicPublish(exchange: "", routingKey: replyProps.ReplyTo, basicProperties: replyProps, body: responseBody);
_channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
}
public IEnumerable < string > GenerateUserNames(int count) {
return Enumerable.Range(1, count).Select(x => $ "UserName {x}");
}
public delegate void LogMessageHandler(string message);
public event LogMessageHandler LogMessage;
private void Dispose(bool disposing) {
if (_isDisposed) return;
if (disposing) {
_channel.Close();
}
_isDisposed = true;
}
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}~RpcClient() {
Dispose(false);
}
}
Join the conversation! Your thoughts help the community grow.