【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 初始化与监听

cpp 复制代码
int sockfd = socket(AF_INET, SOCK_STREAM, 0);       // 创建 TCP socket

struct 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 注册

cpp 复制代码
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 与数据处理)

cpp 复制代码
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],服务器可同时服务多个客户端,无需为每个连接分配线程。

五、完整代码

cpp 复制代码
#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);                                   // 关闭 socket
            break;
        } 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 socket

    struct 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;                                 // 监听主 socket
    epoll_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;   // 就绪的客户端 fd

                char 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); // 输出收到的数据
                }
            }
        }
    }
#endif

    return 0;
}

https://github.com/0voice

相关推荐
sunz_dragon6 分钟前
Claude Code / Codex Git 版本管理完整使用指南
服务器·人工智能
领尚7 分钟前
openclaw 极简安装(Ubuntu 24.04 server)
linux·运维·ubuntu
Gofarlic_OMS22 分钟前
Windchill的license合规使用报告自动化生成与审计追踪系统
大数据·运维·人工智能·云原生·自动化·云计算
SPC的存折29 分钟前
3、主从复制实现同步数据过滤
linux·运维·服务器
SPC的存折31 分钟前
openEuler 24.03 MariaDB Galera 集群部署指南(cz)
linux·运维·服务器·数据库·mysql
xcbrand33 分钟前
文旅行业品牌策划公司找哪家
大数据·运维·人工智能·python
SPC的存折1 小时前
MySQL 8.0 分库分表
linux·运维·服务器·数据库·mysql
风吹迎面入袖凉1 小时前
【Redis】Redisson分布式锁原理
java·服务器·开发语言
cyber_两只龙宝1 小时前
【Oracle】Oracle之DQL中WHERE限制条件查询
linux·运维·数据库·云原生·oracle
斌味代码1 小时前
Shell 性能监控:指标采集、告警规则与可视化大盘设计
运维