大廠是如何設計基于Epoll的網路通信模型
- 背景
- Epoll的優勢
- 設計思想
- 具體實作
- Agent類
- TCPListenAgent類
- Epoll類
- EchoAgent
- RunControl類
- 代碼鏈接
背景
?網上講Epoll的很多,但是都僅僅停留在簡單的示例使用和Epoll介面介紹,但是在真正的工程應用中不可能就使用這種簡單的程序式開發,為了讓小伙伴們對Epoll為什么對高并發網路通信有很好的應用,下面就結合以前在菊廠的開發經歷來講下網路通信物件同Epoll是如何很好得結合的,
Epoll的優勢
? 傳統的處理網路I/O的多行程、多執行緒同步I/O,或者是單執行緒的select和poll的事件驅動模型,其中多執行緒和多行程同步阻塞網路I/O技術,具有模型直觀,使用方便等優點,但當處理高并發的網路連接時,因為存在Fork(執行緒池可部分避免)和背景關系切換操作產生較大的系統開銷;同時記憶體開銷也較大,不能滿足服務器性能要求,適用于并發數不高以及服務器負載不大的場合,因此,為了提升系統的高并發情況下的性能和吞吐率,一般采用IO多路復用模型,IO多路復用包括Select,Poll和Epoll三種方式,Epoll作為Linux內核為處理大批檔案描述符而改進的poll,相對于select和poll,Epoll有以下兩個優勢:
- 支持理論上無限大的socket描述符
? select限制了每個行程打開的socket描述符,例如Linux系統在linux/include/linux/posix_types.h中定義了_FD_SETSIZE為1024,如下圖

? 即在Linux系統中Select最大只能支持1024個描述符,當需要監聽1024個以上的描述符時,Select函式就會監聽出錯,而Epoll使用紅黑樹管理注冊的描述符,理論上能監聽無限個描述符,現實中會收到記憶體的限制,
- 采用回呼函式避免遍歷所有描述符
? select和poll都是通過鏈表管理注冊好的描述符,每次當有描述符監聽到讀寫事件發生時,select和poll都需要遍歷整個鏈表從而找到有事件發生的描述符,在活動描述符較少的情況下,這種方案是非常低效的,Epoll采用紅黑樹管理描述符,如果有描述符監聽到讀寫時間發生,Epoll會通過回呼函式將該描述符插入到就緒佇列中,即每次Epoll掃描的只是就緒佇列中的發生讀寫事件的描述符,
設計思想
下面我們就來講講,
? 在Epoll中有個重要的結構體epoll_event,它被用于注冊所感興趣的事件和回傳所發生的事件,它的定義如下:
struct epoll_event {
__uint32_t events; /* epoll event /
epoll_data_t data; / User data variable */
};
? 它當中的epoll_data_t保存了觸發事件的某個檔案描述符相關的資料,定義如下
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
? 這里的關鍵設計是把epoll_data_t中的地址指標ptr同一個通信物體互動的通信單元類Agent進行系結,為什么這么做呢?
? 我們把每個通信物體對應于一個Agent實體,當這個套接字或者檔案描述符上有I/O事件到達時,Epoll會回傳這個套接字所系結的地址指標,這里把這個地址指標指向這個套接字或者檔案描述符對應的Agent實體,這樣就可以回傳Agent實體的地址,然后根據I/O事件的不同呼叫Agent里面對應的處理函式處理與通信物體間的互動,Agent類處理的互動一般包括讀寫時間處理,以及Agent的啟動和停止,
具體實作
下面通過一個Epoll非阻塞的回射服務器的例子來講解下上面提到的設計思想是如何實作的,
? 主要的涉及到的類有兩個Epoll和Agent:
- Epoll類封裝事件驅動核心,負責事件通知,讀寫事件發生后,由Agent處理網路讀寫,
- Agent類是事件處理器的基類,TCPListenAgent和TCPAgent繼承自Agent,分別處理TCP監聽套接字和普通TCP連接的網路收發,
該模型的簡化類圖如下:

下面我們來介紹下這些類的設計
Agent類
?Agent類是事件處理器的基類,它宣告如下:
class Agent
{
protected:
int conn_type; // Agent連接狀態
int connfd;
public:
Agent(){}
virtual ~Agent(){}
virtual int sendData()=0;
virtual int recvData()=0;
int getState() const
{
return conn_type;
}
void setState(int st)
{
conn_type = st;
}
int getErrorno() const
{
return errno;
}
}
- recvData函式作為純虛函式用于接受客戶端的請求,成功回傳讀取的位元組數,失敗回傳-1,
- sendData函式作為純虛函式用于回復客戶端的請求,成功回傳寫出去的位元組數,失敗回傳-1,
TCPListenAgent類
?主要負責處理客戶端發送過來的TCP連接請求,該類宣告如下:
class TCPListenAgent : public Agent
{
public:
TCPListenAgent(EchoAgent *);
~TCPListenAgent();
bool init(void);
int SetNonblock(int fd);
virtual int RecvData();
virtual int sendData();
private:
void Socket();
private:
struct sockaddr_in m_cliAddr; //存盤客戶端連接的地址
struct sockadd_in m_servAddr; //存盤服務端的地址
EchoAgent *m_pEchoAgent; //執行回射訊息的通信物件
26 };
? TCPListenAgent類主要實作Agent類提供的兩個純虛函式,這里列出TCPListenAgent類提供的主要方法:
- init函式用于初始化TCPListenAgent 本身,初始化成功回傳true,失敗回傳false,
- recvData函式用于處理客戶端發送來的TCP連接請求,
Epoll類
? 通過初始化及運行在單例類RunControl中來實作單例模式,對于整個程式,全域僅有唯一的一個Epoll實體,Epoll類的宣告如下:
class Epoll
{
public:
Epoll();
~Epoll();
int epollInitial(int size);
int doEvent(int fd, Agent *agentPtr, int op, unsigned int event);
int epollRegister(int fd, Agent *agent,int event);
int epollChange(int fd, Agent *agent, int event);
int epollDelete(int fd);
void run(void);
private:
struct epoll_event ev, *events;
int m_epfd; //Epoll 的句柄
int maxevents;
};
- epollInitial函式用于初始化Epoll物件,引數size是Epoll監聽佇列的長度,
- doEvent函式用于對Epoll事件進行操作,增加、洗掉或者修改,引數agentPtr具體的Agent物件,fd為需要加入Epoll的描述符,op為Epoll的具體操作可傳入引數包括EPOLL_ADD,EPOLL_CTL,EPOLL_DEL,event為要監聽的Epoll事件,比如EPOLLOUT, EPOLLIN,
- run函式用于執行EPOLL整個運行流程,函式中主要呼叫epol_wait函式,當struct epoll_event的events判斷是EPOLLIN時,Agent物件呼叫recvData函式,如果是EPOLLOUT事件時呼叫sendData()函式,當events是EPOLLHUP或者是EPOLLERR,則判斷通信鏈路的連通性是否是EISCONN,
EchoAgent
?主要負責接受并決議客戶端發送過來的回射訊息,并重新封裝好后發送回去,
class EchoAgent : public Agent
{
struct iov_req {
iov_req():mComplete(true) {}
iov_req(char *buffer, unsigned int len):mComplete(true)
{
mIov.iov_base = buffer;
mIov.iov_len = len;
}
struct iovec mIov;
bool mComplete ;
};
public:
EchoAgent();
EchoAgent(int fd);
~EchoAgent();
int recvData();
int sendData();
int SendPackage(MsgHeader &header, char *buffer);
int WriteDynamic(char *buf , int len);
private:
int Read();
int write();
private:
unsigned int m_iLen;//存取MsgHeader中的length欄位
unsigned int m_iOffset;//讀取偏移量
struct InReq m_InReq;
bool m_bInit;
bool m_bReadHead;//是否讀取頭部
void* mLastIov;
private:
std::list<iov_req> mIovList;//接識訓沖佇列
};
其中struct MsgHeader同 InReq定義如下
struct MsgHeader
{
uint32_t cmd;
uint32_t length;
} __attribute__((packed));
struct InReq
{
MsgHeader m_msgheader;
char *ioBuf;
};
?在EchoAgent類中定義了緩沖佇列mIovList,它當中的成員都是struct iovec型別,struct iovec定義了一個向量元素,通常這個結構用作一個多元素的陣列,對于每一個傳輸的元素,指標成員iov_base指向一個緩沖區,這個緩沖區是存放的是readv所接收的資料或是writev將要發送的資料,成員iov_len在各種情況下分別確定了接收的最大長度以及實際寫入的長度,
? 當接收到客戶端發過來的回射訊息,EchoAgent呼叫recvData函式,函式定義如下
int EchoAgent::recvData()
{
if(this->Read() < 0)
{
err_sys("Read error");
return -1;
}
return 0;
}
?從定義可以看出,呼叫了私有成員函式read, read通過成員m_InReq接受回射訊息,當回射訊息接收完成,將m_InReq的訊息追加到 mIovList并呼叫doEvent將Epoll事件從EPOLLIN切換為EPOLLOUT,這個時候就會呼叫EchoAgent的成員函式sendData,sendData函式定義如下:
int EchoAgent::sendData()
{
if(write() < 0)
{
err_sys("write error");
return -1;
}
if(mIovList.size() == 0) {
gEpoll->doEvent(connfd, this, EPOLL_CTL_MOD, EPOLLIN);
return 0;
}
}
?可以看到在成員函式sendData中呼叫了私有成員函式write,write主要實作了遍歷 mIovList中的每個成員,并呼叫writev將接受到的回射訊息回復給客戶端,wirtev的第一個引數傳入的檔案描述符就是Agent的成員變數connfd,write呼叫成功后則再呼叫doEvent將Epoll事件從EPOLLOUT切換成EPOLLIN,
RunControl類
?該類實體化了模板類Singleton,負責創建級初始化了Epoll和TCPListenAgent的全域物件,該類宣告如下:
class RunControl : public Singleton<RunControl>
{
friend Singleton<RunControl>;
private:
RunControl() {}
~RunControl() {}
private:
void initEpoll();
void initListenAgent();
public:
void runEpoll();
void run();
};
成員函式runEpoll則通過全域物件指標g_pEpoll呼叫Epoll中的run介面,
void
RunControl::runEpoll()
{
g_pEpoll->run();
}
代碼鏈接
? 以上主要是對該網路模型的邏輯進行了介紹,想查看具體代碼如下:
Epoll非阻塞通信模型原始碼,希望對大家對Epoll的理解有所幫助,謝謝大家!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/280307.html
標籤:AI
上一篇:10種編程語言實作Y組合子
下一篇:上傳圖片格式一句話木馬
