RabbitMQ 之六 RPC

在RabbitMQ系列的第二篇文章Work Queus中,我們學會將耗時的task分佈到多個workers。

那如果我們需要在一臺遠程電腦上執行一個函數然後等待獲取結果?這種模式叫做Remote Procedure Call或者RPC。即遠程程序調用。

這篇文章中我們將構建一個RPC系統:一個客戶端和一個可伸縮的RPC服務器,我們將會構建一個返回計算斐波納契結果遠程服務來模擬需要分佈式的耗時task。

Client interface

我們通過客戶端來調用遠程服務,並阻塞等待結果的返回,調用代碼如下

FibonacciRpcClient fibonacciRpc = new FibonacciRpcClient();
String result = fibonacciRpc.call("4");
System.out.println( "fib(4) is " + result);
Callback queue

一般來說,通過RabbitMQ來實現RPC比較簡單,Client發生請求消息,Server返回應答消息,爲了接收返回結果,我們需要在request裏附帶一個"callback" queue。可以使用默認的queue,代碼如下

callbackQueueName = channel.queueDeclare().getQueue();

BasicProperties props = new BasicProperties
                            .Builder()
                            .replyTo(callbackQueueName)
                            .build();

channel.basicPublish("", "rpc_queue", props, message.getBytes());
Correlation Id

在上面的方法中,我們爲每一個RPC requeest創建一個callback queue,這種效率比較低,我們可以爲每個client創建一個callback queue。

這裏面出現了一個新問題,當我們接收到response,我們不清楚這個response對應於哪個request,這時候就需要correlationId這個屬性了。我們爲每個request設置一個唯一值,當我們從callback queue接收到消息後,我們就可以根據這個值來匹配對於的request和response,當遇到一個不合理的correlationId值,我們就可以安全的丟棄這個消息。

你可能會疑惑爲什麼可以忽視在callback queue中未知的消息,而不是當作失敗或者錯誤?這是由於server端的競爭條件導致的,雖然不太可能,RPC server可能在發送應答之後,在接受來自於request的確認消息之前掛掉,這種情況下,當RPC Server重啓後,RPC Server會重新處理這個request,並再次發送應答,這就是客戶端需要處理重複應答的原因。Rpc應該是冪等的。

rpc的模型如下

client啓動後,client創建一個匿名的callback queue。

對於一個rpc requset,client發送要給消息帶有兩個屬性:replyTo設置爲callback queue,correlationId設置爲唯一值。

對於一個rpc server,等待來自於queue的requests,當一個request到來,處理request並將處理結果返回給client,這個queue來自於replyTo字段的值。

Client等待來自於callback queue的數據,當一個消息到來,檢查correlationId屬性,如果與request中相匹配,那就返回結果。

代碼如下:

RPCServer.java

package org.rabbitmq;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

public class RPCServer {
	private static final String RPC_QUEUE_NAME = "rpc_queue";

	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) {
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost("localhost");
		factory.setPort(5673);
		
		Connection connection = null;
		try {
			connection = factory.newConnection();
			final 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);
						// RabbitMq consumer worker thread notifies the RPC
						// server owner thread
						synchronized (this) {
							this.notify();
						}
					}
				}
			};

			channel.basicConsume(RPC_QUEUE_NAME, false, consumer);
			// Wait and be prepared to consume the message from RPC client.
			while (true) {
				synchronized (consumer) {
					try {
						consumer.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		} catch (IOException | TimeoutException e) {
			e.printStackTrace();
		} finally {
			if (connection != null)
				try {
					connection.close();
				} catch (IOException _ignore) {
				}
		}
	}
}
RPCClient.java

package org.rabbitmq;

import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeoutException;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

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");
		factory.setPort(5673);
		
		connection = factory.newConnection();
		channel = connection.createChannel();

		replyQueueName = channel.queueDeclare().getQueue();
	}

	public String call(String message) throws IOException, InterruptedException {
		final 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<String> response = new ArrayBlockingQueue<String>(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();
	}

	public static void main(String[] argv) {
		RPCClient fibonacciRpc = null;
		String response = null;
		try {
			fibonacciRpc = new RPCClient();

			System.out.println(" [x] Requesting fib(30)");
			response = fibonacciRpc.call("30");
			System.out.println(" [.] Got '" + response + "'");
		} catch (IOException | TimeoutException | InterruptedException e) {
			e.printStackTrace();
		} finally {
			if (fibonacciRpc != null) {
				try {
					fibonacciRpc.close();
				} catch (IOException _ignore) {
				}
			}
		}
	}
}
運行結果如下:

參考:http://www.rabbitmq.com/tutorials/tutorial-six-java.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章