欢迎光临
我们一直在努力

Linux C/C++ 学习日记(20):实现一个简易的百万并发服务器(三):代码测试,理解影响服务器CPU和内存的因素

注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。

代码在最后面,大家可以拷贝下来运行一下,按着我的步骤测试

希望大家做完这个百万并发服务器实验,能够明白影响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 压力更小,核心差异:

场景四次挥手(close())强制中断(RST)
状态维护 需等待 FIN_WAIT_1/FIN_WAIT_2 状态(含超时) 无状态等待,发送 RST 后直接清理资源
报文交互 至少 2-4 个报文(FIN/ACK/FIN/ACK) 仅 1 个 RST 报文,无后续交互
定时器开销 需维护重传定时器、状态超时定时器 无定时器开销,资源回收更直接

简言之:RST 是 “暴力断开”,跳过了四次挥手的状态等待和多轮报文交互,内核操作更 “轻量化”,因此 CPU 峰值通常更低。

(大家不信的话可以通过修改代码来对比观察,本代码是强制中断,大家可用编写一个信号捕捉函数handle捕获SIGINT为2的信号实现正常的close。)

三、CPU和内存的差异

1.本质区别:角色与功能

维度CPU(中央处理器)内存(RAM)
核心功能 执行指令运算(如算术逻辑、控制流),是系统的 “运算大脑”。 临时存储数据(如运行的程序、变量、缓存),是 CPU 的 “临时工作台”。
速度与容量 速度极快(GHz 级时钟),但容量小(核心缓存仅 MB 级)。 速度慢于 CPU(ns 级延迟),但容量大(GB 级,如 16GB/32GB 内存)。
数据流向 从内存中读取指令 / 数据,运算后写回内存或寄存器。 从硬盘加载数据,供 CPU 直接访问;断电后数据丢失。
性能瓶颈 受限于指令执行效率(如主频、核心数、缓存命中率)。 受限于数据读写延迟(如内存带宽、是否触发 Swap)。

2.CPU 使用率变化的核心因素

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飙升“来理解:

影响级别CPU 开销占比具体操作环节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 核心(尤其网卡中断绑定核心)

备注:

  • 主因排序:批量 struct sock 回收(含锁竞争)> nf_conntrack 表项清理 > 批量 RST 软中断处理;
  • 若禁用 nf_conntrack(如配置net.netfilter.nf_conntrack_max=0),CPU 峰值可降低 20%-30%,验证其 “关键辅助” 属性;
  • 所有环节的核心共性:“批量操作” 将单个连接的微小开销放大 100 万倍,叠加内核态锁竞争 / 软中断,最终导致 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;
    }

    赞(0)
    未经允许不得转载:171主机测评 » Linux C/C++ 学习日记(20):实现一个简易的百万并发服务器(三):代码测试,理解影响服务器CPU和内存的因素
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址