IO多路转接之select、poll、epoll

目录

select

Socket就绪条件        

读就绪

写就绪

异常就绪

实现select服务器

 当前存在的问题:

select优点

select的缺点

poll

 实现poll服务器

总结

epoll

epoll工作原理

epoll的优缺点总结

实现epoll服务器

epoll的工作模式

ET vs LT 谁更高效?


select

select是一个多路转接接口,使用select调用来监视多个文件描述符的变化。

函数原型:

 参数说明:

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

相似用法的三个参数:

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

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

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

timeout:输入输出型参数,调用时由用户设置select的等待时间,返回时表示timeout的剩余时间

timeout取值:

nullptr:select调用后进行阻塞等待,直到某个被监视的文件描述符上的某个事件就绪

0:select调用后进行非阻塞等待,无论被监视的文件描述符是否就绪,select检测后都会立即返回

特定的时间值:select调用后在指定的时间内进行阻塞等待,如果被监视的文件描述符都没有就绪,函数就返回0,表示超时返回

返回值:

大于0:有几个fd就绪

=0:timeout超时

<0:表示调用错误,错误码被设置

错误码类型:

EBADF:在其中一个集合中给定了无效的文件描述符(可能是已关闭的文件描述符,或发生错误的文件描述符)。

EINTR:捕获到一个信号。

EINVAL:nfds为负数或timeout包含的值无效。

ENOMEM:无法为内部表分配内存。

 了解fd_set结构

其实就是一个位图,本质是long 类型的数组,数组元素个数为2^4=16个, 

查看源码得:

 __FD_SETSIZE = 1024 

_

 __NFDBITS = 8*sizeof (long) = 64

也即为 long fds_bits[16],所以能标识的比特位为8*16*8=1024个,也就是能检测的最多的文件描述符个数了,可见select能监测的文件描述符是有上限的。

这个位图跟之前学过的信号集sigset_t有点类似,系统提供了一组接口方便操作位图:

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 的全部位

了解timeval结构

  1. tv_sec:表示等待时间的秒数部分。
  2. tv_usec:表示等待时间的微秒数部分(即剩余时间的小数部分,以毫秒为单位)。

Socket就绪条件        

读就绪

socket内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT。此时可以无阻塞的读该文件描述符,并且返回值大于0。
socket TCP通信中,对端关闭连接,此时对该socket读。则返回0。
监听的socket上有新的连接请求。
socket上有未处理的错误。

写就绪

socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0。

socket的写操作被关闭(close或者shutdown)。对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号。
socket使用非阻塞connect连接成功或失败之后。
socket上有未读取的错误。

异常就绪

socket上收到带外数据,带外数据和TCP紧急模式相关,TCP报头当中的URG标志位和16位紧急指针搭配使用,就能发送和接收带外数据。

实现select服务器

基本工作流程(比如只实现只关心读事件)

 1.创建套接字,完成套接字的绑定,监听,这部分代码后续服务器都会使用,直接封装成Sock类,放在头文件。

2.定义一个fdsArray数组,表示用户需要内核关心的套接字,并将监听套接字添加入其中

3.服务器开始循环调用select进行监听,监听之前,需要重设readfds,遍历fdsArray数组添加入readfds中,并更新maxfd作为select的首参数

4.根据select返回值执行后续代码,<=0就继续循环监听,=0表示超时,-1表示出错,>0就调用事件处理函数

5.事件处理函数:判断就绪的文件描述符,如果是监听文件描述符就绪,就accept获取连接,并将其连接对应的套接字加入fdsArray(连接没有达到上限);如果是普通文件描述符就绪,就调用recv读取,返回值<=0时就关闭fd,清除对应fdsArray上的fd,返回值大于0就输出。

代码实现:

Sock类实现:

#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdarg.h>
#include <time.h>
#include <string>
#include <unordered_map>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <iostream>
#include <unistd.h>
#include <string>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <functional>
using namespace std;
class Sock
{
public:
    static const int gBacklog = 20;

    static int Socket()
    {
        // 1.创建套接字
        int listenSock = socket(AF_INET, SOCK_STREAM, 0);
        if (listenSock < 0)
        {
            cerr << "socket error" << endl;
            exit(1);
        }
        int opt = 1;
        setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
        return listenSock;
    }
    static void Bind(int listenSock, uint16_t port)
    {
        // 2. bind
        // 2.1 填充信息到结构体
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;
        // 2.2 bind,本地sock写入内核sock
        if (bind(listenSock, (const struct sockaddr *)&local, sizeof(local)) < 0)
        {
            cerr << "bind error" << endl;
            exit(2);
        }
    }

    static void Listen(int listenSock)
    {
        // 3.监听
        if (listen(listenSock, gBacklog) < 0)
        {
            cerr << "listen error" << endl;
            exit(2);
        }
    }

    static int Accept(int listenSock, uint16_t *peerPort, string *ip) // 后两个是输出型参数
    {
        //  4.连接
        struct sockaddr_in peer;
        socklen_t len = sizeof peer;
        int serviceSock = accept(listenSock, (struct sockaddr *)&peer, &len);
        if (serviceSock < 0)
        {
            cerr << "accept error" << endl;
            return -1;
        }
        // 4.1获取客户端基本信息
        if (peerPort)
            *peerPort = ntohs(peer.sin_port);
        if (ip)
            *ip = inet_ntoa(peer.sin_addr);
        return serviceSock;
    }
};

selectServer.cc实现:

#include "Socket.hpp"
#define DFL -1
int fdsArray[sizeof(fd_set) * 8] = {0};
int gnum = sizeof(fdsArray) / sizeof(fdsArray[0]);

static void showArray()
{
    cout << "当前托管的fd:" << endl;
    for (int i = 0; i < gnum; i++)
    {
        if (fdsArray[i] != DFL)
            cout << fdsArray[i] << " ";
    }
    cout << endl;
}
static void HandlerEvent(int listensock, fd_set &readfds)
{
    for (int i = 0; i < gnum; i++)
    {
        if (i == 0 && FD_ISSET(listensock, &readfds))
        {
            cout << "已有新的连接到来了,需要获取!" << endl;
            uint16_t peerPort;
            string ip;
            int sockfd = Sock::Accept(listensock, &peerPort, &ip);
            int k = 0;
            while (fdsArray[k] != DFL)
                ++k;
            if (k == gnum)
            {
                cout << "服务器已经达到了最大的上限了,无法保持更多连接了" << endl;
                close(sockfd);
            }
            else
            {
                fdsArray[k] = sockfd;
                showArray();
            }
        }
        else if (FD_ISSET(fdsArray[i], &readfds))
        {
            char buffer[1024];
            ssize_t s = recv(fdsArray[i], buffer, sizeof(buffer), 0);
            if (s <= 0)
            {
                cout << "client closed or read error,server close" << fdsArray[i] << endl;
                close(fdsArray[i]);
                fdsArray[i] = DFL;
                ;
                showArray();
            }
            else
            {
                buffer[s] = 0;
                cout << "client[" << fdsArray[i] << "]#" << buffer << endl;
            }
        }
    }
}
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "Usage:\n\t" << argv[0] << " port" << endl;
        exit(-1);
    }
    int listenSock = Sock::Socket();
    Sock::Bind(listenSock, atoi(argv[1]));
    Sock::Listen(listenSock);
    for (int i = 0; i < gnum; i++)
        fdsArray[i] = DFL;
    fdsArray[0] = listenSock;
    while (1)
    {
        int maxfd = DFL;
        fd_set readfds;
        FD_ZERO(&readfds);
        for (int i = 0; i < gnum; i++)
        {
            if (fdsArray[i] == DFL)
                continue;
            FD_SET(fdsArray[i], &readfds);
            if (maxfd < fdsArray[i])
                maxfd = fdsArray[i];
        }
        timeval timeout = {5, 0};
        int n = select(maxfd + 1, &readfds, nullptr, nullptr, &timeout);
        switch (n)
        {
        case 0:
            cout << "time out..." << time(nullptr) << endl;
            break;
        case -1:
            cerr << errno << strerror(errno) << endl;
            break;
        default:
            HandlerEvent(listenSock, readfds);
            break;
        }
    }
    return 0;
}

简单测试:

服务器:

客户端: 

 当前存在的问题:

1.没有定制协议:可能造成粘包问题,因为没有定制协议,读取的报文没有格式边界,不知何时读完了完整报文,HTTP用空行表示报头读取完毕,报头中有Content-Length字段表示正文的长度,最终能读取到完整的报文。

2.没有输入输出缓冲区,代码直接将读取的数据拷贝到字符数组buff中,不严谨,因为可能读取到的不是一个完整的报文,应该将数据读取到缓冲区中,当读取到了一个完整的报文,服务器再对其进行处理。

select优点

可以同时等待多个文件描述符,提高IO效率,相比多线程/多进程占用资源少,高效。

select的缺点

1.因为输入输出使用的是同一参数,所以每次调用前需要重置参数,效率低

2.检测的文件描述符数量有上限,这是由内核结构实现决定的(最多sizeof(fd_set)*8=1024个)

3.每次调用内核向用户传递位图参数,是较为大量的数据拷贝工作

4.编码不方便,需要用户自己维护数组

5.底层需要遍历的方式(遍历maxfd+1次),检测所有需要监听的文件描述符

poll

poll函数原型

 参数说明:

fds:数组传参退化成指针,传递需要监视的文件描述符数组

查看struct pollfd:

 内含监视的文件描述符fd,需要监听的事件events,已经就绪的事件revents

events和revents的取值

nfds:表示fds数组的大小

timeout:表示poll函数的超时时间,单位是ms

timeout取值:

-1:poll调用后进行阻塞等待,直到被监视的某个文件描述符就绪

0:poll调用后进行非阻塞等待,无论被监视的文件描述符是否就绪,poll都会返回

 特定的时间值:poll调用后在指定的时间内进行阻塞等待,时间到了还没有就绪事件就绪,就超时返回

返回值:

>0:函数调用成功,返回有事件就绪的文件描述符个数

=0:过了timeout时间,超时返回

-1:调用失败,错误码被设置

可见poll函数与select函数的返回值和timeout的取值作用是十分相似的。

poll调用失败,错误码类型:

EFAULT: 给定作为参数的数组未包含在调用程序的地址空间中。

EINTR:函数调用时被信号中断。

EINVAL:nfds值超过了RLIMIT_NOFILE值。

ENOMEM:没有空间来分配文件描述符表。

 实现poll服务器

基本工作流程(比如只实现只关心读事件)(与select类似)

1.创建套接字,完成套接字的绑定监听工作,与select服务器相同

2.定义fdsArray数组,表示用户需要关心的套接字,将数组初始化并将listenSock加入其中

3.服务器开始循环调用poll监听,根据返回值执行后续代码,这步也同select

4.事件处理函数:遍历fdsArray数组,处理就绪的文件描述符事件,当文件描述符是listenSock,accept获取连接,遍历fdsArray将其加入首个fd为DFL的位置,如果是普通文件描述符,则调用recv读取,根据返回值不同行为。这步也和select类似。

具体代码:

pollServer.cc

#include "Socket.hpp"
#include <poll.h>
#define DFL -1
#define NUM 1024
struct pollfd fdsArray[NUM];
static void showArray(struct pollfd arr[], int num)
{
    cout << "当前合法sock list# ";
    for (int i = 0; i < num; i++)
    {
        if (arr[i].fd == DFL)
            continue;
        else
            cout << arr[i].fd << " ";
    }
    cout << endl;
}
static void HandEvents(int listenSock)
{
    for (int i = 0; i < NUM; i++)
    {
        if (fdsArray[i].fd == DFL)
            continue;
        if (i == 0 && fdsArray[i].fd == listenSock && (fdsArray[i].revents & POLLIN))
        {
            cout << "有新的链接到来了,正在处理" << endl;
            string ip;
            uint16_t port;
            int sockfd = Sock::Accept(listenSock, &port, &ip);
            if (sockfd < 0)
                return;
            cout << "获取连接成功: [" << ip << ":" << port << "]sockfd:" << sockfd << endl;
            int index = 0;
            while (fdsArray[index].fd != DFL)
                ++index;
            if (index == NUM)
            {
                cout << "服务器已经达到了最大的连接上限了,无法维持更多的连接了。" << endl;
                close(sockfd);
            }
            else
            {
                fdsArray[index].fd = sockfd;
                fdsArray[index].events = POLLIN;
                fdsArray[index].revents = 0;
            }
            showArray(fdsArray, NUM);
        }
        else if (fdsArray[i].revents & POLLIN)
        {
            char buff[1024];
            ssize_t s = recv(fdsArray[i].fd, buff, sizeof(buff), 0);
            if (s > 0)
            {
                buff[s] = 0;
                cout << "client[" << fdsArray[i].fd << "]# " << buff << endl;
            }
            else
            {
                if(s==0)cout << "client[" << fdsArray[i].fd << "]quit,server close" <<endl;
                else cout << "client[" << fdsArray[i].fd << "]quit,server error" <<endl;
                close(fdsArray[i].fd);
                fdsArray[i].fd = DFL; // 去除对该文件描述符的select事件监听
                fdsArray[i].events = 0;
                fdsArray[i].revents = 0;
                showArray(fdsArray, NUM);
            }
        }
    }
}
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "Usage:\n\t" << argv[0] << " port" << endl;
        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;
    fdsArray[0].events = POLLIN;
    int timeout = -1; // 阻塞
    while (1)
    {
        int n = poll(fdsArray, NUM, timeout);
        switch (n)
        {
        case -1: 
            cerr<<errno<<" : "<<strerror(errno)<<endl;
            break;
        case 0:
            cout << "time out..." << endl;
            break;
        default:
            HandEvents(listenSock);
            break;
        }
    }

    return 0;
}

总结

poll相比select改进:

(1)输入输出同一个参数,每次要重设参数

(2)所等待的文件描述符有上限

select和poll具有的不足:
(1)select,poll都是基于对各个fd进行遍历检测识别事件就绪的,当连接数多,遍历周期长

(2)事件使用的数据结构需要程序员自己维护

(3)每次调用函数都需要把内核结构从用户态拷贝到内核态,随着文件描述符增多是一笔较大的开销

(4)都存在惊群现象(后面谈)

这些不足在后续的epoll基本得到了很好的解决。

epoll

简介:epoll也是系统提供的多路转接接口,epoll名字可以理解为扩展的poll,但是它们的区别很大,epoll几乎具备了select和poll的优点,被公认为Linux2.6下性能最好的多路IO就绪通知方法。

三个核心系统调用:
1.epoll_create:用于创建epoll模型

 参数size在2.6版本后被忽略,但需要size>0

返回值:创建epoll模型成功返回文件描述符,创建失败返回-1,错误码被设置

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

参数说明:

epfd:指定的epoll模型

op:表示具体的操作

传入宏:

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

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

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

返回值:函数调用成功返回0,调用失败返回-1,错误码被设置

fd:表示要操作的文件描述符

event:传入用户要内核关心的事件结构

查看其结构如下:

 其中uint32_t events常用取值如下:

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

参数说明:
epfd:指定的epoll模型

events:用户提供数组,内核负责将就绪事件拷贝到数组(内核只负责拷贝,不为用户分配内存)

maxevents:数组最多从就绪队列获取的事件个数,不能大于创建epoll模型时传入的size

timeout,表示函数的超时时间,单位是ms

timeout取值与前面类似:

-1:表示阻塞等待,直到某个被监视的文件描述符就绪

0:调用后立即进行非阻塞等待,无论是否有事件就绪,函数都直接返回

特定时间值:在指定时间内阻塞等待,如果在指定时间过后没有事件就绪,函数就超时返回

返回值:

>0:就绪的文件描述符个数

=0:timeout时间耗尽,超时返回

-1:调用失败,错误码被设置

错误码列表:
EBADF:传入epoll模型的文件描述符无效

EFAULT:events指向的数组空间没有写入权限

EINTR:函数调用被信号中断

EINVAL:epfd不是epoll模型对应的文件描述符或者传入的maxevents的值小于等于0 

epoll工作原理

epoll的工作原理由三个部分组成

1.使用红黑树来维护 

当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关
struct eventpoll{ 
 .... 
 /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/ 
    struct rb_root rbr; 
 /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/ 
    struct list_head rdlist; 
 .... 
};
每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件.
这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度),添加fd时就是创建红黑树节点(内含fd,用户需要内核关心的事件),红黑树的键是key。
2.回调机制:所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法
3.就绪队列:当事件就绪的时候,内核获取就绪事件fd,就绪事件,构建队列节点,链接进入就绪队列,调用epoll_wait时就从就绪队列获取节点,类似于生产者消费者模型。
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem 元素即可. 如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度是O(1).
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}

从工作原理我们就可以看出来epoll模型的效性

(1)管理fd和事件使用内核维护的红黑树,对事件的修改插入和删除操作就不是线性的,而是对数级的,更高效
(2)回调策略,不用OS以线性时间复杂度轮询检测事件就绪
(3)获取就绪事件,不用拷贝所有文件描述符和事件到用户态,只需要将就绪的事件拷贝到内核态就可以了,时间复杂度为O(1)

epoll的优缺点总结

优点:(1)接口使用方便高效,不用重置参数,输入输出参数分离(select)

(2)数据拷贝轻量:只在新增事件的时候,调用epoll_ctl将数据从用户拷贝到内核,而select和poll每次都需要将需要监视的事件从用户拷贝到内核。(因为监视的文件描述符在内核被红黑树维护)此外,epoll_wait获取就绪事件的时候,只会拷贝就绪的事件,不会进行不必要的拷贝(就绪队列)。

(3)事件回调机制:不用OS主动轮询检测事件就绪,采用回调函数的方式,将就绪的文件描述符构建队列节点加入就绪队列,检测是否有文件描述符就绪的时间复杂度O(1),只需检测就绪队列是否为空。

(4)没有数量限制,只要内存允许,就可以往红黑树添加节点。

缺点:

(1)短连接会导致epoll_ctl会调用频繁,对于任何想要从epollfd中添加或者删除的fd都要调用epoll_ctl,比如需要关注一万个事件,就得手动调用一万次该系统调用,频繁的用户态到内核态的切换会大大降低效率

(2)会出现惊群现象,降低效率。

(3)跨平台性不够好

实现epoll服务器

基本工作流程

1.创建套接字,绑定,监听

2.调用epoll_create创建epoll模型

3.创建listenSock的epoll结构,然后调用epoll_ctl将其添加到epoll模型

4.创建epoll_event数组用于存放就绪的事件

5.循环调用epoll_wait,判断返回值与之前的多路转接接口类似

6.事件处理函数:遍历就绪的数组,倘若是监听套接字就绪,调用accept获取连接,创建epoll_event并调用epoll_ctl将其加入epoll模型中,如果是普通文件描述符,调用recv读取,根据recv的返回值有不同的行为。

代码实现:(封装成epollServer类)

epollServer.hpp

#include "log.hpp"
#include "Socket.hpp"
#include <sys/epoll.h>
class EpollServer
{
    using func_t = function<int(int)>;

private:
    int epfd_;
    int listenSock_;
    int port_;
    func_t func_; // 处理普通fd的函数,返回读取的字符个数,其实就是封装recv
public:
    static const int gsize = 128; // epoll实例的大小,即能够监视的文件描述符的数量
    static const int num = 128;   // 从就绪队列至多能获取的文件描述符个数
    EpollServer(int port, func_t func) : epfd_(-1), listenSock_(-1), port_(port), func_(func) {}
    ~EpollServer()
    {
        if (listenSock_ != -1)
            close(listenSock_);
        if (epfd_ != -1)
            close(epfd_);
    }

public:
    void InitEpollServer()
    {
        // 1.创建套接字,绑定,监听
        listenSock_ = Sock::Socket();
        Sock::Bind(listenSock_, port_);
        Sock::Listen(listenSock_);
        // 2.创建epoll模型
        epfd_ = epoll_create(gsize);
        if (epfd_ < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(1);
        }
        logMessage(DEBUG, "listen :%d success", listenSock_);
        logMessage(DEBUG, "epoll_create :%d success", epfd_);
    }

    void Run()
    {
        // 1.创建listenSock_的epoll结构,然后将其添加到epoll模型
        epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = listenSock_;
        int n = epoll_ctl(epfd_, EPOLL_CTL_ADD, listenSock_, &ev);
        assert(n == 0);
        (void)n;
        // 2.循环检测就绪队列
        epoll_event evs[num];
        int timeout = 1000; // 等待的毫秒数
        while (1)
        {
            int n = epoll_wait(epfd_, evs, num, timeout);
            switch (n)
            {
            case 0:
                cout << "time out ...... " << endl;
                /* code */
                break;
            case -1:
                cerr << errno << ":" << strerror(errno) << endl;
                break;
            default:
                HandlerEvents(evs, n);
                break;
            }
        }
    }
    void HandlerEvents(epoll_event *evs, int n) // 就绪的fd个数
    {
        for (int i = 0; i < n; i++)
        {
            epoll_event &ev = evs[i];
            int sock = ev.data.fd;
            uint32_t revent = ev.events;
            if (revent & EPOLLIN) // 读事件就绪
            {
                if (sock == listenSock_) // 监听套接字就绪
                {
                    // 1.accept
                    string ip;
                    uint16_t port;
                    int sockfd = Sock::Accept(listenSock_, &port, &ip);
                    if (sockfd < 0)
                    {
                        logMessage(FATAL, "%d:%s", errno, strerror(errno));
                        continue;
                    }
                    logMessage(DEBUG, "获取连接成功:ip是%s,port是%d", ip.c_str(), port);
                    // 2.创建epoll结构体,添加到epoll模型
                    epoll_event newEv;
                    newEv.data.fd = sockfd;
                    newEv.events = EPOLLIN;
                    int n = epoll_ctl(epfd_, EPOLL_CTL_ADD, sockfd, &newEv);
                    assert(n == 0);
                    (void)n;
                }
                else // 普通文件描述符
                {
                    int n = func_(sock);
                    if (n <= 0)
                    {
                        int x = epoll_ctl(epfd_, EPOLL_CTL_DEL, sock, nullptr);
                        assert(x == 0);
                        (void)x;
                        logMessage(DEBUG, "client quit:%d", sock);
                        close(sock);
                    }
                }
            }
        }
    }
};

main.cc

#include "epollServer.hpp"
#include <memory>
using std::unique_ptr;
int myread(int sock)
{
    char buff[1024];
    ssize_t s = recv(sock, buff, sizeof(buff) - 1, 0);
    if (s > 0)
    {
        buff[s] = 0;
        logMessage(DEBUG, "client[%d]# %s", sock, buff);
    }
    return s;
}
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "Usage:" << argv[0] << " port" << endl;
        exit(1);
    }
    int port = atoi(argv[1]);
    unique_ptr<EpollServer> uptr(new EpollServer(port, myread));
    uptr->InitEpollServer();
    uptr->Run();

    return 0;
}

epoll的工作模式

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

水平触发:只要底层有数据,就会一直通知用户,不用担心底层不通知你而导致的数据丢失问题
边缘触发:只要底层有数据,从无到有,从有到多,变化的时候才通知你,倒逼程序员一旦收到通知就必须将收到的数据从内核中全部读取完毕,否则可能有数据丢失,所有ET模式下,recv和send操作的文件描述符必须设置为非阻塞状态,这是必须的。
select和poll都是水平触发,epoll默认是水平触发,需要边缘触发需要带上选项EPOLLET。

ET vs LT 谁更高效?

不一定,一般而言ET的效率更高,因为ET是让上层更快取走数据的一种机制,意味着更大的窗口大小,而且ET需要程序员调用非阻塞IO将数据全部读取完,所以函数的返回次数更少,ET的效率更高。倘若LT也以ET的方式读取,LT的效率也和ET差不多,所以说ET和LT效率不一定更高。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值