linux 网络:并发服务器及IO多路复用

目录

一、服务器模型:从单客户端到多客户端

[1. 核心概念与基础流程](#1. 核心概念与基础流程)

二、单循环服务器(迭代服务器)

[1. 实现代码](#1. 实现代码)

[2. 核心特点](#2. 核心特点)

三、并发服务器:多进程与多线程模型

[1. 核心思想](#1. 核心思想)

[2. 多进程并发服务器](#2. 多进程并发服务器)

(1)实现代码

(2)关键细节

(3)优缺点

[3. 多线程并发服务器](#3. 多线程并发服务器)

(1)实现代码

(2)关键细节

(3)优缺点

[四、IO 模型:阻塞与非阻塞](#四、IO 模型:阻塞与非阻塞)

[1. 阻塞 IO 模型(默认)](#1. 阻塞 IO 模型(默认))

[2. 非阻塞 IO 模型](#2. 非阻塞 IO 模型)

[五、IO 多路复用(高并发)](#五、IO 多路复用(高并发))

[1. 核心思想](#1. 核心思想)

[2. select函数(基础 IO 多路复用)](#2. select函数(基础 IO 多路复用))

(1)核心函数与参数

[(2)select 服务器实现](#(2)select 服务器实现)

[(3)select 优缺点](#(3)select 优缺点)

[3. poll函数(优化版)](#3. poll函数(优化版))

(1)核心函数与参数

[(2)poll 服务器实现](#(2)poll 服务器实现)

[(3)poll 优缺点](#(3)poll 优缺点)

[4. epoll函数(高性能)(Linux 特有)](#4. epoll函数(高性能)(Linux 特有))

(1)核心函数与参数

[(2)epoll 服务器实现](#(2)epoll 服务器实现)

[(3)epoll 的触发(关键优化)](#(3)epoll 的触发(关键优化))

[(4)epoll 优缺点](#(4)epoll 优缺点)

六、服务器模型对比

一、服务器模型:从单客户端到多客户端

1. 核心概念与基础流程

网络服务器的核心是通过socket接口实现客户端与服务器的通信,基础流程包含 4 个关键步骤:

  1. 创建 socket :生成用于通信的文件描述符(listenfd
  2. 绑定地址(bind) :将socket与服务器的 IP 和端口绑定
  3. 监听连接(listen) :使socket进入监听状态,创建连接请求队列
  4. 接受连接(accept) :从请求队列中提取客户端连接,生成通信文件描述符(connfd

二、单循环服务器(迭代服务器)

1. 实现代码

复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>

int main() {
    int listenfd, connfd;
    struct sockaddr_in serv_addr, cli_addr;
    socklen_t cli_len = sizeof(cli_addr);
    char buf[1024];

    // 1. 创建socket(TCP协议)
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) { perror("socket fail"); return -1; }

    // 2. 绑定地址(IP+端口)
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;         // IPv4协议
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡
    serv_addr.sin_port = htons(8080);       // 端口号(主机字节序转网络字节序)
    if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("bind fail"); return -1;
    }

    // 3. 监听连接(请求队列大小默认)
    if (listen(listenfd, 5) < 0) { perror("listen fail"); return -1; }

    // 4. 单循环处理客户端(一次只能处理一个)
    while (1) {
        // 接受客户端连接(阻塞,直到有连接请求)
        connfd = accept(listenfd, (struct sockaddr*)&cli_addr, &cli_len);
        if (connfd < 0) { perror("accept fail"); continue; }

        // 与客户端通信(循环读写)
        while (1) {
            memset(buf, 0, sizeof(buf));
            // 读客户端数据(阻塞,直到有数据)
            int n = read(connfd, buf, sizeof(buf)-1);
            if (n <= 0) { 
                printf("client disconnect\n"); 
                break; // 客户端断开或读错误
            }
            printf("recv from client: %s", buf);

            // 向客户端回送数据
            sprintf(buf, "server reply: %s", buf);
            write(connfd, buf, strlen(buf));
        }

        // 关闭通信socket
        close(connfd);
    }

    // 关闭监听socket(实际不会执行,需信号处理)
    close(listenfd);
    return 0;
}

2. 核心特点

  • 优点:逻辑简单,代码量少,适合学习基础流程
  • 缺点
    1. 一次只能处理一个客户端,其他客户端需排队等待
    2. 若当前客户端通信耗时(如大文件传输),后续客户端会严重阻塞
    3. 效率极低,仅适用于测试或极低并发场景

三、并发服务器:多进程与多线程模型

1. 核心思想

将 "接受连接" 与 "通信" 两个任务分离:

  • 父进程 / 主线程:仅负责accept接受新连接
  • 子进程 / 子线程:为每个新连接创建独立进程 / 线程,专门处理该客户端的通信
  • 实现 "同时处理多个客户端" 的并发能力

2. 多进程并发服务器

(1)实现代码
复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <signal.h>

// 信号处理:回收僵尸进程(避免资源泄漏)
void sig_chld(int sig) {
    while (waitpid(-1, NULL, WNOHANG) > 0); // 非阻塞回收所有子进程
}

// 子进程:处理单个客户端通信
void do_client(int connfd) {
    char buf[1024];
    while (1) {
        memset(buf, 0, sizeof(buf));
        int n = read(connfd, buf, sizeof(buf)-1);
        if (n <= 0) {
            printf("client disconnect (pid: %d)\n", getpid());
            break;
        }
        printf("pid: %d, recv: %s", getpid(), buf);

        sprintf(buf, "server(pid:%d) reply: %s", getpid(), buf);
        write(connfd, buf, strlen(buf));
    }
    close(connfd); // 子进程关闭通信socket
    exit(0);       // 子进程退出
}

int main() {
    int listenfd, connfd;
    struct sockaddr_in serv_addr, cli_addr;
    socklen_t cli_len = sizeof(cli_addr);
    pid_t pid;

    // 注册信号处理函数(回收僵尸进程)
    signal(SIGCHLD, sig_chld);

    // 1. 创建socket
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) { perror("socket fail"); return -1; }

    // 关键:开启地址重用(避免服务器重启时端口被占用)
    int on = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    // 2. 绑定地址
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(8080);
    if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("bind fail"); return -1;
    }

    // 3. 监听连接
    if (listen(listenfd, 5) < 0) { perror("listen fail"); return -1; }

    // 4. 父进程循环接受连接,创建子进程处理通信
    while (1) {
        connfd = accept(listenfd, (struct sockaddr*)&cli_addr, &cli_len);
        if (connfd < 0) { perror("accept fail"); continue; }

        // 创建子进程
        pid = fork();
        if (pid < 0) { 
            perror("fork fail"); 
            close(connfd); // 创建失败需关闭connfd,避免泄漏
            continue;
        } else if (pid == 0) { 
            close(listenfd); // 子进程不需要监听socket,关闭!
            do_client(connfd); // 子进程处理通信
        } else { 
            close(connfd); // 父进程不需要通信socket,关闭!
        }
    }

    close(listenfd);
    return 0;
}
(2)关键细节
  • 地址重用 :通过setsockopt设置,解决服务器重启时 "端口已被占用(TIME_WAIT 状态)" 的问题
  • 僵尸进程回收 :通过SIGCHLD信号和waitpid非阻塞回收,避免子进程退出后成为僵尸进程占用资源
  • 文件描述符关闭
    • 子进程必须关闭listenfd(无需监听新连接)
    • 父进程必须关闭connfd(无需与客户端通信)
(3)优缺点
  • 优点
    1. 实现真正的并发,多个客户端可同时通信
    2. 进程间地址空间独立,一个客户端崩溃不影响其他
  • 缺点
    1. 进程创建 / 销毁开销大(内存、CPU 资源占用高)
    2. 进程间通信复杂(需管道、共享内存等)
    3. 并发量受限(系统能创建的进程数有限)

3. 多线程并发服务器

(1)实现代码
复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 线程参数:需用结构体封装(pthread_create仅支持单个void*参数)
typedef struct {
    int connfd;
    struct sockaddr_in cli_addr;
} ThreadArg;

// 线程处理函数:处理单个客户端通信
void* do_client(void* arg) {
    ThreadArg* targ = (ThreadArg*)arg;
    int connfd = targ->connfd;
    char buf[1024];

    // 关键:设置线程分离属性(无需主线程pthread_join回收)
    pthread_detach(pthread_self());
    free(targ); // 释放参数内存

    while (1) {
        memset(buf, 0, sizeof(buf));
        int n = read(connfd, buf, sizeof(buf)-1);
        if (n <= 0) {
            printf("client disconnect (tid: %lu)\n", pthread_self());
            break;
        }
        printf("tid: %lu, recv: %s", pthread_self(), buf);

        sprintf(buf, "server(tid:%lu) reply: %s", pthread_self(), buf);
        write(connfd, buf, strlen(buf));
    }

    close(connfd);
    return NULL;
}

int main() {
    int listenfd, connfd;
    struct sockaddr_in serv_addr, cli_addr;
    socklen_t cli_len = sizeof(cli_addr);
    pthread_t tid;

    // 1. 创建socket
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) { perror("socket fail"); return -1; }

    // 开启地址重用
    int on = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    // 2. 绑定地址
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(8080);
    if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("bind fail"); return -1;
    }

    // 3. 监听连接
    if (listen(listenfd, 5) < 0) { perror("listen fail"); return -1; }

    // 4. 主线程循环接受连接,创建子线程处理通信
    while (1) {
        connfd = accept(listenfd, (struct sockaddr*)&cli_addr, &cli_len);
        if (connfd < 0) { perror("accept fail"); continue; }

        // 分配线程参数(堆内存,避免栈内存被覆盖)
        ThreadArg* targ = (ThreadArg*)malloc(sizeof(ThreadArg));
        targ->connfd = connfd;
        targ->cli_addr = cli_addr;

        // 创建子线程
        if (pthread_create(&tid, NULL, do_client, targ) != 0) {
            perror("pthread_create fail");
            free(targ);
            close(connfd);
            continue;
        }
    }

    close(listenfd);
    return 0;
}
(2)关键细节
  • 线程参数传递 :必须用堆内存(malloc)封装参数,避免栈内存被主线程循环覆盖
  • 线程分离(pthread_detach) :设置后线程退出时自动释放资源,无需主线程调用pthread_join
  • 资源共享 :线程共享进程地址空间(如全局变量),需注意互斥锁(pthread_mutex_t)保护共享资源
(3)优缺点
  • 优点
    1. 线程创建 / 销毁开销远小于进程(共享进程内存,无需复制地址空间)
    2. 线程间通信简单(直接访问全局变量,需加锁)
    3. 支持更高的并发量
  • 缺点
    1. 线程共享地址空间,一个线程崩溃可能导致整个进程崩溃
    2. 需处理线程安全问题(互斥、同步),代码复杂度高于多进程

四、IO 模型:阻塞与非阻塞

1. 阻塞 IO 模型(默认)

  • 定义 :当调用read/write/accept等 IO 函数时,若资源未就绪,进程 / 线程会一直等待(阻塞),直到资源就绪才返回
  • eg
    • read(connfd, buf, ...):若客户端未发送数据,read会阻塞,进程暂停执行
    • accept(listenfd, ...):若没有新连接请求,accept会阻塞
  • 特点:逻辑简单,但 IO 等待时 CPU 空闲,资源利用率低

2. 非阻塞 IO 模型

  • 定义 :通过fcntl设置文件描述符为非阻塞模式后,IO 函数会立即返回

    • 资源就绪:返回实际读写的字节数
    • 资源未就绪:返回-1,并设置errno = EAGAINEWOULDBLOCK
  • 实现代码(设置非阻塞)

    #include <fcntl.h>

    // 将fd设置为非阻塞模式
    int set_nonblock(int fd) {
    int flags = fcntl(fd, F_GETFL, 0); // 获取当前文件状态标志
    if (flags < 0) { perror("fcntl F_GETFL fail"); return -1; }
    // 添加非阻塞标志(O_NONBLOCK),不影响其他标志
    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
    perror("fcntl F_SETFL fail"); return -1;
    }
    return 0;
    }

  • 特点

    • 优点:IO 等待时 CPU 可处理其他任务,资源利用率高
    • 缺点:需通过 "轮询"(循环调用 IO 函数)检查资源是否就绪,会占用大量 CPU 时间

五、IO 多路复用(高并发)

1. 核心思想

  • 问题:多进程 / 多线程模型中,每个客户端对应一个进程 / 线程,并发量高时资源开销大;非阻塞 IO 的轮询机制 CPU 利用率低
  • 解决方案 :用一个进程 / 线程 监控多个文件描述符(IO 事件),仅当某个文件描述符就绪(有数据可读 / 可写)时才处理,实现 "多路 IO 复用一个进程 / 线程"
  • 适用场景:高并发服务器(如 Web 服务器、即时通讯服务器),支持上万级并发

2. select函数(基础 IO 多路复用)

(1)核心函数与参数
复制代码
#include <sys/select.h>

int select(int nfds, 
           fd_set *readfds,  // 监控"读就绪"的fd集合
           fd_set *writefds, // 监控"写就绪"的fd集合
           fd_set *exceptfds,// 监控"异常"的fd集合
           struct timeval *timeout); // 超时时间
  • 关键宏(操作 fd 集合)
    • FD_ZERO(fd_set *set):清空 fd 集合
    • FD_SET(int fd, fd_set *set):将 fd 添加到集合
    • FD_CLR(int fd, fd_set *set):将 fd 从集合中移除
    • FD_ISSET(int fd, fd_set *set):判断 fd 是否在就绪集合中
  • 参数说明
    • nfds:监控的 fd 的最大值 + 1(select 按 fd 序号遍历,需知道遍历上限)
    • timeout
      • NULL:永久阻塞,直到有 fd 就绪
      • tv_sec=0, tv_usec=0:非阻塞,立即返回
      • 其他值:阻塞指定时间(秒 + 微秒),超时后返回
(2)select 服务器实现
复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>

#define MAX_FD 1024 // select默认最大监控fd数(FD_SETSIZE)

int main() {
    int listenfd, connfd, maxfd;
    struct sockaddr_in serv_addr, cli_addr;
    socklen_t cli_len = sizeof(cli_addr);
    fd_set readfds, tmpfds; // readfds:总集合;tmpfds:临时集合(select会修改)
    char buf[1024];

    // 1. 创建socket
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) { perror("socket fail"); return -1; }

    // 开启地址重用
    int on = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    // 2. 绑定地址
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(8080);
    if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("bind fail"); return -1;
    }

    // 3. 监听连接
    if (listen(listenfd, 5) < 0) { perror("listen fail"); return -1; }

    // 4. 初始化select监控集合
    FD_ZERO(&readfds);
    FD_SET(listenfd, &readfds); // 监控listenfd(新连接就绪)
    maxfd = listenfd; // 初始最大fd为listenfd

    while (1) {
        tmpfds = readfds; // 复制集合(select会修改原集合,需备份)

        // 调用select监控读就绪事件(永久阻塞)
        int ret = select(maxfd + 1, &tmpfds, NULL, NULL, NULL);
        if (ret < 0) { perror("select fail"); continue; }
        else if (ret == 0) { printf("select timeout\n"); continue; }

        // 遍历所有监控的fd,判断是否就绪
        for (int i = 0; i <= maxfd; i++) {
            if (FD_ISSET(i, &tmpfds)) { // i fd就绪
                if (i == listenfd) { // 新连接就绪
                    connfd = accept(listenfd, (struct sockaddr*)&cli_addr, &cli_len);
                    if (connfd < 0) { perror("accept fail"); continue; }

                    // 将新的connfd加入监控集合
                    FD_SET(connfd, &readfds);
                    if (connfd > maxfd) { maxfd = connfd; } // 更新最大fd
                    printf("new client connect, connfd: %d\n", connfd);
                } else { // 客户端通信fd就绪(有数据可读)
                    memset(buf, 0, sizeof(buf));
                    int n = read(i, buf, sizeof(buf)-1);
                    if (n <= 0) { // 客户端断开或读错误
                        printf("client disconnect, connfd: %d\n", i);
                        FD_CLR(i, &readfds); // 从监控集合中移除
                        close(i); // 关闭fd
                        // 优化:更新maxfd(避免后续无效遍历)
                        for (int j = maxfd; j >= 0; j--) {
                            if (FD_ISSET(j, &readfds)) {
                                maxfd = j;
                                break;
                            }
                        }
                    } else { // 正常读取数据
                        printf("recv from connfd %d: %s", i, buf);
                        sprintf(buf, "server reply: %s", buf);
                        write(i, buf, strlen(buf));
                    }
                }
            }
        }

        close(listenfd);
        return 0;
    }
}
(3)select 优缺点
  • 优点
    1. 跨平台支持(Windows、Linux、macOS)
    2. 实现简单,适合入门学习
  • 缺点
    1. 最大监控 fd 数受限(默认FD_SETSIZE=1024,修改需重新编译内核)
    2. 每次调用需复制 fd 集合到内核,开销大(fd 数多时明显)
    3. 返回后需遍历所有 fd 判断就绪状态,时间复杂度O(n)
    4. 每次调用需重新初始化 fd 集合(内核会修改原集合)

3. poll函数(优化版)

(1)核心函数与参数
复制代码
#include <poll.h>

int poll(struct pollfd *fds,  // 监控的fd数组
         nfds_t nfds,         // 数组中fd的数量
         int timeout);        // 超时时间(ms):-1=永久阻塞,0=非阻塞,>0=阻塞ms
  • struct pollfd结构体

    struct pollfd {
    int fd; // 要监控的文件描述符(-1表示忽略)
    short events; // 期望监控的事件(输入参数)
    short revents; // 实际就绪的事件(输出参数)
    };

  • 常用事件标志

    • POLLIN:读就绪(有数据可读)
    • POLLOUT:写就绪(有空间可写)
    • POLLERR:错误事件(无需主动设置,内核自动返回)
(2)poll 服务器实现
复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <poll.h>
#include <stdio.h>

#define MAX_CLIENT 1024 // 最大支持客户端数

int main() {
    int listenfd, connfd, nfds = 0;
    struct sockaddr_in serv_addr, cli_addr;
    socklen_t cli_len = sizeof(cli_addr);
    struct pollfd fds[MAX_CLIENT]; // poll监控的fd数组
    char buf[1024];

    // 1. 创建socket
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) { perror("socket fail"); return -1; }

    // 开启地址重用
    int on = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    // 2. 绑定地址
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(8080);
    if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("bind fail"); return -1;
    }

    // 3. 监听连接
    if (listen(listenfd, 5) < 0) { perror("listen fail"); return -1; }

    // 4. 初始化poll监控数组
    memset(fds, 0, sizeof(fds));
    fds[0].fd = listenfd;       // 第0个元素监控listenfd
    fds[0].events = POLLIN;     // 监控读就绪(新连接)
    nfds = 1;                   // 初始监控fd数量为1

    while (1) {
        // 调用poll监控事件(永久阻塞)
        int ret = poll(fds, nfds, -1);
        if (ret < 0) { perror("poll fail"); continue; }
        else if (ret == 0) { printf("poll timeout\n"); continue; }

        // 遍历监控数组,处理就绪fd
        for (int i = 0; i < nfds; i++) {
            if (fds[i].revents & POLLIN) { // 读就绪事件
                if (fds[i].fd == listenfd) { // 新连接就绪
                    connfd = accept(listenfd, (struct sockaddr*)&cli_addr, &cli_len);
                    if (connfd < 0) { perror("accept fail"); continue; }

                    // 检查是否超过最大客户端数
                    if (nfds >= MAX_CLIENT) {
                        printf("too many clients\n");
                        close(connfd);
                        continue;
                    }

                    // 将新connfd加入poll数组
                    fds[nfds].fd = connfd;
                    fds[nfds].events = POLLIN; // 监控读就绪
                    nfds++; // 增加监控fd数量
                    printf("new client, connfd: %d, total: %d\n", connfd, nfds-1);
                } else { // 客户端通信fd就绪
                    memset(buf, 0, sizeof(buf));
                    int n = read(fds[i].fd, buf, sizeof(buf)-1);
                    if (n <= 0) { // 客户端断开
                        printf("client disconnect, connfd: %d\n", fds[i].fd);
                        close(fds[i].fd);
                        // 移除该fd:用最后一个元素覆盖,减少数组遍历
                        fds[i] = fds[nfds - 1];
                        nfds--;
                        i--; // 重新检查当前位置(已被覆盖)
                    } else { // 正常通信
                        printf("recv from connfd %d: %s", fds[i].fd, buf);
                        sprintf(buf, "server reply: %s", buf);
                        write(fds[i].fd, buf, strlen(buf));
                    }
                }
            }
        }
    }

    close(listenfd);
    return 0;
}
(3)poll 优缺点
  • 优点(对比 select)
    1. 无最大 fd 数限制(仅受限于MAX_CLIENT和系统 fd 上限)
    2. 无需重新初始化监控集合(events输入,revents输出,分离)
    3. 无需计算maxfd,直接遍历数组,代码更简洁
  • 缺点
    1. 每次调用仍需将整个fds数组复制到内核,fd 数多时开销大
    2. 返回后需遍历所有 fd 判断就绪状态,时间复杂度O(n)

4. epoll函数(高性能)(Linux 特有)

(1)核心函数与参数

epoll 通过 3 个函数实现,采用 "事件驱动" 模型,仅返回就绪的 fd,效率极高:

函数 功能
epoll_create(int size) 创建 epoll 实例(返回 epoll fd),size已忽略(需 > 0)
epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 控制 epoll 实例(添加 / 修改 / 删除 fd 监控)
epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) 等待就绪事件(返回就绪 fd 的数量)
  • struct epoll_event结构体

    typedef union epoll_data {
    void *ptr; // 自定义数据(如客户端信息)
    int fd; // 监控的fd
    uint32_t u32;
    uint64_t u64;
    } epoll_data_t;

    struct epoll_event {
    uint32_t events; // 监控的事件
    epoll_data_t data; // 关联的数据(通常存fd)
    };

  • 关键参数与事件

    • epoll_ctlop
      • EPOLL_CTL_ADD:添加 fd 到 epoll 实例
      • EPOLL_CTL_MOD:修改 fd 的监控事件
      • EPOLL_CTL_DEL:从 epoll 实例中删除 fd
    • events标志:
      • EPOLLIN:读就绪
      • EPOLLOUT:写就绪
      • EPOLLET:边沿触发(ET 模式,高效,默认水平触发 LT)
      • EPOLLONESHOT:只触发一次事件,需重新添加监控
(2)epoll 服务器实现
复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <sys/epoll.h>
#include <stdio.h>

#define MAX_EVENTS 1024 // 每次epoll_wait返回的最大就绪事件数

int main() {
    int listenfd, connfd, epfd;
    struct sockaddr_in serv_addr, cli_addr;
    socklen_t cli_len = sizeof(cli_addr);
    struct epoll_event ev, events[MAX_EVENTS]; // ev:添加事件;events:就绪事件
    char buf[1024];

    // 1. 创建socket
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) { perror("socket fail"); return -1; }

    // 开启地址重用
    int on = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    // 2. 绑定地址
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(8080);
    if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("bind fail"); return -1;
    }

    // 3. 监听连接
    if (listen(listenfd, 5) < 0) { perror("listen fail"); return -1; }

    // 4. 创建epoll实例
    epfd = epoll_create(1); // size=1(已忽略)
    if (epfd < 0) { perror("epoll_create fail"); return -1; }

    // 5. 将listenfd添加到epoll监控(读就绪事件)
    ev.events = EPOLLIN;    // 水平触发(LT),默认
    ev.data.fd = listenfd;  // 关联listenfd
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev) < 0) {
        perror("epoll_ctl add listenfd fail"); return -1;
    }

    while (1) {
        // 等待就绪事件(永久阻塞,超时时间-1)
        int nready = epoll_wait(epfd, events, MAX_EVENTS, -1);
        if (nready < 0) { perror("epoll_wait fail"); continue; }
        else if (nready == 0) { printf("epoll_wait timeout\n"); continue; }

        // 遍历就绪事件(仅处理nready个,效率高)
        for (int i = 0; i < nready; i++) {
            int fd = events[i].data.fd;

            if (fd == listenfd) { // 新连接就绪
                connfd = accept(listenfd, (struct sockaddr*)&cli_addr, &cli_len);
                if (connfd < 0) { perror("accept fail"); continue; }

                // 将新connfd添加到epoll监控
                ev.events = EPOLLIN;
                ev.data.fd = connfd;
                if (epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev) < 0) {
                    perror("epoll_ctl add connfd fail");
                    close(connfd);
                    continue;
                }
                printf("new client, connfd: %d\n", connfd);
            } else { // 客户端通信fd就绪
                memset(buf, 0, sizeof(buf));
                int n = read(fd, buf, sizeof(buf)-1);
                if (n <= 0) { // 客户端断开或读错误
                    printf("client disconnect, connfd: %d\n", fd);
                    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); // 从epoll中删除
                    close(fd);
                } else { // 正常通信
                    printf("recv from connfd %d: %s", fd, buf);
                    sprintf(buf, "server reply: %s", buf);
                    write(fd, buf, strlen(buf));
                }
            }
        }
    }

    // 释放资源
    close(listenfd);
    close(epfd);
    return 0;
}
(3)epoll 的触发(关键优化)
  • 水平触发(LT)

    • 只要 fd 就绪(如还有数据可读),每次epoll_wait都会返回该 fd
    • 优点:逻辑简单,无需一次性读完所有数据
    • 缺点:若数据未读完,会重复触发,略有开销
  • 边沿触发(ET)

    • 仅在 fd 状态从 "未就绪" 变为 "就绪" 时触发一次(如数据刚到达时)
    • 优点:触发次数少,效率极高,适合高并发
    • 缺点:需一次性读完所有数据(用非阻塞 fd + 循环读),否则后续数据无法触发
  • 边沿触发实现

    // 添加connfd时设置ET模式 + 非阻塞
    set_nonblock(connfd); // 先设置fd为非阻塞
    ev.events = EPOLLIN | EPOLLET; // 开启边沿触发
    ev.data.fd = connfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);

    // 读数据时循环读取(直到errno=EAGAIN)
    int n;
    while (1) {
    memset(buf, 0, sizeof(buf));
    n = read(fd, buf, sizeof(buf)-1);
    if (n > 0) {
    // 处理数据
    printf("recv: %s", buf);
    } else if (n == 0) {
    // 客户端断开
    break;
    } else {
    if (errno == EAGAIN || errno == EWOULDBLOCK) {
    // 数据已读完,退出循环
    break;
    } else {
    // 其他错误
    perror("read fail");
    break;
    }
    }
    }

(4)epoll 优缺点
  • 优点(对比 select/poll)
    1. 高效的事件通知机制:仅返回就绪 fd,时间复杂度O(1)
    2. 无 fd 数限制(仅受系统 fd 上限)
    3. 共享内存机制:fd 集合无需每次复制到内核(仅初始化时复制一次)
    4. 支持 LT/ET 两种触发模式,灵活适配不同场景
  • 缺点
    1. 仅支持 Linux 系统,不跨平台
    2. 代码复杂度高于 select/poll(尤其是 ET 模式)

六、服务器模型对比

模型 并发能力 资源开销 代码复杂度 适用场景
单循环服务器 极低(1 个客户端) 测试、学习
多进程服务器 中(数百个) 高(进程创建 / 通信) 要求稳定性、进程独立的场景
多线程服务器 中高(数千个) 中(线程创建 / 锁) 中高(线程安全) 中等并发、需共享资源的场景
select 低(≤1024) 中(fd 复制 / 遍历) 跨平台、低并发场景
poll 中(数千个) 中(数组复制 / 遍历) 跨平台、中等并发场景
epoll(ET) 高(数万至数十万) 低(事件驱动) 高(ET 模式) Linux 高并发服务器(Web、IM、游戏)
相关推荐
蜗牛沐雨1 小时前
驾驭巨量数据:HTTP 协议与大文件传输的多种策略
网络·网络协议·http
问道飞鱼1 小时前
【Linux知识】Linux 设置账号密码永不过期
linux·账号·过期·密码过期
NewCarRen1 小时前
汽车盲点检测系统的网络安全分析和设计
网络·安全·汽车网络安全
skywalk81632 小时前
升级DrRacket8.10到8.18版本@Ubuntu24.04
linux·运维·服务器·lisp·racket
邂逅星河浪漫2 小时前
Docker 详解+示例
linux·docker·容器·kafka
NormalConfidence_Man2 小时前
【RT Thread】使用QEMU模拟器结合GDB调试RT Thread内核
linux·嵌入式硬件
Linux技术芯3 小时前
详细介绍Linux 内存管理 struct page数据结构中有一个锁,请问trylock_page()和lock_page()有什么区别?
linux
钮钴禄·爱因斯晨3 小时前
Linux(一) | 初识Linux与目录管理基础命令掌握
linux·运维·服务器
AllyLi02243 小时前
CondaError: Run ‘conda init‘ before ‘conda activate‘
linux·开发语言·笔记·python
BioRunYiXue4 小时前
FRET、PLA、Co-IP和GST pull-down有何区别? 应该如何选择?
java·服务器·网络·人工智能·网络协议·tcp/ip·eclipse