epoll模型的理解封装与应用

http://www.tuicool.com/articles/vyuAbay

通俗地讲,epoll就是:告诉你有哪些socket准备要做哪些事。在 select 模型中, select 用来检测socket状态,两者的用法大相径庭,但是机制不同。select的检测方法是每次遍历所有需要检测的socket,并返回有动作socket。而epoll的并不会检测所有的句柄状态,通过内核的支持,能避免无意义的检测。

当socket句柄的数目特别大的情况下,首先PPC/TPC模型肯定就挂掉了。而select因为每次要遍历所有句柄,因此在句柄遍历的过程中占用了很多的时间,如果并发的数量接近句柄总数,select并没有浪费太多时间,但对于并发数远低于链接数的情况,比如回合制的网络游戏,select就有浪费时间的嫌疑。因此epoll是相当高效的。

在将epoll封装成c++类之前,对epoll的数据结构以及接口做一下简单介绍:

epoll 事件结构体:

struct epoll_event {
        __uint32_t events;      // Epoll events
        epoll_data_t data;      // User datavariable
    };

这里的events是事件的类型,常用的有:

EPOLLIN该句柄为可读

EPOLLOUT该句柄为可写

EPOLLERR该句柄发生错误

EPOLLETepoll为边缘触发模式

epoll 事件date

typedef union epoll_data {
       void *ptr;
        int fd;
       __uint32_t u32;
       __uint64_t u64;
} epoll_data_t;

注意epoll_data是个union。我们想要挂上句柄或是数据指针都很方便。

epoll创建:

int epoll_create(int size);

调用该函数会创建一个epoll句柄,参数size为监听的最大数量

epoll控制:

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

 这个接口用于对该epdf上的句柄进行注册、修改和删除。

op是要进行的操作,有:

EPOLL_CTL_ADD添加需要监测的文件句柄fd

EPOLL_CTL_MOD更改该fd句柄的模式

EPOLL_CTL_DEL移除掉该句柄

event是所要设置的该fd的事件。

epoll收集信息:

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

    调用该函数后,如果该有epoll所管理的句柄发生对应类型的事件,这些发生事件的句柄的epoll_event将会被写入events数组中,我们便能根据这些句柄执行接下来的I/O以及其他操作。这里的maxevents是每次wait获取的事件最大数。如果使用的是ET边缘触发模式,epoll_wait返回一个事件后,再这个时间的状态没有改变的情况下,epoll_wait不会再对改事件进行通知。

epoll基本的介绍完,就可以先对epoll进行一定的封装以增强代码的复用。

在封装epoll之前,我先给出我封装好的用于tcp的socket:

//总共所需要用到的头文件,有部分是多余的
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cstdlib>

#ifdef WIN32
#include<winsock2.h>
#else
#include<fcntl.h>
#include<sys/ioctl.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<unistd.h>
#include<netdb.h>
#include<errno.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/types.h>
#define SOCKET int
#define SOCKET_ERROR -1
#define INVALID_SOCKET -1
#endif

这里是我自己对普通tcp socket的封装:

class msock
{
public:
	SOCKET sock;
	sockaddr_in addr;
	msock()
	{
		addr.sin_family=AF_INET;
	}
	void setsock(SOCKET fd)
	{
		sock=fd;
	}
	SOCKET getsock()
	{
		return sock;
	}
	void createsock()
	{
		sock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
		if(sock==INVALID_SOCKET)
		{
			puts("socket build error");
			exit(-1);
		}
	}
	void setioctl(bool x)
	{
		fcntl(sock, F_SETFL, O_NONBLOCK);
	}
	bool setip(string ip)
	{
		hostent *hname=gethostbyname(ip.c_str());
		if(!hname)
		{
			puts("can't find address");
			return false;
		}//puts(inet_ntoa(addr.sin_addr));
		addr.sin_addr.s_addr=*(u_long *)hname->h_addr_list[0];
		return true;
	}
	void setport(int port)
	{
		addr.sin_port=htons(port);
	}
	int msend(const char *data,const int len)
	{
		return send(sock,data,len,0);
	}
	int msend(const string data)
	{
		return msend(data.c_str(),data.length());
	}
	int msend(mdata *data)
	{
		return msend(data->buf,data->len);
	}
	int mrecv(char *data,int len)
	{
		return recv(sock,data,len,0);
	}
	int mrecv(char *data)
	{
		return recv(sock,data,2047,0);
	}
	int mclose()
	{
		return close(sock);
	}
	int operator == (msock jb)
	{
		return sock==jb.sock;
	}
};

listen用的sock继承于msock:

class mssock:public msock
{
public:
	sockaddr_in newaddr;
	socklen_t newaddrlen;
	mssock():msock()
	{
		createsock();
		addr.sin_addr.s_addr=htonl(INADDR_ANY);
		newaddrlen=sizeof(newaddr);//hehe
	}
	int mbind()
	{
		return bind(sock,(sockaddr *)&addr,sizeof(addr));
	}
	int mlisten(int num=20)
	{
		return listen(sock,num);
	}
	msock maccept()
	{
		SOCKET newsock=accept(sock,(sockaddr *)&newaddr,&newaddrlen);
		msock newmsock;
		newmsock.setsock(newsock);
		return newmsock;
	}
};

以上的msock和mssock类里面含有socket句柄,可以直接将类强制转换为socket句柄

在对epoll封装之前还有一步就是:定义一个数据结构用于存放不定长度的数据,以便挂入epoll的事件中。

struct mdata
{
	int fd;
	unsigned int len;
	char buf[2048];
	mdata(){}
	mdata(char *s,const int length)
	{
		for(int i=0;i<length;i++)
		{
			buf[i]=s[i];
		}
	}
};

epoll的封装可以开始了,使用的是边缘触发的方式,我的思路是:将epoll的句柄以及参数都记录在类中,并自己维护一个events数据用于对应的事件。外部只需要根据返回事件的临时编号通过类的方法获取返回值即可。

class mepoll
{
public:
	int epfd;		//epoll自身的句柄
	epoll_event ev,*events;	//临时事件和每次wait用于储存的事件数组
	int maxevents;	//最大事件数
	int timeout;	//wait超时
	//构造函数默认最大事件数为20
	mepoll(unsigned short eventsnum=20)
	{
		epfd=epoll_create(0xfff);
		maxevents=eventsnum;
		events=new epoll_event[maxevents];
		timeout=-1;
	}
	//添加新的socket句柄到epoll中
	int add(SOCKET fd)
	{
		fcntl(fd, F_SETFL, O_NONBLOCK);//设置fd为非阻塞
		ev.events=EPOLLIN|EPOLLET;
		ev.data.fd=fd;
		return epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev);
	}
	//设置对应编号的句柄事件为可读
	void ctl_in(int index)
	{
		ev.data.fd=*(int *)events[index].data.ptr;
		ev.events=EPOLLIN|EPOLLET;
		epoll_ctl(epfd,EPOLL_CTL_MOD,*(int *)events[index].data.ptr,&ev);
	}
	//改可写,并将要写的数据data绑定到该句柄对应的事件中
	void ctl_out(int index,mdata *data)
	{
		data->fd=events[index].data.fd;
		ev.data.ptr=data;
		ev.events=EPOLLOUT|EPOLLET;
		epoll_ctl(epfd,EPOLL_CTL_MOD,events[index].data.fd,&ev);
	}
	int wait()
	{
		return epoll_wait(epfd,events,maxevents,timeout);
	}
	unsigned int geteventtype(int index)
	{
		return events[index].events;
	}
	//获取对应编号中的msock
	msock getsock(int index)
	{
		msock sk;
		sk.setsock(events[index].data.fd);
		return sk;
	}
	//从mdata里获取出msock
	msock getsock(mdata *data)
	{
		msock sk;
		sk.setsock(data->fd);
		return sk;
	}
	//获取对应编号的事件
	mdata *getdata(int index)
	{
		return (mdata *)events[index].data.ptr;
	}
};

现在有一个比较好用的epoll类了。于是可以开始实现一个简单的完整服务器程序了。

在实现过程中,有几点需要注意区分用于listen用的句柄和收发数据使用的句柄。因为采用的是边缘触发的方式,很可能会出现同事listen到多个连接的情况,但是这里epoll_wait只会通知一次。如果我们发现有accept事件,我们却没有把所有accept处理完,很多的链接就不能连入。对于这种问题,可以这样处理:在listen发生时,一直accept直到accept失败吧所有链接都处理完再继续。

下面我使用我的游戏逻辑的接口和epoll类实现一个基本的服务器程序:

游戏逻辑的接口很简单,只需要调用gamemain创建出该游戏类的实例。并使用收到的数据调用 mdata *gamemain::dealdata(mdata *data) 函数即可得到游戏逻辑处理后的mdata,将处理好的mdata发回去,这里处理后的mdata*是游戏实例自动分配的,发完之后调用 gamemain::freedatainpool( mdata *data ) 释放(那边也会自动释放的)。(哈哈,没想到自己第一次写游戏服务器逻辑能做得如此低耦合)

#include "ssock.h"
#include "game.h"
int main()
{
	gamemain game;//创建游戏实例

	mepoll ep;//epoll类
	mssock ssock;//服务器listen用的sock
	msock  csock;//临时sock
	mdata rdata;//临时rdata

	ssock.setport(5000);//使用5000端口
	if(SOCKET_ERROR==ssock.mbind())
	{
		puts("bind error");
		return -1;
	}
	if(SOCKET_ERROR==ssock.mlisten())
	{
		puts("listen error");
		return -1;
	}
	//开始listen
	//将listen句柄加入到epoll中
	ep.add(ssock.getsock());
puts("server start");
	int ionum;
	while(1)
	{
		ionum=ep.wait();//获取事件
		//遍历并处理所有事件
		for(int i=0; i<ionum; i++)
		{
printf("some data come: ");
			csock=ep.getsock(i);
			if(ep.geteventtype(i)&EPOLLERR)
			{
				printf("sock %u error\n",csock.sock);
				csock.mclose();
			}
			else if(ssock==csock)//处理listen事件
			{
				while(1)//accept直到没有新连接
				{
					csock=ssock.maccept();
					if(csock.getsock()==SOCKET_ERROR)
					{
						break;
					}
					//将新连接加入到epoll中
					ep.add(csock.getsock());
puts("a newsock comed:");
				}
			}
			else if(ep.geteventtype(i)&EPOLLIN)//处理接收事件
			{
				//根据临时编号获取到对应sock并接收数据
				csock=ep.getsock(i);
printf("sock %u in\n",csock.sock);
				int rlen;
				bool isrecv=false;
				rdata.len=0;
				while(1)
				{
					rlen=csock.mrecv(rdata.buf+rdata.len);
					if(rlen<0)
					{
						if (errno == EAGAIN)
						{
							isrecv = true;
							break;
						}
						else if (errno == EINTR)
						{
							continue;
						}
						else
						{
							break;
						}
					}
				}
				if(isrecv)
				{
					//调用游戏逻辑处理数据并修改sock事件为发送
					ep.ctl_out(i,game.dealdata(&rdata));
				}
			}
			else if(ep.geteventtype(i)&EPOLLOUT)//处理发送事件
			{
				mdata *data=ep.getdata(i);
				csock=ep.getsock(data);
printf("sock %u out type:%u\n",csock.sock,data->buf[4]);
				int slen,cnt=0;
				bool issend=false;
				while(1)
				{
					slen=csock.msend(data);
					if(slen<0)
					{
						if (errno == EAGAIN)
						{
							// 对于nonblocking 的socket而言,这里说明了已经全部发送成功了
							issend = true;
							break;
						}
						else if (errno == EINTR)
						{
							// 被信号中断
							continue;
						}
						else
						{
							// 其他错误
							break;
						}
					}
					if(slen=0)
					{
						break;
					}
					/*cnt+=slen;
					if(cnt>=data->len)*/
					{
						issend=true;
						break;
					}
				}
				game.freedatainpool(data);
				//无论发送情况都要改为可写,以容错
				ep.ctl_in(i);
			}
		}
	}
puts("server ended");
	return 0;
}

这个程序每一次读操作完成后,都是在单线程处理完游戏逻辑在进行下一步。如果游戏逻辑效率高且不会涉及到数据库等待的问题,这种方式可取,否则可以另起线程处理游戏逻辑,实现真正的高并发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值