Ubuntu上部署Drogon:C++高并发Web框架实战指南
简介面向Ubuntu下C14 Web开发者的Drogon框架实战资源包以登录鉴权与Token返回为主线串联MySQL、SQLite3两种数据库的Post/Get请求处理适合希望快速上手高性能异步Web服务的中高级C开发者。压缩包共16个文件以cc源码、h头文件为主辅以json配置、txt说明和db数据库文件整体仅23KB结构简洁便于对照学习。已有374人学习下载。内容包含Drogon项目模板的控制器实现、ORM模型类、请求回调处理示例及数据库文件覆盖从安装依赖、构建应用到配置数据库的典型环节可直接作为本地调试与二次开发的参考蓝本。1. 在 Ubuntu 上跑起 Drogon一个 C 高并发 Web 服务的第一现场之前接手过一个内部数据看板服务QPS 要求不高但单请求内有大量 CPU 密集的聚合计算随手选的框架在压测机上单核就撑到 2000 并发量级开始抖迁移到 Drogon 后同样一台机器直接翻了几倍。Drogon 是个基于 C17/20 的异步 Web 框架不依赖 Boost事件循环跑在 epoll 上核心卖点是「用 C 的代价拿到接近裸写 epoll 的性能」。这套资源面向的是两类人一类是 C 服务端开发者想找个能落地的 Web 框架另一类是已经受够了 Python/Node 在 CPU 密集场景下性能瓶颈的团队打算把核心接口下沉到 C 又不愿意自己撸网络层。下面这段部署和接入数据库的过程是我在 Ubuntu 上完整跑通一遍后的真实记录包括踩过的编译坑、连接池参数和协程使用边界。2. 弄懂 Drogon 的异步运行机制事件循环、协程与数据库回调2.1 事件循环模型为什么说它是异步的但不是多线程万能药Drogon 的底层运行模型是典型的多线程事件循环。每个 worker 线程跑一个独立的事件循环负责监听 socket 上的读写事件、处理定时器、执行已完成的数据库回调。这和 Node.js 的模型类似区别在于 Drogon 默认是多线程的线程数通过app().setThreadNum()配置默认是硬件线程数。理解这个模型对后续调参很重要。我见过有人在 4 核机器上把线程数设成 32结果上下文切换开销比处理请求还大吞吐量反而腰斩。一般经验是如果业务里 CPU 计算多线程数设为物理核心数如果 IO 等待多比如频繁查数据库线程数可以加到核心数的 1.5 到 2 倍。Drogon 的异步体现在业务代码可以用两种方式写。一种是传统的回调风格handler 里发起异步数据库查询传入回调 lambda另一种是 C20 协程用co_await把异步操作写成同步代码的样子。两种方式在框架内部都走同一套事件循环不会因为用协程就额外开线程。// 回调风格异步查询用户信息 auto db app().getDbClient(); db-execSqlAsync(SELECT name, age FROM users WHERE id ?, [this](const orm::Result r) { // 查询完成后在主事件循环线程里执行后续逻辑 auto resp HttpResponse::newHttpResponse(); resp-setBody(r.rows().empty() ? {} : r[0][name].asstd::string()); callback(resp); }, [](const DbClient::Exception e) { LOG_ERROR e.base().what(); }, 1);这段代码里execSqlAsync的参数顺序是SQL 语句、成功回调、失败回调、SQL 中的参数。注意成功回调执行时仍然在请求对应的 worker 线程上所以不要在回调里做耗时超过几十毫秒的同步操作否则会阻塞整个事件循环后面的请求全等着。这是一个新手最容易踩的坑把回调当成普通线程随便跑。2.2 协程是回调的语法糖但用之前要知道三个限制Drogon 从 1.8 版本开始支持 C20 协程我用下来感觉这是写复杂业务链路最舒服的方式。比如一个接口要连续查三次数据库回调写法要嵌套三层 lambda协程写法就是顺序co_await三次。// 协程风格写法连续查库不再嵌套回调 TaskHttpResponsePtr UserController::getUserInfo(const HttpRequestPtr req) { auto db app().getDbClient(); auto result1 co_await db-execSqlCoro(SELECT id, name FROM users WHERE id ?, 1); // 拿到第一次结果后再根据情况查第二张表 auto result2 co_await db-execSqlCoro(SELECT log FROM login_log WHERE user_id ?, result1[0][id].asint64_t()); auto resp HttpResponse::newHttpResponse(); resp-setBody(result2[0][log].asstd::string()); co_return resp; }注意三个限制。第一execSqlCoro返回的是TaskT类型协程函数里第一个co_await之后函数的返回类型必须显式写出TaskHttpResponsePtr而不能用auto。第二协程函数被调用时要保证有一个活跃的co_await点否则编译器会警告协程从未挂起性能上反而比回调差。第三Drogon 的协程不支持超时取消如果一个查询挂住了协程会一直等下去。我在生产环境里的做法是给数据库客户端配置timeout参数从框架层面兜底而不是在协程代码里做取消逻辑。协程和回调可以在同一个项目里混用但建议一个文件中只用一个风格避免维护时精神分裂。另外C20 协程需要编译器支持Ubuntu 22.04 自带的 gcc 11 没问题Ubuntu 20.04 自带的 gcc 9 也支持但 gcc 8 及以下不行这个在避坑章再展开。2.3 中间件、过滤器与控制器的执行顺序Drogon 的 Web 架构是 Controller 处理请求中间件Middleware和过滤器Filter做前置后置逻辑。三者执行顺序是中间件链 — 过滤器 — Controller 处理函数 — 中间件链后置部分。这个顺序决定了你可以在哪一层做鉴权、在哪一层做日志。// 过滤器示例只允许 POST 方法 class MethodFilter : public HttpFilterMethodFilter { public: void doFilter(const HttpRequestPtr req, FilterCallback fcb, FilterChainCallback fccb) override { if (req-method() ! Post) { auto resp HttpResponse::newHttpResponse(); resp-setStatusCode(k405MethodNotAllowed); fcb(resp); // 直接返回响应不往下传递 return; } fccb(); // 放行交给 Controller } };中间件的preHandle在过滤器之前执行postHandle在 Controller 返回之后执行但此时还没有序列化为 HTTP 响应可以修改 headers。实际项目里常见的分工是中间件做跨域和请求 ID 注入过滤器做接口级鉴权Controller 只写业务逻辑。这样的分层在后期加接口时能少写大量重复代码。3. Ubuntu 编译部署依赖安装、CMake 参数与最小服务验证3.1 依赖安装清单与版本踩坑日志在 Ubuntu 上编译 Drogon 前要先装齐依赖。很多人在这一步卡住因为编译错误提示的包名和你实际要apt install的名字对不上。下面是我在 Ubuntu 22.04 和 20.04 上都验证过的安装列表# Ubuntu 22.04/20.04 通用 sudo apt update sudo apt install -y build-essential cmake git libjsoncpp-dev \ libssl-dev libuuid-dev zlib1g-dev libcurl4-openssl-dev \ libmysqlclient-dev # 如果只用 PostgreSQL 就换 libpq-dev注意两点。第一libjsoncpp-dev对应的是 Drogon 的 JSON 支持依赖如果缺了这个编译到 json 相关头文件时会报json/json.h not found。第二如果不小心先装了libjsoncpp1不带 dev系统里只有运行库没有头文件编译照样失败。先dpkg -l | grep jsoncpp确认版本再继续。另外Ubuntu 20.04 的 OpenSSL 版本是 1.1.1编译 Drogon 没问题Ubuntu 22.04 是 OpenSSL 3.0注意如果自己编译了 OpenSSL 并指定了非系统路径要通过-DOPENSSL_ROOT_DIR告诉 CMake否则可能链接到错误的 libssl。3.2 源码克隆与 CMake 参数选择Drogon 的源码托管在 GitHub 上克隆时必须带--recursive因为它依赖若干子模块比如底层网络库。不带这个参数会导致 cmake 阶段直接报缺目录git clone --recursive https://gitee.com/an-tao/drogon.git # 如果网络慢先克隆主仓库再单独初始化子模块 git clone https://gitee.com/an-tao/drogon.git cd drogon git submodule update --init --recursive然后进入构建目录用 CMake 配置。以下几个参数是我实际搭建时用到的mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease \ -DBUILD_EXAMPLESOFF \ -DBUILD_ORMON \ -DBUILD_CTLON \ -DCMAKE_CXX_STANDARD17 make -j$(nproc) sudo make install参数说明BUILD_ORMON会启用 ORM 建模工具数据库接入那一章会用到如果你不用数据库可以关掉BUILD_CTLON会编译drogon_ctl命令行工具用来生成控制器和模型骨架CMAKE_CXX_STANDARD17表示默认用 C17如果你计划用协程就改成 20。注意这里的CMAKE_CXX_STANDARD是给 Drogon 自身编译用的你的业务代码用哪个标准要在自己的 CMakeLists.txt 里再单独指定。编译完sudo make install后库默认装到/usr/local/lib头文件在/usr/local/include/drogon。装完最好执行一次sudo ldconfig否则运行时可能报找不到libdrogon.so。3.3 一分钟跑起最小服务验证环境依赖装好、库装好后创建测试工程验证环境是否真正可用。这是新手翻车重灾区我直接给出完整最小工程。// main.cpp #include drogon/drogon.h using namespace drogon; int main() { // 注册一个最简单的 GET 接口 app().registerHandler(/ping, [](const HttpRequestPtr, std::functionvoid(const HttpResponsePtr) callback) { auto resp HttpResponse::newHttpResponse(); resp-setBody(pong); callback(resp); }, {Get}); // 监听端口打印访问日志 app().addListener(0.0.0.0, 8080); app().setLogLevel(trantor::Logger::kInfo); app().run(); }# CMakeLists.txt cmake_minimum_required(VERSION 3.10) project(drogon_demo) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Drogon REQUIRED) add_executable(demo main.cpp) target_link_libraries(demo Drogon::Drogon)编译后启动用curl http://127.0.0.1:8080/ping验证。如果返回pong说明编译链和依赖都正常。这一步不要跳过很多人直接开始写业务代码最后编译不过都分不清是依赖问题还是代码问题。先把这个最小服务跑通后面加接口越加越顺。4. 接入 Ubuntu 下的数据库连接池、ORM 映射与事务边界4.1 配置文件方式初始化连接池Drogon 的数据库连接池支持 MySQL、PostgreSQL 和 SQLite3。生产环境建议用配置文件统一管理数据库参数而不是硬编码在代码里因为切换环境开发/测试/生产时只需要改配置文件。// config.json { listeners: [ { address: 0.0.0.0, port: 8080 } ], db_clients: [ { name: main_db, client_type: mysql, host: 127.0.0.1, port: 3306, username: drogon_user, password: your_password, dbname: app_db, size: 10, timeout: 5.0 } ] }在 main 函数里加载配置app().loadConfigFile(config.json)。然后代码中通过app().getDbClient(main_db)获取连接池中的连接。参数说明size是连接池最大连接数。这个值不是越大越好MySQL 服务端的max_connections是硬上限而且每个连接都有内存开销。我一般按「每个 worker 线程 2~3 个连接」估算4 核机器设 8~12 比较合理。timeout是连接等待超时秒超过这个时间拿不到连接会抛异常生产环境建议不要低于 3 秒否则突发流量时大量请求直接失败。有个隐藏参数是每个连接的空闲回收时间在代码里通过DbClient的retryLimit和retryInterval控制重连行为// 代码方式配置数据库客户端与配置文件二选一 app().addDbClient(127.0.0.1, 3306, drogon_user, your_password, app_db, ClientType::Mysql, 10);4.2 ORM 模型生成与 CRUD 实战Drogon 的 ORM 模型可以用drogon_ctl工具直接从数据库表结构生成然后业务代码里像操作普通对象一样读写数据。这个功能在BUILD_ORMON时可用。# 生成模型文件到 models 目录 drogon_ctl create model models -cn main_db-cn参数指定配置文件中哪个数据库客户端。模型生成后每个表对应一个.h和.cc文件类的字段是表字段的映射构造函数和 getter/setter 都是自动生成的。注意生成后需要把 models 目录加入 CMake 编译# 在 CMakeLists.txt 中添加 models 目录 target_include_directories(demo PRIVATE ${CMAKE_SOURCE_DIR}/models)接下来用生成的模型写一个查询接口。下面的代码展示通过 ORM 查询一个用户表并返回 JSON#include models/Users.h // 用 ORM 查询 id 为 1 的用户 using namespace drogon::orm; app().registerHandler(/user/{1}, [](const HttpRequestPtr req, std::functionvoid(const HttpResponsePtr) callback, int userId) { auto db app().getDbClient(main_db); MapperUsers mapper(db); mapper.findByPrimaryKey(userId, [callback](const Users user) { auto resp HttpResponse::newHttpResponse(); auto json Json::Value(); json[name] user.getValueOfName(); json[age] user.getValueOfAge(); resp-setBody(json.toStyledString()); callback(resp); }, [callback](const DrogonDbException e) { auto resp HttpResponse::newHttpResponse(); resp-setStatusCode(k404NotFound); resp-setBody(e.base().what()); callback(resp); }); }, {Get});参数说明MapperUsers绑定模型类型findByPrimaryKey第一参数是主键值后面两个回调分别处理成功和失败。注意失败回调里返回的异常包含 SQL 错误信息生产环境建议只记录日志不要直接拼进响应体避免 SQL 细节泄漏到客户端。CRUD 里还有insert和update、delete这些方法同样支持异步回调或者协程方式。4.3 事务边界多步写库的一致性问题单个 SQL 查询用自动提交没问题但多步写操作必须显式开启事务。Drogon 的事务对象是Transaction在事务块里执行多条 SQL提交时要么全部成功要么全部回滚。// 使用事务扣余额 写流水两步操作必须原子完成 auto db app().getDbClient(main_db); auto transaction db-newTransaction(); transaction-execSqlAsync( UPDATE accounts SET balance balance - ? WHERE id ? AND balance ?, [transaction](const orm::Result r) { if (r.affectedRows() 1) { // 扣款成功写入流水 transaction-execSqlAsync( INSERT INTO balance_log (user_id, amount, ts) VALUES (?, ?, NOW()), [transaction](const orm::Result) { // 两条都成功后提交 transaction-commit(); }, [transaction](const DbClient::Exception e) { transaction-rollback(); LOG_ERROR e.base().what(); }, 1001, -50.0); } else { // 余额不足回滚 transaction-rollback(); } }, [](const DbClient::Exception e) { LOG_ERROR e.base().what(); }, 1001, 50.0);这里有三个边界问题。第一事务对象不能跨线程使用同一个事务的所有请求必须在同一个 worker 线程上执行否则线程安全问题会导致各种诡异的内存错误。第二异步回调里再发起异步操作回调嵌套层数深了容易出错我一般用协程版execSqlCoro写事务代码更直观。第三事务的commit()和rollback()必须调用一次且仅一次漏调用会导致连接长时间占在事务状态连接池很快被耗尽。若是异常分支没有主动回收后续请求会一直阻塞到 timeout。5. 避坑排查编译失败、连接超时与协程死锁的四份诊断记录5.1 编译报错 json/json.h: No such file or directory现象执行 cmake 后编译报fatal error: json/json.h: No such file or directory出现在 drogon 源码的 json 相关文件里。原因系统缺少 libjsoncpp-dev 头文件包。常见的误区是只安装了 libjsoncpp1 运行库或者根本没装 jsoncpp 相关包。解决执行sudo apt install libjsoncpp-dev装完之后确认/usr/include/jsoncpp/json/json.h存在。如果还存在问题在 CMakeLists.txt 里手动指定头文件路径include_directories(/usr/include/jsoncpp)5.2 git clone 后 cmake 提示缺少 trantor 子目录现象克隆源码后执行cmake ..报错找不到 trantor/include 等路径整个配置过程直接终止。原因Drogon 依赖独立的网络库 trantor没有用--recursive参数克隆时子模块目录是空的。解决在 drogon 根目录执行git submodule update --init --recursive然后重新跑 cmake。已有 build 目录建议删除重建避免缓存旧路径。5.3 服务能启动但第一次查库就报 connection timeout现象/ping 接口正常返回/user/1 接口调用数据库时报 timeout日志里显示Failed to get a connection from pool.原因连接池参数和 MySQL 服务端状态不匹配。最常见的是host配了localhost但 MySQL 的默认认证走的是 Unix socket 而不是 TCP另外size设太小也会在并发高时拿不到连接。解决检查 config.json 中 host 是否填写127.0.0.1而不是localhost确认 MySQL 用户允许 TCP 登录把size从默认值调大到至少 8同时查看 MySQL 的max_connections确认没到上限。还有一点Linux 下 MySQL 连接默认只能从 127.0.0.1 走 TCP若要跨主机访问需要检查 bind-address 配置。5.4 协程路线的链接错误collect2: error: ld returned 1 exit status现象用 C20 协程写的 controller 文件在链接阶段报 collect2 错误配合undefined reference to std::coroutine相关符号。原因业务代码的CMAKE_CXX_STANDARD设置成 17但协程文件需要 C20 编译支持或者 gcc 版本太老gcc 8 及以下不支持协程语义。解决确认 CMakeLists.txt 中CMAKE_CXX_STANDARD设为20如果服务端系统的 gcc 版本过低用apt install gcc-11 g-11安装新版本并在 CMake 里指定set(CMAKE_CXX_COMPILER /usr/bin/g-11)5.5 不定时 segfault连接对象在事件循环线程之外被使用现象服务运行一段随机时间后崩溃core dump 里看不出来具体业务代码的位置只显示在内存分配相关调用中。原因getDbClient()返回的数据库连接对象被保存在了static或全局变量里然后多个 worker 线程共享使用。连接池内部的连接是有线程绑定的跨线程使用必然触发未定义行为。解决不要在全局保存连接池的连接对象每次需要操作数据库时都通过app().getDbClient()获取。这个函数内部有线程 ID 的绑定逻辑开销极小完全没有优化的必要。6. 性能验证与调优习惯压测数据不会说谎服务跑起来后第一步验证不是功能而是性能上限。我习惯用wrk做一个基准压测观察单核极限和系统瓶颈。wrk -t4 -c100 -d10s http://127.0.0.1:8080/ping压测前先把 Drogon 的日志级别从kInfo调成kWarn因为每次请求打日志的文件 IO 在高并发下是巨大的性能损耗。体验最直接的调优手段是线程数。app().setThreadNum(0)表示使用硬件线程数在 8 核机器上跑纯计算接口8 线程比 16 线程吞吐量高 15% 左右这符合事件循环的特性。另一个隐藏瓶颈是响应体序列化Json::toStyledString()在每次请求时都会分配内存高并发下改成toFastString()后 GC 压力小很多。从那以后我每次部署 Drogon 服务都强制走一遍完整流程最小服务验证依赖、连接池参数核验、压测看单核天花板、再调线程数确认收本文还有配套的精品资源点击获取