关闭

系统拆分解耦利器之消息队列---RabbitMQ-RPC远程调用

标签: rabbitmq远程调用RPC
2380人阅读 评论(0) 收藏 举报
分类:

[一曲广陵不如晨钟暮鼓]

本文,我们来介绍RabbitMQ中的RPC远程调用。在正式开始之前,我们假设RabbitMQ服务已经启动,运行端口为5672,如果各位看官有更改过默认配置,那么就需要修改为对应端口,保持一致即可。

准备工作:

操作系统:window 7 x64 

其他软件:eclipse mars,jdk7,maven 3

--------------------------------------------------------------------------------------------------------------------------------------------------------


Remote procedure call (RPC)


在前文介绍的关于“工作队列”的教程中,我们演示了如何在多个接受这之间分发资源密集型的任务。接下来,我们将继续深入的讨论这个问题。

如果我们想要在一台远程的机器上运行一个资源密集型的任务,那么是不是就意味着需要等待返回结果呢?本文,我们就来介绍RabbitMQ中的RPC远程调用模式的概念及使用。

在下文中,我们将会演示构建一个远程调用系统:一个客户端,一个可伸缩的RPC服务器。由于我们并没有实际的资源密集型的任务,所以我们打算假装执行一个RPC服务,实际上却是返回一个斐波那契数列。


客户端接口(Client interface)


为了说明RPC服务是如何被使用的,我们将创建一个简单的客户端,其将负责暴露一个call方法,并且其会堵塞进程,直到接收到返回值。示例如下:

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

特别提醒:

尽管在系统中,RPC调用时非常普遍存在的模式,但其却常被人们所诟病。问题发生在:当程序开发人员没有及时的注意到一个服务是被本地调用的,或者,远程调用过程非常的缓慢。类似于这样的情况发生在不可预测的系统环境中,并且,为了测试系统,增加了不必要的测试复杂度。本应该简化的软件,由于滥用RPC导致了在系统中增加了大量不可维护的代码。

铭记上面的问题,我们给出以下的几点建议:

  • 确保可以明确看出:那些服务是本地调用,那些服务是远程调用。
  • 将系统结构文档化,使系统结构之间的关系变得清晰可见。
  • 及时进行错误处理。当远程调用服务器发生错误时,如长时间,客户端是如何应对的?

当存在疑问时,尽量的避免的使用RPC调用。如果条件允许的话,推荐使用异步的消息管道---而不是RPC---效果类似于阻塞,最终异步调用被延迟到下一个计算过程(意译为调度过程,方便理解)


回调队列(Callback queue)


一般来讲,在RabbitMQ上搭建RPC调用框架是非常容易的---客户端发送请求消息,服务端回应消息。为了接收响应消息,我们需要在发送请求时,附带一个回调队列地址。可以使用默认队列(Java客户端特有的),具体如下:

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 ...

消息属性(Message properties)

AMQP协议预定义了14个消息属性。其中的大部分都是很少用到的,但是下面的几个,希望各位看官能够牢记:

  • deliveryMode:设置消息持久化功能时,设置为整形数:2。其他任何值都是标示临时的含义。关于这个属性的具体内容可以在前文介绍的工作队列教程中寻找。
  • contentType:为了描述编码mime-type。如常见的JSON格式。非常推荐的良好习惯,就是设置该属性为:application/json。
  • replyTo:通常情况下,用来指明一个回调队列。
  • correlationId:用来关联RPC的请求与相应。

综上,我们需要在类中引入下面这句话:

import com.rabbitmq.client.AMQP.BasicProperties;

关联ID(Correlation ID)

在上面介绍的方法中,我们暗示了需要为每个RPC请求创建一个回调队列。这种做法显然是非常低效率的,但幸运的是,有一种更好的方式供我们选择---我们可以为所有的客户端之创建一个回调队列。

但是,这种做法又带来的新的问题,队列接收到一个响应时,无法确定其归属于哪一个请求。这正是关联ID发挥作用的时机。我们将为每一个请求与返回之间设置一个唯一的关联ID。之后,当回调队列中接收到响应时,我们再来观察该属性,并且基于它,我们就有办法将请求与响应进行匹配。如果我们发现一个未知的关联ID,就可以在保证安全的前提下,丢弃这条消息---因为其不属于我们已经记录下的所发出的请求。

各位看官可能会问:为什么我们可以忽略掉队列当中未知的消息,而不是产生一条错误。这是因为:在服务器上发生竞争条件的可能性,尽管很小很小,但仍然有可能发生:RPC服务器,在我们刚发送完响应之后,发生宕机,但还没来得及向请求方进行消息确认。如果这种情况发生了,重启RPC调用将会再次发起这个请求。这就是为什么在客户端上,我们就必须的完全的处理好重复响应,并且,RPC服务最好是幂等性的。


总结(Summary)


我们的RPC将会类似下面的流程进行工作:

  • 当客户端启动时,其将会创建一个匿名的特定的回调队列。
  • 对于一个RPC调用,客户端发送出的消息都带有两个属性:replyTo,设置回调队列。关联ID(correlationId),对请求设置唯一的id值。
  • 请求被发送到一个称为“rpc_queue”的队列当中。
  • RPC worker(也称之为:server)在队列上一直等待请求的发生。当发生请求时,其处理该任务,并使用relayTo指定的队列,将请求结果以消息的形式发送到客户端当中。
  • 客户端在回调队列当中等待数据。当有一条消息出现时,就会检查关联ID属性(correlationId)。如果其能够匹配到请求当中的任何一个,那么就会将响应返回给应用程序。

综上所述,我们来看看完整的工程吧,具体内容如下:


1.修改pom文件,具体内容请看前文,在此不再赘述。

2.创建RPCClient文件,具体内容如下:

package com.csdn.ingo.rabbitmq_1;
import com.rabbitmq.client.AMQP;  
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("localhost");  
        factory.setPort(AMQP.PROTOCOL.PORT);  
        connection = factory.newConnection();  
        channel = connection.createChannel();  
        //• 注册'回调'队列,这样就可以收到RPC响应  
        replyQueueName = channel.queueDeclare().getQueue();  
        consumer = new QueueingConsumer(channel);  
        channel.basicConsume(replyQueueName, true, consumer);  
    }  
  
    //发送RPC请求  
    public String call(String message) throws Exception {  
        String response = null;  
        String corrId = java.util.UUID.randomUUID().toString();  
        //发送请求消息,消息使用了两个属性:replyto和correlationId  
        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());  
                break;  
            }  
        }  
        return response;  
    }  
    public void close() throws Exception {  
        connection.close();  
    }  
}  
3.创建RPCMain文件,具体内容如下:

package com.csdn.ingo.rabbitmq_1;
public class RPCMain {  
  
    public static void main(String[] args) throws Exception {  
        RPCClient rpcClient = new RPCClient();  
        System.out.println(" [x] Requesting getMd5String(abc)");     
        String response = rpcClient.call("abc");  
        System.out.println(" [.] Got '" + response + "'");  
        rpcClient.close();  
    }  
}  
4.创建RPCServer文件,具体内容如下:

package com.csdn.ingo.rabbitmq_1;

import java.security.MessageDigest;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;


public class RPCServer {
	private static final String RPC_QUEUE_NAME = "rpc_queue";  
    public static void main(String[] args) throws Exception {  
        //• 先建立连接、通道,并声明队列  
        ConnectionFactory factory = new ConnectionFactory();  
        factory.setHost("localhost");  
        factory.setPort(AMQP.PROTOCOL.PORT);  
        Connection connection = factory.newConnection();  
        Channel channel = connection.createChannel();  
        channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);  
        //•可以运行多个服务器进程。通过channel.basicQos设置prefetchCount属性可将负载平均分配到多台服务器上。  
        channel.basicQos(1);  
        QueueingConsumer consumer = new QueueingConsumer(channel);  
        //打开应答机制autoAck=false  
        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());  
            System.out.println(" [.] getMd5String(" + message + ")");  
            String response = getMd5String(message);  
            //返回处理结果队列  
            channel.basicPublish("", props.getReplyTo(), replyProps,  
                    response.getBytes());  
            //发送应答   
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);  
        }  
    }  
    // 模拟RPC方法 获取MD5字符串  
    public static String getMd5String(String str) {  
        MessageDigest md5 = null;  
        try {  
            md5 = MessageDigest.getInstance("MD5");  
        } catch (Exception e) {  
            System.out.println(e.toString());  
            e.printStackTrace();  
            return "";  
        }  
        char[] charArray = str.toCharArray();  
        byte[] byteArray = new byte[charArray.length];  
  
        for (int i = 0; i < charArray.length; i++)  
            byteArray[i] = (byte) charArray[i];  
        byte[] md5Bytes = md5.digest(byteArray);  
        StringBuffer hexValue = new StringBuffer();  
        for (int i = 0; i < md5Bytes.length; i++) {  
            int val = ((int) md5Bytes[i]) & 0xff;  
            if (val < 16)  
                hexValue.append("0");  
            hexValue.append(Integer.toHexString(val));  
        }  
        return hexValue.toString();  
    }  
}
5.测试方法,首先启动Server,在运行main方法,观察控制台输出即可。

6.特别备注:

上面这份源码,摘自其他博文,再次表示感谢。

我们没有斐波那契数列作为演示,但原理一致,有兴趣的看官可以在官方文档中找到源码,自行测试即可。

上面源码中使用的方法在前文中均有解释,有疑问的地方,请各位看官自行查看。

--------------------------------------------------------------------------------------------------------------------------------------------------------

至此,系统拆分解耦利器之消息队列---RabbitMQ-RPC远程调用 结束


参考资料:

官方文档:http://www.rabbitmq.com/tutorials/tutorial-six-java.html


1
0
查看评论

RabbitMQ消息队列(七):适用于云计算集群的远程调用(RPC)

在云计算环境中,很多时候需要用它其他机器的计算资源,我们有可能会在接收到Message进行处理时,会把一部分计算任务分配到其他节点来完成。那么,RabbitMQ如何使用RPC呢?在本篇文章中,我们将会通过其它节点求来斐波纳契完成示例。 1. 客户端接口 Client interface &#...
  • gaowenhui2008
  • gaowenhui2008
  • 2015-03-19 09:07
  • 805

远程调用服务(RPC)和消息队列(Message Queue)对比及其适用/不适用场合分析

最近考虑把公司系统重构升级,将原有的垂直MVC架构迁移为分布式系统,因此着重了解了下远程调用服务(RPC)和消息队列(MQ)。一开始对这两个概念比较模糊,RPC和MQ都是用于分布式系统的两个关键技术,并且里面都有服务提供者和消费者的概念,可在一定程度上对系统进行解耦。但对于彼此应用场景的区分还不是特...
  • cfydaniel
  • cfydaniel
  • 2015-03-25 14:19
  • 5624

RPC和MQ各自适合的应用场景

RPC比较适合- 客户端调用哪个服务器比较明确 - 调用需要立即得到返回结果 - 架构简单   在一个由多个微服务构成的大系统中,某些关键服务间的调用应当在较短的时间内返回,而且各个微服务的专业化程度较高,同一个请求的关注者只有一个。这个时候就应该用RPC。   比如在一个ERP系统中,有一个...
  • xuduorui
  • xuduorui
  • 2017-02-22 22:40
  • 2200

RPC和MQ对比及其适用/不适用场合

在阿里的平台技术部参与开发了Dubbo(远程调用服务)和Napoli(消息解决方案),又给网站应用支持这2个产品很长一段时间,了解了这2个产品的实现及应用对这两个产品的用法。 大部分情况下,“给定场景下应该使用这两个产品中哪个”这个问题,大家都会容易决定,而且不需要多少讨论。 我为什么要...
  • glory1234work2115
  • glory1234work2115
  • 2016-06-21 16:14
  • 5239

RabbitMQ消息队列(七):适用于云计算集群的远程调用(RPC)

在云计算环境中,很多时候需要用它其他机器的计算资源,我们有可能会在接收到Message进行处理时,会把一部分计算任务分配到其他节点来完成。那么,RabbitMQ如何使用RPC呢?在本篇文章中,我们将会通过其它节点求来斐波纳契完成示例。
  • anzhsoft2008
  • anzhsoft2008
  • 2014-02-23 21:37
  • 37553

轻松搞定RabbitMQ(七)——远程过程调用RPC

翻译:http://www.rabbitmq.com/tutorials/tutorial-six-java.html 在第二篇博文中,我们已经了解到了如何使用工作队列来向多个消费者分散耗时任务。 但是付过我们需要在远程电脑上运行一个方法然后等待结果,该怎么办?这是不同的需求。这个模式通常叫做RPC...
  • xiaoxian8023
  • xiaoxian8023
  • 2015-09-30 18:51
  • 13601

RabbitMQ之RPC实现

什么是RPC?RPC是指远程过程调用,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如...
  • u013256816
  • u013256816
  • 2017-02-15 20:28
  • 1745

RabbitMQ几种模式

RabbitMQ几种模式
  • u014263388
  • u014263388
  • 2017-05-30 17:51
  • 809

探索 OpenStack 之(14):OpenStack 中 RabbitMQ 的使用(转)

探索 OpenStack 之(14):OpenStack 中 RabbitMQ 的使用 本文是 OpenStack 中的 RabbitMQ 使用研究 两部分中的第一部分,将介绍 RabbitMQ 的基本概念,即 RabbitMQ 是什么。第二部分将介绍其在 OpenStack 中...
  • tantexian
  • tantexian
  • 2015-04-01 11:19
  • 2463

RabbitMQ入门与消息队列模式详解

RabbitMQ提供了6种模式,分别是HelloWorld,Work Queue,Publish/Subscribe,Routing,Topics,RPC Request/reply,本文详细讲述了前5种,并给出代码实现和思路。其中Publish/Subscribe,Routing,Topics三种...
  • fysuccess
  • fysuccess
  • 2017-04-20 16:20
  • 3699
    个人资料
    • 访问:393698次
    • 积分:4318
    • 等级:
    • 排名:第8346名
    • 原创:117篇
    • 转载:7篇
    • 译文:6篇
    • 评论:48条
    联系方式
    文章分类
    最新评论