目录
第一节:钥匙构建规则
1-1.routing_key
1-2.binding_key
第二节:代码实现
2-1.routing_key的检查
2-2.binding_key的检查
2-3.routing_key和binding_key的匹配
第三节:单元测试
下期预告:
Router在mqserver目录下实现
第一节:钥匙构建规则
1-1.routing_key
routing_key的规则是只含有字符'0'~'9'、小写字母、大写字母、'_'、'.'。
其中'.'作为分隔符,进行匹配时,会以'.'为界限将routing_key分割成多个小块,同时省略空串。例如"news.music.pop"和"...news.music...pop"都会被分割成{"news","music","pop"}。
1-2.binding_key
binding_key的规则是只含有字符'0'~'9'、小写字母、大写字母、'_'、'.',外加'#'和'*'。
其中'.'作为分隔符,作用和routing_key中一样。
'#'和'*'都是通配符,"#"是万能通配符,它可以表示零个或多个单词,例如:
"#"和"news.music.pop"就可以匹配成功;
"#.music.pop"和"news.music.pop"也可以匹配成功;
'*'是一般通配符,它只能表示零个或者一个单词。
其次,通配符必须独立存在,例如:
"news.#123.pop"和"news.*123.pop"都是不合法的。
最后,'#'通配符的两边不能直接相邻其他通配符,例如:
"##"、"#*"、"*#"都是不合法的。
第二节:代码实现
首先做好前置工作:
#ifndef __M_ROUTE_H__ #define __M_ROUTE_H__#include "../mqcommon/mq_logger.hpp" #include "../mqcommon/mq_helper.hpp" #include "../mqcommon/mq_msg.pb.h"namespace zd {class Router{}; };#endif
2-1.routing_key的检查
根据它的构建规则,遍历routing_key,发现非法字符就返回false:
// 验证routing_key的合法性static bool isLegalRoutingkey(const std::string& routing_key){// 1.每个字符合法for(const auto& ch:routing_key){if((ch >= 'a' && ch <= 'z')||(ch >= 'A' && ch <= 'Z')||(ch >= '0' && ch <= '9')||(ch == '_' || ch == '.')){continue;}else{LOG("routing_key 检测到非法字符 %c",ch);return false;}}return true;}
2-2.binding_key的检查
也是先遍历检查是否含有非法字符。
然后把它按照"."进行分割,遍历每个单词,检查通配符是否独立存在。
最后再次遍历binding_key,遇到通配符'#'就检查它是否相邻了其他通配符。
// 验证binding_key合法性static bool isLegalBindingkey(const std::string& binding_key){// 1.每个字符合法for(const auto& ch:binding_key){if((ch >= 'a' && ch <= 'z')||(ch >= 'A' && ch <= 'Z')||(ch >= '0' && ch <= '9')||(ch == '_' || ch == '.')||(ch == '*' || ch == '#')){continue;}else{LOG("binding_key 检测到非法字符 %c",ch);return false;}}// 2.*和#必须独立存在std::vector<std::string> sub_words;StrHelper::split(binding_key,".",sub_words);for(std::string& word:sub_words){if(word.size() > 1 && (word.find("#") != std::string::npos || word.find("*") != std::string::npos)){LOG("通配符的错误使用,通配符没有独立存在");return false;}}// 3.#两边不能有通配符for(int i = 1;i < sub_words.size();i++){if(sub_words[i] == "#" && sub_words[i-1] == "*"){LOG("通配符的错误使用,#前有*");return false;}if(sub_words[i] == "#" && sub_words[i-1] == "#"){LOG("通配符的错误使用,#前有#");return false;}if(sub_words[i] == "*" && sub_words[i-1] == "#"){LOG("通配符的错误使用,#后有*");return false;}}return true;}
2-3.routing_key和binding_key的匹配
如果交换机是直接模式,那么routing_key和binding_key相同才成功。
如果交换机是广播模式,那么与交换机有绑定的队列都能匹配成功,与routing_key和binding_key的内容无关。
如果交换机是主题模式,那么routing_key和binding_key需要按照一定的匹配规则,匹配正确才成功。
static bool route(ExchangeType type,const std::string& routing_key,const std::string& binding_key){switch (type){// 直接:相同才合法case ExchangeType::DIRECT:return routing_key == binding_key;break;// 广播:有绑定就合法case ExchangeType::FANOUT:return true;break;// 主题:匹配成功才合法case ExchangeType::TOPIC:return Topic(routing_key,binding_key);break;default:LOG("非法的匹配模式");return false;}}
其中主题模式的匹配规则比较复杂,把它实现成一个私有静态的TOPIC()接口。
匹配思想:
(1)不含有'#'通配符:
使用二维数组,对两个钥匙分割好的单词进行一一配对,匹配成功的话就从左上角继承结果,失败就设置为0。
最右下角保存的值就是结果。
因为思想是从左上角继承,所以行和列都需要多一行,并把[0][0]位置设置成1,否则第一行的数据就需要特殊处理了:
(2)若'#'在中间:
两个钥匙配对应该是成功的,但是结果是false,说明遇到'#'通配符时,不仅要从左上角继承,还要从上面继承结果:
(3)若#在最前面
上述匹配也应该是成功的,结果却相反,说明遇到'#'时,不仅要从左上、上面继承,还要从左边继承:
(4)若#在最前面,且表示零个单词
![]()
也是应该成功的匹配返回了一个false,所以如果#在最前面,还要把第一行的第零列的位置设置成true:
搞清楚#的4种情况之后,就可以开始编写代码了:
private:static bool Topic(const std::string& routing_key,const std::string& binding_key){std::vector<std::string> sub_r_words;std::vector<std::string> sub_b_words;StrHelper::split(routing_key,".",sub_r_words);StrHelper::split(binding_key,".",sub_b_words);size_t m = sub_r_words.size();size_t n = sub_b_words.size();std::vector<std::vector<bool>> dp(n+1,std::vector<bool>(m+1));dp[0][0] = true;// 如果binding_key以"#"作为起始if(sub_b_words.size() > 0 && sub_b_words[0] == "#")dp[1][0]=true;for(int i = 1;i <= n;i++) {for(int j = 1;j<=m;j++){// 遇到"#"通配符if(sub_b_words[i-1] == "#"){dp[i][j] = dp[i-1][j-1] || dp[i][j-1] || dp[i-1][j];continue;}if(sub_b_words[i-1] == "*" || sub_b_words[i-1] == sub_r_words[j-1]){dp[i][j] = dp[i-1][j-1];continue;}}}return dp[n][m];}
第三节:单元测试
在mqtest下创建mq_route_test.cc。
使用以下代码进行测试:
#include "../mqserver/mq_route.hpp" #include <gtest/gtest.h> #include <iostream> #include <unordered_map>// 全局测试套件------------------------------------------------ // 自己初始化自己的环境,使不同单元测试之间解耦 class RouterTest :public testing::Environment { public:// 全部单元测试之前调用一次virtual void SetUp() override{// std::cout << "单元测试执行前的环境初始化" << std::endl;} // 全部单元测试之后调用一次virtual void TearDown() override{// std::cout << "单元测试执行后的环境清理" << std::endl;} }; // 单元测试 // 测试名称与类名称相同,则会先调用SetUp TEST(RouterTest,RouterTest_test1_Test) {// 测试routing_key合法性检测std::cout << "单元测试-1" << std::endl;ASSERT_EQ(zd::Router::isLegalRoutingkey("news.music.pop.."),true);ASSERT_EQ(zd::Router::isLegalRoutingkey("news.music....pop.."),true);ASSERT_EQ(zd::Router::isLegalRoutingkey("....news.music.pop.."),true);ASSERT_EQ(zd::Router::isLegalRoutingkey("news.music.pop..#"),false);ASSERT_EQ(zd::Router::isLegalRoutingkey("news.music.pop..@"),false);ASSERT_EQ(zd::Router::isLegalRoutingkey("news.music.pop..*"),false); }TEST(RouterTest,RouterTest_test2_Test) {// 测试binding_key合法性检测std::cout << "单元测试-2" << std::endl;ASSERT_EQ(zd::Router::isLegalBindingkey("news.music.pop.."),true);ASSERT_EQ(zd::Router::isLegalBindingkey("news.music....pop.."),true);ASSERT_EQ(zd::Router::isLegalBindingkey("....news.music.pop.."),true);ASSERT_EQ(zd::Router::isLegalBindingkey("news.music.pop..#"),true);ASSERT_EQ(zd::Router::isLegalBindingkey("news.*.*"),true);ASSERT_EQ(zd::Router::isLegalBindingkey("news.music.p&op"),false);ASSERT_EQ(zd::Router::isLegalBindingkey("news.#music.pop"),false);ASSERT_EQ(zd::Router::isLegalBindingkey("news.*music.pop"),false);ASSERT_EQ(zd::Router::isLegalBindingkey("news.##.music.pop"),false);ASSERT_EQ(zd::Router::isLegalBindingkey("news.#*.music.pop"),false);ASSERT_EQ(zd::Router::isLegalBindingkey("news.*#.music.pop"),false); }TEST(RouterTest,RouterTest_test3_Test) {// routing_key 和 binding_key 的匹配测试std::cout << "单元测试-3" << std::endl; ASSERT_EQ(zd::Router::route(zd::ExchangeType::TOPIC,"news.music.pop","#"),true);ASSERT_EQ(zd::Router::route(zd::ExchangeType::TOPIC,"news.music.pop","news.music.#"),true);ASSERT_EQ(zd::Router::route(zd::ExchangeType::TOPIC,"news.music.pop","#.music.pop"),true);ASSERT_EQ(zd::Router::route(zd::ExchangeType::TOPIC,"news.music.pop","#.pop"),true);ASSERT_EQ(zd::Router::route(zd::ExchangeType::TOPIC,"news.music.pop","#.music.pep"),false);ASSERT_EQ(zd::Router::route(zd::ExchangeType::TOPIC,"news.music.pop","nows.music.pop"),false);ASSERT_EQ(zd::Router::route(zd::ExchangeType::TOPIC,"news.music.pop","news.musics.pop"),false);ASSERT_EQ(zd::Router::route(zd::ExchangeType::DIRECT,"news.music.pop","#.pop"),false);ASSERT_EQ(zd::Router::route(zd::ExchangeType::DIRECT,"news.music.pop","news.music.pop"),true); } // 单元测试全部结束后调用TearDown// ---------------------------------------------------------- int main(int argc,char** argv) {testing::InitGoogleTest(&argc,argv);testing::AddGlobalTestEnvironment(new RouterTest); // 注册Test的所有单元测试if(RUN_ALL_TESTS() != 0) // 运行所有单元测试{printf("单元测试失败!\n");}return 0; }
测试结果:
没有报错且符合预期
下期预告:
之后将实现服务器消费者模块的搭建,它的思路和消息管理模块类似,都是每个队列管理自己的消费者/订阅者,最后封装一个管理所有队列消费者的模块。