news 2026/5/10 10:10:49

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进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/9 21:21:50

EtherCAT 逐帧报文解析:EEPROM 读取与配置阶段

主站采用 APRD&#xff08;定点读&#xff09;、APWR&#xff08;定点写&#xff09;指令替代广播指令&#xff0c;实现对单个从站的精准操作。1、APRD 定点读机制说明&#xff1a;访问特性&#xff1a;非统一批量读取&#xff0c;为定点访问模式&#xff1b;从站 adp 地址范围…

作者头像 李华
网站建设 2026/5/2 16:55:48

风-储系统仿真模型;通过模糊逻辑控制策略驱动蓄电池变换器运行,以达到为电网提供惯量的目的

风-储系统仿真模型&#xff1b;通过模糊逻辑控制策略驱动蓄电池变换器运行&#xff0c;以达到为电网提供惯量的目的。 可以实现功率平滑输出在能源转型的大背景下&#xff0c;风能作为一种重要的可再生能源&#xff0c;因其清洁、无污染的特点受到广泛关注。然而&#xff0c;风…

作者头像 李华
网站建设 2026/4/30 23:52:02

async/await 的原理

基础问答问&#xff1a;async/await 的原理是什么&#xff1f;答&#xff1a;关键字本身就是 Promise 的语法糖&#xff0c;依托于生成器函数 &#xff08;Generator&#xff09; 函数能力实现的。async 关键字标志这个函数为异步函数&#xff0c;并且将返回结果封装为一个 Pro…

作者头像 李华
网站建设 2026/4/30 23:52:01

Flutter 应用保活与后台任务:在 OpenHarmony 上实现定时上报

前言 在 OpenHarmony 生态中&#xff0c;许多应用场景&#xff08;如健康监测、设备状态上报、位置追踪&#xff09;要求应用即使在退到后台或屏幕关闭后&#xff0c;仍能周期性执行任务。然而&#xff0c;出于系统资源与电池优化的考虑&#xff0c;OpenHarmony 对后台进程有严…

作者头像 李华
网站建设 2026/5/5 19:21:27

【RL】verl 数据处理

您的 Eurus-2-RL-Data 数据集需要做两个主要适配&#xff1a;文件格式转换和字段映射配置。 快速解决方案 1. 转换文件格式&#xff08;推荐&#xff09; 将 arrow 文件转换为 parquet 格式&#xff1a; from datasets import load_dataset import os# 加载原始数据 ds lo…

作者头像 李华