IO多路转接—select,poll,epoll

news2024/11/15 10:02:17

目录

select

函数介绍

 select基本工作流程

 select的优缺点及适用场景

poll

poll的优缺点

 epoll

epoll的相关系统调用

epoll_create

 epoll_ctl

epoll_wait

 epoll工作原理

epoll服务器编写

 epoll的优点

epoll工作方式 


select

函数介绍

系统提供select函数来实现多路复用输入/输出模型.

select系统调用是用来让我们的程序监视多个文件描述符的状态变化的; 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变

函原型数

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数解释:

nfds:需要监视的最大的文件描述符值+1

rdset:可读文件描述符的集合,是输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的读事件是否就绪,返回时内核告知用户哪些文件描述符的读事件已经就绪。

wrset:可写文件描述符集合,是是输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的写事件是否就绪,返回时内核告知用户哪些文件描述符的写事件已经就绪。

exceptfds:异常文件描述符集合,是输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的异常事件是否就绪,返回时内核告知用户哪些文件描述符的异常事件已经就绪。

timeout:输入输出型参数,调用时由用户设置select的等待时间,返回时表示timeout的剩余时间。它的取值:NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件; 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

fd_set结构体:本质也是一个位图,用位图中对应的位来表示要监视的文件描述符。

比如:(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。 (2)若fd=5,执行FD_SET(fd,&set); 后set变为0001,0000(第5位置为1) *(3)若再加入fd=2,fd=1,则set变为0001,0011 (若fd=1,fd=2上都发生可读事件,则select返回,此时set变为 0000,0011。注意:没有事件发生的fd=5被清空(会自动清理之前加入的文件描述符集合,每次调用都要程序员手动添加,但是这个位操作不需要用户自己进行,)

系统提供了一组专门的接口,用于对fd_set类型的位图进行各种操作。

void FD_CLR(int fd, fd_set *set);      //用来清除描述词组set中相关fd的位
int  FD_ISSET(int fd, fd_set *set);    //用来测试描述词组set中相关fd的位是否为真
void FD_SET(int fd, fd_set *set);      //用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set);             //用来清除描述词组set的全部位

它是用每一个比特位来标记一个文件描述符,所以它的大小决定了它可以关心文件描述符的上限。它的大小是1024个比特位。

返回值说明:

如果函数调用成功,则返回有事件就绪的文件描述符个数。

如果timeout时间耗尽,则返回0。

如果函数调用失败,则返回-1,同时错误码会被设置。

错误码可能存在的情况

  • EBADF:文件描述符为无效的或该文件已关闭。
  • EINTR:此调用被信号所中断。
  • EINVAL:参数nfds为负值。
  • ENOMEM:核心内存不足。

 timeval结构体

结构当中包含两个成员,tv_sec表示的是秒,tv_usec表示的是微秒。

 select基本工作流程

下面以网络通信服务端读事件为例编写代码测试

将服务器创建套接字,绑定,监听,获取新连接等函数封装为一个类,等待被调用。

class Sock
{
public:
    static const int gbacklog = 20;
    static int Socket()//创建套接字
    {
        int listenSock = socket(PF_INET, SOCK_STREAM, 0);
        if (listenSock < 0)
        {
           cout<<"创建套接字失败"<<endl;
           exit(1);
        }
        int opt = 1;
        setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
        return listenSock;
    }
    static void Bind(int socket, uint16_t port)
    {
        struct sockaddr_in local; // 用户栈
        memset(&local, 0, sizeof local);
        local.sin_family = PF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;

        if (bind(socket, (const struct sockaddr *)&local, sizeof local) < 0)//绑定
        {
            cout<<"绑定失败"<<endl;
            exit(2);
        }
    }
    static void Listen(int socket)//监听
    {
        if (listen(socket, gbacklog) < 0)
        {
             cout<<"监听失败"<<endl;
            exit(3);
        }
    }

    static int Accept(int socket, std::string *clientip, uint16_t *clientport)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);

        int serviceSock = accept(socket, (struct sockaddr *)&peer, &len);//获取新连接请求
        if (serviceSock < 0)
        {
            cout<<"获取链接失败"<<endl;
            return 4;
        }
        if (clientport)
            *clientport = ntohs(peer.sin_port);
        if (clientip)
            *clientip = inet_ntoa(peer.sin_addr);
        return serviceSock;
    }
};

编写select.cpp端代码:

#include"sock.hpp"
static void usage(std::string process)
{
    cerr << "\nUsage: " << process << " port\n"
         << endl;
}
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    int listensock = Sock::Socket();
    Sock::Bind(listensock, atoi(argv[1]));
    Sock::Listen(listensock);
    string clientip;
    uint16_t clientport;
    int sock = Sock::Accept(listensock, &clientip, &clientport); // 获取新连接,可能会阻塞
}

当进程执行到获取新链接时,此时若没有客户端请求链接,那么此时服务器会一直在这里阻塞。服务器是要为多个客户端提供服务的,若只有单进程/线程,那么可以采用select方案解决问题。

fd_set *readfds是输入输出型参数,由于每次函数调用返回时都会将其清空,所以我们可以提供一个数组来保存我们要关心的文件描述符集合。

#define DFL -1//将数组中下标未存储要关心的文件描述符设置为-1

int fdsArray[sizeof(fd_set) * 8] = {0}; // 保存历史上所有的合法fd
int gnum = sizeof(fdsArray) / sizeof(fdsArray[0]);

这里将数组0号下标的值设置为listensock

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    int listensock = Sock::Socket();
    Sock::Bind(listensock, atoi(argv[1]));
    Sock::Listen(listensock);
    string clientip;
    uint16_t clientport;
    for (int i = 0; i < gnum; i++)
        fdsArray[i] = DFL;
    fdsArray[0] = listensock;
    while (true)
    {
        int maxFd = DFL;
        fd_set readfds;  //读文件描述符集合
        FD_ZERO(&readfds);
        // fdArray数组存储的是文件描述符,0号下标存储的是监听套接字
        for (int i = 0; i < gnum; i++)
        {
            if (fdsArray[i] == DFL)
                continue;                  // 过滤不合法的fd
            FD_SET(fdsArray[i], &readfds); // 添加所有的合法的文件描述符值添加到readfds中, 
                                           // 方便select统一 ,进行就绪监听
                                        
            if (maxFd < fdsArray[i])
                maxFd = fdsArray[i];       // 更新出最大值
        }

        struct timeval timeout = {2, 0};
        int n = select(maxFd + 1, &readfds, nullptr, nullptr, &timeout);//目前只关心读事件
        switch (n)
        {
        case 0:
            cout << "time out ... : " << (unsigned long)time(nullptr) << endl;
            break;
        case -1:
            cerr << errno << " : " << strerror(errno) << endl;
            break;
        default:
           cout<<"已获取到一个新的连接请求了"<<endl;
            break;
        }
    }
    return 0;
}

结果测试:

 由于没有去获取新连接,所以每次调用该函数,都会进行提醒。

下面对响应进行代码编写

当select函数返回值大于0,表明有读事件就绪了。这里分为两种情况

1.listensock套接字就绪,说明有新的客户端发来请求连接了,这里要注意:(accept函数调用后,返回值也是一个文件描述符,后序二者通信(调用read/write)是根据这个文件描述符进行的,也可能会阻塞,所以也要将该文件描述符加入到第三方数组中,后面添加到readfds集合中)

2.若是普通套接字就绪,那么说明客户端有数据发来,那么可以调用read函数进行读取(这里存在一个问题,read并不能保证可将数据一次性读完),若是连接关闭了,后面也要将该文件描述符从第三方数组中移除掉,这样下次调用该函数时也就不会把它添加进readfds集合中)。

 代码编写:

void HandlerEvent(int listensock, fd_set &readfds) // 说明有读文件描述符就绪了
{
    for (int i = 0; i < gnum; i++)
    {
        if (fdsArray[i] == DFL) // 该文件描述符未被添加,不需要关注
            continue;
        if (i == 0 && fdsArray[i] == listensock) //说明listensock套接字就绪
        {
            if (FD_ISSET(listensock, &readfds))
            {
      
                cout << "已经有一个新链接到来了" << endl;
                string clientip;
                uint16_t clientport = 0;
                int sock = Sock::Accept(listensock, &clientip, &clientport); // 不会阻塞
                if (sock < 0)
                    return;
                cout << "获取新连接成功: " << clientip << ":" << clientport << " | sock: " << sock << endl;

               //此时要把sock加入到读文件描述符集
                int i = 0;
                for (; i < gnum; i++)
                {
                    if (fdsArray[i] == DFL) // 选取一个未被使用的下标存储该文件描述符
                        break;
                }
                if (i == gnum)
                {
                    cerr << "服务器已达上限,无法在承载更多同时保持的连接了" << endl;
                    close(sock);//把该套接字关闭
                }
                else
                {
                    fdsArray[i] = sock; // 将sock添加到select中,进行监听
                }
            }
        }
        else//这里说明普通套接字读事件就绪
        {
            if (FD_ISSET(fdsArray[i], &readfds))
            {
         
                char buffer[1024];
                ssize_t s = recv(fdsArray[i], buffer, sizeof(buffer), 0); // 不会阻塞
                if (s > 0)
                {
                    buffer[s] = 0;
                    cout << "client[" << fdsArray[i] << "]# " << buffer << endl;
                }
                else if (s == 0)
                {
                    cout << "client[" << fdsArray[i] << "] quit, server close " << fdsArray[i] << endl;
                    close(fdsArray[i]);
                    fdsArray[i] = DFL; // 去除对该文件描述符的select事件监听
                }
                else
                {
                    cout << "client[" << fdsArray[i] << "] error, server close " << fdsArray[i] << endl;
                    close(fdsArray[i]);
                    fdsArray[i] = DFL; // 去除对该文件描述符的select事件监听
                }
            }
        }
    }
}

 结果测试:

 select的优缺点及适用场景

优点:

可以同时等待多个文件描述符,并且只负责等待,实际的IO操作由accept、read、write等接口来完成,此时调用这些接口在进行IO操作时不会被阻塞。

select同时等待多个文件描述符,因此可以将“等”的时间重叠,提高了IO的效率

缺点:

select可监控的文件描述符个数是取决于fd_set类型的比特位个数的,所以能关心的文件描述符是有上限的。

需要自己手动去维护第三方的数组去完成文件描述符的添加删除等。

每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

同时每次调用select都需要在内核遍历传递进来的所有fd。

适用场景:

一般适用于多连接,并且这些连接并不频繁的进行通信。也就意味着几乎所有的连接在进行IO操作时,都需要花费大量时间来等待事件就绪,此时使用多路转接接口就可以将这些等的事件进行重叠,提高IO效率,比如聊天工具等。

poll

poll函数功能与select函数是类似的。

函数原型:

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数解释:

  • fds:一个poll函数监视的结构列表,每一个元素包含三部分内容:文件描述符、监视的事件集合、就绪的事件集合。
  • nfds:表示fds数组的长度。
  • timeout:表示poll函数的超时时间,单位是毫秒(ms)。

返回值说明:

  • 如果函数调用成功,则返回有事件就绪的文件描述符个数。
  • 如果timeout时间耗尽,则返回0。
  • 如果函数调用失败,则返回-1,同时错误码会被设置。

timeout取值

-1:poll调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪。
0:poll调用后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,poll检测后都会立即返回。
特定的时间值:poll调用后在指定的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在该时间后poll进行超时返回。

pollfd结构

包含三个成员

struct pollfd {
 int fd; /* file descriptor */
 short events; /* requested events */
 short revents; /* returned events */
};

events和revents的常用取值:

POLLIN:数据(包括普通数据和优先数据)可读.

POLLOUT数据(包括普通数据和优先数据)可写.

取值实际都是以宏的方式进行定义的,它们的二进制序列当中有且只有一个比特位是1,且为1的比特位是各不相同的。

有struct pollfd 结构体,输入输出事件进行了分离,不用每次都去添加要关心文件描述符,对参数设置时可以直接添加,无需调用其它函数去设置,更加方便。

代码编写(与select代码编写类似)

可定义一个struct pollfd结构体数组,用来存储我们要关心的文件描述符及对应的事件。

struct pollfd fdsArray[NUM]; // 保存历史上所有的合法fd

#define DFL -1

static void usage(std::string process)
{
    cerr << "\nUsage: " << process << " port\n"
         << endl;
}
void HandlerEvent(int listensock) // 说明有读文件描述符就绪了
{
    for (int i = 0; i < NUM; i++) // fdsArray存储的是要关心文件描述符读事件
    {
        if (fdsArray[i].fd == DFL) // 改文件描述符未被添加,不需要关注
            continue;
        if (i == 0 && fdsArray[i].fd == listensock) //
        {
            if (fdsArray[0].revents & POLLIN)
            {
                // 具有了一个新链接
                cout << "已经有一个新链接到来了,需要进行获取(读取/拷贝)了" << endl;
                string clientip;
                uint16_t clientport = 0;
                int sock = Sock::Accept(listensock, &clientip, &clientport); // 不会阻塞
                if (sock < 0)
                    return;
                cout << "获取新连接成功: " << clientip << ":" << clientport << " | sock: " << sock << endl;
                int i = 0;
                for (; i < NUM; i++)
                {
                    if (fdsArray[i].fd == DFL) // 数组i的下标未被使用
                        break;
                }
                if (i == NUM)
                {
                    cerr << "我的服务器已经到了最大的上限了,无法在承载更多同时保持的连接了" << endl;
                    close(sock);
                }
                else
                {
                    fdsArray[i].fd = sock; // 将sock添加到select中,对其进行监听
                    fdsArray[i].events = POLLIN;
                    fdsArray[i].revents = 0;
                }
            }
        }
        else
        {
            if (fdsArray[i].revents & POLLIN)
            {

                char buffer[1024];
                ssize_t s = recv(fdsArray[i].fd, buffer, sizeof(buffer), 0); // 不会阻塞
                if (s > 0)
                {
                    buffer[s] = 0;
                    cout << "client[" << fdsArray[i].fd << "]# " << buffer << endl;
                }
                else if (s == 0)
                {
                    cout << "client[" << fdsArray[i].fd << "] quit, server close " << fdsArray[i].fd << endl;
                    close(fdsArray[i].fd);
                    fdsArray[i].fd = DFL; // 去除对该文件描述符的select事件监听
                    fdsArray[i].events = 0;
                    fdsArray[i].revents = 0;
                }
                else
                {
                    cout << "client[" << fdsArray[i].fd << "] error, server close " << fdsArray[i].fd << endl;
                    close(fdsArray[i].fd);
                    fdsArray[i].fd = DFL; // 去除对该文件描述符的select事件监听
                    fdsArray[i].events = 0;
                    fdsArray[i].revents = 0;
                }
            }
        }
    }
}
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    int listensock = Sock::Socket();
    Sock::Bind(listensock, atoi(argv[1]));
    Sock::Listen(listensock);

    for (int i = 0; i < NUM; i++)
    {
        fdsArray[i].fd = DFL;
        fdsArray[i].events = 0;
        fdsArray[i].revents = 0;
    }

    fdsArray[0].fd = listensock;//将数组0号下标添加为listensock
    fdsArray[0].events = POLLIN;//添加挂心的事件,读事件
    int timeout = -1;
    cout << "已添加listensock套接字" << endl;
    while (true)
    {
        int n = poll(fdsArray, NUM, timeout);
        switch (n)
        {
        case 0:
            cout << "time out ... : " << (unsigned long)time(nullptr) << endl;
            break;
        case -1:
            cerr << errno << " : " << strerror(errno) << endl;
            break;
        default:
            HandlerEvent(listensock);
        }
    }
    return 0;
}

poll的优缺点

优点:

1.pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式. 接口使用比 select更方便.

2.poll并没有最大数量限制 (但是数量过大后性能也是会下降).

缺点:

1.与select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符.

2.每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.

3.同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.

 epoll

基本介绍:

按照man手册的说法: 是为处理大批量句柄而作了改进的poll.

它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44) 它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法.

epoll系统调用也可以让我们的程序同时监视多个文件描述符上的事件是否就绪,与select和poll的功能类似。

epoll的相关系统调用

epoll_create

epoll_create函数用于创建一个epoll模型,函数原型如下:

int epoll_create(int size);

参数解释

创建一个epoll的句柄. 自从linux2.6.8之后,size参数是被忽略的,但值要设置大于0的值, 用完之后, 必须调用close()关闭.

返回值说明:

epoll模型创建成功返回其对应的文件描述符,否则返回-1,同时错误码会被设置。

 epoll_ctl

用于向指定的epoll模型中注册事件,函数原型:

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

 参数解释:

epfd:指定的epoll模型。

op:表示具体的动作,用三个宏来表示。

fd:需要监视的文件描述符。

event:需要监视该文件描述符上的哪些事件。

 第二个参数op取值:

EPOLL_CTL_ADD:注册新的文件描述符到epoll模型中。

EPOLL_CTL_MOD:修改已经注册的文件描述符的监听事件。

EPOLL_CTL_DEL:从epoll模型中删除指定的文件描述符

 struct epoll_event结构

第2个成员data是一个联合体结构,一般是选用该结构当中的fd,表示需要监听的文件描述符。

 events可以是以下几个宏的集合,也是以宏的方式进行定义的,它们的二进制序列当中有且只有一个比特位是1。

EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)。
EPOLLOUT:表示对应的文件描述符可以写。
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)。
EPOLLERR:表示对应的文件描述符发送错误。
EPOLLHUP:表示对应的文件描述符被挂断,即对端将文件描述符关闭了。
EPOLLET:将epoll的工作方式设置为边缘触发(Edge Triggered)模式。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听该文件描述符的话,需要重新将该文件描述符添加到epoll模型中。

epoll_wait

用于收集监视的事件中已经就绪的事件,该函数原型:

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

参数解释:

epfd:表示指定的epoll模型。
events:内核会将已经就绪的事件拷贝到events数组当中(events不能是空指针,内核只负责将就绪事件拷贝到该数组中,不会帮我们在用户态中分配内存)。
maxevents:events数组中的元素个数,该值不能大于创建epoll模型时传入的size值。
timeout:表示epoll_wait函数的超时时间,单位是毫秒(ms)。设置为-1位阻塞等待,0为非阻塞等待,设置时间后一直没有就绪,epoll_wait进行超时返回,值为0

返回值说明:

如果函数调用成功,则返回有事件就绪的文件描述符个数。

如果timeout时间耗尽,则返回0。

如果函数调用失败,则返回-1,同时错误码会被设置。

 epoll工作原理

当调用函数epoll_create时,Linux内核会创建一个eventpoll结构体,也就是我们所说的epoll模型。这个结构体中有两个成员与epoll的使用方式密切相关。

struct eventpoll{
	...
	//红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监视的事件
	struct rb_root rbr;

	//就绪队列中则存放着将要通过epoll_wait返回给用户的满足条件的事件
	struct list_head rdlist;
	...
}

有两种数据数据结构,一是红黑树,调用epll_ctl函数,这些事件都会挂载在红黑树中,实际就是在对这颗红黑树进行对应的增删改操作。(效率高)

二是就绪队列,调用epoll_wait函数实际就是在从就绪队列当中获取已经就绪的事件。

在epoll中,对于每一个事件,都会建立一个epitem结构体,红黑树和就绪队列当中的节点分别是基于epitem结构中的rbn成员和rdllink成员的,epitem结构当中的成员ffd记录的是指定的文件描述符值,event成员记录的就是该文件描述符对应的事件。

struct epitem{
	struct rb_node rbn; //红黑树节点
	struct list_head rdllink; //双向链表节点
	struct epoll_filefd ffd; //事件句柄信息
	struct eventpoll *ep; //指向其所属的eventpoll对象
	struct epoll_event event; //期待发生的事件类型
}

回调机制

所有添加到红黑树当中的事件,都会与设备(网卡)驱动程序建立回调方法,这个回调方法在内核中叫ep_poll_callback。

1.对于select和poll来说,操作系统在监视多个文件描述符上的事件是否就绪时,需要让操作系统主动对这多个文件描述符进行轮询检测,这一定会增加操作系统的负担。
2.对于epoll来说,操作系统不需要主动进行事件的检测,当红黑树中监视的事件就绪时,会自动调用对应的回调方法,将就绪的事件添加到就绪队列当中。
3.当用户调用epoll_wait函数获取就绪事件时,只需要关注底层就绪队列是否为空,如果不为空则将就绪队列当中的就绪事件拷贝给用户即可。

补充:epoll是线程安全的。允许多个执行流同时访问。

epoll服务器编写

创建一个EpollServer类,向外提供一些接口。

using namespace std;

class EpollServer
{
public:
    static const int gsize = 128;
    static const int num = 256;
    using func_t = function<int(int)>;//回调方法,用来读取就绪文件描述符的数据

public:
    EpollServer(uint16_t port, func_t func) : port_(port), listensock_(-1), epfd_(-1), func_(func)
    {
    }
    void InitEpollServer()
    {
        listensock_ = Sock::Socket();//创建套接字
        Sock::Bind(listensock_, port_);//监听
        Sock::Listen(listensock_);//绑定

        // 这里直接使用原生接口
        epfd_ = epoll_create(gsize);//设置大于0的数即可
        if (epfd_ < 0)
        {
            cout<<"创建epoll模型失败"<<endl;
            exit(-1);
        }
    }
    ~EpollServer()
    {
        if (listensock_ != -1)
            close(listensock_);
        if (epfd_ != -1)
            close(epfd_);
    }
private:
    int listensock_;
    int epfd_;
    uint16_t port_;
    func_t func_;
};

创建epoll模型后,向其添加要关心的文件描述符及事件。并创建一个   struct epoll_event数组,用来存放就绪的事件。

  void Run()
    {
        //  添加listensock_
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = listensock_;
        int n = epoll_ctl(epfd_, EPOLL_CTL_ADD, listensock_, &ev);
        cout<<"添加listensock success"<<endl;
        assert(n==0);
      
        struct epoll_event revs[num];//存放就绪事件
        int timeout = 1000;
        while (true)
        {
            int n = epoll_wait(epfd_, revs, num, timeout);//返回值是就绪队列的个数
            switch (n)
            {
            case 0:
                cout << "time out ... : " << (unsigned long)time(nullptr) << endl;
                break;
            case -1:
                cerr << errno << " : " << strerror(errno) << endl;
                break;
            default:
                HandlerEvents(revs, n);//对就绪事件做处理
                break;
            }
        }
    }

对就绪事件做处理时,也要判断是监听套接字就绪还是普通套接字字就绪。若是有新的连接请求到来,也要创建 struct epoll_event ev结构体,填充对应信息后,将其加入epfd中进行监听,与select与poll类似。

 void HandlerEvents(struct epoll_event revs[], int n)
    {
        for (int i = 0; i < n; i++)
        {
            int sock = revs[i].data.fd;
            uint32_t revent = revs[i].events;
            if (revent & EPOLLIN) // 读事件就绪
            {
                if (sock == listensock_)  // 监听socket就绪, 获取新链接
                {
                    cout<<"有新连接到来"<<endl;
                    string clientip;
                    uint16_t clientport = 0;
                    int sockfd = Sock::Accept(listensock_, &clientip, &clientport);
                    if (sockfd < 0)
                    {
                        continue;
                    }
                    cout<<"获取新链接成功"<<endl;
                    // 托管给epoll
                    struct epoll_event ev;
                    ev.events = EPOLLIN;
                    ev.data.fd = sockfd;
                    int n = epoll_ctl(epfd_, EPOLL_CTL_ADD, sockfd, &ev);
                    assert(n == 0);
                    (void)n;
                }
                else
                {
                    // 普通socket就绪
                    cout<<"有数据到来"<<endl;
                    int n = func_(sock);//执行回调方法,对数据进行读取

                    if (n == 0 || n < 0)//说明连接断开,要关闭对应文件描述符
                    {
                        // 先移除,在关闭
                        int x = epoll_ctl(epfd_, EPOLL_CTL_DEL, sock, nullptr);
                        assert(x == 0);
                        (void)x;
                        close(sock);
                    }
                }
            }
        }
    }

补充:

1.所谓的事件处理并不是调用epoll_wait将底层就绪队列中的就绪事件拷贝到用户层,比如当这里的监听套接字就绪后,我们应该调用accept获取底层建立好的连接,普通套接字就绪后要调用recv读取客户端发来的数据,这才算是将读事件处理了。

2.如果只是调用epoll_wait将底层就绪队列当中的事件拷贝到应用层,那么这些就绪事件实际并没有被处理掉,底层注册的回调函数会被再次调用,会将就绪的事件重新添加到就绪队列当中

 编写server端代码

static void usage(std::string process)
{
    cerr << "\nUsage: " << process << " port\n"
         << endl;
}
int myfunc(int sock)
{
    char buffer[1024];
    ssize_t s = recv(sock, buffer, sizeof(buffer)-1, 0); //不会被阻塞
    if(s > 0)
    {
        buffer[s] = 0;
        cout<<buffer<<endl;
    }
    return s;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(0);
    }
    unique_ptr<EpollServer> epollserver(new EpollServer(atoi(argv[1]), myfunc));
    cout<<"创建epollserver success"<<endl;
    epollserver->InitEpollServer();
    cout<<"epollserver初始化成功"<<endl;
    epollserver->Run();

    return 0;
}

结果:

 epoll的优点

接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文 件描述符, 也做到了输入输出参数分离开
数据拷贝轻量:只在新增监视事件的时候调用epoll_ctl将数据从用户拷贝到内核,而select和poll每次都需要重新将需要监视的事件从用户拷贝到内核。此外,调用epoll_wait获取就绪事件时,只会拷贝就绪的事件,不会进行不必要的拷贝操作。
事件回调机制:避免操作系统主动轮询检测事件就绪,而是采用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中。调用epoll_wait时直接访问就绪队列就知道哪些文件描述符已经就绪,检测是否有文件描述符就绪的时间复杂度是O ( 1 ) O(1)O(1),因为本质只需要判断就绪队列是否为空即可。
没有数量限制:监视的文件描述符数目无上限,只要内存允许,就可以一直向红黑树当中新增节点。

epoll工作方式 

epoll有2种工作方式-水平触发(LT)和边缘触发(ET)

水平触发(LT)(默认工作方式):只要底层有事件就绪,epoll就会一直通知用户。select和poll其实就是工作是LT模式下的。支持阻塞读写和非阻塞读写

边缘触发(ET):只有底层就绪事件数量由无到有或由有到多发生变化的时候,epoll才会通知用户在ET模式下, 文件描述符上的事件就绪后,只有有1次处理机会。只支持非阻塞的读写

如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式.

 ET工作模式下,recv和send操作要循环进行,且文件描述符必须设置为非阻塞状态。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/439396.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring核心设计思想

目录 前言&#xff1a; Spring是什么 什么是IoC 传统开发思想 IoC开发思想 Spring IoC 什么是DI 小结&#xff1a; 前言&#xff1a; 官网中提出&#xff1a;Spring makes programming Java quicker, easier, and safer for everybody. Spring’s focus on speed, simp…

YOLOv7+单目测距(python)

YOLOv7单目测距&#xff08;python&#xff09; 1. 相关配置2. 测距原理3. 相机标定3.1&#xff1a;标定方法13.2&#xff1a;标定方法2 4. 相机测距4.1 测距添加4.2 主代码 5. 实验效果 相关链接 1. YOLOV5 单目测距&#xff08;python&#xff09; 2. YOLOV5 双目测距&…

基于springboot的招聘信息管理系统源码数据库论文

目 录 1 绪 论 1.1 课题背景与意义 1.2 系统实现的功能 1.3 课题研究现状 2系统相关技术 2.1 Java语言介绍 2.2 B/S架构 2.3 MySQL 数据库介绍 2.4 MySQL环境配置 2.5 SpringBoot框架 3系统需求分析 3.1系统功能 3.2可行性研究 3.2.1 经济可行性 …

力扣sql中等篇练习(六)

力扣sql中等篇练习(六) 1 购买了产品A和产品B却没有购买产品C的顾客 1.1 题目内容 1.1.1 基本题目信息 1.1.2 示例输入输出 1.2 示例sql语句 # 先求出既有的,然后再去筛选掉没有的 # 去重用不了内连接 SELECT t1.customer_id,c.customer_name FROM ( SELECT distinct cust…

《Spring MVC》 第二章 第一个程序

前言 Spring MVC 是 Spring 框架提供的一款基于 MVC 模式的轻量级 Web 开发框架。 Spring MVC 本质是对 Servlet 的进一步封装&#xff0c;其最核心的组件是DispatcherServlet&#xff0c;它是 Spring MVC 的前端控制器&#xff0c;主要负责对请求和响应的统一地处理和分发。C…

C++ auto 内联函数 指针空值

本博客基于 上一篇博客的 序章&#xff0c;主要对 C 当中对C语言的缺陷 做的优化处理。 上一篇博客&#xff1a;C 命名空间 输入输出 缺省参数 引用 函数重载_chihiro1122的博客-CSDN博客 auto关键字 auto作为一个新的类型指示符来指示编译器&#xff0c;auto声明的变量必须由…

uni-app使用时遇到的坑

一.uni-app开发规范 1.微信小程序request请求需要https 小程序端&#xff1a; 在本地运行时&#xff0c;可以使用http 但是预览或者上传时&#xff0c;使用http无法请求 APP端&#xff1a; 一般APP可以使用http访问 高版本的APP可能需要用https访问 二. uni-app项目 配置App升…

Java语言请求示例,电商商品详情接口,接口封装

Java具有大部分编程语言所共有的一些特征&#xff0c;被特意设计用于互联网的分布式环境。Java具有类似于C语言的形式和感觉&#xff0c;但它要比C语言更易于使用&#xff0c;而且在编程时彻底采用了一种以对象为导向的方式。 使用Java编写的应用程序&#xff0c;既可以在一台…

如何更好的进行数据管理?10 条建议给到你

这个时代数据量的快速增长和数据复杂性的大幅度提高&#xff0c;让企业迫切的寻找更加智能的方式管理数据&#xff0c;从而有效提高 IT 效率。 管理数据库不是单一的目标&#xff0c;而是多个目标并行&#xff0c;如数据存储优化、效率、性能、安全。只有管理好数据从创建到删除…

newman结合jenkins实现自动化测试

一、背景 为了更好的保障产品质量和提升工作效率&#xff0c;使用自动化技术来执行测试用例。 二、技术实现 三、工具安装 3.1 安装newman npm install -g newman查看newman版本安装是否成功&#xff0c;打开命令行&#xff0c;输入newman -v&#xff0c;出现 版本信息即安…

浅述 国产仪器 6362D光谱分析仪

6362D光谱分析仪&#xff08;简称&#xff1a;光谱仪&#xff09;是一款高分辨、大动态高速高性能光谱分析仪&#xff0c;适用于600&#xff5e;1700nm光谱范围的DWDM、光放大器等光系统测试&#xff1b; LED、FP-LD、DFB-LD、光收发器等光有源器件测试&#xff1b;光纤、光纤光…

C语言基础应用(五)循环结构

引言 如果要求123…100&#xff0c;你会怎么求解呢&#xff1f; 如果按照常规代码 int main() {int sum 0;sum 1;sum 2;sum 3;...sum 100;printf("The value of sum is %d\n",sum);return 0; }就会特别麻烦&#xff0c;并且代码过于冗长。下面将引入循环的概念…

硬件知识的基础学习

GPIO、继电器、三极管、PWM、MOS管 的 输入与输出。 本人没有系统的学习过专业的硬件知识&#xff0c;只有在实践过程中向前辈简单的学习&#xff0c;若有问题&#xff0c;还请大佬指正。 目录 一、GPIO 1.1 输入与输出的区别 1.2 输入 1.2.1 电流流向和电阻区分上拉输入…

动力节点老杜Vue笔记——Vue程序初体验

目录 一、Vue程序初体验 1.1 下载并安装vue.js 1.2 第一个Vue程序 1.3 Vue的data配置项 1.4 Vue的template配置项 一、Vue程序初体验 可以先不去了解Vue框架的发展历史、Vue框架有什么特点、Vue是谁开发的&#xff0c;对我们编写Vue程序起不到太大的作用&#xff0c;…

计算机网络 实验六

⭐计网实验专栏&#xff0c;欢迎订阅与关注&#xff01; ★观前提示&#xff1a;本篇内容为计算机网络实验。内容可能会不符合每个人实验的要求&#xff0c;因此以下内容建议仅做思路参考。 一、实验目的 掌握以太网帧的格式及各字段的含义掌握IP包的组成格式及各字段的含义掌…

java中HashMap的使用

HashMap 键值对关系&#xff0c;值可以重复&#xff0c;可以实现多对一&#xff0c;可以查找重复元素 记录&#xff1a; 做算法遇到好多次了&#xff0c;就总结一下大概用法。 例如今天遇到的这个题&#xff1a; 寻找出现一次的数&#xff0c;那就使用哈希表来存储&#xf…

X射线吸收光谱知识点

1) 什么是XAS XAS是X-ray Absorbtion Spectra的缩写&#xff0c;全称为X射线吸收光谱。X射线透过样品后&#xff0c;其强度发生衰减且其衰减程度与材料结构、组成有关。这种研究透射强度I与入射X射线强度Io之间的关系&#xff0c;称为X射线吸收光谱;由于其透射光强与元素、原子…

express项目的创建

前言 前端开发者若要进行后端开发&#xff0c;大多都会选择node.js&#xff0c;在node生态下是有大量框架的&#xff0c;其中最受新手喜爱的便是老牌的express.js&#xff0c;接下来我们就从零创建一个express项目。 安装node 在这里&#xff1a;https://nodejs.org/dist/v16…

《Linux0.11源码解读》理解(一)

计算机启动时, 内存(RAM)没有任何东西, 自然也无法跑操作系统. 但是可以执行固化在ROM里面的BIOS程序. 在按下电源键的一刻. CPU的cs和ip寄存器硬件被设置为0xf000和0xfff0, 于是cs:ip也就指向0xffff0这个地址, 而这个地址正是指向了ROM的BIOS范围(这里是0xfe000~0xfffff, 20根…

2023 减少人工标注,获取大量数据的能力

关键词&#xff1a; 零样本泛化能力模型 半监督 减少人工标注成本&#xff1a; 1、CVPR 2023 | 单阶段半监督目标检测SOTA&#xff1a;ARSL https://zhuanlan.zhihu.com/p/620076458 2、CVPR 2023 | 标注500类&#xff0c;检测7000类&#xff01;清华大学等提出通用目标检测算…