news 2025/12/25 1:10:11

cpp-httplib:路径参数解析类剖析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
cpp-httplib:路径参数解析类剖析

文章目录

  • 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&param_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_的内容依次是,可以理解为非变量,此部分是

  1. /api/v1/users/
  2. /books/
  3. /chapter

可以把静态片段数组理解为非变量,此部分是固定的

拆完以后:
参数名数组param_names_的内容依次是

  1. "id"
  2. "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&param_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)&&params["id"]=="123");TEST("id match /123/",id.match("/123/",params)&&params["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)&&params["id"]=="42"&&params["filename"]=="report.pdf");TEST("file match /42/file/report.pdf/",file.match("/42/file/report.pdf/",params)&&params["id"]=="42"&&params["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)&&params["id"]=="42"&&params["dirname"]=="testdir"&&params["filename"]=="report.pdf");TEST("files match /42/dir/testdir/file/report.pdf/",files.match("/42/dir/testdir/file/report.pdf/",params)&&params["id"]=="42"&&params["dirname"]=="testdir"&&params["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!
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/13 21:30:48

python学习第6天

generator 生成器&#xff1a; 如果列表元素可以按照某种算法推算出来&#xff0c;&#xff0c;&#xff0c;不必创建完整的list&#xff0c;&#xff0c;一边循环一边计算&#xff0c;&#xff0c;&#xff0c;就叫生成器,generator 生成式&#xff1a;一次性全部打印出来 ne…

作者头像 李华
网站建设 2025/12/13 21:26:18

Postman + DeepSeek:接口测试效率革命 - 自动化用例生成与断言编写

Postman DeepSeek&#xff1a;接口测试效率革命 - 自动化用例生成与断言编写引言&#xff1a;接口测试的现状与挑战在当今快速迭代的软件开发环境中&#xff0c;应用程序编程接口已成为系统间通信和数据交换的基石。无论是微服务架构、前后端分离&#xff0c;还是与第三方服务…

作者头像 李华
网站建设 2025/12/18 16:08:35

ECS系统入门手记——其一

观前须知 ECS是一种用于处理大量运算&#xff0c;性能极高的架构&#xff0c;在某些特定的情况下可能发挥很大作用&#xff0c;由于所蕴含的知识很多&#xff0c;而我只粗学了10多个小时&#xff0c;某些地方可能会有纰漏&#xff0c;看不懂或者讲错了直接喷 注:需要导入的一些…

作者头像 李华
网站建设 2025/12/16 20:02:00

JAVA 中dao层的实体应该属于哪个层次VO,还是DTO,或者其他

在Java项目中&#xff0c;DAO层的实体通常指的是PO&#xff08;Persistent Object&#xff0c;持久化对象&#xff09;​ 或Entity&#xff08;实体类&#xff09;&#xff0c;而不是DTO或VO。以下是详细解释&#xff1a;1. PO&#xff08;持久化对象&#xff09; / Entity与数…

作者头像 李华
网站建设 2025/12/13 21:08:42

AutoGLM 旧安卓一键变 AI 手机:安装与使用指南

&#x1f4f1; AutoGLM: 让你的旧安卓手机秒变 AI 代理&#xff01;&#x1f680; 本文将为您提供一份“保姆级”指南&#xff0c;详细介绍如何安装、配置并使用 Open-AutoGLM&#xff0c;将您的安卓手机&#xff08;Android 7.0&#xff09;轻松变为一个强大的 AI 代理。 1️…

作者头像 李华