reactor实现与高并发

前言: 已经了解,reactor 实现了事件驱动,针对不同io事件,reactor 负责识别和监测io,并分发任务;handler处理读写错误等工作;具体的说,在高并发中,一个io 映射到一个或多个事件 而基于事件有映射不同的callbck回调函数;比如: serverfd-->EPLOLLIN-->accept, 这是一个需要监听的io并将对面的fd加入到监听集合epollevent中;; clientfd-->EPOLLIN -->recv;; clientfd-->EPOLLOUNT-->send;;

实现注意
1、 event与回调函数一一匹配;既然是事件驱动,我们利用数据结构将fd、指定事件、回调函数(需要的处理函数)绑定在一起;
2、每个io与之对应的参数;对于同一个io 应当同时注意其 写或读的情况,, 它所对应的参数应该如下设置:

#define BUFFER_LENGTH 1024

typedef int (*RCALLBACK)(int fd);

struct conn {
    int fd;                              // 该连接对应的文件描述符
    char rbuffer[BUFFER_LENGTH];          // 读缓冲区:存放从客户端收到的数据
    int rlength;                         // 读缓冲区中有效数据长度
    char wbuffer[BUFFER_LENGTH];          // 写缓冲区:存放要发给客户端的数据
    int wlength;                         // 写缓冲区中待发送数据长度
    RCALLBACK send_callback;              // 可写事件(EPOLLOUT)的回调函数
    union {
        RCALLBACK recv_callback;          // 普通连接:可读事件(EPOLLIN)回调
        RCALLBACK accept_callback;        // 监听socket:有新连接到达时的回调
    } r_action;
    int status;                           // 连接状态(如:0=空闲, 1=发送中, 2=等待发送)
};

3、事件循环:在主线程下的处理io,我们无需判断fd具体情况,只判断是读事件还是写时间,直接调用上述绑定下的回调函数,完成处理。这是稳定状态。简单来说,根据读事件触发读回调,根据写时间触发写回调。早在事件注册时,数据分流已经确定。

完整的事件驱动流程:
//
// 初始化阶段:
// 1. epoll_create() → 创建 epoll 实例(Reactor 核心)
// 2. init_server() × N → 创建 N 个监听 socket
// 3. set_event() → 将监听 socket 注册到 epoll (监听 EPOLLIN)
//
// 事件循环阶段 (Reactor 核心循环):
// 4. epoll_wait() → 阻塞等待事件就绪(单线程处理所有 I/O)
// 5. 遍历就绪事件 → 根据事件类型分发回调:
// EPOLLIN → r_action.recv_callback (accept_cb 或 recv_cb)
// EPOLLOUT → send_callback (send_cb)
// 6. 回到步骤 4,无限循环
//
// 事件驱动状态机:
// 监听 fd: EPOLLIN → accept_cb → 注册 clientfd (监听 EPOLLIN)
// 客户端 fd: EPOLLIN → recv_cb → 读数据 → 处理 → 切换到 EPOLLOUT
// 客户端 fd: EPOLLOUT → send_cb → 发数据 → 切换到 EPOLLIN

函数实现

// ============================================================
// accept_cb — ACCEPT 回调:处理新连接到达
//
// Reactor 事件流程:
//   监听 fd (sockfd) → EPOLLIN 事件触发 → accept_cb 被调用
//
// 核心步骤:
//   1. accept() 系统调用,获取客户端 fd
//   2. event_register() 将客户端 fd 注册到 epoll,绑定 recv_cb
//   3. 每 1000 个连接输出一次统计信息(吞吐量监控)
// ============================================================
int accept_cb(int fd) {

	struct sockaddr_in  clientaddr;
	socklen_t len = sizeof(clientaddr);

	// --- 步骤1: 调用 accept 从全连接队列中取出一个已完成的连接 ---
	int clientfd = accept(fd, (struct sockaddr*)&clientaddr, &len);
	if (clientfd < 0) {
		printf("accept errno: %d --> %s\n", errno, strerror(errno));
		return -1;
	}

	// --- 步骤2: 将新客户端 fd 注册到 epoll,监听 EPOLLIN(等待客户端发数据)---
	// 从此以后,这个 clientfd 上的数据到达会自动触发 recv_cb
	event_register(clientfd, EPOLLIN);  // | EPOLLET

	// --- 步骤3: 每 1000 个连接输出一次吞吐量统计 ---
	if ((clientfd % 1000) == 0) {

		struct timeval current;
		gettimeofday(&current, NULL);

		int time_used = TIME_SUB_MS(current, begin);
		memcpy(&begin, &current, sizeof(struct timeval));

		printf("accept finshed: %d, time_used: %d\n", clientfd, time_used);

	}
	return 0;
}

// ============================================================
// recv_cb — RECV 回调:处理客户端发来的数据
//
// Reactor 事件流程:
//   客户端 fd → EPOLLIN 事件触发 → recv_cb 被调用
//
// 核心步骤:
//   1. recv() 读取数据到该连接的读缓冲区
//   2. 判断返回值:
//        count == 0  → 客户端正常关闭连接 (FIN)
//        count < 0  → 读错误
//        正常情况    → 处理请求 (WebSocket 或 HTTP)
//   3. 处理完毕后,切换监听事件为 EPOLLOUT(准备发送响应)
//
// 关键设计: 读 → 处理 → 切换到写 的 Reactor 状态机
// ============================================================
int recv_cb(int fd) {

	// --- 步骤1: 清空读缓冲区,准备接收新数据 ---
	memset(conn_list[fd].rbuffer, 0, BUFFER_LENGTH);

	// --- 步骤2: 从内核接收缓冲区读取数据到用户态 ---
	int count = recv(fd, conn_list[fd].rbuffer, BUFFER_LENGTH, 0);

	if (count == 0) {
		// --- 客户端发送了 FIN 包,正常断开连接 ---
		// 需要:关闭 fd,从 epoll 中移除监听,释放资源
		printf("client disconnect: %d\n", fd);
		close(fd);
		// 从 epoll 实例中删除该 fd 的监听
		epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);

		return 0;

	} else if (count < 0) {
		// --- 读错误(非 EAGAIN),关闭连接 ---
		printf("count: %d, errno: %d, %s\n", count, errno, strerror(errno));
		close(fd);
		epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);

		return 0;
	}

	// --- 步骤3: 记录实际读取的字节数 ---
	conn_list[fd].rlength = count;

	// --- 步骤4: 协议层处理请求 ---
	// 通过编译时宏切换协议:ECHO / HTTP / WebSocket
#if 0 // 简易 ECHO 协议:直接把收到的数据原样返回
	conn_list[fd].wlength = conn_list[fd].rlength;
	memcpy(conn_list[fd].wbuffer, conn_list[fd].rbuffer, conn_list[fd].wlength);
	printf("[%d]RECV: %s\n", conn_list[fd].rlength, conn_list[fd].rbuffer);

#elif 0 // HTTP 协议:解析 HTTP 请求,填充 wbuffer
	http_request(&conn_list[fd]);

#else // WebSocket 协议(当前激活):解析 WebSocket 帧,填充 wbuffer
	ws_request(&conn_list[fd]);

#endif

	// --- 步骤5: 数据已写入 wbuffer,切换监听事件为 EPOLLOUT ---
	// 当内核发送缓冲区有空闲空间时,epoll_wait 会返回此 fd 的 EPOLLOUT 事件
	// 然后触发 send_cb 完成实际的 send 操作
	set_event(fd, EPOLLOUT, 0);

	return count;
}

// ============================================================
// send_cb — SEND 回调:发送响应数据给客户端
//
// Reactor 事件流程:
//   客户端 fd → EPOLLOUT 事件触发 → send_cb 被调用
//
// 核心步骤:
//   1. 协议层组装响应数据(写入 wbuffer)
//   2. 调用 send() 将 wbuffer 中的数据发往内核发送缓冲区
//   3. 发送完毕后,切换回 EPOLLIN(等待下一个请求)
//
// 关键设计: 写 → 切换回读 的 Reactor 状态机
// 这实现了 "一个连接在一次请求-响应中只经历一次读→写切换"
//
// 注意: 这里没有处理 EAGAIN(发送缓冲区满)的情况
//       在高并发场景下应处理部分发送 + 继续监听 EPOLLOUT
// ============================================================
int send_cb(int fd) {

	// --- 步骤1: 协议层组装响应 ---
	// 将处理结果写入 conn_list[fd].wbuffer,设置 wlength
#if 0
	http_response(&conn_list[fd]);
#else
	ws_response(&conn_list[fd]);
#endif

	int count = 0;

	// --- 步骤2: 将写缓冲区数据通过 send() 发送出去 ---
	if (conn_list[fd].wlength != 0) {
		count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
	}

	// --- 步骤3: 发送完毕,切换回 EPOLLIN,等待客户端下一个请求 ---
	// Reactor 核心状态切换:可写 → 可读
	set_event(fd, EPOLLIN, 0);

	return count;
}

int set_event(int fd, int event, int flag) {

	if (flag) {  // flag 非零 → 首次注册,使用 ADD 操作

		struct epoll_event ev;
		ev.events = event;          // 设置要监听的事件 (EPOLLIN / EPOLLOUT)
		ev.data.fd = fd;            // 携带 fd 信息,事件就绪时内核返回此 fd
		epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);

	} else {      // flag 为零 → 修改已有监听,使用 MOD 操作

		struct epoll_event ev;
		ev.events = event;
		ev.data.fd = fd;
		epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);

	}

}
// ============================================================
// event_register — 为新 accept 的客户端连接注册到 epoll
//
// 调用时机: accept_cb 中 accept 成功后立即调用
//
// 做的事情:
//   1. 在连接池中以 fd 为下标初始化该连接的所有字段
//   2. 绑定读回调 recv_cb(数据到达时自动触发)
//   3. 绑定写回调 send_cb(可写时自动触发)
//   4. 清空读写缓冲区
//   5. 调用 set_event 将 fd 注册到 epoll,监听 EPOLLIN(读就绪)
// ============================================================
int event_register(int fd, int event) {

	if (fd < 0) return -1;

	// --- 在连接池中初始化该 fd 对应的连接对象 ---
	conn_list[fd].fd = fd;
	conn_list[fd].r_action.recv_callback = recv_cb;  // 读就绪 → 调用 recv_cb
	conn_list[fd].send_callback = send_cb;            // 写就绪 → 调用 send_cb

	// 清空读缓冲区
	memset(conn_list[fd].rbuffer, 0, BUFFER_LENGTH);
	conn_list[fd].rlength = 0;

	// 清空写缓冲区
	memset(conn_list[fd].wbuffer, 0, BUFFER_LENGTH);
	conn_list[fd].wlength = 0;

	// 将 fd 注册到 epoll,开始监听事件
	set_event(fd, event, 1);
}

完整代码

#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <poll.h>
#include <sys/epoll.h>      // Linux epoll 高性能 I/O 多路复用
#include <errno.h>
#include <sys/time.h>       // gettimeofday 计时


#include "server.h"


// ============================================================
// 连接池大小:支持最多 1048576(约100万)个并发连接
// 以 fd 为索引直接定位连接对象,O(1) 查找
// ============================================================
#define CONNECTION_SIZE			1048576 // 1024 * 1024

// ============================================================
// 最大监听端口数:可同时监听 20 个端口
// ============================================================
#define MAX_PORTS			20

// ============================================================
// 宏:计算两个 timeval 之间的毫秒差
// 用于统计每 1000 个连接的耗时
// ============================================================
#define TIME_SUB_MS(tv1, tv2)  ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)


// ============================================================
// 回调函数前置声明(Reactor 模式的核心:事件 → 回调)
//
// 三种事件回调:
//   accept_cb — 监听 fd 上有新连接到达时调用
//   recv_cb   — 连接 fd 上有数据可读时调用
//   send_cb   — 连接 fd 上可写时调用
// ============================================================
int accept_cb(int fd);
int recv_cb(int fd);
int send_cb(int fd);


// ============================================================
// 全局变量
//   epfd  — epoll 实例的文件描述符,整个进程唯一
//   begin — 计时起点,用于统计吞吐量
// ============================================================
int epfd = 0;
struct timeval begin;



// ============================================================
// 全局连接池(Reactor 模式的核心数据结构)
//
// 以 fd 为数组下标直接索引 → O(1) 定位连接上下文
// 每个连接持有自己的读写缓冲区和回调函数指针
// ============================================================
struct conn conn_list[CONNECTION_SIZE] = {0};


// ============================================================
// set_event — 注册/修改 epoll 监听事件
//
// 参数:
//   fd    — 要操作的文件描述符
//   event — 要监听的事件类型 (EPOLLIN / EPOLLOUT)
//   flag  — 非零 = EPOLL_CTL_ADD (首次添加)
//           零   = EPOLL_CTL_MOD (修改已有监听)
//
// 这是 Reactor 的核心操作:动态切换监听事件的类型
// 读 → 写 → 读 的切换就是靠这个函数完成的
// ============================================================
int set_event(int fd, int event, int flag) {

	if (flag) {  // flag 非零 → 首次注册,使用 ADD 操作

		struct epoll_event ev;
		ev.events = event;          // 设置要监听的事件 (EPOLLIN / EPOLLOUT)
		ev.data.fd = fd;            // 携带 fd 信息,事件就绪时内核返回此 fd
		epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);

	} else {      // flag 为零 → 修改已有监听,使用 MOD 操作

		struct epoll_event ev;
		ev.events = event;
		ev.data.fd = fd;
		epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);

	}


}


// ============================================================
// event_register — 为新 accept 的客户端连接注册到 epoll
//
// 调用时机: accept_cb 中 accept 成功后立即调用
//
// 做的事情:
//   1. 在连接池中以 fd 为下标初始化该连接的所有字段
//   2. 绑定读回调 recv_cb(数据到达时自动触发)
//   3. 绑定写回调 send_cb(可写时自动触发)
//   4. 清空读写缓冲区
//   5. 调用 set_event 将 fd 注册到 epoll,监听 EPOLLIN(读就绪)
// ============================================================
int event_register(int fd, int event) {

	if (fd < 0) return -1;

	// --- 在连接池中初始化该 fd 对应的连接对象 ---
	conn_list[fd].fd = fd;
	conn_list[fd].r_action.recv_callback = recv_cb;  // 读就绪 → 调用 recv_cb
	conn_list[fd].send_callback = send_cb;            // 写就绪 → 调用 send_cb

	// 清空读缓冲区
	memset(conn_list[fd].rbuffer, 0, BUFFER_LENGTH);
	conn_list[fd].rlength = 0;

	// 清空写缓冲区
	memset(conn_list[fd].wbuffer, 0, BUFFER_LENGTH);
	conn_list[fd].wlength = 0;

	// 将 fd 注册到 epoll,开始监听事件
	set_event(fd, event, 1);
}


// ============================================================
// accept_cb — ACCEPT 回调:处理新连接到达
//
// Reactor 事件流程:
//   监听 fd (sockfd) → EPOLLIN 事件触发 → accept_cb 被调用
//
// 核心步骤:
//   1. accept() 系统调用,获取客户端 fd
//   2. event_register() 将客户端 fd 注册到 epoll,绑定 recv_cb
//   3. 每 1000 个连接输出一次统计信息(吞吐量监控)
// ============================================================
int accept_cb(int fd) {

	struct sockaddr_in  clientaddr;
	socklen_t len = sizeof(clientaddr);

	// --- 步骤1: 调用 accept 从全连接队列中取出一个已完成的连接 ---
	int clientfd = accept(fd, (struct sockaddr*)&clientaddr, &len);
	if (clientfd < 0) {
		printf("accept errno: %d --> %s\n", errno, strerror(errno));
		return -1;
	}

	// --- 步骤2: 将新客户端 fd 注册到 epoll,监听 EPOLLIN(等待客户端发数据)---
	// 从此以后,这个 clientfd 上的数据到达会自动触发 recv_cb
	event_register(clientfd, EPOLLIN);  // | EPOLLET

	// --- 步骤3: 每 1000 个连接输出一次吞吐量统计 ---
	if ((clientfd % 1000) == 0) {

		struct timeval current;
		gettimeofday(&current, NULL);

		int time_used = TIME_SUB_MS(current, begin);
		memcpy(&begin, &current, sizeof(struct timeval));

		printf("accept finshed: %d, time_used: %d\n", clientfd, time_used);

	}

	return 0;
}


// ============================================================
// recv_cb — RECV 回调:处理客户端发来的数据
//
// Reactor 事件流程:
//   客户端 fd → EPOLLIN 事件触发 → recv_cb 被调用
//
// 核心步骤:
//   1. recv() 读取数据到该连接的读缓冲区
//   2. 判断返回值:
//        count == 0  → 客户端正常关闭连接 (FIN)
//        count < 0  → 读错误
//        正常情况    → 处理请求 (WebSocket 或 HTTP)
//   3. 处理完毕后,切换监听事件为 EPOLLOUT(准备发送响应)
//
// 关键设计: 读 → 处理 → 切换到写 的 Reactor 状态机
// ============================================================
int recv_cb(int fd) {

	// --- 步骤1: 清空读缓冲区,准备接收新数据 ---
	memset(conn_list[fd].rbuffer, 0, BUFFER_LENGTH);

	// --- 步骤2: 从内核接收缓冲区读取数据到用户态 ---
	int count = recv(fd, conn_list[fd].rbuffer, BUFFER_LENGTH, 0);

	if (count == 0) {
		// --- 客户端发送了 FIN 包,正常断开连接 ---
		// 需要:关闭 fd,从 epoll 中移除监听,释放资源
		printf("client disconnect: %d\n", fd);
		close(fd);
		// 从 epoll 实例中删除该 fd 的监听
		epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);

		return 0;

	} else if (count < 0) {
		// --- 读错误(非 EAGAIN),关闭连接 ---
		printf("count: %d, errno: %d, %s\n", count, errno, strerror(errno));
		close(fd);
		epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);

		return 0;
	}

	// --- 步骤3: 记录实际读取的字节数 ---
	conn_list[fd].rlength = count;

	// --- 步骤4: 协议层处理请求 ---
	// 通过编译时宏切换协议:ECHO / HTTP / WebSocket
#if 0 // 简易 ECHO 协议:直接把收到的数据原样返回
	conn_list[fd].wlength = conn_list[fd].rlength;
	memcpy(conn_list[fd].wbuffer, conn_list[fd].rbuffer, conn_list[fd].wlength);
	printf("[%d]RECV: %s\n", conn_list[fd].rlength, conn_list[fd].rbuffer);

#elif 0 // HTTP 协议:解析 HTTP 请求,填充 wbuffer
	http_request(&conn_list[fd]);

#else // WebSocket 协议(当前激活):解析 WebSocket 帧,填充 wbuffer
	ws_request(&conn_list[fd]);

#endif


	// --- 步骤5: 数据已写入 wbuffer,切换监听事件为 EPOLLOUT ---
	// 当内核发送缓冲区有空闲空间时,epoll_wait 会返回此 fd 的 EPOLLOUT 事件
	// 然后触发 send_cb 完成实际的 send 操作
	set_event(fd, EPOLLOUT, 0);

	return count;
}


// ============================================================
// send_cb — SEND 回调:发送响应数据给客户端
//
// Reactor 事件流程:
//   客户端 fd → EPOLLOUT 事件触发 → send_cb 被调用
//
// 核心步骤:
//   1. 协议层组装响应数据(写入 wbuffer)
//   2. 调用 send() 将 wbuffer 中的数据发往内核发送缓冲区
//   3. 发送完毕后,切换回 EPOLLIN(等待下一个请求)
//
// 关键设计: 写 → 切换回读 的 Reactor 状态机
// 这实现了 "一个连接在一次请求-响应中只经历一次读→写切换"
//
// 注意: 这里没有处理 EAGAIN(发送缓冲区满)的情况
//       在高并发场景下应处理部分发送 + 继续监听 EPOLLOUT
// ============================================================
int send_cb(int fd) {

	// --- 步骤1: 协议层组装响应 ---
	// 将处理结果写入 conn_list[fd].wbuffer,设置 wlength
#if 0
	http_response(&conn_list[fd]);
#else
	ws_response(&conn_list[fd]);
#endif

	int count = 0;

	// --- 步骤2: 将写缓冲区数据通过 send() 发送出去 ---
	if (conn_list[fd].wlength != 0) {
		count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
	}

	// --- 步骤3: 发送完毕,切换回 EPOLLIN,等待客户端下一个请求 ---
	// Reactor 核心状态切换:可写 → 可读
	set_event(fd, EPOLLIN, 0);

	return count;
}



// ============================================================
// init_server — 初始化一个 TCP 监听 socket
//
// 标准的 TCP 服务器初始化三部曲:
//   socket() → bind() → listen()
//
// 返回处于 LISTEN 状态的 socket fd
// ============================================================
int init_server(unsigned short port) {

	// --- 步骤1: socket() — 创建 TCP socket ---
	// AF_INET = IPv4, SOCK_STREAM = TCP
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);

	// --- 步骤2: bind() — 绑定地址和端口 ---
	struct sockaddr_in servaddr;
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  // 0.0.0.0,监听所有网卡
	servaddr.sin_port = htons(port);               // 主机字节序 → 网络字节序

	if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
		printf("bind failed: %s\n", strerror(errno));
	}

	// --- 步骤3: listen() — 将 socket 转换为被动监听状态 ---
	// backlog = 10: 内核全连接队列的最大长度
	listen(sockfd, 10);

	return sockfd;

}


// ============================================================
// main — Reactor 模式主入口
//
// 完整的事件驱动流程:
//
//   初始化阶段:
//     1. epoll_create()    → 创建 epoll 实例(Reactor 核心)
//     2. init_server() × N → 创建 N 个监听 socket
//     3. set_event()       → 将监听 socket 注册到 epoll (监听 EPOLLIN)
//
//   事件循环阶段 (Reactor 核心循环):
//     4. epoll_wait()  → 阻塞等待事件就绪(单线程处理所有 I/O)
//     5. 遍历就绪事件 → 根据事件类型分发回调:
//          EPOLLIN  → r_action.recv_callback (accept_cb 或 recv_cb)
//          EPOLLOUT → send_callback (send_cb)
//     6. 回到步骤 4,无限循环
//
//   事件驱动状态机:
//     监听 fd:   EPOLLIN → accept_cb  → 注册 clientfd (监听 EPOLLIN)
//     客户端 fd: EPOLLIN → recv_cb    → 读数据 → 处理 → 切换到 EPOLLOUT
//     客户端 fd: EPOLLOUT → send_cb   → 发数据 → 切换到 EPOLLIN
//
// 这种模式的优点:
//   - 单线程即可处理百万级并发连接(C10K/C100K 问题)
//   - 无需为每个连接创建线程,避免线程切换开销
//   - epoll 只返回就绪的 fd,不会像 select/poll 那样遍历全部 fd
// ============================================================
int main() {

	unsigned short port = 2000;     // 起始端口号


	// ===== 初始化阶段 =====

	// --- 步骤1: 创建 epoll 实例(Reactor 的核心)---
	// 参数 1 仅为了兼容旧内核,实际已被忽略
	// epfd 是整个事件循环的"调度中心"
	epfd = epoll_create(1);

	int i = 0;

	// --- 步骤2: 在多个端口上创建监听 socket 并注册到 epoll ---
	// 总共监听 MAX_PORTS=20 个端口(2000~2019)
	for (i = 0;i < MAX_PORTS;i ++) {

		// 2a: 创建监听 socket
		int sockfd = init_server(port + i);

		// 2b: 在连接池中注册该监听 fd
		//     注意:监听 fd 的"读回调"是 accept_cb(而不是 recv_cb)
		//     这意味着当有新连接到达时,epoll 会触发 accept_cb
		conn_list[sockfd].fd = sockfd;
		conn_list[sockfd].r_action.recv_callback = accept_cb;

		// 2c: 将监听 fd 注册到 epoll,监听 EPOLLIN(新连接到达事件)
		set_event(sockfd, EPOLLIN, 1);
	}

	// 记录启动时间,用于吞吐量统计
	gettimeofday(&begin, NULL);


	// ===== 事件循环阶段 (Reactor 的核心循环) =====
	while (1) {

		// --- 步骤3: epoll_wait — 阻塞等待事件就绪 ---
		// 参数:
		//   epfd   — epoll 实例
		//   events — [出参] 内核将就绪的事件列表写入此数组
		//   1024   — 每次最多返回 1024 个就绪事件
		//   -1     — 永久阻塞,直到有事件就绪
		//
		// 返回值 nready = 本次就绪的 fd 数量
		struct epoll_event events[1024] = {0};
		int nready = epoll_wait(epfd, events, 1024, -1);

		// --- 步骤4: 遍历所有就绪事件,分发回调 ---
		int i = 0;
		for (i = 0;i < nready;i ++) {

			// 从 event.data.fd 取出就绪的 fd
			int connfd = events[i].data.fd;

			// --- 步骤5: 根据事件类型分发到对应的回调函数 ---
			// 核心分发逻辑(Reactor 模式的精髓):
			//   EPOLLIN  → 调用 r_action.recv_callback
			//              对于监听 fd → 实际调用 accept_cb(accept 新连接)
			//              对于客户端 fd → 实际调用 recv_cb(读取数据)
			//   EPOLLOUT → 调用 send_callback(发送响应数据)

#if 0
			// 互斥模式: EPOLLIN 和 EPOLLOUT 不同时处理
			// 一个 fd 在一次循环中只处理一种事件
			if (events[i].events & EPOLLIN) {
				conn_list[connfd].r_action.recv_callback(connfd);
			} else if (events[i].events & EPOLLOUT) {
				conn_list[connfd].send_callback(connfd);
			}

#else
			// 非互斥模式(当前使用): EPOLLIN 和 EPOLLOUT 可同时处理
			// 如果一个 fd 同时有读和写事件就绪,两个回调都会调用
			// 先处理读,再处理写
			if (events[i].events & EPOLLIN) {
				conn_list[connfd].r_action.recv_callback(connfd);
			}

			if (events[i].events & EPOLLOUT) {
				conn_list[connfd].send_callback(connfd);
			}
#endif
		}

	}
}


测试产生的错误
**1、in function set_event':** reactor_demo.c:(.text+0x25): relocation truncated to fit: R_X86_64_PC32 against symbol epfd' defined in .bss section in /tmp/ccFoAylJ.o .... 定义的全局变量存在问题?
先补充一些内存分布(分段)的基础知识: C/C++中存在 动态数据区、静态数据区、代码段(文本段)
image
其中.开头表示的区域,是在编译阶段就已经确定的;占用固定大小区域,并有编译器自动分配与释放; 动态数据区的数据是在运行过程中使用的;;具体的,动态数据区有堆和栈,堆向高地址生长,栈向低地址生长;静态数据区包括bss段、静态数据data段和只读数据段rodata,bss段是未初始化或初始为0的数据,固定大小,运行时可读可写,而data和rodata都正常初始化了,代码段存储编译后的CPU指令,固定大小,只读且保持不变

你定义的 epfd 和 begin 是全局变量(非 static),存储在 .bss 段。代码中通过 R_X86_64_PC32 重定位类型访问它们,要求符号地址与引用位置的距离在 ±2GB 以内。当链接器无法将符号安排在合适的距离内(例如代码段与 .bss 段布局跨度太大),就会报错。全局变量定义了一个struct conn conn_list[CONNECTION_SIZE] = {0};它太多了百万级长度超过2GB, 巨大的全局/静态数组,导致 .bss 段非常大,与代码段的距离超过了 32 位 PC 相对寻址的范围。

这里我的内存不够用,那减少struct conn conn_list[CONNECTION_SIZE]的大小

2、结束代码进程,立刻重新启动,出现 端口被使用,bind: Address already in use;?
当你启动一个 TCP 服务器,bind() 将一个端口绑定到本地地址后,该端口就被操作系统标记为已占用。当进程正常退出或被杀死后,操作系统会回收该端口。但是,如果之前在这个端口上建立过 TCP 连接,并且你的进程是主动关闭连接的一方,那么该TCP连接会进入TIME_WAIT状态,持续 2MSL(Maximum Segment Lifetime,通常为 1~4 分钟)。

解决方法:
1)等待系统完全释放连接后,再次启动程序。。 等1分钟;;
2)在 bind() 之前,通过 setsockopt() 设置 SO_REUSEADDR 选项,允许重用处于 TIME_WAIT 状态的地址:
int reuse = 1;
setsockopt(clientfd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));

3、客户端 cannot open too many files?
进程限制的所能打开的文件描述符数量小于实际使用的,通过ulimit -n 1048576修改, 可以ulimit - a 查看。

**4、服务器 Connection refused (io socket file) 连接数止步1023 **
文件系统限定的fd默认个数为1024 和问题3类似
命令: ulimit -a ulimit -n 1048576
Nofile 进程可以打开的最大文件描述符数;;
彻底修改文件系统硬限制和软限制: 通过这个查看ulimit -n(软限制),ulimit -Hn(硬限制);临时修改:/etc/security/limits.conf
软限制:当前生效的限制,用户可以主动调低,但不能调高超过硬限制。
硬限制:系统允许的最大值

	hard	nofile	1048576 
	sort	nofile	1048576

5、connect cannot assign requested address; Error : cannot assign requested address 网络地址的不足?
理论:Socket 与网络地址五元组的关系是一对一的,通过socket找到在网络层构建五元组(目标ip 目标port 本地ip 本地port 协议)
在send 和recv中连接起来。如果说每一个连接一一对应了上面五元组,那么cannot assign requested address就是五元组使用耗尽?实际客户端在连接时,目标ip 目标port 本地ip 协议都是确定的,也就是端口号耗尽了。这是可以理解的一个主机的端口最多665535,不可能达到百万;;
解决办法:打开多个目标端口号,扩大连接数。

**6、总连接数止步:64999 客户端问题:connection timed out : **
排查:

  1. fs.file-max是后者 (注意fd的个数和fd的最大值的区别);; 整个操作系统可以同时打开的文件句柄总数。设置为内存大小的 10% 左右。通过以下命令查看liunx文件系统参数设置; Cat /proc/sys/fs/file-max 经排查 该值为1048576 显然正常,问题不在此;;
    2)另一个点:防火墙系统维护连接数:nf_conntrack_max是Netfilter 连接跟踪模块能够跟踪的最大连接数(主要影响NAT、防火墙状态追踪)
    nf_conntrack_max Cat /proc/sys/net/netfilter/nf_conntrack_max 发现它为65536 很有可能尝试修改内核参数,, 关于修改参数,都可以在 VIM /etc/sysctl.conf 进行修改; 修改后没生效? Sudo sysctl -p 使其生效

7、服务端的问题 Cannot open /proc/meminfo: Too many files in system
同样修改file_max,方法同问题6

8、sysctk: cannit stat /proc/sys/net/net/nf_conntrack_max:no such file or directory
试图添加nf_conntrack_max出现,需要添加包sudo modprobe ip_conntrack, 见问题5

9、在90多万连接时,cpu几乎占满,内存居然减小;出现资源回收的现象;
使用htop指令,可以查看进程使用情况。
如果大量TCP断连,这是不好的业务;内存和cpu的极限工作应该低于80%;
Sudo sysctl -p 还可以查看 修改 优化TCP的占用
Net.ipv4.tcp_mem = 252144 524288 786432 第一个单位是页(4k)
Net.ipv4.tcp_wmem = 1024 1024 2048
Net.ipv4.tcp_rmem = 1024 1024 2048
tcp_mem的 单位是页(4k); 这里分别对应1G 2G 3G 当tcp总缓冲大小在2G-3G之间启用内存优化,回收不活跃的连接;;tcp_wmem tcp_rmem是每个socket分配的缓存,中间是默认值,两端分别最大最小;
缓冲区过小会降低吞吐量,过大浪费内存。高并发场景通常需要适当增大。

10、其他可能影响并发的系统参数
net.core.somaxconn —— listen 的 backlog 上限;当服务器来不及 accept 时,新连接会在内核全连接队列排队,队列满后客户端收到 ECONNREFUSED;sysctl -w net.core.somaxconn=65535

net.ipv4.tcp_max_syn_backlog —— SYN 半连接队列大小,应对 SYN Flood 攻击或瞬时大量连接请求时,此值过小会导致丢包。

net.ipv4.ip_local_port_range —— 本地端口范围,作为客户端时(如反向代理、连接池),可用端口数限制了最大并发出站连接数。

tcp_rmem / tcp_wmem TCP 接收/发送缓冲区大小;; 缓冲区过小会降低吞吐量,过大浪费内存。高并发场景通常需要适当增大。
建议:tcp_rmem = 4096 87380 16777216,tcp_wmem = 4096 65536 16777216

11、好像是SSH断开了

connections: 102999, sockfd:103002, time_used:5326
connections: 103999, sockfd:104002, time_used:5150

Socket error Event: 32 Error: 10053.
Connection closing...Socket close.

Connection closed by foreign host.

12、客户端测试跳出的信息和连接断断续续的问题?
image
图中展示的是 Linux 系统内核日志(通常来自 dmesg 或 /var/log/messages),其核心信息表明系统中的网络适配器(网卡)遭遇了严重的传输故障并触发了自我保护机制。这里我认为是虚拟机server2放置在了固态硬盘上,因此访问受USB传输速率的影响,网卡因此故障。 这个问题非常不稳定,有时完全不触发:
image
由于硬件环境限制最后也没能测试百万,但是可以达到34W:
image

13、最开始的时候,我将服务器部署在云,本地虚拟机部署,往往连接到100多就出现connectd refused
推测,所谓宿主机的本地,在NAT转换时不支持那么多的连接,因此,连接失效。通常,connectd refused表明在accepted队列中的太多没来得及处理,造成后续连接无法进入,从而被拒绝。

需要进行验证的参数
并发量 qps 时延 测试用例

posted @ 2026-07-25 11:19  超级麋鹿  阅读(13)  评论(0)    收藏  举报