网上讲Epoll的很多,但是都仅仅停留在简单的示例使用和Epoll接口介绍,但是在真正的工程应用中不可能就使用这种简单的过程式开发。为了让小伙伴们对Epoll为什么对高并发网络通信有很好的应用,下面就结合以前在菊厂的开发经历来讲下网络通信对象同Epoll是如何很好得结合的。
传统的处理网络I/O的多进程、多线程同步I/O,或者是单线程的select和poll的事件驱动模型。其中多线程和多进程同步阻塞网络I/O技术,具有模型直观,使用方便等优点,但当处理高并发的网络连接时,因为存在Fork(线程池可部分避免)和上下文切换操作产生较大的系统开销;同时内存开销也较大,不能满足服务器性能要求,适用于并发数不高以及服务器负载不大的场合。因此,为了提升系统的高并发情况下的性能和吞吐率,一般采用IO多路复用模型。IO多路复用包括Select,Poll和Epoll三种方式,Epoll作为Linux内核为处理大批文件描述符而改进的poll。相对于select和poll,Epoll有以下两个优势:
即在Linux系统中Select最大只能支持1024个描述符,当需要监听1024个以上的描述符时,Select函数就会监听出错。而Epoll使用红黑树管理注册的描述符,理论上能监听无限个描述符,现实中会收到内存的限制。
下面我们就来讲讲。
在Epoll中有个重要的结构体epoll_event,它被用于注册所感兴趣的事件和回传所发生的事件。它的定义如下:
struct epoll_event {
__uint32_t events; /* epoll event /
epoll_data_t data; / User data variable */
};
它当中的epoll_data_t保存了触发事件的某个文件描述符相关的数据。定义如下
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
这里的关键设计是把epoll_data_t中的地址指针ptr同一个通信实体交互的通信单元类Agent进行绑定。为什么这么做呢?
我们把每个通信实体对应于一个Agent实例。当这个套接字或者文件描述符上有I/O事件到达时,Epoll会返回这个套接字所绑定的地址指针,这里把这个地址指针指向这个套接字或者文件描述符对应的Agent实例,这样就可以返回Agent实例的地址,然后根据I/O事件的不同调用Agent里面对应的处理函数处理与通信实体间的交互。Agent类处理的交互一般包括读写时间处理,以及Agent的启动和停止。
下面通过一个Epoll非阻塞的回射服务器的例子来讲解下上面提到的设计思想是如何实现的。
主要的涉及到的类有两个Epoll和Agent:
下面我们来介绍下这些类的设计
Agent类是事件处理器的基类,它声明如下:
class Agent
{
protected:
int conn_type; // Agent连接状态
int connfd;
public:
Agent(){}
virtual ~Agent(){}
virtual int sendData()=0;
virtual int recvData()=0;
int getState() const
{
return conn_type;
}
void setState(int st)
{
conn_type = st;
}
int getErrorno() const
{
return errno;
}
}
主要负责处理客户端发送过来的TCP连接请求。该类声明如下:
class TCPListenAgent : public Agent
{
public:
TCPListenAgent(EchoAgent *);
~TCPListenAgent();
bool init(void);
int SetNonblock(int fd);
virtual int RecvData();
virtual int sendData();
private:
void Socket();
private:
struct sockaddr_in m_cliAddr; //存储客户端连接的地址
struct sockadd_in m_servAddr; //存储服务端的地址
EchoAgent *m_pEchoAgent; //执行回射消息的通信对象
26 };
TCPListenAgent类主要实现Agent类提供的两个纯虚函数,这里列出TCPListenAgent类提供的主要方法:
通过初始化及运行在单例类RunControl中来实现单例模式,对于整个程序,全局仅有唯一的一个Epoll实例。Epoll类的声明如下:
class Epoll
{
public:
Epoll();
~Epoll();
int epollInitial(int size);
int doEvent(int fd, Agent *agentPtr, int op, unsigned int event);
int epollRegister(int fd, Agent *agent,int event);
int epollChange(int fd, Agent *agent, int event);
int epollDelete(int fd);
void run(void);
private:
struct epoll_event ev, *events;
int m_epfd; //Epoll 的句柄
int maxevents;
};
主要负责接受并解析客户端发送过来的回射消息,并重新封装好后发送回去。
class EchoAgent : public Agent
{
struct iov_req {
iov_req():mComplete(true) {}
iov_req(char *buffer, unsigned int len):mComplete(true)
{
mIov.iov_base = buffer;
mIov.iov_len = len;
}
struct iovec mIov;
bool mComplete ;
};
public:
EchoAgent();
EchoAgent(int fd);
~EchoAgent();
int recvData();
int sendData();
int SendPackage(MsgHeader &header, char *buffer);
int WriteDynamic(char *buf , int len);
private:
int Read();
int write();
private:
unsigned int m_iLen;//存取MsgHeader中的length字段
unsigned int m_iOffset;//读取偏移量
struct InReq m_InReq;
bool m_bInit;
bool m_bReadHead;//是否读取头部
void* mLastIov;
private:
std::list mIovList;//接收缓冲队列
};
其中struct MsgHeader同 InReq定义如下
struct MsgHeader
{
uint32_t cmd;
uint32_t length;
} __attribute__((packed));
struct InReq
{
MsgHeader m_msgheader;
char *ioBuf;
};
在EchoAgent类中定义了缓冲队列mIovList,它当中的成员都是struct iovec类型。struct iovec定义了一个向量元素。通常这个结构用作一个多元素的数组。对于每一个传输的元素,指针成员iov_base指向一个缓冲区,这个缓冲区是存放的是readv所接收的数据或是writev将要发送的数据。成员iov_len在各种情况下分别确定了接收的最大长度以及实际写入的长度。
当接收到客户端发过来的回射消息,EchoAgent调用recvData函数。函数定义如下
int EchoAgent::recvData()
{
if(this->Read() < 0)
{
err_sys("Read error");
return -1;
}
return 0;
}
从定义可以看出,调用了私有成员函数read, read通过成员m_InReq接受回射消息,当回射消息接收完成,将m_InReq的消息追加到 mIovList并调用doEvent将Epoll事件从EPOLLIN切换为EPOLLOUT。这个时候就会调用EchoAgent的成员函数sendData。sendData函数定义如下:
int EchoAgent::sendData()
{
if(write() < 0)
{
err_sys("write error");
return -1;
}
if(mIovList.size() == 0) {
gEpoll->doEvent(connfd, this, EPOLL_CTL_MOD, EPOLLIN);
return 0;
}
}
可以看到在成员函数sendData中调用了私有成员函数write。write主要实现了遍历 mIovList中的每个成员,并调用writev将接受到的回射消息回复给客户端。wirtev的第一个参数传入的文件描述符就是Agent的成员变量connfd。write调用成功后则再调用doEvent将Epoll事件从EPOLLOUT切换成EPOLLIN。
该类实例化了模板类Singleton,负责创建级初始化了Epoll和TCPListenAgent的全局对象。该类声明如下:
class RunControl : public Singleton
{
friend Singleton;
private:
RunControl() {}
~RunControl() {}
private:
void initEpoll();
void initListenAgent();
public:
void runEpoll();
void run();
};
成员函数runEpoll则通过全局对象指针g_pEpoll调用Epoll中的run接口。
void
RunControl::runEpoll()
{
// INFO_LOG("Gateway started. Waiting for connections.");
g_pEpoll->run();
}
以上主要是对该网络模型的逻辑进行了介绍,想查看具体代码如下:
Epoll非阻塞通信模型源码。希望对大家对Epoll的理解有所帮助,谢谢大家!