epoll模型的理解封裝與應用

    本身之前寫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的學問可不止這些,須要在之後的實踐中要慢慢積累。

相關文章
相關標籤/搜索