• 多路转接(使用epoll实现)


    1.epoll的引入

    epoll是poll的升级版本,它是在Linux2.5.44内核中被引进的。它几乎具备了之前所说的一切优点,被公认为是Linux2.6下性能最好的多路I/O就绪通知方法,即以后要使用多路转接,使用epoll时性价比最高的。

    2.epoll相关的系统调用

    不同于select和poll,epoll有三个系统调用来帮助完成等待的工作。

    2.1epoll_create

    int epoll_create(int size);

    它的意思是创建一个epoll空间,epoll空间具体表示的是两段空间,一个策略:红黑树,就绪队列以及回调策略。这个在下面epoll原理中会进行详细的介绍。它的参数已经弃用了。
    我们可以将该空间理解成一个文件,即返回值是一个文件描述符,用完该空间之后必须进行关闭。

    2.2epoll_ctl

    2.2.1参数

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

    它的作用是用户告诉内核,哪些事件需要进行关心。
    第一个参数表示的就是epoll_create创建的epoll空间,第二个参数表示的是操作的选项(可以是删除,更改或者注册一个文件描述符),第三个参数表示要监听的fd,第四个参数告诉内核要监听的事件。

    2.2.2选项

    对于第二个参数的选项,一共有三种:

    作用
    EPOLL_CTL_ADD注册新的fd到epfd中
    EPOLL_CTL_MOD修改已经注册的fd的监听事件
    EPOLL_CTL从epfd中删除一个fd

    一旦设置好需要监听之后,OS就已经记住了,如果想删除或者修改,重新调用epoll_ctl即可。

    2.2.3结构体

    对于第三个参数,它的结构体结构如下:

    struct epoll_event 
    {
    uint32_t     events;      /* Epoll events */
    epoll_data_t data;        /* User data variable */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    event表示对于某个事件的关心,它的作用和poll中的events是一样的,它的取值如下:

    作用
    EPOLLIN表示对应的文件描述符可以读(包括堆对端SOCKET正常关闭)
    EPOLLOUT表示对应的文件描述符可以写
    EPOLLPRI表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据的到来)
    EPOLLERR表示对应的文件描述符发生错误
    EPOLLHUP表示对应的文件描述符被挂断
    EPOLLET将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的
    EPOLLONESHOT只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
    第二个参数也是一个结构体,它的结构是这样的:
    typedef union epoll_data {
     void        *ptr;
     int          fd;
     uint32_t     u32;
     uint64_t     u64;
    } epoll_data_t;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们需要关心的是fd字段。

    2.3epoll_wait

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

    它表示的是OS通知用户,哪些文件描述符已经就绪了。
    参数events是分配好的epoll_event结构体数组(注意是数组)。
    epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会帮助我们在用户态分配内存)
    maxevents告知内核这个events由多大,timeout以毫秒为单位,-1表示永久阻塞。
    如果函数调用成功,返回对应I/O上已经准备好的文件描述符数目,如果返回0表示已经超时,返回小于0表示函数失败。
    值得注意的是,events一定是从下到大有连续继续数据的。

    3.epoll工作原理

    3.1epoll_create

    当创建一个epoll空间的时候,会完成三部分内容:建立一个红黑树,建立一个就绪队列,明确使用回调策略。
    在这里插入图片描述

    3.2epoll_ctrl

    当epoll_ctrl的作用是,向文件描述符中输入节点,每一个节点需要包括文件描述符和该文件描述符对应的events,当增加要监听的文件描述符时就插入节点,当删除文件描述符时,就删除节点。

    3.3epoll_wait

    当我们将timeout设置为-1的时候,没有数据就绪的话进程就会进入等待队列进行等待。一旦有数据就绪了,网卡驱动就会将数据传入到内核中,OS将使用回调策略生成一个就绪序列的节点,并将所有就绪的文件描述符形成的节点插入到就绪序列中。但是其实没有将fd一同放进该就绪序列的节点中,此时epoll_data发挥作用(因为它保存着文件描述符)。
    就绪序列有数据了,进程被唤醒(从等待队列到运行队列),然后进程只需要与就绪队列打交道,以O(1)的时间复杂度检测是否有数据就绪(即检测就绪队列是否为空)
    在这里插入图片描述

    3.epoll实现多路转接

    3.1实现思路

    其实epoll和select以及poll的实现思路是基本相同的。
    1.首先实现服务端的代码。
    2.建立一个epoll模型。
    3.使用epoll_ctl先将listen_sock描述符添加到epoll模型中。
    4.调用循环,每一次进行epoll_wait,当有文件描述符就绪的时候,分为两种情况:
    分别是当监听套接字就绪了以及正常的读取就绪了。
    当监听套接字就绪的时候,除了需要使用accept来进行处理之外,还需要将accept返回的文件描述符添加到epoll的空间中。
    当有正常数据就绪的时候分三种情况进行讨论即可。

    3.2网络通信代码

    #include 
    #include
    #include
    #include
    #include
    #include
    #include
    using namespace std;
    namespace ns_Sock
    {
        class Sock
        {
        private:
        public:
            static int Socket()
            {
                int sock = socket(AF_INET, SOCK_STREAM, 0);
                if (sock < 0)
                {
                    cerr << "创建套接字失败" << endl;
                    exit(-2);
                }
                else
                {
                    return sock;
                }
            }
            static int Listen(int sock)
            {
                if(listen(sock,5)<0)
                {
                    cerr<<"listen error"<<endl;
                    exit(-3);
                }
            }
            static int Accept(int sock)
            {
                struct sockaddr_in peer;
                socklen_t len=sizeof(peer);
                int fd=accept(sock,(struct sockaddr*)&peer,&len);
                if(fd>=0)
                {
                    return fd;
                }
                else
                {
                    exit(-5);
                }
            }
            static void Bind(int sock,uint16_t port)
            {
                sockaddr_in local;
                local.sin_family=AF_INET;
                local.sin_port=htons(port);
                local.sin_addr.s_addr=INADDR_ANY;
                if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
                {
                    cerr<<"bind error"<<endl;
                    exit(-4);
                }
            }
            static void Connect(int sock,string ip,uint16_t port)
            {
                struct sockaddr_in server;
                memset(&server,0,sizeof(server));
                server.sin_family=AF_INET;
                server.sin_port=htons(port);
                server.sin_addr.s_addr=inet_addr(ip.c_str());
                if(connect(sock,(struct sockaddr*)&server,sizeof(server))==0)
                {
                    cout<<"connect success!"<<endl;
                }
                else
                {
                    cout<<"connect fail"<<endl;
                    exit(-5);
                }
            }
        };
    }
    
    • 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

    3.3epoll实现网络通信

    #include"Sock.hpp"
    using namespace ns_Sock;
    #define NUM 128
    #include
    void Usage(char* proc)
    {
        cout<<"Usage \n\t"<<proc<<" port"<<endl;
    }
    int main(int argc,char* argv[])
    {
        if(argc!=2)
        {
            Usage(argv[0]);
            exit(-1);
        }
        uint16_t port=(uint16_t)atoi(argv[1]);
        int listen_sock=Sock::Socket();
        Sock::Bind(listen_sock,port);
        Sock::Listen(listen_sock);
    
        //建立epoll模型,获得epfd
        int epfd=epoll_create(128);
        //先添加listen_sock和它所关心的事件到内核中
        struct epoll_event ev;
        ev.events=EPOLLIN;
        ev.data.fd=listen_sock;//虽然epoll_ctl有文件描述符,但是revs数组中的元素是epoll_event没有fd,因此需要将fd添加都epoll_event的data字段中
        epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);
        //事件循环
        volatile bool quit=false;
        struct epoll_event revs[NUM];//由于epoll_wait的数组是输出型参数,因此需要接收
        while(!quit)
        {
            int timeout=1000;
            int n=epoll_wait(epfd,revs,NUM,-1);//epoll_wait会将epfd中就绪事件的epoll_event结构体放在revs数组中,返回值表示数组大小
            switch(n)
            {
                case 0:
                cout<<"timeout....."<<endl;
                break;
                case -1:
                cerr<<"epoll error"<<endl;
                break;
                default:
                cout<<"有事件就绪了"<<endl;
                //处理就绪事件
                for(int i=0;i<n;i++)
                {
                    int sock=revs[i].data.fd;//暂时方案
                    cout<<"文件描述符"<<sock<<"有数据就绪了"<<endl;
                    if(revs[i].events&EPOLLIN)//读事件就绪
                    {
                        cout<<"文件描述符"<<sock<<"读事件就绪了"<<endl;
                        if(sock==listen_sock)
                        {
                            int fd=Sock::Accept(listen_sock);
                            if(fd>=0)
                            {
                                cout<<"获取新链接成功了"<<endl;//此时还不能读需要添加到epfd的空间中
                                struct epoll_event _ev;
                                _ev.events=EPOLLIN;
                                _ev.data.fd=fd;
                                epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&_ev);
                                cout<<"已经把"<<fd<<"添加到epfd空间中了"<<endl;
                            }
                        }
                        //正常的读处理
                        else
                        {
                            cout<<"文件描述符"<<sock<<"正常数据准备就绪"<<endl;
                            char buffer[1024];
                            ssize_t s=read(sock,buffer,sizeof(buffer)-1);
                            if(s>0)
                            {
                                buffer[s]=0;
                                cout<<"client["<<sock<<"]#"<<buffer<<endl;
                            }
                            else if(s==0)
                            {
                                cout<<"client quit "<<sock<<"将被关闭"<<endl;
                                close(sock);
                                epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr);//将该套接字从epoll空间关注的位置删除
                                cout<<"Sock:"<<sock<<"delete from epoll success"<<endl;
                            }
                            else
                            {
                                cout<<"recv error"<<endl;
                                close(sock);
                                epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr);
                                cout<<"delete sock"<<sock<<endl;
                            }
                        }
                    }
                }
            }
    
        }
    }
    
    • 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
  • 相关阅读:
    【Spring】IOC底层原理
    Prometheus基于Consul的 Redis 多实例监控方案
    中国林业产业杂志中国林业产业杂志社中国林业产业编辑部2022年第5期目录
    基于CentOS8操作cobbler批量装机
    C和指针 第13章 高级指针话题 13.2 高级声明
    2023 江西省赛 【9.26训练补题】
    node.js学习之路由、中间件
    大文件上传时如何做到 秒传?
    Stable Diffusion WebUI如何做到真人和动漫互相转换,就在这里!
    后端工程师的前端之路系列--小程序学习
  • 原文地址:https://blog.csdn.net/qq_51492202/article/details/126803210