Files
blackbird/libmsg/inc/us/MsgServer.hpp
qing b2ecb8e12b fix(libmsg): close fd-reuse race silently killing short-lived connections
worker 收 EOF 失败后过早清除 busy 标记,事件循环对同一 fd 重复投递任务,
迟到的失败标记经 fd 复用落在新连接上被 _reap_failed 误杀(客户端收到空 EOF)。
失败路径改由 _reap_failed 统一清除 busy 并关闭;_handle_client 增加防重复投递守卫。
memdb 短连接压力测试错误率从 5-9% 降至 0。
2026-08-17 14:29:37 +00:00

657 lines
27 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#pragma once
/** MsgServer — 基于 pymsg 协议的多客户端 TCP 服务器(C++ 版,对应 Python 版 pymsg.MsgServer)
*
* 提供 select-based 事件循环 + 可选线程池并发处理,支持 reply()/broadcast() 线程安全发送。
*
* 设计要点(与 Python 版对齐):
* - select 事件循环,支持多客户端并发连接
* - workers=0 同步模式(回调在主线程执行),workers>0 线程池并发模式
* - 同一连接消息严格串行:回调执行期间暂停该连接 select 监听
* - reply()/broadcast() 经每连接发送锁串行化,防止帧交错
* - max_connections 限制活跃连接数,防止连接耗尽攻击
* - max_queue_size 限制线程池任务队列深度,慢 handler 时断开连接防 OOM
* - deadline 模式 recv 超时,防 Slowloris 攻击
* - stop() 可从任意线程(包括回调内部)安全调用,优雅关闭
* - worker 线程出错仅标记 failed,主循环统一清理连接,避免 fd 并发操作
*
* Usage:
* #include <us/MsgServer.hpp>
*
* pmc::net::MsgServer server("0.0.0.0", 8003, {.workers=4});
* server.start([&](int fd, const std::string& addr, const std::string& msg) {
* server.reply(fd, "echo: " + msg); // 回复发送者
* server.broadcast(addr + ": " + msg, fd); // 广播给其他人
* });
*/
#include <us/Msg_Basic.hpp>
#include <us/Msg.hpp>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <cstring>
#include <deque>
#include <functional>
#include <memory>
#include <mutex>
#include <queue>
#include <string>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <sys/types.h>
#include <unistd.h>
#ifndef MSG_NOSIGNAL
#define MSG_NOSIGNAL 0
#endif
namespace pmc {
namespace net {
// ============================================================================
// MsgServer 配置
// ============================================================================
struct MsgServerConfig {
int workers = 0; ///< 线程池大小;0 = 同步模式(回调在主线程执行)
double recv_timeout = 10.0; ///< 接收单条消息的总超时(秒),防 Slowloris
int max_connections = 1000; ///< 最大并发连接数,超过则拒绝新连接
int max_queue_size = 1000; ///< 线程池任务队列上限(workers>0 时),满时断开连接
int listen_backlog = 10; ///< listen() backlog
double poll_timeout = 0.1; ///< select 轮询超时(秒),影响关闭响应速度
};
// ============================================================================
// ConnectionLock — 每连接发送锁 + 活跃标志
// ============================================================================
/// 每连接的发送锁和活跃标志。
///
/// [libmsg-audit P0-1 / P0-2 / P1-3] 组合 send_mtx(互斥锁)+ active(原子标志)彻底消除
/// CWE-362 竞态:
/// - 第一轮 P0-1:单客户端移除路径先持 send_mtx 再 close,reply/broadcast 先获取
/// send_mtx 则 _remove_client 等待,防止 fd 关闭后被 OS 复用导致数据发往错误客户端。
/// - 第二轮 P0-2:批量关闭路径 _close_all_clients 同样持 send_mtx 后 shutdown+close,
/// 修复第一轮只覆盖单路径的竞态回归。
/// - 第四轮 P1-3:active 标志堵住反向竞态——_remove_client 先获取锁并 close(fd) 后,
/// reply/broadcast 后获取锁时通过 active==false 得知 fd 已关闭,立即放弃发送。
struct ConnectionLock {
std::mutex send_mtx;
std::atomic<bool> active{true};
};
// ============================================================================
// MsgServer
// ============================================================================
/// 基于 pymsg 长度前缀协议的多客户端 TCP 服务器。
class MsgServer {
public:
/// 消息回调签名:(客户端 fd, 客户端地址 "ip:port", 消息内容)
using MessageCallback = std::function<void(int sockfd, const std::string& addr, const std::string& message)>;
explicit MsgServer(const std::string& addr, int port, MsgServerConfig config = {})
: _addr(addr), _port(port), _config(config), _server_fd(-1), _running(false) {
// [libmsg-audit P2-3 / P2-5] 全量边界校验:拒绝非法 port / workers / recv_timeout /
// max_connections / max_queue_size / listen_backlog / poll_timeout。防止 htons 截断产生
// 错误端口、负值导致所有连接被拒或队列无法提交、极大值(>300s)绕过 Slowloris 防护、
// poll_timeout=0 导致 select 忙轮询 CPU 100%。
detail::validatePort(port);
if (config.workers < 0 || config.workers > 256) {
throw MsgException("invalid workers: " + std::to_string(config.workers) + " (must be in [0, 256])");
}
detail::validateTimeout(config.recv_timeout);
if (config.max_connections < 1 || config.max_connections > 100000) {
throw MsgException("invalid max_connections: " + std::to_string(config.max_connections));
}
if (config.max_queue_size < 1 || config.max_queue_size > 1000000) {
throw MsgException("invalid max_queue_size: " + std::to_string(config.max_queue_size));
}
if (config.listen_backlog < 1 || config.listen_backlog > 4096) {
throw MsgException("invalid listen_backlog: " + std::to_string(config.listen_backlog));
}
if (config.poll_timeout <= 0 || config.poll_timeout > 5.0) {
throw MsgException("invalid poll_timeout: " + std::to_string(config.poll_timeout));
}
}
~MsgServer() { stop(); }
MsgServer(const MsgServer&) = delete;
MsgServer& operator=(const MsgServer&) = delete;
// ---- 启动(阻塞当前线程)------------------------------------------------
/** 启动服务器,阻塞当前线程直到 stop() 被调用或发生致命错误。
*
* @param on_message 消息回调,签名 void(int fd, const string& addr, const string& msg)。
* - 同步模式(workers=0):回调在主线程执行,同一连接消息天然串行。
* - 线程池模式(workers>0):回调提交到线程池并发执行;同一连接
* 上的消息仍严格串行(回调完成前该连接暂停 select 监听)。
* 回调内可安全调用 reply()/broadcast()/stop()。
*/
void start(MessageCallback on_message) {
// [libmsg-audit P2-8] 空回调会在 worker 线程 / RpcServer 分发中抛 std::bad_function_call,
// 提前校验并给出明确错误,避免行为不符合预期且错误信息含糊。
if (!on_message) {
throw MsgException("MsgServer::start(): on_message callback must not be null");
}
_on_message = std::move(on_message);
_running = true;
// 创建监听 socket
_server_fd.store(::socket(AF_INET, SOCK_STREAM, 0));
if (_server_fd.load() < 0) {
throw MsgConnectionException(std::string("socket() failed: ") + ::strerror(errno));
}
int optval = 1;
::setsockopt(_server_fd.load(), SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
struct sockaddr_in serv_addr {};
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(static_cast<uint16_t>(_port));
if (::inet_pton(AF_INET, _addr.c_str(), &serv_addr.sin_addr) <= 0) {
int fd = _server_fd.exchange(-1);
if (fd >= 0) ::close(fd);
throw MsgConnectionException("invalid bind address: " + _addr);
}
if (::bind(_server_fd.load(), reinterpret_cast<struct sockaddr*>(&serv_addr), sizeof(serv_addr)) < 0) {
int saved_errno = errno;
int fd = _server_fd.exchange(-1);
if (fd >= 0) ::close(fd);
throw MsgConnectionException(std::string("bind() failed: ") + ::strerror(saved_errno));
}
if (::listen(_server_fd.load(), _config.listen_backlog) < 0) {
int saved_errno = errno;
int fd = _server_fd.exchange(-1);
if (fd >= 0) ::close(fd);
throw MsgConnectionException(std::string("listen() failed: ") + ::strerror(saved_errno));
}
// 启动线程池(如果配置了 workers)
std::vector<std::thread> pool_threads;
if (_config.workers > 0) {
_pool_running = true;
pool_threads.reserve(_config.workers);
for (int i = 0; i < _config.workers; ++i) {
pool_threads.emplace_back(&MsgServer::_worker_loop, this);
}
}
// 主事件循环
try {
_event_loop();
} catch (...) {
// 确保线程池和连接被清理
_running = false;
_shutdown_pool(pool_threads);
_close_all_clients();
int fd = _server_fd.exchange(-1);
if (fd >= 0) { ::shutdown(fd, SHUT_RDWR); ::close(fd); }
throw;
}
_shutdown_pool(pool_threads);
_close_all_clients();
int fd = _server_fd.exchange(-1);
if (fd >= 0) { ::shutdown(fd, SHUT_RDWR); ::close(fd); }
}
// ---- 停止 --------------------------------------------------------------
/// 停止服务器(可从任意线程调用,包括回调内部)。
/// 关闭监听 socket 以解除 select 阻塞,事件循环退出前清理所有连接。
void stop() noexcept {
bool expected = true;
if (!_running.compare_exchange_strong(expected, false)) return;
int fd = _server_fd.exchange(-1);
if (fd >= 0) {
::shutdown(fd, SHUT_RDWR);
::close(fd);
}
}
// ---- 发送(线程安全)----------------------------------------------------
/** 向指定客户端发送一条应答消息(线程安全)。
*
* 经每连接发送锁串行化,workers>0 时可在回调线程安全调用。
* 如果连接已断开则静默忽略(fail-safe,不抛异常)。
*/
void reply(int sockfd, const std::string& msg) {
std::shared_ptr<ConnectionLock> conn_lock;
{
std::lock_guard<std::mutex> lock(_clients_mtx);
auto it = _send_locks.find(sockfd);
if (it == _send_locks.end()) return; // 连接已断开
conn_lock = it->second;
}
std::lock_guard<std::mutex> lock(conn_lock->send_mtx);
// [libmsg-audit P1-3] 检查 active:防止 _remove_client 先获取 send_mtx 并 close(fd) 后,
// reply 向已关闭/被 OS 复用的 fd 发送数据(CWE-362 反向竞态)。
if (!conn_lock->active.load()) return;
try {
pmsg_send(sockfd, msg);
} catch (const MsgException&) {
// [libmsg-audit P2-6] 发送失败(连接断开或发送超时),标记为 failed 由主循环清理。
// 与 broadcast() 保持一致,避免连接残留不清理(原 reply 静默忽略,连接滞留)。
std::lock_guard<std::mutex> flk(_failed_mtx);
_failed.insert(sockfd);
}
}
/** 向所有已连接客户端广播消息(线程安全)。
*
* @param msg 要广播的消息。
* @param except_fd 排除的 fd(通常是发送者自己),默认 -1 表示不排除。
*/
void broadcast(const std::string& msg, int except_fd = -1) {
// 先拷贝一份 fd + conn_lock 列表,避免持锁期间发送
std::vector<std::pair<int, std::shared_ptr<ConnectionLock>>> targets;
{
std::lock_guard<std::mutex> lock(_clients_mtx);
targets.reserve(_clients.size());
for (auto& [fd, addr] : _clients) {
if (fd == except_fd) continue;
auto it = _send_locks.find(fd);
if (it != _send_locks.end()) {
targets.emplace_back(fd, it->second);
}
}
}
for (auto& [fd, conn_lock] : targets) {
std::lock_guard<std::mutex> slock(conn_lock->send_mtx);
// [libmsg-audit P1-3] 检查 active:防止 _remove_client 先 close(fd) 后向复用 fd 发送(CWE-362 反向竞态)
if (!conn_lock->active.load()) continue;
try {
pmsg_send(fd, msg);
} catch (const MsgException&) {
// [libmsg-audit P2-6] 发送失败,标记为 failed 由主循环统一清理
std::lock_guard<std::mutex> flk(_failed_mtx);
_failed.insert(fd);
}
}
}
// ---- 状态查询 ----------------------------------------------------------
/// 当前已连接客户端数量。
size_t client_count() const {
std::lock_guard<std::mutex> lock(_clients_mtx);
return _clients.size();
}
private:
// ---- 内部:线程池 -------------------------------------------------------
struct Task {
int fd;
std::string addr;
std::string message;
bool needs_recv = false; // true: worker 负责 pmsg_recv;false: 消息已就绪
};
void _worker_loop() {
while (true) {
Task task;
bool has_task = false;
{
std::unique_lock<std::mutex> lock(_pool_mtx);
_pool_cv.wait(lock, [this] { return !_pool_running || !_pool_queue.empty(); });
if (!_pool_queue.empty()) {
task = std::move(_pool_queue.front());
_pool_queue.pop();
has_task = true;
} else if (!_pool_running) {
return; // 队列空且已停止,退出
}
}
if (!has_task) continue;
// [libmsg-audit P1-1] workers > 0 模式下,消息接收在 worker 线程中完成(防事件循环阻塞 CWE-400),
// 避免攻击者以 max_connections 个半开连接阻塞事件循环长达数小时(Slowloris 变体)。
if (task.needs_recv) {
try {
task.message = pmsg_recv(task.fd, _config.recv_timeout);
} catch (const MsgException&) {
// 不在此清除 busy:fd 保持 busy 直到 _reap_failed 关闭连接。
// 旧实现提前清除 busy,EOF 可读性会让事件循环为同一 fd 重复
// 投递任务,迟到的失败标记经 fd 复用可能落在新连接上误杀之。
std::lock_guard<std::mutex> flk(_failed_mtx);
_failed.insert(task.fd);
continue;
}
}
// 执行回调
bool failed = false;
try {
_on_message(task.fd, task.addr, std::move(task.message));
} catch (const MsgConnectionException&) {
failed = true;
} catch (const std::exception& e) {
std::fprintf(stderr, "[MsgServer] callback error on fd=%d: %s\n",
task.fd, e.what());
std::fflush(stderr);
failed = true;
} catch (...) {
failed = true;
}
if (failed) {
// 失败连接交由 _reap_failed 统一清除 busy 并关闭,
// 不提前恢复 select 监听(同 recv 失败路径,防 fd 复用竞态)。
std::lock_guard<std::mutex> flk(_failed_mtx);
_failed.insert(task.fd);
} else {
// 清除 busy 标记,恢复 select 监听
std::lock_guard<std::mutex> lock(_busy_mtx);
_busy.erase(task.fd);
}
}
}
void _shutdown_pool(std::vector<std::thread>& pool_threads) {
// 停止接收新任务,唤醒所有 worker 排空队列后退出
_pool_running = false;
_pool_cv.notify_all();
for (auto& t : pool_threads) {
if (t.joinable()) t.join();
}
pool_threads.clear();
// worker 已排空队列,此时 busy 集合中的任务都已完成
// 任何仍标记为 failed 的连接由 _close_all_clients 统一清理
}
// ---- 内部:主事件循环 ---------------------------------------------------
void _event_loop() {
while (_running) {
// 清理 failed 连接
_reap_failed();
// 构建 fd_set:server_fd + 非 busy 的客户端 fd
fd_set readfds, exceptfds;
FD_ZERO(&readfds);
FD_ZERO(&exceptfds);
int maxfd = -1;
int srv_fd = _server_fd.load();
if (srv_fd >= 0) {
FD_SET(srv_fd, &readfds);
maxfd = srv_fd;
}
std::vector<int> watched_clients;
{
std::lock_guard<std::mutex> clk(_clients_mtx);
std::lock_guard<std::mutex> blk(_busy_mtx);
watched_clients.reserve(_clients.size());
for (auto& [fd, addr] : _clients) {
if (_busy.count(fd)) continue; // 回调处理中,暂停监听
FD_SET(fd, &readfds);
FD_SET(fd, &exceptfds);
if (fd > maxfd) maxfd = fd;
watched_clients.push_back(fd);
}
}
if (maxfd < 0) {
// 没有任何 fd 可监听(server_fd 已关闭且无客户端),退出
break;
}
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = static_cast<long>(_config.poll_timeout * 1'000'000.0);
int ready = ::select(maxfd + 1, &readfds, nullptr, &exceptfds, &tv);
if (ready < 0) {
if (errno == EINTR) continue;
break; // EBADF 等致命错误
}
if (ready == 0) continue;
// 处理异常 fd
for (int fd : watched_clients) {
if (FD_ISSET(fd, &exceptfds)) {
_remove_client(fd);
}
}
// 处理新连接(重新加载 srv_fd,防止 stop() 期间关闭)
srv_fd = _server_fd.load();
if (srv_fd >= 0 && FD_ISSET(srv_fd, &readfds)) {
_accept_client();
}
// 处理客户端消息
for (int fd : watched_clients) {
if (FD_ISSET(fd, &readfds)) {
// 检查 fd 是否仍在客户端列表中(可能已被异常处理移除)
{
std::lock_guard<std::mutex> lock(_clients_mtx);
if (!_clients.count(fd)) continue;
}
_handle_client(fd);
}
}
}
}
void _accept_client() {
struct sockaddr_in client_addr {};
socklen_t addrlen = sizeof(client_addr);
int srv_fd = _server_fd.load();
int client_fd = ::accept(srv_fd, reinterpret_cast<struct sockaddr*>(&client_addr), &addrlen);
if (client_fd < 0) {
// EINVAL/EBADF: server_fd 已被 stop() 关闭;其他错误忽略
return;
}
if (client_fd >= FD_SETSIZE) { ::close(client_fd); return; }
// [libmsg-audit P3-3] 禁用 Nagle 算法和设置发送超时(在加入 _clients 之前,确保连接配置完整),
// 先完成所有 socket 配置再暴露给其他线程,避免半配置连接被回复。
int flag = 1;
::setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
// [libmsg-audit P1-2] 设置发送超时:防止慢客户端不读数据导致发送缓冲区填满,
// 使 reply()/broadcast() 的 send() 永久阻塞(拒绝服务,CWE-400)。
// 超时后 sendAll() 抛 MsgTimeoutException,被 reply/broadcast 捕获并标记 failed 清理。
struct timeval sndtv {};
constexpr double kServerSendTimeout = 5.0; // 秒
sndtv.tv_sec = static_cast<time_t>(kServerSendTimeout);
sndtv.tv_usec = static_cast<long>(
(kServerSendTimeout - static_cast<double>(sndtv.tv_sec)) * 1'000'000.0);
::setsockopt(client_fd, SOL_SOCKET, SO_SNDTIMEO, &sndtv, sizeof(sndtv));
// 检查连接数上限并注册
{
std::lock_guard<std::mutex> lock(_clients_mtx);
if (static_cast<int>(_clients.size()) >= _config.max_connections) {
::close(client_fd);
return;
}
char ip_str[INET_ADDRSTRLEN];
::inet_ntop(AF_INET, &client_addr.sin_addr, ip_str, sizeof(ip_str));
std::string addr = std::string(ip_str) + ":" + std::to_string(ntohs(client_addr.sin_port));
_clients[client_fd] = addr;
_send_locks[client_fd] = std::make_shared<ConnectionLock>();
}
}
void _handle_client(int fd) {
std::string addr;
{
std::lock_guard<std::mutex> lock(_clients_mtx);
auto it = _clients.find(fd);
if (it == _clients.end()) return;
addr = it->second;
}
if (_config.workers == 0) {
// 同步模式:在主线程接收并执行回调
std::string message;
try {
message = pmsg_recv(fd, _config.recv_timeout);
} catch (const MsgException&) {
_remove_client(fd);
return;
}
try {
_on_message(fd, addr, std::move(message));
} catch (const MsgConnectionException&) {
_remove_client(fd);
} catch (const std::exception& e) {
std::fprintf(stderr, "[MsgServer] callback error on fd=%d: %s\n", fd, e.what());
std::fflush(stderr);
_remove_client(fd);
}
return;
}
// [libmsg-audit P1-1] 线程池模式:标记 busy,提交任务(由 worker 负责 recv + callback,
// 事件循环线程不阻塞接收,防 Slowloris DoS CWE-400)
{
std::lock_guard<std::mutex> blk(_busy_mtx);
if (_busy.count(fd)) return; // 已有在途任务,防同一 fd 重复投递(fd 复用竞态)
_busy.insert(fd);
}
{
std::lock_guard<std::mutex> plk(_pool_mtx);
if (static_cast<int>(_pool_queue.size()) >= _config.max_queue_size) {
// 队列已满:拒绝并断开连接(防 OOM)
std::lock_guard<std::mutex> blk(_busy_mtx);
_busy.erase(fd);
_remove_client(fd);
return;
}
_pool_queue.push(Task{fd, std::move(addr), {}, true});
}
_pool_cv.notify_one();
}
void _reap_failed() {
std::unordered_set<int> failed;
{
std::lock_guard<std::mutex> flk(_failed_mtx);
failed.swap(_failed);
}
for (int fd : failed) {
{
std::lock_guard<std::mutex> blk(_busy_mtx);
_busy.erase(fd);
}
_remove_client(fd);
}
}
void _remove_client(int fd) {
std::shared_ptr<ConnectionLock> conn_lock;
{
std::lock_guard<std::mutex> clk(_clients_mtx);
if (_clients.erase(fd) == 0) return; // 已被移除,防止 double-close
auto it = _send_locks.find(fd);
if (it != _send_locks.end()) {
conn_lock = it->second;
_send_locks.erase(it);
}
}
// [libmsg-audit P0-1 / P1-3] 持有 send_mtx 后设置 active=false 再 close(fd):
// 1. 防止 broadcast/reply 在 fd 关闭后向 OS 复用的新连接发送数据(CWE-362,第一轮 P0-1)
// 2. active=false 让 reply/broadcast 在获取锁后得知 fd 已关闭,放弃发送(第四轮 P1-3 反向竞态)
if (conn_lock) {
std::lock_guard<std::mutex> slk(conn_lock->send_mtx);
conn_lock->active.store(false);
::shutdown(fd, SHUT_RDWR);
::close(fd);
} else {
::shutdown(fd, SHUT_RDWR);
::close(fd);
}
}
void _close_all_clients() {
std::vector<std::pair<int, std::shared_ptr<ConnectionLock>>> to_close;
{
std::lock_guard<std::mutex> clk(_clients_mtx);
to_close.reserve(_clients.size());
for (auto& [fd, addr] : _clients) {
auto it = _send_locks.find(fd);
if (it != _send_locks.end()) {
to_close.emplace_back(fd, it->second);
} else {
to_close.emplace_back(fd, nullptr);
}
}
_clients.clear();
_send_locks.clear();
}
// [libmsg-audit P0-2 / P1-3] 持有每个连接的 send_mtx 后设置 active=false 再 close(fd):
// 防止 stop()/析构时 reply/broadcast 正在发送,导致数据发到 OS 复用的新连接(CWE-362)。
// 第二轮 P0-2 修复批量关闭路径的竞态回归,第四轮 P1-3 的 active 标志堵住反向竞态。
for (auto& [fd, conn_lock] : to_close) {
if (conn_lock) {
std::lock_guard<std::mutex> slk(conn_lock->send_mtx);
conn_lock->active.store(false);
::shutdown(fd, SHUT_RDWR);
::close(fd);
} else {
::shutdown(fd, SHUT_RDWR);
::close(fd);
}
}
std::lock_guard<std::mutex> blk(_busy_mtx);
_busy.clear();
std::lock_guard<std::mutex> flk(_failed_mtx);
_failed.clear();
}
// ---- 成员 --------------------------------------------------------------
std::string _addr;
int _port;
MsgServerConfig _config;
std::atomic<int> _server_fd; ///< [libmsg-audit P2-7] 监听 socket fd(atomic 防止 stop() 与 _event_loop 数据竞争 UB,CWE-362/662)
std::atomic<bool> _running;
MessageCallback _on_message;
// 客户端管理
mutable std::mutex _clients_mtx;
std::unordered_map<int, std::string> _clients; ///< fd -> "ip:port"
std::unordered_map<int, std::shared_ptr<ConnectionLock>> _send_locks; ///< fd -> 发送锁+活跃标志
// 同连接串行
std::mutex _busy_mtx;
std::unordered_set<int> _busy; ///< 回调执行中的 fd
// 失败连接(worker 线程标记,主循环清理)
std::mutex _failed_mtx;
std::unordered_set<int> _failed;
// 线程池
std::atomic<bool> _pool_running{false};
std::mutex _pool_mtx;
std::condition_variable _pool_cv;
std::queue<Task> _pool_queue;
};
} // namespace net
} // namespace pmc