多路转接(使用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 */
};

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;

我们需要关心的是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 <iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<unistd.h>
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);
            }
        }
    };
}

3.3epoll实现网络通信

#include"Sock.hpp"
using namespace ns_Sock;
#define NUM 128
#include<sys/epoll.h>
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;
                        }
                    }
                }
            }
        }

    }
}
posted @ 2022-09-24 20:28  卖寂寞的小男孩  阅读(41)  评论(0)    收藏  举报