当前位置: 首页 > news >正文

【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

http://www.dtcms.com/a/303159.html

相关文章:

  • 开源赋能产业,生态共筑未来 | 开源科学计算与系统建模(openSCS)分论坛圆满举行
  • 笔试——Day21
  • JS面试题
  • Linux 远程连接与文件传输:从基础到高级配置
  • QT之QThread 与 QtConcurrent
  • 【自动化运维神器Ansible】Ansible常用模块之cron模块详解
  • GaussDB as的用法
  • 【GaussDB】内存资源告急:深度诊断一起“memory temporarily unavailable“故障
  • DMETL安装流程及简单使用
  • OpenLayers 入门指南【四】:项目初始化
  • Qt小组件 - 8 图片浏览器
  • MySQL高级配置与优化实战指南
  • 利用 SQL Server 实现字符替换的高效函数
  • 第二十一天(shell练习)
  • IT运维的365天--033 跨交换机部署没有单独供电口的爱快AP到另一个地方去
  • 如何选择适合高并发环境的服务器:性能与稳定性的平衡
  • 短剧小程序系统开发:连接创作者与用户的桥梁
  • Node.js + TypeScript 开发健壮的淘宝商品 API SDK
  • 2025年07月23日秋瑶传媒一面
  • 【学习路线】AI开发工程师成长指南:从机器学习基础到大模型应用
  • 调色总监的“色彩炼金术”:在PS中创建LUT,并应用于Premiere Pro视频
  • TCP/IP 网际层详解
  • RCLAMP2574N.TCT Semtech:超低钳位TVS二极管 0.5pF超低电容+±30kV超强防护
  • 【Blender小技巧】Blender使用多边形建形工具创建多边形模型,挤出面,模型创建修改编辑UV贴图
  • PostgreSQL 与 MySQL 时间类型避坑指南
  • 《Ai智能眼镜的市场定义及用户分析》- 深圳市天趣星空科技有限公司 CEO 王洁
  • Java字符串详解
  • Entity Framework Core (EF Core) 使用ado.net
  • 用latex+vscode+ctex写毕业论文
  • Spring源码解读之 JdbcTemplate源码