RabbitMQ RPC(Remote procedure call) 雲計算集羣的遠程調用

本系列教程主要來自於官網入門教程的翻譯,然後自己進行了部分的修改與實驗,內容僅供參考。

官網地址  https://www.rabbitmq.com/tutorials/tutorial-six-java.html

在第二個教程中我們學習瞭如何使用工作隊列分配耗時的任務在多個工人。
但是,如果我們需要在遠程計算機上運行一個函數並等待結果?嗯,這是一個不同的故事。這種模式是俗稱Remote Procedure Call或RPC。

在本教程中我們將使用RabbitMQ來構建一個RPC系統:一個客戶端和一個可伸縮的RPC服務器。我們沒有任何耗時任務價值分配,我們要創建一個虛擬的RPC服務,返回斐波納契數列。


1. 客戶端接口 Client interface

        爲了展示一個RPC服務是如何使用的,我們將創建一段很簡單的客戶端class。 它將會向外提供名字爲call的函數,這個call會發送RPC請求並且阻塞知道收到RPC運算的結果。代碼如下:

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

2. 回調函數隊列 Callback queue

        總體來說,在RabbitMQ進行RPC遠程調用是比較容易的。client發送請求的Message然後server返回響應結果。爲了收到響應client在publish message時需要提供一個”callback“(回調)的queue地址。code如下:

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

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

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

2.1 Message properties

AMQP 預定義了14個屬性。它們中的絕大多很少會用到。以下幾個是平時用的比較多的:

  • delivery_mode: 持久化一個Message(通過設定值爲2)。其他任意值都是非持久化。請移步RabbitMQ消息隊列(三):任務分發機制
  • content_type: 描述mime-type 的encoding。比如設置爲JSON編碼:設置該property爲application/json
  • reply_to: 一般用來指明用於回調的queue(Commonly used to name a callback queue)。
  • correlation_id: 在請求中關聯處理RPC響應(correlate RPC responses with requests)。


3. 相關id Correlation id

       在上個小節裏,實現方法是對每個RPC請求都會創建一個callback queue。這是不高效的。幸運的是,在這裏有一個解決方法:爲每個client創建唯一的callback queue。

       這又有其他問題了:收到響應後它無法確定是否是它的,因爲所有的響應都寫到同一個queue了。上一小節的correlation_id在這種情況下就派上用場了:對於每個request,都設置唯一的一個值,在收到響應後,通過這個值就可以判斷是否是自己的響應。如果不是自己的響應,就不去處理。


4. 總結

                    

 工作流程:

  • 當客戶端啓動時,它創建了匿名的exclusive callback queue.
  • 客戶端的RPC請求時將同時設置兩個properties: reply_to設置爲callback queue;correlation_id設置爲每個request一個獨一無二的值.
  • 請求將被髮送到an rpc_queue queue.
  • RPC端或者說server一直在等待那個queue的請求。當請求到達時,它將通過在reply_to指定的queue回覆一個message給client。
  • client一直等待callback queue的數據。當message到達時,它將檢查correlation_id的值,如果值和它request發送時的一致那麼就將返回響應。

5. 最終實現

The code for RPCServer.java

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.AMQP.BasicProperties;
  
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) {
    Connection connection = null;
    Channel channel = null;
    try {
      ConnectionFactory factory = new ConnectionFactory();
      factory.setHost("localhost");
  
      connection = factory.newConnection();
      channel = connection.createChannel();
      
      channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
  
      channel.basicQos(1);
  
      QueueingConsumer consumer = new QueueingConsumer(channel);
      channel.basicConsume(RPC_QUEUE_NAME, false, consumer);
  
      System.out.println(" [x] Awaiting RPC requests");
  
      while (true) {
        String response = null;
        
        QueueingConsumer.Delivery delivery = consumer.nextDelivery();
        
        BasicProperties props = delivery.getProperties();
        BasicProperties replyProps = new BasicProperties
                                         .Builder()
                                         .correlationId(props.getCorrelationId())
                                         .build();
        
        try {
          String message = new String(delivery.getBody(),"UTF-8");
          int n = Integer.parseInt(message);
  
          System.out.println(" [.] fib(" + message + ")");
          response = "" + fib(n);
        }
        catch (Exception e){
          System.out.println(" [.] " + e.toString());
          response = "";
        }
        finally {  
          channel.basicPublish( "", props.getReplyTo(), replyProps, response.getBytes("UTF-8"));
  
          channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
      }
    }
    catch  (Exception e) {
      e.printStackTrace();
    }
    finally {
      if (connection != null) {
        try {
          connection.close();
        }
        catch (Exception ignore) {}
      }
    }      		      
  }
}

服務器代碼相當簡單:
(1)像往常一樣,我們首先建立連接和聲明隊列。
(2)我們fibonacci函數聲明。它假定只有有效輸入正整數。(別指望這一個龐大的數字,它可能是最慢遞歸實現可能的)。
(3)我們爲basic_consume聲明回調,RPC服務器的核心。在收到請求時執行。它的工作併發送響應。
(4)我們可能希望運行多個服務器進程。爲了傳播負載同樣在多個服務器,我們需要設置theprefetch_count設置。

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.AMQP.BasicProperties;
  
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) {
    Connection connection = null;
    Channel channel = null;
    try {
      ConnectionFactory factory = new ConnectionFactory();
      factory.setHost("localhost");
  
      connection = factory.newConnection();
      channel = connection.createChannel();
      
      channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
  
      channel.basicQos(1);
  
      QueueingConsumer consumer = new QueueingConsumer(channel);
      channel.basicConsume(RPC_QUEUE_NAME, false, consumer);
  
      System.out.println(" [x] Awaiting RPC requests");
  
      while (true) {
        String response = null;
        
        QueueingConsumer.Delivery delivery = consumer.nextDelivery();
        
        BasicProperties props = delivery.getProperties();
        BasicProperties replyProps = new BasicProperties
                                         .Builder()
                                         .correlationId(props.getCorrelationId())
                                         .build();
        
        try {
          String message = new String(delivery.getBody(),"UTF-8");
          int n = Integer.parseInt(message);
  
          System.out.println(" [.] fib(" + message + ")");
          response = "" + fib(n);
        }
        catch (Exception e){
          System.out.println(" [.] " + e.toString());
          response = "";
        }
        finally {  
          channel.basicPublish( "", props.getReplyTo(), replyProps, response.getBytes("UTF-8"));
  
          channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
      }
    }
    catch  (Exception e) {
      e.printStackTrace();
    }
    finally {
      if (connection != null) {
        try {
          connection.close();
        }
        catch (Exception ignore) {}
      }
    }      		      
  }
}

客戶端代碼稍微涉及:
我們建立一個連接通道,聲明一個獨家“回調”排隊等待回覆。
我們訂閱“回調”隊列,這樣我們就可以收到RPC反應。
我們調用方法使實際的RPC請求。
在這裏,我們首先生成唯一correlationId號碼並將其保存- while循環將使用這個值來捕捉適當的響應。
接下來,我們發佈請求消息,有兩個屬性:replyTo correlationId。
在這一點上我們可以坐下來,等到適當的響應到達。
while循環做一個非常簡單的工作,爲每一個響應消息它檢查如果thecorrelationId是我們正在尋找的人。如果是這樣,它節省了響應。
最後我們將響應返回給用戶。

客戶端請求:

RPCClient fibonacciRpc = new RPCClient();

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

fibonacciRpc.close();

本文提供的設計並不是唯一可能的實現RPC服務,但它有一些重要的優點:
如果RPC服務器太慢了,你可以擴大通過運行另一個。嘗試運行第二個RPCServer在一個新的控制檯。
在客戶端,RPC需要發送和接收消息只有一個。不需要像queueDeclare同步調用。由於RPC客戶端只需要一個網絡往返一個RPC請求。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章