[一曲廣陵不如晨鐘暮鼓]
本文,我們來介紹RabbitMQ中的RPC遠程調用。在正式開始之前,我們假設RabbitMQ服務已經啓動,運行端口爲5672,如果各位看官有更改過默認配置,那麼就需要修改爲對應端口,保持一致即可。
準備工作:
操作系統:window 7 x64
其他軟件:eclipse mars,jdk7,maven 3
--------------------------------------------------------------------------------------------------------------------------------------------------------
Remote procedure call (RPC)
在前文介紹的關於“工作隊列”的教程中,我們演示瞭如何在多個接受這之間分發資源密集型的任務。接下來,我們將繼續深入的討論這個問題。
如果我們想要在一臺遠程的機器上運行一個資源密集型的任務,那麼是不是就意味着需要等待返回結果呢?本文,我們就來介紹RabbitMQ中的RPC遠程調用模式的概念及使用。
在下文中,我們將會演示構建一個遠程調用系統:一個客戶端,一個可伸縮的RPC服務器。由於我們並沒有實際的資源密集型的任務,所以我們打算假裝執行一個RPC服務,實際上卻是返回一個斐波那契數列。
客戶端接口(Client interface)
爲了說明RPC服務是如何被使用的,我們將創建一個簡單的客戶端,其將負責暴露一個call方法,並且其會堵塞進程,直到接收到返回值。示例如下:
FibonacciRpcClient fibonacciRpc = new FibonacciRpcClient();
String result = fibonacciRpc.call("4");
System.out.println( "fib(4) is " + result);
特別提醒:
儘管在系統中,RPC調用時非常普遍存在的模式,但其卻常被人們所詬病。問題發生在:當程序開發人員沒有及時的注意到一個服務是被本地調用的,或者,遠程調用過程非常的緩慢。類似於這樣的情況發生在不可預測的系統環境中,並且,爲了測試系統,增加了不必要的測試複雜度。本應該簡化的軟件,由於濫用RPC導致了在系統中增加了大量不可維護的代碼。
銘記上面的問題,我們給出以下的幾點建議:
- 確保可以明確看出:那些服務是本地調用,那些服務是遠程調用。
- 將系統結構文檔化,使系統結構之間的關係變得清晰可見。
- 及時進行錯誤處理。當遠程調用服務器發生錯誤時,如長時間,客戶端是如何應對的?
當存在疑問時,儘量的避免的使用RPC調用。如果條件允許的話,推薦使用異步的消息管道---而不是RPC---效果類似於阻塞,最終異步調用被延遲到下一個計算過程(意譯爲調度過程,方便理解)。
回調隊列(Callback queue)
一般來講,在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)
AMQP協議預定義了14個消息屬性。其中的大部分都是很少用到的,但是下面的幾個,希望各位看官能夠牢記:
- deliveryMode:設置消息持久化功能時,設置爲整形數:2。其他任何值都是標示臨時的含義。關於這個屬性的具體內容可以在前文介紹的工作隊列教程中尋找。
- contentType:爲了描述編碼mime-type。如常見的JSON格式。非常推薦的良好習慣,就是設置該屬性爲:application/json。
- replyTo:通常情況下,用來指明一個回調隊列。
- correlationId:用來關聯RPC的請求與相應。
綜上,我們需要在類中引入下面這句話:
import com.rabbitmq.client.AMQP.BasicProperties;
關聯ID(Correlation ID)
在上面介紹的方法中,我們暗示了需要爲每個RPC請求創建一個回調隊列。這種做法顯然是非常低效率的,但幸運的是,有一種更好的方式供我們選擇---我們可以爲所有的客戶端之創建一個回調隊列。
但是,這種做法又帶來的新的問題,隊列接收到一個響應時,無法確定其歸屬於哪一個請求。這正是關聯ID發揮作用的時機。我們將爲每一個請求與返回之間設置一個唯一的關聯ID。之後,當回調隊列中接收到響應時,我們再來觀察該屬性,並且基於它,我們就有辦法將請求與響應進行匹配。如果我們發現一個未知的關聯ID,就可以在保證安全的前提下,丟棄這條消息---因爲其不屬於我們已經記錄下的所發出的請求。
各位看官可能會問:爲什麼我們可以忽略掉隊列當中未知的消息,而不是產生一條錯誤。這是因爲:在服務器上發生競爭條件的可能性,儘管很小很小,但仍然有可能發生:RPC服務器,在我們剛發送完響應之後,發生宕機,但還沒來得及向請求方進行消息確認。如果這種情況發生了,重啓RPC調用將會再次發起這個請求。這就是爲什麼在客戶端上,我們就必須的完全的處理好重複響應,並且,RPC服務最好是冪等性的。
總結(Summary)
我們的RPC將會類似下面的流程進行工作:
- 當客戶端啓動時,其將會創建一個匿名的特定的回調隊列。
- 對於一個RPC調用,客戶端發送出的消息都帶有兩個屬性:replyTo,設置回調隊列。關聯ID(correlationId),對請求設置唯一的id值。
- 請求被髮送到一個稱爲“rpc_queue”的隊列當中。
- RPC worker(也稱之爲:server)在隊列上一直等待請求的發生。當發生請求時,其處理該任務,並使用relayTo指定的隊列,將請求結果以消息的形式發送到客戶端當中。
- 客戶端在回調隊列當中等待數據。當有一條消息出現時,就會檢查關聯ID屬性(correlationId)。如果其能夠匹配到請求當中的任何一個,那麼就會將響應返回給應用程序。
綜上所述,我們來看看完整的工程吧,具體內容如下:
1.修改pom文件,具體內容請看前文,在此不再贅述。
2.創建RPCClient文件,具體內容如下:
package com.csdn.ingo.rabbitmq_1;
import com.rabbitmq.client.AMQP;
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("localhost");
factory.setPort(AMQP.PROTOCOL.PORT);
connection = factory.newConnection();
channel = connection.createChannel();
//• 註冊'回調'隊列,這樣就可以收到RPC響應
replyQueueName = channel.queueDeclare().getQueue();
consumer = new QueueingConsumer(channel);
channel.basicConsume(replyQueueName, true, consumer);
}
//發送RPC請求
public String call(String message) throws Exception {
String response = null;
String corrId = java.util.UUID.randomUUID().toString();
//發送請求消息,消息使用了兩個屬性:replyto和correlationId
BasicProperties props = new BasicProperties.Builder()
.correlationId(corrId).replyTo(replyQueueName).build();
channel.basicPublish("", requestQueueName, props, message.getBytes());
//等待接收結果
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
//檢查它的correlationId是否是我們所要找的那個
if (delivery.getProperties().getCorrelationId().equals(corrId)) {
response = new String(delivery.getBody());
break;
}
}
return response;
}
public void close() throws Exception {
connection.close();
}
}
3.創建RPCMain文件,具體內容如下:
package com.csdn.ingo.rabbitmq_1;
public class RPCMain {
public static void main(String[] args) throws Exception {
RPCClient rpcClient = new RPCClient();
System.out.println(" [x] Requesting getMd5String(abc)");
String response = rpcClient.call("abc");
System.out.println(" [.] Got '" + response + "'");
rpcClient.close();
}
}
4.創建RPCServer文件,具體內容如下:
package com.csdn.ingo.rabbitmq_1;
import java.security.MessageDigest;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
public class RPCServer {
private static final String RPC_QUEUE_NAME = "rpc_queue";
public static void main(String[] args) throws Exception {
//• 先建立連接、通道,並聲明隊列
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(AMQP.PROTOCOL.PORT);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
//•可以運行多個服務器進程。通過channel.basicQos設置prefetchCount屬性可將負載平均分配到多臺服務器上。
channel.basicQos(1);
QueueingConsumer consumer = new QueueingConsumer(channel);
//打開應答機制autoAck=false
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());
System.out.println(" [.] getMd5String(" + message + ")");
String response = getMd5String(message);
//返回處理結果隊列
channel.basicPublish("", props.getReplyTo(), replyProps,
response.getBytes());
//發送應答
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
// 模擬RPC方法 獲取MD5字符串
public static String getMd5String(String str) {
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (Exception e) {
System.out.println(e.toString());
e.printStackTrace();
return "";
}
char[] charArray = str.toCharArray();
byte[] byteArray = new byte[charArray.length];
for (int i = 0; i < charArray.length; i++)
byteArray[i] = (byte) charArray[i];
byte[] md5Bytes = md5.digest(byteArray);
StringBuffer hexValue = new StringBuffer();
for (int i = 0; i < md5Bytes.length; i++) {
int val = ((int) md5Bytes[i]) & 0xff;
if (val < 16)
hexValue.append("0");
hexValue.append(Integer.toHexString(val));
}
return hexValue.toString();
}
}
5.測試方法,首先啓動Server,在運行main方法,觀察控制檯輸出即可。
6.特別備註:
上面這份源碼,摘自其他博文,再次表示感謝。
我們沒有斐波那契數列作爲演示,但原理一致,有興趣的看官可以在官方文檔中找到源碼,自行測試即可。
上面源碼中使用的方法在前文中均有解釋,有疑問的地方,請各位看官自行查看。
--------------------------------------------------------------------------------------------------------------------------------------------------------
至此,系統拆分解耦利器之消息隊列---RabbitMQ-RPC遠程調用 結束
參考資料:
官方文檔:http://www.rabbitmq.com/tutorials/tutorial-six-java.html