2024.2.21 模拟实现 RabbitMQ —— 实现转发规则

目录

需求分析

直接交换机(Direct )

主题交换机(Topic )

扇出交换机(Fanout )

Topic 交换机转发规则

routingKey 组成

bindingKey 组成

匹配规则

情况一

情况二

情况三

实现 Router 类

校验 bindingKey 和 routingKey

消息匹配机制

Topic 交换机匹配规则

针对 Router 单元测试


需求分析

直接交换机(Direct )

  • 通过设定 消息的 routingKey = 队列名,以此指定该消息需传给哪个队列

实例理解

  • 如下图所示,此时可直接无视绑定关系,直接从内存中拿到对应队列名的队列
  • 然后再将消息传给该队列即可


主题交换机(Topic )

  • 依据 Topic 交换机的转发规则,判定消息需传给哪些队列

扇出交换机(Fanout )

  • 给该交换机中所有绑定好的队列均传入消息

Topic 交换机转发规则

  • bindingKey:创建绑定时,给绑定指定的特殊字符串(相当于出题)
  • routingKey:发布消息时,给消息上指定的特殊字符串(相当于做答案)
  • 当 routingKey 的答案能够与 bindingKey 相对应时,便可将消息转发给该队列 

routingKey 组成

  1. 数字、字母、下划线
  2. 使用 . 将整个 routingKey 分成多个部分

实例理解

  •  aaa.bbb.ccc    合法
  • aaa.110.bbb     合法
  • aaa                   合法

bindingKey 组成

  1. 数字、字母、下划线
  2. 使用 . 将整个 bindingKey 分成多个部分
  3. 支持两种特殊的符号作为通配符( * 和 # 必须是作为被 . 分割出来的独立的部分)
  •  * ——> 可以匹配任何一个 独立的部分
  • # ——> 可以匹配任何 0 个或者多个独立的部分

实例理解

  • aaa.*.bbb       合法
  • aaa.*bb.ccc   非法

匹配规则

情况一
  • 当 bindingKey 中没有 * 或 # 这两个特殊符号时,必须要求 routingKey 和 bindingKey 一模一样,才能算匹配成功!

实例理解

  • bindingKey = aaa.bbb.ccc
  • routingKey = aaa.bbb.ccc (匹配成功)
  • routingKey = aaa.bbb.ccd (匹配失败)

注意:

  • 情况一非常类似于 Direct 交换机的转发
  • 尤其是将 bindingKey 设置成和队列名字相同,此时就完全等价于 Direct 交换机了!

情况二
  • 当 bindingKey 中有特殊符号 * 时

实例理解 

  • bindingKey = aaa.*.ccc
  • routingKey = aaa.bbb.ccc (匹配成功)
  • routingKey = aaa.b.ccc (匹配成功)
  • routingKey = aaa.b.b.ccc(匹配失败)

情况三
  • 当 bindingKey 中有特殊符号 # 时

实例理解

  • bindingKey = aaa.#.ccc
  • routingKey = aaa.bbb.ccc (匹配成功)
  • routingKey = aaa.b.b.ccc (匹配成功)
  • routingKey = aaa.ccc(匹配成功)
  • routingKey = aaa.b.b.b(匹配失败)

问题:

  • 将交换机中每个队列的 bindingKey 设置成一个 # 时,会有啥效果呢?

回答:

  • 此时,该交换机中的 全部队列 都能匹配所有的 routingKey
  • 即该交换机就相当于 Fanout 交换机了!

注意点一:

  • Direct 交换机 和 Fanout 交换机,均属于 Topic 交换机的 特例

注意点二:

  • 上述规则都是 AMQP 协议所约定的!RabbitMQ 仅仅只是实现了该规则而已!

实现 Router 类

校验 bindingKey 和 routingKey

//    bindingKey 的构造规则:
//    1、数字,字母,下划线
//    2、使用 . 分割成若干部分
//    3、允许使用 * 和 # 作为通配符,但是通配符只能作为独立的分段
    public boolean checkBindingKey(String bindingKey) {
        if(bindingKey.length() == 0) {
//            空字符串也是合法情况,比如在使用 direct / fanout 交换机的时候,bindingKey 是用不上的
//            因为我们在使用 direct 交换机时,是直接将 routingKey 作为 消息队列的名字,直接根据名字来进行匹配
//            在使用 fanout 交换机时,无需匹配,直接将该消息转给交换机中绑定的所有消息队列
            return true;
        }
//        检查字符串中不能存在非法字符
        for (int i = 0;i < bindingKey.length();i++) {
            char ch = bindingKey.charAt(i);
            if(ch >= 'A' && ch <= 'Z') {
                continue;
            }
            if(ch >= 'a' && ch <= 'z') {
                continue;
            }
            if(ch >= '0' && ch <= '9') {
                continue;
            }
            if(ch == '_' || ch == '.' || ch == '*' || ch == '#') {
                continue;
            }
            return false;
        }
//        检查 * 或者 # 是否是独立的部分
//        aaa.*.bbb 合法情况; aaa.a*.bbb 非法情况
        String[] words = bindingKey.split("\\.");
        for (String word : words) {
//            检查 word 长度 > 1 并且包含了 * 或者 #,就是非法的格式了
            if(word.length() > 1 && (word.contains("*") || word.contains("#")) ) {
                return false;
            }
        }
//        约定一下,通配符之间的相邻关系 (人为约定的)
//        为啥这么约定?因为前三种相邻的时候,实现匹配的逻辑会非常繁琐,同时功能性提升不大
//        1、 aaa.#.#.bbb 非法情况
//        2、 aaa.#.*.bbb 非法情况
//        3、 aaa.*.#.bbb 非法情况
//        4、 aaa.*.*.bbb 合法情况
        for (int i = 0;i < words.length;i++) {
//            连续两个 ##
            if(words[i].equals("#") && words[i+1].equals("#")) {
                return false;
            }
//            # *
            if(words[i].equals("#") && words[i+1].equals("*")) {
                return false;
            }
//            * #
            if(words[i].equals("*") && words[i+1].equals("#")) {
                return false;
            }
        }
        return true;
    }

//    routingKey 的构造规则
//    1、数字,字母,下划线
//    2、使用 . 分割成若干部分
    public boolean checkRoutingKey(String routingKey) {
        if(routingKey.length() == 0) {
//            空字符串,合法的情况,比如在使用 fanout 交换机的时候,routingKey 用不上,就可以设定为 ""
            return true;
        }
        for (int i = 0;i<routingKey.length();i++) {
            char ch = routingKey.charAt(i);
//            判定该字符是否是大写字母
            if(ch >= 'A' && ch <= 'Z') {
                continue;
            }
//            判定该字符是否是小写字母
            if(ch >= 'a' && ch <= 'z') {
                continue;
            }
//            判定该字符是否是阿拉伯数字
            if(ch >= '0' && ch <= '9') {
                continue;
            }
//            判定是否是 _ 或者 .
            if(ch == '_' || ch == '.') {
                continue;
            }
//            该字符,不是上述任何一种合法情况,就直接返回 false
            return false;
        }
//        把每个字符都检查过,没有遇到非法情况,此时直接返回 true
        return true;
    }

问题:

  • 观察下图,为啥 split 方法中的参数 "." 需要加两个反斜杠呢?

回答:

  • 首先 "." 在正则表达式中,是一个特殊的符号,此处是将 . 作为原始文本来进行匹配
  • 要想使用 . 的原始文本,就需要进行转义,即 在正则中使用 "\." 的方式来表示
  • 又因为在 Java 的字符串中,"\" 是一个特殊字符
  • 所以要想写入 "\." 这样的文本,又得在其前面加上一个反斜杠来进行转义,即 "\\."

消息匹配机制

//    这个方法用来判定该消息是否可以转发给这个绑定对应的队列
    public boolean route(ExchangeType exchangeType,Binding binding,Message message) throws MqException {
//        根据不同的 exhcangeType 使用不同的判定转发规则
        if(exchangeType == ExchangeType.FANOUT) {
//            如果是 fanout 类型,该交换机上绑定的所有队列都需要转发
            return true;
        }else if(exchangeType == ExchangeType.TOPIC) {
//            如果是 topic 主题交换机,规则就要更复杂一些
            return routeTopic(binding,message);
        }else {
//            其他情况是不应该存在的
            throw new MqException("[Router] 交换机类型非法! exchangeType = " + exchangeType);
        }
    }
  • 当为 Fanout 交换机时,无需匹配 bindingKey 和 routingKey,直接返回 true
  • 让该消息转发给所以绑定了 Fanout 交换机的队列
  • 当为 Topic 交换机时,则需要进行 bindingKey 和 routingKey 的匹配

Topic 交换机匹配规则

处理思路

  • 此处我们采用 双指针算法 进行匹配

  • 针对 bindingKey 的下标,判定当前下标指向部分的具体情况:
  1. 指向的是普通的字符串,此时要求和 routingKey 对应的下标指向的内容得完全一致!
  2. 指向的是 * ,此时无论  routingKey 这边指向的是啥,双方均同时下标前进
  3. 遇到了 # ,且 # 后面没有其他的内容了,直接返回 true,匹配成功!
  4. 遇到了 # ,但 # 后面还有其他的内容,拿着 # 后面的部分,去 routingKey 中查找,找到后面的部分,在 routingKey 中出现的位置,如果后面的部分,在 routingKey 中不存在,直接认为匹配失败,返回 false!如果后面的部分,在 routingKey 中存在,就将 routingKey 的箭头指向这个位置之后,然后继续往后匹配
  5. 两个箭头移动过程中,如果同时到达双方的末尾,则返回 true,如果一个箭头先到了末尾,另一个箭头还没到,则返回 false!

代码实现:

    private boolean routeTopic(Binding binding,Message message) {
//        先把这两个 key 进行切分
        String[] bindingTokens = binding.getBindingKey().split("\\.");
        String[] routingTokens = message.getRoutingKey().split("\\.");

//        引入两个下标,指向上述两个数字,初始情况下都为 0
        int bindingIndex = 0;
        int routingIndex = 0;
//        此处使用 while 更合适,每次循环,下标不一定就是 +1,不适合使用 for
        while (bindingIndex < bindingTokens.length && routingIndex < routingTokens.length) {
            if(bindingTokens[bindingIndex].equals("*")) {
//                【情况二】如果遇到 * 号直接进入下一轮,* 可以匹配到任意一个部分!
                bindingIndex++;
                routingIndex++;
                continue;
            }else if(bindingTokens[bindingIndex].equals("#")) {
//                如果遇到 # 先要看看有没有下一个位置
                bindingIndex++;
                if(bindingIndex == bindingTokens.length) {
//                    【情况三】该 # 后面没有东西了,说明此时一定能匹配成功了!
                    return true;
                }
//                # 【情况四】后面还有东西,拿着这个内容,去 routingKey 中往后找,找到对应的位置
//                findNextMatch 这个方法用来查找该部分在 routingKey 的位置,返回该下标,没找到,就返回 -1
                routingIndex = findNextMatch(routingTokens,routingIndex,bindingTokens[bindingIndex]);
                if(routingIndex == -1) {
//                    没找到匹配的结果,匹配失败
                    return false;
                }
//                找到匹配的情况,继续往后匹配
                bindingIndex++;
                routingIndex++;
            }else {
//                【情况一】如果遇到普通字符串,要求两边的内容是一样的
                if(!bindingTokens[bindingIndex].equals(routingTokens[routingIndex])) {
                    return false;
                }
                bindingIndex++;
                routingIndex++;
            }
        }
//        【情况五】判断是否是双方同时到达末尾
//        比如 aaa.bbb.ccc 和 aaa.bbb 是要匹配失败的
        if(bindingIndex == bindingTokens.length && routingIndex == routingTokens.length) {
            return true;
        }
        return false;
    }

    private int findNextMatch(String[] routingTokens, int routingIndex, String bindingToken) {
        for (int i = routingIndex; i < routingTokens.length; i++) {
            if(routingTokens[i].equals(bindingToken)) {
                return i;
            }
        }
        return -1;
    }

针对 Router 单元测试

  • 编写测试用例代码是十分重要的!
package com.example.demo;

import com.example.demo.common.MqException;
import com.example.demo.mqserver.core.Binding;
import com.example.demo.mqserver.core.ExchangeType;
import com.example.demo.mqserver.core.Message;
import com.example.demo.mqserver.core.Router;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class RouterTests {
    private Router router = new Router();
    private Binding binding = null;
    private Message message = null;

    @BeforeEach
    public void setUp() {
        binding = new Binding();
        message = new Message();
    }

    @AfterEach
    private void tearDown() {
        binding = null;
        message = null;
    }

//    【测试用例】
//    bindingKey               routingKey               result
//    aaa                      aaa                      true
//    aaa.bbb                  aaa.bbb                  true
//    aaa.bbb                  aaa.bbb.ccc              false
//    aaa.bbb                  aaa.ccc                  false
//    aaa.bbb.ccc              aaa.bbb.ccc              true
//    aaa.*                    aaa.bbb                  true
//    aaa.*.bbb                aaa.bbb.ccc              false
//    *.aaa.bbb                aaa.bbb                  false
//    #                        aaa.bbb.ccc              true
//    aaa.#                    aaa.bbb                  true
//    aaa.#                    aaa.bbb.ccc              true
//    aaa.#.ccc                aaa.ccc                  true
//    aaa.#.ccc                aaa.bbb.ccc              true
//    aaa.#.ccc                aaa.aaa.bbb.ccc          true
//    #.ccc                    ccc                      true
//    #.ccc                    aaa.bbb.ccc              true

    @Test
    public void test1() throws MqException {
        binding.setBindingKey("aaa");
        message.setRoutingKey("aaa");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test2() throws MqException {
        binding.setBindingKey("aaa.bbb");
        message.setRoutingKey("aaa.bbb");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test3() throws MqException {
        binding.setBindingKey("aaa.bbb");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertFalse(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test4() throws MqException {
        binding.setBindingKey("aaa.bbb");
        message.setRoutingKey("aaa.ccc");
        Assertions.assertFalse(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test5() throws MqException {
        binding.setBindingKey("aaa.bbb.ccc");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test6() throws MqException {
        binding.setBindingKey("aaa.*");
        message.setRoutingKey("aaa.bbb");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test7() throws MqException {
        binding.setBindingKey("aaa.*.bbb");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertFalse(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test8() throws MqException {
        binding.setBindingKey("*.aaa.bbb");
        message.setRoutingKey("aaa.bbb");
        Assertions.assertFalse(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test9() throws MqException {
        binding.setBindingKey("#");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test10() throws MqException {
        binding.setBindingKey("aaa.#");
        message.setRoutingKey("aaa.bbb");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test11() throws MqException {
        binding.setBindingKey("aaa.#");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test12() throws MqException {
        binding.setBindingKey("aaa.#.ccc");
        message.setRoutingKey("aaa.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test13() throws MqException {
        binding.setBindingKey("aaa.#.ccc");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test14() throws MqException {
        binding.setBindingKey("aaa.#.ccc");
        message.setRoutingKey("aaa.aaa.bbb.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test15() throws MqException {
        binding.setBindingKey("#.ccc");
        message.setRoutingKey("ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }

    @Test
    public void test16() throws MqException {
        binding.setBindingKey("#.ccc");
        message.setRoutingKey("aaa.bbb.ccc");
        Assertions.assertTrue(router.route(ExchangeType.TOPIC,binding,message));
    }
}
  • 27
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

茂大师

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

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

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

打赏作者

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

抵扣说明:

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

余额充值