http://www.cnblogs.com/leocook/p/mq_rabbitmq_2.html 这篇博文中我们实现了怎么去使用work queue来把比较耗时的任务分散给多个worker。

但是,如果我们想在远程的机器上的一个函数并等待它返回结果,我们应该怎么办呢?这就是另外一种模式了,它被称为RPC(Remote procedure call)。

本篇博文中我们来实现怎么用RabbitMQ来构建一个RPC系统:一个client(客户端)和一个可扩展的RPC server(服务端)。这里我们来模拟一个返回斐波拉契数的RPC服务。

1、Client端接口

为了说明一个RPC服务时怎么工作的,我们来创建一个简单的client类。这里来实现一个名字为call的方法来发送RPC请求,并发生阻塞,直到接收到回复:

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

RPC注意事项:

虽然RPC是一种常用的模式,但它也有一些缺陷。当无法确定使用本地调用还是使用RPC时,问题就出来了。有的时候不确定程序的运行环境,这样来做会给程序的调试增加了一定的复杂度。使用RPC并不能够让代码变得更简洁,滥用的话只会让代码变得更不方便维护。

伴随着上边的问题,咱们来看看下边的建议:

  • 确定能很明显的分辨的出哪些调用是本地调用,哪些是远程调用。
  • 完善系统的文档。清楚的标记出,模块间的依赖关系。
  • 处理错误情况。当RPC服务挂了之后,客户端应该怎么去处理呢?

当有疑问时避免使用RPC。如果可以的话,你可以使用异步管道(不用RPC-阻塞),结果被异步推送到下一个计算环节。

2、回调队列(Callback queue)

一般用RabbitMQ来实现RPC是很简单的。客户端发送一个请求消息然后服务器端回应一个响应消息。为了接收服务端的响应消息,我们需要在请求中发送一个callback queue地址。我们也可以使用一个默认的queue(Java客户端独有的)。如下:

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

//绑定callback queue
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个属性连同消息一起发送出去。很多属性都是很少用到的,除了下边的这些:

消息的投递模型(deliveryMode):使消息持久化,和work queue里的设置一样。

上下文类型(contentType):用来描述媒体类型(mime-type)。例如常用的JSON格式,它的mime-type是application/json。

我们需要导包:

import com.rabbitmq.client.AMQP.BasicProperties;

3、Correlation Id

在上边的方法中建议我们为每个RPC请求都创建一个call queue,这样效率很低。我们有更好的办法,为每一个client创建一个call queue。

这样处理的话又出现了一个新的问题,无法确定接收到的响应是对应哪个请求的。这时候就需要correlationId属性,我们为每一个请求都设置一个correlationId属性。当我们从callback queue中接收到一条消息之后,我们将会查看correlationId属性,这样就可以用一个请求来与之匹配了。如果从callback queue接收到了一条消息后,发现其中的correlationId未能找到与之匹配的请求,那么将把这条消息丢掉。

你可能会问我们为什么要要在callback queue里忽略掉不知道的message,而不是报错呢?这是因为服务器端可能会出现的一种情况,虽然可能性很小,但还是有可能性的,有可能在RPC发送了响应之后,在发送确认完成任务的信息之前服务器重启了。如果这种情况发生了的话,重启了RPC服务之后,它将会再次接收到之前的请求,这样的话client将会重复处理响应,RPC服务应该是等幂的。

4、总结

我们的RPC工作原理如下:

  • 当Client启动时,它将会创建一个匿名的callback queue。
  • 对于一次RPC请求,client会发送一条含有两个属性的消息:replyTocorrelationId。Reply是设置的callback queue,correlationId是设置的当前请求的标示符。
  • 请求将会被发送到rpc_queue里。
  • RPC的worker(RPC server)等待queue中的请求。当出现一个请求之后,他将会处理任务,并向replyTo队列中发送消息。
  • 客户端会等待callback queue上的消息。当消息出现时,它将会检查correlationId属性是否能与之前发送请求时的属性一直,若一致的话,client将会处理回复的消息。

5、最终实现

斐波拉契任务:

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);
}

这里定义计算斐波拉契数的方法,假设传进去的整数都是正整数。

RPC服务端的代码实现如下RPCServer.java:

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.AMQP.BasicProperties; 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) {
Connection connection = null;
Channel channel = null;
try {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost"); connection = factory.newConnection();
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) {
String response = null; QueueingConsumer.Delivery delivery = consumer.nextDelivery(); //拿到correlationId属性
BasicProperties props = delivery.getProperties();
BasicProperties replyProps = new BasicProperties
.Builder()
.correlationId(props.getCorrelationId())
.build(); try {
String message = new String(delivery.getBody(),"UTF-8");
int n = Integer.parseInt(message); System.out.println(" [.] fib(" + message + ")");
response = "" + fib(n);
} catch (Exception e){
System.out.println(" [.] " + e.toString());
response = "";
}
finally {
//拿到replyQueue,并绑定为routing key,发送消息
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) {}
}
}
}
}

服务器端代码实现很简单的:

  • 建立连接,信道,声明队列
  • 为了能把任务压力平均的分配到各个worker上,我们在方法channel.basicQos里设置prefetchCount的值。
  • 我们使用basicConsume来接收消息,并等待任务处理,然后发送响应。

RPC客户端代码实现RPCClient.java

import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.AMQP.BasicProperties;
import java.util.UUID; 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");
connection = factory.newConnection();
channel = connection.createChannel(); //拿到一个匿名(并非真的匿名,拿到了一个随机生成的队列名)的队列,作为replyQueue。
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 = UUID.randomUUID().toString();//拿到一个UUID //封装correlationId和replyQueue属性
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(),"UTF-8");
break;
}
} return response;
} public void close() throws Exception {
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 (Exception e) {
e.printStackTrace();
}
finally {
if (fibonacciRpc!= null) {
try {
fibonacciRpc.close();
}
catch (Exception ignore) {}
}
}
}
}

RabbitMQ学习总结 第七篇:RCP(远程过程调用协议)的更多相关文章

  1. RabbitMQ学习总结 第三篇:工作队列Work Queue

    目录 RabbitMQ学习总结 第一篇:理论篇 RabbitMQ学习总结 第二篇:快速入门HelloWorld RabbitMQ学习总结 第三篇:工作队列Work Queue RabbitMQ学习总结 ...

  2. RabbitMQ学习总结 第四篇:发布/订阅 Publish/Subscribe

    目录 RabbitMQ学习总结 第一篇:理论篇 RabbitMQ学习总结 第二篇:快速入门HelloWorld RabbitMQ学习总结 第三篇:工作队列Work Queue RabbitMQ学习总结 ...

  3. RabbitMQ学习总结 第五篇:路由Routing

    目录 RabbitMQ学习总结 第一篇:理论篇 RabbitMQ学习总结 第二篇:快速入门HelloWorld RabbitMQ学习总结 第三篇:工作队列Work Queue RabbitMQ学习总结 ...

  4. RabbitMQ学习总结 第六篇:Topic类型的exchange

    目录 RabbitMQ学习总结 第一篇:理论篇 RabbitMQ学习总结 第二篇:快速入门HelloWorld RabbitMQ学习总结 第三篇:工作队列Work Queue RabbitMQ学习总结 ...

  5. Egret入门学习日记 --- 第七篇(书中 3.9节 内容)

    第七篇(书中 3.9节 内容) 好,今天就来看下 3.9节 的内容. 第一点: 昨天就已经搞定了. 第二点: 也包括在昨天的内容了. 第三点: 如果在构造函数里直接引用组件,就会挂掉. 但是把位置变化 ...

  6. 官网英文版学习——RabbitMQ学习笔记(七)Topic

    在上一篇中使用直接交换器改进了我们的系统,使得它能够有选择的进行接收消息,但它仍然有局限性——它不能基于多个条件进行路由.本节我们就进行能够基于多个条件进行路由的topics exchange学习. ...

  7. MyCat 学习笔记 第七篇.数据分片 之 按数据范围分片

    1 应用场景 Mycat 其实自带了2个数据范围分片的方案,一个是纯数据范围的分片,比如 1至 10000 号的数据放到分片1 ,10001 至 20000号数据放到分片2里. 另一个是数据常量形式的 ...

  8. 【学习opencv第七篇】图像的阈值化

    图像阈值化的基本思想是,给定一个数组和一个阈值,然后根据数组中每个元素是低于还是高于阈值而进行一些处理. cvThreshold()函数如下: double cvThreshold( CvArr* s ...

  9. Vue.js学习笔记 第七篇 表单控件绑定

    本篇主要说明表单控件的数据绑定,这次没有新的知识点 文本框 1.普通文本框 <div id="app-1"> <p><input v-model=&q ...

随机推荐

  1. C/C++中无条件花括号的妙用

    C/C++中无条件花括号可以形成一个代码块,一个作用域.可以使括号内定义的变量就只在本域(就是这个大括号)内有效,而且不会影响其他域,即使名字相同. 在花括号内,如果变量前面带类型,则相当于新创建一个 ...

  2. Oracle Database 11g Release 2(11.2.0.3.0) RAC On Redhat Linux 5.8 Using Vmware Workstation 9.0

    一,简介 二,配置虚拟机 1,创建虚拟机 (1)添加三块儿网卡:   主节点 二节点 eth0:    公网  192.168.1.20/24   NAT eth0:    公网  192.168.1 ...

  3. ASP.NET4.5Web API及非同步程序开发系列(1)

    认识非同步程序开发设计模型 从VS2012开始引入的新的非同步程序设计的支持-------async/await设计模型 之前的当我们支持非同步作业的时候,往往使用多线程开解决,我们比较熟悉的就是 执 ...

  4. iframe无刷新跨域并获得返回值

    参考:http://geeksun.iteye.com/blog/1070607 /** * iframe跨域提交大数据 * @param action 跨域地址 * @param arr [ {na ...

  5. 用JMeter进行Performance Test

    用JMeter可以对Http请求进行Performance Test,来分析哪些方法花费的时间多,哪些方法花费的时间少,有了这样的分析结果后,我们就可以集中力量来改进费时的方法. 官方文档请参考如下链 ...

  6. SwipeRefreshLayout下拉刷新简单用例

    自己的下拉刷新组件 下拉刷新并自动添加数据 MainActivity package com.shaoxin.myswiperefreshlayout; import android.graphics ...

  7. HTTP 头字段总结

    1. Accept: 告诉WEB服务器自己接受什么介质类型,/ 表示任何类型,type/* 表示该类型下的所有子类型,type/sub-type.2. Accept-Charset: 浏览器申明自己接 ...

  8. 防止 jsp被sql注入的五种方法

    一.SQL注入简介 SQL注入是比较常见的网络攻击方式之一,它不是利用操作系统的BUG来实现攻击,而是针对程序员编程时的疏忽,通过SQL语句,实现无帐号登录,甚至篡改数据库. 二.SQL注入攻击的总体 ...

  9. Node.js-视图引擎【1】-Swig集成express的安装与配置

    node.js视图引擎,选来选去发现Swig最符合我的胃口哈哈. 一.安装Swig视图引擎 npm install -g swig 二.在node.js代码中配置如下 var app = requir ...

  10. mysql like 贪婪匹配 同时匹配多个值

    LIKE "%a%b%c%",这样匹配出的就是包含a,b,c三个关键词的记录  (三个关键词不在一起时) 不好用 mysql> select count(1) from dm ...