003 《Drogon Web 框架:从入门到精通》


作者Lou Xiao, gemini创建时间2025-04-26 01:28:52更新时间2025-04-26 01:28:52

🌟🌟🌟本文由Gemini 2.5 Flash Preview 04-17生成,用来辅助学习。🌟🌟🌟

书籍大纲

▮▮ 1. 初识 Drogon:高性能 C++ Web 框架
▮▮▮▮ 1.1 什么是 Drogon?
▮▮▮▮ 1.2 为何选择 Drogon?
▮▮▮▮ 1.3 C++ 在 Web 开发中的地位与挑战
▮▮▮▮ 1.4 本书结构与学习路径
▮▮▮▮ 1.5 目标读者与预备知识
▮▮ 2. 环境搭建与第一个 Drogon 应用
▮▮▮▮ 2.1 Drogon 的安装与依赖
▮▮▮▮ 2.2 创建新项目:使用 drogons CLI 工具
▮▮▮▮ 2.3 项目结构解析
▮▮▮▮ 2.4 运行与构建项目
▮▮▮▮ 2.5 编写一个简单的 Hello World
▮▮ 3. 核心概念:HTTP 请求与响应处理
▮▮▮▮ 3.1 HttpRequest 对象深度解析
▮▮▮▮▮▮ 3.1.1 请求方法与 URL (Method & URL)
▮▮▮▮▮▮ 3.1.2 请求头部与 Cookies (Headers & Cookies)
▮▮▮▮▮▮ 3.1.3 请求体与参数 (Body & Parameters)
▮▮▮▮▮▮ 3.1.4 文件上传处理 (File Upload)
▮▮▮▮ 3.2 HttpResponse 对象构建
▮▮▮▮▮▮ 3.2.1 设置状态码 (Status Code)
▮▮▮▮▮▮ 3.2.2 设置响应头部与 Cookies (Headers & Cookies)
▮▮▮▮▮▮ 3.2.3 构建不同类型的响应体 (Body)
▮▮▮▮▮▮ 3.2.4 重定向响应 (Redirect)
▮▮▮▮ 3.3 请求与响应生命周期
▮▮ 4. 路由系统与控制器(Controller)
▮▮▮▮ 4.1 Drogon 路由机制
▮▮▮▮ 4.2 基于函数的请求处理器 (Function Handlers)
▮▮▮▮ 4.3 基于类的控制器 (Class-based Controllers)
▮▮▮▮▮▮ 4.3.1 生成与声明控制器
▮▮▮▮▮▮ 4.3.2 处理方法的定义
▮▮▮▮▮▮ 4.3.3 路径参数绑定
▮▮▮▮ 4.4 RESTful API 设计与实现
▮▮▮▮ 4.5 路由优先级与冲突解决
▮▮ 5. 深入异步编程:事件循环与纤程
▮▮▮▮ 5.1 事件驱动与非阻塞 I/O
▮▮▮▮ 5.2 Drogon 的事件循环 (Event Loop)
▮▮▮▮ 5.3 回调 (Callback) 与 Promise
▮▮▮▮ 5.4 C++20 协程与 Drogon 纤程 (Fiber)
▮▮▮▮▮▮ 5.4.1 协程/纤程的基本概念
▮▮▮▮▮▮ 5.4.2 在 Drogon 中使用 Fiber
▮▮▮▮▮▮ 5.4.3 Fiber 的挂起与恢复 (Suspend & Resume)
▮▮▮▮ 5.5 异步任务管理
▮▮ 6. 数据库集成:ORM 与异步访问
▮▮▮▮ 6.1 数据库连接配置
▮▮▮▮ 6.2 ORM 简介与模型生成 (Object-Relational Mapping & Model Generation)
▮▮▮▮ 6.3 使用生成的 ORM 模型
▮▮▮▮▮▮ 6.3.1 插入数据 (Insert)
▮▮▮▮▮▮ 6.3.2 查询数据 (Query)
▮▮▮▮▮▮ 6.3.3 更新数据 (Update)
▮▮▮▮▮▮ 6.3.4 删除数据 (Delete)
▮▮▮▮ 6.4 异步数据库访问
▮▮▮▮ 6.5 执行原始 SQL 查询 (Raw SQL)
▮▮▮▮ 6.6 事务处理 (Transaction)
▮▮ 7. 中间件 (Middleware) 与过滤器 (Filter)
▮▮▮▮ 7.1 中间件 (Middleware) 的概念与作用
▮▮▮▮ 7.2 实现自定义中间件
▮▮▮▮ 7.3 过滤器 (Filter) 的概念与作用
▮▮▮▮ 7.4 实现自定义过滤器
▮▮▮▮ 7.5 将中间件与过滤器应用于路由或控制器
▮▮ 8. 视图渲染与静态文件服务
▮▮▮▮ 8.1 静态文件服务 (Static File Serving)
▮▮▮▮ 8.2 Drogon 内置模板引擎
▮▮▮▮ 8.3 集成第三方模板引擎
▮▮▮▮ 8.4 构建视图渲染器 (View Renderer)
▮▮▮▮ 8.5 在控制器中使用视图
▮▮ 9. 高级路由与 RESTful API 实践
▮▮▮▮ 9.1 正则表达式路由 (Regex Routing)
▮▮▮▮ 9.2 嵌套控制器 (Nested Controllers)
▮▮▮▮ 9.3 RESTful 控制器设计与实现
▮▮▮▮ 9.4 API 版本控制 (API Versioning)
▮▮ 10. WebSocket 支持
▮▮▮▮ 10.1 WebSocket 协议基础
▮▮▮▮ 10.2 实现 WebSocket 控制器 (WebSocket Controller)
▮▮▮▮ 10.3 连接的建立、消息发送与接收
▮▮▮▮ 10.4 管理多个 WebSocket 连接
▮▮ 11. 配置管理与日志系统
▮▮▮▮ 11.1 配置文件的使用
▮▮▮▮ 11.2 运行时配置的获取
▮▮▮▮ 11.3 日志系统配置
▮▮▮▮ 11.4 在应用中记录日志
▮▮▮▮ 11.5 集成第三方日志库
▮▮ 12. 单元测试与集成测试
▮▮▮▮ 12.1 测试框架选择与配置
▮▮▮▮ 12.2 单元测试 (Unit Testing)
▮▮▮▮ 12.3 集成测试 (Integration Testing)
▮▮▮▮ 12.4 模拟请求与响应 (Mocking)
▮▮▮▮ 12.5 数据库测试策略
▮▮ 13. 应用安全
▮▮▮▮ 13.1 Web 应用常见的安全威胁
▮▮▮▮ 13.2 Drogon 的安全特性
▮▮▮▮ 13.3 CSRF 防护
▮▮▮▮ 13.4 XSS 防护
▮▮▮▮ 13.5 认证 (Authentication) 与授权 (Authorization)
▮▮▮▮▮▮ 13.5.1 基于 Session 的认证
▮▮▮▮▮▮ 13.5.2 基于 Token 的认证 (如 JWT)
▮▮▮▮▮▮ 13.5.3 实现授权检查
▮▮▮▮ 13.6 HTTPS/SSL 配置
▮▮ 14. 部署与性能优化
▮▮▮▮ 14.1 生产环境配置
▮▮▮▮ 14.2 部署方式
▮▮▮▮ 14.3 使用反向代理 (Reverse Proxy)
▮▮▮▮ 14.4 性能监控与分析
▮▮▮▮ 14.5 性能调优技巧
▮▮ 15. 扩展 Drogon:插件与自定义组件
▮▮▮▮ 15.1 Drogon 插件系统
▮▮▮▮ 15.2 实现自定义插件
▮▮▮▮ 15.3 注册与使用插件
▮▮▮▮ 15.4 自定义服务与组件
▮▮ 16. 实战案例分析
▮▮▮▮ 16.1 案例一:构建一个简单的博客 API
▮▮▮▮ 16.2 案例二:开发一个实时聊天应用
▮▮▮▮ 16.3 案例三:构建一个文件分享服务
▮▮ 17. 社区与进阶学习
▮▮▮▮ 17.1 获取帮助与参与社区
▮▮▮▮ 17.2 贡献给 Drogon 项目
▮▮▮▮ 17.3 持续学习与跟踪更新
▮▮ 附录A: Drogon 配置选项参考手册
▮▮ 附录B: 常见错误码与故障排除
▮▮ 附录C: Drogon 辅助类与工具函数参考
▮▮ 附录D: Drogon ORM 生成器命令参考
▮▮ 附录E: Drogon 依赖的第三方库
▮▮ 附录F: C++20 在 Drogon 中的应用实例


1. 初识 Drogon:高性能 C++ Web 框架

欢迎来到本书的第一章!📚 在这里,我们将共同踏上探索 Drogon —— 一个现代、高性能的 C++ Web 框架的旅程。无论是您刚接触 Web 开发的 C++ 新手,还是希望在后端服务中压榨出极致性能的资深工程师,Drogon 都可能为您提供一种强大的新选择。本章旨在为您构建一个清晰的认知框架,了解 Drogon 是什么,它解决了哪些问题,以及为何值得您投入时间学习。同时,我们也会明确本书的内容结构和建议的学习路径,帮助您根据自身情况找到最适合的阅读方式。

1.1 什么是 Drogon?

首先,让我们来解答最核心的问题:Drogon 是什么? 🤔

简单来说,Drogon 是一个基于 C++11/14/17/20 标准的高性能 HTTP 应用框架(HTTP Application Framework)。它的诞生是为了解决 C++ 在构建 Web 应用,特别是高性能、异步非阻塞的后端服务时所面临的挑战。Drogon 的设计目标是兼顾性能、易用性以及丰富的现代 Web 开发特性。

Drogon 的核心是一个高性能的、多线程的、事件驱动的 I/O 网络库。它构建于一些成熟的 C++ 库之上,例如:

⚝ libuv:提供跨平台的异步 I/O 能力。
⚝ hiredis:用于连接 Redis 数据库。
⚝ libpq:用于连接 PostgreSQL 数据库。
⚝ libmysqlclient:用于连接 MySQL 数据库。
⚝ OpenSSL:提供加密和安全功能(如 HTTPS)。
⚝ zlib:提供压缩功能。
⚝ jsoncpp 或其他 JSON 库:用于 JSON 数据处理。
⚝ uuid:用于生成通用唯一标识符。
⚝ c-ares:用于异步 DNS 解析。

这些底层库的组合使得 Drogon 能够高效地处理大量的并发连接和 I/O 操作。但 Drogon 不仅仅是一个网络库,它是一个全栈式的 Web 框架。这意味着它提供了构建现代 Web 应用所需的各种组件,包括但不限于:

⚝ 高性能的 HTTP 服务器(HTTP Server)。
⚝ 灵活的路由系统(Routing System)。
⚝ 基于类的控制器(Controller)和函数式请求处理器(Function Handler)。
⚝ 对 WebSocket 的一流支持。
⚝ 内置的 ORM (Object-Relational Mapping) 框架,支持多种数据库。
⚝ 强大的中间件(Middleware)和过滤器(Filter)机制。
⚝ 支持多种模板引擎(Template Engine)进行视图渲染(View Rendering)。
⚝ 内置的缓存(Cache)系统。
⚝ 简单的依赖注入(Dependency Injection)机制。
⚝ 对 C++20 协程/纤程(Coroutine/Fiber)的良好支持,极大地简化异步编程。
⚝ 用于生成代码和管理项目的命令行工具(CLI Tool)。

Drogon 的核心价值在于它提供了一套完整的、高度优化的工具集和开发范式,让 C++ 开发者能够以相对高效的方式构建出媲美甚至超越其他高性能语言(如 Go)或框架的 Web 服务,尤其在处理高并发和低延迟的场景下,Drogon 的优势尤为突出。它不像一些轻量级库那样只提供基础的网络通信能力,也不像一些庞大的企业级框架那样臃肿复杂,而是力图在功能完备性和性能之间找到一个绝佳的平衡点。

1.2 为何选择 Drogon?

理解了什么是 Drogon 后,接下来的问题是:在众多 Web 开发语言和框架中,我们为何要选择 Drogon,特别是用 C++ 来做 Web 开发? 🤔

选择 Drogon 的理由主要体现在以下几个方面:

极致的高性能(High Performance): 这是 Drogon 最引以为傲的特性之一。由于 C++ 作为一种编译型语言,其执行效率天然就很高。而 Drogon 在此基础上,采用了事件驱动(Event-Driven)、非阻塞 I/O (Non-blocking I/O) 的设计,并结合多线程处理模式,使得其在处理大量并发连接时能够保持非常低的延迟和很高的吞吐量。与 Python、Ruby、PHP 等解释型或半解释型语言的框架相比,C++ 的原生性能优势是巨大的。即使与 Node.js 或 Go 等强调性能的语言相比,Drogon 在许多基准测试中也表现出色。

优秀的异步编程支持: 传统的 C++ 网络编程往往需要复杂的回调函数(Callback)来处理异步操作,这容易导致“回调地狱”(Callback Hell)。Drogon 借鉴了其他现代语言的异步模型,并紧密结合 C++20 的协程(Coroutine)特性,在其内部实现了纤程(Fiber)机制。这使得开发者可以用接近同步编程(Synchronous Programming)的直观方式编写异步代码(Asynchronous Code),极大地提高了开发效率和代码的可读性,同时又享受着异步非阻塞带来的高性能优势。

拥抱现代 C++ (Modern C++): Drogon 充分利用了 C++11 及更高版本(特别是 C++14, C++17, C++20)的新特性,如智能指针(Smart Pointer)、Lambda 表达式(Lambda Expression)、右值引用(Rvalue Reference)、自动类型推导(auto)、协程(Coroutine)等。这些特性使得 C++ 代码更加安全、简洁和富有表达力。对于熟悉现代 C++ 的开发者来说,Drogon 的代码风格会显得比较自然和舒适。

功能丰富且易于使用: 尽管是高性能框架,Drogon 并未牺牲开发效率。它提供了 Web 开发中常用的各种组件,如功能完备的 ORM、灵活的路由、强大的中间件/过滤器等,这些都极大地减少了开发者需要从头编写的代码量。同时,其命令行工具 drogons 能够快速生成项目骨架、控制器、模型等,进一步提升了开发效率。对于有其他 Web 框架经验的开发者(如 Java Spring, Node.js Express/Koa, Python Django/Flask 等),会发现 Drogon 的某些概念设计(如 MVC 模式、中间件)是熟悉的。

内嵌 HTTP 服务器: Drogon 框架自带高性能的 HTTP 服务器实现,这意味着部署应用时无需依赖 Nginx、Apache 等外部 Web 服务器(尽管在生产环境中通常仍然会配合反向代理使用,以获得更好的负载均衡、SSL 卸载、静态文件服务等能力)。这简化了开发和部署过程。

强大的数据库支持与 ORM: Drogon 内置了强大的 ORM,支持 PostgreSQL、MySQL、SQLite3、Microsoft SQL Server 等多种主流数据库,并提供了方便的工具根据数据库表生成 C++ 模型代码。异步的数据库操作是其亮点之一,能够避免数据库查询阻塞整个事件循环。

总而言之,选择 Drogon 意味着您选择了一个能够提供原生 C++ 性能优势,同时又能利用现代语言特性简化异步编程,并提供一套完整、易用的 Web 开发工具集的框架。它特别适合需要构建对性能要求极高的后端服务、微服务(Microservice)或 API 网关(API Gateway)的场景。

1.3 C++ 在 Web 开发中的地位与挑战

C++ 是一种通用目的的、静态类型、编译型、多范式的编程语言。凭借其强大的性能、对系统资源的精细控制以及庞大的生态系统,C++ 在操作系统、游戏开发、高性能计算、嵌入式系统等领域占据着核心地位。然而,在传统的 Web 开发领域,特别是构建快速迭代、面向用户的 Web 应用时,C++ 并非主流选择。

这是因为 C++ 在 Web 开发中存在一些固有的挑战:

开发效率相对较低: 相较于 Python、Ruby、Node.js 等动态语言或拥有强大运行时支持的语言(如 Java、C#),C++ 的开发周期往往更长。这涉及到手动内存管理(虽然现代 C++ 的智能指针和 RAII 极大地改善了这一点)、编译时间较长、缺乏像其他语言那样丰富的 Web 开发生态库等因素。
复杂性: C++ 语言本身比较复杂,掌握曲线陡峭。在没有好的框架支持下,处理网络通信、HTTP 协议解析、异步 I/O 等底层细节需要大量的样板代码(Boilerplate Code)和深厚的系统编程知识。
安全性风险: C++ 的一些特性(如裸指针、手动内存管理)如果使用不当,容易引入内存安全问题(如缓冲区溢出、悬垂指针),这些在网络服务中可能成为安全漏洞。

尽管存在这些挑战,C++ 在特定的 Web 开发场景下却具有不可替代的优势:

极致性能与低延迟: 对于需要处理海量请求、要求极低响应时间的服务(如金融交易系统、游戏服务器、实时数据处理、高性能 API 网关),C++ 的原生性能是其他语言难以匹敌的。
系统资源控制: C++ 允许开发者对内存、CPU 等系统资源进行精细控制,这对于构建资源密集型应用或在资源受限环境中部署服务至关重要。
与现有 C++ 代码集成: 如果您的核心业务逻辑已经使用 C++ 实现,那么选择一个 C++ Web 框架可以方便地将这些现有代码暴露为 Web 服务或 API,避免跨语言调用带来的开销和复杂性。

Drogon 正是为了应对这些挑战而诞生的。 🚀

它通过以下方式弥合了 C++ 与 Web 开发之间的鸿沟:

提供结构化的框架: Drogon 提供了一套清晰的架构(如 MVC-like),将 HTTP 处理、路由、控制器、模型等职责分离,降低了复杂性。
封装底层网络细节: 开发者无需直接操作 Socket 或处理复杂的 epoll/kqueue/IOCP 等 I/O 复用机制,Drogon 已经为您封装好了。
简化异步编程: 如前所述,通过 Fiber 机制让异步代码看起来像同步代码,降低了异步编程的难度。
提供常用组件: 内置 ORM、日志、配置、缓存等模块,减少了对外部库的依赖和集成成本。
提高开发效率: 命令行工具 drogons 自动化了许多重复性工作,加速了项目搭建和代码生成。
拥抱现代 C++: 鼓励并利用现代 C++ 的安全和便利特性,减少传统 C++ 的陷阱。

因此,Drogon 让 C++ 在 Web 开发领域有了用武之地,它使得构建高性能 Web 服务不再是少数专家的领域,而是对广大的 C++ 开发者开放了一扇新的大门。

1.4 本书结构与学习路径

本书旨在为不同水平的 C++ 开发者提供一条学习 Drogon 的清晰路径。我们从基础概念出发,逐步深入到框架的各个核心组件和高级特性。

本书的结构如下:

基础入门 (章节 1-4):
▮▮▮▮这些章节将帮助您了解 Drogon 的基本概念,完成开发环境搭建,并掌握 HTTP 请求/响应的处理以及路由和控制器的使用。这是所有读者都应仔细阅读的基础部分。

核心技术与常用功能 (章节 5-8):
▮▮▮▮深入探讨 Drogon 的异步核心(事件循环、纤程),这是理解 Drogon 高性能的关键。同时介绍数据库集成(ORM)、中间件/过滤器以及视图渲染,这些是构建实际应用不可或缺的功能。

高级话题与进阶应用 (章节 9-15):
▮▮▮▮这些章节覆盖更高级的主题,如复杂的路由、WebSocket、配置与日志、测试、应用安全、部署与性能优化,以及框架的扩展机制(插件、自定义组件)。这些内容将帮助您构建更健壮、安全、可维护和高性能的应用。

实战案例 (章节 16):
▮▮▮▮通过几个不同类型的实战案例,综合运用前面章节学到的知识,让您看到如何在实际项目中落地 Drogon。

附录 (附录 A-F):
▮▮▮▮提供配置参考、故障排除、工具函数、ORM 命令、依赖库以及 C++20 应用等附加信息,方便查阅和深入学习。

建议的学习路径:

初学者 (Beginners): 建议按章节顺序从头开始阅读。重点关注章节 1-4,理解基础概念和开发流程。然后可以跳到章节 6 (数据库)、章节 8 (视图/静态文件)、章节 11 (配置/日志),先学习如何构建一个简单的、功能完整的 Web 应用。待对基础有信心后,再回过头来学习章节 5 (异步/纤程)、章节 7 (中间件/过滤器) 等,逐步深入。
中级开发者 (Intermediate): 假定您已经熟悉 C++ 和 Web 开发的基本概念。建议从章节 1 开始快速浏览,重点阅读章节 5 (异步/纤程) 以理解 Drogon 的核心机制。然后按章节顺序深入学习各个功能模块(6-15),结合实际项目需求选择性地深入研究。
专家 (Experts): 假定您对 C++ 和高性能网络编程已有较深理解。您可以将本书作为一本参考手册,重点关注 Drogon 特有的高级特性(如纤程的实现细节、插件机制、性能调优)以及与其他框架设计的对比。您可以快速阅读章节 1-4,然后重点深入章节 5、7、9、10、13、14、15,并参考附录。鼓励阅读 Drogon 框架的源代码,结合本书内容进行更深入的学习。

无论您选择哪条路径,都强烈建议您动手实践书中的代码示例。编程是一门实践性很强的学科,只有通过动手,您才能真正掌握 Drogon。

1.5 目标读者与预备知识

本书的目标读者广泛,涵盖了对使用 C++ 构建 Web 服务感兴趣的不同背景的开发者:

希望利用 C++ 的高性能来构建后端服务、API 或微服务的开发者。
希望学习现代 C++ 在 Web 开发中应用的开发者。
其他语言(如 Java, Python, Node.js, Go 等)的开发者,希望了解和掌握 C++ Web 框架。
计算机科学与技术相关专业的学生,希望学习高性能网络编程和 Web 框架设计。

为了能够顺利地阅读本书并掌握 Drogon,您需要具备以下预备知识:

扎实的 C++ 基础: 您应该熟悉 C++ 语言的基本语法、面向对象编程(OOP)概念(类、对象、继承、多态等)、模板(Template)的基本使用、标准模板库 (STL) 的常用容器(如 vector, map, string)和算法。
对现代 C++ 有所了解更佳: 熟悉 C++11/14/17 的一些关键特性(如 Lambda、智能指针、右值引用、移动语义)将有助于您更好地理解 Drogon 的代码和设计模式。章节 5 将介绍 C++20 协程相关的背景知识,但如果您已经有所了解,学习起来会更轻松。
计算机网络基础: 对 HTTP 协议(请求方法、状态码、头部等)、TCP/IP 协议有基本的了解。
数据库基础: 如果您计划使用 Drogon 的 ORM 或数据库功能,需要对关系型数据库的基本概念和 SQL 语言有所了解。
基本命令行操作: Drogon 的项目管理和部分工具通过命令行(Command Line Interface, CLI)使用。

请注意,本书虽然会从入门讲起,但由于 Drogon 框架本身涉及高性能和异步编程,部分章节内容会有一定的深度。如果您在某些地方感到困难,不必气馁,可以先跳过,待有了更多实践经验后再回过头来攻克。重要的是保持热情和持续实践。

现在,让我们准备好环境,开始构建我们的第一个 Drogon 应用吧!

2. 环境搭建与第一个 Drogon 应用

本章将带领读者踏上 Drogon 的实战之旅。我们将从零开始,指导读者在常见的操作系统上完成 Drogon 框架及其必要依赖的安装。接着,学习如何利用 Drogon 提供的命令行工具创建新的项目骨架,并深入解析项目结构的各个组成部分。最后,我们将通过编写并运行一个简单的“Hello World”示例,让读者亲身体验 Drogon 应用的开发流程,为后续深入学习打下坚实的基础。

2.1 Drogon 的安装与依赖

构建任何软件项目的第一步都是搭建合适的开发环境。对于 Drogon 这个基于 C++ 的高性能 Web 框架而言,这意味着需要安装 C++ 编译器、构建工具以及 Drogon 自身及其所需的第三方库。Drogon 的安装过程相对标准化,主要依赖于 C++ 包管理器或者直接从源代码构建。

2.1.1 操作系统支持与准备

Drogon 旨在支持多种主流操作系统,包括但不限于:

⚝ Linux (如 Ubuntu, CentOS, Fedora 等)
⚝ macOS
⚝ Windows (通过 MSVC, MinGW-w64 或 Cygwin)

在开始安装 Drogon 之前,请确保您的系统已安装以下基本组件:

C++ 编译器(C++ Compiler): Drogon 推荐使用支持 C++14 或更高标准的现代编译器。建议使用 GCC (>= 5.0), Clang (>= 3.9) 或 MSVC (>= VS2019)。这些编译器提供了必要的语言特性和优化,以充分发挥 Drogon 的性能。
构建工具(Build Tool): Drogon 使用 CMake (>= 3.5) 进行项目管理和构建。CMake 是一个跨平台的开源构建系统生成工具。
Git: Drogon 源码通常通过 Git 获取,并且一些依赖库的获取也可能依赖 Git。

2.1.2 Drogon 的核心依赖

Drogon 框架自身依赖于一些高性能的 C++ 库来实现其功能。这些依赖通常会在安装 Drogon 时自动处理,但了解它们有助于理解框架底层。核心依赖通常包括:

libuvasio:用于实现底层的事件循环和异步 I/O。libuv 是一个高性能的跨平台异步 I/O 库,广泛应用于 Node.js 等项目;asio 是 Boost 社区的一个异步 I/O 库,也是 C++ 标准库 <networking> 的基础。Drogon 默认使用 libuv。
jsoncppcjson:用于处理 JSON (JavaScript Object Notation) 数据。
libssl/libcrypto (OpenSSL):用于实现 HTTPS (Hypertext Transfer Protocol Secure) 和加密功能。
zlibbrotli:用于 HTTP 压缩。
uuid:用于生成通用唯一识别码(UUID)。

可选的数据库驱动依赖,如果您需要使用 Drogon 的 ORM (Object-Relational Mapping) 或数据库功能:

libpq (PostgreSQL 客户端库)
libmysqlclient (MySQL 客户端库)
sqlite3 (SQLite 数据库库)

2.1.3 安装步骤 (以 Ubuntu/Debian 为例)

在大多数基于 Debian/Ubuntu 的 Linux 发行版上,可以通过包管理器安装部分依赖,然后编译安装 Drogon。

安装基础依赖

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 sudo apt update
2 sudo apt install -y git cmake build-essential libssl-dev uuid-dev zlib1g-dev

如果需要数据库支持,额外安装:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # PostgreSQL
2 sudo apt install -y libpq-dev
3 # MySQL
4 sudo apt install -y libmysqlclient-dev
5 # SQLite3
6 sudo apt install -y libsqlite3-dev

安装 Drogon (推荐从源码安装)
从 GitHub 克隆 Drogon 仓库:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 git clone https://github.com/drogonframework/drogon.git
2 cd drogon
3 git submodule update --init

使用 CMake 构建和安装:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 mkdir build
2 cd build
3 # 配置构建选项,可以根据需求开启/关闭某些模块,例如:
4 # cmake .. # 默认构建
5 # cmake .. -DUSE_MYSQL=on -DUSE_POSTGRESQL=on # 开启MySQL和PostgreSQL支持
6 cmake ..
7 make # 或者 make -jN,N为您希望使用的CPU核心数以加速构建
8 sudo make install

安装完成后,Drogon 库和头文件会被安装到系统目录中。

2.1.4 安装步骤 (以 macOS 为例)

在 macOS 上,通常使用 Homebrew 包管理器安装依赖和 Drogon。

安装 Homebrew
如果尚未安装 Homebrew,请访问其官方网站(https://brew.sh/)按照指引安装。

安装依赖

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 brew update
2 brew install cmake openssl@1.1 # 或者 openssl@3 根据系统需要
3 brew install libuv # 如果使用 libuv 作为事件循环
4 brew install jsoncpp uuid

如果需要数据库支持,额外安装:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # PostgreSQL
2 brew install libpq
3 # MySQL
4 brew install mysql-client
5 # SQLite3
6 brew install sqlite3

注意:Homebrew 安装的库可能链接在特定的路径下,CMake 在构建时可能需要额外的提示。

安装 Drogon
可以使用 Homebrew 安装,或者从源码安装。推荐从源码安装以便控制构建选项。
从源码安装步骤与 Linux 类似,只是在 cmake .. 步骤可能需要指定 OpenSSL 的路径,例如:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 git clone https://github.com/drogonframework/drogon.git
2 cd drogon
3 git submodule update --init
4 mkdir build
5 cd build
6 # 根据 brew info openssl@1.1 的输出,找到lib路径并指定
7 # 示例:cmake .. -DOPENSSL_ROOT_DIR=/usr/local/opt/openssl@1.1
8 cmake ..
9 make -j$(sysctl -n hw.ncpu) # 使用所有CPU核心构建
10 sudo make install

2.1.5 安装步骤 (以 Windows 为例)

在 Windows 上安装 C++ 库可能稍微复杂一些,可以使用 MSVC 配合 vcpkg 或通过 MinGW-w64/Cygwin 环境。推荐使用 MSVC + vcpkg。

安装 Visual Studio
安装 Visual Studio (推荐 VS2019 或更新版本) 并确保勾选了“使用 C++ 的桌面开发”工作负载。

安装 vcpkg
vcpkg 是微软提供的 C/C++ 库管理器。从 GitHub 克隆 vcpkg 仓库并运行其引导脚本:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 git clone https://github.com/microsoft/vcpkg.git
2 cd vcpkg
3 .\bootstrap-vcpkg.bat # 或 .\bootstrap-vcpkg.sh on Linux/macOS

设置环境变量集成到 Visual Studio (可选,但推荐):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 .\vcpkg integrate install

使用 vcpkg 安装 Drogon 和依赖
在 vcpkg 目录下执行:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 安装基础依赖
2 vcpkg install libuv jsoncpp openssl uuid zlib brotli --triplet x64-windows # 根据系统架构选择 triplet
3 # 安装数据库依赖 (按需选择)
4 vcpkg install postgresql mysql sqlite3 --triplet x64-windows
5 # 安装 drogon
6 vcpkg install drogon --triplet x64-windows

vcpkg 会自动处理 Drogon 的依赖关系并进行编译安装。安装路径在 vcpkg 目录下的 installed 子目录中。

配置 Visual Studio 项目
创建新的 C++ 项目时,需要配置 CMake,使其能够找到 vcpkg 安装的库。如果您运行了 vcpkg integrate install,这通常会自动完成。否则,需要在 CMakeLists.txt 中指定 vcpkg 的 toolchain 文件。

重要提示:安装过程中遇到的具体问题可能因操作系统版本、现有软件环境等因素而异。如果遇到问题,请优先查阅 Drogon 官方文档或社区寻求帮助。

2.2 创建新项目:使用 drogons CLI 工具

安装完 Drogon 框架后,最便捷的创建新项目的方式是使用 Drogon 提供的命令行工具 drogons。这个工具可以快速生成一个包含基本结构和配置的项目骨架,极大地提高了开发效率。

2.2.1 drogons 工具简介

drogons 是 Drogon 框架自带的项目管理和辅助工具。它提供了创建项目、生成控制器、模型、视图等多种功能。在 Drogon 安装成功后,drogons 可执行文件通常会被安装到系统的 PATH 环境变量所包含的目录中,或者 Drogon 的安装目录下。您可以在终端中输入 drogons --version 来检查是否安装成功以及版本信息。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogons --version

如果命令可用并显示版本号,说明安装成功。

2.2.2 使用 drogons create project 创建项目

创建新项目非常简单,只需要打开终端,切换到您希望创建项目的父目录,然后运行 drogons create project 命令,并指定项目名称。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 切换到您希望创建项目的目录
2 cd /path/to/your/projects
3
4 # 创建一个名为 my_drogon_app 的新项目
5 drogons create project my_drogon_app

执行此命令后,drogons 工具会在当前目录下创建一个名为 my_drogon_app 的新文件夹,并在其中生成项目的基本文件和目录结构。

2.2.3 创建项目时的选项

drogons create project 命令还提供了一些可选参数,以便在创建项目时进行定制:

-a <IP地址>--host <IP地址>:指定应用监听的 IP 地址(默认为 0.0.0.0,即所有可用地址)。
-p <端口号>--port <端口号>:指定应用监听的端口号(默认为 8000)。
-s <线程数>--threads <线程数>:指定工作线程数(默认为 1,建议在生产环境设置为 CPU 核心数)。
-m <模式>--mode <模式>:指定项目模式,可以是 development (开发) 或 release (发布)(默认为 development)。

例如,创建一个监听在 127.0.0.1:8080 端口,使用 4 个线程的开发模式项目:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogons create project my_api -h 127.0.0.1 -p 8080 -s 4 -m development

这些配置选项会被写入到项目自动生成的配置文件中(通常是 config.json),您之后也可以手动修改。

2.2.4 项目生成后的文件结构预览

成功创建项目后,进入项目目录 (cd my_drogon_app),您会看到类似如下的目录结构:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 my_drogon_app/
2 ├── CMakeLists.txt # 项目的 CMake 构建脚本
3 ├── config.json # 项目的配置文件
4 ├── main.cc # 应用入口文件
5 ├── build/ # 构建目录 (通常是空的,由 CMake 生成)
6 ├── controllers/ # 存放控制器类
7 ├── views/ # 存放视图模板文件
8 ├── models/ # 存放 ORM 生成的模型类
9 └── src/ # 存放业务逻辑相关的源代码

这个结构是 Drogon 推荐的标准项目布局,有助于组织代码和资源。后续章节将详细解析这些目录和文件的作用。

2.3 项目结构解析

理解一个框架的项目结构对于高效开发至关重要。Drogon 生成的项目骨架遵循了一定的约定,旨在帮助开发者更好地组织代码。本节将详细解析这些核心目录和文件的作用。

2.3.1 核心目录详解

build/
这个目录用于存放 CMake 生成的构建文件和编译后的可执行文件、库文件等。初次创建项目时,这个目录是空的。运行 CMake 命令后,它会被填充。通常,我们会在这个目录中执行 makecmake --build . 命令。

controllers/
这是存放控制器(Controller)类的地方。控制器是处理 HTTP 请求的核心组件。您编写的用于响应特定 URL 的类文件(.h.cc.cpp)通常放在这里。drogons create controller 命令会在这里生成新的控制器文件。

views/
这个目录用于存放视图(View)文件,特别是使用 Drogon 内置模板引擎(或集成的第三方模板引擎)的模板文件。这些文件通常包含 HTML (Hypertext Markup Language) 结构以及模板语法,用于动态生成响应内容。

models/
如果您使用 Drogon 的 ORM (Object-Relational Mapping) 功能与数据库交互,这个目录将存放由 drogons create model 命令根据数据库表结构生成的 C++ 模型类文件。每个模型类通常对应数据库中的一张表,提供了方便的数据操作方法。

src/
这是一个通用的源代码目录。您可以将除了控制器、视图、模型之外的、与具体业务逻辑相关的 C++ 源代码文件放在这里。例如,各种服务类(Service)、工具类、数据处理模块等。

2.3.2 关键文件详解

CMakeLists.txt
这是整个项目的 CMake 构建脚本。它定义了项目的名称、依赖项、源文件、编译选项等。CMake 工具会读取这个文件来生成特定平台的构建系统(如 Makefile, Visual Studio Solution 等)。您在项目中添加新的 .cc/.cpp 源文件时,通常需要修改这个文件以包含它们。

config.json
这是 Drogon 应用程序的主要配置文件。它采用 JSON (JavaScript Object Notation) 格式,包含了服务器监听地址、端口、线程数、日志设置、数据库连接信息、静态文件路径、HTTPS 配置等各种运行时选项。drogons create project 命令生成的默认配置会写入这个文件。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // config.json 示例
2 {
3 "listeners": [
4 {
5 "ip": "0.0.0.0",
6 "port": 8000,
7 "send_file_guarantee": false
8 }
9 ],
10 "app": {
11 "document_root": "./",
12 "upload_path": "/tmp",
13 "max_connections": 100000,
14 "max_connections_per_ip": 0,
15 "max_queued_requests": 100000,
16 "client_max_body_size": 1048576,
17 "case_insensitive_url": true,
18 "log_path": ".",
19 "log_file": "",
20 "log_level": "DEBUG",
21 "enable_sendfile": true,
22 "enable_gzip": true,
23 "enable_brotli": false,
24 "idle_connection_timeout": 60,
25 "keep_alive_timeout": 120,
26 "gzip_minimum_length": 1024,
27 "ssl_certificate_file": "",
28 "ssl_private_key_file": "",
29 "use_ssl": false,
30 "load_date_time": true,
31 "supported_http_versions": [ "1.1", "2" ],
32 "session_and_cookie": {
33 "session_timeout": 1200,
34 "session_id_length": 32,
35 "session_double_check": false,
36 "cookie_max_age": 86400,
37 "cookie_secure": false,
38 "cookie_httponly": false,
39 "locale_cookie_name": "drogon_locale",
40 "locale_cookie_timeout": 864000
41 },
42 "enable_server_header": true,
43 "enable_date_header": true,
44 "synchronous_publish_on_destroy": true,
45 "app_views_path": "views",
46 "codegen_output_dir": ".",
47 "plugins_path": "",
48 "ext_files_types": [
49 "html", "js", "css", "xml", "jpg", "png", "gif", "jpeg", "ico", "swf", "svg", "ttf", "woff2", "woff", "otf", "eot", "mp4", "webm", "ogg", "mp3", "wav", "flac", "aac", "json", "pdf", "gz", "zip", "tar", "bz2", "xz", "txt", "csv", "md", "cpp", "h", "hpp", "cc", "c"
50 ],
51 "intranet_ip_prefix": [],
52 "json_checker_path": "",
53 "redis_client_manager": [],
54 "enable_redis": false,
55 "orm": [],
56 "real_ip_headers": [],
57 "log_stream_redirect_to_file": false
58 },
59 "db_clients": [],
60 "plugins": [],
61 "routers": [],
62 "filters": []
63 }

请注意,这个示例配置文件是 Drogon 默认生成的一部分内容,实际内容可能因版本和创建选项而略有差异。我们会 在后续章节详细介绍各个配置项的作用。

main.cc
这是应用程序的入口文件,包含了 main 函数。在典型的 Drogon 应用中,main 函数主要负责加载配置文件、初始化 Drogon 应用程序实例、注册控制器和过滤器、设置监听地址和端口等,然后启动事件循环,开始接受和处理请求。这个文件通常不需要频繁修改,除非需要进行一些全局的初始化或配置。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // main.cc 示例骨架
2 #include <drogon/drogon.h>
3
4 int main() {
5 // Load config file
6 drogon::app().loadConfigFile("../config.json");
7 // Register controllers and filters, etc. (done via code generation or manual registration)
8 // drogon::app().registerHandler(...);
9
10 // Run the app
11 drogon::app().run();
12
13 return 0;
14 }

这里的 ../config.json 路径是相对于 build 目录而言的,因为通常我们在 build 目录下编译和运行程序。

2.4 运行与构建项目

创建项目骨架只是第一步,接下来需要将 C++ 源代码编译成可执行程序,然后运行它。Drogon 项目使用 CMake 进行构建,这使得构建过程跨平台且灵活。

2.4.1 使用 CMake 构建项目

在项目根目录创建 build 目录并进入是 CMake 的标准实践:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 cd my_drogon_app # 进入您的项目目录
2 mkdir build # 创建构建目录
3 cd build # 进入构建目录

接下来,运行 cmake 命令配置项目。这个命令会读取项目根目录下的 CMakeLists.txt 文件,检查系统的编译器和依赖库,并生成用于实际编译的构建文件(如 Linux/macOS 下的 Makefile 或 Windows 下的 Visual Studio Solution)。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 在 build 目录下执行
2 cmake ..

这里的 .. 表示 CMakeLists.txt 文件在当前目录的上一级。

如果您希望构建特定类型的项目(例如,发布模式进行优化),可以在 CMake 配置时指定构建类型:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 构建发布 Release 版本,通常带有优化
2 cmake .. -DCMAKE_BUILD_TYPE=Release
3
4 # 构建调试 Debug 版本,方便调试
5 cmake .. -DCMAKE_BUILD_TYPE=Debug

如果没有显式指定 CMAKE_BUILD_TYPE,默认通常是 Debug。

配置成功后,就可以使用构建工具进行实际的编译了。在 Linux/macOS 上,使用 make 命令:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 在 build 目录下执行
2 make
3 # 或者使用多线程编译以加快速度,N 为线程数,例如 4
4 make -j4

在 Windows 上使用 Visual Studio Solution,可以在 IDE 中打开生成的 .sln 文件进行构建;或者在命令行使用 cmake --build . 命令(它会自动调用 MSBuild):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 在 build 目录下执行
2 cmake --build . --config Release # 构建 Release 版本

编译成功后,您会在 build 目录下找到生成的可执行文件,其名称通常与项目名称相同(例如 my_drogon_app)。

2.4.2 运行 Drogon 应用

编译生成可执行文件后,就可以运行它了。确保您在 build 目录下执行可执行文件,这样程序才能正确地找到 config.json 文件(因为默认路径是 ../config.json)。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 在 build 目录下执行
2 ./my_drogon_app

或者在 Windows 上:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 在 build 目录下执行
2 .\Release\my_drogon_app.exe # 如果构建的是 Release 版本

当 Drogon 应用程序启动后,您会在终端看到类似以下的输出信息,表明服务器正在监听指定的 IP 地址和端口:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ...
2 [DEBUG] trantor version: ...
3 [DEBUG] drogon version: ...
4 ...
5 [INFO] listening on 0.0.0.0:8000
6 ...

此时,您的第一个 Drogon Web 服务器就已经成功运行起来了!您可以打开浏览器或使用 curl 工具尝试访问 http://127.0.0.1:8000。当然,目前这个应用还没有处理任何具体的 URL,访问会得到一个默认的 404 Not Found 响应,这是正常的。

2.4.3 开发模式与发布模式 (Development vs. Release)

在 Drogon 项目中,通过 config.json 或创建项目时的 -m 参数可以指定运行模式。这通常影响日志级别、错误处理行为、性能优化等。

Development (开发模式)
▮▮▮▮⚝ 通常日志级别设置为 DEBUG 或 TRACE,输出详细的调试信息。
▮▮▮▮⚝ 可能启用更多的断言和检查,以帮助发现潜在问题。
▮▮▮▮⚝ 构建时可能不进行最高级别的优化,以便于调试。
▮▮▮▮⚝ 适合在开发阶段使用。
Release (发布模式)
▮▮▮▮⚝ 日志级别通常设置为 INFO 或 WARNING,只输出重要信息,减少日志量。
▮▮▮▮⚝ 禁用调试相关的检查和断言,提高运行时性能。
▮▮▮▮⚝ 构建时会启用最高级别的优化(如 -O3),追求极致性能。
▮▮▮▮⚝ 适合在生产环境部署运行。

在 CMake 构建时通过 -DCMAKE_BUILD_TYPE=Release 指定构建类型,并在 config.json 中设置 "app": { "mode": "release", ... } 是在发布环境中运行 Drogon 应用的标准做法。

2.5 编写一个简单的 Hello World

现在,我们来编写一个最简单的 Drogon 应用程序:一个响应 / 路径,并返回“Hello World!”文本的 Web 端点(Endpoint)。这将帮助您理解如何在 Drogon 中注册路由和处理请求。

2.5.1 创建一个简单的控制器

虽然可以使用函数处理器,但更常见和推荐的方式是使用控制器类 (HttpController)。我们可以使用 drogons 工具生成一个控制器骨架。

在项目根目录下执行:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 cd my_drogon_app # 确保在项目根目录
2 drogons create controller HelloWorldCtrl

这个命令会在 controllers 目录下创建 HelloWorldCtrl.hHelloWorldCtrl.cc 两个文件,并自动添加到 CMakeLists.txt 中(如果您是在项目创建后执行)。

打开 controllers/HelloWorldCtrl.h 文件,您会看到类似如下的结构:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #pragma once
2
3 #include <drogon/HttpController.h>
4
5 using namespace drogon;
6
7 class HelloWorldCtrl : public drogon::HttpController<HelloWorldCtrl>
8 {
9 public:
10 METHOD_LIST_BEGIN
11 // use METHOD_ADD to add your custom processing function here;
12 // METHOD_ADD(HelloWorldCtrl::get,"/{2}",Get);// path format like /<drogon::uuid>
13 // METHOD_ADD(HelloWorldCtrl::syncCoroApi,"/path",Get,Post);
14 // METHOD_ADD(HelloWorldCtrl::asyncApi,"/path",Get,Post);
15 METHOD_LIST_END
16 // your declaration of processing function>>>>>>>>>>>>>>>>>>
17 // void get(const HttpRequestPtr& req,
18 // std::function<void (const HttpResponsePtr &)> &&callback,
19 // drogon::uuid id);
20
21 // void syncCoroApi(const HttpRequestPtr &req,
22 // std::function<void(const HttpResponsePtr &)> &&callback);
23
24 // void asyncApi(const HttpRequestPtr &req,
25 // std::function<void(const HttpResponsePtr &)> &&callback);
26 // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
27 };

METHOD_LIST_BEGINMETHOD_LIST_END 宏用于定义控制器中的请求处理方法及其对应的路由。注释中提供了一些示例。我们将添加一个处理 GET 请求到 / 路径的方法。

修改 controllers/HelloWorldCtrl.h,添加一个公共方法和在 METHOD_LIST_BEGIN/END 中注册它:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #pragma once
2
3 #include <drogon/HttpController.h>
4
5 using namespace drogon;
6
7 class HelloWorldCtrl : public drogon::HttpController<HelloWorldCtrl>
8 {
9 public:
10 METHOD_LIST_BEGIN
11 // 添加一个处理根路径 "/" 的 GET 请求的方法
12 METHOD_ADD(HelloWorldCtrl::get,"/",Get); // 注册 GET 请求到 "/" 路径,由 get 方法处理
13 METHOD_LIST_END
14
15 // 声明用于处理 GET 请求的方法
16 void get(const HttpRequestPtr& req,
17 std::function<void (const HttpResponsePtr &)> &&callback);
18 };

现在,打开 controllers/HelloWorldCtrl.cc 实现这个 get 方法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include "HelloWorldCtrl.h"
2
3 // 实现 get 方法
4 void HelloWorldCtrl::get(const HttpRequestPtr& req,
5 std::function<void (const HttpResponsePtr &)> &&callback)
6 {
7 // 创建一个 HttpResponse 对象,状态码 200 OK,内容类型 text/plain
8 auto resp = HttpResponse::newHttpResponse(k200OK, CT_TEXT_PLAIN);
9 // 设置响应体内容
10 resp->setBody("Hello World!");
11 // 调用 callback 函数将响应发送给客户端
12 callback(resp);
13 }

代码解释

HttpRequestPtr& req: 这是一个智能指针,指向代表当前 HTTP 请求的对象。通过它可以获取请求的各种信息(方法、URL、头部、参数、请求体等)。
std::function<void (const HttpResponsePtr &)> &&callback: 这是一个回调函数。在 Drogon 的异步模型中,当您完成请求处理并准备好响应时,需要调用这个 callback 函数,并将构建好的 HttpResponsePtr 对象作为参数传递进去。Drogon 的事件循环会在后台负责将响应发送给客户端。
HttpResponse::newHttpResponse(...): 这是一个静态方法,用于创建一个新的响应对象。k200OK 是 Drogon 提供的 HTTP 状态码枚举值,代表 200 OK。CT_TEXT_PLAIN 是 Drogon 提供的 MIME (Multipurpose Internet Mail Extensions) 类型枚举值,代表纯文本。
resp->setBody("Hello World!"): 设置响应体的内容为字符串 "Hello World!"。
callback(resp): 调用回调函数,将构建好的响应发送回去。

2.5.2 构建和运行项目

修改了源文件后,需要重新编译项目。回到 build 目录并执行编译命令:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 cd my_drogon_app/build # 进入构建目录
2 make # 或者 cmake --build .

如果编译成功,现在可以运行可执行文件:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ./my_drogon_app

2.5.3 测试 Hello World

应用程序启动并监听端口后,打开浏览器或使用 curl 访问 http://127.0.0.1:8000/ (如果您的配置端口是 8000)。

在浏览器中,您应该能看到页面显示:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 Hello World!

使用 curl

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 curl http://127.0.0.1:8000/

输出:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 Hello World!

恭喜您!🎉 您已经成功创建、构建并运行了您的第一个 Drogon Web 应用程序,并实现了一个简单的“Hello World”接口。这标志着您已经掌握了 Drogon 开发环境的基础搭建和基本流程。

3. 核心概念:HTTP 请求与响应处理

本章深入讲解 Drogon 如何表示和处理 HTTP 请求(Request)和响应(Response)对象。理解这两个核心对象是构建任何基于 Drogon 的 Web 应用的基础。我们将详细解析请求对象的各个组成部分以及如何构建不同类型的响应,并探讨请求在 Drogon 中的完整生命周期。🚀

3.1 HttpRequest 对象深度解析

HttpRequest 对象是 Drogon 接收到的客户端 HTTP 请求的抽象表示。它包含了客户端请求的所有信息,例如请求方法、URL、头部、Cookies、请求体等。在 Drogon 的请求处理函数(包括函数处理器、控制器方法、中间件和过滤器)中,通常都会接收到一个 HttpRequest 对象作为输入参数。本节将详细介绍如何访问和使用 HttpRequest 对象的各个属性。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 示例: 在控制器方法中接收 HttpRequest
2 void handleRequest(const HttpRequestPtr& req, std::function<void (const HttpResponsePtr &)> &&callback);

3.1.1 请求方法与 URL (Method & URL)

请求方法 (Method): HTTP 请求方法(如 GET, POST, PUT, DELETE 等)表示了客户端希望对资源执行的操作类型。在 Drogon 中,可以通过 HttpRequest 对象的成员函数获取请求方法。
▮▮▮▮req->method(): 返回一个枚举类型 HttpMethod,表示请求方法。
▮▮▮▮req->methodString(): 返回请求方法的字符串表示(例如 "GET", "POST")。

示例代码:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 获取请求方法
2 HttpMethod method = req->method();
3 std::string method_str = req->methodString();
4
5 if (method == HttpMethod::Get)
6 {
7 // 处理 GET 请求
8 LOG_INFO << "Received GET request";
9 }
10 else if (method == HttpMethod::Post)
11 {
12 // 处理 POST 请求
13 LOG_INFO << "Received POST request";
14 }

请求 URL (URL): 请求 URL 指明了客户端请求的资源路径。这包括路径本身、查询参数、以及可能的片段标识符(尽管片段标识符不会发送到服务器)。
▮▮▮▮req->path(): 返回请求的路径部分(不包含查询参数)。例如,对于 http://example.com/users/1?name=testpath() 返回 /users/1
▮▮▮▮req->getQuery(): 返回完整的查询字符串。例如,对于 http://example.com/users?name=test&age=30getQuery() 返回 name=test&age=30
▮▮▮▮req->getParameter(const std::string& key): 根据键获取查询参数或 POST 表单参数的值。
▮▮▮▮req->getParameters(): 获取所有查询参数和 POST 表单参数的键值对列表。

示例代码:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 获取请求路径
2 std::string request_path = req->path();
3 LOG_INFO << "Request path: " << request_path;
4
5 // 获取查询参数
6 std::string name = req->getParameter("name");
7 int age = std::stoi(req->getParameter("age")); // 注意类型转换和错误处理
8
9 LOG_INFO << "Query parameters: name=" << name << ", age=" << age;
10
11 // 获取所有参数
12 const auto& params = req->getParameters();
13 for (const auto& [key, value] : params)
14 {
15 LOG_INFO << "Parameter: " << key << " = " << value;
16 }

3.1.2 请求头部与 Cookies (Headers & Cookies)

请求头部 (Headers): HTTP 头部包含客户端、请求主体、连接信息等元数据。常见的头部包括 User-Agent, Content-Type, Authorization, Cookie 等。
▮▮▮▮req->getHeader(const std::string& key): 根据头部名称获取头部值(不区分大小写)。
▮▮▮▮req->getHeaders(): 获取所有请求头部的键值对列表。

示例代码:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 获取特定的请求头部
2 std::string user_agent = req->getHeader("User-Agent");
3 LOG_INFO << "User-Agent: " << user_agent;
4
5 // 获取所有头部
6 const auto& headers = req->getHeaders();
7 for (const auto& [key, value] : headers)
8 {
9 LOG_INFO << "Header: " << key << " = " << value;
10 }

Cookies: Cookies 是存储在客户端的小段文本信息,用于在多次请求中维护状态。它们通过 Cookie 头部发送给服务器。Drogon 提供了便捷的方式来访问这些 Cookies。
▮▮▮▮req->getCookie(const std::string& key): 根据 Cookie 名称获取 Cookie 值。
▮▮▮▮req->getCookies(): 获取所有 Cookies 的键值对列表。

示例代码:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 获取特定的 Cookie
2 std::string session_id = req->getCookie("session_id");
3 if (!session_id.empty())
4 {
5 LOG_INFO << "Session ID: " << session_id;
6 }
7
8 // 获取所有 Cookies
9 const auto& cookies = req->getCookies();
10 for (const auto& [key, value] : cookies)
11 {
12 LOG_INFO << "Cookie: " << key << " = " << value;
13 }

3.1.3 请求体与参数 (Body & Parameters)

请求体(Body)包含 POST、PUT 等请求方法发送的数据。数据格式可以是多种多样的,例如表单数据 (application/x-www-form-urlencodedmultipart/form-data)、JSON (application/json)、XML (application/xml) 等。

获取原始请求体:
▮▮▮▮req->getBody(): 返回请求体的原始数据 const char* 指针。
▮▮▮▮req->getBodyPtr(): 返回请求体的原始数据 const std::shared_ptr<std::string>&
▮▮▮▮req->bodyLength(): 返回请求体的大小。

示例代码:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 获取原始请求体数据
2 if (req->bodyLength() > 0)
3 {
4 std::string request_body(req->getBody(), req->bodyLength());
5 LOG_INFO << "Raw request body: " << request_body;
6 }

解析表单参数: 对于 application/x-www-form-urlencodedmultipart/form-data 类型的请求体,参数会自动被解析并可以通过 getParameter()getParameters() 访问,这与查询参数的处理方式一致。

示例代码(与查询参数示例类似):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 假设请求体是 x-www-form-urlencoded 或 multipart/form-data
2 std::string username = req->getParameter("username");
3 std::string password = req->getParameter("password");
4 LOG_INFO << "Form data: username=" << username << ", password=" << password;

解析 JSON 请求体: 如果请求的 Content-Typeapplication/json,Drogon 会自动尝试将请求体解析为 JSON 对象。
▮▮▮▮req->getJsonObject(): 返回一个 const Json::Value* 指针,指向解析后的 JSON 对象。如果解析失败或不是 JSON 请求,返回 nullptr

示例代码:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 假设请求体是 JSON
2 const auto& json = req->getJsonObject();
3 if (json)
4 {
5 // 使用 jsoncpp 库访问 JSON 数据
6 std::string name = (*json)["name"].asString();
7 int age = (*json)["age"].asInt();
8 LOG_INFO << "JSON data: name=" << name << ", age=" << age;
9 } else {
10 LOG_ERROR << "Failed to parse JSON body or body is not JSON";
11 }

3.1.4 文件上传处理 (File Upload)

对于 multipart/form-data 请求,如果包含文件字段,Drogon 会将其作为上传文件处理。
▮▮▮▮req->getFiles(): 返回一个 const std::vector<UploadFile>&,包含所有上传的文件信息。

UploadFile 对象提供了访问文件信息的方法:
▮▮▮▮getFileName(): 获取原始文件名。
▮▮▮▮getFileExtension(): 获取文件扩展名。
▮▮▮▮getFileType(): 获取文件的 MIME 类型。
▮▮▮▮save(): 将文件保存到指定路径。
▮▮▮▮length(): 获取文件大小。
▮▮▮▮content(): 获取文件内容(std::string)。注意:对于大文件,获取全部内容到内存可能导致 OOM(Out Of Memory)。
▮▮▮▮getFilePath(): 获取文件在临时存储位置的路径。

示例代码:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 处理文件上传
2 const auto& uploaded_files = req->getFiles();
3 for (const auto& file : uploaded_files)
4 {
5 LOG_INFO << "Uploaded file: " << file.getFileName();
6 LOG_INFO << "File type: " << file.getFileType();
7 LOG_INFO << "File size: " << file.length() << " bytes";
8
9 // 将文件保存到服务器指定目录
10 std::string save_path = "./uploads/" + file.getFileName(); // 请确保目录存在且可写
11 if (file.save(save_path))
12 {
13 LOG_INFO << "File saved to: " << save_path;
14 }
15 else
16 {
17 LOG_ERROR << "Failed to save file: " << file.getFileName();
18 }
19
20 // 或者获取文件内容(仅适用于小文件)
21 // std::string content = file.content();
22 // LOG_INFO << "File content (first 100 bytes): " << content.substr(0, 100);
23 }

3.2 HttpResponse 对象构建

HttpResponse 对象表示服务器将发送给客户端的 HTTP 响应。处理函数需要构建并返回一个 HttpResponse 对象(或其智能指针 HttpResponsePtr),或者通过回调函数异步地发送响应。Drogon 提供了多种便捷的方法来构建各种类型的响应。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 示例: 在控制器方法中返回 HttpResponsePtr
2 HttpResponsePtr handleRequest(const HttpRequestPtr& req);
3
4 // 示例: 使用回调函数发送响应 (异步)
5 void handleRequest(const HttpRequestPtr& req, std::function<void (const HttpResponsePtr &)> &&callback)
6 {
7 // ... 构建响应 ...
8 HttpResponsePtr resp = HttpResponse::newHttpResponse();
9 // ... 设置响应内容 ...
10 callback(resp); // 通过回调函数发送响应
11 }

HttpResponse::newHttpResponse() 是创建新响应对象的常用方法。创建后,可以设置其各个属性。

3.2.1 设置状态码 (Status Code)

HTTP 状态码(Status Code)表示服务器对请求的处理结果。常见的状态码包括 200 OK, 201 Created, 400 Bad Request, 404 Not Found, 500 Internal Server Error 等。
▮▮▮▮resp->setStatusCode(HttpStatusCode code): 设置响应的状态码。HttpStatusCode 是 Drogon 定义的一个枚举类型。

示例代码:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 HttpResponsePtr resp = HttpResponse::newHttpResponse();
2
3 // 设置为 200 OK
4 resp->setStatusCode(HttpStatusCode::k200Ok);
5
6 // 设置为 404 Not Found
7 // HttpResponsePtr resp_not_found = HttpResponse::newHttpResponse();
8 // resp_not_found->setStatusCode(HttpStatusCode::k404NotFound);

3.2.2 设置响应头部与 Cookies (Headers & Cookies)

设置响应头部 (Headers): 可以通过 addHeader()setContentTypeCode() 设置响应头部。
▮▮▮▮resp->addHeader(const std::string& key, const std::string& value): 添加或覆盖一个响应头部。
▮▮▮▮resp->setContentTypeCode(ContentType code): 设置 Content-Type 头部,ContentType 是 Drogon 定义的枚举。
▮▮▮▮resp->setContentTypeString(const std::string& type): 直接设置 Content-Type 头部字符串。

示例代码:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 HttpResponsePtr resp = HttpResponse::newHttpResponse();
2 resp->setStatusCode(HttpStatusCode::k200Ok);
3
4 // 设置 Content-Type 为 application/json
5 resp->setContentTypeCode(ContentType::kContentTypeApplicationJson);
6
7 // 添加自定义头部
8 resp->addHeader("X-My-Header", "Drogon is great");

设置 Cookies: 可以通过 addCookie() 方法在响应中设置 Cookies,浏览器接收到响应后会保存这些 Cookies。
▮▮▮▮resp->addCookie(const Cookie& cookie): 添加一个 Cookie 对象。Cookie 类用于构建 Cookie,可以设置名称、值、过期时间、域名、路径等属性。

示例代码:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 HttpResponsePtr resp = HttpResponse::newHttpResponse();
2 resp->setStatusCode(HttpStatusCode::k200Ok);
3
4 // 创建一个 Cookie 对象
5 Cookie session_cookie("session_id", "abc123xyz");
6 session_cookie.setPath("/");
7 session_cookie.setHttpOnly(true);
8 session_cookie.setMaxAge(3600); // 有效期1小时
9
10 // 添加 Cookie 到响应
11 resp->addCookie(session_cookie);

3.2.3 构建不同类型的响应体 (Body)

响应体包含了实际返回给客户端的数据。Drogon 提供了多种方法来设置不同格式的响应体。
▮▮▮▮resp->setBody(const std::string& body): 设置响应体为字符串。
▮▮▮▮resp->setBody(std::string&& body): 设置响应体为字符串(右值引用)。
▮▮▮▮HttpResponse::newHttpResponse(const std::string& body, HttpStatusCode status = HttpStatusCode::k200Ok, ContentType type = ContentType::kContentTypeTextPlain): 快速创建带字符串响应体的响应。
▮▮▮▮HttpResponse::newHttpJsonResponse(const Json::Value& json): 快速创建 JSON 响应。同时自动设置 Content-Typeapplication/json
▮▮▮▮HttpResponse::newFileResponse(const std::string& filepath, const std::string& attachmentFileName = ""): 创建文件下载或显示响应。
▮▮▮▮HttpResponse::newStreamResponse(std::string fileName): 创建流式文件下载响应(Drogon 1.8.0+ 支持)。
▮▮▮▮HttpResponse::newRedirectionResponse(const std::string& location, HttpStatusCode status = HttpStatusCode::k302Found): 创建重定向响应。

示例代码:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 创建一个文本响应
2 HttpResponsePtr text_resp = HttpResponse::newHttpResponse("Hello, World!");
3 text_resp->setStatusCode(HttpStatusCode::k200Ok);
4 text_resp->setContentTypeCode(ContentType::kContentTypeTextPlain); // 可选,newHttpResponse 默认就是 text/plain
5
6 // 创建一个 HTML 响应
7 HttpResponsePtr html_resp = HttpResponse::newHttpResponse("<h1>Welcome</h1><p>This is an HTML response.</p>");
8 html_resp->setStatusCode(HttpStatusCode::k200Ok);
9 html_resp->setContentTypeCode(ContentType::kContentTypeTextHtml);
10
11 // 创建一个 JSON 响应
12 Json::Value json_data;
13 json_data["message"] = "Success";
14 json_data["data"]["id"] = 123;
15 HttpResponsePtr json_resp = HttpResponse::newHttpJsonResponse(json_data); // 自动设置 Content-Type: application/json 和状态码 200
16
17 // 创建一个文件下载响应
18 // HttpResponsePtr file_resp = HttpResponse::newFileResponse("./path/to/your/file.pdf", "report.pdf");
19 // file_resp->setStatusCode(HttpStatusCode::k200Ok); // 文件响应通常默认 200

3.2.4 重定向响应 (Redirect)

重定向响应指示客户端跳转到另一个 URL。常用的状态码是 302 Found(临时重定向)和 301 Moved Permanently(永久重定向)。
▮▮▮▮HttpResponse::newRedirectionResponse(const std::string& location, HttpStatusCode status = HttpStatusCode::k302Found): 创建一个重定向响应,并设置 Location 头部为目标 URL。

示例代码:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 创建一个临时重定向响应到 /new_page
2 HttpResponsePtr redirect_resp = HttpResponse::newRedirectionResponse("/new_page");
3
4 // 创建一个永久重定向响应到外部网站
5 // HttpResponsePtr permanent_redirect_resp = HttpResponse::newRedirectionResponse("https://www.google.com", HttpStatusCode::k301MovedPermanently);

3.3 请求与响应生命周期

理解一个 HTTP 请求在 Drogon 中的完整生命周期对于开发和调试非常重要。这个生命周期是一个线性的处理管道,请求会依次经过不同的阶段。

整个流程可以概括如下:

接收连接与解析请求:
▮▮▮▮Drogon 的事件循环(Event Loop)接受新的 TCP 连接。
▮▮▮▮读取客户端发送的数据流,并解析为 HttpRequest 对象。如果请求无效(例如格式错误),可能会直接返回错误响应或关闭连接。

路由匹配 (Routing):
▮▮▮▮Drogon 的路由系统根据 HttpRequest 的方法和路径,查找匹配的请求处理器(函数、控制器或 WebSocket 控制器)。
▮▮▮▮如果找到匹配的路由,则确定需要执行的过滤器(Filter)和中间件(Middleware)。
▮▮▮▮如果没有找到匹配的路由,通常会返回 404 Not Found 响应。

过滤器链执行 (Filter Chain):
▮▮▮▮在请求到达最终的处理函数之前,会依次执行与该路由关联的所有请求过滤器。
▮▮▮▮每个过滤器都可以选择继续处理请求(调用链中的下一个组件),或直接生成响应并终止后续处理(例如,认证过滤器发现用户未登录,直接返回 401 Unauthorized 响应)。

中间件执行 (Middleware):
▮▮▮▮在过滤器之后,请求会依次通过关联的请求中间件。
▮▮▮▮中间件可以在请求处理前修改请求对象,在请求处理后修改响应对象,或者在请求处理前后执行一些逻辑(例如日志记录、性能统计)。

请求处理器执行 (Request Handler):
▮▮▮▮如果请求通过了所有过滤器和中间件的前置处理,最终会到达匹配的请求处理器(控制器方法、函数等)。
▮▮▮▮请求处理器接收 HttpRequest 对象,执行业务逻辑(例如访问数据库、调用其他服务),并构建 HttpResponse 对象。
▮▮▮▮业务逻辑可能涉及到异步操作(如数据库访问、网络请求),在这里,Drogon 的事件循环和协程/纤程机制发挥作用,避免阻塞主线程。

中间件后置处理:
▮▮▮▮在请求处理器生成 HttpResponse 对象后,响应会反向依次通过请求中间件的后置处理部分。中间件可以在这里修改响应(例如添加头部、压缩响应体)。

生成原始响应数据:
▮▮▮▮HttpResponse 对象被序列化为原始 HTTP 响应字节流,包括状态行、头部和响应体。

发送响应与关闭连接:
▮▮▮▮ Drogon 的事件循环将原始响应数据发送回客户端。
▮▮▮▮根据 HTTP/1.0 或 HTTP/1.1 的连接管理(Connection header),决定是保持连接复用还是关闭连接。

整个过程如下图所示(简化流程):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 graph LR
2 A(客户端发送请求) --> B(Drogon接收请求);
3 B --> C(解析HttpRequest);
4 C --> D(路由匹配);
5 D -- 匹配成功 --> E(过滤器链);
6 E -- 通过过滤器 --> F(中间件前置);
7 F -- 通过前置 --> G(请求处理器\n执行业务逻辑);
8 G -- 生成HttpResponse --> H(中间件后置);
9 H -- 通过后置 --> I(序列化HttpResponse);
10 I --> J(发送响应);
11 J --> K(连接管理\n(保持或关闭));
12 D -- 匹配失败 --> L(生成404响应);
13 L --> J;
14 E -- 过滤器拒绝 --> M(生成拒绝响应);
15 M --> J;

理解这个生命周期有助于开发者确定在哪里插入逻辑(例如,使用过滤器进行身份验证,使用中间件进行日志记录,使用控制器处理核心业务),以及如何调试请求处理流程中的问题。🕵️‍♂️

4. 路由系统与控制器(Controller)

欢迎来到 Drogon Web 框架学习的第四章!在这一章中,我们将深入探讨 Drogon 的核心机制之一:路由系统。路由(Routing)是 Web 框架的基础,它决定了 incoming HTTP 请求如何被分发到相应的代码逻辑进行处理。掌握 Drogon 的路由机制,是构建任何实际应用的关键。同时,我们将学习如何使用 Drogon 提供的两种主要请求处理器:基于函数的处理器(Function Handlers)和基于类的控制器(Class-based Controllers)。本章还将结合路由和控制器,讨论如何设计和实现符合 RESTful 风格的 API,并了解当多个路由规则匹配同一个请求时的优先级处理。

通过本章的学习,您将能够:
⚝ 理解 Drogon 路由的基本原理和不同类型的路由规则。
⚝ 掌握如何使用 Lambda 函数或普通函数快速定义简单的请求处理器。
⚝ 学会如何创建和使用 Drogon 的 HttpController 类来组织复杂的请求处理逻辑。
⚝ 了解如何在控制器方法中便捷地获取 URL 路径中的参数。
⚝ 熟悉 RESTful API 设计的基本原则,并在 Drogon 中实现它们。
⚝ 理解 Drogon 路由的匹配顺序和优先级规则。

4.1 Drogon 路由机制

Drogon 的路由系统负责将接收到的 HTTP 请求的 URL 路径、HTTP 方法等信息与预先注册的处理器(Handler)进行匹配,并将请求导向匹配成功的处理器进行处理。Drogon 支持多种类型的路由规则,以满足不同的需求。

① 静态路由(Static Routing)
这是最简单直观的路由类型,用于匹配固定的 URL 路径。
例如,/users/about/contact 这样的路径。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogon::app().registerHandler(
2 "/hello",
3 [](const drogon::HttpRequestPtr& req,
4 std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
5 auto resp = drogon::HttpResponse::newHttpResponse();
6 resp->setBody("Hello, World!");
7 callback(resp);
8 });

上面的代码注册了一个静态路由 /hello,当收到对该路径的 GET 请求时,将执行 Lambda 函数处理器。

② 参数路由(Parameter Routing)
参数路由允许在 URL 路径中定义可变的部分,这些可变部分通常代表资源的标识符或其他参数。这些参数可以在处理器中被捕获和使用。
在 Drogon 中,参数通常用尖括号 <> 包围,并且可以指定参数类型。
例如,/users/{id} 可以匹配 /users/123/users/abc
可以通过 <int><string> 等指定类型。未指定类型时默认为 <std::string>

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogon::app().registerHandler(
2 "/users/{id}",
3 [](const drogon::HttpRequestPtr& req,
4 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
5 int id) { // id 参数会自动从路径中绑定
6 auto resp = drogon::HttpResponse::newHttpResponse();
7 resp->setBody("Fetching user with ID: " + std::to_string(id));
8 callback(resp);
9 });

上面的代码匹配 /users/{id},并将路径中的 id 部分作为 int 类型参数传递给处理器。

③ 正则表达式路由(Regex Routing)
对于更复杂的 URL 匹配模式,Drogon 支持使用正则表达式。这提供了极大的灵活性,可以匹配复杂的路径结构或验证路径参数的格式。
正则表达式路由通常以 regex: 开头。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogon::app().registerHandler(
2 "regex:/items/([0-9]+)",
3 [](const drogon::HttpRequestPtr& req,
4 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
5 const std::string& itemId) { // 捕获组 ([0-9]+) 会作为参数传递
6 auto resp = drogon::HttpResponse::newHttpResponse();
7 resp->setBody("Fetching item with regex ID: " + itemId);
8 callback(resp);
9 });

上面的代码使用正则表达式 /items/([0-9]+) 匹配路径,并将括号 () 中的第一个捕获组(一个或多个数字)作为 std::string 参数传递给处理器。

不同的 HTTP 方法 (GET, POST, PUT, DELETE 等) 可以注册到同一个路径上,由 Drogon 自动根据请求方法分发。

4.2 基于函数的请求处理器 (Function Handlers)

基于函数的请求处理器是一种轻量级的方式,适用于处理逻辑相对简单或只需要单个端点(Endpoint)的情况。您可以使用 Lambda 函数或普通函数来定义处理器。

① 使用 Lambda 函数作为处理器
Lambda 函数非常方便,可以直接在注册路由的地方定义处理逻辑。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/drogon.h>
2
3 int main() {
4 drogon::app().registerHandler(
5 "/", // 匹配根路径
6 [](const drogon::HttpRequestPtr& req, // 请求对象
7 std::function<void(const drogon::HttpResponsePtr&)>&& callback) { // 响应回调
8 auto resp = drogon::HttpResponse::newHttpResponse();
9 resp->setStatusCode(drogon::k200OK); // 设置状态码
10 resp->setContentTypeCode(drogon::CT_TEXT_PLAIN); // 设置内容类型
11 resp->setBody("Welcome to Drogon!"); // 设置响应体
12 callback(resp); // 发送响应
13 },
14 {drogon::Get}); // 只处理 GET 请求
15
16 drogon::app().registerHandler(
17 "/submit_form",
18 [](const drogon::HttpRequestPtr& req,
19 std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
20 // 获取 POST 请求的表单参数
21 auto params = req->getPostParameters();
22 std::string name = params.count("name") ? params["name"] : "Anonymous";
23
24 auto resp = drogon::HttpResponse::newHttpResponse();
25 resp->setStatusCode(drogon::k200OK);
26 resp->setContentTypeCode(drogon::CT_TEXT_PLAIN);
27 resp->setBody("Hello, " + name + "!");
28 callback(resp);
29 },
30 {drogon::Post}); // 只处理 POST 请求
31
32 drogon::app().run();
33 return 0;
34 }

Lambda 函数处理器通常接受 const drogon::HttpRequestPtr& 作为第一个参数,以及一个 std::function<void(const drogon::HttpResponsePtr&)>&& 作为第二个参数用于发送响应。如果路由中定义了参数(如 /users/{id}),则这些参数会作为后续的参数传递给 Lambda 函数。

② 使用普通函数作为处理器
您也可以定义一个独立的普通函数来作为处理器,然后将函数指针或函数对象注册到路由中。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/drogon.h>
2
3 void my_handler(const drogon::HttpRequestPtr& req,
4 std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
5 auto resp = drogon::HttpResponse::newHttpResponse();
6 resp->setStatusCode(drogon::k200OK);
7 resp->setBody("Handled by a regular function!");
8 callback(resp);
9 }
10
11 int main() {
12 drogon::app().registerHandler("/by_function", &my_handler, {drogon::Get});
13 drogon::app().run();
14 return 0;
15 }

这种方式有助于代码复用,但对于每个需要参数绑定的路径,您可能需要定义不同签名的函数,不如 Lambda 或控制器灵活。

基于函数的处理器简单易用,适合快速原型开发或小型项目。然而,当处理逻辑变得复杂,需要共享状态、依赖其他组件或涉及多个相关端点时,基于类的控制器通常是更好的选择。

4.3 基于类的控制器 (Class-based Controllers)

基于类的控制器是 Drogon 推荐的组织请求处理逻辑的方式,特别是在构建大型或复杂的应用时。Drogon 提供了 HttpController 基类,您可以继承它来创建自己的控制器类。一个控制器类通常包含处理特定 HTTP 方法(GET, POST, PUT, DELETE 等)的方法,这些方法与路由规则关联。

使用类的好处包括:
⚝ 更好的代码组织和模块化。
⚝ 可以定义成员变量来存储状态或依赖(例如数据库连接)。
⚝ 继承 HttpController 可以方便地访问 HttpRequestPtrHttpResponsePtr 等对象以及其他辅助方法。

4.3.1 生成与声明控制器

使用 Drogon 的命令行工具 drogons 是生成控制器代码的最便捷方式。在项目根目录中,您可以运行:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogons create controller UserController

这将生成 UserController.hUserController.cc 两个文件,其中包含一个继承自 drogon::HttpController<UserController> 的类骨架。

UserController.h (示例)

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #pragma once
2
3 #include <drogon/HttpController.h>
4
5 class UserController : public drogon::HttpController<UserController> {
6 public:
7 METHOD_LIST_BEGIN
8 // Add method list here
9 // METHOD_ADD(UserController::get, "/users/{id}", drogon::Get);
10 METHOD_LIST_END
11
12 void get(const drogon::HttpRequestPtr& req,
13 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
14 int id);
15
16 void update(const drogon::HttpRequestPtr& req,
17 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
18 int id);
19 };

UserController.cc (示例)

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include "UserController.h"
2
3 void UserController::get(const drogon::HttpRequestPtr& req,
4 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
5 int id) {
6 // 处理 GET /users/{id} 请求的逻辑
7 auto resp = drogon::HttpResponse::newHttpResponse();
8 resp->setBody("Fetching user " + std::to_string(id) + "...");
9 callback(resp);
10 }
11
12 void UserController::update(const drogon::HttpRequestPtr& req,
13 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
14 int id) {
15 // 处理 PUT /users/{id} 请求的逻辑
16 auto resp = drogon::HttpResponse::newHttpResponse();
17 resp->setBody("Updating user " + std::to_string(id) + "...");
18 callback(resp);
19 }

UserController.h 中的 METHOD_LIST_BEGINMETHOD_LIST_END 宏之间,您需要使用 METHOD_ADDMETHOD_ADD_API 宏来声明控制器方法与路由的映射关系。

声明控制器:
在您的应用程序初始化代码中(通常在 main.cc 中),Drogon 会自动发现并注册继承自 HttpController 的类。您不需要手动调用 registerHandler 来注册整个控制器。只需要在类的定义中正确使用宏即可。

4.3.2 处理方法的定义

控制器中的处理方法(例如上面示例中的 getupdate 方法)负责接收请求、执行业务逻辑并生成响应。这些方法的签名有一定的约定:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 void YourController::your_method_name(const drogon::HttpRequestPtr& req,
2 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
3 /* 路径参数 */);

⚝ 第一个参数是 const drogon::HttpRequestPtr& req,表示当前的 HTTP 请求对象。
⚝ 第二个参数是 std::function<void(const drogon::HttpResponsePtr&)>&& callback,这是一个回调函数,您需要调用它并将生成的 HttpResponsePtr 作为参数传递,以便 Drogon 将响应发送回客户端。
⚝ 后续参数是可选的,用于接收从 URL 路径中提取的参数(详见下一小节)。

使用 METHOD_ADD 宏将方法映射到路由:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // In UserController.h
2 METHOD_LIST_BEGIN
3 // METHOD_ADD(类名::方法名, 路由路径, HTTP方法, 过滤器1, 过滤器2, ...);
4 // 路径中的参数名必须与方法参数名匹配,例如这里的 {id} 对应方法参数 int id
5 METHOD_ADD(UserController::get, "/users/{id}", Get);
6 METHOD_ADD(UserController::update, "/users/{id}", Put);
7 METHOD_ADD(UserController::create, "/users", Post); // 例如,创建一个新用户
8 METHOD_LIST_END

您还可以在 METHOD_ADD 宏中指定一个或多个过滤器(Filter),以便在请求到达处理方法之前执行预处理逻辑(如认证、日志记录等)。过滤器将在第 7 章详细介绍。

4.3.3 路径参数绑定

Drogon 的路由系统支持将参数路由(如 /users/{id})中的变量直接绑定到控制器方法的参数上。参数名(花括号 {} 中的部分)必须与控制器方法的相应参数名相同,并且 Drogon 会尝试根据参数类型进行自动转换。

支持的参数类型包括但不限于:
⚝ 基本数值类型:int, long, float, double 等。
⚝ 字符串类型:std::string
⚝ 其他可流式输入/输出的类型(如果 Drogon 内置支持或您提供了转换)。

示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // METHOD_ADD(Controller::method, "/items/{item_id}/{action}", Get);
2 void MyController::method(const drogon::HttpRequestPtr& req,
3 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
4 int item_id, // 绑定 {item_id}
5 const std::string& action); // 绑定 {action}

如果 Drogon 无法将路径中的参数转换为方法参数所需的类型,或者缺少必要的参数,请求将无法匹配到该方法,可能会返回 404 Not Found 或 400 Bad Request 错误(取决于具体情况和配置)。

4.4 RESTful API 设计与实现

REST (Representational State Transfer) 是一种架构风格,用于设计网络应用程序接口(API)。它强调资源(Resource)的概念,并使用标准的 HTTP 方法(GET, POST, PUT, DELETE 等)对资源进行操作。利用 Drogon 的路由和控制器,可以非常方便地设计和实现符合 RESTful 风格的 API。

RESTful 设计原则:
面向资源 (Resource-Oriented): API 的核心是资源,每个资源都有一个唯一的标识符(URI)。例如,一个用户资源可以用 /users/{id} 表示,用户集合可以用 /users 表示。
使用标准 HTTP 方法 (Standard HTTP Methods): 使用 HTTP 方法来表示对资源的操作:
▮▮▮▮⚝ GET: 获取资源。
▮▮▮▮⚝ POST: 创建新资源(通常在集合 URI 上)或执行操作。
▮▮▮▮⚝ PUT: 更新/替换整个资源(通常在单个资源 URI 上)。
▮▮▮▮⚝ PATCH: 部分更新资源(通常在单个资源 URI 上)。
▮▮▮▮⚝ DELETE: 删除资源。
无状态 (Stateless): 服务器不存储客户端的状态。每个请求都包含所有必要的信息来完成处理。
统一接口 (Uniform Interface): 通过一致的方式与资源交互,例如使用 HTTP 头来指示媒体类型。
通过超媒体作为应用状态引擎 (HATEOAS - Hypermedia as the Engine of Application State): (这是更高级的 REST 约束,在实践中可能不总是严格遵守) 响应中包含指向相关资源的链接,引导客户端发现可用的操作。

在 Drogon 中实现 RESTful API:
您可以通过为一个资源(如用户)创建一个专门的控制器来实现其相关的 RESTful API 端点。

示例:用户资源 API
创建 UserController (如 4.3.1 节所示)。
UserController.h 中定义方法和路由:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #pragma once
2
3 #include <drogon/HttpController.h>
4 #include <drogon/orm/Mapper.h> // 假设使用 ORM
5
6 // 假设有一个 User 模型类 (由 ORM 生成)
7 // #include "models/User.h"
8
9 class UserController : public drogon::HttpController<UserController> {
10 public:
11 METHOD_LIST_BEGIN
12 // GET /users - 获取所有用户
13 METHOD_ADD(UserController::get_all, "/users", Get);
14 // GET /users/{id} - 获取特定用户
15 METHOD_ADD(UserController::get_one, "/users/{id}", Get);
16 // POST /users - 创建新用户
17 METHOD_ADD(UserController::create, "/users", Post);
18 // PUT /users/{id} - 更新特定用户
19 METHOD_ADD(UserController::update, "/users/{id}", Put);
20 // DELETE /users/{id} - 删除特定用户
21 METHOD_ADD(UserController::delete_one, "/users/{id}", Delete);
22 METHOD_LIST_END
23
24 void get_all(const drogon::HttpRequestPtr& req,
25 std::function<void(const drogon::HttpResponsePtr&)>&& callback);
26
27 void get_one(const drogon::HttpRequestPtr& req,
28 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
29 int id);
30
31 void create(const drogon::HttpRequestPtr& req,
32 std::function<void(const drogon::HttpResponsePtr&)>&& callback);
33
34 void update(const drogon::HttpRequestPtr& req,
35 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
36 int id);
37
38 void delete_one(const drogon::HttpRequestPtr& req,
39 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
40 int id);
41 };

UserController.cc 中实现这些方法,例如使用 ORM 或原始 SQL 进行数据库操作(将在第 6 章详细介绍),并返回 JSON 格式的响应体。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // In UserController.cc
2 #include "UserController.h"
3 #include <drogon/drogon.h>
4 #include <drogon/utils/json.h>
5 // #include "models/User.h" // 包含你的 User 模型
6
7 void UserController::get_all(const drogon::HttpRequestPtr& req,
8 std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
9 // TODO: 从数据库获取所有用户,转换为 JSON 数组
10 Json::Value users_json;
11 // ... fetch users and populate users_json
12 auto resp = drogon::HttpResponse::newHttpJsonResponse(users_json);
13 callback(resp);
14 }
15
16 void UserController::get_one(const drogon::HttpRequestPtr& req,
17 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
18 int id) {
19 // TODO: 从数据库获取 ID 为 id 的用户,转换为 JSON 对象
20 Json::Value user_json;
21 // ... fetch user by id and populate user_json
22 auto resp = drogon::HttpResponse::newHttpJsonResponse(user_json);
23 // 如果用户不存在,可以返回 404
24 // auto resp = drogon::HttpResponse::newHttpResponse();
25 // resp->setStatusCode(drogon::k404NotFound);
26 callback(resp);
27 }
28
29 void UserController::create(const drogon::HttpRequestPtr& req,
30 std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
31 // TODO: 解析请求体 (通常是 JSON),创建新用户并保存到数据库
32 auto json_body = req->getJsonObject();
33 if (!json_body) {
34 auto resp = drogon::HttpResponse::newHttpResponse();
35 resp->setStatusCode(drogon::k400BadRequest);
36 callback(resp);
37 return;
38 }
39 // ... create user from json_body and save
40 auto resp = drogon::HttpResponse::newHttpResponse();
41 resp->setStatusCode(drogon::k201Created); // 资源创建成功返回 201
42 // 可选:设置 Location 头指向新创建的资源 URI
43 // resp->addHeader("Location", "/users/" + std::to_string(new_user_id));
44 callback(resp);
45 }
46
47 // 实现 update 和 delete_one 方法类似...
48 void UserController::update(const drogon::HttpRequestPtr& req,
49 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
50 int id) {
51 // ...
52 }
53
54 void UserController::delete_one(const drogon::HttpRequestPtr& req,
55 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
56 int id) {
57 // ...
58 }

通过这种方式,一个控制器类对应一个资源或一组相关的资源,每个方法处理对该资源的特定操作,清晰地组织了代码,符合 RESTful 的精神。

4.5 路由优先级与冲突解决

在实际应用中,您可能会注册多个路由规则。当一个 incoming HTTP 请求到达时,Drogon 需要确定哪个注册的处理器应该响应这个请求。如果多个路由规则都可能匹配同一个请求路径,Drogon 会按照一定的优先级规则进行匹配,并只将请求分发给优先级最高的一个匹配规则对应的处理器。

Drogon 的路由匹配优先级(从高到低):

正则表达式路由 (Regex Routing): 正则表达式路由通常被认为是最高优先级的,因为它们提供了最灵活但也最复杂的匹配模式。当多个正则表达式路由匹配时,它们的优先级取决于注册的顺序(先注册的可能优先级更高,但也可能受正则表达式本身的复杂度和匹配长度影响)。
静态路由 (Static Routing): 静态路由匹配固定的、完整的路径。例如 /users
参数路由 (Parameter Routing): 参数路由包含可变的部分,例如 /users/{id}。在多个参数路由可能匹配时,更具体的参数路由优先级更高。例如,/users/profile 会优先于 /users/{id} 匹配,因为 /users/profile 是一个更具体的静态路径。带有更多固定部分的参数路由也更具体,例如 /users/{id}/details/users/{id} 更具体。
默认路由 / 根路由 (Default/Root Routing): 例如 /

冲突解决的原则:

精确匹配优先: Drogon 会优先选择能够最精确地匹配请求路径的规则。静态路由比参数路由更精确,更长的静态路径比短的更精确。在参数路由中,固定部分越多、参数类型越明确的路由通常更具体。
注册顺序(在相同优先级或相似匹配度下): 对于相同类型的路由(例如两个参数路由)或者匹配度相似的路由,注册的先后顺序可能会影响最终的匹配结果,通常是先注册的规则优先。但在复杂场景下,特别是正则表达式和参数路由的组合,优先级判断可能比较微妙。

避免路由冲突的建议:

⚝ 设计清晰的 URL 结构,尽量避免歧义。
⚝ 仔细检查您的路由定义,特别是当使用参数路由和正则表达式路由时。
⚝ 在设计 API 时,遵循一致的命名约定和资源路径模式。
⚝ 使用 Drogon 的日志输出(通常在 debug 模式下)来查看路由匹配的过程,帮助诊断为什么某个请求没有被预期的处理器处理。

理解路由优先级对于确保您的应用程序按预期工作至关重要。在定义路由时,应考虑到这些规则,以防止意外的请求分发。

<END_OF_CHAPTER/>

5. 深入异步编程:事件循环与纤程

本章是 Drogon 核心特色的重点,深入讲解其基于事件循环的异步模型以及 C++20 纤程(Fiber)的应用。理解这些概念对于构建高性能、可伸缩的 Web 服务至关重要。我们将从事件驱动和非阻塞 I/O 的基础讲起,逐步揭示 Drogon 如何利用这些技术,并通过强大的纤程功能简化异步编程。

5.1 事件驱动与非阻塞 I/O

在本节中,我们将探讨事件驱动编程(Event-Driven Programming)的核心思想以及非阻塞输入/输出(Non-blocking I/O)在构建现代高性能网络应用中的重要性。

5.1.1 事件驱动编程概述

事件驱动编程是一种编程范式(Programming Paradigm),其程序流程由外部事件(Events)或用户行为(User Actions)决定。与传统的顺序执行或多线程模型不同,事件驱动程序等待事件发生,然后调用相应的事件处理器(Event Handler)来响应事件。

核心概念:
▮▮▮▮⚝ 事件 (Event): 指程序运行过程中发生的有意义的事情,如网络连接建立、数据到达、文件读取完成、定时器到期等。
▮▮▮▮⚝ 事件循环 (Event Loop): 是事件驱动模型的核心,负责监听各种事件源(Event Sources),当事件发生时,将事件分发给对应的事件处理器。事件循环通常在一个独立的线程中运行。
▮▮▮▮⚝ 事件处理器 (Event Handler): 响应特定事件的代码块(通常是函数或方法)。当事件循环检测到某个事件发生时,就会调用注册到该事件上的处理器。

优点:
▮▮▮▮⚝ 资源效率高: 在等待事件发生时,事件循环线程可以处于休眠状态,不占用 CPU 资源。这与传统的多线程模型不同,后者可能需要大量线程,每个线程即使在空闲时也占用一定的系统资源。
▮▮▮▮⚝ 适合 I/O 密集型任务: Web 服务器的典型负载是大量的并发 I/O 操作(处理客户端连接、读写网络套接字、访问数据库、读写文件等)。事件驱动模型特别适合处理这些 I/O 密集型任务,因为它可以在等待一个 I/O 操作完成时,去处理其他准备就绪的 I/O 事件。

5.1.2 非阻塞 I/O 的重要性

I/O 操作(如网络通信、文件读写)通常是耗时的,因为它们涉及与外部设备的交互,速度远慢于 CPU 计算。传统的阻塞 I/O(Blocking I/O)模型在执行 I/O 操作时会暂停(Block)当前线程的执行,直到操作完成。

阻塞 I/O 的问题:
▮▮▮▮⚝ 资源浪费: 当一个线程被阻塞在 I/O 上时,它无法执行任何计算任务,但仍然持有线程资源。在高并发场景下,需要创建大量线程来处理并发连接,导致线程上下文切换开销巨大,甚至耗尽系统资源。
▮▮▮▮⚝ 并发限制: 线程数量受限于系统资源,难以支持极高的并发连接数。

非阻塞 I/O(Non-blocking I/O)模型解决了这些问题。在使用非阻塞 I/O 时,发起 I/O 操作后会立即返回,而不管操作是否完成。如果操作尚未完成,则返回一个特定的错误码(如 EAGAINEWOULDBLOCK)。应用程序可以在不阻塞的情况下继续执行其他任务,并通过某种机制(如 I/O 多路复用,I/O Multiplexing)来检查 I/O 操作的状态。

I/O 多路复用 (I/O Multiplexing): 是一种允许单个线程监视多个文件描述符(File Descriptors)的技术。当任何文件描述符准备就绪(例如,套接字上接收到数据,或者套接字可以发送数据而不会阻塞)时,I/O 多路复用机制会通知应用程序。常见的 I/O 多路复用技术包括 selectpollepoll (Linux)、kqueue (macOS/BSD) 和 IOCP (Windows)。

非阻塞 I/O + 事件驱动的优势:
▮▮▮▮⚝ 高并发: 单个线程或少量线程通过 I/O 多路复用可以同时处理数千甚至数万个并发连接,而无需为每个连接创建线程。
▮▮▮▮⚝ 低开销: 减少了线程数量,从而显著降低了线程创建、销毁和上下文切换的开销。
▮▮▮▮⚝ 响应迅速: 事件驱动模型可以快速响应准备就绪的事件,提高系统的整体响应速度。

Drogon 正是采用了这种非阻塞 I/O 与事件驱动相结合的模型,这也是其实现高性能的关键基础。它在底层使用 epoll/kqueue/IOCP 等技术来高效地管理大量并发连接。

5.2 Drogon 的事件循环 (Event Loop)

理解 Drogon 的事件循环及其多线程模型是理解其异步特性的核心。Drogon 构建在 Libuv(或者类似的 I/O 多路复用库)之上,利用操作系统提供的异步 I/O 能力。

5.2.1 Drogon 的事件循环机制

Drogon 的核心是一个或多个事件循环线程。每个事件循环线程负责监听一组文件描述符(通常是网络套接字)上的事件。

工作流程:
初始化: Drogon 启动时,会根据配置创建一定数量的 I/O 线程(默认为 CPU 核心数)。每个 I/O 线程都运行一个独立的事件循环。
监听事件: 每个事件循环进入一个无限循环,调用底层的 I/O 多路复用 API(如 epoll_wait)等待事件发生。
事件分发: 当 I/O 多路复用 API 返回时,表示一个或多个文件描述符上有事件发生(例如,新的连接到来,数据可读,数据可写等)。事件循环将这些事件封装成任务。
任务处理: 事件循环将任务分发给相应的事件处理器。例如,新连接事件由连接接受器处理;数据可读事件由协议解析器和请求处理器处理。
非阻塞执行: 事件处理器在处理任务时,应尽量避免执行阻塞操作。如果需要执行耗时的阻塞操作(如同步数据库访问、复杂的计算),应该将其Offload到其他线程池执行,而不是在事件循环线程中阻塞。

Drogon 的事件循环是其高性能的基础,它确保了 I/O 密集型任务能够以极高的效率被处理,避免了传统多线程模型中的阻塞和上下文切换开销。

5.2.2 多线程处理模型

虽然 Drogon 使用事件驱动和非阻塞 I/O,但它通常不是单线程的。为了充分利用多核 CPU 的处理能力,Drogon 默认会启动多个 I/O 线程,每个线程都有自己的事件循环。

Drogon 的多线程架构:
主线程 (Main Thread): 负责初始化服务器、加载配置、创建 I/O 线程和工作线程池,以及处理信号等。主线程本身不处理客户端连接的 I/O 事件。
I/O 线程 (I/O Threads): 默认数量通常等于 CPU 核心数。每个 I/O 线程运行一个事件循环,负责接受新连接(所有 I/O 线程轮流或负载均衡地接受连接),并在其事件循环中处理所有与该连接相关的非阻塞 I/O 事件(数据的读取、写入、HTTP 请求的解析、HTTP 响应的构建和发送等)。一个客户端连接从建立到关闭的所有 I/O 事件,通常都由同一个 I/O 线程负责。
工作线程池 (Worker Thread Pool): 用于执行可能阻塞 I/O 线程的耗时任务,例如同步数据库操作、文件 I/O、CPU 密集型计算等。Drogon 的异步数据库访问、文件读写等都可以在这个线程池中完成,从而避免阻塞 I/O 线程。用户也可以通过 drogon::async_run 等方法将自定义的阻塞或耗时任务提交到工作线程池。

请求处理流程与线程:
① 新的客户端连接到达时,由某个 I/O 线程接受。
② 该 I/O 线程在其事件循环中读取请求数据,进行 HTTP 协议解析。
③ 解析出完整的 HttpRequest 对象后,根据路由规则找到对应的请求处理器(可能是控制器方法或函数)。
④ 如果请求处理器是非阻塞的(例如,使用异步数据库 ORM 或纤程 co_await 进行数据库访问),它会在当前的 I/O 线程中执行,并在等待异步结果时让出控制权,不阻塞事件循环。
⑤ 如果请求处理器是阻塞的(例如,执行同步 I/O 或 CPU 密集型计算,或者在未使用纤程时进行数据库操作),则应该将其 Offload 到工作线程池执行。Drogon 的异步 API 已经处理了这一点,它们会在内部将阻塞部分提交到工作线程池。
⑥ 请求处理完成后,生成 HttpResponse 对象,由处理该连接的 I/O 线程将其发送回客户端。

这种模型结合了事件驱动的高并发能力和多线程的并行计算能力,是现代高性能服务器的典型架构。

5.3 回调 (Callback) 与 Promise

在异步编程中,传统上主要使用回调函数(Callback Functions)来处理异步操作完成后的逻辑。而 Promise(或 Future/Promise 模式)是另一种处理异步操作结果的方式,旨在改善回调地狱(Callback Hell)的问题。虽然 Drogon 的核心异步模型现在更多地倾向于使用 C++20 协程/纤程,但理解回调和 Promise 有助于对比并理解纤程的优势。

5.3.1 回调函数

回调函数是一种将函数作为参数传递给另一个函数的方式,这个被传递的函数将在特定事件发生或异步操作完成后被调用。

示例 (概念性,非 Drogon 特有):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 void async_read_file(const std::string& filename, std::function<void(const std::string& content)> callback) {
2 // 模拟异步读取文件
3 // ... 文件读取完成后 ...
4 std::string file_content = "文件内容...";
5 callback(file_content); // 调用回调函数处理结果
6 }
7
8 void process_file_content(const std::string& content) {
9 // 处理文件内容的逻辑
10 std::cout << "处理文件内容: " << content << std::endl;
11 }
12
13 void main() {
14 async_read_file("my_file.txt", process_file_content);
15 // ... 做其他事情 ...
16 }

回调的挑战:
▮▮▮▮⚝ 回调地狱 (Callback Hell): 当存在多个依赖顺序的异步操作时,需要层层嵌套回调函数,导致代码可读性和维护性急剧下降。
▮▮▮▮⚝ 错误处理: 错误处理逻辑通常需要在每个回调函数中单独处理,分散且容易出错。
▮▮▮▮⚝ 控制流复杂: 难以使用传统的控制结构(如 if/elsefor 循环)来组织异步逻辑。

Drogon 的早期版本或某些基于回调的 API 可能使用回调,例如某些低级别的网络操作或文件操作。

5.3.2 Promise (Future/Promise)

Promise 模式是一种处理异步操作结果的对象,它代表了一个可能在未来某个时间点可用的值。它提供了一种更结构化的方式来管理异步操作的成功值或失败原因。在 C++ 中,这通常通过 std::futurestd::promise 来实现。

概念:
▮▮▮▮⚝ Promise: 表示一个承诺在未来完成的操作,通常由异步操作的生产者持有,用于设置结果或异常。
▮▮▮▮⚝ Future: 表示异步操作的结果,由异步操作的消费者持有,可以通过它获取操作的结果(可能阻塞)或注册回调(非阻塞)。

示例 (概念性,C++ std::future):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <future>
2 #include <iostream>
3 #include <thread>
4
5 std::future<std::string> async_read_file_promise(const std::string& filename) {
6 std::promise<std::string> promise;
7 std::future<std::string> future = promise.get_future();
8
9 std::thread([&]() {
10 // 模拟异步读取文件
11 // ... 读取文件 ...
12 std::string file_content = "文件内容...";
13 promise.set_value(file_content); // 设置成功结果
14 // promise.set_exception(...); // 设置异常
15 }).detach(); // 分离线程,实际异步框架不会分离
16
17 return future;
18 }
19
20 void main() {
21 std::future<std::string> file_future = async_read_file_promise("my_file.txt");
22
23 // 在某些框架中,可以注册回调
24 // file_future.then([](const std::string& content) {
25 // std::cout << "处理文件内容: " << content << std::endl;
26 // });
27
28 // 或者在支持异步等待的上下文中使用(如下面介绍的协程)
29 // std::string content = file_future.get(); // 阻塞获取结果
30 // std::cout << "处理文件内容: " << content << std::endl;
31 }

Promise/Future 模式提供了一种链式调用(Chaining)或组合异步操作的方式,比纯回调更易管理。

尽管 C++ 标准库提供了 std::futurestd::promise,它们在异步框架中的使用方式可能不同,并且标准库的实现通常涉及线程阻塞。现代 C++ 异步框架(如 Drogon 利用的协程/纤程)提供了更高效、更易于编写异步代码的机制。

5.4 C++20 协程与 Drogon 纤程 (Fiber)

这是 Drogon 最具吸引力的特性之一。Drogon 利用 C++20 协程(Coroutine)的特性,在用户层实现了轻量级的纤程(Fiber),极大地简化了异步代码的编写,使其看起来像同步代码一样直观。

5.4.1 协程/纤程的基本概念

什么是协程 (Coroutine)?
协程是一种用户空间的轻量级并发原语。与线程(Thread)不同,协程的切换(Yield/Resume)是由用户代码显式控制的,而不是由操作系统调度器抢占式调度。这使得协程的上下文切换开销远小于线程。
协程允许函数在其执行过程中暂停(Suspend)并将控制权交回调用者(或调度器),然后在稍后从暂停的地方恢复(Resume)执行。

协程与线程的区别:
调度方式: 线程由操作系统内核调度;协程由用户代码或协程库调度。
上下文切换开销: 线程切换涉及昂贵的内核态操作和 CPU 缓存失效;协程切换在用户空间完成,开销非常低。
栈空间: 每个线程通常有独立的、较大的内核栈;协程可以使用独立的栈(有栈协程,Stackful Coroutine)或共享栈/无栈(无栈协程,Stackless Coroutine),通常占用内存更少。Drogon 的 Fiber 是有栈协程。
数量: 系统能支持的线程数量有限(几千到几万);协程数量可以非常多(几十万甚至几百万),因为它们更轻量。
编程模型: 传统线程编程需要仔细处理锁、条件变量等同步机制以避免数据竞争;协程在单个线程内执行,可以通过协作式多任务简化同步问题(但并发执行的协程仍需同步)。

什么是纤程 (Fiber)?
纤程通常可以被认为是协程的一种实现,特别是指那种拥有独立栈的有栈协程。Drogon 中的 Fiber 就是基于 C++20 协程实现的有栈纤程。每个 Fiber 都有自己的栈空间,这使得在 Fiber 内部编写的代码可以像普通函数一样使用局部变量,极大地简化了异步状态管理。

为什么协程/纤程适合异步编程?
非阻塞 I/O 和事件驱动虽然高效,但使用回调函数或 Promise 来组织逻辑会使代码变得复杂。协程提供了一种“挂起/恢复”的机制,允许在等待异步操作完成时暂停当前任务,让出 CPU 给事件循环去处理其他准备就绪的任务,待异步操作完成后再恢复执行暂停的任务。这使得原本需要用回调或 Promise 链表达的复杂异步流程,可以像写同步代码一样按顺序编写,提高了代码的可读性和可维护性,同时又保持了底层异步 I/O 的高性能。

5.4.2 在 Drogon 中使用 Fiber

Drogon 利用 C++20 的协程特性,提供了一种在请求处理函数(如控制器方法)或其他适合异步的上下文中使用 Fiber 的能力。核心是使用 co_await 关键字来等待一个异步操作的完成。当遇到 co_await 一个“可等待对象”(Awaitable Object)时,如果该操作尚未完成,当前的 Fiber 就会被挂起,控制权返回给事件循环。当异步操作完成时,事件循环会恢复(Resume)之前挂起的 Fiber,使其从 co_await 的位置继续执行。

启用 C++20 协程:
要使用 Drogon 的 Fiber 功能,你的编译器需要支持 C++20 标准,并且编译时需要启用 C++20 支持(通常是添加 -std=c++20 编译选项)。

使用 co_await 的场景:
在 Drogon 中,许多异步操作都返回可等待对象,可以直接 co_await,例如:
▮▮▮▮⚝ 异步数据库操作 (drogon::orm::DbClient 的异步方法)。
▮▮▮▮⚝ 异步 HTTP 客户端请求 (drogon::HttpClient 的异步方法)。
▮▮▮▮⚝ 异步文件操作 (drogon::FileLogger 等)。
▮▮▮▮⚝ Drogon 提供的一些其他异步工具函数。

示例:在控制器中使用 Fiber 进行异步数据库查询

假设我们有一个 User 模型,我们想根据用户 ID 查询用户,这个查询是异步的。

首先,控制器方法需要返回一个 drogon::AsyncTask 或使用支持协程返回类型的方式(Drogon 对请求处理器的返回值做了特殊处理,可以直接返回 drogon::AsyncTask 或可等待对象)。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/HttpController.h>
2 #include <drogon/orm/Mapper.h>
3 #include <drogon/AsyncTask.h> // 需要包含这个头文件
4
5 // 假设你已经使用 drogons create model 命令生成了 User 模型类
6 #include "models/User.h" // 根据你的模型文件路径调整
7
8 class UserController : public drogon::HttpController<UserController>
9 {
10 public:
11 METHOD_LIST_BEGIN
12 // 注册一个 GET 请求路由,使用 Fiber 处理函数
13 ADD_METHOD_TO(UserController::getUserById, "/users/{id}", drogon::Get);
14 METHOD_LIST_END
15
16 // 使用 drogon::AsyncTask 作为返回类型,表示这是一个协程函数
17 drogon::AsyncTask<drogon::HttpResponsePtr> getUserById(
18 const drogon::HttpRequestPtr& req,
19 int id) const
20 {
21 auto client = drogon::app().getDbClient();
22 // 使用 ORM Mapper
23 drogon::orm::Mapper<drogon_model::your_namespace::User> userMapper(client);
24
25 try
26 {
27 // 使用 co_await 等待异步数据库查询结果
28 // findByPrimaryKey 是 ORM 提供的异步方法,返回一个可等待对象
29 auto user = co_await userMapper.findByPrimaryKey(id);
30
31 // 如果找到用户
32 Json::Value userData;
33 userData["id"] = user.getValueOfId();
34 userData["name"] = user.getValueOfName();
35 // ... 获取其他字段 ...
36
37 auto resp = drogon::HttpResponse::newHttpJsonResponse(userData);
38 co_return resp; // 使用 co_return 返回 HttpResponse
39
40 } catch (const drogon::orm::UnexpectedRows& err)
41 {
42 // 未找到用户
43 LOG_WARN << "User not found: " << id;
44 auto resp = drogon::HttpResponse::newHttpResponse(drogon::k404NotFound);
45 co_return resp; // 使用 co_return 返回错误响应
46 } catch (const drogon::orm::DrogonDbException& err)
47 {
48 // 数据库错误
49 LOG_ERROR << "Database error: " << err.what();
50 auto resp = drogon::HttpResponse::newHttpResponse(drogon::k500InternalServerError);
51 co_return resp; // 使用 co_return 返回错误响应
52 }
53 }
54 };

代码解析:
▮▮▮▮⚝ ① 控制器方法 getUserById 的返回类型是 drogon::AsyncTask<drogon::HttpResponsePtr>。这告诉 Drogon 这是一个协程函数,它的结果是 drogon::HttpResponsePtr
▮▮▮▮⚝ ② 在函数体内,我们使用 co_await userMapper.findByPrimaryKey(id);findByPrimaryKey 是 ORM 的一个异步方法,它启动一个数据库查询并返回一个可等待对象。
▮▮▮▮⚝ ③ 当 co_await 执行时,如果数据库查询尚未完成,当前的 Fiber 会被挂起,控制权返回给 I/O 线程的事件循环。I/O 线程可以继续处理其他连接的事件。数据库查询在后台(通常在工作线程池中)执行。
▮▮▮▮⚝ ④ 当数据库查询完成并返回结果时,事件循环会检测到这个完成事件,然后恢复之前挂起的 Fiber,使其从 co_await 的位置继续执行。co_await 表达式会产生查询的结果(在这个例子中是 user 对象)。
▮▮▮▮⚝ ⑤ 我们像编写同步代码一样,使用 try-catch 来处理可能发生的异常(如未找到用户或数据库错误)。
▮▮▮▮⚝ ⑥ 使用 co_return 来返回最终的 HttpResponsePtr

通过使用 Fiber 和 co_await,异步数据库查询的逻辑变得非常清晰和顺序化,避免了回调的层层嵌套。

5.4.3 Fiber 的挂起与恢复 (Suspend & Resume)

理解 Fiber 在 co_await 点如何挂起和恢复是掌握协程异步编程的关键。

挂起 (Suspend):
当 Fiber 执行到 co_await awaitable_object 时,它会检查 awaitable_object 是否已经完成。
▮▮▮▮⚝ 如果 awaitable_object 已经完成,Fiber 会立即继续执行,不会挂起。
▮▮▮▮⚝ 如果 awaitable_object 尚未完成(这是异步操作的典型情况),co_await 会触发当前 Fiber 的挂起操作。Fiber 的当前状态(包括栈上的局部变量、程序计数器等)会被保存。控制权通过某种机制(由协程框架和可等待对象实现)返回给调用者,在 Drogon 的上下文中,通常是返回到 I/O 线程的事件循环。同时,Fiber 会向 awaitable_object 注册一个完成通知的回调。

事件循环的角色:
事件循环现在可以自由地处理其他准备就绪的事件,而无需等待被挂起的 Fiber。后台的异步操作(如数据库查询)在独立的线程(工作线程池)中进行。

恢复 (Resume):
当后台的异步操作完成时,它会触发一个事件。这个事件会被 I/O 线程的事件循环检测到。事件循环根据之前注册的回调,找到对应的被挂起的 Fiber。然后,事件循环安排该 Fiber 在某个合适的时机(通常是事件循环的下一个迭代中,或者立即,取决于实现)恢复执行。
Fiber 从之前挂起的地方 (co_await 后紧跟着的语句) 开始继续执行,并且 co_await 表达式会产生异步操作的结果。

Fiber 的有栈特性:
Drogon 的 Fiber 是有栈协程。这意味着每个 Fiber 都有自己的独立的栈空间。当 Fiber 挂起时,其栈上的所有局部变量都保留原样。恢复时,Fiber 的执行环境被完整恢复,就像从未离开过一样。这与无栈协程(如 C# 的 async/await)不同,无栈协程通常需要将状态保存在堆上的状态机对象中,这使得局部变量的使用在跨 co_await 点时稍显复杂。有栈协程因为保留了完整的栈,使得编写代码更加自然,就像编写同步代码一样,局部变量在不同的 co_await 点之间是持久的。

总结挂起/恢复流程:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 Fiber 执行 --> 遇到 co_await 一个未完成的异步操作
2
3 Fiber 状态保存 (, PC )
4
5 Fiber 挂起, 控制权返回给事件循环
6
7 事件循环处理其他任务,后台执行异步操作
8
9 异步操作完成,通知事件循环
10
11 事件循环找到对应挂起的 Fiber,恢复其执行
12
13 Fiber co_await 点继续执行,获取异步操作结果
14
15 Fiber 继续执行直到完成或再次遇到 co_await

这个过程发生在 I/O 线程内部(或在工作线程池中执行的 Fiber 内部)。虽然有多个 Fiber 可能在同一个 I/O 线程中被调度执行,但它们之间是协作式的,同一时间只有一个 Fiber 正在运行。当一个 Fiber 挂起等待 I/O 时,它主动让出 CPU,让事件循环去调度其他准备就绪的 Fiber 或处理新的 I/O 事件。

5.5 异步任务管理

除了使用 Fiber 来简化异步 I/O 操作外,Drogon 还提供了一些机制来管理通用的异步任务,特别是那些不适合在 I/O 线程中直接执行的耗时或阻塞任务。

5.5.1 使用 drogon::async_run

drogon::async_run 是一个非常实用的工具函数,它允许你将一个函数(或 Lambda)提交到 Drogon 的工作线程池(或指定线程池)中异步执行。这个函数会立即返回,不会阻塞当前调用线程。

用途:
▮▮▮▮⚝ 执行 CPU 密集型计算,避免阻塞 I/O 线程。
▮▮▮▮⚝ 执行同步阻塞式 I/O 操作(尽管对于网络和数据库,Drogon 提供了异步 API 更推荐)。
▮▮▮▮⚝ 调用一些只能在独立线程中运行的第三方库函数。

签名(简化版):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 提交到默认工作线程池
2 void async_run(std::function<void()> task);
3
4 // 提交到指定线程池
5 void async_run(std::function<void()> task, size_t pool_index);

示例:执行一个耗时计算
假设你在一个请求处理函数中需要执行一个耗时几百毫秒的计算。直接在 I/O 线程中执行会阻塞该线程,影响其他请求的处理。你可以使用 drogon::async_run 将计算任务提交到工作线程池。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/drogon.h>
2 #include <thread> // 用于模拟耗时操作
3
4 void my_heavy_calculation() {
5 LOG_INFO << "开始执行耗时计算...";
6 std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 模拟计算
7 LOG_INFO << "耗时计算完成。";
8 // 注意:如果计算完成后需要更新某些状态或发送响应,
9 // 并且这些操作需要在 I/O 线程中进行,需要额外的同步或回调机制。
10 // 例如,可以使用 drogon::app().getloop()->queueInLoop(...)
11 }
12
13 void handle_request_with_heavy_task(const drogon::HttpRequestPtr& req,
14 std::function<void(const drogon::HttpResponsePtr&)> &&callback)
15 {
16 LOG_INFO << "接收到请求,将耗时任务提交到工作线程池...";
17 // 将耗时计算任务提交到默认工作线程池
18 drogon::async_run(my_heavy_calculation);
19
20 // 立即返回一个简单的响应,表示任务已接收,但不等待计算完成
21 auto resp = drogon::HttpResponse::newHttpResponse(drogon::k200OK);
22 resp->setBody("Heavy calculation started in background.\n");
23 callback(resp); // 异步发送响应
24 }
25
26 // 假设在某个地方注册了这个处理函数,例如:
27 // drogon::app().registerHandler("/heavy_task", &handle_request_with_heavy_task);

这个例子中,handle_request_with_heavy_task 接收请求后,立即将 my_heavy_calculation 函数提交到工作线程池,然后快速生成并发送一个响应给客户端。客户端不会被阻塞等待计算完成。计算任务在后台线程中独立执行。

5.5.2 与 Fiber 结合使用

虽然 drogon::async_run 可以独立使用,但它与 Fiber 结合使用时会更加强大。你可以创建自己的可等待对象,其 await_suspend 方法内部使用 drogon::async_run 将实际工作 Offload 到线程池,并在工作完成后恢复 Fiber。Drogon 提供的异步数据库客户端、HTTP 客户端等内部就是这样实现的。

示例 (概念性):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 假设有一个自定义的函数需要在线程池中执行并返回结果
2 drogon::AsyncTask<std::string> run_in_thread_pool_async(int input) {
3 // 创建一个 promise 来存储结果
4 auto promise = std::make_shared<std::promise<std::string>>();
5 auto future = promise->get_future();
6
7 // 将任务提交到工作线程池
8 drogon::async_run([promise, input]() {
9 LOG_INFO << "在线程池中执行任务,输入: " << input;
10 std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时
11 std::string result = "Processed:" + std::to_string(input);
12 promise->set_value(result); // 设置结果
13 });
14
15 // 将 future 转换为可等待对象,或直接返回 Drogon 提供的 future/task
16 // Drogon 的 AsyncTask 就是一种可等待对象
17 co_return co_await drogon::detail::convert_to_awaitable(std::move(future)); // 这是一个简化的概念,实际需要 Drogon 提供的适配
18 }
19
20 // 在控制器中使用
21 drogon::AsyncTask<drogon::HttpResponsePtr> handle_async_task(const drogon::HttpRequestPtr& req) const
22 {
23 LOG_INFO << "在 Fiber 中开始异步任务...";
24 // co_await 一个将在线程池中执行的任务
25 std::string result = co_await run_in_thread_pool_async(123);
26
27 LOG_INFO << "异步任务在 Fiber 中完成,结果: " << result;
28 auto resp = drogon::HttpResponse::newHttpResponse(drogon::k200OK);
29 resp->setBody("Async task result: " + result + "\n");
30 co_return resp;
31 }

在这个概念性例子中,run_in_thread_pool_async 函数本身是一个协程,它在内部使用 drogon::async_run 将实际的耗时工作提交到线程池,然后 co_await 等待结果。这样,调用 handle_async_task 的 Fiber 会在 co_await run_in_thread_pool_async 时挂起,不阻塞 I/O 线程,而耗时工作在线程池中执行。工作完成后,Fiber 被恢复,并获取结果。这种模式结合了线程池的阻塞能力和 Fiber 的异步编排能力。

5.5.3 其他异步机制

Drogon 还提供了其他一些异步相关的工具:

drogon::app().getLoop()->queueInLoop(...) 将一个函数或 Lambda 提交到当前的 I/O 线程的事件循环中执行。这在你需要在工作线程池中完成任务后,回到 I/O 线程执行一些与连接相关的操作(如发送响应)时非常有用。

定时器 (Timer): Drogon 允许你设置单次或周期性的定时器任务,这些任务会在事件循环中执行,可用于实现超时、周期性清理等功能。

通过这些机制,开发者可以在 Drogon 中灵活地管理各种类型的异步任务,充分发挥框架的性能优势。

6. 数据库集成:ORM 与异步访问

欢迎来到本书关于 Drogon 数据库集成的章节。在构建 Web 应用时,与数据库的交互几乎是不可避免的。Drogon 提供了一套强大且灵活的数据库访问机制,其中包括内置的 ORM(对象关系映射)和对异步操作的深度支持。本章将详细讲解如何配置数据库连接、利用 ORM 进行数据操作、执行原始 SQL,以及如何在 Drogon 的异步环境中有效地进行数据库访问。

6.1 数据库连接配置

为了让 Drogon 应用能够连接到数据库,我们需要在配置文件中指定数据库的连接信息。Drogon 主要通过 config.json 文件来管理这些配置。它支持多种主流的数据库系统,包括 PostgreSQL, MySQL, SQLite 等。

▮▮▮▮📝 注意: 配置文件通常位于项目的根目录下,名为 config.json

6.1.1 配置文件的使用

Drogon 启动时会加载 config.json 文件。数据库连接信息通常包含在一个名为 db_clients 的 JSON 数组中。数组中的每个对象代表一个数据库连接配置。

这是一个配置 PostgreSQL 和 MySQL 数据库连接的例子:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 // ... 其他配置项
3 "db_clients": [
4 {
5 // PostgreSQL 连接配置
6 "db_type": "postgresql",
7 "is_sync": false, // 通常设置为 false 表示异步模式
8 "host": "127.0.0.1",
9 "port": 5432,
10 "db_name": "mydatabase_pg",
11 "user": "myuser_pg",
12 "passwd": "mypassword_pg",
13 "number_of_connections": 10, // 连接池大小
14 "timeout": 5 // 连接超时(秒)
15 },
16 {
17 // MySQL 连接配置
18 "db_type": "mysql",
19 "is_sync": false,
20 "host": "127.0.0.1",
21 "port": 3306,
22 "db_name": "mydatabase_mysql",
23 "user": "myuser_mysql",
24 "passwd": "mypassword_mysql",
25 "number_of_connections": 15,
26 "timeout": 5
27 },
28 {
29 // SQLite3 连接配置
30 "db_type": "sqlite3",
31 "is_sync": false,
32 "filename": "mydatabase.sqlite", // SQLite3 只需要文件名
33 "number_of_connections": 1, // SQLite3 通常只需要一个连接
34 "timeout": 5
35 }
36 ],
37 // ... 其他配置项
38 }

db_type (数据库类型):指定数据库类型,如 postgresql, mysql, sqlite3 等。
is_sync (是否同步):绝大多数情况下应设置为 false,表示使用 Drogon 推荐的异步模式。同步模式会阻塞事件循环,不推荐使用。
host (主机)、port (端口)、db_name (数据库名)、user (用户名)、passwd (密码):标准的数据库连接参数。SQLite3 数据库不需要这些,只需指定 filename (文件名)。
number_of_connections (连接数):数据库连接池的大小。这个参数影响应用的并发处理能力。根据预期的并发请求量和数据库的承受能力进行调整。
timeout (超时):连接数据库的超时时间(秒)。

配置完成后,Drogon 在应用启动时会创建并管理这些数据库连接池。

6.2 ORM 简介与模型生成 (Object-Relational Mapping & Model Generation)

对象关系映射(ORM)是一种编程技术,用于在不同类型系统(如面向对象编程语言和关系型数据库)中转换数据。简单来说,ORM 允许您使用 C++ 类和对象来操作数据库表和记录,而无需编写大量的原始 SQL 语句。这大大提高了开发效率和代码的可维护性。

Drogon 提供了一个内置的轻量级 ORM 工具,它可以根据您现有的数据库表结构自动生成对应的 C++ 模型类(Model)和映射类(Mapper)。

6.2.1 ORM 的概念与作用

▮▮▮▮💡 什么是 ORM?
▮▮▮▮⚝ 将数据库表映射到 C++ 类。
▮▮▮▮⚝ 将表中的行映射到 C++ 对象。
▮▮▮▮⚝ 将表中的列映射到 C++ 对象的成员变量。

使用 ORM 的主要优势:
▮▮▮▮⚝ 提高开发效率:减少手动编写 SQL 的工作量。
▮▮▮▮⚝ 代码更易读、更安全:使用类型安全的 C++ 对象操作数据,减少 SQL 注入等风险。
▮▮▮▮⚝ 更好的可维护性:数据库结构的改变通常只需要重新生成模型代码,而不是修改大量 SQL 语句。

Drogon 的 ORM 是围绕其异步特性设计的,模型和映射类都支持异步操作。

6.2.2 使用 drogons CLI 工具生成 C++ 模型类

Drogon 命令行工具 drogons 提供了方便的模型生成功能。首先,确保您的数据库连接配置已正确写入 config.json 并能正常连接数据库。

使用 drogons create model 命令可以自动扫描数据库表并生成相应的 C++ 代码。

例如,如果您想根据默认配置的数据库(通常是 db_clients 数组的第一个配置)生成模型:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogons create model

如果您配置了多个数据库连接,并且想指定使用其中一个(例如,使用连接名为 mydb 的配置):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogons create model -c mydb

▮▮▮▮ℹ️ 提示: 如果你的 config.json 文件不在当前目录,或者名字不是 config.json,你可以使用 -f 参数指定配置文件路径。

命令执行后,drogons 会连接到数据库,读取所有表结构,并在你的项目目录(通常在 models 目录下)生成对应的 .h.cc 文件。

例如,对于一个名为 users 的表,可能会生成 User.h, User.cc, UserMapper.h, UserMapper.cc 等文件。

▮▮▮▮✅ 模型生成后的文件结构:
▮▮▮▮⚝ models/ 目录:
▮▮▮▮▮▮▮▮⚝ User.h, User.cc: 定义了 User 模型类,代表数据库中的 users 表的一行数据。它包含了与表列对应的成员变量以及存取方法。
▮▮▮▮▮▮▮▮⚝ UserMapper.h, UserMapper.cc: 定义了 UserMapper 映射类,提供了对 users 表进行 CRUD(创建、读取、更新、删除)操作的方法,这些方法通常是异步的。

这些生成的文件是 Drogon ORM 的核心,您将在接下来的部分学习如何使用它们。

6.3 使用生成的 ORM 模型

一旦模型类和映射类生成完毕,您就可以在控制器(Controller)、服务(Service)或任何需要与数据库交互的业务逻辑中使用了。使用 ORM 模型进行数据操作通常通过对应的 Mapper 类来完成。

首先,在使用 Mapper 类之前,通常需要获取其单例实例。Drogon 的 Mapper 类是单例的,通过 Mapper<ModelClass>::mapper() 方法获取。

例如,对于 User 模型,获取 UserMapper 实例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 auto userMapper = drogon::orm::Mapper<User>::mapper();

接下来,我们将详细介绍使用 Mapper 进行基本的 CRUD 操作。

6.3.1 插入数据 (Insert)

插入新记录通常涉及到创建模型对象,设置其属性,然后调用 Mapper 的插入方法。

创建模型对象:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 User newUser;
2 newUser.setUserName("alice");
3 newUser.setPassword("secure_password");
4 newUser.setCreateTime(drogon::utilities::getNow()); // 设置时间戳
5 // 根据表结构设置其他属性

模型对象的属性通常都有对应的 setget 方法。

插入操作:
您可以使用同步或异步的插入方法。在 Drogon 的异步模型中,强烈推荐使用异步方法,例如 asyncInsert 或在 Fiber/协程中使用 insert

▮▮▮▮✏️ 使用回调函数进行异步插入:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 userMapper.asyncInsert(newUser,
2 [](const User &user) {
3 // 插入成功的回调,参数user是插入后(可能包含自增ID)的对象
4 LOG_INFO << "用户 " << user.getUserName() << " 插入成功,ID: " << user.getValueOfId();
5 },
6 [](const drogon::orm::DrogonDbException &e) {
7 // 插入失败的回调
8 LOG_ERROR << "用户插入失败: " << e.what();
9 });

asyncInsert 方法接受模型对象以及成功和失败的回调函数。

▮▮▮▮✏️ 在 Fiber/协程中进行插入:
如果您的代码位于 Fiber/协程中(例如使用 drogon::async_run 或在处理请求时使用 drogon::HttpController::handle_with_fiber),可以直接使用看起来像同步的方法 insert

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 假设当前在 Fiber 环境中
2 try {
3 User insertedUser = userMapper.insert(newUser);
4 LOG_INFO << "用户 " << insertedUser.getUserName() << " 插入成功,ID: " << insertedUser.getValueOfId();
5 } catch (const drogon::orm::DrogonDbException &e) {
6 LOG_ERROR << "用户插入失败: " << e.what();
7 }

在 Fiber 中使用 insert 方法时,底层的异步操作会被 Drogon 的运行时自动处理,使得代码结构更简洁。

6.3.2 查询数据 (Query)

查询是 ORM 最常用的功能。Mapper 类提供了多种查询方法,可以根据主键、条件查询单个或多个对象。

按主键查询单个对象:
使用 findByPrimaryKey 或其异步版本 asyncFindByPrimaryKey

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 使用回调
2 userMapper.asyncFindByPrimaryKey(123, // 用户 ID
3 [](const User &user) {
4 // 查询成功,如果找到则user.hasValue()为true
5 if (user.hasValue()) {
6 LOG_INFO << "找到用户: " << user.getUserName();
7 } else {
8 LOG_INFO << "用户 ID 123 不存在";
9 }
10 },
11 [](const drogon::orm::DrogonDbException &e) {
12 LOG_ERROR << "查询失败: " << e.what();
13 });
14
15 // 在 Fiber 中
16 try {
17 User user = userMapper.findByPrimaryKey(123);
18 if (user.hasValue()) {
19 LOG_INFO << "找到用户: " << user.getUserName();
20 } else {
21 LOG_INFO << "用户 ID 123 不存在";
22 }
23 } catch (const drogon::orm::DrogonDbException &e) {
24 LOG_ERROR << "查询失败: " << e.what();
25 }

hasValue() 方法用于检查是否找到了对应的记录。

按条件查询:
使用 findBy 或其异步版本 asyncFindBy。这些方法接受一个查询构建器作为参数。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 使用回调查询名字为 'alice' 的用户列表
2 userMapper.asyncFindBy(drogon::orm::Criteria("user_name", drogon::orm::CompareOperator::EQ, "alice"),
3 [](const std::vector<User> &users) {
4 LOG_INFO << "找到 " << users.size() << " 个名字为 alice 的用户";
5 for (const auto& user : users) {
6 LOG_INFO << "- ID: " << user.getValueOfId() << ", 用户名: " << user.getUserName();
7 }
8 },
9 [](const drogon::orm::DrogonDbException &e) {
10 LOG_ERROR << "查询失败: " << e.what();
11 });
12
13 // 在 Fiber 中查询并排序
14 try {
15 auto users = userMapper.findBy(
16 drogon::orm::Criteria("create_time", drogon::orm::CompareOperator::GT, drogon::utilities::getDatetimeByGmt(0)) // 创建时间大于某个时间点
17 .orderBy("create_time", drogon::orm::SortOrder::DESC) // 按创建时间倒序
18 .limit(10) // 限制10条结果
19 );
20 LOG_INFO << "找到 " << users.size() << " 个用户";
21 // ... 处理 users
22 } catch (const drogon::orm::DrogonDbException &e) {
23 LOG_ERROR << "查询失败: " << e.what();
24 }

drogon::orm::Criteria 用于构建查询条件,支持各种比较运算符(EQ, NE, GT, LT, GE, LE, LIKE, IS NULL, IS NOT NULL, IN, NOT IN 等),并可以通过 &&|| 连接多个条件。

查询所有数据:
使用 findAllasyncFindAll

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 使用回调查询所有用户
2 userMapper.asyncFindAll(
3 [](const std::vector<User> &users) {
4 LOG_INFO << "总共有 " << users.size() << " 个用户";
5 },
6 [](const drogon::orm::DrogonDbException &e) {
7 LOG_ERROR << "查询失败: " << e.what();
8 });

6.3.3 更新数据 (Update)

更新记录通常涉及到先查询到模型对象,修改其属性,然后调用 Mapper 的更新方法。

查询并修改对象:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 假设在 Fiber 中先通过 ID 查询到用户对象
2 try {
3 User userToUpdate = userMapper.findByPrimaryKey(123);
4 if (userToUpdate.hasValue()) {
5 userToUpdate.setUserName("alice_updated");
6 userToUpdate.setUpdateTime(drogon::utilities::getNow());
7 // 修改其他属性
8 } else {
9 LOG_WARNING << "用户 ID 123 不存在,无法更新";
10 return; // 或者抛出异常
11 }
12 } catch (const drogon::orm::DrogonDbException &e) {
13 LOG_ERROR << "查询失败: " << e.what();
14 return;
15 }

更新操作:
使用 updateasyncUpdate

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 使用回调进行异步更新
2 userMapper.asyncUpdate(userToUpdate,
3 [](const User &user) {
4 LOG_INFO << "用户 " << user.getValueOfId() << " 更新成功";
5 },
6 [](const drogon::orm::DrogonDbException &e) {
7 LOG_ERROR << "用户更新失败: " << e.what();
8 });
9
10 // 在 Fiber 中进行更新
11 try {
12 User updatedUser = userMapper.update(userToUpdate); // update 方法返回更新后的对象
13 LOG_INFO << "用户 " << updatedUser.getValueOfId() << " 更新成功";
14 } catch (const drogon::orm::DrogonDbException &e) {
15 LOG_ERROR << "用户更新失败: " << e.what();
16 }

6.3.4 删除数据 (Delete)

删除记录可以通过多种方式,例如根据主键删除,或者根据条件删除。

按主键删除:
使用 deleteByPrimaryKeyasyncDeleteByPrimaryKey

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 使用回调删除 ID 为 123 的用户
2 userMapper.asyncDeleteByPrimaryKey(123,
3 []() {
4 LOG_INFO << "用户 ID 123 删除成功";
5 },
6 [](const drogon::orm::DrogonDbException &e) {
7 LOG_ERROR << "用户删除失败: " << e.what();
8 });
9
10 // 在 Fiber 中删除
11 try {
12 userMapper.deleteByPrimaryKey(123);
13 LOG_INFO << "用户 ID 123 删除成功";
14 } catch (const drogon::orm::DrogonDbException &e) {
15 LOG_ERROR << "用户删除失败: " << e.what();
16 }

按条件删除:
使用 deleteByasyncDeleteBy

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 使用回调删除名字为 'peter' 的用户
2 userMapper.asyncDeleteBy(drogon::orm::Criteria("user_name", drogon::orm::CompareOperator::EQ, "peter"),
3 [](size_t count) {
4 LOG_INFO << "删除 " << count << " 个名字为 peter 的用户";
5 },
6 [](const drogon::orm::DrogonDbException &e) {
7 LOG_ERROR << "用户删除失败: " << e.what();
8 });
9
10 // 在 Fiber 中删除
11 try {
12 size_t count = userMapper.deleteBy(drogon::orm::Criteria("user_name", drogon::orm::CompareOperator::EQ, "peter"));
13 LOG_INFO << "删除 " << count << " 个名字为 peter 的用户";
14 } catch (const drogon::orm::DrogonDbException &e) {
15 LOG_ERROR << "用户删除失败: " << e.what();
16 }

6.4 异步数据库访问

Drogon 设计的核心在于其基于事件循环的异步非阻塞架构。数据库操作通常是 I/O 密集型的,如果使用同步阻塞的方式进行,会导致处理请求的线程(事件循环线程)被长时间阻塞,严重影响应用的并发性能。

因此,Drogon 的数据库模块(无论是 ORM 还是原始 SQL 接口)默认和推荐的都是异步模式。

异步的原理:
当您发起一个异步数据库请求时(例如 asyncInsert),Drogon 会将这个请求提交给一个独立的数据库线程池。处理请求的事件循环线程不会等待数据库操作完成,而是立即返回去处理其他请求。当数据库操作完成后,数据库线程池会通过事件循环通知原先的事件循环线程,然后触发您提供的回调函数来处理结果。

回调函数风格:
这是传统的 C++ 异步编程模式,通过传入成功和失败的回调函数来处理异步操作的结果。在 Drogon 的 ORM 和原始 SQL 接口中,带有 async 前缀的方法通常都使用回调函数。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 异步查询,结果在回调中处理
2 userMapper.asyncFindByPrimaryKey(userId,
3 [resp](const User &user) {
4 if (user.hasValue()) {
5 // 成功找到用户,构建并发送响应
6 auto retJson = user.toJson(); // 将模型对象转换为 JSON
7 auto res = drogon::HttpResponse::newHttpJsonResponse(retJson);
8 resp(res);
9 } else {
10 // 未找到用户,发送 404 响应
11 auto res = drogon::HttpResponse::newHttpResponse(drogon::HttpStatusCode::k404NotFound);
12 resp(res);
13 }
14 },
15 [resp](const drogon::orm::DrogonDbException &e) {
16 // 数据库操作失败,发送 500 响应
17 LOG_ERROR << "数据库查询失败: " << e.what();
18 auto res = drogon::HttpResponse::newHttpResponse(drogon::HttpStatusCode::k500InternalServerError);
19 resp(res);
20 });

这种风格在处理简单异步流程时很直观,但当多个异步操作需要串联或并行执行时,可能会导致“回调地狱”(Callback Hell),代码变得难以阅读和维护。

Fiber/协程风格:
如第 5 章所述,Drogon 充分利用 C++20 协程(通过 libco 库实现为 Fiber)来简化异步编程。在 Fiber 环境中,您可以直接调用 Mapper 或 DbClient 的同步方法(如 insert, findByPrimaryKey, execSqlCoro)。这些方法在内部仍然是异步的,但 Fiber 会在等待数据库结果时自动挂起(yield),当结果返回时再由事件循环恢复(resume),从而让异步代码看起来像同步代码一样简洁易读。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 假设在 Fiber 环境中,例如 HttpSimpleController::handle_with_fiber 方法内
2 try {
3 auto userMapper = drogon::orm::Mapper<User>::mapper();
4 User user = userMapper.findByPrimaryKey(userId);
5
6 if (user.hasValue()) {
7 // 成功找到用户
8 auto retJson = user.toJson();
9 return drogon::HttpResponse::newHttpJsonResponse(retJson);
10 } else {
11 // 未找到用户
12 return drogon::HttpResponse::newHttpResponse(drogon::HttpStatusCode::k404NotFound);
13 }
14 } catch (const drogon::orm::DrogonDbException &e) {
15 // 数据库操作失败
16 LOG_ERROR << "数据库查询失败: " << e.what();
17 return drogon::HttpResponse::newHttpResponse(drogon::HttpStatusCode::k500InternalServerError);
18 }

对比回调函数风格,Fiber 风格的代码逻辑流更清晰,更接近同步代码的编写习惯,尤其适合复杂的业务逻辑,其中包含多个连续的异步操作。

▮▮▮▮🔑 重点: 无论使用哪种风格,数据库操作本身是异步完成的,不会阻塞 Drogon 的事件循环主线程,这是 Drogon 实现高性能的关键之一。

6.5 执行原始 SQL 查询 (Raw SQL)

虽然 ORM 为常见的 CRUD 操作提供了便利,但在某些情况下,您可能需要直接执行原始 SQL 语句。例如:

▮▮▮▮⚝ 执行复杂的联表查询或聚合查询,ORM 表达起来比较困难或效率不高。
▮▮▮▮⚝ 使用特定数据库的非标准 SQL 语法或功能。
▮▮▮▮⚝ 执行数据库管理任务(如创建表、修改列)。

Drogon 提供了访问原始数据库客户端的接口,支持执行原始 SQL 并以异步方式获取结果。

获取数据库客户端实例:
您可以通过 drogon::app().getDbClient() 方法获取一个数据库客户端智能指针。如果您配置了多个数据库连接,可以使用连接名来指定客户端:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 auto dbClient = drogon::app().getDbClient(); // 获取默认连接的客户端
2 // 或者
3 auto dbClient = drogon::app().getDbClient("mydb"); // 获取指定连接名的客户端

执行 SQL 查询:
使用 execSqlAsync (回调) 或 execSqlCoro (Fiber) 方法。这些方法接受 SQL 语句字符串以及参数(如果需要)。

▮▮▮▮✏️ 使用回调执行原始 SQL:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 查询用户数量
2 dbClient->execSqlAsync("SELECT COUNT(*) FROM users;",
3 [resp](const drogon::orm::Result &result) {
4 if (!result.empty()) {
5 long long count = result[0][0].as<long long>();
6 auto res = drogon::HttpResponse::newHttpJsonResponse(drogon::HttpJson::fromValue(count));
7 resp(res);
8 } else {
9 auto res = drogon::HttpResponse::newHttpResponse(drogon::HttpStatusCode::k500InternalServerError);
10 resp(res);
11 }
12 },
13 [resp](const drogon::orm::DrogonDbException &e) {
14 LOG_ERROR << "原始 SQL 查询失败: " << e.what();
15 auto res = drogon::HttpResponse::newHttpResponse(drogon::HttpStatusCode::k500InternalServerError);
16 resp(res);
17 });
18
19 // 带参数的查询示例
20 dbClient->execSqlAsync("SELECT * FROM users WHERE user_name = ? AND password = ?;",
21 [](const drogon::orm::Result &result) {
22 // 处理查询结果 result
23 },
24 [](const drogon::orm::DrogonDbException &e) {
25 // 处理错误
26 },
27 "alice", // 第一个参数绑定到第一个 ?
28 "password123" // 第二个参数绑定到第二个 ?
29 );

execSqlAsync 的第一个回调参数是 drogon::orm::Result 对象,它代表了查询结果集,您可以通过类似二维数组的方式访问数据。

▮▮▮▮✏️ 在 Fiber 中执行原始 SQL:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 假设在 Fiber 环境中
2 try {
3 auto dbClient = drogon::app().getDbClient();
4 // 执行带参数的查询
5 drogon::orm::Result result = dbClient->execSqlCoro("SELECT id, user_name FROM users WHERE user_name = ?;", "bob");
6
7 if (!result.empty()) {
8 // 处理结果集
9 for (const auto& row : result) {
10 long long id = row["id"].as<long long>(); // 按列名或索引访问
11 std::string userName = row[1].as<std::string>(); // 按索引访问
12 LOG_INFO << "找到用户: ID = " << id << ", 用户名 = " << userName;
13 }
14 return drogon::HttpResponse::newHttpResponse(drogon::HttpStatusCode::k200Ok);
15 } else {
16 LOG_INFO << "未找到用户 bob";
17 return drogon::HttpResponse::newHttpResponse(drogon::HttpStatusCode::k404NotFound);
18 }
19 } catch (const drogon::orm::DrogonDbException &e) {
20 LOG_ERROR << "原始 SQL 查询失败: " << e.what();
21 return drogon::HttpResponse::newHttpResponse(drogon::HttpStatusCode::k500InternalServerError);
22 }

在 Fiber 中,execSqlCoro 返回 drogon::orm::Result 对象或抛出异常。这种方式代码更紧凑。

▮▮▮▮⚠️ 安全性: 在执行原始 SQL 时,务必使用参数绑定(使用 ? 占位符并在函数参数中提供值),而不是直接拼接用户输入的字符串到 SQL 语句中,以防止 SQL 注入攻击。

6.6 事务处理 (Transaction)

数据库事务是一系列操作的集合,这些操作要么全部成功提交,要么全部失败回滚,以保证数据的一致性。Drogon 的数据库客户端支持事务处理。

事务处理通常通过获取一个事务对象来完成,在这个对象上执行所有数据库操作。

获取事务对象:
使用 newTransaction() 方法。您可以指定事务的隔离级别(如果数据库支持)。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 使用回调获取事务
2 dbClient->newTransaction([](const drogon::orm::DbClientPtr &trans) {
3 // 在这里使用 trans 执行事务内的操作
4 });
5
6 // 在 Fiber 中获取事务
7 try {
8 auto trans = dbClient->newTransaction();
9 // 在这里使用 trans 执行事务内的操作
10 } catch (const drogon::orm::DrogonDbException &e) {
11 LOG_ERROR << "创建事务失败: " << e.what();
12 }

无论是回调还是 Fiber,获取到的 trans 对象(也是 drogon::orm::DbClientPtr 类型)是专门用于该事务的客户端。所有在该事务客户端上执行的操作都属于同一个事务会话。

事务内的操作:
在事务客户端 (trans) 上调用 ORM Mapper 或 execSql 方法。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 在 Fiber 事务内进行操作
2 try {
3 auto dbClient = drogon::app().getDbClient();
4 auto trans = dbClient->newTransaction(); // 获取事务客户端
5
6 auto userMapper = drogon::orm::Mapper<User>(trans); // 注意:Mapper 需要用事务客户端创建
7
8 // 1. 插入新用户
9 User newUser;
10 newUser.setUserName("transaction_user");
11 newUser.setPassword("tx_pass");
12 User insertedUser = userMapper.insert(newUser);
13
14 // 2. 根据新用户 ID 插入相关记录到另一个表 (例如 orders 表)
15 auto orderMapper = drogon::orm::Mapper<Order>(trans); // Order Mapper 也用事务客户端创建
16 Order newOrder;
17 newOrder.setUserId(insertedUser.getValueOfId());
18 newOrder.setAmount(100.0);
19 orderMapper.insert(newOrder);
20
21 // 如果所有操作都成功,提交事务
22 trans->commit(); // 在 Fiber 中直接调用 commit
23
24 LOG_INFO << "事务提交成功";
25
26 } catch (const drogon::orm::DrogonDbException &e) {
27 // 任何一个操作失败,回滚事务
28 LOG_ERROR << "事务中的数据库操作失败: " << e.what();
29 // 事务客户端的析构函数默认会在异常时自动回滚,
30 // 但显式调用 rollback() 更清晰,或者在回调中处理失败时手动回滚
31 // trans->rollback(); // 在 Fiber 中显式调用 rollback
32 LOG_INFO << "事务已回滚";
33 }

提交与回滚:
事务操作完成后,需要明确地提交 (commit) 或回滚 (rollback) 事务。

▮▮▮▮✏️ 使用回调处理事务的提交与回滚:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 dbClient->newTransaction([&](const drogon::orm::DbClientPtr &trans) {
2 auto userMapper = drogon::orm::Mapper<User>(trans);
3 // ... 事务内的第一个异步操作 ...
4 userMapper.asyncInsert(newUser,
5 [&, trans](const User &user) { // 捕获 trans 以在回调中使用
6 // 第一个操作成功,进行第二个异步操作
7 auto orderMapper = drogon::orm::Mapper<Order>(trans);
8 orderMapper.asyncInsert(newOrder,
9 [&, trans](const Order &order) { // 捕获 trans
10 // 第二个操作也成功,提交事务
11 trans->execSqlAsync("COMMIT;",
12 [](){ LOG_INFO << "事务提交成功"; },
13 [](const drogon::orm::DrogonDbException &e){ LOG_ERROR << "事务提交失败: " << e.what(); }
14 );
15 },
16 [&, trans](const drogon::orm::DrogonDbException &e) { // 第二个操作失败
17 LOG_ERROR << "第二个操作失败,回滚事务: " << e.what();
18 trans->execSqlAsync("ROLLBACK;",
19 [](){ LOG_INFO << "事务回滚成功"; },
20 [](const drogon::orm::DrogonDbException &e){ LOG_ERROR << "事务回滚失败: " << e.what(); }
21 );
22 }
23 );
24 },
25 [&, trans](const drogon::orm::DrogonDbException &e) { // 第一个操作失败
26 LOG_ERROR << "第一个操作失败,回滚事务: " << e.what();
27 trans->execSqlAsync("ROLLBACK;",
28 [](){ LOG_INFO << "事务回滚成功"; },
29 [](const drogon::orm::DrogonDbException &e){ LOG_ERROR << "事务回滚失败: " << e.what(); }
30 );
31 }
32 );
33 });

可以看到,使用回调处理复杂事务流程会变得非常嵌套和难以管理。

▮▮▮▮✏️ 在 Fiber 中处理事务的提交与回滚:
如前面 Fiber 示例所示,在 Fiber 中,您可以直接调用 trans->commit()trans->rollback()。如果 Fiber 中发生异常且未被捕获,Drogon 的 Fiber 运行时会自动回滚事务。因此,通常只需要在 try-catch 块中捕获异常并在 catch 块中处理错误日志,提交则在 try 块的最后成功执行时调用。

▮▮▮▮🔑 重要:
▮▮▮▮⚝ 事务内的所有数据库操作必须使用同一个事务客户端 (trans)。
▮▮▮▮⚝ 在 Fiber 中,事务客户端的析构函数会在 Fiber 退出时自动检查事务状态,如果事务未提交且 Fiber 是因异常退出,会自动发起回滚。但在正常流程成功时,必须手动调用 commit()
▮▮▮▮⚝ 在回调函数风格中,您需要在成功和失败的回调链的末端手动执行 COMMIT;ROLLBACK; 原始 SQL 语句来控制事务。这增加了复杂性,再次凸显了 Fiber 在处理复杂异步流程(包括事务)方面的优势。

通过本章的学习,您应该对 Drogon 的数据库集成有了全面的了解。无论是利用 ORM 快速开发,执行原始 SQL 处理复杂场景,还是充分利用其异步特性和 Fiber 机制来编写高性能且易于维护的代码,Drogon 都提供了强大的支持。接下来,我们将探讨 Drogon 的中间件和过滤器系统。

好的,作为您的讲师,我将为您深度解析 Drogon Web 框架中的中间件 (Middleware) 与过滤器 (Filter) 机制。我们将严格按照您提供的章节大纲和输出格式进行撰写。


7. 中间件 (Middleware) 与过滤器 (Filter)

本章将带您深入了解 Drogon 框架中如何通过中间件 (Middleware) 和过滤器 (Filter) 来拦截、处理和修改 HTTP 请求与响应。掌握这些机制是构建健壮、可维护的 Drogon 应用的关键,它们允许您在请求到达最终的业务逻辑(控制器/处理器)之前或之后插入自定义的处理逻辑,实现诸如身份认证、日志记录、数据验证、权限控制等跨越多个业务场景的功能。

7.1 中间件 (Middleware) 的概念与作用

7.1.1 请求处理管道 (Request Processing Pipeline)

在大多数现代 Web 框架中,一个 HTTP 请求的处理过程并非直接由服务器接收并立即交给最终的业务逻辑代码。相反,请求会经过一个由一系列处理阶段组成的管道 (pipeline)。每个阶段都可以对请求进行检查、修改,或者在某些情况下,直接生成响应并终止管道的进一步处理。这个管道中的各个处理阶段,通常被称为中间件 (Middleware)。

想象一下,当一个 HTTP 请求到达您的 Drogon 应用时,它就像进入了一个处理工厂。在到达最终的目的地(比如一个控制器方法)之前,它会经过一系列的工作站(中间件)。每个工作站都可以检查请求,做一些事情(比如记录请求信息、验证用户身份),然后决定是让请求继续流向下一个工作站,还是直接在这里就生成一个响应并送回客户端。

7.1.2 中间件的位置与功能

中间件通常位于请求处理流程的核心路径上,在路由匹配之后、业务逻辑执行之前,或者业务逻辑执行之后、响应发送之前。

在 Drogon 中,这个管道涉及多个阶段,包括接收连接、解析请求、路由匹配、执行过滤器、执行控制器/处理器、执行后处理、发送响应等。中间件或类似机制(如 Drogon 的 HttpFilterAdvice)就嵌入在这些阶段中。

中间件的主要作用包括但不限于:

日志记录 (Logging)
▮▮▮▮⚝ 记录每个请求的关键信息,例如请求方法、URL、客户端 IP、响应状态码、处理时间等,便于监控和调试。
身份认证 (Authentication)
▮▮▮▮⚝ 验证请求是否携带有效的身份凭证(如 Session ID、Token),确定请求用户的身份。
授权控制 (Authorization)
▮▮▮▮⚝ 检查已认证用户是否拥有访问特定资源的权限。
数据解析与验证 (Data Parsing & Validation)
▮▮▮▮⚝ 解析请求体(如 JSON、表单数据),并对输入数据进行格式或业务规则验证。
安全防护 (Security)
▮▮▮▮⚝ 防止常见的 Web 攻击,如 CSRF、XSS、SQL 注入(部分防护可能集成在框架核心或 ORM 中,但中间件可增强防护)。
CORS 处理 (Cross-Origin Resource Sharing)
▮▮▮▮⚝ 处理跨域请求所需的 HTTP 头部信息。
流量控制 (Rate Limiting)
▮▮▮▮⚝ 限制来自特定源或用户的请求速率,防止滥用。
请求/响应修改 (Request/Response Modification)
▮▮▮▮⚝ 在请求到达处理器前修改请求,或在处理器生成响应后修改响应(如添加头部、压缩响应体)。

通过使用中间件,我们可以将这些通用的、与具体业务逻辑无关的功能从控制器或处理器中剥离出来,提高代码的模块化和可重用性。 🧺

7.2 实现自定义中间件

在 Drogon 中,实现类似于传统意义上的“中间件”功能主要通过 drogon::HttpFilter 或全局的 Advice 机制来完成。鉴于后续章节会详细讲解 HttpFilter,本节我们先通过一个简单的例子,介绍如何创建一个处理请求的类,它可以作为一种“中间件”来拦截请求并进行初步处理。

一个自定义的“中间件”类通常需要继承自 drogon::HttpFilter 或实现特定的接口,以便被 Drogon 框架识别并在请求处理管道中调用。drogon::HttpFilter 是 Drogon 官方推荐的、用于在请求到达控制器之前进行过滤处理的机制。

让我们看一个简单的例子:创建一个用于记录请求路径的过滤器(作为一种中间件的实现方式)。

首先,使用 drogons 工具生成一个过滤器骨架:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogons create filter LogFilter

这会在 filters 目录下生成 LogFilter.hLogFilter.cc 文件。

filters/LogFilter.h (部分关键代码)

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #pragma once
2
3 #include <drogon/HttpFilter.h>
4
5 class LogFilter : public drogon::HttpFilter<LogFilter>
6 {
7 public:
8 /// Add the code to filter non-get and non-post requests Here.
9 bool doFilter(const drogon::HttpRequestPtr &req,
10 drogon::FilterCallback &&fcb,
11 drogon::FilterChainCallback &&fccb);
12 };

filters/LogFilter.cc (部分关键代码)

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include "LogFilter.h"
2 #include <drogon/drogon.h>
3
4 bool LogFilter::doFilter(const drogon::HttpRequestPtr &req,
5 drogon::FilterCallback &&fcb,
6 drogon::FilterChainCallback &&fccb)
7 {
8 // 在请求到达控制器之前执行的逻辑
9 LOG_INFO << "Request received: " << req->getMethodString() << " " << req->getPath();
10
11 // 如果需要继续处理请求,调用 fccb
12 fccb();
13
14 // 返回 true 表示请求被继续处理
15 return true;
16
17 // 如果需要在这里终止请求并返回响应,可以构建一个响应对象,
18 // 调用 fcb(response),然后返回 false。
19 // 例如:
20 /*
21 auto resp = drogon::HttpResponse::newHttpResponse();
22 resp->setStatusCode(drogon::k500InternalServerError);
23 resp->setBody("Internal Server Error");
24 fcb(resp);
25 return false; // 终止处理链
26 */
27 }

在这个例子中:
① 我们继承了 drogon::HttpFilter<LogFilter>
② 实现了 doFilter 方法。这是过滤器的核心逻辑所在。
doFilter 接收三个参数:
▮▮▮▮ⓓ const drogon::HttpRequestPtr &req: 当前的 HTTP 请求对象。
▮▮▮▮ⓔ drogon::FilterCallback &&fcb: 过滤器回调函数。如果过滤器决定在这里终止请求并返回响应,就调用 fcb(response),然后 doFilter 返回 false
▮▮▮▮ⓕ drogon::FilterChainCallback &&fccb: 过滤器链回调函数。如果过滤器处理完毕,希望请求继续流向下一个过滤器或最终的处理器,就调用 fccb(),然后 doFilter 返回 true
⑦ 在 doFilter 方法中,我们使用 LOG_INFO 记录了请求的方法和路径。
⑧ 调用 fccb() 确保请求继续处理链。
⑨ 返回 true 表示请求继续。

这个 LogFilter 就是一个简单的请求日志中间件的实现。它会在匹配到并应用了这个过滤器的请求被处理前执行其 doFilter 方法。

注意,Drogon 的 HttpFilter 是应用于特定路由或控制器的,而不是全局的。更全局的“中间件”行为可以通过 Drogon 的 Advice 机制实现,例如 drogon::HttpAppFramework::instance().registerPreRoutingAdvice(...),它允许在路由匹配前或后插入逻辑。但从章节结构看,此处是为引出 HttpFilter 做准备。

7.3 过滤器 (Filter) 的概念与作用

7.3.1 过滤器在 Drogon 中的定位

在 Drogon 中,HttpFilter 是一种特殊的组件,它的主要作用是拦截发往特定 路由 (route)控制器 (controller) 的 HTTP 请求,并在请求到达目标处理函数之前执行预设的逻辑。它位于请求处理管道中,通常紧随路由匹配之后、实际的控制器方法或函数处理器执行之前。

与更广义的“中间件”可能覆盖整个应用管道不同,Drogon 的 HttpFilter 更侧重于对 特定目标 进行过滤。这使得过滤器非常适合实现针对特定 API 端点或一组端点的功能,而不是影响所有请求。 🎯

7.3.2 过滤器的核心作用

过滤器的核心作用是根据某些条件决定是否允许请求继续流向其预定的处理器。它们可以:

访问控制 (Access Control)
▮▮▮▮⚝ 检查用户是否已登录,或是否拥有访问特定资源的权限。如果条件不满足,过滤器可以直接返回一个错误响应(如 401 Unauthorized 或 403 Forbidden),阻止请求到达控制器。
数据验证 (Data Validation)
▮▮▮▮⚝ 在处理复杂业务逻辑前,对请求中的关键参数、头部或请求体进行初步验证。例如,检查必填字段是否存在、数据格式是否正确等。如果验证失败,同样可以返回错误响应。
预处理请求 (Request Pre-processing)
▮▮▮▮⚝ 在请求到达处理器前,对请求进行必要的修改或增强,例如解析特定的头部信息、为请求添加上下文数据等。
日志与监控 (Logging & Monitoring)(针对特定路径)
▮▮▮▮⚝ 记录特定重要接口的访问日志或性能指标。

通过将这些前置检查和处理逻辑放在过滤器中,我们可以保持控制器代码的整洁,使其只关注核心业务逻辑的实现。 ✨

7.4 实现自定义过滤器

实现一个自定义的 Drogon 过滤器需要遵循以下步骤:

7.4.1 继承与声明

自定义过滤器必须继承自 drogon::HttpFilter<YourFilterClassName> 模板类。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // YourAuthFilter.h
2 #pragma once
3
4 #include <drogon/HttpFilter.h>
5 #include <drogon/HttpResponse.h>
6 #include <drogon/HttpRequest.h>
7
8 class YourAuthFilter : public drogon::HttpFilter<YourAuthFilter>
9 {
10 public:
11 // 重写 doFilter 方法
12 bool doFilter(const drogon::HttpRequestPtr &req,
13 drogon::FilterCallback &&fcb,
14 drogon::FilterChainCallback &&fccb);
15 };

这里,YourFilterClassName (即 YourAuthFilter) 需要作为模板参数传递给基类。

7.4.2 实现 doFilter 方法

核心逻辑在 doFilter 方法中实现。该方法的签名如下:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 bool doFilter(const drogon::HttpRequestPtr &req,
2 drogon::FilterCallback &&fcb,
3 drogon::FilterChainCallback &&fccb);

参数说明:

const drogon::HttpRequestPtr &req: 指向当前 HTTP 请求的智能指针。您可以访问请求的所有信息,如方法、URL、头部、参数、请求体等。
drogon::FilterCallback &&fcb: 这是一个回调函数。当过滤器决定 拦截 请求,不让它继续向下传递到处理器时,您需要调用这个回调函数,并传递一个 drogon::HttpResponsePtr 对象作为参数。例如 fcb(your_error_response);。调用后,doFilter 方法应该返回 false
drogon::FilterChainCallback &&fccb: 这是另一个回调函数。当过滤器处理完毕,并且希望请求继续传递给下一个过滤器(如果存在)或最终的处理器时,您需要调用这个回调函数,即 fccb()。调用后,doFilter 方法应该返回 true

doFilter 方法的返回值 ( bool ) 指示了请求是否应该继续在处理链中传递:

⚝ 返回 true: 请求继续传递到下一个阶段(下一个过滤器或最终处理器)。在返回 true 之前,必须调用 fccb()
⚝ 返回 false: 请求被当前过滤器拦截,处理链终止。在返回 false 之前,必须调用 fcb(response) 来发送一个响应给客户端。

示例:实现一个简单的身份验证过滤器

假设我们要实现一个过滤器,检查请求头部是否包含一个名为 Authorization 且值为 valid-token 的头部。

filters/YourAuthFilter.cc

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include "YourAuthFilter.h"
2 #include <drogon/drogon.h>
3
4 bool YourAuthFilter::doFilter(const drogon::HttpRequestPtr &req,
5 drogon::FilterCallback &&fcb,
6 drogon::FilterChainCallback &&fccb)
7 {
8 // 尝试获取 Authorization 头部
9 auto auth_header = req->getHeader("Authorization");
10
11 if (!auth_header.empty() && auth_header == "valid-token")
12 {
13 // 身份验证通过,请求继续
14 LOG_INFO << "AuthFilter passed for path: " << req->getPath();
15 fccb(); // 调用过滤器链回调,让请求继续
16 return true;
17 }
18 else
19 {
20 // 身份验证失败,拦截请求并返回 401 Unauthorized
21 LOG_WARN << "AuthFilter failed for path: " << req->getPath();
22 auto resp = drogon::HttpResponse::newHttpResponse();
23 resp->setStatusCode(drogon::k401Unauthorized);
24 resp->setBody("Unauthorized Access");
25 // 设置 WWW-Authenticate 头部是 RESTful API 的好习惯
26 resp->addHeader("WWW-Authenticate", "Bearer realm=\"Drogon API\"");
27 fcb(resp); // 调用过滤器回调,返回错误响应
28 return false; // 终止处理链
29 }
30 }

这个过滤器首先检查 Authorization 头部,如果匹配则调用 fccb() 并返回 true;否则,创建一个 401 响应,调用 fcb() 将响应发送回客户端,并返回 false

7.4.3 过滤器名称

每个过滤器都需要一个唯一的名称以便于引用。这个名称通常是过滤器的类名(不包含命名空间)。在上面的例子中,过滤器名称就是 YourAuthFilter。在应用过滤器时,就是使用这个名称。

7.5 将中间件与过滤器应用于路由或控制器

在 Drogon 中,实现好的过滤器(drogon::HttpFilter 的子类)需要被应用到特定的请求处理目标上才能生效。这可以通过两种主要方式完成:应用于函数式处理器或应用于类式控制器。

7.5.1 应用于函数式处理器 (Function Handlers)

对于使用 Lambda 函数或普通函数作为请求处理器的路由,可以在定义路由时使用 .filter() 方法来指定要应用的过滤器。您可以应用一个或多个过滤器。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 应用单个过滤器
2 drogon::app().registerHandler(
3 "/api/resource",
4 [](const drogon::HttpRequestPtr &req,
5 std::function<void(const drogon::HttpResponsePtr &)> &&callback) {
6 // 只有通过 YourAuthFilter 的请求才能到达这里
7 auto resp = drogon::HttpResponse::newHttpResponse();
8 resp->setStatusCode(drogon::k200OK);
9 resp->setBody("Access granted to resource!");
10 callback(resp);
11 },
12 {drogon::HttpMethod::Get},
13 "YourAuthFilter" // 指定过滤器的名称
14 );
15
16 // 应用多个过滤器 (按顺序执行)
17 drogon::app().registerHandler(
18 "/api/secure_data",
19 [](const drogon::HttpRequestPtr &req,
20 std::function<void(const drogon::HttpResponsePtr &)> &&callback) {
21 // 只有通过 AuthFilter 和 ValidateFilter 的请求才能到达这里
22 auto resp = drogon::HttpResponse::newHttpResponse();
23 resp->setStatusCode(drogon::k200OK);
24 resp->setBody("Secret data unlocked!");
25 callback(resp);
26 },
27 {drogon::HttpMethod::Post},
28 "YourAuthFilter,YourValidateFilter" // 使用逗号分隔多个过滤器名称
29 );

.registerHandler() 的最后一个参数中,您可以提供一个字符串,包含一个或多个过滤器名称,多个名称之间用逗号 , 分隔。这些过滤器将按照列出的顺序依次执行。

7.5.2 应用于类式控制器 (Class-based Controllers)

对于继承自 drogon::HttpController 的控制器,可以通过在类定义前使用 Drogon 提供的特定宏来指定要应用于整个控制器的过滤器。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // controllers/UserController.h
2 #pragma once
3
4 #include <drogon/HttpController.h>
5
6 // 在控制器类声明前使用 DROGON_API_ADD_FILTERS 宏
7 // "YourAuthFilter,YourLogFilter" 表示对这个控制器的所有方法应用这两个过滤器
8 DROGON_API_ADD_FILTERS("YourAuthFilter,YourLogFilter")
9 class UserController : public drogon::HttpController<UserController>
10 {
11 public:
12 METHOD_LIST_BEGIN
13 METHOD_ADD(UserController::getInfo, "/{id}", drogon::Get);
14 METHOD_ADD(UserController::updateInfo, "/{id}", drogon::Post);
15 METHOD_LIST_END
16
17 void getInfo(const drogon::HttpRequestPtr &req,
18 std::function<void(const drogon::HttpResponsePtr &)> &&callback,
19 int id);
20
21 void updateInfo(const drogon::HttpRequestPtr &req,
22 std::function<void(const drogon::HttpResponsePtr &)> &&callback,
23 int id);
24 };

使用 DROGON_API_ADD_FILTERS("Filter1,Filter2,...") 宏可以将指定的过滤器应用于该控制器的 所有 方法(即所有通过 METHOD_ADD 定义的路由)。过滤器会按照宏中指定的顺序执行。

7.5.3 过滤器执行顺序

如果同一个请求路径被多个过滤器处理,它们会按照定义时的顺序依次执行。在前一个过滤器调用 fccb() 并返回 true 后,请求会进入下一个过滤器的 doFilter 方法。如果任何一个过滤器调用了 fcb() 并返回 false,则整个过滤器链和后续的处理器都将被跳过,直接将该过滤器提供的响应发送回客户端。

需要注意的是,过滤器是框架在匹配到路由后,执行实际处理逻辑前调用的。它们属于请求处理管道的一部分,用于实现横切关注点 (Cross-cutting concerns)。合理地使用过滤器可以极大地提升代码的可维护性和结构清晰度。

至此,我们全面解析了 Drogon 中的中间件概念以及如何通过 HttpFilter 机制实现和应用过滤器。掌握这些内容,您就能更好地构建功能丰富且结构清晰的 Drogon Web 应用。

8. 视图渲染与静态文件服务

本章将引导读者了解如何在 Drogon 框架中处理动态内容的生成(即视图渲染)和静态资源的提供。高效地处理视图和静态文件是构建现代 Web 应用不可或缺的一部分,本章将详细介绍 Drogon 内置的功能以及如何集成第三方库来实现这些需求。

8.1 静态文件服务 (Static File Serving)

静态文件是指那些不需要服务器端处理,可以直接通过 HTTP 请求发送给客户端的文件,例如 HTML 文件(非模板)、CSS 样式表、JavaScript 脚本、图片、字体文件、视频等。提供高效的静态文件服务对于 Web 应用的性能至关重要,因为这些文件往往构成了用户界面和客户端逻辑的主体。

配置静态文件路径
Drogon 通过配置文件 config.json 来指定哪些目录用于提供静态文件服务。默认情况下,Drogon 会将项目根目录下的 www 目录配置为静态文件根目录。
您可以在 config.jsonuploadAndStaticFiles 字段中配置静态文件目录。这个字段是一个 JSON 对象,其键是 URL 路径前缀,值是服务器文件系统中的物理路径。

例如,以下配置表示将 www 目录下的文件通过 / 前缀提供访问,将 downloads 目录下的文件通过 /dl 前缀提供访问:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "uploadAndStaticFiles": {
3 "/": "www",
4 "/dl": "downloads"
5 },
6 ...
7 }

如果请求的 URL 以 / 开头,并且能在 www 目录中找到对应的文件(例如请求 /index.html 会查找 www/index.html),Drogon 就会直接将该文件作为静态资源发送给客户端。同样,请求 /dl/report.pdf 会查找 downloads/report.pdf

默认配置
即使不显式配置 uploadAndStaticFiles 字段,Drogon 也会默认将项目根目录下的 www 目录配置为静态文件根目录,通过 / 前缀提供服务。这对于简单的项目来说非常方便。

重要的配置项
与静态文件服务相关的还有一些其他配置选项,您可以在 config.json 中找到:
▮▮▮▮⚝ "gzipStatic" (布尔值 bool): 是否对静态文件进行 Gzip 压缩。启用 Gzip 可以显著减小传输数据量,提高加载速度,但会稍微增加服务器的 CPU 负担。建议在生产环境启用。
▮▮▮▮⚝ "staticFileCacheTime" (整数 int, 单位秒): 静态文件在浏览器端的缓存时间。设置合适的缓存时间可以减少浏览器对静态资源的重复请求。生产环境中通常设置为较大的值(例如 3600 秒或更长),开发环境中可以设置为 0 或较小的值以便于更新。
▮▮▮▮⚝ "implicitPageEnable" (布尔值 bool): 是否启用隐式页面功能。如果启用,当请求目录时(例如 //some/dir/),Drogon 会尝试查找该目录下的默认文件,如 index.html
▮▮▮▮⚝ "implicitPage" (字符串数组 string array): 隐式页面的文件名列表。例如 ["index.html", "default.html"]

以下是一个包含这些配置的示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "uploadAndStaticFiles": {
3 "/": "www"
4 },
5 "gzipStatic": true,
6 "staticFileCacheTime": 3600,
7 "implicitPageEnable": true,
8 "implicitPage": ["index.html"],
9 ...
10 }

如何工作
当 Drogon 服务器接收到一个 HTTP 请求时,它首先会检查请求的 URL 是否与任何静态文件配置的路径前缀匹配。如果匹配,Drogon 会检查在对应的物理路径下是否存在请求的文件。如果文件存在,Drogon 会读取文件内容,设置正确的 Content-Type 头部,并根据配置添加 Content-Encoding: gzip (如果启用了 Gzip) 和 Cache-Control 头部,然后将文件内容作为响应体发送给客户端。如果文件不存在,请求会继续传递给路由系统进行处理。

需要注意的是,静态文件服务是同步阻塞的。尽管 Drogon 大部分是异步的,但为了简单和效率(读取磁盘文件通常是瓶颈),静态文件服务被实现在单独的线程池中处理,以避免阻塞主事件循环。

8.2 Drogon 内置模板引擎 (Drogon Built-in Template Engine)

除了提供静态文件,Web 应用通常还需要生成动态 HTML 页面,例如显示数据库中的数据、根据用户状态呈现不同的内容等。模板引擎 (Template Engine) 就是用来解决这个问题的工具,它允许开发者将 HTML 结构与动态数据分离,通过特定的语法将数据“填充”到模板中生成最终的 HTML 字符串。

Drogon 提供了一个内置的简单模板引擎,其设计目标是轻量级和易于使用,尤其适用于生成 HTML 响应。内置模板引擎的文件通常使用 .csp 后缀(Drogon 自定义后缀)。

基本语法
Drogon 内置模板引擎的语法灵感来源于其他 Web 框架的模板系统,它允许在静态 HTML 结构中嵌入 C++ 代码或表达式。

主要语法元素包括:
▮▮▮▮⚝ 输出变量或表达式: 使用 {{ expression }}{%== expression %}。前者会对输出进行 HTML 转义 (HTML Escaping),防止 XSS 攻击;后者则直接输出原始字符串,用于输出已经安全的 HTML 片段。
▮▮▮▮⚝ 执行 C++ 代码块: 使用 {% C++ code %}。这里的 C++ 代码会在服务器端执行,但其输出不会直接进入最终的 HTML,除非您显式地使用输出语法。常用于定义变量、执行逻辑、循环等。
▮▮▮▮⚝ 包含其他模板: 使用 {% include "template_name.csp" %}。这允许模板的复用,例如页眉、页脚等。
▮▮▮▮⚝ 注释: 使用 {# comment #}

创建 .csp 文件
内置模板文件通常存放在项目根目录下的 views 目录中。例如,您可以创建一个名为 views/hello.csp 的文件:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <title>Hello Page</title>
5 </head>
6 <body>
7 <h1>Hello, {%== name %}!</h1>
8 <p>Current Time: {{ time }}</p>
9
10 <h2>Numbers from 1 to 5:</h2>
11 <ul>
12 {% for(int i = 1; i <= 5; ++i) { %}
13 <li>Number: {{ i }}</li>
14 {% } %}
15 </ul>
16
17 {% if (show_message) { %}
18 <p style="color: green;">This is a conditional message.</p>
19 {% } else { %}
20 <p style="color: red;">No message shown.</p>
21 {% } %}
22
23 {# This is a template comment #}
24
25 </body>
26 </html>

在这个例子中:
{%== name %} 会输出一个名为 name 的变量的值,且不进行 HTML 转义。
{{ time }} 会输出一个名为 time 的变量的值,并进行 HTML 转义。
{% for(...) { ... } %} 嵌入了一个 C++ 的 for 循环。
{% if(...) { ... } else { ... } %} 嵌入了一个 C++ 的 if-else 分支。

传递数据
要在控制器中使用模板并向其传递数据,您需要将数据打包成 drogon::AuxiliaryData 类型(通常是一个 std::map<std::string, std::string> 或类似的结构,但 Drogon 实际上使用的是 trantor::any 包装的数据)。控制器会将这个数据对象传递给视图渲染器。在 .csp 文件中,您可以通过变量名直接访问传递进来的数据。

例如,如果在控制器中传递了一个键为 "name" 值为 "Drogon" 的数据项,您就可以在模板中使用 {%== name %} 来获取并输出 "Drogon"

编译模板
Drogon 的内置模板在项目编译时会被处理。drogons CLI 工具或 CMake 构建脚本会找到 views 目录下的 .csp 文件,并将它们转换为 C++ 代码。这些生成的 C++ 代码实现了渲染模板的逻辑,然后在运行时被调用。这意味着模板错误通常会在编译阶段被发现,而不是在运行时。

8.3 集成第三方模板引擎 (Integrating Third-Party Template Engines)

虽然 Drogon 内置模板引擎简单易用,但可能无法满足所有项目的需求。例如,您可能需要更复杂的控制结构、更丰富的过滤器 (Filter)、或者团队成员已经熟悉了其他模板引擎的语法(如 Jinja2、Mustache、Tera 等)。在这种情况下,Drogon 允许您集成任何支持 C++ 的第三方模板引擎。

集成第三方模板引擎的核心是实现 Drogon 提供的 drogon::CustomView 接口,并将其注册到 Drogon 的视图管理系统中。

为什么集成第三方模板引擎
▮▮▮▮⚝ 功能更强大: 第三方引擎通常提供更丰富的特性,如宏、继承、更复杂的表达式处理等。
▮▮▮▮⚝ 语法更友好: 不同的开发者可能偏好不同的模板语法,集成第三方引擎可以选择团队最熟悉的语法。
▮▮▮▮⚝ 生态系统: 一些第三方引擎有更成熟的工具链、社区支持和预制模板。

drogon::CustomView 接口
要集成第三方模板引擎,您需要创建一个新的 C++ 类,继承自 drogon::CustomView 并实现其纯虚函数。drogon::CustomView 接口定义了模板渲染器必须提供的功能。

其核心是 render 方法的签名(可能略有差异,请参考最新 Drogon 文档,但基本概念是):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 virtual void render(
2 std::string& result,
3 const std::string& viewName,
4 const drogon::AuxiliaryData& data
5 ) = 0;

▮▮▮▮⚝ std::string& result: 这是一个输出参数。您的渲染器需要将生成的最终 HTML (或任何文本格式) 字符串写入到这个引用中。
▮▮▮▮⚝ const std::string& viewName: 这是一个输入参数,表示要渲染的模板的名称(例如 hellousers/list)。您的渲染器需要根据这个名称找到对应的模板文件。
▮▮▮▮⚝ const drogon::AuxiliaryData& data: 这是一个输入参数,包含了从控制器传递过来的所有动态数据。您的渲染器需要能够从这个对象中提取数据,并将其应用到模板中。drogon::AuxiliaryData 通常包含了一个 std::map<std::string, trantor::any> 或类似结构,允许您存储各种类型的数据。

实现步骤
▮▮▮▮ⓑ 引入第三方库: 首先,将您选择的第三方模板引擎库添加到您的项目中,并确保可以正确编译和链接。
▮▮▮▮ⓒ 创建自定义视图渲染器类: 创建一个继承自 drogon::CustomView 的类,例如 MyTeraView
▮▮▮▮ⓓ 实现 render 方法: 在 render 方法中:
▮▮▮▮▮▮▮▮❺ 根据 viewName 定位到实际的模板文件路径(例如 views/tera/hello.html)。
▮▮▮▮▮▮▮▮❻ 读取模板文件内容。
▮▮▮▮▮▮▮▮❼ 将 drogon::AuxiliaryData 中的数据转换为第三方模板引擎可识别的数据结构(例如,如果第三方引擎使用 std::map<std::string, SomeVariantType>,您需要进行数据转换)。
▮▮▮▮▮▮▮▮❽ 调用第三方模板引擎的 API,传入模板内容和转换后的数据,执行渲染过程。
▮▮▮▮▮▮▮▮❾ 将第三方引擎生成的输出字符串赋值给 result 参数。
▮▮▮▮ⓙ 注册自定义渲染器: 在您的应用程序初始化代码中,您需要创建一个自定义渲染器类的实例,并使用 drogon::app().addCustomView() 或类似方法将其注册到 Drogon 框架中。注册时需要指定该渲染器负责处理的模板文件扩展名(例如 .tera, .mustache)。

示例注册代码(概念性):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/drogon.h>
2 #include "MyTeraView.h" // 您自定义的 Tera 视图渲染器类
3
4 int main() {
5 // ... 其他 Drogon 配置 ...
6
7 // 创建您的自定义视图渲染器实例
8 auto teraViewRenderer = std::make_shared<MyTeraView>();
9
10 // 将其注册到 Drogon,关联 .tera 文件扩展名
11 drogon::app().addCustomView(teraViewRenderer, ".tera");
12
13 // ... 启动 Drogon ...
14 drogon::app().run();
15 return 0;
16 }

通过这些步骤,当 Drogon 的控制器请求渲染一个 .tera 后缀的视图时,它就会调用您的 MyTeraView 实例的 render 方法来完成渲染。

8.4 构建视图渲染器 (Building View Renderers)

本节将更专注于构建自定义视图渲染器的通用过程和需要注意的细节,即使您不集成第三方库,理解这个过程对于理解 Drogon 的视图系统也非常重要。如上一节所述,核心是实现 drogon::CustomView 接口。

drogon::CustomView 接口回顾

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 class CustomView {
2 public:
3 virtual ~CustomView() {}
4
5 /**
6 * @brief Render the view
7 *
8 * @param result The output result.
9 * @param viewName The name of the view.
10 * @param data The data to be used in the view.
11 * @note This method is called in the event loop thread.
12 * Any blocking code should be avoided.
13 */
14 virtual void render(
15 std::string& result,
16 const std::string& viewName,
17 const drogon::AuxiliaryData& data
18 ) = 0;
19
20 /**
21 * @brief Return true if this view is cached.
22 */
23 virtual bool isCached() const { return false; }
24
25 /**
26 * @brief The extension name of the view file. e.g. ".html"
27 */
28 virtual const std::string& getFileExtension() const = 0;
29 };

除了 render 方法,还需要实现 isCached()getFileExtension()
▮▮▮▮⚝ isCached(): 如果您的渲染器支持模板缓存(即加载和解析模板只需一次,后续渲染直接使用内存中的表示),则返回 true。这可以提高性能。如果返回 false,则每次渲染请求都会重新加载和解析模板文件。
▮▮▮▮⚝ getFileExtension(): 返回该渲染器处理的模板文件扩展名,例如 ".tera"".html"

数据传递与 AuxiliaryData
drogon::AuxiliaryData 是一个关键类,它允许您从控制器向视图传递任意类型的数据。它内部使用 trantor::any 来存储键值对,其中键是 std::string,值是任意类型。

在控制器中,您可以这样构建 AuxiliaryData

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogon::AuxiliaryData data;
2 data.insert("username", "Alice"); // 字符串
3 data.insert("age", 30); // 整数
4 data.insert("is_admin", true); // 布尔值
5 data.insert("items", std::vector<std::string>{"item1", "item2"}); // 容器

在您的自定义渲染器的 render 方法中,您需要从 data 中提取这些数据并转换为您的模板引擎可以处理的格式。例如:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 void MyTeraView::render(
2 std::string& result,
3 const std::string& viewName,
4 const drogon::AuxiliaryData& data
5 ) {
6 // ... 找到模板文件 ...
7
8 // 将 drogon::AuxiliaryData 转换为 Tera 可以用的 Context
9 tera::Context context;
10 for (const auto& pair : data) {
11 const std::string& key = pair.first;
12 const trantor::any& value = pair.second;
13
14 // 您需要根据 value 的类型进行 trantor::any_cast 转换
15 // 并将数据存入 context
16 if (value.type() == typeid(std::string)) {
17 context.insert(key, *trantor::any_cast<std::string>(&value));
18 } else if (value.type() == typeid(int)) {
19 context.insert(key, *trantor::any_cast<int>(&value));
20 }
21 // ... 处理其他类型 ...
22 }
23
24 // 调用 Tera 引擎进行渲染
25 // ...
26 result = rendered_string;
27 }

这个转换过程是集成第三方库时最核心的部分之一。

线程安全性
Drogon 的 render 方法默认在事件循环线程中被调用。这意味着您在实现 render 方法时应该避免执行任何阻塞操作,例如长时间的磁盘 I/O 或网络请求。如果您的模板引擎渲染过程可能阻塞,您需要考虑将其放在 Drogon 的工作线程池中执行,或者使用异步 I/O。不过,大多数模板引擎的渲染本身是 CPU 密集型的,只要模板文件已经加载到内存,渲染过程通常不会长时间阻塞。加载模板文件本身如果可能阻塞,则应该在初始化阶段完成或使用异步文件读取。

错误处理
render 方法中,如果模板渲染失败(例如模板语法错误、数据缺失),您应该捕获异常并在 Drogon 的日志系统中记录错误。通常,渲染错误会导致返回给客户端一个 500 Internal Server Error 响应。

通过实现 drogon::CustomView 接口,您可以为 Drogon 添加对几乎任何 C++ 模板引擎的支持,极大地增强了框架的灵活性。

8.5 在控制器中使用视图

现在,我们来看看如何在实际的 Drogon 控制器中调用视图渲染功能,并将结果返回给客户端。这通常发生在处理一个需要返回完整 HTML 页面的 HTTP 请求时。

控制器处理方法
首先,您需要有一个控制器类(继承自 drogon::HttpController)或一个处理函数来处理特定的 HTTP 请求。例如,一个用来显示用户详情页的控制器方法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/drogon.h>
2 #include "models/User.h" // 假设您有一个 User 模型
3
4 class UserController : public drogon::HttpController<UserController> {
5 public:
6 METHOD_LIST_BEGIN
7 METHOD_ADD(UserController::getUserDetail, "/users/{user_id}", Get);
8 METHOD_LIST_END
9
10 void getUserDetail(
11 const drogon::HttpRequestPtr& req,
12 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
13 int user_id // 路径参数绑定
14 );
15 };

准备数据
在控制器方法中,您需要获取用于填充模板的动态数据。这些数据可能来自:
▮▮▮▮⚝ 请求参数 (Request Parameters)。
▮▮▮▮⚝ URL 路径参数 (URL Path Parameters)。
▮▮▮▮⚝ 数据库查询。
▮▮▮▮⚝ 业务逻辑计算结果。

例如,在 getUserDetail 方法中,您可能需要根据 user_id 去数据库查询用户信息:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 void UserController::getUserDetail(
2 const drogon::HttpRequestPtr& req,
3 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
4 int user_id
5 ) {
6 // 假设使用 ORM 查询用户
7 auto dbClient = drogon::app().getDbClient();
8 dbClient->execSqlAsync("SELECT * FROM users WHERE id = $1",
9 [callback](const drogon::orm::Result& res) {
10 drogon::AuxiliaryData data;
11 if (res.size() > 0) {
12 // 假设第一行就是用户数据
13 auto user_name = res[0]["name"].as<std::string>();
14 auto user_email = res[0]["email"].as<std::string>();
15
16 data.insert("username", user_name);
17 data.insert("email", user_email);
18 data.insert("found_user", true);
19 } else {
20 data.insert("found_user", false);
21 }
22
23 // ... 接下来是渲染视图 ...
24 },
25 [callback](const drogon::orm::DrogonDbException& e) {
26 // 数据库查询失败
27 auto resp = drogon::HttpResponse::newHttpResponse(drogon::k500InternalServerError);
28 resp->setBody("Database error: " + std::string(e.base().what()));
29 callback(resp);
30 },
31 user_id // 绑定参数
32 );
33 }

请注意,数据库查询是异步的,所以渲染视图的代码需要在查询成功的回调函数中进行。

构建 AuxiliaryData
将您准备好的数据存储到 drogon::AuxiliaryData 对象中。确保数据的键名与模板中使用的变量名一致。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // ... 在数据库查询成功的回调中 ...
2 drogon::AuxiliaryData data;
3 if (res.size() > 0) {
4 auto user_name = res[0]["name"].as<std::string>();
5 data.insert("username", user_name);
6 data.insert("found_user", true);
7 } else {
8 data.insert("found_user", false);
9 }
10
11 // ... 传递其他数据,例如请求 ID, 当前时间等 ...
12 data.insert("request_id", req->attributes()->get<std::string>("request_id")); // 假设从请求属性中获取
13 data.insert("current_time", drogon::utils::get <std::string>(trantor::Date::now())); // 假设格式化当前时间

调用视图渲染器
使用 drogon::HttpResponse::newHttpViewResponse() 静态方法来创建 HTTP 响应。这个方法接受视图名称和包含数据的 AuxiliaryData 对象作为参数。Drogon 会根据视图名称(特别是文件扩展名)找到对应的视图渲染器,调用其 render 方法生成 HTML 内容。

假设您的视图文件是 views/user_detail.csp (使用内置模板引擎) 或 views/user_detail.tera (使用自定义 Tera 渲染器)。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // ... 在数据库查询成功的回调中 ...
2 // ... 构建 AuxiliaryData data ...
3
4 // 创建一个 HTTP 响应,使用名为 "user_detail" 的视图和 data 数据
5 // Drogon 会根据视图类型 (例如 .csp 或 .tera) 选择合适的渲染器
6 auto resp = drogon::HttpResponse::newHttpViewResponse("user_detail", data);
7
8 // 设置响应的 Content-Type (通常 Drogon 会自动设置为 text/html)
9 // resp->setContentTypeCode(drogon::CT_TEXT_HTML);
10
11 // 调用 callback 将响应发送给客户端
12 callback(resp);
13 },
14 // ... 数据库查询失败回调 ...
15 user_id
16 ); // execSqlAsync 结束

请注意,newHttpViewResponse 方法的第一个参数是视图的名称,通常是相对于 views 目录的路径,不包含文件扩展名。Drogon 会根据配置的视图渲染器查找具有匹配扩展名的文件(例如,如果注册了 .csp.tera 渲染器,它会查找 views/user_detail.cspviews/user_detail.tera)。

返回响应
最后,通过调用回调函数 callback 并传入生成的 HttpResponsePtr 对象,将响应发送回客户端。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // ... 在视图渲染完成后 ...
2 callback(resp);

通过上述步骤,您可以在 Drogon 控制器中高效地集成视图渲染,将动态数据与静态模板结合,生成完整的 HTML 页面响应给客户端。

9. 高级路由与 RESTful API 实践

欢迎来到本书第九章!在前几章中,我们学习了 Drogon 的基础知识,包括环境搭建、核心的请求响应处理以及基本的路由和控制器用法。这些是构建 Web 应用的基石。然而,在实际开发中,特别是构建复杂的 Web 服务或公共 API (Application Programming Interface) 时,我们会遇到更高级的需求,例如需要更灵活的 URL (Uniform Resource Locator) 匹配、更好的代码组织方式,以及符合行业标准的 API 设计风格。

本章将带您深入探索 Drogon 的高级路由特性,特别是如何利用正则表达式实现灵活的 URL 匹配,以及如何使用嵌套控制器来更好地组织大型项目的代码结构。更重要的是,我们将聚焦于 RESTful 架构风格,探讨如何结合 Drogon 的路由和控制器机制,设计和实现高质量的 RESTful API。最后,我们还将讨论在构建 API 时一个常见且重要的问题:API 版本控制。

通过本章的学习,您将能够运用 Drogon 构建更健壮、更灵活、更易于维护和扩展的 Web 服务和 API。

9.1 正则表达式路由 (Regex Routing)

Drogon 的基础路由已经非常强大,可以处理静态路径、路径参数等常见场景。但有时,我们需要匹配更复杂的 URL 模式,例如包含特定格式标识符、日期或任意嵌套路径等。这时,正则表达式路由就显得尤为重要。

正则表达式路由允许我们使用强大的正则表达式语法来定义 URL 匹配规则,从而实现高度灵活的路由配置。

9.1.1 正则表达式路由的基本用法

在 Drogon 中,我们可以通过在路由路径字符串中使用特殊的语法来指定正则表达式路由。通常,路径中包含的正则表达式部分会用一对大括号 {} 包围,并在其内部使用正则表达式。被匹配到的子表达式(捕获组)可以作为参数传递给处理函数或控制器方法。

Drogon 使用 std::regex 来处理正则表达式。

以下是定义一个正则表达式路由的基本语法示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogon::app().registerHandler(
2 "/users/{id:\\d+}", // 路径中的 {id:\\d+} 是正则表达式部分
3 [](const drogon::HttpRequestPtr& req,
4 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
5 int id) { // id 参数将捕获 \\d+ 匹配到的数值
6 auto resp = drogon::HttpResponse::newHttpResponse();
7 resp->setStatusCode(drogon::k200OK);
8 resp->setContentTypeCode(drogon::CT_TEXT_PLAIN);
9 resp->setBody("User ID: " + std::to_string(id));
10 callback(resp);
11 },
12 {drogon::HttpMethod::kGet} // 只处理 GET 请求
13 );

在这个例子中:
/users/{id:\\d+} 定义了一个路由。
▮▮▮▮⚝ {id:\\d+} 是一个正则表达式部分。id 是这个捕获组的名称,\\d+ 是正则表达式,表示匹配一个或多个数字。
▮▮▮▮⚝ 当 URL 匹配 /users/123 时,123 会被 \\d+ 捕获,并赋值给处理函数的 int id 参数。

需要注意的是,正则表达式中的反斜杠 \ 在 C++ 字符串字面量中需要转义,所以 \d+ 要写成 \\d+

9.1.2 捕获组与参数绑定

正则表达式中的捕获组 (capturing group) 可以用来提取 URL 中的特定部分作为参数。Drogon 会尝试将这些捕获到的字符串转换成处理函数或控制器方法参数的类型(例如 int, std::string 等)。

例如,要匹配 /archive/YYYY/MM/DD 这样的日期格式 URL:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogon::app().registerHandler(
2 "/archive/{year:\\d{4}}/{month:\\d{2}}/{day:\\d{2}}",
3 [](const drogon::HttpRequestPtr& req,
4 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
5 int year, int month, int day) {
6 auto resp = drogon::HttpResponse::newHttpResponse();
7 resp->setStatusCode(drogon::k200OK);
8 resp->setContentTypeCode(drogon::CT_TEXT_PLAIN);
9 resp->setBody("Archive date: " + std::to_string(year) + "-" +
10 std::to_string(month) + "-" + std::to_string(day));
11 callback(resp);
12 },
13 {drogon::HttpMethod::kGet}
14 );

这里,{year:\\d{4}} 匹配四位数字并绑定到 year 参数,{month:\\d{2}} 匹配两位数字绑定到 month{day:\\d{2}} 匹配两位数字绑定到 day

9.1.3 注意事项

① 正则表达式路由的匹配通常比静态路由或普通参数路由要慢一些,因为它需要执行正则表达式匹配。在性能敏感的场景下,应谨慎使用过于复杂的正则表达式。
② 多个正则表达式路由可能存在匹配冲突。Drogon 会根据内部的优先级规则进行匹配,但建议尽量避免定义相互冲突的复杂正则表达式路由,这会增加调试难度。
③ 如果正则表达式没有捕获组,或者捕获组没有命名,那么匹配到的整个正则表达式部分会作为参数传递(通常是 std::string 类型)。但为了清晰和类型安全,建议总是命名捕获组。

9.2 嵌套控制器 (Nested Controllers)

随着 Web 应用规模的增长,路由和控制器会变得越来越多。将所有路由定义和处理逻辑都放在顶层可能会导致代码结构混乱,难以管理。嵌套控制器是一种组织代码的有效方式,它允许我们将相关的控制器或路由分组,形成层级结构。

在 Drogon 中,嵌套控制器(或称为子控制器)通常通过 HttpController 的派生类来实现,它们可以有自己的子路由。

9.2.1 定义嵌套控制器

一个控制器可以作为另一个控制器的“子”控制器,通过在其父控制器的 initAndReg() 方法中注册。子控制器负责处理以其父控制器路径为前缀的 URL。

假设我们有一个 UserController 处理 /users 相关的路由,现在想在 /users 路径下进一步划分,例如 /users/profile/users/settings。我们可以创建一个 UserProfileControllerUserSettingsController 作为 UserController 的子控制器。

首先,定义父控制器 UserController

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // UserController.h
2 #pragma once
3 #include <drogon/HttpController.h>
4
5 class UserController : public drogon::HttpController<UserController>
6 {
7 public:
8 METHOD_LIST_BEGIN
9 // 这里定义 UserController 自己的顶层路由,例如 /users
10 METHOD_ADD(UserController::getUsers, "/users", drogon::HttpMethod::kGet);
11 METHOD_LIST_END
12
13 void getUsers(const drogon::HttpRequestPtr& req,
14 std::function<void(const drogon::HttpResponsePtr&)>&& callback) const;
15
16 private:
17 // 在 private 或 protected 部分声明子控制器
18 // 注意:这里不是实际的子控制器对象,而是在initAndReg中注册
19 };

然后,定义一个子控制器 UserProfileController

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // UserProfileController.h
2 #pragma once
3 #include <drogon/HttpController.h>
4
5 class UserProfileController : public drogon::HttpController<UserProfileController>
6 {
7 public:
8 METHOD_LIST_BEGIN
9 // 这里定义子控制器的路由。这些路由将相对于父控制器的路径
10 METHOD_ADD(UserProfileController::getProfile, "/{id}/profile", drogon::HttpMethod::kGet);
11 METHOD_ADD(UserProfileController::updateProfile, "/{id}/profile", drogon::HttpMethod::kPost);
12 METHOD_LIST_END
13
14 void getProfile(const drogon::HttpRequestPtr& req,
15 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
16 int id) const;
17
18 void updateProfile(const drogon::HttpRequestPtr& req,
19 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
20 int id) const;
21 };

9.2.2 注册嵌套控制器

子控制器需要在父控制器的 initAndReg() 方法中通过 registerSubController() 方法进行注册。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // UserController.cpp
2 #include "UserController.h"
3 #include "UserProfileController.h" // 包含子控制器头文件
4
5 void UserController::initAndReg()
6 {
7 // 注册子控制器 UserProfileController
8 // UserProfileController 的所有路由都将以 /users 为前缀
9 registerSubController<UserProfileController>();
10
11 // 可以在这里继续注册其他子控制器,例如:
12 // registerSubController<UserSettingsController>();
13 }
14
15 void UserController::getUsers(const drogon::HttpRequestPtr& req,
16 std::function<void(const drogon::HttpResponsePtr&)>&& callback) const
17 {
18 // 处理 /users 的 GET 请求逻辑
19 auto resp = drogon::HttpResponse::newHttpResponse();
20 resp->setStatusCode(drogon::k200OK);
21 resp->setContentTypeCode(drogon::CT_TEXT_PLAIN);
22 resp->setBody("List of users");
23 callback(resp);
24 }
25
26 // UserProfileController.cpp
27 #include "UserProfileController.h"
28
29 void UserProfileController::getProfile(const drogon::HttpRequestPtr& req,
30 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
31 int id) const
32 {
33 // 处理 /users/{id}/profile 的 GET 请求逻辑
34 auto resp = drogon::HttpResponse::newHttpResponse();
35 resp->setStatusCode(drogon::k200OK);
36 resp->setContentTypeCode(drogon::CT_TEXT_PLAIN);
37 resp->setBody("Profile for user ID: " + std::to_string(id));
38 callback(resp);
39 }
40
41 void UserProfileController::updateProfile(const drogon::HttpRequestPtr& req,
42 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
43 int id) const
44 {
45 // 处理 /users/{id}/profile 的 POST 请求逻辑
46 auto resp = drogon::HttpResponse::newHttpResponse();
47 resp->setStatusCode(drogon::k200OK);
48 resp->setContentTypeCode(drogon::CT_TEXT_PLAIN);
49 resp->setBody("Profile updated for user ID: " + std::to_string(id));
50 callback(resp);
51 }

在这个例子中,/users/{id}/profile GET 请求会由 UserProfileController::getProfile 处理,而 /users/{id}/profile POST 请求会由 UserProfileController::updateProfile 处理。

9.2.3 嵌套控制器的优势

代码组织: 将相关的路由和处理逻辑分组,使代码结构更清晰,易于理解和维护。
模块化: 每个控制器都可以视为一个独立的模块,负责处理特定领域的请求。
路径管理: 避免在每个路由定义中重复写相同的路径前缀。
过滤器/中间件应用: 可以方便地将过滤器或中间件应用到整个子控制器,从而影响其下的所有路由。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // UserController.h (添加过滤器)
2 #pragma once
3 #include <drogon/HttpController.h>
4 #include "filters/AuthFilter.h" // 假设有一个 AuthFilter
5
6 class UserController : public drogon::HttpController<UserController>
7 {
8 public:
9 METHOD_LIST_BEGIN
10 METHOD_ADD(UserController::getUsers, "/users", drogon::HttpMethod::kGet, {&AuthFilter::filter}); // 应用 AuthFilter 到 /users 路由
11 METHOD_LIST_END
12
13 void initAndReg(); // 需要显式声明 initAndReg
14
15 void getUsers(const drogon::HttpRequestPtr& req,
16 std::function<void(const drogon::HttpResponsePtr&)>&& callback) const;
17 };
18
19 // UserProfileController.h (所有在其父控制器 UserProfileController 下注册的子控制器路由都会继承父控制器的过滤器)
20 // 但更常见的是直接在子控制器方法或其父控制器的registerSubController中应用
21 // 例如,可以在 initAndReg 中对 registerSubController 应用过滤器
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // UserController.cpp (在 initAndReg 中应用过滤器到子控制器)
2 #include "UserController.h"
3 #include "UserProfileController.h"
4 #include "filters/AnotherFilter.h" // 假设有另一个过滤器
5
6 void UserController::initAndReg()
7 {
8 // 注册子控制器 UserProfileController,并应用 AnotherFilter 到 UserProfileController 的所有路由
9 registerSubController<UserProfileController>({&AnotherFilter::filter});
10
11 // 如果子控制器自己的方法上也定义了过滤器,那么两者都会生效
12 }
13 // ... getUsers 方法实现 ...

通过嵌套控制器,我们可以构建一个清晰的路由层级,提高大型应用的内聚性和可维护性。

9.3 RESTful 控制器设计与实现

REST (Representational State Transfer) 是一种用于分布式系统(特别是 Web)的架构风格。它定义了一组设计原则,旨在提高系统的可伸缩性、简单性、可修改性和可见性。设计和实现 RESTful API 是现代 Web 服务开发中的一个重要实践。

RESTful API 的核心概念包括:
资源 (Resource): API 中的核心概念,可以是任何可命名、可寻址的事物(如用户、订单、产品)。每个资源通过一个唯一的 URI (Uniform Resource Identifier) 来标识。
状态的表示 (Representation): 资源的状态可以通过多种格式来表示(如 JSON (JavaScript Object Notation), XML (eXtensible Markup Language), HTML (HyperText Markup Language) 等)。客户端和服务器通过交换资源的某种表示来进行交互。
统一接口 (Uniform Interface): 这是 REST 的关键约束之一,它简化了系统架构,并提高了交互的可视性。主要体现在使用标准的 HTTP 方法(GET, POST, PUT, DELETE 等)来操作资源。
无状态 (Stateless): 服务器不存储客户端的状态信息。每个请求都包含处理该请求所需的所有信息。
可缓存 (Cacheable): 客户端和中间层可以缓存对资源的响应,以提高性能。
分层系统 (Layered System): 系统由多个层组成,每一层只知道与其紧邻的层。
按需代码 (Code on Demand, 可选): 服务器可以将可执行代码发送给客户端执行。

在 Drogon 中实现 RESTful API,主要是将这些原则映射到框架提供的功能上。

9.3.1 资源与 URI 设计

一个良好的 RESTful API 设计始于清晰的资源定义和 URI 设计。URI 应该标识资源本身,而不是对资源的操作。

通常使用名词来表示资源,并通过路径段来体现资源的层级关系。

例如:
⚝ 获取所有用户:GET /users
⚝ 获取 ID 为 123 的用户:GET /users/123
⚝ 创建一个新用户:POST /users (请求体中包含用户数据)
⚝ 更新 ID 为 123 的用户:PUT /users/123 (请求体中包含完整的用户数据) 或 PATCH /users/123 (请求体中包含部分用户数据)
⚝ 删除 ID 为 123 的用户:DELETE /users/123

在 Drogon 中,这可以通过路由和控制器方法来实现:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // UserController.h
2 #pragma once
3 #include <drogon/HttpController.h>
4
5 class UserController : public drogon::HttpController<UserController>
6 {
7 public:
8 METHOD_LIST_BEGIN
9 METHOD_ADD(UserController::getAllUsers, "/users", drogon::HttpMethod::kGet);
10 METHOD_ADD(UserController::getUserById, "/users/{id}", drogon::HttpMethod::kGet);
11 METHOD_ADD(UserController::createUser, "/users", drogon::HttpMethod::kPost);
12 METHOD_ADD(UserController::updateUser, "/users/{id}", drogon::HttpMethod::kPut);
13 METHOD_ADD(UserController::deleteUser, "/users/{id}", drogon::HttpMethod::kDelete);
14 METHOD_LIST_END
15
16 void getAllUsers(const drogon::HttpRequestPtr& req,
17 std::function<void(const drogon::HttpResponsePtr&)>&& callback) const;
18
19 void getUserById(const drogon::HttpRequestPtr& req,
20 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
21 int id) const;
22
23 void createUser(const drogon::HttpRequestPtr& req,
24 std::function<void(const drogon::HttpResponsePtr&)>&& callback) const;
25
26 void updateUser(const drogon::HttpRequestPtr& req,
27 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
28 int id) const;
29
30 void deleteUser(const drogon::HttpRequestPtr& req,
31 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
32 int id) const;
33 };

9.3.2 使用 HTTP 方法操作资源

RESTful API 依赖于标准的 HTTP 方法来指示对资源的操作类型:

GET: 获取资源。应该是幂等的 (idempotent) 和安全的 (safe)(不改变服务器状态)。
POST: 在指定 URI 下创建新资源,或者执行一个不适合其他方法的非幂等操作。
PUT: 更新指定 URI 下的资源(如果资源不存在,则创建)。应该是幂等的。
DELETE: 删除指定 URI 下的资源。应该是幂等的。
PATCH: 对指定 URI 下的资源进行部分更新。
HEAD: 类似于 GET,但只返回响应头部,没有响应体。用于获取资源的元数据。
OPTIONS: 获取资源支持的 HTTP 方法。

在 Drogon 的 METHOD_LIST_BEGIN/METHOD_LIST_END 宏中,我们可以通过 drogon::HttpMethod::k<Method> 来指定方法。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 METHOD_ADD(UserController::createUser, "/users", drogon::HttpMethod::kPost); // POST 用于创建
2 METHOD_ADD(UserController::updateUser, "/users/{id}", drogon::HttpMethod::kPut); // PUT 用于更新
3 METHOD_ADD(UserController::deleteUser, "/users/{id}", drogon::HttpMethod::kDelete); // DELETE 用于删除

9.3.3 状态的表示与内容协商

RESTful API 通常使用 JSON 作为数据交换格式,因为它轻量且易于解析。XML 也是一个选项,但 JSON 更常用。

客户端可以在请求头部通过 Accept 字段指定期望的响应格式,服务器则在响应头部通过 Content-Type 字段告知实际的响应格式。这是一个内容协商 (Content Negotiation) 的过程。

在 Drogon 中,构建 JSON 响应非常方便。您可以使用 Json::Value (通过 Drogon 依赖的 jsoncpp 库) 或 nlohmann::json (如果配置了) 来构建 JSON 对象,然后将其设置到响应体中,并将 Content-Type 设置为 application/json

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/drogon.h>
2 #include <json/json.h> // 或 <nlohmann/json.hpp>
3
4 void UserController::getUserById(const drogon::HttpRequestPtr& req,
5 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
6 int id) const
7 {
8 // 假设从数据库或其他地方获取用户数据
9 // User user = database->findUserById(id);
10
11 Json::Value userJson;
12 userJson["id"] = id;
13 userJson["name"] = "John Doe"; // 示例数据
14 userJson["email"] = "john.doe@example.com"; // 示例数据
15
16 auto resp = drogon::HttpResponse::newHttpJsonResponse(userJson); // Drogon 提供了构建 JSON 响应的便捷方法
17 // newHttpJsonResponse 内部会自动设置 Content-Type 为 application/json 并设置状态码 200 OK
18 callback(resp);
19 }
20
21 void UserController::createUser(const drogon::HttpRequestPtr& req,
22 std::function<void(const drogon::HttpResponsePtr&)>&& callback) const
23 {
24 // 从请求体中解析 JSON 数据
25 auto json = req->getJsonObject();
26 if (!json || !json->isMember("name") || !json->isMember("email"))
27 {
28 auto resp = drogon::HttpResponse::newHttpResponse();
29 resp->setStatusCode(drogon::k400BadRequest); // Bad Request
30 resp->setContentTypeCode(drogon::CT_TEXT_PLAIN);
31 resp->setBody("Missing name or email in request body");
32 callback(resp);
33 return;
34 }
35
36 std::string name = (*json)["name"].asString();
37 std::string email = (*json)["email"].asString();
38
39 // 在数据库中创建用户...
40
41 Json::Value responseJson;
42 responseJson["status"] = "success";
43 responseJson["message"] = "User created successfully";
44 // responseJson["user_id"] = newUser.id; // 如果创建成功并返回新 ID
45
46 auto resp = drogon::HttpResponse::newHttpJsonResponse(responseJson);
47 resp->setStatusCode(drogon::k201Created); // Created
48 callback(resp);
49 }

9.3.4 使用 HTTP 状态码

HTTP 状态码 (Status Code) 是客户端和服务器之间通信的重要组成部分,它们表示请求处理的结果。在 RESTful API 中正确使用状态码至关重要:

2xx (成功):
▮▮▮▮200 OK: 请求成功,并且响应体中包含了请求的结果。
▮▮▮▮201 Created: 请求成功,并且在服务器上创建了新的资源。通常用于 POST 请求。
▮▮▮▮204 No Content: 请求成功,但响应体中没有内容(例如 DELETE 请求成功)。
3xx (重定向):
▮▮▮▮301 Moved Permanently: 资源被永久移动到新的 URI。
▮▮▮▮302 Found: 资源被临时移动。
4xx (客户端错误):
▮▮▮▮400 Bad Request: 客户端请求的语法错误或参数无效。
▮▮▮▮401 Unauthorized: 请求需要用户身份验证。
▮▮▮▮403 Forbidden: 服务器理解请求,但拒绝执行(即使提供了身份验证)。
▮▮▮▮404 Not Found: 服务器找不到请求的资源。
▮▮▮▮405 Method Not Allowed: 请求方法不允许访问该资源。
▮▮▮▮409 Conflict: 请求与服务器当前状态冲突(例如,尝试创建已存在的资源)。
5xx (服务器错误):
▮▮▮▮500 Internal Server Error: 服务器在处理请求时发生了错误。
▮▮▮▮503 Service Unavailable: 服务器当前无法处理请求,通常是由于过载或维护。

在 Drogon 中,可以通过 HttpResponse::setStatusCode() 方法来设置状态码。便捷方法 newHttpJsonResponse() 默认返回 200 OK,但对于创建 (POST) 操作,您通常会手动将其改为 201 Created

9.3.5 无状态性与会话管理

RESTful API 应该是无状态的。这意味着服务器不应该存储客户端的会话状态。每个请求都必须包含所有必要的信息来完成处理。例如,身份验证信息(如 Token (令牌))应该在每个请求中通过头部(如 Authorization)传递,而不是依赖于服务器维护的 Session (会话)。

尽管 REST 提倡无状态,但 Web 应用通常需要管理用户会话。Drogon 提供了 Session 支持,这对于传统的 Web 应用(非纯 API)非常有用。对于构建纯粹的 RESTful API,推荐使用基于 Token 的认证机制(如 JWT (JSON Web Token)),这与无状态原则更契合。您可以在过滤器或中间件中解析和验证 Token。

9.3.6 RESTful API 设计的最佳实践

⚝ 使用名词而非动词作为 URI 的一部分(例如 /users 而不是 /getAllUsers)。
⚝ 使用复数名词表示资源集合,单数名词表示单个资源(例如 /users 表示用户列表,/users/123 表示特定用户)。
⚝ 使用子资源来表达关系(例如 /users/123/orders 表示用户 123 的订单)。
⚝ 利用查询参数 (query parameters) 进行过滤、排序和分页(例如 /users?status=active&sort=created_at&limit=10&offset=20)。
⚝ 在响应中包含资源的链接 (HATEOAS - Hypermedia as the Engine of Application State),这有助于客户端发现相关资源和可能的下一个操作。虽然不是 REST 的强制要求,但被认为是更高阶的 RESTful 设计。
⚝ 提供详细的错误信息,包括状态码和描述性错误消息,帮助客户端诊断问题。使用统一的错误响应格式。
⚝ 考虑 API 的可发现性,例如提供 API 文档(如 OpenAPI/Swagger)。

在 Drogon 中,您可以结合使用路由、控制器、请求/响应对象、过滤器和中间件来实现这些原则,构建符合 RESTful 风格的高质量 API。

9.4 API 版本控制 (API Versioning)

随着 API 的演进,您可能需要修改现有的端点,但这可能会破坏依赖旧版本 API 的客户端。API 版本控制是一种管理 API 变更,允许您在不中断现有客户端的情况下发布新版本的方式。

常见的 API 版本控制策略包括:

9.4.1 URL 版本控制 (URL Versioning)

这是最常见和直观的方式,将版本号直接嵌入到 URL 路径中。
例如:/v1/users, /v2/users

在 Drogon 中的实现:

在路由定义时直接包含版本号。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // API V1
2 drogon::app().registerHandler(
3 "/v1/users",
4 [](const drogon::HttpRequestPtr& req,
5 std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
6 // V1 版本的处理逻辑
7 },
8 {drogon::HttpMethod::kGet}
9 );
10
11 // API V2
12 drogon::app().registerHandler(
13 "/v2/users",
14 [](const drogon::HttpRequestPtr& req,
15 std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
16 // V2 版本的处理逻辑
17 },
18 {drogon::HttpMethod::kGet}
19 );

或者,如果您使用控制器,可以为不同版本的 API 定义不同的控制器类,并在 initAndReg() 中分别注册。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // UserControllerV1.h / UserControllerV2.h
2 // ... 实现 V1 和 V2 的控制器逻辑 ...
3
4 // main.cc 或某个初始化点
5 drogon::app().registerController<UserControllerV1>("/v1"); // 注册 V1 控制器到 /v1 前缀
6 drogon::app().registerController<UserControllerV2>("/v2"); // 注册 V2 控制器到 /v2 前缀

这样,UserControllerV1 中的 /users 路由就会变成 /v1/usersUserControllerV2 中的 /users 路由变成 /v2/users

优点:
⚝ 直观,易于理解。
⚝ URL 本身就指示了版本,对开发者友好。
⚝ 易于通过浏览器测试。

缺点:
⚝ 更改版本会改变 URL,不符合 REST 原则中 URI 标识资源的稳定性要求。
⚝ 在服务器端,每个版本都需要一套独立的路由定义。

9.4.2 Header 版本控制 (Header Versioning)

将版本信息放在自定义的 HTTP 请求头部中。
例如:X-API-Version: 1Accept: application/json; version=1

在 Drogon 中的实现:

在请求处理函数或控制器方法中读取特定的请求头部来判断版本,并根据版本执行不同的逻辑。或者,可以使用过滤器在请求进入控制器之前进行版本检查和分发。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 方式一:在处理函数中判断
2 drogon::app().registerHandler(
3 "/users",
4 [](const drogon::HttpRequestPtr& req,
5 std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
6 std::string version = req->getHeader("X-API-Version");
7 if (version == "1") {
8 // V1 逻辑
9 } else if (version == "2") {
10 // V2 逻辑
11 } else {
12 // 默认版本或错误处理
13 }
14 },
15 {drogon::HttpMethod::kGet}
16 );
17
18 // 方式二:使用过滤器分发
19 class VersionFilter : public drogon::HttpFilter<VersionFilter>
20 {
21 public:
22 virtual void doFilter(const drogon::HttpRequestPtr& req,
23 drogon::FilterCallback&& fcb,
24 drogon::FilterChainCallback&& fccb) override
25 {
26 std::string version = req->getHeader("X-API-Version");
27 if (version == "1") {
28 // 修改请求路径或标志,以便后续路由到 V1 控制器
29 // 例如,可以在 req 对象中设置一个属性
30 req->setAttribute("api_version", 1);
31 fccb(); // 继续过滤器链和路由
32 } else if (version == "2") {
33 req->setAttribute("api_version", 2);
34 fccb();
35 } else {
36 // 返回错误或默认处理
37 auto resp = drogon::HttpResponse::newHttpResponse();
38 resp->setStatusCode(drogon::k400BadRequest);
39 resp->setBody("Unsupported API version");
40 fcb(resp); // 中断链并返回响应
41 }
42 }
43 };
44
45 // 在控制器或路由上应用过滤器
46 drogon::app().registerHandler("/users", /* handler */, {drogon::HttpMethod::kGet}, {&VersionFilter::filter});
47
48 // 在处理函数中根据属性获取版本
49 // int version = req->getAttribute<int>("api_version");

结合嵌套控制器,可以在过滤器中根据版本将请求路由到不同的子控制器层级。

优点:
⚝ URI 保持稳定,符合 REST 原则。
⚝ 客户端可以通过改变头部轻松切换版本。

缺点:
⚝ 不如 URL 版本直观,需要查阅文档才能知道如何指定版本。
⚝ 无法通过浏览器直接方便地测试不同版本。
⚝ 需要在服务器端实现头部解析和版本分发逻辑。

9.4.3 Query Parameter 版本控制 (Query Parameter Versioning)

将版本信息作为查询参数。
例如:/users?version=1

在 Drogon 中的实现:

在请求处理函数或控制器方法中读取查询参数 version 来判断版本。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogon::app().registerHandler(
2 "/users",
3 [](const drogon::HttpRequestPtr& req,
4 std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
5 std::string version = req->getParameter("version");
6 if (version == "1") {
7 // V1 逻辑
8 } else if (version == "2") {
9 // V2 逻辑
10 } else {
11 // 默认版本或错误处理
12 }
13 },
14 {drogon::HttpMethod::kGet}
15 );

与 Header 版本控制类似,也可以结合过滤器或嵌套控制器进行更结构化的处理。

优点:
⚝ 易于通过浏览器测试。
⚝ URI 在核心路径上保持稳定。

缺点:
⚝ 查询参数通常用于过滤或排序等资源属性,用作版本控制可能语义不清。
⚝ 查询参数可能会被中间代理缓存或忽略,不如 Header 稳定。

9.4.4 Media Type 版本控制 (Media Type Versioning)

将版本信息嵌入到 Accept 请求头部中的媒体类型里。
例如:Accept: application/vnd.myapp.v1+jsonAccept: application/json; version=1

在 Drogon 中的实现:

需要解析 Accept 头部中的媒体类型和参数来获取版本信息。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogon::app().registerHandler(
2 "/users",
3 [](const drogon::HttpRequestPtr& req,
4 std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
5 std::string accept_header = req->getHeader("Accept");
6 // 解析 accept_header 字符串,提取版本信息
7 // 例如,查找 "application/vnd.myapp.v1+json" 或 "version=1"
8 std::string version = "default"; // 根据解析结果设置版本
9
10 if (version == "1") {
11 // V1 逻辑
12 } else if (version == "2") {
13 // V2 逻辑
14 } else {
15 // 默认版本或错误处理
16 }
17 },
18 {drogon::HttpMethod::kGet}
19 );

这种方式也可以通过过滤器实现。

优点:
⚝ 符合 RESTful 原则,将版本视为资源的表示形式的一部分。
⚝ 清晰地表达客户端期望的资源格式和版本。

缺点:
⚝ 实现和解析相对复杂。
⚝ 不如 URL 版本直观。

9.4.5 选择合适的版本控制策略

选择哪种版本控制策略取决于您的具体需求和偏好:

⚝ 如果简洁和易用性最重要,且不介意更改 URI,URL 版本控制通常是最佳选择,尤其适合公共 API。
⚝ 如果希望 URI 保持稳定,更倾向于将版本信息放在 HTTP 元数据中,Header 版本控制或 Media Type 版本控制更合适。
⚝ Query Parameter 版本控制通常不推荐用于正式的 API 版本控制,更多用于实验性功能或次要变化。

无论选择哪种策略,关键是保持一致性,并在 API 文档中清晰地说明如何指定版本。在 Drogon 中,您可以灵活地结合使用路由、控制器、过滤器和请求/响应处理来实现各种版本控制策略。

本章我们深入探讨了 Drogon 的高级路由技巧,包括强大的正则表达式路由和用于组织代码的嵌套控制器。随后,我们详细分析了 RESTful API 的设计原则,并学习了如何在 Drogon 中将这些原则转化为实际的代码实现,包括资源映射、HTTP 方法的使用、状态码和数据格式。最后,我们讨论了 API 版本控制的各种策略及其在 Drogon 中的实现方式,帮助您构建更易于管理和演进的 API。掌握这些高级技巧,将使您能够使用 Drogon 构建出更加复杂、健壮和符合行业规范的 Web 服务和 API。

好的,作为一名经验丰富的讲师,我将根据您提供的书籍大纲和选定的章节内容,为您深度撰写《Drogon Web 框架:从入门到精通》一书的第 10 章:WebSocket 支持。本章将全面讲解 Drogon 如何实现高性能的 WebSocket 服务,帮助读者构建实时应用。

10. WebSocket 支持

本章将带您了解 Drogon 如何实现 WebSocket 服务,这是构建实时应用的关键技术之一。我们将从 WebSocket 协议的基础讲起,然后深入探讨如何在 Drogon 中创建和管理 WebSocket 连接,以及如何发送和接收消息。

10.1 WebSocket 协议基础

在深入 Drogon 对 WebSocket 的支持之前,理解 WebSocket 协议本身是至关重要的。WebSocket 提供了一种在客户端和服务器之间进行全双工(Full-duplex)通信的机制,这与传统的 HTTP 请求/响应模型有本质区别。

10.1.1 什么是 WebSocket?

WebSocket 是一种网络传输协议(Protocol),它在单个 TCP 连接上提供全双工通信。这意味着一旦连接建立,数据可以在任何时候双向自由地流动,而无需像 HTTP 那样必须由客户端发起请求。

主要特点:

全双工通信(Full-duplex Communication): 服务器和客户端可以同时发送和接收数据。
持久连接(Persistent Connection): 连接一旦建立就保持打开状态,避免了 HTTP 每次请求都需要建立新连接的开销。
低延迟(Low Latency): 由于连接是持久的且全双工,数据传输的延迟显著降低。
二进制和文本数据支持(Binary and Text Data Support): 可以传输文本数据(如 JSON)或二进制数据。
协议头部开销小(Lower Overhead): 握手(Handshake)之后的数据帧(Data Frame)头部开销比 HTTP 小很多。

10.1.2 与 HTTP 的区别

WebSocket 通常通过 HTTP 握手过程来建立连接,即客户端发送一个特殊的 HTTP 请求(包含 Upgrade: websocketConnection: Upgrade 头部),服务器同意后,连接协议便从 HTTP 升级(Upgrade)到 WebSocket。连接建立后,数据传输就不再遵循 HTTP 的请求/响应模式,而是通过 WebSocket 的数据帧进行。

核心差异:

连接状态(Connection State): HTTP 是无状态(Stateless)的,每个请求相互独立;WebSocket 是有状态(Stateful)的,连接建立后状态会一直保持直到断开。
通信模式(Communication Pattern): HTTP 是请求/响应(Request/Response)模式,必须由客户端发起;WebSocket 是全双工(Full-duplex)模式,服务器和客户端都可以主动发起数据传输。
开销(Overhead): HTTP 每次请求都有完整的头部开销;WebSocket 握手后,数据帧开销很小。
适用场景(Use Cases): HTTP 适合传统的网页浏览、API 调用等;WebSocket 适合需要实时数据推送的场景,如在线聊天、游戏、股票行情、实时协作应用等。

10.2 实现 WebSocket 控制器 (WebSocket Controller)

在 Drogon 中,实现 WebSocket 服务的主要方式是通过编写 WebSocket 控制器。这类似于 HTTP 控制器,但专门用于处理 WebSocket 连接的生命周期和消息事件。

10.2.1 WebSocket 控制器基类

所有 WebSocket 控制器都必须继承自 drogon::WebSocketController<ImplementedClass, ...> 基类。这个基类提供了处理 WebSocket 连接生命周期(建立、接收数据、关闭)所需的纯虚函数,您需要在派生类中实现这些函数。

基类定义了以下核心纯虚方法:

handleNewConnection(const drogon::HttpRequestPtr &req, const drogon::WebSocketConnectionPtr &conn): 在新的 WebSocket 连接成功建立后调用。
handleDataReceived(const drogon::WebSocketConnectionPtr &conn, drogon::WebSocketMessageType type, const char *data, size_t length): 在接收到来自客户端的数据时调用。
handleConnectionClosed(const drogon::WebSocketConnectionPtr &conn): 在 WebSocket 连接关闭时调用。

10.2.2 使用 drogons CLI 工具生成控制器

Drogon 提供了便捷的命令行工具 drogons 来生成 WebSocket 控制器的基本代码框架。这可以帮助您快速开始。

要生成一个名为 ChatRoom 的 WebSocket 控制器,可以在项目根目录下运行:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogons create websocket_controller ChatRoom

执行此命令后,drogons 会在 controllers 目录下生成 ChatRoom.hChatRoom.cc 两个文件,其中包含了 WebSocket 控制器类 ChatRoom 的基本定义和实现模板。

10.2.3 WebSocket 控制器结构

生成的 WebSocket 控制器文件(以 ChatRoom 为例)大致结构如下:

ChatRoom.h:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #pragma once
2
3 #include <drogon/WebSocketController.h>
4
5 class ChatRoom : public drogon::WebSocketController<ChatRoom>
6 {
7 public:
8 virtual void handleNewConnection(const drogon::HttpRequestPtr &req, const drogon::WebSocketConnectionPtr &conn) override;
9 virtual void handleDataReceived(const drogon::WebSocketConnectionPtr &conn,
10 drogon::WebSocketMessageType type,
11 const char *data, size_t length) override;
12 virtual void handleConnectionClosed(const drogon::WebSocketConnectionPtr &conn) override;
13 // 添加 WebSocket 路由宏
14 WS_PATH("/chat"); // 指定这个控制器处理哪个路径下的 WebSocket 连接
15
16 public:
17 // 添加任何您需要的成员变量或方法来管理连接等
18 };

ChatRoom.cc:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include "ChatRoom.h"
2 #include <iostream>
3
4 void ChatRoom::handleNewConnection(const drogon::HttpRequestPtr &req, const drogon::WebSocketConnectionPtr &conn)
5 {
6 // 当有新的 WebSocket 连接建立时调用
7 std::cout << "New WebSocket connection from " << conn->peerAddr().toIpPort() << std::endl;
8 // 可以在这里存储 conn 指针,以便后续发送消息
9 }
10
11 void ChatRoom::handleDataReceived(const drogon::WebSocketConnectionPtr &conn,
12 drogon::WebSocketMessageType type,
13 const char *data, size_t length)
14 {
15 // 当接收到客户端发送的数据时调用
16 if (type == drogon::WebSocketMessageType::Text)
17 {
18 std::string msg(data, length);
19 std::cout << "Received text message from " << conn->peerAddr().toIpPort() << ": " << msg << std::endl;
20 // 可以处理消息,并选择是否回复
21 conn->send("Server received your message: " + msg); // 回复客户端
22 }
23 else if (type == drogon::WebSocketMessageType::Binary)
24 {
25 std::cout << "Received binary message from " << conn->peerAddr().toIpPort() << std::endl;
26 // 处理二进制数据
27 }
28 }
29
30 void ChatRoom::handleConnectionClosed(const drogon::WebSocketConnectionPtr &conn)
31 {
32 // 当 WebSocket 连接关闭时调用
33 std::cout << "WebSocket connection closed from " << conn->peerAddr().toIpPort() << std::endl;
34 // 在这里清理与该连接相关的资源(如从连接列表中移除)
35 }

.h 文件中,WS_PATH("/chat"); 宏用于指定该控制器将处理来自 /chat 路径的 WebSocket 连接请求。客户端需要向 ws://your_server_address/chatwss://your_server_address/chat 发起连接。

10.3 连接的建立、消息发送与接收

本节将详细讲解 WebSocket 控制器中的三个核心处理方法:handleNewConnectionhandleDataReceivedhandleConnectionClosed,以及如何使用 WebSocketConnectionPtr 对象发送消息。

10.3.1 连接的建立 (handleNewConnection)

当客户端向 Drogon 服务器发起一个合法的 WebSocket 握手请求,并且该请求匹配了某个 WebSocket 控制器上通过 WS_PATH 指定的路径时,Drogon 会完成握手过程,并在握手成功后调用对应控制器的 handleNewConnection 方法。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 void ChatRoom::handleNewConnection(const drogon::HttpRequestPtr &req, const drogon::WebSocketConnectionPtr &conn)
2 {
3 // req 参数是建立连接时的 HTTP 请求对象,可以从中获取请求头部、Cookies 等信息
4 // conn 参数是代表当前 WebSocket 连接的对象,通过它可以发送数据或获取连接信息
5
6 std::cout << "新 WebSocket 连接建立: " << conn->peerAddr().toIpPort() << std::endl;
7 // 例如,可以从 req 中获取用户认证信息
8 // std::string user_agent = req->getHeader("User-Agent");
9 // std::cout << "User-Agent: " << user_agent << std::endl;
10
11 // 如果需要管理多个连接(例如,用于广播),可以在这里将 conn 存储起来
12 // 后面会详细介绍如何管理连接
13 }

handleNewConnection 中,您通常会进行一些初始化操作,例如:

⚝ 记录连接信息(如客户端地址)。
⚝ 将 conn 对象存储到一个容器中,以便后续向该连接发送消息。
⚝ 发送欢迎消息给新连接的客户端。
⚝ 进行用户身份验证(如果 WebSocket 连接需要用户登录)。

需要注意的是,conn 是一个智能指针 (drogon::WebSocketConnectionPtr),当连接关闭或发生错误时,Drogon 会自动管理其生命周期。在存储连接时,通常也是存储这个智能指针。

10.3.2 消息的接收与发送 (handleDataReceived & conn->send())

当客户端通过已建立的 WebSocket 连接发送数据到服务器时,Drogon 会调用对应控制器的 handleDataReceived 方法。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 void ChatRoom::handleDataReceived(const drogon::WebSocketConnectionPtr &conn,
2 drogon::WebSocketMessageType type,
3 const char *data, size_t length)
4 {
5 // conn: 发送数据的连接对象
6 // type: 消息类型,Text(文本)或 Binary(二进制)
7 // data: 指向接收到数据的指针
8 // length: 数据长度
9
10 if (type == drogon::WebSocketMessageType::Text)
11 {
12 std::string message(data, length); // 将接收到的数据转换为 std::string
13 std::cout << "收到来自 " << conn->peerAddr().toIpPort() << " 的文本消息: " << message << std::endl;
14
15 // 处理消息,例如:
16 // 1. 解析 JSON 数据
17 // 2. 根据消息内容执行相应的业务逻辑
18 // 3. 将消息转发给其他连接(广播或私聊)
19
20 // 使用 conn->send() 方法发送数据回客户端
21 conn->send("服务器已收到您的消息: " + message);
22
23 // 也可以发送二进制数据
24 // std::vector<char> binary_data = {'h', 'i', 1, 2, 3};
25 // conn->send(binary_data.data(), binary_data.size(), drogon::WebSocketMessageType::Binary);
26 }
27 else if (type == drogon::WebSocketMessageType::Binary)
28 {
29 std::cout << "收到来自 " << conn->peerAddr().toIpPort() << " 的二进制消息,长度: " << length << std::endl;
30 // 处理二进制数据...
31 // conn->send(data, length, drogon::WebSocketMessageType::Binary); // 将收到的二进制数据发送回去
32 }
33 else if (type == drogon::WebSocketMessageType::Ping)
34 {
35 // 收到 Ping 帧,Drogon 会自动回复 Pong 帧,通常无需手动处理
36 std::cout << "收到来自 " << conn->peerAddr().toIpPort() << " 的 Ping 帧" << std::endl;
37 }
38 else if (type == drogon::WebSocketMessageType::Pong)
39 {
40 // 收到 Pong 帧,通常无需手动处理
41 std::cout << "收到来自 " << conn->peerAddr().toIpPort() << " 的 Pong 帧" << std::endl;
42 }
43 else if (type == drogon::WebSocketMessageType::Close)
44 {
45 // 收到 Close 帧,连接即将关闭,通常无需手动处理,handleConnectionClosed 会被调用
46 std::cout << "收到来自 " << conn->peerAddr().toIpPort() << " 的 Close 帧" << std::endl;
47 }
48 }

handleDataReceived 是处理核心业务逻辑的地方。您可以根据 type 参数判断接收到的是文本还是二进制消息,然后从 datalength 中提取数据进行处理。

要向客户端发送数据,使用 drogon::WebSocketConnectionPtr 对象的 send 方法。它有多个重载,可以发送 std::string、C 风格字符串 (const char*, size_t) 或 std::vector<char>,并指定消息类型(文本或二进制)。

10.3.3 连接的关闭 (handleConnectionClosed)

当 WebSocket 连接因客户端断开、服务器端主动关闭、网络错误等原因关闭时,Drogon 会调用对应控制器的 handleConnectionClosed 方法。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 void ChatRoom::handleConnectionClosed(const drogon::WebSocketConnectionPtr &conn)
2 {
3 // conn: 已关闭的连接对象
4 std::cout << "WebSocket 连接关闭: " << conn->peerAddr().toIpPort() << std::endl;
5
6 // 如果您在 handleNewConnection 中存储了 conn 指针或相关信息,
7 // 在这里应该进行清理,例如从连接列表中移除该连接。
8 // 这对于正确管理连接生命周期和避免野指针至关重要。
9 }

handleConnectionClosed 方法是进行资源清理的最佳位置。例如,如果您维护了一个活跃连接列表,应该在此处将已关闭的连接从列表中移除。未能及时清理可能导致后续向已关闭连接发送数据时出现问题,甚至导致程序崩溃。

10.4 管理多个 WebSocket 连接

对于需要多人互动或服务器主动向多个客户端推送数据的实时应用(如聊天室、广播系统),您需要一个机制来存储和管理所有活跃的 WebSocket 连接。

10.4.1 存储连接

通常,您会在 WebSocket 控制器中或一个专门的服务类中维护一个容器来存储 drogon::WebSocketConnectionPtr 对象。常用的容器包括 std::vectorstd::liststd::map。如果需要根据某种标识(如用户 ID)查找连接,std::map<int, drogon::WebSocketConnectionPtr>std::map<std::string, drogon::WebSocketConnectionPtr> 会非常有用。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // ChatRoom.h
2 #pragma once
3
4 #include <drogon/WebSocketController.h>
5 #include <vector>
6 #include <mutex> // 用于多线程安全
7
8 class ChatRoom : public drogon::WebSocketController<ChatRoom>
9 {
10 public:
11 virtual void handleNewConnection(const drogon::HttpRequestPtr &req, const drogon::WebSocketConnectionPtr &conn) override;
12 virtual void handleDataReceived(const drogon::WebSocketConnectionPtr &conn,
13 drogon::WebSocketMessageType type,
14 const char *data, size_t length) override;
15 virtual void handleConnectionClosed(const drogon::WebSocketConnectionPtr &conn) override;
16 WS_PATH("/chat");
17
18 private:
19 // 存储所有活跃的连接
20 std::vector<drogon::WebSocketConnectionPtr> connections_;
21 // 用于保护 connections_ 容器的多线程访问
22 std::mutex connections_mutex_;
23 };
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // ChatRoom.cc
2 #include "ChatRoom.h"
3 #include <iostream>
4 #include <algorithm> // 用于 std::remove
5
6 void ChatRoom::handleNewConnection(const drogon::HttpRequestPtr &req, const drogon::WebSocketConnectionPtr &conn)
7 {
8 std::cout << "新 WebSocket 连接建立: " << conn->peerAddr().toIpPort() << std::endl;
9
10 // 将新连接添加到列表中
11 std::lock_guard<std::mutex> lock(connections_mutex_); // 使用互斥锁保护容器
12 connections_.push_back(conn);
13 std::cout << "当前活跃连接数: " << connections_.size() << std::endl;
14 }
15
16 void ChatRoom::handleDataReceived(const drogon::WebSocketConnectionPtr &conn,
17 drogon::WebSocketMessageType type,
18 const char *data, size_t length)
19 {
20 if (type == drogon::WebSocketMessageType::Text)
21 {
22 std::string message(data, length);
23 std::cout << "收到消息: " << message << std::endl;
24
25 // 简单示例:收到消息后广播给所有连接
26 std::lock_guard<std::mutex> lock(connections_mutex_); // 广播时也需要锁定
27 for (const auto &c : connections_)
28 {
29 // 避免将消息发回给自己,或者根据需求决定是否发回
30 if (c != conn)
31 {
32 c->send("用户[" + conn->peerAddr().toIpPort() + "] 说: " + message);
33 }
34 else
35 {
36 c->send("你说了: " + message);
37 }
38 }
39 }
40 // ... 处理二进制消息等
41 }
42
43 void ChatRoom::handleConnectionClosed(const drogon::WebSocketConnectionPtr &conn)
44 {
45 std::cout << "WebSocket 连接关闭: " << conn->peerAddr().toIpPort() << std::endl;
46
47 // 从列表中移除已关闭的连接
48 std::lock_guard<std::mutex> lock(connections_mutex_); // 移除时锁定
49 // 使用 std::remove_if 或 std::remove + erase idiom
50 connections_.erase(std::remove(connections_.begin(), connections_.end(), conn), connections_.end());
51 std::cout << "当前活跃连接数: " << connections_.size() << std::endl;
52 }

10.4.2 广播消息

有了活跃连接的列表,就可以方便地向所有连接广播消息了。如上所示的 handleDataReceived 示例,遍历连接列表,对每个连接调用 send 方法即可。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 在 ChatRoom::handleDataReceived 或任何需要广播的地方
2 void ChatRoom::handleDataReceived(const drogon::WebSocketConnectionPtr &conn,
3 drogon::WebSocketMessageType type,
4 const char *data, size_t length)
5 {
6 if (type == drogon::WebSocketMessageType::Text)
7 {
8 std::string message(data, length);
9 std::string formatted_message = "用户[" + conn->peerAddr().toIpPort() + "] 说: " + message;
10
11 std::lock_guard<std::mutex> lock(connections_mutex_); // 访问共享容器需要锁定
12 for (const auto &c : connections_)
13 {
14 // 可以添加逻辑判断是否需要发送给当前连接或者特定连接
15 // if (c != conn) // 不发给自己
16 {
17 c->send(formatted_message); // 向每个连接发送消息
18 }
19 }
20 }
21 // ...
22 }

10.4.3 多线程安全性考虑

Drogon 是一个多线程 Web 框架。默认情况下,可能会有多个 IO 线程(由 app().setIoThreads() 配置)同时处理请求和 WebSocket 事件。这意味着 handleNewConnectionhandleDataReceivedhandleConnectionClosed 等方法可能会被不同的线程并发调用。

如果您在控制器或外部服务中维护一个共享的连接容器(如上面示例中的 connections_),那么对这个容器的任何读写操作都必须是线程安全的。最常见和直接的方式是使用互斥锁(Mutex),例如 std::mutexstd::lock_guard。在访问(添加、删除、遍历)连接容器之前获取锁,操作完成后释放锁。

在上面的示例代码中,我们使用了 std::mutex connections_mutex_ 来保护 connections_ 容器,并在 handleNewConnectionhandleDataReceivedhandleConnectionClosed 方法中对容器进行操作时,使用 std::lock_guard<std::mutex> lock(connections_mutex_); 来确保线程安全。

通过妥善地管理连接列表并确保多线程安全,您可以利用 Drogon 的高性能异步能力构建出稳定、高效的实时 WebSocket 应用。

11. 配置管理与日志系统

本章将带领读者深入了解 Drogon 框架的配置管理和日志系统。在高可用的 Web 应用开发中,灵活的配置和完善的日志系统至关重要。配置管理使得应用程序无需修改代码即可适应不同的运行环境(如开发、测试、生产),而强大的日志系统则帮助开发者追踪应用程序的运行状态、诊断问题并进行性能分析。Drogon 提供了内置的 JSON 配置文件解析能力和灵活的日志模块,本章将详细介绍它们的使用方法、配置选项以及如何在应用中有效地利用它们。我们还将探讨如何集成第三方日志库,以满足更高级或定制化的需求。

11.1 配置文件的使用

在任何复杂的应用程序中,将运行时参数与代码分离是一种良好的实践。Drogon 遵循这一原则,主要通过配置文件来管理应用程序的各种设置,例如服务器监听地址、端口、数据库连接信息、线程数、日志级别等等。

Drogon 主要支持 JSON 格式的配置文件,这是一种轻量级、易于阅读和编写的数据交换格式。默认情况下,Drogon 启动时会在当前目录或特定搜索路径下查找名为 config.json 的文件并加载其中的配置。如果找到,这些配置项就会被加载到全局的配置管理器中供应用程序使用。

配置文件通常包含多个顶级键(Key),每个键对应一个配置模块或功能。例如:

"listeners":配置监听的网络端口和协议。
"app":配置应用程序级别的设置,如线程数、工作目录等。
"db_clients":配置数据库连接。
"log":配置日志系统。
"plugins":配置加载的插件。
⚝ 自定义键:您也可以添加自己的顶级键来存储应用程序特定的配置。

以下是一个简化的 config.json 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "listeners": [
3 {
4 "address": "0.0.0.0",
5 "port": 8848,
6 "protocol": "http"
7 },
8 {
9 "address": "0.0.0.0",
10 "port": 8000,
11 "protocol": "https",
12 "cert": "server.pem",
13 "key": "server.key"
14 }
15 ],
16 "app": {
17 "threads_num": 8,
18 "document_root": "./webroot",
19 "upload_path": "./uploads"
20 },
21 "db_clients": [
22 {
23 "type": "postgresql",
24 "host": "127.0.0.1",
25 "port": 5432,
26 "dbname": "mydatabase",
27 "user": "myuser",
28 "password": "mypassword",
29 "client_name": "main_db",
30 "connections_number": 10,
31 "auto_batch": true
32 },
33 {
34 "type": "mysql",
35 "host": "127.0.0.1",
36 "port": 3306,
37 "dbname": "anotherdb",
38 "user": "anotheruser",
39 "password": "anotherpassword",
40 "client_name": "another_db",
41 "connections_number": 5
42 }
43 ],
44 "log": {
45 "log_path": "./logs",
46 "logfile_base_name": "drogon_app",
47 "log_level": "INFO",
48 "log_size_limit": 50000000,
49 "log_files_num": 10,
50 "syslog_on": false,
51 "log_console": true
52 },
53 "plugins": [
54 {
55 "name": "drogon::plugin::RealtimeData",
56 "config": {}
57 }
58 ],
59 "my_custom_settings": {
60 "api_timeout_seconds": 30,
61 "feature_flag_A": true
62 }
63 }

Drogon 启动时会自动加载默认配置文件。您也可以通过命令行参数 -c--config 指定不同的配置文件路径:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ./your_drogon_app -c /path/to/your_custom_config.json

这种方式使得在不同环境中运行同一个编译好的应用二进制文件成为可能,只需切换配置文件即可。

11.2 运行时配置的获取

应用程序在运行时需要访问配置文件中加载的各种设置。Drogon 提供了一个全局的应用程序对象 drogon::app(),通过它可以方便地获取配置信息。这些配置信息通常存储在 drogon::app() 内部的一个配置对象中。

获取配置项的方法通常是根据键名(Key)来获取对应的值。由于 JSON 支持多种数据类型(字符串、整数、布尔值、数组、对象等),Drogon 提供了不同的方法来获取特定类型的值,并通常会有一个默认值参数,以防配置项不存在。

常用的获取配置项的方法示例:

① 获取字符串值:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 std::string uploadPath = drogon::app().get///@note: avoid nested code blocks, use top-level

/ Correct usage /
///@code
std::string uploadPath = drogon::app().getUploadPath(); // 这是一个便捷方法
// 获取自定义字符串配置
std::string dbHost = drogon::app().get//
/ Correct usage /
///@code
std::string dbHost = drogon::app().get<:string>("db_clients[0].host", "127.0.0.1");
std::string customString = drogon::app().get<:string>("my_custom_settings.some_string_key", "default_value");

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 获取整数值
2 ```cpp
3
4 int threadsNum = drogon::app().getThreadNum(); // 这是一个便捷方法
5 // 获取自定义整数配置
6 int apiTimeout = drogon::app().get<int>("my_custom_settings.api_timeout_seconds", 60); // 默认值 60

③ 获取布尔值:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 获取自定义布尔配置
2 bool featureFlagA = drogon::app().get<bool>("my_custom_settings.feature_flag_A", false); // 默认值 false

④ 获取数组或对象(通常使用 Json::Value):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 const Json::Value& listeners = drogon::app().getListenersJson(); // 获取监听器数组的Json对象
2 // 获取自定义对象配置
3 const Json::Value& customSettings = drogon::app().get<Json::Value>("my_custom_settings", Json::Value(Json::objectValue));
4 if (!customSettings["another_key"].empty()) {
5 std::string anotherValue = customSettings["another_key"].asString();
6 // ... use anotherValue
7 }

注意:

  • drogon::app() 的便捷方法(如 getThreadNum()getUploadPath() 等)直接对应一些常用的内置配置项。
  • 对于非内置或自定义的配置项,需要使用模板方法 get<Type>(key_path, default_value)
  • key_path 可以使用点号(.)来访问嵌套的对象属性,例如 "my_custom_settings.api_timeout_seconds"。对于数组元素,可以使用方括号和索引,例如 "db_clients[0].host"
  • 获取数组或复杂的对象时,通常使用 Json::Value 类型。需要包含 drogon/drogon.h 和可能需要 <json/json.h>(如果直接操作 Json::Value)。

通过这些方法,您可以在应用程序的任何地方(控制器、模型、服务等)方便地访问加载的配置信息,使应用的行为可以通过修改配置文件来调整,而无需重新编译代码,极大地增强了灵活性。👍

11.3 日志系统配置

日志是调试和维护应用程序不可或缺的工具。Drogon 内置了一个高性能的异步日志系统,可以在不阻塞主事件循环的情况下记录日志信息。这个日志系统的行为可以通过 config.json 中的 "log" 键进行配置。

主要的日志配置选项包括:

"log_path"(字符串):指定日志文件存放的目录。如果未指定或为空,则日志默认输出到控制台(如果 "log_console"true)。
"logfile_base_name"(字符串):指定日志文件的基础名称。实际文件名会在此基础上添加时间戳和序号,例如 drogon_app_20230101_1.log
"log_level"(字符串):指定日志的输出级别。只有高于或等于此级别的日志消息才会被记录。可能的级别(不区分大小写)从低到高依次是:TRACING(跟踪)、DEBUG(调试)、INFO(信息)、WARN(警告)、ERROR(错误)、FATAL(致命)。例如,如果设置为 "INFO",则 INFO、WARN、ERROR、FATAL 级别的消息会被记录,而 TRACING 和 DEBUG 级别的消息会被忽略。
"log_size_limit"(整数):指定单个日志文件的最大大小(字节)。当文件大小达到此限制时,会创建新的日志文件进行记录(日志文件滚动/切割)。
"log_files_num"(整数):指定保留的最大日志文件数量。当文件数量超过此限制时,会删除最旧的日志文件。设置为 0 表示不限制文件数量。
"syslog_on"(布尔):是否同时将日志发送到系统日志(Syslog)。这在某些服务器环境中很有用。默认为 false
"log_console"(布尔):是否将日志同时输出到标准错误输出(控制台)。在开发环境中通常设置为 true,在生产环境中可能设置为 false 或仅对错误日志开启。
"log_async"(布尔):是否启用异步日志。默认为 true,强烈建议在生产环境保持开启以避免阻塞。
"log_format"(字符串):指定日志消息的格式。这是一个格式字符串,可以使用占位符,例如 %d(日期时间)、%t(线程 ID)、%l(日志级别)、%s(源文件名)、%n(源行号)、%f(源函数名)、%m(日志消息)、%%(百分号)。

一个典型的日志配置示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 "log": {
2 "log_path": "./logs",
3 "logfile_base_name": "my_app",
4 "log_level": "INFO",
5 "log_size_limit": 104857600, // 100MB
6 "log_files_num": 20,
7 "log_console": true,
8 "log_async": true,
9 "log_format": "[%d][%t][%l] %s:%n %m"
10 }

通过调整这些配置项,您可以控制日志的详细程度、存储位置、文件管理策略以及输出方式,以满足不同环境和需求下的日志记录要求。📝

11.4 在应用中记录日志

Drogon 提供了一组方便的宏(Macro)用于在应用程序代码中记录不同级别的日志消息。这些宏会自动获取当前的文件名、行号、函数名等信息,并根据配置的日志级别决定是否实际输出日志。

常用的日志宏包括:

  • LOG_TRACE:用于记录非常详细的跟踪信息,通常用于调试复杂的逻辑流程。对应 TRACING 级别。
  • LOG_DEBUG:用于记录调试信息,例如变量值、函数调用路径等。对应 DEBUG 级别。
  • LOG_INFO:用于记录一般性的信息,表示程序正常运行的关键事件。对应 INFO 级别。
  • LOG_WARN:用于记录警告信息,表示可能存在问题但程序仍可继续运行的情况。对应 WARN 级别。
  • LOG_ERROR:用于记录错误信息,表示发生了错误但程序可能可以从错误中恢复。对应 ERROR 级别。
  • LOG_FATAL:用于记录致命错误,表示发生了严重问题,程序无法继续运行并将退出。对应 FATAL 级别。

使用这些宏非常简单,就像使用 std::cout 一样,可以使用流操作符 << 来输出各种类型的数据。

例如,在一个控制器或服务方法中记录日志:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/drogon.h>
2
3 // ... 在某个类或函数中 ...
4
5 void processRequest(const drogon::HttpRequestPtr& req,
6 drogon::AdviceCallback&& callback)
7 {
8 LOG_INFO << "Received request: " << req->getMethodString() << " " << req->getPath();
9
10 // 假设进行了一些处理,发现潜在问题
11 if (req->getParameter("user_id").empty())
12 {
13 LOG_WARN << "Request received without user_id parameter from " << req->peerAddr().toIpPort();
14 // ... 返回错误响应 ...
15 }
16
17 // 假设执行数据库查询
18 // ... database query ...
19 if (/* database error */)
20 {
21 LOG_ERROR << "Database query failed for request: " << req->getPath();
22 // ... 返回错误响应 ...
23 }
24
25 // 假设发生了无法恢复的错误
26 if (/* fatal error condition */)
27 {
28 LOG_FATAL << "Unrecoverable error processing request " << req->getPath() << ". Application is shutting down.";
29 // 在致命错误后,通常会退出程序
30 // drogon::app().quit();
31 }
32
33 LOG_DEBUG << "Request processing finished successfully for " << req->getPath();
34
35 // ... 构建并发送响应 ...
36 // auto resp = drogon::HttpResponse::newHttpResponse();
37 // callback(resp);
38 }

通过在关键位置插入不同级别的日志,您可以在程序运行时跟踪其执行流程、检查变量状态、定位错误源头。在开发和调试阶段,可以将日志级别设置为 DEBUGTRACING,以获取详细信息;在生产环境中,通常将级别设置为 INFOWARN,只记录重要的事件和潜在问题,避免产生过多的日志数据。

一个良好的日志策略应该包含以下要素:

  • 选择合适的日志级别:根据信息的重要性、是否需要调试等因素选择 TRACE, DEBUG, INFO, WARN, ERROR, FATAL。
  • 包含关键上下文信息:日志消息中应包含有助于定位问题的信息,如用户 ID、请求 ID、关联的业务实体 ID 等。
  • 避免在热点路径(Hot Path)中记录过多低级别日志:在高并发、对性能敏感的代码段中,过多的 DEBUG 或 TRACE 级别的日志输出可能会影响性能。在生产环境应适当调高日志级别。
  • 格式化日志:使用清晰一致的格式,方便日志分析工具解析或人工阅读。Drogon 的 log_format 配置项可以帮助实现这一点。

合理地使用 Drogon 的日志宏,能够极大地提高应用程序的可观察性(Observability)和可维护性。📊

11.5 集成第三方日志库

虽然 Drogon 内置的日志系统功能已经相当完善且性能优异,但在某些特定场景下,开发者可能希望集成第三方的 C++ 日志库。这可能是因为:

  • 需要更丰富的日志输出目标(例如,发送到日志收集服务、数据库、网络目标等)。
  • 需要更灵活的日志格式定制能力。
  • 希望使用已有的、团队熟悉的日志库。
  • 需要更高级的功能,如日志压缩、加密、更复杂的滚动策略等。
  • 希望统一多个 C++ 项目的日志方案。

一些流行的 C++ 日志库包括:

  • spdlog:一个快速、仅头文件(大部分)、现代 C++ 的日志库,功能丰富,支持多种 sink(输出目标)。
  • log4cplus:基于 Java log4j 的 C++ 移植,功能强大但配置和使用可能相对复杂。
  • Boost.Log:Boost 库的一部分,功能非常强大且高度可定制,但学习曲线较陡峭且依赖整个 Boost 库。

集成第三方日志库到 Drogon 应用的通用思路通常是:

禁用 Drogon 内置日志(可选):如果完全替换 Drogon 的日志,可以在 config.json 中将 Drogon 的日志级别设置为 FATAL(或更高,如果未来有更高级别)或者尝试关闭所有输出通道(log_console: false 且不配置 log_path),这样 Drogon 自身的日志输出就会被抑制。然而,Drogon 框架内部的一些核心日志可能仍然会通过其机制输出,完全静默 Drogon 自身日志可能需要更深入的配置或魔改。通常更实际的方式是:在应用代码中使用第三方库记录日志,而保留 Drogon 框架自身的少量必要日志。

选择并集成第三方库:将选定的第三方日志库添加到您的项目中,这通常意味着将其作为依赖项引入(通过包管理器如 Conan, vcpkg 或手动构建)。

初始化第三方日志库:在 Drogon 应用程序启动的早期阶段(例如,在 drogon::app().run() 调用之前,或者在 drogon::app().registerBeginningAdvice() 或插件的初始化方法中),初始化第三方日志库,配置其输出目标、格式、级别等。

在应用代码中使用第三方库的 API 记录日志:在您的控制器、服务、模型等 C++ 代码中,不再使用 LOG_INFO 等 Drogon 宏,而是使用第三方日志库提供的 API 来记录日志。

以 spdlog 为例,集成步骤可能大致如下:

  1. 添加依赖:将 spdlog 加入您的项目依赖。
  2. 配置初始化:在 main 函数或 Drogon 启动回调中初始化 spdlog。例如,创建一个控制台 sink 和一个文件 sink,并创建一个 logger。
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/drogon.h>
2 #include <spdlog/spdlog.h>
3 #include <spdlog/sinks/stdout_color_sinks.h>
4 #include <spdlog/sinks/basic_file_sink.h>
5
6 int main()
7 {
8 // 初始化 spdlog
9 try {
10 auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
11 console_sink->set_level(spdlog::level::warn);
12
13 auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/my_app.log", true);
14 file_sink->set_level(spdlog::level::trace);
15
16 spdlog::sinks_container sinks;
17 sinks.push_back(console_sink);
18 sinks.push_back(file_sink);
19
20 auto combined_logger = std::make_shared<spdlog::logger>("multi_sink", sinks.begin(), sinks.end());
21 combined_logger->set_level(spdlog::level::trace);
22
23 // 设置为默认 logger,以便使用 spdlog::info() 等全局函数
24 spdlog::set_default_logger(combined_logger);
25
26 // 设置全局日志级别,覆盖各个 sink 的级别
27 spdlog::set_level(spdlog::level::trace);
28 spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%P] [%t] [%l] %v"); // 定制格式
29
30 } catch (const spdlog::spdlog_ex &ex) {
31 std::cerr << "Log initialization failed: " << ex.what() << std::endl;
32 return 1; // 日志初始化失败可能是致命错误
33 }
34
35 // 配置并运行 Drogon
36 drogon::app().loadConfigFile("config.json");
37 // ... 其他 Drogon 配置 ...
38
39 drogon::app().run();
40
41 // Drogon 退出前清理 spdlog
42 spdlog::shutdown();
43
44 return 0;
45 }
  1. 在应用代码中使用 spdlog:在您的控制器、服务等地方使用 spdlog 的宏或函数。
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/HttpController.h>
2 #include <spdlog/spdlog.h> // 包含 spdlog 头文件
3
4 class MyController : public drogon::HttpController<MyController>
5 {
6 public:
7 METHOD_LIST_BEGIN
8 // ... 路由定义 ...
9 METHOD_LIST_END
10
11 void myMethod(const drogon::HttpRequestPtr& req,
12 drogon::AdviceCallback&& callback)
13 {
14 spdlog::info("Handling request to {}", req->getPath()); // 使用 spdlog::info
15
16 // ... processing ...
17
18 if (/* some condition */)
19 {
20 spdlog::warn("Something unusual happened for request {}", req->getPath()); // 使用 spdlog::warn
21 }
22
23 // ... build response ...
24 // callback(resp);
25 }
26 };

通过这种方式,您可以充分利用第三方日志库提供的强大功能。需要注意的是,集成第三方库会增加项目的依赖复杂性。在决定是否集成之前,应权衡内置日志功能是否足以满足需求,以及集成第三方库带来的额外开销。✨

12. 单元测试与集成测试

本章讲解如何对 Drogon 应用程序进行有效的测试,确保代码质量和功能正确性。我们将探讨测试的重要性,选择合适的 C++ 测试框架,并详细介绍如何编写单元测试(Unit Testing)和集成测试(Integration Testing),特别是在 Drogon 环境下如何处理请求模拟和数据库相关的测试。

12.1 测试框架选择与配置

软件测试是保证代码质量、减少错误和提高软件可维护性的关键环节。对于高性能的 Web 框架如 Drogon,测试尤为重要,它可以帮助开发者验证路由是否正确、控制器逻辑是否符合预期、异步操作是否按计划执行等。

12.1.1 测试的重要性

软件开发中的测试可以带来多方面的好处:

① 提高代码质量:通过测试可以尽早发现并修复缺陷。
② 增强信心:频繁的测试(特别是自动化测试)让开发者对代码的正确性更有信心,敢于进行重构。
③ 简化调试:当出现问题时,定位到失败的测试用例通常比在复杂的运行环境中调试更容易。
④ 改进设计:可测试性是良好软件设计的一个重要指标。为了方便测试,开发者会倾向于编写更模块化、耦合度更低的代码。
⑤ 支持持续集成/持续部署 (CI/CD):自动化测试是 CI/CD 流程中不可或缺的一部分,它可以自动验证每次代码提交的质量。

12.1.2 选择 C++ 测试框架

C++ 生态系统有许多成熟的测试框架可供选择。选择一个合适的测试框架取决于项目的需求、团队的熟悉程度以及框架的特性(如易用性、功能丰富度、社区支持等)。

一些常用的 C++ 测试框架包括:

① Google Test (GTest): 由 Google 开发,功能强大,易于使用,支持丰富的断言(Assertion)和测试组织方式,社区活跃,文档齐全。
② Catch2: 一个轻量级、只包含头文件的测试框架,编译速度快,语法简洁,易于上手。
③ Boost.Test: Boost 库的一部分,功能全面,但可能引入整个 Boost 库的依赖。
④ CppUnit: 灵感来源于 JUnit 的 C++ 移植,功能相对较老。

在本书中,我们将主要以 Google Test (GTest) 作为示例框架进行讲解,因为它功能强大且广泛应用。

12.1.3 在 Drogon 项目中集成 Google Test

将 Google Test 集成到基于 CMake 的 Drogon 项目中非常方便。通常,你需要做以下几步:

获取 Google Test: 可以将 Google Test 作为项目的子模块(Git Submodule)或者通过包管理器(如 vcpkgConan)获取。将 Google Test 作为子模块添加到项目 lib 目录通常是一个直接的方式:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 cd your_drogon_project
2 git submodule add https://github.com/google/googletest.git lib/googletest
3 git submodule update --init --recursive

CMakeLists.txt 中添加测试目标:
▮▮▮▮⚝ 在项目的顶层 CMakeLists.txt 文件中,找到 Drogon 相关的 CMake 设置之后,添加 Google Test 的引用:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # ... Drogon build setup ...
2
3 # 添加 Google Test 子目录
4 add_subdirectory(lib/googletest)
5
6 # 创建一个可执行文件作为测试运行器
7 add_executable(drogon_tests test/main.cc) # 假设你的测试主文件在 test/main.cc
8
9 # 链接 Drogon 库、Google Test 库以及你的应用代码
10 target_link_libraries(drogon_tests PRIVATE
11 drogon::drogon
12 gtest_main
13 gtest
14 # 链接你的应用的其他库,如你的业务逻辑代码
15 your_app_library # 假设你的业务代码被组织成一个库
16 )
17
18 # 将测试文件添加到测试目标中
19 target_sources(drogon_tests PRIVATE
20 test/main.cc
21 test/unittest1.cc
22 test/integrationtest1.cc
23 # 添加其他测试文件
24 )
25
26 # 可选:使用 CTest 添加测试,方便运行
27 include(CTest)
28 add_test(NAME run_drogon_tests COMMAND drogon_tests)

▮▮▮▮⚝ test/main.cc 是你的测试程序的入口,通常包含 RUN_ALL_TESTS() 调用。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/drogon.h>
2 #include <gtest/gtest.h>
3 #include <thread>
4
5 int main(int argc, char **argv) {
6 ::testing::InitGoogleTest(&argc, argv);
7
8 // 在单独的线程中启动 Drogon 应用(用于集成测试)
9 // 这样测试主线程可以继续运行测试
10 // 注意:这需要在测试中根据需要启动/停止 Drogon
11 // 或者更常见的方式是为集成测试启动一个测试实例
12 // std::thread server_thread([](){
13 // drogon::app().run();
14 // });
15 // // Wait for Drogon to start if necessary
16
17 int ret = RUN_ALL_TESTS();
18
19 // Shut down Drogon if it was started in this process
20 // drogon::app().quit();
21 // server_thread.join(); // 等待服务器线程结束
22
23 return ret;
24 }

▮▮▮▮⚝ 创建 test 目录,并在其中编写测试文件(如 unittest1.cc, integrationtest1.cc)。

构建和运行测试:
▮▮▮▮⚝ 在构建目录下执行 CMake 和构建命令:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 mkdir build && cd build
2 cmake ..
3 make

▮▮▮▮⚝ 运行测试可执行文件:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ./drogon_tests

▮▮▮▮⚝ 如果使用了 CTest,可以运行:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 ctest

这样,你的 Drogon 项目就集成了 Google Test,可以开始编写测试用例了。

12.2 单元测试 (Unit Testing)

单元测试是针对程序中最小的可测试单元(Unit)进行的测试,通常是一个函数、方法或类。其目标是隔离代码的各个部分,并证明这些独立的部分是正确的。在 Drogon 应用中,单元测试通常用于测试那些不直接依赖于 HTTP 请求/响应、数据库连接或全局 Drogon 应用状态的业务逻辑代码。

12.2.1 什么是单元以及如何识别

在 Drogon 应用中,适合进行单元测试的“单元”通常是:

独立的服务类 (Service Classes): 负责处理特定业务逻辑的类,它们可能依赖于其他服务或数据访问层,但在单元测试中可以通过模拟 (Mocking) 来隔离这些依赖。
模型类 (Model Classes): 特别是那些包含自定义方法(而不是 ORM 生成的基础 CRUD 方法)的模型类。
通用的工具函数 (Utility Functions): 不依赖于应用状态或外部资源的通用函数。
某些复杂的计算或数据处理逻辑: 从控制器或服务中提取出来的纯计算函数。

控制器的请求处理方法通常适合作为单元进行测试,因为它们紧密耦合于 HttpRequest 和 HttpResponse 对象以及 Drogon 的路由和过滤器机制。测试控制器通常属于集成测试的范畴。

12.2.2 编写单元测试示例

假设你有一个简单的服务类 UserService,负责处理用户相关的业务逻辑,例如检查用户名是否有效。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // src/services/UserService.h
2 #pragma once
3 #include <string>
4
5 class UserService {
6 public:
7 bool isValidUsername(const std::string& username) const;
8 // 其他用户相关方法...
9 };
10
11 // src/services/UserService.cc
12 #include "UserService.h"
13 #include <algorithm>
14
15 bool UserService::isValidUsername(const std::string& username) const {
16 if (username.length() < 3 || username.length() > 20) {
17 return false;
18 }
19 // 只允许字母、数字和下划线
20 for (char c : username) {
21 if (!isalnum(c) && c != '_') {
22 return false;
23 }
24 }
25 return true;
26 }

现在,你可以为 UserService::isValidUsername 方法编写单元测试。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // test/unittest_UserService.cc
2 #include <gtest/gtest.h>
3 #include "services/UserService.h" // 包含你要测试的头文件
4
5 // 使用 TEST() 宏定义一个测试用例
6 TEST(UserServiceTest, IsValidUsername_ValidCases) {
7 UserService service;
8 // 使用断言验证结果
9 ASSERT_TRUE(service.isValidUsername("user123"));
10 ASSERT_TRUE(service.isValidUsername("another_user"));
11 ASSERT_TRUE(service.isValidUsername("a3_4"));
12 }
13
14 TEST(UserServiceTest, IsValidUsername_InvalidCases) {
15 UserService service;
16 ASSERT_FALSE(service.isValidUsername("us")); // Too short
17 ASSERT_FALSE(service.isValidUsername("very_long_username_that_exceeds_twenty_chars")); // Too long
18 ASSERT_FALSE(service.isValidUsername("user-name")); // Contains hyphen
19 ASSERT_FALSE(service.isValidUsername("user name")); // Contains space
20 ASSERT_FALSE(service.isValidUsername("")); // Empty string
21 }
22
23 // 你可以为 UserService 的其他方法编写更多测试用例

在上面的例子中,我们只关注 isValidUsername 函数的输入和输出,不涉及任何 Drogon 特有的对象或框架机制。这就是典型的单元测试。

12.2.3 隔离依赖

如果你的单元依赖于其他组件(例如 UserService 依赖于一个 UserRepository 来检查用户名是否已存在),在单元测试中通常需要“模拟”(Mock)这些依赖项。模拟对象会模仿真实对象的行为,但你可以控制它们的返回结果或验证它们是否被调用。

许多 C++ 模拟库可以与 Google Test 结合使用,例如 Google Mock (GMock)。通过模拟,你可以确保只测试当前单元的逻辑,而不受其依赖项行为的影响。

12.3 集成测试 (Integration Testing)

集成测试是测试应用程序中多个组件协同工作的情况。在 Web 应用的上下文中,集成测试通常涉及测试从接收 HTTP 请求到生成 HTTP 响应的整个流程,包括路由、控制器、过滤器、中间件、服务层,甚至数据库交互(尽管数据库交互有时被视为更高级别的测试或需要特殊处理)。

集成测试的目标是验证不同模块之间的接口和交互是否正确。

12.3.1 集成测试的范围

在 Drogon 应用中,集成测试主要关注:

路由匹配: 确保特定的 URL 和 HTTP 方法能正确地匹配到预期的控制器或函数。
控制器逻辑: 测试控制器如何处理 HttpRequest、调用服务层或模型、构建 HttpResponse。
过滤器和中间件: 验证过滤器和中间件是否按照预期执行了预处理或后处理逻辑(如身份验证、日志记录)。
组件间的协作: 测试控制器如何与服务类、ORM 模型等协同工作。
端到端 (End-to-End) 模拟: 在不启动完整服务器的情况下,模拟整个请求-响应流程。

12.3.2 启动测试实例

进行集成测试时,你需要一个运行中的 Drogon 应用实例来接收模拟的请求。最常见的方式是在测试程序内部启动 Drogon 的一个测试实例。

Drogon 提供了一个 drogon::app().run() 方法来启动事件循环。在测试中,你可以使用 drogon::app().run()drogon::app().run() 的变体,或者更灵活地,使用 Drogon 提供的接口来模拟请求而无需实际监听网络端口。后一种方法通常更适合自动化的集成测试,因为它更快且不需要管理端口冲突。

Drogon 提供 drogon::app().get=()drogon::app().post() 等方法来直接发送模拟请求到应用程序的路由系统。这些方法通常接受模拟的 HttpRequest 对象,并返回 HttpResponse 对象。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // test/integrationtest_Controller.cc
2 #include <gtest/gtest.h>
3 #include <drogon/drogon.h>
4 #include <drogon/HttpClient.h> // 用于模拟发送请求
5
6 // 一个简单的测试控制器
7 // controllers/TestController.h
8 /*
9 #pragma once
10 #include <drogon/HttpController.h>
11
12 class TestController : public drogon::HttpController<TestController> {
13 public:
14 METHOD_LIST_BEGIN
15 METHOD_ADD(TestController::getHelloWorld, "/test", drogon::Get);
16 METHOD_LIST_END
17
18 void getHelloWorld(const drogon::HttpRequestPtr& req,
19 std::function<void(const drogon::HttpResponsePtr&)>&& callback);
20 };
21 */
22
23 // controllers/TestController.cc
24 /*
25 #include "TestController.h"
26
27 void TestController::getHelloWorld(const drogon::HttpRequestPtr& req,
28 std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
29 auto resp = drogon::HttpResponse::newHttpResponse();
30 resp->setBody("Hello, World!");
31 resp->setStatusCode(drogon::k200OK);
32 callback(resp);
33 }
34 */
35
36 // test/integrationtest_Controller.cc continuation...
37
38 // 测试套件用于组织相关的集成测试
39 TEST_F(DrogonTest, HelloWorldRoute) { // DrogonTest 是一个自定义的测试Fixture,用于启动/停止Drogon
40 // 模拟一个GET请求到 /test 路径
41 auto req = drogon::HttpRequest::newHttpRequest(drogon::HttpMethod::kGet, "/test");
42
43 // 使用 drogons 的模拟请求接口发送请求
44 // 这会在内部路由和处理请求,就像实际的HTTP请求一样
45 drogon::app().sendRequest(req,
46 [this](drogon::ReqResult result, const drogon::HttpResponsePtr& response) {
47 // 这是异步回调,测试需要等待结果
48 ASSERT_EQ(result, drogon::ReqResult::kOk);
49 ASSERT_EQ(response->getStatusCode(), drogon::k200OK);
50 ASSERT_EQ(response->getBody(), "Hello, World!");
51
52 // 通知测试框架异步操作已完成
53 // 在 Google Test 中可能需要使用特定的异步测试支持,或者简单的同步等待机制
54 // 这里为了示例简化,假设在一个支持异步的测试环境中
55 // 在实际应用中,你可能需要一个信号量或条件变量来同步
56 // 对于简单的集成测试,通常会启动一个独立的测试进程或者使用特殊的测试模式
57 });
58
59 // 在一个同步的 Google Test 环境中,你可能需要运行 Drogon 事件循环一部分,
60 // 或者使用 Drogon 提供的同步发送请求方法(如果可用或通过配置实现)
61 // 更常见的是在一个单独的测试进程中,或者使用专门的集成测试工具。
62
63 // 假设使用了一个简化的同步等待机制 (仅为示例)
64 // std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 非常不推荐在实际测试中使用硬编码等待
65 }
66
67 // 为了更可靠地进行异步集成测试,通常会启动 Drogon 的测试模式
68 // 或者使用如 httplib 等库来向实际启动的测试服务器发送请求

注意: 直接在 Google Test 的 TEST 宏内部处理 Drogon 的异步回调需要额外的同步机制(例如使用 gtest_main 提供的 drogon::app().run() 并在回调中调用 drogon::app().quit(),或者使用同步 HTTP 客户端)。更推荐的方式是为集成测试单独设置一个测试环境,或者使用 Drogon 的模拟请求接口在一个测试进程中完成。

12.3.3 测试 Fixture (Test Fixture)

为了在多个测试用例之间共享 setup 和 teardown 逻辑(例如启动/停止 Drogon 应用实例,设置数据库状态),可以使用 Google Test 的 Test Fixture。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // test/integrationtest_Fixture.h
2 #pragma once
3 #include <gtest/gtest.h>
4 #include <drogon/drogon.h>
5 #include <thread>
6 #include <chrono>
7
8 // 定义一个测试 Fixture
9 class DrogonTest : public ::testing::Test {
10 protected:
11 void SetUp() override {
12 // 在每个测试用例开始前执行
13 // 例如:启动 Drogon 应用的测试实例
14 // 这里我们使用一个简单的启动方式,实际中可能需要加载测试配置
15 // drogons::app().loadConfigFile("config_test.json");
16 // drogon::app().run(); // 这会阻塞,不适合直接在这里调用
17 // 启动 Drogon 在一个单独的线程中,或者使用模拟请求接口
18 // 对于模拟请求,可能不需要在这里启动应用
19
20 // 如果需要启动 Drogon 监听端口进行测试
21 // server_thread_ = std::thread([](){
22 // // Load test config if needed
23 // // drogon::app().loadConfigFile("config_test.json");
24 // drogon::app().run();
25 // });
26 // std::this_thread::sleep_for(std::chrono::seconds(1)); // 等待服务器启动 (不推荐)
27 }
28
29 void TearDown() override {
30 // 在每个测试用例结束后执行
31 // 例如:停止 Drogon 应用实例
32 // drogon::app().quit();
33 // if (server_thread_.joinable()) {
34 // server_thread_.join();
35 // }
36 }
37
38 // std::thread server_thread_; // 如果在单独线程启动服务器需要这个
39 };

在测试文件中,使用 TEST_F 宏来使用这个 Fixture:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // test/integrationtest_Controller.cc continuation...
2 #include "integrationtest_Fixture.h"
3
4 TEST_F(DrogonTest, AnotherIntegrationTest) {
5 // 这个测试用例也会在 DrogonTest Fixture 的 SetUp 和 TearDown 中运行
6 // ... 模拟请求和验证响应 ...
7 }

请注意,如何在 Fixture 中启动 Drogon 应用以便于测试是关键。最稳健的方式通常是让测试代码通过 Drogon 的内部 API 发送模拟请求,而不是启动一个完整的监听服务器。Drogon 的 drogon::app().sendRequest() 方法就是为此设计的。

12.4 模拟请求与响应 (Mocking)

在集成测试中,你经常需要模拟 HTTP 请求 (HttpRequest) 和预期响应 (HttpResponse) 对象。尽管 drogon::app().sendRequest() 可以构建实际的请求并获取框架处理后的响应,但在某些场景下,你可能需要更细粒度的控制,例如:

测试特定控制器方法: 在不经过完整路由匹配的情况下,直接调用控制器的处理方法,并传入手动构建的请求对象。
注入特殊的请求头部或体: 精确控制请求的每个细节,以测试边缘情况。
验证控制器如何修改响应: 调用处理方法后,检查返回的 HttpResponse 对象是否符合预期。

12.4.1 构建模拟 HttpRequest

你可以手动创建 HttpRequest 的实例(尽管通常通过 HttpRequest::newHttpRequest 工厂方法)并填充其属性:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/HttpRequest.h>
2 #include <drogon/utilities/Json.h> // 如果需要 JSON body
3
4 // 模拟一个 GET 请求到 /users/123
5 auto req_get = drogon::HttpRequest::newHttpRequest(drogon::HttpMethod::kGet, "/users/123");
6 req_get->addHeader("User-Agent", "TestClient");
7 req_get->addCookie("sessionid", "test_session");
8 // 对于参数路由,实际路径参数通常由框架解析,
9 // 但你可以在 HttpRequest 中模拟查询参数或路径参数(取决于你的测试方式)
10 // 例如,设置查询参数
11 req_get->setParameter("filter", "active");
12
13 // 模拟一个 POST 请求到 /users,带有 JSON body
14 auto req_post = drogon::HttpRequest::newHttpRequest(drogon::HttpMethod::kPost, "/users");
15 req_post->addHeader("Content-Type", "application/json");
16 Json::Value user_data;
17 user_data["username"] = "new_user";
18 user_data["password"] = "secure_password";
19 // 将 JSON 数据转换为字符串并设置到 body
20 req_post->setBody(user_data.toStyledString()); // 或者 toFastWriter().write()

12.4.2 构建模拟 HttpResponse

你也可以手动创建 HttpResponse 的实例并检查其内容,或者期望控制器回调函数构建的响应:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/HttpResponse.h>
2
3 // 模拟一个成功的文本响应
4 auto resp_ok = drogon::HttpResponse::newHttpResponse();
5 resp_ok->setStatusCode(drogon::k200OK);
6 resp_ok->setContentTypeCode(drogon::CT_TEXT_PLAIN);
7 resp_ok->setBody("Operation successful");
8
9 // 模拟一个 JSON 错误响应
10 auto resp_error = drogon::HttpResponse::newHttpJsonResponse(Json::Value()); // 或者构建实际的 JSON
11 resp_error->setStatusCode(drogon::k400BadRequest);
12 resp_error->setContentTypeCode(drogon::CT_APPLICATION_JSON);
13 // ... set JSON body content ...

12.4.3 在测试中使用模拟对象

结合模拟对象和集成测试,你可以更精确地测试控制器方法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 假设有一个控制器方法签名如下:
2 // void UserController::getUser(const drogon::HttpRequestPtr& req,
3 // std::function<void(const drogon::HttpResponsePtr&)>&& callback,
4 // int user_id); // user_id 通过路径参数绑定
5
6 TEST_F(DrogonTest, GetUserEndpoint) {
7 // 模拟一个 GET 请求,路径参数 user_id 为 456
8 // 注意:路径参数绑定通常由 Drogon 路由系统完成。
9 // 如果直接调用控制器方法,你需要手动提供 user_id 参数。
10 // 对于集成测试,更推荐模拟 HttpRequest 并通过 drogon::app().sendRequest() 发送。
11
12 auto req = drogon::HttpRequest::newHttpRequest(drogon::HttpMethod::kGet, "/users/456");
13
14 // 使用 sendRequest 并处理异步回调
15 drogon::app().sendRequest(req,
16 [](drogon::ReqResult result, const drogon::HttpResponsePtr& response) {
17 ASSERT_EQ(result, drogon::ReqResult::kOk);
18 ASSERT_EQ(response->getStatusCode(), drogon::k200OK);
19 // 验证响应体内容,例如,如果是 JSON 响应
20 // Json::Value json_resp;
21 // Json::Reader reader;
22 // ASSERT_TRUE(reader.parse(response->getBody(), json_resp));
23 // ASSERT_EQ(json_resp["id"].asInt(), 456);
24 });
25
26 // 在一个同步测试框架中,你需要确保 Drogon 事件循环有运行的机会来处理这个请求
27 // 或者使用某种同步发送请求的模式。
28 }

这种方式模拟了 HTTP 请求,通过 Drogon 的内部机制处理,并让你检查最终的响应。

12.5 数据库测试策略

测试涉及数据库的代码(如使用 ORM 或执行原始 SQL)会引入额外的复杂性,因为数据库操作通常是异步的、有状态的,并且可能影响后续测试用例的结果。有效的数据库测试策略旨在使测试隔离、可重复和快速。

12.5.1 挑战

状态依赖: 数据库的状态会影响查询结果。测试用例必须确保数据库处于一个已知且干净的状态。
隔离性: 一个测试的数据库操作不应该影响其他测试。
性能: 真实的数据库操作比内存中的计算慢得多,可能导致测试套件运行缓慢。
异步性: Drogon 的数据库操作通常是异步的,测试代码需要正确地等待这些操作完成。

12.5.2 常见策略

使用独立的测试数据库: 为测试环境配置一个与开发/生产环境隔离的数据库实例。这样测试数据不会污染生产数据。在测试开始前或每个测试套件/用例前清空或重置这个数据库。
内存数据库: 对于简单的场景或不需要特定数据库特性时,可以使用内存数据库(如 SQLite 的内存模式)。这可以大大提高测试速度。
事务回滚 (Transaction Rollback): 这是最常用的策略之一。在每个测试用例开始时开启一个数据库事务,在测试结束时(无论成功或失败)回滚该事务。这样,测试对数据库所做的所有更改都会被撤销,确保了测试之间的隔离性。
数据夹具 (Test Fixtures): 在每个测试用例或测试套件运行前,向数据库中插入必要的数据,以确保测试有数据可操作。这通常与事务回滚结合使用。

12.5.3 Drogon 中的数据库测试实践(事务回滚)

Drogon 的数据库客户端支持事务。你可以在测试代码中利用这一点实现事务回滚策略。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/drogon.h>
2 #include <drogon/orm/DbClient.h>
3 #include <gtest/gtest.h>
4 #include "integrationtest_Fixture.h" // 假设你有这个 Fixture
5
6 // 获取数据库客户端
7 auto dbClient = drogon::app().getDbClient();
8
9 // 测试用户创建功能,使用事务回滚
10 TEST_F(DrogonTest, CreateUserWithTransactionRollback) {
11 // 获取数据库客户端,通常在 SetUp 中完成或通过全局函数获取
12
13 // 开始事务
14 dbClient->execSqlAsync("BEGIN;",
15 [this](const drogon::orm::Result& /*r*/) {
16 // 事务开始成功,执行测试逻辑
17 // 假设你的业务逻辑在 UserService::createUser 方法中
18 // 这个方法会调用 ORM 或 raw SQL 插入用户
19
20 // 模拟调用创建用户的功能
21 // 例如:
22 // userService->createUser("test_user_rollback", "password123",
23 // [this](bool success) {
24 // ASSERT_TRUE(success); // 验证创建是否成功
25
26 // 验证数据库中是否存在这个用户 (使用事务内的查询)
27 // dbClient->execSqlAsync("SELECT count(*) FROM users WHERE username = 'test_user_rollback';",
28 // [](const drogon::orm::Result& res) {
29 // ASSERT_EQ(res[0][0].as<long long>(), 1); // 应该能找到用户
30 // // ...
31 // },
32 // [](const drogon::orm::DrogonDbException& e) {
33 // FAIL() << "Database query failed: " << e.what();
34 // });
35
36
37 // 在测试逻辑的异步回调中,需要最终回滚事务
38 // 这是一个异步回调地狱的例子,实际应用中应结合 Fiber 或更高级的异步管理
39 // 如果使用 C++20 Fiber,代码会更线性化
40 // 比如,在 Fiber 中:
41 // co_await dbClient->execSqlCoro("BEGIN;");
42 // co_await userService->createUserCoro("test_user_rollback", "password123");
43 // auto res = co_await dbClient->execSqlCoro("SELECT count(*) FROM users WHERE username = 'test_user_rollback';");
44 // ASSERT_EQ(res[0][0].as<long long>(), 1);
45 // co_await dbClient->execSqlCoro("ROLLBACK;");
46 // // 通知测试完成
47 // this->test_done_.setValue(); // 使用一个Promise/Future或类似机制
48
49 // 如果不使用 Fiber,需要嵌套回调并在最外层回调中进行回滚
50 // 这里展示简化逻辑:
51 // 假设创建用户成功回调
52 // std::cout << "User created, now rollback" << std::endl; // 仅用于演示顺序
53 dbClient->execSqlAsync("ROLLBACK;",
54 [](const drogon::orm::Result& /*r*/) {
55 // 事务回滚成功
56 // 通知测试框架此测试完成
57 // 例如:test_promise_.set_value(); // 使用 std::promise/std::future
58 },
59 [](const drogon::orm::DrogonDbException& e) {
60 FAIL() << "Transaction rollback failed: " << e.what();
61 // 通知测试框架此测试失败并完成
62 // test_promise_.set_exception(...);
63 });
64 // },
65 // [this](const drogon::orm::DrogonDbException& e) { // 创建用户失败回调
66 // FAIL() << "Create user failed: " << e.what();
67 // // 依然需要回滚,虽然创建失败了
68 // dbClient->execSqlAsync("ROLLBACK;", ...); // 再次嵌套回滚逻辑
69 // // 通知测试框架此测试失败并完成
70 // });
71 },
72 [](const drogon::orm::DrogonDbException& e) {
73 FAIL() << "Failed to begin transaction: " << e.what();
74 // 通知测试框架此测试失败并完成
75 });
76
77 // 在异步测试中,主测试函数需要等待异步操作完成
78 // std::future<void> test_future_ = test_promise_.get_future();
79 // test_future_.wait(); // 等待测试完成信号
80 }

重要提示: 在 Drogon 中进行异步数据库测试并同步等待结果(以便 Google Test 能够正确判断测试通过或失败)需要 careful management of asynchronous operations and synchronization primitives (like std::promise/std::future, or using the testing framework's specific asynchronous features if any). Using C++20协程 (Coroutines) with Drogon's coroutine-friendly database methods (execSqlCoro, etc.) makes asynchronous testing code much cleaner and easier to write in a linear, synchronous style within a coroutine test function.

12.5.4 数据夹具 (Data Fixtures)

在每个测试前填充数据库,可以使用 SQL 脚本或 ORM 代码。例如,在测试 Fixture 的 SetUp 方法中执行 SQL 脚本来插入测试数据。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 void SetUp() override {
2 DrogonTest::SetUp(); // 调用父类的 SetUp
3
4 // 执行用于填充测试数据的 SQL 脚本
5 auto dbClient = drogon::app().getDbClient();
6 dbClient->execSqlSync("INSERT INTO users (username, ...) VALUES ('existing_user', ...);");
7 dbClient->execSqlSync("INSERT INTO products (...) VALUES (...);");
8 // 注意:这里使用 Sync 版本简化示例,实际测试中异步可能更合适,但需要等待。
9 }
10
11 void TearDown() override {
12 // 使用事务回滚的话,通常不需要在这里做太多清理,因为回滚会处理。
13 // 如果不使用事务回滚,可能需要在这里执行 DELETE FROM ... 清空表。
14 DrogonTest::TearDown(); // 调用父类的 TearDown
15 }

通过结合事务回滚和数据夹具,你可以为每个数据库相关的测试提供一个干净、预定义的状态,从而实现可靠和可重复的测试。

总结来说,对 Drogon 应用进行测试是构建健壮、可维护服务的关键。单元测试关注独立组件的内部逻辑,而集成测试验证组件之间的交互和整个请求处理流程。利用合适的测试框架(如 Google Test)、模拟对象以及针对数据库操作的策略(如事务回滚),可以有效地提高测试覆盖率和代码质量。

13. 应用安全(Application Security)

Web 应用的安全是一个至关重要的话题,它直接关系到用户数据的保护、服务的可靠性以及组织的声誉。作为一个高性能的 C++ Web 框架,Drogon 为开发者提供了构建安全应用的坚实基础,同时也要求开发者遵循安全开发的最佳实践。本章将讨论 Web 应用常见的安全威胁,介绍 Drogon 提供的安全特性,并指导读者如何在 Drogon 应用中实现认证(Authentication)、授权(Authorization)和 HTTPS 等关键安全措施。

13.1. Web 应用常见的安全威胁(Common Web Application Security Threats)

现代 Web 应用面临着各种各样的安全风险。理解这些威胁是构建安全应用的第一步。本节将概述几种最常见的 Web 安全漏洞。

注入(Injection):这类攻击涉及到向应用程序输入恶意数据,这些数据被解释为命令或代码,而非普通数据。最常见的形式是 SQL 注入(SQL Injection),攻击者在用户输入字段中插入恶意 SQL 代码,从而操纵数据库查询,可能导致数据泄露、损坏或未授权访问。其他形式的注入包括 OS 命令注入、LDAP 注入等。

跨站脚本(Cross-Site Scripting, XSS):XSS 攻击发生在攻击者将恶意脚本(通常是 JavaScript)注入到 Web 页面中,这些脚本随后在其他用户的浏览器中执行。根据脚本被注入的位置和方式,XSS 可以分为:
▮▮▮▮⚝ 存储型 XSS(Stored XSS):恶意脚本被永久存储在服务器端(如数据库、评论区),当用户访问包含该脚本的页面时,脚本被执行。
▮▮▮▮⚝ 反射型 XSS(Reflected XSS):恶意脚本包含在 URL 参数中,服务器将包含脚本的 URL 反射回响应中,用户点击恶意链接时脚本被执行。
▮▮▮▮⚝ DOM-based XSS:攻击发生在用户浏览器中,恶意脚本修改页面 DOM(Document Object Model)环境,导致其他脚本执行非预期行为。
XSS 攻击可能导致会话劫持(Session Hijacking)、敏感信息窃取、页面内容篡改等。

跨站请求伪造(Cross-Site Request Forgery, CSRF):CSRF 攻击诱导用户在不知情的情况下执行某个操作。攻击者构造一个恶意页面,其中包含指向目标网站的请求(例如,修改密码、发送邮件等)。如果用户已经在目标网站登录,且浏览器保留了会话信息(如 Cookie),那么当用户访问恶意页面时,浏览器会自动携带会话信息向目标网站发送请求,目标网站会误认为这是一个合法用户执行的操作。

不安全的直接对象引用(Insecure Direct Object References, IDOR):当应用暴露了内部实现的引用,如文件、数据库键或目录,并且没有足够的访问控制检查时,攻击者可以操纵这些引用来访问未经授权的数据。例如,通过修改 URL 中的用户 ID 参数来访问其他用户的账户信息。

敏感信息泄露(Sensitive Data Exposure):应用程序或其组件未能妥善保护敏感数据,如密码、信用卡号、健康记录等,导致攻击者可以访问或截获这些信息。这可能发生在数据传输过程中(未加密 HTTP)、存储过程中(数据库未加密)或通过日志文件、错误消息等途径。

配置错误(Security Misconfiguration):应用程序、框架、Web 服务器、数据库服务器等的安全设置不当,如使用了默认凭证、开启了不必要的服务、缺少适当的权限限制等,为攻击者提供了可乘之机。

失效的认证和会话管理(Broken Authentication and Session Management):与用户身份验证和会话相关的缺陷,如允许暴力破解密码、会话 ID 可预测、未强制使用安全会话 Cookie、会话超时设置不当等,使攻击者能够冒充用户身份。

13.2. Drogon 的安全特性(Drogon's Security Features)

Drogon 作为一个现代 C++ Web 框架,在设计时考虑了安全性,提供了一些内置的安全特性,并鼓励开发者采用安全实践。

参数验证(Parameter Validation):虽然 Drogon 本身不提供开箱即用的复杂参数验证框架,但其类型安全的接口(例如,通过 req->get<...>(...) 获取参数)以及方便与第三方验证库(如 JsonValidator)集成的能力,使得开发者可以轻松地在控制器或过滤器中实现对用户输入的严格验证,这是防止注入攻击(特别是 SQL 注入,虽然 ORM 也在很大程度上预防了这类问题)和逻辑漏洞的重要手段。

防止常见的 HTTP 攻击:Drogon 在底层处理 HTTP 请求时,会进行一些基本的合法性检查,例如请求行、头部格式等。虽然不是完整的 Web 应用防火墙(Web Application Firewall, WAF),但它提供了一个相对健壮的底层实现。框架本身是基于异步非阻塞设计的,这有助于抵抗一些简单的拒绝服务(Denial-of-Service, DoS)攻击,因为单个慢连接不会轻易耗尽服务器资源。此外,可以配置连接限制等参数来增强抗 DoS 能力。

SSL/TLS 支持:Drogon 内置了对 HTTPS 的支持,通过配置 SSL 证书,可以轻松地为应用启用加密连接,保护数据在传输过程中的机密性和完整性。这对于防止敏感信息泄露至关重要。

会话管理(Session Management):Drogon 提供了灵活的会话管理机制,支持多种存储后端(如内存、Redis)。合理的会话管理(如使用强随机数生成会话 ID、设置合理的会话超时、使用 HttpOnly 和 Secure 标记的 Cookie)是防止会话劫持和会话固定(Session Fixation)攻击的基础。

ORM 的安全性:Drogon 的内置 ORM(Object-Relational Mapping)在执行数据库操作时,通常使用参数化查询(Parameterized Queries)的方式,将用户提供的数据作为参数而不是直接拼接到 SQL 语句中。这是防御 SQL 注入最有效的方法之一。使用 ORM 可以极大地降低 SQL 注入的风险,尽管直接执行原始 SQL 语句(Raw SQL)时仍然需要开发者自行采取预防措施。

灵活的过滤器(Filter)和中间件(Middleware)机制:Drogon 的过滤器和中间件系统是实现自定义安全逻辑的强大工具。开发者可以创建过滤器来在请求到达控制器之前进行身份验证、权限检查、CSRF 令牌验证、输入过滤等操作,或者使用中间件处理响应头部,增加安全相关的 HTTP 头(如 Strict-Transport-Security, X-Content-Type-Options 等)。

13.3. CSRF 防护(CSRF Protection)

CSRF 攻击通过伪造用户的请求来执行敏感操作。Drogon 应用可以通过多种方式实现 CSRF 防护,其中最常用且有效的方法是使用 CSRF 令牌(CSRF Token)

防御原理:
在表单提交或 AJAX 请求中加入一个不可预测的随机令牌。这个令牌在用户第一次访问页面时由服务器生成并存储在用户的会话(Session)中。每次提交包含敏感操作的请求时,客户端会将该令牌也发送到服务器。服务器接收请求后,会验证请求中携带的令牌是否与用户会话中存储的令牌一致。如果不一致,则认为是伪造请求并拒绝。由于攻击者无法预测或获取到这个令牌(因为攻击发生在另一个域,无法读取目标域的 Cookie 或会话信息),因此难以构造有效的恶意请求。

在 Drogon 中实现 CSRF 防护的步骤(示例):

启用会话(Enable Session):CSRF 令牌需要与用户的会话关联,所以首先需要确保 Drogon 应用启用了 Session。这通常在 config.json 中配置或通过 drogon::app().enableSession(); 启用。

在 Session 中生成和存储 CSRF 令牌:可以在用户登录成功或第一次访问需要 CSRF 保护的页面时,生成一个随机字符串作为 CSRF 令牌,并将其存储在用户的 Session 中。

将 CSRF 令牌嵌入到页面表单或 JavaScript 中
▮▮▮▮ⓑ 对于 HTML 表单:在需要保护的 POST 表单中添加一个隐藏的 input 字段,其 name 通常是 _csrfcsrf_tokenvalue 就是服务器生成的令牌。例如:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 <form action="/sensitive_operation" method="POST">
2 <!-- ... 其他表单字段 ... -->
3 <input type="hidden" name="_csrf" value="服务器生成的令牌">
4 <button type="submit">执行操作</button>
5 </form>

▮▮▮▮ⓑ 对于 AJAX 请求:将 CSRF 令牌作为请求头(如 X-CSRF-Token)或请求体的一部分发送。如果使用头部,可能需要服务器端支持 CORS(Cross-Origin Resource Sharing)预检请求。通常更简单的做法是将令牌放在页面的某个 JavaScript 变量中,然后在发送 AJAX 请求时读取并加入。例如:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 const csrfToken = "服务器生成的令牌"; // 从页面中读取
2 fetch('/sensitive_operation_api', {
3 method: 'POST',
4 headers: {
5 'Content-Type': 'application/json',
6 'X-CSRF-Token': csrfToken // 或者在请求体中发送
7 },
8 body: JSON.stringify({ /* ... 数据 ... */ })
9 });

在服务器端验证 CSRF 令牌:在处理接收到包含敏感操作的请求时,编写逻辑来检查请求中提交的令牌(可能是表单参数或请求头)是否与当前用户 Session 中存储的令牌一致。这个验证逻辑可以放在控制器的处理方法中,或者更推荐的做法是实现为一个 Drogon 过滤器(Filter)并将其应用到需要保护的路由上。

示例(过滤器大致逻辑):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // Headers
2 #include <drogon/HttpFilter.h>
3 #include <drogon/drogon.h>
4 #include <iostream>
5
6 class CsrfFilter : public drogon::HttpFilter<CsrfFilter>
7 {
8 public:
9 virtual void doFilter(const drogon::HttpRequestPtr &req,
10 drogon::FilterCallback &&fcb,
11 drogon::FilterChainCallback &&fccb) override
12 {
13 // 假设只对POST/PUT/DELETE等方法进行CSRF检查
14 if (req->method() != drogon::HttpMethod::Post &&
15 req->method() != drogon::HttpMethod::Put &&
16 req->method() != drogon::HttpMethod::Delete)
17 {
18 fccb(); // 继续处理链
19 return;
20 }
21
22 auto session = req->getSession();
23 std::string sessionCsrfToken;
24 if (session && session->find("csrf_token"))
25 {
26 sessionCsrfToken = session->get<std::string>("csrf_token");
27 }
28
29 std::string requestCsrfToken;
30 // 可以从请求体(如表单参数)或请求头中获取令牌
31 if (req->method() == drogon::HttpMethod::Post && req->getBody().length() > 0) {
32 // 示例:从表单参数中获取
33 auto params = req->getParameters();
34 if (params.count("_csrf")) {
35 requestCsrfToken = params.at("_csrf");
36 }
37 // 或者从JSON体中获取
38 // auto json = req->getJsonObject();
39 // if (json && json->isMember("_csrf")) {
40 // requestCsrfToken = (*json)["_csrf"].asString();
41 // }
42 } else {
43 // 示例:从请求头中获取
44 auto headerToken = req->getHeader("X-CSRF-Token");
45 if (!headerToken.empty()) {
46 requestCsrfToken = headerToken;
47 }
48 }
49
50
51 if (!sessionCsrfToken.empty() && sessionCsrfToken == requestCsrfToken)
52 {
53 // 令牌匹配,请求合法
54 fccb(); // 继续处理链
55 }
56 else
57 {
58 // 令牌不匹配或缺失,拒绝请求
59 auto resp = drogon::HttpResponse::newHttpResponse(drogon::HttpStatusCode::k403Forbidden);
60 resp->setBody("CSRF Token validation failed.");
61 fcb(resp); // 阻止请求继续,返回403错误
62 }
63 }
64 };

将此过滤器应用于需要保护的路由或控制器即可。

13.4. XSS 防护(XSS Protection)

XSS 攻击的本质是将不受信任的数据作为代码在用户的浏览器中执行。防止 XSS 的核心原则是 “永远不要相信用户的输入,永远对输出进行转义(Escape)”

在 Drogon 应用中实现 XSS 防护:

输入验证与清理(Input Validation and Sanitization):虽然 XSS 主要通过输出攻击,但对输入进行初步的验证和清理也是有益的。移除或转义潜在的恶意字符(如 <script>, onerror, javascript: 等)可以作为第一道防线。但这不能替代输出转义。

输出转义(Output Escaping):这是防止 XSS 最重要的方法。当在 HTML 页面、JavaScript 代码、URL 或其他上下文中显示来自用户的输入时,必须根据输出的上下文对其进行适当的转义,以便浏览器将其视为数据而不是可执行代码。
▮▮▮▮⚝ HTML 转义:将特殊字符如 <, >, &, ", ' 转换为其 HTML 实体(如 &lt;, &gt;, &amp;, &quot;, &#39;)。
▮▮▮▮⚝ JavaScript 转义:当用户输入要嵌入到 <script> 标签内部或事件处理属性(如 onclick)时,需要进行 JavaScript 字符串转义。
▮▮▮▮⚝ URL 转义:当用户输入要用作 URL 的一部分时,需要进行 URL 百分号编码。

Drogon 在视图渲染方面提供了帮助:
▮▮▮▮⚝ 内置模板引擎:Drogon 的内置模板引擎(或集成的第三方模板引擎)通常提供自动转义功能。默认情况下,在模板中使用变量时,模板引擎应该会自动对输出进行 HTML 转义。例如,在 Drogon 内置模板中,显示变量通常会默认转义,但需要确认并理解其转义规则。如果需要输出原始(未转义)的 HTML(比如用户提交的是富文本),模板引擎通常也提供标记来禁用特定部分的转义,此时开发者必须确保这些内容是经过服务器端或客户端严格清理过的。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 <!-- 示例:Drogon 内置模板伪代码 -->
2 <!-- 默认转义:会转义 potentialXssData 中的 <script> 等 -->
3 <p>{{ potentialXssData }}</p>
4
5 <!-- 禁用转义(谨慎使用!):如果确定 safeHtml 已经过清理 -->
6 <div>{{{ safeHtml }}}</div>

开发者在使用模板引擎时,务必理解并正确使用其转义机制。永远优先使用默认的自动转义功能。

▮▮▮▮⚝ 手动转义:如果在控制器中直接构建 HTML 响应体,或者在处理 JSON API 时返回可能包含用户输入的字符串,开发者需要手动进行转义。可以使用 C++ 字符串处理函数或引入专门的 HTML 转义库来实现。Drogon 提供了一些工具函数,例如 drogon::utils::htmlEscape() 可用于 HTML 转义。

示例:在控制器中手动转义用户输入并在 HTML 中显示

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 假设从请求中获取用户输入的评论内容
2 std::string userComment = req->getParameter("comment");
3
4 // 进行 HTML 转义
5 std::string escapedComment = drogon::utils::htmlEscape(userComment);
6
7 // 构建 HTML 响应
8 std::string htmlResponse = "<html><body><p>最新评论: " + escapedComment + "</p></body></html>";
9
10 auto resp = drogon::HttpResponse::newHttpResponse();
11 resp->setBody(htmlResponse);
12 resp->setContentTypeCode(drogon::ContentType::kTextHtml);
13 callback(resp);

13.5. 认证(Authentication)与授权(Authorization)

安全的核心是知道“谁”在访问资源以及“他们有什么权限”来访问。认证(Authentication)是验证用户身份的过程(你是谁?),而授权(Authorization)是确定已认证用户是否有权限执行特定操作或访问特定资源的过程(你能做什么?)。

13.5.1. 基于 Session 的认证(Session-based Authentication)

基于 Session 的认证是一种传统的 Web 认证方式,广泛应用于 Web 网站。

工作流程:
① 用户通过登录页面提交用户名和密码。
② 服务器接收到凭证后,在数据库中验证用户身份。
③ 如果凭证有效,服务器创建一个会话(Session),生成一个唯一的会话 ID,并将与用户相关的信息(如用户 ID、用户名等)存储在服务器端的 Session 中。
④ 服务器将 Session ID 通过 Set-Cookie 头部发送给用户的浏览器。
⑤ 浏览器将 Session ID 存储为 Cookie。
⑥ 用户后续的请求都会自动携带这个 Session ID Cookie。
⑦ 服务器接收到带有 Session ID 的请求后,根据 Session ID 查找服务器端对应的 Session 数据,从而识别出用户的身份。
⑧ 根据用户的身份(通过 Session 数据获取),进行授权检查。

在 Drogon 中实现基于 Session 的认证:

启用 Session:确保在 config.json 或代码中启用 Drogon 的 Session 功能。可以配置 Session 的存储类型(内存、Redis 等)、超时时间、Cookie 名称等。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "enable_session": true,
3 "session_timeout": 3600, // seconds
4 "session_cookie_name": "DROGON_SESSION_ID"
5 // "session_store_type": "redis",
6 // "redis_ip": "127.0.0.1",
7 // "redis_port": 6379
8 }

登录处理
在登录控制器中,接收用户提交的用户名和密码,验证成功后,通过 req->getSession() 获取当前请求的 Session 对象,并将用户 ID 或其他标识存储到 Session 中。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 假设用户验证成功后获取到 userId
2 long long userId = ...; // 从数据库或用户服务获取
3
4 auto session = req->getSession();
5 session->insert("user_id", userId); // 将用户ID存入Session
6
7 // 认证成功,返回成功响应或重定向
8 auto resp = drogon::HttpResponse::newRedirectionResponse("/profile");
9 callback(resp);

检查登录状态
在需要用户登录后才能访问的接口或页面,通过 req->getSession() 获取 Session,检查其中是否存在用户标识(例如 user_id)。这个检查逻辑通常放在过滤器中实现。

示例(认证过滤器大致逻辑):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // Headers
2 #include <drogon/HttpFilter.h>
3 #include <drogon/drogon.h>
4
5 class AuthFilter : public drogon::HttpFilter<AuthFilter>
6 {
7 public:
8 virtual void doFilter(const drogon::HttpRequestPtr &req,
9 drogon::FilterCallback &&fcb,
10 drogon::FilterChainCallback &&fccb) override
11 {
12 auto session = req->getSession();
13 if (session && session->find("user_id"))
14 {
15 // Session 中存在 user_id,用户已登录
16 // 可以从 session 中获取用户ID或其他信息 req->getAttr<long long>("user_id");
17 // 并可能将其附加到请求对象中方便后续使用
18 // req->attributes()->insert("current_user_id", session->get<long long>("user_id"));
19 fccb(); // 继续处理链
20 }
21 else
22 {
23 // 用户未登录,返回未授权或重定向到登录页
24 auto resp = drogon::HttpResponse::newHttpResponse(drogon::HttpStatusCode::k401Unauthorized);
25 resp->setBody("Unauthorized");
26 // 或者重定向到登录页
27 // auto resp = drogon::HttpResponse::newRedirectionResponse("/login");
28 fcb(resp); // 阻止请求继续
29 }
30 }
31 };

将此过滤器应用到需要认证的路由或控制器上。

登出处理
在登出接口中,通过 req->getSession() 获取 Session,并调用 session->clear()session->erase("user_id") 清除用户身份信息,然后返回成功响应或重定向到首页/登录页。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 auto session = req->getSession();
2 if (session) {
3 session->clear(); // 清除整个session
4 // 或者 session->erase("user_id"); // 只清除user_id
5 }
6 auto resp = drogon::HttpResponse::newRedirectionResponse("/");
7 callback(resp);

基于 Session 的认证优点是简单易懂,服务器端易于管理用户状态,安全性相对较高(会话 ID 本身是难以预测的)。缺点是在分布式环境下(多个服务器实例)需要共享 Session 存储(如 Redis),增加了架构复杂度,且不适合移动应用或前后端完全分离的应用场景,因为它依赖于浏览器自动携带 Cookie。

13.5.2. 基于 Token 的认证(如 JWT)(Token-based Authentication (e.g., JWT))

基于 Token 的认证是一种无状态(Stateless)认证方式,特别适用于前后端分离、移动应用和微服务架构。

工作流程:
① 用户通过登录接口提交用户名和密码。
② 服务器验证凭证。
③ 验证成功后,服务器根据用户身份信息生成一个令牌(Token)。这个令牌是用户身份的凭证,通常包含用户 ID、过期时间等信息,并且经过加密或签名,确保不被篡改。
④ 服务器将生成的 Token 返回给客户端。
⑤ 客户端接收到 Token 后,将其存储起来(如 localStorage, AsyncStorage 等)。
⑥ 客户端在后续需要认证的请求中,将 Token 添加到请求头(通常是 Authorization: Bearer <token>)发送给服务器。
⑦ 服务器接收到请求后,从请求头中提取 Token,验证 Token 的有效性(是否过期、签名是否正确等)。
⑧ 如果 Token 有效,服务器从 Token 中解析出用户身份信息,然后进行授权检查。
⑨ 服务器处理请求并返回响应,不存储用户状态。

JSON Web Token (JWT) 是一种常用的基于 Token 的认证规范。JWT 是一个紧凑的、URL 安全的手段,用于在双方之间安全地传输信息。JWT 通常包含三部分,用点号(.)分隔:
Header(头部):包含 Token 的类型(通常是 JWT)和使用的签名算法(如 HMAC SHA256 或 RSA)。
Payload(载荷):包含声明(Claims),即关于实体(通常是用户)以及其他数据的声明。常见的声明包括 iss (issuer), exp (expiration time), sub (subject), 用户 ID 等。Payload 通常不应包含敏感信息,因为它可以被 Base64 解码(虽然不是加密)。
Signature(签名):使用 Header 中指定的算法,结合编码后的 Header、Payload 和一个秘密密钥(Secret Key)进行签名,用于验证 Token 的完整性和真实性。

在 Drogon 中实现基于 Token 的认证(以 JWT 为例):

选择或实现 JWT 库:Drogon 没有内置 JWT 的生成和解析功能。需要集成一个第三方的 C++ JWT 库,例如 jwt-cpp

登录接口生成 JWT
在登录控制器中,验证用户身份成功后,使用选定的 JWT 库生成一个 JWT。将用户 ID 和过期时间等信息放入 Payload,使用一个安全的密钥对 JWT 进行签名,然后将 JWT 字符串作为响应体的一部分返回给客户端。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 假设用户验证成功,获取 userId
2 long long userId = ...;
3 // 假设使用 jwt-cpp 库
4 using namespace jwt::params;
5
6 // 生成 JWT
7 auto token = jwt::create()
8 .set_issuer("auth.example.com") // 签发者
9 .set_type("JWT")
10 .set_id("uuid") // 可以设置唯一的标识
11 .set_issued_at(std::chrono::system_clock::now()) // 签发时间
12 .set_expires_at(std::chrono::system_clock::now() + std::chrono::seconds{3600}) // 1小时后过期
13 .set_payload_claim("user_id", jwt::claim(std::to_string(userId))) // 添加用户ID声明
14 .sign(jwt::algorithm::hs256{"your_secret_key_here"}); // 使用HS256算法和密钥签名
15
16 // 将 token 返回给客户端
17 Json::Value jsonResp;
18 jsonResp["token"] = token;
19 auto resp = drogon::HttpResponse::newHttpJsonResponse(jsonResp);
20 callback(resp);

认证过滤器验证 JWT
实现一个过滤器,在需要认证的接口前执行。从请求头 Authorization: Bearer <token> 中提取 Token 字符串。使用 JWT 库,结合相同的密钥,验证 Token 的签名和有效期。如果验证通过,从 Payload 中解析出用户 ID 或其他信息,并将其附加到请求对象中(例如使用 req->attributes()->insert(...)),以便后续的控制器或服务使用。如果验证失败(Token 格式错误、签名无效、已过期),则返回 401 未授权错误。

示例(JWT 认证过滤器大致逻辑):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // Headers
2 #include <drogon/HttpFilter.h>
3 #include <drogon/drogon.h>
4 // #include <jwt-cpp/jwt.h> // 假设引入 jwt-cpp 库
5
6 class JwtAuthFilter : public drogon::HttpFilter<JwtAuthFilter>
7 {
8 public:
9 virtual void doFilter(const drogon::HttpRequestPtr &req,
10 drogon::FilterCallback &&fcb,
11 drogon::FilterChainCallback &&fccb) override
12 {
13 auto authHeader = req->getHeader("Authorization");
14 if (authHeader.empty() || !authHeader.starts_with("Bearer "))
15 {
16 // Authorization 头部缺失或格式错误
17 sendUnauthorized(fcb);
18 return;
19 }
20
21 std::string token = authHeader.substr(7); // 提取 token 字符串
22
23 try
24 {
25 // 假设使用 jwt-cpp 进行验证
26 auto decodedToken = jwt::decode(token);
27 auto verifier = jwt::verify()
28 .allow_algorithm(jwt::algorithm::hs256{"your_secret_key_here"}) // 使用相同的算法和密钥
29 .with_issuer("auth.example.com"); // 可选:验证签发者
30
31 verifier.verify(decodedToken);
32
33 // Token 有效,提取用户 ID
34 auto userIdClaim = decodedToken.get_payload_claim("user_id");
35 if (!userIdClaim.is_null()) {
36 // 将用户ID附加到请求属性中
37 req->attributes()->insert("current_user_id", std::stoll(userIdClaim.as_string()));
38 fccb(); // 继续处理链
39 return;
40 } else {
41 // Token 有效但缺少 user_id 声明
42 sendUnauthorized(fcb, "Invalid token payload");
43 return;
44 }
45
46 }
47 catch (const std::exception& e)
48 {
49 // Token 验证失败(签名错误、过期等)
50 LOG_ERROR << "JWT validation failed: " << e.what();
51 sendUnauthorized(fcb, "Invalid or expired token");
52 return;
53 }
54 }
55
56 private:
57 void sendUnauthorized(drogon::FilterCallback &&fcb, const std::string& reason = "Unauthorized")
58 {
59 auto resp = drogon::HttpResponse::newHttpResponse(drogon::HttpStatusCode::k401Unauthorized);
60 resp->setBody(reason);
61 fcb(resp);
62 }
63 };

基于 Token 的认证优点是无状态、易于扩展、跨域友好,适合移动应用和 API 服务。缺点是 Token 一旦签发,在过期前无法使其失效(除非引入额外的机制如黑名单/白名单),且 Token 通常包含用户信息,需要注意 Payload 中不要包含敏感信息。

13.5.3. 实现授权检查(Implementing Authorization Checks)

认证告诉我们“谁”是当前用户,而授权则决定这个用户“能做什么”。授权检查通常在认证成功后进行。

在 Drogon 中实现授权检查的方法:

在控制器内部检查
在每个需要特定权限的处理方法内部,获取当前用户身份(通过 Session 或 JWT 过滤器附加到请求属性的用户 ID),然后根据业务逻辑查询用户的权限,并决定是否允许执行操作。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 假设 JwtAuthFilter 已将 user_id 附加到请求属性
2 void UserController::updateProfile(const drogon::HttpRequestPtr& req,
3 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
4 long long targetUserId)
5 {
6 auto currentUserId = req->getAttr<long long>("current_user_id");
7
8 // 检查当前用户是否有权限修改 targetUserId 的资料
9 // 例如:只能修改自己的资料,或者管理员可以修改任意用户的资料
10 bool isOwner = (currentUserId == targetUserId);
11 bool isAdmin = checkUserIsAdmin(currentUserId); // 假设有函数检查管理员权限
12
13 if (isOwner || isAdmin) {
14 // 执行修改资料的逻辑
15 // ...
16 auto resp = drogon::HttpResponse::newHttpResponse(drogon::HttpStatusCode::k200OK);
17 callback(resp);
18 } else {
19 // 无权限
20 auto resp = drogon::HttpResponse::newHttpResponse(drogon::HttpStatusCode::k403Forbidden);
21 callback(resp);
22 }
23 }

这种方法的缺点是将授权逻辑分散到各个控制器方法中,可能导致代码重复和维护困难。

使用过滤器(Filter)进行授权检查
更推荐的做法是创建专门的授权过滤器。这些过滤器在认证过滤器之后执行。授权过滤器可以根据用户身份检查其是否具备访问当前路由所需的权限。例如,一个 AdminFilter 检查用户是否是管理员,一个 OwnerOrAdminFilter 检查用户是否是资源所有者或管理员。

示例(Admin 授权过滤器大致逻辑):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // Headers
2 #include <drogon/HttpFilter.h>
3 #include <drogon/drogon.h>
4
5 class AdminFilter : public drogon::HttpFilter<AdminFilter>
6 {
7 public:
8 virtual void doFilter(const drogon::HttpRequestPtr &req,
9 drogon::FilterCallback &&fcb,
10 drogon::FilterChainCallback &&fccb) override
11 {
12 // 假设认证过滤器已将 user_id 附加到请求属性
13 auto currentUserIdAttr = req->attributes()->get<long long>("current_user_id");
14 if (!currentUserIdAttr.has_value()) {
15 // 用户未认证(理论上认证过滤器应该先执行并阻止)
16 sendForbidden(fcb, "Authentication required");
17 return;
18 }
19 long long currentUserId = currentUserIdAttr.value();
20
21 // 检查用户是否是管理员(查询数据库或缓存)
22 bool isAdmin = checkUserIsAdmin(currentUserId); // 假设这是一个业务逻辑函数
23
24 if (isAdmin)
25 {
26 fccb(); // 用户是管理员,继续处理链
27 }
28 else
29 {
30 // 用户不是管理员,拒绝访问
31 sendForbidden(fcb, "Requires administrator privileges");
32 }
33 }
34 private:
35 void sendForbidden(drogon::FilterCallback &&fcb, const std::string& reason = "Forbidden")
36 {
37 auto resp = drogon::HttpResponse::newHttpResponse(drogon::HttpStatusCode::k403Forbidden);
38 resp->setBody(reason);
39 fcb(resp);
40 }
41
42 // 示例函数:检查用户是否是管理员
43 bool checkUserIsAdmin(long long userId) {
44 // TODO: 实现具体的业务逻辑,例如查询数据库
45 // return database->isUserAdmin(userId);
46 return userId == 1; // 示例:假设用户ID为1的是管理员
47 }
48 };

将此过滤器应用于只有管理员才能访问的路由。

通过组合使用认证过滤器和授权过滤器,可以清晰地分离认证和授权逻辑,并在路由层面灵活地应用不同的权限要求。

13.6. HTTPS/SSL 配置(HTTPS/SSL Configuration)

HTTPS 是通过 TLS/SSL 协议加密的 HTTP 连接,用于在客户端和服务器之间安全地传输数据。启用 HTTPS 对于保护用户隐私、防止数据被窃听和篡改至关重要,特别是对于处理敏感信息(如登录凭证、支付信息)的应用。

在 Drogon 中配置 HTTPS 非常简单,主要通过修改 config.json 配置文件来实现。

获取 SSL 证书
要启用 HTTPS,首先需要一个有效的 SSL 证书。证书可以从证书颁发机构(Certificate Authority, CA)购买,也可以使用 Let's Encrypt 等服务获取免费证书,或者在开发和测试环境中使用自签名证书。通常需要两个文件:
证书文件(Certificate File):通常是 .crt, .pem.cer 格式,包含公钥和证书信息。
私钥文件(Private Key File):通常是 .key.pem 格式,与证书中的公钥配对。私钥是敏感文件,需要妥善保管。

配置 Drogon 监听 HTTPS
在 Drogon 的 config.json 文件中,找到或添加 listeners 部分。为需要监听 HTTPS 的地址和端口添加一个新的监听器配置,并指定证书文件和私钥文件的路径。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "listeners": [
3 {
4 "ip": "0.0.0.0",
5 "port": 80,
6 "protocol": "http"
7 },
8 {
9 "ip": "0.0.0.0",
10 "port": 443,
11 "protocol": "https",
12 "cert": "/path/to/your/certificate.pem", // 替换为你的证书文件路径
13 "key": "/path/to/your/private_key.pem" // 替换为你的私钥文件路径
14 // "ssl_files_passwd": "your_password_if_key_is_encrypted" // 如果私钥加密,需要密码
15 }
16 ],
17 // ... 其他配置 ...
18 }

ip: 服务器监听的 IP 地址,0.0.0.0 表示监听所有可用网络接口。
port: HTTPS 通常使用标准端口 443。
protocol: 设置为 "https"
cert: 你的 SSL 证书文件路径。
key: 你的 SSL 私钥文件路径。
ssl_files_passwd (可选):如果私钥文件是加密的,需要在此提供密码。

配置 HTTP 强制重定向到 HTTPS (可选)
出于安全考虑,通常会将所有 HTTP 请求强制重定向到 HTTPS。这可以通过在 Drogon 应用中添加一个简单的过滤器或路由来实现,捕获所有 HTTP 请求并返回一个 301 或 302 重定向响应到相应的 HTTPS URL。或者,更常见和推荐的做法是在 Drogon 前面部署一个反向代理服务器(如 Nginx, Caddy),由反向代理来处理 SSL 终端和 HTTP 到 HTTPS 的重定向。

其他 SSL/TLS 配置
Drogon 底层使用 OpenSSL 或 LibreSSL 来处理 SSL/TLS。在 config.json 中,还可以配置更高级的 SSL/TLS 设置,例如:
ssl_protocol: 指定允许使用的 TLS 协议版本(如 TLSv1.2,TLSv1.3)。
ssl_cipher_list: 指定允许使用的加密套件列表,以提高安全性。
ssl_verify_client: 配置是否验证客户端证书(用于双向 TLS)。
详细的配置选项可以参考附录 A 或 Drogon 官方文档。合理配置这些选项可以提高连接的安全性,防止使用弱加密算法和过时协议。

安全相关的 HTTP 头部
除了加密连接本身,通过设置一些安全相关的 HTTP 头部可以进一步增强 Web 应用的安全性。这些头部可以在 Drogon 的过滤器或中间件中设置到 HttpResponse 对象中。
Strict-Transport-Security (HSTS):强制浏览器使用 HTTPS 访问该网站。
X-Content-Type-Options: nosniff:防止浏览器对响应内容进行 MIME 类型嗅探,有助于防止 XSS 攻击。
X-Frame-Options: DENYSAMEORIGIN:防止页面被嵌入到 <iframe> 中,防御点击劫持(Clickjacking)攻击。
Content-Security-Policy (CSP):控制页面可以加载哪些资源,如脚本、样式表、图片等,是防御 XSS 的有力工具。CSP 头部配置比较复杂,需要根据具体应用需求进行设置。

通过以上措施,开发者可以显著提升 Drogon Web 应用的安全性。然而,安全是一个持续的过程,需要不断学习、审查代码、关注安全漏洞和最佳实践。

14. 部署与性能优化

本章作为《Drogon Web 框架:从入门到精通》的重要组成部分,将引导读者进入 Drogon 应用的生产环境。🚀 构建出色的 Web 服务只是第一步,确保它能在实际部署中稳定、高效地运行同样关键。本章将深入探讨 Drogon 应用的部署策略、生产环境的配置要点,以及如何对应用进行性能监控和优化,帮助您构建既强大又可靠的高性能 Web 服务。我们将涵盖从基础的生产环境配置到高级的反向代理配置,从性能监控工具的使用到具体的调优技巧。无论您是准备将第一个 Drogon 应用上线,还是希望优化现有服务的性能瓶颈,本章都将提供宝贵的指导和实践经验。

14.1 生产环境配置

在开发阶段,我们通常使用默认配置或针对开发便利性进行设置。然而,对于生产环境(Production Environment),必须仔细审查和调整配置,以确保应用的性能、稳定性和安全性。Drogon 主要通过 config.json 文件来管理大部分运行时配置。

14.1.1 配置文件的使用

Drogon 默认在当前目录或可执行文件同级目录寻找名为 config.json 的配置文件。这个文件是标准的 JSON 格式,包含了一系列键值对,用于指定 Drogon 应用的行为。

例如,一个基本的 config.json 可能看起来像这样:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "listeners": [
3 {
4 "address": "0.0.0.0",
5 "port": 8080,
6 "protocol": "http"
7 }
8 ],
9 "app": {
10 "thread_num": 16,
11 "document_root": "./web",
12 "log_path": "./logs",
13 "log_level": "info",
14 "upload_path": "./uploads",
15 "max_connections": 10000,
16 "idle_connection_timeout": 60
17 }
18 }

这个文件配置了 Drogon 应用监听的地址和端口、工作线程数、静态文件根目录、日志路径和级别等关键设置。

14.1.2 运行时配置的获取

在 Drogon 应用的代码中,您可以通过 drogon::app() 单例对象来获取和访问配置信息。虽然大部分生产配置在启动前通过 config.json 设置,但在某些场景下,可能需要在运行时获取配置项的值。

例如,获取线程数或日志级别:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/drogon.h>
2
3 int main() {
4 // Load config from file (usually done automatically by drogon::app().run())
5 // drogon::app().loadConfigFile("config.json");
6
7 // Get configuration values
8 int threadNum = drogon::app().getThreadNum();
9 std::string logLevel = drogon::app().get ; // Need to check drogon API for specific config getters
10
11 // Access values from config.json via json object if needed
12 Json::Value config = drogon::app().get ; // Check Drogon API for config access
13
14 // Example (conceptual, check actual API)
15 // if (config.isMember("app") && config["app"].isMember("thread_num")) {
16 // int configuredThreadNum = config["app"]["thread_num"].asInt();
17 // LOG_INFO << "Configured thread num: " << configuredThreadNum;
18 // }
19
20
21 drogon::app().run();
22 return 0;
23 }

要获取配置项的值,通常不需要直接解析 config.json 的 JSON 对象。Drogon 提供了一系列 get... 方法来访问常用配置。例如:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/drogon.h>
2
3 int main() {
4 // Configure Drogon (often done implicitly by run() or explicitly by loadConfigFile)
5 // drogon::app().loadConfigFile("config.json");
6
7 // Get common configuration values
8 int threadNum = drogon::app().getThreadNum();
9 std::string documentRoot = drogon::app().getDocumentRoot();
10 // Note: Getting log level directly via app() might not be the primary API.
11 // Log level is usually set via config or drogon::setLogLevel.
12
13 LOG_INFO << "Current Thread Num: " << threadNum;
14 LOG_INFO << "Document Root: " << documentRoot;
15
16 // Accessing less common config via json object (example pattern)
17 // Make sure the config is loaded first
18 auto configJson = drogon::app().getJsonConfig(); // Check drogons API for this method or similar
19 if (!configJson.empty() && configJson.isMember("app") && configJson["app"].isMember("max_connections")) {
20 int maxConns = configJson["app"]["max_connections"].asInt();
21 LOG_INFO << "Max connections from config: " << maxConns;
22 }
23
24
25 drogon::app().run();
26 return 0;
27 }

请查阅 Drogon 官方文档,获取最新的 API 来访问具体的配置项。

① 关键生产配置项

config.json 中,以下配置项对于生产环境尤为重要:

listeners: 定义服务监听的地址、端口和协议(HTTP/HTTPS)。在生产环境通常监听特定的内网地址(如 127.0.0.1 或内网 IP)并在反向代理后方。
app.thread_num: 工作线程数。这是影响并发处理能力的关键参数。通常设置为 CPU 核心数的 1 到 2 倍。过多的线程可能导致上下文切换开销增大。
app.log_level: 日志级别(trace, debug, info, warn, error, fatal)。生产环境通常设置为 infowarn,以减少日志量,只记录重要信息或错误。
app.log_path: 日志文件输出路径。确保该路径存在且 Drogon 进程有写入权限。
app.document_root: 静态文件服务的根目录。确保静态资源(HTML, CSS, JS, 图片等)放在该目录下,并在生产环境中关闭不必要的静态文件访问(例如,交给反向代理处理)。
app.upload_path: 文件上传的临时或目标路径。确保该路径安全且有足够的空间和权限。
app.max_connections: 服务器接受的最大连接数。根据服务器资源和预期负载进行调整。
app.idle_connection_timeout: 空闲连接的超时时间(秒)。可以设置一个合理的值来清理不活动的连接。
ssl: 如果需要 Drogon 直接处理 HTTPS,需要配置 SSL 证书和密钥路径。通常建议通过反向代理处理 SSL。
db_clients: 数据库连接配置。生产环境应使用连接池(连接数、空闲连接超时等)并确保数据库地址、用户名、密码正确且安全。

14.1.3 生产模式 (Production Mode)

Drogon 提供了一个 production 模式。当以生产模式运行时,Drogon 会进行一些优化,例如关闭调试信息、启用更严格的缓存等。可以通过命令行参数 -m production 或在 config.json 中设置 "app": {"mode": "production"} 来启用。强烈建议在生产环境中使用生产模式。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 运行生产模式
2 ./your_drogon_app -m production

或者在 config.json 中:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "mode": "production",
4 // ... other settings
5 }
6 // ...
7 }

14.2 部署方式

部署 Drogon 应用的常见方式包括直接部署可执行二进制文件和使用容器化技术(如 Docker)。

14.2.1 直接部署二进制文件

这是最简单直接的方式。

① 构建可执行文件

首先,在生产服务器或一个与生产服务器环境兼容的编译环境中,使用 Release 配置编译您的 Drogon 应用。这通常意味着禁用调试信息并启用优化。

▮▮▮▮⚝ 使用 CMake 构建,通常在构建目录下执行:
▮▮▮▮▮▮▮▮⚝ cmake .. -DCMAKE_BUILD_TYPE=Release
▮▮▮▮▮▮▮▮⚝ make -jN (N为核心数)
▮▮▮▮⚝ 可执行文件通常在构建目录的子目录中生成。

② 部署文件

将编译好的可执行文件以及生产环境所需的 config.json 文件、静态资源目录(如 web)、视图文件(如 views)、日志目录(确保存在)等复制到生产服务器的指定目录。

③ 运行应用

在生产服务器上,通过命令行直接运行可执行文件:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 cd /path/to/your/app
2 ./your_drogon_app -m production # 建议使用生产模式
④ 后台运行与进程管理

在生产环境中,应用需要能在后台持续运行,并在崩溃时自动重启。可以使用以下工具:

▮▮▮▮⚝ screen 或 tmux: 用于在终端会话结束后保持程序运行。
▮▮▮▮⚝ systemd: Linux 系统中最常用的服务管理工具。可以创建一个 .service 文件来定义如何启动、停止、重启 Drogon 应用。
▮▮▮▮⚝ Supervisor: 一个通用的进程控制系统,可以监控和管理多个进程。

Systemd 示例 (.service 文件):

创建一个文件,例如 /etc/systemd/system/drogon-app.service

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 [Unit]
2 Description=My Drogon Web Application
3 After=network.target
4
5 [Service]
6 User=your_user # 运行服务的用户
7 Group=your_group # 运行服务的用户组
8 WorkingDirectory=/path/to/your/app # 应用所在的目录
9 ExecStart=/path/to/your/app/your_drogon_app -m production # 启动命令
10 Restart=on-failure # 崩溃时自动重启
11 RestartSec=5 # 崩溃后等待5秒重启
12 StandardOutput=append:/path/to/your/app/logs/stdout.log # 标准输出重定向
13 StandardError=append:/path/to/your/app/logs/stderr.log # 标准错误重定向
14
15 [Install]
16 WantedBy=multi-user.target

然后启用和启动服务:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 sudo systemctl daemon-reload
2 sudo systemctl enable drogon-app.service
3 sudo systemctl start drogon-app.service

14.2.2 使用 Docker 容器

使用 Docker 是现代应用部署的主流方式,它提供了环境隔离、可移植性和易于管理的优点。

① 创建 Dockerfile

在项目根目录创建一个 Dockerfile 文件,描述如何构建 Drogon 应用的 Docker 镜像。

一个简单的 Dockerfile 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 # 使用一个包含 C++ 编译环境的基础镜像,例如 Ubuntu 或 Debian
2 # 建议使用轻量级且包含必要运行时库的镜像作为最终运行环境的基础
3 # 例如:ubuntu:22.04 或 debian:bookworm
4
5 # --- Build Stage ---
6 FROM ubuntu:22.04 AS builder
7
8 # 安装构建依赖
9 RUN apt-get update && apt-get install -y build-essential cmake git libjsoncpp-dev uuid-dev zlib1g-dev libssl-dev # Optional dependencies depending on your usage (e.g., databases, c-ares)
10 libpq-dev libmysqlclient-dev libsqlite3-dev libc-ares-dev # For C++20 Coroutines/Fibers, may need a newer GCC/Clang
11 g++-10 # Clean up apt cache
12 && rm -rf /var/lib/apt/lists/*
13
14 # 设置环境变量使用特定版本的g++,如果需要C++20协程
15 ENV CXX=g++-10
16
17 # 克隆 Drogon 仓库并构建 (或者直接添加Drogon源码或使用vcpkg等方式获取)
18 # 这里假设直接克隆最新代码构建,实际生产建议使用稳定版本或通过包管理器/vcpkg
19 RUN git clone https://github.com/drogonframework/drogon.git /drogon && mkdir /drogon/build && cd /drogon/build && cmake .. && make -j$(nproc) && make install
20
21 # 将应用代码复制到容器中
22 WORKDIR /app
23 COPY . /app
24
25 # 构建 Drogon 应用
26 # 假设你的项目使用 CMake 构建
27 RUN mkdir build && cd build && cmake .. -DCMAKE_BUILD_TYPE=Release && make -j$(nproc)
28
29 # --- Runtime Stage ---
30 # 使用一个更小的运行时镜像,只包含应用运行所需的库
31 FROM ubuntu:22.04
32
33 # 安装运行时依赖
34 RUN apt-get update && apt-get install -y libjsoncpp25 uuid-dev zlib1g libssl3 # Optional runtime libs (e.g., databases)
35 libpq5 libmysqlclient21 libsqlite3-0 libc-ares2 # If using a specific g++ version for fibers/coroutines
36 libstdc++6 libgcc-s1 # Check for specific library versions required by your build
37 # e.g., if build uses g++-10, you might need libstdc++6 from a specific version
38 # Clean up apt cache
39 && rm -rf /var/lib/apt/lists/*
40
41 # 将构建好的应用可执行文件和相关资源复制到运行时镜像
42 WORKDIR /app
43 COPY --from=builder /app/build/your_drogon_app /app/your_drogon_app
44 COPY --from=builder /app/config.json /app/config.json
45 COPY --from=builder /app/web /app/web # 如果有静态文件
46 COPY --from=builder /app/views /app/views # 如果有视图文件
47
48 # 创建日志目录和上传目录,并设置权限(如果需要)
49 RUN mkdir -p logs uploads && chmod -R 777 logs uploads # ⚠️ Consider more granular permissions
50
51 # 暴露应用监听的端口
52 EXPOSE 8080
53
54 # 启动应用 (使用生产模式)
55 CMD ["/app/your_drogon_app", "-m", "production"]

这个 Dockerfile 使用了多阶段构建(Multi-stage build),在一个阶段构建应用,然后在另一个更小的运行时镜像中运行,以减小最终镜像的大小。请根据您的具体项目依赖和结构调整 Dockerfile。

② 构建 Docker 镜像

在包含 Dockerfile 的项目根目录下执行构建命令:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 docker build -t your-drogon-app:latest .
③ 运行 Docker 容器

在生产服务器上运行构建好的镜像:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 docker run -d --name drogon-app -p 80:8080 -v /path/on/host/logs:/app/logs -v /path/on/host/uploads:/app/uploads your-drogon-app:latest

▮▮▮▮⚝ -d: 后台运行容器。
▮▮▮▮⚝ --name drogon-app: 给容器指定一个名字。
▮▮▮▮⚝ -p 80:8080: 将宿主机的 80 端口映射到容器内部的 8080 端口(Drogon 监听的端口)。如果您使用反向代理,这里通常映射到宿主机的某个内部端口(如 8080),然后反向代理监听公网端口(如 80 或 443)并转发到此内部端口。
▮▮▮▮⚝ -v ...: 挂载数据卷,用于持久化日志和上传文件等。/path/on/host/logs/path/on/host/uploads 需要替换为宿主机上的实际路径。

使用 Docker 可以方便地进行版本管理、回滚和扩展。

14.3 使用反向代理 (Reverse Proxy)

在生产环境中,将 Drogon 应用直接暴露在公网上是不推荐的。通常会在 Drogon 前面放置一个反向代理服务器,如 Nginx 或 Caddy。

14.3.1 反向代理的作用

使用反向代理的主要优势包括:

▮▮▮▮⚝ 安全性: 反向代理可以作为第一道防线,过滤恶意请求,提供更安全的 SSL/TLS 终止。
▮▮▮▮⚝ 负载均衡 (Load Balancing): 可以将流量分发到多个 Drogon 实例,提高可用性和扩展性。
▮▮▮▮⚝ SSL/TLS 终止: 集中处理 HTTPS 流量的加密和解密,减轻 Drogon 的负担,简化证书管理。
▮▮▮▮⚝ 静态文件服务: 反向代理通常更擅长高效地服务静态文件,可以将静态资源的请求直接由反向代理处理,而不是转发给 Drogon。
▮▮▮▮⚝ 压缩 (Compression): 对响应内容进行 Gzip 或 Brotli 压缩,减少传输数据量。
▮▮▮▮⚝ 缓存 (Caching): 缓存静态或不经常变化的动态内容,提高响应速度。
▮▮▮▮⚝ 高可用性: 可以配置健康检查,当某个 Drogon 实例失败时,自动将流量切换到健康的实例。

14.3.2 配置 Drogon 配合反向代理

当使用反向代理时,Drogon 本身通常配置为监听本地回环地址 127.0.0.1 或私有 IP 地址,而不是 0.0.0.0,并且监听一个内部端口(例如 8080)。这样外部流量必须经过反向代理才能到达 Drogon。

config.json 示例 (监听本地地址):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "listeners": [
3 {
4 "address": "127.0.0.1",
5 "port": 8080,
6 "protocol": "http"
7 }
8 ],
9 "app": {
10 "thread_num": 16,
11 // ... other settings
12 }
13 // ...
14 }

14.3.3 Nginx 配置示例

以下是一个简单的 Nginx 配置示例,将公网 80 端口的 HTTP 请求转发到 Drogon 在本地 8080 端口监听的服务,并将 443 端口的 HTTPS 请求转发过去(假设 Nginx 处理 SSL):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 server {
2 listen 80;
3 server_name your_domain.com;
4
5 # 将所有HTTP请求重定向到HTTPS (可选,推荐用于生产环境)
6 # return 301 https://$host$request_uri;
7
8 location / {
9 # 转发请求到 Drogon
10 proxy_pass http://127.0.0.1:8080;
11
12 # 传递客户端真实IP和请求头
13 proxy_set_header Host $host;
14 proxy_set_header X-Real-IP $remote_addr;
15 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
16 proxy_set_header X-Forwarded-Proto $scheme;
17
18 # 可选:设置连接超时
19 proxy_connect_timeout 60s;
20 proxy_send_timeout 60s;
21 proxy_read_timeout 60s;
22 }
23
24 # 如果有静态文件,可以让 Nginx 直接处理以提高性能
25 # location /static/ {
26 # alias /path/to/your/app/web/static/; # 对应 Drogon 的 document_root 下的子目录
27 # expires 30d; # 设置浏览器缓存
28 # }
29
30 # 如果使用 WebSocket,需要特殊的 proxy_pass 配置
31 location /ws/ { # 例如 WebSocket 的路径是 /ws/
32 proxy_pass http://127.0.0.1:8080;
33 proxy_http_version 1.1;
34 proxy_set_header Upgrade $http_upgrade;
35 proxy_set_header Connection "Upgrade";
36 proxy_set_header Host $host;
37 proxy_set_header X-Real-IP $remote_addr;
38 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
39 proxy_set_header X-Forwarded-Proto $scheme;
40 }
41
42 # Error pages (可选)
43 # error_page 500 502 503 504 /50x.html;
44 # location = /50x.html {
45 # root /usr/share/nginx/html;
46 # }
47 }
48
49 # HTTPS 配置示例 (需要SSL证书)
50 # server {
51 # listen 443 ssl;
52 # server_name your_domain.com;
53
54 # ssl_certificate /etc/nginx/ssl/your_domain.crt; # 证书文件路径
55 # ssl_certificate_key /etc/nginx/ssl/your_domain.key; # 私钥文件路径
56 # # Add other SSL configurations for security and performance
57
58 # location / {
59 # proxy_pass http://127.0.0.1:8080;
60 # proxy_set_header Host $host;
61 # proxy_set_header X-Real-IP $remote_addr;
62 # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
63 # proxy_set_header X-Forwarded-Proto $scheme;
64 # }
65
66 # # Static files and WebSocket configuration similar to HTTP server block
67 # }

注意配置中的 proxy_set_header,特别是 X-Real-IPX-Forwarded-For,它们用于将客户端的真实 IP 地址传递给 Drogon,以便在应用中获取正确的客户端信息。X-Forwarded-Proto 传递协议(http 或 https)。

14.3.4 Caddy 配置示例

Caddy 是另一个流行的反向代理服务器,以其自动 HTTPS 配置而闻名,配置通常比 Nginx 更简洁。

一个简单的 Caddyfile 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 your_domain.com {
2 # Caddy 自动处理 HTTPS
3
4 # 转发所有请求到 Drogon
5 reverse_proxy 127.0.0.1:8080 {
6 # 自动传递 Host, X-Real-IP, X-Forwarded-For, X-Forwarded-Proto 等头
7 # 这些头对于 Drogon 获取真实客户端信息很重要
8 }
9
10 # 如果有静态文件,可以让 Caddy 直接处理
11 # handle /static/* {
12 # root * /path/to/your/app/web/static/
13 # file_server
14 # }
15
16 # 如果使用 WebSocket
17 # handle /ws/* {
18 # reverse_proxy 127.0.0.1:8080 {
19 # # Caddy handles WebSocket headers automatically
20 # }
21 # }
22
23 # Optional: Logging
24 # log {
25 # output file /path/to/caddy/access.log
26 # }
27
28 # Optional: Error pages
29 # handle_errors {
30 # # ...
31 # }
32 }

Caddy 的配置语法更现代,且默认处理了很多常用的反向代理设置(如传递必要的请求头、WebSocket 升级)。

14.4 性能监控与分析

了解应用的性能状况对于生产环境至关重要。性能监控可以帮助您及时发现问题,性能分析则能定位瓶颈所在。

14.4.1 监控指标

需要关注的关键性能指标(Performance Metrics)包括:

▮▮▮▮⚝ CPU 使用率 (CPU Usage): 应用消耗的 CPU 资源。
▮▮▮▮⚝ 内存使用率 (Memory Usage): 应用占用的内存大小,注意是否存在内存泄漏。
▮▮▮▮⚝ 网络流量 (Network Traffic): 进出服务器的网络流量,判断带宽是否充足。
▮▮▮▮⚝ 每秒请求数 (Requests Per Second, RPS 或 QPS): 服务器处理请求的速度。
▮▮▮▮⚝ 延迟/响应时间 (Latency/Response Time): 从接收请求到发送响应所需的时间。通常关注平均、P95(95%请求的延迟小于此值)和 P99(99%请求的延迟小于此值)延迟。
▮▮▮▮⚝ 错误率 (Error Rate): 客户端或服务器端错误的比例,表明应用的稳定性。
▮▮▮▮⚝ 连接数 (Connection Count): 当前活跃的 TCP 连接数。
▮▮▮▮⚝ 数据库查询性能: 数据库的 CPU、内存、连接数、慢查询等。

14.4.2 监控工具

可以使用多种工具来监控 Drogon 应用和服务器:

▮▮▮▮⚝ 操作系统自带工具: top, htop, vmstat, iostat, netstat 等可以查看基本的 CPU、内存、磁盘 I/O 和网络信息。
▮▮▮▮⚝ Drogon 内置指标: Drogon 提供了一些基本的内部状态和性能计数器,可以在代码中访问(需要查阅文档,可能通过特定 API 或插件)。
▮▮▮▮⚝ 普罗米修斯 (Prometheus) + Grafana: 流行的开源监控方案。可以在 Drogon 中集成 Prometheus 客户端库(需要 C++ 的实现)来暴露自定义指标,然后由 Prometheus 拉取并存储,最后通过 Grafana 进行可视化展示。
▮▮▮▮⚝ 商业 APM (Application Performance Monitoring) 工具: New Relic, Datadog, SkyWalking 等,提供更全面的应用性能洞察,可能需要集成特定的 SDK。
▮▮▮▮⚝ 日志分析工具: ELK Stack (Elasticsearch, Logstash, Kibana) 或 Grafana Loki/Promtail 可以收集、存储和分析 Drogon 的日志,帮助发现错误和异常。

14.4.3 性能分析 (Profiling)

当发现性能瓶颈时,需要进行更深入的分析。

① CPU 分析器 (CPU Profiler)

用于查找代码中哪些函数或代码段消耗了最多的 CPU 时间。

▮▮▮▮⚝ gprof: GNU Profiler,需要在编译时加入 -pg 选项,运行程序生成数据文件,然后使用 gprof 命令分析。
▮▮▮▮⚝ perf: Linux 原生的性能分析工具,功能强大,可以分析 CPU 事件、调用栈等。
▮▮▮▮⚝ Valgrind (Callgrind): 一个内存调试和性能分析框架,Callgrind 工具可以进行函数调用图和消耗分析。用于开发和测试阶段,生产环境开销较大。
▮▮▮▮⚝ Google Perftools: 一个高性能的 CPU/Heap Profiler,对性能影响较小,适合生产环境使用。需要在编译时链接库,运行时设置环境变量。

② 内存分析器 (Memory Profiler)

用于检测内存泄漏和分析内存使用模式。

▮▮▮▮⚝ Valgrind (Memcheck): 最常用的 C++ 内存泄漏检测工具。主要用于开发和测试阶段。
▮▮▮▮⚝ Google Perftools (Heap Profiler): 用于分析堆内存分配情况。
▮▮▮▮⚝ AddressSanitizer (ASan): Clang/GCC 提供的动态内存错误检测工具,能检测内存访问越界、Use-after-Free 等问题。用于开发和测试阶段。

③ 网络分析器

▮▮▮▮⚝ tcpdump/Wireshark: 用于抓取和分析网络包,理解请求和响应的交互过程。
▮▮▮▮⚝ curl/Postman: 用于手动发送请求并查看详细的响应信息和时间。

结合监控数据和分析工具,可以系统地定位和解决性能问题。

14.5 性能调优技巧

针对 Drogon 应用的性能瓶颈,可以从多个层面进行调优。

14.5.1 调整线程池大小

Drogon 的 app.thread_num 配置决定了处理 HTTP 请求的工作线程数量。

▮▮▮▮⚝ 原则: 对于 I/O 密集型应用(如大量数据库操作、文件 I/O、网络请求),线程数可以设置为 CPU 核心数的 2-4 倍甚至更高,以便在某些线程等待 I/O 时,其他线程可以继续处理请求。对于 CPU 密集型应用(如复杂计算),线程数通常设置为 CPU 核心数或核心数 + 1,避免过多的上下文切换。
▮▮▮▮⚝ 实际: Drogon 应用通常是 I/O 密集型的,特别是数据库和外部服务调用。结合异步编程(Fiber/Callback)和合理的线程数,可以最大化吞吐量。通过负载测试,逐步调整 thread_num 并观察 RPS 和延迟变化,找到最优值。

14.5.2 优化数据库访问

数据库操作往往是 Web 应用的性能瓶颈。

▮▮▮▮⚝ 使用数据库连接池 (Database Connection Pool): Drogon 的数据库模块内置连接池。在 config.json 中配置连接池大小(connection_number)、连接最大空闲时间(idle_timeout)等参数。合理设置连接数可以减少连接建立的开销。
▮▮▮▮⚝ 异步数据库操作: 利用 Drogon 提供的异步数据库 API。避免在处理请求的线程中进行阻塞的同步数据库调用。结合 Fiber 可以让异步代码写起来像同步一样简洁高效。
▮▮▮▮⚝ 优化 SQL 查询: 编写高效的 SQL 语句,创建适当的索引。使用数据库的慢查询日志来发现需要优化的查询。
▮▮▮▮⚝ 批量操作: 对于需要插入、更新或删除多条记录的场景,尽量使用批量操作(Batch Operations),而不是单条循环操作,这能显著减少数据库交互次数。
▮▮▮▮⚝ 缓存: 缓存频繁访问但不经常变化的数据,例如使用 Redis 或 Memcached。

14.5.3 优化代码逻辑

代码本身的效率是基础。

▮▮▮▮⚝ 减少不必要的计算和数据拷贝: 避免在请求处理的关键路径上执行耗时的同步计算。使用 C++ 的 Move Semantics 减少拷贝。
▮▮▮▮⚝ 异步化阻塞操作: 将文件读写、外部 API 调用等潜在的阻塞操作改为异步方式,或者放入独立的线程池中处理,不要阻塞 Drogon 的事件循环线程。
▮▮▮▮⚝ 使用高效的数据结构和算法: 选择适合场景的数据结构(如 std::unordered_map 替代 std::map 进行快速查找)和算法。
▮▮▮▮⚝ 内存管理: C++ 需要手动或依赖智能指针管理内存。关注内存分析器的报告,防止内存泄漏和过度分配。
▮▮▮▮⚝ 编译优化: 使用 Release 模式编译,并启用编译器优化标志(如 -O2, -O3, -Os)。

14.5.4 静态资源与缓存

▮▮▮▮⚝ 由反向代理服务静态文件: 如前所述,让 Nginx 或 Caddy 等反向代理直接服务静态文件(CSS, JS, 图片等),并设置合适的缓存头(Expires, Cache-Control),可以极大地减轻 Drogon 的负担并加快客户端加载速度。
▮▮▮▮⚝ 浏览器缓存: 在响应头中设置 Cache-ControlExpires 字段,指示浏览器缓存静态资源。
▮▮▮▮⚝ 服务器端缓存: 对于动态生成但变化不频繁的内容,可以在服务器端进行缓存,例如使用内存缓存或 Redis。

14.5.5 其他优化点

▮▮▮▮⚝ 压缩响应: 配置反向代理或 Drogon 本身对 HTTP 响应进行 Gzip 或 Brotli 压缩。
▮▮▮▮⚝ 连接超时: 合理设置 Drogon 的 idle_connection_timeout 和反向代理的连接/读写超时时间,避免长时间占用资源或过早断开连接。
▮▮▮▮⚝ 负载测试: 使用工具(如 ApacheBench, wrk, hey)对应用进行负载测试,模拟并发用户访问,发现性能瓶颈并验证优化效果。
▮▮▮▮⚝ 持续集成/持续部署 (CI/CD): 集成性能测试到 CI/CD 流程中,及时发现由于代码变更导致的性能回归。

通过以上部署策略、配置调优和性能优化技巧,您可以确保您的 Drogon 应用在生产环境中稳定、高效地运行,为用户提供卓越的体验。

15. 扩展 Drogon:插件与自定义组件

本章将带领读者深入了解 Drogon 框架的扩展机制,学习如何通过插件(Plugin)和自定义服务(Custom Service)来增强应用的功能和可维护性。Drogon 的设计考虑到了灵活性,允许开发者在不修改框架核心代码的情况下,集成自定义逻辑、连接外部系统或提供可复用的功能模块。掌握这些扩展方式,对于构建复杂、大型的 Drogon 应用至关重要。

15.1 Drogon 插件系统

插件(Plugin)是 Drogon 提供的一种重要的扩展机制,它允许开发者注册自定义的功能模块,这些模块可以在应用启动时初始化,并在整个应用生命周期内运行,处理特定的任务或响应框架的事件。

插件系统的核心思想是将某些横切关注点(Cross-cutting Concerns)或可插拔的功能从核心应用逻辑中分离出来,以提高代码的模块化和可复用性。常见的插件应用场景包括:

⚝ 初始化第三方库或连接池。
⚝ 实现定时任务或后台服务。
⚝ 收集应用性能指标。
⚝ 集成消息队列或其他外部服务。
⚝ 在应用启动或关闭时执行特定操作。

一个 Drogon 插件本质上是一个实现了特定接口的 C++ 类。框架在启动时会根据配置文件加载并管理这些插件实例。

要创建一个 Drogon 插件,通常需要继承自 drogon::Plugin<Impl, ConfigType> 类模板。这个模板有两个参数:
Impl: 插件的具体实现类。
ConfigType: 插件的配置类型,通常是一个结构体或类,用于接收配置文件中的参数。如果插件不需要配置,可以使用 void

插件类需要实现几个关键的生命周期方法:

init(const Json::Value& config):
▮▮▮▮在应用初始化阶段调用。此时,应用的配置已经被加载,但事件循环尚未启动。这个方法通常用于读取插件配置、进行初步设置或检查依赖项。参数 config 包含了该插件在配置文件中的具体配置信息。
startup():
▮▮▮▮在应用的主要服务(如 HTTP 服务器、数据库连接)启动之后,事件循环开始运行之前调用。这是执行那些需要依赖于已启动服务的初始化任务的好时机,例如连接到数据库或外部服务。
shutdown():
▮▮▮▮在应用即将关闭时调用。这是执行清理任务的好时机,例如关闭连接、释放资源等。

通过实现这些方法,插件可以在 Drogon 应用的不同阶段介入,完成特定的任务。

15.2 实现自定义插件

现在,我们来演示如何编写一个简单的自定义 Drogon 插件。这个插件的功能是在应用启动时打印一条自定义的消息,并在应用关闭时打印另一条消息。

假设我们的插件名为 MyStartupPlugin,它不需要任何配置。

首先,我们需要定义插件类。我们继承自 drogon::Plugin<MyStartupPlugin, void>

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/plugins/Plugin.h>
2 #include <drogon/drogon.h>
3
4 class MyStartupPlugin : public drogon::Plugin<MyStartupPlugin, void>
5 {
6 public:
7 // 必须实现 init 方法
8 virtual void init(const Json::Value& config) override
9 {
10 // 在应用初始化阶段执行的逻辑
11 LOG_INFO << "MyStartupPlugin init called!";
12 }
13
14 // 必须实现 startup 方法
15 virtual void startup() override
16 {
17 // 在应用启动阶段执行的逻辑
18 LOG_INFO << "MyStartupPlugin startup called!";
19 // 可以在这里执行一些需要依赖于框架已启动服务的操作
20 }
21
22 // 必须实现 shutdown 方法
23 virtual void shutdown() override
24 {
25 // 在应用关闭阶段执行的逻辑
26 LOG_INFO << "MyStartupPlugin shutdown called!";
27 // 在这里进行资源清理
28 }
29 };

在这个例子中,我们使用了 Drogon 内置的日志系统(通过 LOG_INFO 宏)来打印消息。

如果插件需要配置,我们可以定义一个结构体作为 ConfigType

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/plugins/Plugin.h>
2 #include <drogon/drogon.h>
3 #include <json/json.h> // 需要包含 jsoncpp 头文件来处理 Json::Value
4
5 struct MyConfigPluginConfig
6 {
7 std::string message;
8 int repeat_count = 1;
9 };
10
11 class MyConfigPlugin : public drogon::Plugin<MyConfigPlugin, MyConfigPluginConfig>
12 {
13 public:
14 // init 方法会接收配置信息
15 virtual void init(const Json::Value& config) override
16 {
17 // 解析 config 中的数据到 MyConfigPluginConfig 结构体
18 // 注意:需要手动解析 Json::Value
19 if (config.isObject())
20 {
21 if (config.isMember("message"))
22 {
23 pluginConfig_.message = config["message"].asString();
24 }
25 if (config.isMember("repeat_count"))
26 {
27 pluginConfig_.repeat_count = config["repeat_count"].asInt();
28 }
29 }
30 LOG_INFO << "MyConfigPlugin init called with message: " << pluginConfig_.message << " and repeat count: " << pluginConfig_.repeat_count;
31 }
32
33 virtual void startup() override
34 {
35 for (int i = 0; i < pluginConfig_.repeat_count; ++i)
36 {
37 LOG_INFO << "MyConfigPlugin startup: " << pluginConfig_.message;
38 }
39 }
40
41 virtual void shutdown() override
42 {
43 LOG_INFO << "MyConfigPlugin shutdown called!";
44 }
45
46 private:
47 MyConfigPluginConfig pluginConfig_; // 存储配置信息
48 };

重要的注意事项:

⚝ 插件类通常应该定义在 .h.hpp 文件中,并在需要使用它的源文件中包含。
⚝ 在 init 方法中解析 Json::Value 到自定义配置结构体需要手动完成。Drogon 提供了一些辅助宏(如 JSON_PARSE_TO_MEMBER)来简化这个过程,具体可以查阅 Drogon 的官方文档或示例。
⚝ 插件类的生命周期由 Drogon 框架管理,开发者不需要手动创建或删除插件实例。
⚝ 插件可以在 startup 方法中访问 Drogon 应用的核心对象,例如通过 drogon::app() 获取应用实例,进而访问数据库客户端、HTTP 客户端等。但在 init 方法中,这些服务可能尚未完全初始化。

编写完插件类后,还需要在编译时将其包含到项目中,并修改配置文件以注册该插件。

15.3 注册与使用插件

编写好的插件需要在 Drogon 应用的配置文件(通常是 config.json)中进行注册,Drogon 框架在启动时会读取配置文件并加载指定的插件。

config.json 文件中有一个专门用于配置插件的字段,通常是 "plugins"。它是一个 JSON 数组,数组中的每个元素代表一个插件的配置。每个插件配置需要指定插件类的名称 ("name"),还可以包含该插件特有的配置参数。

以下是一个 config.json 文件的示例,演示如何注册上面创建的 MyStartupPluginMyConfigPlugin

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "listeners": [
3 {
4 "address": "0.0.0.0",
5 "port": 8848
6 }
7 ],
8 "plugins": [
9 {
10 "name": "MyStartupPlugin",
11 "config": {} // MyStartupPlugin 不需要配置,但必须有 config 字段
12 },
13 {
14 "name": "MyConfigPlugin",
15 "config": {
16 "message": "Hello from Config Plugin!",
17 "repeat_count": 3
18 }
19 }
20 ],
21 "app": {
22 "thread_num": 8,
23 "enable_session": true
24 }
25 // ... 其他配置 ...
26 }

注册步骤:

将插件源文件添加到项目构建中: 确保你的插件类文件被编译并链接到最终的可执行文件中。如果你使用 CMake,需要修改 CMakeLists.txt 文件,将插件的源文件添加到编译列表。
config.json 中添加插件配置: 编辑 config.json 文件,在 "plugins" 数组中添加一个新的 JSON 对象,指定插件的 "name"(即插件类的名称)和 "config"(一个包含该插件特定配置参数的 JSON 对象)。如果插件不需要参数,"config" 字段可以是一个空的 JSON 对象 {}

当 Drogon 应用启动时,它会读取 config.json 文件,解析 "plugins" 字段,然后根据 "name" 查找对应的插件类,创建实例,并调用其 initstartup 方法,同时将 "config" 字段的内容传递给 init 方法。

注意事项:

⚝ 插件的名称("name")必须与插件类的实际名称完全匹配。
⚝ 插件的配置参数是在 config 字段中传递给插件的 init 方法,格式是 Json::Value。插件需要自己解析这个 Json::Value 来获取具体的配置值。
⚝ 插件的加载和初始化顺序可能很重要,但 Drogon 默认并没有提供明确的顺序控制机制。如果插件之间存在依赖关系,可能需要在插件内部处理等待依赖就绪的逻辑,或者设计插件时避免强依赖启动顺序。

通过这种方式,你可以轻松地为 Drogon 应用添加各种功能,而无需修改应用的核心业务逻辑代码,保持代码的清晰和模块化。

15.4 自定义服务与组件

除了插件系统,开发者在构建大型 Drogon 应用时,还会创建许多用于封装特定业务逻辑或功能的自定义类。这些类可以被视为应用的服务(Service)或组件(Component)。它们通常不像插件那样直接参与框架的生命周期事件,而是负责处理具体的业务操作,例如用户管理、订单处理、数据分析等。

在传统的面向对象设计中,这些服务和组件之间的依赖关系通常通过直接创建对象或通过构造函数/Setter 方法进行依赖注入(Dependency Injection)。Drogon 虽然没有提供一个全功能的依赖注入框架(如 Spring 或 Guice),但它提供了一些机制来方便地获取和管理这些自定义组件。

创建自定义服务/组件类:

自定义服务就是普通的 C++ 类。它们可以包含成员变量和成员函数来封装状态和行为。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <string>
2 #include <iostream>
3
4 // 定义一个简单的用户服务类
5 class UserService
6 {
7 public:
8 UserService()
9 {
10 std::cout << "UserService created." << std::endl;
11 }
12
13 std::string getUserName(int user_id) const
14 {
15 // 模拟从数据库或缓存获取用户名
16 if (user_id == 1)
17 {
18 return "Alice";
19 }
20 else if (user_id == 2)
21 {
22 return "Bob";
23 }
24 else
25 {
26 return "Unknown";
27 }
28 }
29
30 // 可能依赖于其他服务,例如数据库客户端
31 // void setDbClient(...) { ... }
32 };

在 Drogon 应用中使用自定义服务:

自定义服务通常在控制器(Controller)、其他服务或插件中被使用。获取服务实例有几种常见方式:

手动创建实例: 在需要使用服务的地方直接创建其对象。这适用于服务没有复杂依赖或状态简单的情况,但不符合依赖注入的理念。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 在一个控制器方法中
2 void MyController::getInfo(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback)
3 {
4 UserService userService; // 直接创建实例
5 std::string name = userService.getUserName(1);
6
7 auto resp = HttpResponse::newHttpResponse();
8 resp->setBody("User name: " + name);
9 callback(resp);
10 }

单例模式: 如果服务需要全局唯一或管理共享资源,可以将其设计为单例。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // UserService.h
2 class UserService
3 {
4 public:
5 static UserService& instance()
6 {
7 static UserService instance;
8 return instance;
9 }
10
11 // ... getUserName 等方法 ...
12
13 private:
14 UserService() = default; // 构造函数私有化
15 UserService(const UserService&) = delete;
16 UserService& operator=(const UserService&) = delete;
17 };
18
19 // 在一个控制器方法中
20 void MyController::getInfo(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback)
21 {
22 std::string name = UserService::instance().getUserName(1); // 通过单例获取
23
24 auto resp = HttpResponse::newHttpResponse();
25 resp->setBody("User name: " + name);
26 callback(resp);
27 }

单例模式是 C++ 中管理全局唯一实例的常用方式,但可能会使测试变得复杂。

Drogon 的应用对象获取: Drogon 应用实例 drogon::app() 提供了一些方法来获取已注册的组件,例如数据库客户端、HTTP 客户端等。对于自定义组件,Drogon 并没有一个内置的、通用的容器来自动管理它们的生命周期和依赖注入。然而,可以通过一些变通的方式实现类似的效果。

一种常见模式是,如果你的自定义服务依赖于 Drogon 提供的核心组件(如数据库客户端 drogon::orm::DbClient),你可以在服务类的构造函数或初始化方法中通过 drogon::app().getDbClient() 等方法获取这些依赖。

另一种方式是,将自定义服务也作为插件注册。虽然它们的角色更像“服务”而非“插件”,但利用插件的初始化机制,可以在 startup 方法中初始化服务,并将其指针存储在某个全局可访问的地方(例如单例管理器或 drogon::app().getCustomAppData() 中),然后在其他地方获取。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 示例:将 UserService 作为插件进行管理
2 // UserServicePlugin.h
3 #include <drogon/plugins/Plugin.h>
4 #include <drogon/drogon.h>
5 #include "UserService.h" // 包含 UserService 定义
6
7 class UserServicePlugin : public drogon::Plugin<UserServicePlugin, void>
8 {
9 public:
10 virtual void init(const Json::Value& config) override
11 {
12 // 初始化服务实例
13 userService_ = std::make_unique<UserService>();
14 LOG_INFO << "UserServicePlugin init: UserService instance created.";
15 }
16
17 virtual void startup() override
18 {
19 // 可以将服务指针存储到全局可访问的地方,例如 custom app data
20 drogon::app().setCustomAppData(userService_.get()); // 注意:这里存储的是裸指针,需要确保插件生命周期长于所有使用者
21
22 LOG_INFO << "UserServicePlugin startup: UserService pointer stored in custom app data.";
23 }
24
25 virtual void shutdown() override
26 {
27 userService_.reset(); // 销毁服务实例
28 drogon::app().setCustomAppData(nullptr); // 清理
29 LOG_INFO << "UserServicePlugin shutdown: UserService instance destroyed.";
30 }
31
32 // 提供一个方法方便获取服务实例,或者直接通过 drogon::app().getCustomAppData() 获取
33 UserService* getUserService() const { return userService_.get(); }
34
35 private:
36 std::unique_ptr<UserService> userService_;
37 };
38
39 // 在 config.json 中注册 UserServicePlugin
40 // {
41 // "plugins": [
42 // { "name": "UserServicePlugin", "config": {} }
43 // ]
44 // // ...
45 // }
46
47 // 在控制器中获取并使用 UserService
48 // 需要知道如何将 UserServicePlugin 实例存储到 custom app data 中,然后如何获取
49 // 或者,更好的方式是 ServiceProvider 模式或简单的单例容器
50
51 // 更推荐的方式:使用一个简单的服务提供者或注册表
52 #include <map>
53 #include <string>
54 #include <memory>
55 #include <stdexcept>
56
57 // 简单的服务注册表
58 class ServiceProvider
59 {
60 public:
61 template<typename T, typename... Args>
62 void registerService(const std::string& name, Args&&... args)
63 {
64 services_[name] = std::make_shared<T>(std::forward<Args>(args)...);
65 }
66
67 template<typename T>
68 std::shared_ptr<T> getService(const std::string& name) const
69 {
70 auto it = services_.find(name);
71 if (it == services_.end())
72 {
73 throw std::runtime_error("Service not found: " + name);
74 }
75 // 尝试转换为目标类型,如果失败会返回空的 shared_ptr
76 return std::dynamic_pointer_cast<T>(it->second);
77 }
78
79 static ServiceProvider& instance()
80 {
81 static ServiceProvider provider;
82 return provider;
83 }
84
85 private:
86 std::map<std::string, std::shared_ptr<void>> services_; // 使用 void 指针存储不同类型的 shared_ptr
87 ServiceProvider() = default;
88 ServiceProvider(const ServiceProvider&) = delete;
89 ServiceProvider& operator=(const ServiceProvider&) = delete;
90 };
91
92 // 在应用启动阶段(例如 main 函数中,或者一个特殊的启动插件中)注册服务
93 // ServiceProvider::instance().registerService<UserService>("userService");
94 // ServiceProvider::instance().registerService<AnotherService>("anotherService", dependency1, dependency2);
95
96 // 在控制器中使用服务
97 // #include "ServiceProvider.h"
98 // void MyController::getInfo(...) {
99 // auto userService = ServiceProvider::instance().getService<UserService>("userService");
100 // if (userService) {
101 // std::string name = userService->getUserName(1);
102 // // ... use name ...
103 // } else {
104 // // handle error
105 // }
106 // }

这种服务提供者(Service Provider)或服务注册表(Service Registry)模式是更灵活的方式,开发者可以自己实现一个简单的容器来管理自定义服务的生命周期和获取方式,并在应用初始化阶段完成服务的创建和注册。

总结:

⚝ 自定义服务和组件是组织业务逻辑的核心。
⚝ 在 Drogon 中,它们是普通的 C++ 类。
⚝ 获取服务实例可以通过手动创建、单例模式或自定义的服务提供者/注册表实现。
⚝ 如果服务需要依赖 Drogon 的内置组件,可以在初始化时通过 drogon::app() 获取。
⚝ 将服务注册为插件可以利用 Drogon 的配置和生命周期管理机制,但这可能不是服务本身的“核心”功能,只是为了方便管理。更推荐的是自己实现一个轻量级的服务管理机制。

选择哪种方式取决于项目的规模、复杂性以及你对依赖管理的需求。对于小型项目,简单的单例可能足够;对于大型项目,一个自定义的服务提供者模式会更有利于代码的组织和测试。

无论是插件还是自定义服务,它们都是扩展 Drogon 应用功能的重要手段。通过合理地使用这些机制,可以构建出结构清晰、易于维护和扩展的高性能 Web 应用。

16. 实战案例分析

欢迎来到本书的实战案例分析章节!🥳 在前面的章节中,我们已经系统地学习了 Drogon 框架的各项基础和核心技术。从环境搭建到异步编程,从路由控制到数据库操作,再到中间件和视图渲染,我们构建了扎实的理论基础。然而,纸上得来终觉浅,绝知此事要躬行。本章的目的是通过几个具体的、具有代表性的实战案例,将前面学到的知识融会贯通,演示如何利用 Drogon 框架构建功能完备、性能优越的实际应用。

本章精选了三个不同类型的案例:一个典型的 RESTful API 后端应用(博客 API),一个实时交互应用(聊天室),以及一个处理文件操作的应用(文件分享服务)。这三个案例涵盖了 Web 应用开发中的常见场景和关键技术,通过它们,您将看到 Drogon 如何优雅高效地解决这些问题,并进一步提升您使用 Drogon 进行开发的实际能力。

让我们通过具体的代码和详细的讲解,一起探索 Drogon 的强大之处吧!💪

16.1 案例一:构建一个简单的博客 API

构建 RESTful API 是 Web 后端开发中最常见的任务之一。本案例将演示如何使用 Drogon 创建一个简单的博客后端 API,支持博文的创建、读取、更新和删除(CRUD)操作。我们将重点展示 Drogon 的路由(Routing)、基于类的控制器(Class-based Controller)、ORM(Object-Relational Mapping)以及异步数据库访问的用法。

这个博客 API 将提供以下端点(Endpoint):

GET /posts:获取所有博文列表
GET /posts/{id}:获取单篇博文详情
POST /posts:创建新博文
PUT /posts/{id}:更新指定博文
DELETE /posts/{id}:删除指定博文

16.1.1 项目初始化与数据库准备

首先,使用 Drogon 命令行工具创建一个新的项目:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogons create project MyBlogApi
2 cd MyBlogApi

本项目我们将使用 SQLite 数据库,因为它轻量且易于配置,非常适合演示。在项目根目录下创建一个名为 blog.sqlite3 的文件。然后,我们需要定义博文的数据库表结构。创建一个简单的 posts 表:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 CREATE TABLE posts (
2 id INTEGER PRIMARY KEY AUTOINCREMENT,
3 title TEXT NOT NULL,
4 content TEXT,
5 created_at INTEGER NOT NULL,
6 updated_at INTEGER
7 );

这个表包含 id(主键,自增)、title(标题)、content(内容)、created_at(创建时间)和 updated_at(更新时间)字段。时间字段可以使用 Unix 时间戳存储,类型为 INTEGER

接下来,配置 Drogon 连接到 SQLite 数据库。编辑 config.json 文件,找到 databases 部分,添加如下配置:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 // ... other configurations
3 "databases":[
4 {
5 "db_type":"sqlite3",
6 "client_name":"default",
7 "filename":"blog.sqlite3",
8 "auto_commit":true
9 }
10 ],
11 // ... other configurations
12 }

这里我们将数据库类型设置为 sqlite3,客户端名称为 default(方便后续引用),filename 指向我们的数据库文件 blog.sqlite3

16.1.2 生成 ORM 模型

利用 Drogon 强大的 ORM 工具,我们可以根据数据库表自动生成对应的 C++ 模型类。在项目根目录运行以下命令:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogons create model

工具会扫描配置的数据库,找到 posts 表,并生成 Post.hPost.cc 文件到 models 目录下。这些文件包含了对 posts 表进行 CRUD 操作所需的方法。

16.1.3 创建控制器

我们需要一个控制器来处理针对 /posts 路径的各种 HTTP 请求。使用 drogons 工具创建 PostController

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogons create controller PostController

这会在 controllers 目录下生成 PostController.hPostController.cc 文件。编辑 PostController.h,使其继承自 drogon::HttpController<PostController>,并使用 METHOD_LIST_BEGINMETHOD_LIST_END 宏定义我们要处理的路由和方法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // controllers/PostController.h
2 #pragma once
3
4 #include <drogon/HttpController.h>
5 #include "../models/Post.h" // Include the generated model
6
7 using namespace drogon;
8
9 class PostController : public drogon::HttpController<PostController>
10 {
11 public:
12 METHOD_LIST_BEGIN
13 // Use METHOD_ADD to add your endpoints here...
14 METHOD_ADD("/posts", Get, &PostController::getPosts); // GET /posts
15 METHOD_ADD("/posts/{id}", Get, &PostController::getPostById); // GET /posts/{id}
16 METHOD_ADD("/posts", Post, &PostController::createPost); // POST /posts
17 METHOD_ADD("/posts/{id}", Put, &PostController::updatePost); // PUT /posts/{id}
18 METHOD_ADD("/posts/{id}", Delete, &PostController::deletePost); // DELETE /posts/{id}
19 METHOD_LIST_END
20
21 void getPosts(const HttpRequestPtr& req,
22 std::function<void(const HttpResponsePtr&)>&& callback);
23 void getPostById(const HttpRequestPtr& req,
24 std::function<void(const HttpResponsePtr&)>&& callback,
25 long long id); // id will be bound from URL parameter
26 void createPost(const HttpRequestPtr& req,
27 std::function<void(const HttpResponsePtr&)>&& callback);
28 void updatePost(const HttpRequestPtr& req,
29 std::function<void(const HttpResponsePtr&)>&& callback,
30 long long id);
31 void deletePost(const HttpRequestPtr& req,
32 std::function<void(const HttpResponsePtr&)>&& callback,
33 long long id);
34 };

这里,我们为每个 API 端点定义了一个对应的处理函数,并在 METHOD_LIST_BEGIN/METHOD_LIST_END 块中将 HTTP 方法和路径映射到这些函数。注意,对于带有路径参数 {id} 的路由,Drogon 会自动尝试将 id 部分的值绑定到处理函数的同名参数上(这里是 long long id)。

16.1.4 实现控制器方法

现在,我们来实现 PostController.cc 中的各个处理函数,利用 ORM 进行数据库操作。

首先,获取数据库客户端实例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // controllers/PostController.cc
2 #include "PostController.h"
3 #include <drogon/orm/Mapper.h>
4 #include <drogon/drogon.h>
5 #include <json/json.h> // For JSON parsing/building
6
7 using namespace drogon;
8 using namespace drogon::orm;
9 using namespace blogapi::orm; // Namespace for generated ORM models
10
11 // Get the default database client
12 static auto dbClient = drogon::app().getDbClient();

① 获取所有博文 (GET /posts)

这个方法需要查询 posts 表中的所有记录,并返回一个 JSON 数组。使用 ORM 的 Mapper 类进行查询。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // controllers/PostController.cc
2 void PostController::getPosts(const HttpRequestPtr& req,
3 std::function<void(const HttpResponsePtr&)>&& callback)
4 {
5 // Use async_run in C++20 fiber style
6 drogon::async_run([=]() {
7 Mapper<Post> mp(dbClient);
8 try {
9 // Fetch all posts
10 auto posts = mp.findAll();
11 Json::Value jsonArray(Json::arrayValue);
12 for (const auto& post : posts)
13 {
14 Json::Value postJson;
15 postJson["id"] = post.getValueOfId();
16 postJson["title"] = post.getValueOfTitle();
17 postJson["content"] = post.getValueOfContent();
18 postJson["created_at"] = (Json::Int64)post.getValueOfCreatedAt();
19 if (post.getUpdatedAt()) // Check if updated_at is not null
20 {
21 postJson["updated_at"] = (Json::Int64)post.getValueOfUpdatedAt();
22 } else {
23 postJson["updated_at"] = Json::nullValue;
24 }
25 jsonArray.append(postJson);
26 }
27
28 auto resp = HttpResponse::newHttpJsonResponse(jsonArray);
29 callback(resp);
30
31 } catch (const DrogonDbException& e) {
32 LOG_ERROR << "Database error: " << e.what();
33 auto resp = HttpResponse::newHttpResponse(k500InternalServerError);
34 resp->setBody("Database error");
35 callback(resp);
36 }
37 });
38 }

这里我们使用了 drogon::async_run 结合 C++20 Fiber(纤程)来执行异步数据库操作。在 Fiber 中,异步操作(如 mp.findAll())看起来就像同步调用一样,极大地简化了异步代码的编写。查询结果是一个 std::vector<Post>,我们遍历它并构建一个 JSON 数组作为响应体。注意对 updated_at 的空值(NULL)处理。

② 获取单篇博文 (GET /posts/{id})

根据路径参数 id 查询指定的博文。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // controllers/PostController.cc
2 void PostController::getPostById(const HttpRequestPtr& req,
3 std::function<void(const HttpResponsePtr&)>&& callback,
4 long long id)
5 {
6 drogon::async_run([=]() {
7 Mapper<Post> mp(dbClient);
8 try {
9 // Find post by primary key (id)
10 auto post = mp.findByPrimaryKey(id);
11
12 Json::Value postJson;
13 postJson["id"] = post.getValueOfId();
14 postJson["title"] = post.getValueOfTitle();
15 postJson["content"] = post.getValueOfContent();
16 postJson["created_at"] = (Json::Int64)post.getValueOfCreatedAt();
17 if (post.getUpdatedAt())
18 {
19 postJson["updated_at"] = (Json::Int64)post.getValueOfUpdatedAt();
20 } else {
21 postJson["updated_at"] = Json::nullValue;
22 }
23
24 auto resp = HttpResponse::newHttpJsonResponse(postJson);
25 callback(resp);
26
27 } catch (const DrogonDbException& e) {
28 // Catch exception if record not found or other DB error
29 if (e.errCode() == DrogonDbException::kNotFound) {
30 auto resp = HttpResponse::newHttpResponse(k404NotFound);
31 resp->setBody("Post not found");
32 callback(resp);
33 } else {
34 LOG_ERROR << "Database error: " << e.what();
35 auto resp = HttpResponse::newHttpResponse(k500InternalServerError);
36 resp->setBody("Database error");
37 callback(resp);
38 }
39 }
40 });
41 }

这里使用 mp.findByPrimaryKey(id) 方法查询,如果找不到记录,会抛出 DrogonDbException 异常,错误码为 kNotFound,我们捕获这个异常并返回 404 状态码。

③ 创建新博文 (POST /posts)

接收包含博文标题和内容的 JSON 请求体,创建新的博文记录。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // controllers/PostController.cc
2 void PostController::createPost(const HttpRequestPtr& req,
3 std::function<void(const HttpResponsePtr&)>&& callback)
4 {
5 drogon::async_run([=]() {
6 Mapper<Post> mp(dbClient);
7 try {
8 // Parse JSON request body
9 auto json = req->getJson();
10 if (!json || !json->isObject() || !json->isMember("title"))
11 {
12 auto resp = HttpResponse::newHttpResponse(k400BadRequest);
13 resp->setBody("Invalid JSON or missing title");
14 callback(resp);
15 return;
16 }
17
18 std::string title = (*json)["title"].asString();
19 std::string content = (*json)["content"].asString();
20
21 // Create a new Post object
22 Post newPost;
23 newPost.setTitle(title);
24 newPost.setContent(content);
25 newPost.setCreatedAt(static_cast<long long>(time(0))); // Set creation timestamp
26
27 // Insert into database
28 mp.insert(newPost);
29
30 // Respond with the created post (including generated ID)
31 Json::Value postJson;
32 postJson["id"] = newPost.getValueOfId(); // Get the ID after insertion
33 postJson["title"] = newPost.getValueOfTitle();
34 postJson["content"] = newPost.getValueOfContent();
35 postJson["created_at"] = (Json::Int64)newPost.getValueOfCreatedAt();
36 // updated_at is not set on creation
37
38 auto resp = HttpResponse::newHttpJsonResponse(postJson);
39 resp->setStatusCode(k201Created); // 201 Created status code
40 callback(resp);
41
42 } catch (const DrogonDbException& e) {
43 LOG_ERROR << "Database error: " << e.what();
44 auto resp = HttpResponse::newHttpResponse(k500InternalServerError);
45 resp->setBody("Database error");
46 callback(resp);
47 }
48 });
49 }

这里我们从 HttpRequestPtr 中获取 JSON 请求体,解析出 titlecontent。创建一个 Post 对象,设置字段值,然后使用 mp.insert(newPost) 将其插入数据库。插入后,ORM 会自动更新 newPost 对象的 id 字段,我们可以获取并返回。响应状态码设置为 201 Created。

④ 更新指定博文 (PUT /posts/{id})

根据路径参数 id 和请求体中的 JSON 数据更新博文。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // controllers/PostController.cc
2 void PostController::updatePost(const HttpRequestPtr& req,
3 std::function<void(const HttpResponsePtr&)>&& callback,
4 long long id)
5 {
6 drogon::async_run([=]() {
7 Mapper<Post> mp(dbClient);
8 try {
9 // Find the existing post first
10 auto post = mp.findByPrimaryKey(id);
11
12 // Parse JSON request body
13 auto json = req->getJson();
14 if (!json || !json->isObject())
15 {
16 auto resp = HttpResponse::newHttpResponse(k400BadRequest);
17 resp->setBody("Invalid JSON");
18 callback(resp);
19 return;
20 }
21
22 // Update fields if present in JSON
23 if (json->isMember("title") && (*json)["title"].isString())
24 {
25 post.setTitle((*json)["title"].asString());
26 }
27 if (json->isMember("content") && (*json)["content"].isString())
28 {
29 post.setContent((*json)["content"].asString());
30 }
31 post.setUpdatedAt(static_cast<long long>(time(0))); // Set update timestamp
32
33 // Save changes to database
34 mp.update(post);
35
36 // Respond with the updated post
37 Json::Value postJson;
38 postJson["id"] = post.getValueOfId();
39 postJson["title"] = post.getValueOfTitle();
40 postJson["content"] = post.getValueOfContent();
41 postJson["created_at"] = (Json::Int64)post.getValueOfCreatedAt();
42 postJson["updated_at"] = (Json::Int64)post.getValueOfUpdatedAt();
43
44 auto resp = HttpResponse::newHttpJsonResponse(postJson);
45 callback(resp);
46
47 } catch (const DrogonDbException& e) {
48 if (e.errCode() == DrogonDbException::kNotFound) {
49 auto resp = HttpResponse::newHttpResponse(k404NotFound);
50 resp->setBody("Post not found");
51 callback(resp);
52 } else {
53 LOG_ERROR << "Database error: " << e.what();
54 auto resp = HttpResponse::newHttpResponse(k500InternalServerError);
55 resp->setBody("Database error");
56 callback(resp);
57 }
58 }
59 });
60 }

更新操作首先需要通过 ID 查询到现有记录,然后根据请求体中的 JSON 数据更新 Post 对象的字段。这里我们检查 JSON 字段是否存在且类型正确才进行更新,以便支持部分更新。更新 updated_at 字段,然后使用 mp.update(post) 保存更改。

⑤ 删除指定博文 (DELETE /posts/{id})

根据路径参数 id 删除博文。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // controllers/PostController.cc
2 void PostController::deletePost(const HttpRequestPtr& req,
3 std::function<void(const HttpResponsePtr&)>&& callback,
4 long long id)
5 {
6 drogon::async_run([=]() {
7 Mapper<Post> mp(dbClient);
8 try {
9 // Delete the post by primary key
10 mp.deleteByPrimaryKey(id);
11
12 // Respond with 204 No Content on successful deletion
13 auto resp = HttpResponse::newHttpResponse(k204NoContent);
14 callback(resp);
15
16 } catch (const DrogonDbException& e) {
17 if (e.errCode() == DrogonDbException::kNotFound) {
18 auto resp = HttpResponse::newHttpResponse(k404NotFound);
19 resp->setBody("Post not found");
20 callback(resp);
21 } else {
22 LOG_ERROR << "Database error: " << e.what();
23 auto resp = HttpResponse::newHttpResponse(k500InternalServerError);
24 resp->setBody("Database error");
25 callback(resp);
26 }
27 }
28 });
29 }

删除操作使用 mp.deleteByPrimaryKey(id)。成功删除后,通常返回 204 No Content 状态码,表示请求成功但没有响应体。

16.1.5 构建与运行

保存所有文件后,回到项目根目录,构建并运行项目:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 mkdir build
2 cd build
3 cmake ..
4 make
5 ./MyBlogApi

现在,您的博客 API 应该正在运行,监听在 config.json 中配置的端口(默认为 8848)。您可以使用 curl 或 Postman 等工具测试 API。

⚝ 创建博文:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 curl -X POST -H "Content-Type: application/json" -d '{"title":"My First Post", "content":"Hello Drogon!"}' http://127.0.0.1:8848/posts

⚝ 获取博文列表:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 curl http://127.0.0.1:8848/posts

⚝ 获取单篇博文(替换 {id}):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 curl http://127.0.0.1:8848/posts/1

⚝ 更新博文(替换 {id}):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 curl -X PUT -H "Content-Type: application/json" -d '{"content":"Updated content!"}' http://127.0.0.1:8848/posts/1

⚝ 删除博文(替换 {id}):

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 curl -X DELETE http://127.0.0.1:8848/posts/1

通过这个案例,我们成功地使用 Drogon 的核心特性构建了一个基础的 RESTful API 服务,体验了路由、控制器、ORM 和异步数据库操作的协同工作方式。

16.2 案例二:开发一个实时聊天应用

WebSocket 协议允许在客户端和服务器之间建立持久的双向通信连接,非常适合构建实时应用,如在线聊天室、协作工具或游戏。本案例将演示如何使用 Drogon 的 WebSocket 支持开发一个简单的多人在线聊天室。

16.2.1 理解 WebSocket 协议

WebSocket(全双工通信协议)与传统的 HTTP 协议不同,HTTP 是无状态、单向的请求-响应模式。WebSocket 通过一次 HTTP 握手(Handshake)建立连接后,数据可以在客户端和服务器之间自由发送,且协议开销比轮询(Polling)小得多,延迟也更低。

Drogon 提供了 WebSocketController 类来简化 WebSocket 服务器的实现。

16.2.2 创建 WebSocket 控制器

与 HTTP 控制器类似,我们使用 drogons 工具创建 WebSocket 控制器:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogons create controller Chat --websocket

这会在 controllers 目录下生成 Chat.hChat.cc 文件。编辑 Chat.h,使其继承自 drogon::WebSocketController<Chat>,并定义处理 WebSocket 事件的方法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // controllers/Chat.h
2 #pragma once
3
4 #include <drogon/WebSocketController.h>
5 #include <drogon/WebSocketConnection.h>
6 #include <drogon/drogon.h>
7 #include <mutex>
8 #include <unordered_map>
9
10 using namespace drogon;
11
12 // Use a map to store connected clients: connection ID -> connection pointer
13 static std::unordered_map<std::string, WebSocketConnectionPtr> connectedClients;
14 static std::mutex clientsMutex; // Protect access to the map
15
16 class Chat : public drogon::WebSocketController<Chat>
17 {
18 public:
19 virtual void handleNewConnection(const HttpRequestPtr& req,
20 const WebSocketConnectionPtr& wsConn) override;
21 virtual void handleConnectionClosed(const WebSocketConnectionPtr& wsConn) override;
22 virtual void handleTextMessage(const WebSocketConnectionPtr& wsConn,
23 std::string&& message) override;
24
25 METHOD_LIST_BEGIN
26 // Use METHOD_ADD to add your endpoints here...
27 WS_PATH_LIST_BEGIN
28 WS_PATH_ADD("/chat"); // WebSocket endpoint path
29 WS_PATH_LIST_END
30 METHOD_LIST_END
31 };

WebSocketController 需要实现三个虚方法:

handleNewConnection:当有新的 WebSocket 连接建立时调用。
handleConnectionClosed:当 WebSocket 连接关闭时调用。
handleTextMessage:当收到文本消息时调用(还有处理二进制消息的方法)。

我们还定义了一个静态的 std::unordered_map 来存储当前所有连接的 WebSocketConnectionPtr,以便向所有客户端广播消息。使用 std::mutex 来保护对这个共享资源的访问。

METHOD_LIST_BEGIN/METHOD_LIST_END 中,我们使用 WS_PATH_ADD 定义 WebSocket 的访问路径,这里是 /chat

16.2.3 实现 WebSocket 控制器方法

现在来实现 Chat.cc 中的方法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // controllers/Chat.cc
2 #include "Chat.h"
3
4 // Initialize static members
5 std::unordered_map<std::string, WebSocketConnectionPtr> Chat::connectedClients;
6 std::mutex Chat::clientsMutex;
7
8 void Chat::handleNewConnection(const HttpRequestPtr& req,
9 const WebSocketConnectionPtr& wsConn)
10 {
11 LOG_INFO << "New WebSocket connection from " << wsConn->peerAddr().toIpPort() << ", connection ID: " << wsConn->getConnection()->id();
12
13 std::lock_guard<std::mutex> lock(clientsMutex);
14 connectedClients[wsConn->getConnection()->id()] = wsConn;
15
16 // Send a welcome message to the new client
17 wsConn->send("Welcome to the chat! You are user_" + std::to_string(wsConn->getConnection()->id()));
18
19 // Announce new user to everyone
20 std::string announceMsg = "User_" + std::to_string(wsConn->getConnection()->id()) + " has joined the chat.";
21 for (const auto& pair : connectedClients)
22 {
23 if (pair.second != wsConn) // Don't send to the new client itself
24 {
25 pair.second->send(announceMsg);
26 }
27 }
28 }
29
30 void Chat::handleConnectionClosed(const WebSocketConnectionPtr& wsConn)
31 {
32 LOG_INFO << "WebSocket connection closed, connection ID: " << wsConn->getConnection()->id();
33
34 std::lock_guard<std::mutex> lock(clientsMutex);
35 std::string closedConnId = wsConn->getConnection()->id();
36 connectedClients.erase(closedConnId);
37
38 // Announce user left to everyone
39 std::string announceMsg = "User_" + closedConnId + " has left the chat.";
40 for (const auto& pair : connectedClients)
41 {
42 pair.second->send(announceMsg);
43 }
44 }
45
46 void Chat::handleTextMessage(const WebSocketConnectionPtr& wsConn,
47 std::string&& message)
48 {
49 LOG_INFO << "Received message from connection " << wsConn->getConnection()->id() << ": " << message;
50
51 std::string senderId = wsConn->getConnection()->id();
52 std::string broadcastMsg = "User_" + senderId + ": " + message;
53
54 // Broadcast the message to all connected clients
55 std::lock_guard<std::mutex> lock(clientsMutex);
56 for (const auto& pair : connectedClients)
57 {
58 // Use Drogon's thread-safe way to send messages
59 pair.second->send(broadcastMsg);
60 }
61 }

⚝ 在 handleNewConnection 中,我们将新的连接 wsConn 的指针存储到 connectedClients map 中,键是连接的唯一 ID (wsConn->getConnection()->id())。然后向新连接发送欢迎消息,并向其他所有现有连接广播用户加入的消息。
⚝ 在 handleConnectionClosed 中,我们从 map 中移除已关闭的连接,并向其他客户端广播用户离开的消息。
⚝ 在 handleTextMessage 中,我们接收到客户端发来的文本消息。我们将消息加上发送者的身份信息,然后遍历 connectedClients map,使用每个连接的 send() 方法将消息广播出去。

注意,访问 connectedClients map 时使用了 std::lock_guard 来确保线程安全,因为 Drogon 可能在不同的 I/O 线程中处理连接事件。WebSocketConnectionPtrsend() 方法是线程安全的,可以在任何线程中调用。

16.2.4 构建与运行

像之前一样构建并运行项目。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 mkdir build
2 cd build
3 cmake ..
4 make
5 ./MyBlogApi # Or whatever your project name is

现在,您的 Drogon 应用已经支持 WebSocket 服务。您可以使用浏览器开发者工具(控制台中的 WebSocket API)或专门的 WebSocket 客户端工具进行测试。

一个简单的 HTML 客户端示例如下:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <title>Drogon Chat</title>
5 </head>
6 <body>
7 <h1>Drogon Chat Room</h1>
8 <div id="chatbox" style="height: 300px; overflow-y: scroll; border: 1px solid #ccc;">
9 <!-- Messages will appear here -->
10 </div>
11 <input type="text" id="messageInput" placeholder="Type your message...">
12 <button onclick="sendMessage()">Send</button>
13
14 <script>
15 // Replace with your server address
16 const wsUrl = `ws://127.0.0.1:8848/chat`; // Use ws:// for non-HTTPS
17
18 let websocket;
19 const chatbox = document.getElementById('chatbox');
20 const messageInput = document.getElementById('messageInput');
21
22 function connectWebSocket() {
23 websocket = new WebSocket(wsUrl);
24
25 websocket.onopen = function(event) {
26 console.log('WebSocket connected!');
27 appendMessage('System', 'Connected to chat.');
28 };
29
30 websocket.onmessage = function(event) {
31 console.log('Message from server:', event.data);
32 appendMessage('Server', event.data);
33 };
34
35 websocket.onerror = function(event) {
36 console.error('WebSocket error:', event);
37 appendMessage('System', 'WebSocket error occurred.');
38 };
39
40 websocket.onclose = function(event) {
41 if (event.wasClean) {
42 console.log(`WebSocket closed cleanly, code=${event.code} reason=${event.reason}`);
43 appendMessage('System', `Disconnected from chat (Code: ${event.code}).`);
44 } else {
45 console.error('WebSocket connection died');
46 appendMessage('System', 'Disconnected from chat (Connection died).');
47 }
48 // Attempt to reconnect after a delay
49 setTimeout(connectWebSocket, 5000); // Reconnect after 5 seconds
50 };
51 }
52
53 function sendMessage() {
54 const message = messageInput.value;
55 if (message && websocket && websocket.readyState === WebSocket.OPEN) {
56 websocket.send(message);
57 messageInput.value = ''; // Clear input after sending
58 } else {
59 console.warn('WebSocket not connected or message is empty.');
60 }
61 }
62
63 function appendMessage(sender, message) {
64 const messageElement = document.createElement('p');
65 messageElement.textContent = `${sender}: ${message}`;
66 chatbox.appendChild(messageElement);
67 // Scroll to the bottom
68 chatbox.scrollTop = chatbox.scrollHeight;
69 }
70
71 // Connect when the page loads
72 connectWebSocket();
73
74 // Send message on Enter key press
75 messageInput.addEventListener('keypress', function(event) {
76 if (event.key === 'Enter') {
77 sendMessage();
78 event.preventDefault(); // Prevent default form submission if input is inside a form
79 }
80 });
81
82 </script>
83 </body>
84 </html>

将上述 HTML 文件保存并在浏览器中打开,然后打开多个标签页或使用其他客户端连接到 /chat 路径,即可进行简单的实时聊天。

这个案例展示了 Drogon 对 WebSocket 协议的良好支持,使得构建实时应用变得相对简单。

16.3 案例三:构建一个文件分享服务

文件上传和下载是许多 Web 应用的常见功能。本案例将演示如何使用 Drogon 构建一个简单的文件分享服务,用户可以上传文件,并且可以通过一个唯一的 URL 下载这些文件。我们将重点关注静态文件服务、请求体解析以及文件操作。

服务功能概述:

⚝ 提供一个页面用于上传文件。
⚝ 接收文件上传的 POST 请求,保存文件到服务器特定目录。
⚝ 提供一个下载文件的 GET 请求接口。
⚝ 通过静态文件服务直接提供上传的文件。

16.3.1 配置静态文件服务

最简单的文件下载方式就是将文件保存在 Drogon 配置的静态文件目录中,然后通过 HTTP 直接访问。编辑 config.json,找到 app 部分,添加或修改 document_root 配置,指定一个目录用于存放上传的文件。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 // ... other configurations
3 "app": {
4 // ... other app settings
5 "document_root": [
6 "./html", // Default static file directory
7 "./uploads" // Add a directory for uploaded files
8 ],
9 // ... other app settings
10 },
11 // ... other configurations
12 }

在项目根目录下创建 uploads 目录。Drogon 启动时会自动扫描 document_root 数组中的目录,并将它们映射到 URL 路径。默认情况下,./uploads 目录下的文件可以通过 /uploads/filename 的 URL 访问。

16.3.2 实现文件上传控制器

我们需要一个 HTTP 控制器来处理文件上传的 POST 请求。这个请求通常使用 multipart/form-data 编码方式。

创建 UploadController

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogons create controller UploadController

编辑 UploadController.h

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // controllers/UploadController.h
2 #pragma once
3
4 #include <drogon/HttpController.h>
5 #include <drogon/drogon.h>
6
7 using namespace drogon;
8
9 class UploadController : public drogon::HttpController<UploadController>
10 {
11 public:
12 METHOD_LIST_BEGIN
13 // Use METHOD_ADD to add your endpoints here...
14 METHOD_ADD("/upload", Post, &UploadController::uploadFile); // POST /upload
15 METHOD_LIST_END
16
17 void uploadFile(const HttpRequestPtr& req,
18 std::function<void(const HttpResponsePtr&)>&& callback);
19 };

实现 UploadController.cc 中的 uploadFile 方法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // controllers/UploadController.cc
2 #include "UploadController.h"
3 #include <drogon/drogon.h>
4 #include <iostream>
5 #include <fstream>
6
7 void UploadController::uploadFile(const HttpRequestPtr& req,
8 std::function<void(const HttpResponsePtr&)>&& callback)
9 {
10 // Check if the request method is POST and content type is multipart/form-data
11 if (req->method() != HttpMethod::kPost)
12 {
13 auto resp = HttpResponse::newHttpResponse(k405MethodNotAllowed);
14 callback(resp);
15 return;
16 }
17
18 if (!req->isOnMultipart())
19 {
20 auto resp = HttpResponse::newHttpResponse(k400BadRequest);
21 resp->setBody("Request must be multipart/form-data");
22 callback(resp);
23 return;
24 }
25
26 // Get uploaded files
27 const auto& files = req->getUploadedFiles();
28
29 if (files.empty())
30 {
31 auto resp = HttpResponse::newHttpResponse(k400BadRequest);
32 resp->setBody("No file uploaded");
33 callback(resp);
34 return;
35 }
36
37 // Process each uploaded file
38 std::string responseBody = "Uploaded files:\n";
39 for (const auto& file : files)
40 {
41 // Generate a unique filename or use the original filename (be cautious with original)
42 std::string originalFilename = file.getFileName();
43 std::string safeFilename = originalFilename; // In production, sanitize or use UUID
44
45 std::string savePath = "./uploads/" + safeFilename; // Path relative to project root
46
47 try
48 {
49 // Save the file
50 file.save(savePath);
51 responseBody += "- " + originalFilename + " saved as " + safeFilename + "\n";
52 LOG_INFO << "File saved: " << savePath;
53
54 // Optional: You might want to store metadata (original name, saved name, size) in a database
55 // For this simple case, we just save the file and rely on static serving for download.
56
57 }
58 catch (const std::exception& e)
59 {
60 LOG_ERROR << "Failed to save file " << originalFilename << ": " << e.what();
61 responseBody += "- Failed to save " + originalFilename + ": " + e.what() + "\n";
62 // You might want to send a 500 error here instead of just logging
63 }
64 }
65
66 // Prepare the response
67 auto resp = HttpResponse::newHttpResponse(k200OK);
68 resp->setBody(responseBody);
69 callback(resp);
70 }

⚝ 我们首先检查请求是否是 POST 方法以及是否为 multipart/form-data 类型,这是文件上传请求的标准格式。
req->getUploadedFiles() 方法返回一个 std::vector<drogon::UploadFile>,包含了所有上传的文件信息。
⚝ 我们遍历文件列表,获取原始文件名 (file.getFileName())。注意:在生产环境中,直接使用用户提供的原始文件名可能存在安全风险,应该进行净化或使用随机生成的唯一文件名(如 UUID)。这里为了示例简单,我们直接使用原始文件名保存。
⚝ 构建文件保存路径(例如,保存到 ./uploads/ 目录下)。
file.save(savePath) 方法将上传的文件内容保存到指定的路径。这个方法是异步的,但在这里我们没有在 Fiber 中调用它,因为 Drogon 会在处理 multipart/form-data 请求时,在内部将文件保存操作放到后台线程执行,避免阻塞 I/O 线程。
⚝ 最后,构造一个简单的响应,列出成功保存的文件名。

16.3.3 实现文件下载(通过静态服务)

由于我们将文件保存到了 config.json 中配置的 document_root 目录(即 ./uploads),Drogon 的静态文件服务会自动处理对这些文件的 GET 请求。

例如,如果您上传了一个名为 mydoc.pdf 的文件,并成功保存到 ./uploads/mydoc.pdf,那么您就可以通过访问 /uploads/mydoc.pdf 这个 URL 来下载它,无需编写额外的下载控制器代码。Drogon 会自动查找 ./uploads/mydoc.pdf 文件并将其作为响应体发送给客户端,同时设置正确的 MIME 类型。

16.3.4 构建与运行

构建并运行项目:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 mkdir build
2 cd build
3 cmake ..
4 make
5 ./MyBlogApi # Or whatever your project name is

现在,Drogon 应用可以处理 /upload 的文件上传请求,并将文件保存到 ./uploads 目录。同时,通过 /uploads/filename 即可下载对应的文件。

您可以使用 HTML 表单或者 curl 命令来测试文件上传:

一个简单的 HTML 表单上传示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <title>Upload File</title>
5 </head>
6 <body>
7 <h1>Upload a File</h1>
8 <form action="/upload" method="post" enctype="multipart/form-data">
9 Select file to upload:
10 <input type="file" name="myFile" id="myFile">
11 <br><br>
12 <input type="submit" value="Upload File">
13 </form>
14 </body>
15 </html>

将此 HTML 文件保存为 upload.html,然后您可以通过 /upload.html 访问这个页面(如果您的 document_root 包含 ./html 目录并且里面有这个文件),使用表单上传文件。

使用 curl 命令上传文件:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 curl -X POST -F "file=@/path/to/your/local/file.txt" http://127.0.0.1:8848/upload

/path/to/your/local/file.txt 替换为您要上传的本地文件路径。@ 符号告诉 curl 读取文件内容并作为 multipart/form-data 的一部分发送,字段名为 file(您可以根据需要修改代码中的处理逻辑来匹配不同的字段名)。

上传成功后,假设您的文件名为 example.txt,理论上您就可以通过 http://127.0.0.1:8848/uploads/example.txt 来访问或下载该文件了。

这个案例展示了 Drogon 处理文件上传的能力,以及如何利用静态文件服务简化文件下载的实现。对于更复杂的文件服务场景(如需要权限控制、文件列表展示等),您可能需要在控制器中编写更多逻辑,或者结合数据库存储文件元数据,但基本的文件上传和静态下载可以通过 Drogon 提供的功能轻松实现。

通过本章的三个实战案例,我们深入探讨了 Drogon 在构建不同类型 Web 应用中的实际应用。这些案例覆盖了 RESTful API、实时通信和文件处理等核心场景,希望能够帮助您更好地理解和掌握 Drogon 的强大特性。

理论结合实践,是提升技能的不二法门。鼓励您在学习这些案例的同时,尝试修改、扩展它们,或者基于这些案例的思路,开始构建您自己的 Drogon 应用。如果在实践过程中遇到问题,不要忘记利用本书后续章节介绍的社区资源寻求帮助!

下一章,我们将探讨 Drogon 社区以及如何继续您的进阶学习之旅。

17. 社区与进阶学习

本章作为全书的尾声,旨在引导读者在掌握 Drogon 的基础和核心知识后,如何持续学习、获取帮助以及融入社区。一个活跃的开源社区是项目生命力的重要源泉,参与其中不仅能解决遇到的问题,还能发现新的灵感和学习机会。此外,技术的进步永无止境,特别是 C++ 标准和 Drogon 框架本身都在不断演进,保持持续学习是成为一名优秀开发者的必经之路。

17.1 获取帮助与参与社区

在使用 Drogon 开发过程中,遇到问题是常态。Drogon 拥有一个不断壮大的社区,提供了多种获取帮助和参与交流的途径。积极利用这些资源,不仅能更快地解决问题,还能学习到其他开发者的经验和最佳实践。

17.1.1 官方文档(Official Documentation)

官方文档(Official Documentation)是学习和使用 Drogon 最权威、最全面的资源。它详细介绍了框架的各种特性、API(应用程序接口)使用方法、配置选项以及常见问题的解答。

访问方式: 通常通过 Drogon 的官方网站或 GitHub 仓库找到文档链接。
内容特点:
▮▮▮▮⚝ 系统性强,覆盖面广。
▮▮▮▮⚝ 包含 API 参考、教程、示例代码等。
▮▮▮▮⚝ 可能会提供不同版本的文档,请确保查阅与您使用的 Drogon 版本相匹配的文档。
建议: 在遇到具体的功能问题时,首先查阅官方文档。很多时候,答案就在其中。

17.1.2 GitHub 仓库(GitHub Repository)

Drogon 是一个开源项目,其代码、问题追踪(Issue Tracking)和开发活动都集中在 GitHub 上。GitHub 仓库是了解项目最新进展、报告 bug(错误)和提交功能请求的重要场所。

主要功能:
▮▮▮▮⚝ 代码仓库(Code Repository): 查看框架的源代码,理解内部实现细节。
▮▮▮▮⚝ 问题(Issues): 浏览已知的 bug、功能请求和用户遇到的问题。在报告新问题前,建议先搜索现有问题,避免重复提交。
▮▮▮▮⚝ 拉取请求(Pull Requests): 查看正在进行的特性开发和 bug 修复,了解社区的贡献方向。
▮▮▮▮⚝ 讨论(Discussions): GitHub Discussions 提供了一个更结构化的交流平台,用于提问、分享想法和讨论项目方向。

17.1.3 论坛与聊天群组(Forums & Chat Groups)

除了正式的文档和 GitHub,许多开源项目还依赖于非正式的交流平台,如论坛(Forums)或在线聊天群组(Chat Groups),Drogon 社区也不例外。

常见的交流平台:
▮▮▮▮⚝ Slack、Telegram、Discord 群组: 这些平台通常用于实时交流,可以快速提问和获得反馈。社区成员会在这里分享经验、讨论技术细节。
▮▮▮▮⚝ 论坛: 如果有官方或非官方的 Drogon 论坛,也是提问和讨论的场所。论坛通常适合更长篇幅的问题描述和回复。
参与技巧:
▮▮▮▮⚝ 在提问前,尽量详细描述您遇到的问题、已经尝试过的解决方案、Drogon 版本、操作系统、相关代码片段和错误信息。
▮▮▮▮⚝ 保持礼貌和耐心。
▮▮▮▮⚝ 在获得帮助后,及时反馈问题是否解决,并感谢提供帮助的社区成员。
▮▮▮▮⚝ 在力所能及的范围内,也尝试回答其他人的问题,这是回馈社区、提升自己的绝佳方式。

17.2 贡献给 Drogon 项目

作为开源项目,Drogon 的发展离不开社区的贡献。无论是修复 bug、增加新功能、改进文档还是提供翻译,您的贡献都能帮助 Drogon 变得更好,同时也能提升您自身的开源协作能力和对框架的理解深度。

17.2.1 贡献方式概览

贡献并不局限于编写核心代码。有很多方式可以为 Drogon 社区做出贡献:

代码贡献(Code Contribution):
▮▮▮▮⚝ 修复已知的 bug。
▮▮▮▮⚝ 实现社区讨论过的新特性。
▮▮▮▮⚝ 优化现有代码的性能或可读性。
▮▮▮▮⚝ 编写新的测试用例(Test Cases)。
文档贡献(Documentation Contribution):
▮▮▮▮⚝ 改进现有文档的清晰度或准确性。
▮▮▮▮⚝ 补充缺失的功能说明。
▮▮▮▮⚝ 为文档提供翻译(如果项目支持多语言文档)。
▮▮▮▮⚝ 编写新的教程或示例。
问题报告与讨论(Issue Reporting & Discussion):
▮▮▮▮⚝ 提交详细、可复现的 bug 报告。
▮▮▮▮⚝ 参与 GitHub Issues 和 Discussions 中的技术讨论,分享您的见解。
社区支持(Community Support):
▮▮▮▮⚝ 在论坛或聊天群组中回答其他用户的问题。

17.2.2 代码贡献流程

如果您想通过代码为 Drogon 做出贡献,通常遵循以下流程:

Fork 项目(Fork the Repository): 在 GitHub 上将 Drogon 仓库 Fork 到您自己的账户下。
克隆仓库(Clone the Repository): 将您 Fork 的仓库克隆到本地开发环境。
创建分支(Create a Branch): 为您的贡献创建一个新的分支(例如,fix/bug-descriptionfeature/new-feature-name)。
编写代码(Write Code): 在您的分支上实现 bug 修复或新功能。请确保遵循项目的编码规范(Coding Style)。
编写测试(Write Tests): 为您的更改编写相应的测试用例,确保代码的正确性,并防止未来引入回归(Regression)。
提交更改(Commit Changes): 将您的代码更改提交到本地分支。编写清晰的提交信息(Commit Message),说明本次提交的目的。
同步仓库(Sync with Upstream): 定期将主仓库的最新更改同步到您的本地仓库和分支,避免冲突。
提交拉取请求(Submit a Pull Request): 将您的分支推送到您 Fork 的 GitHub 仓库,然后在 GitHub 页面上创建 Pull Request,请求将您的更改合并到主仓库。
参与评审(Participate in Review): 项目维护者会评审您的 Pull Request,可能会提出修改意见。请积极回应评审意见,修改代码并更新 Pull Request。
合并(Merge): 代码通过评审和测试后,您的更改将被合并到主仓库中。恭喜您成为 Drogon 的贡献者!

17.2.3 文档贡献流程

文档对于项目的可访问性和易用性至关重要。贡献文档与代码贡献类似,但侧重点不同:

找到需要改进或补充的地方: 在使用 Drogon 或浏览文档时,记录下不清晰、不准确或缺失的内容。
Fork 和克隆文档仓库: 如果文档与代码分开存放,或者直接 Fork 主仓库找到文档目录。
创建分支。
编辑文档: 根据您的发现修改或添加文档内容。文档通常使用 Markdown 或 reStructuredText 等格式编写。
预览更改: 在提交前,确保您的文档更改在格式上是正确的,并且内容清晰易懂。
提交拉取请求: 提交包含文档更改的 Pull Request。
参与评审: 文档维护者会评审您的更改,可能会提出措辞、格式或内容的建议。
合并。

通过贡献,您不仅帮助了他人,也加深了自己对项目的理解,是进阶学习的有效途径。

17.3 持续学习与跟踪更新

技术领域日新月异,无论是 C++ 语言本身还是 Drogon 框架,都在不断发展。要保持竞争力并充分利用最新技术,持续学习和跟踪更新至关重要。

17.3.1 关注 Drogon 的发展与更新

Drogon 作为一个活跃的开源项目,会定期发布新的版本,包含 bug 修复、性能优化和新功能。

跟踪渠道:
▮▮▮▮⚝ GitHub 发布页面(Releases Page): 关注 Drogon GitHub 仓库的 Releases 页面,了解每个新版本的详细更新日志(Changelog)。
▮▮▮▮⚝ 项目博客或新闻: 如果 Drogon 有官方博客或其他新闻发布渠道,订阅它们以获取更宏观的项目动态。
▮▮▮▮⚝ 社区交流平台: 在社区群组中,开发者们经常讨论新版本的特性和升级中可能遇到的问题。
重要性:
▮▮▮▮⚝ 及时获取 bug 修复,提升应用稳定性。
▮▮▮▮⚝ 利用新功能,简化开发或提升性能。
▮▮▮▮⚝ 了解潜在的 API 变更(Breaking Changes),为升级做好准备。

17.3.2 关注 C++ 语言的新特性

Drogon 积极拥抱现代 C++ 特性,尤其是 C++11 之后的版本,特别是 C++20 引入的协程(Coroutines)对 Drogon 的异步编程模型产生了深远影响。

学习内容:
▮▮▮▮⚝ C++ 标准演进: 学习 C++11, C++14, C++17, C++20, C++23 等新标准引入的关键特性,例如 Lambda 表达式、智能指针(Smart Pointers)、右值引用(Rvalue References)、Concepts(概念)、Ranges(范围)和 Coroutines(协程)等。
▮▮▮▮⚝ 现代 C++ 实践: 学习如何编写更现代化、更安全、更高效的 C++ 代码。
资源:
▮▮▮▮⚝ C++ 官方标准文档(虽然难度较大)。
▮▮▮▮⚝ 各类 C++ 教程、书籍和在线课程。
▮▮▮▮⚝ 关注 C++ 相关的会议(如 CppCon)和技术博客。
联系 Drogon: 理解 C++ 新特性,特别是协程,能帮助您更深入地理解 Drogon 的异步工作原理,写出更高质量、更易于维护的代码。例如,掌握协程的使用是高效利用 Drogon 纤程(Fiber)的关键。

17.3.3 探索相关技术与领域

Web 开发是一个综合性的领域,Drogon 只是其中的一个环节。扩展您的知识领域,学习与 Web 开发相关的其他技术,能让您成为一个更全面的开发者。

相关领域:
▮▮▮▮⚝ 数据库技术: 深入学习您使用的数据库系统(如 PostgreSQL, MySQL),包括性能调优、索引设计、事务隔离级别等。
▮▮▮▮⚝ 前端技术: 了解 HTML, CSS, JavaScript 以及常见的前端框架(如 React, Vue, Angular),虽然 Drogon 主要负责后端,但理解前后端交互是必要的。
▮▮▮▮⚝ 系统知识: 学习操作系统(Linux 居多)、网络协议(TCP/IP, HTTP/HTTPS)、容器技术(Docker, Kubernetes)和云服务。这些知识对于部署、运维和性能优化至关重要。
▮▮▮▮⚝ 安全知识: 深入学习 Web 安全攻击(如 Injection, XSS, CSRF)及其防御手段,提升您的应用安全水平。
▮▮▮▮⚝ 性能工程: 学习如何进行性能测试、剖析和优化,这是 C++ Web 框架的核心优势所在。
学习方法:
▮▮▮▮⚝ 阅读专业书籍和技术文章。
▮▮▮▮⚝ 参与相关的在线课程和讲座。
▮▮▮▮⚝ 通过实际项目练习和应用。

学习 Drogon 是您高性能 Web 开发旅程的一个重要起点。通过积极参与社区、不断学习新的 C++ 特性以及扩展相关领域的知识,您将能够更好地掌握 Drogon,构建出卓越的应用程序,并在快速发展的技术世界中保持领先。祝您在 Drogon 开发之路上取得更大的成功!🚀

Appendix A: Drogon 配置选项参考手册 (Drogon Configuration Options Reference)

Drogon 框架提供了丰富且灵活的配置选项,允许开发者根据不同的运行环境和需求调整应用程序的行为。这些配置通常存储在一个 JSON 格式的文件中,默认情况下是项目根目录下的 config.json。本附录将详细列出 Drogon 中常用和关键的配置选项,并解释它们的用途和取值范围,为读者提供一个权威的参考。

Appendix A1: 配置文件的加载与结构

Drogon 在应用启动时会自动查找并加载配置文件。默认查找路径通常是当前工作目录下的 config.json。您也可以通过命令行参数 -c <file> 或在代码中调用 drogon::app().loadConfigFile("<file>") 来指定其他配置文件。

配置文件采用 JSON(JavaScript Object Notation,JavaScript 对象表示法)格式。顶层是一个 JSON 对象,其键(key)对应于不同的配置模块或选项,值(value)则是具体的配置数据,可以是基本类型(字符串、数字、布尔值)或其他嵌套的 JSON 对象或数组。

一个基本的 config.json 文件结构示例如下:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 // 应用全局配置
4 },
5 "listeners": [
6 // 监听器配置
7 ],
8 "db_clients": [
9 // 数据库客户端配置
10 ],
11 "log": {
12 // 日志配置
13 },
14 // 其他配置项...
15 }

下面将分模块详细介绍主要的配置选项。

Appendix A2: 应用全局配置 (app 字段)

app 字段是一个 JSON 对象,包含了 Drogon 应用的全局设置。

Appendix A2.1: 核心设置

threads_num (Integer,整型)
▮▮▮▮⚝ 作用:设置事件循环(event loop)的线程数量。这是影响 Drogon 性能的关键配置之一。通常建议设置为 CPU 核数的 1 到 2 倍。
▮▮▮▮⚝ 默认值:通常是 1,但在发布模式下会根据 CPU 核数自动调整。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "threads_num": 8
4 }
5 }

run_as_daemon (Boolean,布尔型)
▮▮▮▮⚝ 作用:如果设置为 true,应用将以后台守护进程(daemon)的方式运行。仅在 Linux/macOS 等类 Unix 系统上有效。
▮▮▮▮⚝ 默认值:false
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "run_as_daemon": true
4 }
5 }

document_root (String,字符串)
▮▮▮▮⚝ 作用:设置静态文件服务的根目录。Drogon 会在该目录下查找并提供静态文件。
▮▮▮▮⚝ 默认值:通常是项目根目录下的 www 文件夹。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "document_root": "/var/www/static"
4 }
5 }

upload_path (String,字符串)
▮▮▮▮⚝ 作用:设置文件上传的默认保存目录。
▮▮▮▮⚝ 默认值:通常是项目根目录下的 uploads 文件夹。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "upload_path": "/data/uploads"
4 }

idle_timeout (Integer,整型)
▮▮▮▮⚝ 作用:设置 HTTP 连接的空闲超时时间,单位为秒。如果在该时间内没有收到新的请求,连接将被关闭。
▮▮▮▮⚝ 默认值:通常是 60 秒。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "idle_timeout": 120
4 }
5 }

keep_alive_timeout (Integer,整型)
▮▮▮▮⚝ 作用:设置 HTTP Keep-Alive 连接的超时时间,单位为秒。客户端可以在该时间内重用同一个连接发送多个请求。
▮▮▮▮⚝ 默认值:通常是 10 秒。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "keep_alive_timeout": 15
4 }
5 }

enable_gzip (Boolean,布尔型)
▮▮▮▮⚝ 作用:是否对符合条件的响应开启 Gzip 压缩。
▮▮▮▮⚝ 默认值:true
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "enable_gzip": false
4 }
5 }

gzip_minimum_length (Integer,整型)
▮▮▮▮⚝ 作用:只有当响应体大小超过此阈值(单位字节)时才进行 Gzip 压缩。
▮▮▮▮⚝ 默认值:2048。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "gzip_minimum_length": 1024
4 }
5 }

gzip_compressible_types (Array of Strings,字符串数组)
▮▮▮▮⚝ 作用:指定哪些 MIME 类型(MIME Type)的响应可以进行 Gzip 压缩。
▮▮▮▮⚝ 默认值:["text/plain", "text/html", "text/css", "application/javascript", "application/json", "application/xml"] 等。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "gzip_compressible_types": ["text/*", "application/json"]
4 }
5 }

use_sendfile (Boolean,布尔型)
▮▮▮▮⚝ 作用:是否启用 sendfile 系统调用来发送文件,这可以提高静态文件服务的性能。
▮▮▮▮⚝ 默认值:true
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "use_sendfile": false
4 }
5 }

enable_server_header (Boolean,布尔型)
▮▮▮▮⚝ 作用:是否在响应头部包含 Server 字段,指示服务器软件信息(例如:"Server: Drogon/1.8.0")。
▮▮▮▮⚝ 默认值:true
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "enable_server_header": false
4 }
5 }

enable_date_header (Boolean,布尔型)
▮▮▮▮⚝ 作用:是否在响应头部包含 Date 字段。
▮▮▮▮⚝ 默认值:true
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "enable_date_header": false
4 }
5 }

real_ip_headers (Array of Strings,字符串数组)
▮▮▮▮⚝ 作用:当应用部署在反向代理(reverse proxy)后时,指定从哪些 HTTP 头部获取客户端的真实 IP 地址(例如:"X-Forwarded-For", "X-Real-IP")。按顺序尝试,取第一个找到的有效 IP。
▮▮▮▮⚝ 默认值:[] (空数组)。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "real_ip_headers": ["X-Forwarded-For", "X-Real-IP"]
4 }
5 }

real_ip_from (Array of Strings,字符串数组)
▮▮▮▮⚝ 作用:与 real_ip_headers 配合使用,指定哪些 IP 地址范围是可信的反向代理 IP。只有来自这些 IP 地址的请求,并且包含 real_ip_headers 中指定的头部时,才会信任头部中的客户端 IP。
▮▮▮▮⚝ 默认值:[] (空数组)。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "real_ip_from": ["192.168.1.0/24", "10.0.0.1"]
4 }
5 }

Appendix A2.2: Session 配置 (app.session 字段)

app.session 字段是一个 JSON 对象,用于配置 Session(会话)管理。

enable (Boolean,布尔型)
▮▮▮▮⚝ 作用:是否启用 Session 功能。
▮▮▮▮⚝ 默认值:false
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "session": {
4 "enable": true
5 }
6 }
7 }

timeout (Integer,整型)
▮▮▮▮⚝ 作用:设置 Session 的过期时间,单位为秒。Session 在指定时间内没有活动(未被访问)将过期。
▮▮▮▮⚝ 默认值:1800 秒 (30分钟)。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "session": {
4 "timeout": 3600
5 }
6 }
7 }

client_only (Boolean,布尔型)
▮▮▮▮⚝ 作用:如果设置为 true,Session 数据将仅存储在客户端的 Cookie 中。这会限制 Session 的大小,并且不适合存储敏感信息。
▮▮▮▮⚝ 默认值:false (Session 数据存储在服务器端)。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "session": {
4 "client_only": false
5 }
6 }
7 }

cookie_name (String,字符串)
▮▮▮▮⚝ 作用:设置用于存储 Session ID 的 Cookie 名称。
▮▮▮▮⚝ 默认值:"drogonsession"。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "session": {
4 "cookie_name": "myapp_sid"
5 }
6 }
7 }

path (String,字符串)
▮▮▮▮⚝ 作用:设置 Session Cookie 的路径属性。
▮▮▮▮⚝ 默认值:"/"。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "session": {
4 "path": "/app"
5 }
6 }
7 }

domain (String,字符串)
▮▮▮▮⚝ 作用:设置 Session Cookie 的域名属性。
▮▮▮▮⚝ 默认值:空字符串 (当前域名)。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "session": {
4 "domain": ".example.com"
5 }
6 }
7 }

secure (Boolean,布尔型)
▮▮▮▮⚝ 作用:设置 Session Cookie 的 Secure 属性,如果为 true,Cookie 只在 HTTPS 连接中发送。
▮▮▮▮⚝ 默认值:false
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "session": {
4 "secure": true
5 }
6 }
7 }

http_only (Boolean,布尔型)
▮▮▮▮⚝ 作用:设置 Session Cookie 的 HttpOnly 属性,如果为 true,Cookie 不能通过客户端脚本(如 JavaScript)访问。增强安全性,防止 XSS 攻击窃取 Cookie。
▮▮▮▮⚝ 默认值:true
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "session": {
4 "http_only": true
5 }
6 }
7 }

Appendix A2.3: JSON 解析/生成优化 (app.json_optimization 字段)

app.json_optimization 字段是一个 JSON 对象,用于配置 JSON 相关的优化。

enabled (Boolean,布尔型)
▮▮▮▮⚝ 作用:是否启用 JSON 优化。启用后,Drogon 会尝试更高效地解析和生成 JSON,但在某些边缘情况下可能会略微改变输出格式(例如,浮点数的精度)。
▮▮▮▮⚝ 默认值:false
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "json_optimization": {
4 "enabled": true
5 }
6 }
7 }

Appendix A2.4: 内容安全策略 (CSP) (app.csp 字段)

app.csp 字段是一个 JSON 对象,用于配置 Content Security Policy(内容安全策略)响应头部。

default-src, script-src, style-src, img-src, connect-src, font-src, object-src, media-src, frame-src, manifest-src, worker-src, base-uri, form-action, frame-ancestors, report-uri, report-to, upgrade-insecure-requests, block-all-mixed-content (String,字符串)
▮▮▮▮⚝ 作用:对应 CSP 规范中的各个指令,值是指令的具体策略字符串。
▮▮▮▮⚝ 默认值:均为空字符串,即不设置该指令。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app": {
3 "csp": {
4 "default-src": "'self'",
5 "script-src": "'self' 'unsafe-inline'",
6 "img-src": "'self' data:",
7 "report-uri": "/csp-report"
8 }
9 }
10 }

这将在响应头部添加 Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; img-src 'self' data:; report-uri /csp-report;

Appendix A3: 监听器配置 (listeners 字段)

listeners 字段是一个 JSON 数组,每个元素都是一个 JSON 对象,用于配置 Drogon 监听的网络地址和端口。可以配置多个监听器以在不同的地址/端口上服务。

每个监听器对象包含以下字段:

address (String,字符串)
▮▮▮▮⚝ 作用:监听的 IP 地址。可以是具体的 IPv4 或 IPv6 地址,也可以是 "0.0.0.0" (监听所有 IPv4 地址) 或 "::" (监听所有 IPv6 地址,通常也包含 IPv4)。
▮▮▮▮⚝ 默认值:"0.0.0.0"。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "listeners": [
3 {
4 "address": "0.0.0.0",
5 "port": 80,
6 "protocol": "http"
7 },
8 {
9 "address": "127.0.0.1",
10 "port": 8000,
11 "protocol": "https",
12 "cert": "/path/to/server.crt",
13 "key": "/path/to/server.key"
14 }
15 ]
16 }

Appendix A4: 数据库客户端配置 (db_clients 字段)

db_clients 字段是一个 JSON 数组,每个元素都是一个 JSON 对象,用于配置不同数据库的连接信息。 Drogon 支持多种数据库,如 PostgreSQL, MySQL, SQLite, SQL Server 等。

每个数据库客户端对象包含以下字段:

type (String,字符串)
▮▮▮▮⚝ 作用:数据库类型。可选值包括 "postgresql", "mysql", "sqlite3", "sqlserver"。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "db_clients": [
3 {
4 "type": "postgresql",
5 "host": "localhost",
6 "port": 5432,
7 "dbname": "mydb",
8 "user": "myuser",
9 "passwd": "mypassword",
10 "connections_num": 10
11 },
12 {
13 "type": "mysql",
14 "host": "192.168.1.100",
15 "port": 3306,
16 "dbname": "mysql_db",
17 "user": "mysql_user",
18 "passwd": "mysql_password",
19 "connections_num": 8,
20 "is_master": true
21 }
22 ]
23 }

host (String,字符串)
▮▮▮▮⚝ 作用:数据库服务器地址。
▮▮▮▮⚝ 示例:见上例。

port (Integer,整型)
▮▮▮▮⚝ 作用:数据库服务器端口。
▮▮▮▮⚝ 示例:见上例。

dbname (String,字符串)
▮▮▮▮⚝ 作用:数据库名称。
▮▮▮▮⚝ 示例:见上例。

user (String,字符串)
▮▮▮▮⚝ 作用:连接数据库使用的用户名。
▮▮▮▮⚝ 示例:见上例。

passwd (String,字符串)
▮▮▮▮⚝ 作用:连接数据库使用的密码。
▮▮▮▮⚝ 示例:见上例。

connections_num (Integer,整型)
▮▮▮▮⚝ 作用:连接池中维护的数据库连接数量。这是影响数据库访问性能的重要配置。
▮▮▮▮⚝ 默认值:通常是 1。
▮▮▮▮⚝ 示例:见上例。

is_master (Boolean,布尔型)
▮▮▮▮⚝ 作用:仅用于配置多个数据库客户端时,标记该连接是否是主数据库。对于 ORM 或某些需要默认数据库的场景有用。可以有多个标记为 true 的连接,Drogon 会随机选择一个作为主连接。
▮▮▮▮⚝ 默认值:false
▮▮▮▮⚝ 示例:见上例。

timeout (Integer,整型)
▮▮▮▮⚝ 作用:数据库操作的超时时间,单位为秒。
▮▮▮▮⚝ 默认值:通常是 3 秒。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "db_clients": [
3 {
4 "type": "postgresql",
5 "...": "...",
6 "timeout": 5
7 }
8 ]
9 }

character_set (String,字符串)
▮▮▮▮⚝ 作用:数据库连接的字符集。
▮▮▮▮⚝ 默认值:通常根据数据库类型有默认值。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "db_clients": [
3 {
4 "type": "mysql",
5 "...": "...",
6 "character_set": "utf8mb4"
7 }
8 ]
9 }

auto_commit (Boolean,布尔型)
▮▮▮▮⚝ 作用:是否开启自动提交(auto-commit)模式。如果为 false,需要手动调用事务 API 进行提交或回滚。
▮▮▮▮⚝ 默认值:true
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "db_clients": [
3 {
4 "type": "postgresql",
5 "...": "...",
6 "auto_commit": false
7 }
8 ]
9 }

ssl (Boolean,布尔型)
▮▮▮▮⚝ 作用:是否使用 SSL/TLS 连接数据库。
▮▮▮▮⚝ 默认值:false
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "db_clients": [
3 {
4 "type": "postgresql",
5 "...": "...",
6 "ssl": true
7 }
8 ]
9 }

对于 SQLite3 数据库,配置方式略有不同,通常只需要指定 type 为 "sqlite3" 和 filename (数据库文件路径)即可:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "db_clients": [
3 {
4 "type": "sqlite3",
5 "filename": "/path/to/your/database.sqlite"
6 }
7 ]
8 }

Appendix A5: 日志配置 (log 字段)

log 字段是一个 JSON 对象,用于配置 Drogon 的日志系统。

Appendix A5.1: 日志级别

log_level (String,字符串)
▮▮▮▮⚝ 作用:设置最低输出的日志级别。低于此级别的日志将被忽略。
▮▮▮▮⚝ 可选值:"trace", "debug", "info", "warn", "error", "fatal"。
▮▮▮▮⚝ 默认值:"trace" (开发模式下) 或 "info" (发布模式下)。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "log": {
3 "log_level": "info"
4 }
5 }

Appendix A5.2: 输出目标

日志可以同时输出到控制台和文件。

outputs (Array of Objects,对象数组)
▮▮▮▮⚝ 作用:配置一个或多个日志输出目标。每个对象定义一个输出目标。

每个输出目标对象包含以下字段:

type (String,字符串)
▮▮▮▮⚝ 作用:输出类型。可选值包括 "cout" (标准输出), "cerr" (标准错误输出), "file" (文件)。
▮▮▮▮⚝ 示例:见下例。

log_level (String,字符串)
▮▮▮▮⚝ 作用:仅用于该输出目标的最低日志级别。如果设置,它会覆盖全局的 log_level 设置。
▮▮▮▮⚝ 示例:见下例。

filename (String,字符串)
▮▮▮▮⚝ 作用:仅当 type 为 "file" 时使用,指定日志文件路径。
▮▮▮▮⚝ 示例:见下例。

rotation_size (Integer,整型)
▮▮▮▮⚝ 作用:仅当 type 为 "file" 时使用,设置日志文件旋转(log rotation)的大小阈值,单位为字节。当文件大小超过此值时,会自动备份当前文件并创建新的日志文件。设置为 0 表示不按大小旋转。
▮▮▮▮⚝ 默认值:0。
▮▮▮▮⚝ 示例:见下例。

rotation_time (Integer,整型)
▮▮▮▮⚝ 作用:仅当 type 为 "file" 时使用,设置日志文件旋转的时间阈值,单位为秒。设置为 0 表示不按时间旋转。
▮▮▮▮⚝ 默认值:0。
▮▮▮▮⚝ 示例:见下例。

flush_every (Integer,整型)
▮▮▮▮⚝ 作用:仅当 type 为 "file" 时使用,设置每隔多少次写操作将缓冲区刷新到文件。设置为 0 表示每次写都刷新。
▮▮▮▮⚝ 默认值:0。
▮▮▮▮⚝ 示例:见下例。

日志配置示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "log": {
3 "log_level": "debug", // 全局最低级别
4 "outputs": [
5 {
6 "type": "cout",
7 "log_level": "trace" // 控制台输出所有级别日志
8 },
9 {
10 "type": "file",
11 "filename": "/var/log/drogon/app.log",
12 "log_level": "info", // 文件仅输出 info 及以上级别
13 "rotation_size": 10485760, // 10MB 旋转
14 "rotation_time": 86400 // 每天旋转
15 }
16 ]
17 }
18 }

Appendix A6: ORM 配置 (orm 字段)

orm 字段是一个 JSON 对象,用于配置 ORM 相关的一些全局行为。

json_key_type (String,字符串)
▮▮▮▮⚝ 作用:设置 ORM 模型转为 JSON 时,使用数据库列名还是模型成员变量名作为 JSON 键。
▮▮▮▮⚝ 可选值:"column_name" (列名), "field_name" (成员变量名)。
▮▮▮▮⚝ 默认值:"column_name"。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "orm": {
3 "json_key_type": "field_name"
4 }
5 }

例如,数据库列名为 user_name,对应的模型成员变量可能是 userName_。如果设置为 "column_name",JSON 键是 user_name;如果设置为 "field_name",JSON 键是 userName

Appendix A7: 插件配置 (plugins 字段)

plugins 字段是一个 JSON 数组,每个元素是一个 JSON 对象,用于配置需要加载和初始化的插件(Plugin)。

每个插件对象包含以下字段:

name (String,字符串)
▮▮▮▮⚝ 作用:插件类的名称。Drogon 会根据名称查找并实例化对应的插件类。
▮▮▮▮⚝ 示例:见下例。

config (Any JSON value,任何 JSON 值)
▮▮▮▮⚝ 作用:传递给插件的配置数据。插件可以在初始化时读取并使用这些配置。
▮▮▮▮⚝ 示例:见下例。

插件配置示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "plugins": [
3 {
4 "name": "drogon::Oauth2", // 内置的 OAuth2 插件
5 "config": {
6 "auth_server": "https://auth.example.com",
7 "client_id": "your_client_id",
8 "client_secret": "your_client_secret"
9 }
10 },
11 {
12 "name": "MyApp::MyCustomPlugin", // 自定义插件
13 "config": {
14 "param1": "value1",
15 "param2": 123
16 }
17 }
18 ]
19 }

Appendix A8: 过滤器配置 (filters 字段)

filters 字段是一个 JSON 数组,每个元素是一个 JSON 对象,用于配置全局过滤器(Filter)。全局过滤器会应用于所有请求(除非被特定路由或控制器覆盖)。

每个过滤器对象包含以下字段:

name (String,字符串)
▮▮▮▮⚝ 作用:过滤器类的名称。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "filters": [
3 {
4 "name": "LoginFilter"
5 },
6 {
7 "name": "AccessControlFilter",
8 "config": {
9 "allowed_ips": ["127.0.0.1", "192.168.1.0/24"]
10 }
11 }
12 ]
13 }

config (Any JSON value,任何 JSON 值)
▮▮▮▮⚝ 作用:传递给过滤器的配置数据。

Appendix A9: 中间件配置 (middleware 字段)

middleware 字段是一个 JSON 数组,每个元素是一个 JSON 对象,用于配置全局中间件(Middleware)。全局中间件会应用于所有请求(除非被特定路由或控制器覆盖)。

每个中间件对象包含以下字段:

name (String,字符串)
▮▮▮▮⚝ 作用:中间件类的名称。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "middleware": [
3 {
4 "name": "RequestLoggerMiddleware"
5 },
6 {
7 "name": "CorsMiddleware",
8 "config": {
9 "allow_origin": "*"
10 }
11 }
12 ]
13 }

config (Any JSON value,任何 JSON 值)
▮▮▮▮⚝ 作用:传递给中间件的配置数据。

Appendix A10: 视图配置 (template_paths 字段)

template_paths 字段是一个 JSON 数组,每个元素是一个字符串,用于指定视图模板文件(View Template File)的查找路径。Drogon 会按顺序在这些路径下查找模板文件。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "template_paths": [
3 "views",
4 "templates/common"
5 ]
6 }

Appendix A11: 其他常见配置

除了上述主要字段,Drogon 还提供了一些其他配置选项:

sync_thread_num (Integer,整型)
▮▮▮▮⚝ 作用:设置同步任务(如同步数据库操作)的线程池大小。这些线程不属于事件循环,用于执行阻塞操作,避免阻塞主事件循环线程。
▮▮▮▮⚝ 默认值:通常是 CPU 核数。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "sync_thread_num": 4
3 }

app_client_thread_num (Integer,整型)
▮▮▮▮⚝ 作用:设置应用作为客户端(例如发送 HTTP 请求到其他服务)时的线程池大小。
▮▮▮▮⚝ 默认值:通常是 1。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "app_client_thread_num": 2
3 }

enable_redis (Boolean,布尔型)
▮▮▮▮⚝ 作用:是否启用内置的 Redis 客户端。
▮▮▮▮⚝ 默认值:false
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "enable_redis": true
3 }

redis_clients (Array of Objects,对象数组)
▮▮▮▮⚝ 作用:配置 Redis 连接信息,结构类似于 db_clients
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "redis_clients": [
3 {
4 "host": "127.0.0.1",
5 "port": 6379,
6 "pass": "redis_password",
7 "connection_timeout": 5,
8 "network_timeout": 3
9 }
10 ]
11 }

max_connections_per_ip (Integer,整型)
▮▮▮▮⚝ 作用:限制单个 IP 地址的最大连接数。设置为 0 表示不限制。
▮▮▮▮⚝ 默认值:0。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "max_connections_per_ip": 100
3 }

max_requests_per_ip (Integer,整型)
▮▮▮▮⚝ 作用:限制单个 IP 地址在特定时间窗口内的最大请求数(通常与 requests_arrival_interval 配合使用)。设置为 0 表示不限制。
▮▮▮▮⚝ 默认值:0。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "max_requests_per_ip": 1000,
3 "requests_arrival_interval": 60 // 60秒内最多1000个请求
4 }

requests_arrival_interval (Integer,整型)
▮▮▮▮⚝ 作用:与 max_requests_per_ip 配合使用,设置统计请求数的时间窗口,单位为秒。
▮▮▮▮⚝ 默认值:1秒。
▮▮▮▮⚝ 示例:见上例。

load_balance (String,字符串)
▮▮▮▮⚝ 作用:如果配置了多个监听地址或在集群模式下,指定负载均衡策略。
▮▮▮▮⚝ 可选值:"roundrobin", "leastconn"。
▮▮▮▮⚝ 默认值:"roundrobin"。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "load_balance": "leastconn"
3 }

ssl_protocols (String,字符串)
▮▮▮▮⚝ 作用:配置 HTTPS/SSL 连接允许的协议版本。
▮▮▮▮⚝ 默认值:通常是 "TLSv1.2,TLSv1.3"。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "ssl_protocols": "TLSv1.3"
3 }

ssl_ciphers (String,字符串)
▮▮▮▮⚝ 作用:配置 HTTPS/SSL 连接允许的加密算法套件(cipher suites)。使用 OpenSSL 格式的字符串。
▮▮▮▮⚝ 默认值:通常是安全的默认值。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "ssl_ciphers": "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"
3 }

enable_compression (Boolean,布尔型)
▮▮▮▮⚝ 作用:是否启用 HTTP 压缩。此选项已基本被 enable_gzip 及其相关配置替代。为了向后兼容可能保留。
▮▮▮▮⚝ 默认值:true

root_certificates (String,字符串)
▮▮▮▮⚝ 作用:指定客户端模式下(例如 HttpClient)验证服务器证书使用的根证书文件路径。
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "root_certificates": "/etc/ssl/certs/ca-certificates.crt"
3 }

prefer_ipv6 (Boolean,布尔型)
▮▮▮▮⚝ 作用:当 DNS 解析同时返回 IPv4 和 IPv6 地址时,是否优先使用 IPv6。
▮▮▮▮⚝ 默认值:false
▮▮▮▮⚝ 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "prefer_ipv6": true
3 }

这是一个较为完整的 Drogon 配置选项参考。请注意,Drogon 社区活跃,新版本可能会引入新的配置项或调整现有项。建议始终查阅官方文档以获取最全面和最新的信息。配置文件的灵活使用是构建高可用、高性能 Drogon 应用的关键步骤之一。

Appendix B: 常见错误码与故障排除

本附录旨在帮助读者快速定位和解决在使用 Drogon Web 框架开发和运行应用程序过程中可能遇到的常见问题。这些问题可能表现为程序崩溃、请求无法响应、返回错误状态码、日志中出现异常信息等。理解这些错误的原因并掌握基本的故障排除方法,对于提高开发效率和保证应用稳定性至关重要。

Appendix B1: 常见的运行时错误表现形式

在使用 Drogon 应用时,错误可能以多种形式显现。了解这些表现有助于我们更快地锁定问题范围。

HTTP 状态码错误: 客户端收到非预期的 HTTP 状态码,例如 404 Not Found(未找到资源)、500 Internal Server Error(内部服务器错误)、400 Bad Request(错误请求)等。
程序崩溃或退出: Drogon 应用在运行过程中突然停止,可能伴随核心转储(core dump)文件生成。
请求无响应或超时: 客户端发送请求后长时间没有收到响应,最终导致请求超时。
日志系统中的错误信息: Drogon 或其他集成库(如数据库驱动)在控制台或日志文件中输出错误或警告信息。
功能异常: 应用的某个特定功能未能按照预期工作,例如数据没有正确保存、返回的数据不正确等。
高 CPU 或内存占用: 应用在空闲或低负载时占用过多系统资源。

Appendix B2: 日志系统与错误信息分析

Drogon 的日志系统是故障排除的首要工具。理解如何配置和阅读日志对于诊断问题至关重要。

Appendix B21: 配置日志系统

Drogon 的日志配置通常在 config.json 文件中设置。

① 配置日志文件路径和级别。
▮▮▮▮config.json 示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 {
2 "logfile": "./logs/drogon.log",
3 "loglevel": "DEBUG"
4 }

② 常用的日志级别 (Log Level) 从低到高(详细程度从高到低)包括:
▮▮▮▮⚝ TRACE (追踪): 最详细的日志,包含函数进入/退出等信息,用于深度调试。
▮▮▮▮⚝ DEBUG (调试): 包含用于调试的详细信息,通常在开发环境中开启。
▮▮▮▮⚝ INFO (信息): 运行时重要的事件信息,例如请求处理、服务启动等。
▮▮▮▮⚝ WARN (警告): 可能导致问题但不影响程序正常运行的事件。
▮▮▮▮⚝ ERROR (错误): 运行时发生的错误,可能影响部分功能。
▮▮▮▮⚝ FATAL (致命): 导致程序终止的严重错误。

③ 建议在开发环境中将 loglevel 设置为 DEBUGTRACE 以获取更多信息;在生产环境中设置为 INFOWARN 以减少日志量,只记录重要事件和错误。

Appendix B22: 阅读与分析日志

日志条目通常包含时间戳、日志级别、线程 ID、源文件/行号以及具体的错误或信息描述。

① 识别错误级别:优先关注 ERRORFATAL 级别的日志。
② 追踪错误发生位置:日志中的源文件和行号可以帮助定位问题代码。
③ 查看上下文信息:分析错误发生前后的日志条目,理解事件序列。
④ 特定模块日志:Drogon 的不同模块(如数据库、网络)会输出各自的日志,关注与问题相关的模块日志。
⑤ 异步操作日志:在异步任务中,日志可能会分散在不同线程中,注意通过线程 ID 或请求 ID(如果应用层添加了)来关联。

Appendix B3: 常见错误类型与故障排除

本节将列举一些 Drogon 开发中常见的错误类型,并提供相应的诊断和解决思路。

Appendix B31: 配置相关错误 (Configuration Errors)

配置错误是最常见的问题之一,可能导致应用无法启动或行为异常。

端口冲突 (Port Conflict): 应用尝试监听已被其他进程占用的端口。
▮▮▮▮⚝ 表现: 应用启动失败,日志中可能包含类似 "Address already in use" 或 "bind failed" 的错误信息。
▮▮▮▮⚝ 原因: 配置文件中指定的端口已经被其他程序(包括同一应用的另一个实例)占用。
▮▮▮▮⚝ 排除:
▮▮▮▮▮▮▮▮❶ 检查 Drogon 日志,查找绑定的错误信息。
▮▮▮▮▮▮▮▮❷ 使用系统工具(如 Linux 的 netstat -tulnp 或 Windows 的 netstat -ano)查看哪个进程占用了目标端口。
▮▮▮▮▮▮▮▮❸ 修改 config.json 文件中的端口设置,或者停止占用该端口的进程。

数据库配置错误 (Database Configuration Errors): 数据库连接参数(地址、端口、用户名、密码、数据库名)不正确。
▮▮▮▮⚝ 表现: 应用启动时可能直接失败,或者在首次尝试访问数据库时报错,日志中包含数据库连接失败或认证失败等信息。
▮▮▮▮⚝ 原因: config.json 中数据库部分的配置与实际数据库信息不符。
▮▮▮▮⚝ 排除:
▮▮▮▮▮▮▮▮❶ 仔细核对 config.json 中数据库配置项(如 db_clients 数组下的 host, port, user, passwd, dbname 等)。
▮▮▮▮▮▮▮▮❷ 确保数据库服务正在运行且网络可达。
▮▮▮▮▮▮▮▮❸ 检查防火墙设置,确保 Drogon 应用可以访问数据库服务器及端口。
▮▮▮▮▮▮▮▮❹ 尝试使用第三方客户端工具(如 psql, mysql, DBeaver)连接数据库,验证配置的正确性。

静态文件或视图路径配置错误 (Static File or View Path Errors): app().setDocumentRoot() 或视图路径设置不正确。
▮▮▮▮⚝ 表现: 访问静态资源(CSS, JS, 图片)或需要渲染模板的页面时返回 404 或 500 错误。
▮▮▮▮⚝ 原因: 配置的路径与实际文件存放位置不符。
▮▮▮▮⚝ 排除:
▮▮▮▮▮▮▮▮❶ 检查 config.jsondocument_root 或代码中 app().setDocumentRoot() 设置的路径是否正确,并且该目录存在。
▮▮▮▮▮▮▮▮❷ 检查静态文件或模板文件是否确实存在于配置的目录下。
▮▮▮▮▮▮▮▮❸ 注意相对路径和绝对路径的区别,确保 Drogon 应用有权限访问这些目录。
▮▮▮▮▮▮▮▮❹ 对于视图,确保模板文件后缀与配置的模板引擎匹配。

Appendix B32: 路由与控制器错误 (Routing and Controller Errors)

与请求处理逻辑相关的错误。

404 Not Found (未找到资源): 请求的 URL 没有匹配到任何已注册的路由。
▮▮▮▮⚝ 表现: 客户端收到 404 状态码。
▮▮▮▮⚝ 原因:
▮▮▮▮▮▮▮▮❶ 请求的 URL 不存在或拼写错误。
▮▮▮▮▮▮▮▮❷ 路由没有正确注册(例如,忘记在 main.cc 中实例化控制器或调用 registerCustomApiController 等)。
▮▮▮▮▮▮▮▮❸ 路由定义中的路径或方法与实际请求不匹配。
▮▮▮▮▮▮▮▮❹ 应用了过滤器或中间件,阻止了请求到达控制器(例如,权限不足)。
▮▮▮▮⚝ 排除:
▮▮▮▮▮▮▮▮❶ 仔细检查客户端请求的 URL 和 HTTP 方法。
▮▮▮▮▮▮▮▮❷ 核对 Drogon 代码中所有注册路由的地方,确保路径、方法和处理器(函数或控制器方法)正确对应。
▮▮▮▮▮▮▮▮❸ 检查是否使用了 URL 参数或正则表达式路由,确保匹配规则正确。
▮▮▮▮▮▮▮▮❹ 暂时移除可能影响路由匹配的过滤器或中间件进行测试。

500 Internal Server Error (内部服务器错误): 请求被路由到处理器,但在处理过程中发生了未捕获的异常或严重错误。
▮▮▮▮⚝ 表现: 客户端收到 500 状态码。日志中通常会有详细的错误信息。
▮▮▮▮⚝ 原因: 控制器方法或其调用的业务逻辑代码中发生了运行时错误,例如:
▮▮▮▮▮▮▮▮❶ 野指针、访问非法内存等 C++ 运行时错误。
▮▮▮▮▮▮▮▮❷ 未捕获的异常(如数据库操作异常、文件操作异常等)。
▮▮▮▮▮▮▮▮❸ 除零错误。
▮▮▮▮▮▮▮▮❹ 在非异步上下文中执行了阻塞操作。
▮▮▮▮⚝ 排除:
▮▮▮▮▮▮▮▮❶ 查看日志!查看日志!查看日志! 这是最重要的步骤。Drogon 会尝试记录未捕获的异常信息。
▮▮▮▮▮▮▮▮❷ 使用调试器(如 GDB, LLDB)附加到运行中的 Drogon 进程,或者在开发环境中直接在 IDE 中运行并设置断点。
▮▮▮▮▮▮▮▮❸ 隔离问题代码:逐步注释掉控制器方法中的逻辑,或者简化请求处理过程,找到导致错误的具体代码段。
▮▮▮▮▮▮▮▮❹ 检查所有可能的异常抛出点,确保有适当的 try-catch 块来处理。对于 Drogon 的异步操作,确保正确使用回调或 Fiber 进行错误处理。
▮▮▮▮▮▮▮▮❺ 检查第三方库的使用是否正确,特别是数据库操作、文件 I/O 等。

Appendix B33: 数据库操作错误 (Database Operation Errors)

使用 Drogon 的 ORM 或执行原始 SQL 时可能遇到的问题。

数据库连接池耗尽 (Database Connection Pool Exhaustion): 并发请求量大,而数据库连接池大小不足或连接未正确释放。
▮▮▮▮⚝ 表现: 部分请求在尝试获取数据库连接时长时间等待甚至超时,导致请求失败或响应延迟。
▮▮▮▮⚝ 原因: 数据库连接池配置过小,或者异步操作中的数据库连接使用模式不当,导致连接长时间被占用。
▮▮▮▮⚝ 排除:
▮▮▮▮▮▮▮▮❶ 检查 config.json 中数据库配置的 conn_number (连接池大小) 设置。根据并发量和数据库性能适当增加该值。
▮▮▮▮▮▮▮▮❷ 检查代码中是否所有数据库操作都在适当的时候释放了连接(虽然 Drogon 的异步接口通常会自动管理,但在某些复杂场景或原生 SQL 使用时需注意)。
▮▮▮▮▮▮▮▮❸ 分析应用逻辑,是否存在长时间占用数据库连接的慢查询或阻塞操作。

SQL 语句错误 (SQL Syntax Errors): 原始 SQL 语句语法不正确。
▮▮▮▮⚝ 表现: 执行 SQL 语句时,数据库驱动返回错误,日志中包含 SQL 错误码和描述。
▮▮▮▮⚝ 原因: SQL 语句的关键字、表名、列名、语法格式等有误。
▮▮▮▮⚝ 排除:
▮▮▮▮▮▮▮▮❶ 仔细检查 SQL 语句的拼写和语法。
▮▮▮▮▮▮▮▮❷ 在数据库客户端中直接执行该 SQL 语句,验证其正确性。
▮▮▮▮▮▮▮▮❸ 如果是动态构建 SQL 语句,检查字符串拼接或参数绑定的逻辑。

ORM 模型生成或使用错误 (ORM Model Errors): 生成的模型类与数据库表结构不匹配,或模型方法使用不当。
▮▮▮▮⚝ 表现: 编译错误、运行时类型转换错误、查询结果不正确等。
▮▮▮▮⚝ 原因:
▮▮▮▮▮▮▮▮❶ 数据库表结构发生变化,但未重新生成 ORM 模型。
▮▮▮▮▮▮▮▮❷ 使用 ORM 方法时,字段名、类型与模型定义不符。
▮▮▮▮▮▮▮▮❸ 查询条件、联表操作等 ORM 链式调用逻辑有误。
▮▮▮▮⚝ 排除:
▮▮▮▮▮▮▮▮❶ 如果数据库表结构有更新,使用 drogons create model 命令重新生成模型类。
▮▮▮▮▮▮▮▮❷ 对比模型类的头文件 (.h) 与实际数据库表结构,确保字段名、类型、主键等一致。
▮▮▮▮▮▮▮▮❸ 仔细阅读 Drogon ORM 的文档,确保 ORM 方法的使用方式正确。

Appendix B34: 异步与并发错误 (Asynchronous and Concurrency Errors)

Drogon 的核心是异步编程,不当的使用方式可能导致复杂问题。

死锁 (Deadlock): 两个或多个任务相互等待对方释放资源,导致所有任务都无法继续执行。
▮▮▮▮⚝ 表现: 部分或所有请求无响应,服务吞吐量急剧下降。程序可能不会崩溃,但处于停滞状态。
▮▮▮▮⚝ 原因:
▮▮▮▮▮▮▮▮❶ 在同步代码中尝试获取异步操作的结果(例如,在非 Fiber 上下文中使用 std::future::get() 阻塞)。
▮▮▮▮▮▮▮▮❷ 在不同的线程或 Fiber 中以不一致的顺序获取锁或其他共享资源。
▮▮▮▮⚝ 排除:
▮▮▮▮▮▮▮▮❶ 避免在 Drogon 的事件循环线程中执行任何阻塞操作。所有耗时操作(如同步文件 I/O、长时间计算、同步调用外部服务)都应该放在单独的线程池中执行(使用 drogon::async_run 或其他机制)。
▮▮▮▮▮▮▮▮❷ 如果使用 Fiber,确保异步操作正确地挂起和恢复 Fiber。避免在 Fiber 中调用会导致阻塞的函数。
▮▮▮▮▮▮▮▮❸ 如果使用了互斥锁或其他同步机制,仔细检查锁的获取和释放顺序。考虑使用无锁数据结构或消息传递来避免共享状态。

竞态条件 (Race Condition): 多个线程或 Fiber 并发访问和修改共享数据,结果取决于访问发生的不可预测的时序。
▮▮▮▮⚝ 表现: 结果不正确、数据损坏、难以重现的偶发错误。
▮▮▮▮⚝ 原因: 未对共享的可变状态进行适当的同步保护(如互斥锁)。
▮▮▮▮⚝ 排除:
▮▮▮▮▮▮▮▮❶ 识别代码中的共享可变状态(全局变量、静态变量、通过指针/引用在多个任务间传递的对象)。
▮▮▮▮▮▮▮▮❷ 对所有访问和修改共享状态的代码块使用互斥锁 (Mutex) 或读写锁 (Read-Write Lock) 进行保护。
▮▮▮▮▮▮▮▮❸ 考虑使用线程安全的数据结构。
▮▮▮▮▮▮▮▮❹ 最小化共享状态,尽可能让数据局部化到独立的任务或 Fiber 中。

异步操作未完成导致的问题: 依赖于异步操作结果的代码在结果尚未准备好时执行。
▮▮▮▮⚝ 表现: 使用空指针、访问未初始化数据、逻辑错误。
▮▮▮▮⚝ 原因: 未正确处理异步操作的回调、Promise 或 Fiber 的挂起/恢复。
▮▮▮▮⚝ 排除:
▮▮▮▮▮▮▮▮❶ 如果使用回调,确保依赖结果的代码位于回调函数内部。
▮▮▮▮▮▮▮▮❷ 如果使用 Promise/Future,确保在获取结果之前 Future 已经变为 ready 状态。
▮▮▮▮▮▮▮▮❸ 如果使用 Fiber,确保在 co_await 异步操作后才使用其结果。理解 Fiber 的生命周期和调度机制。
▮▮▮▮▮▮▮▮❹ 仔细检查异步调用的签名和参数传递方式,特别是涉及智能指针或对象生命周期时。

Appendix B35: 内存错误 (Memory Errors)

C++ 中常见的内存问题,在 Web 服务中可能导致崩溃或性能下降。

内存泄漏 (Memory Leak): 程序未能释放不再使用的动态分配内存,导致内存占用持续增长。
▮▮▮▮⚝ 表现: 程序运行一段时间后,内存占用不断增加,最终可能导致系统变慢甚至崩溃(取决于可用内存大小)。
▮▮▮▮⚝ 原因: newmalloc 分配的内存没有对应的 deletefree 调用;智能指针使用不当(如循环引用导致无法释放)。
▮▮▮▮⚝ 排除:
▮▮▮▮▮▮▮▮❶ 优先使用智能指针 (如 std::shared_ptr, std::unique_ptr) 和 RAII (Resource Acquisition Is Initialization) 技术管理资源。
▮▮▮▮▮▮▮▮❷ 避免手动管理内存。如果必须手动分配,确保在所有可能的退出路径(包括异常)中都能正确释放。
▮▮▮▮▮▮▮▮❸ 使用内存调试工具(如 Valgrind 在 Linux 上)来检测内存泄漏。
▮▮▮▮▮▮▮▮❹ 检查 Drogon 回调或异步任务中是否持有对资源的 shared_ptr,并确保不会形成循环引用(可以使用 std::weak_ptr 打破循环)。

访问无效内存 (Accessing Invalid Memory): 访问已释放的内存 (Use-After-Free)、访问未分配的内存、栈溢出等。
▮▮▮▮⚝ 表现: 程序崩溃,通常伴随段错误 (Segmentation Fault)。
▮▮▮▮⚝ 原因:
▮▮▮▮▮▮▮▮❶ 使用悬空指针 (Dangling Pointer) 或野指针 (Wild Pointer)。
▮▮▮▮▮▮▮▮❷ 访问数组或缓冲区越界。
▮▮▮▮▮▮▮▮❸ 栈空间不足(例如,递归调用过深或在栈上分配了大型对象)。
▮▮▮▮⚝ 排除:
▮▮▮▮▮▮▮▮❶ 使用调试器定位崩溃发生的精确位置。
▮▮▮▮▮▮▮▮❷ 仔细检查所有指针的使用,特别是涉及动态分配和释放的场景。
▮▮▮▮▮▮▮▮❸ 使用地址消毒器 (Address Sanitizer, ASan) 在编译时和运行时检测内存错误。
▮▮▮▮▮▮▮▮❹ 避免在堆栈上创建过大的对象,考虑将其分配到堆上。

Appendix B36: 第三方库集成问题 (Third-Party Library Integration Issues)

集成其他 C++ 库时可能遇到的问题。

符号冲突或链接错误 (Symbol Conflicts or Linking Errors): 集成的库与 Drogon 或其依赖的库之间存在符号重定义或找不到符号的问题。
▮▮▮▮⚝ 表现: 编译失败或链接失败,错误信息类似 "symbol redefined" 或 "undefined reference to...".
▮▮▮▮⚝ 原因:
▮▮▮▮▮▮▮▮❶ 不同库使用了相同名称的函数或变量,并且这些符号被导出。
▮▮▮▮▮▮▮▮❷ 链接时库的顺序或依赖关系不正确。
▮▮▮▮▮▮▮▮❸ 使用了不同版本的同一个库,导致符号不兼容。
▮▮▮▮⚝ 排除:
▮▮▮▮▮▮▮▮❶ 检查项目的构建系统 (如 CMakeLists.txt),确保所有库都被正确地链接且顺序合理。
▮▮▮▮▮▮▮▮❷ 如果可能,尝试升级或降级冲突的库版本,或者使用静态链接/动态链接方式来避免冲突。
▮▮▮▮▮▮▮▮❸ 对于符号重定义,可以考虑使用命名空间或符号隐藏技术。

第三方库未正确初始化或使用 (Incorrect Library Initialization or Usage): 集成的库需要特定的初始化步骤或其 API 使用方式不正确。
▮▮▮▮⚝ 表现: 程序崩溃、功能异常、日志输出库的错误信息。
▮▮▮▮⚝ 原因: 未按照第三方库的要求进行初始化、配置或调用其 API。
▮▮▮▮⚝ 排除:
▮▮▮▮▮▮▮▮❶ 仔细阅读第三方库的官方文档和示例。
▮▮▮▮▮▮▮▮❷ 确保库在 Drogon 应用启动时被正确初始化,在应用关闭时被正确清理。
▮▮▮▮▮▮▮▮❸ 检查对库函数的调用参数和返回值。

Appendix B4: 故障排除流程与技巧

掌握一套系统的故障排除流程可以提高效率。

保持冷静,不要慌乱 😎。
查看日志 🪵。这是第一步,也是最重要的一步。仔细阅读日志中的错误信息、警告和上下文。
简化问题 ✂️。尝试用最小的代码重现问题。例如,创建一个只包含出问题路由的简化版应用。
使用调试器 🐛。设置断点,逐步执行代码,观察变量的值和程序执行流程。对于崩溃,分析核心转储文件。
检查最近的代码改动 🔍。问题往往出现在最近修改的代码中。回滚到上一个已知工作的版本,然后逐步应用改动,定位引入问题的具体修改。
搜索 🌐。在 Drogon 的 GitHub Issues、Stack Overflow、相关社区论坛或搜索引擎上搜索遇到的错误信息或问题描述,很可能已经有人遇到并解决了类似问题。
隔离模块 🚧。如果问题涉及多个组件(如控制器、服务、数据库),尝试分别测试每个组件,确定是哪个模块引起的错误。
验证输入与输出 ✅。检查请求的输入数据是否符合预期格式和约束;检查函数调用或异步操作的返回值和输出是否正确。
增加日志输出 ✍️。在怀疑的代码段的关键位置添加详细的日志,打印变量值、执行状态等,帮助追踪问题。
寻求帮助 🙋。如果自己无法解决,向 Drogon 社区、同事或朋友寻求帮助,提供详细的错误信息、日志和代码片段。

Appendix B5: 参考文献

⚝ Drogon 官方文档:详细的 API 参考和用户指南,通常能解答大部分疑问。
⚝ Drogon GitHub 仓库:可以查看 Issues 列表,了解已知问题和解决方案,或者提交新的问题报告。
⚝ C++ Core Guidelines:提供 C++ 编码的最佳实践,有助于编写更健壮、更少出错的代码。
⚝ 各种调试工具(GDB, LLDB, Valgrind, ASan 等)的使用手册。

掌握上述故障排除方法和技巧,将大大提升你在 Drogon 开发中的问题解决能力。记住,详细的日志和系统的排查步骤是解决问题的关键。祝你调试顺利! 👍

Appendix C: Drogon 辅助类与工具函数参考

本附录旨在详细介绍 Drogon Web 框架中提供的一些常用辅助类(Auxiliary Classes)和工具函数(Utility Functions)。这些工具通常位于 drogon::utils 命名空间下,或者作为全局函数提供,它们能帮助开发者更便捷地处理日期时间、字符串操作、编码解码、JSON 数据等常见任务,从而提高开发效率。了解并善练使用这些工具,能够使你的 Drogon 应用代码更加简洁、高效和健壮。

Appendix C1: Drogon 工具库概览

Drogon 不仅是一个高性能的 Web 框架,它还包含了一个实用的工具库,封装了许多日常开发中会用到的功能。这些工具函数和类设计简洁,通常与框架的核心功能紧密集成,同时也尽量保持独立性,方便在 Drogon 项目的各个层面使用。

工具库的主要目的包括:

⚝ 提供对常见数据类型(如日期、字符串)的便捷操作。
⚝ 集成常用的算法和编码方式(如哈希、Base64)。
⚝ 简化文件系统操作。
⚝ 支持 JSON 数据的序列化与反序列化。

这些工具函数的使用通常不需要复杂的初始化,可以直接调用,极大地便利了开发工作。

Appendix C2: 日期与时间工具

Drogon 提供了 drogon::utils::Date 类来处理日期和时间相关的操作。这个类是对底层日期时间接口的封装,提供了更易于使用的接口。

Appendix C2.1: drogon::utils::Date

drogon::utils::Date 类可以表示精确到微秒(Microsecond)的时间点。它可以用来获取当前时间、格式化时间字符串、解析时间字符串以及进行日期时间的比较和计算。

获取当前时间

可以使用静态方法 Date::date() 获取当前日期时间。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/utils/Date.h>
2 #include <iostream>
3
4 int main() {
5 drogon::utils::Date now = drogon::utils::Date::date();
6 std::cout << "Current time: " << now.toFormattedString() << std::endl;
7 return 0;
8 }

时间格式化

toFormattedString() 方法可以将 Date 对象格式化为字符串。它支持多种预定义的格式,也可以指定自定义格式。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/utils/Date.h>
2 #include <iostream>
3
4 int main() {
5 drogon::utils::Date now = drogon::utils::Date::date();
6
7 // 默认格式 (YYYY-MM-DD HH:MM:SS)
8 std::cout << "Default format: " << now.toFormattedString() << std::endl;
9
10 // HTTP Header 格式 (如: Sun, 06 Nov 1994 08:49:37 GMT)
11 std::cout << "HTTP format: " << now.toHttpString() << std::endl;
12
13 // 自定义格式 (例如 ISO 8601: YYYY-MM-DDTHH:MM:SSZ)
14 std::cout << "ISO 8601 format: " << now.toFormattedString(false, false, true) << std::endl;
15
16 return 0;
17 }

注意:toFormattedString 的布尔参数控制是否包含时间、微秒、以及是否使用 ISO 8601 格式。

时间解析

Date 类也支持从格式化的字符串解析时间,尽管不如格式化功能直接暴露为成员函数。通常,时间解析在 Drogon 内部或结合其他库进行。对于从字符串解析到 Date 对象,开发者可能需要手动使用 C++ 标准库的 <chrono><iomanip>,或依赖第三方库。Drogon 主要在处理 HTTP 头部(如 If-Modified-Since)时进行内置解析。

时间戳

Date 对象可以转换为秒级或微秒级的时间戳(Timestamp)。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/utils/Date.h>
2 #include <iostream>
3
4 int main() {
5 drogon::utils::Date now = drogon::utils::Date::date();
6
7 // 微秒时间戳 (since epoch)
8 auto micro_seconds = now.microSecondsSinceEpoch();
9 std::cout << "Microseconds since epoch: " << micro_seconds << std::endl;
10
11 // 秒时间戳 (since epoch)
12 auto seconds = now.secondsSinceEpoch();
13 std::cout << "Seconds since epoch: " << seconds << std::endl;
14
15 return 0;
16 }

时间比较与计算

Date 对象支持比较运算符(==, !=, <, <=, >, >=),以及简单的加减运算(例如,通过 secondsSinceEpoch() 进行秒级计算)。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/utils/Date.h>
2 #include <iostream>
3 #include <chrono>
4 #include <thread>
5
6 int main() {
7 drogon::utils::Date start_time = drogon::utils::Date::date();
8 std::this_thread::sleep_for(std::chrono::seconds(2));
9 drogon::utils::Date end_time = drogon::utils::Date::date();
10
11 if (end_time > start_time) {
12 std::cout << "End time is later than start time." << std::endl;
13 }
14
15 auto duration_us = end_time.microSecondsSinceEpoch() - start_time.microSecondsSinceEpoch();
16 std::cout << "Duration in microseconds: " << duration_us << std::endl;
17
18 return 0;
19 }

Appendix C3: 字符串工具

Drogon 的工具库中包含了一些实用的字符串处理函数,尽管不像一些专门的字符串库那样全面,但足以应对常见的 Web 开发场景。

Appendix C3.1: drogon::utils::StringUtil 命名空间

这个命名空间下提供了一些字符串相关的函数。

URL 编码与解码 (URL Encode & Decode)

在处理 Web 请求参数或构建 URL 时,URL 编码和解码是常见的操作。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/utils/StringUtil.h>
2 #include <iostream>
3
4 int main() {
5 std::string original = "Hello World! 你好世界!";
6 std::string encoded = drogon::utils::StringUtil::urlEncode(original);
7 std::cout << "Original: " << original << std::endl;
8 std::cout << "URL Encoded: " << encoded << std::endl;
9
10 std::string decoded = drogon::utils::StringUtil::urlDecode(encoded);
11 std::cout << "URL Decoded: " << decoded << std::endl;
12
13 return 0;
14 }

Base64 编码与解码 (Base64 Encode & Decode)

Base64 编码常用于在文本环境中传输二进制数据。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/utils/StringUtil.h>
2 #include <iostream>
3
4 int main() {
5 std::string original_data = "Some binary data \x01\x02\x03";
6 std::string base64_encoded = drogon::utils::StringUtil::base64Encode(original_data);
7 std::cout << "Original Data: " << original_data << std::endl;
8 std::cout << "Base64 Encoded: " << base64_encoded << std::endl;
9
10 std::string base64_decoded = drogon::utils::StringUtil::base64Decode(base64_encoded);
11 std::cout << "Base64 Decoded: " << base64_decoded << std::endl;
12
13 return 0;
14 }

HTML 转义与反转义 (HTML Escape & Unescape)

在渲染用户输入的文本到 HTML 页面时,为了防止跨站脚本攻击(XSS),需要对特殊字符进行 HTML 转义。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/utils/StringUtil.h>
2 #include <iostream>
3
4 int main() {
5 std::string unsafe_html = "<script>alert('XSS')</script>";
6 std::string escaped_html = drogon::utils::StringUtil::htmlEscape(unsafe_html);
7 std::cout << "Unsafe HTML: " << unsafe_html << std::endl;
8 std::cout << "Escaped HTML: " << escaped_html << std::endl;
9
10 std::string escaped_html_back = "&lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt;";
11 std::string unescaped_html = drogon::utils::StringUtil::htmlUnescape(escaped_html_back);
12 std::cout << "Escaped HTML (input): " << escaped_html_back << std::endl;
13 std::cout << "Unescaped HTML: " << unescaped_html << std::endl;
14
15 return 0;
16 }

MD5 计算

计算字符串或数据的 MD5 散列值。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/utils/StringUtil.h>
2 #include <iostream>
3
4 int main() {
5 std::string data = "Calculate MD5 hash";
6 std::string md5_hash = drogon::utils::StringUtil::md5(data);
7 std::cout << "Data: " << data << std::endl;
8 std::cout << "MD5 Hash: " << md5_hash << std::endl; // MD5 hash is typically represented as a hex string
9
10 // You might need to manually convert the result to a hex string if the function
11 // returns raw bytes, but Drogon's md5 usually returns a hex string directly.
12 // Check the exact signature in Drogon's header for confirmation.
13 // As of common versions, drogon::utils::StringUtil::md5 returns std::string.
14 return 0;
15 }

SHA1 计算

计算字符串或数据的 SHA1 散列值。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/utils/StringUtil.h>
2 #include <iostream>
3
4 int main() {
5 std::string data = "Calculate SHA1 hash";
6 std::string sha1_hash = drogon::utils::StringUtil::sha1(data);
7 std::cout << "Data: " << data << std::endl;
8 std::cout << "SHA1 Hash: " << sha1_hash << std::endl;
9
10 return 0;
11 }

Appendix C3.2: 其他字符串相关功能

除了 StringUtil,Drogon 在处理 HTTP 请求和响应时,也提供了方便的方法来访问字符串数据,例如 HttpRequestHttpResponse 对象中的头部、Cookie、请求体等都提供了字符串相关的接口。

Appendix C4: JSON 处理

Drogon 使用 JsonCpp 库来处理 JSON 数据。虽然 JsonCpp 本身是一个第三方库,但 Drogon 将其集成并提供了便捷的使用方式,特别是在处理请求体和构建响应体时。

Appendix C4.1: Json::Value

Json::Value 是 JsonCpp 库中表示 JSON 值(对象、数组、字符串、数字、布尔值、null)的核心类。Drogon 的请求体解析和响应体构建大量依赖于它。

解析 JSON 请求体

当接收到 Content-Type 为 application/json 的 POST/PUT 请求时,Drogon 会自动解析请求体并将其存储在 HttpRequest 对象中,可以通过 getJsonContentType() 方法获取解析后的 Json::Value 对象。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 在控制器中处理 POST 请求
2 void MyController::handlePost(const HttpRequestPtr& req,
3 std::function<void(const HttpResponsePtr&)>&& callback)
4 {
5 // 获取 JSON 请求体
6 auto json = req->getJsonContentType();
7 if (json) {
8 // 访问 JSON 数据
9 std::string name = (*json)["name"].asString();
10 int age = (*json)["age"].asInt();
11
12 std::cout << "Received JSON: name=" << name << ", age=" << age << std::endl;
13
14 // 构建 JSON 响应
15 Json::Value resp_json;
16 resp_json["status"] = "success";
17 resp_json["message"] = "Data received.";
18
19 auto resp = HttpResponse::newHttpJsonResponse(resp_json);
20 callback(resp);
21 } else {
22 // 请求体不是有效的 JSON
23 auto resp = HttpResponse::newHttpResponse(drogon::HttpStatusCode::k400BadRequest, drogon::ContentType::kTextPlain);
24 resp->setBody("Invalid JSON request body.");
25 callback(resp);
26 }
27 }

构建 JSON 响应体

可以使用 HttpResponse::newHttpJsonResponse() 静态方法从 Json::Value 对象创建一个 JSON 响应。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 构建 JSON 响应
2 void MyController::handleGet(const HttpRequestPtr& req,
3 std::function<void(const HttpResponsePtr&)>&& callback)
4 {
5 Json::Value data;
6 data["user_id"] = 123;
7 data["username"] = "drogon_user";
8 data["is_active"] = true;
9
10 auto resp = HttpResponse::newHttpJsonResponse(data);
11 callback(resp);
12 }

手动创建 Json::Value

你也可以在代码中手动构建复杂的 Json::Value 对象。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <json/json.h> // 需要包含 JsonCpp 头文件
2 #include <iostream>
3
4 int main() {
5 Json::Value user;
6 user["id"] = 456;
7 user["name"] = "API User";
8 user["roles"].append("admin");
9 user["roles"].append("editor");
10
11 Json::Value response;
12 response["status"] = "ok";
13 response["data"] = user;
14
15 // 可以将其转换为字符串用于调试或存储
16 Json::StreamWriterBuilder builder;
17 builder["indentation"] = " "; // 格式化输出
18 std::string json_string = Json::writeString(builder, response);
19
20 std::cout << "Generated JSON:\n" << json_string << std::endl;
21
22 return 0;
23 }

注意:使用 JsonCpp 需要在 Drogon 项目中启用 JSON 支持,并在 CMakeLists.txt 中链接 JsonCpp 库。

Appendix C5: 其他通用工具函数

Drogon 还提供了一些其他辅助函数,散布在不同的头文件中或作为全局函数。

Appendix C5.1: 配置相关的工具

虽然配置主要通过 config.json 文件管理,但 Drogon 提供了一些全局函数来访问这些配置项。

获取配置值

全局函数 drogon::app().get16BitsIntegerConfig() 等可以获取不同类型的配置值。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/drogon.h>
2 #include <iostream>
3
4 int main() {
5 // 假设 config.json 中有 "listeners": [{"port": 8848, ...}]
6 // 获取监听端口 (通常不需要手动获取,框架会自动处理)
7 // 这是示例如何访问其他自定义配置项
8 auto config = drogon::app().getConfigs();
9 if (config.isMember("custom_setting")) {
10 std::string custom_value = config["custom_setting"].asString();
11 std::cout << "Custom setting: " << custom_value << std::endl;
12 } else {
13 std::cout << "Custom setting not found." << std::endl;
14 }
15
16 // 访问特定类型的配置 (需要知道配置路径和类型)
17 // int thread_num = drogon::app().getInteger("threads_num", 0); // 示例,实际路径可能不同
18 // std::cout << "Configured threads: " << thread_num << std::endl;
19
20 return 0;
21 }

Appendix C5.2: 线程与异步相关的工具

虽然异步编程核心是事件循环和 Fiber,Drogon 也提供了一些工具函数来简化异步任务的调度。

在 I/O 线程中运行任务

drogon::app().getLoop()->runInLoop()drogon::get Loop()->runInLoop() 可以在当前或指定 I/O 线程的事件循环中运行一个任务(Task)。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/drogon.h>
2 #include <iostream>
3 #include <thread>
4
5 void myAsyncTask() {
6 std::cout << "Running task in I/O thread " << std::this_thread::get_id() << std::endl;
7 // 执行非阻塞的异步操作
8 }
9
10 int main() {
11 // 假设在主线程或其他线程中调用
12 // drogon::app().getLoop() 获取主 I/O 线程的事件循环
13 drogon::app().getLoop()->runInLoop(myAsyncTask);
14
15 // 如果在某个特定 I/O 线程内,可以直接用 drogon::getLoop()
16 // drogon::getLoop()->runInLoop(myAsyncTask);
17
18 // ... 启动 drogon 应用 ...
19 // drogon::app().run();
20 return 0;
21 }

异步执行普通函数

drogon::async_run 函数可以在一个独立的线程池中异步执行一个函数,常用于执行潜在阻塞的操作,避免阻塞 I/O 线程。这个函数返回一个 std::future,可以用来获取结果或等待任务完成。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/drogon.h>
2 #include <future>
3 #include <iostream>
4 #include <chrono>
5 #include <thread>
6
7 int blockingTask(int input) {
8 std::cout << "Executing blocking task in thread " << std::this_thread::get_id() << std::endl;
9 std::this_thread::sleep_for(std::chrono::seconds(3)); // 模拟阻塞操作
10 return input * 2;
11 }
12
13 int main() {
14 // 异步运行阻塞任务
15 std::future<int> result_future = drogon::async_run(blockingTask, 10);
16
17 std::cout << "Blocking task submitted, doing other things..." << std::endl;
18
19 // 可以在需要时获取结果,会阻塞当前线程直到任务完成
20 // int result = result_future.get();
21 // std::cout << "Blocking task result: " << result << std::endl;
22
23 // 注意:在 Drogon 控制器或 Fiber 中,通常使用 co_await 代替 get()
24 // auto result = co_await drogon::async_run(blockingTask, 10);
25
26 // ... 启动 drogon 应用 ...
27 // drogon::app().run();
28 return 0;
29 }

使用 Fiber 执行异步代码

如第 5 章所述,drogon::fiber::async_block 是在 Fiber 中执行异步代码的关键工具,它使得异步代码写起来像同步代码。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/drogon.h>
2 #include <drogon/HttpTypes.h>
3 #include <drogon/HttpRequest.h>
4 #include <drogon/HttpResponse.h>
5 #include <drogon/HttpAppFramework.h>
6 #include <drogon/utils/fiber.h>
7 #include <iostream>
8 #include <chrono>
9 #include <thread>
10
11 // 模拟一个异步操作 (例如,数据库查询或网络请求)
12 drogon::fiber::Task<int> mockAsyncOp(int value) {
13 std::cout << "Mock async op started in thread " << std::this_thread::get_id() << std::endl;
14 // 这里的 sleep 应该被真正的异步等待替换,例如使用 ORM 的异步接口
15 // std::this_thread::sleep_for(std::chrono::seconds(1)); // 不要在这里直接 sleep 阻塞 I/O 线程!
16 // 在 Drogon fiber 中,等待异步操作应该使用 co_await
17 // co_await some_drogon_async_function(...);
18 std::cout << "Mock async op finished" << std::endl;
19 co_return value * 10;
20 }
21
22 // 在 Fiber 控制器中使用
23 class FiberTestCtrl : public drogon::HttpController<FiberTestCtrl> {
24 public:
25 METHOD_LIST_BEGIN
26 METHOD_ADD(FiberTestCtrl::testFiber, "/fiber_test", Get);
27 METHOD_LIST_END
28
29 drogon::fiber::Task<void> testFiber(const drogon::HttpRequestPtr& req,
30 std::function<void(const drogon::HttpResponsePtr&)>&& callback)
31 const {
32 std::cout << "Fiber handler started in thread " << std::this_thread::get_id() << std::endl;
33
34 // 在 Fiber 中调用并等待异步操作
35 int result = co_await mockAsyncOp(5);
36
37 std::cout << "Fiber handler resumed, result: " << result << std::endl;
38
39 auto resp = drogon::HttpResponse::newHttpResponse(drogon::HttpStatusCode::k200Ok, drogon::ContentType::kTextPlain);
40 resp->setBody("Fiber test result: " + std::to_string(result));
41 callback(resp);
42 }
43 };

Appendix C5.3: 其他零散工具

Drogon 可能还包含一些其他零散的工具函数,例如用于类型转换、生成随机数、文件路径操作等。这些工具通常分散在不同的头文件中,具体使用时可以查阅 Drogon 的官方文档或源代码。

总结

Drogon 的辅助类和工具函数是框架功能的重要补充,它们覆盖了日期时间处理、字符串操作、JSON 处理、异步任务调度等多个方面。熟练掌握这些工具的使用,能够极大地提升 Drogon 应用的开发效率和代码质量。在实际开发中,遇到需要进行这些常见操作时,不妨首先查阅 Drogon 的工具库,看看是否有现成的解决方案。

Appendix D: Drogon ORM 生成器命令参考

Appendix D.1: ORM 生成器简介

欢迎来到 Drogon ORM 生成器命令参考附录!📚 在现代 Web 应用开发中,与数据库的交互是核心部分。对象关系映射(Object-Relational Mapping, ORM)技术允许开发者使用面向对象的方式来操作数据库,而不是直接编写原始的 SQL 语句,这极大地提高了开发效率和代码的可维护性。

Drogon 内置了一个轻量级且高效的 ORM 系统,它通过命令行工具 drogons 根据你的数据库 schema(模式)自动生成对应的 C++ 模型(Model)类。这些生成的类封装了数据库表的字段(Field)、数据类型以及基本的 CRUD(创建 Create、读取 Read、更新 Update、删除 Delete)操作,让你能够以更直观、更符合 C++ 编程习惯的方式与数据库打交道。

使用 Drogon ORM 生成器为你带来的主要好处包括:

提高开发效率: 自动生成大量重复性的数据访问代码,让你专注于业务逻辑。
增强类型安全: 模型类使用 C++ 的静态类型系统,可以在编译时捕获许多因类型不匹配导致的潜在错误。
提升代码可读性与可维护性: 数据库操作代码更加结构化和易于理解。
支持异步操作: 生成的模型类天然支持 Drogon 的异步数据库访问能力,避免阻塞事件循环(Event Loop)。

本附录将详细讲解 drogons create model 命令的各种选项及其用法,帮助你熟练掌握如何利用这个强大的工具来简化数据库开发。 💪

Appendix D.2: 基本命令格式

Drogon 的命令行工具 drogons 是一个多功能的助手,用于创建项目、生成文件等。生成 ORM 模型是其中的一个重要功能,通过 create model 子命令实现。

drogons create model 命令的基本格式如下:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogons create model [选项]

这个命令会连接到指定的数据库(通常是从项目的 config.json 配置文件中读取数据库连接信息,除非你通过命令行选项覆盖),读取表的 schema(模式),然后根据这些信息生成对应的 C++ 头文件(.h)和源文件(.cc)。

重要提示: 在运行此命令之前,请确保你的数据库服务器正在运行,并且 Drogon 项目的配置文件 config.json 中的数据库连接信息是正确且可访问的,或者你通过命令行提供了有效的连接信息。

Appendix D.3: 主要选项详解

drogons create model 命令提供了丰富的选项,用于控制模型生成的过程,例如指定数据库连接、选择要生成的表、定义生成的代码的命名空间和输出目录等。以下是常用选项的详细解释。

Appendix D.3.1: -o, --output-dir

用途: 指定生成的模型代码文件的输出目录。
语法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 -o <目录路径>
2 --output-dir=<目录路径>

说明: Drogon 会在这个指定的目录下创建相应的子目录(例如 models/model_name)来存放生成的模型文件。如果未指定,通常默认为当前项目的 models 目录。请确保指定的目录存在或者 Drogon 有权限创建。

Appendix D.3.2: -n, --namespace

用途: 指定生成的模型类所在的 C++ 命名空间(Namespace)。
语法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 -n <命名空间名称>
2 --namespace=<命名空间名称>

说明: Drogon 会将生成的模型类放在这个指定的命名空间下。这有助于组织代码,避免命名冲突。如果未指定,通常默认为空命名空间或者项目设定的默认命名空间。

Appendix D.3.3: -m, --models

用途: 指定要生成模型的具体模型名称列表。
语法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 -m <模型名称1> [<模型名称2> ...]
2 --models=<模型名称1>[,<模型名称2>...]

说明: 这里的“模型名称”通常是数据库表名经过转换后的 C++ 类名。如果你只想为数据库中的特定表生成模型,可以使用此选项。可以指定一个或多个模型名称,用空格或逗号分隔(取决于命令行环境)。如果未指定此选项,Drogon 通常会尝试为配置文件中指定数据库的所有表生成模型。

Appendix D.3.4: -t, --tables

用途: 指定要生成模型的具体数据库表名称列表。
语法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 -t <表名1> [<表名2> ...]
2 --tables=<表名1>[,<表名2>...]

说明:-m 类似,但 -t 直接指定数据库中的原始表名。Drogon 会根据这些表名查找并生成对应的模型。同样可以指定多个表名。通常 -t-m 任选一个即可。如果两个都指定,-m 优先级更高。

Appendix D.3.5: -d, --database

用途: 指定要连接的数据库名称。
语法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 -d <数据库名称>
2 --database=<数据库名称>

说明: 如果你的配置文件中配置了多个数据库连接,或者你想要连接一个未在配置文件中但你知道连接详情的数据库,可以使用此选项指定数据库名称。

Appendix D.3.6: -u, --user

用途: 指定连接数据库使用的用户名。
语法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 -u <用户名>
2 --user=<用户名>

说明: 覆盖配置文件中的数据库用户名设置。在某些自动化脚本中可能有用。

Appendix D.3.7: -P, --password

用途: 指定连接数据库使用的密码。
语法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 -P <密码>
2 --password=<密码>

说明: 覆盖配置文件中的数据库密码设置。请注意: 在命令行直接输入密码存在安全风险,密码可能会被记录在历史命令中。尽量通过配置文件管理敏感信息。

Appendix D.3.8: -h, --host

用途: 指定数据库服务器的主机名或 IP 地址。
语法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 -h <主机名或IP>
2 --host=<主机名或IP>

说明: 覆盖配置文件中的数据库主机设置。

Appendix D.3.9: -p, --port

用途: 指定数据库服务器的端口号。
语法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 -p <端口号>
2 --port=<端口号>

说明: 覆盖配置文件中的数据库端口设置。

Appendix D.3.10: -r, --relation

用途: 根据数据库的外键(Foreign Key)信息,生成模型之间的关系映射代码。
语法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 -r
2 --relation

说明: 当数据库表之间存在外键关系时,Drogon 的 ORM 可以通过此选项生成的代码来表示这些关系,例如一个 User 模型可能有一个方法 getPosts() 返回该用户发表的所有 Post 模型列表。这使得导航关联对象变得非常方便。强烈建议在设计好数据库关系后使用此选项。

Appendix D.3.11: -s, --suffix

用途: 为生成的模型类名称添加后缀。
语法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 -s <后缀字符串>
2 --suffix=<后缀字符串>

说明: 例如,如果表名是 users,指定 -s Model 可能会生成 UsersModel 类。这取决于 Drogon 对表名到类名的转换规则以及你指定的后缀。

Appendix D.3.12: --json-config

用途: 指定一个替代的 JSON 配置文件路径。
语法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 --json-config <文件路径>

说明: 默认情况下,Drogon 会查找当前目录或项目根目录下的 config.json。使用此选项可以指定另一个配置文件来获取数据库连接信息等。

Appendix D.3.13: --ddl

用途: 根据指定的 SQL DDL(数据定义语言)文件生成模型,而不是连接到实际数据库。
语法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 --ddl <文件路径>

说明: 如果你不想或不能连接到实际的数据库实例,但拥有描述数据库结构的 SQL 文件,可以使用此选项。Drogon 会解析该文件来生成模型。这是一个非常有用的选项,尤其是在持续集成(CI)环境或数据库尚未完全搭建好的情况下。

Appendix D.4: 使用示例

通过几个实际的例子来演示如何使用 drogons create model 命令。

Appendix D.4.1: 根据配置文件生成所有模型

假设你的 config.json 中已经正确配置了数据库连接信息,这是最简单的用法:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogons create model

这个命令会连接到配置文件中指定的数据库,并尝试为所有可访问的表生成 ORM 模型代码,存放在默认的 models 目录。

Appendix D.4.2: 指定数据库连接信息和特定表

假设你的 config.json 没有数据库配置,或者你想临时连接到另一个数据库,并且只为 usersproducts 这两张表生成模型:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogons create model -h 127.0.0.1 -p 5432 -d myapp_db -u myuser -P mypassword -t users products

这个命令通过命令行选项提供了完整的数据库连接信息,并指定了只为 usersproducts 表生成模型。

Appendix D.4.3: 指定输出目录和命名空间

如果你想将生成的模型放在项目的 src/database/models 目录下,并放置在 MyApp::Models 命名空间下:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogons create model -o src/database/models -n MyApp::Models

这个命令会根据配置文件中的数据库信息,将生成的模型代码放在 src/database/models 目录下,并且这些模型类都会在 MyApp::Models 命名空间内。

Appendix D.4.4: 生成关系映射

假设你的数据库中有 users 表和 posts 表,posts 表有一个外键指向 users 表(表示文章的作者)。使用 -r 选项可以生成关系代码:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogons create model -r

运行后,除了生成 UserPost 模型类外,User 模型可能还会生成一个方法(例如 getPosts()),通过它你可以方便地获取某个用户发表的所有文章。

Appendix D.4.5: 使用 DDL 文件生成模型

如果你有一个名为 schema.sql 的文件,其中包含了你的数据库表结构定义,可以使用 --ddl 选项生成模型,无需连接到实际数据库:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 drogons create model --ddl schema.sql

Drogon 会解析 schema.sql 文件中的 CREATE TABLE 语句,并根据解析出的表结构信息生成对应的模型代码。

Appendix D.5: 注意事项

使用 drogons create model 命令时,请注意以下几点:

数据库连接: 如果通过命令行选项指定了数据库连接信息,它们会覆盖配置文件中的设置。如果命令行和配置文件中都没有,生成器将无法工作。
依赖库: 生成器需要能够连接到指定的数据库类型。这意味着你的系统需要安装相应的数据库客户端库(例如 libpq-dev for PostgreSQL, libmysqlclient-dev for MySQL 等),并且 Drogon 在编译时需要能够找到它们。
权限: 连接数据库的用户需要有足够的权限来读取数据库的 schema 信息。
文件覆盖: 默认情况下,drogons create model 命令会覆盖已存在的同名模型文件。如果你修改了生成的模型文件,再次运行时请注意备份。
复杂的 Schema: 对于非常复杂的数据库 schema,特别是带有触发器、存储过程等非基本结构的情况,ORM 生成器可能无法完全解析或生成对应的代码。通常它主要关注表、字段、主键和外键。
数据类型映射: Drogon 会尽力将数据库的数据类型映射到 C++ 类型。某些特定的数据库类型可能需要手动调整生成的代码。
自定义逻辑: 生成的模型类提供了基本的 CRUD 操作。更复杂的业务逻辑(如跨多个表的查询、自定义聚合等)通常需要在服务层(Service Layer)或仓库层(Repository Layer)中实现,并调用模型类提供的方法。

掌握 drogons create model 命令是高效使用 Drogon ORM 的关键一步。利用好这个工具,可以大幅提升你的开发效率!👍

Appendix E: Drogon 依赖的第三方库

Appendix E1: 引言

构建任何复杂的软件框架,尤其是像 Drogon 这样高性能的 Web 框架,都离不开对现有成熟的第三方库(Third-Party Libraries)的依赖。这些库提供了底层操作系统交互、网络协议处理、数据结构、安全性等基础设施,使得框架开发者能够专注于核心业务逻辑和特定功能的实现,而无需“重复发明轮子”(Reinventing the Wheel)。

了解 Drogon 依赖的第三方库对于开发者来说非常重要。它可以帮助我们:

① 理解 Drogon 的技术栈(Technology Stack)和架构选择。
② 在安装和编译 Drogon 或基于 Drogon 的项目时,知道需要准备哪些依赖环境。
③ 在遇到编译或运行时错误时,更容易定位问题,例如是某个依赖库缺失或版本不兼容。
④ 评估框架在特定环境下的兼容性和性能潜力。

本附录将列出 Drogon 核心功能和常见扩展所依赖的主要第三方 C++ 库(Third-Party C++ Libraries),并简要说明它们的作用。请注意,具体的依赖库及其版本可能随 Drogon 的版本更新而有所变化,建议查阅您使用的 Drogon 版本的官方文档或源代码中的构建文件(例如 CMakeLists.txt)以获取最准确的信息。

Appendix E2: 核心依赖库 (Core Dependencies)

这些是 Drogon 正常工作所必需的库,它们构成了 Drogon 的核心运行时环境。

Appendix E2.1: trantor

Drogon 的核心依赖库是 trantor
▮▮▮▮⚝ 描述 (Description)trantor 是 Drogon 作者自己开发的一个基于事件循环(Event Loop)和非阻塞 I/O(Non-blocking I/O)的 C++ 网络库(Network Library)。它是 Drogon 异步(Asynchronous)和高性能的基础。trantor 提供了跨平台的 I/O 多路复用(I/O Multiplexing)封装(如 epoll, kqueue, IOCP)、定时器(Timer)、TCP 服务器(TCP Server)和客户端(TCP Client)等核心组件。
▮▮▮▮⚝ 重要性 (Importance)极高。没有 trantor,Drogon 无法构建其事件驱动(Event-Driven)的网络模型,也就无法处理 HTTP 请求和响应。

Appendix E2.2: JSON 库 (JSON Library)

Drogon 需要 JSON 库来处理 JSON 格式的请求体或构建 JSON 格式的响应。
▮▮▮▮⚝ 描述 (Description):通常是 jsoncppcjson(较轻量)。这些库负责将 JSON 字符串解析(Parse)成 C++ 对象(如 Json::Value 或自定义结构体)以及将 C++ 对象序列化(Serialize)成 JSON 字符串。
▮▮▮▮⚝ 重要性 (Importance)。在构建 RESTful API 时,JSON 是最常用的数据交换格式。虽然可以在编译时选择使用哪个 JSON 库,但至少需要其中一个。

Appendix E2.3: OpenSSL 或兼容库 (OpenSSL or Compatible)

用于实现 HTTPS 安全连接。
▮▮▮▮⚝ 描述 (Description)libssllibcrypto 是 OpenSSL 项目提供的库,用于实现 SSL/TLS 协议,处理加密(Encryption)、解密(Decryption)、证书验证(Certificate Validation)等安全相关的任务。在 Windows 上,可能是 OpenSSL 的移植版本或其他兼容的 SSL/TLS 实现。
▮▮▮▮⚝ 重要性 (Importance)中等。如果你的应用不需要支持 HTTPS,则可以禁用对 SSL 的依赖,但这在生产环境中通常是不推荐的。对于安全的 Web 应用,它是必需的

Appendix E2.4: Zlib

用于数据压缩。
▮▮▮▮⚝ 描述 (Description):一个通用的无损数据压缩库,常用于 HTTP 响应的 GZIP 压缩,以减少数据传输量。
▮▮▮▮⚝ 重要性 (Importance)中等。支持 GZIP 压缩可以提升 Web 应用的性能,但如果不需要,也可以禁用。

Appendix E2.5: UUID 库 (UUID Library)

用于生成通用唯一标识符(Universally Unique Identifier)。
▮▮▮▮⚝ 描述 (Description):通常是操作系统提供的 UUID 生成函数或第三方库(如 libuuid)。Drogon 可能在内部或某些功能中需要生成唯一的 ID。
▮▮▮▮⚝ 重要性 (Importance)较低到中等。依赖程度取决于 Drogon 具体功能的使用。

Appendix E2.6: 现代 C++ 标准库 (Modern C++ Standard Library)

Drogon 充分利用了现代 C++ 的特性。
▮▮▮▮⚝ 描述 (Description):包括 C++14, C++17, 甚至 C++20 的标准库特性,例如智能指针(Smart Pointers)、右值引用(Rvalue References)、Lambda 表达式(Lambda Expressions)、异步操作(std::future, std::promise)、STL 容器(Containers)和算法(Algorithms)、文件系统(std::filesystem)等。对于支持 C++20 纤程(Fiber)的版本,更是依赖于 <coroutine> 头文件及其相关特性。
▮▮▮▮⚝ 重要性 (Importance)极高。Drogon 是一个纯粹的 C++ 框架,严重依赖于现代 C++ 的语言特性和标准库来实现其高性能和易用性。需要一个支持相应 C++ 标准的编译器(如 GCC, Clang, MSVC)。

Appendix E3: 可选依赖库 (Optional Dependencies)

这些库只有在使用 Drogon 的特定功能时才需要。

Appendix E3.1: 数据库驱动 (Database Drivers)

使用 Drogon 的数据库模块和 ORM 功能时需要。
▮▮▮▮⚝ 描述 (Description):根据你使用的数据库类型,你需要安装相应的客户端库。
▮▮▮▮▮▮▮▮⚝ libpq: PostgreSQL 数据库的官方 C 语言客户端库。
▮▮▮▮▮▮▮▮⚝ libmysqlclientmariadb-connector-c: MySQL 或 MariaDB 的 C 语言客户端库。
▮▮▮▮▮▮▮▮⚝ sqlite3: SQLite 嵌入式数据库的 C 库。
▮▮▮▮▮▮▮▮⚝ odbc: 用于通过 ODBC (Open Database Connectivity) 连接各种数据库的通用接口库。
▮▮▮▮⚝ 重要性 (Importance)(如果你需要数据库功能)。选择和安装哪个驱动取决于你的项目使用的数据库。

Appendix E3.2: 模板引擎库 (Template Engine Libraries)

使用非内置的模板引擎时需要。
▮▮▮▮⚝ 描述 (Description):Drogon 提供了内置的模板引擎,但也可以集成第三方 C++ 模板引擎。
▮▮▮▮▮▮▮▮⚝ mustache-c: 如果你在编译 Drogon 时启用了对 Mustache 模板引擎的支持,则需要这个库。
▮▮▮▮⚝ 重要性 (Importance)中等。取决于你是否使用 Drogon 的视图渲染功能以及选择哪个模板引擎。

Appendix E3.3: 日志库 (Logging Library)

Drogon 有内置日志系统,但也可以集成第三方库。
▮▮▮▮⚝ 描述 (Description):虽然 Drogon 内置了日志功能,但一些用户可能偏好功能更丰富的第三方日志库。
▮▮▮▮▮▮▮▮⚝ spdlog: 一个快速、仅头文件(Header-only)的 C++ 日志库。Drogon 支持集成 spdlog 作为其日志后端。
▮▮▮▮⚝ 重要性 (Importance)较低。Drogon 内置日志系统已经足够满足大多数基本需求。

Appendix E3.4: 构建工具 (Build Tools)

虽然不是运行时依赖,但对于开发过程至关重要。
▮▮▮▮⚝ 描述 (Description)CMake 是一个跨平台的构建系统生成工具(Build System Generator)。Drogon 使用 CMake 来管理项目的编译过程。
▮▮▮▮⚝ 重要性 (Importance)极高(对于构建过程)。编译 Drogon 或 Drogon 项目必需

Appendix E3.5: 测试框架 (Testing Framework)

用于编写和运行单元测试和集成测试。
▮▮▮▮⚝ 描述 (Description):虽然不属于 Drogon 框架本身的运行时依赖,但在开发过程中对代码进行测试时非常有用。
▮▮▮▮▮▮▮▮⚝ googletest: Google 提供的 C++ 测试框架,广泛用于 C++ 项目的单元测试和集成测试。Drogon 自身的测试以及许多 Drogon 项目的测试都使用 googletest
▮▮▮▮⚝ 重要性 (Importance)(对于保证代码质量)。强烈推荐在项目中集成测试框架。

Appendix E4: 依赖管理与安装

安装 Drogon 及其依赖通常通过以下方式完成:

使用系统包管理器 (System Package Managers): 在 Linux (apt, yum, dnf) 或 macOS (brew) 上,可以直接安装大部分核心和可选依赖库的开发包(通常包名以 -dev-devel 结尾)。例如,安装 PostgreSQL 驱动可能是 libpq-dev
使用 C++ 包管理器 (C++ Package Managers): vcpkgconan 等工具可以帮助你在不同平台(包括 Windows)上获取和管理 C++ 依赖库。Drogon 的官方文档通常会提供使用 vcpkg 安装的指南。
从源代码编译 (Compiling from Source): 对于某些特定版本或定制需求,可能需要从第三方库的源代码手动编译安装。
CMake 自动查找: 在编译 Drogon 项目时,CMake 会尝试查找系统上已安装的依赖库。如果找不到,编译过程会失败并提示缺失的库。你可以通过 CMake 的参数指定库的安装路径,或者确保库已经安装在标准的系统路径下。

理解这些依赖库的作用和获取方式,将有助于更顺畅地搭建开发环境和解决潜在的构建问题。

Appendix F: C++20 在 Drogon 中的应用实例(Application of C++20 Features in Drogon)

随着 C++ 标准的不断演进,现代 C++ 特性为开发者提供了强大的工具,能够编写更高效、更安全、更易维护的代码。Drogon 作为一款拥抱现代 C++ 的 Web 框架,能够充分利用 C++20 带来的新特性,极大地提升开发体验和代码质量。本附录将通过代码片段和实例,展示一些关键的 C++20 特性,如概念(Concepts)、范围(Ranges)、协程(Coroutines/Fiber)和模块(Modules)等,如何在 Drogon 项目开发中得到应用。

Appendix F1: C++20 特性简介及其价值

C++20 是 C++ 语言的一个重要版本,引入了许多革命性的特性。对于 Drogon 这样的高性能框架以及基于它构建的应用来说,这些特性能够带来显著的好处:

提升代码清晰度与表达力: 新的语言特性如 Concepts 和 Ranges 使得代码意图更加明确。
增强编译时检查: Concepts 可以在编译时对模板参数进行约束,减少运行时错误。
简化异步编程: Coroutines 彻底改变了异步代码的编写方式,使其看起来像同步代码,极大提高了可读性和可维护性。Drogon 的 Fiber 正是基于 C++20 协程构建的。
改善编译效率与封装性: Modules 有潜力替代传统的头文件包含方式,加快编译速度并提供更好的封装。
提供更安全高效的标准库工具:std::spanstd::jthread 等,使得内存操作更安全,线程管理更便捷。

将这些特性应用到 Drogon 项目中,不仅能写出更现代的 C++ 代码,也能更好地发挥 Drogon 本身的高性能优势。

Appendix F2: 概念(Concepts)在模板约束中的应用

概念(Concepts)是 C++20 引入的一项强大特性,用于在编译时约束模板参数,使得模板错误信息更友好,代码意图更清晰。在 Drogon 项目中,我们可能会编写一些泛型工具函数或类,用于处理不同类型的数据,这时 Concepts 就非常有用。

假设我们需要编写一个通用的函数来处理用户输入,并且这个函数只接受可以被转换为字符串的类型。在 C++20 之前,我们可能需要依赖 SFINAE(Substitution Failure Is Not An Error)或其他技术,代码会比较复杂且错误信息难懂。使用 Concepts,我们可以直接表达这个约束:

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <string>
2 #include <concepts>
3 #include <iostream>
4
5 // 定义一个概念:IsConvertibleToString,要求类型 T 可以被转换为 std::string
6 template<typename T>
7 concept IsConvertibleToString = std::convertible_to<T, std::string>;
8
9 // 这是一个 Drogon 项目中的辅助函数,用于安全地获取请求参数并转为字符串
10 // 使用 Concept 约束,明确指定只有可转换为字符串的类型才能作为参数
11 template<IsConvertibleToString T>
12 std::string get_param_as_string(const T& param)
13 {
14 return std::string(param);
15 }
16
17 // 在 Drogon 控制器中的一个 hypothetical 用例
18 /*
19 void MyController::handle_request(...) {
20 // 假设这里获取到一个请求参数,可能是int、double或string等
21 int user_id = 123;
22 double price = 99.99;
23 std::string status = "active";
24
25 // 使用带 Concept 约束的函数
26 std::string id_str = get_param_as_string(user_id); // OK
27 std::string price_str = get_param_as_string(price); // OK
28 std::string status_str = get_param_as_string(status); // OK
29
30 // 如果尝试传递一个不可转换为字符串的类型(例如自定义的复杂对象,未定义stream或转换函数)
31 // struct ComplexObject {};
32 // ComplexObject obj;
33 // std::string obj_str = get_param_as_string(obj); // <-- 编译错误,并给出友好的 Concept 错误信息
34
35 // ... Drogon response handling ...
36 }
37 */
38
39 // 示例:一个简单的 Concept 和函数使用
40 void concept_example() {
41 int i = 42;
42 double d = 3.14;
43 std::string s = "hello";
44
45 std::cout << "Int as string: " << get_param_as_string(i) << std::endl;
46 std::cout << "Double as string: " << get_param_as_string(d) << std::endl;
47 std::cout << "String as string: " << get_param_as_string(s) << std::endl;
48 }

在这个例子中,IsConvertibleToString 概念明确表达了函数 get_param_as_string 对模板参数 T 的要求。如果传入的类型不满足这个概念(即不能转换为 std::string),编译器会直接报告一个清晰的错误,而不是一长串难以理解的 SFINAE 失败日志。这在编写通用的辅助函数、ORM 查询构建器(query builder)或任何涉及泛型处理的代码时,都能显著提高代码的可读性和可维护性。

Appendix F3: 范围(Ranges)简化数据处理

C++20 的范围(Ranges)库提供了一种新的方式来处理元素序列,使得对容器或范围进行转换、过滤、组合等操作变得更加简洁和富有表现力,避免了嵌套的循环和迭代器操作。在 Drogon 应用中,这可以用于处理从数据库 ORM 查询结果集、处理请求中的多值参数、或在业务逻辑中操作集合数据。

假设我们从 Drogon 的 ORM 查询得到一个用户列表,我们想筛选出活跃(is_active == true)用户的 ID,并将它们收集到一个新的向量中。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <vector>
2 #include <string>
3 #include <ranges>
4 #include <iostream>
5 #include <numeric>
6
7 // 假设这是从 ORM 查询得到的 User 模型列表
8 struct User {
9 int id;
10 std::string name;
11 bool is_active;
12 };
13
14 std::vector<User> get_all_users_from_db() {
15 // 模拟从数据库获取数据
16 return {
17 {1, "Alice", true},
18 {2, "Bob", false},
19 {3, "Charlie", true},
20 {4, "David", true},
21 {5, "Eve", false}
22 };
23 }
24
25 // 在 Drogon 控制器或服务中的 hypothetical 用例
26 /*
27 void MyService::process_active_users() {
28 std::vector<User> users = get_all_users_from_db();
29
30 // 使用 C++20 Ranges 筛选活跃用户并提取 ID
31 std::vector<int> active_user_ids;
32 // 构建一个视图 (view):先过滤,再转换
33 auto active_ids_view = users | std::views::filter([](const User& u){
34 return u.is_active;
35 })
36 | std::views::transform([](const User& u){
37 return u.id;
38 });
39
40 // 将视图的结果收集到向量中
41 for (int id : active_ids_view) {
42 active_user_ids.push_back(id);
43 }
44 // 或者使用 ranges::to (C++23) 或 ranges::copy (C++20)
45 // std::vector<int> active_user_ids = active_ids_view | std::ranges::to<std::vector>(); // C++23
46 // std::ranges::copy(active_ids_view, std::back_inserter(active_user_ids)); // C++20
47
48 // 打印结果
49 std::cout << "Active User IDs:";
50 for (int id : active_user_ids) {
51 std::cout << " " << id;
52 }
53 std::cout << std::endl;
54
55 // ... rest of the logic ...
56 }
57 */
58
59 // 示例:一个简单的 Ranges 使用
60 void ranges_example() {
61 std::vector<User> users = get_all_users_from_db();
62
63 // 使用 C++20 Ranges 筛选活跃用户并提取 ID
64 std::vector<int> active_user_ids;
65 auto active_ids_view = users | std::views::filter([](const User& u){
66 return u.is_active;
67 })
68 | std::views::transform([](const User& u){
69 return u.id;
70 });
71
72 for (int id : active_ids_view) {
73 active_user_ids.push_back(id);
74 }
75
76 std::cout << "Active User IDs:";
77 for (int id : active_user_ids) {
78 std::cout << " " << id;
79 }
80 std::cout << std::endl;
81 }

Ranges 库通过管道操作符 | 将不同的视图适配器(view adapter)链式组合起来,表达了对数据流的操作过程(过滤、转换等),代码比传统的基于迭代器和循环的方式更加直观和易于理解。这对于处理 ORM 查询结果、解析复杂的请求参数列表等场景非常有帮助。

Appendix F4: 协程(Coroutines)与 Drogon 纤程(Fiber)

C++20 协程(Coroutines)是 Drogon 实现其高性能异步处理能力的核心基石之一,尤其体现在 Drogon 的纤程(Fiber)上。尽管 Chapter 5 已经详细介绍了 Drogon 的异步模型和 Fiber 的使用,这里我们再次强调 C++20 协程提供的语言层面的支持,以及它如何让异步代码的编写变得如同同步代码一样自然。

在 Drogon 中,标记为 [[drogon::fiber_async]] 的 HTTP 控制器方法实际上就是 C++20 协程。在这样的方法内部,当遇到一个异步操作(如数据库查询、发送 HTTP 请求到第三方服务),我们可以使用 co_await 关键字。co_await 会将当前的协程(Fiber)挂起,将控制权交还给 Drogon 的事件循环线程,直到异步操作完成。操作完成后,事件循环会负责恢复(resume)对应的协程,继续执行 co_await 之后的代码。

这种模式避免了传统回调函数带来的“回调地狱”(callback hell),也比基于 Future/Promise 的链式调用更具线性可读性。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <drogon/HttpController.h>
2 #include <drogon/drogon.h>
3 #include <drogon/orm/Mapper.h>
4 #include <drogon/orm/Result.h>
5 #include <drogon/HttpClient.h>
6 #include <iostream>
7 #include <string>
8
9 // 假设我们有一个 User 模型 (由 drogons create model 生成)
10 // namespace drogon::orm { class User; }
11
12 class UserApi : public drogon::HttpController<UserApi>
13 {
14 public:
15 METHOD_LIST_BEGIN
16 // 定义一个处理 GET /users/{id} 的异步方法,使用 Fiber (基于 C++20 Coroutine)
17 METHOD_ADD(__fiber_get_user_by_id, "/users/{id}", drogon::Get);
18 METHOD_ADD(__sync_get_user_by_id, "/users/sync/{id}", drogon::Get); // 对比用的同步方法
19 METHOD_LIST_END
20
21 // 使用 [[drogon::fiber_async]] 标记,表示这是一个基于 C++20 Coroutine 的异步方法
22 // 在方法内部可以使用 co_await
23 [[drogon::fiber_async]]
24 void __fiber_get_user_by_id(const drogon::HttpRequestPtr& req,
25 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
26 int user_id) const;
27
28 // 对比用的传统同步方法,不使用 Fiber
29 void __sync_get_user_by_id(const drogon::HttpRequestPtr& req,
30 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
31 int user_id) const;
32 };
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // UserApi.cc 实现文件示例 (简化)
2 #include "UserApi.h"
3 #include <drogon/orm/Mapper.h>
4 #include <drogon/orm/Result.h>
5 #include <drogon/HttpClient.h>
6 #include <drogon/orm/Criteria.h>
7 #include <drogon/HttpTypes.h>
8 #include <drogon/utils/coroutine.h> // 包含 Fiber 相关的头文件
9 #include <json/json.h> // 假设使用 jsoncpp
10
11 // 假设 User 模型定义如下 (为了示例,省略了ORM生成代码)
12 namespace drogon::orm {
13 class User {
14 public:
15 int getValueOfId() const { return id_; }
16 std::string getValueOfName() const { return name_; }
17 bool getValueOfIsActive() const { return is_active_; }
18 // ... 其他字段和方法
19 private:
20 int id_;
21 std::string name_;
22 bool is_active_;
23 // ... 其他成员
24 };
25 // 假设 Mapper<User> 可以通过 app().getMapper<User>() 获取
26 }
27
28 // Fiber 异步方法实现
29 [[drogon::fiber_async]]
30 void UserApi::__fiber_get_user_by_id(const drogon::HttpRequestPtr& req,
31 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
32 int user_id) const
33 {
34 auto resp = drogon::HttpResponse::newHttpResponse();
35 resp->addHeader("Content-Type", "application/json");
36
37 try {
38 // 使用 ORM 进行异步查询,这里可以直接 co_await
39 // Drogon ORM 的 findOneAsync 方法返回一个 awaitable 对象
40 drogon::orm::Mapper<drogon::orm::User> mapper = drogon::app().getMapper<drogon::orm::User>();
41 auto user_opt = co_await mapper.findOneAsync(drogon::orm::Criteria(user_id, drogon::orm::CompareOperator::EQ));
42
43 if (user_opt) {
44 auto user = user_opt.value();
45 Json::Value user_json;
46 user_json["id"] = user.getValueOfId();
47 user_json["name"] = user.getValueOfName();
48 user_json["is_active"] = user.getValueOfIsActive(); // 假设有这些方法
49 resp->setStatusCode(drogon::k200OK);
50 resp->setBody(user_json.toStyledString());
51 } else {
52 resp->setStatusCode(drogon::k404NotFound);
53 resp->setBody("{\"error\":\"User not found\"}");
54 }
55
56 } catch (const drogon::orm::DrogonDbException& e) {
57 // 数据库异常处理
58 resp->setStatusCode(drogon::k500InternalServerError);
59 resp->setBody("{\"error\":\"Database error: " + std::string(e.base().what()) + "\"}");
60 } catch (const std::exception& e) {
61 // 其他异常处理
62 resp->setStatusCode(drogon::k500InternalServerError);
63 resp->setBody("{\"error\":\"Server error: " + std::string(e.what()) + "\"}");
64 }
65
66 // co_await 结束后,代码继续执行,最后发送响应
67 callback(resp);
68 }
69
70 // 传统同步方法实现 (注意:同步方法中执行阻塞操作会阻塞事件循环线程!)
71 // 这个方法仅用于对比,不推荐在生产环境的控制器中直接执行阻塞DB操作
72 void UserApi::__sync_get_user_by_id(const drogon::HttpRequestPtr& req,
73 std::function<void(const drogon::HttpResponsePtr&)>&& callback,
74 int user_id) const
75 {
76 auto resp = drogon::HttpResponse::newHttpResponse();
77 resp->addHeader("Content-Type", "application/json");
78
79 try {
80 // 使用 ORM 进行同步查询 (注意:这是一个阻塞调用!)
81 drogon::orm::Mapper<drogon::orm::User> mapper = drogon::app().getMapper<drogon::orm::User>();
82 auto user_opt = mapper.findOne(drogon::orm::Criteria(user_id, drogon::orm::CompareOperator::EQ)); // 这是一个阻塞调用!
83
84 if (user_opt) {
85 auto user = user_opt.value();
86 Json::Value user_json;
87 user_json["id"] = user.getValueOfId();
88 user_json["name"] = user.getValueOfName();
89 user_json["is_active"] = user.getValueOfIsActive();
90 resp->setStatusCode(drogon::k200OK);
91 resp->setBody(user_json.toStyledString());
92 } else {
93 resp->setStatusCode(drogon::k404NotFound);
94 resp->setBody("{\"error\":\"User not found\"}");
95 }
96
97 } catch (const drogon::orm::DrogonDbException& e) {
98 // 数据库异常处理
99 resp->setStatusCode(drogon::k500InternalServerError);
100 resp->setBody("{\"error\":\"Database error: " + std::string(e.base().what()) + "\"}");
101 } catch (const std::exception& e) {
102 // 其他异常处理
103 resp->setStatusCode(drogon::k500InternalServerError);
104 resp->setBody("{\"error\":\"Server error: " + std::string(e.what()) + "\"}");
105 }
106
107 // 发送响应
108 callback(resp);
109 }

在这个异步示例中,co_await mapper.findOneAsync(...) 是关键。当 Drogon 的 ORM 异步方法开始执行数据库查询时,co_await 会暂停当前的 __fiber_get_user_by_id 协程。这个协程的状态会被保存,执行线程可以去处理其他请求或事件。当数据库查询完成并通过事件循环通知 Drogon 时,之前挂起的协程会被恢复,从 co_await 暂停的地方继续执行。整个过程对开发者来说,代码是线性直观的,极大地简化了异步逻辑的编写和理解。

Appendix F5: 模块(Modules)在项目组织中的潜力

C++20 模块(Modules)旨在替代传统的头文件(header file)包含机制。模块具有更好的封装性(只导出明确指定的内容)和更快的编译速度(不需要重复解析头文件)。对于大型 Drogon 项目,使用模块来组织自己的业务代码或公共工具库,可以改善编译时间并减少命名冲突的风险。

尽管 Drogon 框架本身可能出于兼容性考虑仍主要使用头文件,但在你的项目代码中引入模块是完全可行的。

考虑一个简单的例子,我们有一个自定义的辅助函数模块 my_utilities

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 文件名: my_utilities.ixx (模块接口单元)
2 export module my_utilities;
3
4 import <string>; // 导入标准库模块
5
6 // 导出模块中的函数
7 export std::string capitalize(const std::string& s);
8
9 // 导出模块中的类 (如果需要)
10 // export class MyHelper;
11
12 // ... 其他导出的内容
1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 文件名: my_utilities.cpp (模块实现单元)
2 module my_utilities;
3
4 import <cctype>; // 导入其他标准库模块
5
6 std::string capitalize(const std::string& s) {
7 std::string result = s;
8 if (!result.empty()) {
9 result[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(result[0])));
10 }
11 return result;
12 }
13
14 // ... MyHelper 类实现等

在你的 Drogon 项目的 .cc 文件中,如果你想使用 capitalize 函数,不是通过 #include "my_utilities.h",而是通过 import

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 // 文件名: src/controllers/MyController.cc
2 #include "MyController.h" // Drogon 生成的头文件,通常仍然使用头文件
3 #include <drogon/drogon.h>
4 #include <string>
5
6 import my_utilities; // 导入自定义的 my_utilities 模块
7
8 void MyController::handle_some_request(const drogon::HttpRequestPtr& req,
9 std::function<void(const drogon::HttpResponsePtr&)>&& callback) const
10 {
11 // ... 获取请求参数 ...
12 std::string input_string = req->getParameter("text");
13
14 // 使用从模块导入的函数
15 std::string capitalized_string = my_utilities::capitalize(input_string); // 或者 using namespace my_utilities;
16
17 // ... 构建响应 ...
18 auto resp = drogon::HttpResponse::newHttpJsonResponse(drogon::move(capitalized_string));
19 callback(resp);
20 }

使用模块需要编译器支持(现代 C++ 编译器如 GCC, Clang, MSVC 都已支持 C++20 Modules),并且构建系统(如 CMake)也需要进行相应的配置来处理模块。尽管初期配置可能需要一些学习成本,但对于大型复杂项目,模块带来的编译速度提升和代码组织优势是值得考虑的。在 Drogon 项目中,你可以逐步将自己的业务逻辑或辅助库迁移到模块化结构。

Appendix F6: 其他有益的 C++20 特性

除了 Concepts, Ranges, Coroutines/Fiber, Modules 之外,C++20 还引入了许多其他有用的特性,可以在 Drogon 开发中提升代码质量和效率:

三向比较运算符 (\( <=> \),"宇宙飞船运算符"):
提供了更简洁的方式来实现对象的比较(小于、等于、大于)。这对于在 Drogon ORM 模型中定义自定义排序规则或在业务逻辑中比较对象状态非常方便。

std::span
提供了一个非拥有的、安全的内存区域视图。可以在处理 HTTP 请求体(Request Body)的字节数据或在不同函数间传递数组/向量的一部分时使用,避免不必要的内存复制和裸指针的风险。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <vector>
2 #include <span> // C++20
3 #include <iostream>
4
5 // 假设从 HttpRequest 中获取到的原始字节数据
6 std::vector<char> request_body_data = {'{', '"', 'n', 'a', 'm', 'e', '"', ':', '"', 'T', 'e', 's', 't', '"', '}'};
7
8 // 使用 std::span 安全地传递数据视图
9 void process_data_span(std::span<const char> data) {
10 std::cout << "Processing data (size: " << data.size() << "): ";
11 for (char c : data) {
12 std::cout << c;
13 }
14 std::cout << std::endl;
15 }
16
17 void span_example() {
18 // 在 Drogon 请求处理中调用
19 // HttpRequestPtr req = ...;
20 // std::vector<char> body = req->getBody();
21 process_data_span(request_body_data); // std::vector 可以隐式转换为 std::span
22 }

std::jthread
一个改进版的 std::thread,它会在销毁时自动加入(join),避免了程序意外终止的问题。这对于在 Drogon 应用中启动一些不属于主事件循环的后台任务(如定时任务、独立的服务消费者)非常有用。

1.双击鼠标左键复制此行;2.单击复制所有代码。
                                
                                    
1 #include <jthread> // C++20
2 #include <iostream>
3 #include <chrono>
4
5 void background_task(std::stop_token st) {
6 std::cout << "Background task started." << std::endl;
7 while (!st.stop_requested()) {
8 std::cout << "Background task running..." << std::endl;
9 std::this_thread::sleep_for(std::chrono::seconds(1));
10 }
11 std::cout << "Background task stopped." << std::endl;
12 }
13
14 // 在 Drogon 应用程序启动时可以创建 std::jthread
15 /*
16 int main() {
17 // ... Drogon setup ...
18 drogon::app().addListener("0.0.0.0", 8080);
19
20 // 启动一个后台线程
21 std::jthread worker(background_task);
22
23 // 运行 Drogon 应用,这会阻塞主线程
24 drogon::app().run();
25
26 // 当 app().run() 退出时 (例如收到终止信号)
27 // worker jthread 会在其析构时自动调用 join()
28 // 也可以在退出前手动请求停止: worker.request_stop();
29
30 std::cout << "Drogon app stopped, jthread joined." << std::endl;
31
32 return 0;
33 }
34 */

概念库中的其他概念:
std::totally_ordered (全序)、std::regular (正则类型) 等,可以用来约束算法或数据结构的模板参数。

将这些 C++20 特性融入到你的 Drogon 开发实践中,可以帮助你编写出更符合现代 C++ 规范、性能更优、代码更易于理解和维护的应用程序。

Appendix F7: 总结与进阶

C++20 为现代 C++ 开发带来了革命性的变化,而 Drogon 框架作为 C++ 世界的高性能 Web 解决方案,能够充分利用这些新特性。通过 Concepts 提升模板代码的健壮性和可读性;利用 Ranges 简化序列数据处理;借助 C++20 Coroutines(Drogon 中的 Fiber 实现)彻底改变异步编程范式;以及探索 Modules 在项目组织和编译优化方面的潜力,开发者可以构建出更强大、更高效、更易于维护的 Drogon 应用。

要深入掌握这些 C++20 特性,建议阅读 C++ 相关的权威书籍和在线资源。理解这些特性的底层原理和最佳实践,将有助于你在 Drogon 开发中做出更明智的设计决策,并编写出高质量的 C++ 代码。

\[ \text{现代 C++ 开发实践} \implies \text{充分利用 C++20 特性} \implies \text{构建高性能、可维护的 Drogon 应用} \]

<END_OF_CHAPTER/>