From fae6ddb4d62ae8ad19eac2710f79b18c0769d719 Mon Sep 17 00:00:00 2001 From: nanako <469449812@qq.com> Date: Tue, 4 Nov 2025 20:01:28 +0800 Subject: [PATCH] =?UTF-8?q?network=E6=A8=A1=E5=9D=97=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/network/rpc/engine_rpc.h | 110 +++ src/network/rpc/host_rpc.h | 177 +++++ .../shm/interprocess_synchronization.cpp | 522 ++++++++++++- .../shm/interprocess_synchronization.h | 554 ++++++++++++- src/network/shm/lock_free_ring_buffer.cpp | 214 ++++- src/network/shm/lock_free_ring_buffer.h | 605 ++++++++++++++- src/network/shm/shared_memory_manager.cpp | 380 ++++++++- src/network/shm/shared_memory_manager.h | 592 +++++++++++++- src/network/shm/triple_buffer.cpp | 203 +++++ src/network/shm/triple_buffer.h | 564 +++++++++++++- src/network/transport/transport.cpp | 32 + src/network/transport/transport.h | 155 +++- src/network/transport/zmq_client.cpp | 438 +++++++++++ src/network/transport/zmq_client.h | 729 +++++++++++++++++- src/network/transport/zmq_client_processor.h | 564 ++++++++++++++ src/network/transport/zmq_server.cpp | 514 ++++++++++++ src/network/transport/zmq_server.h | 713 ++++++++++++++++- src/network/transport/zmq_server_processor.h | 623 +++++++++++++++ src/network/transport/zmq_util.h | 345 ++++++++- .../test_process_manager_manager.cpp | 1 - 20 files changed, 7961 insertions(+), 74 deletions(-) diff --git a/src/network/rpc/engine_rpc.h b/src/network/rpc/engine_rpc.h index 0250a77..60b07e7 100644 --- a/src/network/rpc/engine_rpc.h +++ b/src/network/rpc/engine_rpc.h @@ -1,10 +1,120 @@ +/** + * @file engine_rpc.h + * @brief Engine端RPC数据结构定义 + * + * 本文件定义了Engine进程向Host进程发送远程过程调用(RPC)时使用的数据结构。 + * 这些数据结构用于Engine进程与Host进程之间的进程间通信(IPC),主要用于 + * 传递日志消息等运行时信息。 + * + * ## 文件在network模块中的作用 + * - 定义Engine端的RPC消息格式,确保数据序列化的一致性 + * - 作为Engine进程和Host进程通信协议的一部分 + * - 配合Transport子模块(如ZMQ)实现跨进程的消息传递 + * + * ## 与其他模块的关系 + * - **Transport子模块**: 本文件定义的数据结构会通过Transport层(ZMQ)进行传输 + * - **Logger模块**: log_message_t结构体用于将Engine的日志消息转发到Host进程 + * - **Host端RPC**: 与host_rpc.h协同工作,形成双向通信的数据格式定义 + * + * ## 主要数据结构 + * - log_message_t: Engine端发送的日志消息结构 + * + * @note 此文件仅定义数据结构,不包含序列化/反序列化逻辑 + * @note 实际的序列化实现可能在engine_rpc.cpp或使用第三方序列化库 + */ + #pragma once #include #include namespace engine_rpc { + /** + * @struct log_message_t + * @brief Engine进程发送给Host进程的日志消息结构 + * + * ## 用途和设计目的 + * 此结构体用于将Engine进程运行时产生的日志消息传递给Host进程。 + * Engine进程通常运行在受限的沙箱环境中,无法直接访问标准输出或文件系统, + * 因此需要通过RPC将日志消息转发给Host进程进行统一处理和输出。 + * + * ## 使用场景 + * 1. Engine进程初始化时的启动日志 + * 2. 音频处理过程中的调试信息 + * 3. 运行时错误和警告信息 + * 4. 性能监控和统计数据的记录 + * + * ## 与其他组件的交互 + * - **Logger**: Engine进程的Logger会将日志封装成log_message_t + * - **Transport**: 通过ZMQ等传输层发送到Host进程 + * - **Host Sandbox**: Host端接收并处理这些日志消息 + * + * ## 设计考虑 + * - 使用简单的POD结构,便于序列化和跨进程传输 + * - 日志级别和消息内容分离,方便接收端进行过滤和分类 + * - 使用std::string存储消息,支持任意长度和Unicode字符 + * + * @note 此结构体需要支持序列化(如JSON、Protobuf或MessagePack) + * @note 在高频日志场景下,应注意消息队列的内存占用和传输效率 + */ struct log_message_t { + /** + * @brief 日志级别 + * + * ## 含义和作用 + * 表示日志消息的严重程度或重要性级别,用于日志的分类和过滤。 + * + * ## 数据类型选择原因 + * - 使用uint32_t而非枚举类型,提供更好的序列化兼容性 + * - 32位整数足够容纳常见的日志级别定义(如spdlog的级别) + * - 无符号类型避免负值,确保语义清晰 + * + * ## 取值范围和约束 + * 通常对应标准日志级别(参考spdlog或其他日志库): + * - 0: TRACE - 最详细的调试信息 + * - 1: DEBUG - 调试信息 + * - 2: INFO - 一般信息 + * - 3: WARN - 警告信息 + * - 4: ERROR - 错误信息 + * - 5: CRITICAL - 严重错误 + * - 6: OFF - 关闭日志 + * + * ## 使用注意事项 + * - 发送端和接收端应使用相同的日志级别定义 + * - 建议定义常量或枚举来确保一致性 + * - Host端可根据此级别决定是否输出或如何格式化日志 + * + * @warning 确保level值在有效范围内,避免接收端处理时出现未定义行为 + */ uint32_t level; + + /** + * @brief 日志消息内容 + * + * ## 含义和作用 + * 包含实际的日志文本内容,可能包括时间戳、源文件位置、 + * 函数名称以及具体的日志信息。 + * + * ## 数据类型选择原因 + * - std::string提供动态内存管理,支持任意长度的消息 + * - 自动处理内存分配和释放,避免内存泄漏 + * - 支持UTF-8编码,可以包含国际化字符 + * - 提供丰富的字符串操作接口 + * + * ## 取值范围和约束 + * - 理论上无长度限制,但实际应用中建议限制单条消息大小 + * - 推荐单条日志不超过8KB,避免传输效率问题 + * - 超长消息应考虑截断或分段传输 + * + * ## 使用注意事项 + * - 消息应该是已格式化的完整字符串,包含所有必要信息 + * - 避免在消息中包含二进制数据,应使用文本表示 + * - 注意字符编码一致性,推荐使用UTF-8 + * - 在序列化时注意字符串的长度字段编码 + * - 大量日志可能导致性能问题,应在Engine端进行适当的日志级别过滤 + * + * @note 对于结构化日志,可考虑使用JSON格式编码消息内容 + * @note 在跨语言通信时,注意std::string的序列化兼容性 + */ std::string message; }; } diff --git a/src/network/rpc/host_rpc.h b/src/network/rpc/host_rpc.h index 863d595..70a6d08 100644 --- a/src/network/rpc/host_rpc.h +++ b/src/network/rpc/host_rpc.h @@ -1,8 +1,185 @@ +/** + * @file host_rpc.h + * @brief Host端RPC数据结构定义 + * + * 本文件定义了Host进程向Engine进程发送远程过程调用(RPC)时使用的数据结构。 + * 这些数据结构用于Host进程与Engine进程之间的进程间通信(IPC),主要用于 + * 传递配置信息、初始化参数和控制命令等。 + * + * ## 文件在network模块中的作用 + * - 定义Host端的RPC消息格式,确保进程间通信协议的一致性 + * - 作为Host进程和Engine进程握手和初始化流程的核心组件 + * - 配合Transport子模块(如ZMQ)和SHM子模块实现高效的跨进程通信 + * + * ## 与其他模块的关系 + * - **Transport子模块**: 本文件定义的数据结构通过Transport层(ZMQ)进行传输 + * - **SHM子模块**: setup_t中的shm_name用于建立共享内存连接 + * - **Process Manager**: Host进程使用setup_t置Engine进程的运行环境 + * - **Engine端RPC**: 与engine_rpc.h协同工作,形成双向通信的数据格式定义 + * + * ## 主要数据结构 + * - setup_t: Host端发送给Engine端的初始化配置结构 + * + * ## 通信流程 + * 1. Host进程启动Engine进程 + * 2. Host进程创建共享内存区域 + * 3. Host进程通过RPC发送setup_t消息,告知Engine共享内存名称 + * 4. Engine进程接收setup_t,连接到指定的共享内存 + * 5. 后续通过共享内存进行高效的音频数据传输 + * + * @note 此文件仅定义数据结构,不包含序列化/反序列化逻辑 + * @note setup_t是Engine初始过程中的关键配置信息 + */ + #pragma once #include namespace host_rpc { + /** + * @struct setup_t + * @brief Host进程发送给Engine进程的初始化配置结构 + * + * ## 用途和设计目的 + * 此结构体用于在Engine进程启动后,由Host进程向Engine进程传递关键的 + * 初始化配置信息。最重要的是共享内存(SHM)的名称,Engine进程需要 + * 这个信息才能连接到Host创建的共享内存区域,从而建立高效的音频数据 + * 传输通道。 + * + * ## 使用场景 + * 1. **进程启动握手**: Engine进程启动后,Host通过此结构告知连接信息 + * 2. **共享内存建立**: Engine使用shm_name连接到Host创建的共享内存 + * 3. **资源初始化**: 确保Engine进程能够访问必要的通信资源 + * 4. **配置同步**: 将Host端的配置传递给Engine端 + * + * ## 通信时序 + * ``` + * Host Engine + * | | + * |-- 1. 启动Engine进程 ---------------->| + * | | + * |-- 2. 创建共享内存(SHM) ------------->| + * | | + * |-- 3. 发送setup_t(shm_name) -------->| + * | | + * | |-- 4. 连接到SHM + * | | + * |<- 5. 返回初始化成功确认 -------------| + * | | + * |== 6. 开始通过SHM传输音频数据 ========| + * ``` + * + * ## 与其他组件的交互 + * - **Shared Memory Manager**: shm_name用于SharedMemoryManager的连接 + * - **Transport Layer**: 通过ZMQ等传输层发送此结构体 + * - **Process Manager**: Host的ProcessManager负责发送setup_t + * - **Engine RPC Handler**: Engine端接收并处理setup_t消息 + * + * ## 设计考虑 + * - 使用简单的结构体,易于序列化和版本控制 + * - 当前只包含shm_name,但设计上可扩展添加其他配置参数 + * - 未来可能扩展的字段:采样率、缓冲区大小、通道数等 + * + * ## 扩展性设计 + * 虽然当前只有一个字段,但结构体设计考虑了未来的扩展性: + * ```cpp + * // 未来可能的扩展 + * struct setup_t { + * std::string shm_name; // 共享内存名称 + * uint32_t sample_rate; // 采样率 + * uint32_t buffer_size; // 缓冲区大小 + * uint32_t num_channels; // 通道数 + * std::string config_path; // 配置文件路径 + * }; + * ``` + * + * @note 此结构体必须在Engine进程完全初始化之前发送 + * @note shm_name的有效性直接影响Engine进程能否正常工作 + * @warning 如果shm_name错误或共享内存不存在,Engine将无法正常运行 + */ struct setup_t { + /** + * @brief 共享内存区域的名称 + * + * ## 含义和作用 + * 指定Host进程创建的共享内存(Shared Memory)区域的唯一标识符。 + * Engine进程使用这个名称来连接到同一块共享内存,从而实现 + * 高效的跨进程音频数据传输(零拷贝)。 + * + * ## 数据类型选择原因 + * - std::string提供灵活的字符串存储,支持各种命名规范 + * - 不同操作系统的共享内存命名规则不同,string提供足够的灵活性 + * - 自动管理内存,避免缓冲区溢出和内存泄漏 + * - 便于日志记录和调试 + * + * ## 取值范围和约束 + * **命名规范(依操作系统而异)**: + * + * **Windows平台**: + * - 格式: "Local\\name" 或 "Global\\name" + * - 示例: "Local\\AlichoAudioEngine_SHM_12345" + * - 最大长度: 通常不超过MAX_PATH (260字符) + * - 字符限制: 不能包含反斜杠(前缀外) + * + * **Linux/Unix平台**: + * - 格式: "/name" (必须以/开头) + * - 示例: "/alicho_audio_shm_12345" + * - 最大长度: NAME_MAX (通常255字符) + * - 字符限制: 不能包含/(除开头外) + * + * **macOS平台**: + * - 格式: 类似Linux,但有额外限制 + * - 最大长度: 通常31字符(较短) + * - 示例: "/alicho_shm_12345" + * + * **命名最佳实践**: + * - 包含应用程序标识: "alicho_"前缀 + * - 包含唯一标识符: 进程ID、时间戳或UUID + * - 避免特殊字符: 只使用字母、数字、下划线 + * - 示例: "alicho_audio_engine_shm_1234567890" + * + * ## 使用注意事项 + * + * **创建流程**: + * 1. Host进程生成唯一的shm_name(可使用进程ID或时间戳) + * 2. Host调用SharedMemoryManager创建共享内存 + * 3. Host通过RPC将shm_name发送给Engine + * 4. Engine使用相同的shm_name连接到共享内存 + * + * **错误处理**: + * - 如果shm_name为空,Engine应返回错误 + * - 如果指定的共享内存不存在,Engine应尝试重连或报错 + * - 如果shm_name格式不符合操作系统规范,创建将失败 + * + * **安全考虑**: + * - shm_name应该难以猜测,避免未授权访问 + * - 可以包含随机组件或加密哈希值 + * - 在多用户系统上,注意权限设置 + * + * **生命周期管理**: + * - Host负责创建和销毁共享内存 + * - Engine退出时应正确断开共享内存连接 + * - 避免共享内存泄漏(进程崩溃时的清理) + * + * **调试技巧**: + * - 记录完整的shm_name到日志 + * - 使用操作系统工具检查共享内存是否存在 + * * Windows: Process Explorer + * * Linux: ls /dev/shm/ + * * macOS: ipcs命令 + * + * ## 相关函数 + * - SharedMemoryManager::create(shm_name, size) - Host端创建 + * - SharedMemoryManager::open(shm_name) - Engine端打开 + * + * @see SharedMemoryManager 共享内存管理器实现 + * @see LockFreeRingBuffer 基于共享内存的无锁环形缓冲区 + * @see TripleBuffer 基于共享内存的三缓冲实现 + * + * @note 此字段是Engine进程初始化的关键依赖 + * @note 确保Host在发送setup_t之前已成功创建共享内存 + * @warning 错误的shm_name会导致Engine进程无法启动 + * @warning 在跨平台应用中,注意不同操作系统的命名差异 + */ std::string shm_name; }; struct shutdown_t { diff --git a/src/network/shm/interprocess_synchronization.cpp b/src/network/shm/interprocess_synchronization.cpp index 56474c1..fda9897 100644 --- a/src/network/shm/interprocess_synchronization.cpp +++ b/src/network/shm/interprocess_synchronization.cpp @@ -1,12 +1,66 @@ +/** + * @file interprocess_synchronization.cpp + * @brief 跨进程同步管理器的实现文件 + * + * 本文件实现了 interprocess_synchronization 类的所有方法,包括: + * - 互斥量的创建、打开、锁定、解锁和移除 + * - 条件变量的创建、打开、等待、通知和移除 + * - 信号量的创建、打开、等待、释放和移除 + * - RAII风格的作用域锁 + * + * ## 实现要点 + * 1. 基于 Boost.Interprocess 的命名同步原语 + * 2. 所有方法都是线程安全的(使用 local_mutex_ 保护) + * 3. 使用绝对时间实现超时,避免时钟漂移 + * 4. 完善的错误处理和日志记录 + * 5. 维护本地缓存减少系统调用开销 + * + * ## 技术细节 + * - 命名原语通过操作系统共享(POSIX或Windows机制) + * - 超时使用 Boost.Date_Time 的绝对时间 + * - 异常处理:捕获所有Boost异常并转换为错误码 + * + * @see interprocess_synchronization.h + */ + #include "interprocess_synchronization.h" #include #include #include +/** + * @brief 构造函数 + * + * 初始化跨进程同步管理器,保存配置参数。 + * + * ## 初始化行为 + * - 保存配置参数副本 + * - 初始化空的同步对象映射表 + * - 不创建任何同步原语(延迟创建) + * + * @param config 共享内存配置,包含同步原语的命名信息 + * + * @note 构造后需要显式调用 create_xxx 或 open_xxx 方法 + * @note 轻量级操作,无系统调用 + */ interprocess_synchronization::interprocess_synchronization(const shared_memory_config& config) : config_(config) { log_module_info(INTERPROCESS_SYNCHRONIZATION_LOG_MODULE, "创建跨进程同步管理器"); } +/** + * @brief 析构函数 + * + * 清理所有本地资源,释放已打开的同步对象。 + * + * ## 清理行为 + * - 清空互斥量映射表(智能指针自动析构) + * - 清空条件变量映射表 + * - 清空信号量映射表 + * - 不从系统中移除命名原语(需显式调用 remove_xxx) + * + * @note 移除操作应由创建者进程负责 + * @note 即使析构,系统中的命名原语仍然存在 + */ interprocess_synchronization::~interprocess_synchronization() { mutexes_.clear(); conditions_.clear(); @@ -14,30 +68,84 @@ interprocess_synchronization::~interprocess_synchronization() { log_module_debug(INTERPROCESS_SYNCHRONIZATION_LOG_MODULE, "销毁跨进程同步管理器"); } +// ============================================================================ +// 互斥量操作实现 +// ============================================================================ + +/** + * @brief 创建命名互斥量 + * + * 在系统中创建一个新的命名互斥量,并缓存到本地映射表。 + * + * ## 实现流程 + * 1. 获取本地锁保护映射表(线程安全) + * 2. 尝试移除可能存在的同名互斥量(清理旧对象) + * 3. 使用 create_only 模式创建新互斥量 + * 4. 存储到本地映射表以供后续使用 + * 5. 记录调试日志 + * + * ## 异常处理 + * - 捕获 Boost.Interprocess 异常 + * - 转换为统一的错误码 + * - 记录详细的错误信息到日志 + * + * @param name 互斥量的唯一名称 + * @return shared_memory_error::SUCCESS 创建成功 + * @return shared_memory_error::CREATION_FAILED 创建失败 + * + * @note 线程安全:使用 local_mutex_ 保护 + * @note 如果名称已存在,会先移除旧的再创建新的 + */ shared_memory_error interprocess_synchronization::create_mutex(const std::string& name) { + // 获取本地互斥量,保护映射表的线程安全访问 std::lock_guard lock(local_mutex_); try { // 先尝试移除可能存在的同名互斥量 + // 这样可以处理之前进程崩溃留下的僵尸对象 boost::interprocess::named_mutex::remove(name.c_str()); + // 使用 create_only 模式创建互斥量 + // 如果已存在会抛出异常(但我们已经移除了) auto mutex = std::make_unique(boost::interprocess::create_only, name.c_str()); + // 缓存到本地映射表,后续操作直接使用缓存对象 mutexes_[name] = std::move(mutex); log_module_debug(INTERPROCESS_SYNCHRONIZATION_LOG_MODULE, "创建互斥量: %s", name.c_str()); return shared_memory_error::SUCCESS; } catch (const boost::interprocess::interprocess_exception& e) { + // 捕获创建失败的异常,如权限不足、资源耗尽等 log_module_error(INTERPROCESS_SYNCHRONIZATION_LOG_MODULE, "创建互斥量失败 '%s': %s", name.c_str(), e.what()); return shared_memory_error::CREATION_FAILED; } } +/** + * @brief 打开已存在的命名互斥量 + * + * 打开由其他进程创建的互斥量,用于跨进程同步。 + * + * ## 实现流程 + * 1. 获取本地锁保护映射表 + * 2. 使用 open_only 模式打开互斥量 + * 3. 存储到本地映射表 + * 4. 记录调试日志 + * + * @param name 互斥量名称 + * @return shared_memory_error::SUCCESS 打开成功 + * @return shared_memory_error::OPEN_FAILED 打开失败(互斥量不存在) + * + * @note 线程安全 + * @note 打开前必须由某个进程先创建该互斥量 + */ shared_memory_error interprocess_synchronization::open_mutex(const std::string& name) { std::lock_guard lock(local_mutex_); try { + // 使用 open_only 模式打开已存在的互斥量 + // 如果不存在会抛出异常 auto mutex = std::make_unique(boost::interprocess::open_only, name.c_str()); mutexes_[name] = std::move(mutex); @@ -46,35 +154,99 @@ shared_memory_error interprocess_synchronization::open_mutex(const std::string& return shared_memory_error::SUCCESS; } catch (const boost::interprocess::interprocess_exception& e) { + // 打开失败,通常是因为互斥量不存在 log_module_error(INTERPROCESS_SYNCHRONIZATION_LOG_MODULE, "打开互斥量失败 '%s': %s", name.c_str(), e.what()); return shared_memory_error::OPEN_FAILED; } } +/** + * @brief 锁定互斥量 + * + * 获取互斥量的所有权,支持超时等待。 + * + * ## 实现细节 + * 1. 获取本地锁查找互斥量对象 + * 2. 如果指定了超时,使用 timed_lock + * - 计算绝对超时时间点(当前时间 + timeout) + * - 使用 Boost.Date_Time 的时间类型 + * 3. 如果未指定超时,使用 lock(无限等待) + * + * ## 超时机制 + * - 使用绝对时间而非相对时间 + * - 避免系统时钟调整导致的问题 + * - universal_time 使用 UTC 时间 + * + * ## 锁定语义 + * - 如果当前未被锁定:立即获取并返回 + * - 如果已被其他进程锁定:阻塞等待 + * - 如果超时:返回失败 + * + * @param name 互斥量名称 + * @param timeout 超时时间(毫秒),0表示无限等待 + * @return shared_memory_error::SUCCESS 成功获取锁 + * @return shared_memory_error::NOT_FOUND 互斥量不存在 + * @return shared_memory_error::SYNCHRONIZATION_FAILED 超时或其他错误 + * + * @note 线程安全 + * @note 必须由同一线程解锁 + * + * @warning 获取锁后必须调用 unlock_mutex + * @warning 不支持递归锁定 + */ shared_memory_error interprocess_synchronization::lock_mutex(const std::string& name, std::chrono::milliseconds timeout) { std::lock_guard lock(local_mutex_); + // 在映射表中查找互斥量 auto it = mutexes_.find(name); if (it == mutexes_.end()) { return shared_memory_error::NOT_FOUND; } try { if (timeout.count() > 0) { + // 使用超时锁定 + // 计算绝对超时时间点 = 当前时间 + 相对超时 const auto abs_time = boost::posix_time::microsec_clock::universal_time() + boost::posix_time::milliseconds(timeout.count()); + // 尝试在指定时间前获取锁 if (it->second->timed_lock(abs_time)) { return shared_memory_error::SUCCESS; } - return shared_memory_error::SYNCHRONIZATION_FAILED; // 超时 + + // 超时未能获取锁 + return shared_memory_error::SYNCHRONIZATION_FAILED; } + + // 无限等待锁定 it->second->lock(); return shared_memory_error::SUCCESS; } catch (const boost::interprocess::interprocess_exception& e) { + // 捕获锁定过程中的异常 log_module_error(INTERPROCESS_SYNCHRONIZATION_LOG_MODULE, "锁定互斥量失败 '%s': %s", name.c_str(), e.what()); return shared_memory_error::SYNCHRONIZATION_FAILED; } } +/** + * @brief 解锁互斥量 + * + * 释放互斥量的所有权,允许其他进程/线程获取。 + * + * ## 实现流程 + * 1. 获取本地锁查找互斥量 + * 2. 调用 unlock() 释放所有权 + * 3. 唤醒等待的线程(由操作系统处理) + * + * @param name 互斥量名称 + * @return shared_memory_error::SUCCESS 成功解锁 + * @return shared_memory_error::NOT_FOUND 互斥量不存在 + * @return shared_memory_error::SYNCHRONIZATION_FAILED 解锁失败 + * + * @note 线程安全 + * @note 必须由持有锁的线程调用 + * + * @warning 解锁未持有的锁会导致未定义行为 + */ shared_memory_error interprocess_synchronization::unlock_mutex(const std::string& name) { std::lock_guard lock(local_mutex_); @@ -82,22 +254,52 @@ shared_memory_error interprocess_synchronization::unlock_mutex(const std::string if (it == mutexes_.end()) { return shared_memory_error::NOT_FOUND; } try { + // 释放互斥量 it->second->unlock(); return shared_memory_error::SUCCESS; } catch (const boost::interprocess::interprocess_exception& e) { + // 解锁失败,可能是未持有锁或锁已损坏 log_module_error(INTERPROCESS_SYNCHRONIZATION_LOG_MODULE, "解锁互斥量失败 '%s': %s", name.c_str(), e.what()); return shared_memory_error::SYNCHRONIZATION_FAILED; } } +/** + * @brief 移除命名互斥量 + * + * 从本地缓存和系统中移除互斥量。 + * + * ## 实现流程 + * 1. 获取本地锁 + * 2. 从本地映射表中移除(智能指针自动析构) + * 3. 调用静态 remove 方法从系统移除 + * 4. 记录调试日志 + * + * ## 清理效果 + * - 本地对象被销毁 + * - 系统资源被释放 + * - 其他进程无法再打开此互斥量 + * - 已打开的引用不受影响(但不应继续使用) + * + * @param name 互斥量名称 + * @return shared_memory_error::SUCCESS 成功移除 + * @return shared_memory_error::SYNCHRONIZATION_FAILED 移除失败 + * + * @note 线程安全 + * @note 应由创建者进程调用 + * + * @warning 移除前应确保没有进程持有该锁 + */ shared_memory_error interprocess_synchronization::remove_mutex(const std::string& name) { std::lock_guard lock(local_mutex_); try { + // 从本地缓存中移除 auto it = mutexes_.find(name); if (it != mutexes_.end()) { mutexes_.erase(it); } + // 从系统中移除命名对象 boost::interprocess::named_mutex::remove(name.c_str()); log_module_debug(INTERPROCESS_SYNCHRONIZATION_LOG_MODULE, "移除互斥量: %s", name.c_str()); @@ -109,6 +311,28 @@ shared_memory_error interprocess_synchronization::remove_mutex(const std::string } } +// ============================================================================ +// 条件变量操作实现 +// ============================================================================ + +/** + * @brief 创建命名条件变量 + * + * 在系统中创建一个新的命名条件变量。 + * + * ## 实现流程 + * 1. 获取本地锁 + * 2. 移除可能存在的同名条件变量 + * 3. 创建新的条件变量 + * 4. 缓存到本地映射表 + * + * @param name 条件变量的唯一名称 + * @return shared_memory_error::SUCCESS 创建成功 + * @return shared_memory_error::CREATION_FAILED 创建失败 + * + * @note 线程安全 + * @note 条件变量必须与互斥量配合使用 + */ shared_memory_error interprocess_synchronization::create_condition(const std::string& name) { std::lock_guard lock(local_mutex_); @@ -116,6 +340,7 @@ shared_memory_error interprocess_synchronization::create_condition(const std::st // 先尝试移除可能存在的同名条件变量 boost::interprocess::named_condition::remove(name.c_str()); + // 创建新的条件变量 auto condition = std::make_unique(boost::interprocess::create_only, name.c_str()); conditions_[name] = std::move(condition); @@ -129,10 +354,22 @@ shared_memory_error interprocess_synchronization::create_condition(const std::st } } +/** + * @brief 打开已存在的命名条件变量 + * + * 打开由其他进程创建的条件变量。 + * + * @param name 条件变量名称 + * @return shared_memory_error::SUCCESS 打开成功 + * @return shared_memory_error::OPEN_FAILED 打开失败 + * + * @note 线程安全 + */ shared_memory_error interprocess_synchronization::open_condition(const std::string& name) { std::lock_guard lock(local_mutex_); try { + // 打开已存在的条件变量 auto condition = std::make_unique(boost::interprocess::open_only, name.c_str()); conditions_[name] = std::move(condition); @@ -146,26 +383,77 @@ shared_memory_error interprocess_synchronization::open_condition(const std::stri } } +/** + * @brief 等待条件变量 + * + * 在条件变量上阻塞等待,直到被通知或超时。 + * + * ## 实现细节 + * 1. 获取本地锁查找条件变量和互斥量 + * 2. 创建 scoped_lock 自动管理互斥量 + * 3. 调用 wait 或 timed_wait + * 4. 被唤醒后重新获取互斥量 + * + * ## 等待语义(重要) + * - 调用 wait 前必须持有关联的互斥量 + * - wait 内部会原子地: + * 1. 释放互斥量 + * 2. 阻塞线程 + * 3. 被唤醒后重新获取互斥量 + * - 返回时保证持有互斥量 + * + * ## 虚假唤醒 + * - 即使没有 notify,wait 也可能返回 + * - 必须在循环中使用: + * ```cpp + * while (!condition_met) { + * wait_condition(...); + * } + * ``` + * + * @param condition_name 条件变量名称 + * @param mutex_name 关联的互斥量名称 + * @param timeout 超时时间(毫秒) + * @return shared_memory_error::SUCCESS 成功被唤醒 + * @return shared_memory_error::NOT_FOUND 条件变量或互斥量不存在 + * @return shared_memory_error::SYNCHRONIZATION_FAILED 超时或等待失败 + * + * @note 线程安全 + * @note 返回时会重新持有互斥量 + * + * @warning 必须在持有互斥量时调用 + * @warning 应在循环中检查条件,处理虚假唤醒 + */ shared_memory_error interprocess_synchronization::wait_condition(const std::string& condition_name, const std::string& mutex_name, std::chrono::milliseconds timeout) { std::lock_guard lock(local_mutex_); + // 查找条件变量 auto cond_it = conditions_.find(condition_name); if (cond_it == conditions_.end()) { return shared_memory_error::NOT_FOUND; } + // 查找关联的互斥量 auto mutex_it = mutexes_.find(mutex_name); if (mutex_it == mutexes_.end()) { return shared_memory_error::NOT_FOUND; } try { if (timeout.count() > 0) { + // 带超时的等待 const auto abs_time = boost::posix_time::microsec_clock::universal_time() + boost::posix_time::milliseconds(timeout.count()); + + // scoped_lock 自动管理互斥量的锁定状态 + // wait 会原子地释放锁并阻塞,唤醒后重新获取锁 scoped_lock named_lock(*mutex_it->second); if (cond_it->second->timed_wait(named_lock, abs_time)) { return shared_memory_error::SUCCESS; } - return shared_memory_error::SYNCHRONIZATION_FAILED; // 超时 + + // 超时 + return shared_memory_error::SYNCHRONIZATION_FAILED; } + + // 无限等待 scoped_lock named_lock(*mutex_it->second); cond_it->second->wait(named_lock); return shared_memory_error::SUCCESS; @@ -179,6 +467,40 @@ shared_memory_error interprocess_synchronization::wait_condition(const std::stri } } +/** + * @brief 通知条件变量 + * + * 唤醒等待在条件变量上的一个或所有线程。 + * + * ## 通知语义 + * - notify_one: 唤醒一个等待线程(具体哪个由调度器决定) + * - notify_all: 唤醒所有等待线程 + * + * ## 通知时机 + * - 可以在持有互斥量时通知 + * - 也可以在释放互斥量后通知 + * - 释放后通知可以减少"惊群"和上下文切换 + * + * ## 典型模式 + * ```cpp + * // 修改共享状态 + * lock_mutex("mutex"); + * shared_data = new_value; + * unlock_mutex("mutex"); + * + * // 通知等待者 + * notify_condition("cond", false); + * ``` + * + * @param name 条件变量名称 + * @param notify_all true表示通知所有等待者,false表示通知一个 + * @return shared_memory_error::SUCCESS 通知成功 + * @return shared_memory_error::NOT_FOUND 条件变量不存在 + * @return shared_memory_error::SYNCHRONIZATION_FAILED 通知失败 + * + * @note 线程安全 + * @note 如果没有等待者,通知会被忽略(不会缓存) + */ shared_memory_error interprocess_synchronization::notify_condition(const std::string& name, bool notify_all) { std::lock_guard lock(local_mutex_); @@ -187,10 +509,12 @@ shared_memory_error interprocess_synchronization::notify_condition(const std::st try { if (notify_all) { + // 唤醒所有等待线程 it->second->notify_all(); log_module_debug(INTERPROCESS_SYNCHRONIZATION_LOG_MODULE, "通知所有等待者: %s", name.c_str()); } else { + // 唤醒一个等待线程 it->second->notify_one(); log_module_debug(INTERPROCESS_SYNCHRONIZATION_LOG_MODULE, "通知单个等待者: %s", name.c_str()); } @@ -202,13 +526,29 @@ shared_memory_error interprocess_synchronization::notify_condition(const std::st } } +/** + * @brief 移除命名条件变量 + * + * 从本地缓存和系统中移除条件变量。 + * + * @param name 条件变量名称 + * @return shared_memory_error::SUCCESS 成功移除 + * @return shared_memory_error::SYNCHRONIZATION_FAILED 移除失败 + * + * @note 线程安全 + * @note 应由创建者调用 + * + * @warning 移除前应确保没有线程在等待 + */ shared_memory_error interprocess_synchronization::remove_condition(const std::string& name) { std::lock_guard lock(local_mutex_); try { + // 从本地缓存移除 auto it = conditions_.find(name); if (it != conditions_.end()) { conditions_.erase(it); } + // 从系统移除 boost::interprocess::named_condition::remove(name.c_str()); log_module_debug(INTERPROCESS_SYNCHRONIZATION_LOG_MODULE, "移除条件变量: %s", name.c_str()); @@ -220,6 +560,33 @@ shared_memory_error interprocess_synchronization::remove_condition(const std::st } } +// ============================================================================ +// 信号量操作实现 +// ============================================================================ + +/** + * @brief 创建命名信号量 + * + * 在系统中创建一个新的命名信号量,带有初始计数。 + * + * ## 实现流程 + * 1. 获取本地锁 + * 2. 移除可能存在的同名信号量 + * 3. 创建新信号量,设置初始计数 + * 4. 缓存到本地映射表 + * + * ## 初始计数 + * - 表示初始可用资源数量 + * - 例如:连接池有10个连接,初始计数设为10 + * - wait会减少计数,post会增加计数 + * + * @param name 信号量的唯一名称 + * @param initial_count 初始计数值 + * @return shared_memory_error::SUCCESS 创建成功 + * @return shared_memory_error::CREATION_FAILED 创建失败 + * + * @note 线程安全 + */ shared_memory_error interprocess_synchronization::create_semaphore(const std::string& name, unsigned int initial_count) { std::lock_guard lock(local_mutex_); @@ -228,6 +595,7 @@ interprocess_synchronization::create_semaphore(const std::string& name, unsigned // 先尝试移除可能存在的同名信号量 boost::interprocess::named_semaphore::remove(name.c_str()); + // 创建信号量,指定初始计数 auto semaphore = std::make_unique(boost::interprocess::create_only, name.c_str(), initial_count); @@ -243,10 +611,22 @@ interprocess_synchronization::create_semaphore(const std::string& name, unsigned } } +/** + * @brief 打开已存在的命名信号量 + * + * 打开由其他进程创建的信号量。 + * + * @param name 信号量名称 + * @return shared_memory_error::SUCCESS 打开成功 + * @return shared_memory_error::OPEN_FAILED 打开失败 + * + * @note 线程安全 + */ shared_memory_error interprocess_synchronization::open_semaphore(const std::string& name) { std::lock_guard lock(local_mutex_); try { + // 打开已存在的信号量 auto semaphore = std::make_unique(boost::interprocess::open_only, name.c_str()); semaphores_[name] = std::move(semaphore); @@ -260,6 +640,44 @@ shared_memory_error interprocess_synchronization::open_semaphore(const std::stri } } +/** + * @brief 等待信号量(P操作) + * + * 获取一个资源,如果没有可用资源则阻塞等待。 + * + * ## 操作语义 + * - 如果计数 > 0: + * 1. 计数减1 + * 2. 立即返回成功 + * - 如果计数 = 0: + * 1. 阻塞线程 + * 2. 等待其他线程 post + * 3. 被唤醒后计数减1并返回 + * + * ## 超时行为 + * - 指定超时:在指定时间内未获取到资源则返回失败 + * - 不指定超时:无限等待直到获取资源 + * + * ## 使用示例 + * ```cpp + * // 从资源池获取资源 + * if (wait_semaphore("pool_sem") == SUCCESS) { + * // 使用资源 + * use_resource(); + * // 归还资源 + * post_semaphore("pool_sem"); + * } + * ``` + * + * @param name 信号量名称 + * @param timeout 超时时间(毫秒) + * @return shared_memory_error::SUCCESS 成功获取资源 + * @return shared_memory_error::NOT_FOUND 信号量不存在 + * @return shared_memory_error::SYNCHRONIZATION_FAILED 超时或等待失败 + * + * @note 线程安全 + * @note 获取资源后记得调用 post_semaphore 释放 + */ shared_memory_error interprocess_synchronization::wait_semaphore(const std::string& name, std::chrono::milliseconds timeout) { std::lock_guard lock(local_mutex_); @@ -269,12 +687,17 @@ shared_memory_error interprocess_synchronization::wait_semaphore(const std::stri try { if (timeout.count() > 0) { + // 带超时的等待 const auto abs_time = boost::posix_time::microsec_clock::universal_time() + boost::posix_time::milliseconds(timeout.count()); if (it->second->timed_wait(abs_time)) { return shared_memory_error::SUCCESS; } - return shared_memory_error::SYNCHRONIZATION_FAILED; // 超时 + + // 超时未获取到资源 + return shared_memory_error::SYNCHRONIZATION_FAILED; } + + // 无限等待 it->second->wait(); return shared_memory_error::SUCCESS; } @@ -284,6 +707,28 @@ shared_memory_error interprocess_synchronization::wait_semaphore(const std::stri } } +/** + * @brief 释放信号量(V操作) + * + * 释放一个资源,增加信号量计数。 + * + * ## 操作语义 + * 1. 计数加1 + * 2. 如果有等待线程,唤醒一个 + * 3. 被唤醒的线程会获取资源(计数减1) + * + * ## 注意事项 + * - 每次 wait 成功后都应该对应一次 post + * - post 次数不应超过初始计数 + wait 成功次数 + * - 过多的 post 可能导致计数溢出或逻辑错误 + * + * @param name 信号量名称 + * @return shared_memory_error::SUCCESS 成功释放 + * @return shared_memory_error::NOT_FOUND 信号量不存在 + * @return shared_memory_error::SYNCHRONIZATION_FAILED 释放失败 + * + * @note 线程安全 + */ shared_memory_error interprocess_synchronization::post_semaphore(const std::string& name) { std::lock_guard lock(local_mutex_); @@ -291,6 +736,7 @@ shared_memory_error interprocess_synchronization::post_semaphore(const std::stri if (it == semaphores_.end()) { return shared_memory_error::NOT_FOUND; } try { + // 增加信号量计数,唤醒一个等待线程 it->second->post(); return shared_memory_error::SUCCESS; } @@ -300,13 +746,30 @@ shared_memory_error interprocess_synchronization::post_semaphore(const std::stri } } +/** + * @brief 移除命名信号量 + * + * 从本地缓存和系统中移除信号量。 + * + * @param name 信号量名称 + * @return shared_memory_error::SUCCESS 成功移除 + * @return shared_memory_error::SYNCHRONIZATION_FAILED 移除失败 + * + * @note 线程安全 + * @note 应由创建者调用 + * + * @warning 移除前应确保没有线程在等待 + * @warning 移除前应确保资源已全部归还 + */ shared_memory_error interprocess_synchronization::remove_semaphore(const std::string& name) { std::lock_guard lock(local_mutex_); try { + // 从本地缓存移除 auto it = semaphores_.find(name); if (it != semaphores_.end()) { semaphores_.erase(it); } + // 从系统移除 boost::interprocess::named_semaphore::remove(name.c_str()); log_module_debug(INTERPROCESS_SYNCHRONIZATION_LOG_MODULE, "移除信号量: %s", name.c_str()); @@ -318,14 +781,67 @@ shared_memory_error interprocess_synchronization::remove_semaphore(const std::st } } +// ============================================================================ +// RAII 作用域锁实现 +// ============================================================================ + +/** + * @brief 作用域锁构造函数 + * + * 构造时自动尝试获取互斥量的锁。 + * + * ## RAII 原理 + * - Resource Acquisition Is Initialization + * - 资源获取即初始化 + * - 对象构造时获取资源,析构时释放资源 + * - 利用 C++ 的作用域规则保证资源释放 + * + * ## 实现细节 + * 1. 保存同步管理器引用 + * 2. 保存互斥量名称 + * 3. 调用 lock_mutex 获取锁 + * 4. 根据返回值设置 locked_ 标志 + * + * ## 异常安全 + * - 即使后续代码抛出异常 + * - 作用域结束时析构函数仍会被调用 + * - 保证锁被正确释放 + * + * @param sync 同步管理器引用 + * @param mutex_name 互斥量名称 + * + * @note 构造后应检查 is_locked() 确认是否成功获取锁 + */ interprocess_synchronization::scoped_mutex_lock::scoped_mutex_lock(interprocess_synchronization& sync, const std::string& mutex_name) : sync_(sync), mutex_name_(mutex_name), locked_(false) { + // 尝试获取锁 auto result = sync_.lock_mutex(mutex_name_); + + // 根据返回值设置锁定状态 locked_ = (result == shared_memory_error::SUCCESS); } +/** + * @brief 作用域锁析构函数 + * + * 析构时自动释放互斥量的锁(如果持有)。 + * + * ## RAII 保证 + * - 无论正常退出还是异常退出 + * - 析构函数都会被调用 + * - 保证锁被正确释放 + * - 避免死锁和资源泄漏 + * + * ## 实现细节 + * - 只有成功获取锁时(locked_ == true)才解锁 + * - 避免解锁未持有的锁 + * - 异常安全,不抛出异常 + * + * @note 异常安全:析构函数不应抛出异常 + */ interprocess_synchronization::scoped_mutex_lock::~scoped_mutex_lock() { + // 只有成功获取锁时才释放 if (locked_) { sync_.unlock_mutex(mutex_name_); } } diff --git a/src/network/shm/interprocess_synchronization.h b/src/network/shm/interprocess_synchronization.h index acc3e61..2656fe2 100644 --- a/src/network/shm/interprocess_synchronization.h +++ b/src/network/shm/interprocess_synchronization.h @@ -1,3 +1,58 @@ +/** + * @file interprocess_synchronization.h + * @brief 跨进程同步管理器 - 提供进程间同步原语 + * + * 本文件实现了跨进程同步机制的管理,是Network模块共享内存基础设施的重要组成部分。 + * + * ## 模块定位 + * - 配合 shared_memory_manager 使用,提供进程间同步能力 + * - 为上层的无锁数据结构(ring buffer、triple buffer)提供备用同步方案 + * - 支持RPC和Transport层的跨进程协调 + * + * ## 核心功能 + * 1. **互斥量(Mutex)**:提供进程间的互斥访问控制 + * 2. **条件变量(Condition Variable)**:实现程间的等待/通知机制 + * 3. **信号量(Semaphore)**:提供计数信号量用于资源管理 + * 4. **RAII锁(Scoped Lock)**:自动管理锁的获取和释放 + * + * ## 设计特点 + * - 基于Boost.Interprocess的命名同步原语 + * - 统一的错误处理机制 + * - 支持超时操作 + * - 提供RAII封装保证异常安全 + * - 内部使用std::mutex保护本地数据结构 + * + * ## 使用场景 + * - 保护共享内存中的数据结构 + * - 协调多进程的访问顺序 + * - 实现生产者-消费者模式 + * - 控制并发访问数量 + * - 进程间事件通知 + * + * ## 技术实现 + * - 命名同步原语:通过名称在进程间共享 + * - 操作系统级别的同步机制(futex、semaphore等) + * - 超时支持:基于绝对时间的超时机制 + * - 本地缓存:维护已打开的同步对象避免重复创建 + * + * ## 线程安全性 + * - 类的所有方法都是线程安全的 + * - 内部使用 local_mutex_ 保护本地数据结构 + * - 同步原语本身保证进程间和线程间的正确同步 + * + * ## 性能考虑 + * - 命名原语有查找开销,建议复用已打开的对象 + * - 内部维护缓存减少系统调用 + * - 超时操作使用绝对时间避免时钟漂移 + * + * @note 需要与 shared_memory_manager 配合使用 + * @note 命名原语在系统范围内共享,名称必须唯一 + * @note 清理工作(remove)应由创建者负责 + * + * @see shared_memory_manager.h + * @see lock_free_ring_buffer.h (无锁替代方案) + */ + #pragma once #include #include @@ -5,61 +60,556 @@ #include "shared_memory_manager.h" +/// 日志模块标识符,用于标记该模块的日志输出 #define INTERPROCESS_SYNCHRONIZATION_LOG_MODULE "interprocess synchronization" +/** + * @class interprocess_synchronization + * @brief 跨进程同步管理器 + * + * 这个类封装了Boost.Interprocess提供的命名同步原语,提供了统一、 + * 易用的接口来管理进程间的互斥量、条件变量和信号量。 + * + * ## 核心职责 + * 1. 创建和打开命名同步原语 + * 2. 管理同步对象的生命周期 + * 3. 提供线程安全的操作接口 + * 4. 缓存已打开的同步对象 + * 5. 提供RAII风格的锁理 + * + * ## 同步原语说明 + * + * ### 互斥量(Mutex) + * - 提供互斥访问控制 + * - 同一时刻只有一个进程/线程可以持有锁 + * - 支持超时锁定 + * - 必须由持有者解锁 + * + * ### 条件变量(Condition Variable) + * - 实现等待/通知机制 + * - 必须与互斥量配合使用 + * - 支持单个通知和广播通知 + * - 支持超时等待 + * + * ### 信号量(Semaphore) + * - 计数信号量,控制并发访问数量 + * - 支持初始计数设置 + * - wait操作减少计数,post操作增加计数 + * - 支持超时等待 + * + * ## 使用模式 + * + * ### 互斥量示例 + * ```cpp + * interprocess_synchronization sync(config); + * sync.create_mutex("my_mutex"); + * sync.lock_mutex("my_mutex"); + * // 临界区代码 + * sync.unlock_mutex("my_mutex"); + * ``` + * + * ### RAII锁示例 + * ```cpp + * { + * scoped_mutex_lock lock(sync, "my_mutex"); + * if (lock.is_locked()) { + * // 临界区代码 + * } // 自动解锁 + * } + * ``` + * + * ### 条件变量示例 + * ```cpp + * // 等待者 + * sync.wait_condition("my_cond", "my_mutex"); + * + * // 通知者 + * sync.notify_condition("my_cond", false); // 通知一个 + * ``` + * + * ### 信号量示例 + * ```cpp + * sync.create_semaphore("my_sem", 3); // 初始计数3 + * sync.wait_semaphore("my_sem"); // 获取资源 + * // 使用资源 + * sync.post_semaphore("my_sem"); // 释放资源 + * ``` + * + * ## 线程安全性 + * - 所有公开方法都是线程安全的 + * - 可以从多个线程安全地调用 + * - 内部使用 local_mutex_ 保护共享状态 + * + * ## 性能考虑 + * - 首次创建/打开有系统调用开销 + * - 后续操作直接使用缓存的对象 + * - 超时操作可能涉及上下文切换 + * + * @note 命名原语的名称在系统范围内必须唯一 + * @note 不同进程可以使用相同名称打开同一原语 + * @note 析构时不自动移除原语,需显式调用remove方法 + */ class interprocess_synchronization { public: + /// Boost.Interprocess的命名互斥量类型别名 using interprocess_mutex = boost::interprocess::named_mutex; + + /// Boost.Interprocess的命名条件变量类型别名 using interprocess_condition = boost::interprocess::named_condition; + + /// Boost.Interprocess的命名信号量类型别名 using interprocess_semaphore = boost::interprocess::named_semaphore; + + /// Boost.Interprocess的作用域锁类型别名 + /// @note 用于自动管理互斥量的锁定/解锁 using scoped_lock = boost::interprocess::scoped_lock; + /** + * @brief 构造函数 + * + * 创建跨进程同步管理器实例。 + * + * @param config 共享内存配置,包含同步原语的名称信息 + * + * @note 构造函数不会创建任何同步原语 + * @note 需要显式调用 create_xxx 或 open_xxx 方法 + */ explicit interprocess_synchronization(const shared_memory_config& config); + + /** + * @brief 析构函数 + * + * 清理本地资源,释放已打开的同步对象。 + * + * @note 不会从系统中移除命名原语 + * @note 如需移除,应显式调用 remove_xxx 方法 + * @note 移除操作应由创建者负责 + */ ~interprocess_synchronization(); + // ======================================================================== // 互斥量操作 + // ======================================================================== + + /** + * @brief 创建命名互斥量 + * + * 在系统中创建一个新的命名互斥量。 + * + * ## 创建行为 + * - 如果名称已存在,先移除旧的互斥量 + * - 使用 create_only 模式创建新互斥量 + * - 创建后缓存到本地映射表 + * + * @param name 互斥量的唯一名称 + * @return shared_memory_error::SUCCESS 创建成功 + * @return shared_memory_error::CREATION_FAILED 创建失败 + * + * @note 线程安全 + * @note 名称在系统范围内必须唯一 + * @note 创建者负责在不再需要时调用 remove_mutex + * + * @warning 不要在不同类型的同步原语间使用相同名称 + */ shared_memory_error create_mutex(const std::string& name); + + /** + * @brief 打开已存在的命名互斥量 + * + * 打开由其他进程创建的互斥量。 + * + * @param name 互斥量名称 + * @return shared_memory_error::SUCCESS 打开成功 + * @return shared_memory_error::OPEN_FAILED 打开失败(互斥量不存在) + * + * @note 线程安全 + * @note 打开前必须由某个进程先创建 + */ shared_memory_error open_mutex(const std::string& name); + + /** + * @brief 锁定互斥量 + * + * 获取互斥量的所有权,如果已被其他进程持有则阻塞等待。 + * + * ## 锁定行为 + * - 如果指定超时,使用 timed_lock(绝对时间) + * - 如果不指定超时,使用 lock(无限等待) + * - 成功获取锁后,进入临界区 + * + * ## 超时机制 + * - 使用绝对时间避免时钟漂移问题 + * - 超时时间从当前时刻开始计算 + * - 超时返回 SYNCHRONIZATION_FAILED + * + * @param name 互斥量名称 + * @param timeout 超时时间(毫秒),默认5000ms + * @return shared_memory_error::SUCCESS 成功获取锁 + * @return shared_memory_error::NOT_FOUND 互斥量不存在 + * @return shared_memory_error::SYNCHRONIZATION_FAILED 超时或锁定失败 + * + * @note 线程安全 + * @note 必须由同一线程解锁 + * @note 不支持递归锁定 + * + * @warning 获取锁后必须调用 unlock_mutex 释放 + * @warning 死锁风险:注意锁的获取顺序 + */ shared_memory_error lock_mutex(const std::string& name, std::chrono::milliseconds timeout = std::chrono::milliseconds(5000)); + + /** + * @brief 解锁互斥量 + * + * 释放互斥量的所有权,允许其他进程获取。 + * + * @param name 互斥量名称 + * @return shared_memory_error::SUCCESS 成功解锁 + * @return shared_memory_error::NOT_FOUND 互斥量不存在 + * @return shared_memory_error::SYNCHRONIZATION_FAILED 解锁失败 + * + * @note 线程安全 + * @note 必须由持有锁的线程调用 + * + * @warning 解锁未持有的锁会导致未定义行为 + */ shared_memory_error unlock_mutex(const std::string& name); + + /** + * @brief 移除命名互斥量 + * + * 从系统中移除互斥量,释放系统资源。 + * + * ## 移除行为 + * - 从本地缓存中移除 + * - 调用系统API移除命名对象 + * - 移除后其他进程无法再打开 + * + * @param name 互斥量名称 + * @return shared_memory_error::SUCCESS 成功移除 + * @return shared_memory_error::SYNCHRONIZATION_FAILED 移除失败 + * + * @note 线程安全 + * @note 应由创建者进程调用 + * + * @warning 移除前应确保没有进程持有该锁 + * @warning 移除后已打开的引用可能变为无效 + */ shared_memory_error remove_mutex(const std::string& name); + // ======================================================================== // 条件变量操作 + // ======================================================================== + + /** + * @brief 创建命名条件变量 + * + * 在系统中创建一个新的命名条件变量。 + * + * ## 创建行为 + * - 先移除可能存在的同名条件变量 + * - 使用 create_only 模式创建 + * - 缓存到本地映射表 + * + * @param name 条件变量的唯一名称 + * @return shared_memory_error::SUCCESS 创建成功 + * @return shared_memory_error::CREATION_FAILED 创建失败 + * + * @note 线程安全 + * @note 条件变量必须与互斥量配合使用 + */ shared_memory_error create_condition(const std::string& name); + + /** + * @brief 打开已存在的命名条件变量 + * + * 打开由其他进程创建的条件变量。 + * + * @param name 条件变量名称 + * @return shared_memory_error::SUCCESS 打开成功 + * @return shared_memory_error::OPEN_FAILED 打开失败 + * + * @note 线程安全 + */ shared_memory_error open_condition(const std::string& name); + + /** + * @brief 等待条件变量 + * + * 阻塞当前线程,直到条件变量被通知或超时。 + * + * ## 等待语义 + * 1. 原子地释放关联的互斥量 + * 2. 阻塞线程直到被通知 + * 3. 被唤醒后重新获取互斥量 + * 4. 返回给调用者 + * + * ## 虚假唤醒 + * - 条件变量可能发生虚假唤醒 + * - 被唤醒后应重新检查条件 + * - 通常在循环中使用wait + * + * ## 使用模式 + * ```cpp + * sync.lock_mutex("mutex"); + * while (!condition_satisfied) { + * sync.wait_condition("cond", "mutex"); + * } + * // 条件已满足,继续处理 + * sync.unlock_mutex("mutex"); + * ``` + * + * @param condition_name 条件变量名称 + * @param mutex_name 关联的互斥量名称 + * @param timeout 超时时间(毫秒),默认5000ms + * @return shared_memory_error::SUCCESS 成功被唤醒 + * @return shared_memory_error::NOT_FOUND 条件变量或互斥量不存在 + * @return shared_memory_error::SYNCHRONIZATION_FAILED 超时或等待失败 + * + * @note 线程安全 + * @note 调用前必须持有关联的互斥量 + * @note 返回时会重新持有互斥量 + * + * @warning 必须在持有互斥量的情况下调用 + * @warning 应在循环中检查条件以处理虚假唤醒 + */ shared_memory_error wait_condition(const std::string& condition_name, const std::string& mutex_name, std::chrono::milliseconds timeout = std::chrono::milliseconds(5000)); + + /** + * @brief 通知条件变量 + * + * 唤醒等待在条件变量上的一个或所有线程。 + * + * ## 通知模式 + * - notify_one: 唤醒一个等待线程(如果有) + * - notify_all: 唤醒所有等待线程 + * + * ## 通知时机 + * - 通常在修改共享状态后调用 + * - 可以在持有或释放互斥量后调用 + * - 释放锁后通知可以减少上下文切换 + * + * @param name 条件变量名称 + * @param notify_all true表示通知所有等待者,false表示通知一个 + * @return shared_memory_error::SUCCESS 通知成功 + * @return shared_memory_error::NOT_FOUND 条件变量不存在 + * @return shared_memory_error::SYNCHRONIZATION_FAILED 通知失败 + * + * @note 线程安全 + * @note 如果没有等待者,通知会被忽略 + */ shared_memory_error notify_condition(const std::string& name, bool notify_all = false); + + /** + * @brief 移除命名条件变量 + * + * 从系统中移除条件变量。 + * + * @param name 条件变量名称 + * @return shared_memory_error::SUCCESS 成功移除 + * @return shared_memory_error::SYNCHRONIZATION_FAILED 移除失败 + * + * @note 线程安全 + * @note 应由创建者调用 + * + * @warning 移除前应确保没有线程在等待 + */ shared_memory_error remove_condition(const std::string& name); + // ======================================================================== // 信号量操作 + // ======================================================================== + + /** + * @brief 创建命名信号量 + * + * 在系统中创建一个新的命名信号量。 + * + * ## 信号量语义 + * - 内部维护一个非负整数计数 + * - wait操作:计数减1,如果计数为0则阻塞 + * - post操作:计数加1,唤醒一个等待线程 + * + * ## 应用场景 + * - 资源池管理(如连接池、缓冲区池) + * - 限制并发访问数量 + * - 生产者-消费者模 + * + * @param name 信号量的唯一名称 + * @param initial_count 初始计数值 + * @return shared_memory_error::SUCCESS 创建成功 + * @return shared_memory_error::CREATION_FAILED 创建失败 + * + * @note 线程安全 + * @note initial_count 应设置为可用资源的数量 + */ shared_memory_error create_semaphore(const std::string& name, unsigned int initial_count); + + /** + * @brief 打开已存在的命名信号量 + * + * 打开由其他进程创建的信号量。 + * + * @param name 信号量名称 + * @return shared_memory_error::SUCCESS 打开成功 + * @return shared_memory_error::OPEN_FAILED 打开失败 + * + * @note 线程安全 + */ shared_memory_error open_semaphore(const std::string& name); + + /** + * @brief 等待信号量(P操作) + * + * 获取一个资源,如果没有可用资源则阻塞等待。 + * + * ## 操作语义 + * - 如果计数 > 0:计数减1,立即返回 + * - 如果计数 = 0:阻塞直到其他线程post或超时 + * + * @param name 信号量名称 + * @param timeout 超时时间(毫秒),默认5000ms + * @return shared_memory_error::SUCCESS 成功获取资源 + * @return shared_memory_error::NOT_FOUND 信号量不存在 + * @return shared_memory_error::SYNCHRONIZATION_FAILED 超时或等待失败 + * + * @note 线程安全 + * @note 获取资源后记得调用 post_semaphore 释放 + */ shared_memory_error wait_semaphore(const std::string& name, std::chrono::milliseconds timeout = std::chrono::milliseconds(5000)); + + /** + * @brief 释放信号量(V操作) + * + * 释放一个资源,增加信号量计数。 + * + * ## 操作语义 + * - 计数加1 + * - 如果有等待线程,唤醒一个 + * + * @param name 信号量名称 + * @return shared_memory_error::SUCCESS 成功释放 + * @return shared_memory_error::NOT_FOUND 信号量不存在 + * @return shared_memory_error::SYNCHRONIZATION_FAILED 释放失败 + * + * @note 线程安全 + * @note post次数不应超过始计数+wait次数 + */ shared_memory_error post_semaphore(const std::string& name); + + /** + * @brief 移除命名信号量 + * + * 从系统中移除信号量。 + * + * @param name 信号量名称 + * @return shared_memory_error::SUCCESS 成功移除 + * @return shared_memory_error::SYNCHRONIZATION_FAILED 移除失败 + * + * @note 线程安全 + * @note 应由创建者调用 + * + * @warning 移除前应确保没有线程在等待 + */ shared_memory_error remove_semaphore(const std::string& name); - // RAII锁 + // ======================================================================== + // RAII辅助类 + // ======================================================================== + + /** + * @class scoped_mutex_lock + * @brief RAII风格的互斥量锁 + * + * 提供自动管理互斥量锁定和解锁的RAII封装,确保异常安全。 + * + * ## 设计目的 + * - 自动管理锁的生命周期 + * - 异常安全:即使发生异常也能正确解锁 + * - 避免忘记解锁导致的死锁 + * + * ## 使用模式 + * ```cpp + * void critical_section(interprocess_synchronization& sync) { + * scoped_mutex_lock lock(sync, "my_mutex"); + * if (!lock.is_locked()) { + * // 获取锁失败的处理 + * return; + * } + * + * // 临界区代码 + * // 发生异常也会自动解锁 + * + * } // 作用域结束,自动解锁 + * ``` + * + * @note 不可拷贝,不可移动 + * @note 析构时自动解锁 + */ class scoped_mutex_lock { public: + /** + * @brief 构造函数,尝试获取锁 + * + * @param sync 同步管理器引用 + * @param mutex_name 互斥量名称 + * + * @note 构造时自动调用 lock_mutex + * @note 如果锁定失败,is_locked() 返回 false + */ scoped_mutex_lock(interprocess_synchronization& sync, const std::string& mutex_name); + + /** + * @brief 析构函数,自动释放锁 + * + * @note 只有成功获取锁时才会解锁 + * @note 异常安全,不会抛出异常 + */ ~scoped_mutex_lock(); + /** + * @brief 检查是否成功获取了锁 + * + * @return true 成功持有锁 + * @return false 未能获取锁 + * + * @note 应在使用临界区前检查此标志 + */ [[nodiscard]] auto is_locked() const { return locked_; } private: + /// 同步管理器引用,用于解锁 interprocess_synchronization& sync_; + + /// 互斥量名称,用于解锁 std::string mutex_name_; + + /// 是否成功获取了锁 bool locked_; }; private: + /// 配置参数副本 + /// @note 保存构造时传入的配置 shared_memory_config config_; + + /// 互斥量缓存映射表 + /// @note key: 互斥量名称, value: 互斥量对象 + /// @note 避免重复创建/打开,提高性能 std::unordered_map> mutexes_; + + /// 条件变量缓存映射表 + /// @note key: 条件变量名称, value: 条件变量对象 std::unordered_map> conditions_; + + /// 信号量缓存映射表 + /// @note key: 信号量名称, value: 信号量对象 std::unordered_map> semaphores_; - std::mutex local_mutex_; // 保护本地数据结构 + /// 本地互斥量,保护上述映射表 + /// @note 确保多线程环境下对映射表的访问是线程安全的 + /// @note 不要与进程间互斥量混淆,这是本地线程锁 + std::mutex local_mutex_; }; diff --git a/src/network/shm/lock_free_ring_buffer.cpp b/src/network/shm/lock_free_ring_buffer.cpp index f0cfab8..9a60c02 100644 --- a/src/network/shm/lock_free_ring_buffer.cpp +++ b/src/network/shm/lock_free_ring_buffer.cpp @@ -1,7 +1,219 @@ +/** + * @file lock_free_ring_buffer.cpp + * @brief 无锁环形缓冲区的模板显式实例化 + * + * 本文件提供了 lock_free_ring_buffer 模板类常用类型的显式实例化。 + * + * ## 为什么需要显式实例化 + * + * ### 问题背景 + * - lock_free_ring_buffer 是一个模板类 + * - 模板代码通常在头文件中实现 + * - 每个使用不同类型的编译单元都会实例化一次 + * - 导致代码膨胀和编译时间增加 + * + * ### 显式实例化的好处 + * 1. **减少编译时间**:只在这个文件中实例化一次 + * 2. **减小二进制体积**:避免重复的模板实例化代码 + * 3. **加快链接速度**:减少需要链接的符号数量 + * 4. **集中管理**:常用类型的实例化集中在一处 + * + * ### 工作原理 + * - 编译器在编译此文件时生成这些类型的完整实现 + * - 其他编译单元可以直接链接到这些实现 + * - 不需要在每个使用处重新实例化 + * + * ## 实例化的类型 + * + * 这些类型是音频处理和数据传输中最常用的: + * + * - **float**: 32位浮点音频样本 + * - 最常见的音频处理格式 + * - DAW和音频插件的标准格式 + * + * - **double**: 64位浮点音频样本 + * - 高精度音频处理 + * - 科学计算和分析 + * + * - **int16_t**: 16位整数音频样本 + * - CD音质(16-bit/44.1kHz) + * - 紧凑的存储格式 + * + * - **int32_t**: 32位整数音频样本 + * - 高动态范围音频 + * - 专业音频设备 + * + * - **uint8_t**: 8位无符号整数 + * - 控制消息 + * - 字节流传输 + * - MIDI数据 + * + * ## 使用示例 + * + * 由于这些类型已经显式实例化,其他代码可以直接使用而无需 + * 包含完整的模板实现: + * + * ```cpp + * // 在其他文件中 + * #include "lock_free_ring_buffer.h" // 只需要声明 + * + * // 可以直接使用,链接器会找到此文件中的实现 + * lock_free_ring_buffer audio_buffer("audio", 1024); + * lock_free_ring_buffer sample_buffer("samples", 2048); + * ``` + * + * ## 添加新类型 + * + * 如果需要支持新的类型,只需在此文件中添加相应的实例化: + * + * ```cpp + * template class lock_free_ring_buffer; + * ``` + * + * @note 只应该实例化平凡可拷贝(trivially copyable)类型 + * @note 新类型必须满足 lock_free_ring_buffer 的类型要求 + * + * @see lock_free_ring_buffer.h + */ + #include "lock_free_ring_buffer.h" +// ============================================================================ +// 显式模板实例化 +// ============================================================================ + +/** + * @brief float 类型的显式实例化 + * + * 用于 32 位浮点音频样本传输。 + * + * ## 典型应用 + * - 实时音频流处理 + * - DAW 插件间通信 + * - 音频效果器链 + * - VST/AU 插件接口 + * + * ## 性能特点 + * - 样本大小:4 字节 + * - 缓存友好(对齐到 4 字节边界) + * - 支持 SIMD 向量化处理 + */ template class lock_free_ring_buffer; + +/** + * @brief double 类型的显式实例化 + * + * 用于 64 位高精度浮点音频样本传输。 + * + * ## 典型应用 + * - 高精度音频处理 + * - 科学音频分析 + * - 浮点累积计算 + * - 专业母带处理 + * + * ## 性能特点 + * - 样本大小:8 字节 + * - 更高的数值精度 + * - 更大的动态范围 + * - 内存占用是 float 的两倍 + */ template class lock_free_ring_buffer; + +/** + * @brief int16_t 类型的显式实例化 + * + * 用于 16 位整数音频样本传输。 + * + * ## 典型应用 + * - CD 音质音频(16-bit/44.1kHz) + * - 紧凑的音频存储 + * - 音频文件 I/O + * - 网络音频流 + * + * ## 性能特点 + * - 样本大小:2 字节 + * - 内存占用小 + * - 动态范围:96 dB(理论值) + * - 适合批量传输 + */ template class lock_free_ring_buffer; + +/** + * @brief int32_t 类型的显式实例化 + * + * 用于 32 位整数音频样本传输。 + * + * ## 典型应用 + * - 24-bit 音频(存储在 32-bit 容器中) + * - 高动态范围音频 + * - 专业录音设备 + * - 数字信号处理中间格式 + * + * ## 性能特点 + * - 样本大小:4 字节 + * - 动态范围:144 dB(24-bit 有效位) + * - 与 float 大小相同但语义不同 + * - 无舍入误差 + */ template class lock_free_ring_buffer; -template class lock_free_ring_buffer; \ No newline at end of file + +/** + * @brief uint8_t 类型的显式实例化 + * + * 用于字节流和控制消息传输。 + * + * ## 典型应用 + * - MIDI 消息传输 + * - 控制命令队列 + * - 二进制协议数据 + * - 日志消息缓冲 + * - 序列化数据流 + * + * ## 性能特点 + * - 样本大小:1 字节 + * - 最小的内存占用 + * - 适合大量小消息 + * - 字节对齐无填充 + * + * ## 使用注意 + * - 适用于离散消息,不适合音频样本 + * - 可以用于构建变长消息协议 + * - 配合序列化库使用效果更好 + */ +template class lock_free_ring_buffer; + +// ============================================================================ +// 实例化完成 +// ============================================================================ + +/** + * ## 编译后的效果 + * + * 编译此文件后,生成的目标文件(.o 或 .obj)将包含以下符号: + * + * - lock_free_ring_buffer::lock_free_ring_buffer(...) + * - lock_free_ring_buffer::~lock_free_ring_buffer() + * - lock_free_ring_buffer::try_push(...) + * - lock_free_ring_buffer::try_pop(...) + * - ... (所有方法的所有类型实例) + * + * 链接器将这些符号提供给其他编译单元使用。 + * + * ## 代码膨胀对比 + * + * **不使用显式实例化**: + * - 每个使用 lock_free_ring_buffer 的 .cpp 文件都生成一份代码 + * - 10 个文件使用 = 10 份相同代码 + * - 编译慢、目标文件大、链接慢 + * + * **使用显式实例化**: + * - 只在此文件生成一份代码 + * - 其他文件只引用符号 + * - 编译快、目标文件小、链接快 + * + * ## 性能影响 + * + * **运行时性能**:完全相同(编译后的机器码一样) + * **编译时性能**:显著提升(减少重复实例化) + * **二进制大小**:显著减小(消除重复代码) + */ \ No newline at end of file diff --git a/src/network/shm/lock_free_ring_buffer.h b/src/network/shm/lock_free_ring_buffer.h index bca5568..99037ae 100644 --- a/src/network/shm/lock_free_ring_buffer.h +++ b/src/network/shm/lock_free_ring_buffer.h @@ -1,3 +1,84 @@ +/** + * @file lock_free_ring_buffer.h + * @brief 无锁环形缓冲区 - 高性能的跨进程数据传输结构 + * + * 本文件实现了基于原子操作的无锁环形缓冲区(Lock-Free Ring Buffer), + * 是Network模块中用跨进程高性能数据传输的核心数据结构。 + * + * ## 模块定位 + * - 构建在 shared_memory_manager 之上,提供高性能数据传输 + * - 为音频数据、控制消息等提供无锁传输通道 + * - 支持生产者-消费模式的跨进程通信 + * + * ## 核心特性 + * 1. **无锁设计**:使原子操作,无需互斥量 + * 2. **SPSC模型**:单生者-单消费者(Single Producer Single Consumer) + * 3. **零拷贝**:共享存中直接读写 + * 4. **缓存友好**:缓行对齐,减少伪共享 + * 5. **异常安全**:使 RAII 管理资源 + * + * ## 设计原理 + * + * ### 环形结构 + * ``` + * 逻辑视图: + * [0][1][2][3][4][5][6][7] capacity = 7 + * ^tail ^head + * + * 实际分配:capacity + 1 个槽位用于区分满/空 + * [0][1][2][3][4][5][6][7][8] 实际大小 = 8 + * ``` + * + * ### 满/空判断 + * - **空**:tail == head + * - **满**:(head + 1) % (capacity + 1) == tail + * - **原理**:多分配个槽位,用于区分满和空 + * + * ### 内存序(Memory Order) + * - **relaxed**:用于 load head/tail 时的初始读取 + * - **acquire**:用于消者读取 head,确保看到生者的写入 + * - **release**:于生产者更新 head,确保写入对费者可见 + * + * ### 缓存行对 + * - data 数组使用 `alignas(64)` 对齐 + * - 避免伪共享(False Sharing) + * - 提升缓存性能 + * + * ## 使用场景 + * - 音频流传输(实时性要求高) + * - 控制消息传递 + * - 日志数据收集 + * - 任何需要高性能的单向数据流 + * + * ## 性能特点 + * - **延迟**:纳秒级无锁操作) + * - **吞吐量**:取决数据大小和CPU性能 + * - **可扩展性**:SPSC型,无竞争 + * - **内存开销**:(capacity + 1) * sizeof(T) + 元数据 + * + * ## 线程安全性 + * - **SPSC安全**:一个产者 + 一个消费者 + * - **不支持MPMC**:多产者或多消费者会导致数据竞争 + * - **跨进程安全**:持不同进程的生产者和消费者 + * + * ## 类型要求 + * - T 必须是平凡可拷贝类型(trivially copyable) + * - 不能包含指向进程私有内存的指针 + * - 适用于 POD 类型、简单结构体等 + * + * @note 仅支持单生产者-消费者模型 + * @note 不适用于多生产者或多消费者场景 + * @note 需要配合 shared_memory_manager 使用 + * + * @see shared_memory_manager.h + * @see triple_buffer.h (另一种无锁缓冲) + * + * ## 参考资料 + * - Herb Sutter: "Lock-Free Programming" + * - 《C++ Concurrency in Action》 Anthony Williams + * - Linux内核的 kfifo 实现 + */ + #pragma once #include #include @@ -5,60 +86,493 @@ #include "shared_memory_manager.h" +/// 日志模块标识符 #define LOCK_FREE_RING_BUFFER_LOG_MODULE "lock-free ring buffer" +/** + * @class lock_free_ring_buffer + * @brief 无锁环形缓冲区(单生产者-单消费者) + * + * 这是一个模板类,实现了高性能的无锁环形缓冲区。 + * 使用原子操作而非互斥量,适用于对延迟敏感的应用。 + * + * ## 设计模式 + * - **SPSC**:单生产者-单消费者模型 + * - **Wait-Free**:操作证在有限步内完成 + * - **RAII**:自动管理享内存生命周期 + * + * ## 核心机制 + * + * ### 原子变量 + * - `head`:写入位置(生产者修改,消费者读取) + * - `tail`:读取位置(消费者修改,生产者读取) + * - 使用不同的内存序保证可见性和顺序 + * + * ### 索引计算 + * - 使用模运算实现环形:`(index + 1) % (capacity + 1)` + * - 距离计算考虑环绕:`distance(from, to)` + * + * ### 容量策略 + * - 实际分配 `capacity + 1` 个槽位 + * - 可用容量为 `capacity` + * - 额外槽位用于区分满和空 + * + * ## 使用示例 + * + * ### 生产者 + * ```cpp + * lock_free_ring_buffer buffer("audio_buffer", 1024); + * + * // 推入单个元素 + * int data = 42; + * if (buffer.try_push(data)) { + * // 成功 + * } + * + * // 就地构造 + * if (buffer.try_emplace(arg1, arg2)) { + * // 成功 + * } + * + * // 批量推入 + * int batch[100]; + * size_t pushed = buffer.try_push_batch(batch, 100); + * ``` + * + * ### 消费者 + * ```cpp + * lock_free_ring_buffer buffer("audio_buffer", 1024); + * + * // 弹出单个元素 + * int data; + * if (buffer.try_pop(data)) { + * // 处理 data + * } + * + * // 查看但不移除 + * if (buffer.try_peek(data)) { + * // 查看 data + * } + * + * // 批量弹出 + * int batch[100]; + * size_t popped = buffer.try_pop_batch(batch, 100); + * ``` + * + * @tparam T 元素类型,必须是平凡可拷贝的 + * + * @note 构造函数会在共享内存中分配缓冲区 + * @note 析构函数由创建者负责释放共享内存 + * @note 所有操作都是 try 语义,不阻塞 + */ template class lock_free_ring_buffer { public: + /// 编译期类型检查:T 必须是平凡可拷贝类型 + /// @note 平凡可拷贝意味着可以用 memcpy 复制 + /// @note 这对于共享内存中的对象是必需的 static_assert(std::is_trivially_copyable_v, "T 必须是平凡可拷贝类型"); + /** + * @brief 构造函数 + * + * 创建或打开共享内存中的环形缓冲区。 + * + * ## 创建流程 + * 1. 尝试在共享内存中查找缓冲区 + * 2. 如果不存在,分配新的共享内存 + * 3. 使用 placement new 构造 buffer_data + * 4. 初始化 head、tail 和 capacity + * 5. 标记为创建者(负责清理) + * + * ## 打开流程 + * 1. 在共享内存中找到缓冲区 + * 2. 验证容量匹配 + * 3. 标记为非创建者 + * + * @param name 缓冲区在共享内存中的唯一名称 + * @param capacity 缓冲区容量(可存储的元素数量) + * + * @throws std::runtime_error 如果无法分配共享内存 + * @throws std::runtime_error 如果容量不匹配 + * + * @note 实际分配 (capacity + 1) * sizeof(T) 的空间 + * @note 不同进程必须使用相同的 capacity 打开 + */ lock_free_ring_buffer(const std::string& name, size_t capacity); + + /** + * @brief 析构函数 + * + * 如果是创建者,释放共享内存;否则只释放本地资源。 + * + * @note 非创建者进程不会释放共享内存 + * @note 创建者进程负责最终清理 + */ ~lock_free_ring_buffer(); - // 禁止拷贝,允许移动 + // 禁止拷贝(共享内存对象不应拷贝) lock_free_ring_buffer(const lock_free_ring_buffer&) = delete; lock_free_ring_buffer& operator=(const lock_free_ring_buffer&) = delete; + + // 允许移动(转移所有权) lock_free_ring_buffer(lock_free_ring_buffer&&) = default; lock_free_ring_buffer& operator=(lock_free_ring_buffer&&) = default; - // 生产者接口 + // ======================================================================== + // 生产者接口(写入端) + // ======================================================================== + + /** + * @brief 尝试推入一个元素(拷贝) + * + * 将元素拷贝到缓冲区,如果缓冲区已满则失败。 + * + * ## 算法流程 + * 1. 读取当前 head(写入位置) + * 2. 计算 next_head = (head + 1) % (capacity + 1) + * 3. 读取 tail,检查是否满next_head == tail + * 4. 如果未满,将元素写入 data[head] + * 5. 更新 head = next_head(release 语义) + * + * ## 内存序 + * - head load: relaxed(本地线程以看到自己的写入) + * - tail load: acquire(必须看到费者的最新位置) + * - head store: release(确保元素入对消费者可见) + * + * @param item 要推入的元素(const引用,会被拷贝) + * @return true 推入成功 + * @return false 缓冲区已满 + * + * @note Wait-Free 操作,不会阻塞 + * @note 仅生产者线程应调用此方法 + * @note 线程安全(相对于消费者) + */ auto try_push(const T& item) -> bool; + + /** + * @brief 尝试推入一个元素(移动) + * + * 将元素移动到缓冲区,适用于可移动类型。 + * + * @param item 要推入的元素(右值引用,会被移动) + * @return true 推入成功 + * @return false 缓冲区已满 + * + * @note 对于可移动类型,比拷贝更高效 + * @note 对于平凡可拷贝类型,与 try_push(const T&) 等价 + */ auto try_push(T&& item) -> bool; + + /** + * @brief 尝试就地构造一个元素 + * + * 直接在缓冲区中构造元素,避免拷贝和移动。 + * + * ## 就地构造 + * - 使用 placement new 在缓冲区中构造 + * - 避免临时对象的创建和拷贝 + * - 参数完美转发给 T 的构造函数 + * + * ## 使用示例 + * ```cpp + * struct Point { int x, y; Point(int x, int y) : x(x), y(y) {} }; + * lock_free_ring_buffer buffer("points", 100); + * + * // 就地构造 Point(10, 20) + * buffer.try_emplace(10, 20); + * ``` + * + * @tparam Args 构造函数参数类型(自动推导) + * @param args 转发给 T 构造函数的参数 + * @return true 构造成功 + * @return false 缓冲区已满 + * + * @note 最高效的插入方式 + * @note 参数通过完美转发传递 + */ template auto try_emplace(Args&&... args) -> bool; - // 消费者接口 + // ======================================================================== + // 消费者接口(读取端) + // ======================================================================== + + /** + * @brief 尝试弹出一个元素 + * + * 从缓冲区读取并移除一个元素。 + * + * ## 算法流程 + * 1. 读取当前 tail(读取位置) + * 2. 读取 head,检查是否空tail == head + * 3. 如果非空,从 data[tail] 读取元素 + * 4. 更新 tail = (tail + 1) % (capacity + 1)(release 语义) + * + * ## 内存序 + * - tail load: relaxed(本地线程以看到自己的读取) + * - head load: acquire(必须看到产者的最新写入) + * - tail store: release(确保读取成对生产者可见) + * + * @param[out] item 输出参数,接收弹出的元素 + * @return true 弹出成功,item 包含有效数据 + * @return false 缓冲区为空,item 未修改 + * + * @note Wait-Free 操作,不会阻塞 + * @note 仅消费者线程应调用此方法 + * @note 线程安全(相对于生产者) + */ auto try_pop(T& item) -> bool; + + /** + * @brief 尝试查看队首元素但不移除 + * + * 读取队首元素但不改变缓冲区状态。 + * + * ## 使用场景 + * - 需要查看下一个元素再决定是否处理 + * - 实现条件消费逻辑 + * - 调试和监控 + * + * @param[out] item 输出参数,接收队首元素的拷贝 + * @return true 查看成功 + * @return false 缓冲区为空 + * + * @note 不修改缓冲区状态 + * @note 后续仍需调用 try_pop 来真正移除元素 + */ auto try_peek(T& item) const -> bool; + // ======================================================================== // 状态查询 + // ======================================================================== + + /** + * @brief 检查缓冲区是否为空 + * + * @return true 缓冲区为空(没有可读元素) + * @return false 缓冲区非空 + * + * @note 使用 acquire 内存序确保看到最新状态 + * @note 结果可能立即过时(生产者可能同时写入) + */ auto empty() const -> bool; + + /** + * @brief 检查缓冲区是否已满 + * + * @return true 缓冲区已满(无法写入) + * @return false 缓冲区未满 + * + * @note 使用 acquire 内存序确保看到最新状态 + * @note 结果可能立即过时(消费者可能同时读取) + */ auto full() const -> bool; + + /** + * @brief 获取当前元素数量 + * + * 计算缓冲区中当前存储的元素数量。 + * + * ## 计算方法 + * - 使用 distance(tail, head) 计算 + * - 正确处理环绕情况 + * + * @return size_t 当前元素数量(0 到 capacity) + * + * @note 结果是快照,可能立即过时 + * @note 可用于统计和监控 + */ auto size() const -> size_t; + + /** + * @brief 获取缓冲区容量 + * + * @return size_t 缓冲区最大容量 + * + * @note 这是创建时指定的容量 + * @note 实际分配空间为 capacity + 1 + */ auto capacity() const { return capacity_; } + + /** + * @brief 获取可用空间 + * + * 计算还可以写入多少个元素。 + * + * @return size_t 可用空间大小 + * + * @note available_space = capacity - size() + * @note 结果是快照,可能立即过时 + */ auto available_space() const -> size_t; + // ======================================================================== // 批量操作 + // ======================================================================== + + /** + * @brief 尝试批量推入元素 + * + * 尽可能多地推入元素,直到缓冲区满或所有元素已推入。 + * + * ## 实现策略 + * - 逐个调用 try_push + * - 遇到满缓冲区时停止 + * - 返回实际推入的数量 + * + * ## 原子性 + * - **非原子**:不保全部推入或全部失败 + * - 部分推入是允许的 + * - 调用者需检查返回值处理剩余元素 + * + * @param items 元素数组指针 + * @param count 要推入的元素数量 + * @return size_t 实际推入的元素数量(0 到 count) + * + * @note 非原子操作 + * @note 可能部分成功 + * @note 适用于批量数据传输 + */ auto try_push_batch(const T* items, size_t count) -> size_t; + + /** + * @brief 尝试批量弹出元素 + * + * 尽可能多地弹出元素,直到缓冲区空或已弹出足够数量。 + * + * @param[out] items 元素数组指针,接收弹出的元素 + * @param count 要弹出的元素数量 + * @return size_t 实际弹出的元素数量(0 到 count) + * + * @note 非原子操作 + * @note 可能部分成功 + * @note 适用于批量数据接收 + */ auto try_pop_batch(T* items, size_t count) -> size_t; private: + /** + * @struct buffer_data + * @brief 缓冲区共享内存数据结构 + * + * 这是实际存储在共享内存中的数据结构。 + * + * ## 内存布局 + * ``` + * +-------------------+ <- 共享内存起始 + * | head (atomic) | 8 bytes + * +-------------------+ + * | tail (atomic) | 8 bytes + * +-------------------+ + * | capacity | 8 bytes + * +-------------------+ + * | padding | 40 bytes (对齐到 64) + * +-------------------+ <- 64 字节边界 + * | data[0] | sizeof(T) + * | data[1] | sizeof(T) + * | ... | + * | data[capacity] | sizeof(T) + * +-------------------+ + * ``` + * + * ## 对齐说明 + * - data 数组对齐到 64 字节(典型缓存行大小) + * - 避免 head/tail 与 data 在同一缓存行(伪共享) + * - 提升多核性能 + */ struct buffer_data { + /// 写入位置(生产者修改) + /// @note 使用 std::atomic 保证原子性 + /// @note relaxed load, release store std::atomic head{0}; + + /// 读取位置(消费者修改) + /// @note 使用 std::atomic 保证原子性 + /// @note relaxed load, release store std::atomic tail{0}; + + /// 缓冲区容量 + /// @note 非原子,创建后不变 size_t capacity{}; + + /// 数据数组(柔性数组成员) + /// @note alignas(64) 对齐到缓存行 + /// @note 避免与 head/tail 的伪共享 + /// @note [1] 是占位符,实际大小在分配时确定 alignas(64) T data[1]; }; + /// 指向共享内存中 buffer_data 的指针 + /// @note 不拥有内存,由 shared_memory_manager 管理 buffer_data* buffer_; + + /// 缓冲区容量(本地副本,便于访问) size_t capacity_; + + /// 缓冲区在共享内存中的名称 std::string name_; + + /// 是否为创建者标志 + /// @note true: 本进程创建了缓冲区,负责清理 + /// @note false: 本进程只打开了缓冲区 bool creator_; + /** + * @brief 计算下一个索引(环形) + * + * 实现环形索引的递增,考虑容量边界。 + * + * ## 计算公式 + * ``` + * next = (index + 1) % (capacity + 1) + * ``` + * + * ## 为什么 capacity + 1 + * - 实际分配了 capacity + 1 个槽位 + * - 额外槽位用于区分满和空 + * - 如果只用 capacity 个槽位,无法区分 head == tail 是满还是空 + * + * @param index 当前索引 + * @return size_t 下一个索引 + * + * @note 内联函数,无性能开销 + * @note [[nodiscard]] 警告忽略返回值 + */ [[nodiscard]] auto next_index(size_t index) const -> size_t { - return (index + 1) % (capacity_ + 1); // +1 是为了区分满和空的状态 + return (index + 1) % (capacity_ + 1); } + /** + * @brief 计算两个索引之间的距离 + * + * 计算从 from 到 to 的元素数量,正确处理环绕。 + * + * ## 计算逻辑 + * - 如果 to >= from:直接相减(环绕) + * - 如果 to < from:考虑环绕 + * ``` + * distance = (capacity + 1) - from + to + * ``` + * + * ## 示例 + * ``` + * capacity = 7, 实际大小 = 8 + * + * 未环绕:from=2, to=5 + * distance = 5 - 2 = 3 + * + * 环绕:from=6, to=2 + * distance = 8 - 6 + 2 = 4 + * ``` + * + * @param from 起始索引 + * @param to 结束索引 + * @return size_t 距离(元素数量) + * + * @note 内联函数 + * @note 用于实现 size() 方法 + */ [[nodiscard]] auto distance(size_t from, size_t to) const -> size_t { if (to >= from) { return to - from; @@ -67,56 +581,90 @@ private: } }; +// ============================================================================ +// 模板方法实现 +// ============================================================================ + +/** + * @brief 构造函数实现 + * + * 详细说明见类内声明。 + */ template lock_free_ring_buffer::lock_free_ring_buffer(const std::string& name, size_t capacity) : capacity_(capacity), name_(name), creator_(false) { + // 计算需要的缓冲区大小 + // sizeof(buffer_data) 已包含 data[1],所以减去 sizeof(T) + // 然后加上 capacity_ 个 T 的大小 auto buffer_size = sizeof(buffer_data) + (capacity_ * sizeof(T)); + // 尝试在共享内存中查找已存在的缓冲区 buffer_ = shm_find_raw(name_); if (!buffer_) { + // 缓冲区不存在,分配新的共享内存 void* raw_memory = shm_allocate_raw(name_, buffer_size); if (!raw_memory) { throw std::runtime_error("无法分配共享内存"); } + // 使用 placement new 在共享内存中构造 buffer_data buffer_ = new(raw_memory) buffer_data(); buffer_->capacity = capacity_; + // 标记为创建者,负责清理 creator_ = true; - log_module_debug(LOCK_FREE_RING_BUFFER_LOG_MODULE, "创建无锁环形缓冲区: {}, 容量: {}", name_, capacity_); + log_module_debug(LOCK_FREE_RING_BUFFER_LOG_MODULE, "创建无锁环形缓区: {}, 容量: {}", name_, capacity_); } else { + // 缓冲区已存在,验证容量匹配 if (capacity_ != buffer_->capacity) { throw std::runtime_error("共享内存中的缓冲区容量与请求的容量不匹配"); } - log_module_debug(LOCK_FREE_RING_BUFFER_LOG_MODULE, "打开现有无锁环形缓冲区: {}, 容量: {}", name_, capacity_); + log_module_debug(LOCK_FREE_RING_BUFFER_LOG_MODULE, "打开现有无锁环缓冲区: {}, 容量: {}", name_, capacity_); } } +/** + * @brief 析构函数实现 + */ template lock_free_ring_buffer::~lock_free_ring_buffer() { if (creator_) { + // 创建者负责释放共享内存 shm_deallocate_raw(name_); - log_module_debug(LOCK_FREE_RING_BUFFER_LOG_MODULE, "销毁无锁环形缓冲区: {}", name_); + log_module_debug(LOCK_FREE_RING_BUFFER_LOG_MODULE, "销毁无锁环形缓区: {}", name_); } } +/** + * @brief 推入元素(拷贝)实现 + * + * 详细说明见类内声明。 + */ template auto lock_free_ring_buffer::try_push(const T& item) -> bool { + // 读取当前写入位置(relaxed:本线程可见即可) auto head = buffer_->head.load(std::memory_order_relaxed); auto next_head = next_index(head); + // 检查是否已满(acquire:必须看到消费者的最新读取位置) if (next_head == buffer_->tail.load(std::memory_order_acquire)) { return false; // 缓冲区满 } + // 写入数据(普通写入,由下面的 release 保证可见性) buffer_->data[head] = item; + + // 更新写入位置(release:确保数据写入对消费者可见) buffer_->head.store(next_head, std::memory_order_release); return true; } +/** + * @brief 推入元素(移动)实现 + */ template auto lock_free_ring_buffer::try_push(T&& item) -> bool { auto head = buffer_->head.load(std::memory_order_relaxed); @@ -126,12 +674,16 @@ auto lock_free_ring_buffer::try_push(T&& item) -> bool { return false; // 缓冲区满 } + // 移动语义(对于平凡可拷贝类型,等价于拷贝) buffer_->data[head] = std::move(item); buffer_->head.store(next_head, std::memory_order_release); return true; } +/** + * @brief 就地构造元素实现 + */ template template auto lock_free_ring_buffer::try_emplace(Args&&... args) -> bool { @@ -142,25 +694,38 @@ auto lock_free_ring_buffer::try_emplace(Args&&... args) -> bool { return false; // 缓冲区满 } + // 使用 placement new 就地构造对象 + // std::forward 完美转发参数 new(&buffer_->data[head]) T(std::forward(args)...); buffer_->head.store(next_head, std::memory_order_release); return true; } +/** + * @brief 弹出元素实现 + */ template auto lock_free_ring_buffer::try_pop(T& item) -> bool { + // 读取当前读取位置(relaxed:本线程可见即可) auto tail = buffer_->tail.load(std::memory_order_relaxed); + // 检查是否为空(acquire:必须看到生产者的最新写入) if (tail == buffer_->head.load(std::memory_order_acquire)) { return false; // 缓冲区空 } + // 读取数据 item = buffer_->data[tail]; + + // 更新读取位置(release:确保读取完成对生产者可见) buffer_->tail.store(next_index(tail), std::memory_order_release); return true; } +/** + * @brief 查看队首元素实现 + */ template auto lock_free_ring_buffer::try_peek(T& item) const -> bool { auto tail = buffer_->tail.load(std::memory_order_relaxed); @@ -169,16 +734,24 @@ auto lock_free_ring_buffer::try_peek(T& item) const -> bool { return false; // 缓冲区空 } + // 读取但不移除 item = buffer_->data[tail]; return true; } +/** + * @brief 检查空实现 + */ template auto lock_free_ring_buffer::empty() const -> bool { + // 使用 acquire 确保看到最新状态 return buffer_->tail.load(std::memory_order_acquire) == buffer_->head.load(std::memory_order_acquire); } +/** + * @brief 检查满实现 + */ template auto lock_free_ring_buffer::full() const -> bool { auto head = buffer_->head.load(std::memory_order_acquire); @@ -186,6 +759,9 @@ auto lock_free_ring_buffer::full() const -> bool { return next_index(head) == tail; } +/** + * @brief 获取大小实现 + */ template auto lock_free_ring_buffer::size() const -> size_t { auto head = buffer_->head.load(std::memory_order_acquire); @@ -193,21 +769,28 @@ auto lock_free_ring_buffer::size() const -> size_t { return distance(tail, head); } +/** + * @brief 获取可用空间实现 + */ template auto lock_free_ring_buffer::available_space() const -> size_t { return capacity_ - size(); } +/** + * @brief 批量推入实现 + */ template auto lock_free_ring_buffer::try_push_batch(const T* items, size_t count) -> size_t { if (!items || count == 0) { return 0; } + // 逐个推入,直到成功推入所有元素或缓冲区满 size_t pushed = 0; for (size_t i = 0; i < count; ++i) { if (!try_push(items[i])) { - break; + break; // 缓冲区满,停止推入 } ++pushed; } @@ -215,16 +798,20 @@ auto lock_free_ring_buffer::try_push_batch(const T* items, size_t count) -> s return pushed; } +/** + * @brief 批量弹出实现 + */ template auto lock_free_ring_buffer::try_pop_batch(T* items, size_t count) -> size_t { if (!items || count == 0) { return 0; } + // 逐个弹出,直到弹出所有元素或缓冲区空 size_t popped = 0; for (size_t i = 0; i < count; ++i) { if (!try_pop(items[i])) { - break; + break; // 缓冲区空,停止弹出 } ++popped; } diff --git a/src/network/shm/shared_memory_manager.cpp b/src/network/shm/shared_memory_manager.cpp index 2bee1d3..a059aae 100644 --- a/src/network/shm/shared_memory_manager.cpp +++ b/src/network/shm/shared_memory_manager.cpp @@ -1,116 +1,326 @@ +/** + * @file shared_memory_manager.cpp + * @brief 共享内存管理器的实现文件 + * + * 本文件实现了 shared_memory_manager 类的所有方法,包括: + * - 共享内存段的初始化和关闭 + * - 原始内存块的分配、查找和释放 + * - 共享内存段的创建和打开逻辑 + * - 资源清理和统计信息收集 + * + * ## 实现要点 + * 1. 基于 Boost.Interprocess 库实现跨平台共享内存管理 + * 2. 区分创建者和非创建者进程,实现正确的资源管理 + * 3. 完善的错误处理和日志记录 + * 4. 自动资源清理(RAII 模式) + * + * @see shared_memory_manager.h + */ + #include "shared_memory_manager.h" #include "logger.h" +/** + * @brief 初始化共享内存管理器 + * + * 这是管理器的主要入口方法,执行以下操作: + * 1. 检查是否已初始化(幂等性保证) + * 2. 保存配置参数副本 + * 3. 调用 create_or_open_segment() 创建或打开共享内存段 + * 4. 设置初始化标志 + * + * ## 实现细节 + * - 使用 initialized_ 标志防止重复初始化 + * - 配置参数通过值拷贝保存,避免外部修改影响 + * - 失败时保持未初始化状态,不影响后续重试 + * + * ## 错误处理 + * - 如果已初始化,直接返回 SUCCESS + * - 如果创建/打开失败,返回相应错误码且不设置初始化标志 + * - 所有错误都记录到日志系统 + * + * @param config 共享内存配置参数 + * @return shared_memory_error::SUCCESS 初始化成功 + * @return shared_memory_error::CREATION_FAILED 创建共享内存段失败 + * @return shared_memory_error::OPEN_FAILED 打开共享内存段失败 + * + * @note 线程不安全,应在单线程环境(如主线程初始化阶段)调用 + * @note 多次调用是安全的,但只有第一次调用会真正执行初始化 + */ auto shared_memory_manager::init(const shared_memory_config& config) -> shared_memory_error { + // 幂等性检查:如果已初始化,直接返回成功 if (initialized_) return shared_memory_error::SUCCESS; + // 保存配置参数副本,用于后续的清理和统计操作 config_ = config; log_module_info(SHARED_MEMORY_MANAGER_LOG_MODULE, "初始化共享内存段: {}, 大小: {} 字节", config_.segment_name, config_.segment_size); + // 尝试创建或打开共享内存段 auto result = create_or_open_segment(); if (result != shared_memory_error::SUCCESS) { log_module_info(SHARED_MEMORY_MANAGER_LOG_MODULE, "无法创建或打开共享内存段: {}", config_.segment_name); return result; } + // 设置初始化标志,表示管理器已就绪 initialized_ = true; log_module_info(SHARED_MEMORY_MANAGER_LOG_MODULE, "共享内存段初始化成功"); return shared_memory_error::SUCCESS; } +/** + * @brief 关闭共享内存管理器 + * + * 执行清理操作,释放共享内存资源。根据配置和创建者状态决定是否移除共享内存段。 + * + * ## 执行流程 + * 1. 检查是否已初始化(未初始化时直接返回) + * 2. 记录关闭日志 + * 3. 调用 cleanup() 执行实际清理 + * 4. 重置初始化标志 + * + * ## 清理策略 + * - 非创建者进程:只释放本地资源,保留共享内存段 + * - 创建者进程且 remove_on_destroy=true:移除共享内存段 + * - 创建者进程且 remove_on_destroy=false:保留共享内存段 + * + * @return shared_memory_error::SUCCESS 始终返回成功 + * + * @note 析构函数会自动调用此方法 + * @note 多次调用是安全的(幂等性) + * @note 关闭后可以通过 init() 重新初始化 + * + * @warning 关闭前应确保没有其他代码正在使用共享内存对象 + */ auto shared_memory_manager::shutdown() -> shared_memory_error { + // 幂等性检查:如果未初始化,直接返回成功 if (!initialized_) return shared_memory_error::SUCCESS; + log_module_info(SHARED_MEMORY_MANAGER_LOG_MODULE, "正在关闭共享内存段: {}", config_.segment_name); + + // 执行实际的清理操作 cleanup(); + + // 重置初始化标志,允许后续重新初始化 initialized_ = false; return shared_memory_error::SUCCESS; } +/** + * @brief 分配原始内存块 + * + * 在共享内存段中分配指定大小的原始字节数组,并赋予命名。 + * 使用 Boost.Interprocess 的 construct 方法创建字节数组。 + * + * ## 实现细节 + * - 使用 std::byte 类型构造字节数组 + * - 内存未初始化,包含随机数据 + * - 通过名称可以在其他进程中查找 + * + * ## 内存布局 + * ``` + * [元数据][name][size个字节的数据] + * ``` + * + * @param size 要分配的字节数 + * @param name 内存块的唯一名称 + * @return void* 指向分配内存的指针 + * @return nullptr 分配失败或管理器未初始化 + * + * @note 返回的指针指向共享内存,所有进程可见 + * @note 内存未初始化,使用前应正确初始化 + * @note 线程安全,可以从多个线程并发调用 + * + * @warning 如果名称已存在,会抛出异常并返回 nullptr + * @warning 不要在返回的内存中存储指向进程私有内存的指针 + */ auto shared_memory_manager::allocate_raw(size_t size, const std::string& name) -> void* { + // 安全检查:确保共享内存段已初始化 if (!segment_) { log_module_error(SHARED_MEMORY_MANAGER_LOG_MODULE, "尝试在未初始化的共享内存段中分配原始内存: {}", name); return nullptr; } + try { // 使用 construct 创建命名的字节数组 + // [size] 表示分配 size 个 std::byte 类型的数组 + // () 表示使用默认构造(不初始化) void* ptr = segment_->construct(name.c_str())[size](); log_module_debug(SHARED_MEMORY_MANAGER_LOG_MODULE, "在共享内存段中分配了 {} 字节的原始内存: {}", size, name); return ptr; } catch (const boost::interprocess::interprocess_exception& e) { + // 捕获 Boost.Interprocess 异常,如名称冲突、内存不足等 log_module_error(SHARED_MEMORY_MANAGER_LOG_MODULE, "分配原始内存失败: {}, 错误: {}", name, e.what()); return nullptr; } } +/** + * @brief 查找原始内存块 + * + * 根据名称在共享内存段中查找已分配的原始内存块。 + * + * ## 查找机制 + * - 在共享内存段的名称索引中搜索 + * - 查找复杂度 O(log N),N 为命名对象数量 + * - 支持跨进程查找 + * + * ## 返回值说明 + * - find() 返回 pair + * - first 是指向内存的指针,second 是对象数量(数组大小) + * - 未找到时 first 为 nullptr + * + * @param name 内存块名称 + * @return void* 指向内存块的指针 + * @return nullptr 未找到或管理器未初始化 + * + * @note 返回的指针在不同进程中虚拟地址可能不同 + * @note 线程安全 + * + * @warning 不要缓存返回的指针到磁盘,地址在重新映射后会变化 + */ auto shared_memory_manager::find_raw(const std::string& name) -> void* { + // 安全检查:确保共享内存段已初始化 if (!segment_) { log_module_error(SHARED_MEMORY_MANAGER_LOG_MODULE, "尝试在未初始化的共享内存段中查找原始内存: {}", name); return nullptr; } + try { + // 查找指定名称的 std::byte 数组 const auto& result = segment_->find(name.c_str()); if (result.first == nullptr) { + // 未找到时记录警告日志 log_module_warn(SHARED_MEMORY_MANAGER_LOG_MODULE, "未找到共享内存原始对象: {}", name); } return result.first; } catch (const boost::interprocess::interprocess_exception& e) { + // 捕获异常,如段已损坏等 log_module_error(SHARED_MEMORY_MANAGER_LOG_MODULE, "查找原始内存失败: {}, 错误: {}", name, e.what()); return nullptr; } } +/** + * @brief 释放原始内存块 + * + * 销毁并释放指定名称的原始内存块。 + * + * ## 释放流程 + * 1. 查找指定名称的对象 + * 2. 释放内存回共享内存池 + * 3. 从名称索引中移除 + * + * ## 注意事项 + * - 不调用析构函数(std::byte 无析构函数) + * - 释放后所有指向该内存的指针失效 + * - 只需一个进程调用释放即可 + * + * @param name 内存块名称 + * @return true 成功释放 + * @return false 释放失败或内存块不存在或管理器未初始化 + * + * @note 线程安全 + * + * @warning 释放后继续使用指针会导致未定义行为 + * @warning 如果内存中存储了需要析构的对象,调用者需要手动析构 + */ auto shared_memory_manager::deallocate_raw(const std::string& name) -> bool { + // 安全检查:确保共享内存段已初始化 if (!segment_) { log_module_error(SHARED_MEMORY_MANAGER_LOG_MODULE, "尝试在未初始化的共享内存段中释放原始内存: {}", name); return false; } try { + // 销毁指定名称的 std::byte 数组 segment_->destroy(name.c_str()); log_module_debug(SHARED_MEMORY_MANAGER_LOG_MODULE, "释放了共享内存段中的原始内存: {}", name); return true; } catch (const boost::interprocess::interprocess_exception& e) { + // 捕获异常,如对象不存在等 log_module_error(SHARED_MEMORY_MANAGER_LOG_MODULE, "释放原始内存失败: {}, 错误: {}", name, e.what()); return false; } } +/** + * @brief 创建或打开共享内存段(内部方法) + * + * 这是初始化的核心方法,实现了智能的创建/打开逻辑。 + * + * ## 执行流程 + * 1. 尝试打开已存在的共享内存段(open_only 模式) + * - 成功:设置 creator_ = false,返回 SUCCESS + * - 失败:继续下一步 + * + * 2. 检查是否允许创建(create_if_not_exists 配置) + * - 不允许:返回 OPEN_FAILED + * - 允许:继续下一步 + * + * 3. 移除可能存在的损坏的旧段 + * - 使用 shared_memory_object::remove() 清理 + * - 记录警告日志 + * + * 4. 创建新的共享内存段(create_only 模式) + * - 成功:设置 creator_ = true,返回 SUCCESS + * - 失败:返回 CREATION_FAILED + * + * ## 创建者标志 + * - creator_ = true:本进程创建了共享内存段,负责清理 + * - creator_ = false:本进程只打开已存在的段 + * + * ## 异常处理 + * - 捕获所有 Boost.Interprocess 异常 + * - 捕获标准异常作为后备 + * - 所有异常转换为错误码返回 + * + * @return shared_memory_error::SUCCESS 成功创建或打开 + * @return shared_memory_error::OPEN_FAILED 打开失败且不允许创建 + * @return shared_memory_error::CREATION_FAILED 创建失败 + * + * @note 私有方法,仅被 init() 调用 + * @note 方法内部处理所有异常,不向外抛出 + */ auto shared_memory_manager::create_or_open_segment() -> shared_memory_error { try { try { + // 第一步:尝试打开已存在的共享内存段 segment_ = std::make_unique(boost::interprocess::open_only, config_.segment_name.c_str()); + // 成功打开,标记为非创建者 creator_ = false; log_module_debug(SHARED_MEMORY_MANAGER_LOG_MODULE, "成功打开现有的共享内存段: {}", config_.segment_name); } catch (const boost::interprocess::interprocess_exception&) { - // 如果没有找到共享内存段,则尝试创建它 - - // 首先检查是否允许创建 + // 第二步:打开失败,可能是段不存在 + // 检查是否允许创建 if (!config_.create_if_not_exists) { log_module_error(SHARED_MEMORY_MANAGER_LOG_MODULE, "无法打开共享内存段且不允许创建: {}", config_.segment_name); return shared_memory_error::OPEN_FAILED; } - // 先尝试移除可能存在的损坏段 + // 第三步:先尝试移除可能存在的损坏段 + // 某些情况下段可能处于不一致状态(如进程崩溃) if (boost::interprocess::shared_memory_object::remove(config_.segment_name.c_str())) { - log_module_warn(SHARED_MEMORY_MANAGER_LOG_MODULE, "移除已经存在的共享内存段: {}", config_.segment_name); + log_module_warn(SHARED_MEMORY_MANAGER_LOG_MODULE, "移除已经存在的享内存段: {}", config_.segment_name); } - // 尝试创建新的共享内存段 + // 第四步:尝试创建新的共享内存段 segment_ = std::make_unique(boost::interprocess::create_only, config_.segment_name.c_str(), config_.segment_size); + // 成功创建,标记为创建者 creator_ = true; log_module_debug(SHARED_MEMORY_MANAGER_LOG_MODULE, "成功创建新的共享内存段: {}", config_.segment_name); } @@ -118,69 +328,207 @@ auto shared_memory_manager::create_or_open_segment() -> shared_memory_error { return shared_memory_error::SUCCESS; } catch (const boost::interprocess::interprocess_exception& e) { + // 捕获 Boost.Interprocess 的所有异常 + // 如权限不足、资源不足、段名非法等 log_module_error(SHARED_MEMORY_MANAGER_LOG_MODULE, "创建或打开共享 memory 段时出错: {}", e.what()); return shared_memory_error::CREATION_FAILED; } catch (const std::exception& e) { + // 捕获其他标准异常作为后备 log_module_error(SHARED_MEMORY_MANAGER_LOG_MODULE, "创建或打开共享 memory 段时发生异常: {}", e.what()); return shared_memory_error::CREATION_FAILED; } + // 理论上不会到达这里,但为了安全起见返回失败 return shared_memory_error::CREATION_FAILED; } +/** + * @brief 清理资源(内部方法) + * + * 释放共享内存段对象,并根据配置和创建者状态决定是否移除共享内存段。 + * + * ## 清理逻辑 + * 1. 重置 segment_ 智能指针,释放本地资源 + * 2. 如果满足以下两个条件,移除共享内存段: + * - creator_ == true(本进程是创建者) + * - config_.remove_on_destroy == true(配置要求移除) + * + * ## 资源管理策略 + * - **创建者 + remove_on_destroy=true**: 完全清理,移除共享内存段 + * - **创建者 + remove_on_destroy=false**: 保留共享内存段供后续使用 + * - **非创建者**: 只释放本地资源,保留共享内存段 + * + * ## 移除操作 + * - 使用 shared_memory_object::remove() 从系统中移除段 + * - 移除后其他进程无法再打开该段 + * - 已打开的进程仍可使用,但段会在所有进程关闭后被系统回收 + * + * @note 私有方法,被 shutdown() 和析构函数调用 + * @note 异常安全,捕获所有异常并记录日志 + * + * @warning 只有创建者进程才应该移除共享内存段 + * @warning 移除前应确保其他进程已不再需要该段 + */ void shared_memory_manager::cleanup() { + // 释放本地的 managed_shared_memory 对象 + // 这会解除内存映射,但不会删除共享内存段 segment_.reset(); - if (creator_ && config_ - - - . - remove_on_destroy - ) { + // 判断是否需要移除共享内存段 + // 只有创建者且配置要求移除时才执行 + if (creator_ && config_.remove_on_destroy) { try { + // 尝试从系统中移除共享内存段 if (boost::interprocess::shared_memory_object::remove(config_.segment_name.c_str())) { log_module_info(SHARED_MEMORY_MANAGER_LOG_MODULE, "已移除共享内存段: {}", config_.segment_name); } - else { log_module_warn(SHARED_MEMORY_MANAGER_LOG_MODULE, "无法移除共享内存段: {}", config_.segment_name); } + else { + // 移除失败,可能是段已被其他进程移除 + log_module_warn(SHARED_MEMORY_MANAGER_LOG_MODULE, "无法移除共享内存段: {}", config_.segment_name); + } } catch (const boost::interprocess::interprocess_exception& e) { + // 捕获异常,如权限不足等 log_module_error(SHARED_MEMORY_MANAGER_LOG_MODULE, "移除共享内存段时出错: {}", e.what()); } } } +/** + * @brief 获取共享内存使用统计信息 + * + * 收集并返回共享内存段的实时统计数据。 + * + * ## 统计项说明 + * - **total_size**: 共享内存段的总大小(字节) + * - 包括元数据开销 + * - 等于创建时指定的 segment_size + * + * - **free_size**: 当前可用的空闲空间(字节) + * - 可用于新的分配 + * - 不包括已分配但未使用的空间 + * + * - **used_size**: 已使用的空间(字节) + * - used_size = total_size - free_size + * - 包括所有已分配对象的空间 + * + * - **num_allocations**: 当前存在的对象数量 + * - 包括命名对象和唯一对象 + * - 可用于检测内存泄漏 + * + * - **largest_free_block**: 最大连续空闲块大小(字节) + * - 可分配的最大单个对象大小 + * - 由于碎片化,可能远小于 free_size + * + * ## 应用场景 + * - 监控内存使用率,预警即将耗尽 + * - 检测内存泄漏(num_allocations 持续增长) + * - 评估碎片化程度(largest_free_block vs free_size) + * - 性能调优和容量规划 + * + * @return statistics 统计信息结构体 + * 如果未初始化,返回全零的统计信息 + * + * @note 统计信息反映调用时刻的快照 + * @note 在多进程环境下,反映所有进程的累计使用 + * @note 线程安全 + * + * @warning 统计操作有开销,不建议在性能关键路径频繁调用 + */ auto shared_memory_manager::get_statistics() -> statistics { statistics stats{}; + + // 安全检查:如果未初始化,返回空统计信息 if (!segment_) { log_module_error(SHARED_MEMORY_MANAGER_LOG_MODULE, "尝试获取未初始化的共享内存段的统计信息"); return stats; } - stats.total_size = segment_->get_size(); - stats.free_size = segment_->get_free_memory(); - stats.used_size = stats.total_size - stats.free_size; + // 收集基本统计信息 + stats.total_size = segment_->get_size(); // 总大小 + stats.free_size = segment_->get_free_memory(); // 空闲大小 + stats.used_size = stats.total_size - stats.free_size; // 已用大小 + + // 统计对象数量(命名对象 + 唯一对象) stats.num_allocations = segment_->get_num_named_objects() + segment_->get_num_unique_objects(); try { + // 获取最大连续空闲块大小 + // 某些情况下可能失败(如内存高度碎片化) stats.largest_free_block = segment_->get_free_memory(); } catch (...) { + // 如果获取失败,设置为 0 stats.largest_free_block = 0; } return stats; } +/** + * @brief 获取共享内存分配器 + * + * 返回一个 void_allocator,可用于在共享内存中构造 STL 容器。 + * + * ## 分配器用途 + * STL 容器(如 vector、string、map)在共享内存中使用时,需要使用 + * 共享内存分配器来分配内部存储。void_allocator 是通用分配器,可以 + * 通过模板参数适配不同类型。 + * + * ## 使用示例 + * ```cpp + * // 在共享内存中创建 vector + * using shm_vector = boost::interprocess::vector>; + * + * auto alloc = manager.get_allocator(); + * auto* vec = segment_->construct("my_vector")(alloc); + * vec->push_back(42); + * ``` + * + * @return void_allocator 共享内存分配器 + * @throws std::runtime_error 如果管理器未初始化 + * + * @note 分配器必须在容器构造时传入 + * @note 分配器是轻量级对象,可以值传递 + * @note 线程安全 + * + * @see Boost.Interprocess 文档中的分配器章节 + */ auto shared_memory_manager::get_allocator() const -> void_allocator { + // 安全检查:确保共享内存段已初始化 if (!segment_) throw std::runtime_error("共享内存段未初始化,无法获取分配器"); + + // 获取段管理器并构造 void_allocator return void_allocator(segment_->get_segment_manager()); } +/** + * @brief 默认构造函数 + * + * 初始化成员变量为默认值。 + * 实际的资源分配在 init() 方法中进行。 + * + * @note 私有构造函数,仅被 lazy_singleton 调用 + * @note 构造后管理器处于未初始化状态 + */ shared_memory_manager::shared_memory_manager() { } +/** + * @brief 析构函数 + * + * 自动清理资源,调用 shutdown() 方法。 + * + * ## 清理行为 + * - 如果是创建者且 remove_on_destroy=true,移除共享内存段 + * - 否则只释放本地资源,保留共享内存段 + * + * @note 虚析构函数,支持多态删除 + * @note 异常安全,shutdown() 捕获所有异常 + */ shared_memory_manager::~shared_memory_manager() { shutdown(); } diff --git a/src/network/shm/shared_memory_manager.h b/src/network/shm/shared_memory_manager.h index e9ae3bb..1b4587c 100644 --- a/src/network/shm/shared_memory_manager.h +++ b/src/network/shm/shared_memory_manager.h @@ -1,98 +1,562 @@ +/** + * @file shared_memory_manager.h + * @brief 共享内存管理器 - Network模块的底层础设施 + * + * 本文件实现了跨进程共享内存的管理功能,是整个Network模块的核心基础设施之一。 + * + * ## 模块定位 + * - 作为Network模块的层组件,为上层的RPC和Transport提供共享内存支持 + * - 提供统一的共享内存创建、分配、查找和释放接口 + * - 基于Boost.Interprocess库实现跨平台的共享内存管理 + * + * ## 核心功能 + * 1. 共享内存段的创建和打开 + * 2. 命名对象的分配和查找 + * 3. 原始内存块的分配和管理 + * 4. 内存使用统计信息 + * 5. 自动资源清理(RAII) + * + * ## 设计特点 + * - 单例模式:全局唯一的共享内存管理器实例 + * - 类型安全:提供模板接口用于类型化对象分配 + * - 原始内存:支持字节级别的原始内存分配 + * - 错误处理:统一的错误码系统 + * - 资源管理:自动管理共享内存生命周期 + * + * ## 使用场景 + * - 跨进程音频数据传输(通过ring buffer) + * - 跨进程同步原语(互斥量、条件变量等) + * - 跨进程配置数据共享 + * - 进程间大块数据交换 + * + * ## 技术实现 + * - 基于Boost.Interprocess的managed_shared_memory + * - 支持命名对象和匿名对象 + * - 使用placement new在共享内存中构造对象 + * - 提供分配器(allocator)用于STL容器 + * + * ## 线程安全性 + * - 类本身的初始化和关闭操作不是线程安全的,应在主线程调用 + * - 对象分配、查找、释放操作内部由Boost.Interprocess保证线程安全 + * - 多个进程可以安全地并发访问共享内存段 + * + * @note 需要配合interprocess_synchronization模块使用以实现进程间同步 + * @see interprocess_synchronization.h + * @see lock_free_ring_buffer.h + * @see triple_buffer.h + */ + #pragma once #include #include #include "logger.h" +/// 日志模块标识符,用于标记该模块的日志输出 #define SHARED_MEMORY_MANAGER_LOG_MODULE "shared memory manager" +/** + * @enum shared_memory_error + * @brief 共享内存操作错误码 + * + * 统一的错误码枚举,用于表示共享内存操作的各种错误情况。 + * 所有共享内存相关的操作都返回此错误码,便于统一的错误处理。 + */ enum class shared_memory_error { - SUCCESS = 0, - CREATION_FAILED, - OPEN_FAILED, - ALLOCATION_FAILED, - INVALID_SIZE, - NOT_FOUND, - ACCESS_DENIED, - INSUFFICIENT_SPACE, - SYNCHRONIZATION_FAILED, - BUFFER_FULL, - BUFFER_EMPTY, - INVALID_OPERATION + SUCCESS = 0, ///< 操作成功 + CREATION_FAILED, ///< 创建共享内存段失败(可能是权限不足或系统资源不足) + OPEN_FAILED, ///< 打开共享内存段失败(段不存在或权限不足) + ALLOCATION_FAILED, ///< 内存分配失败(共享内存段空间不足) + INVALID_SIZE, ///< 无效的大小参数(如请求分配0字节或超大内存) + NOT_FOUND, ///< 未找到指定名称的对象 + ACCESS_DENIED, ///< 访问被拒绝(权限不足) + INSUFFICIENT_SPACE, ///< 共享内存段空间不足 + SYNCHRONIZATION_FAILED, ///< 同步操作失败(用于同步原语相关操作) + BUFFER_FULL, ///< 缓冲区已满(用于环形缓冲区等) + BUFFER_EMPTY, ///< 缓冲区为空(用于环形缓冲区等) + INVALID_OPERATION ///< 无效的操作(如在未初始化的状态下调用方法) }; +/** + * @struct shared_memory_config + * @brief 共享内存配置参数 + * + * 封装了共享内存段创建和管理所需的所有配置参数。 + * 这些参数控制共享内存的创建行为、大小、命名以及相关的同步原语。 + */ struct shared_memory_config { + /// 共享内存段名称 + /// @note 必须在系统范围内唯一,建议使用应用前缀避免冲突 + /// @note Windows上受到命名制,不能包含反斜杠 std::string segment_name; + + /// 共享内存段大小(字节) + /// @note 应大于实际需要的大小,因为Boost.Interprocess会使用部分空间存储元数据 + /// @note 推荐预留至少10-20%的额外空间用于内存管理开销 size_t segment_size; + + /// 如果共享内存段不存在,是否自动创建 + /// @note true: 不存在时创建新段 + /// @note false: 仅打开已存在的段,不存在则失败 bool create_if_not_exists; + + /// 销毁管理器时是否移除共享内存段 + /// @note true: 自动清理(适用于创建者进程) + /// @note false: 保留段(适用于非创建者进程或需要持久化的场景) bool remove_on_destroy; - + + /// 互斥量名称(用于进程间同步) + /// @note 可选配置,与interprocess_synchronization配合使用 std::string mutex_name; + + /// 条件变量名称(用于进程间等待/通知) + /// @note 可选配置,与interprocess_synchronization配合使用 std::string condition_name; + + /// 信号量名称(用于进程间计数信号) + /// @note 可选配置,与interprocess_synchronization配合使用 std::string semaphore_name; - + + /// 页大小(字节) + /// @note 默认4096字节,应系统页大小对齐以获得最佳性能 + /// @note 在某些系统上可能需要调整为系统实际页大小 size_t page_size = 4096; + + /// 是否使用大页(Huge Pages) + /// @note 大页可以减少TLB miss,提升性能 + /// @note 需要系统支持和足够的权限 + /// @note 仅在处理大量数据时才有明显收益 bool use_large_pages = false; - int memory_policy = 0; // 平台相关的内存策略标志 + + /// 平台相关的内存策略标志 + /// @note Linux: 可用于NUMA策略、存锁定等 + /// @note Windows: 可用于内存保护属性等 + /// @note 默认0表示使用系默认策略 + int memory_policy = 0; }; +/** + * @class shared_memory_manager + * @brief 共享内存管理器(单例) + * + * 这是整个共享内存管理的核心类,采用单例模式确保全局唯一实例。 + * 提供了完整的共享内存生命周期管理、对象分配、查找和统计功能。 + * + * ## 核心职责 + * 1. 创建或打开共享内存段 + * 2. 管理共享内存中的命名对象 + * 3. 提供类型安全的对象分配接口 + * 4. 提供原始内存分配接口 + * 5. 统计内存使用情况 + * 6. 自动清理资源 + * + * ## 设计模式 + * - **单例模式**: 继承自lazy_singleton延迟初始化 + * - **RAII**: 构造时创建/打开,析构时自动清理 + * - **模板方法**: 提供类型安全的泛型接口 + * + * ## 使用流程 + * 1. 获取单例实例: `shared_memory_manager::instance()` + * 2. 配置并初始化: `init(config)` + * 3. 分配对象: `allocate(name)` 或 `allocate_raw(size, name)` + * 4. 使用对象: 通过返回的指针访问 + * 5. 查找对象: `find(name)` `find_raw(name)` + * 6. 释放对象: `deallocate(name)` 或 `deallocate_raw(name)` + * 7. 关闭: `shutdown()` (可选,析构时自动调用) + * + * ## 内存管理策略 + * - 创建者进程负责创建和销毁共享内存段 + * - 非创建者进程只打开和使用,不销毁 + * - 通过`remove_on_destroy`配置控制清理行为 + * + * ## 线程安全性 + * - init/shutdown: 不是线程安全的,应在主线程调用 + * - allocate/find/deallocate: 内部由Boost.Interprocess保证线程安全 + * - 多进程安全: 多个进程可以并发访问 + * + * ## 性能考虑 + * - 对象查找开销: O(log N),N为段内对象数量 + * - 分配开销: 取决于段内碎片情况 + * - 跨进程访问: 无需拷贝,性能优于传统IPC + * + * @note 必须先调用init()再使用其他方法 + * @note 同一进程内多次调用init()会被忽略(保持幂等性) + */ class shared_memory_manager : public lazy_singleton { public: friend class lazy_singleton; + + /// Boost.Interprocess的managed_shared_memory类型别名 using managed_shared_memory = boost::interprocess::managed_shared_memory; + + /// void类型分配器,用于STL容器 + /// @note 用于在共享内存中构造STL容器(如vector、map等) using void_allocator = boost::interprocess::allocator; + /** + * @struct statistics + * @brief 共享内存使用统计信息 + * + * 提供共享内存段的实时统计数据,用于监控内存使用情况、 + * 检测内存泄漏、评估性能等。 + */ struct statistics { + /// 共享内存段总大小(字节) + /// @note 包括元数据开销 size_t total_size; + + /// 当前空闲大小(字节) + /// @note 可用于判断是否有足够空间分配新对象 size_t free_size; + + /// 已使用大小(字节) + /// @note used_size = total_size - free_size size_t used_size; + + /// 当前分配的对象数量 + /// @note 包括命名对象和唯一对象 size_t num_allocations; + + /// 最大连续空闲块大小(字节) + /// @note 用于判断能否分配大对象 + /// @note 由于碎片化,可能远小于free_size size_t largest_free_block; }; + /** + * @brief 初始化共享内存管理器 + * + * 根据提供的配置创建或打开共享内存段。此方法必须在使用其他方法前调用。 + * + * ## 初始化流程 + * 1. 检查是否已初始化(幂等性) + * 2. 保存配置参数 + * 3. 尝试打开已存在的共享内存段 + * 4. 如果不存在且允许创建,则创建新段 + * 5. 设置初始化标志 + * + * ## 创建者与非创建者 + * - 创建者: 第一个创建共享内存段的进程 + * - 非创建者: 后续打开已存在段的进程 + * - 内部通过creator_标区分 + * + * @param config 共享内存配置参数 + * @return shared_memory_error 错误码 + * - SUCCESS: 初始化成功 + * - CREATION_FAILED: 创建失败(权限不足或资源不足) + * - OPEN_FAILED: 打开失败且不允许创建 + * + * @note 多次调用会返回SUCCESS但不会重新初始化 + * @note 不同进程可以用不同的配置打开同一段(段名相同即可) + * @note 线程不安全,应在主线程的初始化阶段调用 + * + * @warning 确保segment_size足够大,后续无法动态扩展 + * @warning segment_name必须在系统范围内唯一 + */ auto init(const shared_memory_config& config) -> shared_memory_error; + /** + * @brief 关闭共享内存管理器 + * + * 释放共享内存资源,根据配置决定是否移除共享内存段。 + * + * ## 清理流程 + * 1. 检查是否已初始化 + * 2. 调用cleanup()释放资源 + * 3. 如果是创建者且配置为remove_on_destroy,则移除共享内存段 + * 4. 重置初始化标志 + * + * ## 创建者责任 + * - 只有创建者进程才应该移除共享内存段 + * - 非创建者进程关闭时应保持段存在 + * + * @return shared_memory_error 始终返回SUCCESS + * + * @note 析构函数会自动调用此方法 + * @note 多次调用是安全的(幂等性) + * @note 关闭后如需继续使用,需重新调用init() + * @note 线程不安全,应在程序退出时调用 + * + * @warning 关闭前应确保所有使用共享内存的操作已完成 + * @warning 如果其他进程仍在使用,不要移除段 + */ auto shutdown() -> shared_memory_error; + /** + * @brief 检查管理器是否已初始化 + * + * @return bool true表示已初始化可以使用;false表示未初始化 + * + * @note 线程安全的读取操作 + */ auto is_initialized() const { return initialized_; } + /** + * @brief 分配类型化对象 + * + * 在共享内存中分配并构造一个指定类型的命名对象。 + * 使用默认构造函数构造对象。 + * + * ## 内存分配 + * - 从共享内存段中分配sizeof(T)大小的空间 + * - 使用placement new在分配的空间上构造对象 + * - 对象可以被其他进程通过名称查找 + * + * ## 命名规则 + * - 名称在共享内存段内必须唯一 + * - 不同类型可以使用相同名称(但不推荐) + * - 建议使用描述性名称,如"audio_buffer_1" + * + * @tparam T 要分配的对象类型,必须满足: + * - 可默认构造 + * - 平凡可拷贝(推荐)或正确实现拷贝语义 + * - 不包含指针成员(除非指向共享内存) + * + * @param name 对象的唯一名称 + * @return T* 指向新分配对象的指针,失败时返回nullptr + * + * @note 分配的对象位于共享内存,所有进程可见 + * @note 返回的指针在进程间可能不同(虚拟地址映射) + * @note 线程安全,可以从多个线程调用 + * + * @warning 不要存储返回的指针到磁盘,下次加载时地址会变化 + * @warning 如果名称已存在,会抛出异常(返回nullptr) + * @warning T类型中不应包含向进程私有内存的指针 + * + * @see find() 查找已存在的对象 + * @see deallocate() 释放对象 + */ template auto allocate(const std::string& name) -> T*; + /** + * @brief 查找类型化对象 + * + * 根据名称在共享内存中查找已存在的对象。 + * + * ## 查找机制 + * - 在共享内存段的名称索引中搜索 + * - 查找复杂度: O(log N),N为对象数量 + * - 支持跨进程查找 + * + * @tparam T 对象类型,必须与分配时的类型匹配 + * @param name 对象名称 + * @return T* 指向对象的指针,未找到时返回nullptr + * + * @note 类型不匹配会导致未定义行为 + * @note 返回的指针在不同进程中地址可能不同 + * @note 线程安全 + * + * @warning 必须确保类型T与配时一致 + * @warning 返回的指针生命周期由共享内存段控制 + */ template auto find(const std::string& name) -> T*; + /** + * @brief 释放类型化对象 + * + * 销毁并释放共享内存中的命名对象。 + * + * ## 释放流程 + * 1. 查找指定名称的对象 + * 2. 调用对象的析构函数 + * 3. 释放内存回共享内存池 + * 4. 从名称索引中移除 + * + * @tparam T 对象类型,必须与分配时的类型匹配 + * @param name 对象名称 + * @return bool true表示成功释放false表示对象不存在或释放失败 + * + * @note 释放后,所有进程中指向该对象的指针都将失效 + * @note 线程安全 + * @note 只需一个进程调用释放,其他进程会感知到对象消失 + * + * @warning 释放后继续使用指针会导致未定义行为 + * @warning 确保没有其他线程/进程正在使用该对象 + */ template auto deallocate(const std::string& name) -> bool; + /** + * @brief 分配原始内存块 + * + * 在共享内存中分配指定大小的原始字节数组。 + * 不进行类型化构造,返回void*指针。 + * + * ## 使用场景 + * - 分配字节数组用于自定义数据结构 + * - 分配缓冲区用于音频数据等 + * - 分配空间用于placement new构造对象 + * + * @param size 要分配的字节数 + * @param name 内存块的唯一名称 + * @return void* 指向分配内存的指针,失败时返回nullptr + * + * @note 内存未初始化,包含随机数据 + * @note 适用于POD类型或手动管理构造/析构的场景 + * @note 线程安全 + * + * @warning 调用者负责正确的类型转换和内存管理 + * @warning 如果size为0,行为定义 + */ auto allocate_raw(size_t size, const std::string& name) -> void*; + /** + * @brief 查找原始内存块 + * + * 根据名称查找已分配的原始内存块。 + * + * @param name 内存块名称 + * @return void* 指向内存块的指针,未找到时返回nullptr + * + * @note 返回指针类型为void*,需要手动转换 + * @note 线程安全 + */ auto find_raw(const std::string& name) -> void*; + /** + * @brief 释放原始内存块 + * + * 释放指定名称的原始内存块。 + * + * @param name 内存块名称 + * @return bool true表示成功释放false表示释放失败或内存块不存在 + * + * @note 不调用析构函数,仅释放内存 + * @note 线程安全 + * + * @warning 如果内存块中存储了需要析构的对象,调用者需要手动析构 + */ auto deallocate_raw(const std::string& name) -> bool; + /** + * @brief 获取内存使用统计信息 + * + * 返回当前共享内存段的详细统计数据,包括总大小、已用大小、 + * 空闲大小、分配数量和最大连续空闲块等信息。 + * + * ## 统计用途 + * - 监控内存使用情况 + * - 检测内存泄 + * - 判断是否需要扩展内存 + * - 评估碎片化程度 + * + * @return statistics 统计信息结构体 + * + * @note 统计信息反映调用时刻的快照 + * @note 多进程环境下,统计会反映所有进程的累计使用 + * @note 如果未初始化,返回全零的统计信息 + * + * @warning 统计操作有一定开销,不要频繁调用 + */ auto get_statistics() -> statistics; + /** + * @brief 获取共享内存分配器 + * + * 返回一个void_allocator,可用于在共享内存中构造STL容器。 + * + * ## 使用示例 + * ```cpp + * using shm_string = boost::interprocess::basic_string< + * char, + * std::char_traits, + * void_allocator + * >; + * auto alloc = manager.get_allocator(); + * auto* str = segment_->construct("my_string")(alloc); + * ``` + * + * @return void_allocator 共享内存分配器 + * @throws std::runtime_error 如果管理器未初始化 + * + * @note 分配器必须在对象构造时传入 + * @note 适用于vector、string、map等STL容器 + */ auto get_allocator() const -> void_allocator; private: + /** + * @brief 私有构造函数 + * + * 单例模式要求,外部不能直接构造。 + * 通过lazy_singleton::instance()获取实例。 + */ shared_memory_manager(); + + /** + * @brief 析构函数 + * + * 自动调用shutdown()理资源。 + * 如果是创建者且配置了remove_on_destroy,会移除共享内存段。 + */ virtual ~shared_memory_manager() override; + /// 是否已初始化标志 + /// @note 用于防止重复初始化和未初始化使用 bool initialized_ = false; + + /// 是否为创建者标志 + /// @note true表示本进程创了共享内存段,负责清理 + /// @note false表示本进程只打开已存在的段 bool creator_ = false; + + /// 共享内存段智能指针 + /// @note 使用unique_ptr管理命周期 + /// @note 析构时自动释放段(但不移除,除非配置要求) std::unique_ptr segment_; + + /// 配置参数副本 + /// @note 保存init()时传入的配置,用于清理时参考 shared_memory_config config_{}; + /** + * @brief 创建或打开共享内存段 + * + * 内部方法,实现共享内存段的创建和打开逻辑。 + * + * ## 执行流程 + * 1. 尝试打开已存在的段(open_only) + * 2. 如果失败且允许创建,则移除可能损坏的旧段 + * 3. 创建新的共享内存段(create_only) + * 4. 设置creator_标志 + * + * @return shared_memory_error 错误码 + * + * @note 私有方法,仅被init()调用 + */ auto create_or_open_segment() -> shared_memory_error; + /** + * @brief 清理资源 + * + * 释放共享内存段对象,根据配置决定是否移除段。 + * + * @note 私有方法,被shutdown()和析构函数调用 + * @note 如果是创建者且remove_on_destroy为true,会移除段 + */ void cleanup(); }; +// ============================================================================ +// 模板方法实现 +// ============================================================================ + +/** + * @brief 分配类型化对象(模板实现) + * + * 详细说明见类内声明。 + * + * @note 实现细节: + * - 使用Boost.Interprocess的construct + * - 异常安全:捕获所有异常并返回nullptr + * - 日志记录:失败时记录错误日志 + */ template auto shared_memory_manager::allocate(const std::string& name) -> T* { if (!segment_) { - log_module_error(SHARED_MEMORY_MANAGER_LOG_MODULE, "尝试在未初始化的共享内存段中分配对象: {}", name); + log_module_error(SHARED_MEMORY_MANAGER_LOG_MODULE, "尝试在未初始化共享内存段中分配对象: {}", name); return nullptr; } @@ -103,16 +567,26 @@ auto shared_memory_manager::allocate(const std::string& name) -> T* { } } +/** + * @brief 查找类型化对象(模板实现) + * + * 详细说明见类内声明。 + * + * @note 实现细节: + * - 使用Boost.Interprocess的find + * - 返回值为pair,first是指针,second是对象数量 + * - 未找到时first为nullptr + */ template auto shared_memory_manager::find(const std::string& name) -> T* { if (!segment_) { - log_module_error(SHARED_MEMORY_MANAGER_LOG_MODULE, "尝试在未初始化的共享内存段中查找对象: {}", name); + log_module_error(SHARED_MEMORY_MANAGER_LOG_MODULE, "尝试在未初始化共享内存段中查找对象: {}", name); return nullptr; } try { const auto& result = segment_->find(name.c_str()); - if (result.first == nullptr) { log_module_warn(SHARED_MEMORY_MANAGER_LOG_MODULE, "未找到共享内存对象: {}", name); } + if (result.first == nullptr) { log_module_warn(SHARED_MEMORY_MANAGER_LOG_MODULE, "未找到共享内存象: {}", name); } return result.first; } catch (const boost::interprocess::interprocess_exception& e) { @@ -121,10 +595,20 @@ auto shared_memory_manager::find(const std::string& name) -> T* { } } +/** + * @brief 释放类型化对象(模板实现) + * + * 详细说明见类内声明。 + * + * @note 实现细节: + * - 使用Boost.Interprocess的destroy + * - 自动调用T的析构数 + * - 释放后内存返回共享内存池 + */ template auto shared_memory_manager::deallocate(const std::string& name) -> bool { if (!segment_) { - log_module_error(SHARED_MEMORY_MANAGER_LOG_MODULE, "尝试在未初始化的共享内存段中释放对象: {}", name); + log_module_error(SHARED_MEMORY_MANAGER_LOG_MODULE, "尝试在未初始化共享内存段中释放对象: {}", name); return false; } @@ -138,33 +622,103 @@ auto shared_memory_manager::deallocate(const std::string& name) -> bool { } } +// ============================================================================ +// 全局辅助函数 - 简化共享内存操作的便捷接口 +// ============================================================================ + +/** + * @brief 分配原始内存块(全局函数) + * + * 便捷函数,直接调用单例的allocate_raw方法。 + * + * @param name 内存块名称 + * @param size 字节数 + * @return void* 指向分配内存的指针,失败返回nullptr + * + * @note 等价于 shared_memory_manager::instance().allocate_raw(size, name) + */ inline auto shm_allocate_raw(const std::string& name, size_t size) { return shared_memory_manager::instance().allocate_raw(size, name); } +/** + * @brief 查找原始内存块(全局函数) + * + * 便捷函数,直接调用单例的find_raw方法。 + * + * @param name 内存块名称 + * @return void* 指向内存块的指针,未找到返回nullptr + */ inline auto shm_find_raw(const std::string& name) { return shared_memory_manager::instance().find_raw(name); } +/** + * @brief 释放原始内存块(全局函数) + * + * 便捷函数,直接调用单例的deallocate_raw方法。 + * + * @param name 内存名称 + * @return bool true表示成功,false表示失败 + */ inline auto shm_deallocate_raw(const std::string& name) { return shared_memory_manager::instance().deallocate_raw(name); } +/** + * @brief 查找并转换原始内存块(模板全局函数) + * + * 查找原始内存块并转换为指定类型的指针。 + * + * @tparam T 目标类型 + * @param name 内存块名称 + * @return T* 类型转换后的指针,未找到返回nullptr + * + * @note 调用者需确保类型转换的正确性 + * @warning 不安全的类型转换可能导致未定义行为 + */ template auto shm_find_raw(const std::string& name) { return static_cast(shared_memory_manager::instance().find_raw(name)); } +/** + * @brief 分配类型化对象(模板全局函数) + * + * 便函数,直接调用单例的allocate方法。 + * + * @tparam T 对象类型 + * @param name 对象名称 + * @return T* 指向新分配对象的指针,失败返回nullptr + */ template auto shm_allocate(const std::string& name) { return shared_memory_manager::instance().allocate(name); } +/** + * @brief 查找类型化对象(模板全局函数) + * + * 便捷函数,直接调用单例的find方法。 + * + * @tparam T 对象类型 + * @param name 对象名称 + * @return T* 指向对象的指针,未找到返回nullptr + */ template auto shm_find(const std::string& name) { return shared_memory_manager::instance().find(name); } +/** + * @brief 释放类型化对象(模板全局函数) + * + * 便捷函数,直接调用单例的deallocate方法。 + * + * @tparam T 对象类型 + * @param name 对象名称 + * @return bool true表示成功,false表示失败 + */ template auto shm_deallocate(const std::string& name) { return shared_memory_manager::instance().deallocate(name); diff --git a/src/network/shm/triple_buffer.cpp b/src/network/shm/triple_buffer.cpp index aa73253..72e2990 100644 --- a/src/network/shm/triple_buffer.cpp +++ b/src/network/shm/triple_buffer.cpp @@ -1 +1,204 @@ +/** + * @file triple_buffer.cpp + * @brief 无锁三缓冲区的包含文件 + * + * 本文件是 triple_buffer 模板类的实现文件。 + * + * ## 文件说明 + * + * triple_buffer 是一个纯模板类,所有实现都在头文件中。 + * 这个 .cpp 文件的存在主要是为了: + * + * 1. **保持项目结构一性** + * - 大多数类都有对应的 .h 和 .cpp 文件 + * - 即使是模板类也保持这种结构 + * - 便于项目管理和构建系统 + * + * 2. **预留扩展空间** + * - 未来可能添加非模板的辅助函数 + * - 可能添加显式模板实例化(如 lock_free_ring_buffer) + * - 可能添加静态成员变量的定义 + * + * 3. **编译单元的完整** + * - 某些构建系统需要每个头文件有对应的 .cpp + * - 便于集成到构建系统中 + * + * ## 为什么不像 lock_free_ring_buffer 那样显式实例化 + * + * triple_buffer 和 lock_free_ring_buffer 虽然都是模板类, + * 但它们的使用场景不同: + * + * ### lock_free_ring_buffer 的情况 + * - 有固定的常用类型(float, double, int16_t 等) + * - 这些类型在整个项目中反复使用 + * - 显式实例化可以显著减少编译时间和二进制大小 + * + * ### triple_buffer 的情况 + * - 通常用于复杂的自定义结构体(如帧数据、状态对象) + * - 每个使用场景的类型可能都不同 + * - 没有明显的"常用型"模式 + * - 显式实例化的收益较小 + * + * ## 使用示例 + * + * triple_buffer 的使用者只需包含头文件即可: + * + * ```cpp + * #include "triple_buffer.h" + * + * // 定义自定义数据结构 + * struct GameState { + * int player_x, player_y; + * int score; + * float time_elapsed; + * }; + * + * // 直接使用,编译器会自动实例化 + * triple_buffer state_buffer("game_state"); + * + * // 写入 + * GameState* write_state = state_buffer.get_write_buffer(); + * if (write_state) { + * write_state->player_x = 100; + * write_state->score = 42; + * state_buffer.commit_write(); + * } + * + * // 读取 + * const GameState* read_state = state_buffer.get_read_buffer(); + * if (read_state) { + * render_game(read_state); + * state_buffer.commit_read(); + * } + * ``` + * + * ## 未来可能的扩展 + * + * 如果发现某些类型被频繁使用,可以在此文件中添加显式实例化: + * + * ```cpp + * // 假设帧数据是常用类型 + * struct AudioFrame { + * float samples[1024]; + * uint64_t timestamp; + * }; + * + * // 显式实例化 + * template class triple_buffer; + * ``` + * + * ## 性能考虑 + * + * 由于 triple_buffer 的模板代码都在头文件中: + * - 每个使用不同类型的编译单元都会实例化一次 + * - 编译器的内联优化可以充分发挥作用 + * - 链接器会移除重复的实例化代码(COMDAT) + * - 对于小型项目,影响微乎其微 + * - 对于大型项目,如果发现编译慢,可考虑显式实例化 + * + * ## 与 lock_free_ring_buffer 的对比 + * + * | 特性 | triple_buffer | lock_free_ring_buffer | + * |------|---------------|----------------------| + * | 显式实例化 | 否 | 是 | + * | 常用类型 | 不固定 | 固定(float, int16_t等) | + * | 使用频率 | 较低 | 高 | + * | 类型多样性 | 高 | 低 | + * | 编译影响 | 较小 | 较大(如不实例化) | + * + * @note 这是一个占位文件,未来可能添加更多实现 + * @note 纯模板实现都在头文件中 + * + * @see triple_buffer.h + */ + #include "triple_buffer.h" + +// ============================================================================ +// 文件结构说明 +// ============================================================================ + +/** + * ## 为什么这个文件几乎是空的 + * + * 这是一个常见的 C++ 模板编程实践: + * + * 1. **模板的特性** + * - 模板代码必须对编译器可见 + * - 通常全部放在头文件中 + * - 编译器需要看到完整实现才能实例化 + * + * 2. **分离编译的局限** + * - 传统的 .h/.cpp 分离适用于非模板代码 + * - 模板代码如果放在 .cpp 中,其他文件无法使用 + * - 除非显式实例化所有可能的类型(不现实) + * + * 3. **现代 C++ 实践** + * - 对于模板库,头文件实现是标准做法 + * - STL、Boost 等都采用这种方式 + * - 虽然增加了编译时间,但保证了灵活性 + * + * ## 编译模型 + * + * ### 包含模型(Inclusion Model)- 当前使用 + * ``` + * 编译单元 A: + * #include "triple_buffer.h" + * triple_buffer buf_a; + * + * → 编译器实例化 triple_buffer + * → 生成 TypeA 特化的所有方法 + * + * 编译单元 B: + * #include "triple_buffer.h" + * triple_buffer buf_b; + * + * → 编译器实例化 triple_buffer + * → 生成 TypeB 特化的所有方法 + * ``` + * + * ### 显式实例化模型(Explicit Instantiation)- 未使用 + * ``` + * triple_buffer.h: + * 只有声明 + * + * triple_buffer.cpp: + * #include "triple_buffer.h" + * template class triple_buffer; + * template class triple_buffer; + * + * → 只在此文件生成实例 + * → 其他文件只能使用这些类型 + * → 类型受限但编译更快 + * ``` + * + * ## 何时添加显式实例化 + * + * 如果满足以下条件,可以考虑添加: + * + * 1. **性能问题** + * - 编译时间过长 + * - 二进制文件过大 + * - 链接时间过长 + * + * 2. **类型固定** + * - 项目中只使用少数几种类型 + * - 类型不会频繁变化 + * - 可以枚举所有需要的类型 + * + * 3. **代码稳定** + * - 模板实现已经稳定 + * - 不需要频繁修改 + * - 适合进行优化 + * + * ## 总结 + * + * 这个文件的存在体现了软件工程中的几个原则: + * + * - **一致性**: 保持项目结构统一 + * - **可扩展性**: 预留未来扩展空间 + * - **灵活性**: 不限制使用的类型 + * - **实用性**: 优先考虑使用便利性 + * + * 对于模板库来说,这是一个合理的折衷方案。 + */ diff --git a/src/network/shm/triple_buffer.h b/src/network/shm/triple_buffer.h index 55d9787..675b3de 100644 --- a/src/network/shm/triple_buffer.h +++ b/src/network/shm/triple_buffer.h @@ -1,3 +1,123 @@ +/** + * @file triple_buffer.h + * @brief 无锁三缓冲区 - 实现读写完全分离的无锁数据交换 + * + * 本文件实现了基于三缓冲区的无锁数据交换机制(Triple Buffer), + * 是Network模块中用于跨进程数据同步的另一种高性能数据结构。 + * + * ## 模块定位 + * - 构建在 shared_memory_manager 之上 + * - 提供读写完全分离的无锁数据交换 + * - 适用于帧数据、快照数据等场景 + * - 与 lock_free_ring_buffer 互补,各有优势 + * + * ## 核心特性 + * 1. **无锁设计**:使用原子操作,无需互斥量 + * 2. **读写分离**:读写操作完全独立,互不阻塞 + * 3. **最新数据**:读取总是获取最新完成的写入 + * 4. **零拷贝**:共享内存中直接交换 + * 5. **无队列**:不维护历史数据,只保留最新 + * + * ## 设计原理 + * + * ### 三缓冲区架构 + * ``` + * 三个缓冲区的角色: + * + * [Write Buffer] ← 生产者正在写入 + * [Available] ← 待交换的缓冲区(最新完成的写入) + * [Read Buffer] ← 消费者正在读取 + * + * 状态转换: + * + * 1. 生产者完成写入: + * Write <-> Available 交换 + * + * 2. 消费者开始读取(有新数据时): + * Read <-> Available 交换 + * ``` + * + * ### 为什么需要三个缓冲区 + * + * **双缓冲的问题**: + * - 只有两个缓冲区时,读写必须同步 + * - 写入完成前,读取必须等待 + * - 读取期间,写入被阻塞 + * + * **三缓冲的优势**: + * - 写缓冲:生产者独占,可以自由写入 + * - 读缓冲:消费者独占,可以自由读取 + * - 可用缓冲:作为中转,实现异步交换 + * - 读写完全独立,无阻塞 + * + * ### 数据新鲜度保证 + * - 写入总是写到最新的缓冲区 + * - 读取总是读到最新完成的数据 + * - 中间的写入可能被覆盖(无队列) + * - 适用于只关心最新状态的场景 + * + * ### 内存序(Memory Order) + * - **acquire**:读取索引时,确保看到完整的数据 + * - **release**:更新索引时,确保数据已写入 + * - **acq_rel**:交换操作,既要读也要写 + * + * ## 与 Ring Buffer 的对比 + * + * | 特性 | Triple Buffer | Ring Buffer | + * |------|---------------|-------------| + * | 数据模型 | 快照/帧 | 流/队列 | + * | 历史数据 | 不保留 | 保留(容量内) | + * | 数据新鲜度 | 总是最新 | 按顺序消费 | + * | 写入阻塞 | 永不阻塞 | 满时阻塞 | + * | 读取阻塞 | 永不阻塞 | 空时阻塞 | + * | 内存开销 | 固定(3×元素大小) | 可变(容量×元素大小) | + * | 适用场景 | 状态同步、帧数据 | 消息队列、日志流 | + * + * ## 使用场景 + * + * ### 适合的场景 + * - **状态同步**:只关心最新状态,不关心中间变化 + * - **帧渲染**:游戏/GUI,只需要最新帧 + * - **传感器数据**:只需要最新读数 + * - **配置更新**:只关心最新配置 + * - **实时监控**:显示最新指标 + * + * ### 不适合的场景 + * - 需要处理所有历史数据 + * - 消息队列(每条消息都要处理) + * - 审计日志(不能丢失) + * - 事务处理(需要顺序保证) + * + * ## 性能特点 + * - **延迟**:极低(只需原子交换) + * - **吞吐量**:取决于数据大小 + * - **可扩展性**:完美(读写无竞争) + * - **内存开销**:固定(3 × sizeof(T)) + * - **缓存友好**:缓存行对齐 + * + * ## 线程安全性 + * - **单写单读安全**:一个生产者 + 一个消费者 + * - **不支持多写/多读**:会导致数据竞争 + * - **跨进程安全**:支持不同进程的读写 + * + * ## 类型要求 + * - T 必须是平凡可拷贝类型(trivially copyable) + * - 不能包含指向进程私有内存的指针 + * - 适用于 POD 类型、简单结构体等 + * + * @note 仅支持单生产者-单消费者模型 + * @note 不保留历史数据,只保证最新数据 + * @note 需要配合 shared_memory_manager 使用 + * + * @see shared_memory_manager.h + * @see lock_free_ring_buffer.h (队列语义的替代方案) + * + * ## 参考资料 + * - "Triple Buffering" - Game Programming Patterns + * - Preshing on Programming: "The Synchronization Quadrant" + * - Real-Time Rendering (书籍) + */ + #pragma once #include #include @@ -5,64 +125,439 @@ #include "shared_memory_manager.h" +/// 日志模块标识符 #define TRIPLE_BUFFER_LOG_MODULE "triple buffer" -// 无锁三缓冲区 +/** + * @class triple_buffer + * @brief 无锁三缓冲区(单生产者-单消费者) + * + * 这是一个模板类,实现了基于三缓冲区的无锁数据交换。 + * 读写操作完全分离,各自拥有独立的缓冲区,通过原子交换实现同步。 + * + * ## 设计模式 + * - **SPSC**:单生产者-单消费者模型 + * - **Lock-Free**:使用原子操作,无锁 + * - **RAII**:自动管理共享内存生命周期 + * + * ## 核心机制 + * + * ### 三个索引 + * - `write_index`:当前写缓冲区索引(生产者修改) + * - `read_index`:当前读缓冲区索引(消费者修改) + * - `available_index`:可用缓冲区索引(交换中转) + * + * ### 新数据标志 + * - `new_data`:标识是否有新数据可读 + * - 写入提交时设置为 true + * - 读取提交时设置为 false + * + * ### 缓冲区交换 + * - 使用 `exchange()` 原子操作实现无锁交换 + * - 保证交换的原子性和可见性 + * + * ## 使用示例 + * + * ### 生产者端 + * ```cpp + * struct Frame { + * int width, height; + * float data[1920*1080]; + * }; + * + * triple_buffer buffer("frame_buffer"); + * + * // 写入流程 + * Frame* write_frame = buffer.get_write_buffer(); + * if (write_frame) { + * // 填充帧数据 + * write_frame->width = 1920; + * write_frame->height = 1080; + * render_to_frame(write_frame); + * + * // 提交写入(使新数据对读取者可见) + * buffer.commit_write(); + * } + * + * // 或者放弃写入 + * // buffer.discard_write(); + * ``` + * + * ### 消费者端 + * ```cpp + * triple_buffer buffer("frame_buffer"); + * + * // 读取流程 + * const Frame* read_frame = buffer.get_read_buffer(); + * if (read_frame) { + * // 使用帧数据 + * display_frame(read_frame); + * + * // 提交读取(允许获取更新的帧) + * buffer.commit_read(); + * } + * + * // 检查是否有新数据 + * if (buffer.has_new_data()) { + * // 有新帧可用 + * } + * ``` + * + * @tparam T 元素类型,必须是平凡可拷贝的 + * + * @note 构造函数会在共享内存中分配缓冲区 + * @note 析构函数由创建者负责释放共享内存 + * @note get_write/read 必须配对 commit 或 discard + */ template class triple_buffer { public: + /// 编译期类型检查:T 必须是平凡可拷贝类型 + /// @note 这对于共享内存中的对象是必需的 static_assert(std::is_trivially_copyable_v, "T 必须是平凡可拷贝类型"); + /** + * @brief 构造函数 + * + * 创建或打开共享内存中的三缓冲区。 + * + * ## 创建流程 + * 1. 尝试在共享内存中查找缓冲区 + * 2. 如果不存在,分配新的共享内存 + * 3. 使用 placement new 构造 buffer_data + * 4. 初始化三个索引(0, 0, 1) + * 5. 初始化新数据标志为 false + * 6. 标记为创建者(负责清理) + * + * ## 初始状态 + * - write_index = 0:生产者使用缓冲区 0 + * - read_index = 0:消费者使用缓冲区 0(相同没关系,各自独立) + * - available_index = 1:缓冲区 1 可用作交换 + * - 缓冲区 2 未使用(保留) + * - new_data = false:初始无新数据 + * + * @param name 缓冲区在共享内存中的唯一名称 + * + * @throws std::runtime_error 如果无法分配共享内存 + * + * @note 不同进程必须使用相同的 T 类型 + */ explicit triple_buffer(const std::string& name); + + /** + * @brief 析构函数 + * + * 如果是创建者,释放共享内存;否则只释放本地资源。 + * + * @note 非创建者进程不会释放共享内存 + * @note 创建者进程负责最终清理 + */ ~triple_buffer(); - // 禁止拷贝,允许移动 + // 禁止拷贝(共享内存对象不应拷贝) triple_buffer(const triple_buffer&) = delete; triple_buffer& operator=(const triple_buffer&) = delete; + + // 允许移动(转移所有权) triple_buffer(triple_buffer&&) = default; triple_buffer& operator=(triple_buffer&&) = default; - // 生产者接口 + // ======================================================================== + // 生产者接口(写入端) + // ======================================================================== + + /** + * @brief 获取写缓冲区指针 + * + * 返回当前可以写入的缓冲区指针。 + * + * ## 获取规则 + * - 如果已有活跃的写操作(未提交/放弃),返回 nullptr + * - 否则,返回当前可用缓冲区的指针 + * + * ## 使用流程 + * 1. 调用 get_write_buffer() 获取指针 + * 2. 检查指针是否为 nullptr + * 3. 写入数据到缓冲区 + * 4. 调用 commit_write() 提交或 discard_write() 放弃 + * + * ## 并发性 + * - 一次只能有一个活跃的写操作 + * - 写操作期间,读操作不受影响(使用不同缓冲区) + * - 多次调用 get_write_buffer() 会返回同一缓冲区 + * + * @return T* 写缓冲区指针 + * @return nullptr 如果已有活跃的写操作 + * + * @note 获取后必须调用 commit_write 或 discard_write + * @note 仅生产者线程应调用此方法 + * @note 返回的指针在 commit 或 discard 前有效 + * + * @warning 不要在多个线程中同时调用 + * @warning 必须配对 commit 或 discard,否则会永久锁定 + */ auto get_write_buffer() -> T*; + + /** + * @brief 提交写入 + * + * 完成写入并使新数据对读取者可见。 + * + * ## 提交流程 + * 1. 检查是否有活跃的写操作 + * 2. 将写缓冲区与可用缓冲区交换(原子操作) + * 3. 更新写索引指向新的写缓冲区 + * 4. 设置 new_data 标志为 true + * 5. 清除活跃写操作标记 + * + * ## 交换语义 + * ``` + * 提交前: + * write_index=0 (正在写入) + * available_index=1 (待交换) + * + * 提交后: + * write_index=1 (新的写缓冲) + * available_index=0 (最新完成的数据,供读取) + * ``` + * + * ## 可见性保证 + * - 使用 release 内存序确保数据写入完成 + * - 读取者使用 acquire 内存序读取索引 + * - 保证读取者看到完整的数据 + * + * @note 提交后,之前的写缓冲区变为可用缓冲区 + * @note 如果没有活跃的写操作,此方法无效果 + * @note 仅生产者线程应调用此方法 + * + * @warning 提交前确保数据已完整写入 + */ void commit_write(); + + /** + * @brief 放弃写入 + * + * 取消当前的写操作,数据不会对读取者可见。 + * + * ## 使用场景 + * - 写入过程中发生错误 + * - 决定不提交当前数据 + * - 写入被中断或取消 + * + * @note 放弃后,缓冲区内容保留但不可见 + * @note 可以再次调用 get_write_buffer 重新开始 + * @note 如果没有活跃的写操作,此方法无效果 + */ void discard_write(); - // 消费者接口 + // ======================================================================== + // 消费者接口(读取端) + // ======================================================================== + + /** + * @brief 获取读缓冲区指针 + * + * 返回当前可以读取的缓冲区指针。 + * + * ## 获取规则 + * - 如果已有活跃的读操作,返回当前读缓冲区 + * - 如果有新数据,交换并返回新数据缓冲区 + * - 如果没有新数据,返回 nullptr + * + * ## 新数据检查 + * - 检查 new_data 标志 + * - 如果为 true,从 available 获取新数据 + * - 如果为 false,返回 nullptr + * + * ## 使用流程 + * 1. 调用 get_read_buffer() 获取指针 + * 2. 检查指针是否为 nullptr + * 3. 读取数据 + * 4. 调用 commit_read() 提交读取 + * + * ## 并发性 + * - 一次只能有一个活跃的读操作 + * - 读操作期间,写操作不受影响 + * - 可以多次调用获取同一缓冲区 + * + * @return const T* 读缓冲区指针(只读) + * @return nullptr 如果没有新数据可读 + * + * @note 获取后必须调用 commit_read + * @note 仅消费者线程应调用此方法 + * @note 返回 const 指针,禁止修改数据 + * + * @warning 不要在多个线程中同时调用 + * @warning 必须配对 commit_read,否则无法获取新数据 + */ auto get_read_buffer() -> const T*; + + /** + * @brief 提交读取 + * + * 完成读取并允许获取新的数据。 + * + * ## 提交流程 + * 1. 检查是否有活跃的读操作 + * 2. 将读缓冲区与可用缓冲区交换 + * 3. 更新可用索引 + * 4. 清除 new_data 标志 + * 5. 清除活跃读操作标记 + * + * ## 交换语义 + * ``` + * 提交前: + * read_index=2 (正在读取) + * available_index=0 (待交换) + * + * 提交后: + * read_index=0 (新的读缓冲,如有新写入) + * available_index=2 (旧的读缓冲,可重用) + * ``` + * + * @note 提交后才能获取新的数据 + * @note 如果没有活跃的读操作,此方法无效果 + * @note 仅消费者线程应调用此方法 + */ void commit_read(); + // ======================================================================== // 状态查询 + // ======================================================================== + + /** + * @brief 检查是否有新数据 + * + * 查询 new_data 标志,判断是否有新完成的写入。 + * + * ## 使用场景 + * - 轮询检查是否有新数据 + * - 避免不必要的读取尝试 + * - 实现条件读取逻辑 + * + * @return true 有新数据可读 + * @return false 没有新数据 + * + * @note 使用 acquire 内存序确保看到最新状态 + * @note 结果可能立即过时(生产者可能同时写入) + * @note 仅作为参考,实际读取仍需调用 get_read_buffer + */ [[nodiscard]] auto has_new_data() const -> bool; + + /** + * @brief 获取待处理的写入数量 + * + * 返回是否有活跃的写操作。 + * + * @return size_t 1 表示有活跃写操作,0 表示无 + * + * @note 主要用于调试和监控 + * @note 生产者可用此检查是否忘记提交 + */ [[nodiscard]] auto pending_writes() const -> size_t; private: + /** + * @struct buffer_data + * @brief 三缓冲区共享内存数据结构 + * + * 这是实际存储在共享内存中的数据结构。 + * + * ## 内存布局 + * ``` + * +----------------------+ <- 共享内存起始 + * | write_index (atomic) | 4 bytes + * +----------------------+ + * | read_index (atomic) | 4 bytes + * +----------------------+ + * | available_index (at) | 4 bytes + * +----------------------+ + * | new_data (atomic) | 1 byte + * +----------------------+ + * | padding | 47 bytes (对齐到 64) + * +----------------------+ <- 64 字节边界 + * | buffers[0] | sizeof(T) + * +----------------------+ + * | buffers[1] | sizeof(T) + * +----------------------+ + * | buffers[2] | sizeof(T) + * +----------------------+ + * ``` + * + * ## 对齐说明 + * - buffers 数组对齐到 64 字节(缓存行) + * - 避免索引与数据在同一缓存行 + * - 提升多核性能 + */ struct buffer_data { - std::atomic write_index{0}; // 写缓冲区索引 - std::atomic read_index{0}; // 读缓冲区索引 - std::atomic available_index{1}; // 可用缓冲区索引 - std::atomic new_data{false}; // 是否有新数据可读 - alignas(64) T buffers[3]; // 三个缓冲区 + /// 当前写缓冲区索引(0-2) + /// @note 生产者修改,消费者读取 + std::atomic write_index{0}; + + /// 当前读缓冲区索引(0-2) + /// @note 消费者修改,生产者读取 + std::atomic read_index{0}; + + /// 可用缓冲区索引(0-2) + /// @note 作为交换中转 + /// @note 存储最新完成的写入 + std::atomic available_index{1}; + + /// 新数据标志 + /// @note true: 有新数据可读 + /// @note false: 无新数据 + std::atomic new_data{false}; + + /// 三个缓冲区数组 + /// @note alignas(64) 对齐到缓存行 + /// @note 避免伪共享 + alignas(64) T buffers[3]; }; + /// 指向共享内存中 buffer_data 的指针 + /// @note 不拥有内存,由 shared_memory_manager 管理 buffer_data* buffer_; + + /// 缓冲区在共享内存中的名称 std::string name_; + + /// 是否为创建者标志 + /// @note true: 本进程创建了缓冲区,负责清理 + /// @note false: 本进程只打开了缓冲区 bool creator_; + /// 当前写缓冲区索引(本地缓存) + /// @note -1 表示无活跃写操作 + /// @note >= 0 表示正在写入的缓冲区索引 int32_t current_write_buffer_ = -1; + + /// 当前读缓冲区索引(本地缓存) + /// @note -1 表示无活跃读操作 + /// @note >= 0 表示正在读取的缓冲区索引 int32_t current_read_buffer_ = -1; }; +// ============================================================================ +// 模板方法实现 +// ============================================================================ + +/** + * @brief 构造函数实现 + */ template triple_buffer::triple_buffer(const std::string& name) : name_(name), creator_(false) { + // 尝试在共享内存中查找已存在的缓冲区 buffer_ = shm_find(name_); if (!buffer_) { + // 缓冲区不存在,分配新的共享内存 buffer_ = shm_allocate(name_); if (!buffer_) { throw std::runtime_error("无法分配共享内存"); } + // 标记为创建者,负责清理 creator_ = true; log_module_debug(TRIPLE_BUFFER_LOG_MODULE, "创建无锁三缓冲区: {}", name_); } @@ -71,82 +566,127 @@ triple_buffer::triple_buffer(const std::string& name) : name_(name), } } +/** + * @brief 析构函数实现 + */ template triple_buffer::~triple_buffer() { if (creator_) { + // 创建者负责释放共享内存 shm_deallocate(name_); log_module_debug(TRIPLE_BUFFER_LOG_MODULE, "销毁无锁三缓冲区: {}", name_); buffer_ = nullptr; } } +/** + * @brief 获取写缓冲区实现 + */ template auto triple_buffer::get_write_buffer() -> T* { + // 如果已有活跃的写操作,返回 nullptr if (current_write_buffer_ >= 0) { return nullptr; } + // 获取当前可用缓冲区索引(acquire:确保看到完整数据) current_write_buffer_ = buffer_->available_index.load(std::memory_order_acquire); + + // 返回写缓冲区指针 return &buffer_->buffers[current_write_buffer_]; } +/** + * @brief 提交写入实现 + */ template void triple_buffer::commit_write() { + // 检查是否有活跃的写操作 if (current_write_buffer_ < 0) { return; // 没有活跃的写操作 } - // 交换写缓冲区和可用缓冲区 + // 原子交换:将写缓冲区与可用缓冲区交换 + // exchange 返回旧的 write_index,将其设置为新的 available auto old_available = buffer_->available_index.exchange( buffer_->write_index.load(std::memory_order_relaxed), std::memory_order_acq_rel); + // 更新写索引为当前写缓冲区(release:确保数据可见) buffer_->write_index.store(current_write_buffer_, std::memory_order_release); + + // 设置新数据标志(release:确保索引更新可见) buffer_->new_data.store(true, std::memory_order_release); + // 清除活跃写操作标记 current_write_buffer_ = -1; } +/** + * @brief 放弃写入实现 + */ template void triple_buffer::discard_write() { + // 只清除活跃写操作标记,不交换缓冲区 current_write_buffer_ = -1; } +/** + * @brief 获取读缓冲区实现 + */ template auto triple_buffer::get_read_buffer() -> const T* { + // 如果已有活跃的读操作,继续使用当前读缓冲区 if (current_read_buffer_ >= 0) { - // 继续使用当前读缓冲区 return &buffer_->buffers[current_read_buffer_]; } + // 检查是否有新数据(acquire:确保看到最新写入) if (!buffer_->new_data.load(std::memory_order_acquire)) { return nullptr; // 没有新数据 } + // 获取最新写入的缓冲区索引(acquire:确保看到完整数据) current_read_buffer_ = buffer_->write_index.load(std::memory_order_acquire); + + // 返回读缓冲区指针(const,禁止修改) return &buffer_->buffers[current_read_buffer_]; } +/** + * @brief 提交读实现 + */ template void triple_buffer::commit_read() { + // 检查是否有活跃的读操作 if (current_read_buffer_ < 0) { return; // 没有活跃的读操作 } - // 交换读缓冲区和可用缓冲区 + // 原子交换:将读缓冲区与可用缓冲区交换 + // exchange 返回旧的 read_index,将其设置为新的 available buffer_->available_index.store( buffer_->read_index.exchange(current_read_buffer_, std::memory_order_acq_rel), std::memory_order_release); + // 清除新数据标志(release:确保交换完成) buffer_->new_data.store(false, std::memory_order_release); + + // 清除活跃读操作标记 current_read_buffer_ = -1; } +/** + * @brief 检查新数据实现 + */ template auto triple_buffer::has_new_data() const -> bool { return buffer_->new_data.load(std::memory_order_acquire); } +/** + * @brief 获取待处理写入数量实现 + */ template auto triple_buffer::pending_writes() const -> size_t { return current_write_buffer_ >= 0 ? 1 : 0; diff --git a/src/network/transport/transport.cpp b/src/network/transport/transport.cpp index 5c284d5..8903774 100644 --- a/src/network/transport/transport.cpp +++ b/src/network/transport/transport.cpp @@ -1 +1,33 @@ +/** + * @file transport.cpp + * @brief 传输层基础实现文件 + * + * 本文件是transport.h的对应实现文件。由于transport.h中定义的audio_processing_state + * 结构体完全由Boost.Interprocess库的模板类组成,不需要额外的实现代码,因此本文件 + * 目前只包含头文件的引用。 + * + * ## 文件存在的意义 + * 尽管本文件目前只有一个#include语句,但它的存在是为了: + * 1. **保持项目结构的一致性**: 遵循头文件/实现文件分离的C++惯例 + * 2. **预留扩展空间**: 如果将来需要为audio_processing_state添加辅助函数、 + * 工厂方法或其他实现代码,可以直接在此文件中添加 + * 3. **编译系统的完整性**: 某些构建系统可能期望每个.h文件都有对应的.cpp文件 + * + * ## 可能的未来扩展 + * 如果需要,可以在此文件中添加: + * - audio_processing_state的构造辅助函数 + * - 信号量的包装函数(如带超时的wait函数) + * - 调试和诊断工具函数 + * - 性能监控代码 + * + * ## 注意事项 + * - 由于audio_processing_state需要在共享内存中构造,任何辅助函数都必须 + * 考虑到进程间通信的特殊性 + * - 不应该在此文件中添加需要动态内存分配的代码,因为共享内存有特殊的 + * 内存管理要求 + * + * @see transport.h 查看audio_processing_state的详细定义 + * @see shared_memory_manager.h 查看如何在共享内存中创建audio_processing_state + */ + #include "transport.h" diff --git a/src/network/transport/transport.h b/src/network/transport/transport.h index f4e901b..6b2b87c 100644 --- a/src/network/transport/transport.h +++ b/src/network/transport/transport.h @@ -1,10 +1,159 @@ + +/** + * @file transport.h + * @brief 传输层基础定义 - 音频处理状态管理 + * + * 本文件是Alicho项目network模块中Transport子模块的基础文件,定义了用于音频处理的 + * 进程间同步机制。虽然文件名为transport,但它实际上定义了音频处理相关的状态结构, + * 这个状态结构会被放置在共享内存中,用于engine进程和host进程之间的音频处理协调。 + * + * ## 与其他模块的关系 + * - **SHM模块**: 本文件定义的结构体将被shared_memory_manager管理,放置在共享内存段中 + * - **RPC模块**: 音频处理请求通过RPC发送,但实际的同步使用本文件定义的信号量机制 + * - **Engine/Host进程**: 两个进程通过这里定义的信号量进行音频处理流程的同步 + * + * ## 技术实现 + * - 使用Boost.Interprocess库的interprocess_semaphore实现跨进程信号量 + * - 采用请求-响应模式的双信号量设计 + * - 支持在共享内存中直接构造和使用 + * + * ## 使用场景 + * 当host进程需要engine进程处理音频数据时: + * 1. Host将音频数据写入共享内存缓冲区 + * 2. Host通过request_semaphore发送处理请求信号 + * 3. Engine等待request_semaphore,收到信号后开始处理 + * 4. Engine处理完成后,通过response_semaphore发送完成信号 + * 5. Host等待response_semaphore,收到信号后读取处理结果 + * + * @note 本文件非常简洁,主要作为共享内存中的状态结构定义 + * @note 信号量的初始化在shared_memory_manager中完成 + */ + #pragma once #include -// 音频处理状态(在共享内存中) +/** + * @struct audio_processing_state + * @brief 音频处理状态结构体 - 用于进程间音频处理同步 + * + * 这个结构体被放置在共享内存中,用于协调engine进程和host进程之间的音频处理流程。 + * 它使用两个信号量实现了一个简单但高效的请求-响应同步模式。 + * + * ## 设计模式 + * 采用生产者-消费者模式的变体: + * - Host进程是生产者(产生音频处理请求) + * - Engine进程是消费者(消费并处理音频数据) + * - 使用双向信号量确保同步和确认 + * + * ## 工作流程 + * ``` + * Host进程 Engine进程 + * | | + * | 1. 写入音频数据到共享内存 | + * |--------------------------------->| + * | 2. post(request_semaphore) | + * |--------------------------------->| 3. wait(request_semaphore) + * | | 4. 处理音频数据 + * | 6. wait(response_semaphore) | 5. post(response_semaphore) + * |<---------------------------------| + * | 7. 读取处理结果 | + * ``` + * + * ## 线程安全性 + * - Boost的interprocess_semaphore本身是线程安全和进程安全的 + * - 多个线程可以安全地等待同一个信号量 + * - 信号量操作具有原子性保证 + * + * ## 性能特点 + * - 信号量操作通常涉及系统调用,比自旋锁慢但比轮询高效 + * - 适合处理时间较长的音频处理任务 + * - 避免了忙等待,节省CPU资源 + * + * ## 内存布局 + * 该结构体的大小取决于Boost库的具体实现,通常: + * - 在Linux上使用POSIX信号量(sem_t) + * - 在Windows上使用Windows信号量对象 + * - 需要确保在共享内存中正确对齐 + * + * @note 该结构体必须在共享内存中构造,不能简单复制 + * @note 信号量的初始值在构造时设置,通常request_semaphore初始为0,response_semaphore初始为0 + * @warning + + * @warning 不要在栈或普通堆上创建此结构体用于进程间通信 + */ struct audio_processing_state { - // 等待处理请求信号量, 由host侧读取, 由engine侧设置 + /** + * @brief 音频处理请求信号量 + * + * 用于engine进程等待host进程的音频处理请求。 + * + * ## 使用方式 + * - **Host进程(生产者)**: + * 当有新的音频数据需要处理时,调用request_semaphore.post()发送信号 + * - **Engine进程(消费)**: + * 调用request_semaphore.wait()阻塞等待处理请求,收到信号后开始处理 + * + * ## 初始值 + * - 通常初始化为0,示初始状态下没有待处理的请求 + * - 每次post()会将计数器加1 + * - 每次wait()会将计数器减1(如果计数器为0则阻塞) + * + * ## 典型使用场景 + * ```cpp + * // Host进程中(发送求) + * write_audio_data_to_shm(); // 写入音频数据 + * state->request_semaphore.post(); // 通知engine处理 + * + * // Engine进程中(等待请求) + * state->request_semaphore.wait(); // 阻塞等待请求 + * process_audio_data(); // 处理音频 + * ``` + * + * @note 这是一个进程间信号量,支持跨进程同步 + * @note 由host进程设置(post),由engine进程读取(wait) + */ boost::interprocess::interprocess_semaphore request_semaphore; - // 处理完成信号量, 由engine侧读取, 由host侧设置 + + /** + * @brief 音频处理完成信号量 + * + * 用于host进程等待engine进程完成音频处理。 + * + * ## 使用方式 + * - **Engine进程(生产)**: + * 完成音频处理后,调用response_semaphore.post()发送完成信号 + * - **Host进程(消费者)**: + * 调用response_semaphore.wait()阻塞等待处理完成,收到信号后读取结果 + * + * ## 初始值 + * - 通常初始化为0,示初始状态下没有完成的处理 + * - 形成与request_semaphore的对称设计 + * + * ## 典型使用场景 + * ```cpp + * // Engine进程中(发送完成信号) + * process_audio_data(); // 处理音频 + * write_result_to_shm(); // 写入处理结果 + * state->response_semaphore.post(); // 通知host已完成 + * + * // Host进程中(等待成) + * state->response_semaphore.wait(); // 阻塞等待处理完成 + * read_result_from_shm(); // 读取处理结果 + * ``` + * + * ## 超时处理 + * 如果需要超时功能,可以使用timed_wait(): + * ```cpp + * auto timeout = boost::posix_time::seconds(5); + * if (state->response_semaphore.timed_wait(timeout)) { + * // 处理完成 + * } else { + * // 超时处理 + * } + * ``` + * + * @note 这是一个进程间信号量,支持跨进程同步 + * @note 由engine进程设置(post),由host进程读取(wait) + */ boost::interprocess::interprocess_semaphore response_semaphore; }; diff --git a/src/network/transport/zmq_client.cpp b/src/network/transport/zmq_client.cpp index 619da8b..27fef83 100644 --- a/src/network/transport/zmq_client.cpp +++ b/src/network/transport/zmq_client.cpp @@ -1,9 +1,111 @@ +/** + * @file zmq_client.cpp + * @brief ZeroMQ客户端实现文件 + * + * 本文件实现了zmq_client.h中声明的方法,包括连接初始化、消息接收、 + * 断开连接和重连等核心功能。 + * + * ## 实现要点 + * 1. **连接管理**: 完整的连接生命周期管理 + * 2. **错误处理**: 详细的日志记录和异常处理 + * 3. **状态同步**: 确保状态转换的正确性 + * 4. **消息分发**: 接收消息后分发到处理器 + * + * ## 与头文件的对应 + * - init(): 创建套接字并连接服务器 + * - recv(): 接收消息并分发 + * - disconnect(): 关闭连接 + * - reconnect(): 重连逻辑 + * + * @see zmq_client.h 查看类声明和详细文档 + */ + #include "zmq_client.h" #include "zmq_client_processor.h" +/** + * @def ZMQ_CLIENT_LOG_MODULE + * @brief 日志模块名称(与头文件保持一致) + * + * 用于在实现文件中记录日志。 + */ #define ZMQ_CLIENT_LOG_MODULE "zmq_client" +/** + * @brief 初始化客户端并连接到服务器 + * + * 实现zmq_client::init()方法。创建DEALER套接字,设置routing_id, + * 并连接到ZMQ_SERVER_ADDRESS定义的服务器地址。 + * + * ## 实现细节 + * + * ### 1. 重复初始化检查 + * 如果客户端已经处于CONNECTED状态: + * - 检查client_id是否变化 + * - 如果ID相同,记录警告并直接返回(避免重复连接) + * - 如果ID不同,记录信息并先断开现有连接 + * + * ### 2. 状态设置 + * - 设置state_为CONNECTING + * - 保存client_id_以供后续使用(如重连) + * + * ### 3. 套接字创建和配置 + * ```cpp + * socket_ = zmq::socket_t(context_, zmq::socket_type::dealer); + * ``` + * - 使用context_创建DEALER类型套接字 + * - DEALER套接字支持异步通信和负载均衡 + * + * ### 4. 设置routing_id + * ```cpp + * socket_.set(zmq::sockopt::routing_id, + * zmq::const_buffer(&client_id_, sizeof(client_id_))); + * ``` + * - routing_id是ZeroMQ用于路由的标识 + * - 服务器的ROUTER套接字通过这个ID识别客户端 + * - 必须在connect()之前设置 + * - 使用client_id_的地址和大小作为缓冲区 + * + * ### 5. 连接到服务器 + * ```cpp + * socket_.connect(ZMQ_SERVER_ADDRESS); + * ``` + * - 连接到预定义的服务器地址 + * - Windows: tcp://127.0.0.1:29623 + * - Linux/macOS: ipc:///tmp/alicho_backend_server.ipc + * - connect()是异步的,不会阻塞 + * + * ### 6. 成功处理 + * - 设置state_为CONNECTED + * - 记录INFO级别日志,包含client_id + * + * ### 7. 异常处理 + * - 捕获zmq::error_t异常 + * - 设置state_为FAILED + * - 记录ERROR级别日志 + * - 重新抛出异常让调用者处理 + * + * ## 可能的错误 + * - 服务器未启动:连接会成功但后续通信失败 + * - 地址格式错误:抛出异常 + * - 资源不足:创建套接字失败 + * - routing_id冲突:可能导致消息路由错误 + * + * ## 日志示例 + * ``` + * [INFO] [zmq_client] 客户端ID变更(1001 -> 1002),断开重连 + * [INFO] [zmq_client] 客户端 1002 已连接 + * [ERROR] [zmq_client] 连接失败: Connection refused + * [WARN] [zmq_client] 客户端已连接,ID=1001 + * ``` + * + * @param client_id 客户端唯一标识符 + * @throws zmq::error_t 当ZeroMQ操作失败时 + * + * @note 该方法是线程不安全的,不要并发调用 + * @note 重复调用时会检查client_id是否变化 + */ void zmq_client::init(uint32_t client_id) { if (state_ == zmq_client::state::CONNECTED) { if (client_id_ != client_id) { @@ -36,6 +138,125 @@ void zmq_client::init(uint32_t client_id) { } } +/** + * @brief 从服务器接收消息 + * + * 实现zmq_client::recv()方法。阻塞等待服务器消息,接收后反序列化 + * 并分发到zmq_client_processor进行处理。 + * + * ## 实现细节 + * + * ### 1. 创建消息对象 + * ```cpp + * zmq::message_t msg; + * ``` + * - ZeroMQ的消息封装对象 + * - 自动管理内存 + * - 支持零拷贝 + * + * ### 2. 接收消息 + * ```cpp + * auto result = socket_.recv(msg, zmq::recv_flags::none); + * ``` + * - 使用none标志,阻塞等待消息 + * - result是optional,包含接收的字节数 + * - 如果失败,result为false或size为0 + * + * ### 3. 接收结果检查 + * ```cpp + * if (!result || *result == 0) { + * log_module_error(...); + * return; + * } + * ``` + * - 检查是否成功接收 + * - size为0表示空消息(异常情况) + * - 失败时记录错误并返回 + * + * ### 4. 消息反序列化 + * ```cpp + * auto pack = zmq_message_pack::deserialize(msg); + * ``` + * - 将ZeroMQ消息反序列化为zmq_message_pack + * - 提取func_id和payload + * - 可能抛出异常(如果格式错误) + * + * ### 5. 日志记录 + * ```cpp + * log_module_debug(..., pack.func_id, pack.payload.size()); + * ``` + * - DEBUG级别记录消息信息 + * - 包含func_id(用于调试分发问题) + * - 包含payload大小(用于性能分析) + * + * ### 6. 消息分发 + * ```cpp + * zmq_client_processor::instance().process( + * pack.func_id, + * pack.payload.data(), + * pack.payload.size() + * ); + * ``` + * - 调用单例处理器的process方法 + * - 根据func_id找到对的处理函数 + * - 传递原始payload数据和大小 + * - 处理器内部会进行类型恢复和业务逻辑处理 + * + * ### 7. 异常处理 + * - 捕获所有std::exception + * - 记录ERROR级别日志,包含异常消息 + * - 不抛出异常,保持接收循环继续 + * - 错误不会导致客户端断开连接 + * + * ## 处理流程图 + * ``` + * recv() 调用 + * ↓ + * socket_.recv() [阻塞等待] + * ↓ + * 检查接收结果 + * ↓ (成功) + * zmq_message_pack::deserialize() + * ↓ + * 记录DEBUG日志 + * ↓ + * zmq_client_processor::process() + * ↓ + * 处理器反序列化payload + * ↓ + * 调用用户注册的处理函数 + * ↓ + * 返回,等待下一条消息 + * ``` + * + * ## 阻塞行为 + * - 默认阻塞直到收到消息 + * - 不消耗CPU资源(操作系统级别的等待) + * - 可以通过信号或其他机制中断 + * + * ## 性能考虑 + * - 接收:ZeroMQ优化的网络I/O + * - 反序列化:struct_pack高效处理 + * - 分发:O(1)哈希查找 + * - 瓶颈通常在处理函数本身 + * + * ## 错误场景 + * - 接收失败:网络错误、套接字关闭 + * - 反序列化失败:数据损坏、格式不匹配 + * - 处理器未找到:func_id不存在 + * - 处理函数异常:业务逻辑错误 + * + * ## 日志示例 + * ``` + * [ERROR] [zmq_client] 接收消息失败 + * [DEBUG] [zmq_client] 收到消息,func_id: 1001, payload大小: 256 + * [ERROR] [zmq_client] 处理消息异常: Invalid data format + * ``` + * + * @note 该方法会阻塞当前线程 + * @note 通常在主循环中调用 + * @note 消息处理在当前线程同步执行 + */ void zmq_client::recv() { zmq::message_t msg; @@ -58,6 +279,78 @@ void zmq_client::recv() { } } +/** + * @brief 断开与服务器的连接 + * + * 实现zmq_client::disconnect()方法。关闭套接字并更新状态。 + * + * ## 实现细节 + * + * ### 1. 状态检查 + * ```cpp + * if (state_ == zmq_client::state::DISCONNECTED) { + * return; + * } + * ``` + * - 如果已经断开,直接返回 + * - 避免重复操作 + * - 使方法幂等 + * + * ### 2. 关闭套接字 + * ```cpp + * socket_.close(); + * ``` + * - 关闭ZeroMQ套接字 + * - 释放相关资源 + * - 停止所有pending的操作 + * - 可能会阻塞,等待linger时间 + * + * ### 3. 更新状态 + * ```cpp + * state_ = zmq_client::state::DISCONNECTED; + * ``` + * - 标记为已断开 + * - 影响后续的is_connected()检查 + * - 允许重新init() + * + * ### 4. 日志记录 + * ```cpp + * log_module_info(..., client_id_); + * ``` + * - INFO级别记录断开信息 + * - 包含client_id便于追踪 + * + * ### 5. 异常处理 + * - 捕获zmq::error_t异常 + * - 记录ERROR日志 + * - 即使失败也不影响状态更新 + * - 不抛出异常(清理操作应该总是成功) + * + * ## linger时间 + * - 默认情况下,close()会等待未发送的消息 + * - 如果设置了linger为0,立即丢弃未发送消息 + * - 析构函数中设置linger为0以快速清理 + * + * ## 使用场景 + * - 程序正常退出 + * - 切换到不同的服务器 + * - 长时间不使用连接 + * - 准备重新初始化 + * + * ## 资源清理 + * - 套接字资源 + * - 未发送的消息(取决于linger设置) + * - 不影响context(可以创建新套接字) + * + * ## 日志示例 + * ``` + * [INFO] [zmq_client] 客户端 1001 已断开连接 + * [ERROR] [zmq_client] 断开连接异常: Socket closed + * ``` + * + * @note 该方法是幂等的,多次调用安全 + * @note 断开后可以重新init() + */ void zmq_client::disconnect() { if (state_ == zmq_client::state::DISCONNECTED) { return; @@ -73,6 +366,151 @@ void zmq_client::disconnect() { } } +/** + * @brief 重新连接到服务器 + * + * 实现zmq_client::reconnect()方法。在连接失败或断开后,尝试重新建立连接。 + * 支持多次重试,每次重试之间有固定延迟。 + * + * ## 实现细节 + * + * ### 1. 开始重连 + * ```cpp + * log_module_info(..., client_id_); + * ``` + * - 记录重连开始 + * - 包含client_id便于追踪 + * + * ### 2. 断开现有连接 + * ```cpp + * disconnect(); + * ``` + * - 清理可能存在的旧连接 + * - 确保从干净状态开始 + * - 如果已经断开,disconnect()会快速返回 + * + * ### 3. 重连循环 + * ```cpp + * for (uint32_t attempt = 1; attempt <= MAX_RECONNECT_ATTEMPTS; ++attempt) + * ``` + * - 最多尝试MAX_RECONNECT_ATTEMPTS次(3次) + * - 使用1-based计数(更易读的日志) + * - 循环变量attempt用于日志 + * + * ### 4. 每次尝试 + * ```cpp + * state_ = zmq_client::state::RECONNECTING; + * log_module_info(..., attempt, MAX_RECONNECT_ATTEMPTS); + * ``` + * - 设置状态为RECONNECTING + * - 记录当前尝试次数 + * - 帮助诊断重连问题 + * + * ### 5. 调用init() + * ```cpp + * init(client_id_); + * ``` + * - 使用保存的client_id_ + * - 重新创建套接字和连接 + * - 可能抛出异常 + * + * ### 6. 成功处理 + * ```cpp + * log_module_info(...); + * return true; + * ``` + * - 记录成功日志 + * - 立即返回true + * - 不需要继续尝试 + * + * ### 7. 失败处理 + * ```cpp + * catch (const std::exception& e) { + * log_module_error(..., e.what()); + * if (attempt < MAX_RECONNECT_ATTEMPTS) { + * std::this_thread::sleep_for(...); + * } + * } + * ``` + * - 捕获异常 + * - 记录错误信息 + * - 如果不是最后一次尝试,等待后重试 + * - 等待时间:RECONNECT_DELAY_MS(1000毫秒) + * + * ### 8. 所有尝试失败 + * ```cpp + * state_ = zmq_client::state::FAILED; + * log_module_error(...); + * return false; + * ``` + * - 设置状态为FAILED + * - 记录最终失败 + * - 返回false通知调用者 + * + * ## 重连策略 + * - **尝试次数**: 3次(MAX_RECONNECT_ATTEMPTS) + * - **重试延迟**: 1秒(RECONNECT_DELAY_MS) + * - **延迟策略**: 固定延迟(可改为指数退避) + * - **总耗时**: 最多约3秒(3次尝试 × 1秒延迟) + * + * ## 状态转换 + * ``` + * 初始状态(任意) + * ↓ + * disconnect() -> DISCONNECTED + * ↓ + * 循环开始 + * ↓ + * RECONNECTING + * ↓ + * init() 成功 -> CONNECTED -> return true + * ↓ (失败) + * sleep(1秒) + * ↓ + * 下一次尝试或FAILED -> return false + * ``` + * + * ## 失败原因 + * - 服务器未启动 + * - 网络不可达 + * - 地址配置错误 + * - 资源不足 + * - 防火墙阻止 + * + * ## 改进建议 + * - 使用指数退避:1s, 2s, 4s... + * - 添加随机抖动:避免多客户端同时重连 + * - 支持无限重试:对于关键服务 + * - 支持回调通知:通知上层重连状态 + * + * ## 日志示例 + * ``` + * [INFO] [zmq_client] 尝试重连客户端 1001 + * [INFO] [zmq_client] 重连尝试 1/3 + * [ERROR] [zmq_client] 重连失败: Connection refused + * [INFO] [zmq_client] 重连尝试 2/3 + * [INFO] [zmq_client] 重连成功 + * ``` + * 或 + * ``` + * [INFO] [zmq_client] 尝试重连客户端 1001 + * [INFO] [zmq_client] 重连尝试 1/3 + * [ERROR] [zmq_client] 重连失败: No route to host + * [INFO] [zmq_client] 重连尝试 2/3 + * [ERROR] [zmq_client] 重连失败: No route to host + * [INFO] [zmq_client] 重连尝试 3/3 + * [ERROR] [zmq_client] 重连失败: No route to host + * [ERROR] [zmq_client] 重连失败,已达最大尝试次数 + * ``` + * + * @return bool 重连是否成功 + * - true: 成功重新连接 + * - false: 所有尝试均失败 + * + * @note 该方法会阻塞,最多约3秒 + * @note 重连成功后状态为CONNECTED + * @note 重连失败后状态为FAILED + */ bool zmq_client::reconnect() { log_module_info(ZMQ_CLIENT_LOG_MODULE, "尝试重连客户端 {}", client_id_); diff --git a/src/network/transport/zmq_client.h b/src/network/transport/zmq_client.h index e3196df..d09d103 100644 --- a/src/network/transport/zmq_client.h +++ b/src/network/transport/zmq_client.h @@ -1,3 +1,84 @@ +/** + * @file zmq_client.h + * @brief ZeroMQ客户端实现 - 基于DEALER套接字的可靠客户端 + * + * 本文件实现了ZeroMQ客户端,用于与zmq_server进行通信。客户端使用DEALER套接字类型, + * 支持自动重连、状态管理和类型安全的消息发送/接收。 + * + * ## 核心功能 + * 1. **连接管理**: 支持初始化、断开、重连等完整的连接生命周期管理 + * 2. **消息发送**: 类型安全的模板方法,自动序列化和打包 + * 3. **消息接收**: 自动反序列化并分发到对应的处理器 + * 4. **状态跟踪**: 维护连接状态,支持状态查询 + * 5. **自动重连**: 连接失败时自动尝试重连,提高可靠性 + * + * ## ZeroMQ套接字类型 + * 使用DEALER套接字的原因: + * - 异步通信:可以发送多个请求而不需要等待响应 + * - 负载均衡:ZeroMQ自动进行负载均衡(如果连接多个服务器) + * - 可路由:与ROUTER套接字配对,支持双向通信 + * - 无阻塞发送:不会因为服务器繁忙而阻塞 + * + * ## 通信模式 + * ``` + * zmq_client (DEALER) <---> zmq_server (ROUTER) + * | | + * | 1. 连接并设置routing_id | + * |---------------------------->| + * | | + * | 2. 发送消息(带类型ID) | + * |---------------------------->| + * | | 3. 根据routing_id识别客户端 + * | | 4. 处理消息 + * | 5. 接收响应 | + * |<----------------------------| + * ``` + * + * ## 与其他模块的关系 + * - **zmq_server**: 通信对端,使用ROUTER套接字 + * - **zmq_client_processor**: 处理接收到的消息 + * - **zmq_util**: 使用其定义的消息格式和服务器地址 + * - **lazy_singleton**: 继承单例模式,全局唯一实例 + * + * ## 设计模式 + * - **单例模式**: 确保全局只有一个客户端实例 + * - **状态模式**: 使用state枚举管理连接状态 + * - **模板方法**: send()使用模板支持任意类型 + * - **RAII**: 析构函数自动清理资源 + * + * ## 使用示例 + * ```cpp + * // 初始化客户端 + * zmq_client::instance().init(client_id); + * + * // 发送消息 + * MyMessage msg{...}; + * zmq_client::instance().send(msg); + * + * // 接收消息(通常在循环中) + * while (running) { + * zmq_client::instance().recv(); + * } + * + * // 断开连接 + * zmq_client::instance().disconnect(); + * ``` + * + * ## 线程安全性 + * - zmq::context_t和zmq::socket_t不是线程安全的 + * - 建议在单个线程中使用一个客户端实例 + * - 如果多线程访问,需要外部同步机制 + * + * ## 性能考虑 + * - DEALER套接字支持异步操作,性能较高 + * - 序列化使用struct_pack,效率优秀 + * - 重连机制使用指数退避,避免频繁重连 + * + * @note 客户端必须在服务器启动后才能成功连接 + * @note 建议在应用程序生命周期内保持客户端存活 + * @warning ZeroMQ对象不是线程安全的,避免多线程同时访问 + */ + #pragma once #include #include @@ -6,20 +87,234 @@ #include "logger.h" #include "zmq_util.h" +/** + * @def ZMQ_CLIENT_LOG_MODULE + * @brief ZeroMQ客户端的日志模块名称 + * + * 用于标识来自zmq_client的日志消息。所有客户端相关的日志都使用这个模块名。 + * + * 日志使用场景: + * - ERROR: 连接失败、发送失败、接收失败、重连失败 + * - WARN: 客户端已连接时重复初始化 + * - INFO: 连接成功、断开连接、重连尝试、重连成功 + * - DEBUG: 消息发送成功 + */ #define ZMQ_CLIENT_LOG_MODULE "zmq_client" +/** + * @class zmq_client + * @brief ZeroMQ客户端类 - 单例模式的网络客户端 + * + * 这个类封装了ZeroMQ DEALER套接字,提供了完整的客户端功能,包括连接管理、 + * 消息收发、状态跟踪和自动重连。 + * + * ## 单例设计 + * 继承自lazy_singleton,确保: + * - 全局只有一个客户端实例 + * - 延迟初始化(第一次使用时创建) + * - 线程安全的初始化 + * + * ## 核心职责 + * 1. 管理与服务器的连接 + * 2. 发送类型安全的消息 + * 3. 接收并分发消息到处理器 + * 4. 维护连接状态 + * 5. 处理连接失败和重连 + * + * ## ZeroMQ DEALER套接字特性 + * - **异步**: 可以连续发送多个消息而不等待响应 + * - **公平队列**: 消息在内部队列中排队 + * - **routing_id**: 设置客户端唯一标识,服务器用于识别和回复 + * - **负载均衡**: 如果连接多个服务器,自动负载均衡 + * + * ## 状态机 + * ``` + * DISCONNECTED ----init()----> CONNECTING ----成功----> CONNECTED + * ^ | | + * | |失败 | + * | v | + * +-------------- FAILED <----- disconnect()----------+ + * | | + * +----reconnect()-----> RECONNECTING + * ``` + * + * ## 使用场景 + * 典型的客户端使用流程: + * ```cpp + * // 1. 获取单例并初始化 + * auto& client = zmq_client::instance(); + * client.init(my_client_id); + * + * // 2. 发送消息 + * RequestMessage req{...}; + * client.send(req); + * + * // 3. 在主循环中接收消息 + * while (running) { + * client.recv(); // 阻塞等待消息 + * // 消息会自动分发到注册的处理器 + * } + * + * // 4. 清理 + * client.disconnect(); + * ``` + * + * @note 这是一个单例类,通过instance()获取唯一实例 + * @note ZeroMQ上下文在第一次使用时创建,程序结束时自动销毁 + */ class zmq_client : public lazy_singleton { + /// 允许lazy_singleton访问私有构造函数 friend class lazy_singleton; public: + /** + * @enum state + * @brief 客户端连接状态枚举 + * + * 定义客户端可能处于的所有状态,用于跟踪连接生命周期。 + * + * ## 状态转换 + * - DISCONNECTED -> CONNECTING: 调用init() + * - CONNECTING -> CONNECTED: 连接成功 + * - CONNECTING -> FAILED: 连接失败 + * - CONNECTED -> DISCONNECTED: 调用disconnect() + * - CONNECTED -> FAILED: 发送失败 + * - FAILED -> RECONNECTING: 调用reconnect() + * - RECONNECTING -> CONNECTED: 重连成功 + * - RECONNECTING -> FAILED: 重连失败 + * + * ## 状态含义 + * 每个状态代表客户端的当前工作状态,影响可以执行的操作。 + */ enum class state { + /** + * @brief 未连接状态 + * + * 初始状态或已断开连接的状态。 + * + * 特征: + * - 套接字未创建或已关闭 + * - 无法发送或接收消息 + * - 可以调用init()进行连接 + * + * 进入方式: + * - 对象刚创建 + * - 调用disconnect() + * - 析构函数执行 + */ DISCONNECTED, + + /** + * @brief 正在连接状态 + * + * init()被调用后,正在建立连接的临时状态。 + * + * 特征: + * - 套接字已创建 + * - 正在执行connect() + * - 尚未确认连接成功 + * + * 进入方式: + * - 调用init() + * + * 退出方式: + * - 连接成功 -> CONNECTED + * - 连接失败 -> FAILED + */ CONNECTING, + + /** + * @brief 已连接状态 + * + * 成功连接到服务器,可以正常通信。 + * + * 特征: + * - 套接字已连接 + * - 可以发送和接收消息 + * - routing_id已设置 + * + * 进入方式: + * - init()成功 + * - reconnect()成功 + * + * 退出方式: + * - 调用disconnect() -> DISCONNECTED + * - 发送失败 -> FAILED + */ CONNECTED, + + /** + * @brief 正在重连状态 + * + * 连接失败后,正在尝试重新连接。 + * + * 特征: + * - 正在执行重连逻辑 + * - 可能多次尝试 + * - 每次尝试之间有延迟 + * + * 进入方式: + * - 调用reconnect() + * - send()时发现未连接并自动重连 + * + * 退出方式: + * - 重连成功 -> CONNECTED + * - 达到最大尝试次数 -> FAILED + */ RECONNECTING, + + /** + * @brief 连接失败状态 + * + * 连接或重连失败后的状态。 + * + * 特征: + * - 无法通信 + * - 需要人工干预或重试 + * - 可能是服务器未启动或网络问题 + * + * 进入方式: + * - init()失败 + * - reconnect()失败 + * - 发送消息异常 + * + * 退出方式: + * - 调用reconnect()尝试恢复 + * - 调用disconnect()清理状态 + */ FAILED }; + /** + * @brief 析构函数 - 自动清理资源 + * + * 在对象销毁时自动关闭套接字并清理资源。使用noexcept确保析构函数不抛出异常。 + * + * ## 清理流程 + * 1. 检查当前状态是否为CONNECTED + * 2. 设置linger为0(立即关闭,不等待未发送的消息) + * 3. 关闭套接字 + * 4. 更新状态为DISCONNECTED + * + * ## linger选项 + * 设置为0的原因: + * - 避免析构时阻塞等待消息发送 + * - 程序退出时快速清理 + * - 未发送的消息会被丢弃 + * + * ## 异常处理 + * - 捕获所有异常,不允许异常逃逸 + * - 不记录日志(logger可能已被析构) + * - 静默失败,确保析构过程安全 + * + * ## 注意事项 + * - 析构函数不应该在正常业务流程中被调用 + * - 建议显式调用disconnect()进行清理 + * - 析构时的错误无法通知调用者 + * + * @note 析构函数不会抛出异常 + * @note 单例对象的析构在程序结束时自动进行 + */ ~zmq_client() { try { if (state_ == state::CONNECTED) { @@ -33,8 +328,136 @@ public: } } + /** + * @brief 初始化客户端并连接到服务器 + * + * 创建DEALER套接字,设置routing_id,并连接到服务器。如果客户端已经连接, + * 会检查client_id是否变化,如果变化则断开重连。 + * + * ## 参数 + * @param client_id 客户端唯一标识符 + * - 用于服务器识别客户端 + * - 服务器通过这个ID回复消息 + * - 必须在所有客户端中唯一 + * + * ## 工作流程 + * 1. 检查当前状态 + * - 如果已连接且ID相同,直接返回 + * - 如果已连接但ID不同,先断开连接 + * 2. 设置状态为CONNECTING + * 3. 创建DEALER套接字 + * 4. 设置routing_id(ZeroMQ用于路由) + * 5. 连接到服务器(使用ZMQ_SERVER_ADDRESS) + * 6. 连接成功,设置状态为CONNECTED + * 7. 记录日志 + * + * ## routing_id说明 + * - routing_id是ZeroMQ的概念,用于DEALER-ROUTER通信 + * - 服务器的ROUTER套接字通过routing_id识别客户端 + * - 必须在connect()之前设置 + * - 使用client_id作为routing_id + * + * ## 异常处理 + * - 捕获zmq::error_t异常 + * - 设置状态为FAILED + * - 记录错误日志 + * - 重新抛出异常(让调用者处理) + * + * ## 使用示例 + * ```cpp + * try { + * zmq_client::instance().init(1001); + * // 连接成功,可以开始通信 + * } + * catch (const zmq::error_t& e) { + * // 处理连接失败 + * std::cerr << "Failed to connect: " << e.what() << std::endl; + * } + * ``` + * + * ## 服务器地址 + * 连接地址由ZMQ_SERVER_ADDRESS宏定义: + * - Windows: tcp://127.0.0.1:29623 + * - Linux/macOS: ipc:///tmp/alicho_backend_server.ipc + * + * @throws zmq::error_t 当ZeroMQ操作失败时 + * @note 服务器必须已经启动,否则连接会失败 + * @note 可以多次调用,如果client_id不同会重新连接 + * @warning 确保client_id在所有客户端中唯一 + */ void init(uint32_t client_id); + /** + * @brief 发送消息到服务器 + * + * 类型安全的模板方法,将任意可序列化的类型T打包并发送到服务器。 + * 如果当前未连接,会自动尝试重连。 + * + * ## 模板参数 + * @tparam T 要发送的消息类型 + * - 必须可被struct_pack序列化 + * - 必须有对应的alicho_type_id_v + * + * ## 函数参数 + * @param message 要发送的消息对象(const引用) + * + * ## 工作流程 + * 1. 检查连接状态 + * - 如果未连接,尝试重连 + * - 重连失败则返回 + * 2. 使用zmq_message_pack::create()打包消息 + * - 自动添加类型ID + * - 序列化消息内容 + * 3. 创建zmq::message_t + * 4. 发送消息 + * 5. 检查发送结果 + * - 成功:记录DEBUG日志 + * - 失败:记录ERROR日志 + * + * ## 自动重连 + * - 如果is_connected()返回false + * - 自动调用reconnect() + * - 重连成功后继续发送 + * - 重连失败则放弃发送 + * + * ## 异常处理 + * - 捕获zmq::error_t异常 + * - 记录错误日志 + * - 设置状态为FAILED + * - 不抛出异常(避免中断业务逻辑) + * + * ## 发送标志 + * 使用zmq::send_flags::none: + * - 阻塞发送(等待直到消息入队) + * - 可选:使用dontwait实现非阻塞 + * + * ## 使用示例 + * ```cpp + * // 定义消息类型 + * struct ChatMessage { + * std::string user; + * std::string text; + * }; + * + * // 发送消息 + * ChatMessage msg{"Alice", "Hello, Server!"}; + * zmq_client::instance().send(msg); + * ``` + * + * ## 性能考虑 + * - 序列化开销:取决于消息大小和复杂度 + * - 网络开销:取决于消息大小 + * - 异步发送:DEALER套接字支持异步,不会阻塞 + * + * ## 可靠性 + * - ZeroMQ提供消息完整性保证 + * - 不保证消息送达(需要应用层确认) + * - 如果服务器未启动,消息会在队列中等待 + * + * @note 该方法不会抛出异常,失败时记录日志 + * @note 如果需要确认消息送达,需要实现应用层ACK机制 + * @warning 消息不会无限等待,超过队列限制会被丢弃 + */ template void send(const T& message) { if (!is_connected()) { @@ -64,23 +487,323 @@ public: } } + /** + * @brief 接收服务器消息 + * + * 从服务器接收一条消息,反序列化后分发到对应的处理器。 + * 这个方法通常在主循环中调用,阻塞等待消息。 + * + * ## 工作流程 + * 1. 创建zmq::message_t对象 + * 2. 调用socket_.recv()接收消息(阻塞) + * 3. 检查接收结果 + * 4. 使用zmq_message_pack::deserialize()解包 + * 5. 记录DEBUG日志(func_id和payload大小) + * 6. 调用zmq_client_processor分发消息 + * + * ## 接收标志 + * 使用zmq::recv_flags::none: + * - 阻塞接收(等待直到有消息) + * - 可选:使用dontwait实现非阻塞 + * + * ## 消息处理 + * - 解包后的func_id用于识别消息类型 + * - payload包含序列化的消息数据 + * - zmq_client_processor根据func_id找到处理器 + * - 处理器反序列化payload并执行业务逻辑 + * + * ## 错误处理 + * **接收失败**: + * - result为false或size为0 + * - 记录ERROR日志 + * - 直接返回,不分发消息 + * + * **反序列化失败**: + * - zmq_message_pack::deserialize()抛出异常 + * - 捕获异常并记录 + * - 不会崩溃,继续等待下一条消息 + * + * ## 异常处理 + * - 捕获所有std::exception + * - 记录错误日志 + * - 不抛出异常(保持接收循环继续) + * + * ## 使用示例 + * ```cpp + * // 在主循环中接收消息 + * while (running) { + * zmq_client::instance().recv(); + * // 消息会自动分发到注册的处理器 + * // 不需要手动处理返回值 + * } + * ``` + * + * ## 阻塞行为 + * - 默认阻塞等待消息 + * - 可以使用ZeroMQ的超时选项 + * - 或使用非阻塞标志配合轮询 + * + * ## 性能考虑 + * - 阻塞接收不占用CPU + * - 反序列化开销取决于消息大小 + * - 处理器执行时间影响整体吞吐量 + * + * @note 该方法会阻塞直到收到消息 + * @note 消息处理在当前线程中同步执行 + * @warning 如果处理器执行耗时,会影响后续消息的接收 + */ void recv(); - // 连接管理 + // ==================== 连接管理方法 ==================== + + /** + * @brief 断开与服务器的连接 + * + * 主动关闭与服务器的连接,清理套接字资源。 + * + * ## 工作流程 + * 1. 检查当前状态 + * - 如果已经是DISCONNECTED,直接返回 + * 2. 关闭套接字 + * 3. 更新状态为DISCONNECTED + * 4. 记录INFO日志 + * + * ## 异常处理 + * - 捕获zmq::error_t异常 + * - 记录错误日志 + * - 即使失败也会更新状态 + * + * ## 使用场景 + * - 程序正常退出前 + * - 需要切换服务器时 + * - 长时间不使用连接时 + * + * ## 使用示例 + * ```cpp + * // 程序退出前清理 + * zmq_client::instance().disconnect(); + * ``` + * + * @note 断开后可以再次调用init()重新连接 + * @note 该方法是幂等的,多次调用无害 + */ void disconnect(); + + /** + * @brief 重新连接到服务器 + * + * 当连接失败或断开时,尝试重新建立连接。支持多次重试,每次重试之间有延迟。 + * + * ## 工作流程 + * 1. 记录重连日志 + * 2. 先断开现有连接(如果有) + * 3. 进入重连循环(最多MAX_RECONNECT_ATTEMPTS次) + * a. 设置状态为RECONNECTING + * b. 记录当前尝试次数 + * c. 调用init(client_id_) + * d. 成功则返回true + * e. 失败则等待RECONNECT_DELAY_MS毫秒 + * 4. 达到最大次数后设置状态为FAILED并返回false + * + * ## 重连策略 + * - 最大尝试次数:MAX_RECONNECT_ATTEMPTS(3次) + * - 重试延迟:RECONNECT_DELAY_MS(1000毫秒) + * - 使用固定延迟(可改为指数退避) + * + * ## 返回值 + * @return bool 重连是否成功 + * - true: 成功重新连接 + * - false: 达到最大尝试次数仍失败 + * + * ## 异常处理 + * - init()抛出的异常会被捕获 + * - 记录ERROR日志 + * - 继续下一次尝试 + * + * ## 使用示例 + * ```cpp + * if (!zmq_client::instance().is_connected()) { + * if (zmq_client::instance().reconnect()) { + * std::cout << "Reconnected successfully" << std::endl; + * } else { + * std::cerr << "Failed to reconnect" << std::endl; + * } + * } + * ``` + * + * ## 性能影响 + * - 每次重试有1秒延迟 + * - 最多阻塞3秒(3次 × 1秒) + * - 建议在后台线程中调用 + * + * @note 重连会使用之前的client_id + * @note 重连过程会阻塞调用线程 + * @warning 多次失败可能表示服务器未启动或网络问题 + */ bool reconnect(); - // 状态查询 + // ==================== 状态查询方法 ==================== + + /** + * @brief 获取当前连接状态 + * + * @return state 当前的连接状态枚举值 + * + * ## 使用示例 + * ```cpp + * auto current_state = zmq_client::instance().get_state(); + * switch (current_state) { + * case zmq_client::state::CONNECTED: + * // 可以发送消息 + * break; + * case zmq_client::state::FAILED: + * // 需要重连 + * break; + * default: + * // 其他状态 + * break; + * } + * ``` + * + * @note 状态可能在多线程环境下瞬间变化 + */ state get_state() const { return state_; } + + /** + * @brief 检查是否已连接 + * + * 便捷方法,检查当前状态是否为CONNECTED。 + * + * @return bool 是否处于已连接状态 + * - true: 状态为CONNECTED + * - false: 其他任何状态 + * + * ## 使用示例 + * ```cpp + * if (zmq_client::instance().is_connected()) { + * // 可以安全发送消息 + * zmq_client::instance().send(msg); + * } else { + * // 需要先连接 + * zmq_client::instance().reconnect(); + * } + * ``` + * + * @note 这是get_state() == state::CONNECTED的快捷方式 + */ bool is_connected() const { return state_ == state::CONNECTED; } private: + /** + * @brief ZeroMQ DEALER套接字 + * + * 用于与服务器通信的主要套接字对象。 + * + * ## DEALER套接字特性 + * - 异步发送/接收 + * - 公平队列(FIFO) + * - 与ROUTER配对使用 + * - 支持负载均衡 + * + * ## 生命周期 + * - init()时创建 + * - disconnect()或析构时销毁 + * - 不能在未初始化时使用 + * + * @note 套接字对象不是线程安全的 + */ zmq::socket_t socket_; + + /** + * @brief ZeroMQ上下文 + * + * ZeroMQ的全局上下文对象,管理I/O线程和内部状态。 + * + * ## 作用 + * - 管理ZeroMQ的I/O线程 + * - 作为创建套接字的工厂 + * - 整个程序生命周期内存在 + * + * ## 线程数 + * - 默认:1个I/O线程 + * - 可通过构造函数参数调整 + * + * @note 一个程序通常只需要一个context + * @note context的销毁会等待所有套接字关闭 + */ zmq::context_t context_; + + /** + * @brief 客户端唯一标识符 + * + * 存储在init()时设置的client_id,用于: + * - 设置DEALER套接字的routing_id + * - 服务器识别和回复客户端 + * - 重连时保持相同的ID + * + * ## 取值范围 + * - uint32_t: 0 到 4,294,967,295 + * - 建议使用有意义的值(如进程ID、用户ID等) + * + * @note 该值在init()时设置,reconnect()时保持不变 + */ uint32_t client_id_; + + /** + * @brief 当前连接状态 + * + * 跟踪客户端的连接状态,影响可以执行的操作。 + * + * ## 初始值 + * - DISCONNECTED + * + * ## 更新时机 + * - init(): CONNECTING -> CONNECTED 或 FAILED + * - disconnect(): 任何状态 -> DISCONNECTED + * - reconnect(): FAILED -> RECONNECTING -> CONNECTED 或 FAILED + * - send()失败: CONNECTED -> FAILED + * + * @note 状态机保证了操作的正确性 + */ state state_ = state::DISCONNECTED; - // 重连配置 + // ==================== 重连配置常量 ==================== + + /** + * @brief 最大重连尝试次数 + * + * 定义reconnect()方法最多尝试重连的次数。 + * + * ## 当前值 + * - 3次 + * + * ## 设计考虑 + * - 太少:可能错过短暂的网络波动恢复机会 + * - 太多:在服务器长时间down时浪费时间 + * - 3次是经验值,适合大多数场景 + * + * @note 可以根据实际需求调整 + */ static constexpr uint32_t MAX_RECONNECT_ATTEMPTS = 3; + + /** + * @brief 重连延迟(毫秒) + * + * 定义每次重连尝试之间的等待时间。 + * + * ## 当前值 + * - 1000毫秒(1秒) + * + * ## 设计考虑 + * - 太短:可能对服务器造成压力,浪费资源 + * - 太长:恢复时间变长,影响用户体验 + * - 1秒是合理的平衡点 + * + * ## 可能的改进 + * - 使用指数退避:1秒、2秒、4秒... + * - 使用随机抖动:避免多个客户端同时重连 + * + * @note 可以根据实际需求调整 + */ static constexpr uint32_t RECONNECT_DELAY_MS = 1000; }; diff --git a/src/network/transport/zmq_client_processor.h b/src/network/transport/zmq_client_processor.h index 49c33e9..e1a4c62 100644 --- a/src/network/transport/zmq_client_processor.h +++ b/src/network/transport/zmq_client_processor.h @@ -1,3 +1,82 @@ +/** + * @file zmq_client_processor.h + * @brief ZeroMQ客户端消息处理器 - 基于类型的消息分发系统 + * + * 本文件实现了客户端侧的消息处理器框架,提供了一个类型安全、可扩展的消息分发机制。 + * 它允许开发者为不同类型的消息注册对应的处理函数,系统会自动根据消息类型调用正确的处理器。 + * + * ## 核心功能 + * 1. **消息处理器注册**: 将消息类型与处理函数关联 + * 2. **自动消息分发**: 根据消息类型自动调用对应的处理器 + * 3. **类型安全**: 编译期确保类型匹配 + * 4. **简化注册**: 提供宏简化处理器注册代码 + * + * ## 与其他模块的关系 + * - **zmq_client**: 接收到消息后,调用本处理器进行分发 + * - **zmq_util**: 使用其定义的消息格式和类型ID系统 + * - **type_util**: 使用alicho_type_id_v获取类型标识 + * - **struct_pack**: 用于反序列化payload + * + * ## 设计模式 + * - **单例模式**: 使用lazy_singleton确保全局唯一实例 + * - **注册模式**: 处理器在程序启动时自动注册 + * - **策略模式**: 不同消息类型有不同的处理策略 + * - **命令模式**: 消息处理函数作为可执行的命令 + * + * ## 工作流程 + * ``` + * 1. 程序启动 + * ↓ + * 2. 静态初始化(zmq_client_register对象构造) + * ↓ + * 3. 处理器自动注册到zmq_client_processor + * ↓ + * 4. 运行时接收消息 + * ↓ + * 5. zmq_client调用processor.process(func_id, payload, size) + * ↓ + * 6. 查找对应的处理器 + * ↓ + * 7. 反序列化payload + * ↓ + * 8. 调用用户定义的处理函数 + * ``` + * + * ## 使用示例 + * ```cpp + * // 定义消息类型 + * struct ChatMessage { + * std::string user; + * std::string text; + * }; + * + * // 方式1: 使用宏注册(推荐) + * ZMQ_CLIENT_REGISTER_PROCESSOR(ChatMessage) { + * std::cout << data.user << ": " << data.text << std::endl; + * } + * + * // 方式2: 手动注册 + * void handle_chat(const ChatMessage& msg) { + * // 处理逻辑 + * } + * static zmq_client_register reg(handle_chat); + * ``` + * + * ## 线程安全性 + * - 注册阶段(程序启动)是单线程的,无竞争 + * - 处理阶段需要确保处理函数本身是线程安全的 + * - 如果zmq_client在多线程中使用,需要额外同步 + * + * ## 性能考虑 + * - 使用unordered_map,查找时间复杂度O(1) + * - 函数指针调用开销minimal + * - 反序列化可能是性能瓶颈 + * + * @note 所有消息类型必须可被struct_pack序列化 + * @note 处理函数应该尽快返回,避免阻塞消息接收 + * @warning 不要在处理函数中执行耗时操作,考虑异步处理 + */ + #pragma once #include "lazy_singleton.h" #include "logger.h" @@ -5,16 +84,191 @@ #include #include "type_util.h" +/** + * @def ZMQ_CLIENT_PROCESSOR_LOG_MODULE + * @brief 客户端处理器的日志模块名称 + * + * 用于标识来自zmq_client_processor的日志消息。 + * 所有处理器相关的日志都会使用这个模块名。 + * + * 日志级别说明: + * - ERROR: 处理器未找到、反序列化失败、处理器执行异常 + * - WARN: 暂无 + * - INFO: 暂无 + * - DEBUG: 可以添加处理器注册成功的日志 + */ #define ZMQ_CLIENT_PROCESSOR_LOG_MODULE "zmq_client_processor" +/** + * @class zmq_client_processor + * @brief ZeroMQ客户端消息处理器 - 单例模式的消息分发器 + * + * 这个类负责管理所有客户端接收到的消息的处理逻辑。它维护一个从消息类型ID + * 到处理函数的映射表,当接收到消息时,根据消息的类型ID找到对应的处理器并执行。 + * + * ## 单例设计 + * 继承自lazy_singleton,确保: + * - 全局只有一个实例 + * - 延迟初始化(第一次使用时创建) + * - 线程安全的初始化 + * + * ## 核心职责 + * 1. 维护处理器注册表(func_id -> handler函数) + * 2. 根据func_id分发消息到对应处理器 + * 3. 处理反序列化和异常 + * 4. 记录错误日志 + * + * ## 存储结构 + * ```cpp + * unordered_map> + * ↑ ↑ ↑ + * func_id 类型标识 原始字节处理函数 + * ``` + * + * ## 为什么使用void*和size_t + * - 因为不同消息类型的处理函数签名不同 + * - 使用类型擦除技术统一存储 + * - 在zmq_client_register中进行类型恢复 + * + * ## 线程安全性 + * - register_processor: 仅在静态初始化阶段调用,单线程安全 + * - process: 如果zmq_client在多线程中调用,需要外部同步 + * - 内部不提供锁保护,假设单线程访问 + * + * ## 使用方式 + * ```cpp + * // 通常不直接使用,而是通过zmq_client_register或宏 + * auto& processor = zmq_client_processor::instance(); + * + * // 手动注册(不推荐) + * processor.register_processor(func_id, [](const void* data, size_t size) { + * // 处理逻辑 + * }); + * + * // 处理消息(由zmq_client调用) + * processor.process(func_id, payload.data(), payload.size()); + * ``` + * + * @note 这是一个单例类,通过instance()获取唯一实例 + * @note 不要尝试创建多个实例 + */ class zmq_client_processor : public lazy_singleton { + /// 允许lazy_singleton访问私有构造函数 friend class lazy_singleton; public: + /** + * @brief 注册消息处理器 + * + * 将一个消息类型ID与对应的处理函数关联起来。这个方法通常不直接调用, + * 而是通过zmq_client_register模板类在程序启动时自动调用。 + * + * ## 参数说明 + * @param func_id 消息类型的唯一标识符(来自alicho_type_id_v) + * @param processor 处理函数,接受原始字节数据 + * - 参数1: const void* - 指向序列化数据的指针 + * - 参数2: size_t - 数据的字节大小 + * + * ## 函数签名说明 + * 使用std::function的原因: + * - 类型擦除:不同消息类型可以用统一接口存储 + * - 灵活性:可以存储lambda、函数指针、函数对象 + * - 标准化:符合C++标准库的设计风格 + * + * ## 注册时机 + * - 通常在程序启动的静态初始化阶段 + * - zmq_client_register的构造函数中调用 + * - 在main()函数执行之前完成 + * + * ## 重复注册处理 + * - 当前实现会覆盖之前的注册 + * - 没有检查重复,需要开发者确保不重复注册 + * - 建议:每种消息类型只注册一次 + * + * ## 使用示例 + * ```cpp + * // 通常由zmq_client_register自动调用 + * zmq_client_processor::instance().register_processor( + * alicho_type_id_v, + * [](const void* data, size_t size) { + * auto msg = struct_pack::deserialize( + * static_cast(data), size); + * if (msg.has_value()) { + * // 处理消息 + * } + * } + * ); + * ``` + * + * @note 该方法不是线程安全的,但只在静态初始化时调用 + * @note 使用std::move转移processor的所有权 + * @warning 不要为同一个func_id注册多次,会导致覆盖 + */ void register_processor(uint32_t func_id, std::function processor) { processors_[func_id] = std::move(processor); } + /** + * @brief 处理接收到的消息 + * + * 根据消息的类型ID找到对应的处理器,并执行处理逻辑。这个方法由zmq_client + * 在接收到消息后调用。 + * + * ## 参数说明 + * @param func_id 消息类型标识符(从zmq_message_pack中提取) + * @param payload 序列化的消息数据指针 + * @param size 数据大小(字节) + * + * ## 处理流程 + * 1. 根据func_id在processors_映射表中查找处理器 + * 2. 如果找不到,记录错误日志并返回 + * 3. 如果找到,调用处理器函数 + * 4. 捕获处理器抛出的异常,记录日志 + * + * ## 错误处理 + * **处理器未找到**: + * - 记录ERROR级别日志 + * - 包含未找到的func_id + * - 静默返回,不抛出异常 + * + * **处理器执行异常**: + * - 捕获所有std::exception + * - 记录ERROR级别日志 + * - 包含异常信息 + * - 不会传播异常(避免中断消息接收循环) + * + * ## 性能分析 + * - unordered_map查找: O(1)平均时间复杂度 + * - 函数调用开销: 很小 + * - 主要开销在处理器函数本身 + * + * ## 线程安全性 + * - 读取processors_映射表(查找操作) + * - 如果只有一个线程调用process,则是安全的 + * - 如果多线程调用,需要外部同步 + * + * ## 使用示例 + * ```cpp + * // 在zmq_client的recv()方法中 + * auto pack = zmq_message_pack::deserialize(msg); + * zmq_client_processor::instance().process( + * pack.func_id, + * pack.payload.data(), + * pack.payload.size() + * ); + * ``` + * + * ## 调试建议 + * 如果消息没有被处理: + * 1. 检查是否正确注册了处理器 + * 2. 检查func_id是否匹配 + * 3. 查看日志中的"未找到处理器"错误 + * 4. 验证消息类型是否正确 + * + * @note 该方法捕获所有异常,不会抛出 + * @note 处理器函数内部的异常会被记录但不会传播 + * @warning 如果处理器函数耗时过长,会阻塞后续消息的接收 + */ void process(uint32_t func_id, const void* payload, size_t size) { const auto it = processors_.find(func_id); if (it == processors_.end()) { @@ -31,12 +285,200 @@ public: } private: + /** + * @brief 处理器注册表 - 消息类型ID到处理函数的映射 + * + * 这是核心数据结构,存储所有已注册的消息处理器。 + * + * ## 数据结构选择 + * 使用unordered_map的原因: + * - O(1)平均查找时间 + * - func_id是整数,哈希效率高 + * - 不需要有序遍历 + * + * ## 键值说明 + * - **键(uint32_t)**: 消息类型的唯一标识符 + * - 来自alicho_type_id_v + * - 每种消息类型有唯一的ID + * - **值(function)**: 处理函数 + * - 接受原始字节数据 + * - 内部会进行类型恢复和反序列化 + * + * ## 内存管理 + * - std::function内部使用小对象优化 + * - lambda捕获的变量会被存储 + * - 整个映射表在程序生命周期内存在 + * + * ## 生命周期 + * - 程序启动时开始填充 + * - 程序运行期间只读(查找) + * - 程序结束时自动销毁 + * + * ## 并发访问 + * - 写入:仅在静态初始化阶段(单线程) + * - 读取:运行时(可能多线程,需要外部同步) + * + * @note 使用私有成员确保只能通过public接口访问 + * @note 不提供直接访问,保证封装性 + */ std::unordered_map> processors_; }; +/** + * @class zmq_client_register + * @brief 客户端消息处理器自动注册器 - RAII模式的注册辅助类 + * + * 这是一个模板类,用于在程序启动时自动注册消息处理器。它利用C++的静态初始化 + * 机制,在main()函数执行之前就完成了处理器的注册。 + * + * ## 模板参数 + * @tparam T 要处理的消息类型 + * - 必须可被struct_pack序列化 + * - 必须有对应的alicho_type_id_v + * + * ## 设计模式 + * - **RAII模式**: 构造时注册,析构时无需清理 + * - **模板模式**: 为不同消息类型生成特化代码 + * - **代理模式**: 代理用户定义的处理函数 + * + * ## 工作原理 + * ```cpp + * // 1. 定义静态变量(程序启动时) + * static zmq_client_register reg(my_handler); + * + * // 2. 构造函数被调用 + * zmq_client_register::zmq_client_register(my_handler) { + * // 3. 注册处理器 + * zmq_client_processor::instance().register_processor( + * alicho_type_id_v, // 自动获取类型ID + * [my_handler](const void* data, size_t size) { // 包装处理函数 + * // 4. 反序列化 + * auto result = struct_pack::deserialize(...); + * // 5. 调用用户处理函数 + * if (result.has_value()) { + * my_handler(result.value()); + * } + * } + * ); + * } + * ``` + * + * ## 类型安全 + * - 编译期确定消息类型 + * - 处理函数签名必须匹配 + * - 反序列化类型检查 + * + * ## 自动化优势 + * - 无需手动指定func_id + * - 无需手动编写反序列化代码 + * - 减少出错可能 + * + * ## 使用示例 + * ```cpp + * // 消息类型 + * struct UserLogin { + * std::string username; + * std::string password; + * }; + * + * // 处理函数 + * void handle_login(const UserLogin& msg) { + * // 处理登录 + * } + * + * // 自动注册 + * static zmq_client_register login_reg(handle_login); + * ``` + * + * @note 通常使用ZMQ_CLIENT_REGISTER_PROCESSOR宏,更简洁 + * @note 注册器对象必须是静态的,确保在程序启动时构造 + */ template class zmq_client_register { public: + /** + * @brief 构造函数 - 自动注册消息处理器 + * + * 在对象构造时,自动将处理函数注册到zmq_client_processor。 + * 这个构造函数会在程序启动的静态初始化阶段被调用。 + * + * ## 模板参数 + * @tparam T 消息类型(隐式从类模板参数推导) + * + * ## 函数参数 + * @param processor 用户定义的处理函数 + * - 类型: auto(编译器自动推导) + * - 要求: 可调用对象,接受const T&参数 + * - 示例: void(const T&)、lambda、函数对象 + * + * ## 工作流程 + * 1. 获取zmq_client_processor单例 + * 2. 调用register_processor注册处理器 + * 3. 创建一个lambda包装用户的处理函数 + * 4. lambda负责: + * - 接收原始字节数据 + * - 反序列化为类型T + * - 调用用户处理函数 + * - 处理反序列化错误 + * + * ## Lambda捕获 + * - 按值捕获processor(拷贝处理函数) + * - 存储在std::function中 + * - 生命周期与程序相同 + * + * ## 反序列化处理 + * 使用struct_pack::deserialize: + * - 返回std::expected + * - 成功:has_value()为true,调用处理函数 + * - 失败:记录错误日志,包含错误码 + * + * ## 异常安全 + * - 反序列化异常被捕获 + * - 记录详细错误信息 + * - 不会中断程序 + * + * ## 错误处理 + * **反序列化失败**(返回值检查失败): + * - 记录ERROR日志 + * - 包含错误码(int类型) + * - 不调用用户处理函数 + * + * **反序列化异常**(抛出异常): + * - 捕获std::exception + * - 记录ERROR日志 + * - 包含异常消息 + * - 不调用用户处理函数 + * + * ## 使用示例 + * ```cpp + * // 完整示例 + * struct ChatMessage { + * std::string user; + * std::string text; + * }; + * + * void handle_chat(const ChatMessage& msg) { + * std::cout << msg.user << ": " << msg.text << std::endl; + * } + * + * // 自动注册 + * static zmq_client_register reg(handle_chat); + * + * // 也可以使用lambda + * static zmq_client_register reg2([](const ChatMessage& msg) { + * // 处理逻辑 + * }); + * ``` + * + * ## 静态初始化顺序 + * - 同一编译单元内:按声明顺序初始化 + * - 不同编译单元:顺序未定义 + * - 但这不影响使用,因为所有注册都在main()前完成 + * + * @note 该构造函数在程序启动时自动调用 + * @note processor参数会被拷贝并存储 + * @warning 确保T类型可被struct_pack正确序列化/反序列化 + */ zmq_client_register(auto processor) { zmq_client_processor::instance().register_processor(alicho_type_id_v, [processor](const void* payload, size_t size) { @@ -62,6 +504,128 @@ public: } }; +/** + * @def ZMQ_CLIENT_REGISTER_PROCESSOR + * @brief 简化消息处理器注册的宏 - 一行代码完成注册 + * + * 这个宏是使用zmq_client_register的推荐方式,它大大简化了处理器的注册代码。 + * 通过这个宏,你只需要定义消息类型和处理逻辑,其他一切都自动完成。 + * + * ## 宏参数 + * @param data_type 消息的数据类型名称(不需要引号) + * + * ## 宏展开 + * ```cpp + * ZMQ_CLIENT_REGISTER_PROCESSOR(MyMessage) + * + * // 展开为: + * void MyMessage_processor(const MyMessage& data); // 1. 函数声明 + * static zmq_client_register zmq_register_MyMessage(MyMessage_processor); // 2. 静态注册器 + * void MyMessage_processor(const MyMessage& data) // 3. 函数定义开始 + * ``` + * + * ## 使用方式 + * ```cpp + * // 定义消息类型 + * struct LoginRequest { + * std::string username; + * std::string password; + * }; + * + * // 注册并实现处理器(一行宏 + 函数体) + * ZMQ_CLIENT_REGISTER_PROCESSOR(LoginRequest) { + * // data参数自动可用,类型是const LoginRequest& + * if (authenticate(data.username, data.password)) { + * std::cout << "Login successful: " << data.username << std::endl; + * } else { + * std::cout << "Login failed" << std::endl; + * } + * } + * ``` + * + * ## 命名约定 + * 宏会自动生成以下标识符: + * - **处理函数**: `data_type_processor` + * - **注册对象**: `zmq_register_data_type` + * + * 示例: + * ```cpp + * ZMQ_CLIENT_REGISTER_PROCESSOR(MyMessage) + * // 生成: + * // - void MyMessage_processor(const MyMessage& data) + * // - static zmq_client_register zmq_register_MyMessage + * ``` + * + * ## 优势 + * 1. **简洁**: 只需一行宏声明 + * 2. **类型安全**: 编译期检查类型匹配 + * 3. **自动化**: 无需手动指定func_id + * 4. **统一**: 所有处理器使用相同模式 + * 5. **可读性**: 清晰表达"为XX类型注册处理器" + * + * ## 完整示例 + * ```cpp + * // === 文件: message_types.h === + * struct ChatMessage { + * std::string user; + * std::string content; + * }; + * + * struct SystemNotification { + * std::string message; + * int level; + * }; + * + * // === 文件: message_handlers.cpp === + * #include "message_types.h" + * #include "zmq_client_processor.h" + * + * // 聊天消息处理器 + * ZMQ_CLIENT_REGISTER_PROCESSOR(ChatMessage) { + * std::cout << "[" << data.user << "]: " << data.content << std::endl; + * } + * + * // 系统通知处理器 + * ZMQ_CLIENT_REGISTER_PROCESSOR(SystemNotification) { + * std::string level_str = (data.level == 0) ? "INFO" : "ERROR"; + * std::cout << "[" << level_str << "] " << data.message << std::endl; + * } + * ``` + * + * ## 注意事项 + * 1. **作用域**: 宏应该在全局作用域或命名空间作用域使用 + * 2. **唯一性**: 每个消息类型只能注册一次 + * 3. **链接**: 确保包含该宏的.cpp文件被链接到最终程序 + * 4. **参数名**: 处理函数的参数固定为`data` + * + * ## 调试技巧 + * 如果处理器没有被调用: + * 1. 检查包含宏的.cpp文件是否被编译 + * 2. 检查消息类型是否匹配 + * 3. 检查是否有链接器优化移除了未使用的静态对象 + * 4. 在处理器中添加日志确认是否被调用 + * + * ## 高级用法 + * ```cpp + * // 在命名空间中使用 + * namespace my_app { + * ZMQ_CLIENT_REGISTER_PROCESSOR(AppMessage) { + * // 处理逻辑 + * } + * } + * + * // 处理复杂数据类型 + * ZMQ_CLIENT_REGISTER_PROCESSOR(ComplexData) { + * // data.nested_field + * // data.vector_field[0] + * // ... + * } + * ``` + * + * @note 这是注册处理器的推荐方式 + * @note 宏展开后的函数名可以在其他地方引用(如果需要) + * @warning 不要在头文件中使用该宏,会导致重复定义 + */ #define ZMQ_CLIENT_REGISTER_PROCESSOR(data_type) \ void data_type##_processor(const data_type& data); \ static zmq_client_register zmq_register_##data_type(data_type##_processor); \ diff --git a/src/network/transport/zmq_server.cpp b/src/network/transport/zmq_server.cpp index 553a905..ef00f44 100644 --- a/src/network/transport/zmq_server.cpp +++ b/src/network/transport/zmq_server.cpp @@ -1,7 +1,116 @@ +/** + * @file zmq_server.cpp + * @brief ZeroMQ服务器实现文件 + * + * 本文件实现了zmq_server.h中声明的方法,包括服务器初始化、消息接收、 + * 客户端管理等核心功能。 + * + * ## 实现要点 + * 1. **ROUTER套接字管理**: 创建、绑定和使用ROUTER套接字 + * 2. **多帧消息处理**: 正确处理ROUTER的两帧消息格式 + * 3. **客户端跟踪**: 自动维护客户端列表 + * 4. **错误处理**: 详细的日志记录和异常处理 + * 5. **消息分发**: 接收消息后分发到处理器 + * + * ## 与头文件的对应 + * - init(): 创建套接字并绑定到服务器地址 + * - recv(): 接收客户端消息并分发 + * - remove_client(): 从客户端列表中移除 + * - has_client(): 检查客户端是否存在 + * + * @see zmq_server.h 查看类声明和详细文档 + */ + #include "zmq_server.h" #include "zmq_server_processor.h" +/** + * @brief 初始化并启动服务器 + * + * 实现zmq_server::init()方法。创建ROUTER套接字,绑定到服务器地址, + * 开始监听客户端连接。 + * + * ## 实现细节 + * + * ### 1. 重复启动检查 + * ```cpp + * if (state_ == zmq_server::state::RUNNING) { + * log_module_warn(...); + * return; + * } + * ``` + * - 如果服务器已经在运行,记录警告 + * - 直接返回,避免重复绑定 + * - 使方法幂等,多次调用安全 + * + * ### 2. 创建ROUTER套接字 + * ```cpp + * socket_ = zmq::socket_t(context_, zmq::socket_type::router); + * ``` + * - 使用context_创建ROUTER类型套接字 + * - ROUTER用于服务器端,支持多客户端 + * - 可以通过routing_id识别客户端 + * + * ### 3. 绑定到服务器地址 + * ```cpp + * socket_.bind(ZMQ_SERVER_ADDRESS); + * ``` + * - 绑定到预定义的服务器地址 + * - Windows: tcp://127.0.0.1:29623 + * - Linux/macOS: ipc:///tmp/alicho_backend_server.ipc + * - bind()会监听该地址的连接请求 + * - 如果地址已被占用会抛出异常 + * + * ### 4. 成功处理 + * - 设置state_为RUNNING + * - 记录INFO级别日志 + * - 服务器进入可用状态 + * + * ### 5. 异常处理 + * - 捕获zmq::error_t异常 + * - 设置state_为FAILED + * - 记录ERROR级别日志 + * - 重新抛出异常让调用者处理 + * + * ## 可能的错误 + * - **Address already in use**: + * - 端口已被其他程序占用 + * - 或者服务器已经在运行 + * - 解决:检查端口占用,关闭占用程序 + * - **Permission denied**: + * - 权限不足,无法绑定端口 + * - 特权端口(<1024)需要root权限 + * - 解决:使用非特权端口或提升权限 + * - **Invalid argument**: + * - 地址格式错误 + * - 解决:检查ZMQ_SERVER_ADDRESS配置 + * + * ## 绑定地址说明 + * ### Windows (TCP) + * - tcp://127.0.0.1:29623 + * - 只监听本地回环接口 + * - 不接受外部连接(安全) + * - 端口29623是项目自定义 + * + * ### Linux/macOS (IPC) + * - ipc:///tmp/alicho_backend_server.ipc + * - Unix域套接字,性能更优 + * - 只能本地连接(安全) + * - /tmp目录重启后自动清理 + * + * ## 日志示例 + * ``` + * [INFO] [zmq server] ZMQ服务器已启动 + * [WARN] [zmq server] 服务器已在运行中 + * [ERROR] [zmq server] 启动失败: Address already in use + * ``` + * + * @throws zmq::error_t 当ZeroMQ操作失败时 + * + * @note 该方法是幂等的,重复调用只会记录警告 + * @note 必须在客户端连接之前调用 + */ void zmq_server::init() { if (state_ == zmq_server::state::RUNNING) { log_module_warn(ZMQ_SERVER_LOG_MODULE, "服务器已在运行中"); @@ -21,6 +130,221 @@ void zmq_server::init() { } } +/** + * @brief 从客户端接收消息 + * + * 实现zmq_server::recv()方法。从任意客户端接收一条消息,提取客户端ID, + * 反序列化后分发到zmq_server_processor进行处理。 + * + * ## 实现细节 + * + * ### ROUTER消息格式 + * ROUTER套接字接收消息时自动添加routing_id帧: + * ``` + * 帧1: [routing_id] <- ROUTER自动添加,用于识别客户端 + * 帧2: [消息内容] <- 客户端发送的实际数据 + * ``` + * + * ### 1. 接收routing_id帧 + * ```cpp + * zmq::message_t id_msg; + * auto result = socket_.recv(id_msg, zmq::recv_flags::none); + * ``` + * - 创建消息对象存储routing_id + * - 阻塞等待第一帧(routing_id) + * - result包含接收的字节数 + * - 失败时result为falsesize为0 + * + * ### 2. 第一帧接收结果检查 + * ```cpp + * if (!result || *result == 0) { + * log_module_error(...); + * return; + * } + * ``` + * - 检查是否成功接收routing_id + * - 失败可能是套接字错误或连接问题 + * - 记录错误并返回,等待下一条消息 + * + * ### 3. 接收消息内容帧 + * ```cpp + * zmq::message_t content_msg; + * result = socket_.recv(content_msg, zmq::recv_flags::none); + * ``` + * - 接收第二帧(实际消息内容) + * - ROUTER消息总是至少两帧 + * - 第二帧包含客户端发送的数据 + * + * ### 4. 第二帧接收结果检查 + * ```cpp + * if (!result || *result == 0) { + * log_module_error(...); + * return; + * } + * ``` + * - 检查是否成功接收消息内容 + * - 失败表示消息不完整 + * - 记录错误并返回 + * + * ### 5. 提取客户端ID + * ```cpp + * if (id_msg.size() != sizeof(uint32_t)) { + * log_module_error(..., id_msg.size()); + * return; + * } + * auto client_id = *static_cast(id_msg.data()); + * ``` + * - 验证routing_id大小是否为4字节(uint32_t) + * - 如果大小不匹配,可能是客户端配置错误 + * - 将二进制数据转换为uint32_t + * - client_id用于后续的处理和回复 + * + * ### 6. 记录调试信息 + * ```cpp + * log_module_debug(..., client_id); + * ``` + * - DEBUG级别记录客户端ID + * - 帮助追踪消息来源 + * - 用于调试客户端通信问题 + * + * ### 7. 保存客户端routing_id + * ```cpp + * clients_[client_id] = std::move(id_msg); + * ``` + * - 将routing_id消息存入clients_映射表 + * - 使用移动语义避免拷贝 + * - 如果客户端已存在,更新其routing_id + * - 后续send()时使用这个routing_id + * + * ### 8. 空消息处理 + * ```cpp + * if (content_msg.empty()) + * return; + * ``` + * - 检查消息内容是否为空 + * - 空消息可能是心跳或探测 + * - 直接返回,不处理 + * + * ### 9. 反序列化消息 + * ```cpp + * auto pack = zmq_message_pack::deserialize(content_msg); + * ``` + * - 将ZeroMQ消息反序列化为zmq_message_pack + * - 提取func_id和payload + * - 可能抛出异常(如果格式错误) + * + * ### 10. 记录消息信息 + * ```cpp + * log_module_debug(..., pack.func_id, pack.payload.size()); + * ``` + * - DEBUG级别记录消息详情 + * - 包含func_id(消息类型) + * - 包含payload大小(数据量) + * - 用于性能分析和调试 + * + * ### 11. 消息分发 + * ```cpp + * zmq_server_processor::instance().process( + * pack.func_id, + * client_id, + * pack.payload.data(), + * pack.payload.size() + * ); + * ``` + * - 调用单例处理器的process方法 + * - 传递func_id用于查找处理器 + * - 传递client_id让处理器知道来源 + * - 传递原始payload数据和大小 + * - 处理器内部会进行类型恢复和业务逻辑处理 + * + * ### 12. 异常处理 + * - 捕获所有std::exception + * - 记录ERROR级别日志,包含异常消息 + * - 不抛出异常,保持接收循环继续 + * - 单个消息错误不影响服务器运行 + * + * ## 完整处理流程 + * ``` + * recv() 调用 + * ↓ + * 接收routing_id帧 [阻塞] + * ↓ + * 检查接收结果 + * ↓ (成功) + * 接收消息内容帧 [阻塞] + * ↓ + * 检查接收结果 + * ↓ (成功) + * 验证routing_id大小 + * ↓ + * 提取client_id + * ↓ + * 保存到clients_映射表 + * ↓ + * 检查消息是否为空 + * ↓ (非空) + * zmq_message_pack::deserialize() + * ↓ + * 记录DEBUG日志 + * ↓ + * zmq_server_processor::process() + * ↓ + * 处理器反序列化payload + * ↓ + * 调用用户注册的处理函数 + * ↓ + * 返回,等待下一条消息 + * ``` + * + * ## 客户端识别机制 + * - ROUTER自动为每个连接添加routing_id + * - 客户端设置的routing_id(uint32_t)用作client_id + * - 首次接收时添加到clients_ + * - 后续消息更新routing_id(防止重连后变化) + * + * ## 多客户端处理 + * - ROUTER公平轮询所有客户端 + * - 不会饿死任何客户端 + * - 每个客户端的消息独立处理 + * + * ## 阻塞行为 + * - 默认阻塞直到收到消息 + * - 不消耗CPU资源(操作系统级别的等待) + * - 可以通过信号或其他机制中断 + * + * ## 性能考虑 + * - 接收:ZeroMQ优化的网络I/O + * - 反序列化:struct_pack高效处理 + * - 分发:O(1)哈希查找 + * - 瓶颈通常在处理函数本身 + * + * ## 错误场景 + * - **接收失败**: 网络错误、套接字关闭 + * - **routing_id大小错误**: 客户端配置错误 + * - **反序列化失败**: 数据损坏、格式不匹配 + * - **处理器未找到**: func_id不存在 + * - **处理函数异常**: 业务逻辑错误 + * + * ## 日志示例 + * ``` + * [ERROR] [zmq server] 接收客户端ID失败 + * [ERROR] [zmq server] 接收消息内容失败 + * [ERROR] [zmq server] 无效的客户端ID大小: 8 + * [DEBUG] [zmq server] 接收到来自客户端 1001 的消息 + * [DEBUG] [zmq server] 收到消息,func_id: 2001, payload大小: 128 + * [ERROR] [zmq server] 处理消息异常: Invalid data format + * ``` + * + * ## 安全考虑 + * - 验证routing_id大小防止缓冲区溢出 + * - 捕获异常防止服务器崩溃 + * - 空消息检查防止无效处理 + * + * @note 该方法会阻塞当前线程 + * @note 通常在主循环中调用 + * @note 消息处理在当前线程同步执行 + * @warning 如果处理器函数耗时过长,会影响其他客户端 + */ void zmq_server::recv() { try { // --- 接收并存储身份 --- @@ -61,6 +385,95 @@ void zmq_server::recv() { } } +/** + * @brief 移除指定客户端 + * + * 实现zmq_server::remove_client()方法。从clients_映射表中移除指定的客户端。 + * + * ## 实现细节 + * + * ### 1. 查找客户端 + * ```cpp + * auto it = clients_.find(client_id); + * ``` + * - 在clients_映射表中查找client_id + * - 返回迭代器,指向找到的元素或end() + * - O(1)平均时间复杂度 + * + * ### 2. 存在性检查 + * ```cpp + * if (it != clients_.end()) { + * ``` + * - 检查客户端是否存在 + * - 如果不存在,静默返回(幂等操作) + * - 避免无效删除操作 + * + * ### 3. 删除客户端 + * ```cpp + * clients_.erase(it); + * ``` + * - 从映射表中删除该客户端 + * - 释放存储的routing_id消息 + * - O(1)时间复杂度 + * + * ### 4. 记录日志 + * ```cpp + * log_module_info(..., client_id); + * ``` + * - INFO级别记录移除操作 + * - 包含client_id便于追踪 + * - 帮助诊断连接问题 + * + * ## 使用场景 + * - **主动断开**: 客户端发送断开请求 + * - **超时检测**: 长时间未收到心跳 + * - **错误处理**: 检测到客户端异常 + * - **手动管理**: 管理员手动移除客户端 + * + * ## 影响 + * - 无法再向该客户端发送消息 + * - send(client_id, ...)会失败 + * - 客户端重新连接时会被重新添加 + * - 不会主动关闭客户端连接(ZeroMQ管理) + * + * ## 资源释放 + * - zmq::message_t自动析构 + * - routing_id内存被释放 + * - 映射表条目被删除 + * + * ## 并发考虑 + * - 单线程使用是安全的 + * - 多线程需要外部同步 + * - 与recv()同线程时无竞争 + * + * ## 使用示例 + * ```cpp + * // 处理断开连接消息 + * ZMQ_SERVER_REGISTER_PROCESSOR(DisconnectRequest) { + * zmq_server::instance().remove_client(client_id); + * log_info("Client {} disconnected", client_id); + * } + * + * // 超时清理 + * void cleanup_timeout_clients() { + * auto now = std::chrono::steady_clock::now(); + * for (auto client_id : timeout_list) { + * zmq_server::instance().remove_client(client_id); + * } + * } + * ``` + * + * ## 日志示例 + * ``` + * [INFO] [zmq server] 移除客户端 1001 + * ``` + * + * @param client_id 要移除的客户端ID + * + * @note 该方法是幂等的,重复调用无害 + * @note 不会主动关闭客户端连接 + * @note 客户端可以重新连接 + */ void zmq_server::remove_client(uint32_t client_id) { auto it = clients_.find(client_id); if (it != clients_.end()) { @@ -69,6 +482,107 @@ void zmq_server::remove_client(uint32_t client_id) { } } +/** + * @brief 检查客户端是否存在 + * + * 实现zmq_server::has_client()方法。检查指定的客户端ID是否在clients_映射表中。 + * + * ## 实现细节 + * + * ### C++20 contains()方法 + * ```cpp + * return clients_.contains(client_id); + * ``` + * - C++20引入的便捷方法 + * - 直接检查键是否存在 + * - 返回bool值 + * - O(1)平均时间复杂度 + * + * ### 等价实现(C++17及更早) + * ```cpp + * return clients_.find(client_id) != clients_.end(); + * ``` + * + * ## 使用场景 + * - **发送前检查**: 验证客户端是否在线 + * - **权限验证**: 检查客户端是否已认证 + * - **统计信息**: 计算在线客户端 + * - **条件处理**: 根据客户端存在性执行不同逻辑 + * + * ## 返回值含义 + * - **true**: 客户端在clients_中,可以发送消息 + * - **false**: 客户端不在clients_中,可能未连接或已断开 + * + * ## 性能 + * - O(1)平均时间复杂度 + * - 非常高效,可以频繁调用 + * - 不修改映射表 + * + * ## 线程安全 + * - 只读操作 + * - 单线程安全 + * - 多线程需要外部同步 + * + * ## 使用示例 + * ```cpp + * // 发送前检查 + * if (zmq_server::instance().has_client(target_id)) { + * zmq_server::instance().send(target_id, notification); + * } else { + * log_warn("Client {} not connected", target_id); + * } + * + * // 广播消息(除了发送者) + * void broadcast(uint32_t sender_id, const Message& msg) { + * for (uint32_t client_id = 1; client_id <= MAX_CLIENTS; ++client_id) { + * if (client_id != sender_id && + * zmq_server::instance().has_client(client_id)) { + * zmq_server::instance().send(client_id, msg); + * } + * } + * } + * + * // 统计在线客户端 + * size_t count_online() { + * size_t count = 0; + * for (uint32_t id : all_client_ids) { + * if (zmq_server::instance().has_client(id)) { + * ++count; + * } + * } + * return count; + * } + * ``` + * + * ## 注意事项 + * - 返回值只代表调用时刻的状态 + * - 客户端可能在检查后立即断开 + * - TOCTOU(Time-of-check to time-of-use)问题 + * - 如果需要原子性,需要额外机制 + * + * ## 调试用途 + * ```cpp + * // 调试输出 + * void debug_clients() { + * std::cout << "Connected clients: "; + * for (uint32_t id = 1; id <= 100; ++id) { + * if (zmq_server::instance().has_client(id)) { + * std::cout << id << " "; + * } + * } + * std::cout << std::endl; + * } + * ``` + * + * @param client_id 要检查的客户端ID + * @return bool 客户端是否存在 + * - true: 客户端存在 + * - false: 客户端不存在 + * + * @note 性能:O(1)哈希查找 + * @note 返回值只代表调用时刻的状态 + * @note 不会抛出异常 + */ bool zmq_server::has_client(uint32_t client_id) const { return clients_.contains(client_id); } diff --git a/src/network/transport/zmq_server.h b/src/network/transport/zmq_server.h index 51979ce..99d8668 100644 --- a/src/network/transport/zmq_server.h +++ b/src/network/transport/zmq_server.h @@ -1,3 +1,90 @@ +/** + * @file zmq_server.h + * @brief ZeroMQ服务器实现 - 基于ROUTER套接字的多客户端服务器 + * + * 本文件实现了ZeroMQ服务器,用于管理多个客户端连接并处理它们的消息。服务器使用 + * ROUTER套接字类型,能够识别每个客户端并针对性地回复消息。 + * + * ## 核心功能 + * 1. **多客户端管理**: 维护客户端列表,跟踪所有连接的客户端 + * 2. **客户端识别**: 通过routing_id识别每个客户端 + * 3. **消息接收**: 接收客户端消息并分发到处理器 + * 4. **针对性发送**: 可以向指定客户端发送响应 + * 5. **状态管理**: 跟踪服务器运行状态 + * + * ## ZeroMQ套接字类型 + * 使用ROUTER套接字的原因: + * - **客户端识别**: 自动跟踪每个连接的routing_id + * - **针对性回复**: 可以向特定客户端发送消息 + * - **多路复用**: 同时处理多个客户端 + * - **公平队列**: 公平地处理来自不同客户端的消息 + * - **与DEALER配对**: DEALER客户端 <-> ROUTER服务器是标准组合 + * + * ## 通信模式 + * ``` + * Client1 (DEALER) ┐ + * Client2 (DEALER) ├──> zmq_server (ROUTER) + * Client3 (DEALER) ┘ + * ↓ ↓ + * 每个客户端有唯一routing_id 通过routing_id识别和回复 + * ``` + * + * ## ROUTER套接字工作原理 + * ``` + * 接收消息: + * [routing_id][消息内容] <- ROUTER自动添加routing_id + * + * 发送消息: + * [routing_id][消息内容] -> 必须手动指定routing_id + * ``` + * + * ## 与其他模块的关系 + * - **zmq_client**: 通信对端,使用DEALER套接字 + * - **zmq_server_processor**: 处理接收到的消息 + * - **zmq_util**: 使用其定义的消息格式和服务器地址 + * - **lazy_singleton**: 继承单例模式,全局唯一实例 + * + * ## 设计模式 + * - **单例模式**: 确保全局只有一个服务器实例 + * - **状态模式**: 使用state枚举管理服务器状态 + * - **观察者模式**: 服务器观察并响应客户端消息 + * - **策略模式**: 不同消息类型有不同的处理策略 + * + * ## 使用示例 + * ```cpp + * // 初始化服务器 + * zmq_server::instance().init(); + * + * // 在主循环中接收消息 + * while (running) { + * zmq_server::instance().recv(); + * // 消息会自动分发到处理器 + * // 处理器中可以使用client_id回复 + * } + * + * // 在处理器中回复客户端 + * void handle_request(uint32_t client_id, const Request& req) { + * Response resp = process(req); + * zmq_server::instance().send(client_id, resp); + * } + * ``` + * + * ## 线程安全性 + * - zmq::context_t和zmq::socket_t不是线程安全的 + * - 建议在单个线程中使用服务器实例 + * - 如果多线程访问,需要外部同步机制 + * + * ## 性能考虑 + * - ROUTER套接字支持高效的多路复用 + * - clients_映射表使用O(1)查找 + * - 序列化使用struct_pack,效率优秀 + * - 瓶颈通常在消息处理逻辑 + * + * @note 服务器必须在客户端之前启动 + * @note 建议在应用程序生命周期内保持服务器运行 + * @warning ZeroMQ对象不是线程安全的,避免多线程同时访问 + */ + #pragma once #include #include "logger.h" @@ -7,18 +94,206 @@ #include "type_util.h" #include "zmq_util.h" +/** + * @def ZMQ_SERVER_LOG_MODULE + * @brief ZeroMQ服务器的日志模块名称 + * + * 用于标识来自zmq_server的日志消息。所有服务器相关的日志都使用这个模块名。 + * + * 日志使用场景: + * - ERROR: 启动失败、接收失败、发送失败、客户端ID无效 + * - WARN: 服务器已在运行时重复初始化、向未知客户端发送消息 + * - INFO: 服务器启动成功、移除客户端 + * - DEBUG: 接收到消息、成功发送消息 + * + * @note 注意拼写包含空格 "zmq server" + */ #define ZMQ_SERVER_LOG_MODULE "zmq server" +/** + * @class zmq_server + * @brief ZeroMQ服务器类 - 单例模式的多客户端网络服务器 + * + * 这个类封装了ZeroMQ ROUTER套接字,提供了完整的服务器功能,包括启动管理、 + * 多客户端连接管理、消息收发和状态跟踪。 + * + * ## 单例设计 + * 继承自lazy_singleton,确保: + * - 全局只有一个服务器实例 + * - 延迟初始化(第一次使用时创建) + * - 线程安全的初始化 + * + * ## 核心职责 + * 1. 绑定到服务器地址,监听客户端连接 + * 2. 接收来自多个客户端的消息 + * 3. 维护客户端列表(通过routing_id) + * 4. 向指定客户端发送响应 + * 5. 分发消息到处理器 + * 6. 管理服务器状态 + * + * ## ZeroMQ ROUTER套接字特性 + * - **自动路由**: 接收时自动添加routing_id帧 + * - **手动路由**: 发送时需要手动指定routing_id帧 + * - **多路复用**: 同时处理多个客户端 + * - **公平队列**: 轮询处理各客户端消息 + * - **客户端管理**: 自动跟踪连接的客户端 + * + * ## 状态机 + * ``` + * STOPPED ----init()----> RUNNING + * ^ | + * | | + * +----析构函数/失败------+ + * 或 | + * FAILED <-----------+ + * ``` + * + * ## 客户端管理 + * - 客户端通过routing_id唯一识别 + * - 首次收到消息时自动添加到clients_ + * - 可以手动移除客户端 + * - clients_映射表存储routing_id -> zmq::message_t + * + * ## 使用场景 + * 典型的服务器使用流程: + * ```cpp + * // 1. 获取单例并初始化 + * auto& server = zmq_server::instance(); + * server.init(); + * + * // 2. 在主循环中接收消息 + * while (running) { + * server.recv(); // 阻塞等待客户端消息 + * // 消息自动分发到处理器 + * } + * + * // 3. 在处理器中回复 + * ZMQ_SERVER_REGISTER_PROCESSOR(RequestType) { + * // 处理请求 + * ResponseType resp = handle(data); + * // 回复该客户端 + * zmq_server::instance().send(client_id, resp); + * } + * ``` + * + * @note 这是一个单例类,通过instance()获取唯一实例 + * @note 服务器必须在客户端连接之前启动 + */ class zmq_server : public lazy_singleton { + /// 允许lazy_singleton访问私有构造函数 friend class lazy_singleton; public: + /** + * @enum state + * @brief 服务器运行状态枚举 + * + * 定义服务器可能处于的所有状态,用于跟踪服务器生命周期。 + * + * ## 状态转换 + * - STOPPED -> RUNNING: 调用init()成功 + * - RUNNING -> STOPPED: 调用析构函数 + * - RUNNING -> FAILED: init()失败 + * - STOPPED -> FAILED: init()失败 + * + * ## 状态含义 + * 每个状态代表服务器的当前工作状态,影响可以执行的操作。 + */ enum class state { + /** + * @brief 已停止状态 + * + * 服务器未运行或已停止。 + * + * 特征: + * - 套接字未创建或已关闭 + * - 不接受客户端连接 + * - 无法收发消息 + * - 可以调用init()启动服务器 + * + * 进入方式: + * - 对象刚创建 + * - 析构函数执行 + * - 主动停止服务器(如果实现) + */ STOPPED, + + /** + * @brief 运行中状态 + * + * 服务器正常运行,可以处理客户端请求。 + * + * 特征: + * - 套接字已创建并绑定 + * - 监听客户端连接 + * - 可以接收和发送消息 + * - 维护客户端列表 + * + * 进入方式: + * - init()成功 + * + * 退出方式: + * - 析构函数 -> STOPPED + * - 严重错误 -> FAILED + */ RUNNING, + + /** + * @brief 失败状态 + * + * 服务器启动或运行过程中发生错误。 + * + * 特征: + * - 无法正常工作 + * - 可能需要人工干预 + * - 可能是端口被占用或权限问题 + * + * 进入方式: + * - init()失败 + * - 严重运行时错误 + * + * 退出方式: + * - 重新init()(如果问题已解决) + */ FAILED }; + /** + * @brief 析构函数 - 自动清理服务器资源 + * + * 在对象销毁时自动关闭套接字并清理资源。使用noexcept确保析构函数不抛出异常。 + * + * ## 清理流程 + * 1. 检查当前状态是否为RUNNING + * 2. 设置linger为0(立即关闭,不等待未发送的消息) + * 3. 关闭套接字 + * 4. 更新状态为STOPPED + * + * ## linger选项 + * 设置为0的原因: + * - 避免析构时阻塞等待消息发送 + * - 程序退出时快速清理 + * - 未发送的消息会被丢弃 + * - 客户端会检测到连接断开 + * + * ## 异常处理 + * - 捕获所有异常,不允许异常逃逸 + * - 不记录日志(logger可能已被析构) + * - 静默失败,确保析构过程安全 + * + * ## 客户端影响 + * - 所有客户端连接会断开 + * - 客户端会收到连接错误 + * - 未发送的消息会丢失 + * + * ## 注意事项 + * - 析构函数不应该在正常业务流程中被调用 + * - 建议优雅关闭:先通知客户端,再退出 + * - 析构时的错误无法通知调用者 + * + * @note 析构函数不会抛出异常 + * @note 单例对象的析构在程序结束时自动进行 + */ ~zmq_server() { if (state_ == state::RUNNING) { try { @@ -32,8 +307,169 @@ public: } } + /** + * @brief 初始化并启动服务器 + * + * 创建ROUTER套接字,绑定到服务器地址,开始监听客户端连接。 + * + * ## 工作流程 + * 1. 检查当前状态 + * - 如果已经RUNNING,记录警告并返回 + * 2. 创建ROUTER套接字 + * 3. 绑定到ZMQ_SERVER_ADDRESS + * 4. 设置状态为RUNNING + * 5. 记录成功日志 + * + * ## 绑定地址 + * 由ZMQ_SERVER_ADDRESS宏定义: + * - Windows: tcp://127.0.0.1:29623 + * - Linux/macOS: ipc:///tmp/alicho_backend_server.ipc + * + * ## bind() vs connect() + * - 服务器使用bind(),监听指定地址 + * - 客户端使用connect(),连接到服务器 + * - bind()必须在connect()之前 + * + * ## 重复初始化处理 + * - 如果已经RUNNING,记录WARN日志 + * - 直接返回,不重复初始化 + * - 避免资源泄漏和端口冲突 + * + * ## 异常处理 + * - 捕获zmq::error_t异常 + * - 设置状态为FAILED + * - 记录ERROR日志 + * - 重新抛出异常(让调用者处理) + * + * ## 可能的错误 + * - 端口已被占用:bind()失败 + * - 权限不足:无法绑定到特权端口 + * - 地址格式错误:配置错误 + * - 资源不足:无法创建套接字 + * + * ## 使用示例 + * ```cpp + * try { + * zmq_server::instance().init(); + * std::cout << "Server started successfully" << std::endl; + * + * // 开始接收循环 + * while (running) { + * zmq_server::instance().recv(); + * } + * } + * catch (const zmq::error_t& e) { + * std::cerr << "Failed to start server: " << e.what() << std::endl; + * return 1; + * } + * ``` + * + * ## 日志示例 + * ``` + * [INFO] [zmq server] ZMQ服务器已启动 + * [WARN] [zmq server] 服务器已在运行中 + * [ERROR] [zmq server] 启动失败: Address already in use + * ``` + * + * @throws zmq::error_t 当ZeroMQ操作失败时 + * @note 该方法是幂等的,重复调用只会记录警告 + * @note 必须在客户端connect()之前调用 + * @warning 确保端口未被其他程序占用 + */ void init(); + /** + * @brief 向指定客户端发送消息 + * + * 类型安全的模板方法,将任意可序列化的类型T打包并发送到指定客户端。 + * 使用客户端的routing_id进行路由。 + * + * ## 模板参数 + * @tparam T 要发送的消息类型 + * - 必须可被struct_pack序列化 + * - 必须有对应的alicho_type_id_v + * + * ## 函数参数 + * @param client_id 目标客户端的唯一标识符 + * @param data 要发送的消息对象(const引用) + * + * ## 工作流程 + * 1. 在clients_映射表中查找client_id + * - 如果找不到,记录错误并返回 + * 2. 使用zmq_message_pack::create()打包消息 + * 3. 创建zmq::message_t + * 4. 发送routing_id帧(使用sndmore标志) + * 5. 发送消息内容帧 + * 6. 检查发送结果并记录日志 + * + * ## ROUTER发送格式 + * ROUTER套接字发送需要两个帧: + * ``` + * 帧1: [routing_id] <- 指定目标客户端(sndmore) + * 帧2: [消息内容] <- 实际消息数据 + * ``` + * + * ## 客户端识别 + * - client_id在clients_中查找 + * - clients_[client_id]存储该客户端的routing_id消息 + * - routing_id是ZeroMQ用于路由的二进制数据 + * - 第一次接收客户端消息时自动添加到clients_ + * + * ## 发送标志 + * - 第一帧使用sndmore:表示后面还有帧 + * - 第二帧使用none:表示消息结束 + * + * ## 客户端未找到 + * - 记录ERROR日志 + * - 包含client_id + * - 直接返回,不发送 + * - 可能原因:客户端未连接或已断开 + * + * ## 异常处理 + * - 捕获zmq::error_t异常 + * - 记录ERROR日志 + * - 不抛出异常(避免中断服务) + * + * ## 使用示例 + * ```cpp + * // 在消息处理器中使用 + * ZMQ_SERVER_REGISTER_PROCESSOR(LoginRequest) { + * LoginResponse resp; + * + * if (authenticate(data.username, data.password)) { + * resp.success = true; + * resp.token = generate_token(); + * } else { + * resp.success = false; + * } + * + * // 向发送请求的客户端回复 + * zmq_server::instance().send(client_id, resp); + * } + * ``` + * + * ## 性能考虑 + * - 查找客户端:O(1)哈希查找 + * - 序列化开销:取决于消息大小 + * - 两帧发送:比单帧略慢 + * - 异步发送:ROUTER支持异步 + * + * ## 可靠性 + * - ZeroMQ提供消息完整性保证 + * - 不保证消息送达(需要应用层ACK) + * - 如果客户端断开,消息会丢失 + * + * ## 日志示例 + * ``` + * [ERROR] [zmq server] 尝试向未知客户端 1001 发送消息 + * [DEBUG] [zmq server] 成功向客户端 1001 发送消息 + * [ERROR] [zmq server] 发送消息异常: Broken pipe + * ``` + * + * @note 该方法不会抛出异常,失败时记录日志 + * @note 必须等客户端连接后才能发送 + * @warning 如果客户端已断开,消息会丢失且不会报错 + */ template void send(uint32_t client_id, const T& data) { const auto& it = clients_.find(client_id); @@ -60,20 +496,293 @@ public: } } + /** + * @brief 接收客户端消息 + * + * 从任意客户端接收一条消息,提取客户端ID,反序列化后分发到对应的处理器。 + * 这个方法通常在主循环中调用,阻塞等待客户端消息。 + * + * ## 工作流程 + * 详见zmq_server.cpp中的实现注释 + * + * @note 该方法会阻塞直到收到消息 + * @note 消息处理在当前线程中同步执行 + * @see zmq_server.cpp 查看详细实现 + */ void recv(); - // 状态管理 + // ==================== 状态管理方法 ==================== + + /** + * @brief 获取当前服务器状态 + * + * @return state 当前的服务器状态枚举值 + * + * ## 使用示例 + * ```cpp + * auto current_state = zmq_server::instance().get_state(); + * switch (current_state) { + * case zmq_server::state::RUNNING: + * // 服务器正常运行 + * break; + * case zmq_server::state::STOPPED: + * // 需要启动服务器 + * break; + * case zmq_server::state::FAILED: + * // 需要处理错误 + * break; + * } + * ``` + * + * @note 状态可能在多线程环境下瞬间变化 + */ state get_state() const { return state_; } + + /** + * @brief 检查服务器是否正在运行 + * + * 便捷方法,检查当前状态是否为RUNNING。 + * + * @return bool 是否处于运行状态 + * - true: 状态为RUNNING + * - false: 其他任何状态 + * + * ## 使用示例 + * ```cpp + * if (zmq_server::instance().is_running()) { + * // 可以接收和发送消息 + * zmq_server::instance().recv(); + * } else { + * // 需要先启动服务器 + * zmq_server::instance().init(); + * } + * ``` + * + * @note 这是get_state() == state::RUNNING的快捷方式 + */ bool is_running() const { return state_ == state::RUNNING; } - // 客户端管理 + // ==================== 客户端管理方法 ==================== + + /** + * @brief 移除指定客户端 + * + * 从clients_映射表中移除指定的客户端。通常在检测到客户端断开连接时调用。 + * + * ## 参数 + * @param client_id 要移除的客户端ID + * + * ## 工作流程 + * 1. 在clients_中查找client_id + * 2. 如果找到,从映射表中删除 + * 3. 记录INFO日志 + * 4. 如果未找到,静默返回 + * + * ## 使用场景 + * - 客户端主动断开连接 + * - 检测到客户端超时 + * - 客户端发送断开请求 + * - 服务器管理员手动移除 + * + * ## 影响 + * - 无法再向该客户端发送消息 + * - send(client_id, ...)会失败并记录误 + * - 如果客户端重新连接,会被重新添加 + * + * ## 使用示例 + * ```cpp + * // 在处理断开连接消息时 + * ZMQ_SERVER_REGISTER_PROCESSOR(DisconnectRequest) { + * zmq_server::instance().remove_client(client_id); + * // 不需要回复,客户端已断开 + * } + * ``` + * + * ## 日志示例 + * ``` + * [INFO] [zmq server] 移除客户端 1001 + * ``` + * + * @note 该方法是幂等的,重复调用无害 + * @note 不会关闭客户端连接,只是从列表中移除 + */ void remove_client(uint32_t client_id); + + /** + * @brief 检查客户端是否存在 + * + * 检查指定的客户端ID是否在clients_映射表中。 + * + * ## 参数 + * @param client_id 要检查的客户端ID + * + * ## 返回值 + * @return bool 客户端是否存在 + * - true: 客户端在列表中 + * - false: 客户端不在列表中 + * + * ## 使用场景 + * - 发送前检查客户端是否在线 + * - 验证客户端权限 + * - 统计在线客户端 + * + * ## 使用示例 + * ```cpp + * if (zmq_server::instance().has_client(target_id)) { + * zmq_server::instance().send(target_id, notification); + * } else { + * log_error("Client {} not connected", target_id); + * } + * ``` + * + * @note 性能:O(1)哈希查找 + */ bool has_client(uint32_t client_id) const; + + /** + * @brief 获取当前连接的客户端数量 + * + * 返回clients_映射表的大小,即当前连接的客户端数量。 + * + * ## 返回值 + * @return size_t 连接的客户端数量 + * + * ## 使用场景 + * - 监控服务器负载 + * - 限制最大连接数 + * - 统计和报告 + * + * ## 使用示例 + * ```cpp + * size_t count = zmq_server::instance().client_count(); + * std::cout << "Currently " << count << " clients connected" << std::endl; + * + * if (count > MAX_CLIENTS) { + * log_warn("Too many clients: {}", count); + * } + * ``` + * + * @note 性能:O(1)操作 + */ size_t client_count() const { return clients_.size(); } private: + /** + * @brief 客户端映射表 - 存储所有连接客户端的routing信息 + * + * 这是服务器管理客户端的核心数据结构。 + * + * ## 数据结构 + * ```cpp + * unordered_map + * ↑ ↑ ↑ + * client_id 键:客户端ID 值:routing_id消息 + * ``` + * + * ## 键:client_id + * - 类型:uint32_t + * - 来源:客户端设置的routing_id + * - 唯一性:每个客户端有唯一ID + * - 用途:识别和路由消息 + * + * ## 值:zmq::message_t + * - 存储routing_id帧的消息对象 + * - ROUTER接收时自动提供 + * - ROUTER发送时必须使用 + * - 包含二进制routing信息 + * + * ## 生命周期 + * - 添加:首次接收客户端消息时 + * - 使用:每次向客户端发送消息时 + * - 删除:调用remove_client()时 + * - 清空:服务器关闭时 + * + * ## 为什么存储zmq::message_t + * - ROUTER要求发送时提供routing_id帧 + * - 直接存储消息对象避免重复创建 + * - 使用移动语义,性能高效 + * + * ## 内存管理 + * - zmq::message_t自动管理内存 + * - unordered_map自动管理映射表 + * - 客户端断开后应该remove_client()释放 + * + * ## 并发访问 + * - 单线程访问是安全的 + * - 多线程需要外部同步 + * + * ## 使用示例 + * ```cpp + * // recv()中添加客户端 + * clients_[client_id] = std::move(id_msg); + * + * // send()中使用 + * socket_.send(clients_[client_id], zmq::send_flags::sndmore); + * + * // remove_client()中删除 + * clients_.erase(client_id); + * ``` + * + * @note 使用unordered_map提供O(1)查找性能 + * @note routing_id是二进制数据,不是字符串 + */ std::unordered_map clients_; + + /** + * @brief ZeroMQ ROUTER套接字 + * + * 用于与多个客户端通信的主要套接字对象。 + * + * ## ROUTER套接字特性 + * - 自动添加routing_id帧(接收时) + * - 手动指定routing_id帧(发送时) + * - 支持多个客户端 + * - 公平队列机制 + * - 与DEALER配对 + * + * ## 生命周期 + * - init()时创建并绑定 + * - 析构时销毁 + * - 不能在未初始化时使用 + * + * @note 套接字对象不是线程安全的 + */ zmq::socket_t socket_; + + /** + * @brief ZeroMQ上下文 + * + * ZeroMQ的全局上下文对象,管理I/O线程和内部状态。 + * + * ## 作用 + * - 管理ZeroMQ的I/O线程 + * - 作为创建套接字的工厂 + * - 整个程序生命周期内存在 + * + * ## 线程数 + * - 默认:1个I/O线程 + * - 可通过构造函数参数调整 + * - 服务器通常需要更多I/O线程 + * + * @note 一个程序通常只需要一个context + * @note context的销毁会等待所有套接字关闭 + */ zmq::context_t context_; + + /** + * @brief 当前服务器状态 + * + * 跟踪服务器的运行状态,影响可以执行的操作。 + * + * ## 初始值 + * - STOPPED + * + * ## 更新时机 + * - init(): STOPPED -> RUNNING 或 FAILED + * - 析构: RUNNING -> STOPPED + * - 严重错误: 任何状态 -> FAILED + * + * @note 状态机保证了操作的正确性 + */ state state_ = state::STOPPED; }; diff --git a/src/network/transport/zmq_server_processor.h b/src/network/transport/zmq_server_processor.h index 98f1788..444e808 100644 --- a/src/network/transport/zmq_server_processor.h +++ b/src/network/transport/zmq_server_processor.h @@ -1,3 +1,106 @@ +/** + * @file zmq_server_processor.h + * @brief ZeroMQ服务器端消息处理器 - 支持多客户端的消息分发系统 + * + * 本文件实现了服务器侧的消息处理器框架,与客户端处理器类似,但增加了客户端识别功能。 + * 服务器可以知道消息来自哪个客户端,从而实现针对性的处理和响应。 + * + * ## 核心功能 + * 1. **多客户端支持**: 处理函数接收客户端ID,可以区分不同客户端 + * 2. **消息处理器注册**: 将消息类型与处理函数关联 + * 3. **自动消息分发**: 根据消息类型自动调用对应的处理器 + * 4. **类型安全**: 编译期确保类型匹配 + * 5. **简化注册**: 提供宏简化处理器注册代码 + * + * ## 与客户端处理器的区别 + * | 特性 | 客户端处理器 | 服务器处理器 | + * |------|-------------|-------------| + * | 处理函数签名 | void(const T&) | void(uint32_t client_id, const T&) | + * | 客户端识别 | 无 | 有(通过client_id) | + * | 使用场景 | 接收服务器消息 | 接收多个客户端消息 | + * | 回复能力 | 可以发送但不知道目标 | 可以针对特定客户端回复 | + * + * ## 与其他模块的关系 + * - **zmq_server**: 接收到消息后,调用本处理器进行分发 + * - **zmq_util**: 使用其定义的消息格式和类型ID系统 + * - **type_util**: 使用alicho_type_id_v获取类型标识 + * - **struct_pack**: 用于反序列化payload + * + * ## 设计模式 + * - **单例模式**: 使用lazy_singleton确保全局唯一实例 + * - **注册模式**: 处理器在程序启动时自动注册 + * - **策略模式**: 不同消息类型有不同的处理策略 + * - **观察者模式**: 服务器观察并响应客户端消息 + * + * ## 工作流程 + * ``` + * 1. 程序启动 + * ↓ + * 2. 静态初始化(zmq_server_register对象构造) + * ↓ + * 3. 处理器自动注册到zmq_server_processor + * ↓ + * 4. 运行时接收客户端消息 + * ↓ + * 5. zmq_server调用processor.process(func_id, client_id, payload, size) + * ↓ + * 6. 查找对应的处理器 + * ↓ + * 7. 反序列化payload + * ↓ + * 8. 调用用户定义的处理函数(带client_id) + * ↓ + * 9. 可选:处理函数中通过zmq_server.send(client_id, response)回复 + * ``` + * + * ## 使用示例 + * ```cpp + * // 定义消息类型 + * struct ClientRequest { + * std::string command; + * std::vector args; + * }; + * + * struct ServerResponse { + * int status; + * std::string result; + * }; + * + * // 注册处理器(知道是哪个客户端发的) + * ZMQ_SERVER_REGISTER_PROCESSOR(ClientRequest) { + * // data是消息内容 + * // client_id是发送者的ID + * + * std::cout << "Client " << client_id << " requests: " + * << data.command << std::endl; + * + * // 处理请求 + * ServerResponse response; + * response.status = 0; + * response.result = "OK"; + * + * // 回复特定客户端 + * zmq_server::instance().send(client_id, response); + * } + * ``` + * + * ## 线程安全性 + * - 注册阶段(程序启动)是单线程的,无竞争 + * - 处理阶段需要确保处理函数本身是线程安全的 + * - 如果zmq_server在多线程中使用,需要额外同步 + * + * ## 性能考虑 + * - 使用unordered_map,查找时间复杂度O(1) + * - 函数指针调用开销minimal + * - 反序列化可能是性能瓶颈 + * - client_id传递是值传递,无额外开销 + * + * @note 所有消息类型必须可被struct_pack序列化 + * @note 处理函数应该尽快返回,避免阻塞消息接收 + * @note 可以在处理函数中使用client_id进行针对性响应 + * @warning 不要在处理函数中执行耗时操作,考虑异步处理 + */ + #pragma once #include "lazy_singleton.h" @@ -6,16 +109,201 @@ #include #include "type_util.h" +/** + * @def ZMQ_SERVER_PROCESSOR_LOG_MODULE + * @brief 服务器处理器的日志模块名称 + * + * 用于标识来自zmq_server_processor的日志消息。 + * 所有服务器处理器相关的日志都会使用这个模块名。 + * + * 日志级别说明: + * - ERROR: 处理器未找到、反序列化失败、处理器执行异常 + * - WARN: 暂无 + * - INFO: 暂无 + * - DEBUG: 可以添加处理器注册成功、消息接收的日志 + */ #define ZMQ_SERVER_PROCESSOR_LOG_MODULE "zmq_server_processor" +/** + * @class zmq_server_processor + * @brief ZeroMQ服务器端消息处理器 - 单例模式的多客户端消息分发器 + * + * 这个类负责管理所有服务器接收到的客户端消息的处理逻辑。它维护一个从消息类型ID + * 到处理函数的映射表,当接收到消息时,根据消息的类型ID和客户端ID找到对应的处理器并执行。 + * + * ## 单例设计 + * 继承自lazy_singleton,确保: + * - 全局只有一个实例 + * - 延迟初始化(第一次使用时创建) + * - 线程安全的初始化 + * + * ## 核心职责 + * 1. 维护处理器注册表(func_id -> handler函数) + * 2. 根据func_id和client_id分发消息到对应处理器 + * 3. 处理反序列化和异常 + * 4. 记录错误日志 + * + * ## 存储结构 + * ```cpp + * unordered_map> + * ↑ ↑ ↑ ↑ ↑ + * func_id 类型标识 client_id payload size + * ``` + * + * ## 与客户端处理器的差异 + * - 处理函数多了一个uint32_t参数(client_id) + * - 可以根据client_id进行不同的处理 + * - 可以使用client_id回复特定客户端 + * + * ## 线程安全性 + * - register_processor: 仅在静态初始化阶段调用,单线程安全 + * - process: 如果zmq_server在多线程中调用,需要外部同步 + * - 内部不提供锁保护,假设单线程访问 + * + * ## 使用方式 + * ```cpp + * // 通常不直接使用,而是通过zmq_server_register或宏 + * auto& processor = zmq_server_processor::instance(); + * + * // 处理消息(由zmq_server调用) + * processor.process(func_id, client_id, payload.data(), payload.size()); + * ``` + * + * @note 这是一个单例类,通过instance()获取唯一实例 + * @note 不要尝试创建多个实例 + */ class zmq_server_processor : public lazy_singleton { + /// 允许lazy_singleton访问私有构造函数 friend class lazy_singleton; public: + /** + * @brief 注册消息处理器 + * + * 将一个消息类型ID与对应的处理函数关联起来。这个方法通常不直接调用, + * 而是通过zmq_server_register模板类在程序启动时自动调用。 + * + * ## 参数说明 + * @param func_id 消息类型的唯一标识符(来自alicho_type_id_v) + * @param processor 处理函数,接受客户端ID和原始字节数据 + * - 参数1: uint32_t - 发送消息的客户端ID + * - 参数2: const void* - 指向序列化数据的指针 + * - 参数3: size_t - 数据的字节大小 + * + * ## 函数签名说明 + * 使用std::function的原因: + * - 类型擦除:不同消息类型可以用统一接口存储 + * - 灵活性:可以存储lambda、函数指针、函数对象 + * - 客户端识别:通过uint32_t参数传递客户端ID + * - 标准化:符合C++标准库的设计风格 + * + * ## 注册时机 + * - 通常在程序启动的静态初始化阶段 + * - zmq_server_register的构造函数中调用 + * - 在main()函数执行之前完成 + * + * ## 重复注册处理 + * - 当前实现会覆盖之前的注册 + * - 没有检查重复,需要开发者确保不重复注册 + * - 建议:每种消息类型只注册一次 + * + * ## 使用示例 + * ```cpp + * // 通常由zmq_server_register自动调用 + * zmq_server_processor::instance().register_processor( + * alicho_type_id_v, + * [](uint32_t client_id, const void* data, size_t size) { + * auto msg = struct_pack::deserialize( + * static_cast(data), size); + * if (msg.has_value()) { + * // 处理消息,知道来自哪个客户端 + * handle_message(client_id, msg.value()); + * } + * } + * ); + * ``` + * + * @note 该方法不是线程安全的,但只在静态初始化时调用 + * @note 使用std::move转移processor的所有权 + * @warning 不要为同一个func_id注册多次,会导致覆盖 + */ void register_processor(uint32_t func_id, std::function processor) { processors_[func_id] = std::move(processor); } + /** + * @brief 处理接收到的客户端消息 + * + * 根据消息的类型ID找到对应的处理器,并执行处理逻辑。这个方法由zmq_server + * 在接收到客户端消息后调用。 + * + * ## 参数说明 + * @param func_id 消息类型标识符(从zmq_message_pack中提取) + * @param client_id 发送消息的客户端ID(从ZeroMQ ROUTER套接字获取) + * @param payload 序列化的消息数据指针 + * @param size 数据大小(字节) + * + * ## 处理流程 + * 1. 根据func_id在processors_映射表中查找处理器 + * 2. 如果找不到,记录错误日志并返回 + * 3. 如果找到,调用处理器函数(传入client_id, payload, size) + * 4. 捕获处理器抛出的异常,记录日志 + * + * ## 客户端ID的作用 + * - 识别消息来源:知道是哪个客户端发送的 + * - 针对性响应:可以使用zmq_server::send(client_id, response)回复 + * - 状态管理:可以维护每个客户端的状态 + * - 权限控制:可以根据client_id进行权限检查 + * + * ## 错误处理 + * **处理器未找到**: + * - 记录ERROR级别日志 + * - 包含未找到的func_id + * - 静默返回,不抛出异常 + * + * **处理器执行异常**: + * - 捕获所有std::exception + * - 记录ERROR级别日志 + * - 包含异常信息 + * - 不会传播异常(避免中断服务器运行) + * + * ## 性能分析 + * - unordered_map查找: O(1)平均时间复杂度 + * - 函数调用开销: 很小 + * - client_id传递: 值传递,4字节 + * - 主要开销在处理器函数本身 + * + * ## 线程安全性 + * - 读取processors_映射表(查找操作) + * - 如果只有一个线程调用process,则是安全的 + * - 如果多线程调用,需要外部同步 + * + * ## 使用示例 + * ```cpp + * // 在zmq_server的recv()方法中 + * auto pack = zmq_message_pack::deserialize(content_msg); + * uint32_t client_id = extract_client_id(id_msg); + * + * zmq_server_processor::instance().process( + * pack.func_id, + * client_id, + * pack.payload.data(), + * pack.payload.size() + * ); + * ``` + * + * ## 调试建议 + * 如果消息没有被处理: + * 1. 检查是否正确注册了处理器 + * 2. 检查func_id是否匹配 + * 3. 查看日志中的"未找到处理器"错误 + * 4. 验证消息类型是否正确 + * 5. 确认client_id是否有效 + * + * @note 该方法捕获所有异常,不会抛出 + * @note 处理器函数内部的异常会被记录但不会传播 + * @warning 如果处理器函数耗时过长,会阻塞后续消息的接收 + */ void process(uint32_t func_id, uint32_t client_id, const void* payload, size_t size) { const auto it = processors_.find(func_id); if (it == processors_.end()) { @@ -32,12 +320,220 @@ public: } private: + /** + * @brief 处理器注册表 - 消息类型ID到处理函数的映射 + * + * 这是核心数据结构,存储所有已注册的消息处理器。 + * + * ## 数据结构选择 + * 使用unordered_map的原因: + * - O(1)平均查找时间 + * - func_id是整数,哈希效率高 + * - 不需要有序遍历 + * + * ## 键值说明 + * - **键(uint32_t)**: 消息类型的唯一标识符 + * - 来自alicho_type_id_v + * - 每种消息类型有唯一的ID + * - **值(function)**: 处理函数 + * - 第一个参数是客户端ID + * - 第二、三个参数是原始字节数据 + * - 内部会进行类型恢复和反序列化 + * + * ## 与客户端处理器的差异 + * 服务器处理器的函数签名多了一个uint32_t参数(client_id) + * + * ## 内存管理 + * - std::function内部使用小对象优化 + * - lambda捕获的变量会被存储 + * - 整个映射表在程序生命周期内存在 + * + * ## 生命周期 + * - 程序启动时开始填充 + * - 程序运行期间只读(查找) + * - 程序结束时自动销毁 + * + * ## 并发访问 + * - 写入:仅在静态初始化阶段(单线程) + * - 读取:运行时(可能多线程,需要外部同步) + * + * @note 使用私有成员确保只能通过public接口访问 + * @note 不提供直接访问,保证封装性 + */ std::unordered_map> processors_; }; +/** + * @class zmq_server_register + * @brief 服务器端消息处理器自动注册器 - RAII模式的注册辅助类 + * + * 这是一个模板类,用于在程序启动时自动注册服务器端消息处理器。它利用C++的静态初始化 + * 机制,在main()函数执行之前就完成了处理器的注册。 + * + * ## 模板参数 + * @tparam T 要处理的消息类型 + * - 必须可被struct_pack序列化 + * - 必须有对应的alicho_type_id_v + * + * ## 设计模式 + * - **RAII模式**: 构造时注册,析构时无需清理 + * - **模板模式**: 为不同消息类型生成特化代码 + * - **代理模式**: 代理用户定义的处理函数 + * + * ## 与客户端注册器的区别 + * - 处理函数签名不同:需要接受client_id参数 + * - 注册到zmq_server_processor而非zmq_client_processor + * - 处理器可以根据client_id进行不同的处理逻辑 + * + * ## 工作原理 + * ```cpp + * // 1. 定义静态变量(程序启动时) + * static zmq_server_register reg(my_handler); + * + * // 2. 构造函数被调用 + * zmq_server_register::zmq_server_register(my_handler) { + * // 3. 注册处理器 + * zmq_server_processor::instance().register_processor( + * alicho_type_id_v, // 自动获取类型ID + * [my_handler](uint32_t client_id, const void* data, size_t size) { + * // 4. 反序列化 + * auto result = struct_pack::deserialize(...); + * // 5. 调用用户处理函数(带client_id) + * if (result.has_value()) { + * my_handler(client_id, result.value()); + * } + * } + * ); + * } + * ``` + * + * ## 类型安全 + * - 编译期确定消息类型 + * - 处理函数签名必须匹配:void(uint32_t, const T&) + * - 反序列化类型检查 + * + * ## 使用示例 + * ```cpp + * // 消息类型 + * struct CommandRequest { + * std::string command; + * std::vector args; + * }; + * + * // 处理函数(注意有client_id参数) + * void handle_command(uint32_t client_id, const CommandRequest& req) { + * std::cout << "Client " << client_id << " command: " + * << req.command << std::endl; + * // 可以针对该客户端回复 + * zmq_server::instance().send(client_id, response); + * } + * + * // 自动注册 + * static zmq_server_register cmd_reg(handle_command); + * ``` + * + * @note 通常使用ZMQ_SERVER_REGISTER_PROCESSOR宏,更简洁 + * @note 注册器对象必须是静态的,确保在程序启动时构造 + */ template class zmq_server_register { public: + /** + * @brief 构造函数 - 自动注册服务器端消息处理器 + * + * 在对象构造时,自动将处理函数注册到zmq_server_processor。 + * 这个构造函数会在程序启动的静态初始化阶段被调用。 + * + * ## 模板参数 + * @tparam T 消息类型(隐式从类模板参数推导) + * + * ## 函数参数 + * @param processor 用户定义的处理函数 + * - 类型: auto(编译器自动推导) + * - 要求: 可调用对象,接受(uint32_t, const T&)参数 + * - 示例: void(uint32_t client_id, const T& data) + * + * ## 工作流程 + * 1. 获取zmq_server_processor单例 + * 2. 调用register_processor注册处理器 + * 3. 创建一个lambda包装用户的处理函数 + * 4. lambda负责: + * - 接收客户端ID和原始字节数据 + * - 反序列化为类型T + * - 调用用户处理函数(传递client_id和反序列化的数据) + * - 处理反序列化错误 + * + * ## Lambda捕获 + * - 按值捕获processor(拷贝处理函数) + * - 存储在std::function中 + * - 生命周期与程序相同 + * + * ## 反序列化处理 + * 使用struct_pack::deserialize: + * - 返回std::expected + * - 成功:has_value()为true,调用处理函数 + * - 失败:记录错误日志,包含错误码 + * + * ## 异常安全 + * - 反序列化异常被捕获 + * - 记录详细错误信息 + * - 不会中断服务器运行 + * + * ## 错误处理 + * **反序列化失败**(返回值检查失败): + * - 记录ERROR日志 + * - 包含错误码(int类型) + * - 不调用用户处理函数 + * + * **反序列化异常**(抛出异常): + * - 捕获std::exception + * - 记录ERROR日志 + * - 包含异常消息 + * - 不调用用户处理函数 + * + * ## 使用示例 + * ```cpp + * // 完整示例 + * struct LoginRequest { + * std::string username; + * std::string password; + * }; + * + * struct LoginResponse { + * bool success; + * std::string token; + * }; + * + * void handle_login(uint32_t client_id, const LoginRequest& req) { + * std::cout << "Client " << client_id << " login: " + * << req.username << std::endl; + * + * LoginResponse resp; + * if (authenticate(req.username, req.password)) { + * resp.success = true; + * resp.token = generate_token(client_id); + * } else { + * resp.success = false; + * } + * + * // 回复该客户端 + * zmq_server::instance().send(client_id, resp); + * } + * + * // 自动注册 + * static zmq_server_register login_reg(handle_login); + * ``` + * + * ## 静态初始化顺序 + * - 同一编译单元内:按声明顺序初始化 + * - 不同编译单元:顺序未定义 + * - 但这不影响使用,因为所有注册都在main()前完成 + * + * @note 该构造函数在程序启动时自动调用 + * @note processor参数会被拷贝并存储 + * @note 处理函数必须接受client_id作为第一个参数 + * @warning 确保T类型可被struct_pack正确序列化/反序列化 + */ zmq_server_register(auto processor) { zmq_server_processor::instance().register_processor(alicho_type_id_v, [processor](uint32_t client_id, const void* payload, @@ -65,6 +561,133 @@ public: } }; +/** + * @def ZMQ_SERVER_REGISTER_PROCESSOR + * @brief 简化服务器端消息处理器注册的宏 - 一行代码完成注册 + * + * 这个宏是使用zmq_server_register的推荐方式,它大大简化了服务器端处理器的注册代码。 + * 通过这个宏,你只需要定义消息类型和处理逻辑(包含client_id),其他一切都自动完成。 + * + * ## 宏参数 + * @param data_type 消息的数据类型名称(不需要引号) + * + * ## 宏展开 + * ```cpp + * ZMQ_SERVER_REGISTER_PROCESSOR(MyMessage) + * + * // 展开为: + * void MyMessage_processor(uint32_t client_id, const MyMessage& data); // 1. 函数声明 + * static zmq_server_register zmq_register_MyMessage(MyMessage_processor); // 2. 静态注册器 + * void MyMessage_processor(uint32_t client_id, const MyMessage& data) // 3. 函数定义开始 + * ``` + * + * ## 与客户端宏的区别 + * - 处理函数多了uint32_t client_id参数 + * - 可以根据client_id进行不同的处理 + * - 可以使用client_id回复特定客户端 + * + * ## 使用方式 + * ```cpp + * // 定义消息类型 + * struct ChatMessage { + * std::string user; + * std::string text; + * }; + * + * // 注册并实现处理器(带client_id) + * ZMQ_SERVER_REGISTER_PROCESSOR(ChatMessage) { + * // client_id参数自动可用 + * // data参数自动可用,类型是const ChatMessage& + * + * std::cout << "Client " << client_id << " says: " + * << data.user << ": " << data.text << std::endl; + * + * // 可以回复该客户端 + * ChatMessage echo; + * echo.user = "Server"; + * echo.text = "Message received"; + * zmq_server::instance().send(client_id, echo); + * } + * ``` + * + * ## 命名约定 + * 宏会自动生成以下标识符: + * - **处理函数**: `data_type_processor` + * - **注册对象**: `zmq_register_data_type` + * + * ## 优势 + * 1. **简洁**: 只需一行宏声明 + * 2. **类型安全**: 编译期检查类型匹配 + * 3. **自动化**: 无需手动指定func_id + * 4. **统一**: 所有处理器使用相同模式 + * 5. **客户端识别**: 自动提供client_id参数 + * + * ## 完整示例 + * ```cpp + * // === 消息类型定义 === + * struct CommandRequest { + * std::string command; + * std::vector args; + * }; + * + * struct CommandResponse { + * int exit_code; + * std::string output; + * }; + * + * // === 处理器实现 === + * ZMQ_SERVER_REGISTER_PROCESSOR(CommandRequest) { + * std::cout << "Client " << client_id << " executes: " + * << data.command << std::endl; + * + * // 执行命令 + * CommandResponse resp; + * resp.exit_code = execute_command(data.command, data.args, resp.output); + * + * // 回复执行结果给该客户端 + * zmq_server::instance().send(client_id, resp); + * } + * ``` + * + * ## 注意事项 + * 1. **作用域**: 宏应该在全局作用域或命名空间作用域使用 + * 2. **唯一性**: 每个消息类型只能注册一次 + * 3. **链接**: 确保包含该宏的.cpp文件被链接到最终程序 + * 4. **参数名**: 处理函数的参数固定为`client_id`和`data` + * + * ## 调试技巧 + * 如果处理器没有被调用: + * 1. 检查包含宏的.cpp文件是否被编译 + * 2. 检查消息类型是否匹配 + * 3. 检查是否有链接器优化移除了未使用的静态对象 + * 4. 在处理器中添加日志确认client_id和消息内容 + * 5. 验证客户端是否正确发送了消息 + * + * ## 高级用法 + * ```cpp + * // 根据客户端ID进行权限检查 + * ZMQ_SERVER_REGISTER_PROCESSOR(AdminCommand) { + * if (!is_admin(client_id)) { + * log_error("Client {} attempted admin command", client_id); + * send_error(client_id, "Permission denied"); + * return; + * } + * + * // 执行管理命令 + * execute_admin_command(data); + * } + * + * // 维护客户端状态 + * ZMQ_SERVER_REGISTER_PROCESSOR(UpdateState) { + * client_states[client_id] = data.new_state; + * broadcast_state_change(client_id, data.new_state); + * } + * ``` + * + * @note 这是注册服务器处理器的推荐方式 + * @note 处理函数可以访问client_id和data两个参数 + * @warning 不要在头文件中使用该宏,会导致重复定义 + */ #define ZMQ_SERVER_REGISTER_PROCESSOR(data_type) \ void data_type##_processor(uint32_t client_id, const data_type& data); \ static zmq_server_register zmq_register_##data_type(data_type##_processor); \ diff --git a/src/network/transport/zmq_util.h b/src/network/transport/zmq_util.h index f0b3de9..4193eb8 100644 --- a/src/network/transport/zmq_util.h +++ b/src/network/transport/zmq_util.h @@ -1,16 +1,287 @@ +/** + * @file zmq_util.h + * @brief ZeroMQ工具函数和消息打包定义 + * + * 本文件是Transport子模块的核心工具文件,提供了ZeroMQ消息序列化/反序列化的基础设施。 + * 它定义了统一的消息打包格式,使得不同类型的数据都能通过ZeroMQ进行传输。 + * + * ## 文件职责 + * 1. **平台相关配置**: 定义不同平台下的ZMQ服务器地址 + * 2. **消息封装**: 提供zmq_message_pack结构体用于消息的打包和解包 + * 3. **类型安全**: 使用编译期类型ID确保消息类型的正确性 + * 4. **序列化支持**: 集成struct_pack库进行高效的二进制序列化 + * + * ## 与其他模块的关系 + * - **zmq_client/zmq_server**: 使用本文件定义的消息格式进行通信 + * - **zmq_client_processor/zmq_server_processor**: 使用func_id进行消息分发 + * - **type_util**: 使用alicho_type_id_v获取类型的唯一标识符 + * - **struct_pack**: 使用该库进行序列化和反序列化 + * + * ## 技术实现 + * - 使用宏定义处理平台差异(Windows使用TCP,Unix使用IPC) + * - 使用模板函数支持任意可序列化类型 + * - 采用两阶段序列化:先序列化数据,再序列化整个消息包 + * + * ## 平台差异说明 + * - **Windows**: 使用TCP套接字(tcp://127.0.0.1:29623) + * - Windows的IPC性能较差,TCP更可靠 + * - 端口29623是项目自定义的端口号 + * - **Linux/macOS**: 使用Unix域套接字(ipc:///tmp/alicho_backend_server.ipc) + * - Unix域套接字在同一主机上性能更优 + * - 不需要网络栈,减少延迟 + * + * @note 本文件定义的消息格式是整个Transport层通信的基础 + * @note 所有通过ZeroMQ传输的消息都必须使用zmq_message_pack进行封装 + */ + #pragma once #include "type_util.h" +/** + * @def ZMQ_SERVER_ADDRESS + * @brief ZeroMQ服务器地址定义(平台相关) + * + * 根据编译目标平台自动选择合适的通信协议和地址: + * + * ## Windows平台 + * - 地址: `tcp://127.0.0.1:29623` + * - 协议: TCP/IP + * - 原因: Windows上的Unix域套接字性能不佳,TCP更稳定可靠 + * - 端口: 29623是项目专用端口,避免与其他服务冲突 + * - 安全性: 绑定到localhost(127.0.0.1),只接受本地连接 + * + * ## Linux/macOS平台 + * - 地址: `ipc:///tmp/alicho_backend_server.ipc` + * - 协议: Unix域套接字(IPC) + * - 原因: 在同一主机上,IPC比TCP性能更优,延迟更低 + * - 路径: /tmp目录确保权限可访问,重启后自动清理 + * - 文件: .ipc后缀表明这是一个IPC套接字文件 + * + * ## 性能对比 + * - IPC(Unix域套接字): + * - 延迟: 约1-2微秒 + * - 吞吐量: 可达数GB/s + * - 无网络栈开销 + * - TCP(本地回环): + * - 延迟: 约10-50微秒 + * - 吞吐量: 通常数百MB/s + * - 需要经过网络栈处理 + * + * ## 使用示例 + * ```cpp + * // 服务器端 + * zmq::socket_t socket(context, zmq::socket_type::router); + * socket.bind(ZMQ_SERVER_ADDRESS); + * + * // 客户端 + * zmq::socket_t socket(context, zmq::socket_type::dealer); + * socket.connect(ZMQ_SERVER_ADDRESS); + * ``` + * + * @note 该地址在编译时确定,运行时不可更改 + * @note 如需修改端口或路径,必须重新编译 + * @warning 服务器和客户端必须使用相同的地址定义 + */ #if ALICHO_PLATFORM_WINDOWS #define ZMQ_SERVER_ADDRESS "tcp://127.0.0.1:29623" #else #define ZMQ_SERVER_ADDRESS "ipc:///tmp/alicho_backend_server.ipc" #endif +/** + * @struct zmq_message_pack + * @brief ZeroMQ消息打包结构体 - 统一的消息封装格式 + * + * 这个结构体定义了所有通过ZeroMQ传输的消息的标准格式。它包含两个核心字段: + * - func_id: 用于消息分发和处理器路由 + * - payload: 实际的消息数据(已序列化) + * + * ## 设计理念 + * 采用"信封"模式(Envelope Pattern): + * - func_id是信封上的地址标签,告诉接收方该如何处理 + * - payload是信封里的信件内容,包含实际数据 + * + * ## 消息流程 + * ``` + * 发送端: + * 1. 原始数据对象 (T) + * 2. 序列化为二进制 -> payload (vector) + * 3. 添加func_id标识 + * 4. 整体序列化为zmq::message_t + * 5. 通过ZeroMQ发送 + * + * 接收端: + * 1. 接收zmq::message_t + * 2. 反序列化为zmq_message_pack + * 3. 根据func_id找到对应处理器 + * 4. 反序列化payload为原始对象 (T) + * 5. 调用处理器处理 + * ``` + * + * ## 类型安全机制 + * - 使用编译期类型ID(alicho_type_id_v)确保类型匹配 + * - 每种数据类型有唯一的func_id + * - 接收端可以验证消息类型是否正确 + * + * ## 序列化技术 + * 使用struct_pack库的优势: + * - 零拷贝序列化,性能高 + * - 自动处理字节序(跨平台兼容) + * - 支持嵌套结构和STL容器 + * - 提供错误检测机制 + * + * ## 性能特点 + * - 两次序列化开销:先序列化数据,再序列化消息包 + * - 额外的4字节开销(func_id) + * - 但换来了类型安全和统一的消息格式 + * + * ## 使用示例 + * ```cpp + * // 发送端 + * MyData data{...}; + * auto packed = zmq_message_pack::create(data); + * zmq::message_t msg(packed.data(), packed.size()); + * socket.send(msg, zmq::send_flags::none); + * + * // 接收端 + * zmq::message_t msg; + * socket.recv(msg, zmq::recv_flags::none); + * auto pack = zmq_message_pack::deserialize(msg); + * // 根据pack.func_id分发到对应处理器 + * ``` + * + * @note 该结构体不应该手动构造,应该使用create()和deserialize()静态方法 + * @note payload使用vector而非string,因为数据是二进制的,可能包含空字符 + */ struct zmq_message_pack { - uint32_t func_id; // 用于标识处理函数 - std::vector payload; // 实际数据负载 + /** + * @brief 函数标识符 - 用于消息类型识别和处理器路由 + * + * 这个字段存储消息类型的唯一标识符,由alicho_type_id_v生成。 + * 它是整个消息分发机制的核心。 + * + * ## 工作原理 + * - 每个可发送的数据类型T都有一个唯一的编译期ID + * - 发送时自动设置为alicho_type_id_v + * - 接收时根据这个ID找到注册的处理器 + * + * ## ID分配机制 + * alicho_type_id_v使用以下方式生成唯一ID: + * - 基于类型的哈希值 + * - 或使用预定义的类型枚举 + * - 确保不同类型有不同的ID + * + * ## 使用场景 + * ```cpp + * // 在processor中注册 + * register_processor(alicho_type_id_v, handler); + * + * // 发送时自动设置 + * zmq_message_pack::create(my_message); // func_id自动设置 + * + * // 接收时分发 + * auto pack = zmq_message_pack::deserialize(msg); + * processor.process(pack.func_id, pack.payload); + * ``` + * + * @note 使用uint32_t类型,支持约42亿种不同的消息类型 + * @note 不要手动设置这个字段,应该使用create()方法 + */ + uint32_t func_id; + /** + * @brief 消息负载 - 序列化后的实际数据 + * + * 存储经过struct_pack序列化后的二进制数据。这是消息的实际内容。 + * + * ## 数据格式 + * - 使用std::vector存储二进制数据 + * - 长度可变,取决于原始数据的大小 + * - 包含完整的序列化信息,可以还原为原始对象 + * + * ## 为什么使用vector + * - char是单字节类型,适合存储二进制数据 + * - vector提供动态大小管理 + * - 比string更适合二进制数据(不会因\0截断) + * - 容易转换为zmq::message_t + * + * ## 内存管理 + * - vector自动管理内存分配和释放 + * - 移动语义避免不必要的拷贝 + * - 可以直接传递给ZeroMQ而无需额外拷贝 + * + * ## 大小限制 + * - 理论上受限于vector的max_size() + * - 实际上受限于可用内存 + * - 建议对大数据进行分块传输 + * + * @note 这是已序列化的数据,不要直接修改 + * @note 使用deserialize()方法还原为原始对象 + */ + std::vector payload; + + /** + * @brief 创建消息包 - 将任意类型的数据打包为可传输格式 + * + * 这是一个模板静态方法,用于将任意可序列化的类型T打包为ZeroMQ消息格式。 + * 它执行两阶段序列化:先序列化数据本身,再序列化整个消息包。 + * + * ## 模板参数 + * @tparam T 要发送的数据类型,必须满足: + * - 可被struct_pack序列化 + * - 已通过alicho_type_id_v注册 + * - 在接收端有对应的处理器 + * + * ## 参数 + * @param data 要发送的数据对象(const引用,避免拷贝) + * + * ## 返回值 + * @return std::vector 完整序列化后的消息包,可直接用于ZeroMQ传输 + * + * ## 工作流程 + * 1. 创建一个临时的zmq_message_pack对象 + * 2. 设置func_id为T的类型ID(alicho_type_id_v) + * 3. 使用struct_pack序列化data,结果存入payload + * 4. 使用struct_pack序列化整个pack对象 + * 5. 返回最终的二进制数据 + * + * ## 两阶段序列化的原因 + * ``` + * 第一阶段: data (T) -> payload (vector) + * 目的: 将用户数据转换为二进制 + * + * 第二阶段: pack (zmq_message_pack) -> vector + * 目的: 将func_id和payload一起打包 + * 结果: [func_id][payload_size][payload_data] + * ``` + * + * ## 性能考虑 + * - 使用移动语义减少拷贝 + * - struct_pack提供高效的序列化 + * - 返回vector可以直接移动到zmq::message_t + * + * ## 使用示例 + * ```cpp + * struct MyMessage { + * int value; + * std::string text; + * }; + * + * MyMessage msg{42, "hello"}; + * auto packed = zmq_message_pack::create(msg); + * + * // 创建ZMQ消息并发送 + * zmq::message_t zmq_msg(packed.data(), packed.size()); + * socket.send(zmq_msg, zmq::send_flags::none); + * ``` + * + * ## 错误处理 + * - struct_pack序列化失败会抛出异常 + * - 调用者应该捕获并处理异常 + * + * @note 该方法不会抛出除序列化错误之外的异常 + * @note 返回值可以安全地移动,避免拷贝开销 + */ template static auto create(const T& data) { zmq_message_pack pack; @@ -19,11 +290,79 @@ struct zmq_message_pack { return struct_pack::serialize(pack); } + /** + * @brief 反序列化消息包 - 从ZeroMQ消息还原为消息包结构 + * + * 这个静态方法将从ZeroMQ接收到的二进制消息反序列化为zmq_message_pack对象。 + * 它是create()方法的操作,恢复出func_id和payload。 + * + * ## 参数 + * @param pack ZeroMQ消息对象,包含序列化的数据 + * + * ## 返回值 + * @return zmq_message_pack 反序列化后的消息包对象 + * + * ## 工作流程 + * 1. 从zmq::message_t获取数据指针和大小 + * 2. 调用struct_pack::deserialize解析数据 + * 3. 检查反序列化是否成功(result.has_value()) + * 4. 成功则返回解析结果,失败则抛出异常 + * + * ## 数据格式验证 + * struct_pack会自动验证: + * - 数据完整性(大小是否匹配) + * - 格式正确性(是否符合zmq_message_pack的结构) + * - 版本兼容性(如果struct_pack包含版本信息) + * + * ## 错误处理 + * 如果反序列化失败,会抛出std::runtime_error异常,包含: + * - 错误描述 + * - struct_pack返回的错误代码 + * + * 可能的失败原因: + * - 数据损坏 + * - 格式不匹配 + * - 数据不完整 + * - 版本不兼容 + * + * ## 使用示例 + * ```cpp + * // 接收ZMQ消息 + * zmq::message_t msg; + * socket.recv(msg, zmq::recv_flags::none); + * + * // 反序列化 + * try { + * auto pack = zmq_message_pack::deserialize(msg); + * + * // 现在可以使用pack.func_id和pack.payload + * processor.process(pack.func_id, pack.payload.data(), pack.payload.size()); + * } + * catch (const std::runtime_error& e) { + * // 处理反序列化错误 + * log_error("Failed to deserialize message: {}", e.what()); + * } + * ``` + * + * ## 性能考虑 + * - 返回值使用移动语义,避免拷贝 + * - struct_pack提供高效的反序列化 + * - payload的vector也通过移动返回 + * + * ## 安全性 + * - 不会访问超出message_t边界的内存 + * - 类型转换是安全的(通过const char*) + * - 异常安全,失败时不会泄露资源 + * + * @throws std::runtime_error 当反序列化失败时 + * @note 该方法不会修改输入的pack参数 + * @note 调用者必须处理可能的异常 + */ static auto deserialize(const zmq::message_t& pack) { const auto& result = struct_pack::deserialize(static_cast(pack.data()), pack.size()); if (!result.has_value()) { - throw std::runtime_error("无法反序列化 zmq_message_pack,错误代码=" + std::to_string(result.error())); + throw std::runtime_error("无法反序列化 zmq_message_pack,错误代码=" + std::to_string(static_cast(result.error()))); } return result.value(); } diff --git a/tests/process_manager/test_process_manager_manager.cpp b/tests/process_manager/test_process_manager_manager.cpp index f06b762..7e5dd20 100644 --- a/tests/process_manager/test_process_manager_manager.cpp +++ b/tests/process_manager/test_process_manager_manager.cpp @@ -453,7 +453,6 @@ TEST_F(ProcessManagerTest, DifferentSandboxTypes) { // 测试不同的沙箱类型 std::vector sandbox_types = { sandbox_type::HOST, - sandbox_type::PLUGIN, sandbox_type::CUSTOM };