RabbitMQ (訊息佇列)專題學習07 RPC

來源:互聯網
上載者:User

標籤: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源碼

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.