1. 交换路由管理

客户端将消息发布到指定的交换机,交换机这时候要考虑这条数据该放入到哪些与自己绑定的队列中,而这个考量是通过交换机类型以及匹配规则来决定的:
- 广播交换:直接将消息交给所有绑定的队列,无需匹配
- 直接交换:队列绑定信息中的 binding_key 与消息中的 routing_key 一致则匹配成功,否则失败。
- 主题交换:只有匹配队列主题的消息才会被放入队列中
其中广播交换和直接交换,都非常简单,唯一较为难以理解的是主题交换。
在这里我们需要先对 binding_key 和 routing_key 作以了解:
binding_key
是由数字字母下划线构成的, 并且使用 . 分成若干部分,并支持 * 和 # 通配符。
例如:news.music.#,这用于表示交换机绑定的当前队列是一个用于发布音乐新闻的队列。
- 支持 * 和 # 两种通配符, 但是 * # 只能作为 . 切分出来的独立部分, 不能和其他数字字母混用,
- 比如
a.*.b是合法的,a.*a.b是不合法的 - * 可以匹配任意一个单词(注意是单词不是字母)
-
可以匹配零个或者多个任意单词(注意是单词不是字母)
- 比如
- 注意事项:
a.#.b- 一个单词中不能既出现 * 又出现 #, 也就是,一个单词中只能有一个通配符,且必须独立存在
- #通配符两边不能出现其他通配符,因为 # 可以匹配任意多个任意单词,因此连续出现是没有意义的。
routing_key
是由数据、字母和下划线构成, 并且可以使用 . 划分成若干部分。
例如:news.music.pop,这用于表示当前发布的消息是一个流行音乐的新闻.
比如,在进行队列绑定时,某队列的 binding_key 约定为:news.music.#表示这个队列用于发布音乐新闻。而这时候客户端发布了一条消息,其中 routing_key 为: news.music.pop 则可以匹配成功, 而如果发布消息的 routing_key 为:news.sport.football,这时候就会匹配失败。

cpp
#ifndef __M_ROUTE_H__
#define __M_ROUTE_H__
#include <iostream>
#include "../mqcommon/logger.hpp"
#include "../mqcommon/helper.hpp"
#include "../mqcommon/msg.pb.h"
namespace rabbitmq
{
class Router
{
public:
static bool isLegalRoutingKey(const std::string &routing_key)
{
//routing_key:只需要判断是否包含有非法字符即可, 合法字符( a~z, A~Z, 0~9, ., _)
for (auto &ch : routing_key)
{
if ((ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
(ch == '_' || ch == '.'))
{
continue;
}
return false;
}
return true;
}
static bool isLegalBindingKey(const std::string &binding_key)
{
//1. 判断是否包含有非法字符, 合法字符:a~z, A~Z, 0~9, ., _, *, #
for (auto &ch : binding_key)
{
if ((ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
(ch == '_' || ch == '.') ||
(ch == '*' || ch == '#'))
{
continue;
}
return false;
}
//2. *和#必须独立存在: news.music#.*.#
std::vector<std::string> sub_words;
StringHelper::split(binding_key, ".", sub_words);
for(auto& word : sub_words)
{
if(word.size() > 1 &&
(word.find("*") != std::string::npos ||
word.find("#") != std::string::npos))
{
return false;
}
}
//3. *和#不能连续出现
for(int i = 1; i < sub_words.size(); i++)
{
if (sub_words[i] == "#" && sub_words[i - 1] == "*")
{
return false;
}
if (sub_words[i] == "#" && sub_words[i - 1] == "#")
{
return false;
}
if (sub_words[i] == "*" && sub_words[i - 1] == "#")
{
return false;
}
}
return true;
}
static bool route(ExchangeType type, const std::string &routing_key, const std::string &binding_key)
{
if(type == ExchangeType::DIRECT)
{
return routing_key == binding_key;
}
else if(type == ExchangeType::FANOUT)
{
return true;
}
//主题交换:要进行模式匹配 news.# & news.music.pop
//1. 将binding_key与routing_key进行字符串分割,得到各个的单词数组
std::vector<std::string> bkeys, rkeys;
int n_bkey = StringHelper::split(binding_key, ".", bkeys);
int n_rkey = StringHelper::split(routing_key, ".", rkeys);
//2. 定义标记数组,并初始化[0][0]位置为true,其他位置为false
std::vector<std::vector<bool>> dp(n_bkey + 1, std::vector<bool>(n_rkey + 1, false));
dp[0][0] = true;
//3. 如果binding_key以#起始,则将#对应行的第0列置为1.
for(int i = 1; i <= bkeys.size(); i++)
{
if (bkeys[i - 1] == "#")
{
dp[i][0] = true;
continue;
}
break;
}
//4. 使用routing_key中的每个单词与binding_key中的每个单词进行匹配并标记数组
for (int i = 1; i <= n_bkey; i++)
{
for (int j = 1; j <= n_rkey; j++)
{
//如果当前bkey是个*,或者两个单词相同,表示单词匹配成功,则从左上方继承结果
if (bkeys[i - 1] == rkeys[j - 1] || bkeys[i - 1] == "*")
{
dp[i][j] = dp[i - 1][j - 1];
}
else if (bkeys[i - 1] == "#")
{
//如果当前bkey是个#,则需要从左上,左边,上边继承结果
dp[i][j] = dp[i - 1][j - 1] || dp[i][j - 1] || dp[i - 1][j];
}
}
}
return dp[n_bkey][n_rkey];
}
};
}
#endif
2. 测试
binding_key和routing_key的合法测试以及二者的匹配测试:
cpp
#include "../mqserver/route.hpp"
#include <gtest/gtest.h>
class QueueTest : public testing::Environment
{
public:
virtual void SetUp() override {}
virtual void TearDown() override
{
//bmp->clear();
}
};
TEST(route_test, legal_routing_key)
{
std::string rkey1 = "news.music.pop";
std::string rkey2 = "news..music.pop";
std::string rkey3 = "news.,music.pop";
std::string rkey4 = "news.music_123.pop";
ASSERT_EQ(rabbitmq::Router::isLegalRoutingKey(rkey1), true);
ASSERT_EQ(rabbitmq::Router::isLegalRoutingKey(rkey2), true);
ASSERT_EQ(rabbitmq::Router::isLegalRoutingKey(rkey3), false);
ASSERT_EQ(rabbitmq::Router::isLegalRoutingKey(rkey4), true);
}
TEST(route_test, legal_binding_key)
{
std::string bkey1 = "news.music.pop";
std::string bkey2 = "news.#.music.pop";
std::string bkey3 = "news.#.*.music.pop";//非法
std::string bkey4 = "news.*.#.music.pop";//非法
std::string bkey5 = "news.#.#.music.pop";//非法
std::string bkey6 = "news.*.*.music.pop";
std::string bkey7 = "news.,music_123.pop";//非法
ASSERT_EQ(rabbitmq::Router::isLegalBindingKey(bkey1), true);
ASSERT_EQ(rabbitmq::Router::isLegalBindingKey(bkey2), true);
ASSERT_EQ(rabbitmq::Router::isLegalBindingKey(bkey3), false);
ASSERT_EQ(rabbitmq::Router::isLegalBindingKey(bkey4), false);
ASSERT_EQ(rabbitmq::Router::isLegalBindingKey(bkey5), false);
ASSERT_EQ(rabbitmq::Router::isLegalBindingKey(bkey6), true);
ASSERT_EQ(rabbitmq::Router::isLegalBindingKey(bkey7), false);
}
TEST(route_test, route)
{
// aaa aaa true
// aaa.bbb aaa.bbb true
// aaa.bbb aaa.bbb.ccc false
// aaa.bbb aaa.ccc false
// aaa.#.bbb aaa.bbb.ccc false
// aaa.bbb.# aaa.ccc.bbb false
// #.bbb.ccc aaa.bbb.ccc.ddd 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
// aaa.#.ccc.ccc aaa.bbb.ccc.ccc.ccc true
// aaa.#.bbb.*.bbb aaa.ddd.ccc.bbb.eee.bbb true
std::vector<std::string> bkeys = {
"aaa",
"aaa.bbb",
"aaa.bbb",
"aaa.bbb",
"aaa.#.bbb",
"aaa.bbb.#",
"#.bbb.ccc",
"aaa.bbb.ccc",
"aaa.*",
"aaa.*.bbb",
"*.aaa.bbb",
"#",
"aaa.#",
"aaa.#",
"aaa.#.ccc",
"aaa.#.ccc",
"aaa.#.ccc",
"#.ccc",
"#.ccc",
"aaa.#.ccc.ccc",
"aaa.#.bbb.*.bbb"
};
std::vector<std::string> rkeys = {
"aaa",
"aaa.bbb",
"aaa.bbb.ccc",
"aaa.ccc",
"aaa.bbb.ccc",
"aaa.ccc.bbb",
"aaa.bbb.ccc.ddd",
"aaa.bbb.ccc",
"aaa.bbb",
"aaa.bbb.ccc",
"aaa.bbb",
"aaa.bbb.ccc",
"aaa.bbb",
"aaa.bbb.ccc",
"aaa.ccc",
"aaa.bbb.ccc",
"aaa.aaa.bbb.ccc",
"ccc",
"aaa.bbb.ccc",
"aaa.bbb.ccc.ccc.ccc",
"aaa.ddd.ccc.bbb.eee.bbb"
};
std::vector<bool> result = {
true,
true,
false,
false,
false,
false,
false,
true,
true,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true
};
for (int i = 0; i < bkeys.size(); i++)
{
ASSERT_EQ(rabbitmq::Router::route(rabbitmq::ExchangeType::TOPIC, rkeys[i], bkeys[i]), result[i]);
}
}
int main(int argc,char *argv[])
{
testing::InitGoogleTest(&argc, argv);
testing::AddGlobalTestEnvironment(new QueueTest);
return RUN_ALL_TESTS();
}
运行结果:
bash
ltx@My-Xshell-8-Pro-Max-Ultra:~/rabbit-mq/mqtest$ make
g++ -std=c++11 mq_routetest.cpp ../mqcommon/msg.pb.cc -o mq_routetest -lgtest /usr/local/lib/libprotobuf.so.32 -lsqlite3
ltx@My-Xshell-8-Pro-Max-Ultra:~/rabbit-mq/mqtest$ ./mq_routetest
[==========] Running 3 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 3 tests from route_test
[ RUN ] route_test.legal_routing_key
[ OK ] route_test.legal_routing_key (0 ms)
[ RUN ] route_test.legal_binding_key
[ OK ] route_test.legal_binding_key (0 ms)
[ RUN ] route_test.route
[ OK ] route_test.route (0 ms)
[----------] 3 tests from route_test (0 ms total)
[----------] Global test environment tear-down
[==========] 3 tests from 1 test suite ran. (0 ms total)
[ PASSED ] 3 tests.