構建現代的server應用程序須要以某種方法同一時候接收數百武鲁、數千甚至數萬個事件故源,不管它們是內部請求還是網絡連接隙咸,都要有效地處理它們的操作窄绒。有很多解決方式贝次,但事件驅動也被廣泛應用到網絡編程中。并大規(guī)模部署在高連接數高吞吐量的server程序中彰导,如http server程序蛔翅、ftp server程序等。
相比于傳統(tǒng)的網絡編程方式位谋,事件驅動可以極大的減少資源占用山析,增大服務接待能力,并提高網絡傳輸效率掏父。這些事件驅動模型中笋轨,libevent庫和libev庫可以大大提高性能和事件處理能力。
在討論libev和libevent之前赊淑,我們看看I/O模型演進變化歷史爵政。
一、堵塞網絡接口:處理單個client
我們第一次接觸到的網絡編程一般都是從socket()陶缺、listen()钾挟、bind()、send()饱岸、recv()等龙、close()等接口開始的。使用這些接口能夠非常方便的構建server/client的模型伶贰。
堵塞I/O模型圖:在調用recv()函數時蛛砰,發(fā)生在內核中等待數據和復制數據的過程。
當調用recv()函數時:系統(tǒng)首先查是否有準備好的數據黍衙;假設數據沒有準備好泥畅,那么系統(tǒng)就處于等待狀態(tài);當數據準備好后琅翻,將數據從系統(tǒng)緩沖區(qū)拷貝到用戶空間位仁,然后該函數才返回。在套接應用程序中方椎,當調用recv()函數時聂抢,未必用戶空間就已經存在數據,那么此時recv()函數就會處于等待狀態(tài)棠众。
我們注意到琳疏。大部分的socket接口都是堵塞型的有决。所謂堵塞型接口是指系統(tǒng)調用(通常是IO接口)不返回調用結果并讓當前線程一直堵塞,僅僅有當該系統(tǒng)調用獲得結果或者超時出錯時才返回空盼。
實際上书幕,除非特別指定,幾乎全部的IO接口(包含socket接口)都是堵塞型的揽趾。這給網絡編程帶來了一個非常大的問題:如在調用send()的同一時候台汇,線程將被堵塞,而在此期間篱瞎,線程將無法運行不論什么運算苟呐、響應,還是網絡請求俐筋。這給多客戶機牵素、多業(yè)務邏輯的網絡編程帶來了挑戰(zhàn)。這時校哎,非常多程序猿可能會選擇多線程的方式來解決問題两波。
使用堵塞模式的套接字瞳步,開發(fā)網絡程序比較簡單闷哆,也容易實現。
當希望可以馬上發(fā)送和接收數據单起。且處理的套接字數量比較少的情況下抱怔。即一個一個處理client,server沒什么壓力嘀倒。使用堵塞模式來開發(fā)網絡程序比較合適屈留。
假設非常多client同一時候訪問server,server就不能同一時候處理這些請求测蘑。這時灌危,我們可能會選擇多線程的方式來解決問題。
二碳胳、多線程/進程處理多個client
應對多客戶機的網絡應用勇蝙,最簡單的解決方案是在server端使用多線程(或多進程)。多線程(或多進程)的目的是讓每一個連接都擁有獨立的線程(或進程)挨约,這樣不論什么一個連接的堵塞都不會影響其它的連接味混。
詳細使用多進程還是多線程,并沒有一個特定的模式诫惭。傳統(tǒng)意義上翁锡,進程的開銷要遠遠大于線程,所以夕土,假設須要同一時候為較多的客戶機提供服務馆衔。則不推薦使用多進程;假設單個服務運行體須要消耗較多的CPU資源,譬如須要進行大規(guī)墓猓或長時間的數據運算或文件訪問荒适,則進程較為安全。通常开镣,使用pthread_create()創(chuàng)建新線程刀诬,fork() 創(chuàng)建新進程:
- 當新的connection進來,用fork()創(chuàng)建一個新的process處理業(yè)務邪财;
- 當新的connection進來陕壹,用pthread_create()產生一個Thread處理;
多線程/進程server同一時候為多個客戶機提供應答服務树埠。模型交互過程如下圖所示:
主線程持續(xù)等待client的連接請求糠馆,假設有連接,則創(chuàng)建新線程怎憋,并在新線程中提供為前例相同的問答服務又碌。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void do_service(int conn);
void err_log(string err, int sockfd) {
perror("binding"); close(sockfd); exit(-1);
}
int main(int argc, char *argv[])
{
unsigned short port = 8000;
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);// 創(chuàng)建通信端點:套接字
if(sockfd < 0) {
perror("socket");
exit(-1);
}
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(port);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));
if( err_log != 0) err_log("binding");
err_log = listen(sockfd, 10);
if(err_log != 0) err_log("listen");
struct sockaddr_in peeraddr; //傳出參數
socklen_t peerlen = sizeof(peeraddr); //傳入傳出參數。必須有初始值
int conn; // 已連接套接字(變?yōu)橹鲃犹捉幼职泶茨軌蛑鲃觕onnect)
pid_t pid;
while (1) {
if ((conn = accept(sockfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0) //3次握手完畢的序列
err_log("accept error");
printf("recv connect ip=%s port=%d/n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
pid = fork();
if (pid == -1)
err_log("fork error");
if (pid == 0) {// 子進程
close(listenfd);
do_service(conn);
exit(EXIT_SUCCESS);
}
else
close(conn); //父進程
}
return 0;
}
void do_service(int conn) {
char recvbuf[1024];
while (1) {
memset(recvbuf, 0, sizeof(recvbuf));
int ret = read(conn, recvbuf, sizeof(recvbuf));
if (ret == 0) { //客戶端關閉了
printf("client close/n");
break;
}
else if (ret == -1)
ERR_EXIT("read error");
fputs(recvbuf, stdout);
write(conn, recvbuf, ret);
}
}
非常多剛開始學習的人可能不明確為何一個socket可以accept多次毕匀。實際上,socket的設計者可能特意為多客戶機的情況留下了伏筆癌别,讓accept()可以返回一個新的socket皂岔。以下是accept接口的原型:
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
輸入參數s是從socket()、bind()和listen()中沿用下來的socket句柄值展姐。運行完bind()和listen()后躁垛,操作系統(tǒng)已經開始在指定的port處監(jiān)聽全部的連接請求。假設有請求圾笨,則將該連接請求增加到請求隊列教馆。調用accept()接口正是從socket的請求隊列抽取第一個連接信息,創(chuàng)建一個與s同類的新的socket句柄并返回擂达。新的socket句柄即是與client調用read()和recv()的輸入參數土铺。假設請求隊列當前沒有請求。則accept()將進入堵塞狀態(tài)直到有請求進入隊列谍婉。
上述多進程的server模型似乎完美的攻克了為多個客戶機提供問答服務的要求舒憾,但事實上并不盡然。假設要同一時候響應成百上千路的連接請求穗熬,則不管多線程還是多進程都會嚴重占領系統(tǒng)資源镀迂,減少系統(tǒng)對外界響應效率。而且線程與進程本身也更easy進入假死狀態(tài)唤蔗。
因此其缺點也很明顯:
- 用fork()的問題在于每個Connection進來時的成本太高探遵,假設同一時候接入的并發(fā)連接數太多easy進程數量非常多窟赏,進程之間的切換開銷會非常大,同一時候對于老的內核(Linux)會產生雪崩效應箱季。
- 用Multi-thread的問題在于Thread-safe與Deadlock問題難以解決涯穷。另外有Memory-leak的問題也要處理,這個問題對于非常多程序猿來說無異于惡夢藏雏,尤其是對于連續(xù)server的server程序更是不能夠接受拷况。且在多CPU的系統(tǒng)上沒有辦法使用到全部的CPU resource。
由此可能會考慮使用“線程池”或“連接池”掘殴。
“線程池”旨在降低創(chuàng)建和銷毀線程的頻率赚瘦,其維持一定合理數量的線程。并讓空暇的線程又一次承擔新的運行任務奏寨∑鹨猓“連接池”維持連接的緩存池,盡量重用已有的連接病瞳、降低創(chuàng)建和關閉連接的頻率揽咕。這兩種技術都能夠非常好的降低系統(tǒng)開銷,都被廣泛應用非常多大型系統(tǒng)套菜,如apache亲善,mysql數據庫等。
可是笼踩,“線程池”和“連接池”技術也僅僅是在一定程度上緩解了頻繁調用IO接口帶來的資源占用逗爹。并且亡嫌,所謂“池”始終有其上限嚎于,當請求大大超過上限時,“池”構成的系統(tǒng)對外界的響應并不比沒有池的時候效果好多少挟冠。所以使用“池”必須考慮其面臨的響應規(guī)模于购,并依據響應規(guī)模調整“池”的大小。
相應上例中的所面臨的可能同一時候出現的上千甚至上萬次的client請求知染,“線程池”或“連接池”也許能夠緩解部分壓力肋僧,可是不能解決全部問題。并且由于多線程/進程還會導致過多的占用內存或CPU等系統(tǒng)資源控淡。
三嫌吠、非堵塞的server模型
以上面臨的非常多問題,一定程度是IO接口的堵塞特性導致的掺炭。多線程是一個解決方式辫诅,還有一個方案就是使用非堵塞的接口。非堵塞的接口相比于堵塞型接口的顯著差異在于:在被調用之后馬上返回涧狮。
使用例如以下的函數能夠將某句柄fd設為非堵塞狀態(tài)炕矮。我們能夠使用fcntl(fd, F_SETFL, flag | O_NONBLOCK)將套接字標志變成非堵塞:
fcntl(fd, F_SETFL, O_NONBLOCK);
以下將給出僅僅用一個線程么夫。但可以同一時候從多個連接中檢測數據是否送達,且接受數據肤视。下圖是使用非堵塞接收數據的模型:
在非堵塞狀態(tài)下档痪,recv()接口在被調用后馬上返回,返回值代表了不同的含義:比如邢滑,假設設備臨時沒有數據可讀就返回-1腐螟,同時設置errno為EWOULDBLOCK(或者EAGAIN,這兩個宏定義的值同樣)困后。
- recv() 返回值大于0遭垛,表示接受數據完成,返回值即是接受到的字節(jié)數操灿;
- recv() 返回0锯仪,表示連接已經正常斷開;
- recv() 返回-1趾盐,且errno等于EAGAIN庶喜,表示recv操作還沒運行完畢;
- recv() 返回-1救鲤,且errno不等于EAGAIN久窟,表示recv操作遇到系統(tǒng)錯誤errno。
這樣的行為方式稱為輪詢(Poll):調用者僅僅是查詢一下本缠,而不是堵塞在這里死等斥扛。這樣能夠同一時候監(jiān)視多個設備:
while(1){
非堵塞read(設備1);
if(設備1有數據到達)
處理數據;
非堵塞read(設備2);
if(設備2有數據到達)
處理數據;
......
}
假設read(設備1)是堵塞的,那么僅僅要設備1沒有數據到達就會一直堵塞在設備1的read調用上丹锹,即使設備2有數據到達也不能處理稀颁,使用非堵塞I/O就能夠避免設備2得不到及時處理。
非堵塞I/O有一個缺點楣黍,假設全部設備都一直沒有數據到達匾灶,調用者須要重復查詢做無用功;假設能堵塞在那里租漂,操作系統(tǒng)就能夠調度別的進程運行阶女,就不會做無用功了。在實際應用中非堵塞I/O模型比較少用哩治。
能夠看到server線程能夠通過循環(huán)調用recv()接口秃踩,在單個線程內實現對全部連接的數據接收工作。
可是上述模型絕不被推薦业筏。因為憔杨,循環(huán)調用recv()將大幅度推高CPU占用率。此外驾孔。在這個方法中芍秆,recv() 很多其它的是起到檢測“操作是否完畢”的作用惯疙,實際操作系統(tǒng)提供了更為高效的檢測“操作是否完畢”作用的接口妖啥,比如 select()、poll()蒿偎、epool()怀读、kqueue()等。
四菜枷、IO復用事件驅動server模型
簡單介紹:主要是select和epoll苍糠,對一個IO端口,兩次調用啤誊,兩次返回岳瞭,對比堵塞IO并沒有什么優(yōu)越性;關鍵是select和epoll能實現同一時候對多個IO端口進行監(jiān)聽蚊锹。
I/O復用模型會用到select瞳筏、poll、epoll函數牡昆,這幾個函數也會使進程堵塞姚炕,可是和堵塞I/O所不同的是:這兩個函數能夠同一時候堵塞多個I/O操作。并且能夠同一時候對多個讀操作丢烘,多個寫操作的I/O函數進行檢測柱宦,直到有數據可讀或可寫時,才真正調用I/O操作函數铅协。
我們先具體解釋select捷沸。SELECT函數進行IO復用server模型的原理是:當一個client連接上server時摊沉。server就將其連接的fd增加到fd_set集合狐史,等到這個連接準備好讀或寫的時候,就通知程序進行IO操作骏全,與client進行數據通信姜贡。大部分Unix/Linux都支持select函數楼咳,該函數用于探測多個文件句柄的狀態(tài)變化余耽。
4.1 select接口原型
FD_ZERO(int fd, fd_set* fds)
FD_SET(int fd, fd_set* fds)
FD_ISSET(int fd, fd_set* fds)
FD_CLR(int fd, fd_set* fds)
int select(
int maxfdp, //Winsock中此參數無意義
fd_set* readfds, //進行可讀檢測的Socket
fd_set* writefds, //進行可寫檢測的Socket
fd_set* exceptfds, //進行異常檢測的Socket
const struct timeval* timeout //非堵塞模式中設置最大等待時間
)
下面對select函數的參數詳細介紹碟贾。
- maxfdp
是一個整數值,意思是“最大fd加1(max fd plus 1)朱巨。在三個描寫敘述符集(readfds, writefds, exceptfds)中找出最大描寫敘述符蔬崩。它是一個編號值沥阳,也可將maxfdp設置為FD_SETSIZE,這是一個< sys/types.h >中的常數功炮,它說明了最大的描寫敘述符數(常常是256或1024)薪伏,可是對大多數應用程序而言嫁怀,此值太大了。
確實存捺,大多數應用程序僅僅應用3 ~ 10個描寫敘述符捌治。假設將第三個參數設置為最高描寫敘述符編號值加 1凹嘲,內核就僅僅需在此范圍內尋找打開的位周蹭,而不必在數百位的大范圍內搜索凶朗。
- readfds
是指向fd_set結構的指針,這個集合中應該包含文件描寫敘述符宛畦,表示我們要監(jiān)視這些文件描寫敘述符的讀變化的次和。即我們關心能否夠從這些文件里讀取數據了踏施,假設這個集合中有一個文件可讀,select就會返回一個大于0的值日熬。表示有文件可讀竖席,假設沒有可讀的文件怕敬。則依據timeout參數再推斷是否超時畸陡,若超出timeout的時間,select返回0斋日,若錯誤發(fā)生返回負值。能夠傳入NULL值贡必;表示不關心不論什么文件的讀變化衫樊。
- writefds
是指向fd_set結構的指針科侈,這個集合中應該包含文件描寫敘述符,表示我們要監(jiān)視這些文件描寫敘述符的寫變化的挂脑,即我們關心能否夠向這些文件里寫入數據了崭闲,假設這個集合中有一個文件可寫,select就會返回一個大于0的值牍戚,表示有文件可寫如孝,假設沒有可寫的文件第晰,則依據timeout參數再推斷是否超時品抽,若超出timeout的時間,select返回0盆昙,若錯誤發(fā)生返回負值淡喜。能夠傳入NULL值,表示不關心不論什么文件的寫變化们镜。
- exceptfds
同上面兩個參數的意圖模狭,用來監(jiān)視文件錯誤異常。readfds锚赤,writefds, exceptfds每一個描寫敘述符集存放在一個fd_set數據類型中线脚。
- timeout
是select的超時時間,這個參數至關重要寓落。它能夠使select處于三種狀態(tài):
- 第一伶选,若將NULL以形參傳入祸憋,即不傳入時間結構,就是將select置于堵塞狀態(tài)塞帐,一定等到監(jiān)視文件描寫敘述符集合中某個文件描寫敘述符發(fā)生變化為止葵姥;
- 第二,若將時間值設為0秒0毫秒削咆,就變成一個純粹的非堵塞函數拨齐,無論文件描寫敘述符是否有變化,都立馬返回繼續(xù)運行歼狼;文件無變化返回0羽峰,有變化返回一個正值;
- 第三履植,timeout的值大于0玫霎,這就是等待的超時時間庶近,即select在timeout時間內堵塞反番。超時時間之內有事件到來就返回了,否則在超時后無論如何一定返回枫疆,返回值同上述息楔。
4.2 使用select的步驟
- 創(chuàng)建所關注的事件的描寫敘述符集合(fd_set)值依。對于一個描寫敘述符,能夠關注其上面的讀(read)拯啦、寫(write)、異常(exception)事件甫匹。所以通常兵迅,要創(chuàng)建三個fd_set:一個用來收集關注讀事件的描寫敘述符,一個用來收集關注寫事件的描寫敘述符瞧省,另外一個用來收集關注異常事件的描寫敘述符集合;
- 調用select()骑科,等待事件發(fā)生咆爽;這里須要注意的一點是掰茶,select的堵塞與是否設置非堵塞I/O是沒有關系的濒蒋;
- 輪詢全部fd_set中的每個fd瓮顽,檢查是否有對應的事件發(fā)生暖混;假設有拣播,就進行處理。
/* 可讀泪勒、可寫圆存、異常三種文件描寫敘述符集的申明和初始化 */
fd_set readfds, writefds, exceptionfds;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&exceptionfds);
int max_fd;
/* socket配置和監(jiān)聽 */
sock = socket(...);
bind(sock, ...);
listen(sock, ...);
/* 對socket描寫敘述符上發(fā)生關心的事件進行注冊 */
FD_SET(&readfds, sock);
max_fd = sock;
while(1) {
int i;
fd_set r,w,e;
/* 為了反復使用readfds 、writefds怕轿、exceptionfds撞羽,將它們復制到暫時變量內谒出。*/
memcpy(&r, &readfds, sizeof(fd_set));
memcpy(&w, &writefds, sizeof(fd_set));
memcpy(&e, &exceptionfds, sizeof(fd_set));
/* 利用暫時變量調用select()堵塞等待笤喳。timeout=null表示等待時間為永遠等待直到發(fā)生事件。*/
select(max_fd + 1, &r, &w, &e, NULL);
/* 測試是否有client發(fā)起連接請求贰镣,假設有則接受并把新建的描寫敘述符增加監(jiān)控恭陡。*/
if(FD_ISSET(&r, sock)){
new_sock = accept(sock, ...);
FD_SET(&readfds, new_sock);
FD_SET(&writefds, new_sock);
max_fd = MAX(max_fd, new_sock);
}
/* 對其他描寫敘述符發(fā)生的事件進行適當處理。描寫敘述符依次遞增劫狠,
最大值各系統(tǒng)有所不同(比方在作者系統(tǒng)上最大為1024)遥赚。
在linux能夠用命令ulimit -a查看(用ulimit命令也對該值進行改動)溉躲;
在freebsd下毫炉,用sysctl -a | grep kern.maxfilesperproc來查詢和改動瞄勾。 */
for(i= sock+1; i <max_fd+1; ++i) {
if(FD_ISSET(&r, i))
doReadAction(i);
if(FD_ISSET(&w, i))
doWriteAction(i);
}
}
4.3 select相關的四個宏
FD_ZERO(int fd, fd_set* fds) //清除其全部位
FD_SET(int fd, fd_set* fds) //在某 fd_set 中標記一個fd的相應位為1
FD_ISSET(int fd, fd_set* fds) // 測試該集中的一個給定位是否仍舊設置
FD_CLR(int fd, fd_set* fds) //刪除相應位
這里愿阐,fd_set類型能夠簡單的理解為按bit位標記句柄的隊列缨历,比如要在某fd_set中標記一個值為16的句柄,則該fd_set的第16個bit位被標記為1赡磅。詳細的置位鲜滩、驗證可使用FD_SET、FD_ISSET等宏實現搞疗。
[圖片上傳失敗...(image-1877de-1558955750837)]
比如,編寫如下代碼:
fd_setreadset,writeset;
FD_ZERO(&readset);
FD_ZERO(&writeset);
FD_SET(0,&readset);
FD_SET(3,&readset);
FD_SET(1,&writeset);
FD_SET(2,&writeset);
select(4,&readset,&writeset,NULL,NULL);
下圖顯示了這兩個描寫敘述符集的情況:
由于描寫敘述符編號從0開始,所以要在最大描寫敘述符編號值上加1宛徊。第一個參數實際上是要檢查的描寫敘述符數(從描寫敘述符0開始)闸天。
4.4 select的返回值
select的返回值有三種可能:
- 返回值-1表示出錯;是可能發(fā)生的笼吟,比如在所指定的描寫敘述符都沒有準備好時捕捉到一個信號艳狐;
- 返回值0表示沒有描寫敘述符準備好;若指定的描寫敘述符都沒有準備好镀虐,并且指定的時間已經超過,則發(fā)生這樣的情況恨旱。
- 返回一個正值說明了已經準備好的描寫敘述符數搜贤;在這樣的情況下仪芒,三個描寫敘述符集中仍舊打開的位是相應于已準備好的描寫敘述符位掂名。
4.5 select接收數據模型
下圖是使用select多路復用技術的數據接收模型:
上述模型僅僅是描寫敘述了使用select()接口同一時候從多個client接收數據的過程;因為select()接口能夠同一時候對多個句柄進行讀狀態(tài)、寫狀態(tài)和錯誤狀態(tài)的探測肿嘲,所以能夠非常easy構建為多個client提供獨立問答服務的server系統(tǒng)。下面是使用select()接口事件驅動的server模型封救。
上述模型中,最關鍵的地方是怎樣動態(tài)維護select()的三個參數readfds掉盅、writefds和exceptfds:
- 作為輸入參數以舒,readfds應該標記全部的須要探測的“可讀事件”的句柄趾痘,當中永遠包含那個探測 connect()的那個“母”句柄;同一時候蔓钟,writefds和exceptfds應該標記全部須要探測的“可寫事件”和“錯誤事件”的句柄(使用FD_SET()標記)永票;
- 作為輸出參數,readfds滥沫、writefds和exceptfds中的保存了select()捕捉到的全部事件的句柄值;程序猿須要檢查的全部的標記位(使用FD_ISSET()檢查),以確定究竟哪些句柄發(fā)生了事件镐牺。
上述模型主要模擬的是“一問一答”的服務流程畦浓。所以论巍,假設select()發(fā)現某句柄捕捉到了“可讀事件”鞋怀,server程序應及時做recv()操作辛友。并依據接收到的數據準備好待發(fā)送數據。并將相應的句柄值增加writefds,準備下一次的“可寫事件”的select()探測尚卫。類似的怎爵,假設select()發(fā)現某句柄捕捉到“可寫事件”乞旦,則程序應及時做send()操作愕秫,并準備好下一次的“可寫事件”探測準備。下圖描寫敘述的是上述模型中的一個運行周期:
這樣的模型的特征在于每個運行周期都會探測一次或一組事件把夸。一個特定的事件會觸發(fā)某個特定的響應。我們能夠將這樣的模型歸類為“事件驅動模型”知市。
4.6 select的優(yōu)缺點
相比其它模型跟啤,使用select()的事件驅動模型僅僅用單線程(進程)運行泛啸,占用資源少聚请,不消耗太多 CPU矾兜,同一時候可以為多client提供服務荆萤。假設試圖建立一個簡單的事件驅動的server程序蝉仇,這個模型有一定的參考價值琅拌。但這個模型依然有著非常多問題灾而。
select的缺點:
- 單個進程可以監(jiān)視的文件描寫敘述符的數量存在最大限制耕餐;
- select須要復制大量的句柄數據結構亚隅,產生巨大的開銷酿联;
- select返回的是含有整個句柄的列表舅桩,應用程序須要消耗大量時間去輪詢各個句柄才干發(fā)現哪些句柄發(fā)生了事件;
- select的觸發(fā)方式是水平觸發(fā),應用程序假設沒有完畢對一個已經就緒的文件描寫敘述符進行IO操作赌蔑,那么之后每次select調用還是會將這些文件描寫敘述符通知進程;與之對應方式的是邊緣觸發(fā)请祖;
- 該模型將事件探測和事件響應夾雜在一起过牙,一旦事件響應的運行體龐大石咬,則對整個模型是災難性的。
例如以下例:龐大的運行體1的將直接導致響應事件2的運行體遲遲得不到運行蜻拨,并在非常大程度上減少了事件探測的及時性。
龐大的運行體對使用select()的事件驅動模型的影響。
非常多操作系統(tǒng)提供了更為高效的接口困鸥,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll等。假設須要實現更高效的server程序寨闹,類似epoll這種接口更被推薦。
4.7 poll事件模型
poll庫是在linux 2.1.23中引入的酬姆,windows平臺不支持poll骨宠。poll與select的基本方式同樣。都是先創(chuàng)建一個關注事件的描寫敘述符的集合相满,然后再去等待這些事件發(fā)生层亿;然后再輪詢描寫敘述符集合,檢查有沒有事件發(fā)生立美,假設有匿又,就進行處理。
因此建蹄。poll有著與select相似的處理流程:
- 創(chuàng)建描寫敘述符集合碌更,設置關注的事件;
- 調用poll()洞慎,等待事件發(fā)生痛单,以下是poll的原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
類似select,poll也能夠設置等待時間劲腿,效果與select一樣旭绒。
- 輪詢描寫敘述符集合,檢查事件,處理事件快压。
在這里要說明的是:poll與select的主要差別在與圆仔,select須要為讀、寫蔫劣、異常事件分別創(chuàng)建一個描寫敘述符集合坪郭,最后輪詢的時候,須要分別輪詢這三個集合脉幢;而poll僅僅須要一個集合歪沃,在每一個描寫敘述符相應的結構上分別設置讀、寫嫌松、異常事件沪曙,最后輪詢的時候,能夠同時檢查三種事件萎羔。
4.8 epoll事件模型
epoll是和上面的poll和select不同的一個事件驅動庫液走,它是在linux-2.5.44中引入的,它屬于poll的一個變種贾陷。
poll和select庫的最大的問題就在于效率缘眶,它們的處理方式都是創(chuàng)建一個事件列表,然后把這個列表發(fā)給內核髓废,返回的時候巷懈,再去輪詢檢查這個列表,這樣在描寫敘述符比較多的應用中慌洪,效率就顯得比較低下了顶燕。
epoll是一種比較好的做法,它把描寫敘述符列表交給內核冈爹,一旦有事件發(fā)生涌攻,內核把發(fā)生事件的描寫敘述符列表通知給進程,這樣就避免了輪詢整個描寫敘述符列表犯助。以下對epoll的使用進行說明:
- 創(chuàng)建一個epoll描寫敘述符癣漆,調用epoll_create()來完畢,epoll_create()有一個整型的參數size剂买,用來告訴內核惠爽,要創(chuàng)建一個有size個描寫敘述符的事件列表(集合);
int epoll_create(int size);
- 給描寫敘述符設置所關注的事件瞬哼,并把它加入到內核的事件列表中去婚肆,這里須要調用epoll_ctl()來完成:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
這里op參數有三種,分別代表三種操作:
- EPOLL_CTL_ADD, 把要關注的描寫敘述符和對其關注的事件的結構坐慰。加入到內核的事件列表中去较性;
- EPOLL_CTL_DEL用僧,把先前加入的描寫敘述符和對其關注的事件的結構,從內核的事件列表中去除赞咙;
- EPOLL_CTL_MOD责循,改動先前加入到內核的事件列表中的描寫敘述符的關注的事件;
- 等待內核通知事件發(fā)生攀操,得到發(fā)生事件的描寫敘述符的結構列表院仿;該過程由epoll_wait()完畢。得到事件列表后速和,就能夠進行事件處理了歹垫。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
在使用epoll的時候,有一個須要特別注意的地方颠放,那就是epoll觸發(fā)事件的文件有兩種方式:
- Edge Triggered(ET)排惨,在這樣的情況下,事件是由數據到達邊界觸發(fā)的碰凶;所以要在處理讀暮芭、寫的時候,要不斷的調用read/write欲低,直到它們返回EAGAIN谴麦,然后再去epoll_wait(),等待下次事件的發(fā)生伸头。這樣的方式適用要遵從以下的原則:
- 使用非堵塞的I/O;
- 直到read/write返回EAGAIN時舷蟀,才去等待下一次事件的發(fā)生恤磷。
- Level Triggered(LT), 在這樣的情況下,epoll和poll類似野宜,但處理速度上比poll更快扫步;在這樣的情況下,僅僅要有數據沒有讀/寫完匈子,調用epoll_wait()的時候河胎,就會有事件被觸發(fā)。
/* 新建并初始化文件描寫敘述符集 */
struct epoll_event ev;
struct epoll_event events[MAX_EVENTS];
/* 創(chuàng)建epoll句柄虎敦。*/
int epfd = epoll_create(MAX_EVENTS);
/* socket配置和監(jiān)聽游岳。*/
sock = socket(...);
bind(sock, ...);
listen(sock, ...);
/* 對socket描寫敘述符上發(fā)生關心的事件進行注冊。*/
ev.events = EPOLLIN;
ev.data.fd = sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);
while(1) {
int i;
/*調用epoll_wait()堵塞等待其徙。等待時間為永遠等待直到發(fā)生事件胚迫。*/
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for(i=0; i <n; ++i) {
/* 測試是否有client發(fā)起連接請求,假設有則接受并把新建的描寫敘述符增加監(jiān)控唾那。*/
if(events.data.fd == sock) {
if(events.events & POLLIN){
new_sock = accept(sock, ...);
ev.events = EPOLLIN | POLLOUT;
ev.data.fd = new_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, new_sock, &ev);
}
}else{
/* 對其他描寫敘述符發(fā)生的事件進行適當處理访锻。*/
if(events.events & POLLIN)
doReadAction(i);
if(events.events & POLLOUT)
doWriteAction(i);
}
}
}
epoll支持水平觸發(fā)和邊緣觸發(fā),理論上來說邊緣觸發(fā)性能更高∑谌可是使用更加復雜河哑,由于不論什么意外的丟失事件都會造成請求處理錯誤。Nginx就使用了epoll的邊緣觸發(fā)模型龟虎。
這里提一下水平觸發(fā)和邊緣觸發(fā)就緒通知的差別璃谨,這兩個詞來源于計算機硬件設計。
它們的差別是:水平觸發(fā)僅僅要句柄滿足某種狀態(tài)遣总,就會發(fā)出通知睬罗;邊緣觸發(fā)則只有當句柄狀態(tài)改變時,才會發(fā)出通知旭斥。比如一個socket經過長時間等待后接收到一段100k的數據容达,兩種觸發(fā)方式都會向程序發(fā)出就緒通知。如果程序從這個socket中讀取了50k數據垂券,并再次調用監(jiān)聽函數花盐,水平觸發(fā)依舊會發(fā)出就緒通知,而邊緣觸發(fā)會由于socket“有數據可讀”這個狀態(tài)沒有發(fā)生變化而不發(fā)出通知且陷入長時間的等待菇爪。
因此在使用邊緣觸發(fā)的API時算芯,要注意每次都要讀到socket返回EWOULDBLOCK為止。
遺憾的是不同的操作系統(tǒng)特供的epoll接口有非常大差異凳宙,所以使用類似于epoll的接口實現具有較好跨平臺能力的server會比較困難熙揍。然而幸運的是,有非常多高效的事件驅動庫能夠屏蔽上述的困難氏涩,常見的事件驅動庫有l(wèi)ibevent庫届囚,還有作為libevent替代者的libev庫。
這些庫會依據操作系統(tǒng)的特點選擇最合適的事件探測接口是尖,而且增加了信號(signal)等技術以支持異步響應意系,這使得這些庫成為構建事件驅動模型的不二選擇。
下章將介紹怎樣使用libev庫替換select或epoll接口饺汹,實現高效穩(wěn)定的server模型蛔添。
五、libevent方法
libevent是一個事件觸發(fā)的網絡庫兜辞,適用于windows迎瞧、linux、bsd等多種平臺逸吵,內部使用select夹攒、epoll、kqueue等系統(tǒng)調用管理事件機制胁塞。著名分布式緩存軟件memcached也是libevent-based咏尝,并且libevent在使用上能夠做到跨平臺压语。并且依據libevent官方站點上發(fā)布的數據統(tǒng)計,似乎也有著非凡的性能编检。
libevent庫實際上沒有更換select()胎食、poll()或其它機制的基礎,而是使用對于每一個平臺最高效的高性能解決方式在實現外加上一個包裝器允懂。
為了實際處理每一個請求厕怜,libevent庫提供一種事件機制,它作為底層網絡后端的包裝器蕾总。事件系統(tǒng)讓為連接加入處理函數變得很簡便粥航,同一時候減少了底層I/O復雜性。這是libevent系統(tǒng)的核心生百。
libevent庫的其它組件提供其它功能:包含緩沖的事件系統(tǒng)(用于緩沖發(fā)送到client/從client接收的數據)以及 HTTP递雀、DNS 和RPC系統(tǒng)的核心實現。
5.1 libevent的特點與優(yōu)勢
- 事件驅動蚀浆,高性能缀程;
- 輕量級,專注于網絡市俊;
- 跨平臺杨凑,支持 Windows、Linux摆昧、Mac Os等撩满;
- 支持多種I/O多路復用技術,epoll绅你、poll鹦牛、dev/poll、select和kqueue等勇吊;
- 支持I/O、定時器和信號等事件窍仰。
5.2 libevent組成部分
- event及event_base事件管理包含各種IO(socket)汉规、定時器、信號等事件驹吮,也是libevent應用最廣的模塊针史;
- evbuffer event及event_base緩存管理是指evbuffer功能,提供了高效的讀寫方法碟狞;
- evdns DNS是libevent提供的一個異步DNS查詢功能啄枕;
- evhttp HTTP是libevent的一個輕量級http實現,包含server和client族沃。
libevent也支持ssl频祝,這對于有安全需求的網絡程序非常的重要泌参。可是其支持不是非常完好常空,比方http server的實現就不支持ssl沽一。
5.3 事件處理框架
libevent是事件驅動的庫,所謂事件驅動漓糙,簡單地說就是你點什么button(即產生什么事件)铣缠,電腦運行什么操作(即調用什么函數)。
Libevent框架本質上是一個典型的Reactor模式昆禽,所以僅僅須要弄懂Reactor模型蝗蛙,libevent就八九不離十了。Reactor模式醉鳖,也是一種事件驅動機制捡硅。
應用程序須要提供對應的接口并注冊到Reactor上,假設對應的事件發(fā)生辐棒,Reactor將主動調用應用程序注冊的接口病曾,這些接口又稱為“回調函數”。在Libevent中也是一樣漾根,向Libevent框架注冊對應的事件和回調函數泰涂;當這些事件發(fā)生時,Libevent會調用這些回調函數處理對應的事件(I/O讀寫辐怕、定時和信號)逼蒙。
使用Reactor模型,必備的幾個組件:事件源寄疏、Reactor框架是牢、多路復用機制和事件處理程序。先來看看Reactor模型的總體框架陕截,接下來再對每一個組件做逐一說明驳棱。
- 事件源:Linux上是文件描寫敘述符,Windows上就是Socket或者Handle了农曲,這里統(tǒng)一稱為“句柄集”社搅;程序在指定的句柄上注冊關心的事件,比方I/O事件乳规;
- event demultiplexer(事件多路分發(fā)機制):由操作系統(tǒng)提供的I/O多路復用機制形葬,比方select和epoll;程序首先將其關心的句柄(事件源)及其事件注冊到event demultiplexer上暮的,當有事件到達時笙以,event demultiplexer會發(fā)出通知“在已經注冊的句柄集中,一個或多個句柄的事件已經就緒”冻辩;程序收到通知后猖腕,就能夠在非堵塞的情況下對事件進行處理了拆祈;在libevent中,依舊是select谈息、poll缘屹、epoll等,可是libevent使用結構體eventop進行了封裝侠仇,以統(tǒng)一的接口來支持這些I/O多路復用機制轻姿,達到了對外隱藏底層系統(tǒng)機制的目的。
- Reactor(反應器):Reactor逻炊,是事件管理的接口互亮,內部使用event demultiplexer注冊、注銷事件余素;并執(zhí)行事件循環(huán)豹休;當有事件進入“就緒”狀態(tài)時,調用注冊事件的回調函數處理事件桨吊;在libevent中威根,就是event_base結構體。
- Event Handler(事件處理程序):事件處理程序提供了一組接口视乐,每一個接口對應了一種類型的事件洛搀;供Reactor在對應的事件發(fā)生時調用,運行對應的事件處理佑淀,通常它會綁定一個有效的句柄留美;對應libevent中,就是event結構體伸刃。
結合Reactor框架谎砾,我們來理一下libevent的事件處理流程,如下圖所示:
- event_init()初始化
首先要隆重介紹event_base對象(結構體):
struct event_base {
const struct eventop *evsel;
void *evbase;
int event_count; /* counts number of total events */
int event_count_active; /* counts number of active events */
int event_gotterm; /* Set to terminate loop */
/* active event management */
struct event_list **activequeues;
int nactivequeues;
struct event_list eventqueue;
struct timeval event_tv;
RB_HEAD(event_tree, event) timetree;
};
event_base對象整合了事件處理的一些全局變量捧颅,角色是event對象的"總管家", 他包含了:
- 事件引擎函數對象(evsel, evbase)景图;
- 當前入列事件列表(event_count, event_count_active, eventqueue);
- 全局終止信號(event_gotterm)碉哑;
- 活躍事件列表(avtivequeues)挚币;
- 事件隊列樹(timetree)...。
初始化時創(chuàng)建event_base對象谭梗,選擇當前OS支持的事件引擎(epoll, poll, select...)并初始化,創(chuàng)建全局信號隊列(signalqueue)宛蚓,活躍隊列的內存分配(依據設置的priority個數激捏,默覺得1)。
- event_set()
event_set來設置event對象凄吏,包含全部者event_base對象远舅、fd闰蛔、事件(EV_READ|EV_WRITE|EV_PERSIST),回掉函數和參數图柏,事件優(yōu)先級是當前event_base的中間級別(current_base->nactivequeues/2)序六。
設置監(jiān)視事件后,事件處理函數能夠僅僅被調用一次或總被調用蚤吹。
- 僅僅調用一次:事件處理函數被調用后例诀,即從事件隊列中刪除。須要在事件處理函數中再次增加事件裁着,才干在下次事件發(fā)生時被調用繁涂;
- 總被調用:設置為EV_PERSIST,僅僅增加一次二驰,處理函數總被調用扔罪,除非採用event_remove顯式地刪除。
- event_add()
int event_add(struct event *ev, struct timeval *tv)
這個接口有兩個參數桶雀,第一個是要加入的事件矿酵,第二個參數作為事件的超時值(timer)。假設該值非NULL矗积,在加入本事件的同一時候加入超時事件(EV_TIMEOUT)到時間隊列樹(timetree)全肮,依據事件類型處理例如以下:
- EV_READ => EVLIST_INSERTED => eventqueue
- EV_WRITE => EVLIST_INSERTED => eventqueue
- EV_TIMEOUT => EVLIST_TIMEOUT => timetree
- EV_SIGNAL => EVLIST_SIGNAL => signalqueue
- event_base_loop()
這里是事件的主循環(huán),僅僅要flags不是設置為EVLOOP_NONBLOCK漠魏,該函數就會一直循環(huán)監(jiān)聽事件/處理事件倔矾。每次循環(huán)過程中,都會處理當前觸發(fā)(活躍)事件:
- 檢測當前是否有信號處理(gotterm柱锹、gotsig)哪自,這些都是全局參數,不適合多線程;
- 時間更新禁熏,找到離當前近期的時間事件壤巷,得到相對超時事件tv;
- 調用事件引擎的dispatch wait事件觸發(fā)瞧毙,超時值為tv胧华,觸發(fā)事件加入到activequeues;
- 處理活躍事件宙彪,調用caller的callbacks (event_process_acitve)矩动。
5.4 libevent典型應用的大致流程
創(chuàng)建libevent server的基本方法是,注冊當發(fā)生某一操作(比方接受來自client的連接)時應該運行的函數释漆,然后調用主事件循環(huán)event_dispatch()悲没。運行過程的控制如今由libevent系統(tǒng)處理。
注冊事件和將調用的函數之后男图,事件系統(tǒng)開始自治示姿。在應用程序執(zhí)行時甜橱,能夠在事件隊列中加入(注冊)或刪除(取消注冊)事件。事件注冊很方便栈戳,能夠通過它加入新事件以處理新打開的連接岂傲,從而構建靈活的網絡處理系統(tǒng):
(環(huán)境設置)-> (創(chuàng)建event_base) -> (注冊event,將此event增加到event_base中) -> (設置event各種屬性子檀、事件等) -> (將event增加事件列表 addevent) -> (開始事件監(jiān)視循環(huán)镊掖、分發(fā)dispatch)
5.5 示例
打開一個監(jiān)聽套接字,然后注冊一個回調函數命锄,每當須要調用accept()函數以打開新連接時調用這個回調函數堰乔,這樣就創(chuàng)建了一個網絡server。例如以下所看到的的代碼片段說明基本過程脐恩。
#include <stdio.h>
#include <string.h>
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <event.h>
using namespace std;
// 事件base
struct event_base* base;
// 讀事件回調函數
void onRead(int iCliFd, short iEvent, void *arg)
{
int iLen;
char buf[1500];
iLen = recv(iCliFd, buf, 1500, 0);
if (iLen <= 0) {
cout << "Client Close" << endl;
// 連接結束(=0)或連接錯誤(<0)镐侯。將事件刪除并釋放內存空間
struct event *pEvRead = (struct event*)arg;
event_del(pEvRead);
delete pEvRead;
close(iCliFd);
return;
}
buf[iLen] = 0;
cout << "Client Info:" << buf << endl;
}
// 連接請求事件回調函數
void onAccept(int iSvrFd, short iEvent, void *arg)
{
int iCliFd;
struct sockaddr_in sCliAddr;
socklen_t iSinSize = sizeof(sCliAddr);
iCliFd = accept(iSvrFd, (struct sockaddr*)&sCliAddr, &iSinSize);
// 連接注冊為新事件 (EV_PERSIST為事件觸發(fā)后不默認刪除)
struct event *pEvRead = new event;
event_set(pEvRead, iCliFd, EV_READ|EV_PERSIST, onRead, pEvRead);
event_base_set(base, pEvRead);
event_add(pEvRead, NULL);
}
int main()
{
int iSvrFd;
struct sockaddr_in sSvrAddr;
memset(&sSvrAddr, 0, sizeof(sSvrAddr));
sSvrAddr.sin_family = AF_INET;
sSvrAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
sSvrAddr.sin_port = htons(8888);
// 創(chuàng)建tcpSocket(iSvrFd),監(jiān)聽本機8888端口
iSvrFd = socket(AF_INET, SOCK_STREAM, 0);
bind(iSvrFd, (struct sockaddr*)&sSvrAddr, sizeof(sSvrAddr));
listen(iSvrFd, 10);
// 初始化base
base = event_base_new();
struct event evListen;
// 設置事件
event_set(&evListen, iSvrFd, EV_READ|EV_PERSIST, onAccept, NULL);
// 設置為base事件
event_base_set(base, &evListen);
// 加入事件
event_add(&evListen, NULL);
// 事件循環(huán)
event_base_dispatch(base);
return 0;
}
event_set()函數創(chuàng)建新的事件結構驶冒;event_add()在事件隊列機制中加入事件苟翻;然后,event_dispatch()啟動事件隊列系統(tǒng)骗污,開始監(jiān)聽(并接收)請求崇猫。
六、libev庫
官方文檔:http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod
與libevent一樣需忿,libev系統(tǒng)也是基于事件循環(huán)的系統(tǒng)诅炉,它在poll()、select()等機制的本機實現的基礎上提供基于事件的循環(huán)屋厘。
libev是libevent之后的一個事件驅動的編程框架涕烧。其接口和libevent基本類似。據官方介紹汗洒,其性能比libevent還要高议纯,bug比libevent還少。
libev API比較原始溢谤,沒有HTTP包裝器瞻凤,可是libev支持在實現中內置很多其它事件類型。比如世杀,一種evstat實現能夠監(jiān)視多個文件的屬性變動阀参,能夠在例4所看到的HTTP文件解決方式中使用它。
可是瞻坝,libevent和libev的基本過程是同樣的蛛壳。創(chuàng)建所需的網絡監(jiān)聽套接字,注冊在運行期間要調用的事件。然后啟動主事件循環(huán)炕吸,讓libev處理過程的其余部分。
Libev是一個event loop:向libev注冊感興趣的events勉痴,比如Socket可讀事件赫模,libev會對所注冊的事件的源進行管理,并在事件發(fā)生時觸發(fā)對應的程序蒸矛。
事件驅動框架:定義一個監(jiān)控器瀑罗、書寫觸發(fā)動作邏輯、初始化監(jiān)控器雏掠、設置監(jiān)控器觸發(fā)條件斩祭、將監(jiān)控器增加大事件驅動器的循環(huán)中就可以。
libev的事件驅動過程能夠想象成例如以下的偽代碼:
do_some_init()
is_run = True
while is_run:
t = caculate_loop_time()
deal_loop(t)
deal_with_pending_event()
do_some_clear()
首先做一些初始化操作乡话,然后進入到循環(huán)中摧玫,該循環(huán)通過一個狀態(tài)位來控制是否運行。在循環(huán)中绑青。計算出下一次輪詢的時間诬像,這里輪詢的實現就採用了系統(tǒng)提供的epoll、kqueue等機制闸婴。再輪詢結束后檢查有哪些監(jiān)控器的被觸發(fā)了坏挠,依次運行觸發(fā)動作。
Libev除了提供了主要的三大類事件(IO事件邪乍、定時器事件、信號事件)外還提供了周期事件、子進程事件巫玻、文件狀態(tài)改變事件等多個事件极祸。libev所實現的功能就是一個強大的reactor,可能notify事件主要包含以下這些:
- ev_io // IO可讀可寫
- ev_stat // 文件屬性變化
- ev_async // 激活線程
- ev_signal // 信號處理
- ev_timer // 定時器
- ev_periodic // 周期任務
- ev_child // 子進程狀態(tài)變化
- ev_fork // 開辟進程
- ev_cleanup // event loop退出觸發(fā)事件
- ev_idle // 每次event loop空暇觸發(fā)事件
- ev_embed // TODO(zhangyan04):I have no idea.
- ev_prepare // 每次event loop之前事件
- ev_check // 每次event loop之后事件
libev相同須要循環(huán)探測事件是否產生姐刁,Libev的循環(huán)體用ev_loop結構來表達芥牌,并用ev_loop()來啟動。
void ev_loop(ev_loop* loop, int flags);
Libev支持八種事件類型聂使,當中包含IO事件壁拉。一個IO事件用ev_io來表征,并用ev_io_init()函數來初始化:
void ev_io_init(ev_io *io, callback, int fd, int events);
初始化內容包含回調函數callback柏靶,被探測的句柄fd和須要探測的事件弃理。EV_READ表“可讀事件”。EV_WRITE 表“可寫事件”屎蜓。如今痘昌,用戶須要做的不過在合適的時候,將某些ev_io從ev_loop增加或剔除。一旦增加辆苔,下個循環(huán)即會檢查ev_io所指定的事件有否發(fā)生算灸;假設該事件被探測到,則ev_loop會自己主動運行ev_io的回調函數callback()驻啤;假設ev_io被注銷菲驴,則不再檢測相應事件。
不管某ev_loop啟動與否骑冗,都能夠對其加入或刪除一個或多個ev_io赊瞬,加入刪除的接口是ev_io_start()和ev_io_stop()。
void ev_io_start(ev_loop *loop, ev_io* io);
void ev_io_stop(EV_A_* );
由此贼涩,我們能夠easy得出例如以下的“一問一答”的server模型巧涧。因為沒有考慮server端主動終止連接機制,所以各個連接能夠維持隨意時間遥倦,client能夠自由選擇退出時機谤绳。IO事件、定時器事件袒哥、信號事件:
#include<ev.h>
#include <stdio.h>
#include <signal.h>
#include <sys/unistd.h>
ev_io io_w;
ev_timer timer_w;
ev_signal signal_w;
void io_action(struct ev_loop *main_loop,ev_io *io_w,int e)
{
int rst;
char buf[1024] = {''};
puts("in io cb\n");
read(STDIN_FILENO,buf,sizeof(buf));
buf[1023] = '';
printf("Read in a string %s \n",buf);
ev_io_stop(main_loop,io_w);
}
void timer_action(struct ev_loop *main_loop,ev_timer *timer_w,int e)
{
puts("in tiemr cb \n");
ev_timer_stop(main_loop,io_w);
}
void signal_action(struct ev_loop *main_loop,ev_signal signal_w,int e)
{
puts("in signal cb \n");
ev_signal_stop(main_loop,io_w);
ev_break(main_loop,EVBREAK_ALL);
}
int main(int argc ,char *argv[])
{
struct ev_loop *main_loop = ev_default_loop(0);
ev_init(&io_w,io_action);
ev_io_set(&io_w,STDIN_FILENO,EV_READ);
ev_init(&timer_w,timer_action);
ev_timer_set(&timer_w,2,0);
ev_init(&signal_w,signal_action);
ev_signal_set(&signal_w,SIGINT);
ev_io_start(main_loop,&io_w);
ev_timer_start(main_loop,&timer_w);
ev_signal_start(main_loop,&signal_w);
ev_run(main_loop,0);
return 0;
}
這里使用了3種事件監(jiān)控器闷供,分別監(jiān)控IO事件、定時器事件以及信號事件统诺。因此定義了3個監(jiān)控器(watcher)歪脏,以及觸發(fā)監(jiān)控器時要運行動作的回調函數。Libev定義了多種監(jiān)控器粮呢,命名方式為ev_xxx這里xxx代表監(jiān)控器類型婿失,事實上現是一個結構體。
typedef struct ev_io
{
....
} ev_io;
通過宏定義能夠簡寫為ev_xxx啄寡『拦瑁回調函數的類型為:
void cb_name(struct ev_loop *main_loop,ev_xxx *io_w,int event);
在main中,首先定義了一個事件驅動器的結構struct ev_loop *main_loop這里調用ev_default_loop(0)生成一個預制的全局驅動器挺物,這里能夠參考Manual中的選擇懒浮。
然后依次初始化各個監(jiān)控器以及設置監(jiān)控器的觸發(fā)條件,初始化監(jiān)控器的過程是將對應的回調函數即觸發(fā)時的動作注冊到監(jiān)控器上识藤。
設置觸發(fā)條件則是該條件產生時才去運行注冊到監(jiān)控器上的動作砚著。對于IO事件,通常是設置特定fd上的可讀或可寫事件痴昧,定時器則是多久后觸發(fā)稽穆。這里定時器的觸發(fā)條件中還有第三參數,表示第一次觸發(fā)后赶撰,是否循環(huán):若為0則不循環(huán)舌镶,否則按該值循環(huán)柱彻。信號觸發(fā)器則是設置觸發(fā)的信號。
在初始化并設置好觸發(fā)條件后餐胀,先調用ev_xxx_start將監(jiān)控器注冊到事件驅動器上哟楷,接著調用ev_run開始事件驅動器。
上述模型能夠接受隨意多個連接否灾,且為各個連接提供全然獨立的問答服務吓蘑。借助libev提供的事件循環(huán)/事件驅動接口,上述模型有機會具備其它模型不能提供的高效率坟冲、低資源占用、穩(wěn)定性好和編寫簡單等特點溃蔫。
因為傳統(tǒng)的web server健提、ftp server及其它網絡應用程序都具有“一問一答”的通訊邏輯,所以上述使用libev庫的“一問一答”模型對構建類似的server程序具有參考價值伟叛;另外私痹,對于須要實現遠程監(jiān)視或遠程遙控的應用程序,上述模型相同提供了一個可行的實現方案统刮。
PHP使用了libev擴展的socket:
<?php
/* 使用異步io訪問socket Use some async I/O to access a socket */
// `sockets' extension still logs warnings
// for EINPROGRESS, EAGAIN/EWOULDBLOCK etc.
error_reporting(E_ERROR);
$e_nonblocking = array (/*EAGAIN or EWOULDBLOCK*/11, /*EINPROGRESS*/115);
// Get the port for the WWW service
$service_port = getservbyname('www', 'tcp');
// Get the IP address for the target host
$address = gethostbyname('google.co.uk');
// Create a TCP/IP socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socket === FALSE) {
echo \"socket_create() failed: reason: \"
.socket_strerror(socket_last_error()) . \"n\";
}
// Set O_NONBLOCK flag
socket_set_nonblock($socket);
// Abort on timeout
$timeout_watcher = new EvTimer(10.0, 0., function () use ($socket) {
socket_close($socket);
Ev::stop(Ev::BREAK_ALL);
});
// Make HEAD request when the socket is writable
$write_watcher = new EvIo($socket, Ev::WRITE, function ($w)
use ($socket, $timeout_watcher, $e_nonblocking) {
// Stop timeout watcher
$timeout_watcher->stop();
// Stop write watcher
$w->stop();
$in = \"HEAD / HTTP/1.1rn\";
$in .= \"Host: google.co.ukrn\";
$in .= \"Connection: Closernrn\";
if (!socket_write($socket, $in, strlen($in))) {
trigger_error(\"Failed writing $in to socket\", E_USER_ERROR);
}
$read_watcher = new EvIo($socket, Ev::READ, function ($w, $re)
use ($socket, $e_nonblocking) {
// Socket is readable. recv() 20 bytes using non-blocking mode
$ret = socket_recv($socket, $out, 20, MSG_DONTWAIT);
if ($ret) {
echo $out;
} elseif ($ret === 0) {
// All read
$w->stop();
socket_close($socket);
return;
}
// Caught EINPROGRESS, EAGAIN, or EWOULDBLOCK
if (in_array(socket_last_error(), $e_nonblocking)) {
return;
}
$w->stop();
socket_close($socket);
});
Ev::run();
});
$result = socket_connect($socket, $address, $service_port);
Ev::run();
?>
七紊遵、總結
libevent和libev都提供靈活且強大的環(huán)境,支持為處理server端或client請求實現高性能網絡(和其它 I/O)接口侥蒙。目標是以高效(CPU/RAM使用量低)的方式支持數千甚至數萬個連接暗膜。
在本文中,您看到了一些演示樣例鞭衩,包含libevent中內置的HTTP服務学搜,能夠使用這些技術支持基于IBM Cloud、EC2或AJAX的web應用程序论衍。