epollモデルの理解パッケージと応用
11996 ワード
自分が以前TCPサーバを書いていたが,リソースとの同時性の問題を考慮する必要はなく,単独スレッドで単一TCP接続を処理する方式(PPC/TPCモデルと呼ぶ)を用いた.今、自分で高同時サーバーを作るには、これらの問題をうまく処理しなければなりません.linux 2を使っているからです.6であるため,I/O多重化技術インタフェースとしてepollを用いることが望ましい(ハハハ).
一般的に言えば、epollは、どのようなsocketがどのようなことをするつもりなのかを教えてくれます.selectモデルでは,selectはsocket状態を検出するために用いられ,両者の用法は大きく異なるが機構は異なる.selectの検出方法は,検出が必要なすべてのsocketを巡回するたびに動作socketを返す.epollはすべてのハンドル状態を検出するのではなく,カーネルのサポートにより無意味な検出を回避できる.
SOcketハンドルの数が特に大きい場合、まずPPC/TPCモデルが切られたに違いない.一方、selectはすべてのハンドルを遍歴するたびに、ハンドル遍歴の過程で多くの時間を費やしており、合併の数がハンドル総数に近づくと、selectはあまり時間を浪費していないが、合併数がリンク数をはるかに下回っている場合、例えばラウンド制のネットゲームでは、selectは時間を浪費している疑いがある.従ってepollはかなり効率的である.
epollをc++クラスにカプセル化する前に、epollのデータ構造とインタフェースについて簡単に紹介します.
epollイベント構造体:
ここのeventsはイベントのタイプで、よく使われています.
EPOLLINこのハンドルは読み取り可能です
EPOLLOUTハンドルは書き込み可能
EPOLLERRハンドルにエラーが発生しました
EPOLLET epollはエッジトリガモード
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をカプセル化します.
ここは私自身が普通のtcp socketのパッケージです.
Listen用sockはmsockに継承されます.
以上のmsockとmssockクラスにはsocketハンドルが含まれており、直接クラスをsocketハンドルに強制的に変換することができます.
epollをカプセル化する前に、epollのイベントに一時停止するために不定長のデータを格納するためのデータ構造を定義するステップがあります.
epollのパッケージは開始できます.エッジトリガ方式を使用しています.私の考えは、epollのハンドルとパラメータをクラスに記録し、対応するイベントのためにeventsデータを自分で維持することです.外部は、返されるイベントの一時番号に基づいてクラスメソッドで返される値を取得するだけです.
今は使いやすいepollクラスがあります.これにより、簡単な完全なサーバプログラムを実現することができます.
実装の過程で、listen用のハンドルとデータの送受信に使用されるハンドルを区別することに注意する必要がある点がいくつかあります.エッジトリガ方式を採用しているため、同僚のlistenが複数の接続に接続される可能性が高いが、ここではepoll_waitは一度だけ通知します.acceptイベントがあることに気づいたのに、すべてのacceptを処理していないと、多くのリンクが接続できません.この問題については、listenが発生したときにacceptが失敗するまですべてのリンクを処理してから続行することができます.
次に、私のゲームロジックのインタフェースとepollクラスを使用して、基本的なサーバプログラムを実現します.
ゲームロジックのインタフェースは簡単で、gamemainを呼び出してゲームクラスのインスタンスを作成するだけです.受信したデータを使用してmdata*gamemain::dealdata(mdata*data)関数を呼び出すと、ゲームロジック処理後のmdataが得られ、処理済みのmdataが返されます.ここで処理後のmdata*はゲームインスタンスが自動的に割り当てられ、送信後にgamemain::freedatainpool(mdata*data)が解放されます(そちらも自動的に解放されます).(ははは、自分が初めてゲームサーバーのロジックを書くことができるとは思わなかった)
このプログラムは、読み取り操作が完了するたびに、ゲームロジックを単一スレッドで処理して次のステップに進みます.ゲームロジックの効率が高く、データベースの待機に関連しない場合は、別のスレッドでゲームロジックを処理し、真の高同時性を実現することができます.
本文の全体の内容はすでに話し終わって、epollの学問はこれらだけではなくて、後での実践の中でゆっくりと蓄積する必要があります.
一般的に言えば、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ハンドルにエラーが発生しました
EPOLLET epollはエッジトリガモード
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が失敗するまですべてのリンクを処理してから続行することができます.
次に、私のゲームロジックのインタフェースと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
",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
",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
",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の学問はこれらだけではなくて、後での実践の中でゆっくりと蓄積する必要があります.