本身之前写TCP服务器,并不须要考虑到并发与资源的问题,使用的都是单独线程处理单个TCP链接的方式(说谓的PPC/TPC模型)。现在本身作高并发服务器,必须处理好这些问题。由于用的是linux2.6,所以选用epoll做为I/O多路复用技术接口再好不过了(其实本身也不太懂这个术语)。linux
通俗地讲,epoll就是:告诉你有哪些socket准备要作哪些事。在select模型中,select用来检测socket状态,二者的用法截然不同,可是机制不一样。select的检测方法是每次遍历全部须要检测的socket,并返回有动做socket。而epoll的并不会检测全部的句柄状态,经过内核的支持,能避免无心义的检测。
ios
当socket句柄的数目特别大的状况下,首先PPC/TPC模型确定就挂掉了。而select由于每次要遍历全部句柄,所以在句柄遍历的过程当中占用了不少的时间,若是并发的数量接近句柄总数,select并无浪费太多时间,但对于并发数远低于连接数的状况,好比回合制的网络游戏,select就有浪费时间的嫌疑。所以epoll是至关高效的。c++
在将epoll封装成c++类以前,对epoll的数据结构以及接口作一下简单介绍:
数据库
epoll 事件结构体:数组
struct epoll_event { __uint32_t events; // Epoll events epoll_data_t data; // User datavariable };
这里的events是事件的类型,经常使用的有:服务器
EPOLLIN 该句柄为可读
网络
EPOLLOUT 该句柄为可写
数据结构
EPOLLERR 该句柄发生错误
并发
EPOLLET epoll为边缘触发模式socket
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; }
这个程序每一次读操做完成后,都是在单线程处理完游戏逻辑在进行下一步。若是游戏逻辑效率高且不会涉及到数据库等待的问题,这种方式可取,不然能够另起线程处理游戏逻辑,实现真正的高并发。
本文的整个内容已经讲完了,epoll的学问可不止这些,须要在之后的实践中要慢慢积累。