RabbitMQ非官方教程(七)RPC

第三篇教程中,我们学习了如何使用工作队列在多个工作人员之间分配耗时的任务。但如果我们需要在远程计算机上运行功能并等待结果怎么办?

那就算是一个不同的故事,这种模式通常称为“ 远程过程调用”或“ RPC”。在本节我们将使用RabbitMQ构建RPC系统:客户端和可伸缩RPC服务器。由于我们没有值得分配的耗时任务,因此我们将创建一个虚拟RPC服务,该服务返回斐波那契数。

客户端界面

为了说明如何使用RPC服务,我们将创建一个简单的客户端类。它将公开一个名为call的方法,该方法发送RPC请求并阻塞,直到收到答案为止:

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

有关RPC的说明

尽管RPC是计算中非常普遍的模式,但它经常受到批评。当程序员不知道函数调用是本地的还是缓慢的RPC时,就会出现问题。这样的混乱会导致系统变幻莫测,并给调试增加了不必要的复杂性。滥用RPC可能会导致无法维护的意大利面条代码,而不是简化软件。

牢记这一点,请考虑以下建议:

  • 确保明显的是哪个函数调用是本地的,哪个是远程的。
  • 记录您的系统。明确组件之间的依赖关系。
  • 处理错误案例。RPC服务器长时间关闭后,客户端应如何反应?

如有疑问,请避免使用RPC。如果可以的话,应该使用异步管道-代替类似RPC的阻塞,将结果异步推送到下一个计算阶段。

回调队列

通常,通过RabbitMQ进行RPC很容易。客户端发送请求消息,服务器发送响应消息。为了接收响应,我们需要发送带有请求的“回调”队列地址。我们可以使用默认队列(在Java客户端中是唯一的)。让我们尝试一下:

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

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

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

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

邮件属性

AMQP 0-9-1协议预定义了消息附带的14个属性集。除以下属性外,大多数属性很少使用:

  • deliveryMode:将消息标记为持久性(值为2)或瞬态(任何其他值);
  • contentType:用于描述编码的mime类型。例如,对于经常使用的JSON编码,将此属性设置为application / json是一个好习惯;
  • replyTo:通常用于命名回调队列;
  • relatedId:用于将RPC响应与请求相关联。

关联ID

在上面介绍的方法中,我们建议为每个RPC请求创建一个回调队列。那是相当低效的,但是我们有更好的方法,可以为每个客户端创建一个回调队列。

这引起了一个新问题,在该队列中收到响应后,尚不清楚响应属于哪个请求。那就是当使用correlationId属性时 。我们将为每个请求将其设置为唯一值。稍后,当我们在回调队列中收到消息时,我们将查看该属性,并基于此属性将响应与请求进行匹配。如果我们看到一个未知的 correlationId值,我们可以放心地丢弃该消息,因为它不属于我们的请求。

您可能会问,为什么我们应该忽略回调队列中的未知消息,而不是因错误而失败?这是由于服务器端可能出现竞争状况。尽管可能性不大,但RPC服务器可能会在向我们发送答案之后但在发送请求的确认消息之前死亡。如果发生这种情况,重新启动的RPC服务器将再次处理该请求。这就是为什么在客户端上我们必须妥善处理重复的响应,并且理想情况下RPC应该是幂等的。

RPC工作过程:

  • 对于RPC请求,客户端发送一条消息,该消息具有两个属性: replyTo(设置为仅为该请求创建的匿名互斥队列)和correlationId(设置为每个请求的唯一值);
  • 该请求被发送到rpc_queue队列;
  • RPC工作程序(又名:服务器)正在等待该队列上的请求。出现请求时,它会使用replyTo字段中的队列来完成工作并将带有结果的消息发送回客户端;
  • 客户端等待答复队列中的数据。出现消息时,它会检查correlationId属性。如果它与请求中的值匹配,则将响应返回给应用程序。

斐波那契函数

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

服务器代码:

  • 像往常一样,我们首先建立连接,通道并声明队列。
  • 我们可能要运行多个服务器进程。为了将负载平均分配到多个服务器,我们需要在channel.basicQos中设置 prefetchCount设置。
  • 我们使用basicConsume访问队列,在队列中我们以对象(DeliverCallback)的形式提供回调,该回调将完成工作并将响应发送回去。
package com.mytest.rabbitMQ.Sixth;

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("127.0.0.1");

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

        }
    }
}

客户端代码稍微复杂一些:

  • 我们建立连接和渠道。
  • 我们的调用方法发出实际的RPC请求。
  • 在这里,我们首先生成一个唯一的relatedId 编号并将其保存-我们的使用者回调将使用该值来匹配适当的响应。
  • 然后,我们为回复创建一个专用的排他队列并订阅它。
  • 接下来,我们发布具有两个属性的请求消息: replyTo和correlationId。
  • 此时,我们可以坐下来等到正确的响应到达。
  • 由于我们的消费者交付处理是在单独的线程中进行的,因此在响应到达之前,我们将需要一些东西来挂起主线程。使用BlockingQueue是一种可行的解决方案。在这里,我们正在创建 容量设置为1的ArrayBlockingQueue,因为我们只需要等待一个响应即可。
  • 消费者的工作很简单,对于每一个消耗的响应消息,它都会检查correlationId 是否为我们要寻找的消息。如果是这样,它将响应放入BlockingQueue。
  • 同时,主线程正在等待响应,以将其从BlockingQueue中获取。
  • 最后,我们将响应返回给用户。
package com.mytest.rabbitMQ.Sixth;

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("127.0.0.1");

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

之后我们先启动RPCServer,然后再启动PRCClient。

这是本节的demo代码地址:https://gitee.com/mjTree/javaDevelop/tree/master/testDemo

 

这里介绍的设计不是RPC服务的唯一可能的实现,但是它具有一些重要的优点:

  • 如果RPC服务器太慢,则可以通过运行另一台RPC服务器来扩大规模。尝试在新控制台中运行第二个RPCServer。
  • 在客户端,RPC只需要发送和接收一条消息。不需要诸如queueDeclare之 类的同步调用。结果,RPC客户端只需要一个网络往返就可以处理单个RPC请求。

我们的代码仍然非常简单,并且不会尝试解决更复杂(但很重要)的问题,例如:

  • 如果没有服务器在运行,客户端应该如何反应?
  • 客户端是否应该为RPC设置某种超时时间?
  • 如果服务器发生故障并引发异常,是否应该将其转发给客户端?
  • 在处理之前防止无效的传入消息(例如检查边界,类型)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值