C语言高性能网络编程:epoll并发模型详解与实现

认真发呆
认真发呆
发布时间:2026-10-14 | 4人浏览过
原创

针对传统IO多路复用性能的瓶颈,C语言网络开发常采用EPOLL技术构建高并发服务器。本文将通过完整的服务端(server.c)与客户端(client.c)代码实例,详细拆解从Socket绑定到Epoll事件循环的标准操作流程,帮助开发者快速掌握Linux下高性能网络编程的核心实现。

环境准备与核心头文件

在开始编码前,需确保运行环境为Linux系统。编写基于Epoll的网络程序需要引入特定的系统头文件,以支持套接字(Socket)、异步I/O通知(Epoll)及非阻塞操作(Fcntl)。核心依赖包括:

  • <sys/socket.h> 和 <netinet/in.h>:用于创建Socket及管理地址结构。
  • <sys/epoll.h>:提供epoll相关的API接口。
  • <fcntl.h>:用于设置文件描述符的非阻塞属性。

第1步:创建Socket并配置非阻塞模式

首先初始化服务端套接字。我们需要使用socket()创建TCP流式套接字,并利用fcntl()将全局或具体的文件描述符设置为非阻塞(O_NONBLOCK),这是配合ET(边缘触发)模式工作的必要条件,以防止因数据未读完导致的死锁。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <unistd.h>

// 定义端口及连接限制
#define PORT 4444
#define BACKLOG 5
#define EPOLL_SIZE 20

void setnonblocking(int sock_fd) {
    int opts = fcntl(sock_fd, F_GETFL);
    if(opts == -1) { perror("fcntl"); exit(1); }
    opts = opts | O_NONBLOCK;
    if(fcntl(sock_fd, F_SETFL, opts) == -1) { perror("set fl"); exit(1); }
}

int main() {
    int sock_fd, epfd, nfds, i;
    struct sockaddr_in server_addr, client_addr;
    socklen_t sin_size = sizeof(struct sockaddr_in);
    
    // 1. 创建监听Socket
    if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("Creating socket failed.");
        exit(1);
    }

    // 2. 设置端口复用选项,便于调试重启服务
    int opt = 1;
    setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 3. 设置非阻塞模式
    setnonblocking(sock_fd);

第2步:绑定地址并启动Epoll监控

完成Socket基础配置后,填充本地IP与端口信息进行绑定。同时,创建一个Epoll实例文件描述符,并将监听Socket加入监控列表,关注其读事件(EPOLLIN)。

    // 1. 配置本地地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("Bind error");
        exit(1);
    }

    // 2. 开启监听,BACKLOG参数控制等待队列长度
    if (listen(sock_fd, BACKLOG) == -1) {
        perror("Listen error");
        exit(1);
    }

    // 3. 创建Epoll句柄
    epfd = epoll_create(EPOLL_SIZE);
    if (epfd == -1) {
        perror("Epoll create failed");
        exit(1);
    }

    // 4. 将监听Socket添加到Epoll管理
    struct epoll_event ev, events[EPOLL_SIZE];
    ev.data.fd = sock_fd;
    ev.events = EPOLLIN | EPOLLET; // ET模式下通常配合非阻塞使用
    epoll_ctl(epfd, EPOLL_CTL_ADD, sock_fd, &ev); 

第3步:主循环与事件分发处理

进入无限循环,使用epoll_wait()等待就绪事件。根据触发事件的类型,分别处理新连接请求、客户端读取及响应写入。

    printf("Server started on port %d...\n", PORT);
    while (1) {
        nfds = epoll_wait(epfd, events, EPOLL_SIZE, -1);
        if (nfds == -1) continue;
        for (i = 0; i < nfds; i++) {
            // 情况A: 有新的客户端连接
            if (events[i].data.fd == sock_fd) {
                int conn_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &sin_size);
                if (conn_fd == -1) {
                    perror("Accept error");
                    continue;
                }
                printf("Got connection from %s\n", inet_ntoa(client_addr.sin_addr));
                setnonblocking(conn_fd);

                // 将新连接加入Epoll,只关心读事件
                ev.data.fd = conn_fd;
                ev.events = EPOLLIN | EPOLLET;
                epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);
            }
            // 情况B: 当前连接有可读数据
            else if (events[i].events & EPOLLIN) {
                int read_fd = events[i].data.fd;
                char buffer[1024];
                int nbytes = recv(read_fd, buffer, sizeof(buffer), 0);
                if (nbytes == -1) {
                    perror("Read error");
                    continue; // 或者关闭连接
                } else if (nbytes == 0) {
                    // 客户端正常断开
                    close(read_fd);
                    epoll_ctl(epfd, EPOLL_CTL_DEL, read_fd, NULL);
                    continue;
                }
                buffer[nbytes] = '\0';
                printf("Client says: %s\n", buffer);

                // 切换事件类型为写,准备回应
                ev.data.fd = read_fd;
                ev.events = EPOLLOUT | EPOLLET;
                epoll_ctl(epfd, EPOLL_CTL_MOD, read_fd, &ev);
            }
            // 情况C: 当前连接有可写能力
            else if (events[i].events & EPOLLOUT) {
                int write_fd = events[i].data.fd;
                const char *response = "Message received!\n";
                send(write_fd, response, strlen(response), 0);

                // 回应完成后,重新切回关注读事件
                ev.data.fd = write_fd;
                ev.events = EPOLLIN | EPOLLET;
                epoll_ctl(epfd, EPOLL_CTL_MOD, write_fd, &ev);
            }
        }
    }
    return 0;
}

客户端测试代码

为了方便验证服务端功能,可以编译以下简单的客户端代码进行联调测试:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>

int main(int argc, char **argv) {
    int sock_fd;
    struct sockaddr_in server_addr;
    struct hostent *server;

    if (argc < 2) {
        fprintf(stderr, "usage: ./client hostname \n");
        exit(0);
    }

    // 解析主机名
    if ((server = gethostbyname(argv[1])) == NULL) {
        herror("ERROR resolving hostname");
        exit(1);
    }

    if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("Error creating socket");
        exit(1);
    }

    // 配置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(4444);
    memcpy(&(server_addr.sin_addr.s_addr), &(server->h_addr), server->h_length);

    if (connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("Connection failed");
        exit(1);
    }

    char buffer[1024];
    printf("Connected to server! Enter text ('quit' to exit):\n");
    while (fgets(buffer, sizeof(buffer), stdin) != NULL) {
        if (strcmp(buffer, "quit\n") == 0) break;

        write(sock_fd, buffer, strlen(buffer));
        read(sock_fd, buffer, sizeof(buffer));
        printf("From Server: %s", buffer);
    }
    close(sock_fd);
    return 0;
}

编译与运行指南

假设上述两段代码保存为server.c和client.c,请按以下步骤执行:

  1. 编译服务端:gcc -o server server.c -lpthread(若线程相关扩展可添加该库)。
  2. 启动服务端:./server。
  3. 启动客户端(新开终端):./client 127.0.0.1。

输入文本后,客户端将收到服务端发回的确认信息,表明Epoll机制成功实现了并发下的读写分离处理。

以上就是C语言利用Epoll构建高性能TCP服务器的详细内容,更多关于网络编程进阶优化的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表