引言:将自己封装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模型源码http://pan.baidu.com/s/1pK98cPX
不足之处望多多指正,共同进步