6.RPC#前山翻譯

注:這是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也是這樣理解的(這一段亂七八糟)。

總結

python-six.png

我們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使用理解的總結文章,歡迎討教。
--謝謝--

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,776評論 18 139
  • “ 消息隊列已經逐漸成為企業IT系統內部通信的核心手段。它具有低耦合、可靠投遞、廣播、流量控制、最終一致性等一系列...
    落羽成霜丶閱讀 4,002評論 1 41
  • 來源 RabbitMQ是用Erlang實現的一個高并發高可靠AMQP消息隊列服務器。支持消息的持久化、事務、擁塞控...
    jiangmo閱讀 10,374評論 2 34
  • kafka的定義:是一個分布式消息系統,由LinkedIn使用Scala編寫,用作LinkedIn的活動流(Act...
    時待吾閱讀 5,342評論 1 15
  • 親愛的愛π: 你好啊!未來已經來到了,你實現了當初你曾經有過的夢想嗎? 你還記得自己曾經的夢想嗎? ...
    美生活閱讀 147評論 0 1