天天看點

高并發伺服器開發與配置

一、4大具有代表性的并發模型及其優缺點

        4大具有代表性的并發模型:Apache模型(Process Per Connection,簡稱PPC),TPC(Thread PerConnection)模型,select模型和poll模型、Epoll模型。

        Apache(PPC)模型和TPC模型是最容易了解的,Apache模型在并發上是通過多程序實作的,而TPC模型是通過多線程實作的,但是這2種方式在大量程序/線程切換時會造成大量的開銷。

        select模型是通過一種輪詢機制來實作的。需要注意select模型有3大不足:

a.Socket檔案數量限制:該模式可操作的Socket數由FD_SETSIZE決定,核心預設32*32=1024. 

b.操作限制:通過周遊FD_SETSIZE(1024)個Socket來完成排程,不管哪個Socket是活躍的,都周遊一遍,這樣效率就會随檔案數量的增多呈現線性下降,把FD_SETSIZE改大的後果就是周遊需要更久的時間。管理檔案數量和管理效率成反比。

c.記憶體複制限制:核心/使用者空間的資訊交換是通過記憶體拷貝來完成的,這樣在高并發情況下就會存在大量的資料拷貝,浪費時間。

        poll模型與select類似,也是通過輪詢來實作,但它與select模型的差別在于Socket數量沒有限制,是以poll模型有2大不足:操作限制和記憶體複制限制。

        Epoll模型改進了poll和sellect模型。Epoll沒有檔案數量限制,上限是目前使用者單個程序最大能打開的檔案數;使用事件驅動,不使用輪詢,而使用基于核心提供的反射模式。有“活躍Socket”時,核心通路該Socket的callback,直接傳回産生事件的檔案句柄;核心/使用者空間資訊交換通過共享記憶體mmap實作,避免了資料複制。

        目前市場用得比較多的就是Apache、Nginx、Lighttpd. Apache的占有率是最高是毋庸置疑的,但它主要是采用select模式開發。目前主流的異步web伺服器Lighttpd和Nginx都是基于Epoll的。它們具有非常好的架構,可以運作在簡單的web叢集中。但在資料結構、記憶體管理都多個細節方面處理nginx考慮更加完善。nginx從event、跨平台、基礎資料結構都很多細節方面進行了考慮和優化。nginx必定是未來的apache,未來的主流。

二、主機環境對高并發應用程式的天然限制

        高并發的應用程式至少需要考慮3大限制條件:使用者程序的預設記憶體空間為4G,線程棧預設為8M,使用者程序最大能管理的檔案描述符預設為1024個,網卡對用戶端端口号數量的限制。

        Linux下高并發socket伺服器端和用戶端最大連接配接數所受的限制問題(修改軟限制和硬限制)

1、配置使用者程序可打開的最多檔案數量的限制

        在Linux平台上,無論編寫用戶端程式還是服務端程式,在進行高并發TCP連接配接處理時,最高的并發數量都要受到系統對使用者單一程序同時可打開檔案數量的限制(這是因為系統為每個TCP連接配接都要建立一個socket句柄,每個socket句柄同時也是一個檔案句柄)。可使用ulimit指令檢視系統允許目前使用者程序打開的檔案數限制:

[speng@as4 ~]$ ulimit -n

1024   #系統預設對某一個使用者打開檔案數的使用者軟限制是1024,使用者硬限制是4096個

這表示目前使用者的每個程序最多允許同時打開1024個檔案,這1024個檔案中還得除去每個程序必然打開的标準輸入,标準輸出,标準錯誤,伺服器監聽 socket,程序間通訊的unix域socket等檔案,那麼剩下的可用于用戶端socket連接配接的檔案數就隻有大概1024-10=1014個左右。也就是說預設情況下,基于Linux的通訊程式最多允許同時1014個TCP并發連接配接。

        如果想支援更高數量的TCP并發連接配接的通訊處理程式,就必須修改Linux對目前使用者的程序同時打開的檔案數量的軟限制(soft limit)和硬限制(hardlimit)。其中軟限制是指Linux在目前系統能夠承受的範圍内進一步限制使用者同時能打開的檔案數;硬限制則是根據系統硬體資源狀況(主要是系統記憶體)計算出來的系統最多可同時打開的檔案數量。通常軟限制小于或等于硬限制。

修改上述限制的最簡單的辦法就是使用ulimit指令:

[speng@as4 ~]$ ulimit -n 100    #(隻能設定比目前soft限制更小的數)

上述指令中,指定要設定的單一程序允許打開的最大檔案數。如果系統回顯類似于"Operation notpermitted"之類的話,說明上述限制修改失敗,實際上是因為在此指定的數值超過了Linux系統對該使用者打開檔案數的軟限制或硬限制。是以,就需要修改Linux系統對使用者的關于打開檔案數的軟限制和硬限制。

如果需要設定比目前軟限制和硬限制更大的數,隻能修改配置檔案,步驟如下:

第一步,修改/etc/security/limits.conf檔案,在檔案中添加如下行:

speng soft   nofile  10240

speng hard nofile  10240

其中speng指定了要修改的使用者的使用者名,可用'*'号表示修改所有使用者的限制;soft或hard指定要修改軟限制還是硬限制;10240則指定了想要修改的新的限制值,即最大打開檔案數(請注意軟限制值要小于或等于硬限制)。修改完後儲存檔案。

第二步,修改/etc/pam.d/login檔案,在檔案中添加如下行:

session required /lib/security/pam_limits.so

        這是告訴Linux在使用者完成系統登入後,應該調用pam_limits.so子產品來設定系統對該使用者可使用的各種資源數量的最大限制(包括使用者可打開的最大檔案數限制),而pam_limits.so子產品就會從/etc/security/limits.conf檔案中讀取配置來設定這些限制值。修改完後儲存此檔案。

第三步,檢視Linux系統級的最大打開檔案數限制-硬限制,使用如下指令:

[speng@as4 ~]$ cat /proc/sys/fs/file-max

12158

這表明這台Linux系統最多允許同時打開(即包含所有使用者打開檔案數總和)12158個檔案,是Linux系統級硬限制,所有使用者級的打開檔案數限制都不應超過這個數值。通常這個系統級硬限制是Linux系統在啟動時根據系統硬體資源狀況計算出來的最佳的最大同時打開檔案數限制,如果沒有特殊需要,不應該修改此限制,除非想為使用者級打開檔案數限制設定超過此限制的值。

修改此硬限制的方法是修改/etc/rc.local腳本,在腳本中添加如下行:

echo 22158 > /proc/sys/fs/file-max

  這是讓Linux在啟動完成後強行将系統級打開檔案數硬限制設定為22158.修改完後儲存此檔案。

第四步,完成上述步驟後重新開機系統,一般情況下就可以将Linux系統對指定使用者的單一程序允許同時打開的最大檔案數限制設為指定的數值。如果重新開機後用 ulimit-n指令檢視使用者可打開檔案數限制仍然低于上述步驟中設定的最大值,這可能是因為在使用者登入腳本/etc/profile中使用ulimit -n指令已經将使用者可同時打開的檔案數做了限制。由于通過ulimit-n修改系統對使用者可同時打開檔案的最大數限制時,新修改的值隻能小于或等于上次 ulimit-n設定的值,是以想用此指令增大這個限制值是不可能的。

是以,如果有上述問題存在,就隻能去打開/etc/profile腳本檔案,在檔案中查找是否使用了ulimit-n限制了使用者可同時打開的最大檔案數量,如果找到,則删除這行指令,或者将其設定的值改為合适的值,然後儲存檔案,使用者退出并重新登入系統即可。 通過上述步驟,就為支援高并發TCP連接配接處理的通訊處理程式解除關于打開檔案數量方面的系統限制。

2、修改網絡核心對TCP連接配接的有關限制

  在Linux上編寫支援高并發TCP連接配接的用戶端通訊處理程式時,有時會發現盡管已經解除了系統對使用者同時打開檔案數的限制,但仍會出現并發TCP連接配接數增加到一定數量時,再也無法成功建立新的TCP連接配接的現象。出現這種現在的原因有多種。

        第一種原因可能是因為Linux網絡核心對本地端口号範圍有限制(用戶端能使用的端口号限制)。此時,進一步分析為什麼無法建立TCP連接配接,會發現問題出在connect()調用傳回失敗,檢視系統錯誤提示消息是"Can't assign requestedaddress".同時,如果在此時用tcpdump工具監視網絡,會發現根本沒有TCP連接配接時用戶端發SYN包的網絡流量。這些情況說明問題在于本地Linux系統核心中有限制。

其實,問題的根本原因在于Linux核心的TCP/IP協定實作子產品對系統中所有的用戶端TCP連接配接對應的本地端口号的範圍進行了限制(例如,核心限制本地端口号的範圍為1024~32768之間)。當系統中某一時刻同時存在太多的TCP用戶端連接配接時,由于每個TCP用戶端連接配接都要占用一個唯一的本地端口号(此端口号在系統的本地端口号範圍限制中),如果現有的TCP用戶端連接配接已将所有的本地端口号占滿(端口耗盡),是以系統會在這種情況下在connect()調用中傳回失敗,并将錯誤提示消息設為"Can't assignrequested address".

有關這些控制邏輯可以檢視Linux核心源代碼,以linux2.6核心為例,可以檢視tcp_ipv4.c檔案中如下函數:

static int tcp_v4_hash_connect(struct sock *sk)

請注意上述函數中對變量sysctl_local_port_range的通路控制。變量sysctl_local_port_range的初始化則是在tcp.c檔案中的如下函數中設定:

void __init tcp_init(void)

核心編譯時預設設定的本地端口号範圍可能太小,是以需要修改此本地端口範圍限制,方法為

第一步,修改/etc/sysctl.conf檔案,在檔案中添加如下行:

net.ipv4.ip_local_port_range = 1024 65000

這表明将系統對本地端口範圍限制設定為1024~65000之間。請注意,本地端口範圍的最小值必須大于或等于1024;而端口範圍的最大值則必須<=65535.修改完後儲存此檔案。

第二步,執行sysctl指令:

[speng@as4 ~]$ sysctl -p

如果系統沒有錯誤提示,就表明新的本地端口範圍設定成功。如果按上述端口範圍進行設定,則理論上單獨一個程序最多可以同時建立60000多個TCP用戶端連接配接。

第二種無法建立TCP連接配接的原因可能是因為Linux網絡核心的IP_TABLE防火牆對最大跟蹤的TCP連接配接數有限制。此時程式會表現為在 connect()調用中阻塞,如同當機,如果用tcpdump工具監視網絡,也會發現根本沒有TCP連接配接時用戶端發SYN包的網絡流量。由于 IP_TABLE防火牆在核心中會對每個TCP連接配接的狀态進行跟蹤,跟蹤資訊将會放在位于核心記憶體中的conntrackdatabase中,這個資料庫的大小有限,當系統中存在過多的TCP連接配接時,資料庫容量不足,IP_TABLE無法為新的TCP連接配接建立跟蹤資訊,于是表現為在connect()調用中阻塞。此時就必須修改核心對最大跟蹤的TCP連接配接數的限制,方法同修改核心對本地端口号範圍的限制是類似的:

net.ipv4.ip_conntrack_max = 10240

這表明将系統對最大跟蹤的TCP連接配接數限制設定為10240.請注意,此限制值要盡量小,以節省對核心記憶體的占用。

如果系統沒有錯誤提示,就表明系統對新的最大跟蹤的TCP連接配接數限制修改成功。如果按上述參數進行設定,則理論上單獨一個程序最多可以同時建立10000多個TCP用戶端連接配接。

三、高并發采用的IO通路方案

       使用支援高并發網絡I/O的程式設計技術在Linux上編寫高并發TCP連接配接應用程式時,必須使用合适的網絡I/O技術和I/O事件分派機制。可用的I/O技術有同步I/O(目前I/O通路完成再進行下一次通路),非阻塞式同步I/O(也稱反應式I/O,select,poll,epoll實作),以及異步I/O.

在高TCP并發的情形下,如果使用同步I/O,這會嚴重阻塞程式的運轉,除非為每個TCP連接配接的I/O建立一個線程。但是,過多的線程又會因系統對線程的排程造成巨大開銷。是以,在高TCP并發的情形下使用同步 I/O是不可取的.

        這時可以考慮使用非阻塞式同步I/O或異步I/O.非阻塞式同步I/O的技術包括使用select(),poll(),epoll等機制。異步I/O的技術就是使用AIO.

        從I/O事件分派機制來看,使用select()是不合适的,因為它所支援的并發連接配接數有限(通常在1024個以内)。如果考慮性能,poll()也是不合适的,盡管它可以支援的較高的TCP并發數,但是由于其采用"輪詢"機制,當并發數較高時,其運作效率相當低,并可能存在I/O事件分派不均,導緻部分TCP連接配接上的I/O出現"饑餓"現象。而如果使用epoll或AIO,則沒有上述問題(早期Linux核心的AIO技術實作是通過在核心中為每個 I/O請求建立一個線程來實作的,這種實作機制在高并發TCP連接配接的情形下使用其實也有嚴重的性能問題。但在最新的Linux核心中,AIO的實作已經得到改進)。

        綜上所述,在開發支援高并發TCP連接配接的Linux應用程式時,應盡量使用epoll或AIO技術來實作并發的TCP連接配接上的I/O控制,這将為提升程式對高并發TCP連接配接的支援提供有效的I/O保證。

        epoll是Linux核心為處理大批量檔案描述符而作了改進的poll,是Linux下多路複用IO接口select/poll的增強版本,它能顯著提高程式在大量并發連接配接中隻有少量活躍的情況下的系統CPU使用率。另一點原因就是擷取事件的時候,它無須周遊整個被偵聽的描述符集,隻要周遊那些被核心IO事件異步喚醒而加入Ready隊列的描述符集合就行了。epoll除了提供select/poll那種IO事件的水準觸發(Level Triggered)外,還提供了邊緣觸發(Edge Triggered),這就使得使用者空間程式有可能緩存IO狀态,減少epoll_wait/epoll_pwait的調用,提高應用程式效率。

1.為什麼是epoll,而不是select?

(1)epoll支援在一個使用者程序内打開最大系統限制的檔案描述符

select 最不能忍受的是一個程序所打開的FD是有一定限制的,由FD_SETSIZE設定,預設值是1024。對于那些需要支援的上萬連接配接數目的IM伺服器來說顯然太少了。這時候一般有2種選擇:一是可以選擇修改這個宏然後重新編譯核心,不過資料也同時指出這樣會帶來網絡效率的下

使用epoll進行高性能網絡程式設計 降,二是可以選擇多程序的解決方案(傳統的Apache方案),不過雖然linux上面建立程序的代價比較小,但仍舊是不可忽視的,加上程序間資料同步遠比不上線程間同步的高效,是以也不是一種完美的方案。

不過 epoll則沒有這個限制,它所支援的FD上限是最大可以打開檔案的數目,這個數字一般遠大于2048,舉個例子,在1GB記憶體的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max檢視,一般來說這個數目和系統記憶體關系很大。

(2)epoll的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之上了。

傳統的select以及poll的效率會因為線上人數的線形遞增而導緻呈二次乃至三次方的下降,這些直接導緻了網絡伺服器可以支援的人數有了個比較明顯的限制。

select/poll線性掃描檔案描述符,epoll事件觸發

(3)epoll使用mmap加速核心與使用者空間的消息傳遞(檔案描述符傳遞)

這點實際上涉及到epoll的具體實作了。無論是select,poll還是epoll都需要核心把FD消息通知給使用者空間,如何避免不必要的記憶體拷貝就很重要,在這點上,epoll是通過核心與使用者空間mmap同一塊記憶體實作的。

(4)epoll有2種工作方式

epoll有2種工作方式:LT和ET。

LT(level triggered電平觸發)是預設的工作方式,并且同時支援block和no-block socket(阻塞和非阻塞).在這種做法中,核心告訴你一個檔案描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,核心還是會繼續通知你的(如果你不作任何操作,會通知多次),是以,這種模式程式設計出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表。

ET (edge-triggered邊緣觸發)是高速工作方式,隻支援no-block socket。在這種模式下,當描述符從未就緒變為就緒時,核心通過epoll告訴你。然後它會假設你知道檔案描述符已經就緒,并且不會再為那個檔案描述符發送更多的就緒通知(無論如何,隻通知一次),直到你做了某些操作導緻那個檔案描述符不再為就緒狀态了(比如,你在發送,接收或者接收請求,或者發送接收的資料少于一定量時導緻了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(進而導緻它再次變成未就緒),核心不會發送更多的通知(only once),不過在TCP協定中,ET模式的加速效用仍需要更多的benchmark确認。

ET和LT的差別就在這裡展現,LT事件不會丢棄,而是隻要讀buffer裡面有資料可以讓使用者讀或寫buffer為空,則不斷的通知你。而ET則隻在事件發生之時通知。可以簡單了解為LT是水準觸發,而ET則為邊緣觸發。LT模式隻要有事件未處理就會觸發,而ET則隻在高低電平變換時(即狀态從1到0或者0到1)觸發。

綜上所述,epoll适合管理百萬級數量的檔案描述符。

2.epoll相關的系統調用

總共不過3個API:epoll_create, epoll_ctl, epoll_wait。

(1)int epoll_create(int maxfds) 

建立一個epoll的句柄,傳回新的epoll裝置句柄。在linux下如果檢視/proc/程序id/fd/,是能夠看到這個fd的,是以在使用完epoll後,必須調用close()關閉,否則可能導緻fd被耗盡。

int epoll_create1(int flags)是int epoll_create(int maxfds) 的變體,已将maxfds廢棄不用。

flags隻有2種取值:flags=0表示epoll_create(int maxfds) 一樣,檔案數上限應該是系統使用者程序的軟上限;

flags=EPOLL_CLOEXEC 表示在新打開的檔案描述符裡設定 close-on-exec (FD_CLOEXEC) 标志。相當于先調用pfd=epoll_create,在使用fcntl設定pfd的FD_CLOEXEC選項。

意思是在使用execl産生的子程序裡面,将此描述符關閉,不能再使用它,但是在使用fork調用的子程序中,此描述符并不關閉,仍可使用。

(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 

epoll的事件注冊函數,傳回0表示設定成功。

第一個參數是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 */

};

epoll能監控的檔案描述符的7個events可以是以下7個宏的集合:

EPOLLIN :表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉);

EPOLLOUT:表示對應的檔案描述符可以寫;

EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來);

EPOLLERR:表示對應的檔案描述符發生錯誤;

EPOLLHUP:表示對應的檔案描述符被挂斷;

EPOLLET: 将EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對于電平觸發(Level Triggered)來說的。如果不設定則為電平觸發。

EPOLLONESHOT:隻監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裡

(3)int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

收集在epoll監控的事件中已經發生的事件,傳回發生的事件個數。

參數events是配置設定好的epoll_event結構體數組,epoll将會把發生的事件指派到events數組中(events不可以是空指針,核心隻負責把資料複制到這個events數組中,不會去幫助我們在使用者态中配置設定記憶體)。

maxevents告之核心這個events數組有多大,這個 maxevents的值不能大于建立epoll_create()時的maxfds。

參數timeout是epoll_wait逾時時間毫秒數,0會立即傳回非阻塞,-1永久阻塞。

如果函數調用成功,傳回對應I/O上已準備好的檔案描述符數目,如傳回0表示已逾時。

   Linux-2.6.19又引入了可以屏蔽指定信号的epoll_wait: epoll_pwait。

3.epoll的使用過程

(1)首先通過kdpfd=epoll_create(int maxfds)來建立一個epoll的句柄,其中maxfds為你epoll所支援的最大句柄數。這個函數會傳回一個新的epoll句柄,之後的所有操作将通過這個句柄來進行操作。在用完之後,記得用close()來關閉這個建立出來的epoll句柄。

(2)之後在你的網絡主循環裡面,每一幀的調用epoll_wait(int epfd, epoll_event *events, int max events, int timeout)來查詢所有的網絡接口,看哪一個可以讀,哪一個可以寫了。基本的文法為:

 nfds=epoll_wait(kdpfd,events,maxevents,-1); 

其中kdpfd為用epoll_create建立之後的句柄,events是一個epoll_event*的指針,當epoll_wait這個函數操作成功之後,epoll_events裡面将儲存所有的讀寫事件。maxevents是最大事件數量。最後一個timeout是epoll_wait的逾時,為0的時候表示馬上傳回,為-1的時候表示一直等下去,直到有事件發生,為任意正整數的時候表示等這麼長的時間,如果一直沒有事件,則傳回。一般如果網絡主循環是單獨的線程的話,可以用-1來等,這樣可以保證一些效率,如果是和主邏輯在同一個線程的話,則可以用0來保證主循環的效率。

epoll_wait範圍之後應該是一個循環,周遊所有的事件:

while(true)

{

  nfds = epoll_wait(epfd,events,20,500);   

  for(n=0;n<nfds;++n) 

 { 

  if(events[n].data.fd==listener) 

  { //如果是主socket的事件的話,則表示 

    //有新連接配接進入了,進行新連接配接的處理。 

    client=accept(listener,(structsockaddr*)&local,&addrlen); 

    if(client<0)   //在此最好将client設定為非阻塞

    { perror("accept"); continue; } 

    setnonblocking(client);//将新連接配接置于非阻塞模式

    ev.events=EPOLLIN|EPOLLET; //并且将新連接配接也加入EPOLL的監聽隊列。注意: 并沒有設定對寫socket的監聽

    ev.data.fd=client; 

    if(epoll_ctl(kdpfd,EPOLL_CTL_ADD,client,&ev)<0) 

      { //設定好event之後,将這個新的event通過epoll_ctl加入到epoll的監聽隊列裡面, 這裡用EPOLL_CTL_ADD來加一個新的epoll事件,通過EPOLL_CTL_DEL來減少一個 ,epoll事件,通過EPOLL_CTL_MOD來改變一個事件的監聽方式。 

        fprintf(stderr,"epollsetinsertionerror:fd=%d0,client); return-1; 

      } 

   } 

   elseif(event[n].events&EPOLLIN) 

   { //如果是已經連接配接的使用者,并且收到資料, 那麼進行讀入 

     int sockfd_r; 

     if((sockfd_r=event[n].data.fd)<0)

    continue; 

     read(sockfd_r,buffer,MAXSIZE); 

     //修改該sockfd_r上要處理的事件為EPOLLOUT,這樣可以監聽寫緩存是否可寫,直到可寫時才寫入資料 

     ev.data.fd=sockfd_r;   

     ev.events=EPOLLOUT|EPOLLET; 

     epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd_r,&ev) );//修改辨別符,等待下一個循環時發送資料,異步處理的精髓

    } 

    elseif(event[n].events&EPOLLOUT) //如果有資料發送 

           { 

     intsockfd_w=events[n].data.fd; 

     write(sockfd_w,buffer,sizeof(buffer)); 

     //修改sockfd_w上要處理的事件為EPOLLIN 

     ev.data.fd=sockfd_w; 

     ev.events=EPOLLIN|EPOLLET; 

     epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd_w,&ev) //修改辨別符,等待下一個循環時接收資料

            } 

    do_use_fd(events[n].data.fd); 

 } 

}

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>

#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);//getaddrinfo解決了把主機名和服務名轉換成套接口位址結構的問題。

  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");

  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");

  //設定檔案狀态标志

  flags |= O_NONBLOCK;

  s = fcntl (sfd, F_SETFL, flags);

  if (s == -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);

  s = listen (sfd, SOMAXCONN);

      perror ("listen");

      abort ();

  //除了參數size被忽略外,此函數和epoll_create完全相同

  efd = epoll_create1 (0);

  if (efd == -1)

      perror ("epoll_create");

  event.data.fd = sfd;

  event.events = EPOLLIN | EPOLLET;//讀入,邊緣觸發方式

  s = epoll_ctl (efd, EPOLL_CTL_ADD, sfd, &event);

      perror ("epoll_ctl");

  /* 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");

                    }

                                  //将位址轉化為主機名或者服務名

                  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);

                      perror ("epoll_ctl");

                      abort ();

                }

          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;

                  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;

                  /* Write the buffer to standard output */

                  s = write (1, buf, count);

                      perror ("write");

              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;

運作方式:

在一個終端運作此程式:epoll.out PORT

另一個終端:telnet  127.0.0.1 PORT

本文轉自 a_liujin 51CTO部落格,原文連結:http://blog.51cto.com/a1liujin/1709875,如需轉載請自行聯系原作者