注:這是RabbitMQ-java版Client的指導教程翻譯系列文章,歡迎大家批評指正
第一篇Hello Word了解RabbitMQ的基本用法
第二篇Work Queues介紹隊列的使用
第三篇Publish/Subscribe介紹轉換器以及其中fanout類型
第四篇Routing介紹direct類型轉換器
第五篇Topics介紹topic類型轉換器
第六篇RPC介紹遠程調用
遠程過程調用(Remote procedure call )
在第二篇指導教程中,我們學會在多個消費者之間使用工作隊列循環分發任務。但是如果我們需要在遠程電腦上運行一個程序并且需要返回結果呢。這就是另一個不同的事情,這個模型就是聞名的遠程過程調用,簡稱RPC。
在這篇指導教程中,我們將會使用RabbitMQ去創建一個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阻塞的方式。異步返回結果會被推送到另一個計算階段。
返回隊列(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 0-9-1協議預先定義了14個屬性,大部分的屬性很少使用,下面有一些說明:
deliveryMode:標記這個消息可以持久化(值為2),或者短暫保留(值為其它的),在第二章里面有提到過這個屬性
contentType:用于描述文本的類型。例如經常使用JSON的編碼方式,這是經常設置的屬性:application/json
replyTo:經常用于存儲返回隊列的名字
correlationId:對請求的RPC相應是有用的
我們使用一個新的引用:
import com.rabbitmq.client.AMQP.BasicProperties;
相關聯的Id(Correlation Id)
按照目前上述的方法,我們需要為每一個RPC請求都創建一個callBack隊列,顯然不夠高效。幸運的是這里有一種更好的方式,一個客戶端我們只需要創建一個callback隊列。
這會導致一個新的問題,在隊列中接收的響應不知道是哪一個請求的,這就需要使用到correlationId屬性,我們為每一個請求都設置唯一correlationId的值,基于這個值我們就能都找到匹配請求的相應。如果我們有一個不匹配correlationId的值,或許可以刪除這條消息,因為它不屬于我們的請求。
你可能會問:在返回隊列中我們為什么應該忽略掉不知道的消息,而不是當做一個錯誤?在服務端有一種可能,在發送我們一條消息的之后,RPC服務死了,但是反饋信息已經發出去了。如果發生這種情況,重新啟動的RPC服務端將會再次處理這個消息,這就是為什么我們必須在客戶端處理這個多余的相應。對于RPC也是這樣理解的(這一段亂七八糟)。
總結
我們RPC就像上圖這樣工作:
當客戶端創建,它創建一個異步唯一的callback隊列
對于一個RPC請求來說,客戶端發送帶有兩個屬相的消息:replyTo,被設置callback隊列的名稱;correlationId被設置為唯一請求的值。
這個請求被發送到rpc-queue隊列
在隊列中RPC工作者等待著請求,當請求來的時候,它處理工作,把帶有反饋隊列的消息發送回客戶端,使用replyTo中的返回隊列。
客戶端在callback隊列中等待返回數據,當消息來得時候,它會檢查correlationId的屬性,如果它匹配請求中的correlationId的值,就會返回相應給應用。
綜合
斐波那契數列任務:
private static int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fib(n-1) + fib(n-2);
}
我們聲明了斐波那契數列的方法,它表明只有一個有效積極的數輸入(不要期望去處理很多的數字,實現起來會很慢很慢的)
下面是RPCServer.javade daima ,這里下載:
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class RPCServer {
private static final String RPC_QUEUE_NAME = "rpc_queue";
public static void main(String[] argv) {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = null;
try {
connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
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);
}
}
};
channel.basicConsume(RPC_QUEUE_NAME, false, consumer);
//...
}}}
服務端代碼是非常直觀明了
跟以往一樣,先建立連接,創建通道,聲明隊列。我們可能想要運行多個服務進程,為了創建更多的服務者,我們需要在channel.basicQos方法中設置prefetchCount的值。
我們使用bacisConsume去連接隊列,隊列中我們提供一個一個返回表單的對象,用于工作并且返回相應。
下面是RPCClient.java的源代碼,這里下載:
import com.rabbitmq.client.*;
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";
private String replyQueueName;
public RPCClient() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
connection = factory.newConnection();
channel = connection.createChannel();
replyQueueName = channel.queueDeclare().getQueue();
}
public String call(String message) throws IOException, InterruptedException {
String corrId = UUID.randomUUID().toString();
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder().correlationId(corrId).replyTo(replyQueueName).build();
channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));
final BlockingQueue response = new ArrayBlockingQueue(1);
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"));
}
}
});
return response.take();
}
public void close() throws IOException {
connection.close();
}
//...
}
客戶端代碼有一些調用:
我們創建連接,通道,聲明了唯一作為響應的返回隊列,我們訂閱了callback的隊列,這樣我們就可以接受到RPC的響應。
我們的call方法將會調用RPC的請求。
這里,我們第一次定義了唯一的correlationId值并且保存它。在DefaultConsumer中實現的handleDelivery的方法將會使用該值去和獲取到響應的correlationId比較。
下一步,我們發布一個請求的消息,并且帶有兩個屬性:replyTo和correlationId.
這個時候,我們可以停下來,等待合適的反饋。
消費者開啟另一個線程中處理消息,在響應前我們應該先擱置主線程。使用BlockingQueue來處理,這就是我們創建只有一個容器ArrayblockingQueue,正如我們只需要去等待一個響應。
這個handleDelivery方法在做一些簡單的工作,對于每一個響應消息它都會檢查correlationId是否是我們想要的那個,然后把結果發送到blockQueue中。
同時主線程將會等待從BloakingQueue中取出消息。
最后我們返回結果給使用者。
客戶端發送請求:
RPCClient fibonacciRpc = new RPCClient();
System.out.println(" [x] Requesting fib(30)");
String response = fibonacciRpc.call("30");
System.out.println(" [.] Got '" + response + "'");
fibonacciRpc.close();
現在希望好好看一下我們例子中的源代碼。
跟以前的指導文件中一樣編譯:
javac -cp $CP RPCClient.java RPCServer.java
我們RPC服務已經準備好了,現在開啟服務:
java -cp $CP RPCServer
# => [x] Awaiting RPC requests
運行想要獲取斐波那契數列的客戶端:
java -cp $CP RPCClient
# => [x] Requesting fib(30)
目前的設計不僅僅只是實現RPC服務的接口,它還有一些其它的重要優勢:如果RPC服務太慢,你可以按比例增加運行一個RPC,在一個新的控制臺運行第二個RPC服務。
在客戶端,RPC只能需要發送和接收一條消息,同步請求像queueDeclare是必須的,因此RPC客戶端的結果需要連接網絡才能獲取到一個簡單的RPC請求。(亂七八糟)
我們的代碼依然是非常的簡單,并沒有解決很復雜的問題,像:
- 如果沒有運行服務端,客戶端如何響應?
- 客戶端應該對RPC設置超時操作么?
- 如果服務端發生故障了并且爆出了異常,應該把異常發送給客戶端么?
- 在進行處理之前,保護無效的消息么(檢查綁定和類型)?
第六節的內容大致翻譯完了,這里是原文鏈接。
終篇是我對RabbitMQ使用理解的總結文章,歡迎討教。
--謝謝--