epoll服务器

10 篇文章 0 订阅

相比select、poll,epoll是I/O多路转接最高效的手段,它几乎具备了之前select、poll的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。epoll实现只有epoll_create()、epoll_ctl()、epoll_wait()三个系统调用函数。

1、epoll_create()函数:

int epoll_create(int size);
  • 含义:创建一个epoll句柄,占用一个fd,使用完成epoll需要关闭此fd;
  • 参数:size:从linux2.6.8之后,size参数是被忽略的;
  • 返回值:返回一个epfd.

2.epoll_ctl()函数:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 含义:事件注册函数,向epoll模型中增加、修改、删除需要监听的fd,并指定对应关心的事件;
  • 参数:
  • epfd:epoll_create返回值;
  • op:表示动作,用三个宏来表示:(1)EPOLL_CTL_ADD:添加新的fd到epfd中;(2)EPOLL_CTL_MOD:修改以及注册fd监听事件;(3)EPOLL_CTL_DEL:从epfd中删除一个fd;
  • fd:需要监听的fd;
  • event:需要监听的事件:struct epoll_event结构如下:
struct epoll_event
{
    _unint32_t events;//关心的事件
    epoll_data_t data;
};
typedef union epoll_data
{
    void* ptr;
    int fd;
    _uint32_t u32;
    _uint64_t u64;
}epoll_data_t;
  • events可以是以下几个宏的集合:
    EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
    EPOLLOUT:表示对应的文件描述符可写;
    EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
    EPOLLERR:表示对应的文件描述符发生错误;
    EPOLLHUP:表示对应的文件描述符被挂断;
    EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的;
    EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个
    socket的话,需要再次把这个socket加入到EPOLL队列里;

3.epoll_wait()函数:

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • 含义:等待epoll模型中哪些fd的事件就绪;
  • 参数:
  • epfd:epoll句柄;
  • events:结构体数组,输出型参数,epoll将会把发生的事件赋值到events数组中;
  • maxevents:告之内核这个events数组有多大,这个 maxevents的值不能大于创建epoll_create()时的size值;
  • timeout:超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞);
  • 返回值:函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时。

以上即为epoll最基本的操作,其中其最基本的原理为:

  1. epoll_create()在底层创建一个红黑树与就绪队列,返回一个epoll模型;
  2. epoll_ctl()即在红黑树插入新的结点,即关心的fd,当其事件就绪,采用回调机制激活;
  3. epll_wait()只关心就绪队列中的就绪fd,直接对对应fd进行相应事件处理;
  4. 当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,只需要去epoll指定的数组中依次取得相应数量的文件描述符即可,其使用了内存映射(mmap)技术,彻底省掉了这些文件描述符在系统调用时复制的开销。
  5. 于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
  6. Epoll的2种工作方式-水平触发(LT)和边缘触发(ET):LT:是epoll缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知,所以,这种模式编程出错误可能性要小一点,传统的select/poll都是这种模型的代表. ET:是高速工作方式,只支持no-block socket,它效率要比LT更高。ET与LT的区别在于,当一个新的事件到来时,ET模式下当然可以从epoll_wait调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字中没有新的事件再次到来时,在ET模式下是无法再次从epoll_wait调用中获取这个事件的。而LT模式正好相反,只要一个事件对应的套接字缓冲区还有数据,就总能从epoll_wait中获取这个事件。因此,LT模式下开发基于epoll的应用要简单些,不太容易出错。而在ET模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲区中的用户请求得不到响应。

epoll模型的优点高效性体现:

  1. select维护数组,epoll维护红黑树,其增删查改效率较高;
  2. select遍历数组,epoll只遍历就绪队列,时间复杂度为o(1),并且队列只存储就绪结点;
  3. epoll监视的fd数目无上限,由于其由红黑树描述的,可以一直创建;
  4. 其用户与内核采用内存映射机制;
  5. 就绪队列从0开始连续放置就绪的fd;
  6. IO效率不随FD数目增加而线性下降;
  7. 内核微调.

以下实现LT模式下的epoll服务器:

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>

static void usage(const char* proc)
{
    printf("%s [local_ip] [local_port]\n",proc);
}

int startup(const char* ip,int port)
{
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0)
    {
        perror("socket");
        exit(3);
    }

    int opt=1;
    setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

    struct sockaddr_in local;
    local.sin_family=AF_INET;
    local.sin_port=htons(port);
    local.sin_addr.s_addr=inet_addr(ip);
    if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
    {
        perror("bind");
        close(sock);
        exit(4);
    }

    if(listen(sock,10)<0)
    {
        perror("listen");
        close(sock);
        exit(5);
    }

    return sock;
}

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

    int listen_sock=startup(argv[1],atoi(argv[2]));    //创建监听套接字 

    int efds=epoll_create(1024);             //创建epoll模型 

    struct epoll_event ev;
    ev.events=EPOLLIN;
    ev.data.fd=listen_sock;
    epoll_ctl(efds,EPOLL_CTL_ADD,listen_sock,&ev);     //将监听套接字加入epoll模型中,并关心其读事件,采用激活回调机制 

    struct epoll_event fds[1024];    //定义共享内存就绪队列大小 
    int maxnum=1024;    //定义最多可以监听多少个事件 
    int timeout=500;
    while(1)
    {
        int nums=epoll_wait(efds,fds,maxnum,timeout);    //一段时间内等待一组文件描述符的事件(实际则直接遍历就绪队列) 
        switch(nums)
        {
            case -1:
                perror("epoll_wait");
                break;
            case 0:
                printf("timeout\n");
                break;
            default:
                {
                    int i=0;
                    for( ;i<nums;++i)
                    {
                        int sock=fds[i].data.fd;
                        if(sock==listen_sock && fds[i].events&EPOLLIN)   //监听套接字就绪 
                        {
                            struct sockaddr_in client;
                            socklen_t len=sizeof(client);
                            int new_sock=accept(sock,(struct sockaddr*)&client,&len);  //建立连接 
                            if(new_sock<0)
                            {
                                perror("accept");
                                continue;
                            }

                            ev.events=EPOLLIN;
                            ev.data.fd=new_sock;
                            epoll_ctl(efds,EPOLL_CTL_ADD,new_sock,&ev);     //将新套接字加入efds模型关心其读事件 
                        }
                        else if(fds[i].events & EPOLLIN)   //nomal sock ready    //读事件就绪 
                        {
                            char buf[1024];
                            ssize_t s=read(sock,buf,sizeof(buf)-1);
                            if(s>0)
                            {
                                buf[s]=0;
                                printf("client# %s\n");

                                ev.events=EPOLLOUT;
                                ev.data.fd=sock;
                                epoll_ctl(efds,EPOLL_CTL_MOD,sock,&ev);     //读成功关心其写事件 
                            }
                            else if(s==0)
                            {
                                printf("client quit!!!\n");
                                close(sock);
                                epoll_ctl(efds,EPOLL_CTL_DEL,sock,NULL);
                            }
                            else
                            {
                                perror("read");
                                continue;
                            }
                        }
                        else if(fds[i].events & EPOLLOUT)   //写事件就绪 
                        {
                            char* msg="I am server!!!\n";
                            ssize_t s=write(sock,msg,strlen(msg));
                            if(s<0)
                            {
                                perror("write");
                                continue;
                            }

                            ev.events=EPOLLIN;
                            ev.data.fd=sock;
                            epoll_ctl(efds,EPOLL_CTL_MOD,sock,&ev);   //写成功关心其读事件 
                        }

                    }
                }
                break;
        }
    }

    close(listen_sock);
    return 0;
}
要在 C 语言中实现一个 epoll 服务器,需要以下几个步骤: 1. 创建监听套接字:首先需要创建一个监听套接字,绑定到服务器的 IP 地址和端口上,用于接受客户端连接请求。可以使用 `socket` 函数创建套接字,使用 `bind` 函数将其绑定到指定的地址和端口上,使用 `listen` 函数将其转换为监听套接字。 ```c int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字 struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到任意地址 servaddr.sin_port = htons(PORT); // 绑定到指定端口 bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)); // 绑定套接字 listen(sockfd, BACKLOG); // 转换为监听套接字,BACKLOG 表示等待连接队列的最大长度 ``` 2. 创建 epoll 实例:可以使用 `epoll_create` 函数创建一个 epoll 实例,用于监听事件。 ```c int epfd = epoll_create(MAX_EVENTS); // 创建 epoll 实例,MAX_EVENTS 表示监听的最大事件数 ``` 3. 将监听套接字添加到 epoll 实例中:使用 `epoll_ctl` 函数将监听套接字添加到 epoll 实例中,以便监听连接请求事件。 ```c struct epoll_event event; event.events = EPOLLIN; // 监听可读事件 event.data.fd = sockfd; // 监听的文件描述符为监听套接字 epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event); // 将监听套接字添加到 epoll 实例中 ``` 4. 进入事件循环:使用 `epoll_wait` 函数进入事件循环,等待事件的发生。当有事件发生时,使用 `accept` 函数接受客户端连接,并将新连接套接字添加到 epoll 实例中,以便监听其读取事件。 ```c struct epoll_event events[MAX_EVENTS]; while (1) { int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1); // 等待事件的发生 for (int i = 0; i < nfds; i++) { if (events[i].data.fd == sockfd) { // 监听套接字有可读事件,表示有新连接请求 struct sockaddr_in cliaddr; socklen_t clilen = sizeof(cliaddr); int connfd = accept(sockfd, (struct sockaddr*)&cliaddr, &clilen); // 接受连接请求 struct epoll_event event; event.events = EPOLLIN; // 监听可读事件 event.data.fd = connfd; // 监听的文件描述符为新连接套接字 epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &event); // 将新连接套接字添加到 epoll 实例中 } else { // 客户端有可读事件 char buf[MAXLINE]; int n = read(events[i].data.fd, buf, MAXLINE); // 读取数据 if (n == 0) { // 客户端关闭连接 epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL); // 从 epoll 实例中删除套接字 close(events[i].data.fd); // 关闭套接字 } else { write(events[i].data.fd, buf, n); // 发送数据 } } } } ``` 上述代码中,`MAX_EVENTS` 表示监听的最大事件数,`MAXLINE` 表示一次读取的最大数据量,`BACKLOG` 表示等待连接队列的最大长度。可以根据实际情况进行调整。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值