Winsock多客户端服务器如何统计客户端IP的每秒数据包数
Winsock多客户端服务器客户端数据包速率统计实现
你当前的实现思路存在多处逻辑问题,无法正常运行,也达不到准确统计速率、防刷的目的:
- 逻辑阻塞:
calculate()采用无限递归+Sleep(10000)的阻塞写法,不仅会在bind()、listen()之后直接调用完全卡住流程,到不了后续连接处理、收包的逻辑,长时间运行还会因为递归没有出口触发栈溢出崩溃。 - 计数逻辑有绕过风险:固定周期全量清空计数的设计,会被跨周期刷包绕过——比如客户端在相邻两个周期的交界点前后各发900个包,2秒内发1800包已经远超100包/秒的阈值,但单周期计数都达不到1000,不会触发封禁。
- 标识维度不合理:仅用IP作为客户端标识,会误杀NAT网络下共用同一个公网IP的正常用户;同时伪代码里直接对监听socket调用
recv,根本拿不到客户端的数据包,TCP下必须通过accept返回的客户端会话socket才能收对应客户端的数据。 - 阈值笔误:你设定的阈值是100包/秒,10秒周期对应封禁阈值应为1000包,伪代码里写的10000差了10倍。
正确实现方案
核心统计逻辑
不要用固定周期全量清空的计数方式,改用滑动窗口思路做单客户端的速率统计,精度更高、难以绕过:
- 以
客户端IP+源端口的会话维度作为统计的唯一标识,避免NAT场景误杀,同时额外维护IP维度的聚合总阈值做兜底,防止单IP下大量模拟端口刷流量。 - 给每个客户端维护独立的统计结构,存储当前统计窗口的起始时间戳、窗口内累计收包数、对应的客户端会话socket。
- 每次收到完整业务包时,先取当前系统时间计算和窗口起始时间的差值:差值小于统计周期就给累计包数+1;差值超过周期就先计算上一周期的平均速率,超阈值就封禁,没超就重置窗口起始时间和计数。
- 启动独立的低优先级巡检线程,每秒遍历一次统计列表:一方面校验所有客户端的周期速率,另一方面清理长时间无数据的非活跃连接,避免内存泄漏。
- 注意TCP是流协议,存在粘包、拆包问题,不要把一次
recv调用的成功返回当成一个完整数据包,必须先完成应用层的拆包逻辑,拿到完整业务包之后再做计数累加,否则统计结果完全不准。 - 封禁触发时,除了关闭对应客户端的socket,还要把客户端IP加入黑名单,后续该IP的连接请求直接在
accept阶段拒绝,减少无效资源消耗。
参考实现伪代码
#include <winsock2.h> #include <ws2tcpip.h> #include <unordered_map> #include <unordered_set> #include <string> #pragma comment(lib, "ws2_32.lib") // 客户端统计结构 struct ClientStat { ULONG lastWindowStart; // 窗口起始时间戳,单位ms int packetCount; // 窗口内累计包数 SOCKET clientSock; // 客户端对应的会话socket }; // key用客户端IP:源端口做唯一标识,避免NAT误杀 std::unordered_map<std::string, ClientStat> clientStatMap; std::unordered_set<std::string> blacklist; const int PERIOD_MS = 10000; // 统计周期10秒 const int THRESHOLD = 1000; // 周期内最大允许包数,对应100包/秒 // 独立的巡检线程,不阻塞IO流程 DWORD WINAPI StatCheckThread(LPVOID param) { while (true) { Sleep(1000); // 每秒巡检一次 ULONG now = GetTickCount(); for (auto it = clientStatMap.begin(); it != clientStatMap.end(); ) { auto& stat = it->second; ULONG timePassed = now - stat.lastWindowStart; // 达到统计周期,校验速率 if (timePassed >= PERIOD_MS) { if (stat.packetCount > THRESHOLD) { // 触发封禁:提取IP加入黑名单,关闭连接 std::string clientIp = it->first.substr(0, it->first.find(':')); blacklist.insert(clientIp); closesocket(stat.clientSock); it = clientStatMap.erase(it); continue; } // 未超阈值,重置统计窗口 stat.lastWindowStart = now; stat.packetCount = 0; } // 清理超过30秒无数据的非活跃连接 if (timePassed > 30000) { closesocket(stat.clientSock); it = clientStatMap.erase(it); continue; } it++; } } return 0; } int main() { WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), &wsaData); SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 省略bind、listen参数配置逻辑 sockaddr_in serverAddr = {0}; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(8888); serverAddr.sin_addr.s_addr = INADDR_ANY; bind(listenSock, (sockaddr*)&serverAddr, sizeof(serverAddr)); listen(listenSock, SOMAXCONN); // 启动统计巡检线程 CreateThread(NULL, 0, StatCheckThread, NULL, 0, NULL); while (true) { sockaddr_in clientAddr = {0}; int addrLen = sizeof(clientAddr); // 生产环境建议用IOCP/WSAPoll等异步模型处理多连接,此处用阻塞accept简化示例 SOCKET clientSock = accept(listenSock, (sockaddr*)&clientAddr, &addrLen); char ipBuf[20] = {0}; inet_ntop(AF_INET, &clientAddr.sin_addr, ipBuf, sizeof(ipBuf)); std::string clientKey = std::string(ipBuf) + ":" + std::to_string(ntohs(clientAddr.sin_port)); // 黑名单IP直接拒绝连接 if (blacklist.count(ipBuf)) { closesocket(clientSock); continue; } // 初始化新客户端统计信息 clientStatMap[clientKey] = {GetTickCount(), 0, clientSock}; char buff[2048] = {0}; int recvLen = recv(clientSock, buff, sizeof(buff), 0); if (recvLen <= 0) { closesocket(clientSock); clientStatMap.erase(clientKey); continue; } // 注意:生产环境必须在此处实现TCP粘包/拆包逻辑,拿到完整业务包后再累加计数 clientStatMap[clientKey].packetCount++; } closesocket(listenSock); WSACleanup(); return 0; }
内容的提问来源于stack exchange,提问作者Chirag Arora
相关产品推荐
相关产品推荐

