在上一文中已经介绍了,为了将Windows下的select()和linux下的epoll()封装为统一接口,所以需要首先定义一个接口类,来约定统一接口和一些类型,做到不同平台,调用接口一致,内部实现根据平台不同的跨平台效果,并给出了接口类的实现,这里为了方便阅读,再贴一遍代码:
统一接口类(多路复用基类):
#ifndef __POLLBASE_H_
#define __POLLBASE_H_
#include <stdlib.h>
#include <map>
#include <vector>
//统一描述符的类型命名,都为SOCKET
#ifndef _WIN32
typedef int SOCKET;
#define INVALID_HANDLE_VALUE (-1)
#endif
//重定义Windows下FD_SETSIZE的大小
#ifdef _WIN32
#define FD_SETSIZE 1024
#include <WinSock2.h>
#else
#include <unistd.h>
#include <sys/time.h>
#include <sys/epoll.h>
#include <sys/select.h>
#endif
#define MAXEVENT 1024
//统一定义Windows下select()和linux下epoll()的事件的宏
#define NONE 0 //无
#define POLLREAD 1 //读事件
#define POLLWRITE 2 //写事件
#define POLLET 3 //边缘触发(用于EPOLL, 是否设置边缘触发,默认是水平触发)
//定义统一的事件结构
typedef struct PollEvent {
int event;
int fd;
}PollEvent;
//接口类(抽象类)
class PollBase
{
public:
virtual ~PollBase() {};
/*
创建(初始化)接口
size: 可关注的描述符数量
返回值:
0: 成功
-1: 失败
*/
virtual int Create(int size) = 0;
/*
为fd描述符添加关注的事件
fd: socket文件描述符
mask: 上面统一定义的事件宏(读事件、写事件)
返回值:
0: 成功
-1: 失败
*/
virtual int AddEvent(SOCKET fd, int mask) = 0;
/*
删除fd描述符关注的事件
fd: socket文件描述符
mask: 上面统一定义的事件宏(读事件、写事件)
返回值:
0: 成功
-1: 失败
*/
virtual int DelEvent(SOCKET fd, int mask) = 0;
/*
监听事件到来,IO多路复用的核心
active_events: 输出参数,如果有事件到来,则会将事件设置到active_events输出
timeout: 超时时间(秒)
返回值:
0:无事件
>0:到来事件的数量
*/
virtual int Poll(PollEvent* active_events, int timeout = 0) = 0;
};
#endif
Epoll.h文件中定义Epoll类继承自PollBase
#ifndef _EPOLL_H_
#define _EPOLL_H_
#include "PollBase.h"
#ifndef _WIN32
class Epoll :
public PollBase
{
public:
~Epoll();
int Create(int size);
int AddEvent(SOCKET fd, int event);
int DelEvent(SOCKET fd, int event);
int Poll(PollEvent* active_events, int timeout = 0);
private:
//epoll文件描述符(epoll实例句柄,epoll_create()的返回值)
int m_EpollFd;
//epoll事件数量
int m_EventSize;
//epoll fd对应关注事件(POLLREAD、POLLWRITE、POLLET)映射表
std::map<SOCKET, int> m_EventMap;
//存储到来的临时事件列表
std::vector<struct epoll_event> m_Events;
};
#endif
#endif
Epoll.cpp
#include "Epoll.h"
#ifndef _WIN32
//析构函数中清理数据
Epoll::~Epoll() {
m_Events.clear();
m_Events.shrink_to_fit();
m_EventMap.clear();
//关闭epoll文件描述符
if (m_EpollFd != -1)
close(m_EpollFd);
}
//创建多路IO
int Epoll::Create(int size) {
m_EventSize = size;
m_Events.resize(m_EventSize);
//创建epoll
m_EpollFd = epoll_create(size);
if (-1 == m_EpollFd) {
return -1;
}
m_EventMap.clear();
return 0;
}
//增加关注事件
int Epoll::AddEvent(SOCKET fd, int event) {
//临时存储旧的关注事件
int old_event = m_EventMap[fd];
//存储新的关注事件
int new_event = m_EventMap[fd];
//判断添加可读事件(|=表示不删除原来关注的事件的前提下,增加关注新的事件)
if (event & POLLREAD) {
new_event |= EPOLLIN;
}
//判断添加可写事件
if (event & POLLWRITE) {
new_event |= EPOLLOUT;
}
//判断设置边缘触发模式(默认是水平触发模式)
if (event & POLLET){
new_event |= EPOLLET;
}
//更新socketfd对应的事件
m_EventMap[fd] = new_event;
//创建新的epoll事件结构,赋值新的关注事件
struct epoll_event ev;
ev.events = new_event;
ev.data.fd = fd;
//old_event>0表示该fd之前有在epoll中关注过其他事件,所以这里应该是使用修改更新事件的模式,
//否则应该使用添加新事件的模式
int ctl_mode = old_event > 0 ? EPOLL_CTL_MOD : EPOLL_CTL_ADD;
if (-1 == epoll_ctl(m_EpollFd, ctl_mode, fd, &ev)) {
return -1;
}
return 0;
}
//移除关注的事件 (基本逻辑同添加)
int Epoll::DelEvent(SOCKET fd, int event) {
int old_event = m_EventMap[fd];
int new_event = old_event;
if (event & POLLREAD) {
new_event ^= EPOLLIN;
}
if (event & POLLWRITE) {
new_event ^= EPOLLOUT;
}
m_EventMap[fd] = new_event;
struct epoll_event ev;
ev.events = new_event;
ev.data.fd = fd;
int ctl_mode = ev.events > 0 ? EPOLL_CTL_MOD : EPOLL_CTL_DEL;
if (-1 == epoll_ctl(m_EpollFd, ctl_mode, fd, &ev)) {
return -1;
}
return 0;
}
//事件分发
int Epoll::Poll(PollEvent* active_events, int timeout) {
//epoll_wait 等待关注的事件到来
//当新事件到来时,会将到来的事件写入m_Events数组
int num = epoll_wait(m_EpollFd, &m_Events[0], m_EventSize, timeout);
//num<=0 表示无事件到来,不作处理(本系列统一使用非阻塞套接字,因此当没有事件的时候,num一般都为0)
if (num <= 0)
return 0;
//当有事件到来时,num则是到来的事件数量,遍历m_Events事件数组处理各个事件
for (int i = 0; i < num; i++) {
int event = 0;
//可读事件
if (m_Events[i].events & EPOLLIN) {
event |= POLLREAD;
}
//可写事件
if (m_Events[i].events & EPOLLOUT) {
event |= POLLWRITE;
}
//将事件信息赋值给输出参数active_events数组
active_events[i].fd = m_Events[i].data.fd;
active_events[i].event = event;
}
//返回事件数量
return num;
}
#endif
封装后的大概调用流程,和前文的Select接口调用代码一致:
......
void EventLoop::Run() {
while (!m_Stop) {
memset(m_ActiveEvents, 0, m_Size);
int num = m_Poll->Poll(m_ActiveEvents);
if (num > 0) {
//分发事件进行处理
this->ActiveEvents(num);
}
}
}
......
经过前面各类基础类的封装,到现在封装之后,接下来就是对各个事件的处理,处理各个tcp连接,以及在数据的流转,最后和上层调用者的交互了。