一.路由匹配模块介绍
路由匹配模块可以验证路由键(routing key
)和绑定键(binding key
)的合法性,并根据不同的交换机类型(如Direct
、Fanout
和Topic
)进行消息的路由匹配。
二.Routine类的实现
设计思路
在消息队列系统中,消息路由是一个关键的功能。
交换机根据路由键将消息发送到与其匹配的队列,而不同类型的交换机有不同的路由规则。Routine
类的设计目标是处理这些规则:
- 合法性校验:验证路由键和绑定键的字符规则是否符合预期。例如,routing_key只允许字母、数字、点和下划线,而binding_key还允许通配符(
*
和#
)。 - 路由匹配算法:根据交换机类型,确定routing_key和binding_key是否匹配,特别是在
Topic
类型的交换机中,复杂的匹配规则需要使用动态规划来实现
代码细节分析
合法性校验:
routineKeyValid()
方法用于验证路由键是否合法,允许的字符包括字母、数字、点和下划线。bindingKeyValid()
方法则允许更多字符,包括通配符*
和#
,并对绑定键的格式进行了更严格的检查。例如,*
和#
必须独立作为单词出现,且#
不能和其他通配符同时出现。
static bool routineKeyValid(const std::string &rkey){// 包含的字符规则: 0-9,a-z,A-Z, . _for (auto &ch : rkey){if (ch >= '0' && ch <= '9' ||ch >= 'a' && ch <= 'z' ||ch >= 'A' && ch <= 'Z' ||ch == '.' || ch == '_')continue;elsereturn false;}return true;}static bool bindingKeyValid(const std::string &bkey){// 1. 包含的字符规则 0-9,a-z,A-Z, . _ * #for (auto &ch : bkey){if (ch >= '0' && ch <= '9' ||ch >= 'a' && ch <= 'z' ||ch >= 'A' && ch <= 'Z' ||ch == '.' || ch == '_' ||ch == '*' || ch == '#')continue;elsereturn false;}// 2. 单词直接以.作为分隔符,#和* 必须独立作为单词出现std::vector<std::string> words;StrHelper::split(bkey, ".", words);for (auto &word : words){if (word.size() > 1 &&(word.find('*') != std::string::npos ||word.find('#') != std::string::npos))return false;}// 3. #可以匹配0-n个单词,即#前后不能有*和#的通配符for (int i = 1; i < words.size(); ++i){if (words[i] == "#" && words[i - 1] == "#")return false;else if (words[i] == "#" && words[i - 1] == "*")return false;else if (words[i] == "*" && words[i - 1] == "#")return false;}return true;}
路由匹配:
route()
方法根据不同的交换机类型决定是否匹配成功:
DIRECT类型:路由键和绑定键必须完全相等。
FANOUT类型:无论路由键如何,都会匹配成功。
TOPIC类型:使用动态规划(DP)方法实现复杂的通配符匹配逻辑。
首先将路由键和绑定键按.
分隔为多个单词,然后通过DP表逐步匹配各个单词。
如果绑定键包含#
或*
,则会根据通配符的规则处理。
动态规划的应用:
DP表的初始化确保了在开始时没有匹配任何字符时(dp[0][0]
)的正确性。如果绑定键的第一个字符是#
,那么也允许匹配空路由键(dp[1][0]
)。
在填充DP表时,#
可以匹配零个或多个单词,而*
则只能匹配一个单词。这种设计有效地解决了通配符的匹配问题。
实现总结
Routine
类处理了不同类型的消息路由需求,特别是在处理Topic
类型的复杂路由规则时,使用了动态规划来高效地完成匹配。
三.全部代码
#pragma once
#include "../common_mq/helper.hpp"
#include "../common_mq/logger.hpp"
#include "../common_mq/msg.pb.h"namespace mq
{class Routine{public:static bool routineKeyValid(const std::string &rkey){// 包含的字符规则: 0-9,a-z,A-Z, . _for (auto &ch : rkey){if (ch >= '0' && ch <= '9' ||ch >= 'a' && ch <= 'z' ||ch >= 'A' && ch <= 'Z' ||ch == '.' || ch == '_')continue;elsereturn false;}return true;}static bool bindingKeyValid(const std::string &bkey){// 1. 包含的字符规则 0-9,a-z,A-Z, . _ * #for (auto &ch : bkey){if (ch >= '0' && ch <= '9' ||ch >= 'a' && ch <= 'z' ||ch >= 'A' && ch <= 'Z' ||ch == '.' || ch == '_' ||ch == '*' || ch == '#')continue;elsereturn false;}// 2. 单词直接以.作为分隔符,#和* 必须独立作为单词出现std::vector<std::string> words;StrHelper::split(bkey, ".", words);for (auto &word : words){if (word.size() > 1 &&(word.find('*') != std::string::npos ||word.find('#') != std::string::npos))return false;}// 3. #可以匹配0-n个单词,即#前后不能有*和#的通配符for (int i = 1; i < words.size(); ++i){if (words[i] == "#" && words[i - 1] == "#")return false;else if (words[i] == "#" && words[i - 1] == "*")return false;else if (words[i] == "*" && words[i - 1] == "#")return false;}return true;}// 路由匹配是否成功static bool route(msg::ExchangeType type, const std::string &rkey, const std::string &bkey){if (type == msg::ExchangeType::DIRECT)return rkey == bkey;else if (type == msg::ExchangeType::FANOUT)return true;else//TOPIC{// 1.字符串分别以.为分隔符切分std::vector<std::string> rkeys;size_t routine_sz = StrHelper::split(rkey, ".", rkeys);std::vector<std::string> bkeys;size_t bind_sz = StrHelper::split(bkey, ".", bkeys);// 2.创建dp表std::vector<std::vector<bool>> dp(bind_sz+1,std::vector<bool>(routine_sz+1,false));// 3.初始化dp表// dp[0][0]=true// binding_key起始字符为0时,dp[1][0]=truedp[0][0]=true;if(bind_sz>0 && bkeys[0] == "#")dp[1][0] = true;// 4.填充dp表for(int i=1;i<=bind_sz;i++){for(int j=1;j<=routine_sz;j++){if(bkeys[i-1] == "#")dp[i][j] = dp[i-1][j-1] | dp[i][j-1] | dp[i-1][j];else if(bkeys[i-1] == "*" || bkeys[i-1] == rkeys[j-1])dp[i][j] = dp[i-1][j-1];}}// 5.返回最后一个元素return dp[bind_sz][routine_sz];}}};
};