6 RPC


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 workerdistribute 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 callmethod 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 predefines 14each 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. (Use MessagePropertiesthe value to set, see the second tutorial.)
  • contentType : used to describe the encoding mime-type. For example, for frequently used JSONencodings, it is best to set this property to application/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, correlationIdattributes 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 correlationIdvalue, 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 replyToset to an anonymous exclusive queue created only for the request, and it will be correlationIdset to a unique value for each request.
  • The request is sent to the rpc_queuequeue.
  • 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 replyTothe 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 correlationIdproperties. 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.javaas 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.basicQosto set it in prefetchCount.
  • We use basicConsumeto access the queue. In the queue, we DeliverCallbackprovide 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.javaas 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 callmethod makes the actual RPC request.
  • Here, we first generate a unique one correlationIdand 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: replyToand correlationId.
  • 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 mainthread before the response arrives . Use BlockingQueueis a possible solution. ArrayBlockingQueueThe capacity we created here is set to 1because we only need to wait for a response.
  • The consumer is doing a very simple job. For each consumer response message, it checks correlationIdwhether it is what we are looking for. If it is, it puts the response in BlockingQueue.
  • At the same time, the mainthread is waiting to BlockingQueueget 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 RPCservice 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 queueDeclarefor 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.

Guess you like

Origin blog.csdn.net/wb1046329430/article/details/115290260
RPC
RPC