diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..1fbb4bf4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,109 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +cpp-tbox (C++ Treasure Box) is a Reactor-based service development framework and component library for Linux, targeting intelligent hardware, edge computing, and backend services. Version 1.13.x, C++11, MIT license. + +## Build Commands + +### GNU Make (primary) +```bash +# Build everything (3rd-party + all modules in config.mk) +make 3rd-party modules RELEASE=1 + +# Build with ASAN (recommended, enables memory leak checking) +make 3rd-party modules ENABLE_ASAN=1 + +# Build and run tests for all enabled modules +make test ENABLE_ASAN=1 +make run_test ENABLE_ASAN=1 # executes each module's test binary + +# Build single module only +make -C modules/base ENABLE_ASAN=1 + +# Build and test single module +make -C modules/base test ENABLE_ASAN=1 + +# Clean +make clean # rm .build +make distclean # rm .build .staging .install + +# Custom staging directory +make 3rd-party modules RELEASE=1 STAGING_DIR=$HOME/.tbox +``` + +> **Note:** Always add `ENABLE_ASAN=1` when building with make to enable AddressSanitizer for memory leak detection. + +### CMake (alternative) +```bash +cmake -B build +cmake --build build +cmake --install build # default: /usr/local +cmake --install build --prefix=$HOME/.tbox # custom prefix + +# Run tests +cd build && ctest +``` + +### Module selection +Edit `config.mk` to enable/disable modules. Add `MODULES += xxx` or comment it out. Core modules (base, util, event, eventx, log, network, terminal, trace, coroutine, main, run) are always enabled. + +## Testing + +Each module has a `xxx_test.cpp` alongside `xxx.cpp`. Tests use gmock/gtest framework. Test binaries are built into `.build//test`. + +Run a single module's test: `make -C modules/ test ENABLE_ASAN=1 && .build//test` + +## Architecture + +### Module dependency hierarchy (bottom-up) +- **base** → standalone (logging macros, backtrace, json, scope\_exit, cabinet, object\_pool) +- **util** → depends on base (argument\_parser, fs, variables, etc.) +- **event** → depends on base (Loop, FdEvent, TimerEvent, SignalEvent; engines: epoll, select) +- **eventx** → depends on event (ThreadPool, TimerPool, Async, WorkThread) +- **log** → depends on event (stdout/syslog/filelog output channels with async frontend-backend model) +- **network** → depends on event+log (serial, terminal, UDP, TCP) +- **terminal** → depends on event+network (shell-like command terminal for runtime interaction) +- **trace** → depends on event (function execution timing recorder, exports icicle diagrams) +- **coroutine** → depends on event (Scheduler, coroutine for sequential async flows) +- **main** → depends on event+eventx+terminal+coroutine (Module lifecycle framework, Context provides loop/thread\_pool/timer\_pool/terminal/coroutine) +- **run** → depends on main (ELF executable that loads lib\*.so modules via `-l` parameter) +- **http/mqtt/flow/alarm/crypto/dbus/jsonrpc** → optional, depend on various core modules + +### Module lifecycle +Every `tbox::main::Module` follows: **construct → onInit() → onStart() → [running] → onStop() → onCleanup() → destruct**. Modules support parent-child composition via `add()`. Required children must succeed; optional children can fail without blocking the app. + +### Reactor pattern +`event::Loop` is the core. Main thread runs `loop->runLoop(Mode::kForever)` handling non-blocking IO/timer/signal events. Cross-thread delegation via `runInLoop()` (thread-safe) or `runNext()` (loop-thread only, faster). `run()` auto-selects based on thread context. + +### Logging +`LogFatal/LogErr/LogWarn/LogNotice/LogImportant/LogInfo/LogDbg/LogTrace` macros defined in `base/log.h`. Each module defines `MODULE_ID` (e.g. `"tbox.base"`) which becomes the log module identifier. Log levels prefixed with `TBOX_LOG_LEVEL_` to avoid conflicts with other libraries. + +### How to create a new app +1. Derive from `tbox::main::Module`, implement `onInit/onStart/onStop/onCleanup` +2. In `main.cpp`, implement `RegisterApps()`, `GetAppDescribe()`, `GetAppBuildTime()`, `GetAppVersion()` +3. Call `tbox::main::Main(argc, argv)` — or use `run` executable with `-l your_lib.so` + +## Code Style + +Follow Google C++ style with these exceptions: +- Source file extension: `.cpp` (not `.cc`) +- Indentation: 4 spaces +- Member function naming: `aaaBbb()` (lowerCamelCase) +- Static function naming: `AaaBbb()` (UpperCamelCase) +- Static variable naming: `_xxx_` (underscore-prefixed-and-suffixed) +- Static variable (local): `_xxx` (underscore-prefixed) +- Avoid smart pointers unless necessary +- File format: Unix, encoding: UTF-8 +- Chinese comments are common and expected + +## File header + +Every source file starts with the ASCII art logo block + copyright notice. Preserve it when modifying files. + +## PR conventions + +- PRs go to `develop` branch, not `master` +- New components must include: `.cpp`, `.h`, `_test.cpp`, and a sample/example diff --git a/CMakeLists.txt b/CMakeLists.txt index ecd55044..cc19805a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,12 +75,14 @@ option(TBOX_ENABLE_EVENT "build event" ON) option(TBOX_ENABLE_EVENTX "build eventx" ON) option(TBOX_ENABLE_LOG "build log" ON) option(TBOX_ENABLE_NETWORK "build network" ON) +option(TBOX_ENABLE_NETWORK_TLS "build network_tls" ON) option(TBOX_ENABLE_TERMINAL "build terminal" ON) option(TBOX_ENABLE_TRACE "build trace" ON) option(TBOX_ENABLE_MAIN "build main" ON) option(TBOX_ENABLE_COROUTINE "build coroutine" ON) option(TBOX_ENABLE_HTTP "build http" ON) +option(TBOX_ENABLE_WEBSOCKET "build websocket" ON) option(TBOX_ENABLE_MQTT "build mqtt" ON) option(TBOX_ENABLE_FLOW "build flow" ON) option(TBOX_ENABLE_ALARM "build alarm" ON) @@ -153,6 +155,11 @@ if(TBOX_ENABLE_NETWORK) list(APPEND TBOX_COMPONENTS network) endif() +if(TBOX_ENABLE_NETWORK_TLS) + message(STATUS "network_tls module enabled") + list(APPEND TBOX_COMPONENTS network_tls) +endif() + if(TBOX_ENABLE_TERMINAL) message(STATUS "terminal module enabled") list(APPEND TBOX_COMPONENTS terminal) @@ -178,6 +185,11 @@ if(TBOX_ENABLE_HTTP) list(APPEND TBOX_COMPONENTS http) endif() +if(TBOX_ENABLE_WEBSOCKET) + message(STATUS "websocket module enabled") + list(APPEND TBOX_COMPONENTS websocket) +endif() + if(TBOX_ENABLE_MQTT) message(STATUS "mqtt module enabled") list(APPEND TBOX_COMPONENTS mqtt) diff --git a/README.md b/README.md index cb7fe872..c8d98c5d 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,8 @@ It contains an event-driven behavior tree that can realize sequential, branching | run | It's an ELF. It loads one or more lib\*.so file which specified by parameter `-l xxx`, then run Modules in side | | mqtt | MQTT Client | | coroutine | coroutine function | -| http | Implemented HTTP Server and Client modules on the basis of network | +| http | Implemented HTTP Server and Client, middleware, and SSE (Server-Sent Events) modules on the basis of network | +| websocket | WebSocket Server & Client modules based on HTTP middleware, RFC 6455 & RFC 7692 (permessage-deflate). Features: split Text/Binary callbacks with rvalue refs, fragmented send with configurable chunk size, fragmented receive with buffered decompression | | alarm | Realized 4 commonly used alarm clocks: CRON alarm clock, single alarm clock, weekly cycle alarm clock, weekday alarm clock | | flow | Contains multi-level state machine and behavior tree to solve the problem of action flow in asynchronous mode | | crypto | Implemented the commonly used AES and MD5 encryption and decryption calculations | @@ -161,6 +162,8 @@ cmake -B build -DCMAKE_INSTALL_PREFIX=$HOME/.tbox For details on how to use cpp-tbox to develop your own programs, see the tutorial: [cpp-tbox-tutorials](https://github.com/cpp-main/cpp-tbox-tutorials/blob/master/README.md) +For module usage documentation, see: [Module Documentation](documents/modules/README.md) + For example to use `find_package`: ``` cmake_minimum_required(VERSION 3.10) diff --git a/README_CN.md b/README_CN.md index cf812a1d..0ca5d9ea 100644 --- a/README_CN.md +++ b/README_CN.md @@ -111,7 +111,8 @@ trace模块能记录被标记的函数每次执行的时间点与时长,可导 | run | 执行器 | 是个可执行程序,可加载多个由参数`-l xxx`指定的动态库,并运行其中的Module | | mqtt | MQTT客户端库 | | | coroutine | 协程库 | 众所周知,异步框架不方便处理顺序性业务,协程弥补之 | -| http | HTTP库 | 在network的基础上实现了HTTP的Server与Client模块 | +| http | HTTP库 | 在network的基础上实现了HTTP的Server与Client、中间件、SSE(服务端推送事件)模块 | +| websocket | WebSocket库 | 在http的基础上实现了WebSocket的Server与Client模块,遵循RFC 6455与RFC 7692(permessage-deflate压缩)。支持:Text/Binary分类型回调(右值引用)、分片发送(可配置分片大小)、分片接收完整后统一解压再回调 | | alarm | 闹钟库 | 实现了4种常用的闹钟:CRON闹钟、单次闹钟、星期循环闹钟、工作日闹钟 | | flow | 流程库| 含多层级状态机与行为树,解决异步模式下动行流程问题 | | crypto | 加密工具库 | 实现了常用的AES、MD5运算 | @@ -163,6 +164,8 @@ cmake -B build -DCMAKE_INSTALL_PREFIX=$HOME/.tbox 关于如何使用 cpp-tbox 开发自己的程序,详见教程: [cpp-tbox-tutorials](https://gitee.com/cpp-master/cpp-tbox-tutorials/blob/master/README.md) +各模块使用文档,详见:[模块使用文档](documents/modules/README_CN.md) + 使用`find_package`的例子: ``` diff --git a/config.mk b/config.mk index 048af477..b5865ef3 100644 --- a/config.mk +++ b/config.mk @@ -25,6 +25,7 @@ MODULES += event MODULES += eventx MODULES += log MODULES += network +MODULES += network_tls ## 需要 TLS 时取消注释,不需要时注释掉即可,不链接 libssl/libcrypto MODULES += terminal MODULES += trace MODULES += coroutine @@ -32,11 +33,12 @@ MODULES += main MODULES += run ## 非核心模块,请根据需要选择 +MODULES += crypto MODULES += http +MODULES += websocket MODULES += mqtt MODULES += flow MODULES += alarm -MODULES += crypto MODULES += dbus MODULES += jsonrpc diff --git a/documents/modules/README.md b/documents/modules/README.md new file mode 100644 index 00000000..f9d31a82 --- /dev/null +++ b/documents/modules/README.md @@ -0,0 +1,127 @@ +# cpp-tbox Module Documentation + +cpp-tbox is an event-driven C++ service application development library that provides a complete service program development framework. + +## Module Dependencies + +![modules-dependence](../images/modules-dependence.png) + +## Module List + +| Module | Description | Brief | Docs Link | +|--------|-------------|-------|-----------| +| **event** | Event-driven | Event loop and IO/timer/signal events | [event.md](event.md) | +| **base** | Base components | Log macros, assertions, object pool, lifetime tags, etc. | [base.md](base.md) | +| **main** | Application framework | Program startup framework, Module lifecycle management | [main.md](main.md) | +| **eventx** | Event extensions | Thread pool, timer pool, LoopThread, async operations | [eventx.md](eventx.md) | +| **network** | Network communication | TCP/UDP/UART communication and byte stream abstraction | [network.md](network.md) | +| **terminal** | Interactive terminal | Runtime command interaction, similar to Bash shell | [terminal.md](terminal.md) | +| **log** | Log channels | File/stdout/syslog and other log outputs | [log.md](log.md) | +| **http** | HTTP service | Express-style HTTP server/client, middleware, and SSE (Server-Sent Events) | [http.md](http.md) | +| **websocket** | WebSocket service | WebSocket server/client, RFC 6455, HTTP middleware-based | [websocket.md](websocket.md) | +| **coroutine** | Coroutine | Coroutine scheduler and Channel/Mutex helper components | [coroutine.md](coroutine.md) | +| **alarm** | Timer alarm | Cron/Oneshot/Weekly/Workday timers | [alarm.md](alarm.md) | +| **util** | Utilities | Buffer/Json/serialization/UUID/Base64 and 17+ tools | [util.md](util.md) | +| **mqtt** | MQTT client | MQTT protocol client with TLS and auto-reconnect | [mqtt.md](mqtt.md) | +| **flow** | Flow control | Multi-level state machine and behavior tree | [flow.md](flow.md) | +| **jsonrpc** | JSON-RPC | JSON-RPC 2.0 protocol implementation | [jsonrpc.md](jsonrpc.md) | +| **trace** | Performance tracing | Function-level performance tracing and binary recording | [trace.md](trace.md) | +| **crypto** | Encryption | MD5 message digest and AES encryption/decryption | [crypto.md](crypto.md) | +| **dbus** | D-Bus integration | D-Bus bus and event loop integration | [dbus.md](dbus.md) | +| **run** | Module runner | Dynamically load business module .so and run | [run.md](run.md) | + +## Quick Start + +### The Simplest Program + +```cpp +// app.cpp +#include +#include + +class App : public tbox::main::Module { + public: + App(tbox::main::Context &ctx) : Module("app", ctx) { } + bool onStart() override { LogInfo("started"); return true; } + void onStop() override { LogInfo("stopped"); } +}; + +namespace tbox { namespace main { +void RegisterApps(Module &apps, Context &ctx) { apps.add(new ::App(ctx)); } +std::string GetAppDescribe() { return "my first tbox app"; } +std::string GetAppBuildTime() { return __DATE__ " " __TIME__; } +void GetAppVersion(int &major, int &minor, int &rev, int &build) { major = 0; minor = 1; rev = 0; build = 0; } +}} +``` + +### Compile and Run + +```bash +# Compile +g++ -o myapp app.cpp -ltbox_main -ltbox_terminal -ltbox_network \ + -ltbox_eventx -ltbox_event -ltbox_util -ltbox_base -lpthread -ldl + +# Run +./myapp # Run in foreground, press Ctrl+C to exit +./myapp -d # Run in background +./myapp -h # Show help +./myapp -v # Show version +``` + +### Core Concepts + +1. **Event Loop (event::Loop)**: The scheduling center for all asynchronous events +2. **Module (main::Module)**: The carrier of business logic, following the initialize -> start -> stop -> cleanup lifecycle +3. **Callback-driven**: All asynchronous operations notify results through callback functions +4. **Single-thread model**: The event loop processes all event callbacks in a single thread; cross-thread operations are injected via runInLoop() + +### Recommended Learning Order + +1. [base](base.md) — Learn about logging, ScopeExit and other fundamentals +2. [event](event.md) — Understand the event loop mechanism +3. [main](main.md) — Master the program framework and Module lifecycle +4. Choose other modules based on business needs + +## Common Patterns + +### Initialize -> Start -> Stop -> Cleanup + +Almost all tbox components follow the same lifecycle pattern: + +```cpp +Component comp(loop); +comp.initialize(config); //! Initialize configuration +comp.setCallback([] { ... }); //! Set callback +comp.start(); // or comp.enable() //! Start/enable +// ... running normally ... +comp.stop(); // or comp.disable() //! Stop/disable +comp.cleanup(); //! Cleanup resources +``` + +### SetScopeExitAction Resource Management + +```cpp +auto ptr = new SomeObject; +SetScopeExitAction([ptr] { delete ptr; }); //! Automatically release on scope exit +``` + +### Cross-thread Task Injection + +```cpp +// Inject task into Loop from other threads +sp_loop->runInLoop([] { LogInfo("task in loop thread"); }); + +// Automatically choose route when thread is uncertain +sp_loop->run([] { LogInfo("auto route task"); }); +``` + +## Reference Images + +| Image | Description | +|-------|-------------| +| ![tbox-loop](../images/0001-tbox-loop.jpg) | Event loop working principle | +| ![main-framework](../images/0008-main-framework.png) | main module framework structure | +| ![modules-dependence](../images/modules-dependence.png) | Module dependency graph | +| ![state-machine](../images/0010-state-machine-graph.png) | State machine example | +| ![action-tree](../images/0010-action-tree-graph.jpg) | Behavior tree example | +| ![trace-view](../images/0011-trace-view.png) | Performance tracing visualization | diff --git a/documents/modules/README_CN.md b/documents/modules/README_CN.md new file mode 100644 index 00000000..591bd511 --- /dev/null +++ b/documents/modules/README_CN.md @@ -0,0 +1,127 @@ +# cpp-tbox 模块使用文档 + +cpp-tbox 是一个基于事件驱动的 C++ 服务应用开发库,提供完整的服务程序开发框架。 + +## 模块依赖关系 + +![modules-dependence](../images/modules-dependence.png) + +## 模块列表 + +| 模块 | 中文名 | 功能简述 | 文档链接 | +|------|--------|---------|---------| +| **event** | 事件驱动 | 事件循环与 IO/定时/信号事件 | [event_CN.md](event_CN.md) | +| **base** | 基础组件 | 日志宏、断言、对象池、生命期标签等 | [base_CN.md](base_CN.md) | +| **main** | 应用框架 | 程序启动框架,Module 生命周期管理 | [main_CN.md](main_CN.md) | +| **eventx** | 事件扩展 | 线程池、定时池、LoopThread、异步操作 | [eventx_CN.md](eventx_CN.md) | +| **network** | 网络通信 | TCP/UDP/UART 通信与字节流抽象 | [network_CN.md](network_CN.md) | +| **terminal** | 交互终端 | 运行时命令交互,类似 Bash shell | [terminal_CN.md](terminal_CN.md) | +| **log** | 日志通道 | 文件/stdout/syslog 等日志输出 | [log_CN.md](log_CN.md) | +| **http** | HTTP 服务 | Express 式 HTTP 服务端与客户端、中间件、SSE(服务端推送事件) | [http_CN.md](http_CN.md) | +| **websocket** | WebSocket 服务 | WebSocket 服务端与客户端,RFC 6455,基于 HTTP 中间件 | [websocket_CN.md](websocket_CN.md) | +| **coroutine** | 协程 | 协程调度器与 Channel/Mutex 等辅助组件 | [coroutine_CN.md](coroutine_CN.md) | +| **alarm** | 定时闹钟 | Cron/Oneshot/Weekly/Workday 定时器 | [alarm_CN.md](alarm_CN.md) | +| **util** | 工具集 | Buffer/Json/序列化/UUID/Base64 等 17+ 工具 | [util_CN.md](util_CN.md) | +| **mqtt** | MQTT 客户端 | MQTT 协议客户端,支持 TLS 和自动重连 | [mqtt_CN.md](mqtt_CN.md) | +| **flow** | 流程控制 | 多层级状态机与行为树 | [flow_CN.md](flow_CN.md) | +| **jsonrpc** | JSON-RPC | JSON-RPC 2.0 协议实现 | [jsonrpc_CN.md](jsonrpc_CN.md) | +| **trace** | 性能追踪 | 函数级性能追踪与二进制记录 | [trace_CN.md](trace_CN.md) | +| **crypto** | 加密 | MD5 消息摘要与 AES 加密解密 | [crypto_CN.md](crypto_CN.md) | +| **dbus** | D-Bus 集成 | D-Bus 总线与事件循环集成 | [dbus_CN.md](dbus_CN.md) | +| **run** | 模块运行器 | 动态加载业务模块 .so 并运行 | [run_CN.md](run_CN.md) | + +## 快速入门 + +### 最简单的程序 + +```cpp +// app.cpp +#include +#include + +class App : public tbox::main::Module { + public: + App(tbox::main::Context &ctx) : Module("app", ctx) { } + bool onStart() override { LogInfo("started"); return true; } + void onStop() override { LogInfo("stopped"); } +}; + +namespace tbox { namespace main { +void RegisterApps(Module &apps, Context &ctx) { apps.add(new ::App(ctx)); } +std::string GetAppDescribe() { return "my first tbox app"; } +std::string GetAppBuildTime() { return __DATE__ " " __TIME__; } +void GetAppVersion(int &major, int &minor, int &rev, int &build) { major = 0; minor = 1; rev = 0; build = 0; } +}} +``` + +### 编译与运行 + +```bash +# 编译 +g++ -o myapp app.cpp -ltbox_main -ltbox_terminal -ltbox_network \ + -ltbox_eventx -ltbox_event -ltbox_util -ltbox_base -lpthread -ldl + +# 运行 +./myapp # 前端运行,按 Ctrl+C 退出 +./myapp -d # 后台运行 +./myapp -h # 显示帮助 +./myapp -v # 显示版本 +``` + +### 核心概念 + +1. **事件循环 (event::Loop)**:所有异步事件的调度中心 +2. **模块 (main::Module)**:业务逻辑的载体,遵循 initialize → start → stop → cleanup 生命周期 +3. **回调驱动**:所有异步操作通过回调函数通知结果 +4. **单线程模型**:事件循环在单线程中处理所有事件回调,跨线程操作通过 runInLoop() 注入 + +### 推荐学习顺序 + +1. [base_CN](base_CN.md) — 了解日志、ScopeExit 等基础 +2. [event_CN](event_CN.md) — 理解事件循环机制 +3. [main_CN](main_CN.md) — 掌握程序框架和 Module 生命周期 +4. 根据业务需要选择其他模块 + +## 通用模式 + +### 初始化 → 启动 → 停止 → 清理 + +几乎所有 tbox 组件遵循相同的生命周期模式: + +```cpp +Component comp(loop); +comp.initialize(config); //! 初始化配置 +comp.setCallback([] { ... }); //! 设置回调 +comp.start(); // 或 comp.enable() //! 启动/使能 +// ... 正常运行 ... +comp.stop(); // 或 comp.disable() //! 停止/禁用 +comp.cleanup(); //! 清理资源 +``` + +### SetScopeExitAction 资源管理 + +```cpp +auto ptr = new SomeObject; +SetScopeExitAction([ptr] { delete ptr; }); //! 作用域退出时自动释放 +``` + +### 跨线程任务注入 + +```cpp +// 其它线程向 Loop 注入任务 +sp_loop->runInLoop([] { LogInfo("task in loop thread"); }); + +// 不确定线程时自动选择 +sp_loop->run([] { LogInfo("auto route task"); }); +``` + +## 参考图片 + +| 图片 | 说明 | +|------|------| +| ![tbox-loop](../images/0001-tbox-loop.jpg) | 事件循环工作原理 | +| ![main-framework](../images/0008-main-framework.png) | main 模块框架结构 | +| ![modules-dependence](../images/modules-dependence.png) | 模块依赖关系图 | +| ![state-machine](../images/0010-state-machine-graph.png) | 状态机示例 | +| ![action-tree](../images/0010-action-tree-graph.jpg) | 行为树示例 | +| ![trace-view](../images/0011-trace-view.png) | 性能追踪可视化 | diff --git a/documents/modules/alarm.md b/documents/modules/alarm.md new file mode 100644 index 00000000..9c0669c5 --- /dev/null +++ b/documents/modules/alarm.md @@ -0,0 +1,204 @@ +# Alarm Module (alarm) + +## What is it? + +The alarm module provides multiple alarm types: CronAlarm (Linux cron expressions), OneshotAlarm (one-shot), WeeklyAlarm (weekly recurring), and WorkdayAlarm (workdays/holidays). They are implemented based on the TimerEvent of the event module and support independent timezone settings. + +## Why do you need it? + +In service-oriented programs, scheduled tasks are one of the most common requirements. The alarm module provides multiple scheduling strategies to meet different scenarios: execute at a fixed time every day, execute on specific days of the week, flexibly schedule via cron expressions, execute only on workdays, etc. + +## Header Files + +```cpp +#include //! Alarm base class +#include //! Cron expression alarm +#include //! One-shot alarm +#include //! Weekly alarm +#include //! Workday alarm +#include //! Workday calendar +``` + +## Core Classes and Interfaces + +### Alarm — Alarm Base Class + +All alarm types share the following interfaces: + +| Method | Description | +|------|------| +| `Alarm(loop)` | Constructor, specify the event loop | +| `setCallback(cb)` | Set the timer trigger callback | +| `setTimezone(offset_minutes)` | Set timezone offset (east zones are positive, e.g. UTC+8 = 480) | +| `enable()` | Enable the timer | +| `disable()` | Disable the timer | +| `isEnabled()` | Check if the timer is enabled | +| `refresh()` | Refresh (should be called after clock synchronization) | +| `remainSeconds()` | Get remaining seconds | +| `cleanup()` | Clean up resources | + +### Alarm Types Comparison + +| Type | Initialization Parameters | Applicable Scenarios | +|------|------|------| +| **CronAlarm** | cron expression string | Flexible scheduling, e.g. "every minute", "the 1st of each month" | +| **OneshotAlarm** | seconds_of_day | Execute once at a fixed time today or tomorrow | +| **WeeklyAlarm** | seconds_of_day + week_mask | Execute on specific days of the week | +| **WorkdayAlarm** | seconds_of_day + calendar + workday | Execute only on workdays or holidays | + +### CronAlarm — Cron Expression Alarm + +Cron expression format (6 fields): + +``` +second minute hour day month weekday +* * * * * * +``` + +Examples: +- `"18 28 14 * * *"` — Every day at 14:28:18 +- `"0 30 8 * * 1-5"` — Monday through Friday at 8:30:00 +- `"0 0 12 1 * *"` — The 1st of each month at 12:00:00 + +### OneshotAlarm — One-shot Alarm + +The `seconds_of_day` parameter is the number of seconds from local 00:00 to the trigger time. + +``` +08:30 = 8 × 3600 + 30 × 60 = 30600 +``` + +If the specified time has already passed (current time is later than the scheduled time), it will execute tomorrow. + +### WeeklyAlarm — Weekly Alarm + +`week_mask` is a fixed-length 7-character string starting from Sunday. `'1'` means execute, other characters mean skip: + +``` +"0111110" → Monday through Friday +"1111111" → Every day +"1000001" → Only Sunday and Saturday +``` + +### WorkdayCalendar — Workday Calendar + +WorkdayCalendar provides date query functionality to WorkdayAlarm: + +| Method | Description | +|------|------| +| `updateSpecialDays(days)` | Update special holiday/makeup workday schedule | +| `updateWeekMask(mask)` | Modify the default weekly workday mask (default: Monday through Friday) | +| `subscribe(alarm)` | Subscribe an alarm (automatically notified when calendar changes) | +| `unsubscribe(alarm)` | Unsubscribe an alarm | +| `isWorkay(day_index)` | Query whether the specified date is a workday | + +### WorkdayAlarm — Workday Alarm + +The `workday` parameter: true = execute only on workdays, false = execute only on holidays. + +## Usage Examples + +### CronAlarm — Execute at 14:28:18 every day + +> Full example at `examples/alarm/cron_alarm/` + +```cpp +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; + +int main() { + LogOutput_Enable(); + + Loop* sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + alarm::CronAlarm tmr(sp_loop); + tmr.initialize("18 28 14 * * *"); //! Every day at 14:28:18 + tmr.setCallback([] { LogInfo("time is up"); }); + tmr.enable(); + + sp_loop->runLoop(Loop::Mode::kForever); + + LogOutput_Disable(); + return 0; +} +``` + +### OneshotAlarm — Execute at 8:30 every morning + +> Full example at `examples/alarm/oneshot_alarm/` + +```cpp +alarm::OneshotAlarm tmr(sp_loop); +tmr.initialize(30600); //! 08:30 = 30600 seconds +tmr.setCallback([] { LogInfo("time is up"); }); +tmr.enable(); +``` + +### WeeklyAlarm — Execute at 8:30 Monday through Friday + +> Full example at `examples/alarm/weekly_alarm/` + +```cpp +alarm::WeeklyAlarm tmr(sp_loop); +tmr.initialize(30600, "0111110"); //! Monday through Friday at 08:30 +tmr.setCallback([] { LogInfo("time is up"); }); +tmr.enable(); +``` + +### Setting an Independent Timezone + +```cpp +tmr.setTimezone(480); //! UTC+8 (+8 × 60 = 480 minutes) +//! If not set, the system timezone is used by default +``` + +### WorkdayAlarm — Execute Only on Workdays + +```cpp +alarm::WorkdayCalendar calendar; +//! Mark January 1, 2024 as a holiday +calendar.updateSpecialDays({{19723, false}}); //! day_index = days from 1970-1-1 + +alarm::WorkdayAlarm tmr(sp_loop); +tmr.initialize(30600, &calendar, true); //! true = only workdays +tmr.setCallback([] { LogInfo("workday alarm"); }); +tmr.enable(); + +//! When the calendar is updated, subscribed alarms will automatically refresh +calendar.updateSpecialDays({{19724, true}}); //! Makeup workday +``` + +### refresh() — Refresh After Clock Synchronization + +```cpp +//! Refresh the timer after system clock synchronization to ensure accurate trigger times +tmr.refresh(); +``` + +## Common Scenarios + +1. **Daily execution at a fixed time**: OneshotAlarm with seconds_of_day +2. **Weekly execution on specific days**: WeeklyAlarm with week_mask +3. **Flexible cron scheduling**: CronAlarm with cron expressions +4. **Execute only on workdays**: WorkdayAlarm + WorkdayCalendar +5. **Cross-timezone scheduling**: setTimezone() to set an independent timezone + +## Important Notes + +1. **seconds_of_day calculation**: Counted from local 00:00, e.g. 08:30 = 30600 +2. **OneshotAlarm "tomorrow" behavior**: If the current time has already passed the scheduled time, it will execute tomorrow +3. **WorkdayCalendar lifetime**: The calendar object passed to WorkdayAlarm must live longer than the WorkdayAlarm +4. **week_mask format**: Fixed 7 characters, starting from Sunday, '1' marks execution days +5. **When to call refresh()**: If the system clock is inaccurate when enable() is called, the scheduled task will also be inaccurate; refresh should be called after clock synchronization + +## Related Modules + +- **event**: Alarm is implemented based on TimerEvent for timing +- **base**: Provides logging, ScopeExit, and other infrastructure diff --git a/documents/modules/alarm_CN.md b/documents/modules/alarm_CN.md new file mode 100644 index 00000000..8955d44e --- /dev/null +++ b/documents/modules/alarm_CN.md @@ -0,0 +1,204 @@ +# 定时闹钟模块 (alarm) + +## 是什么? + +alarm 模块提供了多种定时闹钟类型:CronAlarm(Linux cron 表达式)、OneshotAlarm(一次性)、WeeklyAlarm(每周循环)、WorkdayAlarm(工作日/节假日)。它们基于 event 模块的 TimerEvent 实现,支持独立时区设置。 + +## 为什么需要它? + +在服务型程序中,定时任务是最常见的需求之一。alarm 模块提供了多种定时策略,满足不同场景:每天固定时间执行、每周特定日期执行、按 cron 表达式灵活调度、仅在工作日执行等。 + +## 头文件 + +```cpp +#include //! 闹钟基类 +#include //! Cron 表达式闹钟 +#include //! 一次性闹钟 +#include //! 每周闹钟 +#include //! 工作日闹钟 +#include //! 工作日日历 +``` + +## 核心类与接口 + +### Alarm — 闹钟基类 + +所有闹钟类型共享以下接口: + +| 方法 | 说明 | +|------|------| +| `Alarm(loop)` | 构造,指定事件循环 | +| `setCallback(cb)` | 设置定时触发回调 | +| `setTimezone(offset_minutes)` | 设置时区偏移(东区为正,如东8区=480) | +| `enable()` | 使能定时器 | +| `disable()` | 关闭定时器 | +| `isEnabled()` | 定时器是否已使能 | +| `refresh()` | 刷新(时钟同步后应调用) | +| `remainSeconds()` | 获取剩余秒数 | +| `cleanup()` | 清理资源 | + +### 闹钟类型对比 + +| 类型 | 初始化参数 | 适用场景 | +|------|------|------| +| **CronAlarm** | cron 表达式字符串 | 灵活调度,如"每分钟"、"每月1号" | +| **OneshotAlarm** | seconds_of_day | 每天或明天固定时间执行一次 | +| **WeeklyAlarm** | seconds_of_day + week_mask | 每周特定日期执行 | +| **WorkdayAlarm** | seconds_of_day + calendar + workday | 仅工作日或节假日执行 | + +### CronAlarm — Cron 表达式闹钟 + +Cron 表达式格式(6个字段): + +``` +秒 分 时 日 月 星期 +* * * * * * +``` + +示例: +- `"18 28 14 * * *"` — 每天 14:28:18 +- `"0 30 8 * * 1-5"` — 周一到周五 8:30:00 +- `"0 0 12 1 * *"` — 每月1号 12:00:00 + +### OneshotAlarm — 一次性闹钟 + +`seconds_of_day` 参数是从本地 00:00 起到触发时间的秒数。 + +``` +08:30 = 8 × 3600 + 30 × 60 = 30600 +``` + +如果指定时间已过(当前时间晚于定时时间),将在明天执行。 + +### WeeklyAlarm — 每周闹钟 + +`week_mask` 为固定长度 7 个字符的字符串,星期日开始,`'1'` 表示执行,其他表示不执行: + +``` +"0111110" → 周一到周五执行 +"1111111" → 每天执行 +"1000001" → 仅周日和周六执行 +``` + +### WorkdayCalendar — 工作日日历 + +WorkdayCalendar 用于向 WorkdayAlarm 提供日期查询功能: + +| 方法 | 说明 | +|------|------| +| `updateSpecialDays(days)` | 更新特殊节假日/补班日期表 | +| `updateWeekMask(mask)` | 修改一周默认工作日(默认周一到周五) | +| `subscribe(alarm)` | 订阅闹钟(日历变更时自动通知) | +| `unsubscribe(alarm)` | 取消订阅 | +| `isWorkay(day_index)` | 查询指定日期是否为工作日 | + +### WorkdayAlarm — 工作日闹钟 + +`workday` 参数:true=仅工作日执行,false=仅节假日执行。 + +## 使用示例 + +### CronAlarm — 每天 14:28:18 执行 + +> 完整示例见 `examples/alarm/cron_alarm/` + +```cpp +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; + +int main() { + LogOutput_Enable(); + + Loop* sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + alarm::CronAlarm tmr(sp_loop); + tmr.initialize("18 28 14 * * *"); //! 每天 14:28:18 + tmr.setCallback([] { LogInfo("time is up"); }); + tmr.enable(); + + sp_loop->runLoop(Loop::Mode::kForever); + + LogOutput_Disable(); + return 0; +} +``` + +### OneshotAlarm — 每天早上 8:30 执行 + +> 完整示例见 `examples/alarm/oneshot_alarm/` + +```cpp +alarm::OneshotAlarm tmr(sp_loop); +tmr.initialize(30600); //! 08:30 = 30600秒 +tmr.setCallback([] { LogInfo("time is up"); }); +tmr.enable(); +``` + +### WeeklyAlarm — 周一到周五 8:30 执行 + +> 完整示例见 `examples/alarm/weekly_alarm/` + +```cpp +alarm::WeeklyAlarm tmr(sp_loop); +tmr.initialize(30600, "0111110"); //! 周一到周五 08:30 +tmr.setCallback([] { LogInfo("time is up"); }); +tmr.enable(); +``` + +### 设置独立时区 + +```cpp +tmr.setTimezone(480); //! 东8区(+8 × 60 = 480分钟) +//! 不设置时默认使用系统时区 +``` + +### WorkdayAlarm — 仅工作日执行 + +```cpp +alarm::WorkdayCalendar calendar; +//! 标注2024年1月1日为节假日 +calendar.updateSpecialDays({{19723, false}}); //! day_index从1970-1-1起的天数 + +alarm::WorkdayAlarm tmr(sp_loop); +tmr.initialize(30600, &calendar, true); //! true=仅工作日 +tmr.setCallback([] { LogInfo("workday alarm"); }); +tmr.enable(); + +//! 日历更新后,订阅了该日历的闹钟会自动刷新 +calendar.updateSpecialDays({{19724, true}}); //! 补班日 +``` + +### refresh() — 时钟同步后刷新 + +```cpp +//! 系统时钟同步后刷新定时器,确保触发时间准确 +tmr.refresh(); +``` + +## 常见场景 + +1. **每天定时执行**:OneshotAlarm 指定 seconds_of_day +2. **每周特定日期执行**:WeeklyAlarm 指定 week_mask +3. **灵活 cron 调度**:CronAlarm 使用 cron 表达式 +4. **仅工作日执行**:WorkdayAlarm + WorkdayCalendar +5. **跨时区定时**:setTimezone() 设置独立时区 + +## 注意事项 + +1. **seconds_of_day 计算**:从本地 00:00 起算,如 08:30 = 30600 +2. **OneshotAlarm 的"明天"行为**:如果当前时间已过定时时间,将在明天执行 +3. **WorkdayCalendar 生命期**:传入 WorkdayAlarm 的 calendar 对象生命期必须比 WorkdayAlarm 长 +4. **week_mask 格式**:固定 7 个字符,星期日开始,'1' 为执行标记 +5. **refresh() 的时机**:系统时钟不准时 enable() 的定时任务也不准,时钟同步后应刷新 + +## 相关模块 + +- **event**:Alarm 基于 TimerEvent 实现定时 +- **base**:提供日志、ScopeExit 等基础设施 diff --git a/documents/modules/base.md b/documents/modules/base.md new file mode 100644 index 00000000..eed5c68d --- /dev/null +++ b/documents/modules/base.md @@ -0,0 +1,316 @@ +# Base Module (base) + +## What is it? + +The base module is the lowest-level dependency module of cpp-tbox, providing infrastructure such as log macros, assertions, object pools, lifetime tags, scope exit actions, cabinets, common definitions, and more. All other modules depend on the base module. + +## Why do you need it? + +In C++ project development, logging, resource management, and assertion checking are the most fundamental needs. The base module uniformly encapsulates these common functionalities so that all modules share consistent log output interfaces and resource management patterns, avoiding redundant implementations. + +## Header Files + +```cpp +#include //! Log macros +#include //! Log implementation details +#include //! Log output switch +#include //! Assertion macros +#include //! Common macro definitions (NONCOPYABLE, etc.) +#include //! Scope exit action +#include //! Object pool +#include //! Lifetime tag +#include //! Cabinet +#include //! Cabinet token +#include //! Exception catch +#include //! Memory block +#include //! Recorder +#include //! Call stack backtrace +#include //! JSON library (nlohmann/json) +#include //! JSON forward declaration +#include //! Version info +``` + +## Core Classes and Interfaces + +### Log Macros (log.h) + +Logging is the most commonly used feature in a project. The base module defines 8 log levels and corresponding print macros: + +| Log Level | Macro | Description | +|---------|------|------| +| FATAL (0) | `LogFatal(fmt, ...)` | Program will crash | +| ERROR (1) | `LogErr(fmt, ...)` | Severe problem that the program cannot handle | +| WARN (2) | `LogWarn(fmt, ...)` | Internal anomaly, but the program can handle it | +| NOTICE (3) | `LogNotice(fmt, ...)` | Not very severe but should be noted, such as invalid input | +| IMPORTANT (4) | `LogImportant(fmt, ...)` | Important message | +| INFO (5) | `LogInfo(fmt, ...)` | Normal message | +| DEBUG (6) | `LogDbg(fmt, ...)` | Internal program debug information | +| TRACE (7) | `LogTrace(fmt, ...)` | Temporary debug log | + +Helper macros: + +| Macro | Description | +|------|------| +| `LogTag()` | Prints "==> Run Here <==", marking code execution location | +| `LogUndo()` | Prints "!!! Undo !!!", marking unimplemented functionality | +| `LogErrno(err, fmt, ...)` | Prints errno error code and its meaning | + +> **Note**: `LogDbg` and `LogTrace` can be disabled at compile time via the `STATIC_LOG_LEVEL` compile option, reducing log volume in production environments. + +#### MODULE_ID Definition + +Log output includes a module identifier. You need to define `MODULE_ID` in compile options, e.g. `-DMODULE_ID=alarm`. If not defined, the module name in logs will display as "???". + +#### Log Output Switch (log_output.h) + +```cpp +LogOutput_Enable(); //! Enable log output to stdout +LogOutput_Disable(); //! Disable log output +``` + +> `LogOutput_Enable()` is the simplest way to output logs, printing them to stdout. For richer log configuration, please use the **log module**. + +### Assertion Macros (assert.h) + +```cpp +TBOX_ASSERT(expr); //! In debug mode, if the condition is false, prints LogFatal and abort() +``` + +- In `NDEBUG` mode (Release build), `TBOX_ASSERT` does nothing +- In debug mode, a failed assertion prints an error message and terminates the program + +### Scope Exit Action (scope_exit.hpp) + +The `SetScopeExitAction` macro automatically executes a specified action when a code block exits, similar to Go's defer. + +```cpp +SetScopeExitAction(action); //! Execute action when the current scope exits +``` + +**Typical usage**: managing the release of dynamically allocated resources. + +```cpp +Loop* sp_loop = Loop::New(); +SetScopeExitAction([sp_loop] { delete sp_loop; }); +//! ... use sp_loop ... +//! sp_loop is automatically deleted when the function exits +``` + +> **Note**: The `ScopeExitActionGuard` object created by `SetScopeExitAction` is non-copyable and non-movable (NONCOPYABLE/IMMOVABLE). Execution can be canceled via `cancel()`. + +### Object Pool (object_pool.hpp) + +`ObjectPool` is a template class used to reduce the performance overhead of frequent new/delete operations on objects. It caches freed memory blocks through a free block list, avoiding repeated allocation and deallocation of memory. + +```cpp +//! Create an object pool +ObjectPool op; +//! Or specify the number of free blocks to retain +ObjectPool op(64); + +//! Allocate an object (equivalent to new, but faster) +auto p1 = op.alloc(1, "hello"); //! Supports constructor arguments + +//! Free an object (equivalent to delete) +op.free(p1); + +//! Get statistics +auto stat = op.getStat(); +//! stat.total_alloc_times — Total allocation count +//! stat.total_free_times — Total free count +//! stat.peak_alloc_number — Maximum simultaneous allocations +//! stat.peak_free_number — Maximum free cache count +``` + +> **Important**: Objects allocated via ObjectPool **must be freed using ObjectPool**, not with `delete`. + +### Lifetime Tag (lifetime_tag.hpp) + +`LifetimeTag` is used to mark whether an object's lifetime is valid, solving the risk of a pointer referencing an object that has been prematurely destructed. + +```cpp +struct HostObject { + int value = 0; + LifetimeTag tag; //! Lifetime tag +}; + +HostObject *o = new HostObject; +LifetimeTag::Watcher w = o->tag; //! Create a watcher + +if (w) //! true, object is alive + cout << "value:" << o->value << endl; + +delete o; //! Destruct the object + +if (w) //! false, object has been destructed + cout << "Cannot safely access" << endl; +``` + +> **Note**: LifetimeTag currently does not have lock protection and does not support multithreading. + +### Cabinet (cabinet.hpp) + +`Cabinet` is a secure object container with token-based access. Objects are accessed via a Token; even if an object is deleted, an old Token will not mistakenly retrieve a new object. + +```cpp +Cabinet cab; + +//! Store an object, get a Token +auto token = cab.alloc(new MyClass); + +//! Remove an object +auto p = cab.free(token); //! Removes the record and returns the object pointer + +//! Check if a Token is valid +auto p2 = cab.at(token); //! Does not remove, only looks up +``` + +### Common Definitions (defines.h) + +| Macro | Description | +|------|------| +| `NONCOPYABLE(classname)` | Disable copy constructor and assignment | +| `IMMOVABLE(classname)` | Disable move constructor and assignment | +| `DECLARE_COPY_FUNC(classname)` | Declare copy function (for Variables) | +| `CHECK_DELETE_RESET_OBJ(ptr)` | Delete pointer and set to nullptr | + +## Usage Examples + +### Printing Logs + +> Full example at `examples/base/print_log/` + +```cpp +#include +#include + +#define MODULE_ID "my_app" + +int main() { + LogOutput_Enable(); + + LogInfo("program started"); + LogDbg("debug info: count=%d", 42); + LogWarn("unexpected input: %s", "abc"); + LogErr("file open failed"); + + LogOutput_Disable(); + return 0; +} +``` + +### Assertion Checking + +> Full example at `examples/base/assert/` + +```cpp +#include +#include + +int main() { + LogOutput_Enable(); + + int value = 10; + TBOX_ASSERT(value > 0); //! In debug mode, if the condition holds, execution continues + //! TBOX_ASSERT(value < 0); //! If the condition fails, prints LogFatal and abort + + LogOutput_Disable(); + return 0; +} +``` + +### Object Pool + +> Full example at `examples/base/object_pool/` + +```cpp +#include +#include +#include + +class MyStruct { + public: + MyStruct(int i, const std::string &s) : i_(i), s_(s) { } + void print() { LogInfo("i:%d, s:%s", i_, s_.c_str()); } + private: + int i_; + std::string s_; +}; + +int main() { + LogOutput_Enable(); + + ObjectPool op; + + auto p1 = op.alloc(1, "hello"); //! Equivalent to new MyStruct(1, "hello") + p1->print(); + + op.free(p1); //! Equivalent to delete p1, but the memory block is cached + + //! Re-allocating reuses the previously cached memory block, avoiding malloc + auto p2 = op.alloc(2, "world"); + p2->print(); + op.free(p2); + + auto stat = op.getStat(); + LogInfo("alloc:%zu, free:%zu, peak:%zu", + stat.total_alloc_times, stat.total_free_times, stat.peak_alloc_number); + + LogOutput_Disable(); + return 0; +} +``` + +### Lifetime Tag + +> Full example at `examples/base/lifetime_tag/` + +```cpp +#include +#include +#include + +struct Resource { + int data = 100; + tbox::LifetimeTag tag; +}; + +int main() { + LogOutput_Enable(); + + Resource *res = new Resource; + tbox::LifetimeTag::Watcher watcher = res->tag; + + LogInfo("alive: %d, data: %d", (bool)watcher, res->data); + + delete res; //! Object destructed + + LogInfo("alive: %d", (bool)watcher); //! watcher is false + //! Cannot safely access res->data anymore + + LogOutput_Disable(); + return 0; +} +``` + +## Common Scenarios + +1. **Logging**: All modules uniformly use `LogInfo/LogErr/LogDbg` and other macros to print logs +2. **Automatic resource release**: Use `SetScopeExitAction` to automatically delete new'd objects when a function exits +3. **High-frequency object allocation**: Use `ObjectPool` to reduce the performance overhead of frequent new/delete +4. **Pointer safety checking**: Use `LifetimeTag` + `Watcher` to check whether an object is still alive +5. **Disable copy/move**: Use `NONCOPYABLE/IMMOVABLE` macros to protect class semantic integrity + +## Important Notes + +1. **MODULE_ID must be defined**: If not defined, the module name in logs displays as "???", affecting log identification +2. **ObjectPool free vs delete**: Objects allocated via ObjectPool must only be freed using ObjectPool's `free()`, not with `delete` +3. **LifetimeTag does not support multithreading**: Currently there is no lock protection; it is only suitable for single-threaded or Loop-thread contexts +4. **SetScopeExitAction cannot cross scopes**: Its execution timing depends on the exit of the enclosing code block; be mindful of the lifetime of objects captured by lambdas +5. **TBOX_ASSERT is inactive in Release**: Assertions are ignored under NDEBUG compilation; do not use assertions as a substitute for error handling + +## Related Modules + +- **log**: Log channel implementation based on base/log.h, providing file/stdout/syslog and other output methods +- **event**: Depends on base's Cabinet, ObjectPool, defines, etc. +- **All modules**: base is the foundational dependency of all modules diff --git a/documents/modules/base_CN.md b/documents/modules/base_CN.md new file mode 100644 index 00000000..ea040245 --- /dev/null +++ b/documents/modules/base_CN.md @@ -0,0 +1,316 @@ +# 基础组件模块 (base) + +## 是什么? + +base 模块是 cpp-tbox 最底层的依赖模块,提供了日志宏、断言、对象池、生命期标签、作用域退出、储物柜、通用定义等基础设施。所有其他模块都依赖 base 模块。 + +## 为什么需要它? + +在 C++ 项目开发中,日志打印、资源管理、断言检查是最基础的需求。base 模块将这些常用功能统一封装,使得所有模块共享一致的日志输出接口和资源管理模式,避免重复实现。 + +## 头文件 + +```cpp +#include //! 日志宏 +#include //! 日志实现细节 +#include //! 日志输出开关 +#include //! 断言宏 +#include //! 通用宏定义(NONCOPYABLE 等) +#include //! 作用域退出动作 +#include //! 对象池 +#include //! 生命期标签 +#include //! 储物柜 +#include //! 储物柜凭据 +#include //! 异常捕获 +#include //! 内存块 +#include //! 记录器 +#include //! 调用栈回溯 +#include //! JSON 库(nlohmann/json) +#include //! JSON 前置声明 +#include //! 版本信息 +``` + +## 核心组件 + +### 日志宏 (log.h) + +日志是项目中最常用的功能。base 模块定义了 8 个日志级别和对应的打印宏: + +| 日志级别 | 宏 | 说明 | +|---------|------|------| +| FATAL (0) | `LogFatal(fmt, ...)` | 程序将崩溃 | +| ERROR (1) | `LogErr(fmt, ...)` | 严重问题,程序无法处理 | +| WARN (2) | `LogWarn(fmt, ...)` | 内部异常,但程序可处理 | +| NOTICE (3) | `LogNotice(fmt, ...)` | 不大严重但应关注,如无效输入 | +| IMPORTANT (4) | `LogImportant(fmt, ...)` | 重要消息 | +| INFO (5) | `LogInfo(fmt, ...)` | 正常消息 | +| DEBUG (6) | `LogDbg(fmt, ...)` | 程序内部调试信息 | +| TRACE (7) | `LogTrace(fmt, ...)` | 临时调试日志 | + +辅助宏: + +| 宏 | 说明 | +|------|------| +| `LogTag()` | 打印 "==> Run Here <==",标记代码执行位置 | +| `LogUndo()` | 打印 "!!! Undo !!!",标记未实现功能 | +| `LogErrno(err, fmt, ...)` | 打印 errno 错误码及其含义 | + +> **注意**:`LogDbg` 和 `LogTrace` 可通过 `STATIC_LOG_LEVEL` 编译选项在编译时屏蔽,减少生产环境的日志量。 + +#### MODULE_ID 定义 + +日志输出时会附带模块标识。需要在编译选项中定义 `MODULE_ID`,如 `-DMODULE_ID=alarm`。若未定义,日志中模块名显示为 "???"。 + +#### 日志输出开关 (log_output.h) + +```cpp +LogOutput_Enable(); //! 开启日志输出到 stdout +LogOutput_Disable(); //! 关闭日志输出 +``` + +> `LogOutput_Enable()` 是最简单的日志输出方式,将日志打印到 stdout。更丰富的日志配置请使用 **log 模块**。 + +### 断言宏 (assert.h) + +```cpp +TBOX_ASSERT(expr); //! 调试模式下,条件不成立时打印 LogFatal 并 abort() +``` + +- 在 `NDEBUG` 模式下(Release 编译),`TBOX_ASSERT` 不执行任何操作 +- 在调试模式下,断言失败会打印错误信息并终止程序 + +### 作用域退出动作 (scope_exit.hpp) + +`SetScopeExitAction` 宏用于在代码块退出时自动执行指定动作,类似于 Go 的 defer。 + +```cpp +SetScopeExitAction(action); //! 在当前作用域退出时执行 action +``` + +**典型用法**:管理动态分配资源的释放。 + +```cpp +Loop* sp_loop = Loop::New(); +SetScopeExitAction([sp_loop] { delete sp_loop; }); +//! ... 使用 sp_loop ... +//! 函数退出时自动 delete sp_loop +``` + +> **注意**:`SetScopeExitAction` 创建的 `ScopeExitActionGuard` 对象不可复制和移动(NONCOPYABLE/IMMOVABLE)。可通过 `cancel()` 取消执行。 + +### 对象池 (object_pool.hpp) + +`ObjectPool` 是一个模板类,用于减少频繁 new/delete 对象的性能开销。通过空闲块链表缓存已释放的内存块,避免反复分配与释放内存。 + +```cpp +//! 创建对象池 +ObjectPool op; +//! 或指定保留空闲块数量 +ObjectPool op(64); + +//! 分配对象(等价于 new,但更快) +auto p1 = op.alloc(1, "hello"); //! 支持构造参数 + +//! 释放对象(等价于 delete) +op.free(p1); + +//! 获取统计数据 +auto stat = op.getStat(); +//! stat.total_alloc_times — 总分配次数 +//! stat.total_free_times — 总释放次数 +//! stat.peak_alloc_number — 最大同时分配数 +//! stat.peak_free_number — 最大空闲缓存数 +``` + +> **重要**:凡是使用 ObjectPool 分配的对象,**一定要使用 ObjectPool 进行释放**,不可用 `delete`。 + +### 生命期标签 (lifetime_tag.hpp) + +`LifetimeTag` 用于标记对象的生命期是否有效,解决"指针指向的对象被提前析构"的风险问题。 + +```cpp +struct HostObject { + int value = 0; + LifetimeTag tag; //! 生命期标签 +}; + +HostObject *o = new HostObject; +LifetimeTag::Watcher w = o->tag; //! 创建观察器 + +if (w) //! true,对象存活 + cout << "value:" << o->value << endl; + +delete o; //! 析构对象 + +if (w) //! false,对象已析构 + cout << "不能安全访问" << endl; +``` + +> **注意**:目前 LifetimeTag 未做加锁保护,不支持多线程。 + +### 储物柜 (cabinet.hpp) + +`Cabinet` 是一个带凭据(Token)的安全对象容器。通过 Token 存取对象,即使对象被删除,旧的 Token 也不会误取到新对象。 + +```cpp +Cabinet cab; + +//! 存入对象,获取 Token +auto token = cab.alloc(new MyClass); + +//! 取出对象 +auto p = cab.free(token); //! 取出并删除记录,返回对象指针 + +//! 检查 Token 是否有效 +auto p2 = cab.at(token); //! 不取出,仅查看 +``` + +### 通用定义 (defines.h) + +| 宏 | 说明 | +|------|------| +| `NONCOPYABLE(classname)` | 禁止拷贝构造和赋值操作 | +| `IMMOVABLE(classname)` | 禁止移动构造和赋值操作 | +| `DECLARE_COPY_FUNC(classname)` | 声明拷贝函数(供 Variables 使用) | +| `CHECK_DELETE_RESET_OBJ(ptr)` | delete 指针并置 nullptr | + +## 使用示例 + +### 打印日志 + +> 完整示例见 `examples/base/print_log/` + +```cpp +#include +#include + +#define MODULE_ID "my_app" + +int main() { + LogOutput_Enable(); + + LogInfo("program started"); + LogDbg("debug info: count=%d", 42); + LogWarn("unexpected input: %s", "abc"); + LogErr("file open failed"); + + LogOutput_Disable(); + return 0; +} +``` + +### 断言检查 + +> 完整示例见 `examples/base/assert/` + +```cpp +#include +#include + +int main() { + LogOutput_Enable(); + + int value = 10; + TBOX_ASSERT(value > 0); //! 调试模式下,条件成立则正常继续 + //! TBOX_ASSERT(value < 0); //! 条件不成立时,打印 LogFatal 并 abort + + LogOutput_Disable(); + return 0; +} +``` + +### 对象池 + +> 完整示例见 `examples/base/object_pool/` + +```cpp +#include +#include +#include + +class MyStruct { + public: + MyStruct(int i, const std::string &s) : i_(i), s_(s) { } + void print() { LogInfo("i:%d, s:%s", i_, s_.c_str()); } + private: + int i_; + std::string s_; +}; + +int main() { + LogOutput_Enable(); + + ObjectPool op; + + auto p1 = op.alloc(1, "hello"); //! 等价于 new MyStruct(1, "hello") + p1->print(); + + op.free(p1); //! 等价于 delete p1,但内存块被缓存 + + //! 再次分配时复用之前缓存的内存块,避免 malloc + auto p2 = op.alloc(2, "world"); + p2->print(); + op.free(p2); + + auto stat = op.getStat(); + LogInfo("alloc:%zu, free:%zu, peak:%zu", + stat.total_alloc_times, stat.total_free_times, stat.peak_alloc_number); + + LogOutput_Disable(); + return 0; +} +``` + +### 生命期标签 + +> 完整示例见 `examples/base/lifetime_tag/` + +```cpp +#include +#include +#include + +struct Resource { + int data = 100; + tbox::LifetimeTag tag; +}; + +int main() { + LogOutput_Enable(); + + Resource *res = new Resource; + tbox::LifetimeTag::Watcher watcher = res->tag; + + LogInfo("alive: %d, data: %d", (bool)watcher, res->data); + + delete res; //! 对象析构 + + LogInfo("alive: %d", (bool)watcher); //! watcher 为 false + //! 不能再安全访问 res->data + + LogOutput_Disable(); + return 0; +} +``` + +## 常见场景 + +1. **日志打印**:所有模块统一使用 `LogInfo/LogErr/LogDbg` 等宏打印日志 +2. **资源自动释放**:使用 `SetScopeExitAction` 在函数退出时自动 delete/new 的对象 +3. **高频对象分配**:使用 `ObjectPool` 减少频繁 new/delete 的性能开销 +4. **指针安全检查**:使用 `LifetimeTag` + `Watcher` 检查对象是否存活 +5. **禁用拷贝/移动**:使用 `NONCOPYABLE/IMMOVABLE` 宏保护类的语义完整性 + +## 注意事项 + +1. **MODULE_ID 必须定义**:未定义时日志中模块名显示为 "???",影响日志定位 +2. **ObjectPool 的 free vs delete**:通过 ObjectPool 分配的对象只能用 ObjectPool 的 `free()` 释放,不能用 `delete` +3. **LifetimeTag 不支持多线程**:目前未加锁保护,仅适用于单线程或 Loop 线程内 +4. **SetScopeExitAction 不可跨作用域**:它的执行时机取决于所在代码块的退出,注意 lambda 捕获的对象生命周期 +5. **TBOX_ASSERT 在 Release 下无效**:NDEBUG 编译时断言被忽略,不要用断言替代错误处理 + +## 相关模块 + +- **log**:基于 base/log.h 的日志通道实现,提供文件/stdout/syslog 等输出方式 +- **event**:依赖 base 的 Cabinet、ObjectPool、defines 等 +- **所有模块**:base 是所有模块的基础依赖 diff --git a/documents/modules/coroutine.md b/documents/modules/coroutine.md new file mode 100644 index 00000000..a5e56edd --- /dev/null +++ b/documents/modules/coroutine.md @@ -0,0 +1,251 @@ +# Coroutine Module (coroutine) + +## What is it? + +The coroutine module is a coroutine library built on the event architecture, helping developers handle asynchronous logic with sequential-style code, avoiding callback hell and complex state machines inherent in event-driven programming. + +## Why do you need it? + +Event-driven programs excel at handling "when event X occurs, perform action Y" logic. If events are independent of each other, this is easy to manage. But when dealing with sequential business logic, such as "first do A, then do B, and if either A or B fails, do C...", the event-driven model requires designing complex state machines, resulting in scattered and hard-to-maintain code. + +Advantages of coroutines: +- **Lightweight**: Only requires allocating a stack for each coroutine, with a configurable stack size (default 8KB) +- **Controllable switching**: Coroutine switching is controlled by the program itself, via explicit yield()/wait() calls +- **No resource preemption**: No need for locks or other synchronization mechanisms + +Compared to threads: +- Threads are heavier, consuming both CPU and memory +- Thread switching is uncontrollable +- Resource preemption is difficult to manage + +## Header Files + +```cpp +#include //! Coroutine scheduler +#include //! Channel (similar to Golang chan) +#include //! Mutex +#include //! Semaphore +#include //! Condition +#include //! Broadcast +``` + +## Core Classes and Interfaces + +### Scheduler — Coroutine Scheduler + +| Method | Description | +|------|------| +| `Scheduler(loop)` | Constructor, specifies the event loop | +| `create(entry, run_now, name, stack_size)` | Creates a coroutine, returns RoutineToken | +| `resume(token)` | Resumes the specified coroutine | +| `cancel(token)` | Cancels a coroutine (sends a cancel request, not an immediate stop) | +| `wait()` | Switches to the main coroutine, waits to be woken up by resume | +| `yield()` | Switches to the main coroutine, continues execution in the next event loop iteration | +| `join(other)` | One coroutine waits for another coroutine to finish | +| `getToken()` | Gets the current coroutine Token | +| `isCanceled()` | Whether the current coroutine has been canceled | +| `getName()` | Gets the current coroutine name | +| `getLoop()` | Gets the event loop | +| `cleanup()` | Forcefully stops and cleans up all coroutines | + +### Channel — Channel + +Similar to Golang's chan, used for passing data between coroutines: + +```cpp +Channel ch(sch); + +//! Sender +ch << 42; + +//! Receiver +int value; +ch >> value; //! Waits if the queue is empty +``` + +### Mutex — Mutex + +Mutual exclusion between coroutines: + +```cpp +Mutex mtx(sch); + +//! Recommended: use Locker for automatic management +{ + Mutex::Locker locker(mtx); //! Auto lock + //! ... critical section operations ... +} //! Auto unlock +``` + +### Semaphore — Semaphore + +```cpp +Semaphore sem(sch, 3); //! Initial count of 3 + +sem.acquire(); //! Request a resource (waits when count is 0) +sem.release(); //! Release a resource +``` + +### Condition — Condition + +Wait for multiple conditions to be all satisfied or any one satisfied: + +```cpp +Condition cond(sch, Condition::Logic::kAll); +cond.add("event_a"); +cond.add("event_b"); + +//! Coroutine A waits +cond.wait(); //! Waits for both event_a and event_b to occur + +//! Coroutine B signals +cond.post("event_a"); +//! Coroutine C signals +cond.post("event_b"); //! Both conditions satisfied, wakes up Coroutine A +``` + +### Broadcast — Broadcast + +Multiple coroutines wait for a single signal; when the signal is posted, all waiting coroutines are woken up: + +```cpp +Broadcast bc(sch); + +//! Multiple coroutines wait +bc.wait(); + +//! Post broadcast +bc.post(); //! All waiting coroutines are woken up +``` + +## Usage Examples + +### Basic Usage + +> See the module README and unit test cases for complete examples + +```cpp +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::coroutine; + +int main() { + LogOutput_Enable(); + + Loop *sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + Scheduler sch(sp_loop); + + //! Define coroutine 1 + int routine1_count = 0; + sch.create( + [&] (Scheduler &sch) { + for (int i = 0; i < 20; ++i) { + ++routine1_count; + sch.yield(); //! Voluntarily yield execution + } + }, true, "routine1" + ); + + //! Define coroutine 2 + int routine2_count = 0; + sch.create( + [&] (Scheduler &sch) { + for (int i = 0; i < 10; ++i) { + ++routine2_count; + sch.yield(); + } + }, true, "routine2" + ); + + sp_loop->exitLoop(std::chrono::seconds(1)); + sp_loop->runLoop(); + + LogInfo("r1=%d, r2=%d", routine1_count, routine2_count); + + LogOutput_Disable(); + return 0; +} +``` + +### Inter-Coroutine Communication — Channel + +```cpp +Scheduler sch(sp_loop); + +Channel ch(sch); + +//! Producer coroutine +sch.create([&] (Scheduler &sch) { + for (int i = 0; i < 5; ++i) { + ch << i; + sch.yield(); + } +}); + +//! Consumer coroutine +sch.create([&] (Scheduler &sch) { + int value; + while (ch >> value) { + LogInfo("received: %d", value); + } +}); +``` + +### Inter-Coroutine Mutual Exclusion — Mutex + +```cpp +Mutex mtx(sch); + +sch.create([&] (Scheduler &sch) { + Mutex::Locker locker(mtx); //! Auto lock + LogInfo("locked, doing work"); + sch.yield(); + //! ... critical section operations ... +}); //! Locker destructor auto unlocks +``` + +### Waiting for a Coroutine to Finish — join + +```cpp +auto other_token = sch.create([&] (Scheduler &sch) { + //! Work of another coroutine + sch.yield(); + sch.yield(); +}); + +sch.create([&] (Scheduler &sch) { + sch.join(other_token); //! Wait for the other_token coroutine to finish + LogInfo("other routine finished"); +}); +``` + +## Common Scenarios + +1. **Sequential business logic**: Write multi-step asynchronous flows as sequential code using coroutines +2. **Producer-Consumer**: Use Channel to pass data between coroutines +3. **Shared resource protection**: Use Mutex to protect critical sections between coroutines +4. **Waiting for conditions**: Use Condition to wait for multiple conditions +5. **Broadcast notification**: Use Broadcast to wake up multiple coroutines simultaneously + +## Important Notes + +1. **yield vs wait**: `yield()` switches to the main coroutine and automatically continues in the next event loop iteration; `wait()` switches to the main coroutine and requires `resume()` to be called before it can continue +2. **Coroutines are single-threaded**: All coroutines are scheduled within the same Loop thread; there is no real concurrency, so atomic operations are not needed +3. **Stack size**: The default stack size is 8KB (`ROUTINE_STACK_DEFAULT_SIZE`); a larger stack can be specified via the create() parameter +4. **cancel is asynchronous**: cancel() only sends a cancel request; the coroutine checks isCanceled() at the next wait/yield and exits +5. **Channel >> return value**: When a coroutine is canceled, the `>>` operation returns false +6. **Condition does not support multiple coroutines waiting simultaneously**: Only one coroutine can wait() on the same Condition at a time + +## Related Modules + +- **event**: Coroutines are scheduled and run based on Loop +- **main**: The framework automatically creates a Scheduler, accessible via `ctx.coroutine()` +- **base**: Provides infrastructure such as Cabinet/Token diff --git a/documents/modules/coroutine_CN.md b/documents/modules/coroutine_CN.md new file mode 100644 index 00000000..14ff1f01 --- /dev/null +++ b/documents/modules/coroutine_CN.md @@ -0,0 +1,251 @@ +# 协程模块 (coroutine) + +## 是什么? + +coroutine 模块是基于 event 架构的协程库,帮助开发者用顺序型代码处理异步逻辑,避免事件驱动编程中的回调地狱和复杂状态机。 + +## 为什么需要它? + +基于事件驱动的程序擅长处理 "当发生xx事件,就做yy动作" 的逻辑。如果事件之间相互孤立,很好处理。但一旦遇到顺序型业务逻辑,如"先做A,然后做B,如果A或B失败则做C...",事件驱动模型需要设计复杂的状态机,代码零散难维护。 + +协程的优点: +- **轻量**:只需为每个协程分配一个栈,栈大小可指定(默认 8KB) +- **可控切换**:协程之间切换由程序自行控制,yield()/wait() 主动切换 +- **无资源抢占**:不需要锁等同步机制 + +相比线程: +- 线程较重,占 CPU 且耗内存 +- 线程切换不可控 +- 资源抢占不易管理 + +## 头文件 + +```cpp +#include //! 协程调度器 +#include //! 通道(类似 Golang chan) +#include //! 互斥量 +#include //! 信号量 +#include //! 条件量 +#include //! 广播 +``` + +## 核心类与接口 + +### Scheduler — 协程调度器 + +| 方法 | 说明 | +|------|------| +| `Scheduler(loop)` | 构造,指定事件循环 | +| `create(entry, run_now, name, stack_size)` | 创建协程,返回 RoutineToken | +| `resume(token)` | 恢复指定协程 | +| `cancel(token)` | 取消协程(发送取消请求,非立即停止) | +| `wait()` | 切换到主协程,等待被 resume 唤醒 | +| `yield()` | 切换到主协程,下一个事件循环继续执行 | +| `join(other)` | 一个协程等待另一个协程结束 | +| `getToken()` | 获取当前协程 Token | +| `isCanceled()` | 当前协程是否被取消 | +| `getName()` | 当前协程名称 | +| `getLoop()` | 获取事件循环 | +| `cleanup()` | 强行停止并清理所有协程 | + +### Channel — 通道 + +类似 Golang 的 chan,协程间传递数据: + +```cpp +Channel ch(sch); + +//! 发送端 +ch << 42; + +//! 接收端 +int value; +ch >> value; //! 如果队列空则等待 +``` + +### Mutex — 互斥量 + +协程间互斥访问: + +```cpp +Mutex mtx(sch); + +//! 推荐使用 Locker 自动管理 +{ + Mutex::Locker locker(mtx); //! 自动 lock + //! ... 临界区操作 ... +} //! 自动 unlock +``` + +### Semaphore — 信号量 + +```cpp +Semaphore sem(sch, 3); //! 初始计数为3 + +sem.acquire(); //! 请求资源(计数为0时等待) +sem.release(); //! 释放资源 +``` + +### Condition — 条件量 + +等待多个条件同时满足或任一满足: + +```cpp +Condition cond(sch, Condition::Logic::kAll); +cond.add("event_a"); +cond.add("event_b"); + +//! 协程A等待 +cond.wait(); //! 等待 event_a 和 event_b 都发生 + +//! 协程B发出信号 +cond.post("event_a"); +//! 协程C发出信号 +cond.post("event_b"); //! 两个条件都满足,唤醒协程A +``` + +### Broadcast — 广播 + +多个协程等待一个信号,信号发出时所有等待协程都被唤醒: + +```cpp +Broadcast bc(sch); + +//! 多个协程等待 +bc.wait(); + +//! 发出广播 +bc.post(); //! 所有等待的协程被唤醒 +``` + +## 使用示例 + +### 基础用法 + +> 完整示例见模块 README 和单元测试用例 + +```cpp +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::coroutine; + +int main() { + LogOutput_Enable(); + + Loop *sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + Scheduler sch(sp_loop); + + //! 定义协程1 + int routine1_count = 0; + sch.create( + [&] (Scheduler &sch) { + for (int i = 0; i < 20; ++i) { + ++routine1_count; + sch.yield(); //! 主动让出执行权 + } + }, true, "routine1" + ); + + //! 定义协程2 + int routine2_count = 0; + sch.create( + [&] (Scheduler &sch) { + for (int i = 0; i < 10; ++i) { + ++routine2_count; + sch.yield(); + } + }, true, "routine2" + ); + + sp_loop->exitLoop(std::chrono::seconds(1)); + sp_loop->runLoop(); + + LogInfo("r1=%d, r2=%d", routine1_count, routine2_count); + + LogOutput_Disable(); + return 0; +} +``` + +### 协程间通信 — Channel + +```cpp +Scheduler sch(sp_loop); + +Channel ch(sch); + +//! 生产者协程 +sch.create([&] (Scheduler &sch) { + for (int i = 0; i < 5; ++i) { + ch << i; + sch.yield(); + } +}); + +//! 消费者协程 +sch.create([&] (Scheduler &sch) { + int value; + while (ch >> value) { + LogInfo("received: %d", value); + } +}); +``` + +### 协程间互斥 — Mutex + +```cpp +Mutex mtx(sch); + +sch.create([&] (Scheduler &sch) { + Mutex::Locker locker(mtx); //! 自动加锁 + LogInfo("locked, doing work"); + sch.yield(); + //! ... 临界区操作 ... +}); //! locker 析构时自动解锁 +``` + +### 等待协程结束 — join + +```cpp +auto other_token = sch.create([&] (Scheduler &sch) { + //! 另一个协程的工作 + sch.yield(); + sch.yield(); +}); + +sch.create([&] (Scheduler &sch) { + sch.join(other_token); //! 等待 other_token 协程结束 + LogInfo("other routine finished"); +}); +``` + +## 常见场景 + +1. **顺序型业务逻辑**:将多步骤异步流程用协程写为顺序代码 +2. **生产者-消费者**:使用 Channel 在协程间传递数据 +3. **共享资源保护**:使用 Mutex 保护协程间的临界区 +4. **等待条件满足**:使用 Condition 等待多个条件 +5. **广播通知**:使用 Broadcast 同时唤醒多个协程 + +## 注意事项 + +1. **yield vs wait**:`yield()` 切换到主协程,下一个事件循环自动继续;`wait()` 切换到主协程,需要被 `resume()` 唤醒才能继续 +2. **协程是单线程的**:所有协程在同一个 Loop 线程中调度,不存在真正的并发,不需要原子操作 +3. **栈大小**:默认栈大小 8KB(`ROUTINE_STACK_DEFAULT_SIZE`),可通过 create() 参数指定更大的栈 +4. **cancel 是异步的**:cancel() 只是发送取消请求,协程在下次 wait/yield 时检查 isCanceled() 并退出 +5. **Channel 的 >> 返回值**:当协程被 cancel 时,`>>` 操作返回 false +6. **Condition 不支持多协程同时等**:同一 Condition 只能有一个协程在 wait() + +## 相关模块 + +- **event**:协程基于 Loop 调度运行 +- **main**:框架自动创建 Scheduler,通过 `ctx.coroutine()` 获取 +- **base**:提供 Cabinet/Token 等基础设施 diff --git a/documents/modules/crypto.md b/documents/modules/crypto.md new file mode 100644 index 00000000..df06961c --- /dev/null +++ b/documents/modules/crypto.md @@ -0,0 +1,104 @@ +# Crypto Module (crypto) + +## What is it? + +The crypto module provides implementations of two fundamental cryptographic algorithms: MD5 message digest and AES encryption/decryption. + +## Why do you need it? + +In data security scenarios, MD5 is used for verifying data integrity and generating unique identifiers, while AES is used for data encryption protection. The crypto module provides these two most commonly used cryptographic algorithms with simple and easy-to-use interfaces. + +## Header Files + +```cpp +#include //! MD5 message digest +#include //! AES encryption/decryption +``` + +## Core Classes and Interfaces + +### MD5 — MD5 Message Digest + +| Method | Description | +|------|------| +| `MD5()` | Constructor | +| `update(data, len)` | Feed plaintext data, can be called multiple times | +| `finish(digest)` | Finalize computation, output 16-byte digest | + +> **Note**: After calling `finish()`, you cannot call `update()` again. + +### AES — AES Encryption/Decryption + +AES only implements single 16-byte block operations (AES-128). + +| Method | Description | +|------|------| +| `AES(key)` | Constructor, takes a 16-byte key | +| `setKey(key)` | Set/change the key | +| `cipher(input, output)` | Encrypt a 16-byte block | +| `invcipher(input, output)` | Decrypt a 16-byte block | + +> **Note**: Both input and output are 16 bytes in length, and the key is also 16 bytes. + +## Usage Examples + +> Full examples available in the header file inline examples and unit test cases + +### MD5 Computation + +```cpp +const char *str1 = "cpp-tbox, C++ Treasure Box,"; +const char *str2 = " is an event-based service application development library."; + +crypto::MD5 md5; +md5.update(str1, strlen(str1)); //! Can feed data in segments +md5.update(str2, strlen(str2)); + +uint8_t md5_digest[16]; +md5.finish(md5_digest); //! Get the 16-byte MD5 digest + +//! Convert digest to readable hex string +char hex_str[33]; +for (int i = 0; i < 16; ++i) + snprintf(hex_str + i*2, 3, "%02x", md5_digest[i]); +LogInfo("MD5: %s", hex_str); +``` + +### AES Encryption and Decryption + +```cpp +uint8_t key[16] = {0x01,0x02,...}; //! 16-byte key +uint8_t plain_text[16] = "Hello AES!..."; //! 16-byte plaintext +uint8_t cipher_text[16]; //! Ciphertext output +uint8_t decrypted[16]; //! Decrypted plaintext + +crypto::AES aes(key); + +//! Encrypt +aes.cipher(plain_text, cipher_text); + +//! Decrypt +aes.invcipher(cipher_text, decrypted); + +//! decrypted should match plain_text +``` + +## Common Scenarios + +1. **Data Integrity Verification**: Compute a file's MD5 digest and compare it against a known digest +2. **Unique Identifier Generation**: Use MD5 to generate a unique ID from combined data +3. **Data Encryption Protection**: Use AES to encrypt sensitive data, transmit or store the ciphertext +4. **Key Rotation**: Use setKey() to switch keys without recreating the AES object + +## Important Notes + +1. **MD5 Security**: MD5 is no longer recommended for security authentication scenarios (collision attacks exist); use it only for checksums and identifier generation +2. **AES Single Block Only**: This implementation only handles 16-byte single blocks; for encrypting longer data, you need to implement CBC/CTR or other modes yourself +3. **Key Length**: Only supports 16-byte keys (AES-128); 24/32-byte keys are not supported +4. **finish() Finality**: After MD5's finish() is called, the object state is marked as finalized and update() cannot be called again +5. **Ciphertext Length**: AES encryption output is the same length as input (16 bytes), with no length increase + +## Related Modules + +- **base**: Provides basic type definitions +- **util**: Can be combined with Base64 to encode encryption results as text diff --git a/documents/modules/crypto_CN.md b/documents/modules/crypto_CN.md new file mode 100644 index 00000000..55737820 --- /dev/null +++ b/documents/modules/crypto_CN.md @@ -0,0 +1,104 @@ +# 加密模块 (crypto) + +## 是什么? + +crypto 模块提供了 MD5 消息摘要和 AES 加密/解密两种基础加密算法的实现。 + +## 为什么需要它? + +在数据安全场景中,MD5 用于校验数据完整性和生成唯一标识,AES 用于数据加密保护。crypto 模块提供了这两种最常用的加密算法,接口简洁易用。 + +## 头文件 + +```cpp +#include //! MD5 消息摘要 +#include //! AES 加密/解密 +``` + +## 核心类与接口 + +### MD5 — MD5 消息摘要 + +| 方法 | 说明 | +|------|------| +| `MD5()` | 构造 | +| `update(data, len)` | 喂入明文数据,可多次调用 | +| `finish(digest)` | 结束运算,输出16字节摘要 | + +> **注意**:`finish()` 后不可再调用 `update()`。 + +### AES — AES 加密/解密 + +AES 仅实现单个 16 字节块的运算(AES-128)。 + +| 方法 | 说明 | +|------|------| +| `AES(key)` | 构造,传入16字节密钥 | +| `setKey(key)` | 设置/更换密钥 | +| `cipher(input, output)` | 加密16字节块 | +| `invcipher(input, output)` | 解密16字节块 | + +> **注意**:input/output 均为 16 字节长度,密钥也是 16 字节。 + +## 使用示例 + +> 完整示例见头文件内联示例和单元测试用例 + +### MD5 计算 + +```cpp +const char *str1 = "cpp-tbox, C++ Treasure Box,"; +const char *str2 = " is an event-based service application development library."; + +crypto::MD5 md5; +md5.update(str1, strlen(str1)); //! 可分段喂入 +md5.update(str2, strlen(str2)); + +uint8_t md5_digest[16]; +md5.finish(md5_digest); //! 得到16字节MD5摘要 + +//! 将摘要转为可读字串 +char hex_str[33]; +for (int i = 0; i < 16; ++i) + snprintf(hex_str + i*2, 3, "%02x", md5_digest[i]); +LogInfo("MD5: %s", hex_str); +``` + +### AES 加密与解密 + +```cpp +uint8_t key[16] = {0x01,0x02,...}; //! 16字节密钥 +uint8_t plain_text[16] = "Hello AES!..."; //! 16字节明文 +uint8_t cipher_text[16]; //! 密文输出 +uint8_t decrypted[16]; //! 解密后明文 + +crypto::AES aes(key); + +//! 加密 +aes.cipher(plain_text, cipher_text); + +//! 解密 +aes.invcipher(cipher_text, decrypted); + +//! decrypted 应与 plain_text 一致 +``` + +## 常见场景 + +1. **数据完整性校验**:计算文件的 MD5 摘要,与已知摘要比对 +2. **唯一标识生成**:用 MD5 对组合数据生成唯一 ID +3. **数据加密保护**:使用 AES 加密敏感数据,传输或存储密文 +4. **密钥更换**:使用 setKey() 切换密钥,无需重新创建 AES 对象 + +## 注意事项 + +1. **MD5 安全性**:MD5 已不推荐用于安全认证场景(存在碰撞攻击),建议仅用于校验和标识生成 +2. **AES 仅单块**:本实现仅处理16字节单块,如需加密长数据需自行实现 CBC/CTR 等模式 +3. **密钥长度**:仅支持 16 字节密钥(AES-128),不支持 24/32 字节密钥 +4. **finish() 后不可再用**:MD5 的 finish() 调用后对象状态被标记为已完成,不可再 update() +5. **密文长度**:AES 加密输出与输入等长(16字节),不增加长度 + +## 相关模块 + +- **base**:提供基础类型定义 +- **util**:可配合 Base64 将加密结果编码为文本 diff --git a/documents/modules/dbus.md b/documents/modules/dbus.md new file mode 100644 index 00000000..4e4154f4 --- /dev/null +++ b/documents/modules/dbus.md @@ -0,0 +1,111 @@ +# D-Bus Integration Module (dbus) + +## What is it? + +The dbus module provides integration between D-Bus (the Linux desktop/system inter-process communication bus) and the cpp-tbox event loop, enabling tbox-based service applications to interact with other system services via D-Bus. + +## Why do you need it? + +In Linux systems, D-Bus is the standard mechanism for inter-process communication. Many system services (such as systemd, NetworkManager, Bluetooth services, etc.) expose interfaces through D-Bus. The dbus module allows tbox programs to interact with these services while maintaining an event-driven asynchronous model. + +## Header Files + +```cpp +#include //! D-Bus integration with event::Loop +#include //! D-Bus connection +``` + +## Core Classes and Interfaces + +### Connection — D-Bus Connection + +| Method | Description | +|------|------| +| `Connection(loop)` | Constructor, specifies the event loop | +| `initialize(bus_type)` | Initialize, specifies the bus type (kSession/kSystem/kStarter) | +| `initialize(bus_address)` | Initialize, specifies a custom bus address | +| `cleanup()` | Clean up the connection | + +Bus types: +- `kSession` — Session bus (user-level desktop services) +- `kSystem` — System bus (system-level services) +- `kStarter` — Starter bus + +### AttachLoop / DetachLoop — Attach/Detach Loop + +```cpp +//! Attach an existing DBusConnection object to event::Loop +dbus::AttachLoop(dbus_conn, sp_loop); + +//! Detach from event::Loop +dbus::DetachLoop(dbus_conn); +``` + +Useful for scenarios where you already have a DBusConnection (such as a connection object obtained directly from libdbus). + +## Usage Examples + +### Basic Connection + +> Full example available in `examples/dbus/00-loop/` + +```cpp +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; + +int main() { + LogOutput_Enable(); + + Loop* sp_loop = Loop::New(); + + dbus::Connection dbus_conn(sp_loop); + dbus_conn.initialize(dbus::Connection::kSession); //! Connect to session bus + + //! From here, dbus_conn can be used for D-Bus method calls, signal reception, etc. + + sp_loop->runLoop(); + dbus_conn.cleanup(); + + LogOutput_Disable(); + return 0; +} +``` + +### Attaching an Existing DBusConnection + +```cpp +DBusConnection *raw_conn = dbus_bus_get(DBUS_BUS_SESSION, nullptr); + +//! Integrate the native D-Bus connection into the tbox event loop +dbus::AttachLoop(raw_conn, sp_loop); + +//! D-Bus event listening is now managed by the tbox Loop + +//! Detach +dbus::DetachLoop(raw_conn); +``` + +## Common Scenarios + +1. **Interacting with System Services**: Call NetworkManager, systemd and other system service interfaces via D-Bus +2. **Desktop Application Integration**: Communicate with GNOME/KDE desktop services +3. **Cross-process Signal Delivery**: Pass events between processes using the D-Bus signal mechanism +4. **Hardware Status Queries**: Query Bluetooth, USB device and other hardware status + +## Important Notes + +1. **Dependency on libdbus**: The dbus-1 library must be linked at compile time +2. **Linux Only**: D-Bus is a Linux-specific IPC mechanism and cannot be used on other platforms +3. **Connection Encapsulation**: The Connection object encapsulates DBusConnection creation and event integration, no manual management needed +4. **Event-driven**: D-Bus event listening is managed by Loop, no additional dbus_watch/dbus_timeout handling required +5. **Cleanup Order**: Clean up Connection before cleaning up Loop when the program exits + +## Related Modules + +- **event**: Manages D-Bus event listening based on Loop +- **base**: Provides Log and other infrastructure diff --git a/documents/modules/dbus_CN.md b/documents/modules/dbus_CN.md new file mode 100644 index 00000000..218d3e50 --- /dev/null +++ b/documents/modules/dbus_CN.md @@ -0,0 +1,111 @@ +# D-Bus 集成模块 (dbus) + +## 是什么? + +dbus 模块提供了 D-Bus(Linux 桌面/系统进程间通信总线)与 cpp-tbox 事件循环的集成能力,让基于 tbox 的服务程序能通过 D-Bus 与其他系统服务交互。 + +## 为什么需要它? + +在 Linux 系统中,D-Bus 是进程间通信的标准机制。许多系统服务(如 systemd、NetworkManager、蓝牙服务等)通过 D-Bus 提供接口。dbus 模块让 tbox 程序能与这些服务交互,同时保持事件驱动的异步模式。 + +## 头文件 + +```cpp +#include //! D-Bus 与 event::Loop 的集成 +#include //! D-Bus 连接 +``` + +## 核心类与接口 + +### Connection — D-Bus 连接 + +| 方法 | 说明 | +|------|------| +| `Connection(loop)` | 构造,指定事件循环 | +| `initialize(bus_type)` | 初始化,指定总线类型(kSession/kSystem/kStarter) | +| `initialize(bus_address)` | 初始化,指定自定义总线地址 | +| `cleanup()` | 清理连接 | + +总线类型: +- `kSession` — 会话总线(用户级桌面服务) +- `kSystem` — 系统总线(系统级服务) +- `kStarter` — 启动总线 + +### AttachLoop / DetachLoop — 挂载/卸载 Loop + +```cpp +//! 将已有的 DBusConnection 对象挂载到 event::Loop +dbus::AttachLoop(dbus_conn, sp_loop); + +//! 从 event::Loop 上卸载 +dbus::DetachLoop(dbus_conn); +``` + +适用于已有 DBusConnection 的场景(如从 libdbus 直接获取的连接对象)。 + +## 使用示例 + +### 基本连接 + +> 完整示例见 `examples/dbus/00-loop/` + +```cpp +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; + +int main() { + LogOutput_Enable(); + + Loop* sp_loop = Loop::New(); + + dbus::Connection dbus_conn(sp_loop); + dbus_conn.initialize(dbus::Connection::kSession); //! 连接会话总线 + + //! 此后可使用 dbus_conn 进行 D-Bus 方法调用、信号接收等操作 + + sp_loop->runLoop(); + dbus_conn.cleanup(); + + LogOutput_Disable(); + return 0; +} +``` + +### 挂载已有 DBusConnection + +```cpp +DBusConnection *raw_conn = dbus_bus_get(DBUS_BUS_SESSION, nullptr); + +//! 将原生 D-Bus 连接集成到 tbox 事件循环 +dbus::AttachLoop(raw_conn, sp_loop); + +//! 此后 D-Bus 事件监听由 tbox Loop 管理 + +//! 卸载 +dbus::DetachLoop(raw_conn); +``` + +## 常见场景 + +1. **与系统服务交互**:通过 D-Bus 调用 NetworkManager、systemd 等系统服务接口 +2. **桌面应用集成**:与 GNOME/KDE 桌面服务通信 +3. **跨进程信号传递**:通过 D-Bus 信号机制在进程间传递事件 +4. **硬件状态查询**:查询蓝牙、USB 设备等硬件状态 + +## 注意事项 + +1. **依赖 libdbus**:编译时需要链接 dbus-1 库 +2. **仅 Linux**:D-Bus 是 Linux 特有的 IPC 机制,不可用于其他平台 +3. **Connection 封装**:Connection 对象封装了 DBusConnection 的创建和事件集成,无需手动管理 +4. **事件驱动**:D-Bus 事件监听由 Loop 管理,无需额外的 dbus_watch/dbus_timeout 处理 +5. **cleanup 顺序**:程序退出前先 cleanup Connection,再 cleanup Loop + +## 相关模块 + +- **event**:基于 Loop 管理 D-Bus 事件监听 +- **base**:提供 Log 等基础设施 diff --git a/documents/modules/event.md b/documents/modules/event.md new file mode 100644 index 00000000..66d894fe --- /dev/null +++ b/documents/modules/event.md @@ -0,0 +1,223 @@ +# Event-Driven Module (event) + +## What is it? + +The event module is the core foundation of cpp-tbox, providing an event loop (Loop) and three basic event types (FdEvent, TimerEvent, SignalEvent). It is the heart of the entire framework. Almost all other modules depend on the event module to run. + +## Why do you need it? + +In service-type programs, the program needs to respond to multiple events simultaneously: network data arrival, timed task expiration, signal interrupts, etc. If handled with traditional multi-threading, you encounter issues like complex thread synchronization and resource contention. The event-driven model uses a single-threaded event loop to efficiently handle all asynchronous events, avoiding thread switching overhead. + +## Header Files + +```cpp +#include //! Event loop +#include //! File descriptor event +#include //! Timer event +#include //! Signal event +#include //! Event base class +``` + +## Core Classes and Interfaces + +### Loop — Event Loop + +The event loop is the dispatch center for all event handling. The program enters the loop via `runLoop()`, monitoring and dispatching events within the loop. + +| Method | Description | +|--------|-------------| +| `Loop::New()` | Create an event loop of the default type | +| `Loop::New(engine_type)` | Create an event loop with a specified engine type (e.g., "epoll", "poll") | +| `Loop::Engines()` | Get the list of available engines | +| `runLoop(Mode)` | Run the event loop; Mode::kOnce executes once, Mode::kForever runs continuously | +| `exitLoop(wait_time)` | Exit the event loop, optionally specifying a wait time | +| `isInLoopThread()` | Check whether the current thread is the Loop thread | +| `isRunning()` | Check whether the Loop is currently running | +| `newFdEvent(what)` | Create an FdEvent object | +| `newTimerEvent(what)` | Create a TimerEvent object | +| `newSignalEvent(what)` | Create a SignalEvent object | +| `cleanup()` | Clean up Loop resources | + +#### Task Injection Methods + +Loop provides three methods for injecting functions into the loop for execution. Their differences are as follows: + +| Method | Characteristics | Suitable Scenarios | +|--------|-----------------|--------------------| +| `runInLoop(func)` | Uses locking; supports cross-thread and cross-Loop calls | Delegating tasks between different Loops, or other threads dispatching tasks to the Loop thread | +| `runNext(func)` | No locking; does not support cross-thread calls; only callable within the Loop thread | Execute immediately after the current callback completes, e.g., freeing the object itself | +| `run(func)` | Auto-selects: uses runNext within the Loop thread, otherwise uses runInLoop | When unsure which one to use, just pick this one | + +> **Usage tip**: Use `runNext()` when you are certain you are in the Loop thread, use `runInLoop()` when you are certain you are not in the Loop thread, and use `run()` when you are unsure. + +All injection methods return `RunId`, which can be used to cancel unexecuted tasks via `cancel(RunId)`. + +### FdEvent — File Descriptor Event + +Monitors readable, writable, and exception events on a file descriptor (fd). + +| Method | Description | +|--------|-------------| +| `initialize(fd, events, mode)` | Initialize; events is a combination of kReadEvent/kWriteEvent/kExceptEvent | +| `setCallback(cb)` | Set callback function with parameter `short events` | +| `enable()` | Enable monitoring | +| `disable()` | Disable monitoring | + +Event modes: +- `Mode::kPersist` — Persistent monitoring; does not auto-cancel after the event triggers +- `Mode::kOneshot` — One-time monitoring; auto-cancels after the event triggers + +### TimerEvent — Timer Event + +Triggers a callback after a specified time. + +| Method | Description | +|--------|-------------| +| `initialize(time_span, mode)` | Initialize; time_span is the duration in milliseconds | +| `setCallback(cb)` | Set callback function | +| `enable()` | Enable the timer | +| `disable()` | Disable the timer | + +### SignalEvent — Signal Event + +Monitors Linux signals (e.g., SIGINT, SIGTERM). + +| Method | Description | +|--------|-------------| +| `initialize(signum, mode)` | Initialize; monitor a single signal | +| `initialize(sigset, mode)` | Initialize; monitor a signal set | +| `initialize({sig1,sig2,...}, mode)` | Initialize; monitor a signal list | +| `setCallback(cb)` | Set callback function with parameter `int signum` | + +## Usage Examples + +### Timer Example + +> Full example available at `examples/event/02_timer/` + +```cpp +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; + +int main() { + LogOutput_Enable(); + + Loop* sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + //! Create a periodic timer that triggers every second + auto sp_timer = sp_loop->newTimerEvent("timer"); + SetScopeExitAction([sp_timer] { delete sp_timer; }); + + sp_timer->initialize(std::chrono::seconds(1), Event::Mode::kPersist); + sp_timer->setCallback([] { LogInfo("timer tick"); }); + sp_timer->enable(); + + //! Exit after running for 5 seconds + sp_loop->exitLoop(std::chrono::seconds(5)); + sp_loop->runLoop(); + + LogOutput_Disable(); + return 0; +} +``` + +### Signal Handling Example + +> Full example available at `examples/event/03_signal/` + +```cpp +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; + +int main() { + LogOutput_Enable(); + + Loop* sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + //! Monitor SIGINT and SIGTERM signals + auto sp_sig = sp_loop->newSignalEvent("signal"); + SetScopeExitAction([sp_sig] { delete sp_sig; }); + + sp_sig->initialize({SIGINT, SIGTERM}, Event::Mode::kPersist); + sp_sig->setCallback( + [sp_loop] (int signum) { + LogInfo("received signal %d", signum); + sp_loop->exitLoop(); + } + ); + sp_sig->enable(); + + sp_loop->runLoop(); + LogOutput_Disable(); + return 0; +} +``` + +### runInLoop Cross-Thread Task Injection + +> Full example available at `examples/event/04_run_in_loop/` + +```cpp +//! Inject a task into the Loop from another thread +std::thread t( + [sp_loop] { + //! Safe cross-thread call + sp_loop->runInLoop([] { LogInfo("task from other thread"); }); + } +); +``` + +### runNext Safe Self-Release + +> Full example available at `examples/event/07_delay_delete/` + +```cpp +//! Safely release the object itself within a callback +void MyClass::onTimeout() { + //! Cannot directly delete this, which would cause accessing a destructed object within the callback + //! Use runNext to perform the release after the callback completes + loop_->runNext([this] { delete this; }); +} +``` + +## Common Scenarios + +1. **Program main loop**: Create a Loop, run `runLoop(Mode::kForever)`, exit via `exitLoop()` +2. **Timed tasks**: Use TimerEvent to create periodic or one-time timers +3. **Signal handling**: Use SignalEvent to capture SIGINT/SIGTERM for graceful program exit +4. **Cross-thread communication**: Other threads inject tasks into the Loop thread via `runInLoop()` +5. **Safe object release**: Use `runNext()` to release objects after callbacks finish + +## Important Notes + +1. **Loop is single-threaded**: All event callbacks execute in the Loop thread; do not perform time-consuming operations in callbacks, as this will block the event loop +2. **runNext is limited to the Loop thread**: Calling `runNext()` across threads is prohibited; cross-thread calls must use `runInLoop()` +3. **Event object lifecycle**: Event objects created through Loop (newFdEvent/newTimerEvent/newSignalEvent) need to be manually deleted; it is recommended to use `SetScopeExitAction` to manage their lifecycle +4. **Oneshot mode**: TimerEvent's kOneshot mode auto-disables after triggering; if you need to trigger again, you must re-enable it +5. **cancel() timing**: Tasks that have already started executing cannot be canceled; only unexecuted tasks can be canceled + +## Related Modules + +- **eventx**: Provides advanced features like thread pools and timer pools based on event +- **base**: Provides infrastructure such as log macros and ScopeExit +- **network**: Implements TCP/UDP/UART communication based on event's FdEvent +- **alarm**: Implements timed alarms based on event's TimerEvent +- **main**: The framework's built-in Loop object, accessible via Context + +## Reference Image + +![tbox-loop](../images/0001-tbox-loop.jpg) diff --git a/documents/modules/event_CN.md b/documents/modules/event_CN.md new file mode 100644 index 00000000..b15b4d78 --- /dev/null +++ b/documents/modules/event_CN.md @@ -0,0 +1,223 @@ +# 事件驱动模块 (event) + +## 是什么? + +event 模块是 cpp-tbox 的核心基石,提供了事件循环(Loop)与三种基本事件类型(FdEvent、TimerEvent、SignalEvent),是整个框架运转的心脏。几乎所有其他模块都依赖 event 模块运行。 + +## 为什么需要它? + +在服务型程序中,程序需要同时响应多种事件:网络数据到达、定时任务到期、信号中断等。如果用传统多线程处理,会遇到线程同步复杂、资源抢占等问题。事件驱动模型通过单线程事件循环,高效地处理所有异步事件,避免线程切换开销。 + +## 头文件 + +```cpp +#include //! 事件循环 +#include //! 文件描述符事件 +#include //! 定时器事件 +#include //! 信号事件 +#include //! 事件基类 +``` + +## 核心类与接口 + +### Loop — 事件循环 + +事件循环是所有事件处理的调度中心。程序通过 `runLoop()` 进入循环,在循环中监听和分发事件。 + +| 方法 | 说明 | +|------|------| +| `Loop::New()` | 创建默认类型的事件循环 | +| `Loop::New(engine_type)` | 创建指定引擎类型的事件循环(如 "epoll"、"poll") | +| `Loop::Engines()` | 获取可用的引擎列表 | +| `runLoop(Mode)` | 运行事件循环,Mode::kOnce 执行一次,Mode::kForever 持续运行 | +| `exitLoop(wait_time)` | 退出事件循环,可指定等待时间 | +| `isInLoopThread()` | 判断是否在 Loop 线程内 | +| `isRunning()` | 判断 Loop 是否正在运行 | +| `newFdEvent(what)` | 创建 FdEvent 对象 | +| `newTimerEvent(what)` | 创建 TimerEvent 对象 | +| `newSignalEvent(what)` | 创建 SignalEvent 对象 | +| `cleanup()` | 清理 Loop 资源 | + +#### 任务注入方法 + +Loop 提供三种将函数注入循环执行的方法,它们的区别如下: + +| 方法 | 特点 | 适用场景 | +|------|------|----------| +| `runInLoop(func)` | 有加锁操作,支持跨线程、跨 Loop 调用 | 不同 Loop 之间委派任务,或其它线程向 Loop 线程派任务 | +| `runNext(func)` | 无加锁操作,不支持跨线程,仅 Loop 线程内调用 | 在当前回调完成后立即执行,如释放对象自身 | +| `run(func)` | 自动选择:Loop 线程内用 runNext,否则用 runInLoop | 不确定该用哪个时,选它即可 | + +> **使用建议**:明确在 Loop 线程内的用 `runNext()`,明确不在 Loop 线程内的用 `runInLoop()`,不清楚的用 `run()`。 + +所有注入方法返回 `RunId`,可通过 `cancel(RunId)` 取消未执行的任务。 + +### FdEvent — 文件描述符事件 + +监听文件描述符(fd)上的可读、可写、异常事件。 + +| 方法 | 说明 | +|------|------| +| `initialize(fd, events, mode)` | 初始化,events 为 kReadEvent/kWriteEvent/kExceptEvent 组合 | +| `setCallback(cb)` | 设置回调函数,参数为 `short events` | +| `enable()` | 启用监听 | +| `disable()` | 停用监听 | + +Event 模式: +- `Mode::kPersist` — 持续监听,事件触发后不自动取消 +- `Mode::kOneshot` — 一次性监听,事件触发后自动取消 + +### TimerEvent — 定时器事件 + +在指定时间后触发回调。 + +| 方法 | 说明 | +|------|------| +| `initialize(time_span, mode)` | 初始化,time_span 为毫秒时长 | +| `setCallback(cb)` | 设置回调函数 | +| `enable()` | 启用定时器 | +| `disable()` | 停用定时器 | + +### SignalEvent — 信号事件 + +监听 Linux 信号(如 SIGINT、SIGTERM)。 + +| 方法 | 说明 | +|------|------| +| `initialize(signum, mode)` | 初始化,监听单个信号 | +| `initialize(sigset, mode)` | 初始化,监听信号集合 | +| `initialize({sig1,sig2,...}, mode)` | 初始化,监听信号列表 | +| `setCallback(cb)` | 设置回调函数,参数为 `int signum` | + +## 使用示例 + +### 定时器示例 + +> 完整示例见 `examples/event/02_timer/` + +```cpp +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; + +int main() { + LogOutput_Enable(); + + Loop* sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + //! 创建周期定时器,每秒触发一次 + auto sp_timer = sp_loop->newTimerEvent("timer"); + SetScopeExitAction([sp_timer] { delete sp_timer; }); + + sp_timer->initialize(std::chrono::seconds(1), Event::Mode::kPersist); + sp_timer->setCallback([] { LogInfo("timer tick"); }); + sp_timer->enable(); + + //! 运行5秒后退出 + sp_loop->exitLoop(std::chrono::seconds(5)); + sp_loop->runLoop(); + + LogOutput_Disable(); + return 0; +} +``` + +### 信号处理示例 + +> 完整示例见 `examples/event/03_signal/` + +```cpp +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; + +int main() { + LogOutput_Enable(); + + Loop* sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + //! 监听 SIGINT 与 SIGTERM 信号 + auto sp_sig = sp_loop->newSignalEvent("signal"); + SetScopeExitAction([sp_sig] { delete sp_sig; }); + + sp_sig->initialize({SIGINT, SIGTERM}, Event::Mode::kPersist); + sp_sig->setCallback( + [sp_loop] (int signum) { + LogInfo("received signal %d", signum); + sp_loop->exitLoop(); + } + ); + sp_sig->enable(); + + sp_loop->runLoop(); + LogOutput_Disable(); + return 0; +} +``` + +### runInLoop 跨线程任务注入 + +> 完整示例见 `examples/event/04_run_in_loop/` + +```cpp +//! 在其它线程中向 Loop 注入任务 +std::thread t( + [sp_loop] { + //! 跨线程安全调用 + sp_loop->runInLoop([] { LogInfo("task from other thread"); }); + } +); +``` + +### runNext 释放对象自身 + +> 完整示例见 `examples/event/07_delay_delete/` + +```cpp +//! 在回调中安全释放自身对象 +void MyClass::onTimeout() { + //! 不能直接 delete this,会导致回调中访问已析构对象 + //! 使用 runNext 在回调完成后执行释放 + loop_->runNext([this] { delete this; }); +} +``` + +## 常见场景 + +1. **程序主循环**:创建 Loop,运行 `runLoop(Mode::kForever)`,通过 `exitLoop()` 退出 +2. **定时任务**:使用 TimerEvent 创建周期或一次性定时器 +3. **信号处理**:使用 SignalEvent 捕获 SIGINT/SIGTERM,优雅退出程序 +4. **跨线程通信**:其它线程通过 `runInLoop()` 向 Loop 线程注入任务 +5. **安全释放对象**:通过 `runNext()` 在回调结束后释放对象 + +## 注意事项 + +1. **Loop 是单线程的**:所有事件回调都在 Loop 线程中执行,不要在回调中执行耗时操作,否则会阻塞事件循环 +2. **runNext 仅限 Loop 线程**:禁止跨线程调用 `runNext()`,跨线程必须使用 `runInLoop()` +3. **事件对象生命周期**:通过 Loop 创建的事件对象(newFdEvent/newTimerEvent/newSignalEvent)需要手动 delete,建议使用 `SetScopeExitAction` 管理生命周期 +4. **Oneshot 模式**:TimerEvent 的 kOneshot 模式触发后自动 disable,如需再次触发需重新 enable +5. **cancel() 的时机**:已开始执行的任务无法 cancel,只能取消尚未执行的任务 + +## 相关模块 + +- **eventx**:基于 event 提供线程池、定时池等高级功能 +- **base**:提供日志宏、ScopeExit 等基础设施 +- **network**:基于 event 的 FdEvent 实现 TCP/UDP/UART 通信 +- **alarm**:基于 event 的 TimerEvent 实现定时闹钟 +- **main**:框架内置 Loop 对象,通过 Context 获取 + +## 参考图片 + +![tbox-loop](../images/0001-tbox-loop.jpg) diff --git a/documents/modules/eventx.md b/documents/modules/eventx.md new file mode 100644 index 00000000..139d486b --- /dev/null +++ b/documents/modules/eventx.md @@ -0,0 +1,254 @@ +# Event Extension Module (eventx) + +## What is it? + +The eventx module provides advanced asynchronous programming components built on top of the event module: ThreadPool, TimerPool, LoopThread, Async, TimeoutMonitor, and RequestPool. These components make it easier for developers to handle complex scenarios such as multi-thread coordination, timer task management, and request timeouts. + +## Why do you need it? + +The event module provides a single-threaded event loop, but in practice you often need: +- Move time-consuming computation or I/O operations to background threads, then return to the main thread to process results +- Create a large number of timers without having to manage each TimerEvent's lifecycle individually +- Run another event loop in a separate thread +- Convert blocking system calls (such as file read/write) into asynchronous callback form + +eventx is designed to solve these problems. + +## Header Files + +```cpp +#include //! ThreadPool +#include //! TimerPool +#include //! LoopThread +#include //! Async +#include //! RequestPool +#include //! TimeoutMonitor +#include //! ThreadExecutor interface +``` + +## Core Classes and Interfaces + +### ThreadPool + +ThreadPool is used to delegate time-consuming tasks to background threads for execution, and then return to the main thread to execute callbacks upon completion. + +| Method | Description | +|--------|-------------| +| `ThreadPool(main_loop)` | Constructor, specify the main thread's Loop | +| `initialize(min, max)` | Initialize, specify the number of resident threads and maximum threads | +| `execute(task, prio)` | Execute task in a worker thread, prio is priority [-2,2] | +| `execute(task, main_cb, prio)` | Worker thread executes task, then main thread executes main_cb upon completion | +| `execute(task)` | Execute task in a worker thread (no callback) | +| `execute(task, main_cb)` | Worker thread executes task, then main thread executes main_cb upon completion | +| `getTaskStatus(token)` | Get task status | +| `cancel(token)` | Cancel a task | +| `cleanup()` | Clean up resources, wait for all worker threads to finish | +| `snapshot()` | Get thread pool snapshot (thread count, idle count, task count, etc.) | + +### TimerPool + +TimerPool allows developers to easily create timed tasks without worrying about TimerEvent lifecycle management. + +| Method | Description | +|--------|-------------| +| `TimerPool(loop)` | Constructor, specify the Loop | +| `doEvery(msec, cb)` | Periodic timed task, returns TimerToken | +| `doAfter(msec, cb)` | One-shot delayed task, returns TimerToken | +| `doAt(time_point, cb)` | Execute at a specified time point, returns TimerToken | +| `cancel(token)` | Cancel a timed task | +| `cleanup()` | Clean up all timers | + +> **Note**: When using `cancel()` to cancel a timer, ensure that objects held by the callback function are still alive, to avoid lifetime inversion issues. + +### LoopThread + +Runs an event loop in a separate thread, commonly used in scenarios where events need to be processed in another thread. + +| Method | Description | +|--------|-------------| +| `LoopThread(run_now, name)` | Constructor, specify whether to run immediately and the Loop name | +| `start()` | Start the thread | +| `stop()` | Stop the thread | +| `isRunning()` | Whether the thread is running | +| `loop()` | Return the Loop object | + +> **Note**: During runtime, you can only inject tasks into LoopThread via `loop()->runInLoop()` or `loop()->run()`. Do not call other Loop methods directly. Do not delete the Loop object externally. + +### Async + +Converts blocking system calls into asynchronous callback form, utilizing a thread pool to execute in a background thread, then returning to the main thread for callback upon completion. + +| Method | Description | +|--------|-------------| +| `Async(thread_pool)` | Constructor, specify the thread pool | +| `readFile(filename, cb)` | Asynchronously read a file, cb returns (errcode, content) | +| `readFileLines(filename, cb)` | Asynchronously read file lines list | +| `writeFile(filename, content, sync, cb)` | Asynchronously write a file | +| `appendFile(filename, content, sync, cb)` | Asynchronously append to a file | +| `removeFile(filename, cb)` | Asynchronously delete a file | +| `executeCmd(cmd, cb)` | Asynchronously execute a command, cb returns (errcode) | + +### RequestPool + +RequestPool is used to manage context data for asynchronous requests, automatically handling timeout responses. + +```cpp +template +class RequestPool { + //! Initialize + bool initialize(check_interval, check_times); + //! Set timeout callback + void setTimeoutAction(action); + //! Create a new request, return Token + Token newRequest(T *req_ctx = nullptr); + //! Update request context + bool updateRequest(token, T *req_ctx); + //! Take away request context (remove record) + T* removeRequest(token); + //! Cleanup + void cleanup(); +}; +``` + +## Usage Examples + +### ThreadPool Basic Usage + +> Full example see `examples/eventx/thread_pool/` + +```cpp +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; + +int main() { + LogOutput_Enable(); + + Loop* sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + eventx::ThreadPool tp(sp_loop); + tp.initialize(2, 4); //! Minimum 2 threads, maximum 4 + + //! Background thread executes time-consuming operation, then main thread processes result + tp.execute( + [] { //! Worker thread: execute time-consuming computation + LogInfo("computing in worker thread..."); + // ... time-consuming operation ... + }, + [] { //! Main thread: process result + LogInfo("result received in main thread"); + // ... use computation result ... + } + ); + + //! Exit after 5 seconds + sp_loop->exitLoop(std::chrono::seconds(5)); + sp_loop->runLoop(); + + tp.cleanup(); + LogOutput_Disable(); + return 0; +} +``` + +### TimerPool + +> Full example see `examples/eventx/timer_fd/` + +```cpp +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; + +int main() { + LogOutput_Enable(); + + Loop* sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + eventx::TimerPool timer_pool(sp_loop); + + //! Execute every second + auto token_every = timer_pool.doEvery(std::chrono::seconds(1), + [] { LogInfo("periodic tick"); }); + + //! Execute once after 3 seconds + auto token_after = timer_pool.doAfter(std::chrono::seconds(3), + [] { LogInfo("one-shot timeout"); }); + + //! Cancel all timers and exit after 5 seconds + timer_pool.doAfter(std::chrono::seconds(5), + [&] { + timer_pool.cancel(token_every); + timer_pool.cancel(token_after); + sp_loop->exitLoop(); + }); + + sp_loop->runLoop(); + timer_pool.cleanup(); + + LogOutput_Disable(); + return 0; +} +``` + +### Asynchronous File Operations + +```cpp +#include + +//! Using Async in the main module +class App : public tbox::main::Module { + public: + App(Context &ctx) : Module("app", ctx), async_(ctx.thread_pool()) { } + + bool onStart() override { + //! Asynchronously read file without blocking the main thread + async_.readFile("/data/config.json", + [](int errcode, std::string &content) { + if (errcode == 0) { + LogInfo("file content: %s", content.c_str()); + } else { + LogErr("read file failed, errcode=%d", errcode); + } + }); + return true; + } + + private: + eventx::Async async_; +}; +``` + +## Common Scenarios + +1. **Time-consuming computation**: Put complex computation into ThreadPool, then use the result in the main thread upon completion +2. **File I/O**: Use Async for asynchronous file read/write without blocking the event loop +3. **Large number of timers**: Use TimerPool to manage timed tasks in bulk without manually managing TimerEvent lifetimes +4. **Multi-Loop coordination**: Use LoopThread to run another event loop in a separate thread +5. **Request timeout management**: Use RequestPool to automatically handle request timeout responses + +## Important Notes + +1. **ThreadPool callback threading**: The `main_cb` callback executes in the main thread (Loop thread), and `backend_task` executes in a worker thread +2. **TimerPool lifetime inversion**: If a timer callback holds a pointer to a short-lifetime object, the timer must be canceled before that object is destructed +3. **LoopThread restrictions**: During runtime, you can only interact with LoopThread via `loop()->runInLoop()` or `loop()->run()` +4. **ThreadPool cleanup**: Calling `cleanup()` will wait for all worker threads to finish, ensure it is called before program exit +5. **Async errcode**: errcode=0 indicates success, non-zero indicates failure (such as file not found, insufficient permissions, etc.) + +## Related Modules + +- **event**: Provides the base Loop, which is the underlying dependency of eventx +- **main**: The framework automatically creates ThreadPool/TimerPool/Async and provides them through Context +- **base**: Provides infrastructure such as Cabinet/Token/defines diff --git a/documents/modules/eventx_CN.md b/documents/modules/eventx_CN.md new file mode 100644 index 00000000..a30ec3f6 --- /dev/null +++ b/documents/modules/eventx_CN.md @@ -0,0 +1,254 @@ +# 事件扩展模块 (eventx) + +## 是什么? + +eventx 模块基于 event 模块提供高级异步编程组件:线程池(ThreadPool)、定时池(TimerPool)、独立 Loop 线程(LoopThread)、异步操作(Async)、超时监控(TimeoutMonitor)和请求池(RequestPool)。这些组件让开发者更方便地处理多线程协作、定时任务管理、请求超时等复杂场景。 + +## 为什么需要它? + +event 模块提供了单线程事件循环,但在实际应用中常需要: +- 将耗时的计算或 I/O 操作放到后台线程执行,完成后回到主线程处理结果 +- 创建大量定时器但不想逐个管理 TimerEvent 的生命周期 +- 在独立线程中运行另一个事件循环 +- 将阻塞性的系统调用(如文件读写)转换为异步回调形式 + +eventx 正是为了解决这些问题而设计的。 + +## 头文件 + +```cpp +#include //! 线程池 +#include //! 定时池 +#include //! 独立 Loop 线程 +#include //! 异步操作 +#include //! 请求池 +#include //! 超时监控 +#include //! 线程执行器接口 +``` + +## 核心类与接口 + +### ThreadPool — 线程池 + +线程池用于将耗时任务委派给后台线程执行,并在完成后回到主线程执行回调。 + +| 方法 | 说明 | +|------|------| +| `ThreadPool(main_loop)` | 构造,指定主线程的 Loop | +| `initialize(min, max)` | 初始化,指定常驻线程数与最大线程数 | +| `execute(task, prio)` | 在 worker 线程执行任务,prio 为优先级 [-2,2] | +| `execute(task, main_cb, prio)` | worker 线程执行 task,完成后主线程执行 main_cb | +| `execute(task)` | 在 worker 线程执行任务(无回调) | +| `execute(task, main_cb)` | worker 线程执行 task,完成后主线程执行 main_cb | +| `getTaskStatus(token)` | 获取任务状态 | +| `cancel(token)` | 取消任务 | +| `cleanup()` | 清理资源,等待所有 worker 线程结束 | +| `snapshot()` | 获取线程池快照(线程数、空闲数、任务数等) | + +### TimerPool — 定时池 + +定时池让开发者轻松创建定时任务,无需关心 TimerEvent 的生命周期管理。 + +| 方法 | 说明 | +|------|------| +| `TimerPool(loop)` | 构造,指定 Loop | +| `doEvery(msec, cb)` | 周期性定时任务,返回 TimerToken | +| `doAfter(msec, cb)` | 一次性延迟任务,返回 TimerToken | +| `doAt(time_point, cb)` | 在指定时间点执行,返回 TimerToken | +| `cancel(token)` | 取消定时任务 | +| `cleanup()` | 清理所有定时器 | + +> **注意**:使用 `cancel()` 取消定时器时,要确保回调函数中持有的对象仍然存活,避免生命期倒挂问题。 + +### LoopThread — 独立 Loop 线程 + +在独立线程中运行一个事件循环,常用于需要在另一个线程处理事件的场景。 + +| 方法 | 说明 | +|------|------| +| `LoopThread(run_now, name)` | 构造,指定是否立即运行和 Loop 名称 | +| `start()` | 启动线程 | +| `stop()` | 停止线程 | +| `isRunning()` | 线程是否正在运行 | +| `loop()` | 返回 Loop 对象 | + +> **注意**:运行过程中,只能通过 `loop()->runInLoop()` 或 `loop()->run()` 向 LoopThread 注入任务,不能直接调用其他 Loop 方法。不可在外部 delete Loop 对象。 + +### Async — 异步操作 + +将阻塞性的系统调用转换为异步回调形式,利用线程池在后台线程执行,完成后回到主线程回调。 + +| 方法 | 说明 | +|------|------| +| `Async(thread_pool)` | 构造,指定线程池 | +| `readFile(filename, cb)` | 异步读取文件,cb 返回 (errcode, content) | +| `readFileLines(filename, cb)` | 异步读取文件行列表 | +| `writeFile(filename, content, sync, cb)` | 异步写文件 | +| `appendFile(filename, content, sync, cb)` | 异步追加文件 | +| `removeFile(filename, cb)` | 异步删除文件 | +| `executeCmd(cmd, cb)` | 异步执行命令,cb 返回 (errcode) | + +### RequestPool — 请求池 + +请求池用于管理异步请求的上下文数据,自动处理超时回复。 + +```cpp +template +class RequestPool { + //! 初始化 + bool initialize(check_interval, check_times); + //! 设置超时回调 + void setTimeoutAction(action); + //! 创建新请求,返回 Token + Token newRequest(T *req_ctx = nullptr); + //! 更新请求上下文 + bool updateRequest(token, T *req_ctx); + //! 取走请求上下文(移除记录) + T* removeRequest(token); + //! 清理 + void cleanup(); +}; +``` + +## 使用示例 + +### 线程池基本用法 + +> 完整示例见 `examples/eventx/thread_pool/` + +```cpp +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; + +int main() { + LogOutput_Enable(); + + Loop* sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + eventx::ThreadPool tp(sp_loop); + tp.initialize(2, 4); //! 最少2个线程,最多4个 + + //! 后台线程执行耗时操作,完成后主线程处理结果 + tp.execute( + [] { //! worker 线程:执行耗时计算 + LogInfo("computing in worker thread..."); + // ... 耗时操作 ... + }, + [] { //! 主线程:处理结果 + LogInfo("result received in main thread"); + // ... 使用计算结果 ... + } + ); + + //! 5秒后退出 + sp_loop->exitLoop(std::chrono::seconds(5)); + sp_loop->runLoop(); + + tp.cleanup(); + LogOutput_Disable(); + return 0; +} +``` + +### 定时池 + +> 完整示例见 `examples/eventx/timer_fd/` + +```cpp +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; + +int main() { + LogOutput_Enable(); + + Loop* sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + eventx::TimerPool timer_pool(sp_loop); + + //! 每秒执行一次 + auto token_every = timer_pool.doEvery(std::chrono::seconds(1), + [] { LogInfo("periodic tick"); }); + + //! 3秒后执行一次 + auto token_after = timer_pool.doAfter(std::chrono::seconds(3), + [] { LogInfo("one-shot timeout"); }); + + //! 5秒后取消所有定时器并退出 + timer_pool.doAfter(std::chrono::seconds(5), + [&] { + timer_pool.cancel(token_every); + timer_pool.cancel(token_after); + sp_loop->exitLoop(); + }); + + sp_loop->runLoop(); + timer_pool.cleanup(); + + LogOutput_Disable(); + return 0; +} +``` + +### 异步文件操作 + +```cpp +#include + +//! 在 main 模块中使用 Async +class App : public tbox::main::Module { + public: + App(Context &ctx) : Module("app", ctx), async_(ctx.thread_pool()) { } + + bool onStart() override { + //! 异步读取文件,不阻塞主线程 + async_.readFile("/data/config.json", + [](int errcode, std::string &content) { + if (errcode == 0) { + LogInfo("file content: %s", content.c_str()); + } else { + LogErr("read file failed, errcode=%d", errcode); + } + }); + return true; + } + + private: + eventx::Async async_; +}; +``` + +## 常见场景 + +1. **耗时计算**:将复杂计算放到 ThreadPool,完成后在主线程使用结果 +2. **文件 I/O**:使用 Async 异步读写文件,不阻塞事件循环 +3. **大量定时器**:使用 TimerPool 批量管理定时任务,无需手动管理 TimerEvent 生命期 +4. **多 Loop 协作**:使用 LoopThread 在独立线程运行另一个事件循环 +5. **请求超时管理**:使用 RequestPool 自动处理请求超时回复 + +## 注意事项 + +1. **ThreadPool 回调线程**:`main_cb` 回调在主线程(Loop 线程)中执行,`backend_task` 在 worker 线程中执行 +2. **TimerPool 生命期倒挂**:如果定时器回调持有了短生命期对象的指针,该对象析构前必须 cancel 定时器 +3. **LoopThread 限制**:运行过程中只能通过 `loop()->runInLoop()` 或 `loop()->run()` 与 LoopThread 交互 +4. **ThreadPool cleanup**:调用 `cleanup()` 会等待所有 worker 线程结束,确保在程序退出前调用 +5. **Async errcode**:errcode=0 表示成功,非0表示失败(如文件不存在、权限不足等) + +## 相关模块 + +- **event**:提供基础的 Loop,是 eventx 的底层依赖 +- **main**:框架自动创建 ThreadPool/TimerPool/Async,通过 Context 提供 +- **base**:提供 Cabinet/Token/defines 等基础设施 diff --git a/documents/modules/flow.md b/documents/modules/flow.md new file mode 100644 index 00000000..a442687e --- /dev/null +++ b/documents/modules/flow.md @@ -0,0 +1,190 @@ +# Flow Control Module (flow) + +## What is it? + +The flow module provides two types of flow control tools: multi-level finite state machine (StateMachine) and behavior tree (Action series). StateMachine is used for state-driven business logic, while Action is used for composite complex flows. + +## Why do you need it? + +In event-driven programming, complex business flows need to manage a large number of states and conditional judgments. StateMachine provides clear state definitions and transition rules, while Action provides composite flow control (sequential, parallel, conditional selection, etc.), separating complex control logic from business code. + +![state-machine](../images/0010-state-machine-graph.png) + +![action-tree](../images/0010-action-tree-graph.jpg) + +## Header Files + +```cpp +#include //! State machine +#include //! Action base class +#include //! Action executor +#include //! Event definitions +#include //! Event publisher +#include //! Event subscriber +#include //! Export Graphviz diagram +``` + +## Core Classes and Interfaces + +### StateMachine — Multi-level Finite State Machine + +| Method | Description | +|--------|-------------| +| `newState(state_id, enter_action, exit_action, label)` | Create a state | +| `addRoute(from, event, to, guard, action, label)` | Add a state transition route | +| `addEvent(state_id, event_id, action)` | Add an in-state event handler | +| `setInitState(state_id)` | Set the initial state | +| `setSubStateMachine(state_id, sub_sm)` | Set a sub state machine (hierarchical nesting) | +| `setStateChangedCallback(cb)` | Set state change callback | +| `start()` | Start the state machine | +| `stop()` | Stop the state machine | +| `run(event)` | Run the state machine (pass in an event) | +| `currentState()` | Get the current state | +| `lastState()` | Get the previous state | +| `nextState()` | Get the next state (valid during transition) | +| `isRunning()` | Whether it is running | +| `isTerminated()` | Whether it has terminated | + +#### State Machine Key Concepts + +- **StateID**: State number, 0 is the terminated state, -1 is the invalid state +- **EventID**: Event number, 0 means any event +- **Route**: Defines a conditional route that transitions from one state to another upon receiving a specific event +- **GuardFunc**: Condition judgment function, returns true to indicate the condition is satisfied and the transition can proceed +- **EventFunc**: Event handler function, returns >=0 to indicate a transition to the specified state is needed +- **Sub State Machine**: StateMachine supports nesting; inner state machines can independently manage sub-states + +### Action — Behavior Tree Action Base Class + +Action is the base node of the behavior tree, providing unified lifecycle management: + +| Method | Description | +|--------|-------------| +| `start()` | Start execution | +| `pause()` | Pause | +| `resume()` | Resume | +| `stop()` | Stop | +| `reset()` | Reset to initial state | +| `isReady()` | Whether it is ready (needs subclass implementation) | +| `setFinishCallback(cb)` | Set finish callback | +| `setBlockCallback(cb)` | Set block callback | +| `setTimeout(ms)` | Set timeout duration | +| `finish(is_succ, why, trace)` | Manually finish | +| `block(why, trace)` | Manually pause | + +Action states: kIdle (idle) → kRunning (running) → kFinished (finished)/kStoped (stopped)/kPause (paused) + +Action results: kUnsure (unknown) → kSuccess (success)/kFail (failure) + +#### Action Subclass Types + +Action has various composite and functional subclasses (see unit test cases in `modules/flow/` for complete examples): + +- **Sequential composition**: SequentialAction (execute multiple sub-actions in order) +- **Parallel composition**: ParallelAction (execute multiple sub-actions simultaneously) +- **Conditional selection**: IfElseAction (conditional branch), SwitchAction (multi-way selection) +- **Loop control**: LoopAction (loop execution), WhileAction (conditional loop) +- **Decorators**: RetryAction (retry on failure), TimeoutAction (timeout control), DelayAction (delayed start) +- **Transaction**: TransactionAction (commit on all success, rollback on any failure) + +### Event — Event Definition + +```cpp +struct Event { + using ID = int; + ID id = 0; + const void *extra = nullptr; //! Attached data pointer +}; +``` + +Supports creation from enum types: `Event(MyEvent::kTimeout, &data)` + +## Usage Examples + +### State Machine — Simple Switch + +> See unit test cases in `modules/flow/` for complete examples + +```cpp +enum State { kOff = 1, kOn = 2 }; +enum Event { kToggle = 10 }; + +StateMachine sm; + +//! Create two states +sm.newState(kOff, [](Event) { LogInfo("enter OFF"); }, [](Event) { LogInfo("exit OFF"); }); +sm.newState(kOn, [](Event) { LogInfo("enter ON"); }, [](Event) { LogInfo("exit ON"); }); + +//! Add transition routes: switch to the other state upon receiving Toggle event from any state +sm.addRoute(kOff, kToggle, kOn, nullptr, nullptr); +sm.addRoute(kOn, kToggle, kOff, nullptr, nullptr); + +sm.start(); //! Start from the first state kOff +sm.run(Event(kToggle)); //! OFF → ON +sm.run(Event(kToggle)); //! ON → OFF +``` + +### State Machine — Conditional Route + +```cpp +//! Route with condition judgment: transition only when guard returns true +sm.addRoute(kIdle, kRequest, kBusy, + [](Event ev) { return /* some condition */; }, + nullptr +); +``` + +### Sub State Machine Nesting + +```cpp +StateMachine outer_sm; +StateMachine inner_sm; + +//! Inner state machine definition +inner_sm.newState(kInnerA, ...); +inner_sm.newState(kInnerB, ...); + +//! Attach the inner state machine to a state of the outer state machine +outer_sm.newState(kOuterState, ...); +outer_sm.setSubStateMachine(kOuterState, &inner_sm); +``` + +### Behavior Tree — Sequential Execution + +> See unit test cases in `modules/flow/` for complete examples + +```cpp +//! SequentialAction: execute multiple sub-actions in order +//! Sub-action A completes then automatically starts B, B completes then starts C +//! Any failure causes the overall action to fail +``` + +### Export Graphviz Diagram + +```cpp +Json js; +sm.toJson(js); //! Export state machine as JSON, can be used for visualization +//! Use to_graphviz.h to convert to Graphviz format +``` + +## Common Scenarios + +1. **Device state management**: e.g., IoT device idle → running → fault → recovery state transitions +2. **Protocol state machine**: e.g., TCP connection states, HTTP request processing states +3. **Business flow orchestration**: e.g., order creation → payment → shipping → completion, rollback on failure +4. **Conditional branching**: Select different execution paths based on sensor data +5. **Retry mechanism**: Use RetryAction to automatically retry on failure + +## Important Notes + +1. **StateID 0 is the terminated state**: The state machine automatically terminates upon reaching state 0, no additional handling needed +2. **Sub state machine lifetime**: The sub state machine passed to setSubStateMachine() must have a longer lifetime than the parent state machine +3. **Action finish/block**: finish() indicates normal completion (success or failure), block() indicates pausing to wait for an external condition +4. **Event.extra pointer**: The data pointed to by the extra pointer must remain valid during event handling +5. **toJson export**: The state machine can be exported as JSON for debugging and visualization + +## Related Modules + +- **event**: Run state machines and actions based on Loop +- **util**: Action uses Variables to store variables +- **base**: Provides Json, Log and other infrastructure diff --git a/documents/modules/flow_CN.md b/documents/modules/flow_CN.md new file mode 100644 index 00000000..3bcd85fc --- /dev/null +++ b/documents/modules/flow_CN.md @@ -0,0 +1,190 @@ +# 流程控制模块 (flow) + +## 是什么? + +flow 模块提供了两类流程控制工具:多层级有限状态机(StateMachine)和行为树(Action 系列)。StateMachine 用于状态驱动的业务逻辑,Action 用于组合型复杂流程。 + +## 为什么需要它? + +在事件驱动编程中,复杂业务流程需要管理大量状态和条件判断。StateMachine 提供清晰的状态定义和转换规则,Action 提供组合型流程控制(顺序、并发、条件选择等),将复杂的控制逻辑从业务代码中分离出来。 + +![state-machine](../images/0010-state-machine-graph.png) + +![action-tree](../images/0010-action-tree-graph.jpg) + +## 头文件 + +```cpp +#include //! 状态机 +#include //! 动作基类 +#include //! 动作执行器 +#include //! 事件定义 +#include //! 事件发布器 +#include //! 事件订阅器 +#include //! 导出 Graphviz 图 +``` + +## 核心类与接口 + +### StateMachine — 多层级有限状态机 + +| 方法 | 说明 | +|------|------| +| `newState(state_id, enter_action, exit_action, label)` | 创建状态 | +| `addRoute(from, event, to, guard, action, label)` | 添加状态转换路由 | +| `addEvent(state_id, event_id, action)` | 添加状态内事件处理 | +| `setInitState(state_id)` | 设置起始状态 | +| `setSubStateMachine(state_id, sub_sm)` | 设置子状态机(层级嵌套) | +| `setStateChangedCallback(cb)` | 设置状态变更回调 | +| `start()` | 启动状态机 | +| `stop()` | 停止状态机 | +| `run(event)` | 运行状态机(传入事件) | +| `currentState()` | 获取当前状态 | +| `lastState()` | 获取上一个状态 | +| `nextState()` | 获取下一个状态(转换中有效) | +| `isRunning()` | 是否运行中 | +| `isTerminated()` | 是否已终止 | + +#### 状态机关键概念 + +- **StateID**:状态编号,0 为终止状态,-1 为无效状态 +- **EventID**:事件编号,0 表示任意事件 +- **Route**:定义从某状态收到某事件后转换到另一状态的条件路由 +- **GuardFunc**:条件判定函数,返回 true 表示条件成立可转换 +- **EventFunc**:事件处理函数,返回 >=0 表示需要转换到指定状态 +- **子状态机**:StateMachine 支持嵌套,内部状态机可以独立管理子状态 + +### Action — 行为树动作基类 + +Action 是行为树的基础节点,提供统一的生命周期管理: + +| 方法 | 说明 | +|------|------| +| `start()` | 开始执行 | +| `pause()` | 暂停 | +| `resume()` | 恢复 | +| `stop()` | 停止 | +| `reset()` | 重置到初始状态 | +| `isReady()` | 是否准备就绪(需子类实现) | +| `setFinishCallback(cb)` | 设置完成回调 | +| `setBlockCallback(cb)` | 设置阻塞回调 | +| `setTimeout(ms)` | 设置超时时间 | +| `finish(is_succ, why, trace)` | 主动结束 | +| `block(why, trace)` | 主动暂停 | + +Action 状态:kIdle(空闲)→ kRunning(运行)→ kFinished(完成)/kStoped(停止)/kPause(暂停) + +Action 结果:kUnsure(未知)→ kSuccess(成功)/kFail(失败) + +#### Action 子类类型 + +Action 有多种组合和功能子类(完整示例见单元测试用例 `modules/flow/`): + +- **顺序组合**:SequentialAction(按顺序执行多个子动作) +- **并发组合**:ParallelAction(同时执行多个子动作) +- **条件选择**:IfElseAction(条件分支)、SwitchAction(多路选择) +- **循环控制**:LoopAction(循环执行)、WhileAction(条件循环) +- **装饰器**:RetryAction(失败重试)、TimeoutAction(超时控制)、DelayAction(延迟启动) +- **事务**:TransactionAction(全部成功则提交,任一失败则回滚) + +### Event — 事件定义 + +```cpp +struct Event { + using ID = int; + ID id = 0; + const void *extra = nullptr; //! 附带数据指针 +}; +``` + +支持从枚举类型创建:`Event(MyEvent::kTimeout, &data)` + +## 使用示例 + +### 状态机 — 简单开关 + +> 完整示例见单元测试用例 `modules/flow/` + +```cpp +enum State { kOff = 1, kOn = 2 }; +enum Event { kToggle = 10 }; + +StateMachine sm; + +//! 创建两个状态 +sm.newState(kOff, [](Event) { LogInfo("enter OFF"); }, [](Event) { LogInfo("exit OFF"); }); +sm.newState(kOn, [](Event) { LogInfo("enter ON"); }, [](Event) { LogInfo("exit ON"); }); + +//! 添加转换路由:任何状态收到 Toggle 事件都切换到另一个状态 +sm.addRoute(kOff, kToggle, kOn, nullptr, nullptr); +sm.addRoute(kOn, kToggle, kOff, nullptr, nullptr); + +sm.start(); //! 从第一个状态 kOff 开始 +sm.run(Event(kToggle)); //! OFF → ON +sm.run(Event(kToggle)); //! ON → OFF +``` + +### 状态机 — 条件路由 + +```cpp +//! 带条件判断的路由:仅当 guard 返回 true 时才转换 +sm.addRoute(kIdle, kRequest, kBusy, + [](Event ev) { return /* 某条件 */; }, + nullptr +); +``` + +### 子状态机嵌套 + +```cpp +StateMachine outer_sm; +StateMachine inner_sm; + +//! 内部状态机定义 +inner_sm.newState(kInnerA, ...); +inner_sm.newState(kInnerB, ...); + +//! 将内部状态机挂到外部状态机的某个状态上 +outer_sm.newState(kOuterState, ...); +outer_sm.setSubStateMachine(kOuterState, &inner_sm); +``` + +### 行为树 — 顺序执行 + +> 完整示例见单元测试用例 `modules/flow/` + +```cpp +//! SequentialAction:按顺序执行多个子动作 +//! 子动作A完成后自动启动B,B完成后启动C +//! 任一失败则整体失败 +``` + +### 导出 Graphviz 图 + +```cpp +Json js; +sm.toJson(js); //! 导出状态机为 JSON,可用于可视化 +//! 使用 to_graphviz.h 可转换为 Graphviz 格式 +``` + +## 常见场景 + +1. **设备状态管理**:如 IoT 设备的空闲→运行→故障→恢复状态流转 +2. **协议状态机**:如 TCP 连接状态、HTTP 请求处理状态 +3. **业务流程编排**:如订单创建→支付→发货→完成,失败则回滚 +4. **条件分支**:根据传感器数据选择不同的执行路径 +5. **重试机制**:使用 RetryAction 在失败时自动重试 + +## 注意事项 + +1. **StateID 0 是终止状态**:状态机到达 0 号状态自动终止,不需要额外处理 +2. **子状态机生命期**:setSubStateMachine() 传入的子状态机生命期需比父状态机长 +3. **Action 的 finish/block**:finish() 表示正常结束(成功或失败),block() 表示暂停等待外部条件 +4. **Event.extra 指针**:extra 指针指向的数据生命期需在事件处理期间有效 +5. **toJson 导出**:状态机可导出为 JSON 用于调试和可视化 + +## 相关模块 + +- **event**:基于 Loop 运行状态机和动作 +- **util**:Action 使用 Variables 存储变量 +- **base**:提供 Json、Log 等基础设施 diff --git a/documents/modules/http.md b/documents/modules/http.md new file mode 100644 index 00000000..3dc160ff --- /dev/null +++ b/documents/modules/http.md @@ -0,0 +1,466 @@ +# HTTP Service Module (http) + +## What is it? + +The http module provides lightweight HTTP server and client implementations, designed with reference to the Node.js Express middleware pattern. It features a concise interface and ease of use. It is intended to supplement service programs with the ability to expose RESTful APIs, rather than to replace mature HTTP servers like Apache/Nginx. + +## Why do you need it? + +In embedded devices or small service programs, you may need to provide simple HTTP APIs or web pages without introducing a heavyweight HTTP server. The http module allows C++ programs to serve HTTP directly, supporting middleware chain processing, route dispatching, file downloading, form uploading, and more. + +On the client side, you may need to make HTTP requests to other services (e.g., calling REST APIs, uploading data). The http Client class provides an asynchronous HTTP client with auto-reconnect, request timeout, and convenient request methods. + +## Header Files + +```cpp +#include //! HTTP server +#include //! Request context +#include //! Middleware base class +#include //! Router middleware +#include //! File downloader middleware +#include //! Form data middleware +#include //! HTTP client +#include //! HTTP request +#include //! HTTP response +#include //! HTTP common definitions +#include //! URL parsing +``` + +## Core Classes and Interfaces + +### Server — HTTP Server + +| Method | Description | +|------|------| +| `Server(loop)` | Constructor | +| `initialize(bind_addr, backlog)` | Initialize with bind address | +| `use(handler)` | Add a request handler function | +| `use(middleware)` | Add a middleware | +| `start()` | Start the server | +| `stop()` | Stop the server | +| `cleanup()` | Cleanup | +| `setContextLogEnable(enable)` | Enable/disable detailed send/receive logs (for debugging) | + +### Client — HTTP Client + +| Method | Description | +|------|------| +| `Client(loop)` | Constructor | +| `initialize(server_addr)` | Initialize with server address (SockAddr) | +| `start()` | Start connecting to the server | +| `stop()` | Stop/disconnect from the server | +| `cleanup()` | Cleanup (inverse of initialize) | +| `state()` | Get current state (None/Inited/Connecting/Connected/ReconnWaiting) | +| `request(req, cb)` | Send a full Request object with response callback | +| `request(method, path, cb)` | Convenience: send request with Method and path | +| `request(method, path, body, headers, cb)` | Convenience: send request with body and headers | +| `setAutoReconnect(enable)` | Enable/disable auto-reconnect | +| `setReconnectDelayCalcFunc(func)` | Set custom reconnect delay calculation | +| `setRequestTimeout(ms)` | Set request timeout (default: 30 seconds) | +| `setContextLogEnable(enable)` | Enable/disable detailed send/receive logs | +| `setConnectedCallback(cb)` | Set callback for successful connection | +| `setConnectFailCallback(cb)` | Set callback for connection failure | +| `setDisconnectedCallback(cb)` | Set callback for disconnection | + +**State enum:** + +| State | Description | +|-------|-------------| +| `kNone` | Not initialized | +| `kInited` | Initialized | +| `kConnecting` | Connecting to server | +| `kConnected` | Connected to server | +| `kReconnWaiting` | Disconnected, waiting for auto-reconnect | + +### Context — Request Context + +| Method | Description | +|------|------| +| `ctx.req()` | Get the Request object | +| `ctx.res()` | Get the Respond object (cannot be used after done) | + +### Request / Respond Structures + +```cpp +struct Request { + Method method; //! GET/POST/PUT/DELETE etc. + HttpVer http_ver; //! HTTP version + Url::Path url; //! Request path + Headers headers; //! Request headers + std::string body; //! Request body +}; + +struct Respond { + HttpVer http_ver; + StatusCode status_code; //! 200/404/500 etc. + Headers headers; + std::string body; +}; +``` + +### Middleware — Middleware Base Class + +Middleware is the core of the Express pattern. Each middleware receives a Context and a NextFunc, and can either handle the request or call next() to pass it to the next middleware. + +```cpp +class Middleware { + virtual void handle(ContextSptr ctx, const NextFunc &next) = 0; +}; +``` + +### RouterMiddleware — Router Middleware + +Provides route dispatching similar to Express Router: + +```cpp +RouterMiddleware router; +router.get("/", handler); //! GET request +router.post("/api", handler); //! POST request +router.put("/data", handler); //! PUT request +router.del("/item", handler); //! DELETE request +``` + +### FileDownloaderMiddleware — File Downloader Middleware + +Supports file downloading, Range requests, ETag caching, and CORS, compatible with iOS AVPlayer video playback. + +## Usage Examples + +### Server: Simplest HTTP Service + +> Full example in `examples/http/server/simple/` + +```cpp +#include +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::http; +using namespace tbox::http::server; + +int main() { + LogOutput_Enable(); + + auto sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + Server srv(sp_loop); + srv.initialize(network::SockAddr::FromString("0.0.0.0:12345"), 1); + srv.start(); + + //! Add request handler + srv.use( + [&](ContextSptr ctx, const NextFunc &next) { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().body = "Hello!"; + } + ); + + //! Listen for exit signal + auto sp_sig = sp_loop->newSignalEvent(); + SetScopeExitAction([sp_sig] { delete sp_sig; }); + sp_sig->initialize(SIGINT, Event::Mode::kPersist); + sp_sig->enable(); + sp_sig->setCallback([&] (int) { srv.stop(); sp_loop->exitLoop(); }); + + sp_loop->runLoop(); + srv.cleanup(); + + LogOutput_Disable(); + return 0; +} +``` + +### Server: Route Dispatching + +> Full example in `examples/http/server/router/` + +```cpp +RouterMiddleware router; +srv.use(&router); + +router + .get("/", [](ContextSptr ctx, const NextFunc &next) { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().headers["Content-Type"] = "text/html; charset=UTF-8"; + ctx->res().body = "

Home

"; + }) + .get("/api/data", [](ContextSptr ctx, const NextFunc &next) { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().headers["Content-Type"] = "application/json"; + ctx->res().body = "{\"status\":\"ok\"}"; + }) + .post("/api/upload", [](ContextSptr ctx, const NextFunc &next) { + //! Handle POST upload + ctx->res().status_code = StatusCode::k200_OK; + }); +``` + +### Server: Asynchronous Response + +> Full example in `examples/http/server/async_respond/` + +```cpp +srv.use( + [&](ContextSptr ctx, const NextFunc &next) { + //! Do not reply immediately, process asynchronously later + ctx->res().status_code = StatusCode::k200_OK; + + //! Reply after completion in another thread + tp.execute( + [] { /* Background time-consuming operation */ }, + [ctx] { ctx->res().body = "async result"; /* Reply */ } + ); + } +); +``` + +### Server: File Downloading + +> Full example in `examples/http/server/file_download/` + +```cpp +FileDownloaderMiddleware file_dl; +file_dl.setRootPath("/data/files"); //! Set file root directory +srv.use(&file_dl); +``` + +### Server: Form Upload + +> Full example in `examples/http/server/form_data/` + +```cpp +FormDataMiddleware form_data; +srv.use(&form_data); + +router.post("/upload", [](ContextSptr ctx, const NextFunc &next) { + //! Get uploaded file data + auto files = ctx->req().headers; //! Data processed by FormDataMiddleware +}); +``` + +### Client: Simple HTTP Client + +> Full example in `examples/http/client/simple/` + +```cpp +#include +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::http; +using namespace tbox::http::client; + +int main() { + LogOutput_Enable(); + + auto sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + Client http_client(sp_loop); + http_client.initialize(network::SockAddr::FromString("127.0.0.1:12345")); + http_client.setAutoReconnect(true); + http_client.setRequestTimeout(std::chrono::seconds(10)); + http_client.start(); + + //! Simple GET request + http_client.request(Method::kGet, "/", + [](const Respond &res) { + LogInfo("GET / => status: %d, body: %s", + (int)res.status_code, res.body.c_str()); + }); + + //! POST request with body and headers + http_client.request(Method::kPost, "/api/data", + "{\"key\":\"value\"}", + {{"Content-Type", "application/json"}}, + [](const Respond &res) { + LogInfo("POST /api/data => status: %d", (int)res.status_code); + }); + + //! Full Request object + Request req; + req.method = Method::kPut; + req.http_ver = HttpVer::k1_1; + req.url.path = "/api/update"; + req.headers["Content-Type"] = "application/json"; + req.body = "{\"id\":123}"; + http_client.request(req, + [](const Respond &res) { + LogInfo("PUT /api/update => status: %d", (int)res.status_code); + }); + + //! Listen for exit signal + auto sp_sig = sp_loop->newSignalEvent(); + SetScopeExitAction([sp_sig] { delete sp_sig; }); + sp_sig->initialize(SIGINT, Event::Mode::kPersist); + sp_sig->enable(); + sp_sig->setCallback([&] (int) { http_client.stop(); sp_loop->exitLoop(); }); + + sp_loop->runLoop(); + http_client.cleanup(); + + LogOutput_Disable(); + return 0; +} +``` + +### Client: Connection Callbacks + +```cpp +http_client.setConnectedCallback( + [] { LogInfo("connected to server"); } +); +http_client.setConnectFailCallback( + [] { LogWarn("connect failed"); } +); +http_client.setDisconnectedCallback( + [] { LogInfo("disconnected from server"); } +); +``` + +### Client: Custom Reconnect Delay + +```cpp +//! Exponential backoff: 1s, 2s, 4s, 8s, ... max 30s +http_client.setReconnectDelayCalcFunc( + [](int fail_count) { + int delay = 1 << fail_count; + return delay > 30 ? 30 : delay; + } +); +``` + +## Common Scenarios + +1. **RESTful API**: Use RouterMiddleware to dispatch GET/POST/PUT/DELETE requests +2. **Static file serving**: Use FileDownloaderMiddleware to provide file downloads +3. **Asynchronous processing**: Receive a request without replying immediately, then respond asynchronously after background thread processing completes +4. **Middleware chain**: Multiple middleware process requests in sequence (e.g., logging -> authentication -> business logic) +5. **Video streaming**: FileDownloaderMiddleware supports Range requests, compatible with iOS AVPlayer +6. **Calling external APIs**: Use Client to make HTTP requests to other services +7. **Service-to-service communication**: Client with auto-reconnect for reliable inter-service HTTP calls + +## SSE — Server-Sent Events + +The http module also includes an SSE (Server-Sent Events) sub-package, implementing server-side event push based on the W3C/WHATWG EventSource specification. SSE uses standard HTTP long-lived responses (200 OK) to stream events to the browser, requiring no protocol upgrade like WebSocket. + +### Header Files + +```cpp +#include //! SSE event data structure +#include //! SSE server +#include //! SSE connection (internal) +``` + +### SseServer — SSE Server + +SseServer runs on top of an HTTP server as a middleware. It detects SSE requests (Accept: text/event-stream), sets 200 OK response headers, and takes over the TcpConnection via the `upgrade_cb` mechanism to provide continuous event streaming. + +| Method | Description | +|------|------| +| `SseServer(loop)` | Constructor | +| `initialize(http_server, url_path)` | Initialize: associate with an HTTP server; `url_path` controls URL matching | +| `start()` | Start (registers as HTTP middleware) | +| `stop()` | Stop (unregisters middleware, closes all SSE connections) | +| `cleanup()` | Cleanup | +| `state()` | Get current state (None/Inited/Running) | +| `send(client, data)` | Send data to a client (simple text, event type "message") | +| `send(client, event)` | Send SseEvent to a client | +| `sendToAll(data)` | Broadcast data to all clients | +| `sendToAll(event)` | Broadcast SseEvent to all clients | +| `close(client)` | Close a client connection | +| `sendHeartbeat(client, comment)` | Send heartbeat comment line | +| `setHeartbeatInterval(ms)` | Set auto-heartbeat interval (default: 0 = disabled) | +| `isClientValid(client)` | Check if a client connection is still valid | +| `peerAddr(client)` | Get client address (IP:port) | +| `getLastEventId(client)` | Get the Last-Event-ID from browser reconnect | +| `getUrl(client)` | Get the URL path the client connected to | +| `setContext(client, ctx, deleter)` | Set context data for a client | +| `getContext(client)` | Get context data for a client | +| `setConnectedCallback(cb)` | Set callback: new client connected | +| `setDisconnectedCallback(cb)` | Set callback: client disconnected | +| `IsSseRequest(req)` | Static: check if an HTTP request is a valid SSE request | + +**URL path matching rules:** Same as WsServer — prefix match if url_path ends with `/`, exact match otherwise, empty string matches all. + +**SSE vs WebSocket:** + +| Feature | WebSocket | SSE | +|---------|-----------|------| +| HTTP status code | 101 Switching Protocols | 200 OK | +| Data direction | Bidirectional | Server→Client only | +| Data format | Binary frames | Plain text (`data:`/`event:`/`id:` fields) | +| Client message callback | Yes | No (unidirectional) | +| Heartbeat | Ping/Pong frames | Comment lines + timer | +| Reconnection | Manual implementation | Browser auto-reconnect + Last-Event-ID | +| Module | Separate `websocket` module | Inside `http` module | + +### SseEvent — SSE Event Data Structure + +```cpp +struct SseEvent { + std::string id; //! Event ID (optional), for Last-Event-ID reconnection + std::string event; //! Event type (optional, default "message") + std::string data; //! Data (required, supports multiline) + int retry = 0; //! Reconnect interval in ms (optional) + + //! Format event as SSE text protocol + //! Multiline data auto-splits into multiple `data:` lines + std::string toString() const; +}; +``` + +### SSE Example: Event Push + +> Full example in `examples/http/server/sse/` + +```cpp +#include +#include + +SseServer sse_srv(sp_loop); +sse_srv.initialize(&http_srv, "/sse/events"); +sse_srv.setHeartbeatInterval(std::chrono::seconds(15)); + +sse_srv.setConnectedCallback([](const SseServer::ConnToken &token) { + LogInfo("sse client connected"); + sse_srv.send(token, "Welcome!"); +}); + +//! Push events every 5 seconds +SseEvent evt; +evt.id = "42"; +evt.event = "tick"; +evt.data = "{\"time\":\"2026-06-16 10:30:00\"}"; +sse_srv.sendToAll(evt); +``` + +## Important Notes + +1. **Middleware invocation order**: Middleware added via `use()` executes in the order it was added +2. **NextFunc**: Calling `next()` in a middleware passes the request to the next middleware; not calling next terminates the chain +3. **Context's res()**: You cannot use `res()` after calling `done()` +4. **File download security**: FileDownloaderMiddleware must be configured with the correct root directory to prevent path traversal attacks +5. **Thread safety**: HTTP request handling runs in the Loop thread; asynchronous operations must use runInLoop to return to the main thread +6. **Client lifecycle**: Must follow initialize -> start -> stop -> cleanup; calling request() before start() will cache the request until connected +7. **Client timeout**: Each request has an independent timeout timer; timeout triggers a callback with StatusCode::k408_RequestTimeout +8. **Client disconnection**: When disconnected, all pending requests receive error callbacks with StatusCode::k504_GatewayTimeout; cached requests are sent upon reconnection +9. **Client request order**: Responses are matched with requests in FIFO order; the request queue design is compatible with both pipelined and non-pipelined HTTP/1.1 + +## Related Modules + +- **event**: Server and Client run based on Loop +- **network**: HTTP connection management implemented via TcpServer (server) / TcpClient (client) +- **eventx**: Asynchronous responses require ThreadPool +- **base**: Provides StatusCode, Method, and other definitions diff --git a/documents/modules/http_CN.md b/documents/modules/http_CN.md new file mode 100644 index 00000000..0ea996e4 --- /dev/null +++ b/documents/modules/http_CN.md @@ -0,0 +1,468 @@ +# HTTP 服务模块 (http) + +## 是什么? + +http 模块提供了轻量级 HTTP 服务器和客户端实现,设计参考了 Node.js Express 的中间件模式,接口简洁,使用方便。它旨在补全服务程序对外提供 RESTful API 的能力,而非取代 Apache/Nginx 等成熟 HTTP 服务器。 + +在客户端侧,提供了异步 HTTP 客户端,支持自动重连、请求超时和便捷的请求方法,方便 C++ 程序向其它服务发起 HTTP 请求。 + +## 为什么需要它? + +在嵌入式设备或小型服务程序中,需要提供简单的 HTTP API 或 Web 页面,但不想引入重量级 HTTP 服务器。http 模块让 C++ 程序能直接提供 HTTP 服务,支持中间件链式处理、路由分发、文件下载、表单上传等功能。 + +在客户端侧,可能需要向其它服务发起 HTTP 请求(如调用 REST API、上传数据等)。http Client 类提供了异步 HTTP 客户端,支持自动重连、请求超时和便捷的请求方法。 + +## 头文件 + +```cpp +#include //! HTTP 服务端 +#include //! 请求上下文 +#include //! 中间件基类 +#include //! 路由中间件 +#include //! 文件下载中间件 +#include //! 表单数据中间件 +#include //! HTTP 客户端 +#include //! HTTP 请求 +#include //! HTTP 响应 +#include //! HTTP 公共定义 +#include //! URL 解析 +``` + +## 核心类与接口 + +### Server — HTTP 服务端 + +| 方法 | 说明 | +|------|------| +| `Server(loop)` | 构造 | +| `initialize(bind_addr, backlog)` | 初始化绑定地址 | +| `use(handler)` | 添加请求处理函数 | +| `use(middleware)` | 添加中间件 | +| `start()` | 启动服务 | +| `stop()` | 停止服务 | +| `cleanup()` | 清理 | +| `setContextLogEnable(enable)` | 启用/禁用详细收发日志(调试用) | + +### Client — HTTP 客户端 + +| 方法 | 说明 | +|------|------| +| `Client(loop)` | 构造 | +| `initialize(server_addr)` | 初始化,设置目标服务器地址 (SockAddr) | +| `start()` | 开始连接服务器 | +| `stop()` | 停止/断开连接 | +| `cleanup()` | 清理(与 initialize 逆操作) | +| `state()` | 获取当前状态 (None/Inited/Connecting/Connected/ReconnWaiting) | +| `request(req, cb)` | 发送完整 Request 对象,指定回复回调 | +| `request(method, path, cb)` | 便捷方法:指定 Method 和 path | +| `request(method, path, body, headers, cb)` | 便捷方法:指定 Method、path、body、headers | +| `setAutoReconnect(enable)` | 启用/禁用自动重连 | +| `setReconnectDelayCalcFunc(func)` | 设置自定义重连延迟计算函数 | +| `setRequestTimeout(ms)` | 设置请求超时时间(默认 30 秒) | +| `setContextLogEnable(enable)` | 启用/禁用详细收发日志 | +| `setConnectedCallback(cb)` | 设置连接成功回调 | +| `setConnectFailCallback(cb)` | 设置连接失败回调 | +| `setDisconnectedCallback(cb)` | 设置断线回调 | + +**State 状态枚举:** + +| 状态 | 说明 | +|------|------| +| `kNone` | 未初始化 | +| `kInited` | 已初始化 | +| `kConnecting` | 连接中 | +| `kConnected` | 已连接 | +| `kReconnWaiting` | 断连等待重连中 | + +### Context — 请求上下文 + +| 方法 | 说明 | +|------|------| +| `ctx.req()` | 获取 Request 对象 | +| `ctx.res()` | 获取 Respond 对象(done 后不可再使用) | + +### Request / Respond 结构 + +```cpp +struct Request { + Method method; //! GET/POST/PUT/DELETE 等 + HttpVer http_ver; //! HTTP 版本 + Url::Path url; //! 请求路径 + Headers headers; //! 请求头 + std::string body; //! 请求体 +}; + +struct Respond { + HttpVer http_ver; + StatusCode status_code; //! 200/404/500 等 + Headers headers; + std::string body; +}; +``` + +### Middleware — 中间件基类 + +中间件是 Express 模式的核心。每个中间件接收 Context 和 NextFunc,可以选择处理请求或调用 next() 传递给下一个中间件。 + +```cpp +class Middleware { + virtual void handle(ContextSptr ctx, const NextFunc &next) = 0; +}; +``` + +### RouterMiddleware — 路由中间件 + +提供类似 Express Router 的路由分发: + +```cpp +RouterMiddleware router; +router.get("/", handler); //! GET 请求 +router.post("/api", handler); //! POST 请求 +router.put("/data", handler); //! PUT 请求 +router.del("/item", handler); //! DELETE 请求 +``` + +### FileDownloaderMiddleware — 文件下载中间件 + +支持文件下载、Range 请求、ETag 缓存和 CORS,适配 iOS AVPlayer 视频播放。 + +## 使用示例 + +### Server:最简单的 HTTP 服务 + +> 完整示例见 `examples/http/server/simple/` + +```cpp +#include +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::http; +using namespace tbox::http::server; + +int main() { + LogOutput_Enable(); + + auto sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + Server srv(sp_loop); + srv.initialize(network::SockAddr::FromString("0.0.0.0:12345"), 1); + srv.start(); + + //! 添加请求处理 + srv.use( + [&](ContextSptr ctx, const NextFunc &next) { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().body = "Hello!"; + } + ); + + //! 监听退出信号 + auto sp_sig = sp_loop->newSignalEvent(); + SetScopeExitAction([sp_sig] { delete sp_sig; }); + sp_sig->initialize(SIGINT, Event::Mode::kPersist); + sp_sig->enable(); + sp_sig->setCallback([&] (int) { srv.stop(); sp_loop->exitLoop(); }); + + sp_loop->runLoop(); + srv.cleanup(); + + LogOutput_Disable(); + return 0; +} +``` + +### Server:路由分发 + +> 完整示例见 `examples/http/server/router/` + +```cpp +RouterMiddleware router; +srv.use(&router); + +router + .get("/", [](ContextSptr ctx, const NextFunc &next) { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().headers["Content-Type"] = "text/html; charset=UTF-8"; + ctx->res().body = "

Home

"; + }) + .get("/api/data", [](ContextSptr ctx, const NextFunc &next) { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().headers["Content-Type"] = "application/json"; + ctx->res().body = "{\"status\":\"ok\"}"; + }) + .post("/api/upload", [](ContextSptr ctx, const NextFunc &next) { + //! 处理 POST 上传 + ctx->res().status_code = StatusCode::k200_OK; + }); +``` + +### Server:异步响应 + +> 完整示例见 `examples/http/server/async_respond/` + +```cpp +srv.use( + [&](ContextSptr ctx, const NextFunc &next) { + //! 不立即回复,稍后异步处理 + ctx->res().status_code = StatusCode::k200_OK; + + //! 在其它线程完成后回复 + tp.execute( + [] { /* 后台耗时操作 */ }, + [ctx] { ctx->res().body = "async result"; /* 回复 */ } + ); + } +); +``` + +### Server:文件下载 + +> 完整示例见 `examples/http/server/file_download/` + +```cpp +FileDownloaderMiddleware file_dl; +file_dl.setRootPath("/data/files"); //! 设置文件根目录 +srv.use(&file_dl); +``` + +### Server:表单上传 + +> 完整示例见 `examples/http/server/form_data/` + +```cpp +FormDataMiddleware form_data; +srv.use(&form_data); + +router.post("/upload", [](ContextSptr ctx, const NextFunc &next) { + //! 获取上传的文件数据 + auto files = ctx->req().headers; //! 通过 FormDataMiddleware 处理后的数据 +}); +``` + +### Client:简单的 HTTP 客户端 + +> 完整示例见 `examples/http/client/simple/` + +```cpp +#include +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::http; +using namespace tbox::http::client; + +int main() { + LogOutput_Enable(); + + auto sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + Client http_client(sp_loop); + http_client.initialize(network::SockAddr::FromString("127.0.0.1:12345")); + http_client.setAutoReconnect(true); + http_client.setRequestTimeout(std::chrono::seconds(10)); + http_client.start(); + + //! 简单 GET 请求 + http_client.request(Method::kGet, "/", + [](const Respond &res) { + LogInfo("GET / => status: %d, body: %s", + (int)res.status_code, res.body.c_str()); + }); + + //! POST 请求(带 body 和 headers) + http_client.request(Method::kPost, "/api/data", + "{\"key\":\"value\"}", + {{"Content-Type", "application/json"}}, + [](const Respond &res) { + LogInfo("POST /api/data => status: %d", (int)res.status_code); + }); + + //! 完整 Request 对象 + Request req; + req.method = Method::kPut; + req.http_ver = HttpVer::k1_1; + req.url.path = "/api/update"; + req.headers["Content-Type"] = "application/json"; + req.body = "{\"id\":123}"; + http_client.request(req, + [](const Respond &res) { + LogInfo("PUT /api/update => status: %d", (int)res.status_code); + }); + + //! 监听退出信号 + auto sp_sig = sp_loop->newSignalEvent(); + SetScopeExitAction([sp_sig] { delete sp_sig; }); + sp_sig->initialize(SIGINT, Event::Mode::kPersist); + sp_sig->enable(); + sp_sig->setCallback([&] (int) { http_client.stop(); sp_loop->exitLoop(); }); + + sp_loop->runLoop(); + http_client.cleanup(); + + LogOutput_Disable(); + return 0; +} +``` + +### Client:连接回调 + +```cpp +http_client.setConnectedCallback( + [] { LogInfo("连接成功"); } +); +http_client.setConnectFailCallback( + [] { LogWarn("连接失败"); } +); +http_client.setDisconnectedCallback( + [] { LogInfo("连接断开"); } +); +``` + +### Client:自定义重连延迟 + +```cpp +//! 指数退避:1秒, 2秒, 4秒, 8秒, ... 最大30秒 +http_client.setReconnectDelayCalcFunc( + [](int fail_count) { + int delay = 1 << fail_count; + return delay > 30 ? 30 : delay; + } +); +``` + +## 常见场景 + +1. **RESTful API**:使用 RouterMiddleware 分发 GET/POST/PUT/DELETE 请求 +2. **静态文件服务**:使用 FileDownloaderMiddleware 提供文件下载 +3. **异步处理**:收到请求后不立即回复,在后台线程处理完成后异步响应 +4. **中间件链**:多个中间件依次处理请求(如日志→认证→业务) +5. **视频流**:FileDownloaderMiddleware 支持 Range 请求,适配 iOS AVPlayer +6. **调用外部 API**:使用 Client 向其它服务发起 HTTP 请求 +7. **服务间通信**:使用 Client 配合自动重连,实现可靠的服务间 HTTP 调用 + +## SSE — Server-Sent Events(服务端推送事件) + +http 模块还包含 SSE(Server-Sent Events)子包,实现基于 W3C/WHATWG EventSource 规范的服务端事件推送。SSE 使用标准 HTTP 长响应(200 OK)向浏览器流式推送事件,不像 WebSocket 需要协议升级。 + +### 头文件 + +```cpp +#include //! SSE 事件数据结构 +#include //! SSE 服务端 +#include //! SSE 连接(内部类) +``` + +### SseServer — SSE 服务端 + +SseServer 运行在 HTTP 服务器之上,作为中间件存在。它检测 SSE 请求(Accept 头包含 text/event-stream),设置 200 OK 响应头,通过 `upgrade_cb` 机制接管 TcpConnection,提供持续的事件推送。 + +| 方法 | 说明 | +|------|------| +| `SseServer(loop)` | 构造 | +| `initialize(http_server, url_path)` | 初始化:关联到 HTTP 服务器;`url_path` 控制 URL 匹配规则 | +| `start()` | 启动(注册为 HTTP 中间件) | +| `stop()` | 停止(反注册中间件,关闭所有 SSE 连接) | +| `cleanup()` | 清理 | +| `state()` | 获取当前状态 (None/Inited/Running) | +| `send(client, data)` | 向指定客户端发送数据(简单文本) | +| `send(client, event)` | 向指定客户端发送 SseEvent | +| `sendToAll(data)` | 向所有客户端广播数据 | +| `sendToAll(event)` | 向所有客户端广播 SseEvent | +| `close(client)` | 关闭指定客户端连接 | +| `sendHeartbeat(client, comment)` | 发送心跳注释行 | +| `setHeartbeatInterval(ms)` | 设置自动心跳间隔(默认 0 = 禁用) | +| `isClientValid(client)` | 检查客户端连接是否有效 | +| `peerAddr(client)` | 获取客户端地址 | +| `getLastEventId(client)` | 获取浏览器重连时的 Last-Event-ID | +| `getUrl(client)` | 获取客户端连接的 URL 路径 | +| `setContext(client, ctx, deleter)` | 设置上下文数据 | +| `getContext(client)` | 获取上下文数据 | +| `setConnectedCallback(cb)` | 设置回调:客户端连接 | +| `setDisconnectedCallback(cb)` | 设置回调:客户端断开 | +| `IsSseRequest(req)` | 静态方法:检查是否为 SSE 请求 | + +**URL 路径匹配规则:** 与 WsServer 一致——url_path 以 `/` 结尾为前缀匹配,不以 `/` 结尾为全量匹配,空字符串匹配所有。 + +**SSE 与 WebSocket 对比:** + +| 特性 | WebSocket | SSE | +|------|-----------|------| +| HTTP 状态码 | 101 Switching Protocols | 200 OK | +| 数据方向 | 双向 | 仅服务端→客户端 | +| 数据格式 | 二进制帧 | 纯文本(data:/event:/id: 字段) | +| 客户端消息回调 | 有 | 无(单向) | +| 心跳 | Ping/Pong 帧 | 注释行 + 定时器 | +| 重连机制 | 自行实现 | 浏览器自动重连 + Last-Event-ID | +| 模块位置 | 独立 `websocket` 模块 | `http` 模块内 | + +### SseEvent — SSE 事件数据结构 + +```cpp +struct SseEvent { + std::string id; //! 事件ID(可选),用于 Last-Event-ID 断线续传 + std::string event; //! 事件类型(可选,默认 "message") + std::string data; //! 数据(必须,支持多行) + int retry = 0; //! 重连间隔毫秒数(可选) + + //! 将事件格式化为 SSE 文本协议格式 + //! 多行 data 自动拆分为多个 `data:` 行 + std::string toString() const; +}; +``` + +### SSE 示例:事件推送 + +> 完整示例见 `examples/http/server/sse/` + +```cpp +#include +#include + +SseServer sse_srv(sp_loop); +sse_srv.initialize(&http_srv, "/sse/events"); +sse_srv.setHeartbeatInterval(std::chrono::seconds(15)); + +sse_srv.setConnectedCallback([](const SseServer::ConnToken &token) { + LogInfo("sse 客户端已连接"); + sse_srv.send(token, "欢迎!"); +}); + +//! 每 5 秒推送事件 +SseEvent evt; +evt.id = "42"; +evt.event = "tick"; +evt.data = "{\"time\":\"2026-06-16 10:30:00\"}"; +sse_srv.sendToAll(evt); +``` + +## 注意事项 + +1. **中间件调用顺序**:`use()` 添加的中间件按添加顺序执行 +2. **NextFunc**:中间件中调用 `next()` 才会将请求传递给下一个中间件;不调用 next 则终止链 +3. **Context 的 res()**:调用 `done()` 后不可再使用 `res()` +4. **文件下载安全性**:FileDownloaderMiddleware 需正确设置根目录,防止路径遍历攻击 +5. **线程安全**:HTTP 请求处理在 Loop 线程中执行,异步操作需通过 runInLoop 回到主线程 +6. **Client 生命周期**:必须遵循 initialize → start → stop → cleanup 顺序;在 start() 之前调用 request() 会将请求缓存,连接建立后自动发送 +7. **Client 请求超时**:每个请求有独立的超时定时器,超时后回调返回 StatusCode::k408_RequestTimeout +8. **Client 断线处理**:断线时所有 pending 请求会收到 StatusCode::k504_GatewayTimeout 的错误回调;重连后需重新发起请求 +9. **Client 请求顺序**:响应按 FIFO 顺序与请求匹配,队列设计兼容管线化和非管线化的 HTTP/1.1 + +## 相关模块 + +- **event**:Server 和 Client 基于 Loop 运行 +- **network**:基于 TcpServer(服务端)/ TcpClient(客户端)实现 HTTP 连接管理 +- **eventx**:异步响应需要 ThreadPool +- **base**:提供 StatusCode、Method 等定义 diff --git a/documents/modules/jsonrpc.md b/documents/modules/jsonrpc.md new file mode 100644 index 00000000..122f2399 --- /dev/null +++ b/documents/modules/jsonrpc.md @@ -0,0 +1,147 @@ +# JSON-RPC Module (jsonrpc) + +## What is it? + +The jsonrpc module provides an implementation of the JSON-RPC 2.0 protocol, supporting request/notification/response message interaction patterns, and can be used with custom transport layers (such as TCP, WebSocket). + +## Why do you need it? + +In scenarios requiring Remote Procedure Calls (RPC), JSON-RPC is a lightweight and easy-to-implement protocol. The jsonrpc module encapsulates message encoding/decoding, request timeout management, asynchronous response, and other mechanisms, allowing developers to focus only on service method implementation. + +## Header Files + +```cpp +#include //! RPC core class +#include //! Protocol abstract base class +#include //! Type definitions +``` + +## Core Classes and Interfaces + +### Rpc — RPC Core + +| Method | Description | +|--------|-------------| +| `Rpc(loop, id_type)` | Construct, specifying ID type (kInt or kString) | +| `initialize(proto, timeout_sec)` | Initialize protocol and timeout | +| `addService(method, cb)` | Register a method service | +| `removeService(method)` | Remove a method service | +| `request(method, params, cb)` | Send a request (requires response) | +| `request(method, cb)` | Send a request (no params) | +| `notify(method, params)` | Send a notification (no response needed) | +| `notify(method)` | Send a notification (no params) | +| `respondResult(int_id, result)` | Asynchronous response with success result | +| `respondError(int_id, errcode, message)` | Asynchronous response with error | +| `clear()` | Clear cached data | + +### ServiceCallback — Method Callback + +```cpp +using ServiceCallback = std::function; +``` + +- Return `true`: synchronous response — the function automatically responds based on response after returning +- Return `false`: asynchronous response — manually respond later via `respondResult/respondError` + +### Proto — Protocol Transport Layer + +Proto is the transport layer abstraction of the protocol. Users need to implement the concrete transport method (e.g., based on TcpConnection): + +| Method | Description | +|--------|-------------| +| `setRecvCallback(req_cb, rsp_cb)` | Set receive callbacks | +| `setSendCallback(send_cb)` | Set send callback | +| `onRecvData(data, size)` | Process received data (must be implemented by subclass) | + +## Usage Examples + +### Request Side (Ping) + +> Full example at `examples/jsonrpc/req_rsp/ping/` + +```cpp +Rpc rpc(sp_loop, IdType::kInt); +rpc.initialize(proto, 30); //! Timeout 30 seconds + +//! Send request, wait for response +rpc.request("ping", Json::object{{"data", "hello"}}, + [](const Response &rsp) { + if (rsp.errcode == 0) + LogInfo("result: %s", rsp.result.dump().c_str()); + else + LogErr("error: %d, %s", rsp.errcode, rsp.message.c_str()); + } +); +``` + +### Service Side (Pong) + +> Full example at `examples/jsonrpc/req_rsp/pong/` + +```cpp +Rpc rpc(sp_loop, IdType::kInt); +rpc.initialize(proto, 30); + +//! Register service method +rpc.addService("ping", + [](int int_id, const Json ¶ms, Response &response) { + //! Synchronous response + response.errcode = 0; + response.result = Json::object{{"echo", params["data"]}}; + return true; + } +); +``` + +### Asynchronous Response + +```cpp +rpc.addService("async_query", + [](int int_id, const Json ¶ms, Response &response) { + //! Asynchronous processing: do not respond immediately, respond later via respondResult + //! Return false to indicate no automatic response + thread_pool.execute( + [int_id, params] { /* background query */ }, + [int_id, &rpc] { + rpc.respondResult(int_id, Json::object{{"status", "ok"}}); + } + ); + return false; + } +); +``` + +### Notification (No Response Needed) + +```cpp +//! Send notification +rpc.notify("event", Json::object{{"type", "alert"}}); +``` + +### Message Communication (Ping/Pong One-way) + +> Full example at `examples/jsonrpc/message/ping/` and `pong/` + +Suitable for simple message passing scenarios without the request-response pattern. + +## Common Scenarios + +1. **Request-Response**: Client sends a request, server responds with a result +2. **Asynchronous Processing**: Server receives a request, processes asynchronously, and responds later +3. **Event Notification**: One side sends a notification message, the other side only receives without responding +4. **Timeout Management**: Requests that exceed the timeout automatically trigger a timeout callback + +## Important Notes + +1. **ID Type**: Int ID auto-increment is simple and efficient; String ID (e.g., UUID) is more secure but has higher overhead +2. **Proto Implementation**: You must implement the Proto's `onRecvData()` method yourself to parse transport layer data +3. **Timeout**: Requests sent via `request` that do not receive a response within the timeout period will trigger a timeout callback (errcode != 0) +4. **int_id for Asynchronous Response**: When responding asynchronously, you must save the `int_id` and use it later to respond +5. **clear() Resets State**: Resets the RPC object's cached data, restoring it to a state where no data has been sent or received + +## Related Modules + +- **event**: Runs based on Loop +- **eventx**: Uses TimeoutMonitor for request timeout management +- **network**: Can implement Proto transport layer based on TcpConnection +- **base**: Provides infrastructure such as Json, Cabinet/Token diff --git a/documents/modules/jsonrpc_CN.md b/documents/modules/jsonrpc_CN.md new file mode 100644 index 00000000..ed97ee26 --- /dev/null +++ b/documents/modules/jsonrpc_CN.md @@ -0,0 +1,147 @@ +# JSON-RPC 模块 (jsonrpc) + +## 是什么? + +jsonrpc 模块提供了 JSON-RPC 2.0 协议的实现,支持请求/通知/回复的消息交互模式,可配合自定义传输层(如 TCP、WebSocket)使用。 + +## 为什么需要它? + +在需要远程过程调用(RPC)的场景中,JSON-RPC 是一种轻量级且易于实现的协议。jsonrpc 模块封装了消息编解码、请求超时管理、异步回复等机制,让开发者只需关注服务方法的实现。 + +## 头文件 + +```cpp +#include //! RPC 核心类 +#include //! 协议抽象基类 +#include //! 类型定义 +``` + +## 核心类与接口 + +### Rpc — RPC 核心 + +| 方法 | 说明 | +|------|------| +| `Rpc(loop, id_type)` | 构造,指定 ID 类型(kInt 或 kString) | +| `initialize(proto, timeout_sec)` | 初始化协议和超时时间 | +| `addService(method, cb)` | 注册方法服务 | +| `removeService(method)` | 删除方法服务 | +| `request(method, params, cb)` | 发送请求(需回复) | +| `request(method, cb)` | 发送请求(无参数) | +| `notify(method, params)` | 发送通知(不需要回复) | +| `notify(method)` | 发送通知(无参数) | +| `respondResult(int_id, result)` | 异步回复成功结果 | +| `respondError(int_id, errcode, message)` | 异步回复错误 | +| `clear()` | 清除缓存数据 | + +### ServiceCallback — 方法回调 + +```cpp +using ServiceCallback = std::function; +``` + +- 返回 `true`:同步回复,函数返回后自动根据 response 进行回复 +- 返回 `false`:异步回复,后续通过 `respondResult/respondError` 手动回复 + +### Proto — 协议传输层 + +Proto 是协议的传输层抽象,需要用户实现具体的传输方式(如基于 TcpConnection): + +| 方法 | 说明 | +|------|------| +| `setRecvCallback(req_cb, rsp_cb)` | 设置接收回调 | +| `setSendCallback(send_cb)` | 设置发送回调 | +| `onRecvData(data, size)` | 处理收到的数据(需子类实现) | + +## 使用示例 + +### 请求端 (Ping) + +> 完整示例见 `examples/jsonrpc/req_rsp/ping/` + +```cpp +Rpc rpc(sp_loop, IdType::kInt); +rpc.initialize(proto, 30); //! 超时30秒 + +//! 发送请求,等待回复 +rpc.request("ping", Json::object{{"data", "hello"}}, + [](const Response &rsp) { + if (rsp.errcode == 0) + LogInfo("result: %s", rsp.result.dump().c_str()); + else + LogErr("error: %d, %s", rsp.errcode, rsp.message.c_str()); + } +); +``` + +### 服务端 (Pong) + +> 完整示例见 `examples/jsonrpc/req_rsp/pong/` + +```cpp +Rpc rpc(sp_loop, IdType::kInt); +rpc.initialize(proto, 30); + +//! 注册服务方法 +rpc.addService("ping", + [](int int_id, const Json ¶ms, Response &response) { + //! 同步回复 + response.errcode = 0; + response.result = Json::object{{"echo", params["data"]}}; + return true; + } +); +``` + +### 异步回复 + +```cpp +rpc.addService("async_query", + [](int int_id, const Json ¶ms, Response &response) { + //! 异步处理:不立即回复,稍后通过 respondResult 回复 + //! 返回 false 表示不自动回复 + thread_pool.execute( + [int_id, params] { /* 后台查询 */ }, + [int_id, &rpc] { + rpc.respondResult(int_id, Json::object{{"status", "ok"}}); + } + ); + return false; + } +); +``` + +### 通知(不需要回复) + +```cpp +//! 发送通知 +rpc.notify("event", Json::object{{"type", "alert"}}); +``` + +### 消息通信 (Ping/Pong 单向) + +> 完整示例见 `examples/jsonrpc/message/ping/` 和 `pong/` + +适用于简单的消息传递场景,无需请求-回复模式。 + +## 常见场景 + +1. **请求-回复**:客户端发送请求,服务端回复结果 +2. **异步处理**:服务端收到请求后异步处理,稍后回复 +3. **事件通知**:一方发送通知消息,另一方仅接收不回复 +4. **超时管理**:请求超时自动触发超时回调 + +## 注意事项 + +1. **ID 类型**:Int ID 自增分配简单高效;String ID(如 UUID)更安全但开销更大 +2. **Proto 实现**:需要自行实现 Proto 的 onRecvData() 方法,解析传输层数据 +3. **超时时间**:request 发出的请求如果在超时时间内没有收到回复,将触发超时回调(errcode != 0) +4. **异步回复的 int_id**:异步回复时需要保存 int_id,后续用它回复 +5. **clear() 清除状态**:重置 RPC 对象的缓存数据,恢复到未收发数据的状态 + +## 相关模块 + +- **event**:基于 Loop 运行 +- **eventx**:使用 TimeoutMonitor 实现请求超时管理 +- **network**:可基于 TcpConnection 实现 Proto 传输层 +- **base**:提供 Json、Cabinet/Token 等基础设施 diff --git a/documents/modules/log.md b/documents/modules/log.md new file mode 100644 index 00000000..5a0bcdf7 --- /dev/null +++ b/documents/modules/log.md @@ -0,0 +1,150 @@ +# Log Sink Module (log) + +## What is it? + +The log module provides multiple implementations of log output sinks, routing log data to destinations such as files, standard output, and the system log. It builds a complete logging system on top of the log macros (LogInfo/LogErr, etc.) from the base module. + +## Why do you need it? + +base/log.h only defines the log printing macros and the `LogPrintfFunc` declaration, but does not implement output functionality. The log module provides multiple Sink implementations that, when registered into the logging system, output log data to different destinations at specified levels and formats. + +## Header Files + +```cpp +#include //! Sink base class +#include //! Async Sink base class +#include //! Async file Sink +#include //! Async stdout Sink +#include //! Async syslog Sink +#include //! Sync stdout Sink +``` + +## Core Classes and Interfaces + +### Sink Class Hierarchy + +``` +Sink (base class) + ├── AsyncSink (async base class, uses AsyncPipe) + │ ├── AsyncFileSink → output to file + │ ├── AsyncStdoutSink → output to stdout + │ └── AsyncSyslogSink → output to syslog + └── SyncStdoutSink → sync output to stdout +``` + +### Sink Base Class Methods + +| Method | Description | +|--------|-------------| +| `setLevel(level)` | Set the default log level filter | +| `setLevel(module, level)` | Set the log level for a specific module | +| `unsetLevel(module)` | Remove the level setting for a specific module | +| `enableColor(enable)` | Enable/disable colored output | +| `enable()` | Enable the Sink | +| `disable()` | Disable the Sink | + +### AsyncStdoutSink — Async Standard Output + +The most commonly used log Sink, which outputs logs asynchronously to standard output. The async mode does not block the logging thread. + +```cpp +log::AsyncStdoutSink stdout_sink; +stdout_sink.enable(); +//! After this, LogInfo/LogErr and other logs will output to stdout +``` + +### AsyncFileSink — Async File Output + +Writes logs asynchronously to a file, with automatic date-based file splitting. + +```cpp +log::AsyncFileSink file_sink; +file_sink.setFilePathPrefix("/data/logs/myapp"); //! File path prefix +file_sink.enable(); +``` + +### SyncStdoutSink — Sync Standard Output + +Used in simple scenarios where logs are output directly to stdout without an async pipeline. Suitable for small test programs. + +> `LogOutput_Enable()` / `LogOutput_Disable()` essentially creates/destroys a SyncStdoutSink. + +## Usage Examples + +### Basic Log Output (Simplest Approach) + +```cpp +#include +#include + +int main() { + LogOutput_Enable(); //! Enable log output to stdout + + LogInfo("program started"); + LogErr("some error occurred"); + + LogOutput_Disable(); //! Disable log output + return 0; +} +``` + +### Configuring Log Level Filtering + +```cpp +//! Only output logs at WARN level and above +sink.setLevel(TBOX_LOG_LEVEL_WARN); + +//! Set different levels for specific modules +sink.setLevel("network", TBOX_LOG_LEVEL_DEBUG); //! network module outputs DEBUG level +sink.setLevel("alarm", TBOX_LOG_LEVEL_INFO); //! alarm module only outputs INFO and above +``` + +### Async File Logging + +```cpp +#include +#include + +log::AsyncFileSink file_sink; +file_sink.setFilePathPrefix("/data/logs/myapp"); +file_sink.enable(); + +//! After this, logs will be written to files whose names automatically include the date and process ID +//! e.g.: /data/logs/myapp.20240530_123456.12345/ +``` + +### Multi-Sink Composition + +You can enable multiple Sinks simultaneously, outputting logs to multiple destinations at the same time: + +```cpp +log::AsyncStdoutSink stdout_sink; +stdout_sink.enable(); //! Output to stdout + +log::AsyncFileSink file_sink; +file_sink.setFilePathPrefix("/data/logs/app"); +file_sink.enable(); //! Output to file + +//! Logs now output to both stdout and file +``` + +## Common Scenarios + +1. **Development debugging**: Use AsyncStdoutSink to output to the terminal, set level to DEBUG +2. **Production deployment**: Use AsyncFileSink to write to files, set level to INFO +3. **Combined output**: Enable both stdout + file simultaneously; view real-time logs on the terminal and store history in files +4. **Module-level control**: Set DEBUG level for key modules and INFO level for other modules + +## Important Notes + +1. **Async vs Sync**: AsyncSink uses a dedicated pipe thread to process logs and does not block business threads; SyncStdoutSink outputs directly in the calling thread +2. **Level filtering**: The default level is MAX (outputs all logs), which can be adjusted as needed +3. **Colored output**: enableColor(true) displays colored logs in terminals that support ANSI colors +4. **File splitting**: AsyncFileSink automatically splits log files by date +5. **Cleanup**: Call cleanup() before program exit to ensure all buffered logs are fully written + +## Related Modules + +- **base**: Provides log macros (LogInfo/LogErr, etc.) and the LogPrintfFunc declaration +- **main**: The framework automatically configures the logging system +- **util**: AsyncPipe is the underlying pipeline component used by AsyncSink diff --git a/documents/modules/log_CN.md b/documents/modules/log_CN.md new file mode 100644 index 00000000..25d44389 --- /dev/null +++ b/documents/modules/log_CN.md @@ -0,0 +1,150 @@ +# 日志通道模块 (log) + +## 是什么? + +log 模块提供了日志输出通道(Sink)的多种实现,将日志数据输出到文件、标准输出、系统日志等目标。它基于 base 模块的日志宏(LogInfo/LogErr 等)构建完整的日志系统。 + +## 为什么需要它? + +base/log.h 只定义了日志打印宏和 `LogPrintfFunc` 声明,但没有实现输出功能。log 模块提供了多种 Sink 实现,通过注册到日志系统,将日志数据按指定级别和格式输出到不同目标。 + +## 头文件 + +```cpp +#include //! Sink 基类 +#include //! 异步 Sink 基类 +#include //! 异步文件 Sink +#include //! 异步 stdout Sink +#include //! 异步 syslog Sink +#include //! 同步 stdout Sink +``` + +## 核心类与接口 + +### Sink 类继承层次 + +``` +Sink(基类) + ├── AsyncSink(异步基类,使用 AsyncPipe) + │ ├── AsyncFileSink → 输出到文件 + │ ├── AsyncStdoutSink → 输出到 stdout + │ └── AsyncSyslogSink → 输出到 syslog + └── SyncStdoutSink → 同步输出到 stdout +``` + +### Sink 基类方法 + +| 方法 | 说明 | +|------|------| +| `setLevel(level)` | 设置默认日志级别过滤 | +| `setLevel(module, level)` | 设置指定模块的日志级别 | +| `unsetLevel(module)` | 取消指定模块的级别设置 | +| `enableColor(enable)` | 启用/禁用彩色输出 | +| `enable()` | 启用 Sink | +| `disable()` | 禁用 Sink | + +### AsyncStdoutSink — 异步标准输出 + +最常用的日志 Sink,将日志异步输出到标准输出。异步模式不会阻塞日志线程。 + +```cpp +log::AsyncStdoutSink stdout_sink; +stdout_sink.enable(); +//! 此后 LogInfo/LogErr 等日志将输出到 stdout +``` + +### AsyncFileSink — 异步文件输出 + +将日志异步写入文件,支持按日期自动分割文件。 + +```cpp +log::AsyncFileSink file_sink; +file_sink.setFilePathPrefix("/data/logs/myapp"); //! 文件路径前缀 +file_sink.enable(); +``` + +### SyncStdoutSink — 同步标准输出 + +简单场景下使用,日志直接输出到 stdout,没有异步管道。适合小型测试程序。 + +> `LogOutput_Enable()` / `LogOutput_Disable()` 实际上就是创建/销毁一个 SyncStdoutSink。 + +## 使用示例 + +### 基本日志输出(最简单方式) + +```cpp +#include +#include + +int main() { + LogOutput_Enable(); //! 开启日志输出到 stdout + + LogInfo("program started"); + LogErr("some error occurred"); + + LogOutput_Disable(); //! 关闭日志输出 + return 0; +} +``` + +### 配置日志级别过滤 + +```cpp +//! 只输出 WARN 及以上级别的日志 +sink.setLevel(TBOX_LOG_LEVEL_WARN); + +//! 为指定模块设置不同的级别 +sink.setLevel("network", TBOX_LOG_LEVEL_DEBUG); //! network 模块输出 DEBUG 级别 +sink.setLevel("alarm", TBOX_LOG_LEVEL_INFO); //! alarm 模块只输出 INFO 及以上 +``` + +### 异步文件日志 + +```cpp +#include +#include + +log::AsyncFileSink file_sink; +file_sink.setFilePathPrefix("/data/logs/myapp"); +file_sink.enable(); + +//! 此后日志将写入文件,文件名自动包含日期和进程号 +//! 如:/data/logs/myapp.20240530_123456.12345/ +``` + +### 多 Sink 组合 + +可同时启用多个 Sink,日志同时输出到多个目标: + +```cpp +log::AsyncStdoutSink stdout_sink; +stdout_sink.enable(); //! 输出到 stdout + +log::AsyncFileSink file_sink; +file_sink.setFilePathPrefix("/data/logs/app"); +file_sink.enable(); //! 输出到文件 + +//! 日志同时输出到 stdout 和文件 +``` + +## 常见场景 + +1. **开发调试**:使用 AsyncStdoutSink 输出到终端,级别设为 DEBUG +2. **生产运行**:使用 AsyncFileSink 写入文件,级别设为 INFO +3. **组合输出**:同时启用 stdout + file,终端看实时日志,文件存历史 +4. **模块级别控制**:为关键模块设 DEBUG 级别,其他模块设 INFO 级别 + +## 注意事项 + +1. **异步 vs 同步**:AsyncSink 使用独立管道线程处理日志,不会阻塞业务线程;SyncStdoutSink 直接在调用线程输出 +2. **级别过滤**:默认级别为 MAX(输出所有日志),可根据需要设置 +3. **彩色输出**:enableColor(true) 在支持 ANSI 颜色的终端中显示彩色日志 +4. **文件分割**:AsyncFileSink 自动按日期分割日志文件 +5. **cleanup**:程序退出前调用 cleanup() 确保所有缓冲日志写入完成 + +## 相关模块 + +- **base**:提供日志宏(LogInfo/LogErr 等)和 LogPrintfFunc 声明 +- **main**:框架自动配置日志系统 +- **util**:AsyncPipe 是 AsyncSink 的底层管道组件 diff --git a/documents/modules/main.md b/documents/modules/main.md new file mode 100644 index 00000000..93eec6f1 --- /dev/null +++ b/documents/modules/main.md @@ -0,0 +1,249 @@ +# Application Framework Module (main) + +## What is it? + +The main module is the application startup framework. It provides a unified and complete encapsulation of the program startup process, allowing developers to focus only on business logic without worrying about startup procedures. It automatically creates common components such as the event loop, thread pool, timer pool, and coroutine scheduler, and provides them to business modules through the Context object. + +## Why do you need it? + +When developing service-type programs, you typically need to repeatedly write the following procedures: creating an event loop, initializing logging, configuring a thread pool, handling command-line arguments, responding to exit signals, etc. The main module encapsulates all these procedures uniformly, so developers only need to implement four steps for their business modules: initialize, start, stop, and cleanup. + +![main-framework](../images/0008-main-framework.png) + +## Header Files + +```cpp +#include //! Main entry function and registration interface +#include //! Module base class +#include //! Process context +#include //! Command-line argument parser +#include //! Logging related +#include //! Tracing related +``` + +## Core Classes and Interfaces + +### Main / Start / Stop — Start and Stop + +| Function | Description | +|----------|-------------| +| `Main(argc, argv)` | Run the tbox::main framework in the foreground, blocking until a stop signal is received | +| `Start(argc, argv)` | Run the tbox::main framework in the backend, non-blocking | +| `Stop()` | Stop the backend-running tbox::main framework | +| `RaiseStopSignal()` | Send a stop request to itself | + +### Module — Business Module Base Class + +The Module lifecycle follows this process: + +``` +Construct → Initialize → Start → .Running. → Stop → Cleanup → Destruct +``` + +Using setting up a computer as an analogy: +1. **Construct** — Place the devices one by one +2. **Initialize** — Plug in power, connect cables +3. **Start** — Turn on each device +4. ... Normal operation ... +5. **Stop** — Turn off each device +6. **Cleanup** — Disconnect cables +7. **Destruct** — Remove the devices one by one + +| Method | Description | +|--------|-------------| +| `Module(name, ctx)` | Constructor, name is the module name, ctx is the process context | +| `add(child, required)` | Add a child module. When required=true, failure of the child module's initialization/start will cause the entire program startup to fail | +| `addAs(child, name, required)` | Add a child module and rename it | +| `name()` | Get the module name | +| `ctx()` | Get the process context | +| `state()` | Get the module state (kNone/kInited/kRunning) | + +Virtual functions to override: + +| Virtual Function | Description | +|------------------|-------------| +| `onFillDefaultConfig(Json)` | Fill default configuration parameters (Note: the logging system is not available at this stage) | +| `onInit(const Json &cfg)` | Initialize, read configuration, establish object connections | +| `onStart()` | Start the module, make objects begin working | +| `onStop()` | Stop the module, the reverse operation of onStart() | +| `onCleanup()` | Cleanup the module, the reverse operation of onInit() | + +### Context — Process Context + +Context provides the common components created by the framework, which business modules access via `ctx()`: + +| Interface | Description | +|-----------|-------------| +| `ctx.loop()` | Event loop object | +| `ctx.thread_pool()` | Thread pool object | +| `ctx.timer_pool()` | Timer pool object | +| `ctx.async()` | Async operation object | +| `ctx.terminal()` | Interactive terminal object | +| `ctx.coroutine()` | Coroutine scheduler object | +| `ctx.running_time()` | Program running duration | +| `ctx.start_time_point()` | Program start time point | +| `ctx.args()` | Command-line argument list | + +### Required Functions + +Developers must implement the following functions for the framework to call: + +| Function | Description | +|----------|-------------| +| `RegisterApps(Module &apps, Context &ctx)` | Register application modules | +| `GetAppDescribe()` | Return application description (displayed when executing -h) | +| `GetAppBuildTime()` | Return build time (displayed when executing -v), typically returns `__DATE__ " " __TIME__` | +| `GetAppVersion(major, minor, rev, build)` | Set application version number | + +## Usage Examples + +### Single Application + +> Full example at `examples/main/01_one_app/` + +**Step 1**: Inherit from the Module class + +```cpp +// app.h +#include + +class App : public tbox::main::Module +{ + public: + App(tbox::main::Context &ctx); + ~App(); + + protected: + virtual bool onInit(const tbox::Json &cfg) override; + virtual bool onStart() override; + virtual void onStop() override; + virtual void onCleanup() override; +}; +``` + +```cpp +// app.cpp +#include "app.h" +#include + +App::App(tbox::main::Context &ctx) : Module("app", ctx) +{ + LogTag(); +} + +bool App::onInit(const tbox::Json &cfg) { LogTag(); return true; } +bool App::onStart() { LogTag(); return true; } +void App::onStop() { LogTag(); } +void App::onCleanup() { LogTag(); } +``` + +**Step 2**: Implement the registration functions + +```cpp +// main.cpp +#include +#include "app.h" + +namespace tbox { +namespace main { + +void RegisterApps(Module &apps, Context &ctx) { + apps.add(new ::App(ctx)); +} + +std::string GetAppDescribe() { return "One app sample"; } +std::string GetAppBuildTime() { return __DATE__ " " __TIME__; } + +void GetAppVersion(int &major, int &minor, int &rev, int &build) { + major = 0; minor = 0; rev = 1; build = 0; +} + +}} +``` + +**Step 3**: Add dependency libraries in Makefile + +```makefile +LDFLAGS += -L.. \ + -ltbox_main \ + -ltbox_terminal \ + -ltbox_network \ + -ltbox_eventx \ + -ltbox_event \ + -ltbox_util \ + -ltbox_base \ + -lpthread -ldl +``` + +### Multiple Application Modules + +> Full example at `examples/main/02_more_than_one_apps/` + +```cpp +void RegisterApps(Module &apps, Context &ctx) { + apps.add(new App1(ctx)); //! Required startup module + apps.add(new App2(ctx), false); //! Non-required startup module, failure does not affect other modules +} +``` + +### Submodule Nesting + +Module supports a tree-like nesting structure. The parent module automatically manages the lifecycle of child modules: + +```cpp +class ParentApp : public tbox::main::Module { + public: + ParentApp(Context &ctx) : Module("parent", ctx) { + add(new SubModuleA(ctx)); //! Added as a child module + add(new SubModuleB(ctx)); + } +}; + +//! The initialize/start/stop/cleanup of child modules are automatically called by the parent module +//! Do not manually delete or call lifecycle methods of child modules +``` + +### Backend Running Mode + +> Full example at `examples/main/06_run_in_backend/` + +When you need to integrate the tbox::main framework into an existing program framework, you can use the backend running mode: + +```cpp +int main(int argc, char **argv) { + if (!tbox::main::Start(argc, argv)) + return 0; + + //! The existing program framework continues running + while (true) { + // ... + } + + tbox::main::Stop(); + return 0; +} +``` + +## Common Scenarios + +1. **Standard service programs**: Use `Main()` to run in the foreground, business modules obtain common components via Context +2. **Embedded integration**: Use `Start()/Stop()` to integrate the framework into an existing program without affecting the original architecture +3. **Modular development**: Different functional modules independently inherit from Module, composed via `RegisterApps` +4. **Optional modules**: Use `add(child, false)` to add non-required modules; failure does not affect the main flow + +## Important Notes + +1. **Module lifecycle order**: Must follow the Construct→initialize→start→stop→cleanup→destruct order; no skipping allowed +2. **Do not manually manage child modules**: After add(), the child module's lifecycle is managed by the parent module; do not manually delete or call lifecycle methods +3. **Logging is unavailable in onFillDefaultConfig**: The logging system is not yet initialized at this stage; do not use LogInfo and similar macros +4. **Impact of the required parameter**: A child module with required=true that fails onInit/onStart will cause the entire program startup to fail +5. **RegisterApps function signature**: Must be placed in the `tbox::main` namespace, otherwise the framework cannot find it + +## Related Modules + +- **event**: The framework automatically creates a Loop, accessible via `ctx.loop()` +- **eventx**: The framework automatically creates ThreadPool/TimerPool/Async, accessible via `ctx.thread_pool()/ctx.timer_pool()/ctx.async()` +- **terminal**: The framework automatically creates Terminal, accessible via `ctx.terminal()` +- **coroutine**: The framework automatically creates Scheduler, accessible via `ctx.coroutine()` +- **base**: Provides logging, ScopeExit, and other basic components +- **log**: The framework automatically configures the logging system diff --git a/documents/modules/main_CN.md b/documents/modules/main_CN.md new file mode 100644 index 00000000..7dd924cf --- /dev/null +++ b/documents/modules/main_CN.md @@ -0,0 +1,249 @@ +# 应用框架模块 (main) + +## 是什么? + +main 模块是应用程序的启动框架,对程序启动过程进行了统一完备的封装,让开发者只需关心业务逻辑,不必关心启动流程。它自动创建事件循环、线程池、定时池、协程调度器等公共组件,并通过 Context 对象提供给业务模块使用。 + +## 为什么需要它? + +开发服务型程序时,通常需要重复编写以下流程:创建事件循环、初始化日志、配置线程池、处理命令行参数、响应退出信号等。main 模块将这些流程统一封装,开发者只需实现业务模块的初始化、启动、停止、清理四个步骤即可。 + +![main-framework](../images/0008-main-framework.png) + +## 头文件 + +```cpp +#include //! 主入口函数与注册接口 +#include //! 模块基类 +#include //! 进程上下文 +#include //! 命令行参数解析器 +#include //! 日志相关 +#include //! 追踪相关 +``` + +## 核心类与接口 + +### Main / Start / Stop — 启动与停止 + +| 函数 | 说明 | +|------|------| +| `Main(argc, argv)` | 在前端运行 tbox::main 框架,阻塞直到收到停止信号 | +| `Start(argc, argv)` | 在后端运行 tbox::main 框架,不阻塞 | +| `Stop()` | 停止后端运行的 tbox::main 框架 | +| `RaiseStopSignal()` | 给自身发送停止请求 | + +### Module — 业务模块基类 + +Module 的生命周期遵循以下过程: + +``` +构造 → 初始化 → 启动 → .运行中. → 停止 → 清理 → 析构 +``` + +以使用一台电脑为类比: +1. **构造** — 将设备逐一布置好 +2. **初始化 (initialize)** — 插好电源,连接线缆 +3. **启动 (start)** — 启动各个设备 +4. ... 正常工作 ... +5. **停止 (stop)** — 关闭各个设备 +6. **清理 (cleanup)** — 断开连接线缆 +7. **析构** — 将设备逐一撤走 + +| 方法 | 说明 | +|------|------| +| `Module(name, ctx)` | 构造函数,name 为模块名,ctx 为进程上下文 | +| `add(child, required)` | 添加子模块。required=true 时子模块初始化/启动失败会导致整个程序启动失败 | +| `addAs(child, name, required)` | 添加子模块并重新命名 | +| `name()` | 获取模块名 | +| `ctx()` | 获取进程上下文 | +| `state()` | 获取模块状态(kNone/kInited/kRunning) | + +需要重写的虚函数: + +| 虚函数 | 说明 | +|------|------| +| `onFillDefaultConfig(Json)` | 填充默认配置参数(注意:此阶段日志系统不可用) | +| `onInit(const Json &cfg)` | 初始化,读取配置、建立对象连接 | +| `onStart()` | 启动模块,令对象开始工作 | +| `onStop()` | 停止模块,对应 onStart() 的逆操作 | +| `onCleanup()` | 清理模块,对应 onInit() 的逆操作 | + +### Context — 进程上下文 + +Context 提供了框架创建的公共组件,业务模块通过 `ctx()` 获取: + +| 接口 | 说明 | +|------|------| +| `ctx.loop()` | 事件循环对象 | +| `ctx.thread_pool()` | 线程池对象 | +| `ctx.timer_pool()` | 定时池对象 | +| `ctx.async()` | 异步操作对象 | +| `ctx.terminal()` | 交互终端对象 | +| `ctx.coroutine()` | 协程调度器对象 | +| `ctx.running_time()` | 程序运行时长 | +| `ctx.start_time_point()` | 程序启动时间点 | +| `ctx.args()` | 命令行参数列表 | + +### 必须实现的函数 + +开发者需要实现以下函数供框架调用: + +| 函数 | 说明 | +|------|------| +| `RegisterApps(Module &apps, Context &ctx)` | 注册应用模块 | +| `GetAppDescribe()` | 返回应用描述(执行 -h 时显示) | +| `GetAppBuildTime()` | 返回编译时间(执行 -v 时显示),通常返回 `__DATE__ " " __TIME__` | +| `GetAppVersion(major, minor, rev, build)` | 设置应用版本号 | + +## 使用示例 + +### 单一应用 + +> 完整示例见 `examples/main/01_one_app/` + +**第一步**:继承 Module 类 + +```cpp +// app.h +#include + +class App : public tbox::main::Module +{ + public: + App(tbox::main::Context &ctx); + ~App(); + + protected: + virtual bool onInit(const tbox::Json &cfg) override; + virtual bool onStart() override; + virtual void onStop() override; + virtual void onCleanup() override; +}; +``` + +```cpp +// app.cpp +#include "app.h" +#include + +App::App(tbox::main::Context &ctx) : Module("app", ctx) +{ + LogTag(); +} + +bool App::onInit(const tbox::Json &cfg) { LogTag(); return true; } +bool App::onStart() { LogTag(); return true; } +void App::onStop() { LogTag(); } +void App::onCleanup() { LogTag(); } +``` + +**第二步**:实现注册函数 + +```cpp +// main.cpp +#include +#include "app.h" + +namespace tbox { +namespace main { + +void RegisterApps(Module &apps, Context &ctx) { + apps.add(new ::App(ctx)); +} + +std::string GetAppDescribe() { return "One app sample"; } +std::string GetAppBuildTime() { return __DATE__ " " __TIME__; } + +void GetAppVersion(int &major, int &minor, int &rev, int &build) { + major = 0; minor = 0; rev = 1; build = 0; +} + +}} +``` + +**第三步**:在 Makefile 中添加依赖库 + +```makefile +LDFLAGS += -L.. \ + -ltbox_main \ + -ltbox_terminal \ + -ltbox_network \ + -ltbox_eventx \ + -ltbox_event \ + -ltbox_util \ + -ltbox_base \ + -lpthread -ldl +``` + +### 多个应用模块 + +> 完整示例见 `examples/main/02_more_than_one_apps/` + +```cpp +void RegisterApps(Module &apps, Context &ctx) { + apps.add(new App1(ctx)); //! 必须启动模块 + apps.add(new App2(ctx), false); //! 非必须启动模块,失败不影响其他模块 +} +``` + +### 子模块嵌套 + +Module 支持树状嵌套结构,父模块自动管理子模块的生命周期: + +```cpp +class ParentApp : public tbox::main::Module { + public: + ParentApp(Context &ctx) : Module("parent", ctx) { + add(new SubModuleA(ctx)); //! 作为子模块添加 + add(new SubModuleB(ctx)); + } +}; + +//! 子模块的 initialize/start/stop/cleanup 由父模块自动调用 +//! 不可私自 delete 或手动调用子模块的生命周期方法 +``` + +### 后端运行模式 + +> 完整示例见 `examples/main/06_run_in_backend/` + +当需要将 tbox::main 框架集成到已有程序框架时,可使用后端运行模式: + +```cpp +int main(int argc, char **argv) { + if (!tbox::main::Start(argc, argv)) + return 0; + + //! 原有程序框架继续运行 + while (true) { + // ... + } + + tbox::main::Stop(); + return 0; +} +``` + +## 常见场景 + +1. **标准服务程序**:使用 `Main()` 在前端运行,业务模块通过 Context 获取公共组件 +2. **嵌入式集成**:使用 `Start()/Stop()` 将框架集成到已有程序,不影响原有架构 +3. **模块化开发**:不同功能模块独立继承 Module,通过 `RegisterApps` 组合 +4. **可选模块**:使用 `add(child, false)` 添加非必需模块,失败不影响主流程 + +## 注意事项 + +1. **Module 生命期顺序**:必须按 构造→initialize→start→stop→cleanup→析构 顺序,不可跳跃 +2. **子模块不要手动管理**:add() 后子模块生命期由父模块管控,不可私自 delete 或调用生命周期方法 +3. **onFillDefaultConfig 中日志不可用**:该阶段日志系统尚未初始化,不要使用 LogInfo 等宏 +4. **required 参数的影响**:required=true 的子模块 onInit/onStart 失败会导致整个程序启动失败 +5. **RegisterApps 函数签名**:必须放在 `tbox::main` namespace 中,否则框架找不到 + +## 相关模块 + +- **event**:框架自动创建 Loop,通过 `ctx.loop()` 获取 +- **eventx**:框架自动创建 ThreadPool/TimerPool/Async,通过 `ctx.thread_pool()/ctx.timer_pool()/ctx.async()` 获取 +- **terminal**:框架自动创建 Terminal,通过 `ctx.terminal()` 获取 +- **coroutine**:框架自动创建 Scheduler,通过 `ctx.coroutine()` 获取 +- **base**:提供日志、ScopeExit 等基础组件 +- **log**:框架自动配置日志系统 diff --git a/documents/modules/mqtt.md b/documents/modules/mqtt.md new file mode 100644 index 00000000..abf4ed6a --- /dev/null +++ b/documents/modules/mqtt.md @@ -0,0 +1,198 @@ +# MQTT Client Module (mqtt) + +## What is it? + +The mqtt module provides an MQTT protocol client implementation, built on top of the libmosquitto library and seamlessly integrated with cpp-tbox's event loop. It supports TLS encrypted connections, will message configuration, automatic reconnection, and more. + +## Why do you need it? + +In IoT and message middleware scenarios, MQTT is the most commonly used lightweight messaging protocol. The mqtt module enables C++ service programs to easily connect to an MQTT Broker, subscribe to/publish topics, while enjoying the event-driven asynchronous callback model. + +## Header Files + +```cpp +#include +``` + +## Core Classes and Interfaces + +### Client — MQTT Client + +| Method | Description | +|------|------| +| `Client(loop)` | Constructor | +| `initialize(config, callbacks)` | Initialize configuration and callbacks | +| `start()` | Start connection | +| `stop()` | Stop connection | +| `subscribe(topic, mid, qos)` | Subscribe to a topic | +| `unsubscribe(topic, mid)` | Unsubscribe from a topic | +| `publish(topic, payload, size, qos, retain, mid)` | Publish a message | +| `cleanup()` | Cleanup | +| `getState()` | Get current state | + +### State Transitions + +``` +kNone → kInited → kConnecting → kTcpConnected → kMqttConnected + ↓ (disconnected) + kReconnWaiting → kConnecting (auto reconnect) + ↓ (no reconnect needed) + kEnd +``` + +### Config — Configuration + +```cpp +mqtt::Client::Config conf; + +//! Basic configuration +conf.base.broker.domain = "broker.emqx.io"; //! Broker address +conf.base.broker.port = 1883; //! Broker port +conf.base.client_id = "my_client"; //! Client ID +conf.base.username = "user"; //! Username +conf.base.passwd = "pass"; //! Password +conf.base.keepalive = 60; //! Keepalive interval (seconds) + +//! TLS configuration (optional) +conf.tls.enabled = true; +conf.tls.ca_file = "./ca.pem"; + +//! Will message configuration (optional) +conf.will.enabled = true; +conf.will.topic = "/device/offline"; +conf.will.payload = Memblock("offline", 7); + +//! Auto reconnect configuration +conf.auto_reconnect_enable = true; +conf.auto_reconnect_wait_sec_gen_func = [](int fail_count) { + return 1 << std::min(fail_count, 4); //! Exponential backoff: 1, 2, 4, 8, 16... +}; +``` + +### Callbacks — Callback Functions + +```cpp +mqtt::Client::Callbacks cbs; + +cbs.connected = [] { LogInfo("MQTT connected"); }; +cbs.connect_fail = [] { LogErr("MQTT connect fail"); }; +cbs.disconnected = [] { LogInfo("MQTT disconnected"); }; +cbs.message_recv = [](int mid, const std::string &topic, + const void *payload, int size, int qos, bool retain) { + LogInfo("recv topic:%s, data:%.*s", topic.c_str(), size, (char*)payload); +}; +cbs.message_pub = [](int mid) { LogInfo("published, mid=%d", mid); }; +cbs.subscribed = [](int mid, int qos, const int *granted_qos) { LogInfo("subscribed"); }; +cbs.unsubscribed = [](int mid) { LogInfo("unsubscribed"); }; +cbs.state_changed = [](mqtt::Client::State state) { LogInfo("state: %d", state); }; +``` + +## Usage Examples + +### Connecting to an MQTT Broker + +> Full example in `examples/mqtt/conn/` + +```cpp +#include +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; + +int main() { + LogOutput_Enable(); + + Loop* sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + mqtt::Client mqtt(sp_loop); + + mqtt::Client::Config conf; + conf.auto_reconnect_enable = true; + conf.auto_reconnect_wait_sec_gen_func = [](int fail_count) { + return 1 << std::min(fail_count, 4); //! Exponential backoff reconnect + }; + + if (!mqtt.initialize(conf, mqtt::Client::Callbacks())) { + LogErr("init mqtt fail"); + return 0; + } + + mqtt.start(); + + //! Listen for exit signal + auto stop_ev = sp_loop->newSignalEvent(); + SetScopeExitAction([stop_ev] { delete stop_ev; }); + stop_ev->initialize(SIGINT, Event::Mode::kOneshot); + stop_ev->enable(); + stop_ev->setCallback([sp_loop, &mqtt] (int) { + mqtt.stop(); + sp_loop->exitLoop(); + }); + + sp_loop->runLoop(Loop::Mode::kForever); + mqtt.cleanup(); + + LogOutput_Disable(); + return 0; +} +``` + +### Subscribing to a Topic + +> Full example in `examples/mqtt/sub/` + +```cpp +mqtt::Client::Callbacks cbs; +cbs.connected = [&mqtt] { + LogInfo("connected, subscribing..."); + mqtt.subscribe("/sensor/temperature", nullptr, 1); //! QoS=1 +}; +cbs.message_recv = [](int mid, const std::string &topic, + const void *payload, int size, int qos, bool retain) { + LogInfo("topic:%s, payload:%.*s", topic.c_str(), size, (const char*)payload); +}; + +mqtt.initialize(conf, cbs); +``` + +### Publishing a Message + +> Full example in `examples/mqtt/pub/` + +```cpp +mqtt::Client::Callbacks cbs; +cbs.connected = [&mqtt] { + LogInfo("connected, publishing..."); + mqtt.publish("/sensor/temperature", "25.6", 4, 1, false); //! QoS=1, retain=false +}; + +mqtt.initialize(conf, cbs); +``` + +## Common Scenarios + +1. **IoT data reporting**: Devices connect to the Broker and periodically publish sensor data +2. **Message subscription**: Server-side subscribes to topics to receive data reported by devices +3. **Device status monitoring**: Using the Will message mechanism, devices automatically publish offline messages when disconnected +4. **TLS secure connection**: Enable TLS encryption for scenarios with high security requirements +5. **Automatic reconnection on disconnection**: Enable auto_reconnect with an exponential backoff strategy + +## Important Notes + +1. **Auto reconnect strategy**: Use exponential backoff (`1 << min(fail_count, 4)`) to avoid frequent reconnects that waste resources +2. **Memblock will message**: Will message payload uses the `Memblock` type, which supports binary data +3. **Callbacks run in the Loop thread**: All callbacks execute in the main Loop thread — do not perform time-consuming operations in callbacks +4. **Initialization order**: Call initialize() first, then start(); call cleanup() only after stop() +5. **Depends on libmosquitto**: The mosquitto library must be linked at compile time + +## Related Modules + +- **event**: Implements socket I/O and timers based on Loop +- **base**: Provides Memblock, Log macros, etc. +- **network**: Implements underlying connections using SocketFd/TcpConnection diff --git a/documents/modules/mqtt_CN.md b/documents/modules/mqtt_CN.md new file mode 100644 index 00000000..cd915898 --- /dev/null +++ b/documents/modules/mqtt_CN.md @@ -0,0 +1,198 @@ +# MQTT 客户端模块 (mqtt) + +## 是什么? + +mqtt 模块提供了 MQTT 协议客户端实现,基于 libmosquitto 库封装,与 cpp-tbox 的事件循环无缝集成,支持 TLS 加密连接、遗言配置、自动重连等特性。 + +## 为什么需要它? + +在 IoT 和消息中间件场景中,MQTT 是最常用的轻量级消息协议。mqtt 模块让 C++ 服务程序能方便地连接 MQTT Broker,订阅/发布主题,同时享受事件驱动的异步回调模式。 + +## 头文件 + +```cpp +#include +``` + +## 核心类与接口 + +### Client — MQTT 客户端 + +| 方法 | 说明 | +|------|------| +| `Client(loop)` | 构造 | +| `initialize(config, callbacks)` | 初始化配置和回调 | +| `start()` | 开始连接 | +| `stop()` | 停止连接 | +| `subscribe(topic, mid, qos)` | 订阅主题 | +| `unsubscribe(topic, mid)` | 取消订阅 | +| `publish(topic, payload, size, qos, retain, mid)` | 发布消息 | +| `cleanup()` | 清理 | +| `getState()` | 获取当前状态 | + +### 状态流转 + +``` +kNone → kInited → kConnecting → kTcpConnected → kMqttConnected + ↓ (断连) + kReconnWaiting → kConnecting (自动重连) + ↓ (不需重连) + kEnd +``` + +### Config — 配置 + +```cpp +mqtt::Client::Config conf; + +//! 基础配置 +conf.base.broker.domain = "broker.emqx.io"; //! Broker 地址 +conf.base.broker.port = 1883; //! Broker 端口 +conf.base.client_id = "my_client"; //! 客户端 ID +conf.base.username = "user"; //! 用户名 +conf.base.passwd = "pass"; //! 密码 +conf.base.keepalive = 60; //! 心跳时长(秒) + +//! TLS 配置(可选) +conf.tls.enabled = true; +conf.tls.ca_file = "./ca.pem"; + +//! 遗言配置(可选) +conf.will.enabled = true; +conf.will.topic = "/device/offline"; +conf.will.payload = Memblock("offline", 7); + +//! 自动重连配置 +conf.auto_reconnect_enable = true; +conf.auto_reconnect_wait_sec_gen_func = [](int fail_count) { + return 1 << std::min(fail_count, 4); //! 指数退避:1, 2, 4, 8, 16... +}; +``` + +### Callbacks — 回调函数 + +```cpp +mqtt::Client::Callbacks cbs; + +cbs.connected = [] { LogInfo("MQTT connected"); }; +cbs.connect_fail = [] { LogErr("MQTT connect fail"); }; +cbs.disconnected = [] { LogInfo("MQTT disconnected"); }; +cbs.message_recv = [](int mid, const std::string &topic, + const void *payload, int size, int qos, bool retain) { + LogInfo("recv topic:%s, data:%.*s", topic.c_str(), size, (char*)payload); +}; +cbs.message_pub = [](int mid) { LogInfo("published, mid=%d", mid); }; +cbs.subscribed = [](int mid, int qos, const int *granted_qos) { LogInfo("subscribed"); }; +cbs.unsubscribed = [](int mid) { LogInfo("unsubscribed"); }; +cbs.state_changed = [](mqtt::Client::State state) { LogInfo("state: %d", state); }; +``` + +## 使用示例 + +### 连接 MQTT Broker + +> 完整示例见 `examples/mqtt/conn/` + +```cpp +#include +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; + +int main() { + LogOutput_Enable(); + + Loop* sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + mqtt::Client mqtt(sp_loop); + + mqtt::Client::Config conf; + conf.auto_reconnect_enable = true; + conf.auto_reconnect_wait_sec_gen_func = [](int fail_count) { + return 1 << std::min(fail_count, 4); //! 指数退避重连 + }; + + if (!mqtt.initialize(conf, mqtt::Client::Callbacks())) { + LogErr("init mqtt fail"); + return 0; + } + + mqtt.start(); + + //! 监听退出信号 + auto stop_ev = sp_loop->newSignalEvent(); + SetScopeExitAction([stop_ev] { delete stop_ev; }); + stop_ev->initialize(SIGINT, Event::Mode::kOneshot); + stop_ev->enable(); + stop_ev->setCallback([sp_loop, &mqtt] (int) { + mqtt.stop(); + sp_loop->exitLoop(); + }); + + sp_loop->runLoop(Loop::Mode::kForever); + mqtt.cleanup(); + + LogOutput_Disable(); + return 0; +} +``` + +### 订阅主题 + +> 完整示例见 `examples/mqtt/sub/` + +```cpp +mqtt::Client::Callbacks cbs; +cbs.connected = [&mqtt] { + LogInfo("connected, subscribing..."); + mqtt.subscribe("/sensor/temperature", nullptr, 1); //! QoS=1 +}; +cbs.message_recv = [](int mid, const std::string &topic, + const void *payload, int size, int qos, bool retain) { + LogInfo("topic:%s, payload:%.*s", topic.c_str(), size, (const char*)payload); +}; + +mqtt.initialize(conf, cbs); +``` + +### 发布消息 + +> 完整示例见 `examples/mqtt/pub/` + +```cpp +mqtt::Client::Callbacks cbs; +cbs.connected = [&mqtt] { + LogInfo("connected, publishing..."); + mqtt.publish("/sensor/temperature", "25.6", 4, 1, false); //! QoS=1, retain=false +}; + +mqtt.initialize(conf, cbs); +``` + +## 常见场景 + +1. **IoT 数据上报**:设备连接 Broker,定期 publish 传感器数据 +2. **消息订阅**:服务端 subscribe 主题,接收设备上报数据 +3. **设备状态监控**:利用遗言(Will)机制,设备离线时自动发布离线消息 +4. **TLS 安全连接**:启用 TLS 加密,适用于安全要求高的场景 +5. **断线自动重连**:启用 auto_reconnect,配合指数退避策略 + +## 注意事项 + +1. **自动重连策略**:建议使用指数退避(`1 << min(fail_count, 4)`),避免频繁重连消耗资源 +2. **Memblock 遗言**:遗言 payload 使用 `Memblock` 类型,支持二进制数据 +3. **回调在 Loop 线程执行**:所有回调在主 Loop 线程中执行,不要在回调中做耗时操作 +4. **初始化顺序**:先 initialize(),再 start();stop() 后才能 cleanup() +5. **依赖 libmosquitto**:编译时需要链接 mosquitto 库 + +## 相关模块 + +- **event**:基于 Loop 实现 socket 读写和定时器 +- **base**:提供 Memblock、Log 宏等 +- **network**:使用 SocketFd/TcpConnection 实现底层连接 diff --git a/documents/modules/network.md b/documents/modules/network.md new file mode 100644 index 00000000..fb1be4f1 --- /dev/null +++ b/documents/modules/network.md @@ -0,0 +1,404 @@ +# Network Communication Module (network) + +## What is it? + +The network module provides TCP/UDP/UART communication capabilities based on the event module, including server-side (TcpServer/TcpAcceptor), client-side (TcpClient/TcpConnector), UDP communication (UdpSocket), serial communication (Uart), and byte stream abstraction (ByteStream). + +## Why do you need it? + +In service-oriented programs, network communication is the most fundamental requirement. However, traditional socket programming requires handling a large amount of detail: fd management, event listening, data buffering, connection management, and more. The network module encapsulates all of these into object-oriented interfaces that integrate seamlessly with the event loop, allowing developers to focus solely on business logic. + +## Header Files + +```cpp +#include //! TCP server +#include //! TCP connection acceptor +#include //! TCP connector (with auto-reconnect) +#include //! TCP client (encapsulates connection + communication) +#include //! TCP connection (low-level) +#include //! UDP socket +#include //! Serial communication +#include //! Byte stream abstract interface +#include //! Buffered fd +#include //! Address wrapper +#include //! Socket fd +#include //! IP address +#include //! DNS request +#include //! Domain name resolution +#include //! Network interface +#include //! Standard I/O stream +#include //! TLS configuration +``` + +## Core Classes and Interfaces + +### TCP Class Comparison + +Different TCP classes are suited for different scenarios: + +| Class | Use Case | Characteristics | +|------|------|------| +| **TcpServer** | Server side, accepting multiple client connections | Encapsulates Acceptor + multiple Connections, provides client management interface by Token | +| **TcpAcceptor** | Server side, only accepting connections | Only responsible for accepting connections; after obtaining a TcpConnection, you manage it yourself | +| **TcpConnector** | Client side, with auto-reconnect | Supports reconnect strategies and attempt count limits | +| **TcpClient** | Client side, full encapsulation | Encapsulates Connector + Connection, provides ByteStream interface | + +### TcpServer — TCP Server + +| Method | Description | +|------|------| +| `TcpServer(loop)` | Constructor | +| `initialize(bind_addr, backlog)` | Initialize bind address | +| `setTlsConfig(config)` | Set TLS config (must call before initialize) | +| `setConnectedCallback(cb)` | Set new connection callback | +| `setDisconnectedCallback(cb)` | Set disconnect callback | +| `setReceiveCallback(cb, threshold)` | Set receive callback and data threshold | +| `setSendCompleteCallback(cb)` | Set send complete callback | +| `start()` | Start service | +| `send(client, data, size)` | Send data to specified client | +| `disconnect(client)` | Disconnect specified client | +| `stop()` | Stop service | +| `cleanup()` | Clean up resources | + +### TcpClient — TCP Client + +| Method | Description | +|------|------| +| `TcpClient(loop)` | Constructor | +| `initialize(server_addr)` | Initialize server address | +| `setConnectedCallback(cb)` | Set connection success callback | +| `setDisconnectedCallback(cb)` | Set disconnect callback | +| `setAutoReconnect(enable)` | Set auto-reconnect | +| `setTlsConfig(config)` | Set TLS config (must call before initialize) | +| `start()` | Start connection | +| `send(data, size)` | Send data (ByteStream interface) | +| `bind(receiver)` | Bind receiver (pipeline mode) | +| `stop()` | Stop/disconnect | +| `cleanup()` | Clean up | + +### UdpSocket — UDP Socket + +| Method | Description | +|------|------| +| `UdpSocket(loop, broadcast)` | Constructor, specify whether to enable broadcast | +| `bind(addr)` | Bind address | +| `connect(addr)` | Connect to target address | +| `setRecvCallback(cb)` | Set receive callback `(data, size, from_addr)` | +| `send(data, size, to_addr)` | Send data to specified address | +| `send(data, size)` | Send data (requires prior connect) | +| `enable()` / `disable()` | Enable/disable receiving | + +> **Note**: `bind()` and `connect()` cannot be used together. + +### Uart — Serial Communication + +| Method | Description | +|------|------| +| `Uart(loop)` | Constructor | +| `initialize(dev, mode_str)` | Initialize, dev is the device path such as "/dev/ttyS0", mode_str such as "115200 8n1" | +| `initialize(dev, mode)` | Initialize, using Mode struct | +| `send(data, size)` | Send data (ByteStream interface) | +| `bind(receiver)` | Bind receiver | +| `enable()` / `disable()` | Enable/disable | + +Mode struct fields: `baudrate` (default 115200), `data_bit` (k8bits), `parity` (kNoEnd), `stop_bit` (k1bits) + +### SockAddr — Address Wrapper + +```cpp +//! Create address from string +SockAddr addr = SockAddr::FromString("127.0.0.1:12345"); +SockAddr addr = SockAddr::FromString("0.0.0.0:80"); +``` + +### ByteStream — Byte Stream Abstraction + +ByteStream is a unified stream interface that both TcpClient and Uart implement. It supports binding two ByteStreams together to form a data pipeline: + +```cpp +//! Bind UART with TCP Client, serial data is forwarded directly to TCP +uart->bind(tcp_client); +tcp_client->bind(uart); +``` + +## Usage Examples + +### TCP Echo Server + +> Full example at `examples/network/tcp_server/tcp_echo/` + +```cpp +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::network; + +int main() { + LogOutput_Enable(); + + Loop* sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + TcpServer srv(sp_loop); + srv.initialize(SockAddr::FromString("127.0.0.1:12345"), 2); + + //! Echo: send received data back as-is + srv.setReceiveCallback( + [&srv] (const TcpServer::ConnToken &client, Buffer &buff) { + srv.send(client, buff.readableBegin(), buff.readableSize()); + buff.hasReadAll(); + }, 0 + ); + + srv.start(); + + //! Listen for exit signal + auto sp_sig = sp_loop->newSignalEvent(); + SetScopeExitAction([sp_sig] { delete sp_sig; }); + sp_sig->initialize(SIGINT, Event::Mode::kOneshot); + sp_sig->enable(); + sp_sig->setCallback([&] (int) { srv.stop(); sp_loop->exitLoop(); }); + + sp_loop->runLoop(); + srv.cleanup(); + + LogOutput_Disable(); + return 0; +} +``` + +### TCP Client + +> Full example at `examples/network/tcp_client/tcp_echo/` + +```cpp +TcpClient client(sp_loop); +client.initialize(SockAddr::FromString("127.0.0.1:12345")); + +client.setReceiveCallback( + [] (Buffer &buff) { + LogInfo("received: %.*s", (int)buff.readableSize(), (char*)buff.readableBegin()); + buff.hasReadAll(); + }, 0 +); + +client.start(); +client.send("hello", 5); +``` + +### UDP Ping-Pong + +> Full example at `examples/network/udp_socket/ping_pong/` + +```cpp +UdpSocket udp(sp_loop); +udp.bind(SockAddr::FromString("0.0.0.0:12345")); + +udp.setRecvCallback( + [&udp] (const void *data, size_t size, const SockAddr &from) { + LogInfo("recv from %s", from.toString().c_str()); + udp.send("pong", 4, from); //! Reply to sender + } +); +udp.enable(); +``` + +### Serial Communication + +> Full example at `examples/network/uart/uart_tool/` + +```cpp +Uart uart(sp_loop); +uart.initialize("/dev/ttyS0", "115200 8n1"); //! 115200 baud rate, 8 data bits, no parity, 1 stop bit + +uart.setReceiveCallback( + [] (Buffer &buff) { + LogInfo("uart recv: %.*s", (int)buff.readableSize(), (char*)buff.readableBegin()); + buff.hasReadAll(); + }, 0 +); + +uart.enable(); +uart.send("AT\r\n", 4); +``` + +### UART to TCP Bridge + +> Full example at `examples/network/uart/uart_to_uart/` + +```cpp +//! Bidirectional binding: serial data is automatically forwarded to TCP, and vice versa +uart->bind(tcp_client); +tcp_client->bind(uart); +``` + +## TLS (SSL/TLS Encrypted Communication) + +The network module supports TLS encryption through the optional `network_tls` module. Both TcpServer and TcpClient can be upgraded from plain TCP to TLS by calling `setTlsConfig()` before `initialize()`. The TLS implementation uses OpenSSL and supports TLS 1.2+. + +### TlsConfig — TLS Configuration + +```cpp +#include + +struct TlsConfig { + //! CA certificates (for verifying the peer) + std::string ca_file; //!< CA certificate file path (e.g. "/etc/ssl/certs/ca-bundle.crt") + std::string ca_path; //!< CA certificate directory path (e.g. "/etc/ssl/certs/") + + bool verify_peer = true; //!< Whether to verify the peer's certificate + int verify_depth = 1; //!< Certificate chain verification depth + + //! Local certificate and private key + std::string cert_file; //!< Local certificate file + std::string key_file; //!< Local private key file + + //! Client SNI + std::string hostname; //!< Hostname for SNI (Server Name Indication) + + bool isValid() const; //!< Check if the configuration is valid +}; +``` + +**Key points about `ca_file` / `ca_path`:** + +- They are **optional** — you don't need to specify either one. +- When `verify_peer=true` but no `ca_file`/`ca_path` is provided: + - **Client** automatically uses the system default CA certificates (`SSL_CTX_set_default_verify_paths`), such as `/etc/ssl/certs/` on Linux. This is the most common usage scenario. + - **Server** skips client certificate verification (suitable for plain TLS without mTLS). +- When you do specify them, only one is required — `ca_file` or `ca_path`, not both. OpenSSL accepts either. +- `cert_file` and `key_file` must always be specified together (both set or both empty). + +### How TLS Works + +The TLS feature uses a **weak-symbol plugin** mechanism: + +1. The `network` module defines a weak `CreateTlsFactory()` that returns `nullptr`. +2. The `network_tls` module provides a strong implementation that creates a `TcpTlsFactory`. +3. If your application links `libtbox_network_tls`, TLS is enabled; otherwise, `setTlsConfig()` returns `false` and logs a warning. + +### TLS Echo Server + +> Full example at `examples/network/tcp_server/tls_echo_server/` + +```cpp +TcpServer server(sp_loop); + +//! Set TLS config (must call before initialize) +TlsConfig tls_config; +tls_config.cert_file = "server.crt"; //! Server must have cert + key +tls_config.key_file = "server.key"; +tls_config.verify_peer = false; //! Don't verify client cert (not mTLS) +if (!server.setTlsConfig(tls_config)) { + LogErr("TLS not available, need network_tls module"); + return; +} + +server.initialize(SockAddr::FromString("0.0.0.0:12345"), 2); +server.start(); +``` + +### TLS Client (Verify Server with Custom CA) + +> Full example at `examples/network/tcp_client/tls_echo_client/` + +```cpp +TcpClient client(sp_loop); + +//! Verify server certificate using a custom CA file +TlsConfig tls_config; +tls_config.ca_file = "server.crt"; //! Custom CA certificate +tls_config.verify_peer = true; //! Verify server cert +tls_config.hostname = "myserver"; //! SNI hostname +client.setTlsConfig(tls_config); + +client.initialize(SockAddr::FromString("127.0.0.1:12345")); +client.start(); +``` + +### TLS Client (Use System Default CA) + +```cpp +TcpClient client(sp_loop); + +//! Use system default CA certificates (like /etc/ssl/certs/) +//! No need to specify ca_file or ca_path +TlsConfig tls_config; +tls_config.verify_peer = true; //! Verify server cert with system CA +tls_config.hostname = "example.com"; //! SNI hostname +client.setTlsConfig(tls_config); + +client.initialize(SockAddr::FromString("example.com:443")); +client.start(); +``` + +### TLS Client (Skip Verification, like curl -k) + +```cpp +TcpClient client(sp_loop); + +//! Skip server certificate verification (insecure, for testing only) +TlsConfig tls_config; +tls_config.verify_peer = false; +tls_config.hostname = "127.0.0.1"; +client.setTlsConfig(tls_config); + +client.initialize(SockAddr::FromString("127.0.0.1:12345")); +client.start(); +``` + +### mTLS (Mutual TLS — Both Sides Verify) + +```cpp +//! Server side: verify client certificate +TlsConfig server_config; +server_config.cert_file = "server.crt"; +server_config.key_file = "server.key"; +server_config.ca_file = "client-ca.crt"; //! CA that signed client certs +server_config.verify_peer = true; //! Verify client cert +server.setTlsConfig(server_config); + +//! Client side: verify server and present own cert +TlsConfig client_config; +client_config.cert_file = "client.crt"; //! Present client cert to server +client_config.key_file = "client.key"; +client_config.ca_file = "server-ca.crt"; //! CA that signed server certs +client_config.verify_peer = true; +client_config.hostname = "myserver"; +client.setTlsConfig(client_config); +``` + +## Common Scenarios + +1. **Echo Service**: TcpServer receives data and sends it back as-is +2. **Interactive Client**: TcpClient + StdioStream implements an interactive TCP client +3. **UART Bridge**: ByteStream bind forwards serial data to TCP +4. **UDP Communication**: UdpSocket implements broadcast or point-to-point UDP +5. **Auto-reconnect Client**: TcpClient + setAutoReconnect implements automatic reconnect on disconnection +6. **TLS Server**: TcpServer + setTlsConfig enables encrypted communication +7. **TLS Client with System CA**: TcpClient + TlsConfig(verify_peer=true) verifies server using system CA store +8. **mTLS**: Both sides set verify_peer=true + ca_file + cert_file/key_file for mutual authentication + +## Important Notes + +1. **Data threshold**: In `setReceiveCallback(cb, threshold)`, threshold specifies the minimum data amount to trigger the callback; 0 means any received data triggers it immediately +2. **Buffer hasReadAll**: In callbacks, use `buff.hasReadAll()` to mark that all data has been read; otherwise, old data will be received again in the next callback +3. **TcpServer ConnToken**: Clients are identified by Token; the Token becomes invalid after the connection is disconnected +4. **TcpClient auto-reconnect**: After enabling `setAutoReconnect(true)`, disconnection will automatically attempt to reconnect +5. **UDP bind vs connect**: bind and connect cannot be used together; if you need to specify a target address, pass it in send() +6. **TLS setTlsConfig**: Must be called before `initialize()`, and requires linking the `network_tls` module +7. **TLS ca_file/ca_path**: Optional; when verify_peer=true without specifying them, client uses system default CA; only one is needed when you do specify them +8. **TLS cert_file/key_file**: Must always be specified together — both set or both empty + +## Related Modules + +- **event**: Implements socket event listening based on FdEvent +- **http**: Implements HTTP service based on TcpServer/TcpAcceptor +- **mqtt**: Implements MQTT protocol based on TcpConnection +- **network_tls**: Optional module providing OpenSSL-based TLS implementation for TcpServer/TcpClient +- **base**: Provides foundational infrastructure such as Buffer (i.e., util::Buffer), ScopeExit, etc. diff --git a/documents/modules/network_CN.md b/documents/modules/network_CN.md new file mode 100644 index 00000000..116164ed --- /dev/null +++ b/documents/modules/network_CN.md @@ -0,0 +1,404 @@ +# 网络通信模块 (network) + +## 是什么? + +network 模块基于 event 模块提供了 TCP/UDP/UART 通信能力,包括服务端(TcpServer/TcpAcceptor)、客户端(TcpClient/TcpConnector)、UDP 通信(UdpSocket)、串口通信(Uart)以及字节流抽象(ByteStream)。 + +## 为什么需要它? + +在服务型程序中,网络通信是最基础的需求。但传统 socket 编程需要处理大量细节:fd 管理、事件监听、数据缓冲、连接管理等。network 模块将这些封装为面向对象的接口,与事件循环无缝集成,开发者只需关注业务逻辑。 + +## 头文件 + +```cpp +#include //! TCP 服务端 +#include //! TCP 连接接收器 +#include //! TCP 连接器(带自动重连) +#include //! TCP 客户端(封装连接+通信) +#include //! TCP 连接(底层) +#include //! UDP 套接字 +#include //! 串口通信 +#include //! 字节流抽象接口 +#include //! 带缓冲的 fd +#include //! 地址封装 +#include //! 套接字 fd +#include //! IP 地址 +#include //! DNS 请求 +#include //! 域名解析 +#include //! 网络接口 +#include //! 标准 I/O 流 +#include //! TLS 配置 +``` + +## 核心类与接口 + +### TCP 类对比 + +不同的 TCP 类适用于不同场景: + +| 类 | 适用场景 | 特点 | +|------|------|------| +| **TcpServer** | 服务端,接受多个客户端连接 | 封装 Acceptor + 多个 Connection,提供按 Token 管理客户端的接口 | +| **TcpAcceptor** | 服务端,仅接受连接 | 只负责接受连接,得到 TcpConnection 后自行管理 | +| **TcpConnector** | 客户端,带自动重连 | 支持重连策略、尝试次数限制 | +| **TcpClient** | 客户端,完整封装 | 封装 Connector + Connection,提供 ByteStream 接口 | + +### TcpServer — TCP 服务端 + +| 方法 | 说明 | +|------|------| +| `TcpServer(loop)` | 构造 | +| `initialize(bind_addr, backlog)` | 初始化绑定地址 | +| `setTlsConfig(config)` | 设置 TLS 配置(必须在 initialize 之前调用) | +| `setConnectedCallback(cb)` | 设置新连接回调 | +| `setDisconnectedCallback(cb)` | 设置断开回调 | +| `setReceiveCallback(cb, threshold)` | 设置接收回调与数据阈值 | +| `setSendCompleteCallback(cb)` | 设置发送完成回调 | +| `start()` | 启动服务 | +| `send(client, data, size)` | 向指定客户端发送数据 | +| `disconnect(client)` | 断开指定客户端 | +| `stop()` | 停止服务 | +| `cleanup()` | 清理资源 | + +### TcpClient — TCP 客户端 + +| 方法 | 说明 | +|------|------| +| `TcpClient(loop)` | 构造 | +| `initialize(server_addr)` | 初始化服务端地址 | +| `setConnectedCallback(cb)` | 设置连接成功回调 | +| `setDisconnectedCallback(cb)` | 设置断开回调 | +| `setAutoReconnect(enable)` | 设置自动重连 | +| `setTlsConfig(config)` | 设置 TLS 配置(必须在 initialize 之前调用) | +| `start()` | 开始连接 | +| `send(data, size)` | 发送数据(ByteStream 接口) | +| `bind(receiver)` | 绑定接收端(流水线模式) | +| `stop()` | 停止/断开连接 | +| `cleanup()` | 清理 | + +### UdpSocket — UDP 奆接字 + +| 方法 | 说明 | +|------|------| +| `UdpSocket(loop, broadcast)` | 构造,指定是否启用广播 | +| `bind(addr)` | 绑定地址 | +| `connect(addr)` | 连接目标地址 | +| `setRecvCallback(cb)` | 设置接收回调 `(data, size, from_addr)` | +| `send(data, size, to_addr)` | 发送数据到指定地址 | +| `send(data, size)` | 发送数据(需先 connect) | +| `enable()` / `disable()` | 启用/停用接收 | + +> **注意**:`bind()` 与 `connect()` 不能一起使用。 + +### Uart — 串口通信 + +| 方法 | 说明 | +|------|------| +| `Uart(loop)` | 构造 | +| `initialize(dev, mode_str)` | 初始化,dev 为设备路径,如 "/dev/ttyS0",mode_str 如 "115200 8n1" | +| `initialize(dev, mode)` | 初始化,使用 Mode 结构体 | +| `send(data, size)` | 发送数据(ByteStream 接口) | +| `bind(receiver)` | 绑定接收端 | +| `enable()` / `disable()` | 启用/停用 | + +Mode 结构体字段:`baudrate`(默认115200)、`data_bit`(k8bits)、`parity`(kNoEnd)、`stop_bit`(k1bits) + +### SockAddr — 地址封装 + +```cpp +//! 从字符串创建地址 +SockAddr addr = SockAddr::FromString("127.0.0.1:12345"); +SockAddr addr = SockAddr::FromString("0.0.0.0:80"); +``` + +### ByteStream — 字节流抽象 + +ByteStream 是一个统一的流接口,TcpClient、Uart 都实现了它。支持将两个 ByteStream 绑定形成数据流水线: + +```cpp +//! 将 UART 与 TCP Client 绑定,串口数据直接转发到 TCP +uart->bind(tcp_client); +tcp_client->bind(uart); +``` + +## 使用示例 + +### TCP Echo 服务端 + +> 完整示例见 `examples/network/tcp_server/tcp_echo/` + +```cpp +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::network; + +int main() { + LogOutput_Enable(); + + Loop* sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + TcpServer srv(sp_loop); + srv.initialize(SockAddr::FromString("127.0.0.1:12345"), 2); + + //! 收到数据后原样回发(Echo) + srv.setReceiveCallback( + [&srv] (const TcpServer::ConnToken &client, Buffer &buff) { + srv.send(client, buff.readableBegin(), buff.readableSize()); + buff.hasReadAll(); + }, 0 + ); + + srv.start(); + + //! 监听退出信号 + auto sp_sig = sp_loop->newSignalEvent(); + SetScopeExitAction([sp_sig] { delete sp_sig; }); + sp_sig->initialize(SIGINT, Event::Mode::kOneshot); + sp_sig->enable(); + sp_sig->setCallback([&] (int) { srv.stop(); sp_loop->exitLoop(); }); + + sp_loop->runLoop(); + srv.cleanup(); + + LogOutput_Disable(); + return 0; +} +``` + +### TCP 客户端 + +> 完整示例见 `examples/network/tcp_client/tcp_echo/` + +```cpp +TcpClient client(sp_loop); +client.initialize(SockAddr::FromString("127.0.0.1:12345")); + +client.setReceiveCallback( + [] (Buffer &buff) { + LogInfo("received: %.*s", (int)buff.readableSize(), (char*)buff.readableBegin()); + buff.hasReadAll(); + }, 0 +); + +client.start(); +client.send("hello", 5); +``` + +### UDP Ping-Pong + +> 完整示例见 `examples/network/udp_socket/ping_pong/` + +```cpp +UdpSocket udp(sp_loop); +udp.bind(SockAddr::FromString("0.0.0.0:12345")); + +udp.setRecvCallback( + [&udp] (const void *data, size_t size, const SockAddr &from) { + LogInfo("recv from %s", from.toString().c_str()); + udp.send("pong", 4, from); //! 回复给发送方 + } +); +udp.enable(); +``` + +### 串口通信 + +> 完整示例见 `examples/network/uart/uart_tool/` + +```cpp +Uart uart(sp_loop); +uart.initialize("/dev/ttyS0", "115200 8n1"); //! 115200波特率,8数据位,无校验,1停止位 + +uart.setReceiveCallback( + [] (Buffer &buff) { + LogInfo("uart recv: %.*s", (int)buff.readableSize(), (char*)buff.readableBegin()); + buff.hasReadAll(); + }, 0 +); + +uart.enable(); +uart.send("AT\r\n", 4); +``` + +### UART 转 TCP 桥接 + +> 完整示例见 `examples/network/uart/uart_to_uart/` + +```cpp +//! 双向绑定,串口数据自动转发到 TCP,反之亦然 +uart->bind(tcp_client); +tcp_client->bind(uart); +``` + +## TLS(SSL/TLS 加密通信) + +network 模块通过可选的 `network_tls` 模块支持 TLS 加密通信。TcpServer 和 TcpClient 均可在 `initialize()` 之前调用 `setTlsConfig()` 将普通 TCP 升级为 TLS。TLS 实现基于 OpenSSL,支持 TLS 1.2 及以上版本。 + +### TlsConfig — TLS 配置结构体 + +```cpp +#include + +struct TlsConfig { + //! CA 证书(用于验证对端) + std::string ca_file; //!< CA 证书文件路径(如 "/etc/ssl/certs/ca-bundle.crt") + std::string ca_path; //!< CA 证书目录路径(如 "/etc/ssl/certs/") + + bool verify_peer = true; //!< 是否验证对端证书 + int verify_depth = 1; //!< 证书链验证深度 + + //! 本端证书和私钥 + std::string cert_file; //!< 本端证书文件 + std::string key_file; //!< 本端私钥文件 + + //! Client SNI 配置 + std::string hostname; //!< 用于 SNI (Server Name Indication) 的主机名 + + bool isValid() const; //!< 检查配置是否有效 +}; +``` + +**关于 `ca_file` / `ca_path` 的要点:** + +- 它们是**可选的**,不需要必须设置。 +- 当 `verify_peer=true` 但未指定 `ca_file`/`ca_path` 时: + - **Client** 自动使用系统默认 CA 证书(调用 `SSL_CTX_set_default_verify_paths`),如 Linux 上的 `/etc/ssl/certs/`。这是最常见的使用场景。 + - **Server** 跳过客户端证书验证(适用于普通 TLS,非 mTLS)。 +- 需要指定时,只需其中一个即可——`ca_file` 或 `ca_path`,不需要两者都设。OpenSSL 接受单独指定。 +- `cert_file` 和 `key_file` 必须同时设置或同时为空,不能只设其中一个。 + +### TLS 工作原理 + +TLS 功能采用**弱符号插件**机制: + +1. `network` 模块定义了一个弱符号的 `CreateTlsFactory()`,返回 `nullptr`。 +2. `network_tls` 模块提供强符号实现,创建 `TcpTlsFactory`。 +3. 如果应用链接了 `libtbox_network_tls`,TLS 功能可用;否则 `setTlsConfig()` 返回 `false` 并打印警告。 + +### TLS Echo 服务端 + +> 完整示例见 `examples/network/tcp_server/tls_echo_server/` + +```cpp +TcpServer server(sp_loop); + +//! 设置 TLS 配置(必须在 initialize 之前调用) +TlsConfig tls_config; +tls_config.cert_file = "server.crt"; //! 服务端必须设置证书和密钥 +tls_config.key_file = "server.key"; +tls_config.verify_peer = false; //! 不验证客户端证书(非 mTLS) +if (!server.setTlsConfig(tls_config)) { + LogErr("TLS 不可用,需要链接 network_tls 模块"); + return; +} + +server.initialize(SockAddr::FromString("0.0.0.0:12345"), 2); +server.start(); +``` + +### TLS 客户端(使用自定义 CA 验证服务端) + +> 完整示例见 `examples/network/tcp_client/tls_echo_client/` + +```cpp +TcpClient client(sp_loop); + +//! 使用自定义 CA 证书验证服务端 +TlsConfig tls_config; +tls_config.ca_file = "server.crt"; //! 自定义 CA 证书 +tls_config.verify_peer = true; //! 验证服务端证书 +tls_config.hostname = "myserver"; //! SNI 主机名 +client.setTlsConfig(tls_config); + +client.initialize(SockAddr::FromString("127.0.0.1:12345")); +client.start(); +``` + +### TLS 客户端(使用系统默认 CA) + +```cpp +TcpClient client(sp_loop); + +//! 使用系统默认 CA 证书(如 /etc/ssl/certs/) +//! 不需要指定 ca_file 或 ca_path +TlsConfig tls_config; +tls_config.verify_peer = true; //! 用系统 CA 验证服务端证书 +tls_config.hostname = "example.com"; //! SNI 主机名 +client.setTlsConfig(tls_config); + +client.initialize(SockAddr::FromString("example.com:443")); +client.start(); +``` + +### TLS 客户端(跳过验证,类似 curl -k) + +```cpp +TcpClient client(sp_loop); + +//! 跳过服务端证书验证(不安全,仅用于测试) +TlsConfig tls_config; +tls_config.verify_peer = false; +tls_config.hostname = "127.0.0.1"; +client.setTlsConfig(tls_config); + +client.initialize(SockAddr::FromString("127.0.0.1:12345")); +client.start(); +``` + +### mTLS(双向 TLS — 双方都验证证书) + +```cpp +//! 服务端:验证客户端证书 +TlsConfig server_config; +server_config.cert_file = "server.crt"; +server_config.key_file = "server.key"; +server_config.ca_file = "client-ca.crt"; //! 签发客户端证书的 CA +server_config.verify_peer = true; //! 验证客户端证书 +server.setTlsConfig(server_config); + +//! 客户端:验证服务端并向服务端出示自己的证书 +TlsConfig client_config; +client_config.cert_file = "client.crt"; //! 向服务端出示客户端证书 +client_config.key_file = "client.key"; +client_config.ca_file = "server-ca.crt"; //! 签发服务端证书的 CA +client_config.verify_peer = true; +client_config.hostname = "myserver"; +client.setTlsConfig(client_config); +``` + +## 常见场景 + +1. **Echo 服务**:TcpServer 接收数据后原样回发 +2. **命令行客户端**:TcpClient + StdioStream 实现交互式 TCP 客户端 +3. **UART 桥接**:ByteStream bind 将串口数据转发到 TCP +4. **UDP 通信**:UdpSocket 实现广播或点对点 UDP +5. **自动重连客户端**:TcpClient + setAutoReconnect 实现断线自动重连 +6. **TLS 服务端**:TcpServer + setTlsConfig 实现加密通信 +7. **TLS 客户端(系统 CA)**:TcpClient + TlsConfig(verify_peer=true) 使用系统默认 CA 验证服务端 +8. **mTLS 双向认证**:双方均设置 verify_peer=true + ca_file + cert_file/key_file + +## 注意事项 + +1. **数据阈值 (threshold)**:`setReceiveCallback(cb, threshold)` 中 threshold 指定触发回调的最小数据量,0 表示收到任意数据即触发 +2. **Buffer 的 hasReadAll**:回调中使用 `buff.hasReadAll()` 标记已读完数据,否则下次回调会重复收到旧数据 +3. **TcpServer ConnToken**:通过 Token 标识客户端,Token 在连接断开后失效 +4. **TcpClient 断线重连**:`setAutoReconnect(true)` 启用后,断线会自动尝试重连 +5. **UDP bind vs connect**:bind 与 connect 不能一起使用,如需指定目标地址请在 send() 中传入 +6. **TLS setTlsConfig**:必须在 `initialize()` 之前调用,且需要链接 `network_tls` 模块 +7. **TLS ca_file/ca_path**:可选;verify_peer=true 但未指定时,客户端使用系统默认 CA;指定时只需其中一个 +8. **TLS cert_file/key_file**:必须同时设置或同时为空,不能只设其中一个 + +## 相关模块 + +- **event**:基于 FdEvent 实现 socket 事件监听 +- **http**:基于 TcpServer/TcpAcceptor 实现 HTTP 服务 +- **mqtt**:基于 TcpConnection 实现 MQTT 协议 +- **network_tls**:可选模块,基于 OpenSSL 为 TcpServer/TcpClient 提供 TLS 实现 +- **base**:提供 Buffer(即 util::Buffer)、ScopeExit 等基础设施 diff --git a/documents/modules/run.md b/documents/modules/run.md new file mode 100644 index 00000000..1f5f4551 --- /dev/null +++ b/documents/modules/run.md @@ -0,0 +1,172 @@ +# Module Runner (run) + +## What Is It? + +The run module is a dynamic loader that loads and runs business modules compiled as shared libraries (.so). Business modules only need to export the `RegisterApps` symbol, and the run program specifies which module shared libraries to load via the `-l` or `--load` parameter. + +## Why Do You Need It? + +The run module separates business modules from the startup framework. Business modules are independently compiled into .so files and dynamically loaded and combined through the run program. This allows: +- Different business modules can be independently compiled and deployed +- No need to write a separate main function for each business scenario +- Flexibly combine multiple modules to run together + +## Header File + +The run module itself does not need a header file; it is a startup program implemented in `main.cpp`. Business modules use the header files from the main module. + +```cpp +//! Business modules need: +#include +#include +#include +``` + +## Core Mechanism + +### Running Method + +```bash +# Load a single module +./tbox_run -l echo_server.so + +# Load multiple modules +./tbox_run -l echo_server.so -l nc_client.so + +# Specify module path +./tbox_run --load /path/to/module.so + +# Show help +./tbox_run -h + +# Show version +./tbox_run -v +``` + +### Business Module Export Requirements + +Business .so files need to export the following symbols (consistent with the RegisterApps mechanism of the main module): + +```cpp +//! Must-export symbol +extern "C" +void RegisterApps(tbox::main::Module &apps, tbox::main::Context &ctx) { + apps.add(new MyModule(ctx)); +} + +//! Optional export symbols +std::string GetAppDescribe() { return "echo server module"; } +std::string GetAppBuildTime() { return __DATE__ " " __TIME__; } +void GetAppVersion(int &major, int &minor, int &rev, int &build) { + major = 0; minor = 0; rev = 1; build = 0; +} +``` + +> **Important**: RegisterApps must be exported using `extern "C"`, otherwise dlsym will not be able to find the symbol. + +### Workflow + +```mermaid +flowchart TD + A[Parse -l/--load parameters] --> B[dlopen load .so] + B --> C[dlsym find RegisterApps] + C --> D[Call RegisterApps to register modules] + D --> E[tbox::main::Main run framework] + E --> F[dlclose on program exit] +``` + +## Usage Example + +### Writing a Business Module + +> Complete example at `examples/run/echo_server/` + +```cpp +// echo_server.cpp +#include "echo_server.h" +#include + +namespace echo_server { + +App::App(Context &ctx) : + Module("echo_server", ctx), + server_(new TcpServer(ctx.loop())) +{ } + +App::~App() { CHECK_DELETE_RESET_OBJ(server_); } + +void App::onFillDefaultConfig(Json &cfg) const { + cfg["bind"] = "127.0.0.1:12345"; +} + +bool App::onInit(const tbox::Json &cfg) { + auto js_bind = cfg["bind"]; + if (!js_bind.is_string()) return false; + if (!server_->initialize(SockAddr::FromString(js_bind.get()), 2)) + return false; + server_->setReceiveCallback( + [this] (const TcpServer::ConnToken &client, Buffer &buff) { + server_->send(client, buff.readableBegin(), buff.readableSize()); + buff.hasReadAll(); + }, 0 + ); + return true; +} + +bool App::onStart() { return server_->start(); } +void App::onStop() { server_->stop(); } +void App::onCleanup() { server_->cleanup(); } + +} + +//! Export RegisterApps symbol +extern "C" +void RegisterApps(tbox::main::Module &apps, tbox::main::Context &ctx) { + apps.add(new echo_server::App(ctx)); +} +``` + +### Compiling as a Shared Library + +```makefile +# Makefile example +CXXFLAGS += -fPIC -shared # Compile as shared library +LDFLAGS += -ltbox_network -ltbox_eventx -ltbox_event -ltbox_util -ltbox_base +``` + +### Running + +```bash +# Compile +make + +# Run +./tbox_run -l echo_server.so +``` + +### Other Examples + +- `examples/run/nc_client/` — Command-line TCP client module +- `examples/run/timer_event/` — Timer event module + +## Common Scenarios + +1. **Modular Deployment**: Different business modules are independently compiled into .so files and loaded on demand +2. **Dynamic Extension**: Add new modules at runtime via `-l` parameter without recompiling the main program +3. **Independent Development**: Each module team develops independently, without depending on the main program code +4. **Test Execution**: Load test modules to verify functionality + +## Notes + +1. **extern "C" Export**: RegisterApps must be exported using `extern "C"`, otherwise dlsym cannot find the C++ mangled symbol name +2. **.so Compilation Options**: Must add `-fPIC -shared` compilation options +3. **Library Dependencies**: The .so module needs to link the tbox libraries it depends on (network/eventx/event/util/base etc.) +4. **Load Failure Handling**: The run program prints a warning on load failure but does not terminate; it continues to attempt loading other modules +5. **Module Unloading**: All loaded .so files are automatically dlclosed when the program exits + +## Related Modules + +- **main**: run uses the Main() function of the main module to run the framework; business modules use the Module base class +- **util**: Uses ArgumentParser to parse -l/--load parameters +- **network**: Common business modules use TcpServer/TcpClient and other network components +- **base**: Provides Log, Json and other infrastructure diff --git a/documents/modules/run_CN.md b/documents/modules/run_CN.md new file mode 100644 index 00000000..9c4a59ab --- /dev/null +++ b/documents/modules/run_CN.md @@ -0,0 +1,172 @@ +# 模块运行器 (run) + +## 是什么? + +run 模块是一个动态加载器,它将编译为动态库(.so)的业务模块加载并运行。业务模块只需导出 `RegisterApps` 符号,run 程序通过 `-l` 或 `--load` 参数指定要加载的模块动态库。 + +## 为什么需要它? + +run 模块让业务模块与启动框架分离。业务模块独立编译为 .so 文件,通过 run 程序动态加载组合。这样: +- 不同业务模块可以独立编译和部署 +- 不需要为每个业务场景编写独立的 main 函数 +- 可以灵活组合多个模块运行 + +## 头文件 + +run 模块本身不需要头文件,它是 `main.cpp` 实现的启动程序。业务模块使用 main 模块的头文件。 + +```cpp +//! 业务模块需要: +#include +#include +#include +``` + +## 核心机制 + +### 运行方式 + +```bash +# 加载单个模块 +./tbox_run -l echo_server.so + +# 加载多个模块 +./tbox_run -l echo_server.so -l nc_client.so + +# 指定模块路径 +./tbox_run --load /path/to/module.so + +# 查看帮助 +./tbox_run -h + +# 查看版本 +./tbox_run -v +``` + +### 业务模块导出要求 + +业务 .so 文件需要导出以下符号(与 main 模块的 RegisterApps 机制一致): + +```cpp +//! 必须导出的符号 +extern "C" +void RegisterApps(tbox::main::Module &apps, tbox::main::Context &ctx) { + apps.add(new MyModule(ctx)); +} + +//!可选导出的符号 +std::string GetAppDescribe() { return "echo server module"; } +std::string GetAppBuildTime() { return __DATE__ " " __TIME__; } +void GetAppVersion(int &major, int &minor, int &rev, int &build) { + major = 0; minor = 0; rev = 1; build = 0; +} +``` + +> **重要**:RegisterApps 必须使用 `extern "C"` 导出,否则 dlsym 无法找到符号。 + +### 工作流程 + +```mermaid +flowchart TD + A[解析 -l/--load 参数] --> B[dlopen 加载 .so] + B --> C[dlsym 查找 RegisterApps] + C --> D[调用 RegisterApps 注册模块] + D --> E[tbox::main::Main 运行框架] + E --> F[程序退出时 dlclose] +``` + +## 使用示例 + +### 编写业务模块 + +> 完整示例见 `examples/run/echo_server/` + +```cpp +// echo_server.cpp +#include "echo_server.h" +#include + +namespace echo_server { + +App::App(Context &ctx) : + Module("echo_server", ctx), + server_(new TcpServer(ctx.loop())) +{ } + +App::~App() { CHECK_DELETE_RESET_OBJ(server_); } + +void App::onFillDefaultConfig(Json &cfg) const { + cfg["bind"] = "127.0.0.1:12345"; +} + +bool App::onInit(const tbox::Json &cfg) { + auto js_bind = cfg["bind"]; + if (!js_bind.is_string()) return false; + if (!server_->initialize(SockAddr::FromString(js_bind.get()), 2)) + return false; + server_->setReceiveCallback( + [this] (const TcpServer::ConnToken &client, Buffer &buff) { + server_->send(client, buff.readableBegin(), buff.readableSize()); + buff.hasReadAll(); + }, 0 + ); + return true; +} + +bool App::onStart() { return server_->start(); } +void App::onStop() { server_->stop(); } +void App::onCleanup() { server_->cleanup(); } + +} + +//! 导出 RegisterApps 符号 +extern "C" +void RegisterApps(tbox::main::Module &apps, tbox::main::Context &ctx) { + apps.add(new echo_server::App(ctx)); +} +``` + +### 编译为动态库 + +```makefile +# Makefile 示例 +CXXFLAGS += -fPIC -shared # 编译为共享库 +LDFLAGS += -ltbox_network -ltbox_eventx -ltbox_event -ltbox_util -ltbox_base +``` + +### 运行 + +```bash +# 编译 +make + +# 运行 +./tbox_run -l echo_server.so +``` + +### 其他示例 + +- `examples/run/nc_client/` — 命令行 TCP 客户端模块 +- `examples/run/timer_event/` — 定时器事件模块 + +## 常见场景 + +1. **模块化部署**:不同业务模块独立编译为 .so,按需加载组合 +2. **动态扩展**:运行时通过 `-l` 参数添加新模块,无需重新编译主程序 +3. **独立开发**:各模块团队独立开发,互不依赖主程序代码 +4. **测试运行**:加载测试模块验证功能 + +## 注意事项 + +1. **extern "C" 导出**:RegisterApps 必须使用 `extern "C"` 导出,否则 dlsym 找不到 C++ 编译后的符号名 +2. **.so 编译选项**:需添加 `-fPIC -shared` 编译选项 +3. **库依赖**:.so 模块需要链接它所依赖的 tbox 库(network/eventx/event/util/base 等) +4. **加载失败处理**:run 程序对加载失败打印警告但不终止,继续尝试加载其他模块 +5. **模块卸载**:程序退出时自动 dlclose 所有已加载的 .so + +## 相关模块 + +- **main**:run 使用 main 模块的 Main() 函数运行框架,业务模块使用 Module 基类 +- **util**:使用 ArgumentParser 解析 -l/--load 参数 +- **network**:常见业务模块使用 TcpServer/TcpClient 等网络组件 +- **base**:提供 Log、Json 等基础设施 diff --git a/documents/modules/terminal.md b/documents/modules/terminal.md new file mode 100644 index 00000000..93f303ff --- /dev/null +++ b/documents/modules/terminal.md @@ -0,0 +1,153 @@ +# Interactive Terminal Module (terminal) + +## What is it? + +The terminal module provides a shell-like interactive command terminal for running programs. Developers or operations personnel can log in via telnet and use commands to instruct the program to execute specified functions, enabling runtime debugging, parameter adjustment, status inspection, and more. + +## Why do you need it? + +Service programs at runtime typically only expose their execution process through log output, with no direct interaction. However, the following scenarios strongly require interactive capability: +- During development, you want the program to perform an action but haven't yet implemented a complete UI +- When the program behaves abnormally, you want to print key information to troubleshoot the issue +- Under unexpected conditions, ops personnel want to adjust runtime parameters without stopping the service + +The terminal design mimics Bash, with commands organized like a filesystem directory tree: + +``` +# tree +|-- dir1 +| |-- dir1_1 +| | |-- async* +| | `-- root(R) +| `-- dir1_2 +| `-- sync* +|-- dir2 +`-- sync* +``` + +It supports common commands such as cd, ls, tree, pwd, history, !n, !-n, !!; it also supports UP/DOWN/LEFT/RIGHT/DELETE/HOME/END key actions. + +## Header Files + +```cpp +#include //! Terminal main class +#include //! Node management interface +#include //! Interaction interface +#include //! Helper functions +#include //! Session management +#include //! Connection management +#include //! Type definitions +``` + +## Core Classes and Interfaces + +### Terminal + +Terminal inherits from TerminalInteract and TerminalNodes, providing both interaction capability and node management capability. + +| Method | Description | +|--------|-------------| +| `Terminal(loop)` | Constructor | +| `createFuncNode(func, help)` | Create a function node | +| `createDirNode(help)` | Create a directory node | +| `deleteNode(token)` | Delete a node | +| `rootNode()` | Get the root node | +| `findNode(path)` | Find a node by path | +| `mountNode(parent, child, name)` | Mount a child node onto a parent directory | +| `umountNode(parent, name)` | Unmount a child node | +| `setWelcomeText(text)` | Set welcome text | + +### Func Callback Type + +```cpp +using Func = std::function &args)>; +``` + +The function node callback receives a Session and an argument list. Through Session you can output text to the client. + +### Verified Telnet Clients + +| Client | Description | +|--------|-------------| +| Windows telnet | Built-in command | +| Linux telnet | Terminal command | +| Putty | telnet connection | +| XShell | telnet connection | +| Tabby | telnet profile (Input mode must be set to Normal) | + +## Usage Examples + +### Using Terminal in a main Module + +> See `examples/terminal/telnetd/` for a complete example + +```cpp +class App : public tbox::main::Module { + public: + App(Context &ctx) : Module("app", ctx) { } + + bool onInit(const Json &cfg) override { + auto term = ctx.terminal(); + + //! Create a function node + auto func_node = term->createFuncNode( + [](const terminal::Session &s, const std::vector &args) { + s.send("Hello from terminal!\r\n"); + }, "say hello" + ); + + //! Create a directory node + auto dir_node = term->createDirNode("demo commands"); + + //! Mount nodes into the directory tree + term->mountNode(term->rootNode(), dir_node, "demo"); + term->mountNode(dir_node, func_node, "hello"); + + //! Set welcome text + term->setWelcomeText("Welcome to my app terminal!\r\n"); + + return true; + } +}; +``` + +### Starting a Telnet Service via Terminal + +> See `examples/terminal/telnetd/` for a complete example + +terminal is typically used together with TcpAcceptor to provide a telnet service: + +```cpp +//! In the main module, Terminal is automatically created by the framework +//! Just create a TcpAcceptor to listen on a port and handle connections +``` + +### Stdio Terminal + +> See `examples/terminal/stdio/` for a complete example + +You can also interact with terminal via standard input/output, without needing telnet: + +```cpp +//! Use StdioStream to connect stdin/stdout to Terminal +``` + +## Common Scenarios + +1. **Runtime debugging**: Create function nodes to print key variable values +2. **Parameter adjustment**: Create function nodes to modify runtime parameters (e.g., log level, timer interval) +3. **Status inspection**: Create function nodes that return system status information +4. **Remote ops**: Connect remotely via telnet and adjust without downtime + +## Important Notes + +1. **Terminal is created by the main framework**: When used in a main module, obtain it via `ctx.terminal()`; no need to create it manually +2. **Node path naming**: Use meaningful English names for ease of command-line input +3. **Callback thread safety**: Terminal callbacks execute in the Loop thread, on the same thread as business logic +4. **Session in Func callbacks**: Use Session.send() to output text to the client; include `\r\n` for line breaks + +## Related Modules + +- **event**: Terminal runs on Loop +- **network**: Provides telnet service connections via TcpAcceptor +- **main**: The framework automatically creates the Terminal object diff --git a/documents/modules/terminal_CN.md b/documents/modules/terminal_CN.md new file mode 100644 index 00000000..a0264385 --- /dev/null +++ b/documents/modules/terminal_CN.md @@ -0,0 +1,153 @@ +# 交互终端模块 (terminal) + +## 是什么? + +terminal 模块提供与运行中程序类似 shell 的交互命令终端。开发或运维人员可通过 telnet 登陆,以命令的方式让程序执行指定函数,实现运行时调试、参数调整、状态查看等功能。 + +## 为什么需要它? + +服务程序运行时通常只能通过日志输出执行过程,无法直接交互。但以下场景非常需要交互能力: +- 开发过程中,想让程序执行某个动作但还没有实现完整的界面 +- 程序异常时,希望打印关键信息排查问题 +- 突发状态下,运维希望不停止服务就调整运行参数 + +terminal 的设计模仿 Bash,命令的组织类似文件系统目录树: + +``` +# tree +|-- dir1 +| |-- dir1_1 +| | |-- async* +| | `-- root(R) +| `-- dir1_2 +| `-- sync* +|-- dir2 +`-- sync* +``` + +支持 cd, ls, tree, pwd, history, !n, !-n, !! 等常用命令;还支持 UP/DOWN/LEFT/RIGHT/DELETE/HOME/END 按键动作。 + +## 头文件 + +```cpp +#include //! 终端主类 +#include //! 结点管理接口 +#include //! 交互接口 +#include //! 辅助函数 +#include //! 会话管理 +#include //! 连接管理 +#include //! 类型定义 +``` + +## 核心类与接口 + +### Terminal + +Terminal 继承了 TerminalInteract 和 TerminalNodes,同时提供交互能力和结点管理能力。 + +| 方法 | 说明 | +|------|------| +| `Terminal(loop)` | 构造 | +| `createFuncNode(func, help)` | 创建函数结点 | +| `createDirNode(help)` | 创建目录结点 | +| `deleteNode(token)` | 删除结点 | +| `rootNode()` | 获取根结点 | +| `findNode(path)` | 根据路径查找结点 | +| `mountNode(parent, child, name)` | 将子结点挂载到父目录 | +| `umountNode(parent, name)` | 卸载子结点 | +| `setWelcomeText(text)` | 设置欢迎文字 | + +### Func 回调类型 + +```cpp +using Func = std::function &args)>; +``` + +函数结点的回调接收 Session 和参数列表。通过 Session 可向客户端输出文字。 + +### 已验证的 Telnet 客户端 + +| 客户端 | 说明 | +|--------|------| +| Windows telnet | 自带命令 | +| Linux telnet | 终端命令 | +| Putty | telnet 连接 | +| XShell | telnet 连接 | +| Tabby | telnet profile(Input mode 要设置为 Normal) | + +## 使用示例 + +### 在 main 模块中使用 Terminal + +> 完整示例见 `examples/terminal/telnetd/` + +```cpp +class App : public tbox::main::Module { + public: + App(Context &ctx) : Module("app", ctx) { } + + bool onInit(const Json &cfg) override { + auto term = ctx.terminal(); + + //! 创建函数结点 + auto func_node = term->createFuncNode( + [](const terminal::Session &s, const std::vector &args) { + s.send("Hello from terminal!\r\n"); + }, "say hello" + ); + + //! 创建目录结点 + auto dir_node = term->createDirNode("demo commands"); + + //! 将结点挂载到目录树 + term->mountNode(term->rootNode(), dir_node, "demo"); + term->mountNode(dir_node, func_node, "hello"); + + //! 设置欢迎文字 + term->setWelcomeText("Welcome to my app terminal!\r\n"); + + return true; + } +}; +``` + +### 通过 Terminal 启动 Telnet 服务 + +> 完整示例见 `examples/terminal/telnetd/` + +terminal 通常配合 TcpAcceptor 提供 telnet 服务: + +```cpp +//! 在 main 模块中,Terminal 已由框架自动创建 +//! 只需创建 TcpAcceptor 监听端口,并处理连接 +``` + +### Stdio 终端 + +> 完整示例见 `examples/terminal/stdio/` + +也可通过标准输入输出与 terminal 交互,无需 telnet: + +```cpp +//! 使用 StdioStream 将 stdin/stdout 连接到 Terminal +``` + +## 常见场景 + +1. **运行时调试**:创建函数结点打印关键变量值 +2. **参数调整**:创建函数结点修改运行参数(如日志级别、定时器间隔) +3. **状态查看**:创建函数结点返回系统状态信息 +4. **远程运维**:通过 telnet 远程连接,无需停机调整 + +## 注意事项 + +1. **Terminal 由 main 框架创建**:在 main 模块中使用时,通过 `ctx.terminal()` 获取,无需手动创建 +2. **结点路径命名**:建议使用有意义的英文名称,便于命令行输入 +3. **回调线程安全**:Terminal 回调在 Loop 线程中执行,与业务逻辑同线程 +4. **Func 回调的 Session**:通过 Session.send() 向客户端输出文字,需包含 `\r\n` 换行 + +## 相关模块 + +- **event**:Terminal 基于 Loop 运行 +- **network**:通过 TcpAcceptor 提供 telnet 服务连接 +- **main**:框架自动创建 Terminal 对象 diff --git a/documents/modules/trace.md b/documents/modules/trace.md new file mode 100644 index 00000000..b9e9b0ed --- /dev/null +++ b/documents/modules/trace.md @@ -0,0 +1,114 @@ +# Performance Trace Module (trace) + +## What is it? + +The trace module provides lightweight function-level performance tracing functionality, recording the execution time of functions/events and writing trace data to binary files for subsequent visual analysis. + +## Why do you need it? + +In service applications, understanding which functions execute the slowest and how time is distributed is crucial for performance optimization. The trace module automatically records timestamps and durations at key function entry/exit points without modifying business code, enabling you to obtain detailed performance data. + +![trace-view](../images/0011-trace-view.png) + +## Header Files + +```cpp +#include +``` + +## Core Classes and Interfaces + +### Sink — Trace Data Receiver + +Sink follows the singleton pattern, with a single global instance. + +| Method | Description | +|------|------| +| `Sink::GetInstance()` | Get the singleton instance | +| `setPathPrefix(prefix)` | Set path prefix, e.g. "/data/my_proc", automatically creates a subdirectory with timestamp and PID | +| `setFileSyncEnable(enable)` | Set whether to sync data to disk in real time | +| `setRecordFileMaxSize(size)` | Set the maximum record file size | +| `setFilterStrategy(strategy)` | Set filter strategy (kPermit/kReject) | +| `setFilterExemptSet(exempt_set)` | Set the exempt set | +| `enable()` | Enable tracing | +| `disable()` | Disable tracing | +| `isEnabled()` | Check if tracing is enabled | +| `getDirPath()` | Get the directory path | +| `commitRecord(name, module, line, end_ts, duration)` | Commit a trace record | + +### Directory Structure + +After setting the path prefix, trace automatically creates the following directory structure: + +``` +/data/my_proc.20240525_123300.7723/ +├── names.txt # Function name list (index-encoded) +├── modules.txt # Module name list (index-encoded) +├── threads.txt # Thread name list (index-encoded) +└── records/ # Record file directory + └── 20240530_041046.bin # Binary trace record file +``` + +## Usage Examples + +### Basic Tracing + +> Full example available in `examples/trace/01_demo/` + +```cpp +#include + +//! Enable tracing +auto &sink = tbox::trace::Sink::GetInstance(); +sink.setPathPrefix("/data/my_app_trace"); +sink.enable(); + +//! Commit trace records in key functions +void myFunction() { + uint64_t start_us = /* get start timestamp */; + //! ... execute business logic ... + uint64_t end_us = /* get end timestamp */; + sink.commitRecord("myFunction", "my_module", 0, end_us, end_us - start_us); +} +``` + +### Multi-thread Tracing + +> Full example available in `examples/trace/02_multi_threads/` + +```cpp +//! trace supports multi-thread record commits, thread IDs are automatically encoded +//! Backend thread writes to file asynchronously, does not block business threads +``` + +### Filter Strategy + +```cpp +//! Default strategy: record all modules +sink.setFilterStrategy(tbox::trace::Sink::FilterStrategy::kPermit); + +//! Reverse strategy: reject specific modules only +sink.setFilterStrategy(tbox::trace::Sink::FilterStrategy::kReject); +sink.setFilterExemptSet({"network", "http"}); //! Exempt these modules +//! Effect: only record trace data for network and http modules +``` + +## Common Scenarios + +1. **Performance Analysis**: Trace execution time of key functions, identify slow functions +2. **Bottleneck Discovery**: Analyze time distribution across modules, find performance bottlenecks +3. **Runtime Monitoring**: Continuously trace program execution, generate a complete execution timeline +4. **Selective Tracing**: Use filter strategies to trace only the modules you care about + +## Important Notes + +1. **Singleton Pattern**: Sink is obtained via GetInstance(), globally unique, cannot be manually created +2. **Binary File Format**: Record files are in binary format and require a dedicated viewer tool (such as trace_view) for analysis +3. **Path Prefix**: The prefix set by setPathPrefix() automatically gets a timestamp and PID suffix appended +4. **Real-time Disk Sync**: By default, data is not synced to disk in real time (performance priority); enable via setFileSyncEnable(true) +5. **Filter Strategy Combinations**: kPermit + exempt_set = record all by default, only reject modules in exempt_set; kReject + exempt_set = reject all by default, only record modules in exempt_set + +## Related Modules + +- **util**: Uses AsyncPipe to implement backend thread writing +- **base**: Provides Cabinet, Log and other infrastructure diff --git a/documents/modules/trace_CN.md b/documents/modules/trace_CN.md new file mode 100644 index 00000000..ffe1a4c3 --- /dev/null +++ b/documents/modules/trace_CN.md @@ -0,0 +1,114 @@ +# 性能追踪模块 (trace) + +## 是什么? + +trace 模块提供了轻量级函数级性能追踪功能,记录函数/事件的执行时间,将追踪数据写入二进制文件供后续可视化分析。 + +## 为什么需要它? + +在服务程序运行中,了解哪些函数执行最慢、耗时分布如何,对于性能优化至关重要。trace 模块通过在关键函数入口/出口自动记录时间戳和耗时,无需修改业务代码,就能获取详细的性能数据。 + +![trace-view](../images/0011-trace-view.png) + +## 头文件 + +```cpp +#include +``` + +## 核心类与接口 + +### Sink — 追踪数据接收器 + +Sink 是单例模式,全局唯一实例。 + +| 方法 | 说明 | +|------|------| +| `Sink::GetInstance()` | 获取单例实例 | +| `setPathPrefix(prefix)` | 设置路径前缀,如 "/data/my_proc",自动创建带时间戳和PID的子目录 | +| `setFileSyncEnable(enable)` | 设置是否实时落盘 | +| `setRecordFileMaxSize(size)` | 设置记录文件大小上限 | +| `setFilterStrategy(strategy)` | 设置过滤策略(kPermit/kReject) | +| `setFilterExemptSet(exempt_set)` | 设置豁免集合 | +| `enable()` | 启用追踪 | +| `disable()` | 停用追踪 | +| `isEnabled()` | 是否已启用 | +| `getDirPath()` | 获取目录路径 | +| `commitRecord(name, module, line, end_ts, duration)` | 提交一条追踪记录 | + +### 目录结构 + +设置路径前缀后,trace 自动创建如下目录结构: + +``` +/data/my_proc.20240525_123300.7723/ +├── names.txt # 函数名列表(索引编码) +├── modules.txt # 模块名列表(索引编码) +├── threads.txt # 线程名列表(索引编码) +└── records/ # 记录文件目录 + └── 20240530_041046.bin # 二进制追踪记录文件 +``` + +## 使用示例 + +### 基本追踪 + +> 完整示例见 `examples/trace/01_demo/` + +```cpp +#include + +//! 启用追踪 +auto &sink = tbox::trace::Sink::GetInstance(); +sink.setPathPrefix("/data/my_app_trace"); +sink.enable(); + +//! 在关键函数中提交追踪记录 +void myFunction() { + uint64_t start_us = /* 获取开始时间戳 */; + //! ... 执行业务逻辑 ... + uint64_t end_us = /* 获取结束时间戳 */; + sink.commitRecord("myFunction", "my_module", 0, end_us, end_us - start_us); +} +``` + +### 多线程追踪 + +> 完整示例见 `examples/trace/02_multi_threads/` + +```cpp +//! trace 支持多线程提交记录,线程号自动编码 +//! 后端线程异步写入文件,不阻塞业务线程 +``` + +### 过滤策略 + +```cpp +//! 默认策略:记录所有模块 +sink.setFilterStrategy(tbox::trace::Sink::FilterStrategy::kPermit); + +//! 反向策略:只拒绝特定模块 +sink.setFilterStrategy(tbox::trace::Sink::FilterStrategy::kReject); +sink.setFilterExemptSet({"network", "http"}); //! 豁免这些模块 +//! 效果:只记录 network 和 http 模块的追踪数据 +``` + +## 常见场景 + +1. **性能分析**:追踪关键函数的执行耗时,定位慢函数 +2. **瓶颈发现**:统计各模块的耗时分布,找到性能瓶颈 +3. **运行时监控**:持续追踪程序运行过程,生成完整的执行时间线 +4. **选择性追踪**:使用过滤策略仅追踪关心的模块 + +## 注意事项 + +1. **单例模式**:Sink 使用 GetInstance() 获取,全局唯一,不可手动创建 +2. **二进制文件格式**:记录文件为二进制格式,需要专门的查看工具(如 trace_view)分析 +3. **路径前缀**:setPathPrefix() 设置的前缀会自动添加时间戳和PID后缀 +4. **实时落盘**:默认不实时落盘(性能优先),可通过 setFileSyncEnable(true) 启用 +5. **过滤策略组合**:kPermit + exempt_set = 默认记录所有,仅拒绝 exempt_set 中的模块;kReject + exempt_set = 默认拒绝所有,仅记录 exempt_set 中的模块 + +## 相关模块 + +- **util**:使用 AsyncPipe 实现后端线程写入 +- **base**:提供 Cabinet、Log 等基础设施 diff --git a/documents/modules/util.md b/documents/modules/util.md new file mode 100644 index 00000000..89b3330a --- /dev/null +++ b/documents/modules/util.md @@ -0,0 +1,235 @@ +# Utility Module (util) + +## What is it? + +The util module provides 17+ general-purpose utility components, covering data processing, JSON parsing, serialization, encoding/decoding, process management, argument parsing, and more. These utilities are independent and lightweight, and can be used as needed. + +## Why do you need it? + +In C++ project development, you often need general-purpose utilities that are not in the standard library: binary data buffering, JSON configuration file parsing, command-line argument parsing, data serialization and deserialization, UUID generation, CRC checksums, etc. The util module encapsulates these commonly used utilities in a unified way, avoiding repetitive implementation in every project. + +## Header Files + +```cpp +// Data processing +#include //! Binary buffer + +// JSON utilities +#include //! JSON parsing and field extraction +#include //! JSON deep loading (supports __include__) + +// Serialization +#include //! Serialization/deserialization (big/little endian) + +// Encoding/decoding +#include //! Base64 encoding/decoding +#include //! CRC checksum +#include //! Checksum + +// Variables and arguments +#include //! Variable management object +#include //! Command-line argument parsing + +// Process management +#include //! PID file +#include //! Async pipe +#include //! Execute command +#include //! Filesystem utilities +#include //! fd utilities +#include //! Split command line + +// Others +#include //! UUID generation +#include //! Timestamp +#include //! String utilities +#include //! String conversion +#include //! Scalable integer +``` + +## Core Components + +### Buffer -- Binary Buffer + +Buffer is a read-write separated buffer, supporting append for writing and fetch for reading: + +``` + buffer_ptr_ buffer_size_ + | | + v V + +----+----------------+----------------+ + | | readable bytes | writable bytes | + +----+----------------+----------------+ + ^ ^ + | | + read_index_ write_index_ +``` + +| Method | Description | +|--------|-------------| +| `append(data, size)` | Write data, returns actual written size | +| `fetch(buff, size)` | Read data, returns actual read size | +| `readableSize()` | Readable data size | +| `writableSize()` | Writable space size | +| `readableBegin()` | Readable area start address | +| `writableBegin()` | Writable area start address | +| `hasRead(size)` | Mark size bytes as read | +| `hasReadAll()` | Mark all data as read | +| `hasWritten(size)` | Mark size bytes as written | +| `ensureWritableSize(size)` | Ensure writable space | +| `reset()` | Reset the buffer | +| `shrink()` | Reduce excess capacity | + +> **Note**: Buffer is not thread-safe; external locking is required for multi-threaded use. + +### Json -- JSON Parsing and Field Extraction + +Provides safe JSON field extraction functions: + +```cpp +//! Extract field values from a Json object +bool Get(const Json &js, int &value); +bool Get(const Json &js, std::string &value); +bool GetField(const Json &js, "field_name", int &value); +bool GetField(const Json &js, "field_name", std::string &value); + +//! Check field types +bool HasObjectField(const Json &js, "field"); +bool HasArrayField(const Json &js, "field"); +bool HasStringField(const Json &js, "field"); +bool HasIntegerField(const Json &js, "field"); + +//! Parse JSON file +Json js = json::Load("config.json"); //! Exception-throwing version +bool ok = json::Load("config.json", js); //! Non-throwing version +``` + +### DeepLoader -- JSON Deep Loading + +Supports using `__include__` in JSON files to import other JSON files: + +```json +// main.json +{ + "main.a": 1, + "__include__": ["sub/sub1.json => sub1", "common.json"] +} +``` + +```cpp +Json js = json::LoadDeeply("main.json"); //! Automatically loads referenced files and merges them +``` + +> For a complete example, see `examples/util/json_deep_loader/` + +### ArgumentParser -- Command-line Argument Parsing + +Supports short arguments (-h) and long arguments (--help, --level=6): + +```cpp +bool print_help = false; +int level = 0; + +tbox::util::ArgumentParser parser( + [&](char short_opt, const std::string &long_opt, + ArgumentParser::OptionValue &opt_value) { + if (short_opt == 'h' || long_opt == "help") { + print_help = true; + } else if (short_opt == 'l' || long_opt == "level") { + level = std::stoi(opt_value.get()); + } else { + cerr << "invalid option" << endl; + return false; + } + return true; + } +); + +if (!parser.parse(argc, argv)) + return 0; +``` + +### Serializer / Deserializer -- Serialization + +Supports big/little endian data serialization and deserialization, providing stream-style operations: + +```cpp +std::vector block; +Serializer s(block, Endian::kBig); + +s << uint16_t(0x1234) << int32_t(42) << float(3.14); + +Deserializer d(block.data(), block.size(), Endian::kBig); +uint16_t v1; int32_t v2; float v3; +d >> v1 >> v2 >> v3; +``` + +### Variables -- Variable Management + +Variable management object, supporting hierarchical inheritance (parent lookup): + +```cpp +Variables vars; +vars.define("name", Json("default")); +vars.set("name", Json("new_value")); + +Json value; +vars.get("name", value); //! Look up locally +vars.get("name", value, false); //! Continue lookup from parent + +//! Set parent variable table +vars.setParent(&parent_vars); +``` + +### UUID -- UUID Generation + +```cpp +std::string id = util::UUID::Generate(); //! Generate v4 UUID +``` + +### Base64 -- Base64 Encoding/Decoding + +```cpp +std::string encoded = util::Base64::Encode(data, size); +std::vector decoded = util::Base64::Decode(encoded); +``` + +### CRC -- CRC Checksum + +```cpp +uint16_t crc16 = util::CRC::Calc16(data, size); +uint32_t crc32 = util::CRC::Calc32(data, size); +``` + +### PidFile -- PID File + +Prevents duplicate startup of the same program: + +```cpp +util::PidFile pid_file; +pid_file.setPathPrefix("/var/run/myapp"); //! Automatically creates PID file +pid_file.enable(); +``` + +## Common Scenarios + +1. **Configuration file loading**: Use json::Load or LoadDeeply to read JSON configuration +2. **Command-line arguments**: Use ArgumentParser to parse -h/--help/-l/--level etc. +3. **Binary protocol**: Use Buffer to buffer sent/received data, Serializer to serialize protocol fields +4. **Checksum and encoding**: CRC for data integrity, Base64 for encoding binary data +5. **Prevent duplicate startup**: Use PidFile to ensure only one process instance + +## Important Notes + +1. **Buffer is not thread-safe**: External locking is required for multi-threaded use +2. **Json::Load exceptions**: Load() throws OpenFileError or ParseJsonFileError; LoadDeeply also has DuplicateIncludeError +3. **Serializer byte order**: Pay attention to big/little endian alignment; default is big endian (commonly used for network protocols) +4. **ArgumentParser.get()**: After calling opt_value.get(), the parser will not mistake the value for an argument item +5. **DeepLoader duplicate prevention**: The same file cannot be included twice; it will throw DuplicateIncludeError + +## Related Modules + +- **base**: Provides Json (nlohmann/json) forward declarations and definitions +- **event**: AsyncPipe runs based on Loop +- **flow**: Action uses Variables to store variables +- **main**: Module uses Variables and json to load configuration +- **network**: Uses Buffer as the received data buffer diff --git a/documents/modules/util_CN.md b/documents/modules/util_CN.md new file mode 100644 index 00000000..59dd69df --- /dev/null +++ b/documents/modules/util_CN.md @@ -0,0 +1,235 @@ +# 工具集模块 (util) + +## 是什么? + +util 模块提供了 17+ 个通用工具组件,涵盖数据处理、JSON 解析、序列化、编码解码、进程管理、参数解析等功能。这些工具独立且轻量,可按需使用。 + +## 为什么需要它? + +在 C++ 项目开发中,经常需要一些通用但不在标准库中的工具:二进制数据缓冲、JSON 配置文件解析、命令行参数解析、数据序列化与反序列化、UUID 生成、CRC 校验等。util 模块将这些常用工具统一封装,避免每个项目重复实现。 + +## 头文件 + +```cpp +// 数据处理 +#include //! 二进制缓冲区 + +// JSON 工具 +#include //! JSON 解析与字段提取 +#include //! JSON 深度加载(支持 __include__) + +// 序列化 +#include //! 序列化/反序列化(大端/小端) + +// 编码解码 +#include //! Base64 编解码 +#include //! CRC 校验 +#include //! 校验和 + +// 变量与参数 +#include //! 变量管理对象 +#include //! 命令行参数解析 + +// 进程管理 +#include //! PID 文件 +#include //! 异步管道 +#include //! 执行命令 +#include //! 文件系统工具 +#include //! fd 工具 +#include //! 分割命令行 + +// 其他 +#include //! UUID 生成 +#include //! 时间戳 +#include //! 字串工具 +#include //! 字串转换 +#include //! 可缩放整数 +``` + +## 核心组件 + +### Buffer — 二进制缓冲区 + +Buffer 是一个读写分离的缓冲区,支持 append 写入和 fetch 读取: + +``` + buffer_ptr_ buffer_size_ + | | + v V + +----+----------------+----------------+ + | | readable bytes | writable bytes | + +----+----------------+----------------+ + ^ ^ + | | + read_index_ write_index_ +``` + +| 方法 | 说明 | +|------|------| +| `append(data, size)` | 写入数据,返回实际写入大小 | +| `fetch(buff, size)` | 读取数据,返回实际读取大小 | +| `readableSize()` | 可读数据大小 | +| `writableSize()` | 可写空间大小 | +| `readableBegin()` | 可读区首地址 | +| `writableBegin()` | 可写区首地址 | +| `hasRead(size)` | 标记已读 size 字节 | +| `hasReadAll()` | 标记已读全部数据 | +| `hasWritten(size)` | 标记已写 size 字节 | +| `ensureWritableSize(size)` | 保障可写空间 | +| `reset()` | 重置缓冲区 | +| `shrink()` | 缩减多余容量 | + +> **注意**:Buffer 不是线程安全的,多线程使用需在外部加锁。 + +### Json — JSON 解析与字段提取 + +提供安全的 JSON 字段提取函数: + +```cpp +//! 从 Json 对象中提取字段值 +bool Get(const Json &js, int &value); +bool Get(const Json &js, std::string &value); +bool GetField(const Json &js, "field_name", int &value); +bool GetField(const Json &js, "field_name", std::string &value); + +//! 检查字段类型 +bool HasObjectField(const Json &js, "field"); +bool HasArrayField(const Json &js, "field"); +bool HasStringField(const Json &js, "field"); +bool HasIntegerField(const Json &js, "field"); + +//! 解析 JSON 文件 +Json js = json::Load("config.json"); //! 抛异常版本 +bool ok = json::Load("config.json", js); //! 不抛异常版本 +``` + +### DeepLoader — JSON 深度加载 + +支持在 JSON 文件中使用 `__include__` 导入其他 JSON 文件: + +```json +// main.json +{ + "main.a": 1, + "__include__": ["sub/sub1.json => sub1", "common.json"] +} +``` + +```cpp +Json js = json::LoadDeeply("main.json"); //! 自动加载引用的文件并合并 +``` + +> 完整示例见 `examples/util/json_deep_loader/` + +### ArgumentParser — 命令行参数解析 + +支持短参数(-h)和长参数(--help、--level=6): + +```cpp +bool print_help = false; +int level = 0; + +tbox::util::ArgumentParser parser( + [&](char short_opt, const std::string &long_opt, + ArgumentParser::OptionValue &opt_value) { + if (short_opt == 'h' || long_opt == "help") { + print_help = true; + } else if (short_opt == 'l' || long_opt == "level") { + level = std::stoi(opt_value.get()); + } else { + cerr << "invalid option" << endl; + return false; + } + return true; + } +); + +if (!parser.parse(argc, argv)) + return 0; +``` + +### Serializer / Deserializer — 序列化 + +支持大端/小端的数据序列化与反序列化,提供流式操作: + +```cpp +std::vector block; +Serializer s(block, Endian::kBig); + +s << uint16_t(0x1234) << int32_t(42) << float(3.14); + +Deserializer d(block.data(), block.size(), Endian::kBig); +uint16_t v1; int32_t v2; float v3; +d >> v1 >> v2 >> v3; +``` + +### Variables — 变量管理 + +变量管理对象,支持层级继承(parent 查找): + +```cpp +Variables vars; +vars.define("name", Json("default")); +vars.set("name", Json("new_value")); + +Json value; +vars.get("name", value); //! 从本地查找 +vars.get("name", value, false); //! 从 parent 继续查找 + +//! 设置父变量表 +vars.setParent(&parent_vars); +``` + +### UUID — UUID 生成 + +```cpp +std::string id = util::UUID::Generate(); //! 生成 v4 UUID +``` + +### Base64 — Base64 编解码 + +```cpp +std::string encoded = util::Base64::Encode(data, size); +std::vector decoded = util::Base64::Decode(encoded); +``` + +### CRC — CRC 校验 + +```cpp +uint16_t crc16 = util::CRC::Calc16(data, size); +uint32_t crc32 = util::CRC::Calc32(data, size); +``` + +### PidFile — PID 文件 + +防止同一程序重复启动: + +```cpp +util::PidFile pid_file; +pid_file.setPathPrefix("/var/run/myapp"); //! 自动创建 PID 文件 +pid_file.enable(); +``` + +## 常见场景 + +1. **配置文件加载**:使用 json::Load 或 LoadDeeply 读取 JSON 配置 +2. **命令行参数**:使用 ArgumentParser 解析 -h/--help/-l/--level 等 +3. **二进制协议**:使用 Buffer 缓冲收发数据,Serializer 序列化协议字段 +4. **校验与编码**:CRC 校验数据完整性,Base64 编码二进制数据 +5. **防止重复启动**:使用 PidFile 确保只有一个进程实例 + +## 注意事项 + +1. **Buffer 非线程安全**:多线程使用需在外部加锁 +2. **Json::Load 异常**:Load() 抛 OpenFileError 或 ParseJsonFileError,LoadDeeply 还有 DuplicateIncludeError +3. **Serializer 字节序**:注意大小端对齐,默认大端(网络协议常用) +4. **ArgumentParser.get()**:调用 opt_value.get() 后,解析器不会将该值误认为参数项 +5. **DeepLoader 防重复**:同一文件不能被 include 两次,会抛 DuplicateIncludeError + +## 相关模块 + +- **base**:提供 Json(nlohmann/json)前置声明和定义 +- **event**:AsyncPipe 基于 Loop 运行 +- **flow**:Action 使用 Variables 存储变量 +- **main**:Module 使用 Variables 和 json 加载配置 +- **network**:使用 Buffer 作为接收数据缓冲区 diff --git a/documents/modules/websocket.md b/documents/modules/websocket.md new file mode 100644 index 00000000..3fd3abe4 --- /dev/null +++ b/documents/modules/websocket.md @@ -0,0 +1,523 @@ +# WebSocket Service Module (websocket) + +## What is it? + +The websocket module provides WebSocket server and client implementations based on the HTTP server module. It follows the RFC 6455 specification and integrates with the HTTP middleware pattern — the WsServer itself is an HTTP middleware that detects WebSocket upgrade requests and manages upgraded connections. + +On the client side, the `Client` class establishes a TCP connection to a WebSocket server, performs the HTTP Upgrade handshake, and then enters WebSocket frame communication mode. It supports auto-reconnect with configurable delay strategies. + +## Why do you need it? + +In service programs that already expose HTTP APIs, you may also need real-time bidirectional communication — for example: pushing live updates to browsers, chat rooms, IoT device status streaming, or binary data echo services. The websocket module allows you to add WebSocket capability alongside your existing HTTP server without running a separate service. + +On the client side, C++ programs may need to connect to WebSocket servers to receive real-time push data or send commands. The `Client` class provides an asynchronous WebSocket client with auto-reconnect, Ping/Pong heartbeat, and Close frame handling. + +## Header Files + +```cpp +#include //! WebSocket frame definition +#include //! Frame parser (incremental) +#include //! Frame builder (server/masked) +#include //! Compression (RFC 7692) +#include //! WebSocket server +#include //! WebSocket connection (internal) +#include //! WebSocket client +``` + +## Core Classes and Interfaces + +### WsServer — WebSocket Server + +WsServer runs on top of an HTTP server as a middleware. It detects WebSocket upgrade requests, validates the handshake, and creates WsConnection objects for each upgraded connection. All client operations use `ConnToken` (a cabinet::Token) instead of raw pointers. + +| Method | Description | +|------|------| +| `WsServer(loop)` | Constructor | +| `initialize(http_server, url_path)` | Initialize: associate with an HTTP server; `url_path` controls URL matching | +| `start()` | Start (registers as HTTP middleware) | +| `stop()` | Stop (unregisters middleware, closes all connections) | +| `cleanup()` | Cleanup (inverse of initialize) | +| `state()` | Get current state (None/Inited/Running) | +| `send(client, text)` | Send text frame to a client | +| `send(client, str)` | Send text frame to a client (const char* version, no std::string construction) | +| `send(client, data, len)` | Send binary frame to a client (raw pointer version) | +| `send(client, data)` | Send binary frame to a client (vector version) | +| `close(client, code, reason)` | Close a client connection (sends Close frame) | +| `ping(client, data)` | Send Ping frame to a client | +| `pong(client, data)` | Send Pong frame to a client | +| `isClientValid(client)` | Check if a client connection is still valid | +| `peerAddr(client)` | Get client address (IP:port) | +| `getUrl(client)` | Get the URL path the client connected to | +| `setContext(client, ctx, deleter)` | Set context data for a client connection | +| `getContext(client)` | Get context data for a client connection | +| `setConnectedCallback(cb)` | Set callback: new client connected | +| `setDisconnectedCallback(cb)` | Set callback: client disconnected | +| `setTextMessageCallback(cb)` | Set callback: received complete text message (rvalue ref, after buffered decompression) | +| `setBinaryMessageCallback(cb)` | Set callback: received complete binary message (rvalue ref, after buffered decompression) | +| `setErrorCallback(cb)` | Set callback: client connection error | +| `setCompressionEnable(enable)` | Enable/disable compression support (must call before initialize) | +| `setFragmentSize(size)` | Set max fragment size for sending (default 65535, 0=no fragmentation; must call before initialize) | +| `IsWsUpgradeRequest(req)` | Static: check if an HTTP request is a valid WebSocket upgrade | +| `ComputeWsAcceptKey(key)` | Static: compute Sec-WebSocket-Accept value | + +**URL path matching rules:** + +| `url_path` value | Matching behavior | +|---|---| +| Ends with `/` (e.g. `/ws/`) | Prefix match — matches `/ws/aa`, `/ws/bb/cc` | +| Does not end with `/` (e.g. `/ws`) | Exact match — matches only `/ws` | +| Empty string `""` | Matches all WebSocket upgrade requests | + +**State enum:** + +| State | Description | +|-------|-------------| +| `kNone` | Not initialized | +| `kInited` | Initialized | +| `kRunning` | Running (middleware registered) | + +**Callback signatures:** + +```cpp +using ConnToken = cabinet::Token; + +ConnectedCallback = std::function; +DisconnectedCallback = std::function; +TextMessageCallback = std::function; +BinaryMessageCallback = std::function &&)>; +ErrorCallback = std::function; +``` + +> **Note:** `TextMessageCallback` and `BinaryMessageCallback` use rvalue references for efficiency. Fragmented messages are buffered internally and only delivered to the callback after the complete message is received and decompressed. This means the callback never receives partial fragments — only complete, decompressed messages. + +### WsClient — WebSocket Client + +The WsClient class connects to a WebSocket server via TcpConnector, performs the HTTP Upgrade handshake, and then enters WebSocket frame communication mode. All client-to-server frames are masked per RFC 6455. It supports auto-reconnect with configurable delay strategies. Fragmented messages are buffered and only delivered after complete receipt and decompression. + +| Method | Description | +|------|------| +| `WsClient(loop)` | Constructor | +| `initialize(server_addr, url_path)` | Initialize: set target server address and URL path | +| `start()` | Start connecting to server | +| `stop()` | Stop/disconnect | +| `cleanup()` | Cleanup (inverse of initialize) | +| `state()` | Get current state | +| `send(text)` | Send text frame | +| `send(str)` | Send text frame (const char* version, no std::string construction) | +| `send(data, len)` | Send binary frame (raw pointer version) | +| `send(data)` | Send binary frame (vector version) | +| `close(code, reason)` | Send Close frame and disconnect | +| `ping(data)` | Send Ping frame | +| `pong(data)` | Send Pong frame | +| `isExpired()` | Check if connection is expired | +| `peerAddr()` | Get server address | +| `setContext(ctx, deleter)` | Set context data | +| `getContext()` | Get context data | +| `setConnectedCallback(cb)` | Set callback: connected to server | +| `setDisconnectedCallback(cb)` | Set callback: disconnected from server | +| `setTextMessageCallback(cb)` | Set callback: received complete text message (rvalue ref) | +| `setBinaryMessageCallback(cb)` | Set callback: received complete binary message (rvalue ref) | +| `setErrorCallback(cb)` | Set callback: connection error | +| `setAutoReconnect(enable)` | Enable/disable auto-reconnect (default: enabled) | +| `setReconnectDelayCalcFunc(func)` | Set custom reconnect delay calculation | +| `setCompressionPrefer(enable)` | Enable/disable compression preference (must call before initialize) | +| `setFragmentSize(size)` | Set max fragment size for sending (default 65535, 0=no fragmentation; must call before initialize) | + +**State enum:** + +| State | Description | +|-------|-------------| +| `kNone` | Not initialized | +| `kInited` | Initialized | +| `kConnecting` | TCP connecting | +| `kHandshaking` | HTTP Upgrade handshake in progress | +| `kConnected` | WebSocket connected | + +### WsFrame — WebSocket Frame + +```cpp +struct WsFrame { + enum class OpCode : uint8_t { + kContinue = 0x0, //! Continuation + kText = 0x1, //! Text data + kBinary = 0x2, //! Binary data + kClose = 0x8, //! Close connection + kPing = 0x9, //! Ping + kPong = 0xA, //! Pong + }; + + OpCode opcode; //! Frame opcode + bool fin = true; //! Is this the final frame? + bool rsv1 = false; //! RSV1 bit (true for compressed frame, RFC 7692) + std::string payload; //! Payload data + + bool isControlFrame() const; //! Close/Ping/Pong are control frames + uint16_t closeCode() const; //! Extract close code from Close frame + std::string closeReason() const; //! Extract close reason from Close frame +}; +``` + +### WsFrameParser — Incremental Frame Parser + +An incremental WebSocket frame parser suitable for event-driven scenarios. It parses data from a buffer step by step. + +| Method | Description | +|------|------| +| `parse(data, size)` | Parse data, returns number of bytes consumed | +| `state()` | Get current parsing state | +| `getFrame()` | Get the parsed frame (only when state == kFinished) | +| `reset()` | Reset parser | + +### WsFrameBuilder — Frame Builder + +Static helper class for constructing WebSocket frames. Server frames are unmasked; client frames are masked per RFC 6455. + +| Method | Description | +|------|------| +| `BuildTextFrame(text)` | Build text frame (server, unmasked) | +| `BuildBinaryFrame(data, len)` | Build binary frame (server, unmasked) | +| `BuildBinaryFrame(data)` | Build binary frame (server, vector version) | +| `BuildCloseFrame(code, reason)` | Build Close frame (server, unmasked) | +| `BuildPingFrame(data)` | Build Ping frame (server, unmasked) | +| `BuildPongFrame(data)` | Build Pong frame (server, unmasked) | +| `BuildFrame(opcode, fin, payload, len)` | Build generic frame (server, unmasked) | +| `BuildMaskedTextFrame(text)` | Build text frame (client, masked) | +| `BuildMaskedBinaryFrame(data, len)` | Build binary frame (client, masked) | +| `BuildMaskedBinaryFrame(data)` | Build binary frame (client, masked, vector) | +| `BuildMaskedCloseFrame(code, reason)` | Build Close frame (client, masked) | +| `BuildMaskedPingFrame(data)` | Build Ping frame (client, masked) | +| `BuildMaskedPongFrame(data)` | Build Pong frame (client, masked) | +| `BuildMaskedFrame(opcode, fin, payload, len, mask_key)` | Build generic frame (client, masked) | + +## Usage Examples + +### Server: Chat Room + +> Full example in `examples/websocket/chat_server/` + +Demonstrates multiple chat rooms mounted on the same HTTP server at different URL paths. Each ChatRoom contains a WsServer instance and manages WebSocket connections. The first text message from a client is treated as the username (login), and subsequent messages are broadcast to all logged-in users. + +```cpp +#include +#include + +class ChatRoom { + public: + ChatRoom(event::Loop *wp_loop, const std::string &name) + : ws_srv_(wp_loop) + { } + + bool initialize(http::server::Server *http_srv, const std::string &url_path) + { + if (!ws_srv_.initialize(http_srv, url_path)) + return false; + + ws_srv_.setConnectedCallback([this](const WsServer::ConnToken &token) { + onConnected(token); + }); + ws_srv_.setDisconnectedCallback([this](const WsServer::ConnToken &token) { + onDisconnected(token); + }); + ws_srv_.setTextMessageCallback([this](const WsServer::ConnToken &token, std::string &&text) { + onTextMessage(token, std::move(text)); + }); + + return true; + } + + bool start() { return ws_srv_.start(); } + void stop() { ws_srv_.stop(); } + void cleanup() { ws_srv_.cleanup(); } + + private: + void onTextMessage(const WsServer::ConnToken &token, std::string &&text) + { + //! First message is the username + auto it = conn_to_name_.find(token); + if (it == conn_to_name_.end()) { + conn_to_name_[token] = text; + broadcast(text + " online"); + } else { + broadcast(it->second + ": " + text); + } + } + + void broadcast(const std::string &msg) + { + for (const auto &pair : conn_to_name_) + ws_srv_.send(pair.first, msg); + } + + WsServer ws_srv_; + std::map conn_to_name_; +}; + +int main() +{ + auto sp_loop = Loop::New(); + + //! Create HTTP server + Server http_srv(sp_loop); + http_srv.initialize(network::SockAddr::FromString("0.0.0.0:8080"), 1); + + //! Create two chat rooms at different URL paths + ChatRoom chat_room_1(sp_loop, "Room1"); + ChatRoom chat_room_2(sp_loop, "Room2"); + + chat_room_1.initialize(&http_srv, "/ws/chat-1"); + chat_room_2.initialize(&http_srv, "/ws/chat-2"); + + //! Add HTTP homepage handler + http_srv.use([&](ContextSptr ctx, const NextFunc &next) { + if (ctx->req().url.path == "/") { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().body = "

Chat Server

"; + return; + } + next(); + }); + + //! Start services + http_srv.start(); + chat_room_1.start(); + chat_room_2.start(); + + //! ... run loop, handle SIGINT, cleanup ... +} +``` + +### Server: Binary Echo + +> Full example in `examples/websocket/echo_bin/` + +Demonstrates binary WebSocket frame handling. The server echoes binary data back to the client and periodically pushes statistics frames (4-byte header "STAT" + JSON payload) using `send()` with `vector`. + +```cpp +class EchoService { + public: + EchoService(Loop *wp_loop) + : ws_srv_(wp_loop) + , stat_timer_(wp_loop->newTimerEvent()) + { } + + bool initialize(Server *http_srv, const std::string &url_path) + { + if (!ws_srv_.initialize(http_srv, url_path)) + return false; + + ws_srv_.setBinaryMessageCallback([this](const WsServer::ConnToken &token, std::vector &&data) { + onBinaryMessage(token, std::move(data)); + }); + ws_srv_.setTextMessageCallback([this](const WsServer::ConnToken &token, std::string &&text) { + //! This service only accepts binary frames + ws_srv_.send(token, "This service only accepts binary frames"); + }); + + //! Timer: push stats every 5 seconds + stat_timer_->initialize(std::chrono::milliseconds(5000), Event::Mode::kPersist); + stat_timer_->setCallback([this] { onStatTimer(); }); + + return true; + } + + private: + void onBinaryMessage(const WsServer::ConnToken &token, std::vector &&data) + { + //! Echo binary data back + ws_srv_.send(token, data); + } + + void onStatTimer() + { + //! Build binary statistics frame: "STAT" header + JSON + std::vector stat_data; + stat_data.insert(stat_data.end(), kStatHeader, kStatHeader + 4); + stat_data.insert(stat_data.end(), json.begin(), json.end()); + + for (const auto &token : conns_) + ws_srv_.send(token, stat_data); + } +}; +``` + +### Client: Chat Client + +> Full example in `examples/websocket/chat_client/` + +Demonstrates a WebSocket client connecting to a chat server, reading from stdin, and sending/receiving messages. + +```cpp +#include + +int main() +{ + auto sp_loop = Loop::New(); + + WsClient ws_client(sp_loop); + ws_client.initialize(SockAddr::FromString("127.0.0.1:8080"), "/ws/chat-1"); + + ws_client.setConnectedCallback([&] { + std::cout << "Connected! Enter your username:" << std::endl; + //! Enable stdin reading + sp_stdin_event->enable(); + }); + + ws_client.setTextMessageCallback([&](std::string &&text) { + std::cout << text << std::endl; + }); + + //! Custom reconnect delay: exponential backoff + ws_client.setReconnectDelayCalcFunc([](int fail_count) { + return 1 << std::min(4, fail_count); + }); + + ws_client.start(); + + //! ... run loop, handle SIGINT, cleanup ... +} +``` + +### Server: URL Path Matching + +```cpp +//! Prefix match: matches /ws/anything +ws_srv.initialize(&http_srv, "/ws/"); + +//! Exact match: matches only /ws +ws_srv.initialize(&http_srv, "/ws"); + +//! Match all: matches any WebSocket upgrade request +ws_srv.initialize(&http_srv, ""); +``` + +### Server: Context Data + +Attach custom data to a client connection for per-client state tracking: + +```cpp +ws_srv.setConnectedCallback([](const WsServer::ConnToken &token) { + //! Attach a user session object + auto session = new UserSession(); + ws_srv.setContext(token, session, [](void *p) { delete static_cast(p); }); +}); + +ws_srv.setTextMessageCallback([](const WsServer::ConnToken &token, std::string &&text) { + //! Retrieve the session + auto session = static_cast(ws_srv.getContext(token)); + if (session != nullptr) { + //! ... process message using session data ... + } +}); +``` + +### Client: Custom Reconnect Delay + +```cpp +//! Exponential backoff: 1s, 2s, 4s, 8s, ... max 16s +ws_client.setReconnectDelayCalcFunc([](int fail_count) { + return 1 << std::min(4, fail_count); +}); + +//! Disable auto-reconnect +ws_client.setAutoReconnect(false); +``` + +## Compression (RFC 7692 permessage-deflate) + +The websocket module supports the `permessage-deflate` compression extension defined in RFC 7692. When enabled, WebSocket text and binary frames are compressed using DEFLATE (zlib), significantly reducing bandwidth for repetitive or large messages. + +### How it works + +1. **Server side**: Call `setCompressionEnable(true)` before `initialize()`. If a client requests `permessage-deflate` in its handshake (`Sec-WebSocket-Extensions: permessage-deflate`), the server agrees by responding with the same header. Otherwise, compression is not used. + +2. **Client side**: Call `setCompressionPrefer(true)` before `initialize()`. The client requests `permessage-deflate` in its handshake. If the server agrees, frames are compressed/decompressed; if the server declines, communication proceeds without compression. + +3. **Frame format**: Compressed data frames set the RSV1 bit in the first frame header. Control frames (Close/Ping/Pong) are never compressed. + +4. **Implementation**: Uses raw DEFLATE with 4-byte tail stripping (RFC 7692 Section 7.2.2). Each message is independently compressed (no_context_takeover mode), simplifying implementation and ensuring compatibility. + +### WsCompressionConfig — Compression Configuration + +```cpp +#include + +struct WsCompressionConfig { + bool enabled = false; //! Whether compression is enabled + bool no_context_takeover = true; //! Don't retain zlib context across messages + int max_window_bits = 15; //! Maximum window bits (8~15) +}; +``` + +### Server: Enable Compression + +```cpp +WsServer ws_srv(sp_loop); +ws_srv.setCompressionEnable(true); //! Allow compression (before initialize) +ws_srv.initialize(&http_srv, "/ws/chat"); +``` + +### Client: Prefer Compression + +```cpp +WsClient ws_client(sp_loop); +ws_client.setCompressionPrefer(true); //! Request compression (before initialize) +ws_client.initialize(SockAddr::FromString("127.0.0.1:8080"), "/ws/chat"); +``` + +### Mixed Server (Some Routes Compressed, Some Not) + +```cpp +//! Chat room with compression +WsServer ws_srv_compressed(sp_loop); +ws_srv_compressed.setCompressionEnable(true); +ws_srv_compressed.initialize(&http_srv, "/ws/chat"); + +//! Echo service without compression +WsServer ws_srv_plain(sp_loop); +ws_srv_plain.initialize(&http_srv, "/ws/echo"); +``` + +### Important: Compression Negotiation + +- Compression is **optional** and negotiated per connection during the HTTP Upgrade handshake. +- If either side does not support or declines compression, frames are sent uncompressed — no impact on functionality. +- The `rsv1` field on `WsFrame` indicates whether a received frame was compressed. After decompression, `rsv1` is cleared, so user callbacks receive the original payload data transparently. +- Compression fails gracefully: if compression or decompression fails, the system falls back to uncompressed mode or reports an error. + +## Common Scenarios + +1. **Real-time push**: Mount WsServer on HTTP server, push live data to browser clients +2. **Chat room**: Multiple chat rooms on the same HTTP server at different URL paths +3. **Binary data streaming**: Echo binary frames, send structured binary data with headers +4. **IoT device communication**: Client connects to server, sends status updates and receives commands +5. **Server-to-client heartbeat**: Server sends Ping frames, client auto-replies Pong +6. **Client auto-reconnect**: Client reconnects with exponential backoff after disconnection +7. **Mixed HTTP + WebSocket**: HTTP serves REST APIs and static pages; WebSocket handles real-time communication +8. **Compressed communication**: Enable permessage-deflate to reduce bandwidth for text/binary data + +## Important Notes + +1. **WsServer is an HTTP middleware**: It must be initialized with an `http::server::Server` and started before or after the HTTP server starts. WsServer registers itself as a middleware via `http_server->use()`. +2. **URL path matching**: Pay attention to whether `url_path` ends with `/` — it determines prefix matching vs. exact matching. An empty string matches all upgrade requests. +3. **ConnToken-based operations**: All client operations use `ConnToken` (cabinet::Token), not pointers. This ensures safe access even after the underlying connection is destroyed. +4. **Ping/Pong auto-reply**: Both WsServer and Client automatically reply Pong when receiving Ping frames. +5. **Close frame auto-reply**: Both sides automatically send a Close frame reply when receiving a Close frame, then wait for TCP disconnection. +6. **Client masking**: Per RFC 6455, all frames sent by the Client are masked. Server frames are unmasked. +7. **Client auto-reconnect**: Default is enabled. Disconnection triggers automatic reconnection after the configured delay. Customize delay via `setReconnectDelayCalcFunc()`. +8. **Client handshake**: The Client performs HTTP Upgrade handshake automatically. It validates the 101 response, Sec-WebSocket-Accept header, and Upgrade/Connection headers. Handshake failure triggers reconnection if auto-reconnect is enabled. +9. **Lifecycle order**: Must follow initialize → start → stop → cleanup for both WsServer and Client. +10. **Thread safety**: All callbacks run in the Loop thread. Cross-thread operations must use `runInLoop()`. +11. **Context data**: `setContext()/getContext()` on WsServer delegates to the underlying TcpConnection. Context data is accessible in callbacks but becomes `nullptr` after the connection is destroyed. +12. **Compression**: Call `setCompressionEnable(true)` on WsServer or `setCompressionPrefer(true)` on WsClient **before** `initialize()`. Compression is negotiated per connection; if the other side doesn't support it, frames are sent uncompressed without impact. +13. **Compression fallback**: If compression/decompression fails, the system logs a warning and falls back to sending the frame uncompressed. Decompression failure causes an error callback. +14. **Fragmented receive**: Both WsServer and WsClient buffer fragmented messages internally. Only after the complete message is received (all fragments, fin=true) does it decompress (if needed) and deliver the message to the callback. The callback never receives partial fragments. +15. **Fragmented send**: When sending data larger than `fragment_size`, it is automatically split into WebSocket fragments. The first fragment carries the original opcode and rsv1 (if compressed); continuation fragments use opcode kContinue. Call `setFragmentSize(size)` before `initialize()` to configure the fragment size (default 65535, set to 0 to disable fragmentation). +16. **send(const char\*)**: Both WsServer and WsClient provide a `send(const char *str)` overload that sends text without constructing a temporary `std::string`. It correctly uses kText opcode. + +## Related Modules + +- **http**: WsServer runs as an HTTP middleware on http::server::Server +- **event**: Server and Client run based on Loop +- **network**: Connection management via TcpConnector (client) and TcpConnection +- **crypto**: SHA1 calculation for Sec-WebSocket-Accept +- **base**: Provides Cabinet for connection lifetime management diff --git a/documents/modules/websocket_CN.md b/documents/modules/websocket_CN.md new file mode 100644 index 00000000..0f04b397 --- /dev/null +++ b/documents/modules/websocket_CN.md @@ -0,0 +1,523 @@ +# WebSocket 服务模块 (websocket) + +## 是什么? + +websocket 模块提供了 WebSocket 服务器与客户端实现,遵循 RFC 6455 规范。服务端基于 HTTP 服务器中间件模式运行——WsServer 本身即为 HTTP 中间件,它检测 WebSocket 升级请求、完成握手、接管 TcpConnection 进入帧通信模式。 + +客户端侧,`Client` 类通过 TcpConnector 建立 TCP 连接,发送 HTTP Upgrade 握手请求,验证 101 响应后进入帧通信模式。所有客户端帧必须掩码(RFC 6455 Section 5.3),支持自动重连与可配置的重连延迟策略。 + +## 为什么需要它? + +在已提供 HTTP API 的服务程序中,还需要实时双向通信的场景——例如:向浏览器推送实时数据、聊天室、IoT 设备状态流、二进制数据回传等。websocket 模块让 C++ 程序在现有 HTTP 服务器上叠加 WebSocket 能力,无需额外部署独立服务。 + +客户端侧,C++ 程序可能需要连接 WebSocket 服务器接收实时推送数据或发送指令。`Client` 类提供了异步 WebSocket 客户端,支持自动重连、Ping/Pong 心跳和 Close 帧处理。 + +## 头文件 + +```cpp +#include //! WebSocket 帧定义 +#include //! 帧解析器(增量式) +#include //! 帧构建器(服务端/掩码) +#include //! 压缩(RFC 7692) +#include //! WebSocket 服务端 +#include //! WebSocket 连接(内部类) +#include //! WebSocket 客户端 +``` + +## 核心类与接口 + +### WsServer — WebSocket 服务端 + +WsServer 运行在 HTTP 服务器之上,作为中间件存在。它检测 WebSocket 升级请求,验证握手参数,并为每个升级成功的连接创建 WsConnection 对象。所有客户端操作使用 `ConnToken`(cabinet::Token),而非原始指针。 + +| 方法 | 说明 | +|------|------| +| `WsServer(loop)` | 构造 | +| `initialize(http_server, url_path)` | 初始化:关联到 HTTP 服务器;`url_path` 控制URL匹配规则 | +| `start()` | 启动(注册为 HTTP 中间件) | +| `stop()` | 停止(反注册中间件,关闭所有连接) | +| `cleanup()` | 清理(与 initialize 逆操作) | +| `state()` | 获取当前状态 (None/Inited/Running) | +| `send(client, text)` | 向指定客户端发送文本帧 | +| `send(client, str)` | 向指定客户端发送文本帧(const char* 版本,不构造 std::string) | +| `send(client, data, len)` | 向指定客户端发送二进制帧(原始指针版本) | +| `send(client, data)` | 向指定客户端发送二进制帧(vector 版本) | +| `close(client, code, reason)` | 关闭指定客户端连接(发送 Close 帧) | +| `ping(client, data)` | 向指定客户端发送 Ping 帧 | +| `pong(client, data)` | 向指定客户端发送 Pong 帧 | +| `isClientValid(client)` | 检查客户端连接是否有效 | +| `peerAddr(client)` | 获取客户端地址(IP:端口) | +| `getUrl(client)` | 获取客户端连接的 URL 路径 | +| `setContext(client, ctx, deleter)` | 设置客户端连接的上下文数据 | +| `getContext(client)` | 获取客户端连接的上下文数据 | +| `setConnectedCallback(cb)` | 设置回调:新客户端连接 | +| `setDisconnectedCallback(cb)` | 设置回调:客户端断开 | +| `setTextMessageCallback(cb)` | 设置回调:收到完整文本消息(右值引用,分片数据缓存后统一解压再回调) | +| `setBinaryMessageCallback(cb)` | 设置回调:收到完整二进制消息(右值引用,分片数据缓存后统一解压再回调) | +| `setErrorCallback(cb)` | 设置回调:客户端连接出错 | +| `setCompressionEnable(enable)` | 启用/禁用压缩支持(必须在 initialize 之前调用) | +| `setFragmentSize(size)` | 设置发送分片大小(默认65535,0=不分片;必须在 initialize 之前调用) | +| `IsWsUpgradeRequest(req)` | 静态方法:检查 HTTP 请求是否为有效的 WebSocket 升级请求 | +| `ComputeWsAcceptKey(key)` | 静态方法:计算 Sec-WebSocket-Accept 响应值 | + +**URL 路径匹配规则:** + +| `url_path` 值 | 匹配行为 | +|---|---| +| 以 `/` 结尾(如 `/ws/`) | 前缀匹配——匹配 `/ws/aa`、`/ws/bb/cc` | +| 不以 `/` 结尾(如 `/ws`) | 全量匹配——仅匹配 `/ws` | +| 空字符串 `""` | 匹配所有 WebSocket 升级请求 | + +**State 状态枚举:** + +| 状态 | 说明 | +|------|------| +| `kNone` | 未初始化 | +| `kInited` | 已初始化 | +| `kRunning` | 运行中(中间件已注册) | + +**回调签名:** + +```cpp +using ConnToken = cabinet::Token; + +ConnectedCallback = std::function; +DisconnectedCallback = std::function; +TextMessageCallback = std::function; +BinaryMessageCallback = std::function &&)>; +ErrorCallback = std::function; +``` + +> **注意:** `TextMessageCallback` 和 `BinaryMessageCallback` 使用右值引用提升效率。分片消息在内部缓存,只有接收完整(fin=true)并解压后才回调业务层,回调中永远不会收到部分分片数据。 + +### WsClient — WebSocket 客户端 + +WsClient 类通过 TcpConnector 建立 TCP 连接,发送 HTTP Upgrade 握手请求,验证 101 响应后进入 WebSocket 帧通信模式。所有客户端帧必须掩码(RFC 6455)。支持自动重连与可配置的重连延迟策略。分片消息在内部缓存,接收完整后再解压回调。 + +| 方法 | 说明 | +|------|------| +| `WsClient(loop)` | 构造 | +| `initialize(server_addr, url_path)` | 初始化:设置目标服务器地址与 URL 路径 | +| `start()` | 开始连接服务器 | +| `stop()` | 停止/断开连接 | +| `cleanup()` | 清理(与 initialize 逆操作) | +| `state()` | 获取当前状态 | +| `send(text)` | 发送文本帧 | +| `send(str)` | 发送文本帧(const char* 版本,不构造 std::string) | +| `send(data, len)` | 发送二进制帧(原始指针版本) | +| `send(data)` | 发送二进制帧(vector 版本) | +| `close(code, reason)` | 发送 Close 帧并断开连接 | +| `ping(data)` | 发送 Ping 帧 | +| `pong(data)` | 发送 Pong 帧 | +| `isExpired()` | 检查连接是否已失效 | +| `peerAddr()` | 获取服务器地址 | +| `setContext(ctx, deleter)` | 设置上下文数据 | +| `getContext()` | 获取上下文数据 | +| `setConnectedCallback(cb)` | 设置回调:连接成功 | +| `setDisconnectedCallback(cb)` | 设置回调:连接断开 | +| `setTextMessageCallback(cb)` | 设置回调:收到完整文本消息(右值引用) | +| `setBinaryMessageCallback(cb)` | 设置回调:收到完整二进制消息(右值引用) | +| `setErrorCallback(cb)` | 设置回调:连接出错 | +| `setAutoReconnect(enable)` | 启用/禁用自动重连(默认启用) | +| `setReconnectDelayCalcFunc(func)` | 设置自定义重连延迟计算函数 | +| `setCompressionPrefer(enable)` | 启用/禁用压缩偏好(必须在 initialize 之前调用) | +| `setFragmentSize(size)` | 设置发送分片大小(默认65535,0=不分片;必须在 initialize 之前调用) | + +**State 状态枚举:** + +| 状态 | 说明 | +|------|------| +| `kNone` | 未初始化 | +| `kInited` | 已初始化 | +| `kConnecting` | TCP 连接中 | +| `kHandshaking` | HTTP Upgrade 握手阶段 | +| `kConnected` | WebSocket 已连接 | + +### WsFrame — WebSocket 帧 + +```cpp +struct WsFrame { + enum class OpCode : uint8_t { + kContinue = 0x0, //! 继续 + kText = 0x1, //! 文本 + kBinary = 0x2, //! 二进制 + kClose = 0x8, //! 关闭连接 + kPing = 0x9, //! Ping + kPong = 0xA, //! Pong + }; + + OpCode opcode; //! 帧操作码 + bool fin = true; //! 是否为最后一帧 + bool rsv1 = false; //! RSV1 位(压缩帧首帧为 true,RFC 7692) + std::string payload; //! 负载数据 + + bool isControlFrame() const; //! Close/Ping/Pong 为控制帧 + uint16_t closeCode() const; //! 从 Close 帧中提取关闭码 + std::string closeReason() const; //! 从 Close 帧中提取关闭原因 +}; +``` + +### WsFrameParser — 增量帧解析器 + +适用于事件驱动场景的增量式 WebSocket 帧解析器,逐步从缓冲区中解析帧数据。 + +| 方法 | 说明 | +|------|------| +| `parse(data, size)` | 解析数据,返回已消费的字节数 | +| `state()` | 获取当前解析状态 | +| `getFrame()` | 获取解析完成的帧(仅 state == kFinished 时有效) | +| `reset()` | 重置解析器 | + +### WsFrameBuilder — 帧构建器 + +用于构建 WebSocket 帧的静态辅助类。服务端帧不使用掩码,客户端帧必须使用掩码(RFC 6455)。 + +| 方法 | 说明 | +|------|------| +| `BuildTextFrame(text)` | 构建文本帧(服务端,不掩码) | +| `BuildBinaryFrame(data, len)` | 构建二进制帧(服务端,不掩码) | +| `BuildBinaryFrame(data)` | 构建二进制帧(服务端,vector 版本) | +| `BuildCloseFrame(code, reason)` | 构建关闭帧(服务端,不掩码) | +| `BuildPingFrame(data)` | 构建Ping帧(服务端,不掩码) | +| `BuildPongFrame(data)` | 构建Pong帧(服务端,不掩码) | +| `BuildFrame(opcode, fin, payload, len)` | 通用帧构建(服务端,不掩码) | +| `BuildMaskedTextFrame(text)` | 构建文本帧(客户端,掩码) | +| `BuildMaskedBinaryFrame(data, len)` | 构建二进制帧(客户端,掩码) | +| `BuildMaskedBinaryFrame(data)` | 构建二进制帧(客户端,掩码,vector版本) | +| `BuildMaskedCloseFrame(code, reason)` | 构建关闭帧(客户端,掩码) | +| `BuildMaskedPingFrame(data)` | 构建Ping帧(客户端,掩码) | +| `BuildMaskedPongFrame(data)` | 构建Pong帧(客户端,掩码) | +| `BuildMaskedFrame(opcode, fin, payload, len, mask_key)` | 通用帧构建(客户端,掩码) | + +## 使用示例 + +### 服务端:群聊聊天室 + +> 完整示例见 `examples/websocket/chat_server/` + +演示多个聊天室挂载在同一 HTTP 服务器上的不同 URL 路径。每个 ChatRoom 内含一个 WsServer 实例,管理 WebSocket 连接与聊天逻辑。客户端第一条文本消息作为用户名(登录),后续消息广播给所有已登录用户。 + +```cpp +#include +#include + +class ChatRoom { + public: + ChatRoom(event::Loop *wp_loop, const std::string &name) + : ws_srv_(wp_loop) + { } + + bool initialize(http::server::Server *http_srv, const std::string &url_path) + { + if (!ws_srv_.initialize(http_srv, url_path)) + return false; + + ws_srv_.setConnectedCallback([this](const WsServer::ConnToken &token) { + onConnected(token); + }); + ws_srv_.setDisconnectedCallback([this](const WsServer::ConnToken &token) { + onDisconnected(token); + }); + ws_srv_.setTextMessageCallback([this](const WsServer::ConnToken &token, std::string &&text) { + onTextMessage(token, std::move(text)); + }); + + return true; + } + + bool start() { return ws_srv_.start(); } + void stop() { ws_srv_.stop(); } + void cleanup() { ws_srv_.cleanup(); } + + private: + void onTextMessage(const WsServer::ConnToken &token, std::string &&text) + { + //! 第一条消息作为用户名 + auto it = conn_to_name_.find(token); + if (it == conn_to_name_.end()) { + conn_to_name_[token] = text; + broadcast(text + " 上线"); + } else { + broadcast(it->second + ": " + text); + } + } + + void broadcast(const std::string &msg) + { + for (const auto &pair : conn_to_name_) + ws_srv_.send(pair.first, msg); + } + + WsServer ws_srv_; + std::map conn_to_name_; +}; + +int main() +{ + auto sp_loop = Loop::New(); + + //! 创建 HTTP 服务器 + Server http_srv(sp_loop); + http_srv.initialize(network::SockAddr::FromString("0.0.0.0:8080"), 1); + + //! 创建两个聊天室,挂载到不同 URL 路径 + ChatRoom chat_room_1(sp_loop, "聊天室1"); + ChatRoom chat_room_2(sp_loop, "聊天室2"); + + chat_room_1.initialize(&http_srv, "/ws/chat-1"); + chat_room_2.initialize(&http_srv, "/ws/chat-2"); + + //! 添加 HTTP 主页处理 + http_srv.use([&](ContextSptr ctx, const NextFunc &next) { + if (ctx->req().url.path == "/") { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().body = "

聊天服务器

"; + return; + } + next(); + }); + + //! 启动服务 + http_srv.start(); + chat_room_1.start(); + chat_room_2.start(); + + //! ... 运行事件循环、处理 SIGINT、清理 ... +} +``` + +### 服务端:二进制 Echo + +> 完整示例见 `examples/websocket/echo_bin/` + +演示二进制 WebSocket 帧的处理。服务器将收到的二进制数据原样回传(echo),并每 5 秒通过 `send()` 的 `vector` 版本向所有客户端推送统计帧(4字节头"STAT" + JSON字符串)。 + +```cpp +class EchoService { + public: + EchoService(Loop *wp_loop) + : ws_srv_(wp_loop) + , stat_timer_(wp_loop->newTimerEvent()) + { } + + bool initialize(Server *http_srv, const std::string &url_path) + { + if (!ws_srv_.initialize(http_srv, url_path)) + return false; + + ws_srv_.setBinaryMessageCallback([this](const WsServer::ConnToken &token, std::vector &&data) { + onBinaryMessage(token, std::move(data)); + }); + ws_srv_.setTextMessageCallback([this](const WsServer::ConnToken &token, std::string &&text) { + //! 此服务仅接收二进制帧 + ws_srv_.send(token, "此服务仅接收二进制帧"); + }); + + //! 定时器:每 5 秒推送统计帧 + stat_timer_->initialize(std::chrono::milliseconds(5000), Event::Mode::kPersist); + stat_timer_->setCallback([this] { onStatTimer(); }); + + return true; + } + + private: + void onBinaryMessage(const WsServer::ConnToken &token, std::vector &&data) + { + //! 二进制帧:原样回传 + ws_srv_.send(token, data); + } + + void onStatTimer() + { + //! 构建二进制统计帧:4字节头"STAT" + JSON + std::vector stat_data; + stat_data.insert(stat_data.end(), kStatHeader, kStatHeader + 4); + stat_data.insert(stat_data.end(), json.begin(), json.end()); + + for (const auto &token : conns_) + ws_srv_.send(token, stat_data); + } +}; +``` + +### 客户端:聊天客户端 + +> 完整示例见 `examples/websocket/chat_client/` + +演示 WebSocket 客户端连接到聊天服务器,从标准输入读取文本发送,并接收服务器推送的消息。 + +```cpp +#include + +int main() +{ + auto sp_loop = Loop::New(); + + WsClient ws_client(sp_loop); + ws_client.initialize(SockAddr::FromString("127.0.0.1:8080"), "/ws/chat-1"); + + ws_client.setConnectedCallback([&] { + std::cout << "已连接!请输入用户名:" << std::endl; + //! 启动标准输入读取 + sp_stdin_event->enable(); + }); + + ws_client.setTextMessageCallback([&](std::string &&text) { + std::cout << text << std::endl; + }); + + //! 设置二次退避重连策略 + ws_client.setReconnectDelayCalcFunc([](int fail_count) { + return 1 << std::min(4, fail_count); + }); + + ws_client.start(); + + //! ... 运行事件循环、处理 SIGINT、清理 ... +} +``` + +### 服务端:URL 路径匹配 + +```cpp +//! 前缀匹配:匹配 /ws/ 及其下的所有路径 +ws_srv.initialize(&http_srv, "/ws/"); + +//! 全量匹配:仅匹配 /ws +ws_srv.initialize(&http_srv, "/ws"); + +//! 匹配所有:匹配所有 WebSocket 升级请求 +ws_srv.initialize(&http_srv, ""); +``` + +### 服务端:上下文数据 + +为客户端连接绑定自定义数据,实现每个连接的状态追踪: + +```cpp +ws_srv.setConnectedCallback([](const WsServer::ConnToken &token) { + //! 绑定用户会话对象 + auto session = new UserSession(); + ws_srv.setContext(token, session, [](void *p) { delete static_cast(p); }); +}); + +ws_srv.setTextMessageCallback([](const WsServer::ConnToken &token, std::string &&text) { + //! 获取会话数据 + auto session = static_cast(ws_srv.getContext(token)); + if (session != nullptr) { + //! ... 使用会话数据处理消息 ... + } +}); +``` + +### 客户端:自定义重连延迟 + +```cpp +//! 指数退避:1秒, 2秒, 4秒, 8秒, ... 最大16秒 +ws_client.setReconnectDelayCalcFunc([](int fail_count) { + return 1 << std::min(4, fail_count); +}); + +//! 禁用自动重连 +ws_client.setAutoReconnect(false); +``` + +## 压缩(RFC 7692 permessage-deflate) + +websocket 模块支持 RFC 7692 定义的 `permessage-deflate` 压缩扩展。启用后,WebSocket 文本帧和二进制帧使用 DEFLATE (zlib) 压缩,显著减少带宽占用,尤其适用于重复性或大数据量的消息。 + +### 工作原理 + +1. **服务端**:在 `initialize()` 之前调用 `setCompressionEnable(true)`。若客户端在握手中请求了 `permessage-deflate`(通过 `Sec-WebSocket-Extensions: permessage-deflate` 头部),服务端在 101 响应中同意压缩。否则不使用压缩。 + +2. **客户端**:在 `initialize()` 之前调用 `setCompressionPrefer(true)`。客户端在握手中请求压缩扩展。若服务端同意,帧将压缩/解压;若服务端拒绝,通信继续不压缩。 + +3. **帧格式**:压缩数据帧的首帧设置 RSV1 位。控制帧(Close/Ping/Pong)永远不压缩。 + +4. **实现方式**:使用 raw DEFLATE,按 RFC 7692 Section 7.2.2 规则去除 4 字节尾部。每条消息独立压缩(no_context_takeover 模式),简化实现并确保兼容性。 + +### WsCompressionConfig — 压缩配置 + +```cpp +#include + +struct WsCompressionConfig { + bool enabled = false; //!< 是否启用压缩 + bool no_context_takeover = true; //!< 不跨消息保留 zlib 上下文 + int max_window_bits = 15; //!< 最大窗口位数 (8~15) +}; +``` + +### 服务端:启用压缩 + +```cpp +WsServer ws_srv(sp_loop); +ws_srv.setCompressionEnable(true); //! 允许压缩(在 initialize 之前调用) +ws_srv.initialize(&http_srv, "/ws/chat"); +``` + +### 客户端:偏好压缩 + +```cpp +WsClient ws_client(sp_loop); +ws_client.setCompressionPrefer(true); //! 请求压缩(在 initialize 之前调用) +ws_client.initialize(SockAddr::FromString("127.0.0.1:8080"), "/ws/chat"); +``` + +### 混合服务(部分路由压缩,部分不压缩) + +```cpp +//! 聊天室启用压缩 +WsServer ws_srv_compressed(sp_loop); +ws_srv_compressed.setCompressionEnable(true); +ws_srv_compressed.initialize(&http_srv, "/ws/chat"); + +//! Echo 服务不压缩 +WsServer ws_srv_plain(sp_loop); +ws_srv_plain.initialize(&http_srv, "/ws/echo"); +``` + +### 重要:压缩协商 + +- 压缩是**可选的**,在 HTTP Upgrade 握手阶段按连接协商。 +- 若任一方不支持或拒绝压缩,帧将不压缩发送——对功能无任何影响。 +- `WsFrame` 的 `rsv1` 字段标识接收到的帧是否被压缩。解压后 `rsv1` 被清除,用户回调中收到的 payload 是原始数据,透明无感。 +- 压缩失败时优雅回退:若压缩或解压失败,系统回退到不压缩模式或报告错误。 + +## 常见场景 + +1. **实时推送**:将 WsServer 挂载到 HTTP 服务器上,向浏览器客户端推送实时数据 +2. **聊天室**:同一 HTTP 服务器上不同 URL 路径承载多个聊天室 +3. **二进制数据流**:回传二进制帧、发送带头部标识的结构化二进制数据 +4. **IoT 设备通信**:客户端连接到服务器,发送状态更新并接收指令 +5. **服务端心跳**:服务端发送 Ping 帧,客户端自动回复 Pong +6. **客户端自动重连**:断线后按指数退避策略自动重连 +7. **HTTP + WebSocket 混合**:HTTP 提供 REST API 和静态页面;WebSocket 处理实时通信 +8. **压缩通信**:启用 permessage-deflate 减少文本/二进制数据的带宽占用 + +## 注意事项 + +1. **WsServer 是 HTTP 中间件**:必须关联 `http::server::Server` 并在 HTTP 服务器启动前/后注册。WsServer 通过 `http_server->use()` 将自身注册为中间件。 +2. **URL 路径匹配**:注意 `url_path` 是否以 `/` 结尾——决定前缀匹配还是全量匹配。空字符串匹配所有升级请求。 +3. **ConnToken 操作**:所有客户端操作使用 `ConnToken`(cabinet::Token),而非指针。确保连接销毁后安全访问。 +4. **Ping/Pong 自动回复**:WsServer 和 Client 在收到 Ping 帧时均自动回复 Pong 帧。 +5. **Close 帧自动回复**:双方收到 Close 帧后自动回复 Close 帧,随后等待 TCP 断开。 +6. **客户端帧掩码**:RFC 6455 规定客户端发送的所有帧必须掩码,服务端帧不掩码。 +7. **客户端自动重连**:默认启用。断开后按配置的延迟策略自动重连。通过 `setReconnectDelayCalcFunc()` 自定义延迟。 +8. **客户端握手**:Client 自动执行 HTTP Upgrade 握手,验证 101 响应、Sec-WebSocket-Accept、Upgrade/Connection 头部。握手失败时若自动重连已启用则自动重连。 +9. **生命周期顺序**:WsServer 和 Client 均须遵循 initialize → start → stop → cleanup 顺序。 +10. **线程安全**:所有回调在 Loop 线程中执行,跨线程操作须通过 `runInLoop()` 回到主线程。 +11. **上下文数据**:WsServer 的 `setContext()/getContext()` 委托给底层 TcpConnection。在回调中可访问上下文数据,但连接断开后 `getContext()` 返回 `nullptr`。 +12. **压缩**:在 WsServer 上调用 `setCompressionEnable(true)` 或在 WsClient 上调用 `setCompressionPrefer(true)` **必须在 `initialize()` 之前**。压缩按连接协商;若对方不支持,帧将不压缩发送,不影响功能。 +13. **压缩回退**:若压缩/解压失败,系统打印警告并回退到不压缩发送。解压失败会触发错误回调。 +14. **分片接收**:WsServer 和 WsClient 在内部缓存分片数据。只有接收完整消息(所有分片、fin=true)并解压后才回调业务层,回调中永远不会收到部分分片数据。 +15. **分片发送**:当发送数据大于 `fragment_size` 时,自动分片发送。首帧携带原始 opcode 与 rsv1(如压缩),后续帧为 kContinue。调用 `setFragmentSize(size)` 配置分片大小(默认65535,设为0禁用分片),必须在 `initialize()` 之前。 +16. **send(const char\*)**:WsServer 和 WsClient 都提供 `send(const char *str)` 重载,发送文本帧时不构造 std::string 中间对象,正确使用 kText opcode。 + +## 相关模块 + +- **http**:WsServer 运行在 http::server::Server 之上,作为 HTTP 中间件 +- **event**:Server 和 Client 基于 Loop 运行 +- **network**:通过 TcpConnector(客户端)和 TcpConnection 实现连接管理 +- **crypto**:SHA1 计算 Sec-WebSocket-Accept +- **base**:提供 Cabinet 用于连接生命周期管理 diff --git a/examples/http/client/Makefile b/examples/http/client/Makefile new file mode 100644 index 00000000..5e66fcd9 --- /dev/null +++ b/examples/http/client/Makefile @@ -0,0 +1,26 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2026 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +all test clean distclean: + @for i in $(shell ls) ; do \ + if [ -d $$i ]; then \ + $(MAKE) -C $$i $@ || exit $$? ; \ + fi \ + done diff --git a/examples/http/client/simple/Makefile b/examples/http/client/simple/Makefile new file mode 100644 index 00000000..33388abf --- /dev/null +++ b/examples/http/client/simple/Makefile @@ -0,0 +1,37 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2026 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +PROJECT := examples/http/client/simple +EXE_NAME := ${PROJECT} + +CPP_SRC_FILES := simple.cpp + +CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) +LDFLAGS += \ + -ltbox_http \ + -ltbox_network \ + -ltbox_eventx \ + -ltbox_event \ + -ltbox_log \ + -ltbox_util \ + -ltbox_base \ + -lpthread -ldl + +include $(TOP_DIR)/mk/exe_common.mk diff --git a/examples/http/client/simple/simple.cpp b/examples/http/client/simple/simple.cpp new file mode 100644 index 00000000..8db4b44d --- /dev/null +++ b/examples/http/client/simple/simple.cpp @@ -0,0 +1,122 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::http; +using namespace tbox::http::client; + +int main(int argc, char **argv) +{ + std::string server_addr = "127.0.0.1:12345"; + + if (argc == 2) { + server_addr = argv[1]; + } + + LogOutput_Enable(); + + LogInfo("enter"); + + auto sp_loop = Loop::New(); + auto sp_sig_event = sp_loop->newSignalEvent(); + + SetScopeExitAction( + [=] { + delete sp_sig_event; + delete sp_loop; + } + ); + + sp_sig_event->initialize(SIGINT, Event::Mode::kPersist); + sp_sig_event->enable(); + + Client http_client(sp_loop); + if (!http_client.initialize(network::SockAddr::FromString(server_addr))) { + LogErr("init http_client fail"); + return 0; + } + + http_client.setAutoReconnect(true); + http_client.setRequestTimeout(std::chrono::seconds(10)); + //http_client.setContextLogEnable(true); //! 调试时需要看详细收发数据时可以打开 + + http_client.setConnectedCallback( + [] { + LogInfo("connected to server"); + } + ); + http_client.setDisconnectedCallback( + [] { + LogInfo("disconnected from server"); + } + ); + + http_client.start(); + + //! 简单 GET 请求 + http_client.request(Method::kGet, "/", + [](const Respond &res) { + LogInfo("GET / => status: %d, body: %s", + (int)res.status_code, res.body.c_str()); + }); + + //! POST 请求 + http_client.request(Method::kPost, "/api/data", + "{\"key\":\"value\"}", + {{"Content-Type", "application/json"}}, + [](const Respond &res) { + LogInfo("POST /api/data => status: %d", + (int)res.status_code); + }); + + //! 完整 Request 对象 + Request req; + req.method = Method::kPut; + req.http_ver = HttpVer::k1_1; + req.url.path = "/api/update"; + req.headers["Content-Type"] = "application/json"; + req.body = "{\"id\":123}"; + http_client.request(req, + [](const Respond &res) { + LogInfo("PUT /api/update => status: %d", + (int)res.status_code); + }); + + sp_sig_event->setCallback( + [&] (int) { + http_client.stop(); + sp_loop->exitLoop(); + } + ); + + LogInfo("start"); + sp_loop->runLoop(); + LogInfo("stop"); + http_client.cleanup(); + + LogInfo("exit"); + return 0; +} diff --git a/examples/http/server/https_simple/Makefile b/examples/http/server/https_simple/Makefile new file mode 100644 index 00000000..0e345fc3 --- /dev/null +++ b/examples/http/server/https_simple/Makefile @@ -0,0 +1,39 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. +# + +PROJECT := examples/http/server/https_simple +EXE_NAME := ${PROJECT} + +CPP_SRC_FILES := https_simple.cpp + +CONF_FILES := server.crt server.key + +CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) +LDFLAGS += \ + -ltbox_http \ + -ltbox_network \ + -Wl,--whole-archive -ltbox_network_tls -Wl,--no-whole-archive \ + -ltbox_eventx \ + -ltbox_event \ + -ltbox_log \ + -ltbox_util \ + -ltbox_base \ + -lssl -lcrypto \ + -lpthread -ldl + +include $(TOP_DIR)/mk/exe_common.mk diff --git a/examples/http/server/https_simple/https_simple.cpp b/examples/http/server/https_simple/https_simple.cpp new file mode 100644 index 00000000..bc7487b1 --- /dev/null +++ b/examples/http/server/https_simple/https_simple.cpp @@ -0,0 +1,134 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +/** + * HTTPS 版 simple server 示例 + * 用法:https_simple --cert --key + * 必须指定证书文件和密钥文件 + */ + +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace tbox; +using namespace tbox::event; +using namespace tbox::http; +using namespace tbox::http::server; +using namespace tbox::network; + +void PrintUsage(const char *prog) +{ + cout << "Usage: " << prog << " --cert --key " << endl + << "Exp : " << prog << " 0.0.0.0:12345 --cert server.crt --key server.key" << endl; +} + +int main(int argc, char **argv) +{ + string bind_addr_str = "0.0.0.0:12345"; + string cert_file; + string key_file; + + //! 解析命令行参数 + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--cert") == 0 && i + 1 < argc) { + cert_file = argv[++i]; + } else if (strcmp(argv[i], "--key") == 0 && i + 1 < argc) { + key_file = argv[++i]; + } else if (argv[i][0] != '-') { + bind_addr_str = argv[i]; + } else { + cerr << "Error: invalid option `" << argv[i] << "'" << endl; + PrintUsage(argv[0]); + return 0; + } + } + + if (cert_file.empty() || key_file.empty()) { + PrintUsage(argv[0]); + return 0; + } + + LogOutput_Enable(); + + LogInfo("enter"); + + auto sp_loop = Loop::New(); + auto sp_sig_event = sp_loop->newSignalEvent(); + + SetScopeExitAction( + [=] { + delete sp_sig_event; + delete sp_loop; + } + ); + + sp_sig_event->initialize(SIGINT, Event::Mode::kPersist); + sp_sig_event->enable(); + + Server srv(sp_loop); + + //! 设置 TLS 配置(必须在 initialize() 之前调用) + TlsConfig tls_config; + tls_config.cert_file = cert_file; + tls_config.key_file = key_file; + tls_config.verify_peer = false; //! 测试环境不验证 client 证书 + if (!srv.setTlsConfig(tls_config)) { + LogErr("set tls config fail, need network_tls module"); + return 0; + } + + if (!srv.initialize(SockAddr::FromString(bind_addr_str), 1)) { + LogErr("init srv fail"); + return 0; + } + + srv.start(); + //srv.setContextLogEnable(true); //! 调试时需要看详细收发数据时可以打开 + + //! 添加请求处理 + srv.use( + [&](ContextSptr ctx, const NextFunc &next) { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().body = "Hello HTTPS!"; + } + ); + + sp_sig_event->setCallback( + [&] (int) { + srv.stop(); + sp_loop->exitLoop(); + } + ); + + LogInfo("start"); + sp_loop->runLoop(); + LogInfo("stop"); + srv.cleanup(); + + LogInfo("exit"); + return 0; +} diff --git a/examples/http/server/https_simple/server.crt b/examples/http/server/https_simple/server.crt new file mode 100644 index 00000000..aafbc0be --- /dev/null +++ b/examples/http/server/https_simple/server.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDiTCCAnGgAwIBAgIUfLUbGuJjk5Wg1+v6hnOVyfPJrpEwDQYJKoZIhvcNAQEL +BQAwWDELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAOBgNVBAcMB0Jl +aWppbmcxETAPBgNVBAoMCGNwcC10Ym94MRIwEAYDVQQDDAkxMjcuMC4wLjEwHhcN +MjYwNjI2MTMzMjQ5WhcNMzYwNjIzMTMzMjQ5WjBYMQswCQYDVQQGEwJDTjEQMA4G +A1UECAwHQmVpamluZzEQMA4GA1UEBwwHQmVpamluZzERMA8GA1UECgwIY3BwLXRi +b3gxEjAQBgNVBAMMCTEyNy4wLjAuMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALlvcZ7VBhp0S3tB1ho2Qb0qCHQ8Ezdf3X3OOfviXBPg4IogXZSWy0RM +CBbP7PaxpksX9DHVtsjydss1vb3ts+DU4vFtc57wq8Sf+NQ7xvGJffH12BuYUjyn +1cE0ENdzv7FuruZ1Q/c2wtll2WwL5Bo+ggqyF7Cr+Ja/reHEN1eN26oLtTJQcph0 +YCbJJlmJjLO51sDmweaNe9i8Ck7y7EHypL1ecMMMyhnsTavZmntNavIPdX9CnB9d +MPsQT/yG4FUBve3ZOVL8+MawQ/bbiAlTPYuEjiooBJiZD2+6GmbMfu3tVO/yA+Wg +zLPv7ANNK70H/rysscgyxhct90vzfGUCAwEAAaNLMEkwGgYDVR0RBBMwEYcEfwAA +AYIJbG9jYWxob3N0MAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFK18uhz4KeDIsASC +2wxeJTR1U2SIMA0GCSqGSIb3DQEBCwUAA4IBAQB7W6ZOA3Uun+6THd01l3JlQqk7 +i/oTc9bUterTvc/tI1Quc349pZdVI1d0GkhyFRNMpTF1D5Cph5OkmfugFDsP9a62 +Gjg2CQ+H93WeEDZ5nLJpNQDPwVyMqM3dORPhVHx7S5IEj7vi8q5ko3MIl6L5wGdy +x7cyGxsgFj8INU651rtB8G27B1HL4yQW+Ix1wq/xlh/Lg+5/lIoC6MiywE5+UyMz +FOTETFuI4Zs+vg5Q36fm/hJ+WdLS4gifarY2blKQMZBcegkp0/DnUmAxXEz66zl3 +UPppc85X5/qPRO1GMRgadPxi6JNvha+m5CyCMTvaggVCvOiBpfn3f78fUMDc +-----END CERTIFICATE----- diff --git a/examples/http/server/https_simple/server.key b/examples/http/server/https_simple/server.key new file mode 100644 index 00000000..a9b48be0 --- /dev/null +++ b/examples/http/server/https_simple/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5b3Ge1QYadEt7 +QdYaNkG9Kgh0PBM3X919zjn74lwT4OCKIF2UlstETAgWz+z2saZLF/Qx1bbI8nbL +Nb297bPg1OLxbXOe8KvEn/jUO8bxiX3x9dgbmFI8p9XBNBDXc7+xbq7mdUP3NsLZ +ZdlsC+QaPoIKshewq/iWv63hxDdXjduqC7UyUHKYdGAmySZZiYyzudbA5sHmjXvY +vApO8uxB8qS9XnDDDMoZ7E2r2Zp7TWryD3V/QpwfXTD7EE/8huBVAb3t2TlS/PjG +sEP224gJUz2LhI4qKASYmQ9vuhpmzH7t7VTv8gPloMyz7+wDTSu9B/68rLHIMsYX +LfdL83xlAgMBAAECggEAI3Hg5vpTC1V1ZB8GfMYoNK9HJGijR69kV/rGbJYtAYO3 +h89987wLKIfb9/hQlCsK3Um73Ja8NJbcDCW+mgJIos4ufvVr51Kbkp79Yhv3AA5G +66wRXdz0wzFVk3OPUI+IcbL1bYm2rxdhkUp9j8CKHlYaZ075ZkTI5I/I/eGSroJU +nwSBgvZQnS27+qH0Cw/JC/80q3vcAKJb9eZNcnKtrHo1SIahu7yO+zGAOXF80vA2 +9fSEda0DZhjkP0tuwX/jhYRz33i8um++CQ0irD+Oe5voRpPsMUQbgqQkJTl5LL93 +HjaMVJTOnhMLkprARm0E0RkGJ+T8zo9BryQHm57I1QKBgQDLRaKQs9OqXDmimb+b +O0isGGG8Rw0RMaIXAYunF/K+8hQ1ZMIK01yE+bRpASAK9mAWOKfpqyEkz3hWoetp +ESXIMa32Phmg19EPakfxbd/bhhPd1ce08M9XAilPECXcr5xyDVuxVnWo9MisZJhE +49vMtZjxp8ZBUlJzoGmyli6lVwKBgQDpiVssjoM/ocSjbSiiDAH5PIl7t1S+vG65 +F6Np9rozvwBiQI9F5+BHlU5SwYmCrUREZHvUngHEvcWJxAz4eak2RchbzqKNXEiI +VfI/6QGqxbJuG3adfHOd+tMYMv7ttGU06uqQl3B6WmcAIW/Utiv3UI557LhnRgjc +pKFuLnm6owKBgG9QjeqyH3qOkJ1jltL6Txy3KWaCfjxpMrtohEKX0b4RMVHgAIcP +If5MBCjwjcyTCSGCGynSJg9TcjH278SUuFz+H6bWcRBsvzay2/zxT4KW1PBJbti+ +erzKGTcLv8AvhvvKJulhUIOasP3/BIfNRAPBeqTzXJVO8IoTUW6T4a13AoGAJG65 +OopBD3w9IQG2hRE6fZdkG1jOb7MV0upNJArJoaj6dll8AHvcEU7JmT94JFrDe6fx +aYn83KR+XK+pFlpke4MHbssdsM/kwOAnmrDPAcU1wNen+Ymgv9SRegT6oDq0Tz0W +utflRDE2QF73A0goM7ztfTfgzLuwRjuos3espeECgYEAqbm17B0UZolBVaPOBRuT +a+gYAOUxZpYCi+ajLFcDa67geLgCNTp8EJj/uwKsVCECGgDT5pJLtAnfeNQvYrfq +IN7I12L4L+PMn5Tv5E7HevWBknjd87mujS9YymGdXkBvQHWv9oEq1du0qHrL9tLx +pr+LuwJVR1QmxvWliRSCeew= +-----END PRIVATE KEY----- diff --git a/examples/http/server/sse/Makefile b/examples/http/server/sse/Makefile new file mode 100644 index 00000000..38adaa2d --- /dev/null +++ b/examples/http/server/sse/Makefile @@ -0,0 +1,37 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2026 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +PROJECT := examples/http/server/sse +EXE_NAME := ${PROJECT} + +CPP_SRC_FILES := push.cpp + +CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) +LDFLAGS += \ + -ltbox_http \ + -ltbox_network \ + -ltbox_eventx \ + -ltbox_event \ + -ltbox_log \ + -ltbox_util \ + -ltbox_base \ + -lpthread -ldl + +include $(TOP_DIR)/mk/exe_common.mk diff --git a/examples/http/server/sse/push.cpp b/examples/http/server/sse/push.cpp new file mode 100644 index 00000000..6ffe6f68 --- /dev/null +++ b/examples/http/server/sse/push.cpp @@ -0,0 +1,200 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +/** + * SSE 定时推送示例 + * + * 功能: + * - 创建 HTTP 服务器,挂载 SSE 服务到 /sse/events + * - 每 5 秒向所有 SSE 客户端推送当前时间事件 + * - SSE 连接自动心跳(每 15 秒发送注释行) + * - HTTP 主页提供浏览器 EventSource 客户端代码 + * - Ctrl+C 优雅退出 + * + * 用法: + * ./sse_push [bind_addr] + * 示例: ./sse_push 0.0.0.0:8080 + * 浏览器访问: http://127.0.0.1:8080/ + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::http; +using namespace tbox::http::server; +using namespace tbox::http::sse; + +//! 获取当前时间字符串 +static std::string getTimeString() +{ + auto now = std::chrono::system_clock::now(); + auto time_t_now = std::chrono::system_clock::to_time_t(now); + char buf[64]; + std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", std::localtime(&time_t_now)); + return buf; +} + +//! HTML 主页:包含 EventSource 客户端 JavaScript +static const std::string kIndexHtml = + "\n" + "SSE Push Demo\n" + "\n" + "

SSE Push Demo

\n" + "
\n" + "\n" + "\n"; + +int main(int argc, char **argv) +{ + std::string bind_addr = "0.0.0.0:8080"; + + if (argc == 2) + bind_addr = argv[1]; + + LogOutput_Enable(); + + LogInfo("enter"); + + auto sp_loop = Loop::New(); + auto sp_sig_event = sp_loop->newSignalEvent(); + + //! 心跳定时器 + auto sp_push_timer = sp_loop->newTimerEvent(); + + SetScopeExitAction( + [=] { + delete sp_push_timer; + delete sp_sig_event; + delete sp_loop; + } + ); + + //! 创建 HTTP 服务器 + Server http_srv(sp_loop); + if (!http_srv.initialize(network::SockAddr::FromString(bind_addr), 1)) { + LogErr("init http server fail"); + return 0; + } + //http_srv.setContextLogEnable(true); + + //! 创建 SSE 服务,挂载到 /sse/events + SseServer sse_srv(sp_loop); + if (!sse_srv.initialize(&http_srv, "/sse/events")) { + LogErr("init sse server fail"); + return 0; + } + //sse_srv.setContextLogEnable(true); + + //! 设置自动心跳(每 15 秒发送注释行,保持连接活跃) + sse_srv.setHeartbeatInterval(std::chrono::seconds(15)); + + //! 设置 SSE 连接回调 + sse_srv.setConnectedCallback([&](const SseServer::ConnToken &token) { + auto addr = sse_srv.peerAddr(token); + LogInfo("sse client connected from %s", addr.toString().c_str()); + + //! 向新连接发送欢迎消息 + sse_srv.send(token, "Welcome! SSE connection established."); + }); + + sse_srv.setDisconnectedCallback([](const SseServer::ConnToken &token) { + LogInfo("sse client disconnected"); + }); + + //! 定时推送:每 5 秒向所有客户端推送当前时间 + int event_id = 0; + sp_push_timer->initialize(std::chrono::seconds(5), Event::Mode::kPersist); + sp_push_timer->setCallback([&] { + //! 构造 SSE 事件 + SseEvent evt; + evt.id = std::to_string(++event_id); + evt.event = "tick"; + evt.data = "{\"time\":\"" + getTimeString() + "\",\"id\":" + std::to_string(event_id) + "}"; + + //! 向所有客户端推送 + sse_srv.sendToAll(evt); + LogDbg("push event id:%d to clients", event_id); + }); + + //! 添加 HTTP 主页处理 + http_srv.use( + [&](ContextSptr ctx, const NextFunc &next) { + if (ctx->req().url.path == "/") { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().headers["Content-Type"] = "text/html; charset=utf-8"; + ctx->res().body = kIndexHtml; + return; + } + next(); + } + ); + + //! 启动服务 + http_srv.start(); + sse_srv.start(); + sp_push_timer->enable(); + + //! Ctrl+C 退出 + sp_sig_event->initialize(SIGINT, Event::Mode::kPersist); + sp_sig_event->enable(); + sp_sig_event->setCallback( + [&] (int) { + LogInfo("stopping..."); + sp_push_timer->disable(); + sse_srv.stop(); + http_srv.stop(); + sp_loop->exitLoop(); + } + ); + + LogInfo("start, listen at %s", bind_addr.c_str()); + sp_loop->runLoop(); + LogInfo("stop"); + + sse_srv.cleanup(); + http_srv.cleanup(); + + LogInfo("exit"); + return 0; +} diff --git a/examples/network/tcp_acceptor/tcp_echo/tcp_echo.cpp b/examples/network/tcp_acceptor/tcp_echo/tcp_echo.cpp index 6dd2ea2d..b4ff0b13 100644 --- a/examples/network/tcp_acceptor/tcp_echo/tcp_echo.cpp +++ b/examples/network/tcp_acceptor/tcp_echo/tcp_echo.cpp @@ -23,7 +23,7 @@ #include -#include +#include #include #include @@ -62,7 +62,7 @@ int main(int argc, char **argv) set conns; - TcpAcceptor acceptor(sp_loop); + TcpRawAcceptor acceptor(sp_loop); acceptor.initialize(bind_addr, 1); //! 指定有Client连接上了该做的事务 acceptor.setNewConnectionCallback( diff --git a/examples/network/tcp_acceptor/tcp_nc_server/tcp_nc_server.cpp b/examples/network/tcp_acceptor/tcp_nc_server/tcp_nc_server.cpp index fec1b762..7e4e8afe 100644 --- a/examples/network/tcp_acceptor/tcp_nc_server/tcp_nc_server.cpp +++ b/examples/network/tcp_acceptor/tcp_nc_server/tcp_nc_server.cpp @@ -23,7 +23,7 @@ #include -#include +#include #include #include @@ -75,7 +75,7 @@ int main(int argc, char **argv) }, 0 ); - TcpAcceptor acceptor(sp_loop); + TcpRawAcceptor acceptor(sp_loop); acceptor.initialize(bind_addr, 1); //! 指定有Client连接上了该做的事务 acceptor.setNewConnectionCallback( diff --git a/examples/network/tcp_client/tls_echo_client/Makefile b/examples/network/tcp_client/tls_echo_client/Makefile new file mode 100644 index 00000000..5ac11e20 --- /dev/null +++ b/examples/network/tcp_client/tls_echo_client/Makefile @@ -0,0 +1,34 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. +# + +PROJECT := examples/network/tcp_client/tls_echo_client +EXE_NAME := ${PROJECT} + +CPP_SRC_FILES := tls_echo_client.cpp + +CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) +LDFLAGS += \ + -ltbox_network \ + -Wl,--whole-archive -ltbox_network_tls -Wl,--no-whole-archive \ + -ltbox_event \ + -ltbox_util \ + -ltbox_base \ + -lssl -lcrypto \ + -ldl + +include $(TOP_DIR)/mk/exe_common.mk diff --git a/examples/network/tcp_client/tls_echo_client/tls_echo_client.cpp b/examples/network/tcp_client/tls_echo_client/tls_echo_client.cpp new file mode 100644 index 00000000..a97ebc0f --- /dev/null +++ b/examples/network/tcp_client/tls_echo_client/tls_echo_client.cpp @@ -0,0 +1,141 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +/** + * TLS 版 echo client 示例 + * 用法:tls_echo_client [--ca ] [--hostname ] + * 连接成功后,stdin 输入的内容会发送到 server,server 返回的数据会显示在 stdout + */ + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +using namespace std; +using namespace tbox; +using namespace tbox::event; +using namespace tbox::network; + +void PrintUsage(const char *prog) +{ + cout << "Usage: " << prog << " [--ca ] [--hostname ]" << endl + << "Exp : " << prog << " 127.0.0.1:12345" << endl + << " " << prog << " 127.0.0.1:12345 --ca server.crt --hostname myserver" << endl; +} + +int main(int argc, char **argv) +{ + string server_addr_str; + string ca_file; + string hostname = "127.0.0.1"; + + //! 解析命令行参数 + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--ca") == 0 && i + 1 < argc) { + ca_file = argv[++i]; + } else if (strcmp(argv[i], "--hostname") == 0 && i + 1 < argc) { + hostname = argv[++i]; + } else if (argv[i][0] != '-') { + server_addr_str = argv[i]; + } else { + cerr << "Error: invalid option `" << argv[i] << "'" << endl; + PrintUsage(argv[0]); + return 0; + } + } + + if (server_addr_str.empty()) { + PrintUsage(argv[0]); + return 0; + } + + LogOutput_Enable(); + + SockAddr server_addr = SockAddr::FromString(server_addr_str); + + Loop *sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + StdioStream stdio(sp_loop); + stdio.initialize(); + + TcpClient client(sp_loop); + + //! 设置 TLS 配置(必须在 initialize 之前调用) + TlsConfig tls_config; + tls_config.hostname = hostname; //! SNI hostname + if (!ca_file.empty()) { + tls_config.ca_file = ca_file; + tls_config.verify_peer = true; + } else { + tls_config.verify_peer = false; //! 未指定 CA 证书时,不做对端验证 + } + if (!client.setTlsConfig(tls_config)) { + LogErr("set tls config fail, need network_tls module"); + return 0; + } + + client.initialize(server_addr); + + //! 连接成功后,绑定 stdio 和 client 的双向数据流 + client.setConnectedCallback( + [&client, &stdio] { + cout << "connected!" << endl; + stdio.enable(); //! 连接成功后才启用 stdin 读取 + client.bind(&stdio); //! server 的数据往终端输出 + stdio.bind(&client); //! 终端上的输入往 server 输出 + } + ); + + client.setDisconnectedCallback( + [&client, &stdio] { + cout << "disconnected!" << endl; + stdio.unbind(); + client.unbind(); + } + ); + + client.start(); + + //! 注册 ctrl+C 停止信号 + SignalEvent *sp_stop_ev = sp_loop->newSignalEvent(); + SetScopeExitAction([sp_stop_ev] { delete sp_stop_ev; }); + sp_stop_ev->initialize(SIGINT, Event::Mode::kOneshot); + sp_stop_ev->setCallback( + [sp_loop, &client] (int) { + client.stop(); + sp_loop->exitLoop(); + } + ); + sp_stop_ev->enable(); + + LogInfo("tls echo client running ..."); + sp_loop->runLoop(); + LogInfo("tls echo client stopped"); + + return 0; +} diff --git a/examples/network/tcp_connector/tcp_echo/tcp_echo.cpp b/examples/network/tcp_connector/tcp_echo/tcp_echo.cpp index 8573ccbe..3889ec9c 100644 --- a/examples/network/tcp_connector/tcp_echo/tcp_echo.cpp +++ b/examples/network/tcp_connector/tcp_echo/tcp_echo.cpp @@ -27,7 +27,7 @@ #include -#include +#include #include #include @@ -62,7 +62,7 @@ int main(int argc, char **argv) TcpConnection *sp_curr = nullptr; - TcpConnector connector(sp_loop); + TcpRawConnector connector(sp_loop); connector.initialize(bind_addr); //! 指定有Client连接上后该做的事务 connector.setConnectedCallback( diff --git a/examples/network/tcp_connector/tcp_nc_client/tcp_nc_client.cpp b/examples/network/tcp_connector/tcp_nc_client/tcp_nc_client.cpp index c5bddc90..33e20581 100644 --- a/examples/network/tcp_connector/tcp_nc_client/tcp_nc_client.cpp +++ b/examples/network/tcp_connector/tcp_nc_client/tcp_nc_client.cpp @@ -27,7 +27,7 @@ #include -#include +#include #include #include @@ -67,7 +67,7 @@ int main(int argc, char **argv) TcpConnection *sp_curr = nullptr; - TcpConnector connector(sp_loop); + TcpRawConnector connector(sp_loop); connector.initialize(bind_addr); //! 指定有Client连接上后该做的事务 connector.setConnectedCallback( diff --git a/examples/network/tcp_server/tcp_echo/Makefile b/examples/network/tcp_server/tcp_echo_server/Makefile similarity index 89% rename from examples/network/tcp_server/tcp_echo/Makefile rename to examples/network/tcp_server/tcp_echo_server/Makefile index 34dd0473..e35b9bae 100644 --- a/examples/network/tcp_server/tcp_echo/Makefile +++ b/examples/network/tcp_server/tcp_echo_server/Makefile @@ -18,10 +18,10 @@ # of the source tree. # -PROJECT := examples/network/tcp_server/tcp_echo +PROJECT := examples/network/tcp_server/tcp_echo_server EXE_NAME := ${PROJECT} -CPP_SRC_FILES := tcp_echo.cpp +CPP_SRC_FILES := tcp_echo_server.cpp CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) LDFLAGS += \ diff --git a/examples/network/tcp_server/tcp_echo/tcp_echo.cpp b/examples/network/tcp_server/tcp_echo_server/tcp_echo_server.cpp similarity index 94% rename from examples/network/tcp_server/tcp_echo/tcp_echo.cpp rename to examples/network/tcp_server/tcp_echo_server/tcp_echo_server.cpp index 74092b1e..2295e067 100644 --- a/examples/network/tcp_server/tcp_echo/tcp_echo.cpp +++ b/examples/network/tcp_server/tcp_echo_server/tcp_echo_server.cpp @@ -61,6 +61,8 @@ int main(int argc, char **argv) //! 当收到数据时,直接往client指定对象发回去 server.setReceiveCallback( [&server] (const TcpServer::ConnToken &client, Buffer &buff) { + std::string text((const char*)buff.readableBegin(), buff.readableSize()); + LogInfo("len:%u, text:%s", text.size(), text.c_str()); server.send(client, buff.readableBegin(), buff.readableSize()); buff.hasReadAll(); }, 0 diff --git a/examples/network/tcp_server/tls_echo_server/Makefile b/examples/network/tcp_server/tls_echo_server/Makefile new file mode 100644 index 00000000..ff551dfb --- /dev/null +++ b/examples/network/tcp_server/tls_echo_server/Makefile @@ -0,0 +1,36 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. +# + +PROJECT := examples/network/tcp_server/tls_echo_server +EXE_NAME := ${PROJECT} + +CPP_SRC_FILES := tls_echo_server.cpp + +CONF_FILES := server.crt server.key + +CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) +LDFLAGS += \ + -ltbox_network \ + -Wl,--whole-archive -ltbox_network_tls -Wl,--no-whole-archive \ + -ltbox_event \ + -ltbox_util \ + -ltbox_base \ + -lssl -lcrypto \ + -ldl + +include $(TOP_DIR)/mk/exe_common.mk diff --git a/examples/network/tcp_server/tls_echo_server/server.crt b/examples/network/tcp_server/tls_echo_server/server.crt new file mode 100644 index 00000000..aafbc0be --- /dev/null +++ b/examples/network/tcp_server/tls_echo_server/server.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDiTCCAnGgAwIBAgIUfLUbGuJjk5Wg1+v6hnOVyfPJrpEwDQYJKoZIhvcNAQEL +BQAwWDELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAOBgNVBAcMB0Jl +aWppbmcxETAPBgNVBAoMCGNwcC10Ym94MRIwEAYDVQQDDAkxMjcuMC4wLjEwHhcN +MjYwNjI2MTMzMjQ5WhcNMzYwNjIzMTMzMjQ5WjBYMQswCQYDVQQGEwJDTjEQMA4G +A1UECAwHQmVpamluZzEQMA4GA1UEBwwHQmVpamluZzERMA8GA1UECgwIY3BwLXRi +b3gxEjAQBgNVBAMMCTEyNy4wLjAuMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALlvcZ7VBhp0S3tB1ho2Qb0qCHQ8Ezdf3X3OOfviXBPg4IogXZSWy0RM +CBbP7PaxpksX9DHVtsjydss1vb3ts+DU4vFtc57wq8Sf+NQ7xvGJffH12BuYUjyn +1cE0ENdzv7FuruZ1Q/c2wtll2WwL5Bo+ggqyF7Cr+Ja/reHEN1eN26oLtTJQcph0 +YCbJJlmJjLO51sDmweaNe9i8Ck7y7EHypL1ecMMMyhnsTavZmntNavIPdX9CnB9d +MPsQT/yG4FUBve3ZOVL8+MawQ/bbiAlTPYuEjiooBJiZD2+6GmbMfu3tVO/yA+Wg +zLPv7ANNK70H/rysscgyxhct90vzfGUCAwEAAaNLMEkwGgYDVR0RBBMwEYcEfwAA +AYIJbG9jYWxob3N0MAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFK18uhz4KeDIsASC +2wxeJTR1U2SIMA0GCSqGSIb3DQEBCwUAA4IBAQB7W6ZOA3Uun+6THd01l3JlQqk7 +i/oTc9bUterTvc/tI1Quc349pZdVI1d0GkhyFRNMpTF1D5Cph5OkmfugFDsP9a62 +Gjg2CQ+H93WeEDZ5nLJpNQDPwVyMqM3dORPhVHx7S5IEj7vi8q5ko3MIl6L5wGdy +x7cyGxsgFj8INU651rtB8G27B1HL4yQW+Ix1wq/xlh/Lg+5/lIoC6MiywE5+UyMz +FOTETFuI4Zs+vg5Q36fm/hJ+WdLS4gifarY2blKQMZBcegkp0/DnUmAxXEz66zl3 +UPppc85X5/qPRO1GMRgadPxi6JNvha+m5CyCMTvaggVCvOiBpfn3f78fUMDc +-----END CERTIFICATE----- diff --git a/examples/network/tcp_server/tls_echo_server/server.key b/examples/network/tcp_server/tls_echo_server/server.key new file mode 100644 index 00000000..a9b48be0 --- /dev/null +++ b/examples/network/tcp_server/tls_echo_server/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5b3Ge1QYadEt7 +QdYaNkG9Kgh0PBM3X919zjn74lwT4OCKIF2UlstETAgWz+z2saZLF/Qx1bbI8nbL +Nb297bPg1OLxbXOe8KvEn/jUO8bxiX3x9dgbmFI8p9XBNBDXc7+xbq7mdUP3NsLZ +ZdlsC+QaPoIKshewq/iWv63hxDdXjduqC7UyUHKYdGAmySZZiYyzudbA5sHmjXvY +vApO8uxB8qS9XnDDDMoZ7E2r2Zp7TWryD3V/QpwfXTD7EE/8huBVAb3t2TlS/PjG +sEP224gJUz2LhI4qKASYmQ9vuhpmzH7t7VTv8gPloMyz7+wDTSu9B/68rLHIMsYX +LfdL83xlAgMBAAECggEAI3Hg5vpTC1V1ZB8GfMYoNK9HJGijR69kV/rGbJYtAYO3 +h89987wLKIfb9/hQlCsK3Um73Ja8NJbcDCW+mgJIos4ufvVr51Kbkp79Yhv3AA5G +66wRXdz0wzFVk3OPUI+IcbL1bYm2rxdhkUp9j8CKHlYaZ075ZkTI5I/I/eGSroJU +nwSBgvZQnS27+qH0Cw/JC/80q3vcAKJb9eZNcnKtrHo1SIahu7yO+zGAOXF80vA2 +9fSEda0DZhjkP0tuwX/jhYRz33i8um++CQ0irD+Oe5voRpPsMUQbgqQkJTl5LL93 +HjaMVJTOnhMLkprARm0E0RkGJ+T8zo9BryQHm57I1QKBgQDLRaKQs9OqXDmimb+b +O0isGGG8Rw0RMaIXAYunF/K+8hQ1ZMIK01yE+bRpASAK9mAWOKfpqyEkz3hWoetp +ESXIMa32Phmg19EPakfxbd/bhhPd1ce08M9XAilPECXcr5xyDVuxVnWo9MisZJhE +49vMtZjxp8ZBUlJzoGmyli6lVwKBgQDpiVssjoM/ocSjbSiiDAH5PIl7t1S+vG65 +F6Np9rozvwBiQI9F5+BHlU5SwYmCrUREZHvUngHEvcWJxAz4eak2RchbzqKNXEiI +VfI/6QGqxbJuG3adfHOd+tMYMv7ttGU06uqQl3B6WmcAIW/Utiv3UI557LhnRgjc +pKFuLnm6owKBgG9QjeqyH3qOkJ1jltL6Txy3KWaCfjxpMrtohEKX0b4RMVHgAIcP +If5MBCjwjcyTCSGCGynSJg9TcjH278SUuFz+H6bWcRBsvzay2/zxT4KW1PBJbti+ +erzKGTcLv8AvhvvKJulhUIOasP3/BIfNRAPBeqTzXJVO8IoTUW6T4a13AoGAJG65 +OopBD3w9IQG2hRE6fZdkG1jOb7MV0upNJArJoaj6dll8AHvcEU7JmT94JFrDe6fx +aYn83KR+XK+pFlpke4MHbssdsM/kwOAnmrDPAcU1wNen+Ymgv9SRegT6oDq0Tz0W +utflRDE2QF73A0goM7ztfTfgzLuwRjuos3espeECgYEAqbm17B0UZolBVaPOBRuT +a+gYAOUxZpYCi+ajLFcDa67geLgCNTp8EJj/uwKsVCECGgDT5pJLtAnfeNQvYrfq +IN7I12L4L+PMn5Tv5E7HevWBknjd87mujS9YymGdXkBvQHWv9oEq1du0qHrL9tLx +pr+LuwJVR1QmxvWliRSCeew= +-----END PRIVATE KEY----- diff --git a/examples/network/tcp_server/tls_echo_server/tls_echo_server.cpp b/examples/network/tcp_server/tls_echo_server/tls_echo_server.cpp new file mode 100644 index 00000000..6745f932 --- /dev/null +++ b/examples/network/tcp_server/tls_echo_server/tls_echo_server.cpp @@ -0,0 +1,130 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +/** + * TLS 版 echo server 示例 + * 用法:tls_echo_server --cert --key + * 必须指定证书文件和密钥文件 + */ + +#include +#include + +#include +#include + +#include +#include +#include +#include + +using namespace std; +using namespace tbox; +using namespace tbox::event; +using namespace tbox::network; + +void PrintUsage(const char *prog) +{ + cout << "Usage: " << prog << " --cert --key " << endl + << "Exp : " << prog << " 0.0.0.0:12345 --cert server.crt --key server.key" << endl; +} + +int main(int argc, char **argv) +{ + string bind_addr_str; + string cert_file; + string key_file; + + //! 解析命令行参数 + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--cert") == 0 && i + 1 < argc) { + cert_file = argv[++i]; + } else if (strcmp(argv[i], "--key") == 0 && i + 1 < argc) { + key_file = argv[++i]; + } else if (argv[i][0] != '-') { + bind_addr_str = argv[i]; + } else { + cerr << "Error: invalid option `" << argv[i] << "'" << endl; + PrintUsage(argv[0]); + return 0; + } + } + + if (bind_addr_str.empty() || cert_file.empty() || key_file.empty()) { + PrintUsage(argv[0]); + return 0; + } + + LogOutput_Enable(); + + SockAddr bind_addr = SockAddr::FromString(bind_addr_str); + + Loop *sp_loop = Loop::New(); + SetScopeExitAction([sp_loop] { delete sp_loop; }); + + TcpServer server(sp_loop); + + //! 设置 TLS 配置(必须在 initialize 之前调用) + TlsConfig tls_config; + tls_config.cert_file = cert_file; + tls_config.key_file = key_file; + tls_config.verify_peer = false; //! 测试环境不验证 client 证书 + if (!server.setTlsConfig(tls_config)) { + LogErr("set tls config fail, need network_tls module"); + return 0; + } + + server.initialize(bind_addr, 1); + //! 当收到数据时,直接往 client 指定对象发回去 + server.setReceiveCallback( + [&server] (const TcpServer::ConnToken &client, Buffer &buff) { + std::string text((const char*)buff.readableBegin(), buff.readableSize()); + LogInfo("len:%u, text:%s", text.size(), text.c_str()); + server.send(client, buff.readableBegin(), buff.readableSize()); + buff.hasReadAll(); + }, 0 + ); + server.start(); + + //! 注册 ctrl+C 停止信号 + SignalEvent *sp_stop_ev = sp_loop->newSignalEvent(); + SetScopeExitAction([sp_stop_ev] { delete sp_stop_ev; }); + sp_stop_ev->initialize(SIGINT, Event::Mode::kOneshot); + sp_stop_ev->setCallback( + [sp_loop, &server] (int) { + server.stop(); + sp_loop->exitLoop(); + } + ); + sp_stop_ev->enable(); + + LogInfo("tls echo server running ..."); + + if (bind_addr.type() == SockAddr::Type::kIPv4) { + IPAddress ip; + uint16_t port; + bind_addr.get(ip, port); + cout << "try command: .install/bin/examples/network/tcp_client/tls_echo_client :" << port << endl; + } + + sp_loop->runLoop(); + LogInfo("tls echo server stopped"); + + return 0; +} diff --git a/examples/websocket/Makefile b/examples/websocket/Makefile new file mode 100644 index 00000000..a8fb161b --- /dev/null +++ b/examples/websocket/Makefile @@ -0,0 +1,26 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2025 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +all test clean distclean: + @for i in $(shell ls) ; do \ + if [ -d $$i ]; then \ + $(MAKE) -C $$i $@ || exit $$? ; \ + fi \ + done diff --git a/examples/websocket/chat_client/Makefile b/examples/websocket/chat_client/Makefile new file mode 100644 index 00000000..ab9013ca --- /dev/null +++ b/examples/websocket/chat_client/Makefile @@ -0,0 +1,39 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2026 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +PROJECT := examples/websocket/chat_client +EXE_NAME := ${PROJECT} + +CPP_SRC_FILES := chat_client.cpp + +CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) +LDFLAGS += \ + -ltbox_websocket \ + -ltbox_crypto \ + -ltbox_http \ + -ltbox_network \ + -ltbox_eventx \ + -ltbox_event \ + -ltbox_log \ + -ltbox_util \ + -ltbox_base \ + -lpthread -lz -ldl + +include $(TOP_DIR)/mk/exe_common.mk diff --git a/examples/websocket/chat_client/chat_client.cpp b/examples/websocket/chat_client/chat_client.cpp new file mode 100644 index 00000000..1e66cf41 --- /dev/null +++ b/examples/websocket/chat_client/chat_client.cpp @@ -0,0 +1,163 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +/** + * WebSocket 聊天客户端示例 + * + * 功能: + * - 连接到 WebSocket 聊天服务器(chat 示例) + * - 从标准输入读取文本行,发送为 WebSocket 文本帧 + * - 收到服务器消息打印到标准输出 + * - Ctrl+C 断开连接并退出 + * + * 用法: + * ./chat_client + * 示例: ./chat_client 127.0.0.1:8080 /ws/chat-1 + */ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::network; +using namespace tbox::websocket; +using namespace tbox::websocket::client; + +int main(int argc, char **argv) +{ + std::string server_addr = "127.0.0.1:8080"; + std::string url_path = "/ws/chat-1"; + + if (argc >= 2) + server_addr = argv[1]; + if (argc >= 3) + url_path = argv[2]; + + LogOutput_Enable(); + + LogInfo("enter"); + + auto sp_loop = Loop::New(); + auto sp_sig_event = sp_loop->newSignalEvent(); + + //! 创建 stdin 读取事件(非阻塞读取标准输入) + auto sp_stdin_event = sp_loop->newFdEvent(); + + SetScopeExitAction( + [=] { + delete sp_stdin_event; + delete sp_sig_event; + delete sp_loop; + } + ); + + //! 创建 WebSocket 客户端 + WsClient ws_client(sp_loop); + if (!ws_client.initialize(SockAddr::FromString(server_addr), url_path)) { + LogErr("init ws client fail"); + return 0; + } + + //! 启用压缩(RFC 7692 permessage-deflate) + ws_client.setCompressionPrefer(true); + ws_client.setFragmentSize(65535); + ws_client.setPingInterval(10); + ws_client.setPingTimeout(2); + + //! 设置回调 + ws_client.setConnectedCallback([&] { + LogInfo("connected to %s%s", server_addr.c_str(), url_path.c_str()); + std::cout << "== 已连接到 " << server_addr << url_path << " ==" << std::endl; + std::cout << "请输入用户名(第一条消息为登录名):" << std::endl; + + //! 启动 stdin 读取 + sp_stdin_event->initialize(STDIN_FILENO, FdEvent::kReadEvent, Event::Mode::kPersist); + sp_stdin_event->setCallback([&](short) { + std::string line; + if (std::getline(std::cin, line)) { + if (!line.empty()) { + ws_client.send(line); + } + } else { + //! stdin 关闭(EOF),断开连接 + ws_client.close(); + sp_stdin_event->disable(); + } + }); + sp_stdin_event->enable(); + }); + + ws_client.setDisconnectedCallback([&] { + LogInfo("disconnected"); + std::cout << "== 已断开连接 ==" << std::endl; + }); + + ws_client.setTextMessageCallback([&](std::string &&text) { + std::cout << text << std::endl; + }); + + ws_client.setBinaryMessageCallback([&](std::vector &&data) { + //! 此示例不处理二进制帧 + }); + + ws_client.setErrorCallback([&] { + LogNotice("ws error"); + std::cout << "== 连接出错 ==" << std::endl; + }); + + //! 设置二次退避策略 + ws_client.setReconnectDelayCalcFunc([] (int fail_count) { return 1 << (std::min(4, fail_count)); }); + + //! Ctrl+C 退出 + sp_sig_event->initialize(SIGINT, Event::Mode::kOneshot); + sp_sig_event->enable(); + sp_sig_event->setCallback( + [&] (int) { + LogInfo("stopping..."); + ws_client.close(); + sp_loop->exitLoop(); + } + ); + + //! 启动连接 + if (!ws_client.start()) { + LogErr("start ws client fail"); + return 0; + } + + LogInfo("connecting to %s%s ...", server_addr.c_str(), url_path.c_str()); + + sp_loop->runLoop(); + + ws_client.stop(); + ws_client.cleanup(); + + LogInfo("exit"); + return 0; +} diff --git a/examples/websocket/chat_server/Makefile b/examples/websocket/chat_server/Makefile new file mode 100644 index 00000000..396c1479 --- /dev/null +++ b/examples/websocket/chat_server/Makefile @@ -0,0 +1,39 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2026 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +PROJECT := examples/websocket/chat_server +EXE_NAME := ${PROJECT} + +CPP_SRC_FILES := chat_server.cpp html_text.cpp + +CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) +LDFLAGS += \ + -ltbox_websocket \ + -ltbox_crypto \ + -ltbox_http \ + -ltbox_network \ + -ltbox_eventx \ + -ltbox_event \ + -ltbox_log \ + -ltbox_util \ + -ltbox_base \ + -lpthread -lz -ldl + +include $(TOP_DIR)/mk/exe_common.mk diff --git a/examples/websocket/chat_server/chat_server.cpp b/examples/websocket/chat_server/chat_server.cpp new file mode 100644 index 00000000..1eb03693 --- /dev/null +++ b/examples/websocket/chat_server/chat_server.cpp @@ -0,0 +1,214 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "html_text.h" + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::http; +using namespace tbox::http::server; +using namespace tbox::websocket; +using namespace tbox::websocket::server; + +//! 群聊聊天室 +//! 内含 WsServer,统一管理 WebSocket 连接与聊天逻辑 +//! 第一条文本消息为用户名(登录),之后为聊天消息 +class ChatRoom { + public: + ChatRoom(event::Loop *wp_loop, const std::string &name) + : wp_loop_(wp_loop) + , name_(name) + , ws_srv_(wp_loop) + { } + + bool initialize(http::server::Server *http_srv, const std::string &url_path) + { + if (!ws_srv_.initialize(http_srv, url_path)) + return false; + + using namespace std::placeholders; + ws_srv_.setConnectedCallback(std::bind(&ChatRoom::onConnected, this, _1)); + ws_srv_.setDisconnectedCallback(std::bind(&ChatRoom::onDisconnected, this, _1)); + ws_srv_.setTextMessageCallback(std::bind(&ChatRoom::onTextMessage, this, _1, _2)); + ws_srv_.setCompressionEnable(true); + ws_srv_.setFragmentSize(256); + ws_srv_.setPingInterval(10); + ws_srv_.setPingTimeout(2); + + LogInfo("chat room '%s' mounted at %s", name_.c_str(), url_path.c_str()); + return true; + } + + bool start() { return ws_srv_.start(); } + void stop() { ws_srv_.stop(); } + void cleanup() + { + ws_srv_.cleanup(); + conns_.clear(); + conn_to_name_.clear(); + } + + private: + //! 连接建立:暂不广播,等收到用户名后再广播上线 + void onConnected(const WsServer::ConnToken &token) + { + auto url_path = ws_srv_.getUrl(token); + LogInfo("url_path:%s", url_path.c_str()); + + conns_.insert(token); + } + + //! 连接断开:若已登录则广播下线消息 + void onDisconnected(const WsServer::ConnToken &token) + { + auto it = conn_to_name_.find(token); + if (it != conn_to_name_.end()) { + std::string name = it->second; + conn_to_name_.erase(token); + conns_.erase(token); + LogInfo("[%s] user '%s' offline", name_.c_str(), name.c_str()); + broadcast(name + " 下线"); + } else { + conns_.erase(token); + } + } + + //! 收到消息:第一条为用户名(登录),后续为聊天消息 + void onTextMessage(const WsServer::ConnToken &token, std::string &&text) + { + auto it = conn_to_name_.find(token); + if (it == conn_to_name_.end()) { + //! 第一条消息作为用户名 + conn_to_name_[token] = text; + LogInfo("[%s] user '%s' online", name_.c_str(), text.c_str()); + broadcast(text + " 上线"); + } else { + LogInfo("[%s] user: %s", it->second.c_str(), text.c_str()); + broadcast(it->second + ": " + text); + } + } + + //! 仅向已登录的用户广播(有用户名的连接) + void broadcast(const std::string &msg) + { + for (const auto &pair : conn_to_name_) + ws_srv_.send(pair.first, msg); + } + + private: + event::Loop *wp_loop_; + std::string name_; + WsServer ws_srv_; + std::set conns_; + std::map conn_to_name_; +}; + +int main(int argc, char **argv) +{ + std::string bind_addr = "0.0.0.0:8080"; + + if (argc == 2) + bind_addr = argv[1]; + + LogOutput_Enable(); + + LogInfo("enter"); + + auto sp_loop = Loop::New(); + auto sp_sig_event = sp_loop->newSignalEvent(); + + SetScopeExitAction( + [=] { + delete sp_sig_event; + delete sp_loop; + } + ); + + sp_sig_event->initialize(SIGINT, Event::Mode::kPersist); + sp_sig_event->enable(); + + //! 创建 HTTP 服务器 + Server http_srv(sp_loop); + if (!http_srv.initialize(network::SockAddr::FromString(bind_addr), 1)) { + LogErr("init http server fail"); + return 0; + } + + //! 创建两个聊天室,分别挂载到 /ws/chat-1 和 /ws/chat-2 + ChatRoom chat_room_1(sp_loop, "聊天室1"); + ChatRoom chat_room_2(sp_loop, "聊天室2"); + + if (!chat_room_1.initialize(&http_srv, "/ws/chat-1")) { + LogErr("init chat room 1 fail"); + return 0; + } + if (!chat_room_2.initialize(&http_srv, "/ws/chat-2")) { + LogErr("init chat room 2 fail"); + return 0; + } + + //! 添加 HTTP 请求处理(主页面) + http_srv.use( + [&](ContextSptr ctx, const NextFunc &next) { + if (ctx->req().url.path == "/") { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().headers["Content-Type"] = "text/html; charset=utf-8"; + ctx->res().body = kChatHtml; + return; + } + next(); + } + ); + + //! 启动服务 + http_srv.start(); + chat_room_1.start(); + chat_room_2.start(); + + //! Ctrl+C 退出 + sp_sig_event->setCallback( + [&] (int) { + chat_room_1.stop(); + chat_room_2.stop(); + http_srv.stop(); + sp_loop->exitLoop(); + } + ); + + LogInfo("start, listen at %s", bind_addr.c_str()); + sp_loop->runLoop(); + LogInfo("stop"); + + chat_room_1.cleanup(); + chat_room_2.cleanup(); + http_srv.cleanup(); + + LogInfo("exit"); + return 0; +} diff --git a/examples/websocket/chat_server/html_text.cpp b/examples/websocket/chat_server/html_text.cpp new file mode 100644 index 00000000..551c3256 --- /dev/null +++ b/examples/websocket/chat_server/html_text.cpp @@ -0,0 +1,245 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2025 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "html_text.h" + +const std::string kChatHtml = +R"rawliteral( + + + +WebSocket 群聊 + + + + + +
+
+

🟢 WebSocket 群聊

+ +
+ + +
+ +
+
+
+ + +
+
+
+ + +
+
+ +
+
+
    +
    + + +
    +
    + + + + + + +)rawliteral"; diff --git a/examples/websocket/chat_server/html_text.h b/examples/websocket/chat_server/html_text.h new file mode 100644 index 00000000..bf592523 --- /dev/null +++ b/examples/websocket/chat_server/html_text.h @@ -0,0 +1,28 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef EXAMPLES_WEBSOCKET_CHAT_HTML_TEXT_H_ +#define EXAMPLES_WEBSOCKET_CHAT_HTML_TEXT_H_ + +#include + +//! 聊天室页面 HTML 源数据 +extern const std::string kChatHtml; + +#endif diff --git a/examples/websocket/echo_bin/Makefile b/examples/websocket/echo_bin/Makefile new file mode 100644 index 00000000..9fa1c2f3 --- /dev/null +++ b/examples/websocket/echo_bin/Makefile @@ -0,0 +1,39 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2026 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +PROJECT := examples/websocket/echo_bin +EXE_NAME := ${PROJECT} + +CPP_SRC_FILES := echo_bin.cpp html_text.cpp + +CXXFLAGS := -DMODULE_ID='"$(EXE_NAME)"' $(CXXFLAGS) +LDFLAGS += \ + -ltbox_websocket \ + -ltbox_crypto \ + -ltbox_http \ + -ltbox_network \ + -ltbox_eventx \ + -ltbox_event \ + -ltbox_log \ + -ltbox_util \ + -ltbox_base \ + -lpthread -lz -ldl + +include $(TOP_DIR)/mk/exe_common.mk diff --git a/examples/websocket/echo_bin/echo_bin.cpp b/examples/websocket/echo_bin/echo_bin.cpp new file mode 100644 index 00000000..a3628d71 --- /dev/null +++ b/examples/websocket/echo_bin/echo_bin.cpp @@ -0,0 +1,275 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ + +/** + * WebSocket 二进制 Echo 示例 + * + * 功能: + * - 客户端发送二进制数据帧,服务器原样回传(echo) + * - 服务器统计收发帧数与字节数 + * - 服务器每 5 秒向所有客户端推送二进制统计帧(4字节头"STAT" + JSON字符串) + * + * 演示要点: + * - WsServer::send() 的 void* + len 版本:发送原始二进制 + * - WsServer::send() 的 vector 版本:发送 vector 二进制 + * - WsServer::send() 的 const char* 版本:发送文本字符串 + * - WsFrame::OpCode::kBinary:区分文本帧与二进制帧 + * - event::TimerEvent:定时推送统计数据 + * - WsServer 的 start()/stop() 生命周期 + * - WsServer::setFragmentSize():可配置分片大小 + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "html_text.h" + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::http; +using namespace tbox::http::server; +using namespace tbox::websocket; +using namespace tbox::websocket::server; + +//! 统计帧的头部标识:0x53 0x54 0x41 0x54 = "STAT" +static const uint8_t kStatHeader[4] = {0x53, 0x54, 0x41, 0x54}; + +//! 二进制 Echo 服务 +//! 内含 WsServer + 统计信息 + 定时器推送 +class EchoService { + public: + EchoService(Loop *wp_loop) + : wp_loop_(wp_loop) + , ws_srv_(wp_loop) + , stat_timer_(wp_loop->newTimerEvent()) + { } + + ~EchoService() + { + CHECK_DELETE_RESET_OBJ(stat_timer_); + } + + bool initialize(Server *http_srv, const std::string &url_path) + { + //! 初始化 WsServer,指定 URL 路径 + if (!ws_srv_.initialize(http_srv, url_path)) + return false; + + //! 设置回调 + using namespace std::placeholders; + ws_srv_.setConnectedCallback(std::bind(&EchoService::onConnected, this, _1)); + ws_srv_.setDisconnectedCallback(std::bind(&EchoService::onDisconnected, this, _1)); + ws_srv_.setTextMessageCallback(std::bind(&EchoService::onTextMessage, this, _1, _2)); + ws_srv_.setBinaryMessageCallback(std::bind(&EchoService::onBinaryMessage, this, _1, _2)); + ws_srv_.setCompressionEnable(true); + ws_srv_.setFragmentSize(256); + ws_srv_.setPingInterval(10); + ws_srv_.setPingTimeout(2); + + //! 初始化定时器:每 5 秒推送统计帧 + stat_timer_->initialize(std::chrono::milliseconds(5000), Event::Mode::kPersist); + stat_timer_->setCallback([this] { onStatTimer(); }); + + LogInfo("echo service mounted at %s", url_path.c_str()); + return true; + } + + bool start() + { + if (!ws_srv_.start()) + return false; + + stat_timer_->enable(); + return true; + } + + void stop() + { + stat_timer_->disable(); + ws_srv_.stop(); + } + + void cleanup() + { + ws_srv_.cleanup(); + conns_.clear(); + } + + private: + //! 新连接:记录 token + void onConnected(const WsServer::ConnToken &token) + { + conns_.insert(token); + LogDbg("client connected, total: %d", conns_.size()); + } + + //! 断开连接:移除 token + void onDisconnected(const WsServer::ConnToken &token) + { + conns_.erase(token); + LogDbg("client disconnected, total: %d", conns_.size()); + } + + //! 收到消息:区分文本帧与二进制帧 + void onBinaryMessage(const WsServer::ConnToken &token, std::vector &&data) + { + auto hex_str = util::string::RawDataToHexStr(data.data(), data.size()); + LogTrace("hex: %s", hex_str.c_str()); + + //! 二进制帧:echo 回传原数据 + //! 演示 WsServer::send() 的 void* + len 版本 + //! 更新统计 + recv_frames_++; + recv_bytes_ += data.size(); + sent_frames_++; + sent_bytes_ += data.size(); + ws_srv_.send(token, data); + } + + void onTextMessage(const WsServer::ConnToken &token, std::string &&text) + { + LogTrace("text: %s", text.c_str()); + + ws_srv_.send(token, "此服务仅接收二进制帧,请发送 ArrayBuffer"); + } + + //! 定时器回调:构建统计帧,推送给所有客户端 + void onStatTimer() + { + //! 构建 JSON 统计信息 + std::string json = "{" + "\"recv_frames\":" + std::to_string(recv_frames_) + "," + "\"recv_bytes\":" + std::to_string(recv_bytes_) + "," + "\"sent_frames\":" + std::to_string(sent_frames_) + "," + "\"sent_bytes\":" + std::to_string(sent_bytes_) + "," + "\"clients\":" + std::to_string(conns_.size()) + + "}"; + + //! 演示 WsServer::send() 的 vector 版本 + //! 格式:4字节头 "STAT" + JSON 字符串字节 + std::vector stat_data; + stat_data.reserve(4 + json.size()); + stat_data.insert(stat_data.end(), kStatHeader, kStatHeader + 4); + stat_data.insert(stat_data.end(), json.begin(), json.end()); + + //! 向所有客户端推送统计帧 + for (const auto &token : conns_) + ws_srv_.send(token, stat_data); + } + + private: + Loop *wp_loop_; + WsServer ws_srv_; + TimerEvent *stat_timer_; + + std::set conns_; //! 所有连接 + + //! 收发统计 + uint64_t recv_frames_ = 0; + uint64_t recv_bytes_ = 0; + uint64_t sent_frames_ = 0; + uint64_t sent_bytes_ = 0; +}; + +int main(int argc, char **argv) +{ + std::string bind_addr = "0.0.0.0:8080"; + + if (argc == 2) + bind_addr = argv[1]; + + LogOutput_Enable(); + + LogInfo("enter"); + + auto sp_loop = Loop::New(); + auto sp_sig_event = sp_loop->newSignalEvent(); + + SetScopeExitAction( + [=] { + delete sp_sig_event; + delete sp_loop; + } + ); + + sp_sig_event->initialize(SIGINT, Event::Mode::kPersist); + sp_sig_event->enable(); + + //! 创建 HTTP 服务器 + Server http_srv(sp_loop); + if (!http_srv.initialize(network::SockAddr::FromString(bind_addr), 1)) { + LogErr("init http server fail"); + return 0; + } + + //! 创建 Echo 服务,挂载到 /ws/echo + EchoService echo_srv(sp_loop); + if (!echo_srv.initialize(&http_srv, "/ws/echo")) { + LogErr("init echo service fail"); + return 0; + } + + //! 添加 HTTP 请求处理(主页面) + http_srv.use( + [&](ContextSptr ctx, const NextFunc &next) { + if (ctx->req().url.path == "/") { + ctx->res().status_code = StatusCode::k200_OK; + ctx->res().headers["Content-Type"] = "text/html; charset=utf-8"; + ctx->res().body = kEchoBinHtml; + return; + } + next(); + } + ); + + //! 启动服务 + http_srv.start(); + echo_srv.start(); + + //! Ctrl+C 退出 + sp_sig_event->setCallback( + [&] (int) { + echo_srv.stop(); + http_srv.stop(); + sp_loop->exitLoop(); + } + ); + + LogInfo("start, listen at %s", bind_addr.c_str()); + sp_loop->runLoop(); + LogInfo("stop"); + + echo_srv.cleanup(); + http_srv.cleanup(); + + LogInfo("exit"); + return 0; +} diff --git a/examples/websocket/echo_bin/html_text.cpp b/examples/websocket/echo_bin/html_text.cpp new file mode 100644 index 00000000..0704f26c --- /dev/null +++ b/examples/websocket/echo_bin/html_text.cpp @@ -0,0 +1,276 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "html_text.h" + +const std::string kEchoBinHtml = +R"rawliteral( + + + +WebSocket 二进制 Echo + + + + +
    + + +
    +

    发送二进制数据

    +
    + + + +
    +
    + +
    +

    统计信息

    +
    +
    发送帧数
    0
    +
    发送字节
    0
    +
    接收帧数
    0
    +
    接收字节
    0
    +
    +
    +
    服务器推送统计
    +
    等待推送...
    +
    +
    + +
    +

    通信日志

    +
      +
      +
      + + + + + +)rawliteral"; diff --git a/examples/websocket/echo_bin/html_text.h b/examples/websocket/echo_bin/html_text.h new file mode 100644 index 00000000..fccab4b1 --- /dev/null +++ b/examples/websocket/echo_bin/html_text.h @@ -0,0 +1,28 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef EXAMPLES_WEBSOCKET_ECHO_BIN_HTML_TEXT_H_ +#define EXAMPLES_WEBSOCKET_ECHO_BIN_HTML_TEXT_H_ + +#include + +//! 二进制 Echo 页面 HTML 源数据 +extern const std::string kEchoBinHtml; + +#endif diff --git a/modules/crypto/CMakeLists.txt b/modules/crypto/CMakeLists.txt index ed2bc53b..02127448 100644 --- a/modules/crypto/CMakeLists.txt +++ b/modules/crypto/CMakeLists.txt @@ -31,15 +31,18 @@ set(TBOX_LIBRARY_NAME tbox_crypto) set(TBOX_CRYPTO_HEADERS md5.h - aes.h) + aes.h + sha1.h) set(TBOX_CRYPTO_SOURCES md5.cpp - aes.cpp) + aes.cpp + sha1.cpp) set(TBOX_CRYPTO_TEST_SOURCES md5_test.cpp - aes_test.cpp) + aes_test.cpp + sha1_test.cpp) add_library(${TBOX_LIBRARY_NAME} ${TBOX_BUILD_LIB_TYPE} ${TBOX_CRYPTO_SOURCES}) diff --git a/modules/crypto/Makefile b/modules/crypto/Makefile index 3ba70668..5d43ec3f 100644 --- a/modules/crypto/Makefile +++ b/modules/crypto/Makefile @@ -27,10 +27,12 @@ LIB_VERSION_Z = 1 HEAD_FILES = \ md5.h \ aes.h \ + sha1.h \ CPP_SRC_FILES = \ md5.cpp \ aes.cpp \ + sha1.cpp \ CXXFLAGS := -DMODULE_ID='"tbox.crypto"' $(CXXFLAGS) @@ -38,6 +40,7 @@ TEST_CPP_SRC_FILES = \ $(CPP_SRC_FILES) \ md5_test.cpp \ aes_test.cpp \ + sha1_test.cpp \ TEST_LDFLAGS := $(LDFLAGS) -ltbox_util -ltbox_base -ldl diff --git a/modules/crypto/sha1.cpp b/modules/crypto/sha1.cpp new file mode 100644 index 00000000..66c2de39 --- /dev/null +++ b/modules/crypto/sha1.cpp @@ -0,0 +1,178 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2025 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "sha1.h" + +#include + +namespace tbox { +namespace crypto { + +namespace { + +//! SHA-1 常量 +constexpr uint32_t K0 = 0x5A827999; //!< 0~19 +constexpr uint32_t K1 = 0x6ED9EBA1; //!< 20~39 +constexpr uint32_t K2 = 0x8F1BBCDC; //!< 40~59 +constexpr uint32_t K3 = 0xCA62C1D6; //!< 60~79 + +inline uint32_t RotLeft(uint32_t x, uint32_t n) { return (x << n) | (x >> (32 - n)); } +inline uint32_t Ch(uint32_t x, uint32_t y, uint32_t z) { return (x & y) ^ (~x & z); } +inline uint32_t Parity(uint32_t x, uint32_t y, uint32_t z) { return x ^ y ^ z; } +inline uint32_t Maj(uint32_t x, uint32_t y, uint32_t z) { return (x & y) ^ (x & z) ^ (y & z); } + +} + +SHA1::SHA1() +{ + state_[0] = 0x67452301; + state_[1] = 0xEFCDAB89; + state_[2] = 0x98BADCFE; + state_[3] = 0x10325476; + state_[4] = 0xC3D2E1F0; + count_ = 0; + buffer_index_ = 0; +} + +void SHA1::update(const void *data_ptr, size_t data_len) +{ + if (is_finished_ || data_ptr == nullptr || data_len == 0) + return; + + const uint8_t *p = static_cast(data_ptr); + + while (data_len > 0) { + size_t copy_len = 64 - buffer_index_; + if (copy_len > data_len) + copy_len = data_len; + + memcpy(buffer_ + buffer_index_, p, copy_len); + buffer_index_ += copy_len; + p += copy_len; + data_len -= copy_len; + count_ += copy_len; + + if (buffer_index_ == 64) { + transform(buffer_); + buffer_index_ = 0; + } + } +} + +void SHA1::finish(uint8_t digest[20]) +{ + if (is_finished_) + return; + + //! 填充:1 bit of 1 + 0 bits + 64 bit length + uint64_t total_bits = count_ * 8; + + buffer_[buffer_index_++] = 0x80; + + if (buffer_index_ > 56) { + //! 需要额外一个块 + while (buffer_index_ < 64) + buffer_[buffer_index_++] = 0; + transform(buffer_); + buffer_index_ = 0; + } + + //! 填0直到56字节位置 + while (buffer_index_ < 56) + buffer_[buffer_index_++] = 0; + + //! 写入总长度(大端序) + buffer_[56] = static_cast((total_bits >> 56) & 0xFF); + buffer_[57] = static_cast((total_bits >> 48) & 0xFF); + buffer_[58] = static_cast((total_bits >> 40) & 0xFF); + buffer_[59] = static_cast((total_bits >> 32) & 0xFF); + buffer_[60] = static_cast((total_bits >> 24) & 0xFF); + buffer_[61] = static_cast((total_bits >> 16) & 0xFF); + buffer_[62] = static_cast((total_bits >> 8) & 0xFF); + buffer_[63] = static_cast((total_bits >> 0) & 0xFF); + + transform(buffer_); + + //! 输出摘要(大端序) + for (int i = 0; i < 5; ++i) { + digest[i * 4 + 0] = static_cast((state_[i] >> 24) & 0xFF); + digest[i * 4 + 1] = static_cast((state_[i] >> 16) & 0xFF); + digest[i * 4 + 2] = static_cast((state_[i] >> 8) & 0xFF); + digest[i * 4 + 3] = static_cast((state_[i] >> 0) & 0xFF); + } + + is_finished_ = true; +} + +void SHA1::transform(const uint8_t block[64]) +{ + uint32_t w[80]; + + //! 将64字节块扩展为80个32位字(大端序) + for (int i = 0; i < 16; ++i) + w[i] = (static_cast(block[i * 4 + 0]) << 24) + | (static_cast(block[i * 4 + 1]) << 16) + | (static_cast(block[i * 4 + 2]) << 8) + | (static_cast(block[i * 4 + 3]) << 0); + + for (int i = 16; i < 80; ++i) + w[i] = RotLeft(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1); + + uint32_t a = state_[0]; + uint32_t b = state_[1]; + uint32_t c = state_[2]; + uint32_t d = state_[3]; + uint32_t e = state_[4]; + + for (int i = 0; i < 20; ++i) { + uint32_t temp = RotLeft(a, 5) + Ch(b, c, d) + e + K0 + w[i]; + e = d; d = c; c = RotLeft(b, 30); b = a; a = temp; + } + + for (int i = 20; i < 40; ++i) { + uint32_t temp = RotLeft(a, 5) + Parity(b, c, d) + e + K1 + w[i]; + e = d; d = c; c = RotLeft(b, 30); b = a; a = temp; + } + + for (int i = 40; i < 60; ++i) { + uint32_t temp = RotLeft(a, 5) + Maj(b, c, d) + e + K2 + w[i]; + e = d; d = c; c = RotLeft(b, 30); b = a; a = temp; + } + + for (int i = 60; i < 80; ++i) { + uint32_t temp = RotLeft(a, 5) + Parity(b, c, d) + e + K3 + w[i]; + e = d; d = c; c = RotLeft(b, 30); b = a; a = temp; + } + + state_[0] += a; + state_[1] += b; + state_[2] += c; + state_[3] += d; + state_[4] += e; +} + +void SHA1::Calc(const void *data_ptr, size_t data_len, uint8_t digest[20]) +{ + SHA1 sha1; + sha1.update(data_ptr, data_len); + sha1.finish(digest); +} + +} +} diff --git a/modules/crypto/sha1.h b/modules/crypto/sha1.h new file mode 100644 index 00000000..5fd67db4 --- /dev/null +++ b/modules/crypto/sha1.h @@ -0,0 +1,72 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2025 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_CRYPTO_SHA1_H_20260612 +#define TBOX_CRYPTO_SHA1_H_20260612 + +#include +#include + +namespace tbox { +namespace crypto { + +/** + * SHA-1 计算器 + * + * 使用方法与 MD5 类似: + * + * SHA1 sha1; + * sha1.update(data_ptr, data_len); + * uint8_t digest[20]; + * sha1.finish(digest); + * + * 或使用一次性接口: + * SHA1::Calc(data_ptr, data_len, digest); + * + * 注意:SHA-1 已不推荐用于安全目的,仅用于 WebSocket 握手等非安全场景 + */ +class SHA1 { + public: + SHA1(); + + public: + //! 将数据喂给SHA-1,可重复调用 + void update(const void *data_ptr, size_t data_len); + + //! 结束运算,输出20字节摘要到digest + void finish(uint8_t digest[20]); + + //! 一次性计算,便捷接口 + static void Calc(const void *data_ptr, size_t data_len, uint8_t digest[20]); + + private: + void transform(const uint8_t block[64]); + + uint32_t state_[5]; + uint64_t count_; + uint8_t buffer_[64]; + size_t buffer_index_; + + bool is_finished_ = false; +}; + +} +} + +#endif //TBOX_CRYPTO_SHA1_H_20260612 \ No newline at end of file diff --git a/modules/crypto/sha1_test.cpp b/modules/crypto/sha1_test.cpp new file mode 100644 index 00000000..7eabe2f5 --- /dev/null +++ b/modules/crypto/sha1_test.cpp @@ -0,0 +1,76 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2025 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include +#include + +#include "sha1.h" + +namespace tbox { +namespace crypto { + +TEST(SHA1, EmptyString) +{ + //! SHA-1("") = da39a3ee5e6b4b0d3255bfef95601890afd80709 + uint8_t digest[20]; + SHA1::Calc("", 0, digest); + + const uint8_t expected[] = { + 0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, + 0x32, 0x55, 0xbf, 0xef, 0x95, 0x60, 0x18, 0x90, + 0xaf, 0xd8, 0x07, 0x09 + }; + + EXPECT_EQ(0, memcmp(digest, expected, 20)); +} + +TEST(SHA1, Abc) +{ + //! SHA-1("abc") = a9993e364706816aba3e25717850c26c9cd0d89d + SHA1 sha1; + sha1.update("abc", 3); + uint8_t digest[20]; + sha1.finish(digest); + + const uint8_t expected[] = { + 0xa9, 0x99, 0x3e, 0x36, 0x47, 0x06, 0x81, 0x6a, + 0xba, 0x3e, 0x25, 0x71, 0x78, 0x50, 0xc2, 0x6c, + 0x9c, 0xd0, 0xd8, 0x9d + }; + + EXPECT_EQ(0, memcmp(digest, expected, 20)); +} + +TEST(SHA1, UpdateTwice) +{ + //! SHA-1("abc") via two updates should equal SHA-1("abc") via one update + SHA1 sha1_1; + sha1_1.update("a", 1); + sha1_1.update("bc", 2); + uint8_t digest1[20]; + sha1_1.finish(digest1); + + uint8_t digest2[20]; + SHA1::Calc("abc", 3, digest2); + + EXPECT_EQ(0, memcmp(digest1, digest2, 20)); +} + +} +} diff --git a/modules/http/CMakeLists.txt b/modules/http/CMakeLists.txt index 440b5fd0..2eed1c88 100644 --- a/modules/http/CMakeLists.txt +++ b/modules/http/CMakeLists.txt @@ -42,14 +42,21 @@ set(TBOX_HTTP_SOURCES server/middlewares/form_data.cpp server/middlewares/form_data_middleware.cpp server/middlewares/file_downloader_middleware.cpp - client/client.cpp) + server/sse/sse_event.cpp + server/sse/sse_connection.cpp + server/sse/sse_server_impl.cpp + client/client.cpp + client/client_impl.cpp + client/respond_parser.cpp) set(TBOX_HTTP_TEST_SOURCES common_test.cpp respond_test.cpp request_test.cpp url_test.cpp - server/request_parser_test.cpp) + server/request_parser_test.cpp + server/sse/sse_event_test.cpp + server/sse/sse_server_impl_test.cpp add_library(${TBOX_LIBRARY_NAME} ${TBOX_BUILD_LIB_TYPE} ${TBOX_HTTP_SOURCES}) add_library(tbox::${TBOX_LIBRARY_NAME} ALIAS ${TBOX_LIBRARY_NAME}) @@ -110,6 +117,14 @@ install( DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/tbox/http/client ) +install( + FILES + server/sse/sse_event.h + server/sse/sse_server.h + server/sse/sse_connection.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/tbox/http/server/sse +) + # generate and install export file install( EXPORT ${TBOX_LIBRARY_NAME}_targets diff --git a/modules/http/Makefile b/modules/http/Makefile index 6fc3bb02..de0442c8 100644 --- a/modules/http/Makefile +++ b/modules/http/Makefile @@ -37,6 +37,8 @@ HEAD_FILES = \ server/middlewares/form_data.h \ server/middlewares/form_data_middleware.h \ server/middlewares/file_downloader_middleware.h \ + server/sse/sse_event.h \ + server/sse/sse_server.h \ client/client.h \ CPP_SRC_FILES = \ @@ -52,7 +54,12 @@ CPP_SRC_FILES = \ server/middlewares/form_data.cpp \ server/middlewares/form_data_middleware.cpp \ server/middlewares/file_downloader_middleware.cpp \ + server/sse/sse_event.cpp \ + server/sse/sse_connection.cpp \ + server/sse/sse_server_impl.cpp \ client/client.cpp \ + client/client_impl.cpp \ + client/respond_parser.cpp \ CXXFLAGS := -DMODULE_ID='"tbox.http"' $(CXXFLAGS) @@ -63,6 +70,8 @@ TEST_CPP_SRC_FILES = \ request_test.cpp \ url_test.cpp \ server/request_parser_test.cpp \ + server/sse/sse_event_test.cpp \ + server/sse/sse_server_impl_test.cpp \ TEST_LDFLAGS := $(LDFLAGS) -ltbox_network -ltbox_log -ltbox_eventx -ltbox_event -ltbox_util -ltbox_base -ldl diff --git a/modules/http/README.md b/modules/http/README.md index cf398176..be33b63e 100644 --- a/modules/http/README.md +++ b/modules/http/README.md @@ -5,6 +5,8 @@ 本模块在设计时参考了 node.js 中 Express 的中间件设计思想。接口简洁,使用方便。 +## Server 端 + 示例: ```c++ @@ -14,7 +16,7 @@ using namespace tbox::network; using namespace tbox::http; using namespace tbox::http::server; -//! 假设已存在Loop的实例指针 sp_loop +//! 假设已存在Loop的实例指针 sp_loop Server srv(sp_loop); if (!srv.initialize(network::SockAddr::FromString(bind_addr), 1)) { @@ -39,4 +41,78 @@ srv.cleanup(); ``` -具体使用,请参考 example/ 下的示例。 +具体使用,请参考 `examples/http/server/` 下的示例。 + +## Client 端 + +Client 类实现了异步 HTTP 客户端,基于 TcpClient 实现连接管理与自动重连。 + +### 生命周期 + +与其它 tbox 组件一致,遵循 `initialize → start → stop → cleanup` 的生命周期模式。 + +### 示例 + +```c++ + +using namespace tbox; +using namespace tbox::event; +using namespace tbox::network; +using namespace tbox::http; +using namespace tbox::http::client; + +//! 假设已存在Loop的实例指针 sp_loop + +Client http_client(sp_loop); +if (!http_client.initialize(network::SockAddr::FromString("127.0.0.1:12345"))) { + LogErr("init http_client fail"); + return 0; +} + +http_client.setAutoReconnect(true); +http_client.setRequestTimeout(std::chrono::seconds(10)); +http_client.start(); + +//! 简单 GET 请求 +http_client.request(Method::kGet, "/", + [](const Respond &res) { + LogInfo("GET / => status: %d, body: %s", + (int)res.status_code, res.body.c_str()); + }); + +//! POST 请求 +http_client.request(Method::kPost, "/api/data", + "{\"key\":\"value\"}", + {{"Content-Type", "application/json"}}, + [](const Respond &res) { + LogInfo("POST /api/data => status: %d", (int)res.status_code); + }); + +//! 完整 Request 对象 +Request req; +req.method = Method::kPut; +req.http_ver = HttpVer::k1_1; +req.url.path = "/api/update"; +req.headers["Content-Type"] = "application/json"; +req.body = "{\"id\":123}"; +http_client.request(req, + [](const Respond &res) { + LogInfo("PUT /api/update => status: %d", (int)res.status_code); + }); + +sp_loop->runLoop(); +http_client.cleanup(); + +``` + +具体使用,请参考 `examples/http/client/` 下的示例。 + +### 特性说明 + +| 特性 | 说明 | +|------|------| +| 自动重连 | 断线后自动重连服务器,可通过 setAutoReconnect() 控制 | +| 请求超时 | 每个请求有独立的超时定时器,默认 30 秒 | +| 请求缓存 | 未连接时请求会被缓存,连接建立后自动发送 | +| 回调通知 | 支持连接成功、连接失败、断线等回调通知 | +| 便捷方法 | request() 支持 3 种重载,降低使用门槛 | diff --git a/modules/http/client/client.cpp b/modules/http/client/client.cpp index b6e073ea..94f5956a 100644 --- a/modules/http/client/client.cpp +++ b/modules/http/client/client.cpp @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2026 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -18,36 +18,102 @@ * of the source tree. */ #include "client.h" +#include "client_impl.h" namespace tbox { namespace http { namespace client { -using namespace event; -using namespace network; +Client::Client(event::Loop *wp_loop) + : impl_(new Impl(this, wp_loop)) +{ } -Client::Client(Loop *wp_loop) +Client::~Client() { - (void)wp_loop; + CHECK_DELETE_RESET_OBJ(impl_); } -Client::~Client() -{ } +bool Client::initialize(const network::SockAddr &server_addr) +{ + return impl_->initialize(server_addr); +} -bool Client::initialize(const SockAddr &server_addr) +void Client::setTlsConfig(const network::TlsConfig &config) { - (void)server_addr; - return false; + impl_->setTlsConfig(config); } -void Client::request(const Request &req, const RespondCallback &cb) +bool Client::start() +{ + return impl_->start(); +} + +void Client::stop() { - (void)req; - (void)cb; + impl_->stop(); } void Client::cleanup() -{ } +{ + impl_->cleanup(); +} + +Client::State Client::state() const +{ + return impl_->state(); +} + +void Client::request(const Request &req, const RespondCallback &cb) +{ + impl_->request(req, cb); +} + +void Client::request(Method method, const std::string &path, const RespondCallback &cb) +{ + impl_->request(method, path, cb); +} + +void Client::request(Method method, const std::string &path, + const std::string &body, const Headers &headers, + const RespondCallback &cb) +{ + impl_->request(method, path, body, headers, cb); +} + +void Client::setConnectedCallback(const ConnectedCallback &cb) +{ + impl_->setConnectedCallback(cb); +} + +void Client::setConnectFailCallback(const ConnectFailCallback &cb) +{ + impl_->setConnectFailCallback(cb); +} + +void Client::setDisconnectedCallback(const DisconnectedCallback &cb) +{ + impl_->setDisconnectedCallback(cb); +} + +void Client::setAutoReconnect(bool enable) +{ + impl_->setAutoReconnect(enable); +} + +void Client::setReconnectDelayCalcFunc(const ReconnectDelayCalc &func) +{ + impl_->setReconnectDelayCalcFunc(func); +} + +void Client::setRequestTimeout(std::chrono::milliseconds ms) +{ + impl_->setRequestTimeout(ms); +} + +void Client::setContextLogEnable(bool enable) +{ + impl_->setContextLogEnable(enable); +} } } diff --git a/modules/http/client/client.h b/modules/http/client/client.h index 539f19fb..34125fce 100644 --- a/modules/http/client/client.h +++ b/modules/http/client/client.h @@ -9,7 +9,7 @@ * \\ \ \ / * -============' * - * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * Copyright (c) 2026 Hevake and contributors, all rights reserved. * * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) * Use of this source code is governed by MIT license that can be found @@ -22,7 +22,10 @@ #include #include +#include +#include +#include "../common.h" #include "../request.h" #include "../respond.h" @@ -35,22 +38,63 @@ class Client { explicit Client(event::Loop *wp_loop); virtual ~Client(); + NONCOPYABLE(Client); + IMMOVABLE(Client); + public: - //! 初始化,设置目标服务器 + //! 状态 + enum class State { + kNone, //!< 未初始化 + kInited, //!< 已初始化 + kConnecting, //!< 连接中 + kConnected, //!< 已连接 + kReconnWaiting, //!< 断连等待重连中 + }; + + //! 初始化,设置目标服务器地址 bool initialize(const network::SockAddr &server_addr); + //! 设置 TLS 配置(必须在 initialize() 之前调用) + //! 需要 network_tls 模块支持,未链接时调用无效 + void setTlsConfig(const network::TlsConfig &config); + + bool start(); //!< 开始连接 + void stop(); //!< 停止/断开连接 + void cleanup(); //!< 清理,与 initialize() 是逆操作 + + State state() const; + + public: //! 收到回复时的回调 using RespondCallback = std::function; - /** - * \brief 发送请求 - * \param req 请求数据 - * \param cb 回复的回调 - */ + //! 发送请求(完整 Request 对象) void request(const Request &req, const RespondCallback &cb); - //! 清理,与initialize()是逆操作 - void cleanup(); + //! 发送请求(便捷方法:指定 Method 和 path) + void request(Method method, const std::string &path, const RespondCallback &cb); + + //! 发送请求(便捷方法:指定 Method、path、body、headers) + void request(Method method, const std::string &path, + const std::string &body, const Headers &headers, + const RespondCallback &cb); + + public: + //! 连接相关回调 + using ConnectedCallback = std::function; + using ConnectFailCallback = std::function; + using DisconnectedCallback = std::function; + using ReconnectDelayCalc = std::function; + + void setConnectedCallback(const ConnectedCallback &cb); + void setConnectFailCallback(const ConnectFailCallback &cb); + void setDisconnectedCallback(const DisconnectedCallback &cb); + + //! 配置 + void setAutoReconnect(bool enable); + void setReconnectDelayCalcFunc(const ReconnectDelayCalc &func); + void setRequestTimeout(std::chrono::milliseconds ms); + void setContextLogEnable(bool enable); private: class Impl; @@ -60,5 +104,4 @@ class Client { } } } - #endif //TBOX_HTTP_CLIENT_H_20220504 diff --git a/modules/http/client/client_impl.cpp b/modules/http/client/client_impl.cpp new file mode 100644 index 00000000..748331bb --- /dev/null +++ b/modules/http/client/client_impl.cpp @@ -0,0 +1,451 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S E / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "client_impl.h" + +#include +#include +#include +#include +#include + +namespace tbox { +namespace http { +namespace client { + +using namespace std; +using namespace std::placeholders; +using namespace event; +using namespace network; + +Client::Impl::Impl(Client *wp_parent, Loop *wp_loop) + : wp_parent_(wp_parent) + , wp_loop_(wp_loop) + , tcp_client_(wp_loop) +{ } + +Client::Impl::~Impl() +{ + TBOX_ASSERT(cb_level_ == 0); + cleanup(); +} + +bool Client::Impl::initialize(const SockAddr &server_addr) +{ + if (state_ != State::kNone) { + LogWarn("state not right, can't initialize"); + return false; + } + + if (!tcp_client_.initialize(server_addr)) + return false; + + tcp_client_.setConnectedCallback(bind(&Impl::onTcpConnected, this)); + tcp_client_.setDisconnectedCallback(bind(&Impl::onTcpDisconnected, this)); + tcp_client_.setReceiveCallback(bind(&Impl::onTcpReceived, this, _1), 0); + + state_ = State::kInited; + return true; +} + +void Client::Impl::setTlsConfig(const TlsConfig &config) +{ + tcp_client_.setTlsConfig(config); +} + +bool Client::Impl::start() +{ + if (state_ != State::kInited) { + LogWarn("state not right, can't start"); + return false; + } + + if (tcp_client_.start()) { + state_ = State::kConnecting; + return true; + } + return false; +} + +void Client::Impl::stop() +{ + if (state_ == State::kConnected || state_ == State::kConnecting || state_ == State::kReconnWaiting) { + tcp_client_.stop(); + //! 清理所有缓存和待处理的请求 + failAllPendingRequests(StatusCode::k503_ServiceUnavailable, "client stopped"); + for (auto &item : cached_requests_) + CHECK_DELETE_RESET_OBJ(item.sp_req); + cached_requests_.clear(); + res_parser_.reset(); + state_ = State::kInited; + } +} + +void Client::Impl::cleanup() +{ + if (state_ != State::kNone) { + stop(); + + //! 释放所有超时定时器 + for (auto &item : pending_requests_) + CHECK_DELETE_RESET_OBJ(item.sp_timer); + pending_requests_.clear(); + + //! 释放所有缓存请求 + for (auto &item : cached_requests_) + CHECK_DELETE_RESET_OBJ(item.sp_req); + cached_requests_.clear(); + + tcp_client_.cleanup(); + + state_ = State::kNone; + } +} + +void Client::Impl::request(const Request &req, const RespondCallback &cb) +{ + if (state_ == State::kNone) { + LogWarn("state is kNone, need initialize first"); + return; + } + + sendRequest(req, cb); +} + +void Client::Impl::request(Method method, const string &path, const RespondCallback &cb) +{ + Request req; + req.method = method; + req.http_ver = HttpVer::k1_1; + req.url.path = path; + request(req, cb); +} + +void Client::Impl::request(Method method, const string &path, const string &body, + const Headers &headers, const RespondCallback &cb) +{ + Request req; + req.method = method; + req.http_ver = HttpVer::k1_1; + req.url.path = path; + req.body = body; + req.headers = headers; + request(req, cb); +} + +void Client::Impl::setAutoReconnect(bool enable) +{ + auto_reconnect_ = enable; + tcp_client_.setAutoReconnect(enable); +} + +void Client::Impl::setReconnectDelayCalcFunc(const ReconnectDelayCalc &func) +{ + tcp_client_.setReconnectDelayCalcFunc(func); +} + +void Client::Impl::setRequestTimeout(chrono::milliseconds ms) +{ + request_timeout_ms_ = ms; +} + +void Client::Impl::setContextLogEnable(bool enable) +{ + context_log_enable_ = enable; +} + +void Client::Impl::setConnectedCallback(const ConnectedCallback &cb) +{ + connected_cb_ = cb; +} + +void Client::Impl::setConnectFailCallback(const ConnectFailCallback &cb) +{ + connect_fail_cb_ = cb; +} + +void Client::Impl::setDisconnectedCallback(const DisconnectedCallback &cb) +{ + disconnected_cb_ = cb; +} + +//! ============ TCP 连接回调 ============ + +void Client::Impl::onTcpConnected() +{ + RECORD_SCOPE(); + state_ = State::kConnected; + + LogDbg("connected"); + + //! 发送缓存中的请求 + sendCachedRequests(); + + if (connected_cb_) { + ++cb_level_; + connected_cb_(); + --cb_level_; + } +} + +void Client::Impl::onTcpConnectFail() +{ + RECORD_SCOPE(); + LogDbg("connect fail"); + + //! failAllPendingRequests 中会处理所有 pending 请求 + //! 但 connect fail 时,pending_requests_ 应为空(因为未连接时请求在 cached_requests_ 中) + //! cached_requests_ 中的请求也要失败回调 + for (auto &item : cached_requests_) { + Respond res; + res.http_ver = HttpVer::k1_1; + res.status_code = StatusCode::k503_ServiceUnavailable; + res.body = "connect fail"; + if (item.cb) { + ++cb_level_; + item.cb(res); + --cb_level_; + } + CHECK_DELETE_RESET_OBJ(item.sp_req); + } + cached_requests_.clear(); + + if (connect_fail_cb_) { + ++cb_level_; + connect_fail_cb_(); + --cb_level_; + } + + if (!auto_reconnect_) { + state_ = State::kInited; + } else { + state_ = State::kReconnWaiting; + } +} + +void Client::Impl::onTcpDisconnected() +{ + RECORD_SCOPE(); + LogDbg("disconnected"); + + //! 对所有 pending 请求执行错误回调 + failAllPendingRequests(StatusCode::k504_GatewayTimeout, "connection lost"); + + res_parser_.reset(); + + if (disconnected_cb_) { + ++cb_level_; + disconnected_cb_(); + --cb_level_; + } + + if (auto_reconnect_) { + state_ = State::kReconnWaiting; + } else { + state_ = State::kInited; + } +} + +void Client::Impl::onTcpReceived(Buffer &buff) +{ + RECORD_SCOPE(); + + while (buff.readableSize() > 0) { + size_t rsize = res_parser_.parse(buff.readableBegin(), buff.readableSize()); + buff.hasRead(rsize); + + if (res_parser_.state() == RespondParser::State::kFinishedAll) { + Respond *sp_respond = res_parser_.getRespond(); + if (sp_respond == nullptr) { + LogWarn("getRespond() return nullptr, skip"); + continue; + } + + if (context_log_enable_) + LogDbg("RES: [%s]", sp_respond->toString().c_str()); + + //! 取出 pending_requests_ 队首的请求 + if (!pending_requests_.empty()) { + auto &pending = pending_requests_.front(); + + //! 停止超时定时器 + if (pending.sp_timer != nullptr) { + pending.sp_timer->disable(); + CHECK_DELETE_RESET_OBJ(pending.sp_timer); + } + + //! 调用回调 + if (pending.cb) { + ++cb_level_; + pending.cb(*sp_respond); + --cb_level_; + } + + pending_requests_.pop_front(); + } else { + LogWarn("no pending request for this respond"); + } + + delete sp_respond; + + } else if (res_parser_.state() == RespondParser::State::kFail) { + LogNotice("parse http respond fail"); + failAllPendingRequests(StatusCode::k502_BadGateway, "respond parse fail"); + //! 断开连接,让 TcpClient 重连 + tcp_client_.stop(); + if (auto_reconnect_) { + tcp_client_.start(); + state_ = State::kReconnWaiting; + } else { + state_ = State::kInited; + } + break; + + } else { + //! 数据不完整,等待更多数据 + break; + } + } +} + +//! ============ 请求发送 ============ + +void Client::Impl::sendRequest(const Request &req, const RespondCallback &cb) +{ + if (state_ == State::kConnected) { + //! 已连接,直接发送 + const string &content = req.toString(); + tcp_client_.send(content.data(), content.size()); + + if (context_log_enable_) + LogDbg("REQ: [%s]", content.c_str()); + + //! 创建 PendingRequest,加入队列 + PendingRequest pending; + pending.req_id = next_req_id_++; + pending.cb = cb; + + //! 创建超时定时器 + if (request_timeout_ms_.count() > 0) { + auto sp_timer = wp_loop_->newTimerEvent(); + sp_timer->initialize(request_timeout_ms_, Event::Mode::kOneshot); + sp_timer->setCallback(bind(&Impl::onRequestTimeout, this, pending.req_id)); + sp_timer->enable(); + pending.sp_timer = sp_timer; + } + + pending_requests_.push_back(pending); + + } else { + //! 未连接,缓存请求,等连接建立后发送 + CachedRequest cached; + cached.sp_req = new Request(req); + cached.cb = cb; + cached_requests_.push_back(cached); + } +} + +void Client::Impl::sendCachedRequests() +{ + //! 将缓存中的所有请求发送出去 + for (auto &item : cached_requests_) { + sendRequest(*item.sp_req, item.cb); + CHECK_DELETE_RESET_OBJ(item.sp_req); + } + cached_requests_.clear(); +} + +//! ============ 超时与失败处理 ============ + +void Client::Impl::onRequestTimeout(int req_id) +{ + RECORD_SCOPE(); + LogDbg("request %d timeout", req_id); + + //! 在 pending_requests_ 中查找该请求 + for (auto iter = pending_requests_.begin(); iter != pending_requests_.end(); ++iter) { + if (iter->req_id == req_id) { + //! 构造超时响应 + Respond res; + res.http_ver = HttpVer::k1_1; + res.status_code = StatusCode::k408_RequestTimeout; + res.body = "request timeout"; + + if (iter->cb) { + ++cb_level_; + iter->cb(res); + --cb_level_; + } + + //! 清理定时器 + CHECK_DELETE_RESET_OBJ(iter->sp_timer); + + //! 从队列中移除 + //! 注意:超时中间的请求被移除后,后续请求仍可正常接收响应 + //! 但如果服务器按序回复,中间的请求超时意味着后续请求可能也无法收到响应 + //! 这里采用保守策略:仅移除超时的请求,不影响其他请求 + pending_requests_.erase(iter); + break; + } + } +} + +void Client::Impl::failAllPendingRequests(StatusCode status_code, const string &message) +{ + for (auto &item : pending_requests_) { + //! 停止超时定时器 + if (item.sp_timer != nullptr) { + item.sp_timer->disable(); + CHECK_DELETE_RESET_OBJ(item.sp_timer); + } + + //! 构造错误响应 + Respond res; + res.http_ver = HttpVer::k1_1; + res.status_code = status_code; + res.body = message; + + if (item.cb) { + ++cb_level_; + item.cb(res); + --cb_level_; + } + } + pending_requests_.clear(); + + //! 也处理缓存中的请求 + for (auto &item : cached_requests_) { + Respond res; + res.http_ver = HttpVer::k1_1; + res.status_code = status_code; + res.body = message; + + if (item.cb) { + ++cb_level_; + item.cb(res); + --cb_level_; + } + CHECK_DELETE_RESET_OBJ(item.sp_req); + } + cached_requests_.clear(); +} + +} +} +} diff --git a/modules/http/client/client_impl.h b/modules/http/client/client_impl.h new file mode 100644 index 00000000..8ddc31ae --- /dev/null +++ b/modules/http/client/client_impl.h @@ -0,0 +1,128 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_HTTP_CLIENT_IMP_H_20260614 +#define TBOX_HTTP_CLIENT_IMP_H_20260614 + +#include +#include +#include + +#include +#include +#include + +#include "client.h" +#include "respond_parser.h" + +namespace tbox { +namespace http { + +struct Request; + +namespace client { + +using namespace event; +using namespace network; +using namespace std; + +class Client::Impl { + public: + Impl(Client *wp_parent, event::Loop *wp_loop); + ~Impl(); + + public: + bool initialize(const SockAddr &server_addr); + void setTlsConfig(const TlsConfig &config); + bool start(); + void stop(); + void cleanup(); + State state() const { return state_; } + + void request(const Request &req, const RespondCallback &cb); + void request(Method method, const string &path, const RespondCallback &cb); + void request(Method method, const string &path, const string &body, + const Headers &headers, const RespondCallback &cb); + + void setAutoReconnect(bool enable); + void setReconnectDelayCalcFunc(const ReconnectDelayCalc &func); + void setRequestTimeout(chrono::milliseconds ms); + void setContextLogEnable(bool enable); + + void setConnectedCallback(const ConnectedCallback &cb); + void setConnectFailCallback(const ConnectFailCallback &cb); + void setDisconnectedCallback(const DisconnectedCallback &cb); + + private: + void onTcpConnected(); + void onTcpConnectFail(); + void onTcpDisconnected(); + void onTcpReceived(Buffer &buff); + + //! 发送一个请求 + void sendRequest(const Request &req, const RespondCallback &cb); + + //! 对所有 pending 请求执行错误回调(断线或超时) + void failAllPendingRequests(StatusCode status_code, const string &message); + + //! 发送缓存中的所有请求 + void sendCachedRequests(); + + //! 请求超时处理 + void onRequestTimeout(int req_id); + + private: + Client *wp_parent_; + event::Loop *wp_loop_; + + network::TcpClient tcp_client_; + RespondParser res_parser_; + + //! 请求队列 + struct PendingRequest { + int req_id; + RespondCallback cb; + event::TimerEvent *sp_timer = nullptr; //! 超时定时器 + }; + deque pending_requests_; + int next_req_id_ = 0; + chrono::milliseconds request_timeout_ms_ = chrono::seconds(30); + + //! 在未连接时缓存的请求 + struct CachedRequest { + Request *sp_req; + RespondCallback cb; + }; + deque cached_requests_; + + State state_ = State::kNone; + bool auto_reconnect_ = true; + bool context_log_enable_ = false; + + ConnectedCallback connected_cb_; + ConnectFailCallback connect_fail_cb_; + DisconnectedCallback disconnected_cb_; + + int cb_level_ = 0; +}; + +} +} +} +#endif //TBOX_HTTP_CLIENT_IMP_H_20260614 diff --git a/modules/http/client/respond_parser.cpp b/modules/http/client/respond_parser.cpp new file mode 100644 index 00000000..ef66575c --- /dev/null +++ b/modules/http/client/respond_parser.cpp @@ -0,0 +1,189 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "respond_parser.h" +#include +#include +#include +#include +#include + +namespace tbox { +namespace http { +namespace client { + +RespondParser::~RespondParser() +{ + CHECK_DELETE_RESET_OBJ(sp_respond_); +} + +size_t RespondParser::parse(const void *data_ptr, size_t data_size) +{ + std::string str(static_cast(data_ptr), data_size); + size_t pos = 0; + + if (state_ == State::kInit) { + content_length_ = std::numeric_limits::max(); + if (sp_respond_ == nullptr) + sp_respond_ = new Respond; + + //! 解析首行:"HTTP/1.1 200 OK\r\n" + auto end_pos = str.find(CRLF, pos); + if (end_pos == std::string::npos) //! 首行不完整 + return 0; + + //! 提取 HTTP 版本 + auto space1_pos = str.find_first_of(' ', pos); + if (space1_pos == std::string::npos || space1_pos >= end_pos) { + LogNotice("respond start line format invalid"); + state_ = State::kFail; + return pos; + } + + auto ver_str = str.substr(pos, space1_pos - pos); + if (ver_str.compare(0, 5, "HTTP/") != 0) { + LogNotice("respond version invalid, ver_str:%s", ver_str.c_str()); + state_ = State::kFail; + return pos; + } + + auto ver = StringToHttpVer(ver_str); + if (ver == HttpVer::kUnset) { + LogNotice("respond version invalid, ver_str:%s", ver_str.c_str()); + state_ = State::kFail; + return pos; + } + + sp_respond_->http_ver = ver; + + //! 提取状态码 + auto code_begin = str.find_first_not_of(' ', space1_pos); + if (code_begin == std::string::npos || code_begin >= end_pos) { + LogNotice("respond status code not exist"); + state_ = State::kFail; + return pos; + } + + auto code_end = str.find_first_of(' ', code_begin); + if (code_end == std::string::npos || code_end > end_pos) + code_end = end_pos; + + auto code_str = str.substr(code_begin, code_end - code_begin); + int status_code_num = 0; + if (!util::StringTo(code_str, status_code_num)) { + LogNotice("respond status code not number, code_str:%s", code_str.c_str()); + state_ = State::kFail; + return pos; + } + + auto status_code = StringToStatusCode(std::to_string(status_code_num)); + if (status_code == StatusCode::kUnset) { + LogNotice("respond status code invalid, code:%d", status_code_num); + state_ = State::kFail; + return pos; + } + + sp_respond_->status_code = status_code; + + pos = end_pos + 2; + state_ = State::kFinishedStartLine; + } + + if (state_ == State::kFinishedStartLine) { + //! 解析 headers:"Key: Value\r\n" 直到空行 "\r\n" + for (;;) { + auto end_pos = str.find(CRLF, pos); + + if (end_pos == pos) { //! 找到了空行,headers 结束 + state_ = State::kFinishedHeads; + pos += 2; + break; + + } else if (end_pos == std::string::npos) { //! 当前的 header 不完整 + break; + } + + auto colon_pos = str.find_first_of(':', pos); + if (colon_pos == std::string::npos || colon_pos >= end_pos) { + LogNotice("can't find ':' in header line"); + state_ = State::kFail; + return pos; + } + + auto head_key = util::string::Strip(str.substr(pos, colon_pos - pos)); + auto head_value_start_pos = str.find_first_not_of(' ', colon_pos + 1); + + if (head_value_start_pos < end_pos) { + auto head_value = util::string::Strip(str.substr(head_value_start_pos, end_pos - head_value_start_pos)); + sp_respond_->headers[head_key] = head_value; + + if (head_key == "Content-Length") { + if (!util::StringTo(head_value, content_length_)) { + LogNotice("Content-Length should be number"); + state_ = State::kFail; + return pos; + } + } + } else { + sp_respond_->headers[head_key] = ""; + } + + pos = end_pos + 2; + } + } + + if (state_ == State::kFinishedHeads) { + if (content_length_ != std::numeric_limits::max()) { //! 有 Content-Length + if ((data_size - pos) >= content_length_) { + sp_respond_->body = str.substr(pos, content_length_); + pos += content_length_; + state_ = State::kFinishedAll; + } + } else { + //! 没有 Content-Length,body 到连接关闭为止(本次全部当作 body) + if (data_size > pos) + sp_respond_->body = str.substr(pos); + pos = data_size; + state_ = State::kFinishedAll; + } + } + + return pos; +} + +Respond* RespondParser::getRespond() +{ + Respond *ret = nullptr; + if (state_ == State::kFinishedAll) { + std::swap(ret, sp_respond_); + state_ = State::kInit; + } + return ret; +} + +void RespondParser::reset() +{ + CHECK_DELETE_RESET_OBJ(sp_respond_); + state_ = State::kInit; + content_length_ = std::numeric_limits::max(); +} + +} +} +} diff --git a/modules/http/client/respond_parser.h b/modules/http/client/respond_parser.h new file mode 100644 index 00000000..07910bdf --- /dev/null +++ b/modules/http/client/respond_parser.h @@ -0,0 +1,76 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_HTTP_CLIENT_RESPOND_PARSER_H_20260614 +#define TBOX_HTTP_CLIENT_RESPOND_PARSER_H_20260614 + +#include "../respond.h" + +namespace tbox { +namespace http { +namespace client { + +//! 响应解析器 +class RespondParser { + public: + //! 状态 + enum class State { + kInit, //!< 初始化,未开始 + kFinishedStartLine, //!< 完成了首行解析 + kFinishedHeads, //!< 完成 heads 的解析 + kFinishedAll, //!< 完成了整个 HTTP 响应的解析 + kFail, //!< 解析出错 + }; + + ~RespondParser(); + + /** + * \brief 解析 + * \param data_ptr 数据地址 + * \param data_size 数据大小 + * \return size_t 已处理数据大小 + */ + size_t parse(const void *data_ptr, size_t data_size); + + //! 获取状态 + State state() const { return state_; } + + /** + * \brief 取走 Respond 对象 + * \return Respond* 响应对象 + * \note 只有 state 为 kFinishedAll 才会返回真实的对象,否则都是返回 nullptr + * 一旦 Respond 对象被取走,RespondParser 则不再管辖被取走对象的生命期 + * 交由用户自己管理 + */ + Respond* getRespond(); + + //! 重置 + void reset(); + + private: + State state_ = State::kInit; + Respond *sp_respond_ = nullptr; + size_t content_length_ = 0; +}; + +} +} +} + +#endif //TBOX_HTTP_CLIENT_RESPOND_PARSER_H_20260614 diff --git a/modules/http/common.cpp b/modules/http/common.cpp index 769b6f42..a8c4e707 100644 --- a/modules/http/common.cpp +++ b/modules/http/common.cpp @@ -107,6 +107,7 @@ Method StringToMethod(const std::string &str) namespace { using StatusCodePair = std::pair; StatusCodePair _status_code_map[] = { + { StatusCode::k101_SwitchingProtocols, "101 Switching Protocols"}, { StatusCode::k200_OK, "200 OK"}, { StatusCode::k201_Created, "201 Created"}, { StatusCode::k202_Accepted, "202 Accepted"}, diff --git a/modules/http/common.h b/modules/http/common.h index b68cd8f5..ed79d7f8 100644 --- a/modules/http/common.h +++ b/modules/http/common.h @@ -62,6 +62,7 @@ enum class StatusCode { kUnset, //! 正常 + k101_SwitchingProtocols = 101, k200_OK = 200, k201_Created = 201, k202_Accepted = 202, diff --git a/modules/http/respond.cpp b/modules/http/respond.cpp index f527d082..4619bfeb 100644 --- a/modules/http/respond.cpp +++ b/modules/http/respond.cpp @@ -40,7 +40,10 @@ std::string Respond::toString() const has_content_length = true; } - if (!has_content_length) + //! 当 upgrade_cb 已设置时(WebSocket 101、SSE 200 等),不自动添加 Content-Length + //! 原因:升级/流式响应后面是持续的数据流(WebSocket 帧、SSE 事件),不是定长 body + //! Content-Length 会误导浏览器认为响应已完成,阻止流式数据接收 + if (!has_content_length && !upgrade_cb) oss << "Content-Length: " << body.length() << CRLF; oss << CRLF; diff --git a/modules/http/respond.h b/modules/http/respond.h index 0d4571c9..e3f51746 100644 --- a/modules/http/respond.h +++ b/modules/http/respond.h @@ -22,6 +22,14 @@ #include "common.h" +#include + +namespace tbox { +namespace network { +class TcpConnection; +} +} + namespace tbox { namespace http { @@ -32,6 +40,12 @@ struct Respond { Headers headers; std::string body; + //! 协议升级回调(用于 WebSocket、SSE 等场景) + //! 中间件检测到升级请求后,设置适当的响应头,并将接管连接的回调注册于此 + //! HTTP 服务器发送响应后,通过此回调将 TcpConnection 交给升级协议处理 + using UpgradeCallback = std::function; + UpgradeCallback upgrade_cb; + bool isValid() const; std::string toString() const; }; diff --git a/modules/http/server/middlewares/file_downloader_middleware.cpp b/modules/http/server/middlewares/file_downloader_middleware.cpp index 6c326e05..48db8e7d 100644 --- a/modules/http/server/middlewares/file_downloader_middleware.cpp +++ b/modules/http/server/middlewares/file_downloader_middleware.cpp @@ -237,6 +237,11 @@ void FileDownloaderMiddleware::setPathMapping(const std::string& url, const std: d_->path_mappings[url] = file; } +void FileDownloaderMiddleware::unsetPathMapping(const std::string& url) +{ + d_->path_mappings.erase(url); +} + void FileDownloaderMiddleware::setDefaultMimeType(const std::string& mime_type) { d_->default_mime_type = mime_type; diff --git a/modules/http/server/middlewares/file_downloader_middleware.h b/modules/http/server/middlewares/file_downloader_middleware.h index 9a61a1ba..9f7f3eb7 100644 --- a/modules/http/server/middlewares/file_downloader_middleware.h +++ b/modules/http/server/middlewares/file_downloader_middleware.h @@ -79,6 +79,12 @@ class FileDownloaderMiddleware : public Middleware { * \param file 文件路径 */ void setPathMapping(const std::string& url, const std::string& file); + /** + * 取消路径映射 + * + * \param url URL路径 + */ + void unsetPathMapping(const std::string& url); /** * 设置默认的MIME类型 diff --git a/modules/http/server/server.cpp b/modules/http/server/server.cpp index 1951253e..d002695d 100644 --- a/modules/http/server/server.cpp +++ b/modules/http/server/server.cpp @@ -38,6 +38,11 @@ bool Server::initialize(const network::SockAddr &bind_addr, int listen_backlog) return impl_->initialize(bind_addr, listen_backlog); } +bool Server::setTlsConfig(const network::TlsConfig &config) +{ + return impl_->setTlsConfig(config); +} + bool Server::start() { return impl_->start(); @@ -63,14 +68,19 @@ void Server::setContextLogEnable(bool enable) return impl_->setContextLogEnable(enable); } -void Server::use(RequestHandler &&handler) +MiddlewareToken Server::use(RequestHandler &&handler) +{ + return impl_->use(std::move(handler)); +} + +MiddlewareToken Server::use(Middleware *wp_middleware) { - impl_->use(std::move(handler)); + return impl_->use(wp_middleware); } -void Server::use(Middleware *wp_middleware) +bool Server::unuse(const MiddlewareToken &token) { - impl_->use(wp_middleware); + return impl_->unuse(token); } } diff --git a/modules/http/server/server.h b/modules/http/server/server.h index d3bcee96..d7a5a715 100644 --- a/modules/http/server/server.h +++ b/modules/http/server/server.h @@ -22,6 +22,7 @@ #include #include +#include #include "../common.h" #include "../request.h" @@ -45,6 +46,9 @@ class Server { public: bool initialize(const network::SockAddr &bind_addr, int listen_backlog); + //! 设置 TLS 配置(必须在 initialize() 之前调用) + //! 需要 network_tls 模块支持,未链接时调用无效 + bool setTlsConfig(const network::TlsConfig &config); bool start(); void stop(); void cleanup(); @@ -54,8 +58,9 @@ class Server { void setContextLogEnable(bool enable); public: - void use(RequestHandler &&handler); - void use(Middleware *wp_middleware); + MiddlewareToken use(RequestHandler &&handler); + MiddlewareToken use(Middleware *wp_middleware); + bool unuse(const MiddlewareToken &token); private: class Impl; diff --git a/modules/http/server/server_imp.cpp b/modules/http/server/server_imp.cpp index cb5f28d5..800cb7c3 100644 --- a/modules/http/server/server_imp.cpp +++ b/modules/http/server/server_imp.cpp @@ -19,10 +19,13 @@ */ #include "server_imp.h" +#include + #include #include #include #include +#include #include "middleware.h" @@ -35,9 +38,10 @@ using namespace std::placeholders; using namespace event; using namespace network; -Server::Impl::Impl(Server *wp_parent, Loop *wp_loop) : - wp_parent_(wp_parent), - tcp_server_(wp_loop) +Server::Impl::Impl(Server *wp_parent, Loop *wp_loop) + : wp_parent_(wp_parent) + , wp_loop_(wp_loop) + , tcp_server_(wp_loop) { } Server::Impl::~Impl() @@ -59,6 +63,11 @@ bool Server::Impl::initialize(const network::SockAddr &bind_addr, int listen_bac return true; } +bool Server::Impl::setTlsConfig(const network::TlsConfig &config) +{ + return tcp_server_.setTlsConfig(config); +} + bool Server::Impl::start() { if (tcp_server_.start()) { @@ -81,21 +90,58 @@ void Server::Impl::cleanup() if (state_ != State::kNone) { stop(); - req_handler_.clear(); + mw_cabinet_.foreach([](RequestHandler *ptr) { delete ptr; }); + mw_cabinet_.clear(); + mw_order_.clear(); tcp_server_.cleanup(); state_ = State::kNone; } } -void Server::Impl::use(RequestHandler &&handler) +MiddlewareToken Server::Impl::use(RequestHandler &&handler) +{ + if (cb_level_ > 0) { + LogWarn("不能在 next 链中调用 use(),请使用 Loop 的 runNext() 来处理"); + return MiddlewareToken(); + } + + auto token = mw_cabinet_.alloc(new RequestHandler(std::move(handler))); + mw_order_.push_back(token); + return token; +} + +MiddlewareToken Server::Impl::use(Middleware *wp_middleware) { - req_handler_.push_back(std::move(handler)); + if (cb_level_ > 0) { + LogWarn("不能在 next 链中调用 use(),请使用 Loop 的 runNext() 来处理"); + return MiddlewareToken(); + } + + auto token = mw_cabinet_.alloc(new RequestHandler(bind(&Middleware::handle, wp_middleware, _1, _2))); + mw_order_.push_back(token); + return token; } -void Server::Impl::use(Middleware *wp_middleware) +bool Server::Impl::unuse(const MiddlewareToken &token) { - req_handler_.push_back(bind(&Middleware::handle, wp_middleware, _1, _2)); + if (cb_level_ > 0) { + LogWarn("不能在 next 链中调用 unuse(),请使用 Loop 的 runNext() 来处理"); + return false; + } + + //! 从 Cabinet 中释放 + RequestHandler *handler = mw_cabinet_.free(token); + if (handler == nullptr) + return false; //! token 无效或已释放 + delete handler; + + //! 从 order 中删除该 token(低频 O(n) 操作) + auto iter = find(mw_order_.begin(), mw_order_.end(), token); + if (iter != mw_order_.end()) + mw_order_.erase(iter); + + return true; } void Server::Impl::onTcpConnected(const TcpServer::ConnToken &ct) @@ -145,6 +191,12 @@ void Server::Impl::onTcpReceived(const TcpServer::ConnToken &ct, Buffer &buff) return; } + //! 如果已被标记为升级请求,停止解析 HTTP 数据 + if (conn->is_upgrade) { + //! 不消费剩余数据,留给升级后的协议处理 + return; + } + while (buff.readableSize() > 0) { size_t rsize = conn->req_parser.parse(buff.readableBegin(), buff.readableSize()); buff.hasRead(rsize); @@ -155,17 +207,25 @@ void Server::Impl::onTcpReceived(const TcpServer::ConnToken &ct, Buffer &buff) if (context_log_enable_) LogDbg("REQ: [%s]", req->toString().c_str()); + auto sp_ctx = make_shared(wp_parent_, ct, conn->req_index++, req); + handle(sp_ctx, 0); + + //! 检查是否有协议升级回调(WebSocket、SSE 等) + //! 如果有,标记连接为升级模式,停止继续解析 HTTP 数据 + if (sp_ctx->res().upgrade_cb) { + conn->is_upgrade = true; + //! 升级请求:保留 buffer 中未消费的数据,留给升级后的协议 + break; + } + + //! 非升级请求:检查是否为最后一个请求 if (IsLastRequest(req)) { - //! 标记当前请求为close请求 conn->close_index = conn->req_index; LogDbg("mark close at %d", conn->close_index); tcp_server_.shutdown(ct, SHUT_RD); } - auto sp_ctx = make_shared(wp_parent_, ct, conn->req_index++, req); - handle(sp_ctx, 0); - } else if (conn->req_parser.state() == RequestParser::State::kFail) { LogNotice("parse http from %s fail", tcp_server_.getClientAddress(ct).toString().c_str()); tcp_server_.disconnect(ct); @@ -195,6 +255,9 @@ void Server::Impl::onTcpSendCompleted(const TcpServer::ConnToken &ct) * 为了保证管道化连接中Respond与Request的顺序一致性,要做特殊处理。 * 如果所提交的index不是当前需要回复的res_index,那么就先暂存起来,等前面的发送完成后再发送; * 如果是,则可以直接回复。然后再将暂存中的未发送的其它数据也一同发送。 + * + * 对于协议升级请求(WebSocket 101、SSE 200 等),发送完响应后, + * 将从HTTP服务器分离TcpConnection,并通过Respond::upgrade_cb回调交给升级协议。 */ void Server::Impl::commitRespond(const TcpServer::ConnToken &ct, int index, Respond *res) { @@ -204,6 +267,35 @@ void Server::Impl::commitRespond(const TcpServer::ConnToken &ct, int index, Resp return; } + //! 处理协议升级请求(WebSocket 101、SSE 200 等) + //! 触发条件:res->upgrade_cb 已设置(中间件负责设置) + if (res->upgrade_cb) { + //! 注意:必须在 std::move(upgrade_cb) 之前调用 toString() + //! 否则 move 后 upgrade_cb 为空,toString() 会误判为普通响应而添加 Content-Length + //! SSE 响应添加 Content-Length:0 会导致浏览器认为响应已完成并断开重连 + const string content = res->toString(); + + //! 发送响应后,将 TcpConnection 从 HTTP 服务器分离,交给升级协议 + auto upgrade_cb = std::move(res->upgrade_cb); + delete res; + + tcp_server_.send(ct, content.data(), content.size()); + if (context_log_enable_) + LogDbg("RES: [%s]", content.c_str()); + + //! 当前回调结束后立即执行 detach(使用 runNext,更高效) + //! 因为 commitRespond() 是在 Loop 线程中执行的 + wp_loop_->runNext([this, ct, upgrade_cb] { + TcpConnection *tcp_conn = tcp_server_.detachConnection(ct); + if (tcp_conn != nullptr) + upgrade_cb(tcp_conn); + else + LogWarn("tcp_conn == nullptr"); + }, "HttpUpgrade: detach connection"); + + return; + } + Connection *conn = static_cast(tcp_server_.getContext(ct)); TBOX_ASSERT(conn != nullptr); @@ -252,18 +344,23 @@ void Server::Impl::commitRespond(const TcpServer::ConnToken &ct, int index, Resp } } -void Server::Impl::handle(ContextSptr sp_ctx, size_t cb_index) +void Server::Impl::handle(ContextSptr sp_ctx, size_t index) { RECORD_SCOPE(); - if (cb_index >= req_handler_.size()) + if (index >= mw_order_.size()) return; - auto func = req_handler_.at(cb_index); + auto token = mw_order_.at(index); + RequestHandler *handler_ptr = mw_cabinet_.at(token); - ++cb_level_; - if (func) - func(sp_ctx, std::bind(&Impl::handle, this, sp_ctx, cb_index + 1)); - --cb_level_; + if (handler_ptr && *handler_ptr) { + ++cb_level_; + (*handler_ptr)(sp_ctx, std::bind(&Impl::handle, this, sp_ctx, index + 1)); + --cb_level_; + } else { + //! handler 已被移除或为空,跳过,执行下一个 + handle(sp_ctx, index + 1); + } } Server::Impl::Connection::~Connection() diff --git a/modules/http/server/server_imp.h b/modules/http/server/server_imp.h index df958441..3a7d34ca 100644 --- a/modules/http/server/server_imp.h +++ b/modules/http/server/server_imp.h @@ -25,6 +25,8 @@ #include #include #include +#include +#include #include "server.h" #include "request_parser.h" @@ -47,6 +49,7 @@ class Server::Impl { public: bool initialize(const SockAddr &bind_addr, int listen_backlog); + bool setTlsConfig(const network::TlsConfig &config); bool start(); void stop(); void cleanup(); @@ -55,13 +58,13 @@ class Server::Impl { void setContextLogEnable(bool enable) { context_log_enable_ = enable; } public: - void use(RequestHandler &&handler); - void use(Middleware *wp_middleware); + MiddlewareToken use(RequestHandler &&handler); + MiddlewareToken use(Middleware *wp_middleware); + bool unuse(const MiddlewareToken &token); void commitRespond(const TcpServer::ConnToken &ct, int index, Respond *res); private: - void onTcpConnected(const TcpServer::ConnToken &ct); void onTcpReceived(const TcpServer::ConnToken &ct, Buffer &buff); void onTcpSendCompleted(const TcpServer::ConnToken &ct); @@ -73,17 +76,20 @@ class Server::Impl { int res_index = 0; //!< 下一个要求回复的index,用于实现按顺序回复 int close_index = numeric_limits::max(); //!< 需要关闭连接的index map res_buff; //!< 暂存器 + bool is_upgrade = false; //!< 是否为升级请求(WebSocket等) ~Connection(); }; - void handle(ContextSptr ctx, size_t cb_index); + void handle(ContextSptr ctx, size_t index); private: Server *wp_parent_; + event::Loop *wp_loop_; TcpServer tcp_server_; - vector req_handler_; + cabinet::Cabinet mw_cabinet_; //!< 中间件存储 + vector mw_order_; //!< 调用顺序 State state_ = State::kNone; bool context_log_enable_ = false; diff --git a/modules/http/server/sse/sse_connection.cpp b/modules/http/server/sse/sse_connection.cpp new file mode 100644 index 00000000..4468e615 --- /dev/null +++ b/modules/http/server/sse/sse_connection.cpp @@ -0,0 +1,191 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "sse_connection.h" + +#include +#include +#include + +namespace tbox { +namespace http { +namespace sse { + +using namespace std::placeholders; + +SseConnection::SseConnection(event::Loop *wp_loop, + network::TcpConnection *tcp_conn, + const std::string &url, + const std::string &last_event_id) + : wp_loop_(wp_loop) + , sp_tcp_conn_(tcp_conn) + , url_(url) + , last_event_id_(last_event_id) +{ + TBOX_ASSERT(wp_loop != nullptr); + TBOX_ASSERT(tcp_conn != nullptr); + + //! 设置 TcpConnection 的回调 + //! SSE 是单向推送协议(服务端→客户端),不需要处理客户端发送的数据 + //! 但必须保持 receiveCallback 注册(阈值=0),否则底层 BufferedFd 会停止监听 + //! socket 读事件,导致:(1) 无法检测浏览器关闭连接;(2) TCP 写事件也可能受影响 + //! 与 WsConnection 一样,设置空函数体回调而非 nullptr,确保读事件持续监听 + sp_tcp_conn_->setReceiveCallback([](util::Buffer &buff) { buff.hasReadAll(); }, 0); + sp_tcp_conn_->setDisconnectedCallback(std::bind(&SseConnection::onTcpDisconnected, this)); + sp_tcp_conn_->setSendCompleteCallback(std::bind(&SseConnection::onTcpSendCompleted, this)); +} + +SseConnection::~SseConnection() +{ + TBOX_ASSERT(cb_level_ == 0); + + if (sp_tcp_conn_ == nullptr) + return; + + //! 先取消 TcpConnection 的回调,防止断开时回调到已销毁的 SseConnection + //! 注意:receiveCallback 不能设为 nullptr,否则会停止 socket 读事件监听 + //! 设置空函数体回调即可 + sp_tcp_conn_->setDisconnectedCallback(nullptr); + sp_tcp_conn_->setSendCompleteCallback(nullptr); + + sp_tcp_conn_->disconnect(); + auto tcp_conn = sp_tcp_conn_; + sp_tcp_conn_ = nullptr; + wp_loop_->runNext([tcp_conn] { CHECK_DELETE_OBJ(tcp_conn); }, + "SseConnection::~SseConnection, delete tcp_conn"); +} + +//! === 发送事件 === + +bool SseConnection::send(const std::string &data) +{ + if (sp_tcp_conn_ == nullptr) + return false; + + //! 简单数据:只输出 data 字段 + //! 格式:"data: xxx\n\n" + std::string sse_text = "data: " + data + "\n\n"; + + if (context_log_enable_) + LogDbg("SEND: %s", sse_text.c_str()); + + return sp_tcp_conn_->send(sse_text.data(), sse_text.size()); +} + +bool SseConnection::send(const SseEvent &event) +{ + if (sp_tcp_conn_ == nullptr) + return false; + + //! 完整事件:调用 SseEvent::toString() 格式化后发送 + std::string sse_text = event.toString(); + + if (context_log_enable_) + LogDbg("SEND: %s", sse_text.c_str()); + + return sp_tcp_conn_->send(sse_text.data(), sse_text.size()); +} + +bool SseConnection::sendHeartbeat(const std::string &comment) +{ + if (sp_tcp_conn_ == nullptr) + return false; + + //! 心跳注释行:": \n\n" + //! 浏览器 EventSource 会忽略以 ":" 开头的行,用于保持连接活跃 + std::string sse_text = ": " + comment + "\n\n"; + + if (context_log_enable_) + LogDbg("SEND: %s", sse_text.c_str()); + + return sp_tcp_conn_->send(sse_text.data(), sse_text.size()); +} + +bool SseConnection::close() +{ + if (sp_tcp_conn_ == nullptr) + return false; + + //! SSE 没有特殊的关闭协议,直接断开 TCP 连接 + sp_tcp_conn_->disconnect(); + return true; +} + +//! === 客户端信息 === + +network::SockAddr SseConnection::peerAddr() const +{ + if (sp_tcp_conn_ != nullptr) + return sp_tcp_conn_->peerAddr(); + return network::SockAddr(); +} + +bool SseConnection::isExpired() const +{ + return sp_tcp_conn_ == nullptr || sp_tcp_conn_->isExpired(); +} + +//! === 上下文数据 === + +void SseConnection::setContext(void *context, ContextDeleter &&deleter) +{ + if (sp_tcp_conn_ != nullptr) + sp_tcp_conn_->setContext(context, std::move(deleter)); +} + +void* SseConnection::getContext() const +{ + if (sp_tcp_conn_ != nullptr) + return sp_tcp_conn_->getContext(); + return nullptr; +} + +//! === TCP 回调 === + +void SseConnection::onTcpDisconnected() +{ + LogInfo("sse disconnected"); + + //! 通知 SseServer(通过 close_cb_ 绑定了 ConnToken) + if (close_cb_) { + ++cb_level_; + close_cb_(); + --cb_level_; + } + + //! 清理 TcpConnection:先断空指针,延后删除 + //! 必须在 close_cb_() 之后清理,否则回调中 getContext() 拿到空值 + auto tcp_conn = sp_tcp_conn_; + sp_tcp_conn_ = nullptr; + wp_loop_->runNext([tcp_conn] { CHECK_DELETE_OBJ(tcp_conn); }, + "SseConnection::onTcpDisconnected, delete tcp_conn"); +} + +void SseConnection::onTcpSendCompleted() +{ + if (send_complete_cb_) { + ++cb_level_; + send_complete_cb_(); + --cb_level_; + } +} + +} +} +} diff --git a/modules/http/server/sse/sse_connection.h b/modules/http/server/sse/sse_connection.h new file mode 100644 index 00000000..e9e936ef --- /dev/null +++ b/modules/http/server/sse/sse_connection.h @@ -0,0 +1,114 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_HTTP_SSE_CONNECTION_H_20260616 +#define TBOX_HTTP_SSE_CONNECTION_H_20260616 + +#include +#include +#include + +#include "sse_event.h" + +namespace tbox { +namespace http { +namespace sse { + +//! SSE 连接 +//! 包装从 HTTP 升级后分离出来的 TcpConnection,提供 SSE 事件推送功能 +//! 生命期由 SseServer 通过 Cabinet 管理,用户通过 ConnToken 访问 +//! SSE 是单向推送协议(服务端→客户端),不需要解析客户端数据 +class SseConnection { + public: + //! 内部回调:SseServer::Impl 绑定 ConnToken,不传递 SseConnection* + using CloseCallback = std::function; + using SendCompleteCallback = std::function; + + ~SseConnection(); + + NONCOPYABLE(SseConnection); + IMMOVABLE(SseConnection); + + public: + //! 设置回调(由 SseServer::Impl 调用,绑定 ConnToken) + void setCloseCallback(const CloseCallback &cb) { close_cb_ = cb; } + void setSendCompleteCallback(const SendCompleteCallback &cb) { send_complete_cb_ = cb; } + void setContextLogEnable(bool enable) { context_log_enable_ = enable; } + + public: + //! 发送简单数据(event 类型默认 "message") + bool send(const std::string &data); + + //! 发送完整 SSE 事件 + bool send(const SseEvent &event); + + //! 发送心跳注释行(保持连接活跃) + //! 格式:": \n\n" + bool sendHeartbeat(const std::string &comment = "keep-alive"); + + //! 关闭连接(断开 TcpConnection) + bool close(); + + //! 获取客户端地址 + network::SockAddr peerAddr() const; + + //! 获取浏览器重连时携带的 Last-Event-ID + std::string getLastEventId() const { return last_event_id_; } + + //! 获取客户端连接的 URL 路径 + std::string getUrl() const { return url_; } + + //! 连接是否已失效 + bool isExpired() const; + + //! 设置/获取上下文数据(委托给底层 TcpConnection) + using ContextDeleter = network::TcpConnection::ContextDeleter; + void setContext(void *context, ContextDeleter &&deleter = nullptr); + void* getContext() const; + + private: + //! 仅由 SseServer 创建(生命期由 Cabinet 管理) + SseConnection(event::Loop *wp_loop, + network::TcpConnection *tcp_conn, + const std::string &url, + const std::string &last_event_id); + + void onTcpDisconnected(); + void onTcpSendCompleted(); + + private: + event::Loop *wp_loop_; + network::TcpConnection *sp_tcp_conn_; + std::string url_; + std::string last_event_id_; //! 浏览器重连时的 Last-Event-ID + + CloseCallback close_cb_; + SendCompleteCallback send_complete_cb_; + + int cb_level_ = 0; + bool context_log_enable_ = false; + + friend class SseServer; +}; + +} +} +} + +#endif //TBOX_HTTP_SSE_CONNECTION_H_20260616 diff --git a/modules/http/server/sse/sse_event.cpp b/modules/http/server/sse/sse_event.cpp new file mode 100644 index 00000000..0bc17b27 --- /dev/null +++ b/modules/http/server/sse/sse_event.cpp @@ -0,0 +1,78 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "sse_event.h" + +#include + +namespace tbox { +namespace http { +namespace sse { + +std::string SseEvent::toString() const +{ + //! SSE 协议格式(W3C/WHATWG EventSource 规范): + //! 每个字段以 "field: value\n" 格式输出 + //! 事件以空行 "\n" 结束(标志事件完成) + //! + //! 字段输出顺序:retry → id → event → data → 空行 + //! 顺序不影响浏览器解析,但统一顺序便于调试 + + std::string result; + + //! retry 字段(可选,仅在 retry > 0 时输出) + //! 告知浏览器断线后多久自动重连 + if (retry > 0) + result += "retry: " + std::to_string(retry) + "\n"; + + //! id 字段(可选) + //! 浏览器重连时通过 Last-Event-ID 头部携带此值 + if (!id.empty()) + result += "id: " + id + "\n"; + + //! event 字段(可选) + //! 默认为 "message",浏览器通过 .onmessage 监听 + //! 自定义 event 类型通过 .addEventListener(event, ...) 监听 + //! 不输出默认值 "message",减少传输量 + if (!event.empty() && event != "message") + result += "event: " + event + "\n"; + + //! data 字段(必须) + //! 多行 data 自动拆分为多个 `data:` 行 + //! 例如 data="line1\nline2" 输出为 "data: line1\ndata: line2\n" + if (!data.empty()) { + std::istringstream iss(data); + std::string line; + while (std::getline(iss, line)) + result += "data: " + line + "\n"; + } else { + //! data 为空时仍需输出空 data 行(保持协议完整性) + result += "data:\n"; + } + + //! 事件结束标志(空行) + //! 浏览器 EventSource 在收到空行时认为事件完成并触发回调 + result += "\n"; + + return result; +} + +} +} +} diff --git a/modules/http/server/sse/sse_event.h b/modules/http/server/sse/sse_event.h new file mode 100644 index 00000000..20e5fa42 --- /dev/null +++ b/modules/http/server/sse/sse_event.h @@ -0,0 +1,73 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_HTTP_SSE_EVENT_H_20260616 +#define TBOX_HTTP_SSE_EVENT_H_20260616 + +#include +#include + +namespace tbox { +namespace http { +namespace sse { + +//! SSE 事件(Server-Sent Events, W3C/WHATWG 规范) +//! 对应 SSE 协议中的字段:id、event、data、retry +//! toString() 将事件格式化为标准 SSE 文本格式 +struct SseEvent { + //! 事件 ID(可选) + //! 对应 `id:` 字段,浏览器重连时通过 Last-Event-ID 头部携带此值 + //! 用于实现断线续传:服务端可根据 Last-Event-ID 从断点继续推送 + std::string id; + + //! 事件类型(可选) + //! 对应 `event:` 字段,默认为 "message" + //! 浏览器 EventSource 对象通过 .onmessage 或 .addEventListener(event, ...) 监听 + std::string event; + + //! 数据(必须) + //! 对应 `data:` 字段,支持多行文本 + //! toString() 会自动将多行 data 拆分为多个 `data:` 行 + std::string data; + + //! 重连间隔毫秒数(可选) + //! 对应 `retry:` 字段,告知浏览器断线后多久自动重连 + //! 仅在 retry > 0 时输出 + int retry = 0; + + //! 将事件格式化为 SSE 文本协议格式 + //! 输出规则(W3C/WHATWG EventSource 规范): + //! - retry > 0 时输出 "retry: \n" + //! - id 非空时输出 "id: \n" + //! - event 非空且不等于 "message" 时输出 "event: \n" + //! - data 按行拆分,每行输出 "data: \n" + //! - 最后以空行 "\n" 结束(标志事件完成) + //! + //! 示例输出: + //! "id: 42\nevent: update\ndata: hello\n\n" + //! 多行 data: + //! "data: line1\ndata: line2\n\n" + std::string toString() const; +}; + +} +} +} + +#endif //TBOX_HTTP_SSE_EVENT_H_20260616 diff --git a/modules/http/server/sse/sse_event_test.cpp b/modules/http/server/sse/sse_event_test.cpp new file mode 100644 index 00000000..6a5460c7 --- /dev/null +++ b/modules/http/server/sse/sse_event_test.cpp @@ -0,0 +1,140 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include +#include "sse_event.h" + +namespace tbox { +namespace http { +namespace sse { + +//! === SseEvent 构造测试 === + +TEST(SseEvent, DefaultValues) +{ + SseEvent evt; + EXPECT_EQ(evt.id, ""); + EXPECT_EQ(evt.event, ""); + EXPECT_EQ(evt.data, ""); + EXPECT_EQ(evt.retry, 0); +} + +//! === SseEvent::toString() 格式化测试 === + +TEST(SseEvent, ToStringSimpleData) +{ + //! 简单数据:只输出 data 字段 + SseEvent evt; + evt.data = "hello world"; + + //! 预期输出:"data: hello world\n\n" + EXPECT_EQ(evt.toString(), "data: hello world\n\n"); +} + +TEST(SseEvent, ToStringWithId) +{ + //! 带 id 的数据 + SseEvent evt; + evt.id = "42"; + evt.data = "hello"; + + //! 预期输出:"id: 42\ndata: hello\n\n" + EXPECT_EQ(evt.toString(), "id: 42\ndata: hello\n\n"); +} + +TEST(SseEvent, ToStringWithEvent) +{ + //! 带 event 类型(非默认 "message") + SseEvent evt; + evt.event = "update"; + evt.data = "status ok"; + + //! 预期输出:"event: update\ndata: status ok\n\n" + EXPECT_EQ(evt.toString(), "event: update\ndata: status ok\n\n"); +} + +TEST(SseEvent, ToStringWithDefaultEvent) +{ + //! event 为 "message"(默认值)时不输出 event 字段 + SseEvent evt; + evt.event = "message"; + evt.data = "hello"; + + //! 预期输出:"data: hello\n\n"(不输出 "event: message") + EXPECT_EQ(evt.toString(), "data: hello\n\n"); +} + +TEST(SseEvent, ToStringWithRetry) +{ + //! 带 retry 字段 + SseEvent evt; + evt.retry = 3000; + evt.data = "hello"; + + //! 预期输出:"retry: 3000\ndata: hello\n\n" + EXPECT_EQ(evt.toString(), "retry: 3000\ndata: hello\n\n"); +} + +TEST(SseEvent, ToStringRetryZeroNotOutput) +{ + //! retry = 0时不输出 retry 字段 + SseEvent evt; + evt.retry = 0; + evt.data = "hello"; + + //! 预期输出:"data: hello\n\n"(不输出 "retry: 0") + EXPECT_EQ(evt.toString(), "data: hello\n\n"); +} + +TEST(SseEvent, ToStringMultilineData) +{ + //! 多行 data 自动拆分为多个 data: 行 + SseEvent evt; + evt.data = "line1\nline2\nline3"; + + //! 预期输出:"data: line1\ndata: line2\ndata: line3\n\n" + EXPECT_EQ(evt.toString(), "data: line1\ndata: line2\ndata: line3\n\n"); +} + +TEST(SseEvent, ToStringCompleteEvent) +{ + //! 完整事件:所有字段 + SseEvent evt; + evt.id = "123"; + evt.event = "update"; + evt.data = "status ok"; + evt.retry = 5000; + + //! 预期输出:"retry: 5000\nid: 123\nevent: update\ndata: status ok\n\n" + EXPECT_EQ(evt.toString(), "retry: 5000\nid: 123\nevent: update\ndata: status ok\n\n"); +} + +TEST(SseEvent, ToStringEmptyData) +{ + //! data 为空时输出空 data 行 + SseEvent evt; + evt.id = "1"; + + //! 预期输出:"id: 1\ndata:\n\n" + EXPECT_EQ(evt.toString(), "id: 1\ndata:\n\n"); +} + +} +} +} diff --git a/modules/http/server/sse/sse_server.h b/modules/http/server/sse/sse_server.h new file mode 100644 index 00000000..2f0ac1f6 --- /dev/null +++ b/modules/http/server/sse/sse_server.h @@ -0,0 +1,135 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_HTTP_SSE_SERVER_H_20260616 +#define TBOX_HTTP_SSE_SERVER_H_20260616 + +#include +#include + +#include +#include +#include +#include + +#include "sse_event.h" + +namespace tbox { +namespace http { + +namespace server { +class Server; +} + +namespace sse { + +//! SSE 服务器(Server-Sent Events, W3C/WHATWG 规范) +//! 基于 HTTP 服务器运行,本身即为 HTTP 中间件 +//! 检测 SSE 请求(Accept: text/event-stream),设置 200 响应头 +//! 通过 upgrade_cb 机制接管 TcpConnection,提供 SSE 事件推送功能 +//! SSE 是单向推送协议(服务端→客户端),无 MessageCallback +//! 通过 Cabinet 管理 SseConnection 生命期,用户通过 ConnToken 操作连接 +class SseServer { + public: + using ConnToken = cabinet::Token; + + explicit SseServer(event::Loop *wp_loop); + ~SseServer(); + + NONCOPYABLE(SseServer); + IMMOVABLE(SseServer); + + public: + //! 初始化:关联到 HTTP 服务器 + //! URL 路径匹配规则: + //! - url_path 以 '/' 结尾:前缀匹配,如 "/sse/" 匹配 "/sse/aa"、" /sse/bb/cc" + //! - url_path 不以 '/' 结尾:全量匹配,如 "/sse" 仅匹配 "/sse" + //! - url_path 为空字符串:匹配所有 SSE 请求 + bool initialize(http::server::Server *http_server, const std::string &url_path = ""); + bool start(); + void stop(); + void cleanup(); + + enum class State { kNone, kInited, kRunning }; + State state() const; + + public: + //! 设置回调(SSE 是单向推送,无 MessageCallback) + using ConnectedCallback = std::function; + using DisconnectedCallback = std::function; + + void setConnectedCallback(const ConnectedCallback &cb); + void setDisconnectedCallback(const DisconnectedCallback &cb); + + public: + //! 向指定客户端发送数据(简单文本,event 类型默认 "message") + //! 格式:"data: \n\n" + bool send(const ConnToken &client, const std::string &data); + + //! 向指定客户端发送完整 SSE 事件 + bool send(const ConnToken &client, const SseEvent &event); + + //! 向所有客户端广播数据 + bool sendToAll(const std::string &data); + + //! 向所有客户端广播事件 + bool sendToAll(const SseEvent &event); + + //! 关闭指定客户端连接 + bool close(const ConnToken &client); + + //! 发送心跳注释行(保持连接活跃) + //! 格式:": \n\n",浏览器 EventSource 忽略以 ":" 开头的行 + bool sendHeartbeat(const ConnToken &client, const std::string &comment = "keep-alive"); + + //! 设置自动心跳间隔(默认 0 = 禁用) + //! 启用后,定时器每 interval 毫秒向所有连接发送 ": keep-alive\n\n" + void setHeartbeatInterval(std::chrono::milliseconds interval); + + //! 检查客户端连接是否有效 + bool isClientValid(const ConnToken &client) const; + + //! 获取客户端地址(含 IP 与端口) + network::SockAddr peerAddr(const ConnToken &client) const; + + //! 获取客户端请求的 Last-Event-ID(浏览器重连时携带) + //! 用于实现断线续传:服务端可根据此值从断点继续推送 + std::string getLastEventId(const ConnToken &client) const; + + //! 获取客户端连接的 URL 路径 + std::string getUrl(const ConnToken &client) const; + + //! 设置/获取客户端连接的上下文数据 + using ContextDeleter = std::function; + void setContext(const ConnToken &client, void *context, ContextDeleter &&deleter = nullptr); + void* getContext(const ConnToken &client) const; + + void setContextLogEnable(bool enable); + + class Impl; + + private: + Impl *impl_; +}; + +} +} +} + +#endif //TBOX_HTTP_SSE_SERVER_H_20260616 diff --git a/modules/http/server/sse/sse_server_impl.cpp b/modules/http/server/sse/sse_server_impl.cpp new file mode 100644 index 00000000..33c8cf95 --- /dev/null +++ b/modules/http/server/sse/sse_server_impl.cpp @@ -0,0 +1,505 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "sse_server.h" +#include "sse_server_impl.h" + +#include +#include +#include +#include + +#include +#include + +#undef MODULE_ID +#define MODULE_ID "tbox.http.sse" + +namespace tbox { +namespace http { +namespace sse { + +using namespace std::placeholders; + +//! === 生命周期 === + +SseServer::Impl::Impl(SseServer *wp_parent, event::Loop *wp_loop) + : wp_parent_(wp_parent) + , wp_loop_(wp_loop) + , sp_heartbeat_timer_(wp_loop->newTimerEvent()) +{ } + +SseServer::Impl::~Impl() +{ + TBOX_ASSERT(cb_level_ == 0); + cleanup(); + CHECK_DELETE_RESET_OBJ(sp_heartbeat_timer_); +} + +bool SseServer::Impl::initialize(http::server::Server *http_server, const std::string &url_path) +{ + if (state_ != SseServer::State::kNone) + return false; + + //! 记录 URL 路径 + url_path_ = url_path; + + //! 记录 HTTP 服务器指针(不立即注册中间件,等 start() 时注册) + wp_http_server_ = http_server; + + state_ = SseServer::State::kInited; + return true; +} + +bool SseServer::Impl::start() +{ + if (state_ != SseServer::State::kInited) + return false; + + //! 注册自身到 HTTP 服务器(SseServer::Impl 即为 Middleware) + mw_token_ = wp_http_server_->use(this); + + //! 如果心跳间隔已设置,启用心跳定时器 + if (heartbeat_interval_.count() > 0) { + sp_heartbeat_timer_->initialize(heartbeat_interval_, event::Event::Mode::kPersist); + sp_heartbeat_timer_->setCallback(std::bind(&SseServer::Impl::onHeartbeatTimer, this)); + sp_heartbeat_timer_->enable(); + } + + state_ = SseServer::State::kRunning; + return true; +} + +void SseServer::Impl::stop() +{ + if (state_ != SseServer::State::kRunning) + return; + + //! 从 HTTP 服务器反注册中间件 + wp_http_server_->unuse(mw_token_); + mw_token_.reset(); + + //! 停止心跳定时器 + sp_heartbeat_timer_->disable(); + + //! 清除 SseConnection 内部回调,防止断开时回调到 Impl + sse_conns_.foreach([](SseConnection *conn) { + conn->setCloseCallback(nullptr); + conn->setSendCompleteCallback(nullptr); + }); + + //! 删除所有 SseConnection(析构时会断开并延后删除 TcpConnection) + sse_conns_.foreach([](SseConnection *conn) { delete conn; }); + sse_conns_.clear(); + + state_ = SseServer::State::kInited; +} + +void SseServer::Impl::cleanup() +{ + if (state_ == SseServer::State::kNone) + return; + + if (state_ == SseServer::State::kRunning) + stop(); + + wp_http_server_ = nullptr; + + connected_cb_ = nullptr; + disconnected_cb_ = nullptr; + heartbeat_interval_ = std::chrono::milliseconds(0); + + state_ = SseServer::State::kNone; +} + +//! === Middleware 接口实现 === +void SseServer::Impl::handle(http::server::ContextSptr sp_ctx, const http::server::NextFunc &next) +{ + auto &req = sp_ctx->req(); + + if (!IsSseRequest(req)) { + //! 非 SSE 请求,传递给下一个中间件 + next(); + } + + //! URL 路径匹配规则: + //! - url_path_ 以 '/' 结尾:前缀匹配 + //! - url_path_ 不以 '/' 结尾:全量匹配 + //! - url_path_ 为空字符串:匹配所有 SSE 请求 + if (!url_path_.empty()) { + bool matched = false; + if (url_path_.back() == '/') { + //! 前缀匹配 + matched = util::string::IsStartWith(req.url.path, url_path_); + } else { + //! 全量匹配 + matched = (req.url.path == url_path_); + } + if (!matched) { + //! 不是本服务关心的 URL,传递给下一个中间件 + next(); + return; + } + } + + LogDbg("sse request: %s", req.url.path.c_str()); + + auto &res = sp_ctx->res(); + + //! 设置 200 OK 响应(SSE 不是协议升级,使用 200) + res.status_code = http::StatusCode::k200_OK; + res.http_ver = http::HttpVer::k1_1; + + //! SSE 必需的响应头 + res.headers["Content-Type"] = "text/event-stream"; + res.headers["Cache-Control"] = "no-cache"; + res.headers["Connection"] = "keep-alive"; + + //! 从请求中提取 Last-Event-ID(浏览器重连时携带) + std::string last_event_id; + auto id_iter = req.headers.find("Last-Event-ID"); + if (id_iter != req.headers.end()) + last_event_id = id_iter->second; + + //! 注册升级回调:HTTP 服务器发送 200 响应后,将 TcpConnection 交给 SseServer + //! 与 WebSocket 使用同一套 upgrade_cb 机制 + res.upgrade_cb = std::bind(&SseServer::Impl::onSseUpgrade, this, _1, req.url.path, last_event_id); + + //! SSE 请求已处理,不再调用 next() +} + +//! === 升级与连接管理 === +void SseServer::Impl::onSseUpgrade(network::TcpConnection *tcp_conn, + const std::string &url_path, + const std::string &last_event_id) +{ + RECORD_SCOPE(); + LogDbg("sse upgrade: new connection from %s", tcp_conn->peerAddr().toString().c_str()); + + //! 创建 SseConnection,并存入 Cabinet + //! 传入 URL 路径和 Last-Event-ID,供用户后续查询 + SseConnection *sse_conn = new SseConnection(wp_loop_, tcp_conn, url_path, last_event_id); + ConnToken sse_token = sse_conns_.alloc(sse_conn); + + //! 设置 SseConnection 的回调(bind 捕获 ConnToken,不传递 SseConnection*) + sse_conn->setCloseCallback(std::bind(&SseServer::Impl::onSseDisconnected, this, sse_token)); + sse_conn->setContextLogEnable(context_log_enable_); + + //! 通知用户(传递 ConnToken) + if (connected_cb_) { + ++cb_level_; + connected_cb_(sse_token); + --cb_level_; + } +} + +void SseServer::Impl::onSseDisconnected(const ConnToken &client) +{ + RECORD_SCOPE(); + LogDbg("sse disconnected"); + + //! 先通知用户(此时 ConnToken 在 Cabinet 中仍有效) + //! 用户可通过 ConnToken 调用 SseServer 方法获取连接信息 + if (disconnected_cb_) { + ++cb_level_; + disconnected_cb_(client); + --cb_level_; + } + + //! 从 Cabinet 中移除并获取指针 + SseConnection *sse_conn = sse_conns_.free(client); + + //! 延后删除 SseConnection(确保回调中还能访问对象) + wp_loop_->runNext([sse_conn] { CHECK_DELETE_OBJ(sse_conn); }, + "SseServer::onSseDisconnected, delete sse_conn"); +} + +//! === 心跳定时器 === +void SseServer::Impl::onHeartbeatTimer() +{ + //! 向所有连接发送心跳注释行 + sse_conns_.foreach([](SseConnection *conn) { + conn->sendHeartbeat("keep-alive"); + }); +} + +void SseServer::Impl::setHeartbeatInterval(std::chrono::milliseconds interval) +{ + heartbeat_interval_ = interval; + + //! 如果已在运行中,动态调整心跳定时器 + if (state_ == SseServer::State::kRunning) { + sp_heartbeat_timer_->disable(); + + if (interval.count() > 0) { + sp_heartbeat_timer_->initialize(interval, event::Event::Mode::kPersist); + sp_heartbeat_timer_->setCallback(std::bind(&SseServer::Impl::onHeartbeatTimer, this)); + sp_heartbeat_timer_->enable(); + } + } +} + +//! === 通过 ConnToken 操作连接 === +bool SseServer::Impl::send(const ConnToken &client, const std::string &data) +{ + auto sse_conn = sse_conns_.at(client); + if (sse_conn != nullptr) + return sse_conn->send(data); + return false; +} + +bool SseServer::Impl::send(const ConnToken &client, const SseEvent &event) +{ + auto sse_conn = sse_conns_.at(client); + if (sse_conn != nullptr) + return sse_conn->send(event); + return false; +} + +bool SseServer::Impl::sendToAll(const std::string &data) +{ + bool all_ok = true; + sse_conns_.foreach([&](SseConnection *conn) { + if (!conn->send(data)) + all_ok = false; + }); + return all_ok; +} + +bool SseServer::Impl::sendToAll(const SseEvent &event) +{ + bool all_ok = true; + sse_conns_.foreach([&](SseConnection *conn) { + if (!conn->send(event)) + all_ok = false; + }); + return all_ok; +} + +bool SseServer::Impl::close(const ConnToken &client) +{ + auto sse_conn = sse_conns_.at(client); + if (sse_conn != nullptr) + return sse_conn->close(); + return false; +} + +bool SseServer::Impl::sendHeartbeat(const ConnToken &client, const std::string &comment) +{ + auto sse_conn = sse_conns_.at(client); + if (sse_conn != nullptr) + return sse_conn->sendHeartbeat(comment); + return false; +} + +bool SseServer::Impl::isClientValid(const ConnToken &client) const +{ + return sse_conns_.at(client) != nullptr; +} + +network::SockAddr SseServer::Impl::peerAddr(const ConnToken &client) const +{ + auto sse_conn = sse_conns_.at(client); + if (sse_conn != nullptr) + return sse_conn->peerAddr(); + return network::SockAddr(); +} + +std::string SseServer::Impl::getLastEventId(const ConnToken &client) const +{ + auto sse_conn = sse_conns_.at(client); + if (sse_conn != nullptr) + return sse_conn->getLastEventId(); + return ""; +} + +std::string SseServer::Impl::getUrl(const ConnToken &client) const +{ + auto sse_conn = sse_conns_.at(client); + if (sse_conn != nullptr) + return sse_conn->getUrl(); + return ""; +} + +void SseServer::Impl::setContext(const ConnToken &client, void *context, ContextDeleter &&deleter) +{ + auto sse_conn = sse_conns_.at(client); + if (sse_conn != nullptr) + sse_conn->setContext(context, std::move(deleter)); +} + +void* SseServer::Impl::getContext(const ConnToken &client) const +{ + auto sse_conn = sse_conns_.at(client); + if (sse_conn != nullptr) + return sse_conn->getContext(); + return nullptr; +} + +void SseServer::Impl::setContextLogEnable(bool enable) +{ + context_log_enable_ = enable; + sse_conns_.foreach([&](SseConnection *conn) { + conn->setContextLogEnable(enable); + }); +} + +bool SseServer::Impl::IsSseRequest(const http::Request &req) +{ + //! SSE 请求检测条件: + //! 1) 必须是 GET 方法 + //! 2) Accept 头必须包含 "text/event-stream" + //! 3) 不检查 Upgrade 头(SSE 不是协议升级) + + if (req.method != http::Method::kGet) + return false; + + auto accept_iter = req.headers.find("Accept"); + if (accept_iter == req.headers.end()) + return false; + + //! 检查 Accept 头是否包含 text/event-stream + //! 注意:Accept 头可能包含多个值,如 "text/event-stream, text/html" + if (accept_iter->second.find("text/event-stream") == std::string::npos) + return false; + + return true; +} + +//! === SseServer 外部接口 === +SseServer::SseServer(event::Loop *wp_loop) + : impl_(new Impl(this, wp_loop)) +{ + TBOX_ASSERT(wp_loop != nullptr); +} + +SseServer::~SseServer() +{ + CHECK_DELETE_RESET_OBJ(impl_); +} + +bool SseServer::initialize(http::server::Server *http_server, const std::string &url_path) +{ + TBOX_ASSERT(http_server != nullptr); + return impl_->initialize(http_server, url_path); +} + +bool SseServer::start() +{ + return impl_->start(); +} + +void SseServer::stop() +{ + impl_->stop(); +} + +void SseServer::cleanup() +{ + impl_->cleanup(); +} + +SseServer::State SseServer::state() const +{ + return impl_->state(); +} + +void SseServer::setConnectedCallback(const ConnectedCallback &cb) +{ + impl_->setConnectedCallback(cb); +} + +void SseServer::setDisconnectedCallback(const DisconnectedCallback &cb) +{ + impl_->setDisconnectedCallback(cb); +} + +bool SseServer::send(const ConnToken &client, const std::string &data) +{ + return impl_->send(client, data); +} + +bool SseServer::send(const ConnToken &client, const SseEvent &event) +{ + return impl_->send(client, event); +} + +bool SseServer::sendToAll(const std::string &data) +{ + return impl_->sendToAll(data); +} + +bool SseServer::sendToAll(const SseEvent &event) +{ + return impl_->sendToAll(event); +} + +bool SseServer::close(const ConnToken &client) +{ + return impl_->close(client); +} + +bool SseServer::sendHeartbeat(const ConnToken &client, const std::string &comment) +{ + return impl_->sendHeartbeat(client, comment); +} + +void SseServer::setHeartbeatInterval(std::chrono::milliseconds interval) +{ + impl_->setHeartbeatInterval(interval); +} + +bool SseServer::isClientValid(const ConnToken &client) const +{ + return impl_->isClientValid(client); +} + +network::SockAddr SseServer::peerAddr(const ConnToken &client) const +{ + return impl_->peerAddr(client); +} + +std::string SseServer::getLastEventId(const ConnToken &client) const +{ + return impl_->getLastEventId(client); +} + +std::string SseServer::getUrl(const ConnToken &client) const +{ + return impl_->getUrl(client); +} + +void SseServer::setContext(const ConnToken &client, void *context, ContextDeleter &&deleter) +{ + impl_->setContext(client, context, std::move(deleter)); +} + +void* SseServer::getContext(const ConnToken &client) const +{ + return impl_->getContext(client); +} + +void SseServer::setContextLogEnable(bool enable) +{ + impl_->setContextLogEnable(enable); +} + +} +} +} diff --git a/modules/http/server/sse/sse_server_impl.h b/modules/http/server/sse/sse_server_impl.h new file mode 100644 index 00000000..de0121de --- /dev/null +++ b/modules/http/server/sse/sse_server_impl.h @@ -0,0 +1,136 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_HTTP_SSE_SERVER_IMPL_H_20260616 +#define TBOX_HTTP_SSE_SERVER_IMPL_H_20260616 + +#include +#include +#include +#include +#include +#include + +#include "../server.h" +#include "../middleware.h" +#include "../context.h" + +#include "sse_server.h" +#include "sse_connection.h" + +namespace tbox { +namespace http { +namespace sse { + +//! SseServer::Impl 同时充当 HTTP 中间件 +//! 检测 SSE 请求(Accept: text/event-stream),设置 200 响应头,注册 upgrade_cb +//! 通过 Cabinet 管理 SseConnection 生命期,所有操作基于 ConnToken +//! SSE 是单向推送协议,无 MessageCallback +class SseServer::Impl : public http::server::Middleware { + public: + Impl(SseServer *wp_parent, event::Loop *wp_loop); + virtual ~Impl(); + + public: + bool initialize(http::server::Server *http_server, const std::string &url_path = ""); + bool start(); + void stop(); + void cleanup(); + + SseServer::State state() const { return state_; } + + public: + void setConnectedCallback(const SseServer::ConnectedCallback &cb) { connected_cb_ = cb; } + void setDisconnectedCallback(const SseServer::DisconnectedCallback &cb) { disconnected_cb_ = cb; } + + public: + //! 通过 ConnToken 操作连接 + bool send(const ConnToken &client, const std::string &data); + bool send(const ConnToken &client, const SseEvent &event); + bool sendToAll(const std::string &data); + bool sendToAll(const SseEvent &event); + bool close(const ConnToken &client); + bool sendHeartbeat(const ConnToken &client, const std::string &comment); + bool isClientValid(const ConnToken &client) const; + network::SockAddr peerAddr(const ConnToken &client) const; + std::string getLastEventId(const ConnToken &client) const; + std::string getUrl(const ConnToken &client) const; + void setHeartbeatInterval(std::chrono::milliseconds interval); + + //! 上下文数据操作(委托到 SseConnection → TcpConnection) + using ContextDeleter = network::TcpConnection::ContextDeleter; + void setContext(const ConnToken &client, void *context, ContextDeleter &&deleter = nullptr); + void* getContext(const ConnToken &client) const; + + void setContextLogEnable(bool enable); + + //! 静态辅助方法 + static bool IsSseRequest(const http::Request &req); + + public: + //! Middleware 接口:处理 HTTP 请求,检测 SSE 请求 + virtual void handle(http::server::ContextSptr sp_ctx, const http::server::NextFunc &next) override; + + private: + //! 当 HTTP 服务器发送 200 响应后回调此函数 + //! 将 TcpConnection 从 HTTP 服务器分离,交给 SseServer 管理 + void onSseUpgrade(network::TcpConnection *tcp_conn, + const std::string &url_path, + const std::string &last_event_id); + + //! 当 SseConnection 断开时回调(参数为 ConnToken) + void onSseDisconnected(const ConnToken &client); + + //! 心跳定时器回调:向所有连接发送注释行 + void onHeartbeatTimer(); + + private: + SseServer *wp_parent_; + event::Loop *wp_loop_; + event::TimerEvent *sp_heartbeat_timer_ = nullptr; //! 心跳定时器(可选,默认禁用) + http::server::Server *wp_http_server_ = nullptr; + + //! URL 路径匹配规则: + //! - url_path_ 以 '/' 结尾:前缀匹配,如 "/sse/" 匹配 "/sse/aa" + //! - url_path_ 不以 '/' 结尾:全量匹配,如 "/sse" 仅匹配 "/sse" + //! - url_path_ 为空字符串:匹配所有 SSE 请求 + std::string url_path_; + + //! 中间件 token(由 HTTP Server 的 use() 返回,用于 unuse() 反注册) + http::server::MiddlewareToken mw_token_; + + //! SseConnection 容器(生命期管理) + cabinet::Cabinet sse_conns_; + + std::chrono::milliseconds heartbeat_interval_{0}; + + SseServer::State state_ = SseServer::State::kNone; + + SseServer::ConnectedCallback connected_cb_; + SseServer::DisconnectedCallback disconnected_cb_; + + int cb_level_ = 0; + bool context_log_enable_ = false; +}; + +} +} +} + +#endif //TBOX_HTTP_SSE_SERVER_IMPL_H_20260616 diff --git a/modules/http/server/sse/sse_server_impl_test.cpp b/modules/http/server/sse/sse_server_impl_test.cpp new file mode 100644 index 00000000..a534cbb4 --- /dev/null +++ b/modules/http/server/sse/sse_server_impl_test.cpp @@ -0,0 +1,90 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include +#include +#include "sse_server_impl.h" + +namespace tbox { +namespace http { +namespace sse { + +TEST(SseServerImpl, DetectSseRequest) +{ + //! 合法的 SSE 请求:GET + Accept: text/event-stream + http::Request req; + req.method = http::Method::kGet; + req.http_ver = http::HttpVer::k1_1; + req.headers["Accept"] = "text/event-stream"; + + EXPECT_TRUE(SseServer::Impl::IsSseRequest(req)); +} + +TEST(SseServerImpl, DetectSseRequestWithMultipleAccept) +{ + //! Accept 头包含多个值时,仍能检测到 text/event-stream + http::Request req; + req.method = http::Method::kGet; + req.headers["Accept"] = "text/event-stream, text/html;q=0.9"; + + EXPECT_TRUE(SseServer::Impl::IsSseRequest(req)); +} + +TEST(SseServerImpl, DetectNonSseRequestNoAccept) +{ + //! 缺少 Accept 头:不是 SSE 请求 + http::Request req; + req.method = http::Method::kGet; + + EXPECT_FALSE(SseServer::Impl::IsSseRequest(req)); +} + +TEST(SseServerImpl, DetectNonSseRequestHtmlAccept) +{ + //! Accept 头不包含 text/event-stream:不是 SSE 请求 + http::Request req; + req.method = http::Method::kGet; + req.headers["Accept"] = "text/html"; + + EXPECT_FALSE(SseServer::Impl::IsSseRequest(req)); +} + +TEST(SseServerImpl, DetectNonSsePostRequest) +{ + //! POST 方法:不是 SSE 请求(SSE 必须是 GET) + http::Request req; + req.method = http::Method::kPost; + req.headers["Accept"] = "text/event-stream"; + + EXPECT_FALSE(SseServer::Impl::IsSseRequest(req)); +} + +TEST(SseServerImpl, DetectNonSsePutRequest) +{ + //! PUT 方法:不是 SSE 请求 + http::Request req; + req.method = http::Method::kPut; + req.headers["Accept"] = "text/event-stream"; + + EXPECT_FALSE(SseServer::Impl::IsSseRequest(req)); +} + +} +} +} diff --git a/modules/http/server/types.h b/modules/http/server/types.h index ff0db76a..d368451b 100644 --- a/modules/http/server/types.h +++ b/modules/http/server/types.h @@ -22,6 +22,7 @@ #include #include +#include namespace tbox { namespace http { @@ -31,6 +32,7 @@ class Context; using ContextSptr = std::shared_ptr; using NextFunc = std::function; using RequestHandler = std::function; +using MiddlewareToken = cabinet::Token; } } diff --git a/modules/network/CMakeLists.txt b/modules/network/CMakeLists.txt index a2fe0b85..bc765e65 100644 --- a/modules/network/CMakeLists.txt +++ b/modules/network/CMakeLists.txt @@ -39,10 +39,17 @@ set(TBOX_NETWORK_HEADERS sockaddr.h udp_socket.h tcp_connection.h + tcp_raw_connection.h tcp_acceptor.h + tcp_raw_acceptor.h tcp_connector.h + tcp_raw_connector.h tcp_client.h tcp_server.h + tcp_factory.h + tcp_raw_factory.h + tls_config.h + tls_factory_entry.h net_if.h domain_name.h dns_request.h) @@ -56,10 +63,16 @@ set(TBOX_NETWORK_SOURCES sockaddr.cpp udp_socket.cpp tcp_connection.cpp + tcp_raw_connection.cpp tcp_acceptor.cpp + tcp_raw_acceptor.cpp tcp_connector.cpp + tcp_raw_connector.cpp tcp_client.cpp tcp_server.cpp + tcp_raw_factory.cpp + tls_config.cpp + tls_factory_entry.cpp net_if.cpp dns_request.cpp) diff --git a/modules/network/Makefile b/modules/network/Makefile index e5823adf..4d828401 100644 --- a/modules/network/Makefile +++ b/modules/network/Makefile @@ -34,10 +34,17 @@ HEAD_FILES = \ sockaddr.h \ udp_socket.h \ tcp_connection.h \ + tcp_raw_connection.h \ tcp_acceptor.h \ + tcp_raw_acceptor.h \ tcp_connector.h \ + tcp_raw_connector.h \ tcp_client.h \ tcp_server.h \ + tcp_factory.h \ + tcp_raw_factory.h \ + tls_config.h \ + tls_factory_entry.h \ net_if.h \ domain_name.h \ dns_request.h \ @@ -51,10 +58,16 @@ CPP_SRC_FILES = \ sockaddr.cpp \ udp_socket.cpp \ tcp_connection.cpp \ + tcp_raw_connection.cpp \ tcp_acceptor.cpp \ + tcp_raw_acceptor.cpp \ tcp_connector.cpp \ + tcp_raw_connector.cpp \ tcp_client.cpp \ tcp_server.cpp \ + tcp_raw_factory.cpp \ + tls_config.cpp \ + tls_factory_entry.cpp \ net_if.cpp \ dns_request.cpp \ diff --git a/modules/network/README b/modules/network/README index e8eaa06e..c5503522 100644 --- a/modules/network/README +++ b/modules/network/README @@ -1,2 +1,2 @@ 通信模块 -包含:串口、TCP、UDP 等 +包含:串口、TCP、UDP、TLS 等 diff --git a/modules/network/buffered_fd.cpp b/modules/network/buffered_fd.cpp index e9d1bedd..aa15aad8 100644 --- a/modules/network/buffered_fd.cpp +++ b/modules/network/buffered_fd.cpp @@ -145,7 +145,7 @@ bool BufferedFd::send(const void *data_ptr, size_t data_size) send_buff_.append(data_ptr, data_size); } else { //! 否则尝试发送 - ssize_t wsize = fd_.write(data_ptr, data_size); + ssize_t wsize = doWrite(data_ptr, data_size); if (wsize >= 0) { //! 如果发送正常 //! 如果没有发送完,还有剩余的数据 if (static_cast(wsize) < data_size) { @@ -179,6 +179,16 @@ void BufferedFd::shrinkSendBuffer() send_buff_.shrink(); } +ssize_t BufferedFd::doReadv(const struct iovec *iov, int iovcnt) +{ + return fd_.readv(iov, iovcnt); +} + +ssize_t BufferedFd::doWrite(const void *data, size_t size) +{ + return fd_.write(data, size); +} + void BufferedFd::onReadCallback(short) { RECORD_SCOPE(); @@ -193,7 +203,7 @@ void BufferedFd::onReadCallback(short) rbuf[1].iov_base = extbuf; rbuf[1].iov_len = sizeof(extbuf); - ssize_t rsize = fd_.readv(rbuf, 2); + ssize_t rsize = doReadv(rbuf, 2); if (rsize > 0) { //! 读到了数据 do { if (static_cast(rsize) > writable_size) { @@ -209,7 +219,7 @@ void BufferedFd::onReadCallback(short) writable_size = recv_buff_.writableSize(); rbuf[0].iov_base = recv_buff_.writableBegin(); rbuf[0].iov_len = writable_size; - } while ((rsize = fd_.readv(rbuf, 2)) > 0); + } while ((rsize = doReadv(rbuf, 2)) > 0); //! 如果有绑定接收者,则应将数据直接转发给接收者 if (wp_receiver_ != nullptr) { @@ -261,7 +271,7 @@ void BufferedFd::onWriteCallback(short) } //! 下面是有数据要发送的 - ssize_t wsize = fd_.write(send_buff_.readableBegin(), send_buff_.readableSize()); + ssize_t wsize = doWrite(send_buff_.readableBegin(), send_buff_.readableSize()); if (wsize >= 0) { send_buff_.hasRead(wsize); } else { @@ -276,3 +286,4 @@ void BufferedFd::onWriteCallback(short) } } + diff --git a/modules/network/buffered_fd.h b/modules/network/buffered_fd.h index fa2b13f1..10fe5dad 100644 --- a/modules/network/buffered_fd.h +++ b/modules/network/buffered_fd.h @@ -87,12 +87,17 @@ class BufferedFd : public ByteStream { inline Fd fd() const { return fd_; } inline State state() const { return state_; } + protected: + //! 可被子类覆写的底层I/O方法(如 BufferedSslFd 使用 SSL_read/SSL_write) + virtual ssize_t doReadv(const struct iovec *iov, int iovcnt); + virtual ssize_t doWrite(const void *data, size_t size); + private: void onReadCallback(short); void onWriteCallback(short); private: - event::Loop *wp_loop_ = nullptr; //! 事件驱动 + event::Loop *wp_loop_ = nullptr; Fd fd_; State state_ = State::kEmpty; diff --git a/modules/network/socket_fd.h b/modules/network/socket_fd.h index 4f2c5af4..db729dcd 100644 --- a/modules/network/socket_fd.h +++ b/modules/network/socket_fd.h @@ -37,6 +37,10 @@ class SocketFd : public util::Fd { using Fd::operator=; using Fd::swap; + //! 显式声明拷贝构造与拷贝赋值,消除 GCC -Wdeprecated-copy 警告 + SocketFd(const SocketFd &other) : Fd(other) { } + SocketFd& operator=(const SocketFd &other) { return static_cast(Fd::operator=(other)); } + public: static SocketFd CreateSocket(int domain, int type, int protocal); static SocketFd CreateUdpSocket(); diff --git a/modules/network/stdio_stream.cpp b/modules/network/stdio_stream.cpp index 681defaf..57b52201 100644 --- a/modules/network/stdio_stream.cpp +++ b/modules/network/stdio_stream.cpp @@ -24,8 +24,8 @@ namespace tbox { namespace network { -StdinStream::StdinStream(event::Loop *wp_loop) : - buff_fd_(wp_loop) +StdinStream::StdinStream(event::Loop *wp_loop) + : buff_fd_(wp_loop) { } bool StdinStream::initialize() diff --git a/modules/network/tcp_acceptor.cpp b/modules/network/tcp_acceptor.cpp index f41c1db5..31b36856 100644 --- a/modules/network/tcp_acceptor.cpp +++ b/modules/network/tcp_acceptor.cpp @@ -29,16 +29,14 @@ #include #include -#include "tcp_connection.h" - #undef MODULE_ID #define MODULE_ID "tbox.tcp" namespace tbox { namespace network { -TcpAcceptor::TcpAcceptor(event::Loop *wp_loop) : - wp_loop_(wp_loop) +TcpAcceptor::TcpAcceptor(event::Loop *wp_loop) + : wp_loop_(wp_loop) { } TcpAcceptor::~TcpAcceptor() @@ -162,15 +160,8 @@ void TcpAcceptor::onClientConnected() SockAddr peer_addr(addr, addr_len); LogInfo("%s accepted new connection: %s", bind_addr_.toString().c_str(), peer_addr.toString().c_str()); - if (new_conn_cb_) { - auto sp_connection = new TcpConnection(wp_loop_, peer_sock, peer_addr); - sp_connection->enable(); - ++cb_level_; - new_conn_cb_(sp_connection); - --cb_level_; - } else { - LogWarn("%s need connect cb", bind_addr_.toString().c_str()); - } + //! 调用子类方法处理新连接 + onClientAccepted(peer_sock, peer_addr); } } diff --git a/modules/network/tcp_acceptor.h b/modules/network/tcp_acceptor.h index 7a398de3..05b3083d 100644 --- a/modules/network/tcp_acceptor.h +++ b/modules/network/tcp_acceptor.h @@ -56,10 +56,19 @@ class TcpAcceptor { virtual SocketFd createSocket(SockAddr::Type addr_type); virtual int bindAddress(SocketFd sock_fd, const SockAddr &bind_addr); + //! 子类覆写:创建对应类型的 TcpConnection + virtual TcpConnection* createConnection(event::Loop *wp_loop, SocketFd fd, + const SockAddr &peer_addr) = 0; + + //! 子类覆写:接受新连接后的处理 + //! TcpRawAcceptor: 立即 enable + 触发 new_conn_cb_ + //! TcpTlsAcceptor: 开始 SSL 握手,握手成功后才触发 new_conn_cb_ + virtual void onClientAccepted(SocketFd fd, const SockAddr &peer_addr) = 0; + void onSocketRead(short events); //! 处理新的连接请求 void onClientConnected(); - private: + protected: event::Loop *wp_loop_ = nullptr; SockAddr bind_addr_; @@ -73,5 +82,4 @@ class TcpAcceptor { } } - #endif //TBOX_NETWORK_TCP_ACCEPTOR_20180114 diff --git a/modules/network/tcp_client.cpp b/modules/network/tcp_client.cpp index 07d0a190..8da49a5f 100644 --- a/modules/network/tcp_client.cpp +++ b/modules/network/tcp_client.cpp @@ -26,6 +26,9 @@ #include "tcp_connector.h" #include "tcp_connection.h" +#include "tcp_factory.h" +#include "tcp_raw_factory.h" +#include "tls_factory_entry.h" #undef MODULE_ID #define MODULE_ID "tbox.tcp" @@ -45,19 +48,21 @@ struct TcpClient::Data { ByteStream *wp_receiver = nullptr; bool reconnect_enabled = true; - TcpConnector *sp_connector = nullptr; + TcpFactory *sp_factory = nullptr; + TcpConnector *sp_connector = nullptr; TcpConnection *sp_connection = nullptr; int cb_level = 0; }; -TcpClient::TcpClient(event::Loop *wp_loop) : - d_(new Data) +TcpClient::TcpClient(event::Loop *wp_loop) + : d_(new Data) { TBOX_ASSERT(d_ != nullptr); d_->wp_loop = wp_loop; - d_->sp_connector = new TcpConnector(wp_loop); + d_->sp_factory = new TcpRawFactory; + d_->sp_connector = d_->sp_factory->createConnector(wp_loop); } TcpClient::~TcpClient() @@ -68,10 +73,44 @@ TcpClient::~TcpClient() CHECK_DELETE_RESET_OBJ(d_->sp_connection); CHECK_DELETE_RESET_OBJ(d_->sp_connector); + CHECK_DELETE_RESET_OBJ(d_->sp_factory); delete d_; } +bool TcpClient::setTlsConfig(const TlsConfig &config) +{ + if (d_->state != State::kNone) { + LogWarn("cannot set TLS config after initialization"); + return false; + } + + if (!config.isValid()) { + LogWarn("invalid TLS config"); + return false; + } + + //! 替换 factory 和 connector + TcpFactory *tls_factory = CreateTlsFactory(TlsRole::kClient, config); + if (tls_factory == nullptr) { + LogWarn("failed to create TLS factory, TLS module may not be linked"); + return false; + } + + if (!tls_factory->initialize()) { + LogWarn("failed init TLS factory, config may invalid"); + delete tls_factory; + return false; + } + + CHECK_DELETE_RESET_OBJ(d_->sp_connector); + CHECK_DELETE_RESET_OBJ(d_->sp_factory); + d_->sp_factory = tls_factory; + d_->sp_connector = d_->sp_factory->createConnector(d_->wp_loop); + + return true; +} + bool TcpClient::initialize(const SockAddr &server_addr) { if (d_->state != State::kNone) { diff --git a/modules/network/tcp_client.h b/modules/network/tcp_client.h index 9cbcca1c..7b55d7aa 100644 --- a/modules/network/tcp_client.h +++ b/modules/network/tcp_client.h @@ -27,6 +27,7 @@ #include "byte_stream.h" #include "sockaddr.h" +#include "tls_config.h" #include #include @@ -36,6 +37,7 @@ namespace network { class TcpConnector; class TcpConnection; +class TcpFactory; class TcpClient : public ByteStream { public: @@ -64,6 +66,9 @@ class TcpClient : public ByteStream { void setAutoReconnect(bool enable); void setReconnectDelayCalcFunc(const ReconnectDelayCalc &func); + //! 设置 TLS 配置(必须在 initialize() 之前调用) + bool setTlsConfig(const TlsConfig &config); + bool start(); //!< 开始连接服务端 void stop(); //!< 如果没有连接则成,则停止连接;否则断开连接 diff --git a/modules/network/tcp_connection.cpp b/modules/network/tcp_connection.cpp index 8cdb1e03..63c3d321 100644 --- a/modules/network/tcp_connection.cpp +++ b/modules/network/tcp_connection.cpp @@ -30,16 +30,17 @@ namespace network { using namespace std::placeholders; -TcpConnection::TcpConnection(event::Loop *wp_loop, SocketFd fd, const SockAddr &peer_addr) : - wp_loop_(wp_loop), - sp_buffered_fd_(new BufferedFd(wp_loop)), - peer_addr_(peer_addr) +TcpConnection::TcpConnection(event::Loop *wp_loop, const SockAddr &peer_addr) + : wp_loop_(wp_loop) + , peer_addr_(peer_addr) +{ + //! sp_buffered_fd_ 由子类在构造函数中创建,然后调用 setupBufferedFd() +} + +void TcpConnection::setupBufferedFd() { - sp_buffered_fd_->initialize(fd); sp_buffered_fd_->setReadZeroCallback(std::bind(&TcpConnection::onSocketClosed, this)); sp_buffered_fd_->setReadErrorCallback(std::bind(&TcpConnection::onReadError, this, _1)); - - sp_buffered_fd_->enable(); } TcpConnection::~TcpConnection() @@ -62,17 +63,7 @@ bool TcpConnection::disconnect() if (sp_buffered_fd_ == nullptr) return false; - sp_buffered_fd_->disable(); - - BufferedFd *tmp = nullptr; - std::swap(tmp, sp_buffered_fd_); - - wp_loop_->runNext( - [tmp] { CHECK_DELETE_OBJ(tmp); }, - "TcpConnection::disconnect, delete tmp" - ); - - return true; + return doDisconnect(); } bool TcpConnection::shutdown(int howto) @@ -81,8 +72,7 @@ bool TcpConnection::shutdown(int howto) if (sp_buffered_fd_ == nullptr) return false; - SocketFd socket_fd(sp_buffered_fd_->fd()); - return socket_fd.shutdown(howto) == 0; + return doShutdown(howto); } SocketFd TcpConnection::socketFd() const diff --git a/modules/network/tcp_connection.h b/modules/network/tcp_connection.h index 5c290a53..179da3cf 100644 --- a/modules/network/tcp_connection.h +++ b/modules/network/tcp_connection.h @@ -3,7 +3,7 @@ * // M A K E / \ * // C++ DEV / \ * // E A S Y / \/ \ - * ++ ----------. \/\ . +6 * ++ ----------. \/\ . * \\ \ \ /\ / * \\ \ \ / * \\ \ \ / @@ -33,6 +33,7 @@ namespace network { class TcpConnection : public ByteStream { friend class TcpAcceptor; friend class TcpConnector; + friend class TcpFactory; public: virtual ~TcpConnection(); @@ -43,7 +44,7 @@ class TcpConnection : public ByteStream { public: using DisconnectedCallback = std::function; void setDisconnectedCallback(const DisconnectedCallback &cb) { disconnected_cb_ = cb; } - bool disconnect(); //! 主动断开 + bool disconnect(); bool shutdown(int howto); SockAddr peerAddr() const { return peer_addr_; } @@ -56,6 +57,10 @@ class TcpConnection : public ByteStream { void setContext(void *context, ContextDeleter &&deleter = nullptr); void* getContext() const { return sp_context_; } + //! 启用连接(开始 I/O 事件驱动) + //! 由 TcpAcceptor/TcpConnector 在创建后调用 + void enable(); + public: //! 实现ByteStream的接口 virtual void setReceiveCallback(const ReceiveCallback &cb, size_t threshold) override; @@ -66,16 +71,22 @@ class TcpConnection : public ByteStream { virtual Buffer* getReceiveBuffer() override; protected: - void onSocketClosed(); - void onReadError(int errnum); + //! 基类构造函数,子类需在构造后自行创建 sp_buffered_fd_ 并调用 setupBufferedFd() + explicit TcpConnection(event::Loop *wp_loop, const SockAddr &peer_addr); - private: - explicit TcpConnection(event::Loop *wp_loop, SocketFd fd, const SockAddr &peer_addr); - void enable(); + //! 初始化 BufferedFd 的回调(在子类创建 sp_buffered_fd_ 后调用) + void setupBufferedFd(); - private: + //! 断开连接的具体操作 + //! 子类可覆写此方法以在断开前执行额外操作(如 SSL_shutdown) + virtual bool doDisconnect() = 0; + + //! 子类可覆写此方法以在 shutdown 时执行额外操作 + virtual bool doShutdown(int howto) = 0; + + protected: event::Loop *wp_loop_; - BufferedFd *sp_buffered_fd_; + BufferedFd *sp_buffered_fd_ = nullptr; SockAddr peer_addr_; DisconnectedCallback disconnected_cb_; @@ -83,9 +94,12 @@ class TcpConnection : public ByteStream { ContextDeleter context_deleter_; int cb_level_ = 0; + + private: + void onSocketClosed(); + void onReadError(int errnum); }; } } - #endif //TBOX_NETWORK_TCP_CONNECTION_H_20180113 diff --git a/modules/network/tcp_connector.cpp b/modules/network/tcp_connector.cpp index 47baf61f..c5d4e9da 100644 --- a/modules/network/tcp_connector.cpp +++ b/modules/network/tcp_connector.cpp @@ -25,17 +25,15 @@ #include #include -#include "tcp_connection.h" - #undef MODULE_ID #define MODULE_ID "tbox.tcp" namespace tbox { namespace network { -TcpConnector::TcpConnector(event::Loop *wp_loop) : - wp_loop_(wp_loop), - reconn_delay_calc_func_([](int) {return 1;}) +TcpConnector::TcpConnector(event::Loop *wp_loop) + : wp_loop_(wp_loop) + , reconn_delay_calc_func_([](int) {return 1;}) { } TcpConnector::~TcpConnector() @@ -170,8 +168,9 @@ void TcpConnector::enterConnectingState() int conn_errno = conn_ret == 0 ? 0 : errno; //! 检查错误码 - if ((conn_errno == 0) || (conn_errno == EINPROGRESS) - || (conn_errno == EINTR) || (conn_errno == EISCONN)) { + if ((conn_errno == 0) || (conn_errno == EINPROGRESS) || + (conn_errno == EINTR) || (conn_errno == EISCONN)) + { //! 正常情况 sock_fd_ = std::move(new_sock_fd); @@ -286,14 +285,9 @@ void TcpConnector::onSocketWritable() state_ = State::kInited; LogInfo("connect to %s success", server_addr_.toString().c_str()); - if (connected_cb_) { - auto sp_conn = new TcpConnection(wp_loop_, conn_sock_fd, server_addr_); - sp_conn->enable(); - ++cb_level_; - connected_cb_(sp_conn); - --cb_level_; - } else - LogWarn("connected callback is not set"); + + //! 调用子类方法处理连接成功 + onTcpConnected(conn_sock_fd, server_addr_); } else { //! 连接失败 LogNotice("connect fail, errno:%d, %s", sock_errno, strerror(sock_errno)); diff --git a/modules/network/tcp_connector.h b/modules/network/tcp_connector.h index c303bf2c..3db2c860 100644 --- a/modules/network/tcp_connector.h +++ b/modules/network/tcp_connector.h @@ -74,6 +74,14 @@ class TcpConnector { virtual SocketFd createSocket(SockAddr::Type addr_type) const; virtual int connect(SocketFd sock_fd, const SockAddr &addr) const; + //! 子类覆写:创建对应类型的 TcpConnection + virtual TcpConnection* createConnection(event::Loop *wp_loop, SocketFd fd, const SockAddr &peer_addr) = 0; + + //! 子类覆写:TCP 连接成功后的处理 + //! TcpRawConnector: 立即 enable + 触发 connected_cb_ + //! TcpTlsConnector: 开始 SSL 握手,握手成功后才触发 connected_cb_ + virtual void onTcpConnected(SocketFd fd, const SockAddr &peer_addr) = 0; + void checkSettingAndTryEnterIdleState(); void enterConnectingState(); //!< 进入连接状态的操作 @@ -85,7 +93,7 @@ class TcpConnector { void onSocketWritable(); //!< 当连接成功时的处理 void onDelayTimeout(); //!< 当等待延时到期后的处理 - private: + protected: event::Loop *wp_loop_ = nullptr; State state_ = State::kNone; //! 当前状态 @@ -108,5 +116,4 @@ class TcpConnector { } } - #endif //TBOX_NETWORK_TCP_CONNECTOR_H_20180115 diff --git a/modules/network/tcp_factory.h b/modules/network/tcp_factory.h new file mode 100644 index 00000000..b642efd0 --- /dev/null +++ b/modules/network/tcp_factory.h @@ -0,0 +1,44 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_NETWORK_TCP_FACTORY_H_20260616 +#define TBOX_NETWORK_TCP_FACTORY_H_20260616 + +#include + +namespace tbox { +namespace network { + +class TcpConnector; +class TcpAcceptor; + +//! TCP 抽象工厂,用于创建 Connector 和 Acceptor +//! TcpServer 和 TcpClient 通过工厂决定使用 raw 还是 TLS 传输 +class TcpFactory { + public: + virtual ~TcpFactory() {} + virtual bool initialize() = 0; + + virtual TcpConnector* createConnector(event::Loop *wp_loop) = 0; + virtual TcpAcceptor* createAcceptor(event::Loop *wp_loop) = 0; +}; + +} +} +#endif //TBOX_NETWORK_TCP_FACTORY_H_20260616 diff --git a/modules/network/tcp_raw_acceptor.cpp b/modules/network/tcp_raw_acceptor.cpp new file mode 100644 index 00000000..79d9aefa --- /dev/null +++ b/modules/network/tcp_raw_acceptor.cpp @@ -0,0 +1,57 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "tcp_raw_acceptor.h" +#include "tcp_raw_connection.h" + +#include + +#undef MODULE_ID +#define MODULE_ID "tbox.tcp" + +namespace tbox { +namespace network { + +TcpRawAcceptor::TcpRawAcceptor(event::Loop *wp_loop) + : TcpAcceptor(wp_loop) +{ } + +TcpConnection* TcpRawAcceptor::createConnection(event::Loop *wp_loop, SocketFd fd, const SockAddr &peer_addr) +{ + return new TcpRawConnection(wp_loop, fd, peer_addr); +} + +void TcpRawAcceptor::onClientAccepted(SocketFd fd, const SockAddr &peer_addr) +{ + //! accept 后立即创建 TcpRawConnection 并触发回调 + if (new_conn_cb_) { + auto sp_connection = createConnection(wp_loop_, fd, peer_addr); + sp_connection->enable(); + ++cb_level_; + new_conn_cb_(sp_connection); + --cb_level_; + } else { + LogWarn("%s need connect cb", bind_addr_.toString().c_str()); + //! 没有回调,需要关闭 fd + fd.close(); + } +} + +} +} diff --git a/modules/network/tcp_raw_acceptor.h b/modules/network/tcp_raw_acceptor.h new file mode 100644 index 00000000..58e0553c --- /dev/null +++ b/modules/network/tcp_raw_acceptor.h @@ -0,0 +1,41 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_NETWORK_TCP_RAW_ACCEPTOR_H_20260616 +#define TBOX_NETWORK_TCP_RAW_ACCEPTOR_H_20260616 + +#include "tcp_acceptor.h" + +namespace tbox { +namespace network { + +//! 原始 TCP 接收器(无 TLS) +//! accept 后立即创建 TcpRawConnection 并触发 new_conn callback +class TcpRawAcceptor : public TcpAcceptor { + public: + explicit TcpRawAcceptor(event::Loop *wp_loop); + + protected: + virtual TcpConnection* createConnection(event::Loop *wp_loop, SocketFd fd, const SockAddr &peer_addr) override; + virtual void onClientAccepted(SocketFd fd, const SockAddr &peer_addr) override; +}; + +} +} +#endif //TBOX_NETWORK_TCP_RAW_ACCEPTOR_H_20260616 diff --git a/modules/network/tcp_raw_connection.cpp b/modules/network/tcp_raw_connection.cpp new file mode 100644 index 00000000..e7b2d57e --- /dev/null +++ b/modules/network/tcp_raw_connection.cpp @@ -0,0 +1,60 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "tcp_raw_connection.h" + +#include + +#undef MODULE_ID +#define MODULE_ID "tbox.tcp" + +namespace tbox { +namespace network { + +TcpRawConnection::TcpRawConnection(event::Loop *wp_loop, SocketFd fd, const SockAddr &peer_addr) + : TcpConnection(wp_loop, peer_addr) +{ + sp_buffered_fd_ = new BufferedFd(wp_loop); + sp_buffered_fd_->initialize(fd); + setupBufferedFd(); +} + +bool TcpRawConnection::doDisconnect() +{ + sp_buffered_fd_->disable(); + + BufferedFd *tmp = nullptr; + std::swap(tmp, sp_buffered_fd_); + + wp_loop_->runNext( + [tmp] { CHECK_DELETE_OBJ(tmp); }, + "TcpRawConnection::doDisconnect, delete tmp" + ); + + return true; +} + +bool TcpRawConnection::doShutdown(int howto) +{ + SocketFd socket_fd(sp_buffered_fd_->fd()); + return socket_fd.shutdown(howto) == 0; +} + +} +} diff --git a/modules/network/tcp_raw_connection.h b/modules/network/tcp_raw_connection.h new file mode 100644 index 00000000..b5b221d8 --- /dev/null +++ b/modules/network/tcp_raw_connection.h @@ -0,0 +1,41 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_NETWORK_TCP_RAW_CONNECTION_H_20260616 +#define TBOX_NETWORK_TCP_RAW_CONNECTION_H_20260616 + +#include "tcp_connection.h" + +namespace tbox { +namespace network { + +//! 原始 TCP 连接(无 TLS) +//! 使用 BufferedFd 进行普通 socket I/O +class TcpRawConnection : public TcpConnection { + public: + explicit TcpRawConnection(event::Loop *wp_loop, SocketFd fd, const SockAddr &peer_addr); + + protected: + virtual bool doDisconnect() override; + virtual bool doShutdown(int howto) override; +}; + +} +} +#endif //TBOX_NETWORK_TCP_RAW_CONNECTION_H_20260616 diff --git a/modules/network/tcp_raw_connector.cpp b/modules/network/tcp_raw_connector.cpp new file mode 100644 index 00000000..22048f3a --- /dev/null +++ b/modules/network/tcp_raw_connector.cpp @@ -0,0 +1,57 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "tcp_raw_connector.h" +#include "tcp_raw_connection.h" + +#include + +#undef MODULE_ID +#define MODULE_ID "tbox.tcp" + +namespace tbox { +namespace network { + +TcpRawConnector::TcpRawConnector(event::Loop *wp_loop) + : TcpConnector(wp_loop) +{ } + +TcpConnection* TcpRawConnector::createConnection(event::Loop *wp_loop, SocketFd fd, const SockAddr &peer_addr) +{ + return new TcpRawConnection(wp_loop, fd, peer_addr); +} + +void TcpRawConnector::onTcpConnected(SocketFd fd, const SockAddr &peer_addr) +{ + //! TCP 连接成功后,立即创建 TcpRawConnection 并触发回调 + if (connected_cb_) { + auto sp_conn = createConnection(wp_loop_, fd, peer_addr); + sp_conn->enable(); + ++cb_level_; + connected_cb_(sp_conn); + --cb_level_; + } else { + LogWarn("connected callback is not set"); + //! 没有回调,需要关闭 fd + fd.close(); + } +} + +} +} diff --git a/modules/network/tcp_raw_connector.h b/modules/network/tcp_raw_connector.h new file mode 100644 index 00000000..6193c25f --- /dev/null +++ b/modules/network/tcp_raw_connector.h @@ -0,0 +1,41 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_NETWORK_TCP_RAW_CONNECTOR_H_20260616 +#define TBOX_NETWORK_TCP_RAW_CONNECTOR_H_20260616 + +#include "tcp_connector.h" + +namespace tbox { +namespace network { + +//! 原始 TCP 连接器(无 TLS) +//! TCP 连接成功后立即创建 TcpRawConnection 并触发 connected callback +class TcpRawConnector : public TcpConnector { + public: + explicit TcpRawConnector(event::Loop *wp_loop); + + protected: + virtual TcpConnection* createConnection(event::Loop *wp_loop, SocketFd fd, const SockAddr &peer_addr) override; + virtual void onTcpConnected(SocketFd fd, const SockAddr &peer_addr) override; +}; + +} +} +#endif //TBOX_NETWORK_TCP_RAW_CONNECTOR_H_20260616 diff --git a/modules/network/tcp_raw_factory.cpp b/modules/network/tcp_raw_factory.cpp new file mode 100644 index 00000000..d2f74f81 --- /dev/null +++ b/modules/network/tcp_raw_factory.cpp @@ -0,0 +1,38 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "tcp_raw_factory.h" +#include "tcp_raw_connector.h" +#include "tcp_raw_acceptor.h" + +namespace tbox { +namespace network { + +TcpConnector* TcpRawFactory::createConnector(event::Loop *wp_loop) +{ + return new TcpRawConnector(wp_loop); +} + +TcpAcceptor* TcpRawFactory::createAcceptor(event::Loop *wp_loop) +{ + return new TcpRawAcceptor(wp_loop); +} + +} +} diff --git a/modules/network/tcp_raw_factory.h b/modules/network/tcp_raw_factory.h new file mode 100644 index 00000000..1780c326 --- /dev/null +++ b/modules/network/tcp_raw_factory.h @@ -0,0 +1,39 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_NETWORK_TCP_RAW_FACTORY_H_20260616 +#define TBOX_NETWORK_TCP_RAW_FACTORY_H_20260616 + +#include "tcp_factory.h" + +namespace tbox { +namespace network { + +//! 原始 TCP 工厂(无 TLS) +//! 创建 TcpRawConnector 和 TcpRawAcceptor +class TcpRawFactory : public TcpFactory { + public: + virtual bool initialize() override { return true; } + virtual TcpConnector* createConnector(event::Loop *wp_loop) override; + virtual TcpAcceptor* createAcceptor(event::Loop *wp_loop) override; +}; + +} +} +#endif //TBOX_NETWORK_TCP_RAW_FACTORY_H_20260616 diff --git a/modules/network/tcp_server.cpp b/modules/network/tcp_server.cpp index 89a017e6..bb7fbe16 100644 --- a/modules/network/tcp_server.cpp +++ b/modules/network/tcp_server.cpp @@ -28,6 +28,9 @@ #include "tcp_acceptor.h" #include "tcp_connection.h" +#include "tcp_factory.h" +#include "tcp_raw_factory.h" +#include "tls_factory_entry.h" #undef MODULE_ID #define MODULE_ID "tbox.tcp" @@ -49,6 +52,7 @@ struct TcpServer::Data { size_t receive_threshold = 0; SendCompleteCallback send_complete_cb; + TcpFactory *sp_factory = nullptr; TcpAcceptor *sp_acceptor = nullptr; TcpConns conns; //!< TcpConnection 容器 @@ -62,7 +66,8 @@ TcpServer::TcpServer(event::Loop *wp_loop) : TBOX_ASSERT(d_ != nullptr); d_->wp_loop = wp_loop; - d_->sp_acceptor = new TcpAcceptor(wp_loop); + d_->sp_factory = new TcpRawFactory; + d_->sp_acceptor = d_->sp_factory->createAcceptor(wp_loop); } TcpServer::~TcpServer() @@ -71,10 +76,44 @@ TcpServer::~TcpServer() cleanup(); CHECK_DELETE_RESET_OBJ(d_->sp_acceptor); + CHECK_DELETE_RESET_OBJ(d_->sp_factory); delete d_; } +bool TcpServer::setTlsConfig(const TlsConfig &config) +{ + if (d_->state != State::kNone) { + LogWarn("cannot set TLS config after initialization"); + return false; + } + + if (!config.isValid()) { + LogWarn("invalid TLS config"); + return false; + } + + //! 替换 factory 和 acceptor + TcpFactory *tls_factory = CreateTlsFactory(TlsRole::kServer, config); + if (tls_factory == nullptr) { + LogWarn("failed to create TLS factory, TLS module may not be linked"); + return false; + } + + if (!tls_factory->initialize()) { + LogWarn("failed init TLS factory, config may invalid"); + delete tls_factory; + return false; + } + + CHECK_DELETE_RESET_OBJ(d_->sp_acceptor); + CHECK_DELETE_RESET_OBJ(d_->sp_factory); + d_->sp_factory = tls_factory; + d_->sp_acceptor = d_->sp_factory->createAcceptor(d_->wp_loop); + + return true; +} + bool TcpServer::initialize(const SockAddr &bind_addr, int listen_backlog) { if (d_->state != State::kNone) @@ -229,6 +268,7 @@ TcpConnection* TcpServer::detachConnection(const ConnToken &client) { auto conn = d_->conns.free(client); if (conn != nullptr) { + conn->setContext(nullptr); conn->setReceiveCallback(nullptr, 0); conn->setDisconnectedCallback(nullptr); conn->setSendCompleteCallback(nullptr); diff --git a/modules/network/tcp_server.h b/modules/network/tcp_server.h index 8a7c1216..55f53223 100644 --- a/modules/network/tcp_server.h +++ b/modules/network/tcp_server.h @@ -26,6 +26,7 @@ #include #include "sockaddr.h" +#include "tls_config.h" namespace tbox { namespace network { @@ -34,6 +35,7 @@ using Buffer = util::Buffer; class TcpAcceptor; class TcpConnection; +class TcpFactory; class TcpServer { public: @@ -55,6 +57,9 @@ class TcpServer { //! 设置绑定地址与backlog bool initialize(const SockAddr &bind_addr, int listen_backlog); + //! 设置 TLS 配置(必须在 initialize() 之前调用) + bool setTlsConfig(const TlsConfig &config); + using ConnectedCallback = std::function; using DisconnectedCallback = std::function; using ReceiveCallback = std::function; diff --git a/modules/network/tls_config.cpp b/modules/network/tls_config.cpp new file mode 100644 index 00000000..2d5ead78 --- /dev/null +++ b/modules/network/tls_config.cpp @@ -0,0 +1,48 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "tls_config.h" + +#include + +namespace tbox { +namespace network { + +bool TlsConfig::isValid() const +{ + //! cert_file 和 key_file 必须同时设置或同时为空 + if (!cert_file.empty() && key_file.empty()) { + LogErr("cert_file is set but key_file is not"); + return false; + } + if (!key_file.empty() && cert_file.empty()) { + LogErr("key_file is set but cert_file is not"); + return false; + } + //! ca_file 与 ca_path 可选,不要求必须设置: + //! - verify_peer=true 但未指定 ca_file/ca_path 时,Client 使用系统默认 CA + //! (SSL_CTX_set_default_verify_paths),如 /etc/ssl/certs + //! - Server 不验证客户端证书时不需要 CA + //! - 指定了 ca_file 或 ca_path 时,两者至少有一个非空即可,OpenSSL 会正常加载 + + return true; +} + +} +} diff --git a/modules/network/tls_config.h b/modules/network/tls_config.h new file mode 100644 index 00000000..9a4ec20d --- /dev/null +++ b/modules/network/tls_config.h @@ -0,0 +1,52 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_NETWORK_TLS_CONFIG_H_20260616 +#define TBOX_NETWORK_TLS_CONFIG_H_20260616 + +#include + +namespace tbox { +namespace network { + +//! TLS 配置结构体 +struct TlsConfig { + //! 通用配置 + std::string ca_file; //!< CA 证书文件路径 + std::string ca_path; //!< CA 证书目录路径 + + bool verify_peer = true; //!< 是否验证对端证书 + int verify_depth = 1; //!< 证书链验证深度 + + //! 本端证书和私钥 + //! Server 场景:必须设置,用于向 client 出示证书 + //! Client 场景:可选设置,用于双向 TLS(mTLS)向 server 出示证书 + std::string cert_file; //!< 本端证书文件 + std::string key_file; //!< 本端私钥文件 + + //! Client SNI 配置 + std::string hostname; //!< 用于 SNI (Server Name Indication) 的主机名 + + //! 检查配置是否有效 + bool isValid() const; +}; + +} +} +#endif //TBOX_NETWORK_TLS_CONFIG_H_20260616 diff --git a/modules/network/tls_factory_entry.cpp b/modules/network/tls_factory_entry.cpp new file mode 100644 index 00000000..bfeba18e --- /dev/null +++ b/modules/network/tls_factory_entry.cpp @@ -0,0 +1,34 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S E / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "tls_factory_entry.h" + +#include + +namespace tbox { +namespace network { + +__attribute__((weak)) TcpFactory* CreateTlsFactory(TlsRole, const TlsConfig &) +{ + LogWarn("TLS module not linked, cannot create TLS factory"); + return nullptr; +} + +} +} diff --git a/modules/network/tls_factory_entry.h b/modules/network/tls_factory_entry.h new file mode 100644 index 00000000..a7466356 --- /dev/null +++ b/modules/network/tls_factory_entry.h @@ -0,0 +1,43 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S E / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_NETWORK_TLS_FACTORY_ENTRY_H_20260626 +#define TBOX_NETWORK_TLS_FACTORY_ENTRY_H_20260626 + +#include "tls_config.h" +#include "tcp_factory.h" + +namespace tbox { +namespace network { + +//! TLS 角色 +enum class TlsRole { + kClient, //!< 作为 TLS Client(用于 TcpClient) + kServer, //!< 作为 TLS Server(用于 TcpServer) +}; + +//! TLS 工厂创建入口函数 +//! 默认为弱实现(返回 nullptr),由 network_tls 模块提供强实现 +//! 当链接了 libtbox_network_tls 时,强符号覆盖弱符号,TLS 功能可用 +//! 当未链接 network_tls 时,弱符号生效,调用将返回 nullptr 并打印警告 +extern TcpFactory* CreateTlsFactory(TlsRole role, const TlsConfig &config); + +} +} +#endif //TBOX_NETWORK_TLS_FACTORY_ENTRY_H_20260626 diff --git a/modules/network_tls/CMakeLists.txt b/modules/network_tls/CMakeLists.txt new file mode 100644 index 00000000..1385e0f2 --- /dev/null +++ b/modules/network_tls/CMakeLists.txt @@ -0,0 +1,82 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S E / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +cmake_minimum_required(VERSION 3.15) + +set(TBOX_NETWORK_TLS_VERSION_MAJOR 0) +set(TBOX_NETWORK_TLS_VERSION_MINOR 0) +set(TBOX_NETWORK_TLS_VERSION_PATCH 1) +set(TBOX_NETWORK_TLS_VERSION ${TBOX_NETWORK_TLS_VERSION_MAJOR}.${TBOX_NETWORK_TLS_VERSION_MINOR}.${TBOX_NETWORK_TLS_VERSION_PATCH}) + +add_definitions(-DMODULE_ID="tbox.network_tls") + +find_package(OpenSSL REQUIRED) + +set(TBOX_LIBRARY_NAME tbox_network_tls) + +set(TBOX_NETWORK_TLS_HEADERS + buffered_ssl_fd.h + tcp_tls_connection.h + tcp_tls_acceptor.h + tcp_tls_connector.h + tcp_tls_factory.h) + +set(TBOX_NETWORK_TLS_SOURCES + tls_factory_entry.cpp + buffered_ssl_fd.cpp + tcp_tls_connection.cpp + tcp_tls_acceptor.cpp + tcp_tls_connector.cpp + tcp_tls_factory.cpp) + +add_library(${TBOX_LIBRARY_NAME} ${TBOX_BUILD_LIB_TYPE} ${TBOX_NETWORK_TLS_SOURCES}) +add_library(tbox::${TBOX_LIBRARY_NAME} ALIAS ${TBOX_LIBRARY_NAME}) + +set_target_properties( + ${TBOX_LIBRARY_NAME} PROPERTIES + VERSION ${TBOX_NETWORK_TLS_VERSION} + SOVERSION ${TBOX_NETWORK_TLS_VERSION_MAJOR} +) + +target_link_libraries(${TBOX_LIBRARY_NAME} tbox_network OpenSSL::SSL OpenSSL::Crypto) + +# install the target and create export-set +install( + TARGETS ${TBOX_LIBRARY_NAME} + EXPORT ${TBOX_LIBRARY_NAME}_targets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) + +# install header file +install( + FILES ${TBOX_NETWORK_TLS_HEADERS} + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/tbox/network +) + +# generate and install export file +install( + EXPORT ${TBOX_LIBRARY_NAME}_targets + FILE ${TBOX_LIBRARY_NAME}_targets.cmake + NAMESPACE tbox:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/tbox +) diff --git a/modules/network_tls/Makefile b/modules/network_tls/Makefile new file mode 100644 index 00000000..65203d38 --- /dev/null +++ b/modules/network_tls/Makefile @@ -0,0 +1,54 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S E / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2018 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +PROJECT = network_tls +LIB_NAME = network_tls +LIB_VERSION_X = 0 +LIB_VERSION_Y = 0 +LIB_VERSION_Z = 1 + +HEAD_FILES = \ + buffered_ssl_fd.h \ + tcp_tls_factory.h \ + tcp_tls_connection.h \ + tcp_tls_connector.h \ + tcp_tls_acceptor.h \ + +CPP_SRC_FILES = \ + tls_factory_entry.cpp \ + buffered_ssl_fd.cpp \ + tcp_tls_factory.cpp \ + tcp_tls_connection.cpp \ + tcp_tls_connector.cpp \ + tcp_tls_acceptor.cpp \ + +CXXFLAGS := -DMODULE_ID='"tbox.network_tls"' $(CXXFLAGS) + +TEST_CPP_SRC_FILES = \ + $(CPP_SRC_FILES) \ + +TEST_LDFLAGS := $(LDFLAGS) \ + -ltbox_network -ltbox_network_tls \ + -ltbox_log -ltbox_eventx -ltbox_event -ltbox_util -ltbox_base \ + -lssl -lcrypto -ldl + +ENABLE_SHARED_LIB = no + +include $(TOP_DIR)/mk/lib_tbox_common.mk diff --git a/modules/network_tls/buffered_ssl_fd.cpp b/modules/network_tls/buffered_ssl_fd.cpp new file mode 100644 index 00000000..ccd6d8a1 --- /dev/null +++ b/modules/network_tls/buffered_ssl_fd.cpp @@ -0,0 +1,239 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S E / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "buffered_ssl_fd.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace tbox { +namespace network { + +BufferedSslFd::BufferedSslFd(event::Loop *wp_loop) : + BufferedFd(wp_loop) +{ } + +BufferedSslFd::~BufferedSslFd() +{ + if (ssl_ != nullptr) { + //! 尝试优雅关闭 SSL 连接 + SSL_shutdown(ssl_); + SSL_free(ssl_); + ssl_ = nullptr; + } +} + +bool BufferedSslFd::initialize(Fd fd, SSL *ssl, short events) +{ + if (ssl == nullptr) { + LogWarn("ssl is null"); + return false; + } + + ssl_ = ssl; + + //! 设置 SSL 模式:允许写缓冲区移动(因为我们的 send buffer 在发送过程中可能被修改) + SSL_set_mode(ssl_, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); + + //! 调用父类 initialize + return BufferedFd::initialize(fd, events); +} + +ssize_t BufferedSslFd::doReadv(const struct iovec *iov, int iovcnt) +{ + //! SSL_read 只能向单一连续 buffer 写入数据,无法像 readv() 那样 scatter-gather + //! 实现:逐个 iov 调用 doRead(),将 SSL 解密数据依次填入各 iov 缓冲区 + //! 已填满的 iov 不再参与后续读取,确保数据不会溢出 + + ssize_t total_read = 0; + for (int i = 0; i < iovcnt; ++i) { + if (iov[i].iov_len == 0) + continue; + + ssize_t rsize = doRead(iov[i].iov_base, iov[i].iov_len); + if (rsize > 0) { + total_read += rsize; + //! 如果本次读取未填满当前 iov,说明 SSL 内部缓冲已暂无更多数据 + //! 后续 iov 也无法再填充,直接返回已读总量 + if (static_cast(rsize) < iov[i].iov_len) + return total_read; + } else if (rsize == 0) { + //! 对端关闭连接(close_notify),立即返回 + //! 若已有部分数据读入前面 iov,total_read > 0,由上层判断 + //! 若无数据读入,total_read == 0,上层会走 read_zero_cb_ 逻辑 + return (total_read > 0) ? total_read : 0; + } else { + //! 读取出错(EAGAIN 或其他错误) + //! 若已有部分数据读入前面 iov,total_read > 0,优先返回已读数据 + //! errno 已由 doRead 设置(EAGAIN 等),上层据此判断 + return (total_read > 0) ? total_read : -1; + } + } + + //! 所有 iov 都被填满,但 SSL 内部缓冲可能还有 pending 数据 + //! 上层 onReadCallback 的 while 循环会继续调用 doReadv 来提取 + return total_read; +} + +ssize_t BufferedSslFd::doRead(void *buffer, size_t size) +{ + RECORD_SCOPE(); + + if (ssl_ == nullptr) + return -1; + + ERR_clear_error(); + ssize_t rsize = SSL_read(ssl_, buffer, size); + + if (rsize > 0) { + //! 读取成功,清除 renegotiation 标记 + ssl_read_wants_write_ = false; + ssl_write_wants_read_ = false; + return rsize; + } + + int ssl_error = SSL_get_error(ssl_, rsize); + + if (ssl_error == SSL_ERROR_ZERO_RETURN) { + //! 对端关闭连接(close_notify) + errno = 0; //! 这不是错误,类似 read 返回 0 + return 0; + } + + if (ssl_error == SSL_ERROR_WANT_READ) { + //! 需要更多网络数据才能完成 SSL_read,等下次读事件触发即可 + //! 返回 -1 并设置 errno = EAGAIN,让上层 BufferedFd::onReadCallback 知道暂时没数据 + errno = EAGAIN; + return -1; + } + + if (ssl_error == SSL_ERROR_WANT_WRITE) { + //! renegotiation:SSL_read 需要写入数据 + //! 需要临时启用写事件 + handleSslRenegotiation(ssl_error, true); + errno = EAGAIN; + return -1; + } + + if (ssl_error == SSL_ERROR_SYSCALL) { + //! 系统调用错误 + if (errno == 0) { + //! EOF:对端关闭了连接(未发送 close_notify) + return 0; + } + //! 其他系统错误,errno 已由系统设置 + return -1; + } + + if (ssl_error == SSL_ERROR_SSL) { + //! SSL 协议错误 + //! 当 errno == 0 时,通常是对端非正常关闭连接(未发送 close_notify) + //! 这在实践中很常见,应该视为正常断开,而不是错误 + if (errno == 0) { + return 0; + } + //! 其他 SSL 协议错误 + LogWarn("SSL_read error: SSL_ERROR_SSL, errno:%d", errno); + errno = ECONNRESET; + return -1; + } + + //! 其他未知 SSL 错误 + LogWarn("SSL_read error: %d, errno:%d", ssl_error, errno); + errno = ECONNRESET; //! 将 SSL 错误映射为连接错误 + return -1; +} + +ssize_t BufferedSslFd::doWrite(const void *data, size_t size) +{ + RECORD_SCOPE(); + + if (ssl_ == nullptr) + return -1; + + ERR_clear_error(); + ssize_t wsize = SSL_write(ssl_, data, size); + + if (wsize > 0) { + //! 写入成功,清除 renegotiation 标记 + ssl_read_wants_write_ = false; + ssl_write_wants_read_ = false; + return wsize; + } + + int ssl_error = SSL_get_error(ssl_, wsize); + + if (ssl_error == SSL_ERROR_WANT_WRITE) { + //! 需要等 fd 可写才能继续 SSL_write + //! 返回 0 表示"没写成功但不是错误",让 BufferedFd 保持数据在 send buffer + //! 注意:不能返回 -1 因为 BufferedFd 的 send() 会把 -1 当成错误丢弃数据 + errno = EAGAIN; + //! 返回 0 让 BufferedFd::send() 认为"未发送",数据留在 send buffer + return 0; + } + + if (ssl_error == SSL_ERROR_WANT_READ) { + //! renegotiation:SSL_write 需要读取数据 + handleSslRenegotiation(ssl_error, false); + errno = EAGAIN; + return 0; + } + + if (ssl_error == SSL_ERROR_ZERO_RETURN) { + //! 对端发送了 close_notify + errno = EPIPE; + return -1; + } + + if (ssl_error == SSL_ERROR_SYSCALL) { + if (errno == 0) { + errno = EPIPE; + return -1; + } + return -1; + } + + //! 其他 SSL 错误 + LogWarn("SSL_write error: %d", ssl_error); + errno = ECONNRESET; + return -1; +} + +void BufferedSslFd::handleSslRenegotiation(int ssl_error, bool is_read_op) +{ + if (is_read_op && ssl_error == SSL_ERROR_WANT_WRITE) { + //! SSL_read 需要 fd 可写(renegotiation) + ssl_read_wants_write_ = true; + //! 临时启用写事件,即使 send buffer 为空 + //! 注意:onWriteCallback 在 send buffer 为空时会检查此标记, + //! 如果为 true 则不清除标记、不关闭写事件,而是继续驱动 SSL_read + } else if (!is_read_op && ssl_error == SSL_ERROR_WANT_READ) { + //! SSL_write 需要 fd 可读(renegotiation) + ssl_write_wants_read_ = true; + //! 读事件已经持续启用,无需额外操作 + } +} + +} +} diff --git a/modules/network_tls/buffered_ssl_fd.h b/modules/network_tls/buffered_ssl_fd.h new file mode 100644 index 00000000..b71b528e --- /dev/null +++ b/modules/network_tls/buffered_ssl_fd.h @@ -0,0 +1,64 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_NETWORK_BUFFERED_SSL_FD_H_20260616 +#define TBOX_NETWORK_BUFFERED_SSL_FD_H_20260616 + +#include + +#include + +namespace tbox { +namespace network { + +//! 基于 SSL 的 BufferedFd,用于 TLS 加密通信 +//! 继承自 BufferedFd,覆写 doRead()/doWrite() 使用 SSL_read/SSL_write +//! SSL 握手由外部(TcpTlsConnector/TcpTlsAcceptor)负责,本类只处理已建立 SSL 连接的 I/O +class BufferedSslFd : public BufferedFd { + public: + explicit BufferedSslFd(event::Loop *wp_loop); + virtual ~BufferedSslFd(); + + NONCOPYABLE(BufferedSslFd); + IMMOVABLE(BufferedSslFd); + + //! 初始化,传入已完成握手的 SSL 对象 + //! 注意:SSL 对象的生命期由本对象管理,析构时会调用 SSL_free() + bool initialize(Fd fd, SSL *ssl, short events = kReadWrite); + + protected: + //! 覆写底层 I/O 方法 + virtual ssize_t doReadv(const struct iovec *iov, int iovcnt); + virtual ssize_t doWrite(const void *data, size_t size) override; + + private: + ssize_t doRead(void *buffer, size_t size); + //! 处理 SSL 读写过程中的 WANT_READ/WANT_WRITE(renegotiation) + void handleSslRenegotiation(int ssl_error, bool is_read_op); + + SSL *ssl_ = nullptr; + + //! renegotiation 状态标记 + bool ssl_read_wants_write_ = false; //!< SSL_read 返回了 WANT_WRITE + bool ssl_write_wants_read_ = false; //!< SSL_write 返回了 WANT_READ +}; + +} +} +#endif //TBOX_NETWORK_BUFFERED_SSL_FD_H_20260616 diff --git a/modules/network_tls/tcp_tls_acceptor.cpp b/modules/network_tls/tcp_tls_acceptor.cpp new file mode 100644 index 00000000..0ed46c18 --- /dev/null +++ b/modules/network_tls/tcp_tls_acceptor.cpp @@ -0,0 +1,209 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "tcp_tls_acceptor.h" +#include "tcp_tls_connection.h" + +#include +#include +#include +#include +#include + +#undef MODULE_ID +#define MODULE_ID "tbox.tcp_tls" + +namespace tbox { +namespace network { + +TcpTlsAcceptor::TcpTlsAcceptor(event::Loop *wp_loop, SSL_CTX *ssl_ctx, const TlsConfig &tls_config) + : TcpAcceptor(wp_loop) + , ssl_ctx_(ssl_ctx) + , tls_config_(tls_config) +{ } + +TcpTlsAcceptor::~TcpTlsAcceptor() +{ + //! 如果握手还在进行中,需要清理 + if (sp_handshake_ev_ != nullptr) { + sp_handshake_ev_->disable(); + CHECK_DELETE_RESET_OBJ(sp_handshake_ev_); + } + + if (handshake_ssl_ != nullptr) { + SSL_free(handshake_ssl_); + handshake_ssl_ = nullptr; + } + + handshake_fd_.close(); +} + +TcpConnection* TcpTlsAcceptor::createConnection(event::Loop *, SocketFd, const SockAddr &) +{ + //! 注意:此方法不直接使用,由 onSslHandshakeSuccess 创建 TcpTlsConnection + return nullptr; +} + +void TcpTlsAcceptor::onClientAccepted(SocketFd fd, const SockAddr &peer_addr) +{ + //! accept 后不立即创建 Connection,而是开始 SSL 握手 + startSslHandshake(fd, peer_addr); +} + +void TcpTlsAcceptor::startSslHandshake(SocketFd fd, const SockAddr &peer_addr) +{ + //! 创建 SSL 对象(server 端使用 SSL_accept) + handshake_ssl_ = SSL_new(ssl_ctx_); + if (handshake_ssl_ == nullptr) { + LogErr("SSL_new fail"); + fd.close(); + return; + } + + //! 将 fd 绑定到 SSL + SSL_set_fd(handshake_ssl_, fd.get()); + + //! 设置 SSL 为 server 模式 + SSL_set_accept_state(handshake_ssl_); + + //! 保存握手需要的参数 + handshake_fd_ = fd; //! 不要使用 handshake_fd_.swap(fd) 否则会有问题 + handshake_peer_addr_ = peer_addr; + + //! 开始 SSL_accept + ERR_clear_error(); + int ret = SSL_accept(handshake_ssl_); + + if (ret == 1) { + //! SSL 握手立即完成 + onSslHandshakeSuccess(); + return; + } + + int ssl_error = SSL_get_error(handshake_ssl_, ret); + if (ssl_error == SSL_ERROR_WANT_READ || ssl_error == SSL_ERROR_WANT_WRITE) { + //! 正常的异步握手过程 + short events = (ssl_error == SSL_ERROR_WANT_READ) ? event::FdEvent::kReadEvent : event::FdEvent::kWriteEvent; + + CHECK_DELETE_RESET_OBJ(sp_handshake_ev_); + sp_handshake_ev_ = wp_loop_->newFdEvent("TcpTlsAcceptor::sp_handshake_ev_"); + sp_handshake_ev_->initialize(fd.get(), events, event::Event::Mode::kOneshot); + sp_handshake_ev_->setCallback(std::bind(&TcpTlsAcceptor::onSslHandshakeEvent, this, std::placeholders::_1)); + sp_handshake_ev_->enable(); + + LogDbg("SSL accept in progress, waiting for %s", (ssl_error == SSL_ERROR_WANT_READ) ? "READ" : "WRITE"); + } else { + //! SSL 握手失败 + LogErr("SSL_accept fail, error:%d", ssl_error); + onSslHandshakeFail(); + } +} + +void TcpTlsAcceptor::onSslHandshakeEvent(short) +{ + //! 继续 SSL_accept + ERR_clear_error(); + int ret = SSL_accept(handshake_ssl_); + + if (ret == 1) { + //! 握手成功 + onSslHandshakeSuccess(); + return; + } + + int ssl_error = SSL_get_error(handshake_ssl_, ret); + if (ssl_error == SSL_ERROR_WANT_READ || ssl_error == SSL_ERROR_WANT_WRITE) { + //! 需要继续等待 + //! kOneshot 事件触发后已自动 disable,可以直接 reinitialize,无需重新创建 FdEvent + //! 这避免了在回调中删除 FdEvent 导致的 assert 失败 + short next_events = (ssl_error == SSL_ERROR_WANT_READ) ? event::FdEvent::kReadEvent : event::FdEvent::kWriteEvent; + sp_handshake_ev_->initialize(handshake_fd_.get(), next_events, event::Event::Mode::kOneshot); + sp_handshake_ev_->enable(); + + LogDbg("SSL accept continue, waiting for %s", (ssl_error == SSL_ERROR_WANT_READ) ? "READ" : "WRITE"); + } else { + //! 握手失败 + LogErr("SSL accept fail, error:%d", ssl_error); + onSslHandshakeFail(); + } +} + +void TcpTlsAcceptor::onSslHandshakeSuccess() +{ + RECORD_SCOPE(); + + //! 清理握手相关的 FdEvent + //! 不能在回调中直接删除 FdEvent(cb_level_ > 0),需要延后删除 + if (sp_handshake_ev_ != nullptr) { + sp_handshake_ev_->disable(); + event::FdEvent *tmp = nullptr; + std::swap(tmp, sp_handshake_ev_); + wp_loop_->runNext( + [tmp] { delete tmp; }, + "TcpTlsAcceptor::onSslHandshakeSuccess, delete ev" + ); + } + + //! 将 SSL 和 fd 从握手状态转移到 TcpTlsConnection + SSL *ssl = handshake_ssl_; + handshake_ssl_ = nullptr; + SocketFd fd = handshake_fd_; + handshake_fd_.reset(); + SockAddr peer_addr = handshake_peer_addr_; + + LogInfo("TLS handshake from %s success", peer_addr.toString().c_str()); + + //! 创建 TcpTlsConnection 并触发回调 + if (new_conn_cb_) { + auto sp_connection = new TcpTlsConnection(wp_loop_, fd, peer_addr, ssl); + sp_connection->enable(); + ++cb_level_; + new_conn_cb_(sp_connection); + --cb_level_; + } else { + LogWarn("%s need connect cb", bind_addr_.toString().c_str()); + SSL_free(ssl); + fd.close(); + } +} + +void TcpTlsAcceptor::onSslHandshakeFail() +{ + //! 清理握手相关的资源 + //! 不能在回调中直接删除 FdEvent(cb_level_ > 0),需要延后删除 + if (sp_handshake_ev_ != nullptr) { + sp_handshake_ev_->disable(); + event::FdEvent *tmp = nullptr; + std::swap(tmp, sp_handshake_ev_); + wp_loop_->runNext( + [tmp] { delete tmp; }, + "TcpTlsAcceptor::onSslHandshakeFail, delete ev" + ); + } + + SSL_free(handshake_ssl_); + handshake_ssl_ = nullptr; + handshake_fd_.close(); + + //! 握手失败不影响其他连接,仅关闭当前 fd + LogNotice("TLS handshake fail, close connection"); +} + +} +} diff --git a/modules/network_tls/tcp_tls_acceptor.h b/modules/network_tls/tcp_tls_acceptor.h new file mode 100644 index 00000000..bde74b5c --- /dev/null +++ b/modules/network_tls/tcp_tls_acceptor.h @@ -0,0 +1,68 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_NETWORK_TCP_TLS_ACCEPTOR_H_20260616 +#define TBOX_NETWORK_TCP_TLS_ACCEPTOR_H_20260616 + +#include + +#include +#include +#include + +namespace tbox { +namespace network { + +//! TLS 接收器 +//! accept 后进行 SSL 握手,握手成功才创建 TcpTlsConnection 并触发 new_conn callback +//! 握手失败则关闭 fd,不影响其他连接 +class TcpTlsAcceptor : public TcpAcceptor { + public: + explicit TcpTlsAcceptor(event::Loop *wp_loop, SSL_CTX *ssl_ctx, const TlsConfig &tls_config); + ~TcpTlsAcceptor(); + + protected: + virtual TcpConnection* createConnection(event::Loop *wp_loop, SocketFd fd, + const SockAddr &peer_addr) override; + virtual void onClientAccepted(SocketFd fd, const SockAddr &peer_addr) override; + + private: + //! 开始 SSL 握手 + void startSslHandshake(SocketFd fd, const SockAddr &peer_addr); + //! SSL 握手事件处理 + void onSslHandshakeEvent(short events); + //! SSL 握手成功 + void onSslHandshakeSuccess(); + //! SSL 握手失败 + void onSslHandshakeFail(); + + private: + SSL_CTX *ssl_ctx_ = nullptr; + TlsConfig tls_config_; + + //! 握手期间的临时状态 + SSL *handshake_ssl_ = nullptr; + SocketFd handshake_fd_; + SockAddr handshake_peer_addr_; + event::FdEvent *sp_handshake_ev_ = nullptr; +}; + +} +} +#endif //TBOX_NETWORK_TCP_TLS_ACCEPTOR_H_20260616 diff --git a/modules/network_tls/tcp_tls_connection.cpp b/modules/network_tls/tcp_tls_connection.cpp new file mode 100644 index 00000000..d6197531 --- /dev/null +++ b/modules/network_tls/tcp_tls_connection.cpp @@ -0,0 +1,79 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S E / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "tcp_tls_connection.h" + +#include + +#undef MODULE_ID +#define MODULE_ID "tbox.tcp_tls" + +namespace tbox { +namespace network { + +TcpTlsConnection::TcpTlsConnection(event::Loop *wp_loop, SocketFd fd, const SockAddr &peer_addr, SSL *ssl) + : TcpConnection(wp_loop, peer_addr) +{ + //! 创建 BufferedSslFd 并初始化 + auto *ssl_fd = new BufferedSslFd(wp_loop); + ssl_fd->initialize(fd, ssl); + sp_buffered_fd_ = ssl_fd; + setupBufferedFd(); +} + +bool TcpTlsConnection::doDisconnect() +{ + //! 先尝试 SSL_shutdown(发送 close_notify) + if (sp_buffered_fd_ != nullptr) { + //! 获取 BufferedSslFd 中的 SSL 对象 + //! 通过 fd() 可以获取底层 fd,但我们需要 SSL 对象来 shutdown + //! BufferedSslFd 的析构函数会处理 SSL_shutdown 和 SSL_free + //! 所以这里只需要 disable 和 delete BufferedSslFd 即可 + //! 但为了优雅关闭,先调用一次 SSL_shutdown + //! 注意:由于 SSL 已在 BufferedSslFd 中,我们需要另一种方式 + + //! 禁用事件驱动,停止 I/O + sp_buffered_fd_->disable(); + + BufferedFd *tmp = nullptr; + std::swap(tmp, sp_buffered_fd_); + + //! 延后删除,让 SSL_shutdown 在析构中完成 + wp_loop_->runNext( + [tmp] { CHECK_DELETE_OBJ(tmp); }, + "TcpTlsConnection::doDisconnect, delete tmp" + ); + } + + return true; +} + +bool TcpTlsConnection::doShutdown(int howto) +{ + //! TLS 不支持半关闭的 shutdown(SSL 层面) + //! 只能对底层 socket 执行 shutdown + if (sp_buffered_fd_ != nullptr) { + SocketFd socket_fd(sp_buffered_fd_->fd()); + return socket_fd.shutdown(howto) == 0; + } + return false; +} + +} +} diff --git a/modules/network_tls/tcp_tls_connection.h b/modules/network_tls/tcp_tls_connection.h new file mode 100644 index 00000000..92e5084e --- /dev/null +++ b/modules/network_tls/tcp_tls_connection.h @@ -0,0 +1,47 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S E / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_NETWORK_TCP_TLS_CONNECTION_H_20260616 +#define TBOX_NETWORK_TCP_TLS_CONNECTION_H_20260616 + +#include + +#include +#include "buffered_ssl_fd.h" + +namespace tbox { +namespace network { + +//! TLS 加密 TCP 连接 +//! 使用 BufferedSslFd 进行 SSL I/O +//! SSL 握手由 TcpTlsConnector/TcpTlsAcceptor 在创建本对象之前完成 +class TcpTlsConnection : public TcpConnection { + public: + //! 构造函数,传入已完成握手的 SSL 对象 + //! 注意:本对象接管 SSL 的生命周期,析构时会 SSL_free() + explicit TcpTlsConnection(event::Loop *wp_loop, SocketFd fd, const SockAddr &peer_addr, SSL *ssl); + + protected: + virtual bool doDisconnect() override; + virtual bool doShutdown(int howto) override; +}; + +} +} +#endif //TBOX_NETWORK_TCP_TLS_CONNECTION_H_20260616 diff --git a/modules/network_tls/tcp_tls_connector.cpp b/modules/network_tls/tcp_tls_connector.cpp new file mode 100644 index 00000000..52ac1377 --- /dev/null +++ b/modules/network_tls/tcp_tls_connector.cpp @@ -0,0 +1,220 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S E / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "tcp_tls_connector.h" +#include "tcp_tls_connection.h" + +#include +#include +#include +#include +#include + +#undef MODULE_ID +#define MODULE_ID "tbox.tcp_tls" + +namespace tbox { +namespace network { + +TcpTlsConnector::TcpTlsConnector(event::Loop *wp_loop, SSL_CTX *ssl_ctx, const TlsConfig &tls_config) : + TcpConnector(wp_loop), + ssl_ctx_(ssl_ctx), + tls_config_(tls_config) +{ } + +TcpTlsConnector::~TcpTlsConnector() +{ + //! 如果握手还在进行中,需要清理 + if (sp_handshake_ev_ != nullptr) { + sp_handshake_ev_->disable(); + CHECK_DELETE_RESET_OBJ(sp_handshake_ev_); + } + + if (handshake_ssl_ != nullptr) { + SSL_free(handshake_ssl_); + handshake_ssl_ = nullptr; + } + + handshake_fd_.close(); +} + +TcpConnection* TcpTlsConnector::createConnection(event::Loop *, SocketFd, const SockAddr &) +{ + //! 注意:此方法只在 SSL 握手成功后由 onSslHandshakeSuccess 调用 + //! 此时 handshake_ssl_ 已经是完全建立的 SSL 连接 + //! 但 handshake_ssl_ 已在 onSslHandshakeSuccess 中置 nullptr,需要通过参数传入 + //! 实际上不直接使用此方法,而是在 onSslHandshakeSuccess 中直接创建 TcpTlsConnection + return nullptr; //! 不直接使用此方法 +} + +void TcpTlsConnector::onTcpConnected(SocketFd fd, const SockAddr &peer_addr) +{ + //! TCP 连接成功后,不立即创建 Connection,而是开始 SSL 握手 + startSslHandshake(fd, peer_addr); +} + +void TcpTlsConnector::startSslHandshake(SocketFd fd, const SockAddr &peer_addr) +{ + //! 创建 SSL 对象 + handshake_ssl_ = SSL_new(ssl_ctx_); + if (handshake_ssl_ == nullptr) { + LogErr("SSL_new fail"); + fd.close(); + onConnectFail(); + return; + } + + //! 设置 SNI (Server Name Indication) + if (!tls_config_.hostname.empty()) { + SSL_set_tlsext_host_name(handshake_ssl_, tls_config_.hostname.c_str()); + } + + //! 将 fd 绑定到 SSL + SSL_set_fd(handshake_ssl_, fd.get()); + + //! 保存握手需要的参数 + handshake_fd_ = fd; //! 不要使用 handshake_fd_.swap(fd) 否则会有问题 + handshake_peer_addr_ = peer_addr; + + //! 开始 SSL_connect + ERR_clear_error(); + int ret = SSL_connect(handshake_ssl_); + + if (ret == 1) { + //! SSL 握手立即完成(罕见,通常需要多次 WANT_READ/WANT_WRITE) + onSslHandshakeSuccess(); + return; + } + + int ssl_error = SSL_get_error(handshake_ssl_, ret); + if (ssl_error == SSL_ERROR_WANT_READ || ssl_error == SSL_ERROR_WANT_WRITE) { + //! 正常的异步握手过程,需要等待 fd 事件 + short events = (ssl_error == SSL_ERROR_WANT_READ) ? event::FdEvent::kReadEvent : event::FdEvent::kWriteEvent; + + CHECK_DELETE_RESET_OBJ(sp_handshake_ev_); + sp_handshake_ev_ = wp_loop_->newFdEvent("TcpTlsConnector::sp_handshake_ev_"); + sp_handshake_ev_->initialize(fd.get(), events, event::Event::Mode::kOneshot); + sp_handshake_ev_->setCallback(std::bind(&TcpTlsConnector::onSslHandshakeEvent, this, std::placeholders::_1)); + sp_handshake_ev_->enable(); + + LogDbg("SSL handshake in progress, waiting for %s", (ssl_error == SSL_ERROR_WANT_READ) ? "READ" : "WRITE"); + } else { + //! SSL 握手失败 + LogErr("SSL_connect fail, error:%d", ssl_error); + SSL_free(handshake_ssl_); + handshake_ssl_ = nullptr; + handshake_fd_.close(); + onConnectFail(); + } +} + +void TcpTlsConnector::onSslHandshakeEvent(short) +{ + //! 继续 SSL_connect + ERR_clear_error(); + int ret = SSL_connect(handshake_ssl_); + + if (ret == 1) { + //! 握手成功 + onSslHandshakeSuccess(); + return; + } + + int ssl_error = SSL_get_error(handshake_ssl_, ret); + if (ssl_error == SSL_ERROR_WANT_READ || ssl_error == SSL_ERROR_WANT_WRITE) { + //! 需要继续等待 + //! kOneshot 事件触发后已自动 disable,可以直接 reinitialize,无需重新创建 FdEvent + //! 这避免了在回调中删除 FdEvent 导致的 assert 失败 + short next_events = (ssl_error == SSL_ERROR_WANT_READ) ? event::FdEvent::kReadEvent : event::FdEvent::kWriteEvent; + sp_handshake_ev_->initialize(handshake_fd_.get(), next_events, event::Event::Mode::kOneshot); + sp_handshake_ev_->enable(); + + LogDbg("SSL handshake continue, waiting for %s", (ssl_error == SSL_ERROR_WANT_READ) ? "READ" : "WRITE"); + } else { + //! 握手失败 + LogErr("SSL handshake fail, error:%d", ssl_error); + onSslHandshakeFail(); + } +} + +void TcpTlsConnector::onSslHandshakeSuccess() +{ + RECORD_SCOPE(); + + //! 清理握手相关的 FdEvent + //! 不能在回调中直接删除 FdEvent(cb_level_ > 0),需要延后删除 + if (sp_handshake_ev_ != nullptr) { + sp_handshake_ev_->disable(); + event::FdEvent *tmp = nullptr; + std::swap(tmp, sp_handshake_ev_); + wp_loop_->runNext( + [tmp] { delete tmp; }, + "TcpTlsConnector::onSslHandshakeSuccess, delete ev" + ); + } + + //! 将 SSL 和 fd 从握手状态转移到 TcpTlsConnection + SSL *ssl = handshake_ssl_; + handshake_ssl_ = nullptr; //! 防止析构时重复释放 + SocketFd fd = handshake_fd_; + handshake_fd_.reset(); //! 防止析构时重复关闭 + SockAddr peer_addr = handshake_peer_addr_; + + LogInfo("TLS handshake to %s success", peer_addr.toString().c_str()); + + //! 创建 TcpTlsConnection 并触发回调 + if (connected_cb_) { + auto sp_conn = new TcpTlsConnection(wp_loop_, fd, peer_addr, ssl); + sp_conn->enable(); + ++cb_level_; + connected_cb_(sp_conn); + --cb_level_; + } else { + LogWarn("connected callback is not set"); + //! 没有回调,需要释放 SSL 和关闭 fd + SSL_free(ssl); + fd.close(); + } +} + +void TcpTlsConnector::onSslHandshakeFail() +{ + //! 清理握手相关的资源 + //! 不能在回调中直接删除 FdEvent(cb_level_ > 0),需要延后删除 + if (sp_handshake_ev_ != nullptr) { + sp_handshake_ev_->disable(); + event::FdEvent *tmp = nullptr; + std::swap(tmp, sp_handshake_ev_); + wp_loop_->runNext( + [tmp] { delete tmp; }, + "TcpTlsConnector::onSslHandshakeFail, delete ev" + ); + } + + SSL_free(handshake_ssl_); + handshake_ssl_ = nullptr; + handshake_fd_.close(); + + //! SSL 握手失败视为连接失败,触发重连逻辑 + LogNotice("TLS handshake fail, treat as connection fail"); + onConnectFail(); +} + +} +} diff --git a/modules/network_tls/tcp_tls_connector.h b/modules/network_tls/tcp_tls_connector.h new file mode 100644 index 00000000..ce568acb --- /dev/null +++ b/modules/network_tls/tcp_tls_connector.h @@ -0,0 +1,67 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S E / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_NETWORK_TCP_TLS_CONNECTOR_H_20260616 +#define TBOX_NETWORK_TCP_TLS_CONNECTOR_H_20260616 + +#include + +#include +#include +#include + +namespace tbox { +namespace network { + +//! TLS 连接器 +//! TCP 连接成功后,先进行 SSL 握手,握手成功后才创建 TcpTlsConnection 并触发 connected callback +//! 握手失败视为连接失败,触发重连逻辑 +class TcpTlsConnector : public TcpConnector { + public: + explicit TcpTlsConnector(event::Loop *wp_loop, SSL_CTX *ssl_ctx, const TlsConfig &tls_config); + ~TcpTlsConnector(); + + protected: + virtual TcpConnection* createConnection(event::Loop *wp_loop, SocketFd fd, const SockAddr &peer_addr) override; + virtual void onTcpConnected(SocketFd fd, const SockAddr &peer_addr) override; + + private: + //! 开始 SSL 握手 + void startSslHandshake(SocketFd fd, const SockAddr &peer_addr); + //! SSL 握手事件处理 + void onSslHandshakeEvent(short events); + //! SSL 握手成功 + void onSslHandshakeSuccess(); + //! SSL 握手失败 + void onSslHandshakeFail(); + + private: + SSL_CTX *ssl_ctx_ = nullptr; + TlsConfig tls_config_; + + //! 握手期间的临时状态 + SSL *handshake_ssl_ = nullptr; + SocketFd handshake_fd_; + SockAddr handshake_peer_addr_; + event::FdEvent *sp_handshake_ev_ = nullptr; +}; + +} +} +#endif //TBOX_NETWORK_TCP_TLS_CONNECTOR_H_20260616 diff --git a/modules/network_tls/tcp_tls_factory.cpp b/modules/network_tls/tcp_tls_factory.cpp new file mode 100644 index 00000000..9cc37096 --- /dev/null +++ b/modules/network_tls/tcp_tls_factory.cpp @@ -0,0 +1,194 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "tcp_tls_factory.h" +#include "tcp_tls_connector.h" +#include "tcp_tls_acceptor.h" + +#include +#include + +#include +#include +#include + +#undef MODULE_ID +#define MODULE_ID "tbox.tcp_tls" + +namespace tbox { +namespace network { + +namespace { +//! 加载证书与私钥到 SSL_CTX +//! 成功返回 true,失败返回 false +bool LoadCertAndKey(SSL_CTX *ctx, const std::string &cert_file, const std::string &key_file) +{ + if (SSL_CTX_use_certificate_file(ctx, cert_file.c_str(), SSL_FILETYPE_PEM) != 1) { + LogErr("SSL_CTX_use_certificate_file fail, file:%s", cert_file.c_str()); + ERR_print_errors_fp(stderr); + return false; + } + if (SSL_CTX_use_PrivateKey_file(ctx, key_file.c_str(), SSL_FILETYPE_PEM) != 1) { + LogErr("SSL_CTX_use_PrivateKey_file fail, file:%s", key_file.c_str()); + ERR_print_errors_fp(stderr); + return false; + } + if (SSL_CTX_check_private_key(ctx) != 1) { + LogErr("SSL_CTX_check_private_key fail"); + return false; + } + return true; +} + +//! 加载 CA 证书到 SSL_CTX +//! 成功返回 true,失败返回 false +bool LoadCaCert(SSL_CTX *ctx, const TlsConfig &tls_config) +{ + const char *ca_file = tls_config.ca_file.empty() ? nullptr : tls_config.ca_file.c_str(); + const char *ca_path = tls_config.ca_path.empty() ? nullptr : tls_config.ca_path.c_str(); + + if (SSL_CTX_load_verify_locations(ctx, ca_file, ca_path) != 1) { + LogErr("SSL_CTX_load_verify_locations fail, ca_file:%s, ca_path:%s", + tls_config.ca_file.c_str(), tls_config.ca_path.c_str()); + ERR_print_errors_fp(stderr); + return false; + } + return true; +} + +SSL_CTX* CreateClientSslCtx(const TlsConfig &tls_config) +{ + SSL_CTX *ctx = SSL_CTX_new(TLS_client_method()); + if (ctx == nullptr) { + LogErr("SSL_CTX_new(TLS_client_method) fail"); + return nullptr; + } + + ScopeExitActionGuard guard([ctx] { SSL_CTX_free(ctx); }); + + //! 设置最低 TLS 版本为 1.2 + SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); + + //! 加载 CA 证书(用于验证 server) + if (!tls_config.ca_file.empty() || !tls_config.ca_path.empty()) { + if (!LoadCaCert(ctx, tls_config)) + return nullptr; + + if (tls_config.verify_peer) + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, nullptr); + } else if (tls_config.verify_peer) { + //! 使用系统默认 CA 证书 + if (SSL_CTX_set_default_verify_paths(ctx) != 1) { + LogErr("SSL_CTX_set_default_verify_paths fail"); + return nullptr; + } + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, nullptr); + } else { + SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, nullptr); + } + + //! 加载本端证书和密钥(可选,用于双向 TLS) + if (!tls_config.cert_file.empty() && !tls_config.key_file.empty()) { + if (!LoadCertAndKey(ctx, tls_config.cert_file, tls_config.key_file)) { + return nullptr; + } + } + + guard.cancel(); + return ctx; +} + +SSL_CTX* CreateServerSslCtx(const TlsConfig &tls_config) +{ + SSL_CTX *ctx = SSL_CTX_new(TLS_server_method()); + if (ctx == nullptr) { + LogErr("SSL_CTX_new(TLS_server_method) fail"); + return nullptr; + } + + ScopeExitActionGuard guard([ctx] { SSL_CTX_free(ctx); }); + + //! 设置最低 TLS 版本为 1.2 + SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); + + //! 加载本端证书和密钥(Server 必须设置) + if (!tls_config.cert_file.empty() && !tls_config.key_file.empty()) { + if (!LoadCertAndKey(ctx, tls_config.cert_file, tls_config.key_file)) + return nullptr; + + } else { + LogErr("server cert_file and key_file must be set"); + return nullptr; + } + + //! 加载 CA 证书(可选,用于验证 client - 双向 TLS) + if (!tls_config.ca_file.empty() || !tls_config.ca_path.empty()) { + if (!LoadCaCert(ctx, tls_config)) + return nullptr; + + if (tls_config.verify_peer) { + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr); + SSL_CTX_set_verify_depth(ctx, tls_config.verify_depth); + } + } + + guard.cancel(); + return ctx; +} +} + +/////////////////////////////////////////////////////// + +TcpTlsFactory::TcpTlsFactory(TlsRole role, const TlsConfig &config) + : role_(role) + , tls_config_(config) +{ } + +TcpTlsFactory::~TcpTlsFactory() +{ + if (ssl_ctx_ != nullptr) + SSL_CTX_free(ssl_ctx_); +} + +bool TcpTlsFactory::initialize() +{ + if (role_ == TlsRole::kClient) { + ssl_ctx_ = CreateClientSslCtx(tls_config_); + } else if (role_ == TlsRole::kServer) { + ssl_ctx_ = CreateServerSslCtx(tls_config_); + } + return ssl_ctx_ != nullptr; +} + +TcpConnector* TcpTlsFactory::createConnector(event::Loop *wp_loop) +{ + TBOX_ASSERT(role_ == TlsRole::kClient); + TBOX_ASSERT(ssl_ctx_ != nullptr); + return new TcpTlsConnector(wp_loop, ssl_ctx_, tls_config_); +} + +TcpAcceptor* TcpTlsFactory::createAcceptor(event::Loop *wp_loop) +{ + TBOX_ASSERT(role_ == TlsRole::kServer); + TBOX_ASSERT(ssl_ctx_ != nullptr); + return new TcpTlsAcceptor(wp_loop, ssl_ctx_, tls_config_); +} + +} +} diff --git a/modules/network_tls/tcp_tls_factory.h b/modules/network_tls/tcp_tls_factory.h new file mode 100644 index 00000000..e842b36a --- /dev/null +++ b/modules/network_tls/tcp_tls_factory.h @@ -0,0 +1,53 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_NETWORK_TCP_TLS_FACTORY_H_20260616 +#define TBOX_NETWORK_TCP_TLS_FACTORY_H_20260616 + +#include + +#include +#include +#include + +namespace tbox { +namespace network { + +//! TLS 工厂 +//! 根据 TlsRole 创建对应的 SSL_CTX,仅持有本端所需的那一个 +//! kClient 角色:创建 client SSL_CTX,仅支持 createConnector +//! kServer 角色:创建 server SSL_CTX,仅支持 createAcceptor +class TcpTlsFactory : public TcpFactory { + public: + TcpTlsFactory(TlsRole role, const TlsConfig &config); + ~TcpTlsFactory(); + + virtual bool initialize() override; + virtual TcpConnector* createConnector(event::Loop *wp_loop) override; + virtual TcpAcceptor* createAcceptor(event::Loop *wp_loop) override; + + private: + TlsRole role_; + TlsConfig tls_config_; + SSL_CTX *ssl_ctx_ = nullptr; +}; + +} +} +#endif //TBOX_NETWORK_TCP_TLS_FACTORY_H_20260616 diff --git a/modules/network_tls/tls_factory_entry.cpp b/modules/network_tls/tls_factory_entry.cpp new file mode 100644 index 00000000..1ad97bfe --- /dev/null +++ b/modules/network_tls/tls_factory_entry.cpp @@ -0,0 +1,32 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S E / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2018 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include +#include "tcp_tls_factory.h" + +namespace tbox { +namespace network { + +TcpFactory* CreateTlsFactory(TlsRole role, const TlsConfig &config) +{ + return new TcpTlsFactory(role, config); +} + +} +} diff --git a/modules/run/Makefile b/modules/run/Makefile index a9e994dc..a92f34fb 100644 --- a/modules/run/Makefile +++ b/modules/run/Makefile @@ -39,4 +39,8 @@ LDFLAGS += \ -ldl \ -rdynamic +ifeq ($(findstring network_tls,$(MODULES)),network_tls) +LDFLAGS += -Wl,--whole-archive -ltbox_network_tls -Wl,--no-whole-archive -lssl -lcrypto +endif + include $(TOP_DIR)/mk/exe_common.mk diff --git a/modules/websocket/CMakeLists.txt b/modules/websocket/CMakeLists.txt new file mode 100644 index 00000000..c4508491 --- /dev/null +++ b/modules/websocket/CMakeLists.txt @@ -0,0 +1,103 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2026 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +cmake_minimum_required(VERSION 3.15) + +set(TBOX_WEBSOCKET_VERSION_MAJOR 0) +set(TBOX_WEBSOCKET_VERSION_MINOR 0) +set(TBOX_WEBSOCKET_VERSION_PATCH 1) +set(TBOX_WEBSOCKET_VERSION ${TBOX_WEBSOCKET_VERSION_MAJOR}.${TBOX_WEBSOCKET_VERSION_MINOR}.${TBOX_WEBSOCKET_VERSION_PATCH}) + +add_definitions(-DMODULE_ID="tbox.ws") + +set(TBOX_LIBRARY_NAME tbox_websocket) + +set(TBOX_WEBSOCKET_SOURCES + ws_frame_parser.cpp + ws_frame_builder.cpp + ws_compressor.cpp + server/ws_connection.cpp + server/ws_server_impl.cpp + client/ws_client.cpp + client/ws_client_impl.cpp) + +set(TBOX_WEBSOCKET_TEST_SOURCES + server/ws_server_impl_test.cpp + ws_frame_parser_test.cpp + ws_frame_builder_test.cpp + ws_compressor_test.cpp) + +find_package(ZLIB REQUIRED) + +add_library(${TBOX_LIBRARY_NAME} ${TBOX_BUILD_LIB_TYPE} ${TBOX_WEBSOCKET_SOURCES}) +add_library(tbox::${TBOX_LIBRARY_NAME} ALIAS ${TBOX_LIBRARY_NAME}) + +target_link_libraries(${TBOX_LIBRARY_NAME} ZLIB::ZLIB) + +set_target_properties( + ${TBOX_LIBRARY_NAME} PROPERTIES + VERSION ${TBOX_WEBSOCKET_VERSION} + SOVERSION ${TBOX_WEBSOCKET_VERSION_MAJOR} +) + +if(${TBOX_ENABLE_TEST}) + add_executable(${TBOX_LIBRARY_NAME}_test ${TBOX_WEBSOCKET_TEST_SOURCES}) + target_link_libraries(${TBOX_LIBRARY_NAME}_test gmock_main gmock gtest pthread ${TBOX_LIBRARY_NAME} tbox_base tbox_crypto tbox_http tbox_network tbox_log tbox_eventx tbox_event tbox_util rt dl) + add_test(NAME ${TBOX_LIBRARY_NAME}_test COMMAND ${TBOX_LIBRARY_NAME}_test) +endif() + +# install the target and create export-set +install( + TARGETS ${TBOX_LIBRARY_NAME} + EXPORT ${TBOX_LIBRARY_NAME}_targets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) + +# install header files +install( + FILES + ws_frame.h + ws_compressor.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/tbox/websocket +) + +install( + FILES + server/ws_server.h + server/ws_connection.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/tbox/websocket/server +) + +install( + FILES + client/ws_client.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/tbox/websocket/client +) + +# generate and install export file +install( + EXPORT ${TBOX_LIBRARY_NAME}_targets + FILE ${TBOX_LIBRARY_NAME}_targets.cmake + NAMESPACE tbox:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/tbox +) diff --git a/modules/websocket/Makefile b/modules/websocket/Makefile new file mode 100644 index 00000000..8e207b6a --- /dev/null +++ b/modules/websocket/Makefile @@ -0,0 +1,54 @@ +# +# .============. +# // M A K E / \ +# // C++ DEV / \ +# // E A S Y / \/ \ +# ++ ----------. \/\ . +# \\ \ \ /\ / +# \\ \ \ / +# \\ \ \ / +# -============' +# +# Copyright (c) 2026 Hevake and contributors, all rights reserved. +# +# This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) +# Use of this source code is governed by MIT license that can be found +# in the LICENSE file in the root of the source tree. All contributing +# project authors may be found in the CONTRIBUTORS.md file in the root +# of the source tree. +# + +PROJECT = websocket +LIB_NAME = websocket + +LIB_VERSION_X = 0 +LIB_VERSION_Y = 0 +LIB_VERSION_Z = 1 + +HEAD_FILES = \ + server/ws_server.h \ + client/ws_client.h \ + +CPP_SRC_FILES = \ + ws_frame_parser.cpp \ + ws_frame_builder.cpp \ + ws_compressor.cpp \ + server/ws_connection.cpp \ + server/ws_server_impl.cpp \ + client/ws_client.cpp \ + client/ws_client_impl.cpp \ + +CXXFLAGS := -DMODULE_ID='"tbox.ws"' $(CXXFLAGS) + +TEST_CPP_SRC_FILES = \ + $(CPP_SRC_FILES) \ + server/ws_server_impl_test.cpp \ + ws_frame_parser_test.cpp \ + ws_frame_builder_test.cpp \ + ws_compressor_test.cpp \ + +TEST_LDFLAGS := $(LDFLAGS) -ltbox_crypto -ltbox_http -ltbox_network -ltbox_log -ltbox_eventx -ltbox_event -ltbox_util -ltbox_base -lz -ldl + +ENABLE_SHARED_LIB = no + +include $(TOP_DIR)/mk/lib_tbox_common.mk diff --git a/modules/websocket/client/ws_client.cpp b/modules/websocket/client/ws_client.cpp new file mode 100644 index 00000000..7869bbbb --- /dev/null +++ b/modules/websocket/client/ws_client.cpp @@ -0,0 +1,181 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "ws_client.h" +#include "ws_client_impl.h" +#include + +namespace tbox { +namespace websocket { +namespace client { + +WsClient::WsClient(event::Loop *wp_loop) + : impl_(new Impl(this, wp_loop)) +{ + TBOX_ASSERT(wp_loop != nullptr); +} + +WsClient::~WsClient() +{ + CHECK_DELETE_RESET_OBJ(impl_); +} + +bool WsClient::initialize(const network::SockAddr &server_addr, const std::string &url_path) +{ + return impl_->initialize(server_addr, url_path); +} + +bool WsClient::start() +{ + return impl_->start(); +} + +void WsClient::stop() +{ + impl_->stop(); +} + +void WsClient::cleanup() +{ + impl_->cleanup(); +} + +WsClient::State WsClient::state() const +{ + return impl_->state(); +} + +void WsClient::setConnectedCallback(const ConnectedCallback &cb) +{ + impl_->setConnectedCallback(cb); +} + +void WsClient::setDisconnectedCallback(const DisconnectedCallback &cb) +{ + impl_->setDisconnectedCallback(cb); +} + +void WsClient::setTextMessageCallback(const TextMessageCallback &cb) +{ + impl_->setTextMessageCallback(cb); +} + +void WsClient::setBinaryMessageCallback(const BinaryMessageCallback &cb) +{ + impl_->setBinaryMessageCallback(cb); +} + +void WsClient::setErrorCallback(const ErrorCallback &cb) +{ + impl_->setErrorCallback(cb); +} + +void WsClient::setAutoReconnect(bool enable) +{ + impl_->setAutoReconnect(enable); +} + +void WsClient::setReconnectDelayCalcFunc(const ReconnectDelayCalc &func) +{ + impl_->setReconnectDelayCalcFunc(func); +} + +void WsClient::setCompressionPrefer(bool enable) +{ + impl_->setCompressionPrefer(enable); +} + +void WsClient::setFragmentSize(size_t size) +{ + impl_->setFragmentSize(size); +} + +void WsClient::setPingInterval(int seconds) +{ + impl_->setPingInterval(seconds); +} + +void WsClient::setPingTimeout(int seconds) +{ + impl_->setPingTimeout(seconds); +} + +void WsClient::setTlsConfig(const network::TlsConfig &config) +{ + impl_->setTlsConfig(config); +} + +bool WsClient::send(const std::string &text) +{ + return impl_->send(text); +} + +bool WsClient::send(const char *str) +{ + return impl_->send(str); +} + +bool WsClient::send(const void *data, size_t len) +{ + return impl_->send(data, len); +} + +bool WsClient::send(const std::vector &data) +{ + return impl_->send(data); +} + +bool WsClient::close(uint16_t code, const std::string &reason) +{ + return impl_->close(code, reason); +} + +bool WsClient::ping(const std::string &data) +{ + return impl_->ping(data); +} + +bool WsClient::pong(const std::string &data) +{ + return impl_->pong(data); +} + +bool WsClient::isExpired() const +{ + return impl_->isExpired(); +} + +network::SockAddr WsClient::peerAddr() const +{ + return impl_->peerAddr(); +} + +void WsClient::setContext(void *context, ContextDeleter &&deleter) +{ + impl_->setContext(context, std::move(deleter)); +} + +void* WsClient::getContext() const +{ + return impl_->getContext(); +} + +} +} +} diff --git a/modules/websocket/client/ws_client.h b/modules/websocket/client/ws_client.h new file mode 100644 index 00000000..4173f8fd --- /dev/null +++ b/modules/websocket/client/ws_client.h @@ -0,0 +1,145 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_WS_CLIENT_H_20260615 +#define TBOX_WS_CLIENT_H_20260615 + +#include +#include + +#include +#include +#include +#include + +namespace tbox { +namespace websocket { +namespace client { + +//! WebSocket 客户端 +//! 通过 TcpConnector 建立 TCP 连接,发送 HTTP Upgrade 握手 +//! 握手成功后进入 WebSocket 帧通信模式(客户端帧必须掩码) +//! 断连后支持自动重连(默认开启),重连延迟策略委托给 TcpConnector +//! 分片消息接收完整后统一解压再回调,使用右值引用提升效率 +class WsClient { + public: + //! 默认分片发送的最大帧 payload 大小 + static constexpr size_t kDefaultFragmentSize = 65535; + + explicit WsClient(event::Loop *wp_loop); + ~WsClient(); + + NONCOPYABLE(WsClient); + IMMOVABLE(WsClient); + + public: + //! 初始化:设置目标服务器地址与 URL 路径 + //! server_addr 为服务器地址(如 SockAddr::FromString("127.0.0.1:8080")) + //! url_path 为 WebSocket 路径(如 "/ws/chat") + bool initialize(const network::SockAddr &server_addr, const std::string &url_path = "/"); + + bool start(); + void stop(); + void cleanup(); + + enum class State { kNone, kInited, kConnecting, kHandshaking, kConnected }; + State state() const; + + public: + //! 设置回调(分片消息接收完整后统一解压再回调,使用右值引用提升效率) + using ConnectedCallback = std::function; + using DisconnectedCallback = std::function; + using TextMessageCallback = std::function; + using BinaryMessageCallback = std::function &&)>; + using ErrorCallback = std::function; + + //! 重连延迟策略(与 TcpClient 一致,委托给 TcpConnector) + using ReconnectDelayCalc = std::function; + + void setConnectedCallback(const ConnectedCallback &cb); + void setDisconnectedCallback(const DisconnectedCallback &cb); + void setTextMessageCallback(const TextMessageCallback &cb); + void setBinaryMessageCallback(const BinaryMessageCallback &cb); + void setErrorCallback(const ErrorCallback &cb); + + //! 是否启用自动重连(默认开启) + void setAutoReconnect(bool enable); + //! 设置自定义重连延迟策略(委托给底层 TcpConnector) + void setReconnectDelayCalcFunc(const ReconnectDelayCalc &func); + + //! 设置是否尽可能使用压缩(必须在 initialize 之前调用) + //! 启用后,将在握手请求中请求 permessage-deflate 扩展 + void setCompressionPrefer(bool enable); + + //! 设置分片大小(仅影响发送,接收时自动组装;必须在 initialize 之前调用) + //! 默认为 kDefaultFragmentSize (65535) + //! 值为 0 表示不分片(所有数据单帧发送) + void setFragmentSize(size_t size); + + //! 设置 Ping 发送间隔(秒),0=不自动 Ping(默认;必须在 initialize 之前调用) + //! 启用后,每隔指定秒数向服务器发送 Ping 帧 + void setPingInterval(int seconds); + + //! 设置 Pong 超时时间(秒),0=不检测超时(默认;必须在 initialize 之前调用) + //! 发送 Ping 后若在此时间内未收到 Pong,则判定连接断开并关闭 + void setPingTimeout(int seconds); + + //! 设置 TLS 配置(必须在 initialize() 之前调用) + //! 需要 network_tls 模块支持,未链接时调用无效 + void setTlsConfig(const network::TlsConfig &config); + + public: + //! 发送文本帧 + bool send(const std::string &text); + //! 发送文本帧(const char* 版本,方便直接传字符串字面量) + bool send(const char *str); + //! 发送二进制帧 + bool send(const void *data, size_t len); + //! 发送二进制帧(vector 版本) + bool send(const std::vector &data); + + //! 发送关闭帧并关闭连接 + bool close(uint16_t code = 1000, const std::string &reason = ""); + + //! 发送 Ping 帧 + bool ping(const std::string &data = ""); + //! 发送 Pong 帧 + bool pong(const std::string &data = ""); + + //! 连接是否已失效 + bool isExpired() const; + + //! 获取服务器地址 + network::SockAddr peerAddr() const; + + //! 设置/获取上下文数据 + using ContextDeleter = std::function; + void setContext(void *context, ContextDeleter &&deleter = nullptr); + void* getContext() const; + + private: + class Impl; + Impl *impl_; +}; + +} +} +} + +#endif //TBOX_WS_CLIENT_H_20260615 diff --git a/modules/websocket/client/ws_client_impl.cpp b/modules/websocket/client/ws_client_impl.cpp new file mode 100644 index 00000000..a40596b4 --- /dev/null +++ b/modules/websocket/client/ws_client_impl.cpp @@ -0,0 +1,874 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "ws_client.h" +#include "ws_client_impl.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "../ws_frame_parser.h" +#include "../ws_frame_builder.h" + +#include +#include + +#undef MODULE_ID +#define MODULE_ID "tbox.ws.client" + +namespace tbox { +namespace websocket { +namespace client { + +//! ODR-used static constexpr 成员须在类外定义(C++11) +constexpr size_t WsClient::Impl::kDefaultFragmentSize; + +using namespace std::placeholders; + +//! === 静态辅助方法 === + +//! 生成 16 字节随机数并 Base64 编码,作为 Sec-WebSocket-Key +static std::string GenerateSecWebSocketKey() +{ + uint8_t random_bytes[16]; + for (int i = 0; i < 16; ++i) + random_bytes[i] = static_cast(rand() & 0xFF); + + return util::base64::Encode(random_bytes, 16); +} + +//! 计算 Sec-WebSocket-Accept(与 Server 端一致) +static std::string ComputeWsAcceptKey(const std::string &sec_ws_key) +{ + static const std::string ws_guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + std::string combined = sec_ws_key + ws_guid; + uint8_t digest[20]; + crypto::SHA1::Calc(combined.data(), combined.size(), digest); + + return util::base64::Encode(digest, 20); +} + +//! === 生命周期 === + +WsClient::Impl::Impl(WsClient *wp_parent, event::Loop *wp_loop) + : wp_parent_(wp_parent) + , wp_loop_(wp_loop) +{ } + +WsClient::Impl::~Impl() +{ + cleanup(); +} + +bool WsClient::Impl::initialize(const network::SockAddr &server_addr, const std::string &url_path) +{ + if (state_ != WsClient::State::kNone) + return false; + + server_addr_ = server_addr; + url_path_ = url_path; + + //! 创建 TcpFactory(默认使用 Raw)和 Connector + sp_factory_ = new network::TcpRawFactory; + sp_connector_ = sp_factory_->createConnector(wp_loop_); + sp_connector_->initialize(server_addr_); + sp_connector_->setConnectedCallback(std::bind(&WsClient::Impl::onTcpConnected, this, _1)); + + state_ = WsClient::State::kInited; + return true; +} + +void WsClient::Impl::setTlsConfig(const network::TlsConfig &config) +{ + if (state_ != WsClient::State::kNone) { + LogWarn("cannot set TLS config after initialization"); + return; + } + + if (!config.isValid()) { + LogWarn("invalid TLS config"); + return; + } + + //! 替换 factory 和 connector + CHECK_DELETE_RESET_OBJ(sp_connector_); + CHECK_DELETE_RESET_OBJ(sp_factory_); + + network::TcpFactory *tls_factory = network::CreateTlsFactory(network::TlsRole::kClient, config); + if (tls_factory == nullptr) { + LogWarn("failed to create TLS factory, TLS module may not be linked"); + //! 回退到 Raw Factory + sp_factory_ = new network::TcpRawFactory; + } else { + sp_factory_ = tls_factory; + } + + sp_connector_ = sp_factory_->createConnector(wp_loop_); + sp_connector_->initialize(server_addr_); + sp_connector_->setConnectedCallback(std::bind(&WsClient::Impl::onTcpConnected, this, _1)); +} + +void WsClient::Impl::setReconnectDelayCalcFunc(const WsClient::ReconnectDelayCalc &func) +{ + if (sp_connector_ != nullptr) + sp_connector_->setReconnectDelayCalcFunc(func); +} + +bool WsClient::Impl::start() +{ + if (state_ != WsClient::State::kInited) + return false; + + //! 每次连接(含重连)都需要生成新的 Sec-WebSocket-Key + sec_ws_key_ = GenerateSecWebSocketKey(); + is_closing_ = false; + frame_parser_.reset(); + + //! 清理分片缓存 + fragment_buffer_.clear(); + is_fragmenting_ = false; + + //! 开始 TCP 连接(TcpConnector 内部处理重连延迟) + sp_connector_->start(); + state_ = WsClient::State::kConnecting; + return true; +} + +void WsClient::Impl::stop() +{ + if (state_ == WsClient::State::kNone || state_ == WsClient::State::kInited) + return; + + //! 清除 TcpConnection 内部回调,防止断开时回调到 Impl + if (sp_tcp_conn_ != nullptr) { + sp_tcp_conn_->setReceiveCallback(nullptr, 0); + sp_tcp_conn_->setDisconnectedCallback(nullptr); + + sp_tcp_conn_->disconnect(); + auto tcp_conn = sp_tcp_conn_; + sp_tcp_conn_ = nullptr; + wp_loop_->runNext([tcp_conn] { CHECK_DELETE_OBJ(tcp_conn); }, + "WsClient::stop, delete tcp_conn"); + } + + //! 停止 TcpConnector(停止正在进行的连接或重连等待) + if (sp_connector_ != nullptr) + sp_connector_->stop(); + + state_ = WsClient::State::kInited; +} + +void WsClient::Impl::cleanup() +{ + if (state_ == WsClient::State::kNone) + return; + + if (state_ != WsClient::State::kInited) + stop(); + + CHECK_DELETE_RESET_OBJ(sp_connector_); + CHECK_DELETE_RESET_OBJ(sp_factory_); + + CHECK_DELETE_RESET_OBJ(sp_ping_timer_); + CHECK_DELETE_RESET_OBJ(sp_pong_timer_); + + connected_cb_ = nullptr; + disconnected_cb_ = nullptr; + text_message_cb_ = nullptr; + binary_message_cb_ = nullptr; + error_cb_ = nullptr; + reconnect_enabled_ = true; + + //! 清理分片缓存 + fragment_buffer_.clear(); + is_fragmenting_ = false; + + state_ = WsClient::State::kNone; +} + +//! === TCP 连接回调 === + +void WsClient::Impl::onTcpConnected(network::TcpConnection *tcp_conn) +{ + RECORD_SCOPE(); + LogInfo("tcp connected to %s", tcp_conn->peerAddr().toString().c_str()); + + //! 连接成功,TcpConnector 停止但保持存活(不删除,供重连使用) + sp_connector_->stop(); + + //! 保存 TcpConnection,进入握手阶段 + sp_tcp_conn_ = tcp_conn; + state_ = WsClient::State::kHandshaking; + + //! 设置 TcpConnection 回调(握手阶段:阈值=0,立即触发) + sp_tcp_conn_->setReceiveCallback(std::bind(&WsClient::Impl::onTcpReceived, this, _1), 0); + sp_tcp_conn_->setDisconnectedCallback(std::bind(&WsClient::Impl::onTcpDisconnected, this)); + + //! 发送握手请求 + sendHandshakeRequest(); +} + +void WsClient::Impl::onTcpDisconnected() +{ + RECORD_SCOPE(); + LogInfo("ws client disconnected"); + + //! 清理分片缓存 + fragment_buffer_.clear(); + is_fragmenting_ = false; + + //! 禁用心跳定时器 + if (sp_ping_timer_ != nullptr) + sp_ping_timer_->disable(); + if (sp_pong_timer_ != nullptr) + sp_pong_timer_->disable(); + is_pong_pending_ = false; + + //! 通知用户 + if (disconnected_cb_) { + RECORD_SCOPE(); + ++cb_level_; + disconnected_cb_(); + --cb_level_; + } + + //! 延后删除 TcpConnection(本函数是 sp_tcp_conn_ 自己调用的) + auto tobe_delete = sp_tcp_conn_; + sp_tcp_conn_ = nullptr; + wp_loop_->runNext([tobe_delete] { CHECK_DELETE_OBJ(tobe_delete); }, + "WsClient::onTcpDisconnected, delete tobe_delete"); + + state_ = WsClient::State::kInited; + + //! 自动重连(与 TcpClient 一致:先重连再通知用户) + if (reconnect_enabled_) + start(); +} + +//! === 握手阶段 === + +void WsClient::Impl::sendHandshakeRequest() +{ + //! RFC 6455 Section 4.1:客户端握手请求 + //! GET /path HTTP/1.1\r\n + //! Host: host:port\r\n + //! Upgrade: websocket\r\n + //! Connection: Upgrade\r\n + //! Sec-WebSocket-Key: \r\n + //! Sec-WebSocket-Version: 13\r\n\r\n + + std::string host = server_addr_.toString(); + + std::string request = + "GET " + url_path_ + " HTTP/1.1\r\n" + + "Host: " + host + "\r\n" + + "Upgrade: websocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Key: " + sec_ws_key_ + "\r\n" + + "Sec-WebSocket-Version: 13\r\n"; + + //! RFC 7692:若 prefer_compression_=true,请求压缩扩展 + //! 必须声明 client_no_context_takeover 和 server_no_context_takeover + //! 与我们的实现一致(每条消息独立压缩) + if (prefer_compression_) { + request += "Sec-WebSocket-Extensions: permessage-deflate; client_no_context_takeover; server_no_context_takeover\r\n"; + } + + request += "\r\n"; + + LogDbg("ws client handshake request sent"); + sp_tcp_conn_->send(request.data(), request.size()); +} + +bool WsClient::Impl::parseHandshakeResponse(network::Buffer &buff) +{ + //! 查找 \r\n\r\n 分隔符(HTTP 响应头结束标志) + const char *data = reinterpret_cast(buff.readableBegin()); + size_t size = buff.readableSize(); + + const char *end = static_cast(memmem(data, size, "\r\n\r\n", 4)); + if (end == nullptr) + return false; //! 响应不完整,等待更多数据 + + size_t header_len = end - data + 4; + + //! 简单解析 HTTP 响应行:HTTP/1.1 101 Switching Protocols + //! 仅检查状态码是否为 101 + std::string header(data, header_len); + + //! 检查状态码 101 + if (header.find("101") == std::string::npos) { + LogNotice("ws client handshake fail: not 101 response"); + buff.hasRead(header_len); + return true; //! 解析完成但失败 + } + + //! 检查 Upgrade: websocket + if (header.find("Upgrade: websocket") == std::string::npos && + header.find("Upgrade: WebSocket") == std::string::npos) { + LogNotice("ws client handshake fail: missing Upgrade: websocket"); + buff.hasRead(header_len); + return true; + } + + //! 检查 Connection: Upgrade + if (header.find("Connection: Upgrade") == std::string::npos) { + LogNotice("ws client handshake fail: missing Connection: Upgrade"); + buff.hasRead(header_len); + return true; + } + + //! 验证 Sec-WebSocket-Accept + std::string expected_accept = ComputeWsAcceptKey(sec_ws_key_); + //! 查找 Sec-WebSocket-Accept 头部值 + size_t accept_pos = header.find("Sec-WebSocket-Accept: "); + if (accept_pos == std::string::npos) { + LogNotice("ws client handshake fail: missing Sec-WebSocket-Accept"); + buff.hasRead(header_len); + return true; + } + size_t value_start = accept_pos + strlen("Sec-WebSocket-Accept: "); + size_t value_end = header.find("\r\n", value_start); + std::string actual_accept = header.substr(value_start, value_end - value_start); + + if (actual_accept != expected_accept) { + LogNotice("ws client handshake fail: Sec-WebSocket-Accept mismatch"); + buff.hasRead(header_len); + return true; + } + + //! 握手成功!消耗响应头,切换到帧通信模式 + buff.hasRead(header_len); + LogInfo("ws client handshake success"); + + //! RFC 7692:检查压缩协商结果 + //! 若客户端请求了压缩且服务器同意了 permessage-deflate + if (prefer_compression_ && + header.find("Sec-WebSocket-Extensions: permessage-deflate") != std::string::npos) { + //! 服务器同意压缩 + compression_config_.enabled = true; + compression_config_.no_context_takeover = true; + compression_config_.max_window_bits = 15; + LogInfo("ws client compression agreed: permessage-deflate"); + } else { + //! 服务器不同意压缩,或客户端未请求 + compression_config_.enabled = false; + } + + onHandshakeSuccess(); + return true; +} + +void WsClient::Impl::onHandshakeSuccess() +{ + state_ = WsClient::State::kConnected; + frame_parser_.reset(); + + //! 初始化压缩器 + if (compression_config_.enabled) { + if (!compressor_.initialize(compression_config_)) { + LogErr("WsClient compressor init fail, fallback to no compression"); + compression_config_.enabled = false; + } + } + + //! 初始化 Ping/Pong 心跳定时器 + //! 每次连接(含重连)都重新创建定时器 + CHECK_DELETE_RESET_OBJ(sp_ping_timer_); + CHECK_DELETE_RESET_OBJ(sp_pong_timer_); + is_pong_pending_ = false; + + if (ping_interval_ > 0) { + sp_ping_timer_ = wp_loop_->newTimerEvent(); + sp_ping_timer_->initialize(std::chrono::seconds(ping_interval_), event::Event::Mode::kPersist); + sp_ping_timer_->setCallback(std::bind(&WsClient::Impl::onPingTimerFired, this)); + sp_ping_timer_->enable(); + + if (ping_timeout_ > 0) { + sp_pong_timer_ = wp_loop_->newTimerEvent(); + sp_pong_timer_->initialize(std::chrono::seconds(ping_timeout_), event::Event::Mode::kOneshot); + sp_pong_timer_->setCallback(std::bind(&WsClient::Impl::onPongTimeoutFired, this)); + } + } + + //! 通知用户 + if (connected_cb_) { + RECORD_SCOPE(); + ++cb_level_; + connected_cb_(); + --cb_level_; + } +} + +void WsClient::Impl::onHandshakeFail() +{ + //! 握手失败,断开连接,若启用重连则自动重连 + auto tobe_delete = sp_tcp_conn_; + sp_tcp_conn_ = nullptr; + + //! 延后删除 TcpConnection + wp_loop_->runNext([tobe_delete] { CHECK_DELETE_OBJ(tobe_delete); }, + "WsClient::onHandshakeFail, delete tobe_delete"); + + //! 清除 TcpConnection 回调(防止延后删除期间回调到 Impl) + //! tobe_delete 已 disconnect,延后删除时不会再回调 + + state_ = WsClient::State::kInited; + + //! 自动重连(与 onTcpDisconnected 一致) + if (reconnect_enabled_) + start(); +} + +//! === 帧通信阶段 === + +//! 将完整数据交付给业务层 +//! data 为解压后的完整数据(若不需要解压则为原始 payload) +//! opcode 为消息类型(kText 或 kBinary) +void WsClient::Impl::deliverMessage(WsFrame::OpCode opcode, std::string &data) +{ + if (opcode == WsFrame::OpCode::kText) { + if (text_message_cb_) { + ++cb_level_; + text_message_cb_(std::move(data)); + --cb_level_; + } + } else if (opcode == WsFrame::OpCode::kBinary) { + //! 将 std::string 转换为 std::vector + std::vector vec(data.begin(), data.end()); + if (binary_message_cb_) { + ++cb_level_; + binary_message_cb_(std::move(vec)); + --cb_level_; + } + } +} + +void WsClient::Impl::onWsFrameReceived(network::Buffer &buff) +{ + //! 与 server::WsConnection 的帧解析逻辑相同:分片数据先缓存,接收完整后统一解压再回调 + while (buff.readableSize() > 0) { + size_t consumed = frame_parser_.parse(buff.readableBegin(), buff.readableSize()); +#if 1 + auto hex_str = util::string::RawDataToHexStr(buff.readableBegin(), buff.readableSize()); + LogTrace("hex: %s, consumed:%u", hex_str.c_str(), consumed); +#endif + buff.hasRead(consumed); + + if (frame_parser_.state() == WsFrameParser::State::kFinished) { + WsFrame *frame = frame_parser_.getFrame(); + if (frame != nullptr) { + //! ===== 控制帧处理(Close/Ping/Pong 不受分片状态影响) ===== + if (frame->isControlFrame()) { + switch (frame->opcode) { + case WsFrame::OpCode::kClose: + //! 收到关闭帧,自动回复关闭帧(掩码) + if (!is_closing_) { + auto close_frame = WsFrameBuilder::BuildMaskedCloseFrame(frame->closeCode(), frame->closeReason()); + sp_tcp_conn_->send(close_frame.data(), close_frame.size()); + is_closing_ = true; + } + buff.hasReadAll(); + //! 清理分片缓存 + fragment_buffer_.clear(); + is_fragmenting_ = false; + delete frame; + //! 等待 TCP 断开,由 onTcpDisconnected 通知用户并自动重连 + return; + + case WsFrame::OpCode::kPing: + //! 自动回复 Pong(掩码) + pong(frame->payload); + break; + + case WsFrame::OpCode::kPong: + //! 心跳:收到 Pong,取消超时定时器 + if (is_pong_pending_) { + is_pong_pending_ = false; + if (sp_pong_timer_ != nullptr) + sp_pong_timer_->disable(); + } + break; + + default: + LogNotice("ws client unknown control opcode: 0x%02x", static_cast(frame->opcode)); + delete frame; + buff.hasReadAll(); + fragment_buffer_.clear(); + is_fragmenting_ = false; + onError(); + return; + } + delete frame; + continue; //! 控制帧处理完毕,继续解析下一个帧 + } + + //! ===== 数据帧处理(TEXT / BINARY / CONTINUE) ===== + //! 核心逻辑:分片数据先缓存,接收完整后统一解压再回调 + //! 原因:压缩数据不能逐片解压,必须拼接完整后才能解压 + + if (frame->opcode == WsFrame::OpCode::kText || + frame->opcode == WsFrame::OpCode::kBinary) { + //! 新消息的首帧 + if (is_fragmenting_) { + //! 正在接收分片消息时又收到新消息首帧,协议违规 + LogNotice("ws client protocol error: new data frame while fragmenting"); + delete frame; + buff.hasReadAll(); + fragment_buffer_.clear(); + is_fragmenting_ = false; + onError(); + return; + } + + if (frame->fin) { + //! 单帧完整消息(无分片) + bool is_need_decompress = frame->rsv1 && compression_config_.enabled; + if (is_need_decompress) { + std::string decompressed = compressor_.decompress(frame->payload); + if (!decompressed.empty()) { + frame->payload = std::move(decompressed); + } else { + //! 解压失败 + LogNotice("ws client decompress fail"); + delete frame; + buff.hasReadAll(); + onError(); + return; + } + } + + //! 交付完整消息给业务层 + deliverMessage(frame->opcode, frame->payload); + delete frame; + + } else { + //! 分片消息的首帧(fin=false) + //! 记录原始 opcode 和是否需要解压,缓存 payload + is_fragmenting_ = true; + fragment_opcode_ = frame->opcode; + fragment_need_decompress_ = frame->rsv1 && compression_config_.enabled; + fragment_buffer_ = std::move(frame->payload); + delete frame; + } + + } else if (frame->opcode == WsFrame::OpCode::kContinue) { + //! 分片消息的后续帧 + if (!is_fragmenting_) { + //! 没有首帧却收到续帧,协议违规 + LogNotice("ws client protocol error: continue frame without fragment start"); + delete frame; + buff.hasReadAll(); + onError(); + return; + } + + //! 将本片 payload 追加到缓存区 + fragment_buffer_.append(frame->payload); + + if (frame->fin) { + //! 最后一帧(fin=true),消息完整 + //! 对完整数据统一解压,然后回调业务层 + if (fragment_need_decompress_) { + std::string decompressed = compressor_.decompress(fragment_buffer_); + if (!decompressed.empty()) { + fragment_buffer_ = std::move(decompressed); + } else { + //! 解压失败 + LogNotice("ws client decompress fail"); + delete frame; + buff.hasReadAll(); + fragment_buffer_.clear(); + is_fragmenting_ = false; + onError(); + return; + } + } + + //! 交付完整消息给业务层 + deliverMessage(fragment_opcode_, fragment_buffer_); + + //! 重置分片状态 + fragment_buffer_.clear(); + is_fragmenting_ = false; + } + //! fin=false: 继续缓存,不回调 + + delete frame; + + } else { + //! 未知数据帧 opcode + LogNotice("ws client unknown opcode: 0x%02x", static_cast(frame->opcode)); + delete frame; + buff.hasReadAll(); + fragment_buffer_.clear(); + is_fragmenting_ = false; + onError(); + return; + } + } + } else if (frame_parser_.state() == WsFrameParser::State::kError) { + LogNotice("ws client frame parse error"); + buff.hasReadAll(); + fragment_buffer_.clear(); + is_fragmenting_ = false; + onError(); + return; + } else { + //! 需要更多数据 + break; + } + } +} + +//! === TCP 收到数据(握手/帧共用) === + +void WsClient::Impl::onTcpReceived(network::Buffer &buff) +{ + RECORD_SCOPE(); + + if (state_ == WsClient::State::kHandshaking) { + //! 握手阶段:解析 HTTP 响应 + bool parsed = parseHandshakeResponse(buff); + if (parsed) { + if (state_ == WsClient::State::kHandshaking) { + //! parseHandshakeResponse 没有改变 state,说明验证失败 + onHandshakeFail(); + } else { + //! state 已变为 kConnected,握手成功 + //! buff 中可能还有剩余数据(服务器在 101 后立即发来的帧) + if (buff.readableSize() > 0) + onWsFrameReceived(buff); + } + } + //! parsed == false:响应不完整,等待更多数据 + } else if (state_ == WsClient::State::kConnected) { + //! 帧通信阶段 + onWsFrameReceived(buff); + } +} + +void WsClient::Impl::onError() +{ + //! 清理分片缓存 + fragment_buffer_.clear(); + is_fragmenting_ = false; + + //! 出错后断开连接,由 onTcpDisconnected 处理重连 + if (sp_tcp_conn_ != nullptr) + sp_tcp_conn_->disconnect(); +} + +//! === 通过 ConnToken 操作连接 === + +bool WsClient::Impl::send(const std::string &text) +{ + return sendData(WsFrame::OpCode::kText, text.data(), text.size()); +} + +bool WsClient::Impl::send(const char *str) +{ + return sendData(WsFrame::OpCode::kText, str, strlen(str)); +} + +bool WsClient::Impl::send(const void *data, size_t len) +{ + return sendData(WsFrame::OpCode::kBinary, data, len); +} + +bool WsClient::Impl::send(const std::vector &data) +{ + return sendData(WsFrame::OpCode::kBinary, data.data(), data.size()); +} + +bool WsClient::Impl::close(uint16_t code, const std::string &reason) +{ + if (sp_tcp_conn_ == nullptr || state_ != WsClient::State::kConnected) + return false; + + is_closing_ = true; + + auto frame = WsFrameBuilder::BuildMaskedCloseFrame(code, reason); + sp_tcp_conn_->send(frame.data(), frame.size()); + + //! 延后断开,确保 Close 帧已发送 + wp_loop_->runNext([this] { + if (sp_tcp_conn_ != nullptr) + sp_tcp_conn_->disconnect(); + }, "WsClient::close, disconnect"); + + return true; +} + +bool WsClient::Impl::ping(const std::string &data) +{ + if (is_closing_ || sp_tcp_conn_ == nullptr || state_ != WsClient::State::kConnected) + return false; + + auto frame = WsFrameBuilder::BuildMaskedPingFrame(data); + return sp_tcp_conn_->send(frame.data(), frame.size()); +} + +bool WsClient::Impl::pong(const std::string &data) +{ + if (is_closing_ || sp_tcp_conn_ == nullptr || state_ != WsClient::State::kConnected) + return false; + + auto frame = WsFrameBuilder::BuildMaskedPongFrame(data); + return sp_tcp_conn_->send(frame.data(), frame.size()); +} + +bool WsClient::Impl::sendMaskedFrame(WsFrame::OpCode opcode, bool fin, const void *payload, size_t payload_len) +{ + if (sp_tcp_conn_ == nullptr) + return false; + + auto frame = WsFrameBuilder::BuildMaskedFrame(opcode, fin, payload, payload_len); + return sp_tcp_conn_->send(frame.data(), frame.size()); +} + +//! 统一发送数据:前置检查 → 压缩(如需要) → sendFragmented +//! opcode 为 kText 或 kBinary +bool WsClient::Impl::sendData(WsFrame::OpCode opcode, const void *data_ptr, size_t data_len) +{ + if (is_closing_ || sp_tcp_conn_ == nullptr || state_ != WsClient::State::kConnected) + return false; + + //! 压缩协商达成时,压缩数据 + if (compression_config_.enabled && compressor_.isInitialized()) { + std::string compressed = compressor_.compress(data_ptr, data_len); + if (!compressed.empty()) { + return sendFragmented(opcode, compressed.data(), compressed.size(), true); + } + //! 压缩失败,回退到不压缩 + LogNotice("ws client compress fail, fallback to uncompressed"); + } + + return sendFragmented(opcode, data_ptr, data_len, false); +} + +//! 分片发送 payload(客户端版本,掩码) +//! 若 payload 大小超过 fragment_size_,则分片发送: +//! - 首帧:原始 opcode,fin=false,rsv1=is_compressed(掩码) +//! - 中间帧:kContinue,fin=false(掩码) +//! - 末帧:kContinue,fin=true(掩码) +//! 若 payload 大小不超过 fragment_size_,则单帧发送 +bool WsClient::Impl::sendFragmented(WsFrame::OpCode opcode, const void *payload, size_t payload_len, bool is_compressed) +{ + if (sp_tcp_conn_ == nullptr) + return false; + + //! 单帧即可发送(fragment_size_ 为 0 时表示不分片) + if (fragment_size_ == 0 || payload_len <= fragment_size_) { + auto frame = WsFrameBuilder::BuildMaskedFrame(opcode, true, payload, payload_len, nullptr, is_compressed); + return sp_tcp_conn_->send(frame.data(), frame.size()); + } + + //! 分片发送 + const uint8_t *data = static_cast(payload); + size_t offset = 0; + + //! 首帧:原始 opcode,fin=false,rsv1=is_compressed(掩码) + size_t first_chunk = fragment_size_; + auto frame = WsFrameBuilder::BuildMaskedFrame(opcode, false, data, first_chunk, nullptr, is_compressed); + if (!sp_tcp_conn_->send(frame.data(), frame.size())) + return false; + + offset += first_chunk; + + //! 中间帧与末帧:opcode=kContinue,rsv1=false(掩码) + while (offset < payload_len) { + size_t remaining = payload_len - offset; + size_t chunk_size = std::min(remaining, fragment_size_); + bool is_last = (offset + chunk_size == payload_len); + + auto cont_frame = WsFrameBuilder::BuildMaskedFrame(WsFrame::OpCode::kContinue, is_last, + data + offset, chunk_size, nullptr, false); + if (!sp_tcp_conn_->send(cont_frame.data(), cont_frame.size())) + return false; + + offset += chunk_size; + } + + return true; +} + +bool WsClient::Impl::isExpired() const +{ + return sp_tcp_conn_ == nullptr || sp_tcp_conn_->isExpired(); +} + +network::SockAddr WsClient::Impl::peerAddr() const +{ + if (sp_tcp_conn_ != nullptr) + return sp_tcp_conn_->peerAddr(); + return server_addr_; +} + +void WsClient::Impl::setContext(void *context, ContextDeleter &&deleter) +{ + if (sp_tcp_conn_ != nullptr) + sp_tcp_conn_->setContext(context, std::move(deleter)); +} + +void* WsClient::Impl::getContext() const +{ + if (sp_tcp_conn_ != nullptr) + return sp_tcp_conn_->getContext(); + return nullptr; +} + +//! Ping 定时器触发:发送 Ping,启动 Pong 超时检测 +void WsClient::Impl::onPingTimerFired() +{ + if (is_closing_ || sp_tcp_conn_ == nullptr || state_ != WsClient::State::kConnected) + return; + + //! 发送 Ping 帧 + ping(""); + + //! 如果有超时检测,标记等待 Pong 并启动超时定时器 + if (ping_timeout_ > 0 && sp_pong_timer_ != nullptr) { + is_pong_pending_ = true; + sp_pong_timer_->enable(); + } +} + +//! Pong 超时触发:未收到 Pong 回复,判定连接已断开 +void WsClient::Impl::onPongTimeoutFired() +{ + if (is_closing_) + return; + + LogNotice("ws client pong timeout, closing connection"); + is_pong_pending_ = false; + close(1006, "pong timeout"); +} + +} +} +} diff --git a/modules/websocket/client/ws_client_impl.h b/modules/websocket/client/ws_client_impl.h new file mode 100644 index 00000000..526579a4 --- /dev/null +++ b/modules/websocket/client/ws_client_impl.h @@ -0,0 +1,199 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_WS_CLIENT_IMPL_H_20260615 +#define TBOX_WS_CLIENT_IMPL_H_20260615 + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ws_client.h" +#include "../ws_frame.h" +#include "../ws_frame_parser.h" +#include "../ws_compressor.h" + +namespace tbox { +namespace websocket { +namespace client { + +//! WsClient::Impl 实现完整的 WebSocket 客户端 +//! 流程:TcpConnector 建立 TCP → 发送 HTTP Upgrade → 验证 101 → 帧通信 +//! 发送大数据时先压缩再分片发送,避免单帧过大 +class WsClient::Impl { + public: + //! 默认分片发送的最大帧 payload 大小(64KB) + //! 选择 65535 是因为:不超过 16-bit payload length 编码范围,避免 64-bit 编码开销 + static constexpr size_t kDefaultFragmentSize = 65535; + Impl(WsClient *wp_parent, event::Loop *wp_loop); + ~Impl(); + + public: + bool initialize(const network::SockAddr &server_addr, const std::string &url_path); + bool start(); + void stop(); + void cleanup(); + + WsClient::State state() const { return state_; } + + public: + void setConnectedCallback(const WsClient::ConnectedCallback &cb) { connected_cb_ = cb; } + void setDisconnectedCallback(const WsClient::DisconnectedCallback &cb) { disconnected_cb_ = cb; } + void setTextMessageCallback(const WsClient::TextMessageCallback &cb) { text_message_cb_ = cb; } + void setBinaryMessageCallback(const WsClient::BinaryMessageCallback &cb) { binary_message_cb_ = cb; } + void setErrorCallback(const WsClient::ErrorCallback &cb) { error_cb_ = cb; } + void setAutoReconnect(bool enable) { reconnect_enabled_ = enable; } + void setReconnectDelayCalcFunc(const WsClient::ReconnectDelayCalc &func); + void setCompressionPrefer(bool enable) { prefer_compression_ = enable; } + void setFragmentSize(size_t size) { fragment_size_ = size; } + void setPingInterval(int seconds) { ping_interval_ = seconds; } + void setPingTimeout(int seconds) { ping_timeout_ = seconds; } + void setTlsConfig(const network::TlsConfig &config); + + public: + bool send(const std::string &text); + bool send(const char *str); + bool send(const void *data, size_t len); + bool send(const std::vector &data); + bool close(uint16_t code, const std::string &reason); + bool ping(const std::string &data); + bool pong(const std::string &data); + bool isExpired() const; + network::SockAddr peerAddr() const; + + using ContextDeleter = network::TcpConnection::ContextDeleter; + void setContext(void *context, ContextDeleter &&deleter = nullptr); + void* getContext() const; + + private: + //! TCP 连接建立成功 + void onTcpConnected(network::TcpConnection *tcp_conn); + + //! TCP 连接断开 + void onTcpDisconnected(); + + //! TCP 收到数据(握手阶段与帧通信阶段共用) + void onTcpReceived(network::Buffer &buff); + + //! === 握手阶段 === + + //! 构造并发送 HTTP Upgrade 握手请求 + void sendHandshakeRequest(); + + //! 解析服务器握手响应,验证 101 + Sec-WebSocket-Accept + bool parseHandshakeResponse(network::Buffer &buff); + + //! 握手成功,进入帧通信模式 + void onHandshakeSuccess(); + + //! 握手失败 + void onHandshakeFail(); + + //! === 帧通信阶段 === + + //! 解析 WebSocket 帧 + void onWsFrameReceived(network::Buffer &buff); + + //! 发送 WebSocket 帧(客户端,掩码) + bool sendMaskedFrame(WsFrame::OpCode opcode, bool fin, const void *payload, size_t payload_len); + + //! 分片发送 payload(客户端,掩码,内部使用) + //! opcode: 首帧 opcode(kText 或 kBinary) + //! payload/payload_len: 完整的 payload 数据(可能为压缩后数据) + //! is_compressed: 是否为压缩数据(首帧设置 rsv1=true) + bool sendFragmented(WsFrame::OpCode opcode, const void *payload, size_t payload_len, bool is_compressed); + + //! 统一发送数据(内部使用) + //! opcode: kText 或 kBinary + //! data_ptr/data_len: 原始数据指针与长度 + //! 流程:前置检查 → 压缩(如需要) → sendFragmented + bool sendData(WsFrame::OpCode opcode, const void *data_ptr, size_t data_len); + + //! 将完整消息交付给业务层(opcode 为 kText 或 kBinary) + void deliverMessage(WsFrame::OpCode opcode, std::string &data); + + //! 出错处理 + void onError(); + + //! Ping/Pong 心跳定时器回调 + void onPingTimerFired(); + void onPongTimeoutFired(); + + private: + WsClient *wp_parent_; + event::Loop *wp_loop_; + + network::TcpFactory *sp_factory_ = nullptr; + network::TcpConnector *sp_connector_ = nullptr; + network::TcpConnection *sp_tcp_conn_ = nullptr; + + network::SockAddr server_addr_; + std::string url_path_; + + //! 握手阶段暂存:Sec-WebSocket-Key(用于验证 Accept) + std::string sec_ws_key_; + + //! 帧解析器 + WsFrameParser frame_parser_; + + //! 压缩相关 + bool prefer_compression_ = false; + WsCompressionConfig compression_config_; //! 握手成功后确认的压缩配置 + WsCompressor compressor_; + + //! 分片发送的最大帧 payload 大小(可配置,默认 kDefaultFragmentSize) + size_t fragment_size_ = WsClient::kDefaultFragmentSize; + + //! Ping/Pong 心跳参数 + int ping_interval_ = 0; + int ping_timeout_ = 0; + event::TimerEvent *sp_ping_timer_ = nullptr; + event::TimerEvent *sp_pong_timer_ = nullptr; + bool is_pong_pending_ = false; + + WsClient::State state_ = WsClient::State::kNone; + + WsClient::ConnectedCallback connected_cb_; + WsClient::DisconnectedCallback disconnected_cb_; + WsClient::TextMessageCallback text_message_cb_; + WsClient::BinaryMessageCallback binary_message_cb_; + WsClient::ErrorCallback error_cb_; + + bool is_closing_ = false; + bool reconnect_enabled_ = true; + int cb_level_ = 0; + + //! 分片组装相关 + //! 只有接收完整数据帧(fin=true)并进行解压后,才回调业务层 + bool is_fragmenting_ = false; //!< 是否正在接收分片消息 + WsFrame::OpCode fragment_opcode_; //!< 分片消息的原始 opcode(kText 或 kBinary) + bool fragment_need_decompress_ = false; //!< 分片消息是否需要解压 + std::string fragment_buffer_; //!< 分片数据的缓存区 +}; + +} +} +} + +#endif //TBOX_WS_CLIENT_IMPL_H_20260615 diff --git a/modules/websocket/server/ws_connection.cpp b/modules/websocket/server/ws_connection.cpp new file mode 100644 index 00000000..eb8f99e3 --- /dev/null +++ b/modules/websocket/server/ws_connection.cpp @@ -0,0 +1,627 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "ws_connection.h" + +#include +#include +#include + +#include "../ws_frame_parser.h" +#include "../ws_frame_builder.h" + +namespace tbox { +namespace websocket { +namespace server { + +//! ODR-used static constexpr 成员须在类外定义(C++11) +constexpr size_t WsConnection::kDefaultFragmentSize; + +using namespace std::placeholders; + +WsConnection::WsConnection(event::Loop *wp_loop, + network::TcpConnection *tcp_conn, + const std::string &url, + const WsCompressionConfig &compress_config, + size_t fragment_size, + int ping_interval, + int ping_timeout) + : wp_loop_(wp_loop) + , sp_tcp_conn_(tcp_conn) + , url_(url) + , compression_config_(compress_config) + , fragment_size_(fragment_size) + , ping_interval_(ping_interval) + , ping_timeout_(ping_timeout) +{ + TBOX_ASSERT(wp_loop != nullptr); + TBOX_ASSERT(tcp_conn != nullptr); + + //! 初始化压缩器 + if (compression_config_.enabled) { + if (!compressor_.initialize(compression_config_)) { + LogErr("WsConnection compressor init fail"); + } + } + + //! 设置 TcpConnection 的回调 + sp_tcp_conn_->setReceiveCallback(std::bind(&WsConnection::onTcpReceived, this, _1), 0); + sp_tcp_conn_->setDisconnectedCallback(std::bind(&WsConnection::onTcpDisconnected, this)); + sp_tcp_conn_->setSendCompleteCallback(std::bind(&WsConnection::onTcpSendCompleted, this)); + + //! 初始化 Ping/Pong 心跳定时器 + if (ping_interval_ > 0) { + sp_ping_timer_ = wp_loop_->newTimerEvent(); + sp_ping_timer_->initialize(std::chrono::seconds(ping_interval_), event::Event::Mode::kPersist); + sp_ping_timer_->setCallback(std::bind(&WsConnection::onPingTimerFired, this)); + sp_ping_timer_->enable(); + + if (ping_timeout_ > 0) { + sp_pong_timer_ = wp_loop_->newTimerEvent(); + sp_pong_timer_->initialize(std::chrono::seconds(ping_timeout_), event::Event::Mode::kOneshot); + sp_pong_timer_->setCallback(std::bind(&WsConnection::onPongTimeoutFired, this)); + } + } +} + +WsConnection::~WsConnection() +{ + TBOX_ASSERT(cb_level_ == 0); + + //! 清理心跳定时器 + CHECK_DELETE_RESET_OBJ(sp_ping_timer_); + CHECK_DELETE_RESET_OBJ(sp_pong_timer_); + + if (sp_tcp_conn_ == nullptr) + return; + + //! 先取消 TcpConnection 的回调,防止断开时回调到已销毁的 WsConnection + sp_tcp_conn_->setReceiveCallback(nullptr, 0); + sp_tcp_conn_->setDisconnectedCallback(nullptr); + sp_tcp_conn_->setSendCompleteCallback(nullptr); + + sp_tcp_conn_->disconnect(); + auto tcp_conn = sp_tcp_conn_; + sp_tcp_conn_ = nullptr; + wp_loop_->runNext([tcp_conn] { CHECK_DELETE_OBJ(tcp_conn); }, + "WsConnection::~WsConnection, delete tcp_conn"); +} + +bool WsConnection::send(const std::string &text) +{ + return sendData(WsFrame::OpCode::kText, text.data(), text.size()); +} + +bool WsConnection::send(const char *str) +{ + return sendData(WsFrame::OpCode::kText, str, strlen(str)); +} + +bool WsConnection::send(const void *data, size_t len) +{ + return sendData(WsFrame::OpCode::kBinary, data, len); +} + +bool WsConnection::send(const std::vector &data) +{ + return sendData(WsFrame::OpCode::kBinary, data.data(), data.size()); +} + +bool WsConnection::close(uint16_t code, const std::string &reason) +{ + if (sp_tcp_conn_ == nullptr) + return false; + + is_closing_ = true; + + auto frame = WsFrameBuilder::BuildCloseFrame(code, reason); + sp_tcp_conn_->send(frame.data(), frame.size()); + + //! 延后断开,确保 Close 帧已发送 + wp_loop_->runNext([this] { + if (sp_tcp_conn_ != nullptr) + sp_tcp_conn_->disconnect(); + }, "WsConnection::close, disconnect"); + + return true; +} + +bool WsConnection::ping(const std::string &data) +{ + if (is_closing_ || sp_tcp_conn_ == nullptr) + return false; + + auto frame = WsFrameBuilder::BuildPingFrame(data); + return sp_tcp_conn_->send(frame.data(), frame.size()); +} + +bool WsConnection::pong(const std::string &data) +{ + if (is_closing_ || sp_tcp_conn_ == nullptr) + return false; + + auto frame = WsFrameBuilder::BuildPongFrame(data); + return sp_tcp_conn_->send(frame.data(), frame.size()); +} + +network::SockAddr WsConnection::peerAddr() const +{ + if (sp_tcp_conn_ != nullptr) + return sp_tcp_conn_->peerAddr(); + return network::SockAddr(); +} + +std::string WsConnection::getUrl() const +{ + return url_; +} + +bool WsConnection::isExpired() const +{ + return sp_tcp_conn_ == nullptr || sp_tcp_conn_->isExpired(); +} + +void WsConnection::setContext(void *context, ContextDeleter &&deleter) +{ + if (sp_tcp_conn_ != nullptr) + sp_tcp_conn_->setContext(context, std::move(deleter)); +} + +void* WsConnection::getContext() const +{ + if (sp_tcp_conn_ != nullptr) + return sp_tcp_conn_->getContext(); + return nullptr; +} + +bool WsConnection::sendFrame(WsFrame::OpCode opcode, bool fin, + const void *payload, size_t payload_len) +{ + if (sp_tcp_conn_ == nullptr) + return false; + + auto frame = WsFrameBuilder::BuildFrame(opcode, fin, payload, payload_len); + return sp_tcp_conn_->send(frame.data(), frame.size()); +} + +//! 统一发送数据:前置检查 → 压缩(如需要) → sendFragmented +//! opcode 为 kText 或 kBinary +bool WsConnection::sendData(WsFrame::OpCode opcode, const void *data_ptr, size_t data_len) +{ + if (is_closing_ || sp_tcp_conn_ == nullptr) + return false; + + //! 压缩协商达成时,压缩数据 + if (compression_config_.enabled && compressor_.isInitialized()) { + std::string compressed = compressor_.compress(data_ptr, data_len); + if (!compressed.empty()) { + return sendFragmented(opcode, compressed.data(), compressed.size(), true); + } + //! 压缩失败,回退到不压缩 + LogNotice("ws compress fail, fallback to uncompressed"); + } + + return sendFragmented(opcode, data_ptr, data_len, false); +} + +//! 分片发送 payload +//! 若 payload 大小超过 fragment_size_,则分片发送: +//! - 首帧:原始 opcode,fin=false,rsv1=is_compressed +//! - 中间帧:kContinue,fin=false +//! - 末帧:kContinue,fin=true +//! 若 payload 大小不超过 fragment_size_,则单帧发送 +bool WsConnection::sendFragmented(WsFrame::OpCode opcode, const void *payload, size_t payload_len, bool is_compressed) +{ + if (sp_tcp_conn_ == nullptr) + return false; + + //! 单帧即可发送(fragment_size_ 为 0 时表示不分片) + if (fragment_size_ == 0 || payload_len <= fragment_size_) { + auto frame = WsFrameBuilder::BuildFrame(opcode, true, payload, payload_len, is_compressed); + return sp_tcp_conn_->send(frame.data(), frame.size()); + } + + //! 分片发送 + const uint8_t *data = static_cast(payload); + size_t offset = 0; + + //! 首帧:原始 opcode,fin=false,rsv1=is_compressed + size_t first_chunk = fragment_size_; + auto frame = WsFrameBuilder::BuildFrame(opcode, false, data, first_chunk, is_compressed); + if (!sp_tcp_conn_->send(frame.data(), frame.size())) + return false; + + offset += first_chunk; + + //! 中间帧与末帧:opcode=kContinue,rsv1=false + while (offset < payload_len) { + size_t remaining = payload_len - offset; + size_t chunk_size = std::min(remaining, fragment_size_); + bool is_last = (offset + chunk_size == payload_len); + + auto cont_frame = WsFrameBuilder::BuildFrame(WsFrame::OpCode::kContinue, is_last, + data + offset, chunk_size, false); + if (!sp_tcp_conn_->send(cont_frame.data(), cont_frame.size())) + return false; + + offset += chunk_size; + } + + return true; +} + +//! 将完整数据交付给业务层 +//! data 为解压后的完整数据(若不需要解压则为原始 payload) +//! opcode 为消息类型(kText 或 kBinary) +void WsConnection::deliverMessage(WsFrame::OpCode opcode, std::string &data) +{ + if (opcode == WsFrame::OpCode::kText) { + if (text_message_cb_) { + ++cb_level_; + text_message_cb_(std::move(data)); + --cb_level_; + } + } else if (opcode == WsFrame::OpCode::kBinary) { + //! 将 std::string 转换为 std::vector + std::vector vec(data.begin(), data.end()); + if (binary_message_cb_) { + ++cb_level_; + binary_message_cb_(std::move(vec)); + --cb_level_; + } + } +} + +void WsConnection::onTcpReceived(network::Buffer &buff) +{ + //! 从缓冲区中逐步解析 WebSocket 帧 + while (buff.readableSize() > 0) { + size_t consumed = frame_parser_.parse(buff.readableBegin(), buff.readableSize()); +#if 1 + auto hex_str = util::string::RawDataToHexStr(buff.readableBegin(), buff.readableSize()); + LogTrace("hex: %s, consumed:%u", hex_str.c_str(), consumed); +#endif + buff.hasRead(consumed); + + if (frame_parser_.state() == WsFrameParser::State::kFinished) { + WsFrame *frame = frame_parser_.getFrame(); + if (frame != nullptr) { + //! ===== 控制帧处理(Close/Ping/Pong 不受分片状态影响) ===== + if (frame->isControlFrame()) { + switch (frame->opcode) { + case WsFrame::OpCode::kClose: + //! 收到关闭帧,自动回复关闭帧 + if (!is_closing_) { + auto close_frame = WsFrameBuilder::BuildCloseFrame(frame->closeCode(), frame->closeReason()); + sp_tcp_conn_->send(close_frame.data(), close_frame.size()); + is_closing_ = true; + } + //! 不再处理后续数据 + buff.hasReadAll(); + //! 清理分片缓存 + fragment_buffer_.clear(); + is_fragmenting_ = false; + delete frame; + if (close_cb_) { + ++cb_level_; + close_cb_(); + --cb_level_; + } + return; + + case WsFrame::OpCode::kPing: + //! 自动回复 Pong + pong(frame->payload); + if (ping_cb_) { + ++cb_level_; + ping_cb_(frame->payload); + --cb_level_; + } + break; + + case WsFrame::OpCode::kPong: + //! 心跳:收到 Pong,取消超时定时器 + if (is_pong_pending_) { + is_pong_pending_ = false; + if (sp_pong_timer_ != nullptr) + sp_pong_timer_->disable(); + } + if (pong_cb_) { + ++cb_level_; + pong_cb_(frame->payload); + --cb_level_; + } + break; + + default: + LogNotice("unknown ws control opcode: 0x%02x", static_cast(frame->opcode)); + delete frame; + buff.hasReadAll(); + fragment_buffer_.clear(); + is_fragmenting_ = false; + if (error_cb_) { + ++cb_level_; + error_cb_(); + --cb_level_; + } + return; + } + delete frame; + continue; //! 控制帧处理完毕,继续解析下一个帧 + } + + //! ===== 数据帧处理(TEXT / BINARY / CONTINUE) ===== + //! 核心逻辑:分片数据先缓存,接收完整后统一解压再回调 + //! 原因:压缩数据不能逐片解压,必须拼接完整后才能解压 + + if (frame->opcode == WsFrame::OpCode::kText || + frame->opcode == WsFrame::OpCode::kBinary) { + //! 新消息的首帧 + if (is_fragmenting_) { + //! 正在接收分片消息时又收到新消息首帧,协议违规 + LogNotice("ws protocol error: new data frame while fragmenting"); + delete frame; + buff.hasReadAll(); + fragment_buffer_.clear(); + is_fragmenting_ = false; + if (error_cb_) { + ++cb_level_; + error_cb_(); + --cb_level_; + } + return; + } + + if (frame->fin) { + //! 单帧完整消息(无分片) + bool is_need_decompress = frame->rsv1 && compression_config_.enabled; + if (is_need_decompress) { + std::string decompressed = compressor_.decompress(frame->payload); + if (!decompressed.empty()) { + frame->payload = std::move(decompressed); + } else { + //! 解压失败 + LogNotice("ws decompress fail"); + delete frame; + buff.hasReadAll(); + if (error_cb_) { + ++cb_level_; + error_cb_(); + --cb_level_; + } + return; + } + } + + //! 交付完整消息给业务层 + deliverMessage(frame->opcode, frame->payload); + delete frame; + + } else { + //! 分片消息的首帧(fin=false) + //! 记录原始 opcode 和是否需要解压,缓存 payload + is_fragmenting_ = true; + fragment_opcode_ = frame->opcode; + fragment_need_decompress_ = frame->rsv1 && compression_config_.enabled; + fragment_buffer_ = std::move(frame->payload); + delete frame; + } + + } else if (frame->opcode == WsFrame::OpCode::kContinue) { + //! 分片消息的后续帧 + if (!is_fragmenting_) { + //! 没有首帧却收到续帧,协议违规 + LogNotice("ws protocol error: continue frame without fragment start"); + delete frame; + buff.hasReadAll(); + if (error_cb_) { + ++cb_level_; + error_cb_(); + --cb_level_; + } + return; + } + + //! 将本片 payload 追加到缓存区 + fragment_buffer_.append(frame->payload); + + if (frame->fin) { + //! 最后一帧(fin=true),消息完整 + //! 对完整数据统一解压,然后回调业务层 + if (fragment_need_decompress_) { + std::string decompressed = compressor_.decompress(fragment_buffer_); + if (!decompressed.empty()) { + fragment_buffer_ = std::move(decompressed); + } else { + //! 解压失败 + LogNotice("ws decompress fail"); + delete frame; + buff.hasReadAll(); + fragment_buffer_.clear(); + is_fragmenting_ = false; + if (error_cb_) { + ++cb_level_; + error_cb_(); + --cb_level_; + } + return; + } + } + + //! 交付完整消息给业务层 + deliverMessage(fragment_opcode_, fragment_buffer_); + + //! 重置分片状态 + fragment_buffer_.clear(); + is_fragmenting_ = false; + } + //! fin=false: 继续缓存,不回调 + + delete frame; + + } else { + //! 未知数据帧 opcode + LogNotice("unknown ws opcode: 0x%02x", static_cast(frame->opcode)); + delete frame; + buff.hasReadAll(); + fragment_buffer_.clear(); + is_fragmenting_ = false; + if (error_cb_) { + ++cb_level_; + error_cb_(); + --cb_level_; + } + return; + } + } + } else if (frame_parser_.state() == WsFrameParser::State::kError) { + LogNotice("ws frame parse error"); + buff.hasReadAll(); + fragment_buffer_.clear(); + is_fragmenting_ = false; + if (error_cb_) { + ++cb_level_; + error_cb_(); + --cb_level_; + } + return; + } else { + //! 需要更多数据 + break; + } + } +} + +void WsConnection::onTcpDisconnected() +{ + LogInfo("ws disconnected"); + + //! 清理分片缓存 + fragment_buffer_.clear(); + is_fragmenting_ = false; + + //! 禁用心跳定时器 + if (sp_ping_timer_ != nullptr) + sp_ping_timer_->disable(); + if (sp_pong_timer_ != nullptr) + sp_pong_timer_->disable(); + is_pong_pending_ = false; + + if (close_cb_) { + ++cb_level_; + close_cb_(); + --cb_level_; + } + + //! 清理 TcpConnection:先断开再延后删除 + //! 断空指针,防止析构函数重复操作已删除的对象 + //! 必须要 close_cb_() 之后才能清理,否则回调中 getContext() 拿到是空的 + auto tcp_conn = sp_tcp_conn_; + sp_tcp_conn_ = nullptr; + wp_loop_->runNext([tcp_conn] { CHECK_DELETE_OBJ(tcp_conn); }, + "WsConnection::onTcpDisconnected, delete tcp_conn"); +} + +void WsConnection::onTcpSendCompleted() +{ + if (send_complete_cb_) { + ++cb_level_; + send_complete_cb_(); + --cb_level_; + } +} + +void WsConnection::setPingInterval(int seconds) +{ + ping_interval_ = seconds; + + if (sp_ping_timer_ != nullptr) { + if (seconds > 0) { + sp_ping_timer_->initialize(std::chrono::seconds(seconds), event::Event::Mode::kPersist); + sp_ping_timer_->enable(); + } else { + sp_ping_timer_->disable(); + is_pong_pending_ = false; + if (sp_pong_timer_ != nullptr) + sp_pong_timer_->disable(); + } + } else if (seconds > 0) { + //! 之前没有创建过定时器,现在需要创建 + sp_ping_timer_ = wp_loop_->newTimerEvent(); + sp_ping_timer_->initialize(std::chrono::seconds(seconds), event::Event::Mode::kPersist); + sp_ping_timer_->setCallback(std::bind(&WsConnection::onPingTimerFired, this)); + sp_ping_timer_->enable(); + + if (ping_timeout_ > 0) { + sp_pong_timer_ = wp_loop_->newTimerEvent(); + sp_pong_timer_->initialize(std::chrono::seconds(ping_timeout_), event::Event::Mode::kOneshot); + sp_pong_timer_->setCallback(std::bind(&WsConnection::onPongTimeoutFired, this)); + } + } +} + +void WsConnection::setPingTimeout(int seconds) +{ + ping_timeout_ = seconds; + + if (sp_pong_timer_ != nullptr) { + if (seconds > 0) { + sp_pong_timer_->initialize(std::chrono::seconds(seconds), event::Event::Mode::kOneshot); + } else { + sp_pong_timer_->disable(); + CHECK_DELETE_RESET_OBJ(sp_pong_timer_); + } + } else if (seconds > 0 && sp_ping_timer_ != nullptr) { + //! ping 已启用但 pong_timer 未创建,现在创建 + sp_pong_timer_ = wp_loop_->newTimerEvent(); + sp_pong_timer_->initialize(std::chrono::seconds(seconds), event::Event::Mode::kOneshot); + sp_pong_timer_->setCallback(std::bind(&WsConnection::onPongTimeoutFired, this)); + } +} + +//! Ping 定时器触发:发送 Ping,启动 Pong 超时检测 +void WsConnection::onPingTimerFired() +{ + if (is_closing_ || sp_tcp_conn_ == nullptr) + return; + + //! 发送 Ping 帧 + ping(); + + //! 如果有超时检测,标记等待 Pong 并启动超时定时器 + if (ping_timeout_ > 0 && sp_pong_timer_ != nullptr) { + is_pong_pending_ = true; + sp_pong_timer_->enable(); + } +} + +//! Pong 超时触发:未收到 Pong 回复,判定连接已断开 +void WsConnection::onPongTimeoutFired() +{ + if (is_closing_) + return; + + LogNotice("ws pong timeout, closing connection"); + is_pong_pending_ = false; + close(); +} + +} +} +} diff --git a/modules/websocket/server/ws_connection.h b/modules/websocket/server/ws_connection.h new file mode 100644 index 00000000..dce51497 --- /dev/null +++ b/modules/websocket/server/ws_connection.h @@ -0,0 +1,194 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_WS_CONNECTION_H_20260612 +#define TBOX_WS_CONNECTION_H_20260612 + +#include +#include + +#include +#include +#include + +#include "../ws_frame.h" +#include "../ws_frame_parser.h" +#include "../ws_compressor.h" + +namespace tbox { +namespace websocket { +namespace server { + +//! WebSocket 连接 +//! 包装从 HTTP 升级后分离出来的 TcpConnection,解析/构建 WebSocket 帧 +//! 生命期由 WsServer 通过 Cabinet 管理,用户通过 ConnToken 访问 +//! 支持分片消息的完整接收:缓存分片数据,接收完整后再解压并回调 +//! 发送大数据时先压缩再分片发送,避免单帧过大 +class WsConnection { + public: + //! 默认分片发送的最大帧 payload 大小(64KB) + //! 选择 65535 是因为:不超过 16-bit payload length 编码范围,避免 64-bit 编码开销 + static constexpr size_t kDefaultFragmentSize = 65535; + + //! 内部回调:WsServer::Impl 绑定 ConnToken,不传递 WsConnection* + using CloseCallback = std::function; + using TextMessageCallback = std::function; + using BinaryMessageCallback = std::function &&)>; + using ErrorCallback = std::function; + using PingCallback = std::function; + using PongCallback = std::function; + using SendCompleteCallback = std::function; + + ~WsConnection(); + + NONCOPYABLE(WsConnection); + IMMOVABLE(WsConnection); + + public: + //! 设置回调(由 WsServer::Impl 调用,绑定 ConnToken) + void setCloseCallback(const CloseCallback &cb) { close_cb_ = cb; } + void setTextMessageCallback(const TextMessageCallback &cb) { text_message_cb_ = cb; } + void setBinaryMessageCallback(const BinaryMessageCallback &cb) { binary_message_cb_ = cb; } + void setErrorCallback(const ErrorCallback &cb) { error_cb_ = cb; } + void setPingCallback(const PingCallback &cb) { ping_cb_ = cb; } + void setPongCallback(const PongCallback &cb) { pong_cb_ = cb; } + void setSendCompleteCallback(const SendCompleteCallback &cb) { send_complete_cb_ = cb; } + + public: + //! 发送文本帧 + bool send(const std::string &text); + //! 发送文本帧(const char* 版本,方便直接传字符串字面量) + bool send(const char *str); + //! 发送二进制帧 + bool send(const void *data, size_t len); + bool send(const std::vector &data); + + //! 发送关闭帧并关闭连接 + bool close(uint16_t code = 1000, const std::string &reason = ""); + + //! 发送 Ping 帧 + bool ping(const std::string &data = ""); + //! 发送 Pong 帧 + bool pong(const std::string &data = ""); + + //! 获取客户端地址 + network::SockAddr peerAddr() const; + + //! 获取客户端连接的 URL 路径 + std::string getUrl() const; + + //! 连接是否已失效 + bool isExpired() const; + + //! 设置/获取上下文数据(直接委托给底层 TcpConnection) + using ContextDeleter = network::TcpConnection::ContextDeleter; + void setContext(void *context, ContextDeleter &&deleter = nullptr); + void* getContext() const; + + //! 设置/获取分片大小(仅影响发送,接收时自动组装) + void setFragmentSize(size_t size) { fragment_size_ = size; } + size_t fragmentSize() const { return fragment_size_; } + + //! 设置 Ping/Pong 心跳参数 + void setPingInterval(int seconds); + void setPingTimeout(int seconds); + + private: + //! 仅由 WsServer 创建(生命期由 Cabinet 管理) + //! compress_config 为握手时协商的压缩配置 + //! fragment_size 为分片发送的最大帧 payload 大小 + //! ping_interval/ping_timeout 为心跳参数(0=不自动 Ping/不检测超时) + WsConnection(event::Loop *wp_loop, network::TcpConnection *tcp_conn, const std::string &url, + const WsCompressionConfig &compress_config, size_t fragment_size, + int ping_interval, int ping_timeout); + + void onTcpReceived(network::Buffer &buff); + void onTcpDisconnected(); + void onTcpSendCompleted(); + + //! 发送 WebSocket 帧(内部使用) + bool sendFrame(WsFrame::OpCode opcode, bool fin, const void *payload, size_t payload_len); + + //! 分片发送 payload(内部使用) + //! opcode: 首帧 opcode(kText 或 kBinary) + //! payload/payload_len: 完整的 payload 数据(可能为压缩后数据) + //! is_compressed: 是否为压缩数据(首帧设置 rsv1=true) + bool sendFragmented(WsFrame::OpCode opcode, const void *payload, size_t payload_len, bool is_compressed); + + //! 统一发送数据(内部使用) + //! opcode: kText 或 kBinary + //! data_ptr/data_len: 原始数据指针与长度 + //! 流程:前置检查 → 压缩(如需要) → sendFragmented + bool sendData(WsFrame::OpCode opcode, const void *data_ptr, size_t data_len); + + //! 将完整消息交付给业务层(opcode 为 kText 或 kBinary) + void deliverMessage(WsFrame::OpCode opcode, std::string &data); + + //! Ping/Pong 心跳定时器回调 + void onPingTimerFired(); + void onPongTimeoutFired(); + + private: + event::Loop *wp_loop_; + network::TcpConnection *sp_tcp_conn_; + std::string url_; + + WsFrameParser frame_parser_; + + //! 压缩相关 + WsCompressionConfig compression_config_; + WsCompressor compressor_; + + //! 分片发送的最大帧 payload 大小(可配置,默认 kDefaultFragmentSize) + size_t fragment_size_ = kDefaultFragmentSize; + + CloseCallback close_cb_; + TextMessageCallback text_message_cb_; + BinaryMessageCallback binary_message_cb_; + ErrorCallback error_cb_; + PingCallback ping_cb_; + PongCallback pong_cb_; + SendCompleteCallback send_complete_cb_; + + bool is_closing_ = false; + + //! Ping/Pong 心跳相关 + int ping_interval_ = 0; //! Ping 发送间隔(秒,0=不自动 Ping) + int ping_timeout_ = 0; //! Pong 超时时间(秒,0=不检测超时) + event::TimerEvent *sp_ping_timer_ = nullptr; //! Ping 定时器(周期触发) + event::TimerEvent *sp_pong_timer_ = nullptr; //! Pong 超时定时器(单次触发) + bool is_pong_pending_ = false; //! 发送 Ping 后是否在等待 Pong 回复 + + //! 分片组装相关 + //! 只有接收完整数据帧(fin=true)并进行解压后,才回调业务层 + bool is_fragmenting_ = false; //!< 是否正在接收分片消息 + WsFrame::OpCode fragment_opcode_; //!< 分片消息的原始 opcode(kText 或 kBinary) + bool fragment_need_decompress_ = false; //!< 分片消息是否需要解压 + std::string fragment_buffer_; //!< 分片数据的缓存区 + + int cb_level_ = 0; + + friend class WsServer; +}; + +} +} +} + +#endif //TBOX_WS_CONNECTION_H_20260612 diff --git a/modules/websocket/server/ws_server.h b/modules/websocket/server/ws_server.h new file mode 100644 index 00000000..b121163d --- /dev/null +++ b/modules/websocket/server/ws_server.h @@ -0,0 +1,145 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_WS_SERVER_H_20260612 +#define TBOX_WS_SERVER_H_20260612 + +#include +#include + +#include +#include +#include +#include + +namespace tbox { +namespace http { +namespace server { +class Server; +} +} + +namespace websocket { +namespace server { + +//! WebSocket 服务器 +//! 基于 HTTP 服务器运行,本身即为 HTTP 中间件 +//! 支持指定 URL 路径(前缀匹配),实现多个 WebSocket 服务挂载于同一 HTTP 服务器 +//! 升级后接管 TcpConnection,提供 WebSocket 通信功能 +//! 通过 Cabinet 管理 WsConnection 生命期,用户通过 ConnToken 操作连接 +//! 分片消息接收完整后统一解压再回调,使用右值引用提升效率 +class WsServer { + public: + using ConnToken = cabinet::Token; + + //! 默认分片发送的最大帧 payload 大小 + static constexpr size_t kDefaultFragmentSize = 65535; + + explicit WsServer(event::Loop *wp_loop); + ~WsServer(); + + NONCOPYABLE(WsServer); + IMMOVABLE(WsServer); + + public: + //! 初始化:关联到 HTTP 服务器 + //! URL 路径匹配规则: + //! - url_path_ 以 '/' 结尾:前缀匹配,如 "/api/" 匹配 "/api/aa"、" /api/bb/cc" + //! - url_path_ 不以 '/' 结尾:全量匹配,如 "/api" 仅匹配 "/api" + //! - url_path_ 为空字符串:匹配所有 WebSocket 升级请求 + bool initialize(http::server::Server *http_server, const std::string &url_path = ""); + + //! 设置是否允许压缩(必须在 initialize 之前调用) + //! 启用后,若客户端请求 permessage-deflate,将在握手响应中同意压缩 + void setCompressionEnable(bool enable); + + //! 设置分片大小(仅影响发送,接收时自动组装;必须在 initialize 之前调用) + //! 默认为 kDefaultFragmentSize (65535) + //! 值为 0 表示不分片(所有数据单帧发送) + void setFragmentSize(size_t size); + + //! 设置 Ping 发送间隔(秒),0=不自动 Ping(默认;必须在 initialize 之前调用) + //! 启用后,每隔指定秒数向客户端发送 Ping 帧 + void setPingInterval(int seconds); + + //! 设置 Pong 超时时间(秒),0=不检测超时(默认;必须在 initialize 之前调用) + //! 发送 Ping 后若在此时间内未收到 Pong,则判定连接断开并关闭 + void setPingTimeout(int seconds); + + bool start(); + void stop(); + void cleanup(); + + enum class State { kNone, kInited, kRunning }; + State state() const; + + public: + //! 设置回调(所有回调均使用 ConnToken,不暴露 WsConnection 指针) + using ConnectedCallback = std::function; + using DisconnectedCallback = std::function; + using TextMessageCallback = std::function; + using BinaryMessageCallback = std::function &&)>; + using ErrorCallback = std::function; + + void setConnectedCallback(const ConnectedCallback &cb); + void setDisconnectedCallback(const DisconnectedCallback &cb); + void setTextMessageCallback(const TextMessageCallback &cb); + void setBinaryMessageCallback(const BinaryMessageCallback &cb); + void setErrorCallback(const ErrorCallback &cb); + + public: + //! 向指定客户端发送文本数据 + bool send(const ConnToken &client, const std::string &text); + //! 向指定客户端发送文本数据(const char* 版本,方便直接传字符串字面量) + bool send(const ConnToken &client, const char *str); + //! 向指定客户端发送二进制数据 + bool send(const ConnToken &client, const void *data, size_t len); + //! 向指定客户端发送二进制数据(vector 版本) + bool send(const ConnToken &client, const std::vector &data); + + //! 关闭指定客户端连接(发送 Close 帧) + bool close(const ConnToken &client, uint16_t code = 1000, const std::string &reason = ""); + + //! 发送 Ping 帧 + bool ping(const ConnToken &client, const std::string &data = ""); + //! 发送 Pong 帧 + bool pong(const ConnToken &client, const std::string &data = ""); + + //! 检查客户端连接是否有效 + bool isClientValid(const ConnToken &client) const; + //! 获取客户端地址(含 IP 与端口,toString() 可得 "ip:port" 格式) + network::SockAddr peerAddr(const ConnToken &client) const; + //! 获取客户端连接的 URL 路径 + std::string getUrl(const ConnToken &client) const; + + //! 设置/获取客户端连接的上下文数据 + using ContextDeleter = std::function; + void setContext(const ConnToken &client, void *context, ContextDeleter &&deleter = nullptr); + void* getContext(const ConnToken &client) const; + + class Impl; + private: + Impl *impl_; +}; + +} +} +} + +#endif //TBOX_WS_SERVER_H_20260612 diff --git a/modules/websocket/server/ws_server_impl.cpp b/modules/websocket/server/ws_server_impl.cpp new file mode 100644 index 00000000..71fd72c1 --- /dev/null +++ b/modules/websocket/server/ws_server_impl.cpp @@ -0,0 +1,737 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "ws_server.h" +#include "ws_server_impl.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#undef MODULE_ID +#define MODULE_ID "tbox.ws" + +namespace tbox { +namespace websocket { +namespace server { + +using namespace std::placeholders; + +WsServer::Impl::Impl(WsServer *wp_parent, event::Loop *wp_loop) + : wp_parent_(wp_parent) + , wp_loop_(wp_loop) +{ } + +WsServer::Impl::~Impl() +{ + TBOX_ASSERT(cb_level_ == 0); + cleanup(); +} + +bool WsServer::Impl::initialize(http::server::Server *http_server, const std::string &url_path) +{ + if (state_ != WsServer::State::kNone) + return false; + + //! 记录 URL 路径 + url_path_ = url_path; + + //! 记录 HTTP 服务器指针(不立即注册中间件,等 start() 时注册) + wp_http_server_ = http_server; + + state_ = WsServer::State::kInited; + return true; +} + +bool WsServer::Impl::start() +{ + if (state_ != WsServer::State::kInited) + return false; + + //! 注册自身到 HTTP 服务器(WsServer::Impl 即为 Middleware) + mw_token_ = wp_http_server_->use(this); + + state_ = WsServer::State::kRunning; + return true; +} + +void WsServer::Impl::stop() +{ + if (state_ != WsServer::State::kRunning) + return; + + //! 从 HTTP 服务器反注册中间件 + wp_http_server_->unuse(mw_token_); + mw_token_.reset(); + + //! 清除 WsConnection 内部回调,防止断开时回调到 Impl + ws_conns_.foreach([](WsConnection *conn) { + conn->setCloseCallback(nullptr); + conn->setTextMessageCallback(nullptr); + conn->setBinaryMessageCallback(nullptr); + conn->setErrorCallback(nullptr); + }); + + //! 删除所有 WsConnection(析构时会断开并延后删除 TcpConnection) + ws_conns_.foreach([](WsConnection *conn) { + CHECK_DELETE_OBJ(conn); + }); + ws_conns_.clear(); + + state_ = WsServer::State::kInited; +} + +void WsServer::Impl::cleanup() +{ + if (state_ == WsServer::State::kNone) + return; + + if (state_ == WsServer::State::kRunning) + stop(); + + wp_http_server_ = nullptr; + + state_ = WsServer::State::kNone; +} + +//! === permessage-deflate 扩展协商解析 === + +//! 客户端 Sec-WebSocket-Extensions 头部中 permessage-deflate 扩展的解析结果 +//! RFC 7692 Section 4.1: 扩展参数定义 +struct WsExtOfferParams { + bool found = false; //!< 是否找到 permessage-deflate 扩展 + bool server_no_context_takeover = false; //!< 服务器不保持压缩上下文 + bool client_no_context_takeover = false; //!< 客户端不保持压缩上下文 + bool server_max_window_bits_present = false; //!< 是否包含 server_max_window_bits + int server_max_window_bits = 15; //!< 服务器滑动窗口位数(默认15) + bool client_max_window_bits_present = false; //!< 是否包含 client_max_window_bits + int client_max_window_bits = 0; //!< 客户端滑动窗口位数,0=不带值(支持8~15) +}; + +//! 解析 Sec-WebSocket-Extensions 头部中的 permessage-deflate 扩展参数 +//! 格式示例: "permessage-deflate; client_max_window_bits; server_max_window_bits=15" +//! 多个扩展以逗号分隔: "permessage-deflate; client_max_window_bits, x-other-ext" +static WsExtOfferParams ParseWsExtOffer(const std::string &ext_header) +{ + WsExtOfferParams params; + + //! 找到 permessage-deflate 扩展的起始位置(需完整匹配,非子串) + static const std::string kExtName = "permessage-deflate"; + size_t pos = 0; + while (pos < ext_header.size()) { + size_t found_pos = ext_header.find(kExtName, pos); + if (found_pos == std::string::npos) + break; + + //! 前面应为逗号、空格或字符串开头;后面应为分号、逗号、空格或结尾 + bool valid_prefix = (found_pos == 0) || + (ext_header[found_pos - 1] == ',') || + (ext_header[found_pos - 1] == ' '); + size_t name_end = found_pos + kExtName.size(); + bool valid_suffix = (name_end >= ext_header.size()) || + (ext_header[name_end] == ';') || + (ext_header[name_end] == ',') || + (ext_header[name_end] == ' '); + if (valid_prefix && valid_suffix) { + pos = found_pos; + break; + } + pos = name_end; + } + + if (pos >= ext_header.size()) + return params; + + params.found = true; + + //! 确定参数区域:扩展名之后到下一个扩展(逗号)或字符串结尾 + size_t param_start = pos + kExtName.size(); + size_t comma_pos = ext_header.find(',', param_start); + size_t param_end = (comma_pos != std::string::npos) ? comma_pos : ext_header.size(); + + //! 在参数区域内逐个解析分号分隔的参数 + std::string section = ext_header.substr(param_start, param_end - param_start); + size_t search_pos = 0; + while (search_pos < section.size()) { + size_t semi_pos = section.find(';', search_pos); + if (semi_pos == std::string::npos) + break; + + //! 提取参数文本(跳过分号和空格) + size_t text_start = semi_pos + 1; + while (text_start < section.size() && section[text_start] == ' ') + text_start++; + + //! 找到参数结束位置(下一个分号或区域结尾) + size_t text_end = section.find(';', text_start); + if (text_end == std::string::npos) + text_end = section.size(); + + //! 去掉尾部空格 + while (text_end > text_start && section[text_end - 1] == ' ') + text_end--; + + std::string param_text = section.substr(text_start, text_end - text_start); + if (param_text.empty()) { + search_pos = text_end; + continue; + } + + //! 解析参数名=值 + size_t eq_pos = param_text.find('='); + std::string param_name = (eq_pos != std::string::npos) + ? param_text.substr(0, eq_pos) : param_text; + std::string param_value = (eq_pos != std::string::npos) + ? param_text.substr(eq_pos + 1) : ""; + + //! 去掉参数名尾部空格和参数值首尾空格 + while (!param_name.empty() && param_name.back() == ' ') + param_name.pop_back(); + while (!param_value.empty() && param_value.front() == ' ') + param_value.erase(param_value.begin()); + while (!param_value.empty() && param_value.back() == ' ') + param_value.pop_back(); + + //! 匹配已知参数(RFC 7692 Section 4.1) + if (param_name == "server_no_context_takeover") { + params.server_no_context_takeover = true; + } else if (param_name == "client_no_context_takeover") { + params.client_no_context_takeover = true; + } else if (param_name == "server_max_window_bits") { + params.server_max_window_bits_present = true; + if (!param_value.empty()) + params.server_max_window_bits = std::stoi(param_value); + } else if (param_name == "client_max_window_bits") { + params.client_max_window_bits_present = true; + if (!param_value.empty()) + params.client_max_window_bits = std::stoi(param_value); + else + params.client_max_window_bits = 0; //!< 不带值,表示客户端支持 8~15 + } + + search_pos = text_end; + } + + return params; +} + +//! === Middleware 接口实现 === + +void WsServer::Impl::handle(http::server::ContextSptr sp_ctx, const http::server::NextFunc &next) +{ + auto &req = sp_ctx->req(); + + if (IsWsUpgradeRequest(req)) { + //! URL 路径匹配规则: + //! - url_path_ 以 '/' 结尾:前缀匹配,如 "/api/" 匹配 "/api/aa"、" /api/bb/cc" + //! - url_path_ 不以 '/' 结尾:全量匹配,如 "/api" 仅匹配 "/api" + //! - url_path_ 为空字符串:匹配所有 WebSocket 升级请求 + if (!url_path_.empty()) { + bool matched = false; + if (url_path_.back() == '/') { + //! 前缀匹配 + matched = util::string::IsStartWith(req.url.path, url_path_); + } else { + //! 全量匹配 + matched = (req.url.path == url_path_); + } + if (!matched) { + //! 不是本服务关心的 URL,传递给下一个中间件 + next(); + return; + } + } + + LogDbg("ws upgrade request: %s", req.url.path.c_str()); + + auto &res = sp_ctx->res(); + + //! 设置 101 响应 + res.status_code = http::StatusCode::k101_SwitchingProtocols; + res.http_ver = http::HttpVer::k1_1; + + //! 从请求头中获取 Upgrade 和 Connection 信息 + auto upgrade_iter = req.headers.find("Upgrade"); + if (upgrade_iter != req.headers.end()) + res.headers["Upgrade"] = upgrade_iter->second; + else + res.headers["Upgrade"] = "websocket"; + + auto connection_iter = req.headers.find("Connection"); + if (connection_iter != req.headers.end()) + res.headers["Connection"] = connection_iter->second; + else + res.headers["Connection"] = "Upgrade"; + + //! 计算 Sec-WebSocket-Accept + auto key_iter = req.headers.find("Sec-WebSocket-Key"); + if (key_iter != req.headers.end()) + res.headers["Sec-WebSocket-Accept"] = ComputeWsAcceptKey(key_iter->second); + + //! RFC 7692:压缩扩展协商 + //! 若 server 允许压缩且客户端请求了 permessage-deflate,同意压缩 + bool compression_agreed = false; + if (compression_config_.enabled) { + auto ext_iter = req.headers.find("Sec-WebSocket-Extensions"); + if (ext_iter != req.headers.end()) { + //! 解析客户端的 permessage-deflate 扩展参数 + WsExtOfferParams offer_params = ParseWsExtOffer(ext_iter->second); + std::string resp_value; + if (offer_params.found) { + //! 构建响应参数: + //! 1) server_no_context_takeover: 服务器每条消息独立压缩,必须声明 + //! 2) client_no_context_takeover: 要求客户端每条消息独立压缩 + //! 3) client_max_window_bits: 若客户端 offered,RFC 7692 MUST 包含 + //! 否则 Chrome 等浏览器会关闭连接(RFC 7692 Section 4.3) + //! 4) server_max_window_bits: 若客户端 offered,可选包含(MAY) + resp_value = "permessage-deflate; server_no_context_takeover; client_no_context_takeover"; + + //! RFC 7692 Section 4.2.2: + //! "If a server received an extension offer containing the client_max_window_bits + //! parameter, the server MUST include the client_max_window_bits parameter + //! in the corresponding extension response." + if (offer_params.client_max_window_bits_present) { + //! 不带值(client_max_window_bits=0)表示客户端支持 8~15 + //! 带值时须 ≤ 客户端 offered 值 + //! 响应值同时须 ≤ 服务器 max_window_bits + int respond_bits = (offer_params.client_max_window_bits == 0) + ? compression_config_.max_window_bits + : std::min(offer_params.client_max_window_bits, compression_config_.max_window_bits); + if (respond_bits < 8) respond_bits = 8; + if (respond_bits > 15) respond_bits = 15; + resp_value += "; client_max_window_bits=" + std::to_string(respond_bits); + } + + //! RFC 7692 Section 4.2.2: + //! server_max_window_bits 为 MAY,非 MUST + //! 此处显式声明,方便客户端明确知道服务器使用的窗口位数 + if (offer_params.server_max_window_bits_present) { + //! 响应值须 ≤ 客户端 offered 值,同时须 ≤ 服务器 max_window_bits + int respond_bits = std::min(offer_params.server_max_window_bits, compression_config_.max_window_bits); + if (respond_bits < 8) respond_bits = 8; + if (respond_bits > 15) respond_bits = 15; + resp_value += "; server_max_window_bits=" + std::to_string(respond_bits); + } + + compression_agreed = true; + LogDbg("ws compression agreed: %s", resp_value.c_str()); + } + if (!resp_value.empty()) + res.headers["Sec-WebSocket-Extensions"] = resp_value; + } + } + + //! 注册升级回调:HTTP 服务器发送 101 响应后,将 TcpConnection 交给 WsServer + //! 同时传递压缩协商结果 + WsCompressionConfig conn_compress_config; + if (compression_agreed) { + conn_compress_config.enabled = true; + conn_compress_config.no_context_takeover = compression_config_.no_context_takeover; + conn_compress_config.max_window_bits = compression_config_.max_window_bits; + } + res.upgrade_cb = std::bind(&WsServer::Impl::onWsUpgrade, this, _1, req.url.path, conn_compress_config); + + //! 升级请求已处理,不再调用 next() + return; + } + + //! 非 WebSocket 升级请求,传递给下一个中间件 + next(); +} + +//! === 升级与连接管理 === + +void WsServer::Impl::onWsUpgrade(network::TcpConnection *tcp_conn, const std::string &url_path, + const WsCompressionConfig &compress_config) +{ + RECORD_SCOPE(); + LogDbg("ws upgrade: new connection from %s", tcp_conn->peerAddr().toString().c_str()); + + //! 创建 WsConnection,并存入 Cabinet(直接 alloc 并存入指针) + //! 传入升级时的 URL 路径、压缩配置、分片大小、心跳参数 + WsConnection *ws_conn = new WsConnection(wp_loop_, tcp_conn, url_path, compress_config, + fragment_size_, ping_interval_, ping_timeout_); + ConnToken ws_token = ws_conns_.alloc(ws_conn); + + //! 设置 WsConnection 的回调(bind 捕获 ConnToken,不传递 WsConnection*) + ws_conn->setCloseCallback(std::bind(&WsServer::Impl::onWsDisconnected, this, ws_token)); + ws_conn->setTextMessageCallback(std::bind(&WsServer::Impl::onWsTextMessage, this, ws_token, _1)); + ws_conn->setBinaryMessageCallback(std::bind(&WsServer::Impl::onWsBinaryMessage, this, ws_token, _1)); + ws_conn->setErrorCallback(std::bind(&WsServer::Impl::onWsError, this, ws_token)); + + //! 通知用户(传递 ConnToken) + if (connected_cb_) { + ++cb_level_; + connected_cb_(ws_token); + --cb_level_; + } +} + +void WsServer::Impl::onWsDisconnected(const ConnToken &client) +{ + RECORD_SCOPE(); + LogDbg("ws disconnected"); + + //! 先通知用户(此时 ConnToken 在 Cabinet 中仍有效) + //! 用户可通过 ConnToken 调用 WsServer 方法获取连接信息 + if (disconnected_cb_) { + ++cb_level_; + disconnected_cb_(client); + --cb_level_; + } + + //! 从 Cabinet 中移除并获取指针 + WsConnection *ws_conn = ws_conns_.free(client); + + //! 延后删除 WsConnection(确保回调中还能访问对象) + wp_loop_->runNext([ws_conn] { CHECK_DELETE_OBJ(ws_conn); }, + "WsServer::onWsDisconnected, delete ws_conn"); +} + +void WsServer::Impl::onWsTextMessage(const ConnToken &client, std::string &&data) +{ + if (text_message_cb_) { + ++cb_level_; + text_message_cb_(client, std::move(data)); + --cb_level_; + } +} + +void WsServer::Impl::onWsBinaryMessage(const ConnToken &client, std::vector &&data) +{ + if (binary_message_cb_) { + ++cb_level_; + binary_message_cb_(client, std::move(data)); + --cb_level_; + } +} + +void WsServer::Impl::onWsError(const ConnToken &client) +{ + if (error_cb_) { + ++cb_level_; + error_cb_(client); + --cb_level_; + } + + //! 出错后关闭连接 + auto ws_conn = ws_conns_.at(client); + if (ws_conn != nullptr) + ws_conn->close(); +} + +//! === 通过 ConnToken 操作连接 === + +bool WsServer::Impl::send(const ConnToken &client, const std::string &text) +{ + auto ws_conn = ws_conns_.at(client); + if (ws_conn != nullptr) + return ws_conn->send(text); + return false; +} + +bool WsServer::Impl::send(const ConnToken &client, const char *str) +{ + auto ws_conn = ws_conns_.at(client); + if (ws_conn != nullptr) + return ws_conn->send(str); + return false; +} + +bool WsServer::Impl::send(const ConnToken &client, const void *data, size_t len) +{ + auto ws_conn = ws_conns_.at(client); + if (ws_conn != nullptr) + return ws_conn->send(data, len); + return false; +} + +bool WsServer::Impl::send(const ConnToken &client, const std::vector &data) +{ + auto ws_conn = ws_conns_.at(client); + if (ws_conn != nullptr) + return ws_conn->send(data); + return false; +} + +bool WsServer::Impl::close(const ConnToken &client, uint16_t code, const std::string &reason) +{ + auto ws_conn = ws_conns_.at(client); + if (ws_conn != nullptr) + return ws_conn->close(code, reason); + return false; +} + +bool WsServer::Impl::ping(const ConnToken &client, const std::string &data) +{ + auto ws_conn = ws_conns_.at(client); + if (ws_conn != nullptr) + return ws_conn->ping(data); + return false; +} + +bool WsServer::Impl::pong(const ConnToken &client, const std::string &data) +{ + auto ws_conn = ws_conns_.at(client); + if (ws_conn != nullptr) + return ws_conn->pong(data); + return false; +} + +bool WsServer::Impl::isClientValid(const ConnToken &client) const +{ + return ws_conns_.at(client) != nullptr; +} + +network::SockAddr WsServer::Impl::peerAddr(const ConnToken &client) const +{ + auto ws_conn = ws_conns_.at(client); + if (ws_conn != nullptr) + return ws_conn->peerAddr(); + return network::SockAddr(); +} + +std::string WsServer::Impl::getUrl(const ConnToken &client) const +{ + auto ws_conn = ws_conns_.at(client); + if (ws_conn != nullptr) + return ws_conn->getUrl(); + return ""; +} + +void WsServer::Impl::setContext(const ConnToken &client, void *context, ContextDeleter &&deleter) +{ + auto ws_conn = ws_conns_.at(client); + if (ws_conn != nullptr) + ws_conn->setContext(context, std::move(deleter)); +} + +void* WsServer::Impl::getContext(const ConnToken &client) const +{ + auto ws_conn = ws_conns_.at(client); + if (ws_conn != nullptr) + return ws_conn->getContext(); + return nullptr; +} + +//! === 静态辅助方法 === + +bool WsServer::Impl::IsWsUpgradeRequest(const http::Request &req) +{ + //! RFC 6455 Section 4.1: + //! 1) 必须是 GET 方法 + //! 2) 必须包含 Upgrade: websocket 头部 + //! 3) 必须包含 Connection: Upgrade 头部 + //! 4) 必须包含 Sec-WebSocket-Key 头部 + //! 5) 必须包含 Sec-WebSocket-Version: 13 头部 + + if (req.method != http::Method::kGet) + return false; + + auto upgrade_iter = req.headers.find("Upgrade"); + if (upgrade_iter == req.headers.end() + || upgrade_iter->second.find("websocket") == std::string::npos) + return false; + + auto connection_iter = req.headers.find("Connection"); + if (connection_iter == req.headers.end() + || connection_iter->second.find("Upgrade") == std::string::npos) + return false; + + if (req.headers.find("Sec-WebSocket-Key") == req.headers.end()) + return false; + + //! 检查版本号 + auto version_iter = req.headers.find("Sec-WebSocket-Version"); + if (version_iter == req.headers.end() + || version_iter->second != "13") + return false; + + return true; +} + +std::string WsServer::Impl::ComputeWsAcceptKey(const std::string &sec_ws_key) +{ + //! RFC 6455 Section 4.2.2: + //! Sec-WebSocket-Accept = Base64(SHA1(Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) + static const std::string ws_guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + std::string combined = sec_ws_key + ws_guid; + uint8_t digest[20]; + crypto::SHA1::Calc(combined.data(), combined.size(), digest); + + return util::base64::Encode(digest, 20); +} + +void WsServer::Impl::setCompressionEnable(bool enable) +{ + compression_config_.enabled = enable; +} + +WsServer::WsServer(event::Loop *wp_loop) + : impl_(new Impl(this, wp_loop)) +{ + TBOX_ASSERT(wp_loop != nullptr); +} + +WsServer::~WsServer() +{ + CHECK_DELETE_RESET_OBJ(impl_); +} + +void WsServer::setCompressionEnable(bool enable) +{ + impl_->setCompressionEnable(enable); +} + +void WsServer::setFragmentSize(size_t size) +{ + impl_->setFragmentSize(size); +} + +void WsServer::setPingInterval(int seconds) +{ + impl_->setPingInterval(seconds); +} + +void WsServer::setPingTimeout(int seconds) +{ + impl_->setPingTimeout(seconds); +} + +bool WsServer::initialize(http::server::Server *http_server, const std::string &url_path) +{ + TBOX_ASSERT(http_server != nullptr); + return impl_->initialize(http_server, url_path); +} + +bool WsServer::start() +{ + return impl_->start(); +} + +void WsServer::stop() +{ + impl_->stop(); +} + +void WsServer::cleanup() +{ + impl_->cleanup(); +} + +WsServer::State WsServer::state() const +{ + return impl_->state(); +} + +void WsServer::setConnectedCallback(const ConnectedCallback &cb) +{ + impl_->setConnectedCallback(cb); +} + +void WsServer::setDisconnectedCallback(const DisconnectedCallback &cb) +{ + impl_->setDisconnectedCallback(cb); +} + +void WsServer::setTextMessageCallback(const TextMessageCallback &cb) +{ + impl_->setTextMessageCallback(cb); +} + +void WsServer::setBinaryMessageCallback(const BinaryMessageCallback &cb) +{ + impl_->setBinaryMessageCallback(cb); +} + +void WsServer::setErrorCallback(const ErrorCallback &cb) +{ + impl_->setErrorCallback(cb); +} + +bool WsServer::send(const ConnToken &client, const std::string &text) +{ + return impl_->send(client, text); +} + +bool WsServer::send(const ConnToken &client, const char *str) +{ + return impl_->send(client, str); +} + +bool WsServer::send(const ConnToken &client, const void *data, size_t len) +{ + return impl_->send(client, data, len); +} + +bool WsServer::send(const ConnToken &client, const std::vector &data) +{ + return impl_->send(client, data); +} + +bool WsServer::close(const ConnToken &client, uint16_t code, const std::string &reason) +{ + return impl_->close(client, code, reason); +} + +bool WsServer::ping(const ConnToken &client, const std::string &data) +{ + return impl_->ping(client, data); +} + +bool WsServer::pong(const ConnToken &client, const std::string &data) +{ + return impl_->pong(client, data); +} + +bool WsServer::isClientValid(const ConnToken &client) const +{ + return impl_->isClientValid(client); +} + +network::SockAddr WsServer::peerAddr(const ConnToken &client) const +{ + return impl_->peerAddr(client); +} + +std::string WsServer::getUrl(const ConnToken &client) const +{ + return impl_->getUrl(client); +} + +void WsServer::setContext(const ConnToken &client, void *context, ContextDeleter &&deleter) +{ + impl_->setContext(client, context, std::move(deleter)); +} + +void* WsServer::getContext(const ConnToken &client) const +{ + return impl_->getContext(client); +} + +} +} +} diff --git a/modules/websocket/server/ws_server_impl.h b/modules/websocket/server/ws_server_impl.h new file mode 100644 index 00000000..9576a296 --- /dev/null +++ b/modules/websocket/server/ws_server_impl.h @@ -0,0 +1,159 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_WS_SERVER_IMPLH_20260612 +#define TBOX_WS_SERVER_IMPLH_20260612 + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "ws_server.h" +#include "ws_connection.h" +#include "../ws_compressor.h" + +namespace tbox { +namespace websocket { +namespace server { + +//! WsServer::Impl 同时充当 HTTP 中间件 +//! 检测 WebSocket 升级请求,设置 101 响应,注册 upgrade_cb +//! 通过 Cabinet 管理 WsConnection 生命期,所有操作基于 ConnToken +class WsServer::Impl : public http::server::Middleware { + public: + Impl(WsServer *wp_parent, event::Loop *wp_loop); + virtual ~Impl(); + + public: + bool initialize(http::server::Server *http_server, const std::string &url_path = ""); + bool start(); + void stop(); + void cleanup(); + + WsServer::State state() const { return state_; } + + public: + void setConnectedCallback(const WsServer::ConnectedCallback &cb) { connected_cb_ = cb; } + void setDisconnectedCallback(const WsServer::DisconnectedCallback &cb) { disconnected_cb_ = cb; } + void setTextMessageCallback(const WsServer::TextMessageCallback &cb) { text_message_cb_ = cb; } + void setBinaryMessageCallback(const WsServer::BinaryMessageCallback &cb) { binary_message_cb_ = cb; } + void setErrorCallback(const WsServer::ErrorCallback &cb) { error_cb_ = cb; } + + //! 压缩配置 + void setCompressionEnable(bool enable); + + //! 分片大小配置 + void setFragmentSize(size_t size) { fragment_size_ = size; } + + //! Ping/Pong 心跳配置 + void setPingInterval(int seconds) { ping_interval_ = seconds; } + void setPingTimeout(int seconds) { ping_timeout_ = seconds; } + + public: + //! 通过 ConnToken 操作连接(转发到 WsConnection) + bool send(const ConnToken &client, const std::string &text); + bool send(const ConnToken &client, const char *str); + bool send(const ConnToken &client, const void *data, size_t len); + bool send(const ConnToken &client, const std::vector &data); + bool close(const ConnToken &client, uint16_t code, const std::string &reason); + bool ping(const ConnToken &client, const std::string &data); + bool pong(const ConnToken &client, const std::string &data); + bool isClientValid(const ConnToken &client) const; + network::SockAddr peerAddr(const ConnToken &client) const; + std::string getUrl(const ConnToken &client) const; + + //! 上下文数据操作(委托到 WsConnection → TcpConnection) + using ContextDeleter = network::TcpConnection::ContextDeleter; + void setContext(const ConnToken &client, void *context, ContextDeleter &&deleter = nullptr); + void* getContext(const ConnToken &client) const; + + public: + //! Middleware 接口:处理 HTTP 请求,检测 WebSocket 升级 + virtual void handle(http::server::ContextSptr sp_ctx, const http::server::NextFunc &next) override; + + //! 静态辅助方法(供 WsServer 外部接口转发) + static bool IsWsUpgradeRequest(const http::Request &req); + static std::string ComputeWsAcceptKey(const std::string &sec_ws_key); + + private: + //! 当 HTTP 服务器发送 101 响应后回调此函数 + void onWsUpgrade(network::TcpConnection *tcp_conn, const std::string &url_path, + const WsCompressionConfig &compress_config); + + //! 当 WsConnection 断开时回调(参数为 ConnToken) + void onWsDisconnected(const ConnToken &client); + + //! 当 WsConnection 收到完整文本消息时回调 + void onWsTextMessage(const ConnToken &client, std::string &&data); + + //! 当 WsConnection 收到完整二进制消息时回调 + void onWsBinaryMessage(const ConnToken &client, std::vector &&data); + + //! 当 WsConnection 出错时回调 + void onWsError(const ConnToken &client); + + private: + WsServer *wp_parent_; + event::Loop *wp_loop_; + + http::server::Server *wp_http_server_ = nullptr; + //! URL 路径匹配规则: + //! - url_path_ 以 '/' 结尾:前缀匹配,如 "/api/" 匹配 "/api/aa" + //! - url_path_ 不以 '/' 结尾:全量匹配,如 "/api" 仅匹配 "/api" + //! - url_path_ 为空字符串:匹配所有 WebSocket 升级请求 + std::string url_path_; + + //! 中间件 token(由 HTTP Server 的 use() 返回,用于 unuse() 反注册) + http::server::MiddlewareToken mw_token_; + + //! 压缩配置 + WsCompressionConfig compression_config_; + + //! 分片发送的最大帧 payload 大小(可配置,默认 kDefaultFragmentSize) + size_t fragment_size_ = WsServer::kDefaultFragmentSize; + + //! Ping/Pong 心跳参数 + int ping_interval_ = 0; + int ping_timeout_ = 0; + + //! WsConnection 容器(生命期管理) + cabinet::Cabinet ws_conns_; + + WsServer::State state_ = WsServer::State::kNone; + + WsServer::ConnectedCallback connected_cb_; + WsServer::DisconnectedCallback disconnected_cb_; + WsServer::TextMessageCallback text_message_cb_; + WsServer::BinaryMessageCallback binary_message_cb_; + WsServer::ErrorCallback error_cb_; + + int cb_level_ = 0; +}; + +} +} +} +#endif //TBOX_WS_SERVER_IMPLH_20260612 diff --git a/modules/websocket/server/ws_server_impl_test.cpp b/modules/websocket/server/ws_server_impl_test.cpp new file mode 100644 index 00000000..36bb3564 --- /dev/null +++ b/modules/websocket/server/ws_server_impl_test.cpp @@ -0,0 +1,101 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2025 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include + +#include +#include "ws_server_impl.h" + +namespace tbox { +namespace websocket { +namespace server { +namespace { + +//! RFC 6455 Section 4.2.2 示例: +//! Sec-WebSocket-Key = "dGhlIHNhbXBsZSBub25jZQ==" +//! Sec-WebSocket-Accept = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=" +const char *kTestKey = "dGhlIHNhbXBsZSBub25jZQ=="; +const char *kTestAccept = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="; + +} + +TEST(WsHandshake, ComputeAcceptKey) +{ + //! RFC 6455 示例 + std::string accept = WsServer::Impl::ComputeWsAcceptKey(kTestKey); + EXPECT_EQ(kTestAccept, accept); +} + +TEST(WsServer, DetectUpgradeRequest) +{ + http::Request req; + req.method = http::Method::kGet; + req.http_ver = http::HttpVer::k1_1; + req.headers["Upgrade"] = "websocket"; + req.headers["Connection"] = "Upgrade"; + req.headers["Sec-WebSocket-Key"] = "dGhlIHNhbXBsZSBub25jZQ=="; + req.headers["Sec-WebSocket-Version"] = "13"; + + EXPECT_TRUE(WsServer::Impl::IsWsUpgradeRequest(req)); +} + +TEST(WsServer, DetectNonUpgradeRequest) +{ + http::Request req; + req.method = http::Method::kGet; + req.http_ver = http::HttpVer::k1_1; + + EXPECT_FALSE(WsServer::Impl::IsWsUpgradeRequest(req)); +} + +TEST(WsServer, DetectPostNotUpgrade) +{ + http::Request req; + req.method = http::Method::kPost; + req.headers["Upgrade"] = "websocket"; + + EXPECT_FALSE(WsServer::Impl::IsWsUpgradeRequest(req)); +} + +TEST(WsServer, DetectMissingKey) +{ + http::Request req; + req.method = http::Method::kGet; + req.headers["Upgrade"] = "websocket"; + req.headers["Connection"] = "Upgrade"; + //! 缺少 Sec-WebSocket-Key + + EXPECT_FALSE(WsServer::Impl::IsWsUpgradeRequest(req)); +} + +TEST(WsServer, DetectWrongVersion) +{ + http::Request req; + req.method = http::Method::kGet; + req.headers["Upgrade"] = "websocket"; + req.headers["Connection"] = "Upgrade"; + req.headers["Sec-WebSocket-Key"] = "dGhlIHNhbXBsZSBub25jZQ=="; + req.headers["Sec-WebSocket-Version"] = "8"; //! 不是 13 + + EXPECT_FALSE(WsServer::Impl::IsWsUpgradeRequest(req)); +} + +} +} +} diff --git a/modules/websocket/ws_compressor.cpp b/modules/websocket/ws_compressor.cpp new file mode 100644 index 00000000..8f4699fb --- /dev/null +++ b/modules/websocket/ws_compressor.cpp @@ -0,0 +1,198 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the project source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "ws_compressor.h" + +#include +#include + +#include + +namespace tbox { +namespace websocket { + +//! RFC 7692 要求去除的 DEFLATE 尾部:0x00 0x00 0xFF 0xFF +static const uint8_t kDeflateTail[4] = {0x00, 0x00, 0xFF, 0xFF}; + +WsCompressor::WsCompressor() +{ } + +WsCompressor::~WsCompressor() +{ + reset(); +} + +bool WsCompressor::initialize(const WsCompressionConfig &config) +{ + if (!config.isValid()) { + LogErr("invalid compression config"); + return false; + } + + config_ = config; + initialized_ = true; + return true; +} + +void WsCompressor::reset() +{ + //! no_context_takeover 模式不需要持久化 zlib 上下文 + //! 每次调用 compress/decompress 时各自初始化并结束 zlib stream + initialized_ = false; +} + +//! === compress === + +std::string WsCompressor::compress(const std::string &data) +{ + return compress(data.data(), data.size()); +} + +std::string WsCompressor::compress(const void *data_ptr, size_t data_size) +{ + if (!initialized_ || !config_.enabled) + return ""; + + //! no_context_takeover:每次消息独立压缩 + z_stream strm; + memset(&strm, 0, sizeof(strm)); + + //! 初始化 deflate,使用 raw deflate(不写 zlib/gzip 头) + //! window_bits 取负值表示 raw deflate,值的绝对值为窗口位数 + int ret = deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, + -config_.max_window_bits, 8, Z_DEFAULT_STRATEGY); + if (ret != Z_OK) { + LogErr("deflateInit2 fail, ret=%d", ret); + return ""; + } + + //! 设置输入数据 + strm.next_in = reinterpret_cast(const_cast(data_ptr)); + strm.avail_in = static_cast(data_size); + + //! 输出缓冲区:压缩后可能比原始数据更大(如随机数据),预留足够空间 + //! 注意:deflateBound 不包含 Z_SYNC_FLUSH 的空存储块开销(5字节) + //! 实测 deflateBound 比 Z_SYNC_FLUSH 完整输出少约3字节,须额外预留 + size_t max_out = deflateBound(&strm, static_cast(data_size)) + 6; + std::string output; + output.resize(max_out); + + strm.next_out = reinterpret_cast(&output[0]); + strm.avail_out = static_cast(max_out); + + //! 执行压缩 + ret = deflate(&strm, Z_SYNC_FLUSH); + if (ret != Z_OK) { + LogErr("deflate fail, ret=%d", ret); + deflateEnd(&strm); + return ""; + } + + //! 计算实际输出大小 + size_t out_len = max_out - strm.avail_out; + + //! 去掉 4 字节尾部 0x00 0x00 0xFF 0xFF(RFC 7692 Section 7.2.2) + //! Z_SYNC_FLUSH 在末尾追加空存储块:头部(0x00) + LEN(0x00 0x00) + NLEN(0xFF 0xFF) + //! 此处仅去掉 LEN+NLEN 共4字节,保留头部字节0x00——与浏览器实现一致 + //! 解压时追加回这4字节即可还原完整空存储块 + if (out_len >= 4 && + memcmp(reinterpret_cast(&output[out_len - 4]), kDeflateTail, 4) == 0) { + out_len -= 4; + } + + deflateEnd(&strm); + + output.resize(out_len); + return output; +} + +//! === decompress === + +std::string WsCompressor::decompress(const std::string &data) +{ + return decompress(data.data(), data.size()); +} + +std::string WsCompressor::decompress(const void *data_ptr, size_t data_size) +{ + if (!initialized_ || !config_.enabled) + return ""; + + //! no_context_takeover:每次消息独立解压 + z_stream strm; + memset(&strm, 0, sizeof(strm)); + + //! 初始化 inflate,使用 raw inflate(不读 zlib/gzip 头) + //! window_bits 取负值表示 raw inflate + int ret = inflateInit2(&strm, -config_.max_window_bits); + if (ret != Z_OK) { + LogErr("inflateInit2 fail, ret=%d", ret); + return ""; + } + + //! RFC 7692 Section 7.2.2:解压前须在数据末尾加回 4 字节尾部 + //! 将原始数据 + 4字节尾部拼入一个临时缓冲区,避免修改原始数据 + size_t input_len = data_size + 4; + std::string input; + input.resize(input_len); + memcpy(&input[0], data_ptr, data_size); + memcpy(&input[data_size], kDeflateTail, 4); + + //! 输出缓冲区:预估解压后大小 + //! 解压后通常比压缩数据大,预估为输入的 4 倍,不够时动态扩容 + std::string output; + size_t out_capacity = input_len * 4; + if (out_capacity < 256) + out_capacity = 256; + output.resize(out_capacity); + + strm.next_in = reinterpret_cast(&input[0]); + strm.avail_in = static_cast(input_len); + + size_t total_out = 0; + + do { + strm.next_out = reinterpret_cast(&output[total_out]); + strm.avail_out = static_cast(out_capacity - total_out); + + ret = inflate(&strm, Z_SYNC_FLUSH); + + if (ret == Z_STREAM_ERROR || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR) { + LogErr("inflate fail, ret=%d", ret); + inflateEnd(&strm); + return ""; + } + + total_out = strm.total_out; + + //! 输出缓冲区不够大时扩容 + if (strm.avail_out == 0 && ret != Z_STREAM_END) { + out_capacity *= 2; + output.resize(out_capacity); + } + } while (ret != Z_STREAM_END && strm.avail_in > 0); + + inflateEnd(&strm); + + output.resize(total_out); + return output; +} + +} +} diff --git a/modules/websocket/ws_compressor.h b/modules/websocket/ws_compressor.h new file mode 100644 index 00000000..d2c7acdf --- /dev/null +++ b/modules/websocket/ws_compressor.h @@ -0,0 +1,91 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the project source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_WS_COMPRESSOR_H_20260708 +#define TBOX_WS_COMPRESSOR_H_20260708 + +#include +#include + +namespace tbox { +namespace websocket { + +//! WebSocket 压缩配置(RFC 7692 permessage-deflate) +struct WsCompressionConfig { + bool enabled = false; //!< 是否启用压缩 + bool no_context_takeover = true; //!< 是否不保留压缩上下文(每次消息独立) + int max_window_bits = 15; //!< 最大窗口位数 (8~15) + + //! 检查配置是否有效 + bool isValid() const { + if (!enabled) + return true; + if (max_window_bits < 8 || max_window_bits > 15) + return false; + return true; + } +}; + +//! WebSocket 帧压缩/解压缩器(RFC 7692 permessage-deflate) +//! +//! RFC 7692 关键规则: +//! - 使用 DEFLATE (zlib),压缩后数据须去掉 4 字节尾 0x00 0x00 0xFF 0xFF +//! - 解压前须将 4 字节尾加回 +//! - 控制帧(Close/Ping/Pong)永远不压缩 +//! - no_context_takeover=true 时,每条消息独立压缩/解压(不跨消息保持 zlib 上下文) +class WsCompressor { + public: + WsCompressor(); + ~WsCompressor(); + + //! 初始化压缩器(须在使用前调用) + bool initialize(const WsCompressionConfig &config); + + //! 重置压缩器内部状态 + void reset(); + + //! 压缩数据(去掉 4 字节尾 0x00 0x00 0xFF 0xFF) + //! 成功返回压缩后数据,失败返回空字符串 + //! 控制帧不应调用此方法 + std::string compress(const std::string &data); + //! 直接接受原始指针与长度,避免二进制数据构造 std::string 的额外拷贝 + std::string compress(const void *data_ptr, size_t data_size); + + //! 解压数据(先加回 4 字节尾再解压) + //! 成功返回解压后数据,失败返回空字符串 + //! 仅对 RSV1=1 的数据帧调用此方法 + std::string decompress(const std::string &data); + //! 直接接受原始指针与长度,避免二进制数据构造 std::string 的额外拷贝 + std::string decompress(const void *data_ptr, size_t data_size); + + //! 是否已初始化 + bool isInitialized() const { return initialized_; } + + //! 获取配置 + const WsCompressionConfig& config() const { return config_; } + + private: + WsCompressionConfig config_; + bool initialized_ = false; +}; + +} +} + +#endif //TBOX_WS_COMPRESSOR_H_20260708 diff --git a/modules/websocket/ws_compressor_test.cpp b/modules/websocket/ws_compressor_test.cpp new file mode 100644 index 00000000..0807f63a --- /dev/null +++ b/modules/websocket/ws_compressor_test.cpp @@ -0,0 +1,296 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the project source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include + +#include "ws_compressor.h" +#include "ws_frame_builder.h" +#include "ws_frame_parser.h" + +namespace tbox { +namespace websocket { + +//! === WsCompressor 测试 === + +TEST(WsCompressor, CompressDecompressRoundTrip) +{ + WsCompressionConfig config; + config.enabled = true; + + WsCompressor compressor; + ASSERT_TRUE(compressor.initialize(config)); + + //! 压缩 → 解压 → 验证 + std::string original = "Hello, WebSocket permessage-deflate! This is a test message."; + std::string compressed = compressor.compress(original); + ASSERT_FALSE(compressed.empty()); + + //! 压缩后应该比原数据短(对重复性文本) + EXPECT_LT(compressed.size(), original.size()); + + std::string decompressed = compressor.decompress(compressed); + EXPECT_EQ(original, decompressed); +} + +TEST(WsCompressor, CompressDecompressLargeData) +{ + WsCompressionConfig config; + config.enabled = true; + + WsCompressor compressor; + ASSERT_TRUE(compressor.initialize(config)); + + //! 大数据测试 + std::string original(10000, 'A'); + std::string compressed = compressor.compress(original); + ASSERT_FALSE(compressed.empty()); + EXPECT_LT(compressed.size(), original.size()); + + std::string decompressed = compressor.decompress(compressed); + EXPECT_EQ(original, decompressed); +} + +TEST(WsCompressor, CompressDecompressEmptyData) +{ + WsCompressionConfig config; + config.enabled = true; + + WsCompressor compressor; + ASSERT_TRUE(compressor.initialize(config)); + + //! 空数据:压缩应该返回空(或不压缩的数据) + std::string original = ""; + std::string compressed = compressor.compress(original); + //! 空数据压缩后可能有少量数据(zlib 头信息) + //! 但解压后应恢复为空 + if (!compressed.empty()) { + std::string decompressed = compressor.decompress(compressed); + EXPECT_EQ(original, decompressed); + } +} + +TEST(WsCompressor, NoContextTakeover) +{ + WsCompressionConfig config; + config.enabled = true; + config.no_context_takeover = true; + + WsCompressor compressor; + ASSERT_TRUE(compressor.initialize(config)); + + //! 连续压缩多条不同消息,每条独立 + std::string msg1 = "First message with some repeated words words words"; + std::string msg2 = "Second message with different content xyz xyz xyz"; + std::string msg3 = "Third message 1234567890"; + + std::string c1 = compressor.compress(msg1); + std::string c2 = compressor.compress(msg2); + std::string c3 = compressor.compress(msg3); + + ASSERT_FALSE(c1.empty()); + ASSERT_FALSE(c2.empty()); + ASSERT_FALSE(c3.empty()); + + EXPECT_EQ(msg1, compressor.decompress(c1)); + EXPECT_EQ(msg2, compressor.decompress(c2)); + EXPECT_EQ(msg3, compressor.decompress(c3)); +} + +TEST(WsCompressor, CompressDecompressRecompressSymmetry) +{ + //! 验证:compress(data) → decompress → compress 应产生相同结果 + //! 这是 echo 服务器的核心场景:收到客户端压缩数据 → 解压 → 再压缩发回 + //! 如果 compress 输出不一致,客户端将无法正确解压服务端的回传数据 + + WsCompressionConfig config; + config.enabled = true; + config.no_context_takeover = true; + config.max_window_bits = 15; + + WsCompressor compressor; + ASSERT_TRUE(compressor.initialize(config)); + + //! 测试多种数据长度,特别覆盖 16、256、1024 等典型 WebSocket 帧大小 + std::vector test_sizes = {16, 32, 64, 128, 256, 512, 1024}; + + for (size_t size : test_sizes) { + //! 生成随机二进制数据(模拟浏览器发送的随机 payload) + std::string original(size, '\0'); + for (size_t i = 0; i < size; i++) + original[i] = static_cast(rand() % 256); + + //! 第1步:压缩原始数据,得 data1 + std::string data1 = compressor.compress(original); + ASSERT_FALSE(data1.empty()) << "compress failed for size=" << size; + + //! 第2步:解压 data1,还原原始数据 + std::string decompressed = compressor.decompress(data1); + ASSERT_EQ(original.size(), decompressed.size()) << "decompress size mismatch for size=" << size; + ASSERT_EQ(original, decompressed) << "decompress content mismatch for size=" << size; + + //! 第3步:将解压后的数据再次压缩,得 data2 + std::string data2 = compressor.compress(decompressed); + ASSERT_FALSE(data2.empty()) << "recompress failed for size=" << size; + + //! 第4步:data1 与 data2 应完全一致(相同输入 + 相同参数 = 相同输出) + EXPECT_EQ(data1.size(), data2.size()) + << "compressed size mismatch: data1=" << data1.size() << ", data2=" << data2.size() + << " for original size=" << size; + EXPECT_EQ(data1, data2) + << "compressed content mismatch for original size=" << size; + } +} + +TEST(WsCompressor, DisabledCompression) +{ + WsCompressionConfig config; + config.enabled = false; + + WsCompressor compressor; + ASSERT_TRUE(compressor.initialize(config)); + + //! 禁用时 compress/decompress 返回空 + std::string data = "test data"; + EXPECT_EQ("", compressor.compress(data)); + EXPECT_EQ("", compressor.decompress(data)); +} + +//! === RSV1 帧解析测试 === + +TEST(WsFrameParser, Rsv1CompressedTextFrame) +{ + //! RSV1=1 的文本帧(压缩帧首帧) + //! 第1字节: FIN=1, RSV1=1, opcode=0x01(text) = 0xC1 + //! 第2字节: MASK=0, len=5 = 0x05 + //! Payload: 5 字节压缩数据 + uint8_t data[] = {0xC1, 0x05, 0x48, 0x65, 0x6C, 0x6C, 0x6F}; + WsFrameParser parser; + size_t consumed = parser.parse(data, sizeof(data)); + + EXPECT_EQ(sizeof(data), consumed); + EXPECT_EQ(WsFrameParser::State::kFinished, parser.state()); + + WsFrame *frame = parser.getFrame(); + ASSERT_NE(frame, nullptr); + EXPECT_TRUE(frame->fin); + EXPECT_TRUE(frame->rsv1); //! RSV1 应为 true + EXPECT_EQ(WsFrame::OpCode::kText, frame->opcode); + delete frame; +} + +TEST(WsFrameParser, Rsv2Rejected) +{ + //! RSV2=1 的帧应报错(目前不支持 RSV2) + //! 第1字节: FIN=1, RSV2=1, opcode=0x01 = 0xA1 + uint8_t data[] = {0xA1, 0x05, 'H', 'e', 'l', 'l', 'o'}; + WsFrameParser parser; + parser.parse(data, sizeof(data)); + + EXPECT_EQ(WsFrameParser::State::kError, parser.state()); +} + +//! === RSV1 帧构建测试 === + +TEST(WsFrameBuilder, CompressedTextFrameServer) +{ + //! 服务端压缩帧:RSV1=1,不掩码 + auto frame = WsFrameBuilder::BuildFrame(WsFrame::OpCode::kText, true, "Hello", 5, true); + EXPECT_EQ(0xC1, frame[0]); //! FIN + RSV1 + text opcode +} + +TEST(WsFrameBuilder, CompressedTextFrameClient) +{ + //! 客户端压缩帧:RSV1=1,掩码 + uint8_t mask_key[4] = {0x37, 0xfa, 0x21, 0x3d}; + auto frame = WsFrameBuilder::BuildMaskedFrame(WsFrame::OpCode::kText, true, "Hello", 5, mask_key, true); + //! 第1字节: FIN + RSV1 + text opcode = 0xC1 + EXPECT_EQ(0xC1, frame[0]); + //! 第2字节: MASK=1 + len=5 = 0x85 + EXPECT_EQ(0x85, frame[1]); +} + +//! === 压缩帧 roundtrip 测试 === + +TEST(WsCompressor, CompressedFrameRoundTripServer) +{ + //! 模拟服务端发送压缩帧 → 客户端解析并解压 + WsCompressionConfig config; + config.enabled = true; + + WsCompressor compressor; + ASSERT_TRUE(compressor.initialize(config)); + + std::string original = "Hello WebSocket compression!"; + std::string compressed = compressor.compress(original); + ASSERT_FALSE(compressed.empty()); + + //! 服务端构建 RSV1=1 的帧(不掩码) + auto frame = WsFrameBuilder::BuildFrame(WsFrame::OpCode::kText, true, compressed.data(), compressed.size(), true); + + //! 客户端解析帧 + WsFrameParser parser; + parser.parse(frame.data(), frame.size()); + EXPECT_EQ(WsFrameParser::State::kFinished, parser.state()); + + WsFrame *parsed = parser.getFrame(); + ASSERT_NE(parsed, nullptr); + EXPECT_TRUE(parsed->rsv1); + EXPECT_EQ(WsFrame::OpCode::kText, parsed->opcode); + + //! 解压 payload + std::string decompressed = compressor.decompress(parsed->payload); + EXPECT_EQ(original, decompressed); + delete parsed; +} + +TEST(WsCompressor, CompressedFrameRoundTripClient) +{ + //! 模拟客户端发送压缩帧 → 服务端解析并解压 + WsCompressionConfig config; + config.enabled = true; + + WsCompressor compressor; + ASSERT_TRUE(compressor.initialize(config)); + + std::string original = "Client compressed message!"; + std::string compressed = compressor.compress(original); + ASSERT_FALSE(compressed.empty()); + + //! 客户端构建 RSV1=1 的掩码帧 + auto frame = WsFrameBuilder::BuildMaskedFrame(WsFrame::OpCode::kText, true, + compressed.data(), compressed.size(), + nullptr, true); + //! 服务端解析帧 + WsFrameParser parser; + parser.parse(frame.data(), frame.size()); + EXPECT_EQ(WsFrameParser::State::kFinished, parser.state()); + + WsFrame *parsed = parser.getFrame(); + ASSERT_NE(parsed, nullptr); + EXPECT_TRUE(parsed->rsv1); + EXPECT_EQ(WsFrame::OpCode::kText, parsed->opcode); + + //! 解压 payload + std::string decompressed = compressor.decompress(parsed->payload); + EXPECT_EQ(original, decompressed); + delete parsed; +} + +} +} diff --git a/modules/websocket/ws_frame.h b/modules/websocket/ws_frame.h new file mode 100644 index 00000000..12d2df6d --- /dev/null +++ b/modules/websocket/ws_frame.h @@ -0,0 +1,74 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_WS_FRAME_H_20260612 +#define TBOX_WS_FRAME_H_20260612 + +#include +#include + +namespace tbox { +namespace websocket { + +//! WebSocket 帧(RFC 6455) +struct WsFrame { + //! 操作码 + enum class OpCode : uint8_t { + kContinue = 0x0, //!< 继续 + kText = 0x1, //!< 文本 + kBinary = 0x2, //!< 二进制 + kClose = 0x8, //!< 关闭连接 + kPing = 0x9, //!< Ping + kPong = 0xA, //!< Pong + }; + + OpCode opcode = OpCode::kContinue; + bool fin = true; //!< 是否为最后一帧 + bool rsv1 = false; //!< RSV1 位(压缩帧首帧为 true,RFC 7692) + std::string payload; //!< 负载数据 + + //! 是否为控制帧(Close/Ping/Pong) + bool isControlFrame() const + { + return opcode == OpCode::kClose + || opcode == OpCode::kPing + || opcode == OpCode::kPong; + } + + //! 从 Close 帧中提取关闭码和原因 + uint16_t closeCode() const + { + if (opcode != OpCode::kClose || payload.size() < 2) + return 0; + return (static_cast(static_cast(payload[0])) << 8) + | static_cast(static_cast(payload[1])); + } + + std::string closeReason() const + { + if (opcode != OpCode::kClose || payload.size() <= 2) + return ""; + return payload.substr(2); + } +}; + +} +} + +#endif //TBOX_WS_FRAME_H_20260612 diff --git a/modules/websocket/ws_frame_builder.cpp b/modules/websocket/ws_frame_builder.cpp new file mode 100644 index 00000000..3b483887 --- /dev/null +++ b/modules/websocket/ws_frame_builder.cpp @@ -0,0 +1,195 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "ws_frame_builder.h" + +#include +#include + +namespace tbox { +namespace websocket { + +//! === 服务端帧(不掩码) === + +std::vector WsFrameBuilder::BuildTextFrame(const std::string &text) +{ + return BuildFrame(WsFrame::OpCode::kText, true, text.data(), text.size()); +} + +std::vector WsFrameBuilder::BuildBinaryFrame(const void *data, size_t len) +{ + return BuildFrame(WsFrame::OpCode::kBinary, true, data, len); +} + +std::vector WsFrameBuilder::BuildBinaryFrame(const std::vector &data) +{ + return BuildFrame(WsFrame::OpCode::kBinary, true, data.data(), data.size()); +} + +std::vector WsFrameBuilder::BuildCloseFrame(uint16_t code, const std::string &reason) +{ + //! Close 帧的 payload: 2字节关闭码(大端序) + 可选的原因字符串 + std::vector payload; + payload.push_back(static_cast((code >> 8) & 0xFF)); + payload.push_back(static_cast(code & 0xFF)); + if (!reason.empty()) + payload.insert(payload.end(), reason.begin(), reason.end()); + + return BuildFrame(WsFrame::OpCode::kClose, true, payload.data(), payload.size()); +} + +std::vector WsFrameBuilder::BuildPingFrame(const std::string &data) +{ + return BuildFrame(WsFrame::OpCode::kPing, true, data.data(), data.size()); +} + +std::vector WsFrameBuilder::BuildPongFrame(const std::string &data) +{ + return BuildFrame(WsFrame::OpCode::kPong, true, data.data(), data.size()); +} + +std::vector WsFrameBuilder::BuildFrame(WsFrame::OpCode opcode, bool fin, const void *payload_ptr, size_t payload_len, bool rsv1) +{ + std::vector frame; + + //! 第1字节:FIN + RSV1(permessage-deflate) + RSV2-3(0) + Opcode + uint8_t byte0 = static_cast(opcode); + if (fin) + byte0 |= 0x80; + if (rsv1) + byte0 |= 0x40; + frame.push_back(byte0); + + //! 第2字节:MASK=0(服务端不掩码) + Payload length + //! 服务端发送的帧不使用掩码(RFC 6455 Section 5.3) + //! RFC 6455 Section 5.2:payload_len 0~125 用 7-bit,126~65535 用 16-bit,>65535 用 64-bit + if (payload_len <= 125) { + frame.push_back(static_cast(payload_len)); + } else if (payload_len <= 65535) { + frame.push_back(126); + frame.push_back(static_cast((payload_len >> 8) & 0xFF)); + frame.push_back(static_cast(payload_len & 0xFF)); + } else { + frame.push_back(127); + for (int i = 7; i >= 0; --i) + frame.push_back(static_cast((payload_len >> (i * 8)) & 0xFF)); + } + + //! Payload 数据(无掩码) + if (payload_ptr != nullptr && payload_len > 0) { + const uint8_t *p = static_cast(payload_ptr); + frame.insert(frame.end(), p, p + payload_len); + } + + return frame; +} + +//! === 客户端帧(掩码) === + +std::vector WsFrameBuilder::BuildMaskedTextFrame(const std::string &text) +{ + return BuildMaskedFrame(WsFrame::OpCode::kText, true, text.data(), text.size()); +} + +std::vector WsFrameBuilder::BuildMaskedBinaryFrame(const void *data, size_t len) +{ + return BuildMaskedFrame(WsFrame::OpCode::kBinary, true, data, len); +} + +std::vector WsFrameBuilder::BuildMaskedBinaryFrame(const std::vector &data) +{ + return BuildMaskedFrame(WsFrame::OpCode::kBinary, true, data.data(), data.size()); +} + +std::vector WsFrameBuilder::BuildMaskedCloseFrame(uint16_t code, const std::string &reason) +{ + std::vector payload; + payload.push_back(static_cast((code >> 8) & 0xFF)); + payload.push_back(static_cast(code & 0xFF)); + if (!reason.empty()) + payload.insert(payload.end(), reason.begin(), reason.end()); + + return BuildMaskedFrame(WsFrame::OpCode::kClose, true, payload.data(), payload.size()); +} + +std::vector WsFrameBuilder::BuildMaskedPingFrame(const std::string &data) +{ + return BuildMaskedFrame(WsFrame::OpCode::kPing, true, data.data(), data.size()); +} + +std::vector WsFrameBuilder::BuildMaskedPongFrame(const std::string &data) +{ + return BuildMaskedFrame(WsFrame::OpCode::kPong, true, data.data(), data.size()); +} + +std::vector WsFrameBuilder::BuildMaskedFrame(WsFrame::OpCode opcode, bool fin, + const void *payload_ptr, size_t payload_len, + const uint8_t *mask_key, bool rsv1) +{ + std::vector frame; + + //! 生成或使用提供的掩码密钥 + uint8_t mk[4]; + if (mask_key != nullptr) { + memcpy(mk, mask_key, 4); + } else { + //! 随机生成掩码密钥 + mk[0] = static_cast(rand() & 0xFF); + mk[1] = static_cast(rand() & 0xFF); + mk[2] = static_cast(rand() & 0xFF); + mk[3] = static_cast(rand() & 0xFF); + } + + //! 第1字节:FIN + RSV1(permessage-deflate) + RSV2-3(0) + Opcode + uint8_t byte0 = static_cast(opcode); + if (fin) + byte0 |= 0x80; + if (rsv1) + byte0 |= 0x40; + frame.push_back(byte0); + + //! 第2字节:MASK=1(客户端必须掩码) + Payload length + //! RFC 6455 Section 5.2:payload_len 0~125 用 7-bit,126~65535 用 16-bit,>65535 用 64-bit + if (payload_len <= 125) { + frame.push_back(static_cast(0x80 | payload_len)); + } else if (payload_len <= 65535) { + frame.push_back(0x80 | 126); + frame.push_back(static_cast((payload_len >> 8) & 0xFF)); + frame.push_back(static_cast(payload_len & 0xFF)); + } else { + frame.push_back(0x80 | 127); + for (int i = 7; i >= 0; --i) + frame.push_back(static_cast((payload_len >> (i * 8)) & 0xFF)); + } + + //! 掩码密钥(4字节) + frame.insert(frame.end(), mk, mk + 4); + + //! Payload 数据(掩码后) + if (payload_ptr != nullptr && payload_len > 0) { + const uint8_t *p = static_cast(payload_ptr); + for (size_t i = 0; i < payload_len; ++i) + frame.push_back(p[i] ^ mk[i % 4]); + } + + return frame; +} + +} +} diff --git a/modules/websocket/ws_frame_builder.h b/modules/websocket/ws_frame_builder.h new file mode 100644 index 00000000..720dfa5d --- /dev/null +++ b/modules/websocket/ws_frame_builder.h @@ -0,0 +1,90 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_WS_FRAME_BUILDER_H_20260612 +#define TBOX_WS_FRAME_BUILDER_H_20260612 + +#include "ws_frame.h" +#include +#include +#include + +namespace tbox { +namespace websocket { + +//! WebSocket 帧构建器(RFC 6455) +//! 服务端帧不使用掩码,客户端帧必须使用掩码 +class WsFrameBuilder { + public: + //! === 服务端帧(不掩码) === + + //! 构建文本帧(服务端) + static std::vector BuildTextFrame(const std::string &text); + + //! 构建二进制帧(服务端) + static std::vector BuildBinaryFrame(const void *data, size_t len); + static std::vector BuildBinaryFrame(const std::vector &data); + + //! 构建关闭帧(服务端) + static std::vector BuildCloseFrame(uint16_t code = 1000, const std::string &reason = ""); + + //! 构建 Ping 帧(服务端) + static std::vector BuildPingFrame(const std::string &data = ""); + + //! 构建 Pong 帧(服务端) + static std::vector BuildPongFrame(const std::string &data = ""); + + //! 通用帧构建(服务端,不掩码) + //! rsv1 为 true 时设置 RSV1 位(用于 permessage-deflate 压缩帧) + static std::vector BuildFrame(WsFrame::OpCode opcode, bool fin, + const void *payload, size_t payload_len, + bool rsv1 = false); + + //! === 客户端帧(掩码) === + //! RFC 6455 Section 5.3:客户端发送的帧必须使用掩码 + + //! 构建文本帧(客户端,掩码) + static std::vector BuildMaskedTextFrame(const std::string &text); + + //! 构建二进制帧(客户端,掩码) + static std::vector BuildMaskedBinaryFrame(const void *data, size_t len); + static std::vector BuildMaskedBinaryFrame(const std::vector &data); + + //! 构建关闭帧(客户端,掩码) + static std::vector BuildMaskedCloseFrame(uint16_t code = 1000, const std::string &reason = ""); + + //! 构建 Ping 帧(客户端,掩码) + static std::vector BuildMaskedPingFrame(const std::string &data = ""); + + //! 构建 Pong 帧(客户端,掩码) + static std::vector BuildMaskedPongFrame(const std::string &data = ""); + + //! 通用帧构建(客户端,掩码) + //! mask_key 为 4 字节掩码密钥,若为 nullptr 则自动随机生成 + //! rsv1 为 true 时设置 RSV1 位(用于 permessage-deflate 压缩帧) + static std::vector BuildMaskedFrame(WsFrame::OpCode opcode, bool fin, + const void *payload, size_t payload_len, + const uint8_t *mask_key = nullptr, + bool rsv1 = false); +}; + +} +} + +#endif //TBOX_WS_FRAME_BUILDER_H_20260612 diff --git a/modules/websocket/ws_frame_builder_test.cpp b/modules/websocket/ws_frame_builder_test.cpp new file mode 100644 index 00000000..d499d5e2 --- /dev/null +++ b/modules/websocket/ws_frame_builder_test.cpp @@ -0,0 +1,104 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2025 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include + +#include "ws_frame_builder.h" +#include "ws_frame_parser.h" + +namespace tbox { +namespace websocket { + +TEST(WsFrameBuilder, TextFrame) +{ + auto frame = WsFrameBuilder::BuildTextFrame("Hello"); + //! 期望: 0x81 0x05 'H' 'e' 'l' 'l' 'o' + EXPECT_EQ(7u, frame.size()); + EXPECT_EQ(0x81, frame[0]); + EXPECT_EQ(0x05, frame[1]); + EXPECT_EQ('H', frame[2]); +} + +TEST(WsFrameBuilder, BinaryFrame) +{ + std::vector data = {0x01, 0x02, 0x03}; + auto frame = WsFrameBuilder::BuildBinaryFrame(data); + //! 期望: 0x82 0x03 0x01 0x02 0x03 + EXPECT_EQ(5u, frame.size()); + EXPECT_EQ(0x82, frame[0]); + EXPECT_EQ(0x03, frame[1]); +} + +TEST(WsFrameBuilder, CloseFrame) +{ + auto frame = WsFrameBuilder::BuildCloseFrame(1000, "normal"); + //! 期望: 0x88 + len + 0x03E8 + "normal" + //! payload = 2 + 6 = 8 bytes + EXPECT_EQ(0x88, frame[0]); + EXPECT_EQ(8u, frame[1]); + //! 关闭码: 0x03 0xE8 (1000 大端序) + EXPECT_EQ(0x03, frame[2]); + EXPECT_EQ(0xE8, frame[3]); +} + +TEST(WsFrameBuilder, PingFrame) +{ + auto frame = WsFrameBuilder::BuildPingFrame("test"); + EXPECT_EQ(0x89, frame[0]); + EXPECT_EQ(4u, frame[1]); +} + +TEST(WsFrameBuilder, PongFrame) +{ + auto frame = WsFrameBuilder::BuildPongFrame("test"); + EXPECT_EQ(0x8A, frame[0]); + EXPECT_EQ(4u, frame[1]); +} + +TEST(WsFrameBuilder, LargePayload16) +{ + //! Payload 超过 125 字节,使用 16 位扩展长度 + std::string large_text(200, 'A'); + auto frame = WsFrameBuilder::BuildTextFrame(large_text); + EXPECT_EQ(0x81, frame[0]); + EXPECT_EQ(126, frame[1]); //! 16-bit extended length marker + //! 长度 = 200 = 0x00C8 + EXPECT_EQ(0x00, frame[2]); + EXPECT_EQ(0xC8, frame[3]); + EXPECT_EQ(200u + 4u, frame.size()); //! 4 header bytes + 200 payload +} + +TEST(WsFrameBuilder, RoundTrip) +{ + //! 构建 → 解析 → 验证 + auto built = WsFrameBuilder::BuildTextFrame("RoundTrip Test"); + + WsFrameParser parser; + parser.parse(built.data(), built.size()); + + WsFrame *parsed = parser.getFrame(); + ASSERT_NE(parsed, nullptr); + EXPECT_EQ(WsFrame::OpCode::kText, parsed->opcode); + EXPECT_TRUE(parsed->fin); + EXPECT_EQ("RoundTrip Test", parsed->payload); + delete parsed; +} + +} +} diff --git a/modules/websocket/ws_frame_parser.cpp b/modules/websocket/ws_frame_parser.cpp new file mode 100644 index 00000000..69c8c3c6 --- /dev/null +++ b/modules/websocket/ws_frame_parser.cpp @@ -0,0 +1,236 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include "ws_frame_parser.h" + +#include +#include +#include + +namespace tbox { +namespace websocket { + +WsFrameParser::WsFrameParser() +{ + state_ = State::kInit; + payload_received_ = 0; +} + +size_t WsFrameParser::parse(const void *data_ptr, size_t data_size) +{ + if (state_ == State::kError || state_ == State::kFinished || data_ptr == nullptr) + return 0; + + const uint8_t *p = static_cast(data_ptr); + size_t remaining = data_size; + size_t consumed = 0; + + while (remaining > 0) { + switch (state_) { + case State::kInit: { + //! 第1字节:FIN + RSV1-3 + Opcode + fin_ = (p[0] >> 7) & 1; + rsv1_ = (p[0] >> 6) & 1; + + //! 检查:RSV2/RSV3 必须为0(目前仅支持 RSV1 用于 permessage-deflate) + if ((p[0] & 0x30) != 0) { + state_ = State::kError; + return consumed; + } + + opcode_ = p[0] & 0x0F; + + ++p; --remaining; ++consumed; + state_ = State::kHeader2Bytes; + break; + } + + case State::kHeader2Bytes: { + //! 第2字节:MASK + Payload length (7 bits) + //! RFC 6455 Section 5.2:len7 0~125 为 7-bit 长度,126 为 16-bit,127 为 64-bit + masked_ = (p[0] >> 7) & 1; + uint8_t len7 = p[0] & 0x7F; + + if (len7 <= 125) { + payload_len_ = len7; + ++p; --remaining; ++consumed; + payload_.clear(); + payload_received_ = 0; + + if (payload_len_ == 0 && !masked_) { + //! 无负载,也无mask,创建 WsFrame + sp_frame_ = new WsFrame; + sp_frame_->fin = fin_; + sp_frame_->rsv1 = rsv1_; + sp_frame_->opcode = static_cast(opcode_); + sp_frame_->payload = std::move(payload_); + state_ = State::kFinished; + return consumed; + } + state_ = masked_ ? State::kMaskKey : State::kPayload; + } else if (len7 == 126) { + payload_len_ = 0; //! 待读取16位长度 + ++p; --remaining; ++consumed; + state_ = State::kPayloadLen16; + } else { //! len7 == 127 + payload_len_ = 0; //! 待读取64位长度 + ++p; --remaining; ++consumed; + state_ = State::kPayloadLen64; + } + break; + } + + case State::kPayloadLen16: { + //! 需要2字节 + if (remaining < 2) + return consumed; + + payload_len_ = (static_cast(p[0]) << 8) + | static_cast(p[1]); + p += 2; remaining -= 2; consumed += 2; + + //! RFC 6455 Section 5.2:16位扩展长度必须 >= 126 + if (payload_len_ <= 125) { + state_ = State::kError; + return consumed; + } + + state_ = masked_ ? State::kMaskKey : State::kPayload; + payload_.clear(); + payload_received_ = 0; + break; + } + + case State::kPayloadLen64: { + //! 需要8字节 + if (remaining < 8) + return consumed; + + //! 64位长度,大端序 + uint64_t len = 0; + for (int i = 0; i < 8; ++i) + len = (len << 8) | static_cast(p[i]); + + //! 最高位必须为0 + if (len >= (1ULL << 63)) { + state_ = State::kError; + return consumed; + } + + payload_len_ = len; + p += 8; remaining -= 8; consumed += 8; + + //! 64位长度必须 > 65535 + if (payload_len_ <= 65535) { + state_ = State::kError; + return consumed; + } + + state_ = masked_ ? State::kMaskKey : State::kPayload; + payload_.clear(); + payload_received_ = 0; + break; + } + + case State::kMaskKey: { + //! 需要4字节掩码 + if (remaining < 4) + return consumed; + + memcpy(mask_key_, p, 4); + p += 4; remaining -= 4; consumed += 4; + + if (payload_len_ == 0) { + //! 无负载,创建 WsFrame + sp_frame_ = new WsFrame; + sp_frame_->fin = fin_; + sp_frame_->rsv1 = rsv1_; + sp_frame_->opcode = static_cast(opcode_); + sp_frame_->payload = std::move(payload_); + state_ = State::kFinished; + return consumed; + } + + state_ = State::kPayload; + break; + } + + case State::kPayload: { + //! 读取负载数据 + uint64_t need = payload_len_ - payload_received_; + size_t copy_len = (need > remaining) ? remaining : static_cast(need); + + if (masked_) { + //! 解掩码:payload[i] ^= mask_key[(i + payload_received_) % 4] + for (size_t i = 0; i < copy_len; ++i) { + uint8_t byte = p[i] ^ mask_key_[(payload_received_ + i) % 4]; + payload_.push_back(byte); + } + } else { + payload_.append(reinterpret_cast(p), copy_len); + } + + payload_received_ += copy_len; + p += copy_len; remaining -= copy_len; consumed += copy_len; + + if (payload_received_ == payload_len_) { + //! 帧完整,创建 WsFrame + sp_frame_ = new WsFrame; + sp_frame_->fin = fin_; + sp_frame_->rsv1 = rsv1_; + sp_frame_->opcode = static_cast(opcode_); + sp_frame_->payload = std::move(payload_); + state_ = State::kFinished; + return consumed; + } + break; + } + + case State::kFinished: + case State::kError: + return consumed; + } + } + + return consumed; +} + +WsFrame* WsFrameParser::getFrame() +{ + if (state_ != State::kFinished) + return nullptr; + + WsFrame *frame = sp_frame_; + sp_frame_ = nullptr; + state_ = State::kInit; + payload_.clear(); + payload_received_ = 0; + return frame; +} + +void WsFrameParser::reset() +{ + CHECK_DELETE_RESET_OBJ(sp_frame_); + state_ = State::kInit; + payload_.clear(); + payload_received_ = 0; +} + +} +} diff --git a/modules/websocket/ws_frame_parser.h b/modules/websocket/ws_frame_parser.h new file mode 100644 index 00000000..710ce679 --- /dev/null +++ b/modules/websocket/ws_frame_parser.h @@ -0,0 +1,82 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2026 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#ifndef TBOX_WS_FRAME_PARSER_H_20260612 +#define TBOX_WS_FRAME_PARSER_H_20260612 + +#include "ws_frame.h" +#include + +namespace tbox { +namespace websocket { + +//! WebSocket 增量帧解析器(RFC 6455) +//! 适用于事件驱动场景,逐步从缓冲区中解析帧 +class WsFrameParser { + public: + //! 解析状态 + enum class State { + kInit, //!< 初始状态,等待新帧 + kHeader2Bytes, //!< 已读取首2字节,等待剩余头部 + kPayloadLen16, //!< 等待16位扩展长度 + kPayloadLen64, //!< 等待64位扩展长度 + kMaskKey, //!< 等待4字节掩码 + kPayload, //!< 等待负载数据 + kFinished, //!< 一帧解析完成 + kError, //!< 解析出错 + }; + + WsFrameParser(); + + //! 从数据中解析,返回已消费的字节数 + size_t parse(const void *data_ptr, size_t data_size); + + //! 获取当前状态 + State state() const { return state_; } + + //! 获取解析完成的帧(仅 state == kFinished 时有效) + //! 取走后,解析器自动重置为 kInit + WsFrame* getFrame(); + + //! 重置解析器 + void reset(); + + private: + State state_ = State::kInit; + + //! 当前帧的头部信息 + bool fin_; + bool rsv1_; //!< RSV1 位(permessage-deflate 压缩帧标记) + uint8_t opcode_; + bool masked_; + uint64_t payload_len_; + uint8_t mask_key_[4]; + + //! 已接收的负载数据 + std::string payload_; + uint64_t payload_received_; + + //! 解析完成的帧 + WsFrame *sp_frame_ = nullptr; +}; + +} +} + +#endif //TBOX_WS_FRAME_PARSER_H_20260612 diff --git a/modules/websocket/ws_frame_parser_test.cpp b/modules/websocket/ws_frame_parser_test.cpp new file mode 100644 index 00000000..36a29b88 --- /dev/null +++ b/modules/websocket/ws_frame_parser_test.cpp @@ -0,0 +1,144 @@ +/* + * .============. + * // M A K E / \ + * // C++ DEV / \ + * // E A S Y / \/ \ + * ++ ----------. \/\ . + * \\ \ \ /\ / + * \\ \ \ / + * \\ \ \ / + * -============' + * + * Copyright (c) 2025 Hevake and contributors, all rights reserved. + * + * This file is part of cpp-tbox (https://github.com/cpp-main/cpp-tbox) + * Use of this source code is governed by MIT license that can be found + * in the LICENSE file in the root of the source tree. All contributing + * project authors may be found in the CONTRIBUTORS.md file in the root + * of the source tree. + */ +#include + +#include "ws_frame_parser.h" + +namespace tbox { +namespace websocket { + +TEST(WsFrameParser, UnmaskedTextFrame) +{ + //! RFC 6455 Section 5.7 示例:单帧无掩码文本 "Hello" + //! 0x81 0x05 0x48 0x65 0x6c 0x6c 0x6f + uint8_t data[] = {0x81, 0x05, 'H', 'e', 'l', 'l', 'o'}; + WsFrameParser parser; + size_t consumed = parser.parse(data, sizeof(data)); + + EXPECT_EQ(sizeof(data), consumed); + EXPECT_EQ(WsFrameParser::State::kFinished, parser.state()); + + WsFrame *frame = parser.getFrame(); + ASSERT_NE(frame, nullptr); + EXPECT_TRUE(frame->fin); + EXPECT_EQ(WsFrame::OpCode::kText, frame->opcode); + EXPECT_EQ("Hello", frame->payload); + delete frame; +} + +TEST(WsFrameParser, MaskedTextFrame) +{ + //! RFC 6455 Section 5.7 示例:单帧有掩码文本 "Hello" + //! 0x81 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58 + uint8_t data[] = {0x81, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58}; + WsFrameParser parser; + size_t consumed = parser.parse(data, sizeof(data)); + + EXPECT_EQ(sizeof(data), consumed); + EXPECT_EQ(WsFrameParser::State::kFinished, parser.state()); + + WsFrame *frame = parser.getFrame(); + ASSERT_NE(frame, nullptr); + EXPECT_TRUE(frame->fin); + EXPECT_EQ(WsFrame::OpCode::kText, frame->opcode); + EXPECT_EQ("Hello", frame->payload); + delete frame; +} + +TEST(WsFrameParser, PingFrame) +{ + //! Ping 帧,无掩码 + uint8_t data[] = {0x89, 0x05, 'H', 'e', 'l', 'l', 'o'}; + WsFrameParser parser; + parser.parse(data, sizeof(data)); + + WsFrame *frame = parser.getFrame(); + ASSERT_NE(frame, nullptr); + EXPECT_EQ(WsFrame::OpCode::kPing, frame->opcode); + EXPECT_EQ("Hello", frame->payload); + delete frame; +} + +TEST(WsFrameParser, CloseFrame) +{ + //! Close 帧,无掩码,code=1000 + uint8_t data[] = {0x88, 0x02, 0x03, 0xe8}; + WsFrameParser parser; + parser.parse(data, sizeof(data)); + + WsFrame *frame = parser.getFrame(); + ASSERT_NE(frame, nullptr); + EXPECT_EQ(WsFrame::OpCode::kClose, frame->opcode); + EXPECT_EQ(1000, frame->closeCode()); + delete frame; +} + +TEST(WsFrameParser, ExtendedPayloadLen16) +{ + //! 126 表示 16 位扩展长度 + //! 创建一个 payload_len = 200 的二进制帧 + std::vector frame_data; + frame_data.push_back(0x82); //! FIN + binary + frame_data.push_back(126); //! 16-bit extended length + frame_data.push_back(0x00); //! 高字节: 200 >> 8 = 0 + frame_data.push_back(0xC8); //! 低字节: 200 & 0xFF = 200 + + //! 200 字节的 payload (全是 0xAA) + for (int i = 0; i < 200; ++i) + frame_data.push_back(0xAA); + + WsFrameParser parser; + size_t consumed = parser.parse(frame_data.data(), frame_data.size()); + + EXPECT_EQ(frame_data.size(), consumed); + EXPECT_EQ(WsFrameParser::State::kFinished, parser.state()); + + WsFrame *frame = parser.getFrame(); + ASSERT_NE(frame, nullptr); + EXPECT_EQ(WsFrame::OpCode::kBinary, frame->opcode); + EXPECT_EQ(200u, frame->payload.size()); + delete frame; +} + +TEST(WsFrameParser, IncrementalParse) +{ + //! 测试增量解析:数据分两次喂入 + uint8_t data[] = {0x81, 0x05, 'H', 'e', 'l', 'l', 'o'}; + + WsFrameParser parser; + + //! 第一次只喂2字节 + size_t consumed1 = parser.parse(data, 2); + EXPECT_EQ(2, consumed1); + EXPECT_EQ(WsFrameParser::State::kPayload, parser.state()); + + //! 第二次喂剩余5字节 + size_t consumed2 = parser.parse(data + 2, 5); + EXPECT_EQ(5, consumed2); + EXPECT_EQ(WsFrameParser::State::kFinished, parser.state()); + + WsFrame *frame = parser.getFrame(); + ASSERT_NE(frame, nullptr); + EXPECT_EQ("Hello", frame->payload); + delete frame; +} + +} +} diff --git a/version.mk b/version.mk index c02801df..de4d8f8e 100644 --- a/version.mk +++ b/version.mk @@ -20,5 +20,5 @@ # TBOX版本号 TBOX_VERSION_MAJOR := 1 -TBOX_VERSION_MINOR := 13 -TBOX_VERSION_REVISION := 10 +TBOX_VERSION_MINOR := 15 +TBOX_VERSION_REVISION := 6