【C语言网络编程基础】TCP并发网络编程:io多路复用
在高并发场景下,传统的“一请求一线程”模型面临着线程开销大、上下文切换频繁的问题。为了解决这个瓶颈,本文介绍一个基于 epoll 的 TCP 服务器实现。它通过 I/O 多路复用机制 同时监听多个连接 socket,从而实现轻量级并发处理,显著提升服务器性能。
一、epoll 是什么?
epoll
是 Linux 内核提供的 高效 I/O 多路复用机制,用于同时监听多个文件描述符(通常是 socket),并在某个描述符“就绪”时通知应用程序进行处理。
epoll 的优势:
-
边沿触发与水平触发模式灵活高效;
-
内核维护事件队列,避免重复遍历(相比 select/poll);
-
支持上万级别连接,适合高并发服务器场景。
二、TCP epoll 服务器流程图
启动程序↓
创建监听 socket 并绑定端口↓
创建 epoll 实例并注册监听 socket↓
========= 循环开始 =========↓
epoll_wait 等待事件发生↓
├── 如果是监听 socket ⇒ accept 新连接并加入 epoll
└── 如果是客户端 fd ⇒ recv 接收数据 or 关闭连接↓
========= 循环继续 =========
三、epoll 高并发模型原理
在高并发 TCP 网络服务器中,epoll
允许程序通过一个线程同时监听多个连接,一旦某个连接有数据可读或断开,系统立即通知应用程序处理,无需为每个连接分配线程或进程,从而节省了系统资源,提高了性能。
四、核心代码讲解
模块一:服务端 socket 初始化与监听
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建 TCP socketstruct sockaddr_in addr;
memset(&addr, 0, sizeof(addr)); // 清零地址结构
addr.sin_family = AF_INET; // IPv4
addr.sin_port = htons(port); // 端口号(主机转网络字节序)
addr.sin_addr.s_addr = INADDR_ANY; // 接收任意地址bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)); // 绑定地址和端口
listen(sockfd, 5); // 启动监听,最大队列为 5
模块二:epoll 初始化与监听 socket 注册
int epfd = epoll_create(1); // 创建 epoll 实例
struct epoll_event events[EPOLL_SIZE] = {0}; // 存储就绪事件数组struct epoll_event ev;
ev.events = EPOLLIN; // 监听“可读事件”
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); // 注册监听 socket
-
创建 epoll 文件描述符
-
将监听 socket 注册到 epoll,用于接收客户端连接
模块三:主事件循环(accept 与数据处理)
while(1) {int nready = epoll_wait(epfd, events, EPOLL_SIZE, -1); // 等待事件if(nready == -1) continue;for(int i = 0; i < nready; i++) {if(events[i].data.fd == sockfd) { // 有新连接struct sockaddr_in client_addr;socklen_t client_len = sizeof(client_addr);int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len);ev.events = EPOLLIN | EPOLLET; // 边沿触发,提高效率ev.data.fd = clientfd;epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);} else {int clientfd = events[i].data.fd;char buffer[BUFFER_LENGTH] = {0};int len = recv(clientfd, buffer, BUFFER_LENGTH, 0);if(len <= 0) { // 客户端断开close(clientfd);epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, NULL);} else {printf("Recv: %s, %d byte(s)\n", buffer, len);}}}
}
-
epoll_wait 是 epoll 的核心,它阻塞等待多个 fd 的“事件就绪”通知。
-
监听 socket 触发时,说明有新的客户端请求,此时使用
accept()
获取新连接并加入 epoll 监听。-
已连接的客户端 socket 触发时,调用
recv()
读取数据,如果返回值<= 0
表示客户端关闭或异常断开,需清除 fd。 -
否则,就正常处理客户端发送的数据。
-
通过循环处理所有 events[i]
,服务器可同时服务多个客户端,无需为每个连接分配线程。
五、完整代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unistd.h>#include <errno.h>
#include <fcntl.h>#include <sys/epoll.h>#define BUFFER_LENGTH 1024 // 接收缓冲区大小
#define EPOLL_SIZE 1024 // epoll 同时监听的最大事件数// 线程函数(旧线程模型中使用)
void *client_routine(void *arg){int clientfd = *(int *)arg; // 客户端 socket 描述符while (1){char buffer[BUFFER_LENGTH] = {0}; // 接收缓冲区清零int len = recv(clientfd, buffer, BUFFER_LENGTH, 0); // 接收数据if(len < 0){ // 接收出错close(clientfd); // 关闭连接break;} else if(len == 0){ // 客户端关闭连接close(clientfd); // 关闭 socketbreak;} else {printf("Recv: %s, %d byte(s)\n", buffer, len); // 正常接收到数据}}
}// ./tcp_server 8888
int main(int argc,char *argv[]){if (argc < 2){ // 参数不足printf("Param Error\n");return -1;}int port = atoi(argv[1]); // 将字符串端口号转换为整数int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建 TCP socketstruct sockaddr_in addr;memset(&addr, 0, sizeof(struct sockaddr_in)); // 地址结构清零addr.sin_family = AF_INET; // 使用 IPv4 协议addr.sin_port = htons(port); // 设置端口(转换为网络字节序)addr.sin_addr.s_addr = INADDR_ANY; // 接收任意 IP 地址连接if(bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0){perror("bind"); // 绑定地址失败return 2;}if(listen(sockfd, 5) < 0){ // 启动监听,最多允许5个等待连接perror("listen");return 3;}#if 0// ================= 旧的一请求一线程模型 =================while (1){struct sockaddr_in client_addr;memset(&client_addr, 0, sizeof(struct sockaddr_in));socklen_t client_len = sizeof(client_addr);int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len); // 接受连接pthread_t thread_id;pthread_create(&thread_id, NULL, client_routine, &clientfd); // 为每个连接开一个线程}
#else// ================= epoll 多路复用模型 =================int epfd = epoll_create(1); // 创建 epoll 实例struct epoll_event events[EPOLL_SIZE] = {0}; // 事件数组用于存储就绪事件struct epoll_event ev;ev.events = EPOLLIN; // 设置为输入事件(可读)ev.data.fd = sockfd; // 监听主 socketepoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); // 将监听 socket 添加到 epoll 监听列表中while(1){int nready = epoll_wait(epfd, events, EPOLL_SIZE, -1); // 等待就绪事件,阻塞直到事件发生if(nready == -1) continue; // 错误继续下一轮for(int i = 0; i < nready; i++){if(events[i].data.fd == sockfd){ // 如果是监听 socket,有新连接struct sockaddr_in client_addr;memset(&client_addr, 0, sizeof(struct sockaddr_in));socklen_t client_len = sizeof(client_addr);int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len); // 接受连接ev.events = EPOLLIN | EPOLLET; // 设置为边沿触发 + 可读事件ev.data.fd = clientfd;epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev); // 新连接加入 epoll 监听} else {int clientfd = events[i].data.fd; // 就绪的客户端 fdchar buffer[BUFFER_LENGTH] = {0};int len = recv(clientfd, buffer, BUFFER_LENGTH, 0); // 接收数据if(len < 0){ // 读取失败,关闭连接close(clientfd);ev.events = EPOLLIN;ev.data.fd = clientfd;epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev); // 从 epoll 删除} else if(len == 0){ // 客户端断开close(clientfd);ev.events = EPOLLIN;ev.data.fd = clientfd;epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev); // 删除监听} else {printf("Recv: %s, %d byte(s)\n", buffer, len); // 输出收到的数据}}}}
#endifreturn 0;
}
https://github.com/0voice