標籤:rabbitmq rpc
(使用Java用戶端)
一、概述
在Work Queue的章節中我們學習了如何使用Work Queue分配耗時的任務給多個工作者,但是如果我們需要運行一個函數在遠端電腦上,這是一個完全不同的情景,這種模式通常被稱之為RPC。
在本章節的學習中,我們將使用RabbitMQ來構建一個RPC系統:一個遠程用戶端和一個可擴充的RPC伺服器,我們沒有任何費時的任務進行分配,我們將建立一個虛擬RPC服務返回Fibonacci數。
1.1、用戶端介面(Client Interface)
為了說明一個RPC服務可以使用,我們將建立一個簡單的用戶端類,這將通過方法名的調用發送一個RPC請求和接收塊得到回覆:
FibonacciRpcClient fibonacciRpc = new FibonacciRpcClient(); String result = fibonacciRpc.call("4");System.out.println( "fib(4) is " + result);注意:
儘管RPC是電腦領域中非常普遍的模式,它經常受到批評,當程式不知道是否這是一個緩慢的RPC調用函數,像在不可預知介面的系統進行調試,增加了不必要的複雜性,而不是簡化軟體,濫用會導致不可修複的代碼,如果要使用它記住考慮以下建議:
1、能明確區分被調用的函數是局部的還是遠端。
2、您的檔案系統、組件之間的依賴關係是很清晰的。
3、處理問題?客戶應該知道當RPC伺服器掛掉的時候該如何做。
1.2、回調隊列(Callback Queue)
總的說來使用RabbitMQ來實現RPC是比較簡單的,當用戶端發送請求訊息和伺服器響應訊息的回覆,為了接收到響應我們需要發送一個callback隊列地址在請求中,我們可以使用預設的隊列,讓我們試試:
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 ...說明:
AMQP協議對一個訊息預設了一個14個屬性的集合,大部分屬性很少被使用,有以下例外:
1、deliveryMode:標誌一個訊息的持久化(值為2)或者狀態(其他任何值)。
2、contentType:用於描述編碼的MIME類型,比如,要經常使用JSON編碼大的一個好的設定方法為application/json
3、replyTo:常用的回調隊列名稱。
4、correlationId:被用於一個RPC相應請求相關。
同時我們需要一個新的類:
import com.rabbitmq.client.AMQP.BasicProperties;
1.3、相關ID (correlation Id)
在上述方法我們為每個RPC請求建立一個回調隊列,那是很低效的但是幸運的是有一個更好的方式,讓我們建立一個單一回調隊列供每個用戶端調用。
這出現了一個新的問題,在隊列中接收到一個不清楚這個請求屬於哪個響應時的響應,我們要將它設定為每個請求的一個特有的值,然後從一個回調隊列中接收一個訊息時就要查看這個屬性值,在此基礎上,我們將能匹配一個請求的響應,如果我們看到一個未知的correlationId值,我們可以安全地將這些訊息丟棄因為它不屬於我們的要求。
你也許會問,我們為什麼要丟棄回調隊列中未知的訊息呢?而不是一個錯誤引起的失敗呢?這是由於一個可能在伺服器的競爭引起的,雖然不太可能,但是它還是有可能發生的,RPC伺服器在給我們大回覆之後將掛掉,但是發送確認訊息的請求,如果這種情況發生,將再次重啟RPC伺服器處理請求,這就是為什麼在用戶端必須處理重複的響應。
二、實現
2.1、結構如所示:
從可知,我們RPC工作流程如下:
1、當用戶端啟動時,它建立一個匿名的獨立的回調隊列。
2、一個RPC請求中,用戶端發送一個訊息具有兩個特性:replyTo它包含將要到達的回調隊列和correlation_id,這是每個請求的一個固有的值。
3、請求發送到一個rpc_queue隊列。
4、RPC伺服器正在等待隊列的請求,當一個請求到達時,它的工作久是發送一個訊息結果返回給用戶端,使用了replyTo隊列。
5、用戶端等待回調replyTo隊裡的資料,當訊息出現時,它檢查correlationId值是否和請求返回給應用程式響應的值匹配。
2.2、代碼實現
Fibonacci 函數:
private static int fib(int n) throws Exception { if (n == 0) return 0; if (n == 1) return 1; return fib(n-1) + fib(n-2);}
我們聲明的Fibonacci函數,它假定只有有效整整輸入(別指望一個大的數字,它可能是最慢的遞迴實現),我們RPC伺服器RPCServer.java代碼如下:
private static final String RPC_QUEUE_NAME = "rpc_queue";ConnectionFactory factory = new ConnectionFactory();factory.setHost("localhost");Connection connection = factory.newConnection();Channel 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) { QueueingConsumer.Delivery delivery = consumer.nextDelivery(); BasicProperties props = delivery.getProperties(); BasicProperties replyProps = new BasicProperties .Builder() .correlationId(props.getCorrelationId()) .build(); String message = new String(delivery.getBody()); int n = Integer.parseInt(message); System.out.println(" [.] fib(" + message + ")"); String response = "" + fib(n); channel.basicPublish( "", props.getReplyTo(), replyProps, response.getBytes()); channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);}說明:
1、像之前所有執行個體一樣,開始建立串連、通道和隊列
2、可能要運行多個伺服器處理序,為了傳播同樣的負載在多個伺服器上,我們需要通過channel.basicQos來設定prefetchcount值。
3、通過basicConsume訪問隊列,然後進入迴圈,等待請求訊息、處理訊息和發送響應。
RPCClient.java代碼如下:
private Connection connection;private Channel channel;private String requestQueueName = "rpc_queue";private String replyQueueName;private QueueingConsumer consumer;public RPCClient() throws Exception { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); connection = factory.newConnection(); channel = connection.createChannel(); replyQueueName = channel.queueDeclare().getQueue(); consumer = new QueueingConsumer(channel); channel.basicConsume(replyQueueName, true, consumer);}public String call(String message) throws Exception { String response = null; String corrId = java.util.UUID.randomUUID().toString(); BasicProperties props = new BasicProperties .Builder() .correlationId(corrId) .replyTo(replyQueueName) .build(); channel.basicPublish("", requestQueueName, props, message.getBytes()); while (true) { QueueingConsumer.Delivery delivery = consumer.nextDelivery(); if (delivery.getProperties().getCorrelationId().equals(corrId)) { response = new String(delivery.getBody()); break; } } return response; }public void close() throws Exception { connection.close();}說明:
1、建立一個串連通道和聲明一個專屬的回調隊列的回複。
2、訂閱回調隊列,這樣可以接受RPC響應
3、調用方法發起實際的RPC請求。
4、產生一個唯一的correlationId並且儲存它,while迴圈將使用這個值來匹配相對應的響應。
5、發送請求訊息,它有兩個屬性值relpTo和correlationId。
6、等待相匹配的響應。
7、while迴圈做簡單的工作,為每個響應檢查是否correlationId就是我們需要的,如果是,儲存該響應。
8、返迴響應給用戶端。
用戶端請求代碼:
RPCClient fibonacciRpc = new RPCClient();System.out.println(" [x] Requesting fib(30)"); String response = fibonacciRpc.call("30");System.out.println(" [.] Got '" + response + "'");fibonacciRpc.close();
2.3、完整的代碼清單
RPCClient.java
package com.xuz.rpc;import java.util.UUID;import com.rabbitmq.client.Channel;import com.rabbitmq.client.Connection;import com.rabbitmq.client.ConnectionFactory;import com.rabbitmq.client.QueueingConsumer;import com.rabbitmq.client.AMQP.BasicProperties;public class RPCClient {private Connection connection;private Channel channel;private String requestQueueName = "rpc_queue";private String replyQueueName;private QueueingConsumer consumer;public RPCClient() throws Exception {ConnectionFactory factory = new ConnectionFactory();factory.setHost("127.0.0.1");connection = factory.newConnection();channel = connection.createChannel();//響應隊列名,服務端會把返回的資訊發送到這個隊列中。replyQueueName = channel.queueDeclare().getQueue();consumer = new QueueingConsumer(channel);channel.basicConsume(replyQueueName, true, consumer);}public String call(String message) throws Exception {String response = null;//每個請求產生一個唯一的correlationIdString corrId = UUID.randomUUID().toString();//佈建要求響應基本參數:correlationId(UUID)和rpc_queueBasicProperties props = new BasicProperties.Builder().correlationId(corrId).replyTo(replyQueueName).build();System.out.println("用戶端響應隊列的屬性:["+props.getCorrelationId()+","+props.getReplyTo()+"]");channel.basicPublish("", requestQueueName, props, message.getBytes());while (true) {QueueingConsumer.Delivery delivery = consumer.nextDelivery();//如果獲得響應隊列中的getCorrelationId和當前corrId相等,則儲存響應並返回if (delivery.getProperties().getCorrelationId().equals(corrId)) {response = new String(delivery.getBody(), "UTF-8");break;}}return response;}/** * 關閉串連 * @throws Exception */public void close() throws Exception {connection.close();}public static void main(String[] argv) {RPCClient rpcClient = null;String response = null;try {rpcClient = new RPCClient();//調用Call方法傳入請求訊息:測試RPCresponse = rpcClient.call("測試RPC");System.out.println(" 響應訊息:[" + response + "]");} catch (Exception e) {e.printStackTrace();} finally {if (rpcClient != null) {try {rpcClient.close();} catch (Exception ignore) {}}}}}
RPCServer.java:
package com.xuz.rpc;import com.rabbitmq.client.Channel;import com.rabbitmq.client.Connection;import com.rabbitmq.client.ConnectionFactory;import com.rabbitmq.client.QueueingConsumer;import com.rabbitmq.client.AMQP.BasicProperties;public class RPCServer {private static final String RPC_QUEUE_NAME = "rpc_queue";/** * 定義函數 * @param n 輸入的正整數 * @return */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("127.0.0.1");//建立串連connection = factory.newConnection();//建立通道channel = connection.createChannel();//聲明RPC隊列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("[等待RPC遠程請求!]");while (true) {String response = null;System.out.println("[服務端等待接收訊息!]");QueueingConsumer.Delivery delivery = consumer.nextDelivery();System.out.println("[服務端成功接收訊息!]");BasicProperties props = delivery.getProperties();//從響應隊列擷取reply參數BasicProperties replyProps = new BasicProperties.Builder().correlationId(props.getCorrelationId()).build();System.out.println("服務端響應隊列的屬性:["+replyProps.getCorrelationId()+"]");try {String message = new String(delivery.getBody(), "UTF-8");response = "服務端已經處理了訊息:[" + message+"]";} 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) {}}}}}2.4、RPC測試
1、運行RPCClient發送響應請求:
2、運行PRCServer接收處理響應請求:
源碼下載:
RabbitMQ RPC源碼