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

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

本文,我们来介绍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


  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值