前言

哈喽大家好~ 今天跟大家深度拆解一下Linux高并发编程的核心——epoll。做过服务端开发的同学应该都知道,IO多路复用是解决“单线程管理海量连接”的关键,而epoll作为Linux专属的最优方案,几乎是所有高性能服务(Nginx、Redis等)的底层核心。
在这里插入图片描述

一、epoll到底是什么?核心作用与应用场景

在讲复杂原理前,先搞懂最基础的:epoll的核心价值是什么?什么时候该用它?

  1. 核心作用
    epoll 是 Linux 系统特有的 IO多路转接模型,核心作用就是:批量监听大量文件描述符(fd),高效检测哪些fd发生了可读、可写或异常事件
    简单说:传统IO一次只能处理一个连接,而epoll能让一个线程同时“盯着”成千上万个连接,不用阻塞在单个连接上,极大提升服务器的并发处理能力,解决传统IO的性能瓶颈。
  2. 主流应用场景
    只要涉及“高并发连接”,基本都离不开epoll,常见场景:
  • 高性能Web服务器:Nginx的底层网络监听核心,支撑百万级并发
  • 缓存数据库:Redis的网络事件调度,处理大量客户端连接
  • 后端服务:TCP长连接、网关服务、游戏服务端
  • 即时通讯:聊天APP、消息推送、直播流媒体服务
  • 嵌入式Linux:高并发IO读写程序(如工业控制、物联网设备)

总结:只要你的服务需要同时处理上千、上万甚至百万级连接,epoll就是最优选择。

二、epoll底层逻辑与工作原理(大白话拆解)

很多同学觉得epoll难,其实核心就是理解它的内核底层结构和工作流程,用大白话讲清楚,一点都不复杂。

  1. 内核底层两大核心结构
    epoll在内核中会维护两个关键数据结构,这也是它比select、poll高效的核心原因:
  • 红黑树:存储所有用户注册的“待监听fd”,负责快速增、删、改、查(时间复杂度O(logn))。比如你新增一个监听的socket,就会插入这棵树;删除一个连接,就从树中移除。
  • 就绪双向链表:专门存放“已经触发IO事件的fd”(比如有数据可读、可写)。这个链表的作用就是“筛选”——不用遍历所有fd,只需要处理链表中的fd即可。
  1. 完整工作流程(4步走)
    用通俗的方式拆解,就像“服务员盯桌子”的逻辑:
  • 创建“监听台”:调用epoll_create(),在内核中创建一个epoll实例(相当于服务员的“工作台”),同时初始化红黑树和就绪链表。
  • 登记“要盯的桌子”:通过epoll_ctl(),把需要监听的fd(比如客户端socket)注册到红黑树中(相当于服务员记下所有要服务的桌子)。
  • 主动“报菜”:内核持续监听红黑树中的fd,一旦某个fd触发事件(比如客户端发来了数据),内核会主动把这个fd移入就绪链表(相当于客人举手,服务员立刻记下)。
  • 处理“订单”:调用epoll_wait(),从就绪链表中取出所有触发事件的fd,交给用户态处理(相当于服务员去服务举手的客人,不用一个个问所有桌子)。

核心优势:摒弃了select/poll “逐个问桌子” 的轮询模式,采用“客人主动举手”的事件回调机制,海量空闲连接不会消耗系统资源,效率直接拉满。

三、epoll内核三大核心函数(必懂!附参数详解)

epoll的使用非常固定,核心就是三个函数:epoll_create()、epoll_ctl()、epoll_wait(),分工明确,掌握这三个函数,就掌握了epoll的基本使用。
下面逐个拆解,包括函数原型、作用、参数含义

epoll_create()—— 创建epoll实例

//函数原型
#include <sys/epoll.h>
int epoll_create(int size);

核心作用
在内核中创建一个epoll监听实例,开辟空间存储红黑树和就绪链表,最终返回一个“epoll专属文件描述符”(相当于这个实例的“身份证”)。

参数解析

  • size:早期版本用于指定“最大监听fd数量”,现在已经失效,仅作占位使用(随便传一个正数,比如1024、4096都可以)。
    返回值
    成功:返回epoll实例的文件描述符(非负整数);失败:返回-1(比如内核空间不足)。

epoll_ctl()—— 管理监听事件(增/改/删)

这是epoll的“核心管理函数”,负责给epoll实例添加、修改、删除监听的fd和事件。

//函数原型
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

核心作用
对epoll实例(epfd)中的监听fd执行“添加、修改、删除”操作,是连接用户态和内核态的关键
参数详解(重点!)

  • epfd:epoll实例的文件描述符(由epoll_create()返回,相当于指定“操作哪个工作台”)。

  • op:操作指令,只能是以下三个宏之一,决定对fd做什么操作:

    • EPOLL_CTL_ADD:新增监听——把fd添加到epoll的红黑树中,开始监听。
    • EPOLL_CTL_MOD:修改监听——修改已存在的fd的监听事件(比如从“监听读”改成“监听读写”)。
    • EPOLL_CTL_DEL:删除监听——把fd从红黑树中移除,不再监听(比如客户端断开连接后)。
  • fd:需要监听的目标文件描述符(比如socket、必须是有效的、非阻塞的fd,)。

  • event:指向epoll_event结构体的指针,用于设置“监听什么事件”和“事件触发后返回什么数据”,结构体定义如下:

   struct epoll_event {
    uint32_t events;  // 监听的事件类型(宏组合)
    epoll_data_t data; // 用户数据,事件触发后回传给用户态
};
// 共用体,最常用的是data.fd(存储监听的fd)
typedef union epoll_data {
    void        *ptr;
    int          fd;    // 重点:存要监听的文件描述符
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;

补充:events常用宏(可按位或组合):

  • EPOLLIN:监听读事件(有数据可读)。
  • EPOLLOUT:监听写事件(可发送数据)。
  • EPOLLET:设置为边缘触发模式(默认是水平触发)。
  • EPOLLERR:监听错误(内核自动监听,无需手动添加)。
    返回值
    成功:返回0;失败:返回-1(比如fd无效、重复添加fd)。

epoll_wait()—— 阻塞等待事件就绪

负责“等待”就绪事件,从内核的就绪链表中取出事件,交给用户态处理。

//函数原型
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

核心作用
阻塞(或非阻塞)等待epoll实例中的fd触发事件,一旦有事件就绪,就把事件拷贝到用户态的events数组中,返回就绪事件的数量。
参数详解

  • epfd:epoll实例的文件描述符(指定监听哪个实例)。

  • events:用户态定义的epoll_event数组,用于存放内核返回的“就绪事件”(相当于“接收订单的容器”)。

  • maxevents:单次最多能获取的就绪事件数量,不能大于events数组的大小(比如数组大小是1024,maxevents就传1024)。

  • timeout:阻塞等待的超时时间(单位:毫秒),分三种情况:

    • -1:永久阻塞,直到有事件触发才返回。
    • 0:非阻塞,不管有没有事件,立即返回(常用于轮询)。
    • 正数:阻塞指定毫秒数,超时后无事件则返回0。

返回值

  • 大于0:返回就绪事件的数量(即有多少个fd触发了事件)。
  • 等于0:超时无事件。
  • 等于-1:调用失败(比如epfd无效)。
    在这里插入图片描述

四、epoll两种工作模式:LT vs ET(详细拆解,必区分!)

epoll有两种触发模式:水平触发(LT)和边缘触发(ET),这是epoll的核心知识点,也是面试高频考点,很多同学容易混淆,这里详细拆解,讲清区别和使用场景。

LT 水平触发(Level Trigger)—— 默认模式,新手首选

LT是epoll的默认触发模式,逻辑简单、不易出错,适合大多数日常开发场景。
核心触发规则
只要文件缓冲区中存在未处理的数据,每次调用epoll_wait(),都会持续重复通知该fd“就绪”。
举个例子:客户端给服务器发了1000字节数据,服务器第一次读取了500字节,还剩500字节在缓冲区,那么下一次调用epoll_wait(),会再次通知这个fd“可读”,直到所有数据都被读完。
运行特点

  • 数据可分次读取/写入,无需一次性处理完。
  • 支持阻塞IO和非阻塞IO(不用强制设置非阻塞)。
  • 事件会重复触发,直到数据处理完毕。

优缺点

  • 优点:逻辑简单、编码难度低、不易丢失事件、稳定性高,调试方便。
  • 缺点:空闲数据会重复触发事件,频繁唤醒线程,高并发场景下会有性能损耗。

适用场景
日常业务开发、低并发服务、新手入门网络程序(比如小型接口服务)。

ET 边缘触发(Edge Trigger)—— 高性能模式,高并发首选

ET是epoll的高性能模式,也是Nginx、Redis等高性能服务的首选模式,核心是“减少事件唤醒次数”,提升并发效率,但编码难度更高。

核心触发规则
仅在fd状态发生改变的一瞬间,触发一次事件,后续即使有未处理的数据,也不会再触发。

举个例子:客户端发了1000字节数据,服务器第一次调用epoll_wait()时,会收到“可读”通知,如果程序本次仅读取 500 字节,缓冲区还剩余 500 字节未读取,在没有新数据抵达缓冲区的前提下,再次调用 epoll_wait 将不会重复触发可读事件

强制使用规范(必看!)
使用ET模式,必须遵守两个规则,否则极易丢数据:

    1. 监听的fd必须设置为非阻塞IO(防止一次读/写阻塞,导致其他fd无法处理)。
    1. 事件触发后,必须循环读/写,直到缓冲区无数据(一次性处理完所有数据)。

运行特点

  • 同一批数据仅触发一次事件,极大减少epoll_wait()的唤醒次数。
  • 内核开销极低,资源利用率拉满,适合海量并发。
  • 必须严格遵循使用规范,否则会丢失数据。

优缺点

  • 优点:唤醒次数少、并发性能顶尖,是百万级并发服务器的首选。
  • 缺点:编码难度高,逻辑严谨,处理失误会直接丢包、丢数据。

适用场景
Nginx、Redis、高并发网关、海量长连接服务端(比如直播、即时通讯)。
两种模式核心区别总结

  • 通知频率:LT持续通知(直到数据处理完),ET仅状态跳转时通知一次。
  • IO模式:LT支持阻塞/非阻塞,ET强制非阻塞。
  • 数据处理:LT可分次处理,ET必须一次性处理完毕。
  • 性能:ET远优于LT。
  • 开发难度:LT简单易上手,ET严谨易错。

解释为什么ET远优于LT
边缘触发减少了大量无效的事件通知与线程唤醒,降低系统调用和上下文切换开销,在海量并发长连接场景下资源利用率更高,所以整体运行效率远超默认的水平触发。

五、select、poll、epoll 三大IO多路复用模型对比(表格清晰版)

对比维度 select poll epoll()
跨平台性 全平台支持(Windows、Linux、Unix) 全平台支持 仅Linux系统
监听fd上限 有固定上限(默认1024,可修改但麻烦) 无数量限制 无数量限制
内核存储结构 位图数组 动态链表 红黑树 + 就绪链表
事件检索方式 全量轮询所有fd(效率低) 全量轮询所有fd(效率一般) 仅遍历就绪fd(效率极高)
数据拷贝开销 用户态与内核态频繁拷贝(开销大) 频繁拷贝(开销大) 内存映射(mmap),拷贝开销极小
触发模式 仅支持LT水平触发 仅支持LT水平触发 LT默认 + ET边缘触发(可选)
并发性能 差,连接越多,轮询开销越大,卡顿明显 中等,无fd上限,但轮询依旧低效,海量连接性能下滑 极佳,海量连接下性能稳定,无明显卡顿
编码复杂度 偏高,参数繁琐(需要维护三个fd集合) 简单,只需维护一个链表 中等,基础使用简单,ET模式难度高
主流使用场景 老旧项目兼容、简单跨平台项目 跨平台简易服务、低并发场景 Linux高性能高并发服务(Nginx、Redis等)
致命缺点 fd上限、轮询效率极低 无事件优化,轮询浪费系统资源 平台不通用,ET模式易丢失数据

使用epoll实现一个客服端和服务端(简单回显客户端发的消息)

在这里插入图片描述

使用的软件:vscode ,xshell
工作模式:LT水平触发模式

实现这个demo还是比较简单的。这里一共用到了七个文件:分别是:
EpollServer.hpp,EpollServer.cc(服务端),EpollClinet.cc(客户端),common.hpp,InetAddr.hpp,Makefile ,Socket.hpp

EpollServer.hpp

#pragma once

#include <iostream>
#include <memory>
#include <unistd.h>
#include <sys/epoll.h>
#include "Socket.hpp"

class EpollServer
{
    const static int size = 64;
    const static int defaultfd = -1;

public:
    EpollServer(int port) : _listensock(std::make_unique<TcpSocket>()), _isrunning(false), _epfd(defaultfd)
    {
        // 1. 创建listensocket
        _listensock->TcpSocketBulid(port);
        // 2. 创建epoll模型
        _epfd = epoll_create(256);
        if (_epfd < 0)
        {
            std::cout << "EPOLL_ERR" << std::endl;
            exit(EPOLL_ERR);
        }
        std::cout << "EPOLL_SUCCESS" << std::endl;

        // 3. 将listensocket设置到内核中!
        struct epoll_event ev; // 有没有设置到内核中,有没有rb_tree中新增节点??没有!!
        ev.events = EPOLLIN;
        ev.data.fd = _listensock->Socketfd(); // 这里未来是维护的是用户的数据,常见的是fd
        int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->Socketfd(), &ev);
        if (n < 0)
        {
            std::cout << "EPOLL_CTL_ERR" << std::endl;
            exit(EPOLL_CTL_ERR);
        }
        std::cout << "EPOLL_CTL_SUCCESS" << std::endl;
    }
    void Start()
    {
        int timeout = -1;
        _isrunning = true;
        while (_isrunning)
        {
            // 能不能直接accept呢??不能!应该干什么?
            int n = epoll_wait(_epfd, _revs, size, timeout);
            switch (n)
            {
            case 0:
                std::cout << "timeout..." << std::endl;
                break;
            case -1:
                std::cout << "epoll error" << std::endl;
                break;
            default:
                Dispatcher(n);
                break;
            }
        }

        _isrunning = false;
    }
    // 事件派发器
    void Dispatcher(int rnum)
    {
        std::cout << "event ready ..." << std::endl; // LT: 水平触发模式--epoll默认
        for (int i = 0; i < rnum; i++)
        {
            // epoll也要循环处理就绪事件--这是应该的,本来就有可能有多个fd就绪!
            int sockfd = _revs[i].data.fd;
            uint32_t revent = _revs[i].events;
            if (revent & EPOLLIN)
            { // 读事件就绪
                // listensockfd ready? normal socfd ready??
                if (sockfd == _listensock->Socketfd())
                {
                    // 读事件就绪 && 新连接到来
                    Accepter();
                }
                else
                {
                    // 读事件就绪 && 普通socket可读
                    Recver(sockfd);
                }
            }
            // if(_revs[i].events & EPOLLOUT)
            // {// 写事件就绪

            // }
        }
    }
    // 链接管理器
    void Accepter()
    {
        InetAddr client;
        // 新连接到来 --- 至少有一个连接到来 --- accept一次 --- 绝对不会阻塞
        int sockfd = _listensock->Accept(&client); // 
        if (sockfd >= 0)
        {
            // read/recv(), sockfd是否读就绪,我们不清楚
         
            std::cout << "get a new link, sockfd: " << sockfd << ", client is: " << client.StringAddr() << std::endl;
            struct epoll_event ev;
            ev.events = EPOLLIN;
            ev.data.fd = sockfd;
            int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sockfd, &ev);
            if (n < 0)
            {
                std::cout << "add listensockfd failed" << std::endl;
            }
            else
            {
                std::cout << "epoll_ctl add sockfd success: " << sockfd << std::endl;
            }
        }
    }

    // IO处理器
    void Recver(int sockfd)
    {
        char buffer[1024];
        // 我在这里读取的时候,会不会阻塞? 本次读取,不会被阻塞
        ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0); // recv写的时候有bug吗?
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "client say@ " << buffer << std::endl;
            std::string echo = "server say: ";
            echo += buffer;
            send(sockfd, echo.c_str(), echo.size(), 0);
        }
        else if (n == 0)
        {
            std::cout << "clien quit..." << std::endl;
            // 2. 从epoll中移除fd的关心 && 关闭fd -- 细节:epoll_ctl: 只能移除合法fd -- 先移除,在关闭!!
            int m = epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
            if (m > 0)
            {
                std::cout << "epoll_ctl remove sockfd success: " << sockfd << std::endl;
            }
            close(sockfd);
        }
        else
        {
            std::cout << "recv error" << std::endl;
            // 2. 关闭fd
            int ret = epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
            if (ret > 0)
            {
                std::cout << "epoll_ctl remove sockfd success: " << sockfd << std::endl;
            }
            close(sockfd);
        }
    }
    void Stop()
    {
        _isrunning = false;
    }
    ~EpollServer()
    {
        _listensock->Close();
        if (_epfd > 0)
            close(_epfd);
    }

private:
    std::unique_ptr<Socket> _listensock;
    bool _isrunning;
    int _epfd;

    struct epoll_event _revs[size];
};

EpollServer.cc(服务端)

#include <iostream>
#include "InetAddr.hpp"
#include "Common.hpp"
#include "EpollServer.hpp"
int main(int argc, char *argv[])
{
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<EpollServer> strv = std::make_unique<EpollServer>(port);
    strv->Start();
    return 0;
}

EpollClinet.cc(客户端)

#include <iostream>
#include "InetAddr.hpp"
#include "Common.hpp"
#include "EpollServer.hpp"
int main(int argc, char *argv[])
{
    uint16_t port = std::stoi(argv[2]);
    std::string ip = argv[1];

    std::unique_ptr<Socket> sock = std::make_unique<TcpSocket>();
    sock->SocketBuild();

    int n = sock->Connect(ip, port);
    if (n < 0)
    {
        std::cout << "connect error" << std::endl;
        exit(CONNECT_ERR);
    }
    while (true)
    {
        std::cout << "please Enter: " << std::endl;
        std::string line;
        std::getline(std::cin, line);
        int w = send(sock->Socketfd(), line.c_str(), line.size(), 0);
        (void)w;

        char buffer[1024];
        memset(&buffer, 0, sizeof(buffer));
        int s = recv(sock->Socketfd(), buffer, sizeof(buffer) - 1, 0);
        if (s > 0)
        {
            buffer[s] = 0;

            std::cout << buffer << std::endl;
        }
        else if (s == 0)
        {
            std::cout << "server quit " << std::endl;
            break;
        }
        else
        {
            std::cout << "recv error " << std::endl;
            break;
        }
    }
    return 0;
}

common.hpp

#include <iostream>
#pragma once
#include <iostream>
#include <iostream>
#include <functional>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

enum Exitcode
{
    OK = 0,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR,
    FORK_ERR,
    EPOLL_ERR,
    EPOLL_CTL_ERR

};
class NoCopy
{
public:
    NoCopy() {}
    NoCopy(const NoCopy &) = delete;
    const NoCopy &operator=(const NoCopy &) = delete;
    ~NoCopy() {}
};
#define CONV(addr) ((struct sockaddr *)&addr)

InetAddr.hpp

#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include "Common.hpp"
class InetAddr
{
public:
    InetAddr() {}
    InetAddr(const struct sockaddr_in &addr) // 网络格式转换成本地格式
        : _addr(addr)
    {
        SetAddr(_addr);
    }

    InetAddr(uint16_t port)
        : _port(port)
    {

        bzero(&_addr, sizeof(_addr)); // 清空sockaddr_in
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port); // 本地格式转化成网络格式
        _addr.sin_addr.s_addr = INADDR_ANY;
    }
    InetAddr(std::string ip, uint16_t port)
        : _ip(ip), _port(port)
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        _addr.sin_addr.s_addr = inet_addr(_ip.c_str());
    }
    std::string StringAddr() const
    {
        return _ip + ":" + std::to_string(_port);
    }
    bool operator==(const InetAddr &addr)
    {
        return addr._ip == _ip && addr._port == _port;
    }

    // 返回port和ip
    uint16_t port() { return _port; }
    std::string ip() { return _ip; }

    const struct sockaddr_in &NetAddr() const { return _addr; }

    const struct sockaddr *NetAddrPtr() const { return CONV(_addr); }

    socklen_t InetAddrLen() { return sizeof(_addr); }
    void SetAddr(struct sockaddr_in &addr)
    {
        _port = ntohs(addr.sin_port);
        //_ip = inet_ntoa(_addr.sin_addr); // 四字节网络ip风格转换成点分十进制
        char ipbuffer[64];
        inet_ntop(AF_INET, &addr.sin_addr, ipbuffer, sizeof(ipbuffer));
        _ip = ipbuffer;
    }

    ~InetAddr() {}

private:
    struct sockaddr_in _addr;
    uint16_t _port;
    std::string _ip;
};

Socket.hpp

#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include <unistd.h>
#include <memory>
#include "Common.hpp"
#include "InetAddr.hpp"

class Socket : public NoCopy
{
public:
    Socket() {}
    virtual void SocketBuild() = 0;
    virtual void SocketBind(uint16_t poer) = 0;
    virtual void SocketListen() = 0;
    virtual int Accept(InetAddr *client) = 0;
    virtual int Socketfd() = 0;
    virtual void Close() = 0;
    virtual int Recv(std::string *out) = 0;
    virtual ssize_t Write(std::string &line) = 0;
    virtual int Connect(std::string &server_ip, uint16_t server_port) = 0;

    void TcpSocketBulid(uint16_t port)
    {
        SocketBuild();
        SocketBind(port);
        SocketListen();
    }
    void UdpSocketBuild(uint16_t port)
    {
        SocketBuild();
        SocketBind(port);
    }
    ~Socket() {}

private:
};
const static int sockfd = -1;
class TcpSocket : public Socket
{
public:
    TcpSocket() : _socket(sockfd) {}
    // TcpSocket(int fd) : _socket(fd) {}
    void SocketBuild() override
    {
        _socket = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_socket < 0)
        {
            std::cout << "socket error" << std::endl;
            exit(SOCKET_ERR);
        }
        std::cout << "socket success:" << _socket << std::endl;
    }
    void SocketBind(uint16_t port) override
    {
        InetAddr peer(port);
        int b = ::bind(_socket, peer.NetAddrPtr(), peer.InetAddrLen());
        if (b < 0)
        {
            std::cout << "BIND error" << strerror(errno) << std::endl;
            exit(BIND_ERR);
        }
        std::cout << "bind success:" << _socket << std::endl;
    }
    void SocketListen() override
    {
        int n = ::listen(_socket, backlog);
        if (n < 0)
        {
            std::cout << "listen error" << std::endl;
            exit(LISTEN_ERR);
        }
        std::cout << "listen success:" << _socket << std::endl;
    }
    int Accept(InetAddr *client) override
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int fd = ::accept(_socket, CONV(peer), &len);
        if (fd < 0)
        {
            std::cout << "accept error" << std::endl;
            return -1;
        }
        else
            client->SetAddr(peer);
        return fd;
    }
    int Connect(std::string &server_ip, uint16_t server_port) override
    {
        InetAddr client(server_ip, server_port);
        return connect(_socket, client.NetAddrPtr(), client.InetAddrLen());
    }
    int Recv(std::string *out) override
    {
        char buffer[1024];
        ssize_t s = read(_socket, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;
            *out += buffer;
        }
        return s;
    }
    ssize_t Write(std::string &line) override
    {
        return write(_socket, line.c_str(), line.size());
    }
    int Socketfd() override { return _socket; }
    void Close() override
    {
        if (_socket >= 0)
        {
            ::close(_socket);
        }
    }
    ~TcpSocket() {}

private:
    int _socket;
    int backlog = 253;
};

Makefile

.PHONY:all
all: epollserver epollclient

epollserver:EpollServer.cc
	g++ -o $@ $^ -std=c++17
epollclient:EpollClient.cc
	g++ -o $@ $^ -std=c++17
.PYONY:clean
clean:
	rm -f epollserver epollclient

七、总结与实战建议

看到这里,相信大家对epoll已经有了全面的了解,最后给大家几个实战建议,帮大家避坑:

  • Linux平台开发,优先使用epoll;跨平台开发,优先使用poll(比select更灵活)。
  • 日常业务开发,优先用LT模式(稳为主);追求极致性能,用ET模式+非阻塞IO(严格遵循使用规范)。
  • epoll的核心优势是“事件回调+就绪链表”,避开select/poll的轮询坑,适合海量并发。
  • 新手入门,先掌握三大核心函数的使用,再尝试ET模式,避免一开始就踩坑。
    以上就是epoll的全方位详解啦,从原理到API,再到触发模式和模型对比,覆盖了开发和面试的核心知识点。如果有疑问,欢迎在评论区留言讨论~
    后续会补充epoll实战代码(ET模式完整示例—>reactor的实现),关注我,不迷路!

在这里插入图片描述

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐