epoll网路模型

引言:将自己封装epoll模型的收获记录下来,以便后续使用与改进

目录
1、select、poll、epoll对比分析
2、epoll工作模式
3、epoll相关函数原型与结构体分析
4、epoll模型源码分析

1、select、poll、epoll对比分析

性能测试图(网上扒的)
性能

select:
(1) 每次调用select都需要吧fd集合从用户态拷贝到内核态(此过程占用程序自身时间片),调用到内核态之后,采用轮询fd方式检测哪个fd有数据到来的信号,fd很大时开销很大。
(2) select对支持的文件描述符监视有数量限制,默认为1024.虽然可以进行对内核微调,但无法解决根本上的问题。
poll:
(1) poll比之select本质上差不多,都是采用内核轮询fd的方式,当fd很大时开销很大
(2) 比之select,poll没有1024的限制,理想状态下65535(系统允许打开的最大文件描述符数目),实际中受CPU、内存等限制
epoll:
(1) epoll为Linux下最好的并发模型,是select与poll模型的增强版本
(2) 更加的灵活,没有描述符的限制,理想状态下65535(系统允许打开的最大文件描述符数目),实际中受CPU、内存等限制,为了程序安全着想,我们需要限制下最大的连接数,防止cpu、内存占用率过高
(3) 使用一个描述符(epoll句柄)管理多个描述符
(4) 检测描述符信号时不是采用select与poll的内核轮询fd方式,而是采用的事件机制来管理(只要注册的事件有信号,内核立刻就能检测到)
(5) epoll通过epoll_wait,直接将有信号的事件存放到数组中,可以直接使用,而select与poll模型中只是返回有信号的数量,具体是谁有信号还需要我们去循环检测。

2、epoll工作模式

内核根据高低电平来判断fd是否有数据到来,高位表示有数据可读,低位表示无数据可读,电平的高低对应1和0的bit变化
水平与边缘触发
(1) 水平触发模式LT(默认的工作模式)
原理:当有数据来的时候,电平置高位(读完后置低位),只要fd的电平处于高位,epoll句柄就会标志出来,如果这次我们不去读数据,下次仍然会标志出这fd可读,直到我们读完为止。
举例说明:
如果epoll_wait的第二个参数(events)大小为10,此时有5个可读信号的到来,就把这5个的epoll_event信息填入 events中,然会返回5.
如果epoll_wait的第二个参数(events)大小为10,此时有35个可读信号的到来,就把这10个的epoll_event信息填入 events中,然会返回10.剩下的采用循环读取的方式再读取

(2) 边缘触发模式ET
原理:当fd有数据到来,电平从低位变为高位,此时epoll句柄就标志出这个fd有数据可读,如果这次不去读数据,即使下次fd有数据到来,epoll也不会标志出这个fd可读。因为边缘触发是根据电平的变化来标志可读,而数据到来的时候会置电平为高位,此时由于上次没有读取数据导致电平一直处在高位,电平也就不会发生变化,于是永远读取不到该fd的数据。使用
举例说明:
如果epoll_wait的第二个参数(events)大小为10,此时有15个可读信号的到来,由于数组的限制,就将这10个的epoll_event信息填入 events中,此时返回10.这10个fd的电平变为低位,等待下次数据的到来,剩下的5个fd的电平一直为高位。下次这5个fd有数据到来,也不会监视到这5个有数据到来。

3、epoll相关函数原型与结构体分析

函数所需要的头文件:#include

int  epoll_create(int  size); 

功能:创建一个检测fd事件信号的epoll句柄,用于监视所有的fd描述符是否发生了相关事件。该函数其实在系统内核申请了一个size大小空间,该空间用于存放我们要监控的套接字fd,
返回值:返回一个epoll的文件描述符
size:监听的文件描述符个数(现在的版本里面已经废弃),所以填个1就行


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

功能:(事件注册函数)对套接字描述符注册要监听的事件,注册之后,内核进行监听事件工作,此过程不占用用户空间的时间片。(类似于Windows中的WSACreateEvent事件对象功能)
epfd:用于监视fd是否有数据到来的epoll句柄,即epoll_create的返回值
op:要进行的操作,操作的类型如:EPOLL_CTL_ADD|DEL|MOD(增删改)
fd:需要监听的fd
event:内核要监听的事件

struct epoll_event {
    _uint32_t        events; 
    epoll_data_t     data;
}

typedef union epoll_data {
    void      *ptr;
    int        fd;
    uint32    u32;
    uint64    u64;  
} epoll_data_t

data:联合体类型,一般填需要监听的fd
events:监听的事件类型,需要监听一个fd的多个事件时,通过或运算实现。例如EPOLLIN|EPOLLOUT
EPOLLIN fd可读
EPOLLOUT fd可写
EPOLLPRI fd紧急数据可读
EPOLLERR fd发生错误
EPOLLHUP fd 被挂起
EPOLLONESHOT fd 只监控 1 次,监控完后自动删除
EPOLLLT epoll 工作模式,设置为 水平触发模式
EPOLLET epoll 工作模式,设置为 边缘触发模式


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

功能:等待多个事件的发生,并把产生事件信号对应epoll_event结构体存放到events数组中,并返回待处理事件个数(类似于Windows中重叠IO模型中的WSAWaitForMultipleEvents)。这个过程为内核检测事件信号,不占用程序自身时间片。
返回值:超时返回0,正常返回待处理文件描述符个数,错误返回负数错误码
epfd:用于监视fd是否有数据到来的epoll句柄
events:存储从内核返回的待处理的事件集合
maxevents:所能存放的最大个数,防止越界
timeout:超时时间(毫秒,0立即返回,-1一直等待直到有事件发生)

4、epoll模型源码分析

epoll.h

#ifndef _EPOLL_MODEL_
#define _EPOLL_MODEL_

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <sys/epoll.h>

#include <map>

#endif

#define MAX_LISTEN      100         // 监听的最大数量
#define MONNITOR_NUM    100         // epoll_wait一次检测的最大事件数量
#define MAX_CONN        4096        // 最大的连接数
#define MAX_LEN         1024        // 一次接收的最大字节数

typedef struct _FDINFO              // 存储用户信息的结构体
{
    int fd;
    sockaddr_in addr;
}FDINFO,*PFDINFO;

typedef void(*NetCallBack) (FDINFO fdinfo, char* buf, int len);

class epollModel
{
public:
    epollModel();
    ~epollModel();

    // 初始化网络
    void InitServerNet(NetCallBack func, int port, int bUdp = 0/*是否添加udp*/);

protected:
    // 消息处理函数
    NetCallBack NetFunc;

    // 初始化服务端tcp套接字
    int InitTcpServ(int port, const sockaddr_in& addr, socklen_t addrlen);

    // 初始化服务端udp套接字
    int InitUdpServ(int port, const sockaddr_in& addr, socklen_t addrlen);

    // 给fd添加检测的事件,并加入检测
    void AddMonitor(int fd);

    // 删除fd及注册的事件
    void DelMonitor(std::map<int, struct sockaddr_in>::iterator it, struct epoll_event evt);

    // 开始epoll进行检测
    int _beginEpoll(int bUdp);

    // 处理epoll检测到的事件
    void Epoll_Process(struct epoll_event* pevts, int num);

private:
    int nConn;                              // 连接数

    int m_fdTcpServ;                        // tcp fd
    int m_fdUdpServ;                        // udp fd  

    int m_fdEpoll;                          // epoll fd
    struct epoll_event m_evt;               // epoll_event

    std::map<int, struct sockaddr_in> m_fdmap;  // 存储用户信息
    std::map<int, struct sockaddr_in>::iterator it; 
};

void InitServerNet(NetCallBack func, int port, int bUdp = 0/是否添加udp/);

void epollModel::InitServerNet(NetCallBack func, int port, int bUdp /*= 0*/)
{
    NetFunc = func; // init net callback

    sockaddr_in addr;
    socklen_t addrlen = sizeof(addr);

    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if (-1 == InitTcpServ(port, addr, addrlen))
        return;

    if (bUdp == 0) // 是否启动udp监听
    {
        if (-1 == InitUdpServ(port, addr, addrlen))
            return;
    }

    _beginEpoll(bUdp);
}

套路:初始化套接字、绑定、监听。这里UDP绑定的地址与TCP绑定的地址为同一个地址addr


int _beginEpoll(int bUdp);

int epollModel::_beginEpoll(int bUdp)
{
    int iRet;

    m_fdEpoll = epoll_create(1); // 创建用于检测的epoll描述符 
    if (m_fdEpoll < 0)
    {
        perror("\rfail to epoll_create\n");
        close(m_fdEpoll);
        return -1;
    }

    AddMonitor(m_fdTcpServ);// 使m_fdTcpServ被检测
    if (bUdp == 0)
        AddMonitor(m_fdUdpServ);// 使m_fdUdpServ被检测

    struct epoll_event evts[MONNITOR_NUM];
    printf("\rserver start...\n");

    while (1)
    {
        iRet = epoll_wait(m_fdEpoll, evts, MONNITOR_NUM, -1);
        if (iRet < 0) // epoll_wait error
        {
            perror("\rfail to epoll_wait\n");
            continue;
        }
        if (iRet == 0) // timeout
        {
            continue;
        }
        Epoll_Process(evts, iRet);
    }
}

通过epoll_create创建一个用于检测fd事件信号的epoll句柄,(AddMonitor)将tcp与udp服务端的套接字绑定EPOLLIN可读信号,并(epoll_ctl)注册检测事件。然后通过epoll_wait等待有事件信号到来,到来之后,将有信号的事件保存到events中,此时就开始我们的处理了…
注意:epoll_ctl注册事件之后,内核就开始检测事件信号了,然后通过epoll_wait将有信号的事件保存到events数组中。


void Epoll_Process(struct epoll_event* pevts, int num);

void epollModel::Epoll_Process(struct epoll_event *evts, int num)
{
    int i;
    int newfd;
    int iRet;
    sockaddr_in addr;
    socklen_t addrlen = sizeof(addr);
    char szBuf[MAX_LEN];
    FDINFO fdInfo;
    memset(&fdInfo, 0, sizeof(FDINFO));

    for (i = 0; i < num; i++)
    {
        if (evts[i].data.fd == m_fdTcpServ) // 用户连接(tcp fd有信号)
        {
            newfd = accept(m_fdTcpServ, (struct sockaddr*)&addr, &addrlen);
            if (newfd < 0)
            {
                perror("\rfail to accept\n");
                continue;
            }

            if (nConn == MAX_CONN) // 达到最大连接数
            {
                write(newfd, "Over limit!", 12);
                printf("\rOver connect...\n");
                close(newfd);
                continue;
            }

            m_fdmap.insert(std::make_pair(newfd, addr));
            AddMonitor(newfd);
            printf("\rnew connect %d from [%s:%d]\n",
                newfd, inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
            nConn++;
            continue;
        }

        if (evts[i].data.fd == m_fdUdpServ) // udp 有信号
        {
            memset(szBuf, 0, MAX_LEN);
            iRet = recvfrom(m_fdUdpServ, szBuf, MAX_LEN, 0, (struct sockaddr*)&addr, &addrlen);
            if (iRet > 0)
            {
                printf("\rUdp msg[%s:%d]:%s\n",
                    inet_ntoa(addr.sin_addr), ntohs(addr.sin_port), szBuf);
                // 处理udp数据 ...
                sendto(m_fdUdpServ, szBuf, strlen(szBuf), 0, (struct sockaddr*)&addr, addrlen);
            }
            continue;
        }

        ////////////////////////////////////////////////////////////////////
        //// 客户端有数据信息到来
        it = m_fdmap.find(evts[i].data.fd);
        if (it == m_fdmap.end()) // 判断fd是否在map中
        {
            printf("\rUnknow client fd:%d\n", evts[i].data.fd);
            continue;
        }


        memset(szBuf, 0, MAX_LEN);
        // iRet = recv(evts[i].data.fd, szBuf, MAX_LEN, 0);
        iRet = read(evts[i].data.fd, szBuf, MAX_LEN);// 接收数据 recv或read都可以
        if (iRet < 0) // 读取数据错误
        {
            perror("\rfail to read\n");
            continue;
        }

        if (iRet == 0) // 用户下线
        {
            DelMonitor(it, evts[i]);
            printf("\rDisconnect fd:%d [%s:%d]\n",
                evts[i].data.fd, inet_ntoa((it->second).sin_addr), ntohs((it->second).sin_port));
            continue;
        }

        fdInfo.fd = evts[i].data.fd;
        fdInfo.addr = it->second;
        NetFunc(fdInfo, szBuf, strlen(szBuf)); // 消息处理
    }
    return;
}

这里使用流程图展示
epoll流程

自己封装的epoll模型源码http://pan.baidu.com/s/1pK98cPX

不足之处望多多指正,共同进步

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值