一.epoll概念

epoll 是一个在 Linux 操作体系上不同 I/O 模型挑选中常用的事情告诉机制。它是一种高效的、可扩展的 I/O 事情告诉机制,用于监督文件描绘符上的事情并进行异步事情驱动的 I/O 操作。

以下是 epoll 的一些关键概念:

  1. 事情驱动: epoll 经过监听文件描绘符上的事情来完成事情驱动的异步 I/O 操作。可以监听多个文件描绘符,并依据文件描绘符上产生的事情进行相应的处理。

  2. 高功能和可扩展性: epoll 运用了依据事情的规划,经过供给高效的内核等级事情告诉机制,可以在大规模并发的网络运用中处理不计其数个衔接而不受功能影响。

  3. 文件描绘符: 在 epoll 中,需求注册要监督的文件描绘符。文件描绘符可以是套接字、管道、文件等,用于标识打开的文件或 I/O 通道。

  4. 事情类型: epoll 中存在几种不同类型的事情,例如可读(EPOLLIN)、可写(EPOLLOUT)、过错(EPOLLERR)等。每个事情都与一个文件描绘符相相关,指示该文件描绘符上产生了相应的事情。

  5. 安排妥当列表: 经过调用 epoll_wait 函数来获取现已安排妥当的事情列表。这个函数会堵塞,直到有事情抵达或超时。一旦事情抵达,该函数回来安排妥当的文件描绘符及相应的事情类型,供运用程序进一步处理。

epoll 在与非堵塞 I/O 结合运用时,可以完成高效的事情驱动网络编程。它相对于传统的 select 和 poll 等事情告诉机制来说,在大规模并发运用中具有更好的功能和可扩展性。

二.epoll的特色

epoll 具有以下几个首要特色:

  1. 高效的事情告诉机制:epoll 运用了依据事情的规划,经过集中式的内核事情表来办理文件描绘符上的事情,防止了遍历整个文件描绘符调集的开支。这样可以进步事情告诉的功率,尤其在大规模并发的网络运用中表现出色。

  2. 大规模并发支撑:epoll 支撑大规模并发衔接,可以一起监听不计其数的文件描绘符,而不受功能影响。这使得它特别适用于高功能、高并发的服务器运用,如Web服务器、消息行列等。

  3. 边际触发和水平触发:epoll 供给了两种监听形式:边际触发(Edge-Triggered)和水平触发(Level-Triggered)。边际触发形式会在事情状况产生改变时告诉运用程序,而水平触发形式会在文件描绘符处于安排妥当状况时持续告诉运用程序。开发者可以依据实践需求挑选恰当的触发形式。

  4. 支撑多种文件描绘符类型:epoll 不仅可以用于网络套接字(socket),还可以用于管道(pipe)、定时器(timerfd)和设备文件等其他类型的文件描绘符。

  5. 内核级编程接口:epoll 供给了一套内核级的编程接口,可以在用户空间直接运用。开发者可以经过 epoll_createepoll_ctlepoll_wait 等函数来创立 epoll 实例,注册和取消事情,并等候安排妥当事情。

总的来说,epoll 是一种高效且可扩展的事情告诉机制,适用于高功能的网络编程。它的优势在于可一起监听很多的文件描绘符,而不会有功能瓶颈,并能依据事情状况改变或可读/可写状况持续告诉运用程序。这使得开发者可以运用异步的事情驱动方法来处理并发衔接。

三.epoll机制与原理

epoll机制的完成首要依赖于红黑树和安排妥当链表两个中心数据结构。

  1. 红黑树:在Linux内核中,epoll中的每个监督的文件描绘符都会维护一个对应的红黑树节点。这些红黑树节点依照文件描绘符的巨细有序摆放,方便进行快速的查找和刺进操作。红黑树的特色是平衡性和快速的查找、刺进和删去操作。

  2. 安排妥当链表:为了进步功率,epoll维护了一个安排妥当链表,用于记录所有现已安排妥当的文件描绘符。当一个文件描绘符上的事情被触发时,内核会将该文件描绘符增加到安排妥当链表中。

epoll的作业流程如下:

  1. 运用程序运用epoll_create函数创立一个epoll文件描绘符,用于办理需求监督的文件描绘符调集。

  2. 运用epoll_ctl函数将需求监督的文件描绘符增加到epoll文件描绘符中,指定重视的事情类型(如可读、可写等)。

  3. 调用epoll_wait函数堵塞当时线程,并等候注册的事情产生。当有一个或多个文件描绘符上的事情安排妥当时,内核会将安排妥当的文件描绘符增加到安排妥当链表中。

  4. 运用程序从安排妥当链表中获取安排妥当的文件描绘符,并对其进行处理,如读取或写入数据。

经过运用红黑树和安排妥当链表,epoll可以高效地进行文件描绘符的办理和事情的触发。红黑树供给了快速的查找和刺进操作,保证了监督的文件描绘符调集的高效拜访;而安排妥当链表供给了快速的事情告诉,防止了遍历整个文件描绘符调集的开支。这种机制使得epoll在高并发场景下具有很好的功能。

Epoll:高性能网络编程的必备利器

四.epoll的函数接口

1.epoll_create

epoll_create函数是Linux体系中用于创立epoll实例的函数。epoll是一种高效的I/O事情告诉机制,用于监听很多文件描绘符上的事情。

函数原型如下:

int epoll_create(int size);

参数size是epoll实例的巨细,指定了可以一起监听的最大文件描绘符数量。但是,自从Linux 2.6.8内核版本开始,这个参数现已不再生效,可以传入恣意值。

函数回来一个非负整数,表明创立的epoll实例的文件描绘符。假如回来值为负数,则表明创立失败,经过检查errno变量获取详细的过错信息。

运用epoll实例进行事情监听的首要过程如下:

  1. 创立epoll实例:调用epoll_create函数创立一个epoll实例,获取其文件描绘符。
  2. 增加事情:运用epoll_ctl函数将需求监听的文件描绘符增加到epoll实例中,并指定需求监听的事情类型。
  3. 等候事情产生:调用epoll_wait函数等候事情的产生,该函数会堵塞直到有事情产生或超时。
  4. 处理事情:当epoll_wait函数回来时,表明有事情产生,可以经过遍历回来的事情行列来获取详细的事情信息。
  5. 重复过程3和4,完成对多个文件描绘符的事情监听。

运用epoll机制可以完成高效的事情驱动编程,并且可以一起监听很多的文件描绘符,防止了传统的轮询方法的功能问题。比较其他I/O事情告诉机制,如select和poll,epoll在大规模并发环境下表现更出色。经过合理运用epoll的各种接口,可以编写高功能的网络服务器、多线程运用程序等。

2.epoll_ctl

epoll_ctl函数是在epoll实例中增加、修正或删去文件描绘符的监听事情的函数。

函数原型如下:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数阐明:

  • epfd:epoll实例的文件描绘符。
  • op:操作类型,可以取以下三个值之一:
    • EPOLL_CTL_ADD:将文件描绘符fd增加到epoll实例中进行监听。
    • EPOLL_CTL_MOD:修正现已在epoll实例中监听的文件描绘符fd的监听事情。
    • EPOLL_CTL_DEL:将文件描绘符fd从epoll实例中移除,中止监听该文件描绘符上的事情。
  • fd:需求被增加、修正或删去的文件描绘符。
  • event:指向epoll_event结构的指针,该结构用于指定需求监听的事情类型。

struct epoll_event结构界说如下:

typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;
struct epoll_event {
    uint32_t events;      // 事情类型
    epoll_data_t data;    // 用户数据
};

events字段是需求监听的事情类型,可以经过EPOLLINEPOLLOUTEPOLLERR等常量进行设置。常用的事情类型有:

  • EPOLLIN:表明可读事情。
  • EPOLLOUT:表明可写事情。
  • EPOLLERR:表明过错事情。
  • EPOLLHUP:表明挂起事情。 详细的事情类型可以经过man epoll_ctl指令在终端上检查。

data字段用于存储与文件描绘符相关的用户数据,可以是一个指针、整数或64位无符号整数。

epoll_ctl函数用于办理epoll实例中的文件描绘符的监听事情,依据不同的操作类型(op),它可以增加、修正或删去文件描绘符的监听事情装备。在增加或修正操作时,需求经过epoll_event结构来指定需求监听的事情类型。经过调用epoll_ctl可以完成动态地增加、修正和删去文件描绘符的事情监听,然后灵敏地对不同的I/O事情进行呼应。

3.epoll_wait

epoll_wait函数是在epoll实例上等候事情产生的函数。它会堵塞当时线程,直到有事情安排妥当或者超时。

函数原型如下:

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

参数阐明:

  • epfd:epoll实例的文件描绘符。
  • events:指向epoll_event结构数组的指针,用于接收产生的事情信息。
  • maxevents:指定最大回来事情数量,即events数组的巨细。
  • timeout:等候超时时刻,单位为毫秒。传入-1表明无限等候,传入0表明立即回来,传入正整数表明等候超时时刻。

函数回来一个非负整数,表明安排妥当事情的数量。假如回来值为0,表明超时;假如回来值为负数,表明呈现过错,可以经过检查errno变量获取详细的过错信息。

在调用epoll_wait函数之前,需求先调用epoll_ctl函数将需求监听的文件描绘符增加到epoll实例中。

epoll_wait函数回来时,即表明有事情安排妥当。用户可以运用循环遍历回来的events数组,获取详细的事情信息。每个事情结构包含以下字段:

  • events:表明产生的事情类型,可以经过与EPOLLINEPOLLOUT等事情类型进行按位与操作来判别详细事情。
  • data:是一个epoll_data联合体类型,存储了与事情相关的数据。

经过调用epoll_wait函数,可以完成高效地等候和处理多个事情,防止了传统的轮询方法的功能问题。运用epoll机制可以构建高功能的事情驱动程序,例如网络服务器、实时通信运用等。

五.编程实战

运用epoll完成一个高并发服务器,当服务衔接成功后,客户端发送小写字母的字符串,服务器端发送其大写形式。

server.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <signal.h>
#include <sys/select.h>
#include <poll.h>
#include <sys/epoll.h>
int main(int argc, char const *argv[])
{
    //1.创立套接字,回来树立链接的文件描绘符
    int sockfp = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfp == -1)
    {
        perror("socket is err");
        exit(0);
    }
    printf("%d\n", sockfp);
    //2.绑定ip和端口号
    struct sockaddr_in saddr, caddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[1]));
    saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
    socklen_t len = sizeof(struct sockaddr_in);
    if (bind(sockfp, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind is err");
        exit(0);
    }
    //3.listen监听
    if (listen(sockfp, 5))
    {
        perror("liste err");
        exit(0);
    }
    //4.创立红黑树和安排妥当链表
    struct epoll_event event;
    struct epoll_event events[1024];
    int epfd = epoll_create(10);
    if (epfd < 0)
    {
        perror("epoll is err");
        exit(0);
    }
    //5.将关怀的文件描绘符上树
    event.events = EPOLLIN; //监听读事情
    event.data.fd = sockfp; //关怀的文件描绘符
    epoll_ctl(epfd, EPOLL_CTL_ADD, sockfp, &event);
    //6.进行相应的逻辑操作
    int size = 0;
    char buf[1024] = {0};
    while (1)
    {
        int ret = epoll_wait(epfd, events, 1024, -1);
        if (ret < 0)
        {
            perror("epoll_wait is err");
            exit(-1);
        }
        for (int i = 0; i < ret; ++i)
        {
            if (events[i].data.fd == sockfp)
            {
                int accepted = accept(sockfp, (struct sockaddr *)(&caddr), &len);
                if (accepted < 0)
                {
                    perror("accept is err");
                    exit(0);
                }
                printf("port:%d   ip:  %s\n", ntohs(caddr.sin_port), inet_ntoa(caddr.sin_addr));
                event.events = EPOLLIN;   //监听读事情
                event.data.fd = accepted; //关怀的文件描绘符
                epoll_ctl(epfd, EPOLL_CTL_ADD, accepted, &event);
            }
            else
            {
                int flage = recv(events[i].data.fd, buf, sizeof(buf), 0);
                if (flage < 0)
                {
                    perror("recv is err");
                }
                else if (flage == 0)
                {
                    printf("ip:%s is close\n", inet_ntoa(caddr.sin_addr));
                    close(events[i].data.fd);
                    epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
                }
                else
                {
                    size = strlen(buf);
                    for (int i = 0; i < size; ++i)
                    {
                        if (buf[i] >= 'a' && buf[i] <= 'z')
                            buf[i] = buf[i] + ('A' - 'a');
                        else
                            buf[i] = buf[i] + ('a' - 'A');
                    }
                    printf("%s\n", buf);
                    send(events[i].data.fd, buf, sizeof(buf), 0);
                }
            }
        }
    }
    return 0;
}

client.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
    //1.socket树立文件描绘符
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0)
    {
        perror("socket is err");
    }
    //2.connect衔接服务器
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[2]));
    saddr.sin_addr.s_addr = inet_addr(argv[1]);
    int flage = connect(fd, (struct sockaddr *)(&saddr), sizeof(saddr));
    if (flage < 0)
    {
        perror("connect is err");
    }
    //3.服务器端不断发送数据,承受服务器转化后的数据
    char buf[1024] = {0};
    while (1)
    {
        //memset(buf,0,sizeof(buf));
        fgets(buf, sizeof(buf), stdin);
        if (strncmp(buf,"quit#",5)==0)
        {
            break;
        }
        if (buf[strlen(buf) - 1] == '\n')
            buf[strlen(buf) - 1] = '\0';
        send(fd, buf, sizeof(buf), 0);
        flage = recv(fd, buf, sizeof(buf), 0);
        if (flage < 0)
        {
            perror("recv is err");
        }
        else
        {
            fprintf(stdout, "%s\n", buf);
        }
    }
    close(fd);
    return 0;
}

六.ET形式和LT形式

6.1概念

ET 形式和 LT 形式是在计算机编程中用来描绘事情触发方法的两种形式,特别是在网络编程和操作体系中广泛运用。下面是对这两种形式的解说:

  1. ET 形式(边际触发):

    • ET 形式是指当监督事情的文件描绘符上产生状况改变时,只会告诉一次,无论是否对事情进行处理。
    • 在 ET 形式下,假如有多个事情一起产生,而只读取其中一个事情,那么下次检查到事情时仍然会收到告诉。
    • ET 形式需求运用非堵塞 I/O 操作,以确保在事情处理期间没有堵塞。
  2. LT 形式(水平触发):

    • LT 形式是指当监督事情的文件描绘符上产生状况改变时,只需状况仍然处于活动状况,就会持续告诉。
    • 在 LT 形式下,假如有多个事情一起产生,而只读取其中一个事情,那么下次检查到事情时将不会再次告诉。
    • LT 形式可以运用堵塞和非堵塞 I/O 操作,但需求小心处理或许引起堵塞的状况。

整体来说,ET 形式对事情的处理要求愈加高效,需求确保在处理事情期间不会产生堵塞,不然或许会错过部分事情。而 LT 形式则答应在处理事情期间进行堵塞操作,但需求处理好每次告诉的事情,以防止重复处理现已处理过的事情。

ET形式:

  • 边际触发: 缓冲区剩下未读尽的数据会导致epo11_wait回来。

LT形式:

  • 水平触发–默许选用形式。 缓冲区剩下未读尽的数据不会导致epo11_wait回来。

在实践编程中,详细挑选运用 ET 形式仍是 LT 形式,要依据运用的要求和场景来决定。

6.2设置方法

 event.events = EPOLLIN;           //LT形式
 event.events = EPOLLIN|EPOLLET;   //ET形式

6.3ET非堵塞形式

ET非堵塞形式是指事情触发(Event-Triggered)的非堵塞形式,通常在编程中用于异步操作和事情驱动的场景。在非堵塞形式下,程序可以持续履行其他任务而不用等候当时任务完成。

ET非堵塞形式的完成通常依赖于时刻循环(Event Loop)。在事情驱动的架构中,程序会监听各种事情,并在事情触发时履行相应的回调函数或任务。这种形式可以进步程序的并发功能和呼应才能。

常见的运用场景包括网络编程中的异步IO操作,例如运用非堵塞Socket进行网络通信、处理很多客户端衔接;以及图形界面编程中的事情处理,例如用户点击按钮、鼠标移动等。

在ET非堵塞形式下,程序需求经过轮询或回调的方法监听事情,并及时呼应。比较于堵塞形式,在非堵塞形式下程序可以一起处理多个任务,进步体系的资源运用率和吞吐量。

简略来说,就是把recv/send设置为非堵塞形式,在缓冲区一直读或者一直写,直到不能写和读停止,减少epoll_wait的调用,进步功率。

6.4设置文件描绘符非堵塞

fcntl 是一个在 Unix-like 体系下运用的函数,用于对文件描绘符进行操控操作。它的原型如下:

#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* argument */);

其中,fd 是要操作的文件描绘符,cmd 是要履行的操作指令,... 是可选的额定参数,取决于详细的指令。

fcntl 函数首要用于履行以下几种操作:

  1. 文件描绘符仿制(F_DUPFD、F_DUPFD_CLOEXEC):运用 F_DUPFD 指令可以仿制一个文件描绘符,回来一个新的文件描绘符,指向与原始文件描绘符所指向的文件相同的位置和状况。而 F_DUPFD_CLOEXEC 指令除了仿制文件描绘符外,还将新的文件描绘符设置为在 exec 调用时主动封闭。

  2. 获取和设置文件状况标志(F_GETFL、F_SETFL):运用 F_GETFL 指令可以获取与文件描绘符相相关的状况标志,例如是否是非堵塞形式、是否是追加形式等。而 F_SETFL 指令可以用于设置文件描绘符的状况标志。常用的状况标志包括 O_NONBLOCK(非堵塞形式)、O_APPEND(追加形式)等。

  3. 文件操控操作(F_GETFD、F_SETFD):运用 F_GETFD 指令可以获取与文件描绘符相关的文件描绘符标志,例如 FD_CLOEXEC(在履行 exec 调用时封闭文件描绘符)等。而 F_SETFD 指令可以设置文件描绘符的标志。

  4. 文件锁(F_SETLK、F_SETLKW、F_GETLK):运用 F_SETLKF_SETLKW 指令可以给文件加锁,以防止其他进程对文件的并发拜访。文件锁可以是共享锁(读锁)或独占锁(写锁)。F_SETLK 是非堵塞方法加锁,假如锁现已被其他进程持有,则回来过错。而 F_SETLKW 则是堵塞方法加锁,假如锁现已被其他进程持有,则会等候直到获取到锁。运用 F_GETLK 指令可以获取文件的当时锁状况。

上述列举的仅是 fcntl 函数的一些常见操作,实践上 fcntl 可以支撑更多的操作指令,详细取决于地点的操作体系和文件体系的特性。在运用 fcntl 函数时,可以经过查阅体系的相关文档或 man 页面来获取更详细的信息和支撑的操作指令。 要将文件描绘符设置为非堵塞形式,可以运用 fcntl 函数以及指令 F_SETFL 和标志 O_NONBLOCK。以下是一个示例代码片段:

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
    int fd = open("example.txt", O_RDONLY); // 打开文件(示例)
    // 获取当时文件描绘符的状况标志
    int flags = fcntl(fd, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl error");
        return -1;
    }
    // 设置非堵塞形式
    flags |= O_NONBLOCK;
    int result = fcntl(fd, F_SETFL, flags);
    if (result == -1) {
        perror("fcntl error");
        return -1;
    }
    // 运用非堵塞形式进行读取操作
    char buffer[1024];
    ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
    if (bytesRead == -1) {
        perror("read error");
        return -1;
    }
    // 封闭文件描绘符
    close(fd);
    return 0;
}

在上述代码中,首要经过 open 函数打开了一个文件(示例中为 “example.txt”)。然后运用 fcntl 函数和 F_GETFL 指令获取当时文件描绘符的状况标志。接着,将 O_NONBLOCK 标志增加到状况标志中,以设置为非堵塞形式。最终,运用 fcntl 函数和 F_SETFL 指令将更新后的状况标志设置回文件描绘符。

之后,你可以运用非堵塞形式进行读取、写入等操作,例如运用 read 函数读取数据。需求留意的是,在非堵塞形式下,读操作或许会立即回来,即便没有数据可用。因此,在读取时需求处理回来值为 -1(表明犯错)和 0(表明暂时没有数据可读)的状况。

最终,运用 close 函数封闭文件描绘符,开释相关资源。

请留意,以上是一个简略示例,详细的运用方法和处理方法或许依据实践需求有所不同。在实践运用中,你或许需求依据详细的过错码和回来值进行恰当的过错处理和重试机制。

七.epoll反应堆模型

当运用epoll的反应堆模型时,咱们可以结合ET形式、void*指针、自界说结构体、盲轮询和回调函数来构建高效的事情驱动体系。

  1. ET形式(边际触发形式): 在ET形式下,只有当与文件描绘符相关的状况产生改变时,才会触发一次事情告诉。与LT形式(水平触发形式)比较,ET形式需求立即对事情做出呼应,不然或许会丢掉一些事情。这可以有效地减少事情告诉的次数,进步体系功率。

  2. void指针和自界说结构体: 为了可以在处理事情时传递自界说的数据,咱们可以运用void指针和自界说结构体。void指针可以存储恣意类型的数据,而自界说结构体可以安排和办理更复杂的信息。当注册文件描绘符的事情时,可以将自界说结构体作为void指针的值传递给epoll_ctl函数,以便在事情产生时获取相应的数据。

  3. 盲轮询: 在某些状况下,或许需求进行盲轮询,即在没有获取到任何事情时,等候一定的时刻后再次调用epoll_wait函数。这样可以防止过于频繁地进行体系调用,以进步功能。经过设置适宜的超时时刻,可以操控盲轮询的频率。

  4. 回调函数: 在epoll的反应堆模型中,通常会运用回调函数来处理事情。当读写事情产生时,可以调用相应的回调函数来处理数据,进行逻辑操作,并做出相应的呼应。回调函数可以依据自界说结构体中存储的信息来履行特定的操作,这样可以完成事情驱动的编程。

运用上述技术手段,可以搭建一个高效的epoll反应堆模型。经过ET形式,只触发关键的事情告诉;运用void*指针和自界说结构体,传递相关的数据;运用盲轮询来节约体系调用的开支;一起运用回调函数来处理详细的事情操作。这些技术一起效果,可以构建出高功能、可扩展的事情驱动体系。

以下是一个简略的示例代码,演示了如何运用epoll的反应堆模型来完成一个依据TCP/IP的网络服务器:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#define MAX_EVENTS 10
#define BUFFER_SIZE 1024
void handle_request(int client_sock) {
    char buffer[BUFFER_SIZE];
    memset(buffer, 0, BUFFER_SIZE);
    // 读取客户端发送的数据
    ssize_t recv_len = recv(client_sock, buffer, BUFFER_SIZE, 0);
    if (recv_len > 0) {
        printf("Received message from client: %s\n", buffer);
    } else if (recv_len == 0) {
        printf("Client disconnected\n");
        close(client_sock);
    } else {
        perror("Error in recv");
        close(client_sock);
    }
}
int main() {
    int server_sock, client_sock;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len;
    // 创立监听socket
    server_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sock < 0) {
        perror("Error in socket");
        exit(1);
    }
    // 绑定地址和端口
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(8080);
    if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("Error in bind");
        exit(1);
    }
    // 监听衔接恳求
    if (listen(server_sock, 5) < 0) {
        perror("Error in listen");
        exit(1);
    }
    // 创立epoll实例
    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        perror("Error in epoll_create1");
        exit(1);
    }
    struct epoll_event event, events[MAX_EVENTS];
    event.events = EPOLLIN;
    event.data.fd = server_sock;
    // 将监听socket增加到epoll的事情调集中
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_sock, &event) == -1) {
        perror("Error in epoll_ctl");
        exit(1);
    }
    while (1) {
        // 等候事情产生
        int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (num_events == -1) {
            perror("Error in epoll_wait");
            exit(1);
        }
        for (int i = 0; i < num_events; i++) {
            if (events[i].data.fd == server_sock) {
                // 有新的衔接恳求
                client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_addr_len);
                if (client_sock == -1) {
                    perror("Error in accept");
                    exit(1);
                }
                printf("Client connected\n");
                event.events = EPOLLIN;
                event.data.fd = client_sock;
                // 将客户端socket增加到epoll的事情调集中
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_sock, &event) == -1) {
                    perror("Error in epoll_ctl");
                    exit(1);
                }
            } else {
                // 读写事情产生
                handle_request(events[i].data.fd);
            }
        }
    }
    // 封闭监听socket
    close(server_sock);
    return 0;
}

上述代码首要创立了一个监听socket,并将其绑定到本地地址和端口上。然后,它进入一个无限循环,经过epoll_wait函数等候事情产生。当有新的衔接恳求抵达时,会将客户端socket增加到epoll的事情调集中。当读写事情产生时,会调用handle_request函数来处理恳求。在该函数中,咱们运用recv函数读取客户端发送的数据,并输出到操控台上。

经过运用epoll的反应堆模型,服务器可以高效地处理多个客户端衔接,并异步地处理事情,进步体系的功能和呼应才能。