注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。
代码在最后面,大家可以拷贝下来运行一下,按着我的步骤测试
希望大家做完这个百万并发服务器实验,能够明白影响CPU、内存变化的因素,这样我们才能够依此优化我们的代码和参数配置!!!
一、运行代码测试
运行4台虚拟机:1台做服务器、3台做客户端。3台客户端同时向服务器请求连接。
该代码测试需要花费11-12分钟(根据测试可知每0.7s建立1000个连接可知)
htop实时查看CPU、内存的大小。
1.第一次记录(连接请求前)

初始内存:1.58G
2.之后每隔20万个连接记录一次

图1(20万连接)

图2(40万连接)

图3(60万连接)

图4(80万连接)

图5(100万连接)
3. 此时3个客户端都已到达设置的最大连接数(34000条连接),服务器端有1020000个连接:
3.1 我们快速断开3个客户端程序,同一时间关闭100万条连接。观察CPU突然暴增!!!!

3.2 过一会后回复正常

3.3 .此时关闭服务器,发现CPU暴增!!!

二、 数据分析
1. 百万服务器内存变化量
0-20万:0.56GB
20万-40万:0.56GB
40万-60万:0.56GB
60万-80万:0.56GB
80万-100万:0.54GB
根据我的参数配置解释
| 内存构成(每 20 万连接) |
– 单连接缓冲区:2KB / 连接 → 20 万连接共 390.625MB(1KB 发送 + 1KB 接收,取最小值) – TCB 控制块:每 20 万连接约 60-100MB(百万连接 300-500MB 分摊) – 连接跟踪表:每 20 万连接约 60MB(百万连接 300MB 分摊)→ 理论总和:约 510-550MB(与实际 0.56GB 接近) |
| 0-80 万连接内存增量稳定(~0.56GB)的原因 |
内核内存分配策略稳定: – 未触发关键内存限制(如tcp_mem、nf_conntrack上限),新连接按配置的最小缓冲区(2KB)+ 完整 TCB + 跟踪表分配内存- 差异源于内核内存碎片、TCB 实际大小波动,整体增量稳定。 |
| 80-100 万连接增量略降(0.54GB)的原因 |
内核动态调节: 1. 0-80万连接所占内存有0.56*4 = 2.24G 2. 80万-100万 内存占用量 2.24G – 2.78G (逐步接近tcp_mem的上限3GB) 3. 系统感知内存压力,优化新连接内存分配(如复用缓冲区、限制非必要开销),所以呈现为0.54G。 |
| 总结 | 内存增量稳定源于 “单连接最小资源固定 + 前期无内存限制触发”;末尾略降因内核感知压力,动态优化新连接内存分配,减少额外开销。 |
2. 三个客户端同时按“CTRL C”强制关闭,服务器端CPU突然飙升
| CPU 飙升核心原因 |
百万级连接突然断开时,内核需短时间内完成: 1.客户端 CTRL C终止程序,触发SIGINT(信号 2) -若客户端程序通过信号处理函数(如 signal(SIGINT, handler))捕获 SIGINT,且handle执行close则: 服务器处理:优雅的TCP 四次挥手处理; -若没有(比如本程序),则: 客户端TCP 连接会通过内核向服务器发送 RST 报文强制断开。 2. struct sock(TCB)及 tcp_wmem/tcp_rmem 缓冲区的资源回收; 3. nf_conntrack 跟踪表百万条目的哈希表操作(查找、删除、锁竞争)。 |
| “突然断开” 加剧压力的因素 |
1. 并发处理风暴:多客户端同时断连,触发网卡中断、软中断(如 ksoftirqd),CPU 核心被占满; 2. 锁竞争:TCP 全局锁、nf_conntrack 哈希表锁等全局锁竞争激烈,上下文切换开销大; 3. 处理TCP断开连接的系列操作(资源回收) |
| 配置与监控验证 |
1. nf_conntrack_max=1048576:跟踪表百万条目删除的哈希表操作是 CPU 飙升主因之一;2. htop 显示 new_server 进程 CPU 占比高:用户态 close() 系统调用陷入内核态,内核态网络栈、连接跟踪的开销更隐蔽(需 top -H/perf 分析); 3. 内存充足(4.42GB/7.72GB)但 CPU 瓶颈:资源回收为 CPU 密集型操作。 |
| 优化建议 |
1. 连接跟踪: – 无需跟踪时,设 net.netfilter.nf_conntrack_max=0 禁用; – 需保留时,调小 nf_conntrack_timeout_tcp_established 缩短超时; 2. TCP 关闭: – 缩短 tcp_fin_timeout(如 60→30 秒)减少 FIN_WAIT_2 保留; – 谨慎启用 tcp_tw_reuse/tcp_tw_recycle 加速 TIME_WAIT 回收; 3. 断开速率:客户端分批关闭(如每批 1 万连接,间隔 1 秒),分散内核压力。 |
| 总结 | 百万连接突然断开时,内核短时间内高强度执行连接回收操作导致 CPU 飙升;优化核心是减少并发回收的资源量或分散回收时间。 |
总的来说:
1.同时要处理100万个TCP断开,内核同时回收大量的连接资源
2.TCP 全局锁、nf_conntrack 哈希表锁等全局锁竞争激烈,上下文切换开销大;
3.我们再制造大型服务器时一定要对大量用户同时断开连接导致CPU飙升这一情况做出优化!!!
3.服务器强制关闭,CPU飙升原因
当服务器进程被强制终止(如 kill -2)且未主动调用close()时。
| 1. 标记连接为 “孤儿连接” | 接管进程终止后残留的 TCP socket,脱离原进程控制,转为内核维护 | 进程终止后,其打开的 socket 从文件描述符表移除,不再与进程关联,仅由内核管理 | 暂未感知,连接状态暂时不变 |
| 2. 发送 RST 报文强制断开 | 向客户端发送 RST(复位)报文,触发强制关闭 | 跳过正常四次挥手流程,无需等待 FIN/ACK 交互及超时,快速终止连接 | 收到 RST,返回 “Connection reset by peer” 错误 |
| 3. 回收 socket 资源 | 释放连接关联的所有资源:struct sock(TCB 控制块)、tcp_wmem/tcp_rmem 缓冲区 | 清理连接的 TCP 状态(序列号、窗口等)及内存,完成资源回收 | 连接彻底断开,无法再进行数据通信 |
以上这些都会使CPU增加。至于为什么客户端明明都已经断开了,服务器CPU突然飙升,然后又逐步下降,接着再关闭服务器端,服务器端CPU又突然飙射的原因:
我的看法是,服务器端强制关闭,会更快的触发TCP连接断开的信号,资源回收的更加迅速,所以有种CPU又飙升的现象。
4. 对比 “服务器主动 close() 触发四次挥手”,强制中断的 CPU 压力更小,核心差异:
| 状态维护 | 需等待 FIN_WAIT_1/FIN_WAIT_2 状态(含超时) | 无状态等待,发送 RST 后直接清理资源 |
| 报文交互 | 至少 2-4 个报文(FIN/ACK/FIN/ACK) | 仅 1 个 RST 报文,无后续交互 |
| 定时器开销 | 需维护重传定时器、状态超时定时器 | 无定时器开销,资源回收更直接 |
简言之:RST 是 “暴力断开”,跳过了四次挥手的状态等待和多轮报文交互,内核操作更 “轻量化”,因此 CPU 峰值通常更低。
(大家不信的话可以通过修改代码来对比观察,本代码是强制中断,大家可用编写一个信号捕捉函数handle捕获SIGINT为2的信号实现正常的close。)
三、CPU和内存的差异
1.本质区别:角色与功能
| 核心功能 | 执行指令运算(如算术逻辑、控制流),是系统的 “运算大脑”。 | 临时存储数据(如运行的程序、变量、缓存),是 CPU 的 “临时工作台”。 |
| 速度与容量 | 速度极快(GHz 级时钟),但容量小(核心缓存仅 MB 级)。 | 速度慢于 CPU(ns 级延迟),但容量大(GB 级,如 16GB/32GB 内存)。 |
| 数据流向 | 从内存中读取指令 / 数据,运算后写回内存或寄存器。 | 从硬盘加载数据,供 CPU 直接访问;断电后数据丢失。 |
| 性能瓶颈 | 受限于指令执行效率(如主频、核心数、缓存命中率)。 | 受限于数据读写延迟(如内存带宽、是否触发 Swap)。 |
2.CPU 使用率变化的核心因素
CPU 使用率反映其运算资源的繁忙程度,波动由 “指令执行的密集度” 决定:
| 进程的运算负载 | 高计算任务(视频渲染、3D 建模、数据分析等) | 触发大量算术逻辑运算(ALU 操作),直接推高 CPU 使用率。 |
| 多线程程序(如并发服务器) | 通过多核心并行运算,可能导致 CPU 核心占满。 | |
| 系统级任务与中断 | 内核态操作(网络软中断、磁盘 I/O 中断等) | 如 ksoftirqd 处理 TCP 包、irqbalance 调度 I/O,直接占用 CPU。 |
| 后台服务(杀毒软件扫描、系统更新等) | 周期性任务运行时,导致 CPU 使用率波动。 | |
| 硬件调度与竞争 | 锁竞争(多线程全局锁,如 nf_conntrack 哈希表锁) | 导致 CPU 上下文切换开销剧增,推高使用率。 |
| 缓存未命中(CPU 缓存 L1/L2/L3 未命中) | 需从内存加载数据,触发 “内存墙”,间接降低 CPU 效率(表现为使用率虚高或运算停滞)。 |
结合”强制关闭客户端,100万个连接同时断开,服务器CPU飙升“来理解:
| 核心主因 | 60%+ | 1. struct sock(TCB)批量释放2. TCP 缓冲区(tcp_wmem/tcp_rmem)回收 |
1. 清理内核态数据结构(TCP 状态机节点、定时器),调用 slab allocator 释放内存时触发全局锁竞争(如 TCP 全局锁、内存页锁),CPU 大量时间消耗在 “等待锁→执行释放” 循环; 2. 检查缓冲区残留数据(丢弃 / 标记无效),遍历内存页链表回收至 page cache,百万级操作放大算力消耗 |
| 关键辅助因素 | 20%-30% | nf_conntrack 表项批量清理(仅启用连接跟踪时) |
1. 根据 “源 IP + 源端口 + 目的 IP + 目的端口” 计算哈希值定位表项,加哈希表全局锁避免并发冲突,多核场景下锁等待时间显著增加; 2. 批量删除表项并更新链表、释放内存,百万级表项的遍历查找进一步消耗 CPU |
| 次要触发因素 | 10%-20% | 批量 RST 报文处理(客户端 Ctrl+C 强制关闭场景) |
1. 网卡接收 RST 后触发硬中断,内核将报文入队; 2. 软中断进程(ksoftirqd)解析报文、标记 TCP 连接 “异常关闭”,短时间百万级报文形成 “软中断风暴”,占用大量 CPU 核心(尤其网卡中断绑定核心) |
备注:
3.内存使用率变化的核心因素
| 应用程序的内存分配 | 大对象创建(加载大文件、初始化大数组等) | 直接占用内存,导致内存使用率上升。 |
| 内存泄漏(未释放无用内存,如未关闭连接、未清空缓存) | 无用内存持续堆积,导致内存使用率持续增长。 | |
| 系统缓存机制 | 文件系统缓存(如 Linux 的 page cache) | 频繁读写文件时,内核用内存缓存数据以提升性能,直接推高内存占用。 |
| 网络缓存(TCP 接收缓冲区 tcp_rmem、发送缓冲区 tcp_wmem) | 随连接数和数据量动态分配内存,连接增多或数据量大时,内存使用率上升。 | |
| 高并发与资源回收 | 百万连接场景 | 每个 TCP 连接占用 struct sock(TCB 控制块)和缓冲区内存,连接建立时内存飙升;断开时内核回收资源,内存短暂波动。 |
| Swap 交换 | 物理内存不足时,数据暂存到硬盘,释放物理内存(使用率下降),但会导致性能骤降。 |
四、代码样例:
代码设计看不懂的可以去看我之前的文章:这里简要说明一下
Linux C/C++ 学习日记(11)select/poll/epoll 实现网络io的多路复用-CSDN博客
Linux C/C++ 学习日记(14):TCP协议头、TCP的五元组结构、TCP连接的状态转移详解-CSDN博客
客户端使用的端口有1024 – 65535 (64511)个,3台客户端共可用的端口有193,533个。
根据TCP的五元组结构可知,服务器提供可连接的端口至少为6个(本程序中提供了100个)。
1. 服务器代码
#include <stdio.h> // 标准输入输出库(用于printf、perror等)
#include <string.h> // 字符串处理库(用于memset、memcpy等内存操作)
#include <stdlib.h> // 标准库(用于atoi、exit等)
#include <netinet/tcp.h> // TCP协议相关定义(如TCP选项)
#include <arpa/inet.h> // 网络地址转换库(用于htons、inet_addr等)
#include <pthread.h> // 线程库(本代码未使用,仅包含,可根据需求移除)
#include <errno.h> // 错误处理库(用于errno变量,判断系统调用错误)
#include <fcntl.h> // 文件控制库(用于fcntl设置非阻塞IO)
#include <unistd.h> // 系统调用库(用于close、read、write、sleep等)
#include <sys/epoll.h> // epoll IO多路复用库(核心:epoll_create、epoll_wait、epoll_ctl)
#include <sys/time.h> // 时间库(用于gettimeofday获取系统时间,统计连接建立耗时)
// 宏定义:全局常量配置
#define BUFFER_LENGTH 1024 // 接收数据的缓冲区大小(1024字节)
#define EPOLL_SIZE 1024 // epoll事件数组的最大容量(一次最多处理1024个就绪事件)
#define MAX_PORT 100 // 服务器监听的端口数量(共100个连续端口)
// 宏定义:计算两个timeval结构体的时间差(单位:毫秒)
#define TIME_SUB_MS(tv1, tv2) ((tv1.tv_sec – tv2.tv_sec) * 1000 + (tv1.tv_usec – tv2.tv_usec) / 1000)
/**
* @brief 判断当前文件描述符(fd)是否为监听fd
* @param fd 待判断的文件描述符
* @param fds 存储所有监听fd的数组(sockfds)
* @return 若fd是监听fd,返回该fd;否则返回0
*/
int islistenfd(int fd, int *fds)
{
// 遍历所有监听fd(共MAX_PORT个)
int i = 0;
for (i = 0; i < MAX_PORT; i++)
{
// 匹配到监听fd,返回该fd
if (fd == *(fds + i))
return fd;
}
// 未匹配,返回0(表示是客户端fd)
return 0;
}
// 程序入口:./tcp_server 起始端口号(如./tcp_server 8888,监听8888~8987共100个端口)
int main(int argc, char *argv[])
{
// 1. 命令行参数校验:需传入起始端口号(argc至少为2)
if (argc < 2)
{
printf("Param Error: 请传入起始端口号,格式:./tcp_server [起始端口]\\n");
return -1;
}
// 时间变量:用于统计每1000个连接的建立耗时
struct timeval tv_begin;
gettimeofday(&tv_begin, NULL); // 初始化起始时间(获取当前系统时间)
int connections = 0; // 统计当前服务器的总连接数
// 解析命令行参数:获取起始端口号(字符串转整数)
int port = atoi(argv[1]);
int sockfds[MAX_PORT] = {0}; // 存储所有监听fd的数组(共MAX_PORT个)
// 创建epoll实例:参数1为"size"(已废弃,仅需传入大于0的值即可),返回epoll文件描述符
int epfd = epoll_create(1);
if (epfd < 0) { // 容错:epoll创建失败
perror("epoll_create failed");
return 1;
}
// 2. 循环创建MAX_PORT个监听socket(每个socket对应一个端口)
int i = 0;
for (i = 0; i < MAX_PORT; i++)
{
// 创建TCP socket:AF_INET(IPv4)、SOCK_STREAM(流式TCP)、0(默认协议)
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) { // 容错:socket创建失败
perror("socket failed");
return 2;
}
// 初始化服务器地址结构(IPv4)
struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in)); // 内存清零(避免脏数据)
addr.sin_family = AF_INET; // 地址族:IPv4
addr.sin_port = htons(port + i); // 端口号:起始端口+i(如8888+0=8888,8888+1=8889…)
addr.sin_addr.s_addr = INADDR_ANY; // 绑定所有网卡的IP(0.0.0.0)
// 绑定socket到指定端口(将sockfd与addr关联)
if (bind(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)) < 0)
{
perror("bind failed"); // 打印错误原因(如端口被占用)
close(sockfd); // 关闭已创建的socket,避免资源泄漏
return 2;
}
// 监听socket:进入被动连接状态,backlog=5(未完成连接队列的最大长度)
if (listen(sockfd, 5) < 0)
{
perror("listen failed");
close(sockfd);
return 3;
}
// 打印监听信息:提示当前监听的端口号
printf("tcp server listen on port : %d\\n", port + i);
// 将监听fd加入epoll:关注"读事件"(EPOLLIN,有新连接时触发)
struct epoll_event ev; // epoll事件结构
ev.events = EPOLLIN; // 监听的事件类型:读事件(新连接请求)
ev.data.fd = sockfd; // 绑定当前监听fd到事件结构
// epoll控制:EPOLL_CTL_ADD(添加事件)、epfd(epoll实例)、sockfd(目标fd)、&ev(事件配置)
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
// 将当前监听fd存入数组,供后续islistenfd函数判断
sockfds[i] = sockfd;
}
// 3. epoll主循环:持续等待并处理就绪事件(新连接/客户端数据)
struct epoll_event events[EPOLL_SIZE] = {0}; // 存储就绪事件的数组
while (1) // 无限循环(服务器常驻)
{
// 等待epoll事件:epfd(epoll实例)、events(就绪事件数组)、EPOLL_SIZE(数组最大容量)、5(超时时间,单位ms,5ms无事件则返回0)
int nready = epoll_wait(epfd, events, EPOLL_SIZE, 5);
if (nready == -1) { // 容错:epoll_wait出错(如被信号中断),跳过本次循环
continue;
}
// 遍历所有就绪事件(nready为就绪事件数量)
int i = 0;
for (i = 0; i < nready; i++)
{
// 3.1 处理监听fd的事件(新连接请求)
// 判断当前就绪fd是否为监听fd(调用islistenfd函数)
int sockfd = islistenfd(events[i].data.fd, sockfds);
if (sockfd) // 是监听fd:有新客户端连接请求
{
// 初始化客户端地址结构:用于存储客户端的IP和端口
struct sockaddr_in client_addr;
memset(&client_addr, 0, sizeof(struct sockaddr_in));
socklen_t client_len = sizeof(client_addr); // 地址长度(传入传出参数)
// 接受新连接:从监听队列中取出一个连接,返回客户端fd(与客户端通信的句柄)
int clientfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);
if (clientfd < 0) { // 容错:accept失败(如连接队列空)
perror("accept failed");
continue;
}
// 设置客户端fd为非阻塞模式:避免recv/send阻塞导致服务器卡死
fcntl(clientfd, F_SETFL, O_NONBLOCK);
// 设置socket选项:SO_REUSEADDR(端口复用),避免连接断开后端口处于TIME_WAIT状态无法立即复用
int reuse = 1;
setsockopt(clientfd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));
// 将客户端fd加入epoll:关注"读事件+边缘触发"(EPOLLET,高并发场景常用,减少事件触发次数)
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 边缘触发(仅在状态变化时触发一次)
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
// 连接数+1,并统计耗时(每1000个连接或达到100万连接时打印)
connections++;
if (connections % 1000 == 999 || connections >= 1000000)
{
struct timeval tv_cur;
memcpy(&tv_cur, &tv_begin, sizeof(struct timeval)); // 保存上一次统计的起始时间
gettimeofday(&tv_begin, NULL); // 更新当前时间为新的起始时间
int time_used = TIME_SUB_MS(tv_begin, tv_cur); // 计算这1000个连接的建立耗时(ms)
printf("connections: %d, time_used:%d ms\\n", connections, time_used);
}
}
// 3.2 处理客户端fd的事件(客户端发送数据/断开连接)
else
{
int clientfd = events[i].data.fd; // 获取就绪的客户端fd
// 接收客户端数据:buffer(接收缓冲区)、BUFFER_LENGTH(缓冲区大小)、0(默认标志)
char buffer[BUFFER_LENGTH] = {0};
int len = recv(clientfd, buffer, BUFFER_LENGTH, 0);
// 处理recv返回值:三种情况
if (len < 0) // 情况1:接收错误(如客户端强制断开)
{
close(clientfd); // 关闭客户端fd,释放资源
// 从epoll中删除该客户端fd(避免后续再监听该fd的事件)
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
connections–; // 连接数-1
}
else if (len == 0) // 情况2:客户端正常断开(发送FIN包)
{
close(clientfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, NULL); // 删除epoll事件(结构体可传NULL)
connections–;
}
else // 情况3:成功接收数据(len为接收的字节数)
{
// 注释说明:本代码为高并发连接测试,暂不处理数据回显(避免打印/发送影响性能)
// printf("Recv: %s, %d byte(s), clientfd: %d\\n", buffer, len, clientfd);
// send(clientfd, buffer, len, 0); // 数据回显(客户端发什么,服务器回什么)
}
}
}
}
// 程序理论上不会走到这里,若退出需关闭所有监听fd和epoll实例(避免资源泄漏)
for (i = 0; i < MAX_PORT; i++) {
close(sockfds[i]);
}
close(epfd);
return 0;
}
2. 客户端代码
#include <stdio.h> // 标准输入输出库(printf、perror等)
#include <string.h> // 字符串处理库(memset、strcpy、strcmp等)
#include <stdlib.h> // 标准库(exit、atoi等)
#include <sys/types.h> // 系统基础类型定义(如socket相关类型)
#include <sys/socket.h> // socket核心库(socket、connect、send、recv等)
#include <sys/epoll.h> // epoll IO多路复用库(epoll_create、epoll_wait、epoll_ctl)
#include <errno.h> // 错误处理库(errno变量、strerror等)
#include <netinet/tcp.h> // TCP协议相关定义(如TCP选项)
#include <arpa/inet.h> // 网络地址转换库(inet_addr、htons等)
#include <netdb.h> // 网络主机相关(本代码未实际使用,仅包含)
#include <fcntl.h> // 文件控制库(fcntl设置非阻塞IO)
#include <sys/time.h> // 时间库(gettimeofday获取系统时间,统计耗时)
#include <unistd.h> // 系统调用库(close、usleep等)
// 宏定义:全局常量配置
#define MAX_BUFFER128 // 接收/发送数据的缓冲区大小(128字节)
#define MAX_EPOLLSIZE(384*1024) // epoll事件数组最大容量(支持最多384*1024个连接事件)
#define MAX_PORT100 // 客户端连接的端口范围(对应服务器的100个监听端口)
// 宏定义:计算两个timeval结构体的时间差(单位:毫秒)
#define TIME_SUB_MS(tv1, tv2) ((tv1.tv_sec – tv2.tv_sec) * 1000 + (tv1.tv_usec – tv2.tv_usec) / 1000)
// 全局标志:控制是否继续建立新连接(收到服务器"quit"指令后设为0,停止建连)
int isContinue = 0;
/**
* @brief 设置文件描述符(fd)为非阻塞模式
* @param fd 待设置的文件描述符(如socket fd)
* @return 0:设置成功;-1:设置失败
*/
static int ntySetNonblock(int fd) {
int flags; // 存储fd当前的文件状态标志
// 1. 获取fd当前的状态标志(F_GETFL:Get File Status Flags)
flags = fcntl(fd, F_GETFL, 0);
if (flags < 0) return flags; // 获取失败,返回错误码
// 2. 给标志添加"非阻塞"位(O_NONBLOCK)
flags |= O_NONBLOCK;
// 3. 设置新的状态标志(F_SETFL:Set File Status Flags)
if (fcntl(fd, F_SETFL, flags) < 0) return -1; // 设置失败返回-1
return 0; // 设置成功
}
/**
* @brief 设置socket的SO_REUSEADDR选项(允许端口复用)
* @param fd 待设置的socket文件描述符
* @return 0:设置成功;-1:设置失败(返回setsockopt的错误码)
*/
static int ntySetReUseAddr(int fd) {
int reuse = 1; // 启用端口复用(1:启用,0:禁用)
// setsockopt参数说明:
// fd:目标socket;SOL_SOCKET:操作socket层选项;SO_REUSEADDR:选项名;
// &reuse:选项值地址;sizeof(reuse):选项值长度
return setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));
}
/**
* @brief 主函数:高并发TCP客户端逻辑(建立大量连接到服务器,用epoll管理连接)
* @param argc 命令行参数个数
* @param argv 命令行参数数组(argv[1]:服务器IP,argv[2]:服务器起始端口)
* @return 0:正常退出;非0:异常退出
*/
int main(int argc, char **argv) {
// 1. 命令行参数校验:需传入服务器IP和起始端口(argc至少为3)
if (argc <= 2) {
printf("Usage: %s ip port\\n", argv[0]); // 提示正确用法(如./client 127.0.0.1 8888)
exit(0); // 退出程序
}
// 初始化核心变量
const char *ip = argv[1]; // 服务器IP地址(从命令行获取)
int port = atoi(argv[2]); // 服务器起始端口(字符串转整数)
int connections = 0; // 统计当前与服务器的总连接数
char buffer[128] = {0}; // 发送数据的缓冲区
int i = 0, index = 0; // 循环索引(index用于切换连接的端口)
struct epoll_event events[MAX_EPOLLSIZE]; // 存储epoll就绪事件的数组
// 创建epoll实例:参数为最大事件数(已废弃,仅需传入大于0的值,这里用MAX_EPOLLSIZE)
int epoll_fd = epoll_create(MAX_EPOLLSIZE);
if (epoll_fd < 0) { // 容错:epoll创建失败
perror("epoll_create failed");
exit(1);
}
// 初始化发送缓冲区(默认填充测试数据,可后续覆盖)
strcpy(buffer, " Data From MulClient\\n");
// 2. 初始化服务器地址结构(IPv4)
struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in)); // 内存清零(避免脏数据)
addr.sin_family = AF_INET; // 地址族:IPv4
addr.sin_addr.s_addr = inet_addr(ip); // 服务器IP(将字符串IP转为网络字节序)
// 初始化时间统计:记录连接建立的起始时间(用于计算每1000个连接的耗时)
struct timeval tv_begin;
gettimeofday(&tv_begin, NULL);
// 3. 主循环:持续建立连接 + 处理epoll事件(服务器常驻)
while (1) {
// 切换连接的端口:循环使用0~MAX_PORT-1(对应服务器的100个监听端口)
if (++index >= MAX_PORT) index = 0;
struct epoll_event ev; // epoll事件结构(用于添加连接到epoll)
int sockfd = 0; // 当前创建的socket fd(与服务器的单个连接)
// 3.1 建立新连接:连接数未达上限(34万)且允许继续建连(isContinue=0)
if (connections < 340000 && !isContinue) {
// 创建TCP socket:AF_INET(IPv4)、SOCK_STREAM(流式TCP)、0(默认协议)
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) { // 容错:socket创建失败
perror("socket");
goto err; // 跳转到错误处理标签
}
// (可选注释)设置端口复用(注释掉不影响,按需启用)
// ntySetReUseAddr(sockfd);
// 设置当前连接的服务器端口:起始端口 + 循环索引(如8888+0=8888,8888+1=8889…)
addr.sin_port = htons(port + index);
// 连接服务器:非阻塞socket的connect会立即返回(后续通过epoll的EPOLLOUT判断连接是否成功)
if (connect(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
// 非阻塞connect失败可能是正常的(如连接在建立中),仅打印错误不退出
perror("connect");
goto err;
}
// 设置socket为非阻塞模式(避免send/recv阻塞导致客户端卡死)
ntySetNonblock(sockfd);
// 启用端口复用(避免连接断开后端口处于TIME_WAIT状态无法立即复用)
ntySetReUseAddr(sockfd);
// 构造发送数据(包含当前连接数,用于标识客户端)
sprintf(buffer, "Hello Server: client –> %d\\n", connections);
// (可选注释)发送数据到服务器(注释掉不影响连接建立,仅用于测试数据交互)
// send(sockfd, buffer, strlen(buffer), 0);
// 将当前socket添加到epoll:关注读事件(EPOLLIN,服务器发数据)和写事件(EPOLLOUT,可发送数据)
ev.data.fd = sockfd;
ev.events = EPOLLIN | EPOLLOUT;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev);
connections ++; // 连接数+1
}
// 3.2 统计连接耗时 + 处理epoll就绪事件
// 条件:每建立1000个连接(connections%1000==999)或连接数达上限(34万)
if (connections % 1000 == 999 || connections >= 340000) {
struct timeval tv_cur;
memcpy(&tv_cur, &tv_begin, sizeof(struct timeval)); // 保存上一次统计的起始时间
gettimeofday(&tv_begin, NULL); // 更新当前时间为新的起始时间
int time_used = TIME_SUB_MS(tv_begin, tv_cur); // 计算这1000个连接的建立耗时(毫秒)
// 打印统计信息:当前连接数、当前socket fd、耗时
printf("connections: %d, sockfd:%d, time_used:%d ms\\n", connections, sockfd, time_used);
// 等待epoll事件:最多等待100ms,返回就绪事件数(nfds)
int nfds = epoll_wait(epoll_fd, events, connections, 100);
// 遍历所有就绪事件,处理每个连接的读写需求
for (i = 0; i < nfds; i++) {
int clientfd = events[i].data.fd; // 当前就绪的socket fd
// 3.2.1 处理写事件(EPOLLOUT:socket可写,可发送数据)
if (events[i].events & EPOLLOUT) {
// 构造发送数据(包含当前socket fd,标识发送方)
sprintf(buffer, "data from client fd: %d\\n", clientfd);
// (可选注释)发送数据到服务器(注释掉不影响事件处理)
// send(clientfd, buffer, strlen(buffer), 0);
}
// 3.2.2 处理读事件(EPOLLIN:socket可读,服务器发来了数据)
else if (events[i].events & EPOLLIN) {
char rBuffer[MAX_BUFFER] = {0}; // 接收数据的缓冲区
// 接收服务器数据:非阻塞recv,返回接收的字节数
ssize_t length = recv(clientfd, rBuffer, MAX_BUFFER, 0);
if (length > 0) { // 接收成功(length为接收的字节数)
printf("Recv from server: %s\\n", rBuffer); // 打印接收的数据
// 若收到服务器的"quit"指令,设置标志停止建立新连接
if (!strcmp(rBuffer, "quit")) {
isContinue = 0;
}
}
else if (length == 0) { // 服务器正常断开连接(发送FIN包)
printf("Server disconnected, clientfd:%d\\n", clientfd);
connections –; // 连接数-1
close(clientfd); // 关闭socket,释放资源
}
else { // 接收错误(length < 0)
if (errno == EINTR) continue; // 被信号中断(如Ctrl+C),跳过本次处理
// 其他错误(如客户端强制断开),打印错误信息并关闭socket
printf("Recv error, clientfd:%d, errno:%d\\n", clientfd, errno);
close(clientfd);
}
}
// 3.2.3 处理其他事件(如错误事件)
else {
printf("Unexpected event, clientfd:%d, errno:%d\\n", clientfd, errno);
close(clientfd); // 关闭异常socket,避免资源泄漏
}
}
}
// 每次循环休眠1毫秒:降低CPU占用率(避免空循环过度消耗CPU)
usleep(1 * 1000);
}
return 0; // 程序理论上不会走到这里(主循环无限)
// 错误处理标签:处理socket/connect创建失败的情况
err:
printf("Error: %s\\n", strerror(errno)); // 打印具体错误原因(如端口被占用、连接超时)
return 0;
}


