微信公眾號【黃小斜】作者是螞蟻金服 JAVA 工程師,目前在螞蟻財富負責后端開發工作,專注于 JAVA 后端技術棧,同時也懂點投資理財,堅持學習和寫作,用大廠程序員的視角解讀技術與互聯網,我的世界里不只有 coding!關注公眾號后回復”架構師“即可領取 Java基礎、進階、項目和架構師等免費學習資料,更有數據庫、分布式、微服務等熱門技術學習視頻,內容豐富,兼顧原理和實踐,另外也將贈送作者原創的Java學習指南、Java程序員面試指南等干貨資源
Linux epoll實現原理詳解
在linux 沒有實現epoll事件驅動機制之前,我們一般選擇用select或者poll等IO多路復用的方法來實現并發服務程序。在大數據、高并發、集群等一些名詞唱得火熱之年代,select和poll的用武之地越來越有限,風頭已經被epoll占盡。
本文便來介紹epoll的實現機制,并附帶講解一下select和poll。通過對比其不同的實現機制,真正理解為何epoll能實現高并發。
這部分轉自https://jeff.wtf/2017/02/IO-multiplexing/
為什么要 I/O 多路復用
當需要從一個叫 r_fd
的描述符不停地讀取數據,并把讀到的數據寫入一個叫 w_fd
的描述符時,我們可以用循環使用阻塞 I/O :
<pre>123</pre> | <pre>while((n = read(r_fd, buf, BUF_SIZE)) > 0) if(write(w_fd, buf, n) != n) err_sys("write error");</pre> |
---|
但是,如果要從兩個地方讀取數據呢?這時,不能再使用會把程序阻塞住的 read
函數。因為可能在阻塞地等待 r_fd1
的數據時,來不及處理 r_fd2
,已經到達的 r_fd2
的數據可能會丟失掉。
這個情況下需要使用非阻塞 I/O。
只要做個標記,把文件描述符標記為非阻塞的,以后再對它使用 read
函數:如果它還沒有數據可讀,函數會立即返回并把 errorno 這個變量的值設置為 35,于是我們知道它沒有數據可讀,然后可以立馬去對其他描述符使用 read
;如果它有數據可讀,我們就讀取它數據。對所有要讀的描述符都調用了一遍 read
之后,我們可以等一個較長的時間(比如幾秒),然后再從第一個文件描述符開始調用 read
。這種循環就叫做輪詢(polling)。
這樣,不會像使用阻塞 I/O 時那樣因為一個描述符 read
長時間處于等待數據而使程序阻塞。
輪詢的缺點是浪費太多 CPU 時間。大多數時候我們沒有數據可讀,但是還是用了 read
這個系統調用,使用系統調用時會從用戶態切換到內核態。而大多數情況下我們調用 read
,然后陷入內核態,內核發現這個描述符沒有準備好,然后切換回用戶態并且只得到 EAGAIN (errorno 被設置為 35),做的是無用功。描述符非常多的時候,每次的切換過程就是巨大的浪費。
所以,需要 I/O 多路復用。I/O 多路復用通過使用一個系統函數,同時等待多個描述符的可讀、可寫狀態。
為了達到這個目的,我們需要做的是:建立一個描述符列表,以及我們分別關心它們的什么事件(可讀還是可寫還是發生例外情況);調用一個系統函數,直到這個描述符列表里有至少一個描述符關聯的事件發生時,這個函數才會返回。
select, poll, epoll 就是這樣的系統函數。
select,poll,epoll 源碼分析
select
我們可以在所有 POSIX 兼容的系統里使用 select 函數來進行 I/O 多路復用。我們需要通過 select 函數的參數傳遞給內核的信息有:
- 我們關心哪些描述符
- 我們關心它們的什么事件
- 我們希望等待多長時間
select 的返回時,內核會告訴我們:
- 可讀的描述符的個數
- 哪些描述符發生了哪些事件
<pre>123456</pre> | <pre>#include <sys/select.h>int select(int maxfdp1, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);// 返回值: 已就緒的描述符的個數。超時時為 0 ,錯誤時為 -1</pre> |
---|
maxfdp1
意思是 “max file descriptor plus 1” ,就是把你要監視的所有文件描述符里最大的那個加上 1 。(它實際上決定了內核要遍歷文件描述符的次數,比如你監視了文件描述符 5 和 20 并把 maxfdp1
設置為 21 ,內核每次都會從描述符 0 依次檢查到 20。)
中間的三個參數是你想監視的文件描述符的集合??梢园?fd_set 類型視為 1024 位的二進制數,這意味著 select 只能監視小于 1024 的文件描述符(1024 是由 Linux 的 sys/select.h 里 FD_SETSIZE
宏設置的值)。在 select 返回后我們通過 FD_ISSET
來判斷代表該位的描述符是否是已準備好的狀態。
最后一個參數是等待超時的時長:到達這個時長但是沒有任一描述符可用時,函數會返回 0 。
用一個代碼片段來展示 select 的用法:
<pre>12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455</pre> | <pre>// 這個例子要監控文件描述符 3, 4 的可讀狀態,以及 4, 5 的可寫狀態// 初始化兩個 fd_set 以及 timevalfd_set read_set, write_set;FD_ZERO(read_set);FD_ZERO(write_set);timeval t;t.tv_sec = 5; // 超時為 5 秒t.tv_usec = 0; // 加 0 微秒// 設置好兩個 fd_setint fd1 = 3;int fd2 = 4;int fd3 = 5;int maxfdp1 = 5 + 1;FD_SET(fd1, &read_set);FD_SET(fd2, &read_set);FD_SET(fd2, &write_set);FD_SET(fd3, &write_set);// 準備備用的 fd_setfd_set r_temp = read_set;fd_set w_temp = write_set;while(true){ // 每次都要重新設置放入 select 的 fd_set read_set = r_temp; write_set = w_temp; // 使用 select int n = select(maxfdp1, &read_set, &write_set, NULL, &t); // 上面的 select 函數會一直阻塞,直到 // 3, 4 可讀以及 4, 5 可寫這四件事中至少一項發生 // 或者等待時間到達 5 秒,返回 0 for(int i=0; i<maxfdp1 && n>0; i++){ if(FD_ISSET(i, &read_set)){ n--; if(i==fd1) prinf("描述符 3 可讀"); if(i==fd2) prinf("描述符 4 可讀"); } if(FD_ISSET(i, &write_set)){ n--; if(i==fd2) prinf("描述符 3 可寫"); if(i==fd3) prinf("描述符 4 可寫"); } } // 上面的 printf 語句換成對應的 read 或者 write 函數就 // 可以立即讀取或者寫入相應的描述符而不用等待}</pre> |
---|
可以看到,select 的缺點有:
- 默認能監視的文件描述符不能大于 1024,也代表監視的總數不超過1024。即使你因為需要監視的描述符大于 1024 而改動內核的
FD_SETSIZE
值,但由于 select 是每次都會線性掃描整個fd_set,集合越大速度越慢,所以性能會比較差。 - select 函數返回時只能看見已準備好的描述符數量,至于是哪個描述符準備好了需要循環用
FD_ISSET
來檢查,當未準備好的描述符很多而準備好的很少時,效率比較低。 - select 函數每次執行的時候,都把參數里傳入的三個 fd_set 從用戶空間復制到內核空間。而每次 fd_set 里要監視的描述符變化不大時,全部重新復制一遍并不劃算。同樣在每次都是未準備好的描述符很多而準備好的很少時,調用 select 會很頻繁,用戶/內核間的的數據復制就成了一個大的開銷。
還有一個問題是在代碼的寫法上給我一些困擾的,就是每次調用 select 前必須重新設置三個 fd_set。 fd_set 類型只是 1024 位的二進制數(實際上結構體里是幾個 long 變量的數組;比如 64 位機器上 long 是 64 bit,那么 fd_set 里就是 16 個 long 變量的數組),由一位的 1 和 0 代表一個文件描述符的狀態,但是其實調用 select 前后位的 1/0 狀態意義是不一樣的。
先講一下幾個對 fd_set 操作的函數的作用:FD_ZERO
把 fd_set 所有位設置為 0 ;FD_SET
把一個位設置為 1 ;FD_ISSET
判斷一個位是否為 1 。
調用 select 前:我們用 FD_ZERO
把 fd_set 先全部初始化,然后用 FD_SET
把我們關心的代表描述符的位設置為 1 。我們這時可以用 FD_ISSET
判斷這個位是否被我們設置,這時的含義是我們想要監視的描述符是否被設置為被監視的狀態。
調用 select 時:內核判斷 fd_set 里的位并把各個 fd_set 里所有值為 1 的位記錄下來,然后把 fd_set 全部設置成 0 ;一個描述符上有對應的事件發生時,把對應 fd_set 里代表這個描述符的位設置為 1 。
在 select 返回之后:我們同樣用 FD_ISSET
判斷各個我們關心的位是 0 還是 1 ,這時的含義是,這個位是否是發生了我們關心的事件。
所以,在下一次調用 select 前,我們不得不把已經被內核改掉的 fd_set 全部重新設置一下。
select 在監視大量描述符尤其是更多的描述符未準備好的情況時性能很差?!禪nix 高級編程》里寫,用 select 的程序通常只使用 3 到 10 個描述符。
poll
poll 和 select 是相似的,只是給的接口不同。
<pre>1234</pre> | <pre>#include <poll.h>int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);// 返回值: 已就緒的描述符的個數。超時時為 0 ,錯誤時為 -1</pre> |
---|
fdarray
是 pollfd
的數組。pollfd
結構體是這樣的:
<pre>12345</pre> | <pre>struct pollfd { int fd; // 文件描述符 short events; // 我期待的事件 short revents; // 實際發生的事件:我期待的事件中發生的;或者異常情況};</pre> |
---|
nfds
是 fdarray
的長度,也就是 pollfd 的個數。
timeout
代表等待超時的毫秒數。
相比 select ,poll 有這些優點:由于 poll 在 pollfd 里用 int fd
來表示文件描述符而不像 select 里用的 fd_set 來分別表示描述符,所以沒有必須小于 1024 的限制,也沒有數量限制;由于 poll 用 events
表示期待的事件,通過修改 revents
來表示發生的事件,所以不需要像 select 在每次調用前重新設置描述符和期待的事件。
除此之外,poll 和 select 幾乎相同。在 poll 返回后,需要遍歷 fdarray
來檢查各個 pollfd
里的 revents
是否發生了期待的事件;每次調用 poll 時,把 fdarray
復制到內核空間。在描述符太多而每次準備好的較少時,poll 有同樣的性能問題。
epoll
epoll 是在 Linux 2.5.44 中首度登場的。不像 select 和 poll ,它提供了三個系統函數而不是一個。
epoll_create 用來創建一個 epoll 描述符:
<pre>1234</pre> | <pre>#include <sys/epoll.h>int epoll_create(int size);// 返回值:epoll 描述符</pre> |
---|
size
用來告訴內核你想監視的文件描述符的數目,但是它并不是限制了能監視的描述符的最大個數,而是給內核最初分配的空間一個建議。然后系統會在內核中分配一個空間來存放事件表,并返回一個 epoll 描述符,用來操作這個事件表。
epoll_ctl 用來增/刪/改內核中的事件表:
<pre>123</pre> | <pre>int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);// 返回值:成功時返回 0 ,失敗時返回 -1</pre> |
---|
epfd
是 epoll 描述符。
op
是操作類型(增加/刪除/修改)。
fd
是希望監視的文件描述符。
event
是一個 epoll_event 結構體的指針。epoll_event 的定義是這樣的:
<pre>1234567891011</pre> | <pre>typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64;} epoll_data_t;struct epoll_event { uint32_t events; // 我期待的事件 epoll_data_t data; // 用戶數據變量};</pre> |
---|
這個結構體里,除了期待的事件外,還有一個 data
,是一個 union,它是用來讓我們在得到下面第三個函數的返回值以后方便的定位文件描述符的。
epoll_wait 用來等待事件
<pre>1234</pre> | <pre>int epoll_wait(int epfd, struct epoll_event *result_events, int maxevents, int timeout);// 返回值:已就緒的描述符個數。超時時為 0 ,錯誤時為 -1</pre> |
---|
epfd
是 epoll 描述符。
result_events
是 epoll_event 結構體的指針,它將指向的是所有已經準備好的事件描述符相關聯的 epoll_event(在上個步驟里調用 epoll_ctl 時關聯起來的)。下面的例子可以讓你知道這個參數的意義。
maxevents
是返回的最大事件個數,也就是你能通過 result_events 指針遍歷到的最大的次數。
timeout
是等待超時的毫秒數。
用一個代碼片段來展示 epoll 的用法:
<pre>123456789101112131415161718192021222324252627282930313233343536373839404142434445</pre> | <pre>// 這個例子要監控文件描述符 3, 4 的可讀狀態,以及 4, 5 的可寫狀態/* 通過 epoll_create 創建 epoll 描述符 /int epfd = epoll_create(4);int fd1 = 3;int fd2 = 4;int fd3 = 5;/ 通過 epoll_ctl 注冊好四個事件 /struct epoll_event ev1;ev1.events = EPOLLIN; // 期待它的可讀事件發生ev1.data = fd1; // 我們通常就把 data 設置為 fd ,方便以后查看epoll_ctl(epfd, EPOLL_CTL_ADD, fd1, &ev1); // 添加到事件表struct epoll_event ev2;ev2.events = EPOLLIN;ev2.data = fd2;epoll_ctl(epfd, EPOLL_CTL_ADD, fd2, &ev2);struct epoll_event ev3;ev3.events = EPOLLOUT; // 期待它的可寫事件發生ev3.data = fd2;epoll_ctl(epfd, EPOLL_CTL_ADD, fd2, &ev3);struct epoll_event ev4;ev4.events = EPOLLOUT;ev4.data = fd3;epoll_ctl(epfd, EPOLL_CTL_ADD, fd3, &ev4);/ 通過 epoll_wait 等待事件 */# DEFINE MAXEVENTS 4struct epoll_event result_events[MAXEVENTS];while(true){ int n = epoll_wait(epfd, &result_events, MAXEVENTS, 5000); for(int i=0; i<n; n--){ // result_events[i] 一定是 ev1 到 ev4 中的一個 if(result_events[i].events&EPOLLIN) printf("描述符 %d 可讀", result_events[i].fd); else if(result_events[i].events&EPOLLOUT) printf("描述符 %d 可寫", result_events[i].fd) }}</pre> |
---|
所以 epoll 解決了 poll 和 select 的問題:
只在 epoll_ctl 的時候把數據復制到內核空間,這保證了每個描述符和事件一定只會被復制到內核空間一次;每次調用 epoll_wait 都不會復制新數據到內核空間。相比之下,select 每次調用都會把三個 fd_set 復制一遍;poll 每次調用都會把
fdarray
復制一遍。epoll_wait 返回 n ,那么只需要做 n 次循環,可以保證遍歷的每一次都是有意義的。相比之下,select 需要做至少 n 次至多
maxfdp1
次循環;poll 需要遍歷完 fdarray 即做nfds
次循環。在內部實現上,epoll 使用了回調的方法。調用 epoll_ctl 時,就是注冊了一個事件:在集合中放入文件描述符以及事件數據,并且加上一個回調函數。一旦文件描述符上的對應事件發生,就會調用回調函數,這個函數會把這個文件描述符加入到就緒隊列上。當你調用 epoll_wait 時,它只是在查看就緒隊列上是否有內容,有的話就返回給你的程序。
select()
poll()``epoll_wait()
三個函數在操作系統看來,都是睡眠一會兒然后判斷一會兒的循環,但是 select 和 poll 在醒著的時候要遍歷整個文件描述符集合,而 epoll_wait 只是看看就緒隊列是否為空而已。這是 epoll 高性能的理由,使得其 I/O 的效率不會像使用輪詢的 select/poll 隨著描述符增加而大大降低。
注 1 :select/poll/epoll_wait 三個函數的等待超時時間都有一樣的特性:等待時間設置為 0 時函數不阻塞而是立即返回,不論是否有文件描述符已準備好;poll/epoll_wait 中的 timeout 為 -1,select 中的 timeout 為 NULL 時,則無限等待,直到有描述符已準備好才會返回。
注 2 :有的新手會把文件描述符是否標記為阻塞 I/O 等同于 I/O 多路復用函數是否阻塞。其實文件描述符是否標記為阻塞,決定了你
read
或write
它時如果它未準備好是阻塞等待,還是立即返回 EAGAIN ;而 I/O 多路復用函數除非你把 timeout 設置為 0 ,否則它總是會阻塞住你的程序。注 3 :上面的例子只是入門,可能是不準確或不全面的:一是數據要立即處理防止丟失;二是 EPOLLIN/EPOLLOUT 不完全等同于可讀可寫事件,具體要去搜索 poll/epoll 的事件具體有哪些;三是大多數實際例子里,比如一個 tcp server ,都會在運行中不斷增加/刪除的文件描述符而不是記住固定的 3 4 5 幾個描述符(用這種例子更能看出 epoll 的優勢);四是 epoll 的優勢更多的體現在處理大量閑連接的情況,如果場景是處理少量短連接,用 select 反而更好,而且用 select 的代碼能運行在所有平臺上。
Epoll數據結構:
select()和poll() IO多路復用模型
select的缺點:
- 單個進程能夠監視的文件描述符的數量存在最大限制,通常是1024,當然可以更改數量,但由于select采用輪詢的方式掃描文件描述符,文件描述符數量越多,性能越差;(在linux內核頭文件中,有這樣的定義:#define __FD_SETSIZE 1024)
- 內核 / 用戶空間內存拷貝問題,select需要復制大量的句柄數據結構,產生巨大的開銷;
- select返回的是含有整個句柄的數組,應用程序需要遍歷整個數組才能發現哪些句柄發生了事件;
- select的觸發方式是水平觸發,應用程序如果沒有完成對一個已經就緒的文件描述符進行IO操作,那么之后每次select調用還是會將這些文件描述符通知進程。
相比select模型,poll使用鏈表保存文件描述符,因此沒有了監視文件數量的限制,但其他三個缺點依然存在。
拿select模型為例,假設我們的服務器需要支持100萬的并發連接,則在__FD_SETSIZE 為1024的情況下,則我們至少需要開辟1k個進程才能實現100萬的并發連接。除了進程間上下文切換的時間消耗外,從內核/用戶空間大量的無腦內存拷貝、數組輪詢等,是系統難以承受的。因此,基于select模型的服務器程序,要達到10萬級別的并發訪問,是一個很難完成的任務。
因此,該epoll上場了。
epoll IO多路復用模型實現機制
由于epoll的實現機制與select/poll機制完全不同,上面所說的 select的缺點在epoll上不復存在。
設想一下如下場景:有100萬個客戶端同時與一個服務器進程保持著TCP連接。而每一時刻,通常只有幾百上千個TCP連接是活躍的(事實上大部分場景都是這種情況)。如何實現這樣的高并發?
在select/poll時代,服務器進程每次都把這100萬個連接告訴操作系統(從用戶態復制句柄數據結構到內核態),讓操作系統內核去查詢這些套接字上是否有事件發生,輪詢完后,再將句柄數據復制到用戶態,讓服務器應用程序輪詢處理已發生的網絡事件,這一過程資源消耗較大,因此,select/poll一般只能處理幾千的并發連接。
epoll的設計和實現與select完全不同。epoll通過在Linux內核中申請一個簡易的文件系統(文件系統一般用什么數據結構實現?B+樹)。把原先的select/poll調用分成了3個部分:
1)調用epoll_create()建立一個epoll對象(在epoll文件系統中為這個句柄對象分配資源)
2)調用epoll_ctl向epoll對象中添加這100萬個連接的套接字
3)調用epoll_wait收集發生的事件的連接
如此一來,要實現上面說是的場景,只需要在進程啟動時建立一個epoll對象,然后在需要的時候向這個epoll對象中添加或者刪除連接。同時,epoll_wait的效率也非常高,因為調用epoll_wait時,并沒有一股腦的向操作系統復制這100萬個連接的句柄數據,內核也不需要去遍歷全部的連接。
下面來看看Linux內核具體的epoll機制實現思路。
當某一進程調用epoll_create方法時,Linux內核會創建一個eventpoll結構體,這個結構體中有兩個成員與epoll的使用方式密切相關。eventpoll結構體如下所示:
[cpp] view plain copy
- struct eventpoll{
....
/*紅黑樹的根節點,這顆樹中存儲著所有添加到epoll中的需要監控的事件*/
struct rb_root rbr;
/*雙鏈表中則存放著將要通過epoll_wait返回給用戶的滿足條件的事件*/
struct list_head rdlist;
....
- };
每一個epoll對象都有一個獨立的eventpoll結構體,用于存放通過epoll_ctl方法向epoll對象中添加進來的事件。這些事件都會掛載在紅黑樹中,如此,重復添加的事件就可以通過紅黑樹而高效的識別出來(紅黑樹的插入時間效率是lgn,其中n為樹的高度)。
而所有添加到epoll中的事件都會與設備(網卡)驅動程序建立回調關系,也就是說,當相應的事件發生時會調用這個回調方法。這個回調方法在內核中叫ep_poll_callback,它會將發生的事件添加到rdlist雙鏈表中。
在epoll中,對于每一個事件,都會建立一個epitem結構體,如下所示:
[cpp] view plain copy
- struct epitem{
struct rb_node rbn;//紅黑樹節點
struct list_head rdllink;//雙向鏈表節點
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所屬的eventpoll對象
struct epoll_event event; //期待發生的事件類型
- }
當調用epoll_wait檢查是否有事件發生時,只需要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素即可。如果rdlist不為空,則把發生的事件復制到用戶態,同時將事件數量返回給用戶。
epoll數據結構示意圖
從上面的講解可知:通過紅黑樹和雙鏈表數據結構,并結合回調機制,造就了epoll的高效。
OK,講解完了Epoll的機理,我們便能很容易掌握epoll的用法了。一句話描述就是:三步曲。
第一步:epoll_create()系統調用。此調用返回一個句柄,之后所有的使用都依靠這個句柄來標識。
第二步:epoll_ctl()系統調用。通過此調用向epoll對象中添加、刪除、修改感興趣的事件,返回0標識成功,返回-1表示失敗。
第三部:epoll_wait()系統調用。通過此調用收集收集在epoll監控中已經發生的事件。
epoll實例
最后,附上一個epoll編程實例。
幾乎所有的epoll程序都使用下面的框架:
[cpp] view plaincopyprint?
- for( ; ; )
{
nfds = epoll_wait(epfd,events,20,500);
for(i=0;i<nfds;++i) < span="" >
{
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的程序實例
[cpp] view plaincopyprint?
-
include
-
include
-
include
-
include
-
include
-
include
-
include
-
include
-
include
-
define MAXEVENTS 64
- //函數:
- //功能:創建和綁定一個TCP socket
- //參數:端口
- //返回值:創建的socket
- static int
- 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為非阻塞的
- static int
- 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;
}
- return 0;
- }
- //端口由參數argv[1]指定
- int
- main (int argc, char *argv[])
- {
- int sfd, s;
- int efd;
- struct epoll_event event;
- 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, sizeof event);
- /* 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 socket is not
ready for reading (why were we notified then?) */
fprintf (stderr, "epoll error\n");
close (events[i].data.fd);
continue;
}
else if (sfd == events[i].data.fd)
{
/* We have a notification on the listening socket, which
means one 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 = sizeof in_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, sizeof hbuf,
sbuf, sizeof sbuf,
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 add it to the
list of 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. Read and
display it. We must read whatever data is available
completely, as we are running in edge-triggered mode
and won't get a notification again for the same
data. */
int done = 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;
}
else if (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 remove it
from the set of descriptors which are monitored. */
close (events[i].data.fd);
}
}
}
}
- free (events);
- close (sfd);
- return EXIT_SUCCESS;
- }
微信公眾號【Java技術江湖】一位阿里 Java 工程師的技術小站。(關注公眾號后回復”Java“即可領取 Java基礎、進階、項目和架構師等免費學習資料,更有數據庫、分布式、微服務等熱門技術學習視頻,內容豐富,兼顧原理和實踐,另外也將贈送作者原創的Java學習指南、Java程序員面試指南等干貨資源)