I/O多路复用机制(三)

       在实际的开发中,我们经常会遇到这样的场景,我们需要接受多个端口的数据、多个终端的数据抑或是多个文件描述符对应的数据。那么,遇到这样的问题,你在程序中该怎么做呢?通常的做法,在程序中对数据交互的描述符进行轮询。那么问题来了,轮询的时间设置为多少呢?设置的太短,可以保证处理性能和速度,但是CPU的使用率太高,一旦处理的描述符数量多了起来,CPU可能就扛不住了。设置的时间太长,描述符处理的时间片太短,处于空闲的时间较长,性能和速度达不到要求。如果是服务器的话,面对多个用户的连接,处理速度和CPU使用性能是必须考虑的,而且最好要兼顾。这里就需要使用到I/O多路复用机制,这就是博主即将要和小伙伴们探讨的内容。

epoll简介

1、基本知识

  epoll是在2.6内核中提出的,是之前的selectpoll的增强版本。相对于selectpoll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

2epoll接口

  epoll操作过程需要三个接口,分别如下:

#include <sys/epoll.h>

int epoll_create(int size);

int epoll_ctl(int epfd, int op, int fd, structepoll_event *event);

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


1 int epoll_create(intsize);

  创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

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

  epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:

EPOLL_CTL_ADD:注册新的fdepfd中;

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

EPOLL_CTL_DEL:从epfd中删除一个fd

第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:

struct epoll_event {

 

  __uint32_t events;  /* Epoll events */

  epoll_data_tdata;  /* User data variable */

}

};

typedef union epoll_data {

    void *ptr;

    int fd;

    __uint32_t u32;

    __uint64_t u64;

} epoll_data_t;

events可以是以下几个宏的集合:

EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);

EPOLLOUT:表示对应的文件描述符可以写;

EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

EPOLLERR:表示对应的文件描述符发生错误;

EPOLLHUP:表示对应的文件描述符被挂断;

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

EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

3 int epoll_wait(int epfd, structepoll_event * events, int maxevents, int timeout);

  等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

3、工作模式

  epoll对文件描述符的操作有两种模式:LTlevel trigger)和ETedge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

  LT模式:epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

  ET模式:epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

  ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

 

程序实例

服务端程序

#include<stdio.h>

#include<arpa/inet.h>

#include<sys/epoll.h>

#include<time.h>

#include<unistd.h>

#include<assert.h>

#include<sys/socket.h>

#include<error.h>

#include<sys/types.h>

#include<stdlib.h>

#include<string.h>

 

const intMAXFD = FD_SETSIZE;//FD_SETSIZE 1024 可以监控的最大描述符数量

constchar *SERVERIP = "127.0.0.1";//服务端IP

constunsigned short PORT = 6666;//服务端端口号

 

typedefstruct server_st

{

    int                 cli_num;        //已连接客户端数量

    int                 cli_fd[MAXFD];  //已连接客户端数组

    struct epoll_event  events[MAXFD];  //事件数组

    int                 epollfd;        //epoll_create创建的描述符

}server_st_t;

 

//打印出错信息并推出程序

#definehandle_error(msg)\

    do {perror(msg);exit(EXIT_FAILURE);}while(0)

 

//定义类

classepollsocket

{

    public:

        //构造函数

        epollsocket(const char *server_ip =SERVERIP, unsigned short port = PORT);

        //析构函数

        virtual ~epollsocket();

 

        //客户处理函数(对外接口)

        int handle_cli_proc();

 

 

    private:

        int server_init();//初始化server_st_t结构体

        int server_uninit();//释放server_st_t结构体

        int handle_create_proc();//创建服务端监听套接字

        int handle_accept_proc();//处理客户端连接

        int handle_recv_proc(int&cli_fd);//处理客户端数据发送

 

        epollsocket(const epollsocket&ref);

        epollsocket& operator=(constepollsocket &ref);

    private:

        server_st_t     *m_server_st;

        char            m_server_IP[16];

        unsigned short  m_port;

        int             m_server_fd;

};

 

epollsocket::epollsocket(constchar *server_ip /*= SERVERIP*/, unsigned short port /*= PORT*/)

{

    bzero(m_server_IP, sizeof(m_server_IP));//将类成员地址空间清零

    memcpy(m_server_IP, server_ip,strlen(server_ip));//给类成员负值

    m_port = port;

    m_server_st = NULL;

 

    handle_create_proc();//创建监听套接字

    server_init();//初始化

}

 

epollsocket::~epollsocket()

{

    //close(m_server_fd);

 

 

    server_uninit();//释放结构体

}

 

intepollsocket::server_init()

{

    m_server_st =(server_st_t*)malloc(sizeof(server_st_t));

 

    if (NULL == m_server_st)handle_error("malloc");

 

    //将结构体空间清零

    bzero(m_server_st, sizeof(server_st_t));

 

    //初始化已连接客户端数组

    for (int i = 0; i<MAXFD; ++i)

    {

        m_server_st->cli_fd[i] = -1;

    }

 

    //创建epoll

    m_server_st->epollfd =epoll_create(MAXFD);

 

    //将监听描述符添加到epoll描述符中

    struct epoll_event ev;

    ev.data.fd = m_server_fd;

    ev.events = EPOLLIN | EPOLLET;//监听读事件和边沿触发

    epoll_ctl(m_server_st->epollfd,EPOLL_CTL_ADD, m_server_fd, &ev);

 

    return 0;

}

 

intepollsocket::server_uninit()

{

    //关闭epoll描述符

    close(m_server_st->epollfd);

 

    //关闭所有的描述符(包括监听描述符)

    for (int i = 0; i < MAXFD; ++i)

    {

        int cli_fd = m_server_st->cli_fd[i];

 

        if (-1 != cli_fd)

        {

            close(cli_fd);

        }

    }

 

    if (NULL != m_server_st)

    {

        free(m_server_st);

    }

 

    m_server_st = NULL;

}

 

intepollsocket::handle_create_proc()

{

    //创建TCP套接字

    //参数1:协议族

    //参数2:套接字类型

    //参数3:使用的协议(0:使用套接字类型对应的默认协议)

    if (-1 == (m_server_fd = socket(AF_INET,SOCK_STREAM, 0))) handle_error("socket");

 

    //地址结构体

    struct sockaddr_in serveraddr;

    //将结构体变量空间清零

    bzero(&serveraddr, sizeof(serveraddr));

    serveraddr.sin_family = AF_INET;//协议族

    //注意,设置端口和IP时,要将主机字节序转换为网络字节序

    serveraddr.sin_port = htons(m_port);

    //serveraddr.sin_addr.s_addr =htonl(INADDR_ANY);

    inet_pton(AF_INET, m_server_IP,&serveraddr.sin_addr);

   

    int op = true;

    ///*一个端口释放后会等待两分钟之后才能再被使用(TIME_WAIT状态),SO_REUSEADDR是让端口释放后立即就可以被再次使用*/

    if (-1 == setsockopt(m_server_fd,SOL_SOCKET, SO_REUSEADDR, &op, sizeof(op)))handle_error("setsockopt");

 

    //命名套接字

    if (-1 == bind(m_server_fd, (structsockaddr*)&serveraddr, sizeof(serveraddr)))handle_error("serveraddr");

 

    //将套接字由主动变为被动(接受客户端连接状态)

    if (-1 == listen(m_server_fd, SOMAXCONN))handle_error("listen");

 

    return 0;

}

 

intepollsocket::handle_accept_proc()

{

    //地址结构体

    struct sockaddr_in cli_addr;

    socklen_t len =(socklen_t)sizeof(cli_addr);

    //将结构体变量空间清零

    bzero(&cli_addr, sizeof(cli_addr));

 

    //接受客户端的连接请求

    int cli_fd = accept(m_server_fd, (structsockaddr*)&cli_addr, &len);

   

    if (-1 == cli_fd)handle_error("accept");

 

    ++m_server_st->cli_num;//已连接客户数加1

    fprintf(stdout, "#%d   %s:%d connected server!\n", m_server_st->cli_num,inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));

 

    //将新连接的客户端描述符添加到已连接客户端数组中

    for (int i = 0; i<MAXFD; ++i)

    {

        if (-1 == m_server_st->cli_fd[i])

        {

            m_server_st->cli_fd[i] = cli_fd;

            break;

        }

    }

 

    //将新连接的客户端端描述符添加到epoll监控中

    struct epoll_event ev;

    ev.data.fd = cli_fd;

    ev.events = EPOLLIN | EPOLLET;//监听读事件和边沿触发

    epoll_ctl(m_server_st->epollfd,EPOLL_CTL_ADD, cli_fd, &ev);

}

 

intepollsocket::handle_recv_proc(int &cli_fd)

{

   

    char buf[256] = {0};

 

    //从网络中读取数据

    int r = read(cli_fd, buf, sizeof(buf));

 

    //如果读取出错

    if (r <= 0)

    {

        for (int i = 0; i<MAXFD; ++i)

        {

            if (cli_fd ==m_server_st->cli_fd[i])

            {

                m_server_st->cli_fd[i] = -1;

                break;

            }

        }

 

        //关闭当前描述符(客户端断开)

        close(cli_fd);

 

        //将当前描述符从监控中删除

        epoll_ctl(m_server_st->epollfd,EPOLL_CTL_DEL, cli_fd, NULL);

        cli_fd = -1;//将描述符置为-1

 

 

        //已连接客户数减1

        --m_server_st->cli_num;

    }

 

    //如果接受成功,将数据写回网络

    write(cli_fd, buf, sizeof(buf));

 

    //清空缓存区

   memset(buf, 0x00, sizeof(buf));

   

    return 0;

}

 

intepollsocket::handle_cli_proc()

{

    int ready = -1;

 

    while (1)

    {

        //epoll阻塞等待描述符集合中是否有就绪描述符

        //参数1epoll_create创建的epoll描述符

        //参数2:返回的已就绪事件数组

        //参数3:返回的已就绪事件数组的长度

        //参数4:最长等待时间(-1:无限等待,直到有描述符就绪;0:立即返回;等待指定时间,单位毫秒)

        //一旦有描述符就绪则返回,返回值为以就绪描述符的个数

        ready =epoll_wait(m_server_st->epollfd, m_server_st->events, MAXFD,  5000);

 

        //epoll函数返回异常

        if (-1 == ready) break;

 

        //epoll函数等待超时

        if (0 == ready)

        {

            fprintf(stdout, "epolltimeout\n");

            continue;

        }

 

        //轮询已就绪事件数组

        for (int i = 0; i<ready; ++i)

        {

            //处理读事件

            if (m_server_st->events[i].events& EPOLLIN)

            {

                //如果有客户连接服务器,监听套接字就绪,执行以下代码

                if (m_server_fd ==m_server_st->events[i].data.fd)

                {

                    //fprintf(stdout,"connect\n");

                    //处理客户端的连接

                    handle_accept_proc();

                    //已经处理过来sfd描述符,read1

                    //如果此时read<=0,表示所有就绪描述符已处理完毕,返回poll处继续等待就绪描述符

 

                    //没有客户端发送数据,退出本次循环

                    if (--ready <= 0)

                    {

                        break;

                    }

                }

                else//处理客户端数据接收

                {

                    //处理客户端发送过来的数据

                   handle_recv_proc(m_server_st->events[i].data.fd);

                }

            }

            else

            {

                //其他事件处理逻辑

 

            }

        }

 

    }

 

    return 0;

}

 

 

int main()

{

    epollsocket epollsock;

    epollsock.handle_cli_proc();

}

 

客户端程序

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <arpa/inet.h>

#include <sys/socket.h>

#include <string.h>

 

int main()

{

    //创建IPV4 TCP套接字

    int sfd =socket(AF_INET, SOCK_STREAM, 0);

 

    if (-1 ==sfd) perror("socket"), exit(EXIT_FAILURE);

 

    //地址结构体

    structsockaddr_in addr;

   addr.sin_family = AF_INET;//协议族

   addr.sin_port = htons(6666);//端口

 

    //将地址串转换为网络字节序,存储到addr.sin_addr

   inet_aton("127.0.0.1", &addr.sin_addr);//连接的服务端IP地址

 

    //连接服务器

    if (-1 ==connect(sfd, (struct sockaddr *)&addr, sizeof(addr)))

    {

       perror("connect");

       exit(EXIT_FAILURE);

    }

 

    charbuf[256] = {};

 

    //从标准输入读取数据

    while (NULL!= fgets(buf, sizeof(buf), stdin))

    {

        //将数据发送给服务器

       write(sfd, buf, strlen(buf));

       

        //清空缓存区

       memset(buf, 0x00, sizeof(buf));

 

        //读取服务器放送过来的数据

        int r =read(sfd, buf, sizeof(buf));

 

        //接受失败

        if (r<= 0)

        {

           break;

        }

 

        //输出

       fprintf(stdout, buf, r);

 

        //清空缓存区

       memset(buf, 0x00, sizeof(buf));

    }

 

    //关闭套接字描述符

    close(sfd);

 

    return 0;

}

 

程序运行结果


参考:epoll工作总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值