当前位置: 首页 > 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.lryc.cn/news/602956.html

相关文章:

  • 五自由度磁悬浮轴承转子:基于自适应陷波器的零振动攻克不平衡质量扰动的终极策略
  • linux du、df命令使用教程
  • 面向对象设计原则和设计模式分类
  • 开源AI智能体-JoyAgent集成Deepseek
  • C++模板元编程从入门到精通
  • [论文阅读] 人工智能 | 机器学习工作流的“救星”:数据虚拟化服务如何解决数据管理难题?
  • [机缘参悟-236]:通过AI人工神经网络理解人的思维特征:惯性思维、路径依赖、适应性、不同场合不同言行、经验、概率、常规与特殊情形(正态分布)、环境适应性
  • 5 分钟上手 Firecrawl
  • Java项目:基于SSM框架实现的社区团购管理系统【ssm+B/S架构+源码+数据库+毕业论文+答辩PPT+远程部署】
  • js的学习1
  • 如何理解有符号数在计算机中用​​补码​​存储
  • 阿里给AI To C战略戴上眼镜
  • 案例开发 - 日程管理 - 第三期
  • Android Handler 完全指南
  • 【QT搭建opencv环境】
  • 商城系统-项目测试
  • redis未授权getshell四种方式
  • Ubuntu24安装MariaDB/MySQL后不知道root密码如何解决
  • 基于STM32设计的智慧果园云监测系统_256
  • 基于Uniapp及Spring Boot的奢侈品二手交易平台的设计与实现/基于微信小程序的二手交易系统
  • linux安装zsh,oh-my-zsh,配置zsh主题及插件的方法
  • 机器学习基础-numpy
  • OpenMP 并行编程核心机制详解:从变量作用域到同步优化
  • SwinTransformer改进(14):集成MLCA注意力机制的Swin Transformer模型
  • Linux DNS解析2 -- 网关DNS代理的作用
  • 如何实现安卓端与苹果端互通的多种方案
  • unisS5800XP-G交换机配置命令之端口篇
  • 【安卓笔记】OOM与内存优化
  • React Router v6 核心组件
  • Linux进程概念(五)进程地址空间