• IO多路复用技术(二)


    一、概念

    epoll 全称 eventpoll,是 linux 内核实现IO多路复用的一个实现。
    epollselectpoll 的升级版,相较于这两个,epoll 改进了工作方式,因此它更加高效。

    • 对于待检测集合 selectpoll 是基于线性方式处理的,需要线性遍历;epoll 是基于红黑树来管理待检测集合的;
    • selectpoll 每次都会线性扫描整个待检测集合,集合越大速度越慢;epoll 使用的是回调机制,效率高,处理效率也不会随着检测集合的变大而下降;
    • 我们需要对 selectpoll 返回的集合进行判断才能知道哪些文件描述符是就绪的;而通过 epoll 可以直接得到已就绪的文件描述符集合,无需再次检测;
    • 使用 epoll 没有最大文件描述符的限制,仅受系统中进程能打开的最大文件数目限制;

    当连接数量比较大,IO处理比较频繁的时候,此时再使用 selectpoll 效率就比较低了。这时最好使用 epoll

    二、函数原型

    epoll 常用的 API 函数一共有三个:

    #include 
    // 创建epoll实例,通过一棵红黑树管理待检测集合
    int epoll_create(int size);
    // 管理红黑树上的文件描述符(添加、修改、删除)
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    // 检测epoll树中是否有就绪的文件描述符
    int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    1.epoll_create 是创建一个红黑树的实例,用来管理待检测的文件描述符的集合。

    int epoll_create(int size);
    
    • 1
    • size :在 Linux 2.6.8 版本以后,这个参数是被忽略的,只需要指定一个大于 0 0 0 的数即可;
      函数返回值:
      • 失败,返回 − 1 -1 1
      • 成功,返回一个有效的文件描述符 epfd ,通过这个文件描述符就能访问 epoll 实例了;

    2.epoll_ctl 是管理 epoll 实例上的节点,可以对其进行增加、修改、删除操作。

    // 联合体, 多个变量共用同一块内存        
    typedef union epoll_data {
     	void        *ptr;
    	int          fd;	// 通常情况下使用这个成员, 和epoll_ctl的第三个参数相同即可
    	uint32_t     u32;
    	uint64_t     u64;
    } epoll_data_t;
    
    struct epoll_event {
    	uint32_t     events;      /* Epoll events */
    	epoll_data_t data;        /* User data variable */
    };
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • epfdepoll_create() 的返回值,通过 epfd 就能访问 epoll 实例模型;
    • op :这是一个枚举量,通过它控制 epoll_ctl 进行对应的处理:
      • EPOLL_CTL_ADD:往epoll模型中添加新的节点
      • EPOLL_CTL_MOD:修改epoll模型中已经存在的节点
      • EPOLL_CTL_DEL:删除epoll模型中的指定的节点
    • fd :即要添加、修改、删除的文件描述符;
    • eventepoll 事件,用来指定给这个文件描述符 fd 对应的事件;
      • events :委托 epoll 检测的事件。
        • EPOLLIN :读事件,检测读缓冲区是否有数据;
        • EPOLLOUT:写事件,检测写缓冲区是否有容量;
        • EPOLLERR:异常事件;
      • data:用户数据变量。这是一个联合体类型,我们通常使用里面的 fd ,用于存储待检测的文件描述符的值。在调用 epoll_wait() 的时候,这个值会被传出来;

    函数返回值:

    • 成功,返回 0 0 0
    • 失败,返回 − 1 -1 1
    1. epoll_wait 是检测被创建的 epoll 实例中有没有就绪的文件描述符。
    int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    
    • 1
    • epfdepoll_create() 的返回值,通过 epfd 就能访问 epoll 实例模型;
    • events :这是一个传出参数,它是一个结构体数组的地址,里面存储了已经就绪的文件描述的信息;
    • maxevents :修饰 events ,表示其结构体数组的容量;
    • timeout:如果待检测的 epoll 实例中没有就绪的文件描述符,那么就会阻塞 timeout 毫秒。
      • 0 0 0 ,函数不阻塞。不管 epoll 实例中有没有已经就绪的文件描述符,函数执行完毕之后都直接返回;
      • > 0 > 0 >0,如果 epoll 实例中没有就绪的文件描述符,函数就会阻塞 timeout 毫秒再返回;
      • − 1 -1 1,函数一直阻塞,直到 epoll 实例中有文件描述符就绪就解除阻塞;

    函数返回值:

    • 成功:
      • 0 0 0,函数是被强制解除阻塞了,没有检测到就绪的文件描述符;
      • > 0 > 0 >0,检测到的已经就绪的文件描述符的总数量;
    • 失败,返回 − 1 -1 1

    三、epoll的使用

    在服务端使用 epoll 的步骤如下:

    1.创建用于监听的文件描述符 lfd

    //使用 ipv4 , TCP协议
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    
    • 1
    • 2

    2.设置端口复用(可选可不选)。

    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    • 1
    • 2

    3.绑定 ip 与 端口号。

    int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    
    • 1

    4.设置监听。

    listen(lfd, 128);
    
    • 1

    5.创建 epoll 实例。

    int epfd = epoll_create(100);
    
    • 1

    6.将用于监听的文件描述符 lfd 添加到 epoll 实例中去。

    struct epoll_event ev;
    ev.events = EPOLLIN;    // 检测lfd读读缓冲区是否有数据
    ev.data.fd = lfd;
    int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
    
    • 1
    • 2
    • 3
    • 4

    7.检测 epoll 实例中是否有文件描述符就绪了,并对这些就绪的文件描述符做相应的处理。

    //cnt 是 epoll 实例中已经就绪的文件描述符的数量
    int cnt= epoll_wait(epfd, evs, size, -1);
    
    • 1
    • 2

    8.如果是用于监听的文件描述符 lfd 就绪了,那么就和客户端建立连接,将得到的用于通信的文件描述符 cfd 添加到 epoll 实例中。

    int cfd = accept(lfd, NULL, NULL);
    ev.events = EPOLLIN;
    ev.data.fd = cfd;
    // 新得到的文件描述符添加到epoll模型中, 下一轮循环的时候就可以被检测了
    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    9.如果是用于通信的文件描述符 cfd 就绪了,那么就和客户端进行通信。如果连接已经断开,就将用于通信的文件描述符 cfdepoll 实例中删除,并且关闭这个文件描述符。

    int len = recv(curfd, buf, sizeof(buf), 0);
    if(len == 0)
    {
        // 将这个文件描述符从epoll模型中删除
        epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
        close(curfd);
    }
    else if(len > 0)
    {
        send(curfd, buf, len, 0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    10.重复第 7 7 7 步。

    1.代码

    客户端的代码:

    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(){
    
        //1.创建用于通信的文件描述符 cfd
        int cfd = socket(AF_INET,SOCK_STREAM,0);
        if(cfd == -1){
            perror("socket");
            return -1;
        }
    
        printf("1.成功创建了用于通信的文件描述符 : %d\n",cfd);
    
        //2.连接服务器
        unsigned short port = 10000;
        const char* ip = "10.0.8.14";
    
        struct sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        inet_pton(AF_INET,ip,&addr.sin_addr.s_addr);
    
        int ret = connect(cfd,(struct sockaddr*)&addr,sizeof(addr));
        if(ret == -1){
            perror("connet");
            return -1;
        }
        printf("2.成功连接了服务器 , ip : %s , port : %d\n",ip,port);
    
        //3.开始通信
        char send_buf[1024];
        char recv_buf[1024];
    
        int cnt = 0;
    
        while(1){
            memset(send_buf,0,sizeof send_buf);
            memset(recv_buf,0,sizeof recv_buf);
    
            sprintf(send_buf,"hello i love you : %d",cnt++);
    
            //发送数据
            send(cfd,send_buf,strlen(send_buf) + 1,0);
            //接收数据
            int len = recv(cfd,recv_buf,sizeof(recv_buf),0);
    
            if(len > 0){
                printf("服务端 : %s\n",recv_buf);
            }
            else if(len == 0){
                printf("服务端已经断开了连接...\n");
                break;
            }
            else{
                perror("recv");
                break;
            }
    
            sleep(1);
        }
    
        close(cfd);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    服务端的代码:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main()
    {
    
        // 1.创建用于监听的文件描述符
        int lfd = socket(AF_INET, SOCK_STREAM, 0);
        if (lfd == -1)
        {
            perror("socket");
            return -1;
        }
    
        // 2.绑定 ip 和 端口号
        unsigned short port = 10000;
    
        struct sockaddr_in saddr;
        saddr.sin_family = AF_INET;
        saddr.sin_port = htons(port);
        saddr.sin_addr.s_addr = INADDR_ANY;
    
        int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
        if (ret == -1)
        {
            perror("bind");
            return -1;
        }
    
        // 3.设置监听
        ret = listen(lfd, 128);
        if (ret == -1)
        {
            perror("listen");
            return -1;
        }
        printf("设置监听成功...\n");
    
        // 4.获取连接
    
        // 创建一个 epoll 模型
        int epfd = epoll_create(1);
    
        if (epfd == -1)
        {
            perror("epoll_create");
            exit(0);
        }
    
        // 往 epoll 模型中添加节点 , 目前只有用于监听的文件描述符
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = lfd;
    
        ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
    
        if (ret == -1)
        {
            perror("epoll_ctl");
            exit(0);
        }
    
        struct epoll_event evs[1024];
        int len = sizeof(evs) / sizeof(evs[0]);
        int addr_len = sizeof(struct sockaddr_in);
    
        char buf[1024];
        char *str = "ok";
    
        while (1)
        {
    
            int cnt = epoll_wait(epfd, evs, len, -1);
    
            for (int i = 0; i < cnt; i++)
            {
                // 取出当前文件描述符 cur_fd
                int cur_fd = evs[i].data.fd;
    
                // 这个文件描述符 是用于监听的文件描述符
                if (cur_fd == lfd)
                {
                    struct sockaddr_in addr;
                    // 获取连接 , 返回用于通信的文件描述符 cfd
                    int cfd = accept(cur_fd, (struct sockaddr *)&addr, &addr_len);
    
                    // 把用于通信的文件描述符 cfd 添加到 epoll 模型中
                    ev.events = EPOLLIN;
                    ev.data.fd = cfd;
    
                    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
    
                    if (ret == -1)
                    {
                        perror("epoll_ctl ... accept");
                        exit(0);
                    }
    
                    printf("获取连接成功 , 客户端 ip : %s  , port : %d\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
                }
    
                else
                {
    
                    // 开始通信
                    memset(buf, 0, sizeof buf);
                    int len = read(cur_fd, buf, sizeof buf);
                    printf("客户端 : %s\n", buf);
    
                    if (len > 0)
                    {
                        write(cur_fd, str, strlen(str) + 1);
                    }
                    else if (len == 0)
                    {
                        // 客户端已经关闭了连接
                        printf("客户端已经关闭了连接...\n");
                        epoll_ctl(epfd,EPOLL_CTL_DEL,cur_fd,NULL);
                        close(cur_fd);
                    }
                    else
                    {
                        perror("read");
                        exit(0);
                    }
                }
            }
        }
    
        close(lfd);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140

    四、epoll的工作模式

    1.水平工作模式

    水平模式,即 LT(level triggered),是默认的工作模式,并且同时支持 block socketno block socket 。在这种工作模式下,内核会通知调用者那些文件描述符已经就绪了,我们就可以的对这些已经就绪的文件描述符做操作。

    水平模式的特点:

    读事件: 如果文件描述符对应的读缓冲区还有数据,读事件就会被触发,epoll_wait() 解除阻塞。

    • 当读事件被触发,epoll_wait() 解除阻塞,之后就可以接收数据了;
    • 如果接收数据的缓冲区很小,不能全部将缓冲区数据读出,那么读事件会继续被触发,直到数据被全部读出;如果接收数据的缓冲区相对较大,读数据的效率也会相对较高(减少了读数据的次数);
    • 因为读数据是被动的,必须要通过读事件才能知道有数据到达了,因此对于读事件的检测是必须的;

    写事件: 如果文件描述符对应的写缓冲区可写,写事件就会被触发,epoll_wait() 解除阻塞。

    • 当写事件被触发,epoll_wait() 解除阻塞,之后就可以将数据写入到写缓冲区了;
    • 写事件的触发发生在写数据之前而不是之后,被写入到写缓冲区中的数据是由内核自动发送出去的;
    • 如果写缓冲区没有被写满,写事件会一直被触发;
    • 因为写数据是主动的,并且写缓冲区一般情况下都是可写的(缓冲区不满),因此对于写事件的检测不是必须的;

    2.边沿工作模式

    边沿模式,即 ET(edge-triggered) 是高速工作方式,只支持 no-block socket

    在这种模式下,当文件描述符从 未就绪 变为 就绪 时,内核会通过 epoll 通知调用者。然后它会假设调用者知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知(只会发送一次)。如果我们对这个文件描述符做IO操作,从而导致它再次变成未就绪,当这个未就绪的文件描述符再次变成就绪状态,内核会再次进行通知,并且还是只通知一次。

    ET模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比LT模式高。

    边沿模式的特点:

    读事件: 当读缓冲区有新的数据进入,读事件被触发一次,没有新数据不会触发该事件

    • 如果有新数据进入到读缓冲区,读事件被触发,epoll_wait() 解除阻塞;
    • 读事件被触发,可以通过调用 read()/recv() 函数将缓冲区数据读出;
    • 如果数据没有被全部读走,并且没有新数据进入,读事件不会再次触发,只通知一次;
    • 如果数据被全部读走或者只读走一部分,此时有新数据进入,读事件被触发,并且只通知一次;

    写事件: 当写缓冲区状态可写,写事件只会触发一次;

    • 如果写缓冲区被检测到可写,写事件被触发,epoll_wait() 解除阻塞;
    • 写事件被触发,就可以通过调用 write()/send() 函数,将数据写入到写缓冲区中;
    • 写缓冲区从 不满被写满,期间写事件只会被触发一次;
    • 写缓冲区从 不满,状态变为可写,写事件只会被触发一次;

    综上所述:epoll 的边沿模式下 epoll_wait() 检测到文件描述符有新事件才会通知,如果不是新的事件就不通知,通知的次数比水平模式少,效率比水平模式要高。

    1.ET工作模式的设置

    边沿模式不是默认的 epoll 模式,需要额外进行设置。

    epoll 设置边沿模式是非常简单的,epoll 管理的红黑树示例中每个节点都是 struct epoll_event 类型,只需要将 EPOLLET 添加到结构体的 events 成员中即可:

    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET;	// 设置边沿模式
    
    • 1
    • 2

    代码如下:

    int num = epoll_wait(epfd, evs, size, -1);
    for(int i=0; i<num; ++i)
    {
        // 取出当前的文件描述符
        int curfd = evs[i].data.fd;
        // 判断这个文件描述符是不是用于监听的
        if(curfd == lfd)
        {
            // 建立新的连接
            int cfd = accept(curfd, NULL, NULL);
            // 新得到的文件描述符添加到epoll模型中, 下一轮循环的时候就可以被检测了
            // 读缓冲区是否有数据, 并且将文件描述符设置为边沿模式
            struct epoll_event ev;
            ev.events = EPOLLIN | EPOLLET;   
            ev.data.fd = cfd;
            ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
            if(ret == -1)
            {
                perror("epoll_ctl-accept");
                exit(0);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    2.设置非阻塞

    对于写事件的触发一般情况下是不需要进行检测的,因为写缓冲区大部分情况下都是有足够的空间可以进行数据的写入。

    对于读事件的触发就必须要检测了,因为服务器也不知道客户端什么时候发送数据,如果使用 epoll边沿模式 进行读事件的检测,有新数据达到只会通知一次,那么必须要保证得到通知后将数据全部从读缓冲区中读出。那么,应该如何读这些数据呢?

    方式1:准备一块特别大的内存,用于存储从读缓冲区中读出的数据,但是这种方式有很大的弊端:

    • 内存的大小没有办法界定,太大浪费内存,太小又不够用;
    • 系统能够分配的最大堆内存也是有上限的,栈内存就更小;

    方式2:循环接收数据

    int len = 0;
    while((len = recv(curfd, buf, sizeof(buf), 0)) > 0)
    {
        // 数据处理...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这样做也是有弊端的,因为套接字操作默认是阻塞的,当读缓冲区数据被读完之后,读操作就阻塞了也就是调用的 read()/recv() 函数被阻塞了,当前进程/线程 被阻塞之后就无法处理其他操作了。

    要解决阻塞问题,就需要将套接字默认的阻塞行为修改为非阻塞,需 要使用 fcntl()函数进行处理:

    // 设置完成之后, 读写都变成了非阻塞模式
    int flag = fcntl(cfd, F_GETFL);
    flag |= O_NONBLOCK;                                                        
    fcntl(cfd, F_SETFL, flag);
    
    • 1
    • 2
    • 3
    • 4

    通过上述分析就可以得出一个结论:epoll边沿模式 下,必须要将套接字设置为非阻塞模式

    但是,这样就会引发另外的一个bug,在非阻塞模式下,循环地将读缓冲区数据读到本地内存中,当缓冲区数据被读完了,调用的 read()/recv() 函数还会继续从缓冲区中读数据,此时函数调用就失败了,返回 − 1 -1 1,对应的全局变量 errno 值为 EAGAIN 或者 EWOULDBLOCK 。如果打印错误信息会得到如下的信息:Resource temporarily unavailable

    // 非阻塞模式下recv() / read()函数返回值 len == -1
    int len = recv(curfd, buf, sizeof(buf), 0);
    if(len == -1)
    {
        if(errno == EAGAIN)
        {
            printf("数据读完了...\n");
        }
        else
        {
            perror("recv");
            exit(0);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    3.代码
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main()
    {
    
        // 1.创建用于监听的文件描述符
        int lfd = socket(AF_INET, SOCK_STREAM, 0);
        if (lfd == -1)
        {
            perror("socket");
            return -1;
        }
    
        // 2.绑定 ip 和 端口号
        unsigned short port = 10000;
    
        struct sockaddr_in saddr;
        saddr.sin_family = AF_INET;
        saddr.sin_port = htons(port);
        saddr.sin_addr.s_addr = INADDR_ANY;
    
        // 设置端口复用
        int opt = 1;
        setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
        int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
        if (ret == -1)
        {
            perror("bind");
            return -1;
        }
    
        // 3.设置监听
        ret = listen(lfd, 128);
        if (ret == -1)
        {
            perror("listen");
            return -1;
        }
        printf("设置监听成功...\n");
    
        // 4.获取连接
    
        // 创建一个 epoll 模型
        int epfd = epoll_create(1);
    
        if (epfd == -1)
        {
            perror("epoll_create");
            exit(0);
        }
    
        // 往 epoll 模型中添加节点 , 目前只有用于监听的文件描述符
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = lfd;
    
        ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
    
        if (ret == -1)
        {
            perror("epoll_ctl");
            exit(0);
        }
    
        struct epoll_event evs[1024];
        int size = sizeof(evs) / sizeof(evs[0]);
        int addr_len = sizeof(struct sockaddr_in);
    
        char buf[5];
    
        while (1)
        {
    
            int cnt = epoll_wait(epfd, evs, size, -1);
    
            for (int i = 0; i < cnt; i++)
            {
                // 取出当前文件描述符 cur_fd
                int cur_fd = evs[i].data.fd;
    
                // 这个文件描述符 是用于监听的文件描述符
                if (cur_fd == lfd)
                {
                    struct sockaddr_in addr;
                    // 获取连接 , 返回用于通信的文件描述符 cfd
                    int cfd = accept(cur_fd, (struct sockaddr *)&addr, &addr_len);
    
                    // 设置用于通信的文件描述符 cfd 的属性为非阻塞
                    int flag = fcntl(cfd, F_GETFL);
                    flag |= O_NONBLOCK;
                    fcntl(cfd, F_SETFL, flag);
    
                    // 把用于通信的文件描述符 cfd 添加到 epoll 模型中 , 设置为边沿模式
                    ev.events = EPOLLIN | EPOLLET;
                    ev.data.fd = cfd;
    
                    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
    
                    if (ret == -1)
                    {
                        perror("epoll_ctl ... accept");
                        exit(0);
                    }
    
                    printf("获取连接成功 , 客户端 ip : %s  , port : %d\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
                }
    
                else
                {
    
                    // 开始通信
                    memset(buf, 0, sizeof buf);
                    while (1)
                    {
                        int len = read(cur_fd, buf, sizeof buf);
    
                        if (len > 0)
                        {
                            printf("客户端 : %s\n", buf);
                            for (int i = 0; i < len; i++)
                                buf[i] = toupper(buf[i]);
    
                            write(cur_fd, buf, strlen(buf) + 1);
                        }
                        else if (len == 0)
                        {
                            // 客户端已经关闭了连接
                            printf("客户端已经关闭了连接...\n");
                            epoll_ctl(epfd, EPOLL_CTL_DEL, cur_fd, NULL);
                            close(cur_fd);
                            break;
                        }
                        else
                        {
                            if(errno == EAGAIN){
                                printf("数据读完了...\n");
                                break;
                            }
                            else{
                                perror("read");
                                exit(0);
                            }
    
                        }
                    }
                }
            }
        }
    
        close(lfd);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165

    五、参考

    IO多路复用 epoll

  • 相关阅读:
    字符串数数——考虑循环节:1108T3
    多个服务器之间免密登录
    centos7安装git客户端
    CUDA 基础 01 - 概念
    【设计模式】Java 语言不同的编程范式-第1章
    《Python趣味工具》——自制emoji3
    简单列举客户关系管理系统的核心功能
    猿创征文|公众号开发之路——为了研究公众号,我注册了公司
    Pytest 源码解读 [1] - [pluggy] 核心设计理念浅读
    一次说清楚BCD编码
  • 原文地址:https://blog.csdn.net/m0_74396439/article/details/134037351