〇、epoll與select對比
一).Epoll 介紹
Epoll 可是當前在 Linux 下開發大規模併發網絡程序的熱門人選, Epoll 在 Linux2.6 內核中正式引入,和 select 相似,其實都 I/O 多路複用技術而已 ,並沒有什麼神祕的。其實在 Linux 下設計併發網絡程序,向來不缺少方法,比如典型的 Apache 模型( Process Per Connection ,簡稱 PPC ), TPC ( Thread Per Connection )模型,以及 select 模型和 poll 模型,那爲何還要再引入 Epoll 這個東東呢?那還是有得說說的 …
二). 常用模型的缺點
如果不擺出來其他模型的缺點,怎麼能對比出 Epoll 的優點呢。
① PPC/TPC 模型
這兩種模型思想類似,就是讓每一個到來的連接一邊自己做事去,別再來煩我 。只是 PPC 是爲它開了一個進程,而 TPC 開了一個線程。可是別煩我是有代價的,它要時間和空間啊,連接多了之後,那麼多的進程 / 線程切換,這開銷就上來了;因此這類模型能接受的最大連接數都不會高,一般在幾百個左右。
② select 模型
1. 最大併發數限制,因爲一個進程所打開的 FD (文件描述符)是有限制的,由 FD_SETSIZE 設置,默認值是 1024/2048 ,因此 Select 模型的最大併發數就被相應限制了。自己改改這個 FD_SETSIZE ?想法雖好,可是先看看下面吧 …
2. 效率問題, select 每次調用都會線性掃描全部的 FD 集合,這樣效率就會呈現線性下降,把 FD_SETSIZE 改大的後果就是,大家都慢慢來,什麼?都超時了。
3. 內核 / 用戶空間 內存拷貝問題,如何讓內核把 FD 消息通知給用戶空間呢?在這個問題上 select 採取了內存拷貝方法。
總結爲:1.連接數受限 2.查找配對速度慢 3.數據由內核拷貝到用戶態
③ poll 模型
基本上效率和 select 是相同的, select 缺點的 2 和 3 它都沒有改掉。
三). Epoll 的提升
把其他模型逐個批判了一下,再來看看 Epoll 的改進之處吧,其實把 select 的缺點反過來那就是 Epoll 的優點了。
①. Epoll 沒有最大併發連接的限制,上限是最大可以打開文件的數目,這個數字一般遠大於 2048, 一般來說這個數目和系統內存關係很大 ,具體數目可以 cat /proc/sys/fs/file-max 察看。
②. 效率提升, Epoll 最大的優點就在於它只管你“活躍”的連接 ,而跟連接總數無關,因此在實際的網絡環境中, Epoll 的效率就會遠遠高於 select 和 poll 。
③. 內存拷貝, Epoll 在這點上使用了“共享內存 ”,這個內存拷貝也省略了。
四). Epoll 爲什麼高效
Epoll 的高效和其數據結構的設計是密不可分的,這個下面就會提到。
首先回憶一下 select 模型,當有 I/O 事件到來時, select 通知應用程序有事件到了快去處理,而應用程序必須輪詢所有的 FD 集合,測試每個 FD 是否有事件發生,並處理事件;代碼像下面這樣:
int res = select(maxfd+1, &readfds, NULL, NULL, 120);
if (res > 0)
{
for (int i = 0; i < MAX_CONNECTION; i++)
{
if (FD_ISSET(allConnection[i], &readfds))
{
handleEvent(allConnection[i]);
}
}
}
// if(res == 0) handle timeout, res < 0 handle error
Epoll 不僅會告訴應用程序有I/0 事件到來,還會告訴應用程序相關的信息,這些信息是應用程序填充的,因此根據這些信息應用程序就能直接定位到事件,而不必遍歷整個FD 集合。
int res = epoll_wait(epfd, events, 20, 120);
for (int i = 0; i < res;i++)
{
handleEvent(events[n]);
}
五). Epoll 關鍵數據結構
前面提到 Epoll 速度快和其數據結構密不可分,其關鍵數據結構就是:
struct epoll_event {
__uint32_t events; // Epoll events
epoll_data_t data; // User data variable
};
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
可見 epoll_data 是一個 union 結構體 , 藉助於它應用程序可以保存很多類型的信息 :fd 、指針等等。有了它,應用程序就可以直接定位目標了。
一、什麼是epoll
epoll是什麼?按照man手冊的說法:是爲處理大批量句柄而作了改進的poll。當然,這不是2.6內核纔有的,它是在2.5.44內核中被引進的(epoll(4) is a new API introduced in Linuxkernel 2.5.44),它幾乎具備了之前所說的一切優點,被公認爲Linux2.6下性能最好的多路I/O就緒通知方法。
二、epoll的相關係統調用
epoll只有epoll_create,epoll_ctl,epoll_wait 3個系統調用。
1. int epoll_create(int size);
創建一個epoll的句柄。自從linux2.6.8之後,size參數是被忽略的。需要注意的是,當創建好epoll句柄後,它就是會佔用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須調用close()關閉,否則可能導致fd被耗盡。
2. int epoll_ctl(int epfd, int op,int fd, struct epoll_event *event);
epoll的事件註冊函數,它不同於select()是在監聽事件時告訴內核要監聽什麼類型的事件,而是在這裏先註冊要監聽的事件類型。
第一個參數是epoll_create()的返回值。
第二個參數表示動作,用三個宏來表示:
EPOLL_CTL_ADD:註冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;
第三個參數是需要監聽的fd。
第四個參數是告訴內核需要監聽什麼事,struct epoll_event結構如下:
//保存觸發事件的某個文件描述符相關的數據(與具體使用方式有關)
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
// 感興趣的事件和被觸發的事件
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events可以是以下幾個宏的集合:
EPOLLIN :表示對應的文件描述符可以讀(包括對端SOCKET正常關閉);
EPOLLOUT:表示對應的文件描述符可以寫;
EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這裏應該表示有帶外數據到來);
EPOLLERR:表示對應的文件描述符發生錯誤;
EPOLLHUP:表示對應的文件描述符被掛斷;
EPOLLET: 將EPOLL設爲邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(LevelTriggered)來說的。
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裏
3. int epoll_wait(int epfd, structepoll_event * events, int maxevents, int timeout);
收集在epoll監控的事件中已經發生的事件。參數events是分配好的epoll_event結構體數組,epoll將會把發生的事件賦值到events數組中(events不可以是空指針,內核只負責把數據複製到這個events數組中,不會去幫助我們在用戶態中分配內存)。maxevents告之內核這個events有多大,這個 maxevents的值不能大於創建epoll_create()時的size,參數timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。如果函數調用成功,返回對應I/O上已準備好的文件描述符數目,如返回0表示已超時。
還有一個與這個類似的函數epoll_pwait:
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);
與epoll_wait的區別是可以通過最後一個參數設置阻塞過程中信號屏蔽字。
上面的函數原型等價於:
sigset_toriginmask;
sigpromask(SIG_SETMASK,&sigmask,& originmask);
ready = epoll_wait(epfd,&events,maxevents,timeout);
sigpromask(SIG_SETMASK,& originmask ,NULL);
三、epoll工作原理
epoll同樣只告知那些就緒的文件描述符,而且當我們調用epoll_wait()獲得就緒文件描述符時,返回的不是實際的描述符,而是一個代表就緒描述符數量的值,你只需要去epoll指定的一個數組中依次取得相應數量的文件描述符即可,這裏也使用了內存映射(mmap)技術,這樣便徹底省掉了這些文件描述符在系統調用時複製的開銷。
另一個本質的改進在於epoll採用基於事件的就緒通知方式。在select/poll中,進程只有在調用一定的方法後,內核纔對所有監視的文件描述符進行掃描,而epoll事先通過epoll_ctl()來註冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會採用類似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便得到通知。
Epoll的2種工作方式-水平觸發(LT)和邊緣觸發(ET)
假如有這樣一個例子:
1. 我們已經把一個用來從管道中讀取數據的文件句柄(RFD)添加到epoll描述符
2. 這個時候從管道的另一端被寫入了2KB的數據
3. 調用epoll_wait(2),並且它會返回RFD,說明它已經準備好讀取操作
4. 然後我們讀取了1KB的數據
5. 調用epoll_wait(2)......
EdgeTriggered工作模式:
如果我們在第1步將RFD添加到epoll描述符的時候使用了EPOLLET標誌,那麼在第5步調用epoll_wait(2)之後將有可能會掛起,因爲剩餘的數據還存在於文件的輸入緩衝區內,而且數據發出端還在等待一個針對已經發出數據的反饋信息。只有在監視的文件句柄上發生了某個事件的時候 ET 工作模式纔會彙報事件。因此在第5步的時候,調用者可能會放棄等待仍在存在於文件輸入緩衝區內的剩餘數據。在上面的例子中,會有一個事件產生在RFD句柄上,因爲在第2步執行了一個寫操作,然後,事件將會在第3步被銷燬。因爲第4步的讀取操作沒有讀空文件輸入緩衝區內的數據,因此我們在第5步調用epoll_wait(2)完成後,是否掛起是不確定的。epoll工作在ET模式的時候,必須使用非阻塞套接口,以避免由於一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。最好以下面的方式調用ET模式的epoll接口,在後面會介紹避免可能的缺陷。
i 基於非阻塞文件句柄
ii 只有當read(2)或者write(2)返回EAGAIN時才需要掛起,等待。但這並不是說每次read()時都需要循環讀,直到讀到產生一個EAGAIN才認爲此次事件處理完成,當read()返回的讀到的數據長度小於請求的數據長度時,就可以確定此時緩衝中已沒有數據了,也就可以認爲此事讀事件已處理完成。
LevelTriggered工作模式
相反的,以LT方式調用epoll接口的時候,它就相當於一個速度比較快的poll(2),並且無論後面的數據是否被使用,因此他們具有同樣的職能。因爲即使使用ET模式的epoll,在收到多個chunk的數據的時候仍然會產生多個事件。調用者可以設定EPOLLONESHOT標誌,在 epoll_wait(2)收到事件後epoll會與事件關聯的文件句柄從epoll描述符中禁止掉。因此當EPOLLONESHOT設定後,使用帶有EPOLL_CTL_MOD標誌的epoll_ctl(2)處理文件句柄就成爲調用者必須作的事情。
LT(level triggered)是epoll缺省的工作方式,並且同時支持block和no-block socket.在這種做法中,內核告訴你一個文件描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內核還是會繼續通知你的,所以,這種模式編程出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表.
ET (edge-triggered)是高速工作方式,只支持no-block socket,它效率要比LT更高。ET與LT的區別在於,當一個新的事件到來時,ET模式下當然可以從epoll_wait調用中獲取到這個事件,可是如果這次沒有把這個事件對應的套接字緩衝區處理完,在這個套接字中沒有新的事件再次到來時,在ET模式下是無法再次從epoll_wait調用中獲取這個事件的。而LT模式正好相反,只要一個事件對應的套接字緩衝區還有數據,就總能從epoll_wait中獲取這個事件。
因此,LT模式下開發基於epoll的應用要簡單些,不太容易出錯。而在ET模式下事件發生時,如果沒有徹底地將緩衝區數據處理完,則會導致緩衝區中的用戶請求得不到響應。
圖示說明:
注:Nginx默認採用ET模式來使用epoll。
epoll的優點:
1.支持一個進程打開大數目的socket描述符(FD)
select 最不能忍受的是一個進程所打開的FD是有一定限制的,由FD_SETSIZE設置,默認值是2048。對於那些需要支持的上萬連接數目的IM服務器來說顯然太少了。這時候你一是可以選擇修改這個宏然後重新編譯內核,不過資料也同時指出這樣會帶來網絡效率的下降,二是可以選擇多進程的解決方案(傳統的 Apache方案),不過雖然linux上面創建進程的代價比較小,但仍舊是不可忽視的,加上進程間數據同步遠比不上線程間同步的高效,所以也不是一種完美的方案。不過 epoll則沒有這個限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大於2048,舉個例子,在1GB內存的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統內存關係很大。
2.IO效率不隨FD數目增加而線性下降
傳統的select/poll另一個致命弱點就是當你擁有一個很大的socket集合,不過由於網絡延時,任一時間只有部分的socket是"活躍"的,但是select/poll每次調用都會線性掃描全部的集合,導致效率呈現線性下降。但是epoll不存在這個問題,它只會對"活躍"的socket進行操作---這是因爲在內核實現中epoll是根據每個fd上面的callback函數實現的。那麼,只有"活躍"的socket纔會主動的去調用 callback函數,其他idle狀態socket則不會,在這點上,epoll實現了一個"僞"AIO,因爲這時候推動力在os內核。在一些 benchmark中,如果所有的socket基本上都是活躍的---比如一個高速LAN環境,epoll並不比select/poll有什麼效率,相反,如果過多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idle connections模擬WAN環境,epoll的效率就遠在select/poll之上了。
3.使用mmap加速內核與用戶空間的消息傳遞
這點實際上涉及到epoll的具體實現了。無論是select,poll還是epoll都需要內核把FD消息通知給用戶空間,如何避免不必要的內存拷貝就很重要,在這點上,epoll是通過內核於用戶空間mmap同一塊內存實現的。而如果你想我一樣從2.5內核就關注epoll的話,一定不會忘記手工 mmap這一步的。
4.內核微調
這一點其實不算epoll的優點了,而是整個linux平臺的優點。也許你可以懷疑linux平臺,但是你無法迴避linux平臺賦予你微調內核的能力。比如,內核TCP/IP協議棧使用內存池管理sk_buff結構,那麼可以在運行時期動態調整這個內存pool(skb_head_pool)的大小--- 通過echoXXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函數的第2個參數(TCP完成3次握手的數據包隊列長度),也可以根據你平臺內存大小動態調整。更甚至在一個數據包面數目巨大但同時每個數據包本身大小卻很小的特殊系統上嘗試最新的NAPI網卡驅動架構。
linux下epoll如何實現高效處理百萬句柄的
開發高性能網絡程序時,windows開發者們言必稱iocp,linux開發者們則言必稱epoll。大家都明白epoll是一種IO多路複用技術,可以非常高效的處理數以百萬計的socket句柄,比起以前的select和poll效率高大發了。我們用起epoll來都感覺挺爽,確實快,那麼,它到底爲什麼可以高速處理這麼多併發連接呢?
使用起來很清晰,首先要調用epoll_create建立一個epoll對象。參數size是內核保證能夠正確處理的最大句柄數,多於這個最大數時內核可不保證效果。
epoll_ctl可以操作上面建立的epoll,例如,將剛建立的socket加入到epoll中讓其監控,或者把 epoll正在監控的某個socket句柄移出epoll,不再監控它等等。
epoll_wait在調用時,在給定的timeout時間內,當在監控的所有句柄中有事件發生時,就返回用戶態的進程。
從上面的調用方式就可以看到epoll比select/poll的優越之處:因爲後者每次調用時都要傳遞你所要監控的所有socket給select/poll系統調用,這意味着需要將用戶態的socket列表copy到內核態,如果以萬計的句柄會導致每次都要copy幾十幾百KB的內存到內核態,非常低效。而我們調用epoll_wait時就相當於以往調用select/poll,但是這時卻不用傳遞socket句柄給內核,因爲內核已經在epoll_ctl中拿到了要監控的句柄列表。
所以,實際上在你調用epoll_create後,內核就已經在內核態開始準備幫你存儲要監控的句柄了,每次調用epoll_ctl只是在往內核的數據結構裏塞入新的socket句柄。
當一個進程調用epoll_create方法時,Linux內核會創建一個eventpoll結構體,這個結構體中有兩個成員與epoll的使用方式密切相關:
/*
*This structure is stored inside the "private_data" member of the file
*structure and represents the main data structure for the eventpoll
*interface.
*/
structeventpoll {
/* Protect the access to thisstructure */
spinlock_t lock;
/*
* This mutex is used to ensurethat files are not removed
* while epoll is using them. Thisis held during the event
* collection loop, the filecleanup path, the epoll file * exit code and the ctl operations.
*/
struct mutex mtx;
/* Wait queue used bysys_epoll_wait() */
wait_queue_head_t wq;
/* Wait queue used byfile->poll() */
wait_queue_head_t poll_wait;
/* List of ready file descriptors*/
struct list_head rdllist;
/* RB tree root used to storemonitored fd structs */
/*紅黑樹根節點,這棵樹存儲着所有添加到epoll中的事件,也就是這個epoll監控的事件 */
struct rb_root rbr;
/*
* This is a single linked listthat chains all the "struct epitem" that
* happened while transferringready events to userspace w/out
* holding ->lock.
*/
struct epitem*ovflist;
/* wakeup_source used whenep_scan_ready_list is running */
struct wakeup_source*ws;
/* The user that created theeventpoll descriptor */
struct user_struct*user;
struct file*file;
/* used to optimize loopdetection check */
int visited;
/*雙向鏈表中保存着將要通過epoll_wait返回給用戶的、滿足條件的事件 */
struct list_head visited_list_link;
};
每一個epoll對象都有一個獨立的eventpoll結構體,這個結構體會在內核空間中創造獨立的內存,用於存儲使用epoll_ctl方法向epoll對象中添加進來的事件。這樣,重複的事件就可以通過紅黑樹而高效的識別出來。
在epoll中,對於每一個事件都會建立一個epitem結構體:
/*
* Each file descriptor added to the eventpoll interfacewill
* have an entry of this type linked to the"rbr" RB tree.
* Avoid increasing the size of this struct, there can bemany thousands
* of these on a server and we do not want this to takeanother cache line.
*/
struct epitem {
/* RB tree node used to link this structure to theeventpoll RB tree */
struct rb_node rbn;
/* List header used to link this structure to the eventpollready list */
struct list_head rdllink;
/*
* Workstogether "struct eventpoll"->ovflist in keeping the
* singlelinked chain of items.
*/
struct epitem*next;
/* The file descriptor information this item refers to */
struct epoll_filefd ffd;
/* Number of active wait queue attached to poll operations*/
int nwait;
/* List containing poll wait queues */
struct list_head pwqlist;
/* The "container" of this item */
struct eventpoll*ep;
/* List header used to link this item to the "structfile" items list */
struct list_head fllink;
/* wakeup_source used when EPOLLWAKEUP is set */
struct wakeup_source __rcu*ws;
/* The structure that describe the interested events andthe source fd */
struct epoll_event event;
};
此外,epoll還維護了一個雙鏈表,用戶存儲發生的事件。當epoll_wait調用時,僅僅觀察這個list鏈表裏有沒有數據即eptime項即可。有數據就返回,沒有數據就sleep,等到timeout時間到後即使鏈表沒數據也返回。所以,epoll_wait非常高效。
而且,通常情況下即使我們要監控百萬計的句柄,大多一次也只返回很少量的準備就緒句柄而已,所以,epoll_wait僅需要從內核態copy少量的句柄到用戶態而已,如何能不高效?!
那麼,這個準備就緒list鏈表是怎麼維護的呢?當我們執行epoll_ctl時,除了把socket放到epoll文件系統裏file對象對應的紅黑樹上之外,還會給內核中斷處理程序註冊一個回調函數,告訴內核,如果這個句柄的中斷到了,就把它放到準備就緒list鏈表裏。所以,當一個socket上有數據到了,內核在把網卡上的數據copy到內核中後就來把socket插入到準備就緒鏈表裏了。
如此,一顆紅黑樹,一張準備就緒句柄鏈表,少量的內核cache,就幫我們解決了大併發下的socket處理問題。執行epoll_create時,創建了紅黑樹和就緒鏈表,執行epoll_ctl時,如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹幹上,然後向內核註冊回調函數,用於當中斷事件來臨時向準備就緒鏈表中插入數據。執行epoll_wait時立刻返回準備就緒鏈表裏的數據即可。
四、epoll的使用方法
那麼究竟如何來使用epoll呢?其實非常簡單。
通過在包含一個頭文件#include <sys/epoll.h> 以及幾個簡單的API將可以大大的提高你的網絡服務器的支持人數。
首先通過create_epoll(intmaxfds)來創建一個epoll的句柄。這個函數會返回一個新的epoll句柄,之後的所有操作將通過這個句柄來進行操作。在用完之後,記得用close()來關閉這個創建出來的epoll句柄。
之後在你的網絡主循環裏面,每一幀的調用epoll_wait(int epfd, epoll_event events, int max events, inttimeout)來查詢所有的網絡接口,看哪一個可以讀,哪一個可以寫了。基本的語法爲:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd爲用epoll_create創建之後的句柄,events是一個epoll_event*的指針,當epoll_wait這個函數操作成功之後,epoll_events裏面將儲存所有的讀寫事件。max_events是當前需要監聽的所有socket句柄數。最後一個timeout是 epoll_wait的超時,爲0的時候表示馬上返回,爲-1的時候表示一直等下去,直到有事件返回,爲任意正整數的時候表示等這麼長的時間,如果一直沒有事件,則返回。一般如果網絡主循環是單獨的線程的話,可以用-1來等,這樣可以保證一些效率,如果是和主邏輯在同一個線程的話,則可以用0來保證主循環的效率。
epoll_wait返回之後應該是一個循環,遍歷所有的事件。
幾乎所有的epoll程序都使用下面的框架:
for( ; ; )
{
nfds = epoll_wait(epfd,events,20,500);
for(i=0;i<nfds;++i)
{
if(events[i].data.fd==listenfd)//有新的連接
{
connfd = accept(listenfd,(sockaddr*)&clientaddr,&clilen);//accept這個連接
ev.data.fd=connfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);//將新的fd添加到epoll的監聽隊列中
}
else if( events[i].events&EPOLLIN )//接收到數據,讀socket
{
n = read(sockfd, line, MAXLINE))<0 //讀
ev.data.ptr = md; //md爲自定義類型,添加數據
ev.events=EPOLLOUT|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改標識符,等待下一個循環時發送數據,異步處理的精髓
}
else if(events[i].events&EPOLLOUT)//有數據待發送,寫socket
{
struct myepoll_data* md= (myepoll_data*)events[i].data.ptr; //取數據
sockfd = md->fd;
send( sockfd, md->ptr, strlen((char*)md->ptr),0 ); //發送數據
ev.data.fd=sockfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改標識符,等待下一個循環時接收數據
}
else
{
//其他的處理
}
}
}
五、epoll的程序實例
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<sys/socket.h>
#include<netdb.h>
#include<fcntl.h>
#include<sys/epoll.h>
#include<string.h>
#defineMAXEVENTS 64
//函數:
//功能:創建和綁定一個TCP socket
//參數:端口
//返回值:創建的socket
staticint
create_and_bind(char*port)
{
struct addrinfo hints;
struct addrinfo*result,*rp;
int s, sfd;
memset(&hints,0,sizeof(struct addrinfo));
hints.ai_family=AF_UNSPEC; /* Return IPv4 and IPv6 choices */
hints.ai_socktype=SOCK_STREAM;/* We want a TCP socket */
hints.ai_flags=AI_PASSIVE; /* All interfaces */
s= getaddrinfo(NULL, port,&hints,&result);
if(s!=0)
{
fprintf(stderr,"getaddrinfo: %s\n", gai_strerror(s));
return-1;
}
for(rp= result; rp!= NULL; rp= rp->ai_next)
{
sfd= socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if(sfd==-1)
continue;
s= bind(sfd, rp->ai_addr, rp->ai_addrlen);
if(s==0)
{
/* We managed to bind successfully! */
break;
}
close(sfd);
}
if(rp== NULL)
{
fprintf(stderr,"Could not bind\n");
return-1;
}
freeaddrinfo(result);
return sfd;
}
//函數
//功能:設置socket爲非阻塞的
staticint
make_socket_non_blocking(int sfd)
{
int flags, s;
//得到文件狀態標誌
flags= fcntl(sfd, F_GETFL,0);
if(flags==-1)
{
perror("fcntl");
return-1;
}
//設置文件狀態標誌
flags|= O_NONBLOCK;
s= fcntl(sfd, F_SETFL, flags);
if(s==-1)
{
perror("fcntl");
return-1;
}
return0;
}
//端口由參數argv[1]指定
int
main (int argc,char*argv[])
{
int sfd, s;
int efd;
struct epoll_eventevent;
struct epoll_event*events;
if(argc!=2)
{
fprintf(stderr,"Usage: %s [port]\n", argv[0]);
exit(EXIT_FAILURE);
}
sfd= create_and_bind(argv[1]);
if(sfd==-1)
abort();
s= make_socket_non_blocking(sfd);
if(s==-1)
abort();
s= listen(sfd, SOMAXCONN);
if(s==-1)
{
perror("listen");
abort();
}
//除了參數size被忽略外,此函數和epoll_create完全相同
efd= epoll_create1(0);
if(efd==-1)
{
perror("epoll_create");
abort();
}
event.data.fd= sfd;
event.events= EPOLLIN | EPOLLET;//讀入,邊緣觸發方式
s= epoll_ctl(efd, EPOLL_CTL_ADD, sfd,&event);
if(s==-1)
{
perror("epoll_ctl");
abort();
}
/* Buffer where events are returned */
events= calloc(MAXEVENTS,sizeofevent);
/* The event loop */
while(1)
{
int n, i;
n= epoll_wait(efd, events,MAXEVENTS,-1);
for(i=0; i<n; i++)
{
if((events[i].events& EPOLLERR)||
(events[i].events& EPOLLHUP)||
(!(events[i].events& EPOLLIN)))
{
/* An error has occured on this fd, or the socketis not
readyfor reading (why were we notified then?) */
fprintf(stderr,"epoll error\n");
close(events[i].data.fd);
continue;
}
elseif(sfd== events[i].data.fd)
{
/* We have a notification on the listening socket,which
meansone or more incoming connections. */
while(1)
{
struct sockaddr in_addr;
socklen_t in_len;
int infd;
char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
in_len=sizeofin_addr;
infd= accept(sfd,&in_addr,&in_len);
if(infd==-1)
{
if((errno== EAGAIN)||
(errno== EWOULDBLOCK))
{
/* We have processed all incoming
connections.*/
break;
}
else
{
perror("accept");
break;
}
}
//將地址轉化爲主機名或者服務名
s= getnameinfo(&in_addr, in_len,
hbuf,sizeofhbuf,
sbuf,sizeofsbuf,
NI_NUMERICHOST| NI_NUMERICSERV);//flag參數:以數字名返回
//主機地址和服務地址
if(s==0)
{
printf("Accepted connection on descriptor %d "
"(host=%s,port=%s)\n", infd, hbuf,sbuf);
}
/* Make the incoming socket non-blocking and addit to the
listof fds to monitor. */
s= make_socket_non_blocking(infd);
if(s==-1)
abort();
event.data.fd=infd;
event.events= EPOLLIN | EPOLLET;
s= epoll_ctl(efd, EPOLL_CTL_ADD,infd,&event);
if(s==-1)
{
perror("epoll_ctl");
abort();
}
}
continue;
}
else
{
/* We have data on the fd waiting to be read. Readand
displayit. We must read whatever data is available
completely,as we are running in edge-triggered mode
andwon't get a notification again for the same
data.*/
intdone=0;
while(1)
{
ssize_t count;
char buf[512];
count= read(events[i].data.fd, buf,sizeof(buf));
if(count==-1)
{
/* If errno == EAGAIN, that means we have read all
data.So go back to the main loop. */
if(errno!= EAGAIN)
{
perror("read");
done=1;
}
break;
}
elseif(count==0)
{
/* End of file. The remote has closed the
connection.*/
done=1;
break;
}
/* Write the buffer to standard output */
s= write(1, buf, count);
if(s==-1)
{
perror("write");
abort();
}
}
if(done)
{
printf("Closed connection on descriptor %d\n",
events[i].data.fd);
/* Closing the descriptor will make epoll removeit
fromthe set of descriptors which are monitored. */
close(events[i].data.fd);
}
}
}
}
free(events);
close(sfd);
return EXIT_SUCCESS;
}