ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用 C++ 写 Web 服务实战(一):从零搭建你的第一个 libuvcpp Web 应用

用 C++ 写 Web 服务实战(一):从零搭建你的第一个 libuvcpp Web 应用 用 C 写 Web 服务实战一从零搭建你的第一个 libuvcpp Web 应用在上一篇介绍中我们聊了 libuvcpp 的设计理念——在 libuv 的高性能之上提供符合 C 习惯的 RAII 封装和std::function回调让 C 开发者也能像写 Express 或 Spring Boot 那样快速搭建 Web 服务。这篇是实战系列的第一篇我们从最基础的开始创建一个可运行的 Web 应用监听端口绑定静态目录做好性能规划再配上日志系统。一、构建与依赖准备WebApp 模块建在 Web 模块之上使用之前需要在 CMake 阶段显式开启。完整的构建命令如下# Linux 先安装系统依赖sudoapt-getinstalllibssl-dev zlib1g-devmkdirbuildcdbuild cmake..-DCMAKE_BUILD_TYPERelease\-DUVCPP_BUILD_WEBON\-DUVCPP_BUILD_WEBAPPON\-DUVCPP_ENABLE_OPENSSLON\-DUVCPP_ENABLE_ZLIBON\-DUVCPP_BUILD_EXAMPLESON cmake--build.--configRelease--parallel这里的关键选项选项作用默认值UVCPP_BUILD_WEB启用 HTTP/WebSocket 模块OFFUVCPP_BUILD_WEBAPP启用 Web 应用框架层OFFUVCPP_ENABLE_OPENSSL启用 TLS 封装OFFUVCPP_ENABLE_ZLIB启用压缩支持OFFUVCPP_BUILD_EXPAND启用 TCMalloc 风格内存池OFF注意Expand 内存池模块自 v1.1.0 起默认关闭。预编译包是带池发布的如果你用的是预编译包uvcpp/uvcpp_config.h里已经给定了实际值自己再定义成别的值会直接#error不会静默产生分配器错配。源码构建时如果需要启用加-DUVCPP_BUILD_EXPANDON。二、创建第一个 Web 应用引入头文件创建一个uvcpp_web_app实例配置路由和端口然后启动。以下是最小可运行骨架#includewebapp/uvcpp_web_app.husingnamespaceuvcpp;intmain(){uvcpp_web_app app;// 配置监听地址和端口app.set_host(0.0.0.0).set_port(8080);// 注册一条最简单的 JSON 路由app.get(/hello,[](uvcpp_web_requestreq,uvcpp_web_responseresp,uvcpp_web_next next){resp.json_str({\message\:\hello from libuvcpp\});resp.end();});// 启动bind 之后在后台线程跑事件循环app.start();// 阻塞等待直到收到退出信号app.join();return0;}uvcpp_web_app是整个应用的本体承担配置、路由、中间件和生命周期管理的职责。它的生命周期 API 很直观start()在后台线程启动事件循环stop()停止join()阻塞等待。默认监听0.0.0.0但建议显式调用set_host()和set_port()避免部署时出现“以为监听了却连不上”的问题。编译时链接库g-stdc11 main.cpp-luvcpp_webapp-luvcpp_web-luvcpp-luv-lssl-lcrypto三、绑定静态目录Web 应用几乎必然要提供静态资源。uvcpp_web_static提供了完整的静态文件服务能力——ETag、Last-Modified、Range 请求206/416、LRU 缓存和 SPA 回落都内置了。3.1 基础用法// 将 URL 路径 /assets 映射到本地目录 ./publicapp.serve_static(/assets,./public);请求GET /assets/style.css时框架自动读取./public/style.css附带正确的Content-Type、Content-Length、ETag和Last-Modified响应头。如果客户端带If-None-Match命中 ETag 直接返回 304省去传输开销。3.2 SPA 回落单页应用SPA通常需要把未匹配的路径都回落到index.html由前端路由处理app.serve_static(/,./dist,/* spa_fallback */true);访问/user/123时如果./dist/user/123不存在框架自动返回./dist/index.html。3.3 性能注意静态文件服务涉及磁盘 I/O 和 ETag 计算需要读取文件内容做哈希。在生产环境中如果前置了 Nginx 或 CDNlibuvcpp 的静态服务主要服务于内网场景或轻量级部署。高并发静态资源场景下把静态服务交给 Nginx 仍然是更稳妥的选择。四、性能优化让单进程跑满多核这是 v1.3.0 最重要的能力增量。一条事件循环最多占满一个核这是所有单线程异步框架的天花板。set_loops(n)在同一个进程内启动1 条接受者循环 n−1 条工作循环接受连接始终在一个线程上连接的 I/O 摊到各条工作循环。uvcpp_web_app app;app.set_host(0.0.0.0).set_port(8080);// 1 条接受者 3 条工作循环// 注意返回 int不能链式调用必须在 start() 之前调用constintrcapp.set_loops(4);if(rc!0)return1;app.get(/json,[](uvcpp_web_request,uvcpp_web_responseresp,uvcpp_web_next){resp.json_str({\hello\:\world\});resp.end();});app.start();// n 1 时只能用 start()/start_background()run(md) 会被拒app.join();使用set_loops有几条必须知道的规则必须在start()/run()之前调用。路由注册同样要在start()之前——n 1时这条从建议变为必须。set_loops返回int不能接在set_host(...).set_port(...)链上。成功返回0n不在1..64范围返回UV_EINVAL运行时已经启动过返回UV_EBUSY。n 1与从不调用逐字节相同——不建额外格子、不装钩子、不多起线程。n 1时run(md)会被UV_EINVAL拒绝必须用start()或start_background()收尾用stop()与join()。接受者循环不承载任何连接。loop_count()返回总循环数connection_count_at(i)返回每格连接数——下标0是接受者恒为 0。需要注意的是socket 转手在 Windows 上走WSADuplicateSocketWWSASocketW其余平台走dup()。Windows 上这条路径压着一个已知的 libuv 缺陷开多循环之前建议先读doc/net-guide.md§4 和RELEASE.md。对于绝大多数场景set_loops(4)到set_loops(8)已经能充分利用现代多核 CPU。配合 Expand 内存池单事件循环的实测数据是75k RPS10 分钟 soak 跑 3840 万请求零错误。五、日志系统与自定义日志中间件5.1 内置日志libuvcpp 内置了uvcpp_log/uvcpp_log_console提供两级日志体系——等级 模块sink 可插拔。等级涵盖 DEBUG / INFO / WARN / ERROR模块标识用于区分不同组件的输出。默认的 console sink 输出到标准输出也支持接入 syslog 等自定义 sink。最简单的初始化#includeuvcpp/uvcpp_log.husingnamespaceuvcpp;uvcpp_log::init(uvcpp_log_level::INFO);但内置日志主要服务于框架内部。业务访问日志更适合用中间件自己实现这样格式完全可控且能按需求写入文件。5.2 写一个日志中间件uvcpp_web_app的.use()方法接收中间件。中间件的签名与路由 handler 类似但多了一个uvcpp_web_next参数用于放行请求#includewebapp/uvcpp_web_app.h#includefstream#includesstream#includectime#includemutexusingnamespaceuvcpp;// 全局日志文件和互斥锁staticstd::ofstream g_access_log;staticstd::mutex g_log_mutex;std::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;}intmain(){// 打开本地日志文件追加模式g_access_log.open(access.log,std::ios::app);if(!g_access_log.is_open()){std::cerr无法打开日志文件std::endl;return1;}uvcpp_web_app app;app.set_host(0.0.0.0).set_port(8080);// 注册日志中间件app.use([](uvcpp_web_requestreq,uvcpp_web_responseresp,uvcpp_web_next next){// 请求进入时记录时间戳conststd::string tsnow_str();// 放行到下一个中间件或路由 handlernext();// 响应完成后记录日志resp 中已有状态码等信息std::lock_guardstd::mutexlock(g_log_mutex);if(g_access_log.is_open()){g_access_log[ts] req.method_str() req.path() static_castint(resp.status_code())\n;g_access_log.flush();}});app.get(/hello,[](uvcpp_web_request,uvcpp_web_responseresp,uvcpp_web_next){resp.json_str({\hello\:\world\});resp.end();});app.serve_static(/assets,./public);app.start();app.join();return0;}这段代码的关键点用std::mutex保护文件写入。多事件循环模式下set_loops(n)且n 1多个工作循环线程会并发调用中间件不锁文件会导致日志交错甚至崩溃。每次写入后flush()。异步框架下崩溃可能发生在任何时刻不 flush 意味着最后几条日志丢失——往往恰恰是排查问题最需要的那几条。next()之前记录时间戳之后记录状态码。next()调用后会执行后续的中间件和路由 handler执行完毕后resp中已经有最终的状态码和响应体信息。这个“前后包裹”的模式是 Express/Koa 中间件模型的经典写法。挂载顺序影响执行顺序。.use()的注册顺序就是中间件的执行顺序。日志中间件通常注册在所有其他中间件之前这样即使后续中间件拒绝了请求比如 CORS 中间件返回 403日志也能记录下来。六、完整示例把上面的一切串起来#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;}intmain(){g_log.open(access.log,std::ios::app);uvcpp_web_app app;app.set_host(0.0.0.0).set_port(8080);// 性能4 条工作循环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();}});// CORS 中间件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.serve_static(/assets,./public,/* spa_fallback */true);app.start();std::coutServer running on :8080 with 4 loopsstd::endl;app.join();return0;}七、本篇小结与下一步这篇覆盖了 Web 应用搭建的五个核心步骤构建配置——开启UVCPP_BUILD_WEBAPP和UVCPP_ENABLE_OPENSSL注意 Expand 内存池默认关闭。创建应用——uvcpp_web_appset_host/set_portstart/join加上路由注册。静态目录——serve_static()支持 ETag、Range、SPA 回落一条调用搞定。性能优化——set_loops(n)突破单核天花板注意返回int、必须在start()前调用、n 1时不能用run(md)。日志中间件——用.use()注册中间件前后包裹模式记录请求/响应用std::mutex保证多线程安全写入。下一篇会在这个骨架之上继续扩展路由进阶、中间件链的深度使用、请求体流式处理和文件上传。如果你在搭建过程中遇到问题欢迎在 GitHub Issues 提出。
返回列表