RabbitMQ教程 (六) Remote procedure call (RPC)

Remote procedure call (RPC)

(using the Java client)

Prerequisites

  This tutorial assumes RabbitMQ is installed and running on localhost on standard port (5672). In case you use a different host, port or credentials, connections settings would require adjusting.

Where to get help

  If you’re having trouble going through this tutorial you can contact us through the mailing list.

  In the second tutorial we learned how to use Work Queues to distribute time-consuming tasks among multiple workers.
  在第二篇教程中,我们学习了如何使用工作队列在多个工作人员之间分配耗时的任务。

  But what if we need to run a function on a remote computer and wait for the result? Well, that’s a different story. This pattern is commonly known as Remote Procedure Call or RPC.
  但是如果我们需要在远程计算机上运行一个函数并等待结果呢?嗯,这是一个不同的故事。此模式通常称为远程过程调用或RPC

  In this tutorial we’re going to use RabbitMQ to build an RPC system: a client and a scalable RPC server. As we don’t have any time-consuming tasks that are worth distributing, we’re going to create a dummy RPC service that returns Fibonacci numbers.
  在本教程中,我们将使用RabbitMQ构建RPC系统:客户端和可伸缩的RPC服务器。由于我们没有任何值得分发的耗时任务,我们将创建一个返回Fibonacci数字的虚拟RPC服务。

Client interface 客户端接口

  To illustrate how an RPC service could be used we’re going to create a simple client class. It’s going to expose a method named call which sends an RPC request and blocks until the answer is received:
  为了说明如何使用RPC服务,我们将创建一个简单的客户端类。它将公开一个名为call的方法,该方法 发送一个RPC请求并阻塞,直到收到答案为止:

  FibonacciRpcClient fibonacciRpc = new FibonacciRpcClient();
  String result = fibonacciRpc.call("4");
  System.out.println( "fib(4) is " + result);
A note on RPC 关于RPC的说明

  Although RPC is a pretty common pattern in computing, it’s often criticised. The problems arise when a programmer is not aware whether a function call is local or if it’s a slow RPC. Confusions like that result in an unpredictable system and adds unnecessary complexity to debugging. Instead of simplifying software, misused RPC can result in unmaintainable spaghetti code.
  尽管RPC在计算中是一种非常常见的模式,但它经常受到批评。当程序员不知道函数调用是本地的还是慢的RPC时,会出现问题。这样的混淆导致系统不可预测,并增加了调试的不必要的复杂性。错误使用RPC可以导致不可维护的代码,而不是简化软件开发。

Bearing that in mind, consider the following advice:
考虑到这一点,请考虑以下建议:

  Make sure it’s obvious which function call is local and which is remote.
  确保明显哪个函数调用是本地的,哪个是远程的。
  Document your system. Make the dependencies between components clear.
  记录您的系统。使组件之间的依赖关系变得清晰。
  Handle error cases. How should the client react when the RPC server is down for a long time?
  处理错误案例。当RPC服务器长时间停机时,客户端应该如何反应?
  
  When in doubt avoid RPC. If you can, you should use an asynchronous pipeline - instead of RPC-like blocking, results are asynchronously pushed to a next computation stage.
  如有疑问,请避免使用RPC。如果可以,您应该使用异步管道 - 而不是类似RPC的阻塞,将结果异步推送到下一个计算阶段。

Callback queue 回调队列

  In general doing RPC over RabbitMQ is easy. A client sends a request message and a server replies with a response message. In order to receive a response we need to send a ‘callback’ queue address with the request. We can use the default queue (which is exclusive in the Java client). Let’s try it:
  一般来说,通过RabbitMQ进行RPC很容易。客户端发送请求消息,服务器回复响应消息。为了接收响应,我们需要发送带有请求的“回调”队列地址。我们可以使用默认队列(在Java客户端中是独占的)。我们来试试吧:

	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 消息属性

  The AMQP 0-9-1 protocol predefines a set of 14 properties that go with a message. Most of the properties are rarely used, with the exception of the following:
  AMQP 0-9-1协议预定义了一组带有消息的14个属性。大多数属性很少使用,但以下情况除外:
  
  deliveryMode: Marks a message as persistent (with a value of 2) or transient (any other value). You may remember this property from the second tutorial.
  deliveryMode:将消息标记为持久性(值为2)或瞬态(任何其他值)。您可能会记住第二个教程中的这个属性。
  
  contentType: Used to describe the mime-type of the encoding. For example for the often used JSON encoding it is a good practice to set this property to: application/json.
  contentType:用于描述编码的mime类型。例如,对于经常使用的JSON编码,将此属性设置为:application / json是一种很好的做法。
  
  replyTo: Commonly used to name a callback queue.
  replyTo:通常用于命名回调队列。
  
  correlationId: Useful to correlate RPC responses with requests.
  correlationId:用于将RPC响应与请求相关联。

We need this new import:我们需要这个新的导入:

import com.rabbitmq.client.AMQP.BasicProperties;

Correlation Id 相关ID

  In the method presented above we suggest creating a callback queue for every RPC request. That’s pretty inefficient, but fortunately there is a better way - let’s create a single callback queue per client.
  在上面介绍的方法中,我们建议为每个RPC请求创建一个回调队列。这是非常低效的,但幸运的是有更好的方法 - 让我们为每个客户端创建一个回调队列。

  That raises a new issue, having received a response in that queue it’s not clear to which request the response belongs. That’s when the correlationId property is used. We’re going to set it to a unique value for every request. Later, when we receive a message in the callback queue we’ll look at this property, and based on that we’ll be able to match a response with a request. If we see an unknown correlationId value, we may safely discard the message - it doesn’t belong to our requests.
  这引发了一个新问题,在该队列中收到响应后,不清楚响应属于哪个请求。那是在使用correlationId属性的时候 。我们将为每个请求将其设置为唯一值。稍后,当我们在回调队列中收到一条消息时,我们将查看此属性,并根据该属性,我们将能够将响应与请求进行匹配。如果我们看到未知的 correlationId值,我们可以安全地丢弃该消息 - 它不属于我们的请求。

  You may ask, why should we ignore unknown messages in the callback queue, rather than failing with an error? It’s due to a possibility of a race condition on the server side. Although unlikely, it is possible that the RPC server will die just after sending us the answer, but before sending an acknowledgment message for the request. If that happens, the restarted RPC server will process the request again. That’s why on the client we must handle the duplicate responses gracefully, and the RPC should ideally be idempotent.
  您可能会问,为什么我们应该忽略回调队列中的未知消息,而不是失败并出现错误?这是由于服务器端可能存在竞争条件。虽然不太可能,但RPC服务器可能会在向我们发送答案之后,但在发送请求的确认消息之前死亡。如果发生这种情况,重新启动的RPC服务器将再次处理请求。这就是为什么在客户端上我们必须优雅地处理重复的响应,理想情况下RPC应该是幂等的。

Summary摘要

在这里插入图片描述
  Our RPC will work like this 我们的RPC将这样工作:

  For an RPC request, the Client sends a message with two properties: replyTo, which is set to a anonymous exclusive queue created just for the request, and correlationId, which is set to a unique value for every request.
  对于RPC请求,客户端发送带有两个属性的消息: replyTo,它被设置为仅为请求创建的匿名独占队列;以及correlationId,设置为每个请求的唯一值。
  The request is sent to an rpc_queue queue.
  请求被发送到rpc_queue队列。

  The RPC worker (aka: server) is waiting for requests on that queue. When a request appears, it does the job and sends a message with the result back to the Client, using the queue from the replyTo field.
  RPC worker(aka:server)正在等待该队列上的请求。当出现请求时,它会执行该作业,并使用来自replyTo字段的队列将带有结果的消息发送回客户端。

  The client waits for data on the reply queue. When a message appears, it checks the correlationId property. If it matches the value from the request it returns the response to the application.
  客户端等待回复队列上的数据。出现消息时,它会检查correlationId属性。如果它与请求中的值匹配,则返回对应用程序的响应。

Putting it all together 整合代码

The Fibonacci task 斐波纳契任务:

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

  We declare our fibonacci function. It assumes only valid positive integer input. (Don’t expect this one to work for big numbers, and it’s probably the slowest recursive implementation possible).
  我们声明我们的斐波那契函数。它假定只有有效的正整数输入。(不要指望这个适用于大数字,它可能是最慢的递归实现)。

  The code for our RPC server can be found here: RPCServer.java.
  我们的RPC服务器的代码可以在这里找到:RPCServer.java

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Envelope;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

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) {
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");

    Connection connection = null;
    try {
      connection      = factory.newConnection();
      final 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");

      Consumer consumer = new DefaultConsumer(channel) {
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
          AMQP.BasicProperties replyProps = new AMQP.BasicProperties
                  .Builder()
                  .correlationId(properties.getCorrelationId())
                  .build();

          String response = "";

          try {
            String message = new String(body,"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( "", properties.getReplyTo(), replyProps, response.getBytes("UTF-8"));
            channel.basicAck(envelope.getDeliveryTag(), false);
            // RabbitMq consumer worker thread notifies the RPC server owner thread 
            synchronized(this) {
            	this.notify();
            }
          }
        }
      };

      channel.basicConsume(RPC_QUEUE_NAME, false, consumer);
      // Wait and be prepared to consume the message from RPC client.
      while (true) {
      	synchronized(consumer) {
      		try {
      			consumer.wait();
      	    } catch (InterruptedException e) {
      	    	e.printStackTrace();	    	
      	    }
      	}
      }
    } catch (IOException | TimeoutException e) {
      e.printStackTrace();
    }
    finally {
      if (connection != null)
        try {
          connection.close();
        } catch (IOException _ignore) {}
    }
  }
}

  The server code is rather straightforward:服务器代码非常简单:

  As usual we start by establishing the connection, channel and declaring the queue.
  像往常一样,我们首先建立连接,通道和声明队列。

  We might want to run more than one server process. In order to spread the load equally over multiple servers we need to set the prefetchCount setting in channel.basicQos.
  我们可能希望运行多个服务器进程。为了在多个服务器上平均分配负载,我们需要在channel.basicQos中设置 prefetchCount设置。

  We use basicConsume to access the queue, where we provide a callback in the form of an object (DefaultConsumer) that will do the work and send the response back.
  我们使用basicConsume来访问队列,我们​​以对象(DefaultConsumer)的形式提供回调,它将完成工作并发回响应。

  The code for our RPC client can be found here: RPCClient.java.
  我们的RPC客户端的代码可以在这里找到:RPCClient.java

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Envelope;

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 {

  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 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<String>(1);

    String ctag = channel.basicConsume(replyQueueName, true, new DefaultConsumer(channel) {
      @Override
      public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        if (properties.getCorrelationId().equals(corrId)) {
          response.offer(new String(body, "UTF-8"));
        }
      }
    });

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

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

  public static void main(String[] argv) {
    RPCClient fibonacciRpc = null;
    String response = null;
    try {
      fibonacciRpc = new RPCClient();

      for (int i = 0; i < 32; i++) {
        String i_str = Integer.toString(i);
        System.out.println(" [x] Requesting fib(" + i_str + ")");
        response = fibonacciRpc.call(i_str);
        System.out.println(" [.] Got '" + response + "'");
      }
    }catch  (IOException | TimeoutException | InterruptedException e) {
      e.printStackTrace();
    }finally {
      if (fibonacciRpc!= null) {
        try {
          fibonacciRpc.close();
        }
        catch (IOException _ignore) {}
      }
    }
  }
}

  The client code is slightly more involved:客户端代码稍微复杂一些:

  We establish a connection and channel.
  我们建立了一个连接和渠道。

  Our call method makes the actual RPC request.
  我们的调用方法生成实际的RPC请求。

  Here, we first generate a unique correlationId number and save it - our implementation of handleDelivery in RpcConsumer will use this value to catch the appropriate response.
  在这里,我们首先生成一个唯一的correlationId 数并保存它 - 我们 在RpcConsumer中的handleDelivery实现将使用该值来捕获适当的响应。

  Then, we create a dedicated exclusive queue for the reply and subscribe to it.
  然后,我们为回复创建一个专用的独占队列并订阅它。

  Next, we publish the request message, with two properties: replyTo and correlationId.
  接下来,我们发布请求消息,其中包含两个属性: replyTo和correlationId。

  At this point we can sit back and wait until the proper response arrives.
  在这一点上,我们可以坐下来等待正确的响应到来。

  Since our consumer delivery handling is happening in a separate thread, we’re going to need something to suspend the main thread before the response arrives. Usage of BlockingQueue is one possible solutions to do so.
Here we are creating ArrayBlockingQueue with capacity set to 1 as we need to wait for only one response.
  由于我们的消费者交付处理是在一个单独的线程中进行的,因此我们需要在响应到来之前暂停主线程。使用BlockingQueue是一种可能的解决方案。这里我们创建了ArrayBlockingQueue ,容量设置为1,因为我们只需要等待一个响应。

  The handleDelivery method is doing a very simple job, for every consumed response message it checks if the correlationId is the one we’re looking for. If so, it puts the response to BlockingQueue.
  该handleDelivery方法是做一个很简单的工作,对每一位消费响应消息它会检查的correlationID 是我们要找的人。如果是这样,它会将响应置于BlockingQueue。

  At the same time main thread is waiting for response to take it from BlockingQueue.
  同时主线程正在等待响应从BlockingQueue获取它。

  Finally we return the response back to the user.
  最后,我们将响应返回给用户。

Making the 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();

  Now is a good time to take a look at our full example source code (which includes basic exception handling) for RPCClient.java and RPCServer.java.
  现在是查看RPCClient.java和RPCServer.java的完整示例源代码(包括基本异常处理)的 好时机。

Compile and set up the classpath as usual (see tutorial one):
像往常一样编译和设置类路径(参见教程一):

  javac -cp $CP RPCClient.java RPCServer.java

Our RPC service is now ready. We can start the server:
我们的RPC服务现已准备就绪。我们可以启动服务器:

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

To request a fibonacci number run the client:
要请求斐波纳契数,请运行客户端:

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

  The design presented here is not the only possible implementation of a RPC service, but it has some important advantages:
  此处介绍的设计并不是RPC服务的唯一可能实现,但它具有一些重要优势:

  If the RPC server is too slow, you can scale up by just running another one. Try running a second RPCServer in a new console.
  如果RPC服务器太慢,您可以通过运行另一个服务器来扩展。尝试在新控制台中运行第二个RPCServer。

  On the client side, the RPC requires sending and receiving only one message. No synchronous calls like queueDeclare are required. As a result the RPC client needs only one network round trip for a single RPC request.
  在客户端,RPC只需要发送和接收一条消息。不需要像queueDeclare这样的同步调用 。因此,对于单个RPC请求,RPC客户端只需要一次网络往返。

  Our code is still pretty simplistic and doesn’t try to solve more complex (but important) problems, like:
  我们的代码仍然相当简单,并不试图解决更复杂(但重要)的问题,例如:

How should the client react if there are no servers running?
如果没有运行服务器,客户应该如何反应?

Should a client have some kind of timeout for the RPC?
客户端是否应该为RPC设置某种超时?

If the server malfunctions and raises an exception, should it be forwarded to the client?
如果服务器出现故障并引发异常,是否应将其转发给客户端?

Protecting against invalid incoming messages (eg checking bounds, type) before processing.
在处理之前防止无效的传入消息(例如检查边界,键入)。

  If you want to experiment, you may find the management UI useful for viewing the queues.
  如果您想进行实验,您可能会发现管理UI对于查看队列非常有用。

Production [Non-]Suitability Disclaimer

  Please keep in mind that this and other tutorials are, well, tutorials. They demonstrate one new concept at a time and may intentionally oversimplify some things and leave out others. For example topics such as connection management, error handling, connection recovery, concurrency and metric collection are largely omitted for the sake of brevity. Such simplified code should not be considered production ready.

  Please take a look at the rest of the documentation before going live with your app. We particularly recommend the following guides: Publisher Confirms and Consumer Acknowledgements, Production Checklist and Monitoring.

Getting Help and Providing Feedback

  If you have questions about the contents of this tutorial or any other topic related to RabbitMQ, don’t hesitate to ask them on the RabbitMQ mailing list.

Help Us Improve the Docs ❤️

  If you’d like to contribute an improvement to the site, its source is available on GitHub. Simply fork the repository and submit a pull request. Thank you!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值