边缘模式EPOLLET EPOLLOUT简单例子

本文通过简单的示例讲解EPOLLET和EPOLLOUT在边缘触发模式下的工作原理。在EPOLLET模式下,EPOLLIN关注输入缓冲区从空到不空的变化,EPOLLOUT关注输出缓冲区从满到不满的变化。在ET模式下,不需要依赖EPOLLOUT调用send/write,而是根据返回值判断是否需要注册EPOLLOUT。边缘模式的主要优点是减少epoll_wait的返回次数,降低内存拷贝。注意不要对监听socket设置EPOLLET,以免丢失客户端连接。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

给群里写的,简单的EPOLLET 例子:

EPOLLOUT详细说明

下面2个服务器例子 ,一个没有处理EPOLLOUT, 另一个处理了;

第一个例子只用来展示EPOLLET 的特点

第2个例子是一个echo服务器, 处理了EPOLLOUT,

如果要看EPOLLOUT的直接往下拉到第个例子, 主要还是看 errno == EAGAIN 这些地方

 

在ET模式下:

socket 一般情况下需要非阻塞的, 与EAGAIN 这个错误 配合使用. 

如果是默认的阻塞socket, 不是不可以,而是你需要做额外的事, 每一次的read/write , 你都需要去getsockopt( sock,SO_ERROR,...)

直到获取 EAGAIN 算是正常读/写. 否则, 你完蛋了. 这个socket将不再收到 epoll 的触发条件了

 

对于EPOLLIN : 如果状态改变了[ 比如 从无到有],那么只要输入缓冲区可读就会触发

对于EPOLLOUT: 如果状态改变了[比如 从满到不满],只要输出缓冲区可写就会触发;

EPOLLOUT那么具体什么时间点触发呢 

1. EPOLL_CTL_ADD或EPOLL_CTL_MOD 时 , 如果输出缓冲区状态改变了

2. 不论注册方式是 EPOLLIN | EPOLLOUT | EPOLLET 还是 EPOLLOUT | EPOLLET 只要含EPOLLOUT

只要状态改变就能触发.

     状态改变到底是什么?   简单理解:

     EPOLLOUT:  满->不满 

     EPOLLIN   :  空->不空

     有没有发现 , 都跟边缘有关

 

3.对于 send / write 需要依靠 EPOLLOUT 触发才调用吗 ? 什么时候需要 注册上 EPOLLOUT ?

   不需要.  如果要 send / write 那么就直接调用, 如果返回值 > 0  , 证明数据已经复制到发送缓冲区中.一切正常.

    如果 send / write 返回 < 0 且 errno == EAGAIN . 此时说明发送缓冲区满了. 那么需要把 剩余的字节保存起来,

    然后注册上 EPOLLOUT , 直到epoll_wait 返回 , 说明发送缓冲区可写, 再把 之前保存起来的数据 发送,

    如果此时 write 返回 > 0  那就把EPOLLOUT 取消掉. 

    简单来说 :  1. 直接发送  2. 看返回值, 没发送完才挂上EPOLLOUT  3. 发送完就把EPOLLOUT 取消掉

 

 

 

 

在LT模式下: 与select的触发条件一致

EPOLLIN: 可读就一直触发

EPOLLOUT:可写就一直触发

 

 

边缘的意思是只对客户端数据到达的次数感兴趣;

不会因为此次缓冲区没读完继续调用epoll_wait再次返回 <- 这种情况是select, poll 和 epoll的默认情况;

比如客户端发送了10个字节, 服务器端epoll_wait 返回,然后read读取数据,但由自定义缓冲区太小或输入缓冲区太小或其他网络原因,read函数现在只读了4个字节,那么还有6个字节在缓冲区内,这6个字节只能

等到下一次客户端再次发送数据时才有机会读到(而对于条件触发比如select和默认的epoll 会再次返回), 因此对于EPOLLET只能

使用nonblock去读取,才能完全读完输入缓冲区;

下面的第一个服务器代码中也同样让自定义接受缓冲区变小来显示epollET的特点;

 

边缘模式主要减少epoll_wait的返回次数,即减少内存拷贝;

一般的条件触发例如 select 和 没使用EPOLLET的epoll 都是只要接受缓冲区有数据就返回;

因此对于epollet 需要使用nonblock的socket, 否则无法简单的完整读取缓冲区内所有数据;

对监听sock和客户端sock 都使用了nonblock ;另外注意注意 不要对监听sock设置EPOLLET ,可能会丢失客户端连接;

除非用 while(1) accpet()... if(errno == EAGAIN) break;

 

 

 

 

 

util.h

这个只是包含了一些头文件 , 内部的函数在服务器代码中都没使用, 省的看起来麻烦

#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/epoll.h>
#include <signal.h>



#define EPOLL_SIZE 128
#define PORT 9988
#define BACKLOG 10
#define MAXLINE 4096
#define SA struct sockaddr
ssize_t readn(int fd , void *ptr, size_t n)
{
    size_t left = n;
    char * p = (char *)ptr;
    int nread = 0;
    while (left > 0){
        if((nread = read(fd,p,left)) < 0){
                if(errno == EINTR)
                    nread = 0;
                else
                    return -1;
        }
        else if(nread == 0)
            break;
        left -= nread;
        p += nread;
    }
    return n - left;
}
ssize_t writen(int fd , void * ptr , size_t n)
{
    size_t left = n;
    char * p = (char*)ptr;
    int nwrite = 0;
    while( left > 0){
        if((nwrite = write(fd,p,left)) <= 0){
            if(errno == EINTR && nwrite < 0)
                nwrite = 0;
            else
                return -1;
        }
        left -= nwrite;
        p += nwrite;
    }
    return n - left;
}

int conn_timeout(int sockfd , const struct sockaddr_in * sin, socklen_t socklen, int secs)
{
    int old_flag = fcntl(sockfd,F_GETFL,0);
    if( fcntl(sockfd,F_SETFL,old_flag|O_NONBLOCK) < 0){
        perror("fcntl failed");
        return -1;
    }

    int ret = 0;
    if((ret = connect(sockfd,(SA*)&sin,socklen)) < 0){
        if(errno != EINPROGRESS){
            perror("connect error");
            return -1;
        }
    }
    if(0 == ret)
        goto done;

    fd_set rset,wset;
    FD_ZERO(&rset);
    FD_SET(sockfd,&rset);
    wset = rset;
    struct timeval timeout;
    timeout.tv_sec= secs;
    timeout.tv_usec = 0;
    ret = select(sockfd+1,&rset,&wset,NULL,secs?&timeout:NULL);
    if(ret < 0){
        perror("select error");
        return -1;
    }
    else if( 0 == ret){
        close(sockfd);
        return -1;
    }
    if(!(FD_ISSET(sockfd,&rset) || FD_ISSET(sockfd,&wset))){
        return -1;
    }
    done:
    fcntl(sockfd,F_SETFL,old_flag);
    return 0;
}

 

 

第一个例子 : 只是用来展示 EPOLLET 特性的,因此 BUFF_SIZE = 4  特别小 ,  如果要用telnet来测试,

尽量每次发送 4 字节以上数据

#include "util.h"
#define  BUFF_SIZE 4
// 设置非阻塞
static int setnonblock(int fd){
    int flag = fcntl(fd,F_GETFL,0);
    return fcntl(fd,F_SETFL,flag|O_NONBLOCK);
}

//每个套接字关联的信息
typedef struct _ev_data
{
    int fd; //套接字
    struct sockaddr_in addr; //地址信息

} ev_data;

int main(int argc, char ** argv) {

    int listensock = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serv_addr, cli_addr;
    socklen_t socklen = sizeof(serv_addr);
    memset(&serv_addr, 0, socklen);
    memset(&cli_addr, 0, socklen);
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(PORT);
    serv_addr.sin_family = AF_INET;

    if (bind(listensock, (SA *) &serv_addr, sizeof(serv_addr)) < 0) {
        perror("bind");
        return 0;
    }

    if (listen(listensock, BACKLOG) < 0) {
        perror("listen");
        return 0;
    }

    //epoll_size = 128, 此大小看操作系统
    int epfd = epoll_create(EPOLL_SIZE);
    if (epfd < 0) {
        perror("epoll_create");
        return 0;
    }

    setnonblock(listensock); //这行可以省略

    struct epoll_event ev = {0, {0}};
    ev.events = EPOLLIN ;

    //创建一块内存,用于存放listenfd 的信息,对于客户端套接字也同样
    ev_data * data = malloc(sizeof(ev_data));
    data->fd = listensock;
    data->addr  = serv_addr;
    ev.data.ptr = data;
    //把此事件与套接字关联
    epoll_ctl(epfd,EPOLL_CTL_ADD , listensock , &ev);

    int nready = -1 , len = -1;

    //需要自己创建一个event数组,epoll_wait返回时将把触发事件的socket 复制到这里
    struct epoll_event * evts = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);

// 很小的缓冲区
    char buff[BUFF_SIZE]  ={0};
    while(1)
    {

        //返回后触发事件的socket相关信息将被复制到 evts 数组
        nready = epoll_wait(epfd,evts,EPOLL_SIZE, -1);
        printf("epoll_wait return : %d\n" , nready);
        for( int i =0 ; i < nready ; ++i){
            //如果是输入事件
            if( evts[i].events & EPOLLIN ){
                //如果是监听套接字发生输入事件
                if( ( (ev_data * )(evts[i].data.ptr) )->fd == listensock){
                    socklen = sizeof(cli_addr);
                    int clt_fd = accept(listensock,(SA*)&cli_addr,&socklen);
                    if( clt_fd < 0){
                        puts("accept failed");
                        continue;
                    }
                    setnonblock(clt_fd);//nonblock 别忘记了
                    ev.events = EPOLLIN | EPOLLET; // 设置边缘模式
                    ev_data * data = malloc(sizeof(ev_data));
                    data->addr = cli_addr;
                    data->fd = clt_fd;
                    ev.data.ptr = data;
                    epoll_ctl(epfd,EPOLL_CTL_ADD,clt_fd, &ev); //加入这个epoll
                }
                else{
                    // 如果是有数据来了
                    ev_data *  pData = (ev_data * )(evts[i].data.ptr);

                //由于是非阻塞,因此while
                    while(1) {
                        read_again:
                        len = read(pData->fd, buff, BUFF_SIZE - 1);
                        printf("read from ip:%s, port:%d\n" ,
                        inet_ntoa(pData->addr.sin_addr),
                        ntohs(pData->addr.sin_port));

                        //如果出错了
                        if (len < 0){
                            if(errno == EINTR)
                                goto read_again;
                            else if(errno == EAGAIN){
                                puts("read error , no more data!");
                                break;
                            }
                            else{
                                perror("read error , close socket . ");
                                epoll_ctl(epfd,EPOLL_CTL_DEL,
                                pData->fd,NULL);
                                close(pData->fd);
                                free(pData);
                                break;
                            }
                        }
                        //对端关闭
                        else if( 0 == len){
                            printf("socket : %d closed!\n" , pData->fd);
                            epoll_ctl(epfd,EPOLL_CTL_DEL,
                                      pData->fd,NULL);
                            close(pData->fd);
                            free(pData);
                            break;
                        }
                        else{
                            buff[len] = 0;
                            printf("write buff:%s\n" , buff);
                            write(pData->fd,buff,len);
                        }
                    }
                }
            } else{
                printf("epoll_wait returned , somethingelse happened nready:%d\n",
                nready);
            }
        }
    }

}

 

 

第2个例子: 处理EPOLLOUT的情况

EPOLLOUT 总的来说就是只要EPOLLIN被触发了, 并且输出缓冲区不满 也触发一次, 或者输出缓冲区由满到不满时也触发;

#define  BUFF_SIZE 1024
#define  MAX_EVENTS 1024



static int setnonblock(int fd , int nonblock){
    int flag = fcntl(fd,F_GETFL,0);
    if(nonblock)
        return fcntl(fd,F_SETFL,flag|O_NONBLOCK);
    else
        return fcntl(fd,F_SETFL,flag&~O_NONBLOCK);
}

//每个套接字关联的信息
typedef struct _ev_data
{
    int fd; //socket
    char * buffer; //接受缓冲区
    int nread; //读了N个字节
} ev_data;

void free_event(ev_data * pEv){ //释放内存用的
    if(!pEv)
        return;
    if(pEv->buffer){
        free(pEv->buffer);
    }
    free(pEv);
}

int main(int argc, char ** argv) {

    signal(SIGPIPE, SIG_IGN);
    int listensock = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serv_addr, cli_addr;
    socklen_t socklen = sizeof(serv_addr);
    memset(&serv_addr, 0, socklen);
    memset(&cli_addr, 0, socklen);
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(PORT);
    serv_addr.sin_family = AF_INET;
    int on = 1;
    setsockopt(listensock,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(int));
    if (bind(listensock, (SA *) &serv_addr, sizeof(serv_addr)) < 0) {
        perror("bind");
        return 0;
    }

    if (listen(listensock, BACKLOG) < 0) {
        perror("listen");
        return 0;
    }

    /* 以上都是初始化 没什么好说的*/
    

    //也可以用 epoll_create1() ,MAX_EVENTS这个常量没什么用
    int epfd = epoll_create(MAX_EVENTS);
    if (epfd < 0) {
        perror("epoll_create");
        return 0;
    }
    setnonblock(listensock,1);
    struct epoll_event ev = {0, {0}};
    ev.events = EPOLLIN ;  //输入事件
    ev_data * data = malloc(sizeof(ev_data));
    data->fd = listensock;
    data->buffer = NULL;

    //放入ptr中
    ev.data.ptr = data;

    //加到epoll
    epoll_ctl(epfd,EPOLL_CTL_ADD , listensock , &ev);

    int nready = -1 , len = -1;

    //存放返回事件用的
    struct epoll_event * evts = calloc(MAX_EVENTS, sizeof(struct epoll_event));
    while(1)
    {
        nready = epoll_wait(epfd,evts,MAX_EVENTS, -1);
        printf("\nepoll_wait return : %d\n" , nready);
        if(nready < 0){
            perror("epoll_wait");
            break;
        }

        for( int i =0 ; i < nready ; ++i){
            printf("evts[%d].events = %d\n" , i,evts[i].events);


            //如果是这2个出错事件, 就把套接字给关了
            if( ( evts[i].events & EPOLLERR ) ||
                    evts[i].events & EPOLLHUP){
                printf("i=%d, occurs err, events:%d\n",
                i, evts[i].events);
                ev_data *  pData = (ev_data * )(evts[i].data.ptr);
                if(pData){
                    evts[i].data.ptr = NULL;
                    close(pData->fd);
                    free_event(pData);
                }
                continue;
            }
            //如果是输入事件
            if( evts[i].events & EPOLLIN ){
                if( !evts[i].data.ptr){
                    puts("ptr is empty");
                    continue;
                }
                //如果是监听套接字发生输入事件
                if( ( (ev_data * )(evts[i].data.ptr) )->fd == listensock){
                    socklen = sizeof(cli_addr);


                /*
                        如果监听sock 也是EPOLLET的话, 需要while(1) accept
                */
                    int clt_fd = accept(listensock,(SA*)&cli_addr,&socklen);
                    if( clt_fd < 0){
                        puts("accept failed");
                        continue;
                    }
                    setnonblock(clt_fd,1);//nonblock
                    ev.events = EPOLLIN | EPOLLOUT |EPOLLET; // 注册2个事件
                    ev_data * data = malloc(sizeof(ev_data));
                    data->fd = clt_fd;
                    data->nread = 0;
                    data->buffer = malloc(BUFF_SIZE); //创建一个接受缓冲区
                    ev.data.ptr = data;
                    epoll_ctl(epfd,EPOLL_CTL_ADD,clt_fd, &ev);
                    puts("\taccept success");
                }
                else{
                    // 如果是有数据来了
                    printf("\tdata is comming, u can read !\n");
                    ev_data *  pData = (ev_data * )(evts[i].data.ptr);
                    if(!pData){
                        printf("read error ! ptr is empty\n");
                        continue;
                    }
                
                    //下面接受数据
                    int left = BUFF_SIZE - pData->nread;
                    char * pbuff = pData->buffer + pData->nread;

                    while(left > 0 )
                    {
                        len = read(pData->fd, pbuff,left);
                        if (len < 0){
                            if(errno == EINTR)
                                len = 0;
                
                            //sock输入缓冲区空了
                            else if(errno == EAGAIN || errno == EWOULDBLOCK){
                                printf("read error , no more data!  len:%d\n" , pData->nread);
                                break;
                            }
                            else{

                                //出错了
                                perror("read error , close socket . ");
                                epoll_ctl(epfd,EPOLL_CTL_DEL,
                                pData->fd,NULL);
                                evts[i].data.ptr = NULL;
                                close(pData->fd);
                                free_event(pData);
                                break;
                            }
                        }
                        else if( 0 == len){

                            //客户端断开了
                            printf("socket : %d closed!\n" , pData->fd);

                            //EPOLL_CTL_DEL 这行可以不写, close() 将把epoll中的fd移除
                            epoll_ctl(epfd,EPOLL_CTL_DEL,
                                      pData->fd,NULL);
                            evts[i].data.ptr = NULL;
                            close(pData->fd);
                            free_event(pData);
                            break;
                        }
                        pbuff += len;
                        left -= len;
                        pData->nread += len;
                    }
                    printf("-> nread: %d\n" , pData->nread);

                }
            }
            if(evts[i].events & EPOLLOUT){

                // 如果有输出事件
                printf("\t****u can write now!! !\n");
                ev_data *  pData = (ev_data * )(evts[i].data.ptr);

                /*
                    这里做判断是, EPOLLIN也将附带着触发一次EPOLLOUT,如果sock的输入
                    缓冲区不满的话
                */
                if(!pData){
                    puts("oh no ... socket is closed");
                    continue;
                }
                char * p = pData->buffer;
                int nwrite = 0;
                int left = pData->nread;

                //如果接受缓冲区没数据,就直接返回了
                if(0 == left){
                    puts("buff is empty!");
                    continue;
                }
                int write_bytes = 0;
                while( left > 0){
                    write_again:
                    nwrite = write(pData->fd,p,left);
                    if(nwrite < 0){
                        if(errno == EINTR)
                            goto write_again;

                        //如果输出缓冲区满了
                        else if( EAGAIN == errno || EWOULDBLOCK == errno){
                            perror("sock buff is full , write error :");
                            break;
                        }
                        else {
                            perror("other write error :");
                            close(pData->fd);
                            free_event(pData);
                            pData = NULL;
                            break;
                        }
                    }
                    p+= nwrite;
                    left -= nwrite;
                    write_bytes += nwrite;
                }
            
                //这里主要是为了echo 客户端做的
                if( write_bytes > 0 && pData){
                    memcpy(pData->buffer,pData->buffer + write_bytes ,pData->nread - write_bytes);
                    pData->nread -= write_bytes;
                }
                printf("break on write , write_bytes : %d  left bytes:%d\n" , write_bytes,
                       pData->nread);


            }
        }
    }

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值