天天看點

epoll模型的了解封裝與應用

    自己以前寫TCP伺服器,并不需要考慮到并發與資源的問題,使用的都是單獨線程處理單個TCP連接配接的方式(說謂的PPC/TPC模型)。如今自己做高并發伺服器,必須處理好這些問題。因為用的是linux2.6,是以選用epoll作為I/O多路複用技術接口再好不過了(其實自己也不太懂這個術語)。

    通俗地講,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直到accept失敗吧所有連結都處理完再繼續。

    下面我使用我的遊戲邏輯的接口和epoll類實作一個基本的伺服器程式:

    遊戲邏輯的接口很簡單,隻需要調用gamemain建立出該遊戲類的執行個體。并使用收到的資料調用mdata *gamemain::dealdata(mdata *data) 函數即可得到遊戲邏輯處理後的mdata,将處理好的mdata發回去,這裡處理後的mdata*是遊戲執行個體自動配置設定的,發完之後調用gamemain::freedatainpool(mdata *data)釋放(那邊也會自動釋放的)。(哈哈,沒想到自己第一次寫遊戲伺服器邏輯能做得如此低耦合)

    這個程式每一次讀操作完成後,都是在單線程處理完遊戲邏輯在進行下一步。如果遊戲邏輯效率高且不會涉及到資料庫等待的問題,這種方式可取,否則可以另起線程處理遊戲邏輯,實作真正的高并發。

    本文的整個内容已經講完了,epoll的學問可不止這些,需要在以後的實踐中要慢慢積累。