Article Directory
Official document address: 6 RPC
Example of request/response mode.
Prerequisites
This tutorial assumes that you have installed RabbitMQ and is running on the local host port (5672).
Remote procedure call (RPC)
RPC:Remote Procedure Call。
In the second tutorial, we learned how to use work queues to worker
distribute time-consuming tasks among multiple tasks.
But what if we need to run a function on a remote computer and wait for the result? Well, that is another matter. This mode is usually called 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 don't have any time-consuming tasks worth distributing, we will create a virtual RPC service that returns the Fibonacci sequence (an integer sequence).
Client interface
To illustrate how to use RPC services, we will create a simple client class. It will expose a call
method named , send an RPC request and block until it receives a response:
FibonacciRpcClient fibonacciRpc = new FibonacciRpcClient();
String result = fibonacciRpc.call("4");
System.out.println( "fib(4) is " + result);
A note on RPC
Although RPC is a very common pattern in computing, it is often criticized. Problems arise when the programmer does not know whether the function call is local or slow RPC. Such confusion can lead to unpredictable systems and add unnecessary complexity to debugging. Misuse of RPC can lead to unmaintainable spaghetti-style code, rather than simplifying the code.
With this in mind, consider the following suggestions:
- It can be clearly distinguished which function call is local and which is remote.
- System documentation. Clarify the dependencies between components.
- Handling error conditions. How should the client react when the RPC server hangs for a long time?
When in doubt, avoid using RPC. If you can, you should use asynchronous transmission-instead of blocking like RPC, the result is pushed asynchronously to the next stage of calculation.
Callback queue
Generally, it is easy to perform RPC on RabbitMQ. The client sends a request message, and the server responds with a response message. In order to receive the response, we need to send a 'callback'
queue address in the request . We can use the default queue (it is exclusive in the Java client, which is covered in the temporary queue in the third tutorial). Let's try it:
callbackQueueName = channel.queueDeclare().getQueue();
BasicProperties props = new BasicProperties
.Builder()
.replyTo(callbackQueueName)
.build();
channel.basicPublish("", "rpc_queue", props, message.getBytes());
// ... 然后编写代码从callback_queue读取响应消息 ...
The message attribute
AMQP 0-9-1
protocol predefines14
each attribute to be used with the message . Most attributes are rarely used, except for the following:
- deliveryMode : Mark the message as durable (value
2
) or instantaneous (any other value). You may remember this attribute from the second tutorial. (UseMessageProperties
the value to set, see the second tutorial.)- contentType : used to describe the encoding
mime-type
. For example, for frequently usedJSON
encodings, it is best to set this property toapplication/json
.- replyTo : Usually used to name the callback queue.
- correlationId : Used to correlate the RPC response with the request.
We need this new import:
import com.rabbitmq.client.AMQP.BasicProperties;
Association ID
In the above method, we propose to create a callback queue for each RPC request. This is very inefficient, but fortunately, there is a better way-we can create a callback queue for each client.
This creates a new problem. After receiving the response in the queue, it is not clear which request the response belongs to. At this time, correlationId
attributes are used . We will set a unique value for each request. Later, when we receive a message in the callback queue, we will look at this property. Based on this property, we will be able to match the response with the request. If we see an unknown correlationId
value, we can safely discard the message-it does not belong to our request.
You may ask, why should we ignore unknown messages in the callback queue instead of failing due to errors? This is due to possible race conditions on the server side. Although unlikely, the RPC server may die after sending us a response before sending the confirmation message of the request. If this happens, the restarted RPC server will process the request again. This is why we must handle repeated responses gracefully on the client, and RPC should be idempotent under ideal circumstances.
Summary
Our RPC will work like this:
- For RPC requests, the client sends a message with two attributes: it will be
replyTo
set to an anonymous exclusive queue created only for the request, and it will becorrelationId
set to a unique value for each request. - The request is sent to the
rpc_queue
queue. - The RPC worker (that is, the server) is waiting for a request on this queue. When a request comes up, it executes the task and uses
replyTo
the queue in the field to send the result back to the client. - The client waits for data on the reply queue. When the message appears, it checks the
correlationId
properties. If it matches the requested value, the application's response is returned.
Put them together
Fibonacci’s tasks:
private static int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fib(n-1) + fib(n-2);
}
We declared the Fibonacci function. It assumes only valid positive integer input. (Don't expect this method to work for large numbers, it may be the slowest recursive implementation).
The code of our RPC server is RPCServer.java
as follows:
import com.rabbitmq.client.*;
public class RPCServer {
private static final String RPC_QUEUE_NAME = "rpc_queue";
private static int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fib(n - 1) + fib(n - 2);
}
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
channel.queuePurge(RPC_QUEUE_NAME);
channel.basicQos(1);
System.out.println(" [x] Awaiting RPC requests");
Object monitor = new Object();
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
AMQP.BasicProperties replyProps = new AMQP.BasicProperties
.Builder()
.correlationId(delivery.getProperties().getCorrelationId())
.build();
String response = "";
try {
String message = new String(delivery.getBody(), "UTF-8");
int n = Integer.parseInt(message);
System.out.println(" [.] fib(" + message + ")");
response += fib(n);
} catch (RuntimeException e) {
System.out.println(" [.] " + e.toString());
} finally {
channel.basicPublish("", delivery.getProperties().getReplyTo(), replyProps, response.getBytes("UTF-8"));
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
//RabbitMq消费者线程通知RPC服务器所有者线程
synchronized (monitor) {
monitor.notify();
}
}
};
channel.basicConsume(RPC_QUEUE_NAME, false, deliverCallback, (consumerTag -> {
}));
//等待并准备使用来自RPC客户端的消息
while (true) {
synchronized (monitor) {
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
The server code is fairly simple:
- As always, we first establish the connection, channel, and declaration queue.
- We may need to run multiple server processes. In order to evenly distribute the load on multiple servers, we need
channel.basicQos
to set it inprefetchCount
. - We use
basicConsume
to access the queue. In the queue, weDeliverCallback
provide a callback in the form of an object ( ), which will complete the work and send the response back.
The code of our RPC client is RPCClient.java
as follows:
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeoutException;
public class RPCClient implements AutoCloseable {
private Connection connection;
private Channel channel;
private String requestQueueName = "rpc_queue";
public RPCClient() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
connection = factory.newConnection();
channel = connection.createChannel();
}
public static void main(String[] argv) {
try (RPCClient fibonacciRpc = new RPCClient()) {
for (int i = 0; i < 32; i++) {
String i_str = Integer.toString(i);
System.out.println(" [x] Requesting fib(" + i_str + ")");
String response = fibonacciRpc.call(i_str);
System.out.println(" [.] Got '" + response + "'");
}
} catch (IOException | TimeoutException | InterruptedException e) {
e.printStackTrace();
}
}
public String call(String message) throws IOException, InterruptedException {
final String corrId = UUID.randomUUID().toString();
String replyQueueName = channel.queueDeclare().getQueue();
AMQP.BasicProperties props = new AMQP.BasicProperties
.Builder()
.correlationId(corrId)
.replyTo(replyQueueName)
.build();
channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));
final BlockingQueue<String> response = new ArrayBlockingQueue<>(1);
String ctag = channel.basicConsume(replyQueueName, true, (consumerTag, delivery) -> {
if (delivery.getProperties().getCorrelationId().equals(corrId)) {
response.offer(new String(delivery.getBody(), "UTF-8"));
}
}, consumerTag -> {
});
String result = response.take();
channel.basicCancel(ctag);
return result;
}
public void close() throws IOException {
connection.close();
}
}
The client code is slightly more complicated:
- We establish connections and
channel
. - Our
call
method makes the actual RPC request. - Here, we first generate a unique one
correlationId
and save it-our consumer callback will use this value to match the appropriate response. - Then, we create a dedicated exclusive queue for the response and subscribe to it.
- Next, we publish the request message using two attributes:
replyTo
andcorrelationId
. - At this point, we can sit down and wait for the appropriate response to arrive.
- Because our consumer delivery processing is performed in a separate thread, we need to suspend the
main
thread before the response arrives . UseBlockingQueue
is a possible solution.ArrayBlockingQueue
The capacity we created here is set to1
because we only need to wait for a response. - The consumer is doing a very simple job. For each consumer response message, it checks
correlationId
whether it is what we are looking for. If it is, it puts the response inBlockingQueue
. - At the same time, the
main
thread is waiting toBlockingQueue
get a response from it. - Finally, we return the response to the user.
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();
Compile and set the classpath as usual (see tutorial 1
):
javac -cp $CP RPCClient.java RPCServer.java
Our RPC
service is now ready. We can start the server:
java -cp $CP RPCServer
# => [x] Awaiting RPC requests
Run the client and request a Fibonacci number:
java -cp $CP RPCClient
# => [x] Requesting fib(30)
The design presented here is not the only possible implementation of RPC services, but it has some important advantages:
- If the RPC server is too slow, you can extend it by running another server. Try to run the second one in the new console
RPCServer
. - On the client side, RPC only needs to send and receive one message. There is no need
queueDeclare
for synchronous calls like this. Therefore, the RPC client only needs one network round trip for a single RPC request.
Our code is still very simple and does not attempt to solve more complex (but important) problems, such as:
- If no server is running, how should the client react?
- Should the client set some kind of timeout for RPC?
- If the server fails and throws an exception, should it be forwarded to the client?
- Prevent invalid incoming messages (such as checking boundaries, types) before processing.
If you want to experiment, you may find the management interface very useful for viewing queues.