6.远程过程调用(RPC)

远程过程调用

Remote procedure call(RPC,using the java client)

在前面的章节中,介绍了如何使用工作队列将耗时的任务分配给多个工作者。
但是,如果我们需要在远程计算机上运行一个函数并等待结果呢?好吧,那就是另外一回事了。这种模式通常称为远程过程调用(Remote Procedure Call)或RPC。
在本节中,将使用rabbitMQ来构建一个RPC系统:一个客户端和一个可伸缩的RPC服务器。由于没有任何值得分配的耗时任务,所以将创建一个虚拟RPC服务来返回斐波那契数字。

客户端接口(client interface)

为了说明如何使用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那样的阻塞,结果将异步推送到下一个计算阶段。

回调队列(callback queue)

一般来说,在rabbitMQ上进行RPC是很容易的。客户端发送请求消息,服务器用响应消息进行响应。为了接收响应,需要在请求中发送一个回调队列的地址。可以使用默认队列(它在java客户端中是独占的)。让我们试一试:

// 需要一个新的导入
import com.rabbitmq.client.AMQP.BasicProperties;

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

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

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

// ... then code to read a response message from the callback_queue ...
消息属性(message properties)

AMQP 0-9-1协议预定义了一组包含14个属性的消息。大多数属性都很少使用,除了以下几个:

  • deliveryMode:将消息标记为持久的(值为2)或临时的(任何其他值)。
  • contentType:用于描述编码的mime类型。例如,对于经常使用的JSON编码,将此属性设置为:application/json是一种很好的做法。
  • replyTo:通常用于给回调队列命名。
  • correlationId:用于将RPC响应与请求关联起来。
Correlation 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);
}

我们声明了fib函数。它假设只有有效的正整数输入(不要期望这个方法适用于大数字,它可能是最慢的递归实现)。
RPC服务器RPCServer.java类的代码:

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("192.168.1.254");
        factory.setUsername("admin");
        factory.setPassword("admin123");

        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(), StandardCharsets.UTF_8);
                    int n = Integer.parseInt(message);

                    System.out.println(" [.] fib(" + message + ")");
                    response += fib(n);
                } catch (RuntimeException e) {
                    System.out.println(" [.] " + e);
                } finally {
                    channel.basicPublish("", delivery.getProperties().getReplyTo(), replyProps, 
                            response.getBytes(StandardCharsets.UTF_8));
                    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                    // RabbitMq consumer worker thread notifies the RPC server owner thread
                    synchronized (monitor) {
                        monitor.notify();
                    }
                }
            };

            channel.basicConsume(RPC_QUEUE_NAME, false, deliverCallback, (consumerTag -> {}));
            // Wait and be prepared to consume the message from RPC client.
            while (true) {
                synchronized (monitor) {
                    try {
                        monitor.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

服务器代码相当简单:

  • 和往常一样,我们从建立连接、信道和声明队列开始。
  • 我们可能需要运行多个服务器进程。为了在多个服务器上平均地分配负载,需要在channel.basicQos中对prefetchCount进行设置。
  • 我们使用basicConsume来访问队列,在队列中以对象(DeliverCallback)的形式提供一个回调,它将执行工作并将响应发送回来。

RPC客户端RPCClient.java类的代码:

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("192.168.1.254");
        factory.setUsername("admin");
        factory.setPassword("admin123");

        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(StandardCharsets.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(), StandardCharsets.UTF_8));
            }
        }, consumerTag -> {
        });

        String result = response.take();
        channel.basicCancel(ctag);
        return result;
    }

    public void close() throws IOException {
        connection.close();
    }
}

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

  • 建立连接和信道。
  • call方法发出实际的RPC请求。
  • 在这里,首先生成一个唯一的correlationId号并保存它,而消费者回调将使用这个值来匹配合适的响应。
  • 然后,为应答创建一个专用的独占队列并订阅它。
  • 接下来,发布请求消息,带有两个属性:replyTocorrelationId
  • 此时,一直等待直到合适的响应到来。
  • 由于消费者的交付处理是在一个单独的线程中发生的,因此需要一些东西来在响应到达之前挂起主线程。使用BlockingQueue是一种可能的解决方案。这里创建的ArrayBlockingQueue容量设置为1,因为只需要等待一个响应。
  • 消费者做的工作非常简单,对于每一条被消费的响应消息,它检查correlationId是否是我们正在寻找的。如果是,它将响应放置到BlockingQueue
  • 与此同时,主线程正在等待从BlockingQueue中获取响应。
  • 最后,将响应返回给用户。

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

  • 如果RPC服务器太慢,可以通过运行另一个RPC服务器来扩展。尝试在新控制台中运行第二个RPCServer
  • 在客户端,RPC只需要发送和接收一条消息。不需要像queueDeclare这样的同步调用。因此,对于单个RPC请求,RPC客户端只需要一次网络连接来回。

以上的代码仍然非常简单,没有尝试解决更复杂(但重要)的问题,比如:

  • 如果没有服务器在运行,客户端对此应该如何反应。
  • 客户端是否应该为RPC设置某种超时。
  • 如果服务器出现故障并引发异常,是否应该将其转发给客户端。
  • 在处理前避免无效的传入消息(如检查边界、类型)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值