【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

相关推荐
Arvin62722 分钟前
Linux 完整删除 Systemd 服务的步骤
linux·服务器
老实巴交的麻匪31 分钟前
Logs 可观测性 | Grafana Loki 架构窥探与实践
运维·云原生·容器
MarkGosling1 小时前
【开源项目】轻量加速利器 HubProxy 自建 Docker、GitHub 下载加速服务
运维·git·docker·容器·开源·github·个人开发
java叶新东老师1 小时前
Linux /proc/目录详解
linux·运维·服务器
都给我1 小时前
服务器中涉及节流(Throttle)的硬件组件及其应用注意事项
服务器·网络·express
吹个口哨写代码2 小时前
防止包含 XSS 攻击风险的内容提交成功
java·服务器·前端
默|笙3 小时前
【Linux】基本指令(2)
linux·运维·服务器
ZY小袁3 小时前
MGRE综合实验
服务器·网络·笔记·网络安全·学习方法·信息与通信·p2p
conkl6 小时前
构建 P2P 网络与分布式下载系统:从底层原理到安装和功能实现
linux·运维·网络·分布式·网络协议·算法·p2p
π大星星️8 小时前
Nginx 四层(stream)反向代理 + DNS 负载均衡
运维·nginx·负载均衡