多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

用 C++ 写 Web 服务实战(二):路由进阶、中间件链与文件上传

用 C++ 写 Web 服务实战(二):路由进阶、中间件链与文件上传 用 C 写 Web 服务实战二路由进阶、中间件链与文件上传上一篇我们搭起了一个能跑的 Web 应用骨架——监听端口、注册 JSON 路由、绑定静态目录、开启多事件循环、写了一个文件日志中间件。这篇在这个骨架之上继续往上盖路由参数与通配符、中间件链的深度使用、请求体流式接收与文件上传。读完这篇你的 C Web 应用就能处理真实业务了。一、路由进阶不止是app.get(/hello)上篇只用了最简单的精确匹配路由。实际业务中你需要从 URL 中提取参数、匹配前缀、区分 HTTP 方法。uvcpp_web_router提供的模式路由覆盖了这些场景匹配优先级为静态 参数 通配。1.1 路径参数/user/:idapp.get(/user/:id,[](uvcpp_web_requestreq,uvcpp_web_responseresp,uvcpp_web_next next){conststd::string*idreq.param(id);resp.text(id!nullptr?*id:std::string());resp.end();});req.param(id)返回的是const std::string*参数不存在时返回nullptr——这比返回空字符串更明确不会把“参数没传”和“参数是空串”混在一起[reference:0]。多个参数可以混搭app.get(/api/v:version/users/:uid/posts/:pid,handler);1.2 通配符/files/*path*path匹配一段或多段路径捕获的内容同样通过req.param(path)获取app.get(/files/*path,[](uvcpp_web_requestreq,uvcpp_web_responseresp,uvcpp_web_next next){conststd::string*preq.param(path);// 请求 /files/docs/readme.txt → p docs/readme.txtresp.text(你请求了: *p);resp.end();});通配符至少吃一段。/files/*path匹配/files/a但不匹配/files——如果连前缀本身也要处理需要单独注册/files的路由。serve_static内部就是靠这个机制工作的[reference:1]。1.3 HTTP 方法app.get()、app.post()、app.put()、app.del()分别注册对应方法的路由。同一个路径可以注册多个方法uvcpp_web_router会正确区分app.get(/resource,get_handler);app.post(/resource,post_handler);当请求方法不匹配已注册的路由时框架自动返回 405 并填充Allow头OPTIONS请求自动生成允许的方法列表。这些语义是框架保证的不需要你手写[reference:2]。二、中间件链Express 风格的next()模型上篇的日志中间件已经展示了.use()的基本用法。这一节深入中间件链的执行模型。2.1 中间件就是“会调 next() 的 handler”uvcpp_web_middlewareauth_middleware(){return[](uvcpp_web_requestreq,uvcpp_web_responseresp,uvcpp_web_next next){conststd::string*tokenreq.header(Authorization);if(tokennullptr||!verify_token(*token)){resp.status(401).json_str({\error\:\unauthorized\});resp.end();return;// 不调 next()链到此结束}next();// 放行进入下一个中间件或路由 handler};}中间件和路由 handler 的类型完全相同uvcpp_web_handler。区别只在行为不调next()就是这一环直接结束链不再往下走[reference:3]。2.2 执行顺序与“前后包裹”.use()的注册顺序就是执行顺序。next()调用之前的代码在“请求进入”阶段执行之后的代码在“响应完成”阶段执行——这是 Express/Koa 的经典洋葱模型app.use([]{/* ① 请求进入 */next();/* ③ 响应完成后 */});app.use([]{/* ② 请求进入 */next();/* ② 响应完成后 */});app.get(/x,handler);// ④ 最后执行上篇的日志中间件正是利用了这个特性next()之前记录时间戳之后记录状态码。2.3 异步中间件与hold()/release()如果中间件需要做异步操作查数据库、调外部 API不能直接next()后返回——框架可能在这一环还没完成时就结束了请求。uvcpp_web_context提供hold()和release()来显式声明“我还没完成”app.use([](uvcpp_web_requestreq,uvcpp_web_responseresp,uvcpp_web_next next){uvcpp_web_context*ctxreq.context();ctx-hold();// 告诉框架这一环还在进行中别往下走// 异步操作比如查数据库do_async_query(req.path(),[ctx,next](boolallowed){if(allowed){ctx-release();// 解除 holdnext();// 放行}else{// 拒绝请求ctx-release();// ...写响应...}});});hold()的本质是给上下文加一个引用计数release()减一。计数归零时框架才认为这一环完成。这在异步中间件中是必须的——不 hold 直接返回框架会在异步回调执行前就结束请求。2.4 内置中间件libuvcpp 自带了一些常用中间件app.use(web_middleware_access_log());// 访问日志app.use(web_middleware_cors());// CORS 跨域web_middleware_access_log()会写框架标准格式的访问日志。但如果你需要自定义格式比如上篇那样输出到本地文件自己写中间件更灵活。web_middleware_cors()处理跨域预检请求和Access-Control-*头前端调试时很省心。2.5 中间件挂载建议推荐的注册顺序// 1. 日志最先注册记录一切app.use(my_log_middleware());// 2. CORS在鉴权之前预检请求不应被鉴权拦截app.use(web_middleware_cors());// 3. 鉴权app.use(auth_middleware());// 4. 业务路由app.get(/api/...,handler);// 5. 静态文件最后注册避免拦截 API 路由app.serve_static(/assets,./public);三、请求体流式接收默认情况下框架会把整个请求体缓冲在内存中handler 触发时req.body()已经完整可用。这对于小体积 JSON 或表单请求完全够用。但大文件上传或流式数据不能走这条路——一个 1 GB 的上传会把整个请求体拉进内存。uvcpp_web_stream提供了流式接收能力通过on_data/on_end回调逐块处理请求体app.post(/upload/stream,[](uvcpp_web_requestreq,uvcpp_web_responseresp,uvcpp_web_next next){auto*streamreq.stream();stream-on_data([](constchar*data,size_t len){// 逐块到达的数据可以直接写入磁盘或做增量处理// 注意data 只在回调期间有效需要持久化就拷贝或立刻写入});stream-on_end([](){// 请求体全部接收完毕});// 流式模式下handler 返回后框架不会自动结束请求// 需要在 on_end 中调用 resp.end()});stream-pause()和stream-resume()可以在消费端忙不过来时暂停接收形成背压。这在写入磁盘较慢的场景下很有用——暂停接收比在内存中无限堆积要安全得多。四、文件上传multipart 流式落盘uvcpp_web_upload/uvcpp_web_multipart构建在流式接收之上专门处理multipart/form-data请求。4.1 基本用法app.post(/upload,[](uvcpp_web_requestreq,uvcpp_web_responseresp,uvcpp_web_next next){auto*uploadreq.upload();// 设置落盘目录和大小上限upload-set_upload_dir(./uploads);upload-set_max_file_size(50*1024*1024);// 单文件 50 MBupload-set_max_total_size(200*1024*1024);// 总大小 200 MBupload-on_file([](constuvcpp_web_uploaded_filefile){// 每个文件落盘完成后触发// file.field_name —— 表单字段名// file.original_name —— 原始文件名// file.saved_path —— 实际保存路径随机叶子名// file.size —— 文件大小});upload-on_end([resp](){resp.json_str({\status\:\ok\});resp.end();});upload-on_error([](interr){// 上传过程中出错超限、磁盘写入失败等});});4.2 大小限制与安全策略uvcpp_web_upload内置了六条大小上限分别覆盖限制项作用单文件大小防止单个超大文件打满磁盘总请求大小防止大量小文件组合成 DoS文件数量防止 multipart 边界攻击字段名长度防止超长字段名消耗内存文件名长度同上part 头部大小防止畸形 multipart 头落盘策略是随机叶子名 流式写入文件名在服务端生成随机名不使用客户端提供的原始文件名作为磁盘路径——这直接封死了路径穿越攻击../../etc/passwd这类。原始文件名只作为元数据保留在original_name中。4.3 与流式接收的区别场景用uvcpp_web_stream用uvcpp_web_uploadraw body 流式处理✅❌multipart 表单❌✅需要解析字段名/文件名❌✅自己控制落盘逻辑✅❌框架落盘如果上传的是非 multipart 格式的原始数据比如直接 PUT 一个文件用uvcpp_web_stream自己处理如果是标准 HTML 表单上传用uvcpp_web_upload更省事。五、请求对象操作速查uvcpp_web_request封装了日常业务最常用的访问方式// 方法 路径req.method()// http_method 枚举req.method_str()// GET / POST ...req.path()// /user/123req.query()// 查询串不含 ?// 路径参数req.param(id)// const std::string*// 查询串参数req.query_param(page)// const std::string*// 请求头req.header(Content-Type)// const std::string*// Cookiereq.cookie(session_id)// const std::string*// 表单字段application/x-www-form-urlencodedreq.form(username)// const std::string*// 请求体req.body()// uvcpp_buf完整请求体响应侧resp.status(200);resp.set_header(X-Custom,value);resp.json_str({\ok\:true});resp.text(plain text);resp.html(h1Hello/h1);resp.redirect(/new-location);resp.send_file(./path/to/file.pdf);resp.end();六、完整示例一个带鉴权、上传和路由参数的 API 服务#includewebapp/uvcpp_web_app.h#includefstream#includemutex#includectime#includeiostreamusingnamespaceuvcpp;staticstd::ofstream g_log;staticstd::mutex g_log_mutex;staticstd::stringnow_str(){std::time_t tstd::time(nullptr);charbuf[64];std::strftime(buf,sizeof(buf),%Y-%m-%d %H:%M:%S,std::localtime(t));returnbuf;}// 简单鉴权中间件uvcpp_web_middlewareauth_middleware(){return[](uvcpp_web_requestreq,uvcpp_web_responseresp,uvcpp_web_next next){conststd::string*tokenreq.header(Authorization);if(tokennullptr||token-empty()){resp.status(401).json_str({\error\:\missing token\});resp.end();return;}next();};}intmain(){g_log.open(access.log,std::ios::app);uvcpp_web_app app;app.set_host(0.0.0.0).set_port(8080);constintrcapp.set_loops(4);if(rc!0){std::cerrset_loops: rcstd::endl;return1;}// ---- 中间件链 ----app.use([](uvcpp_web_requestreq,uvcpp_web_responseresp,uvcpp_web_next next){conststd::string tsnow_str();next();std::lock_guardstd::mutexlock(g_log_mutex);if(g_log.is_open()){g_log[ts] req.method_str() req.path() - static_castint(resp.status_code())\n;g_log.flush();}});app.use(web_middleware_cors());// ---- 公开路由 ----app.get(/hello,[](uvcpp_web_request,uvcpp_web_responseresp,uvcpp_web_next){resp.json_str({\hello\:\world\});resp.end();});// ---- 路径参数 ----app.get(/user/:id,[](uvcpp_web_requestreq,uvcpp_web_responseresp,uvcpp_web_next){conststd::string*idreq.param(id);resp.json_str({\user_id\:\(id?*id:)\});resp.end();});// ---- 通配符 ----app.get(/files/*path,[](uvcpp_web_requestreq,uvcpp_web_responseresp,uvcpp_web_next){conststd::string*preq.param(path);resp.text(requested: (p?*p:std::string()));resp.end();});// ---- 鉴权中间件只作用于 /api 前缀 ----// 实际项目中可以用路径前缀路由组这里简化处理app.use(auth_middleware());// ---- 文件上传 ----app.post(/api/upload,[](uvcpp_web_requestreq,uvcpp_web_responseresp,uvcpp_web_next){auto*uploadreq.upload();upload-set_upload_dir(./uploads);upload-set_max_file_size(50*1024*1024);upload-set_max_total_size(200*1024*1024);upload-on_file([](constuvcpp_web_uploaded_filef){std::cout收到文件: f.original_name (f.size bytes) - f.saved_pathstd::endl;});upload-on_end([resp](){resp.json_str({\status\:\uploaded\});resp.end();});});// ---- 异步 handler ----app.get(/api/slow,[app](uvcpp_web_request,uvcpp_web_responseresp,uvcpp_web_next next){uvcpp_web_response*rpresp;auto*wnewuvcpp_work();w-init();w-queue_work(app.loop(),[](uvcpp_work*){std::this_thread::sleep_for(std::chrono::milliseconds(20));},[next,rp](uvcpp_work*done,int){rp-json_str({\slow\:true});rp-end();next();deletedone;});});// ---- 静态资源 ----app.serve_static(/assets,./public,true);app.start();std::coutServer on :8080, 4 loopsstd::endl;app.join();return0;}几个关键点app.use(auth_middleware())放在公开路由之后这样/hello、/user/:id、/files/*path不需要鉴权而后续注册的/api/upload和/api/slow会经过鉴权。中间件的“路径前缀”作用域通过注册顺序来控制——先注册的中间件作用于所有后续路由。异步 handler 中next按值捕获。框架看到next被捕获到异步回调中就知道这一环会异步恢复不会提前结束请求。如果只是同步处理next可以直接调用[reference:4]。resp.end()是“我写完了”的标记不是发送。框架在 handler 返回后统一收尾时才真正把响应写出去。七、本篇小结与下一步这篇覆盖了从“Hello World”到真实业务的关键跨越路由进阶——路径参数:id、通配符*path、HTTP 方法区分匹配优先级静态 参数 通配。中间件链——next()洋葱模型异步中间件用hold()/release()声明生命周期web_middleware_cors()开箱即用。流式请求体——uvcpp_web_stream的on_data/on_end配合pause()/resume()实现背压。文件上传——uvcpp_web_upload的 multipart 流式落盘、随机叶子名、六条大小上限。请求对象——param()/query_param()/header()/cookie()/form()的统一访问入口。下一篇会进入WebSocket 实时通信uvcpp_web_ws端点的创建、连接生命周期管理、广播与房间模型以及与 HTTP 路由共存在同一个应用中的实践。如果你在实践过程中遇到问题欢迎在 GitHub Issues 提出。
返回列表