I/O复用之epoll服务器

本文详细解析了epoll的工作原理及其实现方式,通过对比poll突出epoll的优势,并提供了具体的代码示例,帮助读者深入理解epoll在高效并发场景中的应用。

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

原理剖析

  其实在写完poll服务器之后,“写”一个epoll服务器貌似很简单了,但是实际上epoll服务器的实现和poll的实现差了十万八千里,仅仅在内核态到用户态的拷贝中,就省却了很大的时间。最底层的源码本人因为才疏学浅,剖析不了。但是原理还是能说一点点。epoll在底层使用了三个关键函数:
  

 int epoll_create(int size);

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

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

  epoll_wait函数会在底层建立一个epoll模型,返回一个句柄,至于句柄怎么理解,你可以想象成你使用一个东西一下子提起了一串东西,那个就是句柄。size表示的是最大的维护数量,超过这个数量就不能保证质量。他会在底层建立一个红黑树的模型,使得后面的epoll_ctl函数的增删改查都会变得效率很高,并且维护一个就绪链表,一旦红黑树上的某个节点代表的套接字就绪或有读写事件发生,他就会将此节点拷贝到就绪链表上,等待系统调用。此时系统只将这个套接字上的信息拷贝到用户态进行处理。

  epoll_ctl函数的op参数代表选项,是对传入的文件描述符进行什么操作。具体选项有:
  EPOLL_CTL_ADD:往事件表上注册fd事件;
  
  EPOLL_CTL_MOD:修改事件表上的fd事件;
  
  EPOLL_CTL_DEL:删除事件表上的fd事件。
  
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 events */
               epoll_data_t data;        /* User data variable */
           };

  其中,events和poll中的events差不了多少,只不过在每一个的宏定义前加了一个E。data字段表示的是fd的常用信息,最多使用的当然是fd,但是data是一个联合体,在使用fd的时候就无法使用ptr了,否则就会造成很多麻烦。所以可以使用一个ptr成员,放弃使用fd成员,然后我们只需要将ptr指向一个包含fd的结构体就行了。我在代码中也是这样写的。
  epoll_ctl并不仅仅做了这么点事,在添加进一个文件描述符后,则给内核中断注册一个中断函数,一旦当前的文件描述符事件就绪,就这个就绪的socket插入到就绪链表里。

  epoll_wait函数成功则会返回就绪的文件描述符的个数,失败返回-1并设置errno。timeout表示等待时间,maxevents表示最多监听的事件的个数。struct epoll_event *events代表的是:一旦epoll_wait检测事件,所有的就绪时间从内核事件表中拷贝到这个数组中,这个数组只用于epoll_wait检测到的就绪事件。

  具体的也可以看看这篇博客,是我转自百度知道的以为大神写的:
  http://blog.youkuaiyun.com/sinat_36118270/article/details/77596669

代码实现

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SIZE 64

typedef struct fdbuff
{
    int fd;
    char buff[1024];
}epoll_buff_t,*epoll_buff_p;

struct epoll_event中data中的ptr指向的结构体,并放弃使用data中的fd字段。fdbuff结构体里面包含一个fd文件描述符,也包括一个当前文件描述符与之对应的缓冲区。


void usage(const char* str)
{
    printf("%s [local_ip]-[local port]\n",str);
}

int startup(const char* ip,const char* port)
{
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
        perror("socket");
        exit(2);
    }
    struct sockaddr_in local;
    local.sin_family=AF_INET;
    local.sin_port=htons(atoi(port));
    local.sin_addr.s_addr=inet_addr(ip);
    if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
    {
        perror("bind");
        exit(3);
    }

    if(listen(sock,5)<0)
    {
        perror("listen");
        exit(4);
    }
    return sock;
}

static void* alloc_buff(int fd)
{
    epoll_buff_p temp=malloc(sizeof(epoll_buff_t));
    if(temp==NULL)
    {
        perror("malloc");
        exit(7);
    }
    temp->fd=fd;
    return temp;
}

int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        usage(argv[0]);
        exit(1);
    }

    int listen_sock=startup(argv[1],argv[2]);
    int epfd=epoll_create(128);
    if(epfd<0)
    {
        perror("epoll_create");
        exit(5);
    }
    struct epoll_event ev;
    ev.events=EPOLLIN;
    ev.data.ptr = alloc_buff(listen_sock);

将struct epoll_event中data字段中的ptr指向自定义结构体,并设置fdbuff的fd。

    if(epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev)<0)

将此文件描述符假如到epoll模型中,实际是在epoll模型的红黑树结构中增加一个节点


    {
        perror("epoll_ctl");
        exit(6);
    }

    int timeout=2000;
    struct epoll_event rev[64];
    int size=sizeof(rev)/sizeof(rev[0]);

    int nums=-1;
    while(1)
    {
        switch(  (nums = epoll_wait(epfd,rev,size,timeout))  )
        {
            case -1:
                perror("epoll_wait");
                break;
            case 0:
                printf("timeout...\n");
                break;
            default:
            {
               for(i=0;i<nums;i++)

因为nums返回的是就绪的文件描述符的个数,所以在遍历的过程中,遍历到的每个节点都是就绪的socket,相当与遍历就绪链表上的节点。

                {
                    epoll_buff_p p=((epoll_buff_p)(rev[i].data.ptr));
                    int fd=p->fd;
                    if( (fd==listen_sock) && ( (rev[i].events) & (EPOLLIN) ) )
                    {
                        struct sockaddr_in client;
                        socklen_t len=sizeof(client);
                        int newfd = accept(fd,(struct sockaddr*)&client,&len);
                        if(newfd<0)
                        {
                            perror("accept");
                        }
                        printf("get a client:%s:%d #",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
                        fflush(stdout);
                        ev.data.ptr=alloc_buff(newfd);
                        if( epoll_ctl(epfd,EPOLL_CTL_ADD,newfd,&ev)<0  )
                        {
                            perror("epoll_ctl");
                            continue;
                        }

accept接收成功,返回的文件描述符表示新的连接的socket,则向epfd的epoll模型中添加这个socket,实际上是往红黑树中添加节点。

                    }//fi
                    else if( (fd!=listen_sock) && ( (rev[i].events) & (EPOLLIN) ) )
                    {
                        ssize_t s=read( fd,(p->buff),SIZE-1 );
                        if(s>0)
                        {
                            (p->buff)[s]=0;
                            printf("client say# %s",p->buff);
                            ev.events=EPOLLIN | EPOLLOUT;
                            const char* msg="HTTP/1.0 200 OK\r\n\r\n<html><h1>Hello Epoll!</h1></html>";
                            strcpy( (p->buff),msg );
                            ev.data.ptr=p;
                            if ( epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev)<0 )
                            {
                                perror("epoll_ctl");
                            }

设置当前fd,将标志位更改为关心读和写事件,并往他对应缓冲区中写入一个http报头和基本语句。最后使用epoll_ctl函数更改fd的信息。

                        }//if
                        else if(s==0)
                        {
                            printf("client quit...\n");
                            close(fd);
                            if ( epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)<0 )
                            {
                                perror("epoll_ctl");
                            }
                            free(p);
                        }//end of elif 
                        else
                        {
                            perror("read");
                        }
                    }//end of elif
                    else if(  (fd!=listen_sock) && ( (rev[i].events) & (EPOLLOUT) ) )
                    {
                        write(fd,p->buff,strlen(p->buff));
                        close(fd);
                        if ( epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)<0 )
                        {
                            perror("epoll_ctl");
                        }
                        free(p);
                    }
                }//end of for
            }//end of default
        }//end of switch
    }//end of while

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值