柯南君:看大数据时代下的IT架构(8)消息队列之RabbitMQ--案例(topic起航)

本文通过实战演示了如何利用RabbitMQ的Topic交换器实现复杂的消息路由,包括发送带有特定路由键的消息以及消费者如何根据不同的绑定键来接收消息。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、回顾

   让我们回顾一下,在上几章里都讲了什么?总结如下:
  
  
  
  
  
  

二、Topic(主题) (using the Java client)

      上一篇文章中,我们进步改良了我们的日志系统。我们使用direct类型转发器,使得接收者有能力进行选择性的接收日志,,而非fanout那样,只能够无脑的转发。
虽然使用direct类型改良了我们的系统,但是仍然存在一些局限性:
   
  • 它不能够基于多重条件进行路由选择。
在我们的日志系统中,我们有可能不仅希望根据日志的级别而且想根据日志的来源进行订阅。这个概念类似unix工具:syslog,它转发日志基于严重性(info/warning/crit…)和设备(auth/cron/kern…)
       这样可能给我们更多的灵活性:我们可能只想订阅来自’cron’的致命错误日志,而不是来自’kern’的。
为了在我们的系统中实现上述的需求,我们需要学习稍微复杂的主题类型的转发器(topic exchange)。
   
  

三、Topic exchange(主题转换)

 
主题类型的转发器的消息不能随意的设置选择键(routing_key),必须是由点隔开的一系列的标识符组成。
标识符可以是任何东西,但是一般都与消息的某些特性相关。一些合法的选择键的例子:
"stock.usd.nyse", "nyse.vmw","quick.orange.rabbit".你可以定义任何数量的标识符,上限为255个字节。
  
绑定键和选择键的形式一样。主题类型的转发器背后的逻辑和直接类型的转发器很类似: 一个附带特殊的选择键将会被转发到绑定键与之匹配的队列中。需要注意的是:关于绑定键有两种特殊的情况。
  • * 可以匹配一个标识符。
  • # 可以匹配0个或多个标识符。
下面一个简单的图片实例:
我们准备发送关于动物的消息。消息会附加一个选择键包含3个标识符(两个点隔开)。
   
第一个标识符描述动物的速度,
   
第二个标识符描述动物的颜色,
   
第三个标识符描述动物的物种:<speed>.<color>.<species>。
   
我们创建3个绑定键:Q1与*.orange.*绑定Q2与*.*.rabbit和lazy.#绑定。:
可以简单的认为,Q1对所有的橙色动物感兴趣。 Q2想要知道关于兔子的一切以及关于懒洋洋的动物的一切。
  
一个附带quick.orange.rabbit的选择键的消息将会被转发到两个队列。附带lazy.orange.elephant的消息也会被转发到两个队列。另一方面quick.orange.fox只会被转发到Q1,lazy.brown.fox将会被转发到Q2。lazy.pink.rabbit虽然与两个绑定键匹配,但是也只会被转发到Q2一次。quick.brown.fox不能与任何绑定键匹配,所以会被丢弃。 如果我们违法我们的约定,发送一个或者四个标识符的选择键,类似:orange,quick.orange.male.rabbit,这些选择键不能与任何绑定键匹配,所以消息将会被丢弃。
另一方面,lazy.orange.male.rabbit,虽然是四个标识符,也可以与lazy.#匹配,从而转发至Q2。
   
注:主题类型的转发器非常强大,可以实现其他类型的转发器。
  
当一个队列与绑定键#绑定,将会收到所有的消息,类似fanout类型转发器。 当绑定键中不包含任何#与*时,类似direct类型转发器

四、Putting it all together(全部代码)

我们要 在我们的日志系统, 用一个主题转换。我们先假设工作日志的路由键将两个词: <facility>.<severity>
发送 EmitLogTopic.java:
public class EmitLogTopic {

    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv)
                  throws Exception {

        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, "topic");

        String routingKey = getRouting(argv);
        String message = getMessage(argv);

        channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes());
        System.out.println(" [x] Sent '" + routingKey + "':'" + message + "'");

        connection.close();
    }
    //...
}
 接收端:ReceiveLogsTopic.java :
public class ReceiveLogsTopic {

    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv)
                  throws Exception {

        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, "topic");
        String queueName = channel.queueDeclare().getQueue();

        if (argv.length < 1){
            System.err.println("Usage: ReceiveLogsTopic [binding_key]...");
            System.exit(1);
        }

        for(String bindingKey : argv){
            channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);
        }

        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        QueueingConsumer consumer = new QueueingConsumer(channel);
        channel.basicConsume(queueName, true, consumer);

        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            String routingKey = delivery.getEnvelope().getRoutingKey();

            System.out.println(" [x] Received '" + routingKey + "':'" + message + "'");
        }
    }
}

Run the following examples, including the classpath as in Tutorial 1 - on Windows, use %CP%.

To receive all the logs:

$ java -cp $CP ReceiveLogsTopic "#"

To receive all logs from the facility "kern":

$ java -cp $CP ReceiveLogsTopic "kern.*"

Or if you want to hear only about "critical" logs:

$ java -cp $CP ReceiveLogsTopic "*.critical"

You can create multiple bindings:

$ java -cp $CP ReceiveLogsTopic "kern.*" "*.critical"

And to emit a log with a routing key "kern.critical" type:

$ java -cp $CP EmitLogTopic "kern.critical" "A critical kernel error"

Have fun playing with these programs. Note that the code doesn't make any assumption about the routing or binding keys, you may want to play with more than two routing key parameters.

Some teasers:

  • Will "*" binding catch a message sent with an empty routing key?
  • Will "#.*" catch a message with a string ".." as a key? Will it catch a message with a single word key?
  • How different is "a.*.#" from "a.#"?

五、注释版的程序实例(全部代码)

package com.abin.rabbitmq;  
  
import com.rabbitmq.client.Channel;  
import com.rabbitmq.client.Connection;  
import com.rabbitmq.client.ConnectionFactory;  
  
public class EmitLogTopic {  
    private static final String EXCHANGE_NAME = "topic_logs";  
  
    public static void main(String[] argv) throws Exception {  
  
        ConnectionFactory factory = new ConnectionFactory();  
        factory.setHost("localhost");  
        Connection connection = factory.newConnection();  
        Channel channel = connection.createChannel();  
  
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");//声明topic类型的Exchange  
  
        String routingKeyOne = "logs.error.one";// 定义一个路由名为“error”  
        for (int i = 0; i <= 1; i++) {  
            String messageOne = "this is one error logs:" + i;  
            channel.basicPublish(EXCHANGE_NAME, routingKeyOne, null, messageOne  
                    .getBytes());  
            System.out.println(" [x] Sent '" + routingKeyOne + "':'"  
                    + messageOne + "'");  
        }  
  
        System.out.println("################################");  
        String routingKeyTwo = "logs.error.two";  
        for (int i = 0; i <= 2; i++) {  
            String messageTwo = "this is two error logs:" + i;  
            channel.basicPublish(EXCHANGE_NAME, routingKeyTwo, null, messageTwo  
                    .getBytes());  
            System.out.println(" [x] Sent '" + routingKeyTwo + "':'"  
                    + messageTwo + "'");  
        }  
  
        System.out.println("################################");  
        String routingKeyThree = "logs.info.one";  
        for (int i = 0; i <= 3; i++) {  
            String messageThree = "this is one info logs:" + i;  
            channel.basicPublish(EXCHANGE_NAME, routingKeyThree, null,  
                    messageThree.getBytes());  
            System.out.println(" [x] Sent '" + routingKeyThree + "':'"  
                    + messageThree + "'");  
        }  
  
        channel.close();  
        connection.close();  
    }  
}  
运行结果可能如下:

 [x] Sent 'logs.error.one':'this is one error logs:0'  
 [x] Sent 'logs.error.one':'this is one error logs:1'  
################################  
 [x] Sent 'logs.error.two':'this is two error logs:0'  
 [x] Sent 'logs.error.two':'this is two error logs:1'  
 [x] Sent 'logs.error.two':'this is two error logs:2'  
################################  
 [x] Sent 'logs.info.one':'this is one info logs:0'  
 [x] Sent 'logs.info.one':'this is one info logs:1'  
 [x] Sent 'logs.info.one':'this is one info logs:2'  
 [x] Sent 'logs.info.one':'this is one info logs:3' 
第一个C端的代码如下:

package com.abin.rabbitmq;  
  
import com.rabbitmq.client.Channel;  
import com.rabbitmq.client.Connection;  
import com.rabbitmq.client.ConnectionFactory;  
import com.rabbitmq.client.QueueingConsumer;  
  
public class ReceiveLogsTopic {  
    private static final String EXCHANGE_NAME = "topic_logs";// 定义Exchange名称  
  
    public static void main(String[] argv) throws Exception {  
  
        ConnectionFactory factory = new ConnectionFactory();  
        factory.setHost("localhost");  
        Connection connection = factory.newConnection();  
        Channel channel = connection.createChannel();  
  
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");// 声明topic类型的Exchange  
  
        String queueName = "queue_topic_logs1";// 定义队列名为“queue_topic_logs1”的Queue  
        channel.queueDeclare(queueName, false, false, false, null);  
//      String routingKeyOne = "*.error.two";// "error"路由规则  
//      channel.queueBind(queueName, EXCHANGE_NAME, routingKeyOne);// 把Queue、Exchange及路由绑定  
        String routingKeyTwo = "logs.*.one";//通配所有logs下第三词(最后一个)词为one的消息  
        channel.queueBind(queueName, EXCHANGE_NAME, routingKeyTwo);  
  
        System.out.println(" [*] Waiting for messages.");  
  
        QueueingConsumer consumer = new QueueingConsumer(channel);  
        channel.basicConsume(queueName, true, consumer);  
  
        while (true) {  
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();  
            String message = new String(delivery.getBody());  
            String routingKey = delivery.getEnvelope().getRoutingKey();  
  
            System.out.println(" [x] Received '" + routingKey + "':'" + message  
                    + "'");  
        }  
    }  
}  

第二个C端的程序如下: 

package com.abin.rabbitmq;  
  
import com.rabbitmq.client.Channel;  
import com.rabbitmq.client.Connection;  
import com.rabbitmq.client.ConnectionFactory;  
import com.rabbitmq.client.QueueingConsumer;  
  
public class ReceiveLogsTopicTwo {  
    private static final String EXCHANGE_NAME = "topic_logs";//定义Exchange名称  
  
    public static void main(String[] argv) throws Exception {  
  
        ConnectionFactory factory = new ConnectionFactory();  
        factory.setHost("localhost");  
        Connection connection = factory.newConnection();  
        Channel channel = connection.createChannel();  
  
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");//声明topic类型的Exchange  
  
        String queueName = "queue_topic_logs2";//定义队列名为“queue_topic_logs2”的Queue  
        channel.queueDeclare(queueName, false, false, false, null);  
        String routingKeyOne = "logs.#";//通配所有logs下的消息  
        channel.queueBind(queueName, EXCHANGE_NAME, routingKeyOne);//把Queue、Exchange及路由绑定  
  
        System.out.println(" [*] Waiting for messages.");  
  
        QueueingConsumer consumer = new QueueingConsumer(channel);  
        channel.basicConsume(queueName, true, consumer);  
  
        while (true) {  
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();  
            String message = new String(delivery.getBody());  
            String routingKey = delivery.getEnvelope().getRoutingKey();  
  
            System.out.println(" [x] Received '" + routingKey + "':'" + message  
                    + "'");  
        }  
    }  
}  

第二个C端的运行结果如下:

[*] Waiting for messages.  
[x] Received 'logs.error.one':'this is one error logs:0'  
[x] Received 'logs.error.one':'this is one error logs:1'  
[x] Received 'logs.error.two':'this is two error logs:0'  
[x] Received 'logs.error.two':'this is two error logs:1'  
[x] Received 'logs.error.two':'this is two error logs:2'  
[x] Received 'logs.info.one':'this is one info logs:0'  
[x] Received 'logs.info.one':'this is one info logs:1'  
[x] Received 'logs.info.one':'this is one info logs:2'  
[x] Received 'logs.info.one':'this is one info logs:3'  





































































### 实现方案 为了实现比较两个PDF文件的不同之处并将差异输出到新的PDF文件中,可以采用如下方式: #### 使用 `PyPDF2` 提取文本和元数据 `PyPDF2` 可以用来读取PDF文档的内容以及提取页面上的文字信息[^1]。 ```python import PyPDF2 def extract_text_from_pdf(file_path): pdf_reader = PyPDF2.PdfReader(file_path) text = "" for page_num in range(len(pdf_reader.pages)): page = pdf_reader.pages[page_num] text += page.extract_text() return text ``` #### 利用 `diff-pdf` 进行视觉化差异检测 `diff-pdf` 工具专注于找出两份PDF之间的可视区别,并可通过命令行调用生成带有标注的对比结果。 由于 `diff-pdf` 并不是一个纯Python库而是外部工具,因此需要安装该软件并通过子进程接口来执行其命令。下面是一个简单的例子展示如何通过 Python 调用 `diff-pdf` 来创建一个包含差异的新 PDF 文件: ```python import subprocess def compare_pdfs_with_diff_pdf(original, modified, output="output.pdf"): command = ["diff-pdf", "--to", output, original, modified] result = subprocess.run(command, capture_output=True, text=True) if result.returncode != 0: raise Exception(f"Error running diff-pdf: {result.stderr}") print("Comparison completed successfully.") ``` 请注意,在实际环境中运行上述代码之前,需确保已正确安装了 `diff-pdf` 命令行工具并且可以从系统的 PATH 中访问它。 #### 将两者结合起来完成整个流程 结合以上两种技术手段,完整的解决方案可能涉及以下几个方面的工作: - 预处理阶段:利用 `PyPDF2` 对输入的 PDF 文档进行初步解析; - 执行差异分析:借助于 `diff-pdf` 的能力识别出具体的变动位置; - 后期处理:根据需求调整最终报告的形式,比如保存为 HTML 或者直接嵌入到另一个 PDF 当中。 最后一步取决于具体应用场景的要求;如果目标是生成一份易于阅读的人类友好的报告,则可以选择将变化部分导出为带颜色标记的HTML页面或是重新构建一个新的PDF文档。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小黑师傅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值