• RabbitMQ学习整理————基于RabbitMQ实现RPC


    前言

    这边参考了RabbitMQ的官网,想整理一篇关于RabbitMQ实现RPC调用的博客,打算把两种实现RPC调用的都整理一下,一个是使用官方提供的一个Java client,还有一个是Spring AMQP的整合使用。
    代码路径:https://github.com/yzh19961031/blogDemo/tree/master/rabbitmq

    什么是RPC

    RPC是远程过程调用(Remote Procedure Call)的缩写形式,简单说就是一个节点去请求另一个节点上面的服务并获得响应结果。
    我们之前总结的工作模式都是发送消息到指定的队列,再由相关的消费者进行消费,如果存在这样的场景,比如消费者消费完消息需给生产者一个具体的响应,然后生产者再根据这个响应进行其他的业务逻辑,这样就需要使用到RabbitMQ提供的RPC能力。

    RabbitMQ如何实现RPC

    官方有很详细的介绍文档,这边贴一下地址:https://www.rabbitmq.com/tutorials/tutorial-six-java.html
    RabbitMQ实现RPC很简单,正常的流程就是请求以及响应,我们只需要在请求的消息的属性里面添加一个响应队列的地址,这边需要使用到一个BasicProperties这个类。具体配置如下:

    // 指定一个回调队列
    callbackQueueName = channel.queueDeclare().getQueue();
    // 设置replyTo的属性为指定的回调队列
    BasicProperties props = new BasicProperties
                                .Builder()
                                .replyTo(callbackQueueName)
                                .build();
    
    channel.basicPublish("", "rpc_queue", props, message.getBytes());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    BasicProperties这个类中提供了很多的属性,有14个,很多基本上很少用到,常用的就是几个,我这边也贴一下,其实在我上一篇文章中基于RabbitMQ实现的一个RPC工具里面都有用到这些属性。

    1. contentType 这个属性用来表明消息的类型,默认是"application/octet-stream"这种流的类型,还有常用的比如"application/json","text/plain"等,这些在我的RPC工具里面都有用到。
    2. replyTo 这个就是上面指定的回调队列。
    3. correlationId 这个id可以用来进行消息的确认,将相应与请求相关联。主要是可以确认服务端收到的消息是不是指定客户端发过来的,用于确认。

    首先先贴一张官方提供的图,这个是RabbitMQ实现RPC的主要工作流程:
    在这里插入图片描述
    实现RPC的具体工作流程:

    1. 首先客户端发送一个请求消息,这个请求消息里面有两个属性,一个是replyTo回调队列的地址,一个是correlationId用于标识当前消息唯一的id信息。
    2. 这个消息是发送到指定的rpc_queue这个队列上面。
    3. 对应我们的服务端Server就会等待rpc_queue上面的请求消息,当请求消息来得时候,服务端会进行处理,处理完成会将相应的消息再发送到请求消息属性中的replyTo回调的队列上面。
    4. 客户端发送消息之后,会等待replyTo队列中的消息。当有消息来得时候,会检查响应消息中correlationId属性和请求消息中correlationId是否一致,完成一次PRC调用。

    RPC简单示例

    我这边根据官网上面提供的例子简单修改整理了一下,这边提供一个大小写转换的功能,就是客户端发送一段小写的字符串,服务端将字符串转为大写再响应过来。详细逻辑可以看下代码中注释,具体代码如下:
    首先服务端:

    /**
     * RPC服务端
     *
     * @author yuanzhihao
     * @since 2020/11/21
     */
    public class RPCServer {
    
        public static void main(String[] args) throws IOException, TimeoutException {
            // 首先还是正常获得connection以及channel对象
            ConnectionFactory connectionFactory = new ConnectionFactory();
            connectionFactory.setHost("192.168.1.108");
            connectionFactory.setPort(5672);
            connectionFactory.setVirtualHost("/");
            Connection connection = connectionFactory.newConnection();
            Channel channel = connection.createChannel();
    
            // 定义一个rpc的队列
            String queueName = "test_rpc";
            channel.queueDeclare(queueName, false, false, false, null);
    
            Object monitor = new Object();
            // 具体的消费代码里面实现
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                // 消费者将请求消息中的correlationId信息再作为响应传回replyTo队列
                AMQP.BasicProperties replyProps = new AMQP.BasicProperties
                        .Builder()
                        .correlationId(delivery.getProperties().getCorrelationId())
                        .build();
    
                String response = "";
                try {
                    // 提供一个大小写转换的方法
                    String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
                    System.out.println("toUpperCase(" + message + ")");
                    response = toUpperCase(message);
                } catch (RuntimeException e) {
                    System.out.println(e.toString());
                } finally {
                    // 将响应传回replyTo队列
                    channel.basicPublish("", delivery.getProperties().getReplyTo(), replyProps, response.getBytes(StandardCharsets.UTF_8));
                    // 设置了手动应答 需要手动确认消息
                    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                    // 执行完成会释放主线程的锁
                    // RabbitMq consumer worker thread notifies the RPC server owner thread
                    synchronized (monitor) {
                        monitor.notify();
                    }
                }
            };
    
            // 监听"test_rpc"队列
            channel.basicConsume(queueName, false, deliverCallback, (consumerTag -> { }));
            // 这个锁对象是确保我们server的调用逻辑执行完成 首先挂起主线程
            // Wait and be prepared to consume the message from RPC client.
            while (true) {
                synchronized (monitor) {
                    try {
                        monitor.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        // 提供一个大小写转换的方法
        private static String toUpperCase(String msg) {
            return msg.toUpperCase();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    客户端:

    /**
     * RPC客户端
     *
     * @author yuanzhihao
     * @since 2020/11/21
     */
    
    public class RPCClient {
    
        public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
            // 创建connection以及channel对象
            ConnectionFactory connectionFactory = new ConnectionFactory();
            connectionFactory.setHost("192.168.1.108");
            connectionFactory.setPort(5672);
            connectionFactory.setVirtualHost("/");
    
            try ( Connection connection = connectionFactory.newConnection();
                  Channel channel = connection.createChannel()) {
                // 声明一个队列
                String queueName = "test_rpc";
    
                // 请求消息中需要带一个唯一标识ID 
                String corrId = UUID.randomUUID().toString();
                // 声明一个回调队列
                String replayQueueName = channel.queueDeclare().getQueue();
                // 将correlationId以及回调队列设置在消息的属性中
                AMQP.BasicProperties properties = new AMQP.BasicProperties
                        .Builder()
                        .correlationId(corrId)
                        .replyTo(replayQueueName)
                        .build();
                // 具体消息内容
                String msg = "hello rpc";
                // 发送请求消息
                channel.basicPublish("",queueName,properties,msg.getBytes());
                // 设置一个阻塞队列  等待服务端的响应
                final BlockingQueue<String> response = new ArrayBlockingQueue<>(1);
    
                String ctag = channel.basicConsume(replayQueueName, true, (consumerTag, message) -> {
                    // 注意 这边根据correlationId进行下判断
                    if (message.getProperties().getCorrelationId().equals(corrId)) {
                        response.offer(new String(message.getBody(), StandardCharsets.UTF_8));
                    }
                }, consumerTag -> {});
    
                // 获取响应结果
                String take = response.take();
                System.out.println("rpc result is "+ take);
                channel.basicCancel(ctag);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    执行代码,具体的客户端与服务端运行结果在这里插入图片描述 在这里插入图片描述

    通过Spring AMQP实现RPC

    通过Spring来实现RPC也很简单,主要通过spring提供的一个RabbitTemplate对象中sendAndReceive方法来实现,这个方法是发送消息然后一直等待响应。监听器里面实现的和之前的逻辑大致相同,都需要将response响应消息发送到对应的replyTo回调队列上。下面直接贴一下代码。
    首先是服务端,我这边直接是使用配置类的形式,具体一些的配置项可以参考下我之前的那篇博客或者上网搜一下~

    /**
     * 主配置类
     *
     * @author yuanzhihao
     * @since 2021/1/9
     */
    @Configuration
    public class RabbitMQConfig {
    
        private static final Logger log = LoggerFactory.getLogger(RabbitMQConfig.class);
    
        // 注入connectionFactory对象
        @Bean
        public ConnectionFactory connectionFactory() {
            CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
            connectionFactory.setAddresses("192.168.1.108:5672");
            connectionFactory.setUsername("guest");
            connectionFactory.setPassword("guest");
            connectionFactory.setVirtualHost("/");
            return connectionFactory;
        }
    
        // 声明队列
        @Bean
        public Queue rpcQueue() {
            return new Queue("test_rpc",false);
        }
    
        @Bean
        public RabbitTemplate rabbitTemplate() {
            return new RabbitTemplate(connectionFactory());
        }
    
        // 创建初始化RabbitAdmin对象
        @Bean
        public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
            RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
            // 只有设置为 true,spring 才会加载 RabbitAdmin 这个类
            rabbitAdmin.setAutoStartup(true);
            return rabbitAdmin;
        }
    
        // 消息监听器
        @Bean
        public SimpleMessageListenerContainer simpleMessageListenerContainer(RabbitTemplate rabbitTemplate) {
            SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory());
            // 监听的队列
            container.setQueues(rpcQueue());
            MessageListener messageListener = message -> {
                String receiveMsg = new String(message.getBody(), StandardCharsets.UTF_8);
                log.info("Receive a message message is {}", receiveMsg);
                // 执行对应逻辑
                String responseMsg = toUpperCase(receiveMsg);
                MessageProperties messageProperties = MessagePropertiesBuilder.newInstance().
                        setCorrelationId(message.getMessageProperties().getCorrelationId()).
                        build();
                // 响应消息 这边就是如果没有绑定交换机和队列的话 消息应该直接传到对应的队列上面
                rabbitTemplate.send("", message.getMessageProperties().getReplyTo(), new Message(responseMsg.getBytes(StandardCharsets.UTF_8), messageProperties));
            };
            // 设置监听器
            container.setMessageListener(messageListener);
            return container;
        }
    
        // 提供一个大小写转换的方法
        private String toUpperCase(String msg) {
            return msg.toUpperCase();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    客户端我采用test单元测试的形式

    /**
     * spring amqp rpc 测试类
     *
     * @author yuanzhihao
     * @since 2021/1/9
     */
    @ContextConfiguration(classes = {RabbitMQConfig.class})
    @RunWith(SpringRunner.class)
    public class RabbitMQRpcTest {
        private static final Logger log = LoggerFactory.getLogger(RabbitMQConfig.class);
    
        @Autowired
        private RabbitTemplate rabbitTemplate;
    
        // 测试RPC客户端
        @Test
        public void testRpcClient() {
            // 设置correlationId
            String corrId = UUID.randomUUID().toString();
            String msg = "hello rpc";
            MessageProperties messageProperties = MessagePropertiesBuilder.newInstance().setCorrelationId(corrId).build();
            // 注意 这边如果使用sendAndReceive不指定replyTo回调队列 spring会默认帮我们添加一个回调队列
            // 格式默认 "amq.rabbitmq.reply-to" 前缀
            Message message = rabbitTemplate.sendAndReceive("", "test_rpc", new Message(msg.getBytes(StandardCharsets.UTF_8), messageProperties));
            log.info("The response is {}", new String(message.getBody(), StandardCharsets.UTF_8));
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    具体实现可以看下代码的注释
    代码执行结果:
    在这里插入图片描述

  • 相关阅读:
    go项目实现mysql接入以及web api
    外汇天眼:经济衰退无阻加息,欧美货币政策齐收紧
    Zabbix图形监控
    3D object detection 论文、代码和数据集汇总
    Git 常用基本命令
    威固新能源GO野 伊士曼旗下品牌威固加速布局新能源车后市场
    2023-11-08 monetdb-事务-只有RR隔离级别-原因分析
    JavaScript中async和await的使用以及队列问题
    Gin源码之gin.Engine结构体及其方法
    图像分割 人脸分割CVPR2023笔记
  • 原文地址:https://blog.csdn.net/qq_32238611/article/details/112107762