6 RPC


Dirección del documento oficial: 6 RPC


Ejemplo de modo de solicitud / respuesta.

Requisitos previos

Este tutorial asume que ha instalado RabbitMQ y se está ejecutando en el puerto de host local (5672).

Llamada a procedimiento remoto (RPC)

RPC : Llamada a procedimiento remoto

En el segundo tutorial, aprendimos cómo usar las colas de trabajo para workerdistribuir las tareas que consumen mucho tiempo entre varias tareas.

Pero, ¿qué pasa si necesitamos ejecutar una función en una computadora remota y esperar el resultado? Bueno, eso es otro asunto. Este modo generalmente se denomina llamada a procedimiento remoto o RPC.

En este tutorial, usaremos RabbitMQ para construir un sistema RPC: un cliente y un servidor RPC extensible. Debido a que no tenemos tareas que requieran mucho tiempo que valga la pena distribuir, crearemos un servicio RPC virtual que devuelve la secuencia de Fibonacci (una secuencia de números enteros).

Interfaz de cliente

Para ilustrar cómo usar los servicios RPC, crearemos una clase de cliente simple. Expondrá un callmétodo nombrado , enviará una solicitud RPC y bloqueará hasta que reciba una respuesta:

FibonacciRpcClient fibonacciRpc = new FibonacciRpcClient();
String result = fibonacciRpc.call("4");
System.out.println( "fib(4) is " + result);

Una nota sobre RPC

Aunque RPC es un patrón muy común en informática, a menudo es criticado. Los problemas surgen cuando el programador no sabe si la llamada a la función es local o RPC lento. Tal confusión puede conducir a sistemas impredecibles y agregar una complejidad innecesaria a la depuración. El mal uso de RPC puede conducir a un código estilo espagueti que no se puede mantener, en lugar de simplificar el código.

Teniendo esto en cuenta, considere las siguientes sugerencias:

  • Se puede distinguir claramente qué llamada de función es local y cuál es remota.
  • Documentación del sistema. Aclare las dependencias entre componentes.
  • Manejo de condiciones de error. ¿Cómo debería reaccionar el cliente cuando el servidor RPC se cuelga durante mucho tiempo?

En caso de duda, evite utilizar RPC. Si puede, debe utilizar la transmisión asincrónica: en lugar de bloquear como RPC, el resultado se envía de forma asincrónica a la siguiente etapa de cálculo.

Cola de devolución de llamada

Generalmente, es fácil realizar RPC en RabbitMQ. El cliente envía un mensaje de solicitud y el servidor responde con un mensaje de respuesta. Para recibir la respuesta, debemos enviar una 'callback'dirección de cola en la solicitud . Podemos usar la cola predeterminada (es exclusiva en el cliente Java, que se trata en la cola temporal en el tercer tutorial). Vamos a intentarlo:

callbackQueueName = channel.queueDeclare().getQueue();

BasicProperties props = new BasicProperties
                            .Builder()
                            .replyTo(callbackQueueName)
                            .build();

channel.basicPublish("", "rpc_queue", props, message.getBytes());

// ... 然后编写代码从callback_queue读取响应消息 ...

El

AMQP 0-9-1 protocolo de atributo de mensaje predefine 14cada atributo que se utilizará con el mensaje . La mayoría de los atributos se utilizan con poca frecuencia, excepto los siguientes:

  • deliveryMode : marca el mensaje como duradero (valor 2) o instantáneo (cualquier otro valor). Puede recordar este atributo del segundo tutorial. (Utilice MessagePropertiesel valor para establecer, consulte el segundo tutorial).
  • contentType : utilizado para describir la codificación mime-type. Por ejemplo, para JSONcodificaciones de uso frecuente , es mejor establecer esta propiedad en application/json.
  • replyTo : generalmente se usa para nombrar la cola de devolución de llamada.
  • correlationId : se utiliza para correlacionar la respuesta RPC con la solicitud.

Necesitamos esta nueva importación:

import com.rabbitmq.client.AMQP.BasicProperties;

ID de asociación

En el método anterior, proponemos crear una cola de devolución de llamada para cada solicitud RPC. Esto es muy ineficiente, pero afortunadamente, hay una mejor manera: podemos crear una cola de devolución de llamada para cada cliente.

Esto crea un nuevo problema: después de recibir la respuesta en la cola, no está claro a qué solicitud pertenece la respuesta. En este momento, se utilizan correlationIdatributos. Estableceremos un valor único para cada solicitud. Más tarde, cuando recibamos un mensaje en la cola de devolución de llamada, veremos esta propiedad y, en función de esta propiedad, podremos hacer coincidir la respuesta con la solicitud. Si vemos un correlationIdvalor desconocido , podemos descartar el mensaje de forma segura, ya que no pertenece a nuestra solicitud.

Puede preguntar, ¿por qué deberíamos ignorar los mensajes desconocidos en la cola de devolución de llamada en lugar de fallar debido a errores? Esto se debe a posibles condiciones de carrera en el lado del servidor. Aunque es poco probable, el servidor RPC puede morir después de enviarnos una respuesta antes de enviar el mensaje de confirmación de la solicitud. Si esto sucede, el servidor RPC reiniciado procesará la solicitud nuevamente. Es por eso que debemos manejar las respuestas repetidas con elegancia en el cliente, y RPC debe ser idempotente en circunstancias ideales.

Resumen


Nuestro RPC funcionará así:

  • Para las solicitudes RPC, el cliente envía un mensaje con dos atributos: se replyToconfigurará en una cola exclusiva anónima creada solo para la solicitud, y se correlationIdconfigurará en un valor único para cada solicitud.
  • La solicitud se envía a la rpc_queuecola.
  • El trabajador de RPC (es decir, el servidor) está esperando una solicitud en esta cola. Cuando surge una solicitud, ejecuta la tarea y usa replyTola cola en el campo para enviar el resultado al cliente.
  • El cliente espera datos en la cola de respuestas. Cuando aparece el mensaje, comprueba las correlationIdpropiedades. Si coincide con el valor solicitado, se devuelve la respuesta de la aplicación.

Póngalos juntos

Tareas de Fibonacci:

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

Declaramos la función de Fibonacci. Solo asume una entrada de entero positivo válida. (No espere que este método funcione para grandes números, puede ser la implementación recursiva más lenta).

El código de nuestro servidor RPC es el RPCServer.javasiguiente:

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();
                    }
                }
            }
        }
    }
}

El código del servidor es bastante simple:

  • Como siempre, primero establecemos la cola de conexión, canal y declaración.
  • Es posible que necesitemos ejecutar varios procesos de servidor. Para distribuir uniformemente la carga en varios servidores, debemos channel.basicQosconfigurarlo prefetchCount.
  • Usamos basicConsumepara acceder a la cola. En la cola, DeliverCallbackproporcionamos una devolución de llamada en forma de un objeto ( ), que completará el trabajo y enviará la respuesta.

El código de nuestro cliente RPC es el RPCClient.javasiguiente:

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();
    }
}

El código del cliente es un poco más complicado:

  • Establecemos conexiones y channel.
  • Nuestro callmétodo realiza la solicitud RPC real.
  • Aquí, primero generamos uno único correlationIdy lo guardamos ; nuestra devolución de llamada del consumidor utilizará este valor para hacer coincidir la respuesta adecuada.
  • Luego, creamos una cola exclusiva dedicada para la respuesta y nos suscribimos a ella.
  • A continuación, publicamos el mensaje de solicitud utilizando dos atributos: replyToy correlationId.
  • En este punto, podemos sentarnos y esperar a que llegue la respuesta adecuada.
  • Debido a que nuestro procesamiento de entrega al consumidor se realiza en un subproceso separado, debemos suspender el mainsubproceso antes de que llegue la respuesta . El uso BlockingQueuees una posible solución. ArrayBlockingQueueLa capacidad que creamos aquí está configurada 1porque solo necesitamos esperar una respuesta.
  • El consumidor está haciendo un trabajo muy simple: para cada mensaje de respuesta del consumidor, verifica correlationIdsi es lo que estamos buscando. Si es así, pone la respuesta BlockingQueue.
  • Al mismo tiempo, el mainhilo está esperando BlockingQueueobtener una respuesta.
  • Finalmente, devolvemos la respuesta al usuario.

Solicitud de cliente:

RPCClient fibonacciRpc = new RPCClient();

System.out.println(" [x] Requesting fib(30)");
String response = fibonacciRpc.call("30");
System.out.println(" [.] Got '" + response + "'");

fibonacciRpc.close();

Compile y configure el classpath como de costumbre (vea el tutorial 1):

javac -cp $CP RPCClient.java RPCServer.java

Nuestro RPCservicio ya está listo. Podemos iniciar el servidor:

java -cp $CP RPCServer
# => [x] Awaiting RPC requests

Ejecute el cliente y solicite un número de Fibonacci:

java -cp $CP RPCClient
# => [x] Requesting fib(30)

El diseño que se presenta aquí no es la única implementación posible de los servicios RPC, pero tiene algunas ventajas importantes:

  • Si el servidor RPC es demasiado lento, puede ampliarlo ejecutando otro servidor. Intente ejecutar el segundo en la nueva consola RPCServer.
  • En el lado del cliente, RPC solo necesita enviar y recibir un mensaje. No es necesario queueDeclarerealizar llamadas sincrónicas como esta. Por lo tanto, el cliente RPC solo necesita un viaje de ida y vuelta de red para una única solicitud RPC.

Nuestro código sigue siendo muy simple y no intenta resolver problemas más complejos (pero importantes), como:

  • Si no se está ejecutando ningún servidor, ¿cómo debería reaccionar el cliente?
  • ¿Debería el cliente establecer algún tipo de tiempo de espera para RPC?
  • Si el servidor falla y lanza una excepción, ¿debería reenviarse al cliente?
  • Evite los mensajes entrantes no válidos (como la verificación de límites, tipos) antes de procesarlos.

Si desea experimentar, puede encontrar la interfaz de administración muy útil para ver las colas.

Supongo que te gusta

Origin blog.csdn.net/wb1046329430/article/details/115290260
RPC
Recomendado
Clasificación