IO多路转接模型-----epoll及epoll版本服务器的实现

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/DX_Jone/article/details/100941145

epoll的实现原理:

epoll模型是linux下性能最高的IO多路转接,在epoll下就绪事件变得不同。
对于可读事件就绪就是接受缓冲区的数据大小大于低水位标记(一般为一字节);
对于可写事件就绪就是可写缓冲区的空闲空间大小大于低水位标记(1B);
epoll在内核中是一红黑树进行节点的删除和添加,还有一个rdlist双向链表,用来存储就绪事件的链表;epoll也是采用事件结构来进行监控的;

1.epoll的3个函数及其功能的实现:

创建epoll结点:int epoll_creat(int size)
1. 在操作系统内核构建一个红黑树

    节点 : 表示要关心的哪个文件描述符的事件
    key键 :用文件描述符作为key键

 2. 在操作系统内核构建一个回调机制 

    作用:就是减少了操作系统的开销(不用操作系统再去轮询的找就绪事件)
         有这么一个机制告诉我们,我们所关心的文件描述符的时间已经就绪
 3. 在操作系统内核构建一个就绪队列

    如何构建的:有了回调机制,告诉了我们所关心的文件描述符的事件已经就绪
              接下来就是把该文件描述符拷贝到就绪队列中;等我们处理的时候
              就不用轮询的去找就绪事件,而是 从就绪队列的开始找epoll_wait() 
              的返回值(>0,成功的情况下)这么大的一个区间, 
              这段区间就是当前的就绪事件

这三个组在一起的返回值是fd(文件描述符);


向epoll树上添加结点,删除结点,修改结点:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event):
poll的事件注册函数:要关心哪个文件描述符的事件

1. 第一个参数是epoll_create()的返回值:一个文件描述符;

2.第二个参数表示动作,三个宏来表示:

 EPOLL_CTL_ADD:注册新的fd到epfd中;

 EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

 EPOLL_CTL_DEL:从epfd中删除一个fd;

3. 第三个参数是需要监听的fd ;

4. 第四个参数是告诉内核需要监听什么事 .

events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这个应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发⽣错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式, 
          这是相对于水平触发(LevelTriggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续  
             监听这个socket的话,需要再次把这个socket加入到EPOLL队列中。



开始监控:int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
返回 :关心事件已经就绪的事件

1. > 0 :满足就绪条件的事件个数
2. 0 : 在规定的时间内没有事件发生(超出timeout设置的时间)
3. -1 :错误 
       原因由errno标识;此时中间三个参数的值变得不可预测。

2.epoll的两种工作方式:

1. 水平触发(LT): 默认的

2. 边缘触发(ET)

   另外:selete 和 poll其实也是在LT工作模式下的;而epoll是可以支持LT,又可以支持ET

   从我们的 epoll_ctl()中的第四个参数 struct epoll_event *event 可以看出,
   这里的结构体epoll_event中有一个字段: events;而这里的参数有一个: 

   EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式, 
          这是相对于水平触发(LevelTriggered)来说的。 

   这就是将LT默认的方式变成ET模式的方式。

下面我们来讲一下ET模式和LT模式的区别:


关于ET模式和LT模式的区别:
假如 :

  1. 我有5个快递,当一个快递到的时候,快递员就打电话让你取,一直打直到你把这个快递取走为止,下一个你的来了依然如此;很显然这样的快递员工作方式效率会很慢。

上面的就是属于LT模式;

2… 同样的,如果你有5个快递,当一个快递到的时候,快递员第一次给你送的的时候打一次电话,你不来他就替你收着(而这个时候,快递员不会等你),第二个你的来了再给你打一次,你不来他依然替你收着,每次只有快递数量变化的时候才会打电话,这个时候只有你哪一次有时间,将所有的快递都拿走。此种方式效率较高:因为快递员并没有去等

这种模式属于 ET模式;


LT模式 :

    1. 当epoll检测到socket上的事件就绪时,可以不立即处理或者只处理一部分
    (例如:2KB的数据好了,此时可以一次读1KB,然后剩1KB)
    2. 在第二次调用epoll_wait的时候它依然会立即通知你,并且通知socket的读事件就绪
       直到缓存区内的数据都读完了,epoll_wait才不会立即返回
    3. 支持非阻塞与阻塞

ET模式 :

   1. 当epoll检测到socket上的事件就绪时,必须立即处理
   (例如:2KB的数据好了,此时可以一次读1KB,然后剩1KB)
   2. 但是在第二次调用epoll_wait的时候,它不再立即返回通知你
      也就是说,ET模式下,数据就绪以后只有一次处理机会,所以要么不读,要么读完,
      不会有只读一部分的情况
      (只有在数据从 无变有 或者 少变多 的时候,才会通知你)
   3. 性能比LT高
   4. 只能采用非阻塞

另外为什么ET模式只支持非阻塞读写呢?

   因为: 数据就绪只通知一次,必须在通知后,一次处理完
         也就是说:如果使用ET模式,当数据就绪的时候就要一直读,直到数据读完为止

     1. 但是如果当前的fd是阻塞的,而读是循环的:那么在读完缓存区的时候,
        如果对端每一偶数据在写进来,那么该read函数就会一直阻塞,
        这不符合逻辑,不能这么使用 
      2. 那么就需要将fd设置成非阻塞,当没有数据的时候,read虽然读取不到任何的数据,
      但是肯定不会被阻塞住,那么此时说明缓冲区内数据已经读完,read返回继续后序的逻辑

3.epoll的优缺点:

epoll的优缺点:

优点
1)epoll没有监控的上线
2)采用事件结构简化了select监控集合的监控流程
3)epoll是一个异步阻塞操作,发起调用,让操作系统进行文件描述符的监控,使用事件回调函数对描述符进行监控,避免了select的遍历轮询,性能不会随着文件描述符增多而下降
4)epoll发起调用进行等待,循环判断内核中epoll就绪时间链表是不是为空来确定是否有就绪事件,若有就绪事件,则将对应的事件拷贝到用户态,直接告诉了用户那些描述符就绪了,不需要循环判断。
5)epoll描述符的事件结构,只需要向内核中拷贝一次,不需要每次都拷贝

缺点:
1)不能跨平台;
2)延时时间只能精确到毫秒;

4.实现epoll版本的HTTP服务器:

步骤:
1)第一步告诉内核要开始对文件描述符进行监控了
2)操作系统对描述符进行监控,采用的是事件触发方式进行监控。为每一个要监控的描述符都定义了一个事件,并且对这个事件都有回调函数(ep_poll_callback())。
3)这个事件回调函数要做的就是将就绪的这个描述符所对应的epoll_event事件结构添加到双向链表中(将事件结构地址添加到双向链表中)
4)epoll_wait并没有立即返回,是每隔一会儿来看看双向链表中是否为空,进而判断是否有描述符就绪。
5)若链表不为空的话就表示有文件描述符就绪,则epoll_wait直接返回。在返回之前,将就绪的描述符对应事件结构向用户太拷贝一份,
6)epoll会将就绪描述符对应的事件拷贝到用户态,直接告诉用户有那些描述符就绪,进而用户可以直接操作就绪的描述符;

MakeFile的实现:

epoll_server:epoll_server.c
    gcc $^ -o $@

epoll_server.c :

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

void handler_events(int epfd,struct epoll_event revs[],int num,int listen_sock)
{
    struct epoll_event ev;
    int i = 0;
    for( ; i < num; i++ )
    {
    int fd = revs[i].data.fd;

    // 如果是监听文件描述符,则调用accept接受新连接

    if( fd == listen_sock && (revs[i].events & EPOLLIN) )
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int new_sock = accept(fd,(struct sockaddr *)&client,&len);

        if( new_sock < 0 )
        {
        perror("accept fail ... \n");
        continue;
        }

       printf("get a new link![%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));

       //因为只是一个http协议:连接成功后,下面就是要 请求和响应 
       // 而服务器端响应之前:要先去读客户端要请求的内容

       ev.events = EPOLLIN;
       ev.data.fd = new_sock;
       epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev);

       continue;
    }

    // 如果是普通文件描述符,则调用read提供读取数据的服务

    if(revs[i].events & EPOLLIN)
    {
        char buf[10240];
        ssize_t s = read(fd,buf,sizeof(buf)-1);
        if( s > 0 )// 读成功了
        {
        buf[s] = 0;
        printf(" %s ",buf);

        // 读成功后,就是要给服务端响应了
        // 而这里的事件是只读事件,所以要进行修改

        ev.events = EPOLLOUT;// 只写事件
        ev.data.fd = fd;
        epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev);// 其中EPOLL_CTL_MOD 表示修改

        }

        else if( s == 0 )
        {
        printf(" client quit...\n ");
        close(fd);// 这里的fd 就是 revs[i].fd
        epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);// 连接关闭,那么就要把描述该连接的描述符关闭
        }
        else// s = -1 失败了
        {   
        printf("read fai ...\n");
        close(fd);// 这里的fd 就是 revs[i].fd
        epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);// 连接关闭,那么就要把描述该连接的描述符关闭
        }
        continue;
    }

    // 服务器端给客户端响应: 写

    if( revs[i].events & EPOLLOUT )
    {
        const char* echo = "HTTP/1.1 200 ok \r\n\r\n<html>hello epoll server!!!</html>\r\n";
        write(fd,echo,strlen(echo));
        close(fd);
        epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
    }
    }
}

int startup( int port )
{
    // 1. 创建套接字
    int sock = socket(AF_INET,SOCK_STREAM,0);//这里第二个参数表示TCP
    if( sock < 0 )
    {
    perror("socket fail...\n");
    exit(2);
    }

    // 2. 解决TIME_WAIT时,服务器不能重启问题;使服务器可以立即重启
    int opt = 1;
    setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = htonl(INADDR_ANY);// 地址为任意类型
    local.sin_port = htons(port);// 这里的端口号也可以直接指定8080
    // 3. 绑定端口号

    if( bind(sock,(struct sockaddr *)&local,sizeof(local)) < 0 )
    {
    perror("bind fail...\n");
    exit(3);
    }

    // 4. 获得监听套接字
    if( listen(sock,5) < 0 )
    {
    perror("listen fail...\n");
    exit(4);
    }
    return sock;
}

int main(int argc,char* argv[] )
{
    if( argc != 2 )
    {
    printf("Usage:%s port\n ",argv[0]);
    return 1;
    }

    // 1. 创建一个epoll模型: 返回值一个文件描述符

    int epfd = epoll_create(256);
    if( epfd < 0 )
    {
    perror("epoll_create fail...\n");
    return 2;
    }

    // 2. 获得监听套接字

   int listen_sock = startup(atoi(argv[1]));//端口号传入的时候是以字符串的形式传入的,需要将其转为整型


    // 3. 初始化结构体----监听的结构列表

    struct epoll_event  ev;
    ev.events = EPOLLIN;//关心读事件
    ev.data.fd = listen_sock;// 关心的描述文件描述符

    // 4. epoll的事件注册函数---添加要关心的文件描述符的只读事件

    epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);

    struct epoll_event revs[128];
    int n = sizeof(revs)/sizeof(revs[0]);

    int timeout = 3000;
    int num = 0;


    while(1)
    {

       // 5 . 开始调用epoll等待所关心的文件描述符集就绪

       switch( num = epoll_wait(epfd,revs,n,timeout) )
       {
       case 0:// 表示词状态改变前已经超过了timeout的时间
           printf("timeout...\n");
           continue;
       case -1:// 失败了
           printf("epoll_wait fail...\n");
           continue;
       default: // 成功了

           handler_events(epfd,revs,num,listen_sock);
           break;
       }
    }
    close(epfd);
    close(listen_sock);
    return 0;
}


猜你喜欢

转载自blog.csdn.net/DX_Jone/article/details/100941145