1、4大具備表明性的併發模型及其優缺點
4大具備表明性的併發模型:Apache模型(Process Per Connection,簡稱PPC),TPC(Thread PerConnection)模型,select模型和poll模型、Epoll模型。
Apache(PPC)模型和TPC模型是最容易理解的,Apache模型在併發上是經過多進程實現的,而TPC模型是經過多線程實現的,可是這2種方式在大量進程/線程切換時會形成大量的開銷。
select模型是經過一種輪詢機制來實現的。須要注意select模型有3大不足:linux
a.Socket文件數量限制:該模式可操做的Socket數由FD_SETSIZE決定,內核默認32*32=1024.
b.操做限制:經過遍歷FD_SETSIZE(1024)個Socket來完成調度,無論哪一個Socket是活躍的,都遍歷一遍,這樣效率就會隨文件數量的增多呈現線性降低,把FD_SETSIZE改大的後果就是遍歷須要更久的時間。管理文件數量和管理效率成反比。nginx
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,將來的主流。web
2、主機環境對高併發應用程序的自然限制數據庫
高併發的應用程序至少須要考慮3大限制條件:用戶進程的默認內存空間爲4G,線程棧默認爲8M,用戶進程最大能管理的文件描述符默認爲1024個,網卡對客戶端端口號數量的限制。
Linux下高併發socket服務器端和客戶端最大鏈接數所受的限制問題(修改軟限制和硬限制)
一、配置用戶進程可打開的最多文件數量的限制
在Linux平臺上,不管編寫客戶端程序仍是服務端程序,在進行高併發TCP鏈接處理時,最高的併發數量都要受到系統對用戶單一進程同時可打開文件數量的限制(這是由於系統爲每一個TCP鏈接都要建立一個socket句柄,每一個socket句柄同時也是一個文件句柄)。可以使用ulimit命令查看系統容許當前用戶進程打開的文件數限制:
[speng@as4 ~]$ ulimit -n
1024 #系統默認對某一個用戶打開文件數的用戶軟限制是1024,用戶硬限制是4096個apache
這表示當前用戶的每一個進程最多容許同時打開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鏈接處理的通信處理程序解除關於打開文件數量方面的系統限制。緩存
二、修改網絡內核對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鏈接數的限制,方法同修改內核對本地端口號範圍的限制是相似的:
第一步,修改/etc/sysctl.conf文件,在文件中添加以下行:
net.ipv4.ip_conntrack_max = 10240
這代表將系統對最大跟蹤的TCP鏈接數限制設置爲10240.請注意,此限制值要儘可能小,以節省對內核內存的佔用。
第二步,執行sysctl命令:
[speng@as4 ~]$ sysctl -p
若是系統沒有錯誤提示,就代表系統對新的最大跟蹤的TCP鏈接數限制修改爲功。若是按上述參數進行設置,則理論上單獨一個進程最多能夠同時創建10000多個TCP客戶端鏈接。
3、高併發採用的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");
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;
}
運行方式:
在一個終端運行此程序:epoll.out PORT
另外一個終端:telnet 127.0.0.1 PORT