文章目录
- MatcherBase
- PathParamsMatcher
- 构造函数
- match
- 实现一个自己的路径参数提取
- 测试
在Java里,springboot能实现如下代码:
@GetMapping("/user/{userId}/")publicUsergetuser(@PathVariableintuserId){returnuserMapper.selectById(userId);}即可获取用户路径参数,在cpp-httplib开源库中也有这个功能,在该库中,叫macher,一共实现了两个匹配器,本文聚焦于PathParamsMatcher,源码如下:
classMatcherBase{public:MatcherBase(std::string pattern):pattern_(std::move(pattern)){}virtual~MatcherBase()=default;conststd::string&pattern()const{returnpattern_;}// Match request path and populate its matches andvirtualboolmatch(Request&request)const=0;private:std::string pattern_;};/** * Captures parameters in request path and stores them in Request::path_params * * Capture name is a substring of a pattern from : to /. * The rest of the pattern is matched against the request path directly * Parameters are captured starting from the next character after * the end of the last matched static pattern fragment until the next /. * * Example pattern: * "/path/fragments/:capture/more/fragments/:second_capture" * Static fragments: * "/path/fragments/", "more/fragments/" * * Given the following request path: * "/path/fragments/:1/more/fragments/:2" * the resulting capture will be * {{"capture", "1"}, {"second_capture", "2"}} */classPathParamsMatcherfinal:publicMatcherBase{public:PathParamsMatcher(conststd::string&pattern);boolmatch(Request&request)constoverride;private:// Treat segment separators as the end of path parameter capture// Does not need to handle query parameters as they are parsed before path// matchingstaticconstexprcharseparator='/';// Contains static path fragments to match against, excluding the '/' after// path params// Fragments are separated by path paramsstd::vector<std::string>static_fragments_;// Stores the names of the path parameters to be used as keys in the// Request::path_params mapstd::vector<std::string>param_names_;};inlinePathParamsMatcher::PathParamsMatcher(conststd::string&pattern):MatcherBase(pattern){constexprconstcharmarker[]="/:";// One past the last ending position of a path param substringstd::size_t last_param_end=0;#ifndefCPPHTTPLIB_NO_EXCEPTIONS// Needed to ensure that parameter names are unique during matcher// construction// If exceptions are disabled, only last duplicate path// parameter will be setstd::unordered_set<std::string>param_name_set;#endifwhile(true){constautomarker_pos=pattern.find(marker,last_param_end==0?last_param_end:last_param_end-1);if(marker_pos==std::string::npos){break;}static_fragments_.push_back(pattern.substr(last_param_end,marker_pos-last_param_end+1));constautoparam_name_start=marker_pos+str_len(marker);autosep_pos=pattern.find(separator,param_name_start);if(sep_pos==std::string::npos){sep_pos=pattern.length();}autoparam_name=pattern.substr(param_name_start,sep_pos-param_name_start);#ifndefCPPHTTPLIB_NO_EXCEPTIONSif(param_name_set.find(param_name)!=param_name_set.cend()){std::string msg="Encountered path parameter '"+param_name+"' multiple times in route pattern '"+pattern+"'.";throwstd::invalid_argument(msg);}#endifparam_names_.push_back(std::move(param_name));last_param_end=sep_pos+1;}if(last_param_end<pattern.length()){static_fragments_.push_back(pattern.substr(last_param_end));}}inlineboolPathParamsMatcher::match(Request&request)const{request.matches=std::smatch();request.path_params.clear();request.path_params.reserve(param_names_.size());// One past the position at which the path matched the pattern last timestd::size_t starting_pos=0;for(size_t i=0;i<static_fragments_.size();++i){constauto&fragment=static_fragments_[i];if(starting_pos+fragment.length()>request.path.length()){returnfalse;}// Avoid unnecessary allocation by using strncmp instead of substr +// comparisonif(std::strncmp(request.path.c_str()+starting_pos,fragment.c_str(),fragment.length())!=0){returnfalse;}starting_pos+=fragment.length();// Should only happen when we have a static fragment after a param// Example: '/users/:id/subscriptions'// The 'subscriptions' fragment here does not have a corresponding paramif(i>=param_names_.size()){continue;}autosep_pos=request.path.find(separator,starting_pos);if(sep_pos==std::string::npos){sep_pos=request.path.length();}constauto¶m_name=param_names_[i];request.path_params.emplace(param_name,request.path.substr(starting_pos,sep_pos-starting_pos));// Mark everything up to '/' as matchedstarting_pos=sep_pos+1;}// Returns false if the path is longer than the patternreturnstarting_pos>=request.path.length();}100行即可实现优雅的参数提取,用法如下:
svr.Get("/user/:userId/",[](consthttplib::Request&req,httplib::Response&res){autouserId=req.path_params.at("userId");res.set_content("User ID: "+userId,"text/plain");});MatcherBase
- 定义接口
- 保存占位符,例如/user/:id,会保存
:id
PathParamsMatcher
构造函数:把路径拆成“静态片段数组”+“参数名数组”
match接口:用静态片段做“锚点”,把两段锚点之间的子串当成参数值,塞进request.path_params
路径:/api/v1/users/:id/books/:isbn/chapter
拆完以后:
静态片段数组static_fragments_的内容依次是,可以理解为非变量,此部分是
/api/v1/users//books//chapter
可以把静态片段数组理解为非变量,此部分是固定的
拆完以后:
参数名数组param_names_的内容依次是
"id""isbn"
可以把参数名数组理解为非变量,此部分是根据不同的用户进行变更的
构造函数
constexprconstcharmarker[]="/:";// constexpr const char marker[] = "/:";定义匹配方式,后续代码用这个找出变量数组// 注意:已经声明了constexpr,marker已经是编译期常量,不需要再加const,不过无所谓std::size_t last_param_end=0;// 上次匹配的下标while(true){// code..}// 不断匹配constautomarker_pos=pattern.find(marker,last_param_end==0?last_param_end:last_param_end-1);if(marker_pos==std::string::npos){break;}// 开始在路径里查找标记,如果是第一次匹配,则从0开始,不然从上次的前一个下标开始// 第一次匹配last_param_end为0// 如果没有找到,则跳出循环static_fragments_.push_back(pattern.substr(last_param_end,marker_pos-last_param_end+1));// 裁剪从上次匹配的下标开始的字符串,字符串的长度为:查找到的新一处的标记的下标 - 上次匹配的下标 + 1// 也就是裁剪区间:[上次匹配的下标,查找到的新一处的标记的下标]// 第一次运行的话,上次匹配的下标为0,查找到的新一处的标记的下标为x,则中间都是静态数组// 例如:/api/v1/users/:id/books/:isbn/chapter// 则last_param_end == 0,marker_pos == 12(users后面的:/)// 此时会裁剪出/api/v1/users,存放到静态数组里// /:id/user// -> static_fragments[0] == '/';constautoparam_name_start=marker_pos+str_len(marker);// 查找到的新一处的标记的下标 + 标记的长度就是占位符起始下标autosep_pos=pattern.find(separator,param_name_start);if(sep_pos==std::string::npos){sep_pos=pattern.length();}// 注:separator为"/"// 在从参数名开始,路径里查找/// 如果没有找到,说明参数名就是路径的最后一节,则sep_pos更改为路径尾// 如果找到了,说明参数名是路径里中间一节,后面还有静态节autoparam_name=pattern.substr(param_name_start,sep_pos-param_name_start);// 裁剪字符串,字符串从参数名开始,长度为分割符 - 参数名// 也就是裁剪区间,[参数名起始下标,分割符前一位]// 例如:/api/:id/123// sep_pos == 8(/)// param_name_start == 6(i)// /api/:id// param_name_start == 6(i),// sep_pos == 7(d)param_names_.push_back(std::move(param_name));last_param_end=sep_pos+1;// 把参数名存入数组// 更新上次参数尾match
std::size_t starting_pos=0;// 起点for(size_t i=0;i<static_fragments_.size();++i){// code...}// 遍历静态数组constauto&fragment=static_fragments_[i];if(starting_pos+fragment.length()>request.path.length()){returnfalse;}// 先获取当前成员// 起点 + 当前成员的长度超过了http请求的路径的长度,则说明出错了if(std::strncmp(request.path.c_str()+starting_pos,fragment.c_str(),fragment.length())!=0){returnfalse;}// 比较http路径和静态数组当前成员是否匹配,如果不匹配则表示出错了// 第一次fragement为"/"starting_pos+=fragment.length();// 跳过静态片段,接下来是参数段if(i>=param_names_.size()){continue;}// 如果当前索引超过参数格式,说明已经全匹配完毕autosep_pos=request.path.find(separator,starting_pos);if(sep_pos==std::string::npos){sep_pos=request.path.length();}// 从HTTP路径里以starting_pos为起点,开始查找分割符/// 如果没有找到,说明路径参数已经被匹配完全constauto¶m_name=param_names_[i];request.path_params.emplace(param_name,request.path.substr(starting_pos,sep_pos-starting_pos));// Mark everything up to '/' as matchedstarting_pos=sep_pos+1;// 获取参数数组的当前成员// 裁剪字符串,以starting_pos为起点,长度为sep_pos - starting_pos// 把结果存成map,key是参数名,值是从路径里裁剪出来的// 更新每次匹配的起点returnstarting_pos>=request.path.length();// 每次匹配必须完全,否则说明中间出错了实现一个自己的路径参数提取
// @author: NemaleSu// @brief: http请求路径里提取参数#pragmaonce#include<string>#include<vector>#include<unordered_map>/* * todo * add * - 非 /: 格式的占位符 * - 路径分隔符非 / */classHttpPathMatcher{public:explicitHttpPathMatcher(conststd::string&pat);boolmatch(conststd::string&path,std::unordered_map<std::string,std::string>&out)const;private:structSegment{boolis_param=false;std::string literal;std::string name;};std::vector<Segment>segments_;voidbuild(conststd::string&pat);};测试
#include<iostream>#include<string>#include<vector>#include<unordered_map>#include"httppathmatcher.h"usingnamespacestd;// 测试框架宏#defineTEST(name,expr)do{\if(!(expr)){\std::cerr<<"❌ "<<name<<" FAILED\n";\std::abort();\}else{\std::cout<<"✅ "<<name<<" PASSED\n";\}\}while(0)// 测试用例intmain(){std::unordered_map<std::string,std::string>params;// 测试根路径HttpPathMatcherroot("/");TEST("root match /",root.match("/",params));TEST("root not match /extra",!root.match("/extra",params));// 测试单参数路径HttpPathMatcherid("/:id");TEST("id match /123",id.match("/123",params)&¶ms["id"]=="123");TEST("id match /123/",id.match("/123/",params)&¶ms["id"]=="123");TEST("id not match /",!id.match("/",params));TEST("id not match /123/extra",!id.match("/123/extra",params));// 测试多参数路径HttpPathMatcherfile("/:id/file/:filename");TEST("file match /42/file/report.pdf",file.match("/42/file/report.pdf",params)&¶ms["id"]=="42"&¶ms["filename"]=="report.pdf");TEST("file match /42/file/report.pdf/",file.match("/42/file/report.pdf/",params)&¶ms["id"]=="42"&¶ms["filename"]=="report.pdf");TEST("file not match /42/file",!file.match("/42/file",params));TEST("file not match /42/file/",!file.match("/42/file/",params));// 测试多段参数路径HttpPathMatcherfiles("/:id/dir/:dirname/file/:filename");TEST("files match /42/dir/testdir/file/report.pdf",files.match("/42/dir/testdir/file/report.pdf",params)&¶ms["id"]=="42"&¶ms["dirname"]=="testdir"&¶ms["filename"]=="report.pdf");TEST("files match /42/dir/testdir/file/report.pdf/",files.match("/42/dir/testdir/file/report.pdf/",params)&¶ms["id"]=="42"&¶ms["dirname"]=="testdir"&¶ms["filename"]=="report.pdf");TEST("files not match /42/dir/file/report.pdf",!files.match("/42/dir/file/report.pdf",params));std::cout<<"\n🎉 All tests passed!\n";return0;}测试结果:
✅ root match / PASSED ✅ root not match /extra PASSED ✅idmatch /123 PASSED ✅idmatch /123/ PASSED ✅idnot match / PASSED ✅idnot match /123/extra PASSED ✅filematch /42/file/report.pdf PASSED ✅filematch /42/file/report.pdf/ PASSED ✅filenot match /42/file PASSED ✅filenot match /42/file/ PASSED ✅ files match /42/dir/testdir/file/report.pdf PASSED ✅ files match /42/dir/testdir/file/report.pdf/ PASSED ✅ files not match /42/dir/file/report.pdf PASSED 🎉 All tests passed!