轉(zhuǎn)自:http://hi.baidu.com/galoisxin/item/1761410b29778990a3df43b8 作者的博客內(nèi)容很豐富很有價(jià)值:http://hi.baidu.com/new/galoisxin 互聯(lián)網(wǎng)時(shí)代,幾乎所有大型應(yīng)用都或多或少與網(wǎng)絡(luò)有關(guān),而I/O模型又是一切網(wǎng)絡(luò)編程的基礎(chǔ)。本文以筆者熟悉的Unix/Linux接口為例,著重介紹了幾種常用的I/O模型,希望能給各位服務(wù)器領(lǐng)域的同仁提供參考價(jià)值。
阻塞型的網(wǎng)絡(luò)編程接口
初學(xué)Socket編程,大家都是從 listen()、send()、recv() 等接口開始的。使用這些接口可以實(shí)現(xiàn)一個(gè)非常簡(jiǎn)單的網(wǎng)絡(luò)通訊程序,如下圖所示。 ![]() 在這個(gè)線程 / 時(shí)間圖例中,黑色的粗線代表線程的等待時(shí)間,或者說成是被阻塞的。我們注意到accept()、send()、recv()這幾個(gè)接口在調(diào)用時(shí)都發(fā)生了阻塞。實(shí)際上,在缺省狀態(tài)下,socket 接口都是阻塞型的。這意味著當(dāng)一個(gè)socket調(diào)用不能立即完成時(shí),進(jìn)程或線程將進(jìn)入睡眠狀態(tài),等待操作完成。這給網(wǎng)絡(luò)編程帶來了一個(gè)很大的問題,如在調(diào)用 send() 的同時(shí),線程將被阻塞,在此期間,線程將無法執(zhí)行任何運(yùn)算或響應(yīng)任何的網(wǎng)絡(luò)請(qǐng)求。這給多客戶機(jī)、多業(yè)務(wù)邏輯的網(wǎng)絡(luò)編程帶來了挑戰(zhàn)。要解決這個(gè)問題,我們自然而然的想到多線程。 多線程的服務(wù)器程序 應(yīng)對(duì)多客戶機(jī)的網(wǎng)絡(luò)應(yīng)用,最簡(jiǎn)單的解決方式是在服務(wù)器端使用多線程(或多進(jìn)程)。多線程(或多進(jìn)程)的目的是讓每個(gè)連接都擁有獨(dú)立的線程(或進(jìn)程),這樣任何一個(gè)連接的阻塞都不會(huì)影響其他的連接。 具體使用多進(jìn)程還是多線程,并沒有一個(gè)特定的模式。傳統(tǒng)意義上,進(jìn)程的開銷要遠(yuǎn)遠(yuǎn)大于線程,所以,如果需要同時(shí)為較多的客戶機(jī)提供服務(wù),則不推薦使用多進(jìn)程;如果單個(gè)服務(wù)執(zhí)行體需要消耗較多的 CPU 資源,譬如需要進(jìn)行大規(guī)?;蜷L(zhǎng)時(shí)間的數(shù)據(jù)運(yùn)算或文件訪問,則進(jìn)程較為安全。在Unix/Linux環(huán)境中,使用 pthread_create () 創(chuàng)建新線程,fork() 創(chuàng)建新進(jìn)程。 我們假設(shè)對(duì)上述的服務(wù)器 / 客戶機(jī)模型,提出更高的要求,即讓服務(wù)器同時(shí)為多個(gè)客戶機(jī)提供一問一答的服務(wù)。于是有了如下的模型。 ![]() 在上述的線程 / 時(shí)間圖例中,主線程持續(xù)等待客戶端的連接請(qǐng)求,如果有連接,則創(chuàng)建新線程,并在新線程中提供為前例同樣的問答服務(wù)。很多初學(xué)者可能不明白為何一個(gè) socket 可以 accept 多次。實(shí)際上,socket 的設(shè)計(jì)者可能特意為多客戶機(jī)的情況留下了伏筆,讓 accept() 能夠返回一個(gè)新的 socket。下面是 accept 接口的原型: int accept(int s, struct sockaddr *addr, socklen_t *addrlen);輸入?yún)?shù) s 是從 socket(),bind() 和 listen() 中沿用下來的 socket 句柄值。執(zhí)行完 bind() 和 listen() 后,操作系統(tǒng)已經(jīng)開始在指定的端口處監(jiān)聽所有的連接請(qǐng)求,如果有請(qǐng)求,則將該連接請(qǐng)求加入請(qǐng)求隊(duì)列。調(diào)用 accept() 接口正是從 socket s 的請(qǐng)求隊(duì)列抽取第一個(gè)連接信息,創(chuàng)建一個(gè)與 s 同類的新的 socket 返回句柄。新的 socket 句柄即是后續(xù) read() 和 recv() 的輸入?yún)?shù)。如果請(qǐng)求隊(duì)列當(dāng)前沒有請(qǐng)求,則 accept() 將進(jìn)入阻塞狀態(tài)直到有請(qǐng)求進(jìn)入隊(duì)列。 上述多線程的服務(wù)器模型似乎完美的解決了為多個(gè)客戶機(jī)提供問答服務(wù)的要求,但其實(shí)并不盡然。如果要同時(shí)響應(yīng)成百上千路的連接請(qǐng)求,則無論多線程還是多進(jìn)程都會(huì)嚴(yán)重占據(jù)系統(tǒng)資源,降低系統(tǒng)對(duì)外界響應(yīng)效率,而線程與進(jìn)程本身也更容易進(jìn)入假死狀態(tài)。 很多程序員可能會(huì)考慮使用“線程池”或“連接池”。“線程池”旨在減少創(chuàng)建和銷毀線程的頻率,其維持一定合理數(shù)量的線程,并讓空閑的線程重新承擔(dān)新的執(zhí)行任務(wù)。“連接池”維持連接的緩存池,盡量重用已有的連接、減少創(chuàng)建和關(guān)閉連接的頻率。這兩種技術(shù)都可以很好的降低系統(tǒng)開銷,都被廣泛應(yīng)用很多大型系統(tǒng),如 websphere、tomcat 和各種數(shù)據(jù)庫等。 但是,“線程池”和“連接池”技術(shù)也只是在一定程度上緩解了頻繁調(diào)用 IO 接口帶來的資源占用。而且,所謂“池”始終有其上限,當(dāng)請(qǐng)求大大超過上限時(shí),“池”構(gòu)成的系統(tǒng)對(duì)外界的響應(yīng)并不比沒有池的時(shí)候效果好多少。所以使用“池”必須考慮其面臨的響應(yīng)規(guī)模,并根據(jù)響應(yīng)規(guī)模調(diào)整“池”的大小。 對(duì)應(yīng)上例中的所面臨的可能同時(shí)出現(xiàn)的上千甚至上萬次的客戶端請(qǐng)求,“線程池”或“連接池”或許可以緩解部分壓力,但是不能解決所有問題。 總之,多線程模型可以方便高效的解決小規(guī)模的服務(wù)請(qǐng)求,但面對(duì)大規(guī)模的服務(wù)請(qǐng)求,多線程模型并不是最佳方案。 附:一個(gè)簡(jiǎn)單的多進(jìn)程并發(fā)服務(wù)器demo
/* ========================================================================== Purpose: * Fork模型 TCP并發(fā)服務(wù)器 Demo Notes: * 采用TCP短連接,服務(wù)器將接收到從不同客戶端傳來的消息并打印到終端 * 編譯: gcc fork.c -o fork Author: * Shelley Date: * 23th December 2010 Updates: * ========================================================================== */
#include <netinet/in.h> // for sockaddr_in #include <sys/types.h> // for socket #include <sys/socket.h> // for socket #include <stdio.h> // for printf #include <stdlib.h> // for exit #include <string.h> // for bzero #include <unistd.h> // for fork #include <sys/signal.h> // for signal #include <sys/wait.h> // for wait
#define HELLO_WORLD_SERVER_PORT 8888 #define LENGTH_OF_LISTEN_QUEUE 20 #define BUFFER_SIZE 1024 void reaper(int sig) { int status; //調(diào)用wait3讀取子進(jìn)程的返回值,使zombie狀態(tài)的子進(jìn)程徹底釋放 while(wait3(&status,WNOHANG,(struct rusage*)0) >=0) ; } int main(int argc, char **argv) { //設(shè)置一個(gè)socket地址結(jié)構(gòu)server_addr,代表服務(wù)器internet地址, 端口 struct sockaddr_in server_addr; bzero(&server_addr,sizeof(server_addr)); //把一段內(nèi)存區(qū)的內(nèi)容全部設(shè)置為0 server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htons(INADDR_ANY); server_addr.sin_port = htons(HELLO_WORLD_SERVER_PORT);
//創(chuàng)建用于internet的流協(xié)議(TCP)socket,用server_socket代表服務(wù)器socket int server_socket = socket(AF_INET,SOCK_STREAM,0); if( server_socket < 0) { printf("Create Socket Failed!"); exit(1); }
//把socket和socket地址結(jié)構(gòu)聯(lián)系起來 if( bind(server_socket,(struct sockaddr*)&server_addr,sizeof(server_addr))) { printf("Server Bind Port : %d Failed!", HELLO_WORLD_SERVER_PORT); exit(1); }
//server_socket用于監(jiān)聽 if ( listen(server_socket, LENGTH_OF_LISTEN_QUEUE) ) { printf("Server Listen Failed!"); exit(1); } //通知操作系統(tǒng),當(dāng)收到子進(jìn)程的退出信號(hào)(SIGCHLD)時(shí),執(zhí)行reaper函數(shù),釋放zombie狀態(tài)的進(jìn)程 (void)signal(SIGCHLD,reaper);
while (1) //服務(wù)器端要一直運(yùn)行 { //定義客戶端的socket地址結(jié)構(gòu)client_addr struct sockaddr_in client_addr; socklen_t length = sizeof(client_addr);
int new_server_socket;
//接受一個(gè)到server_socket代表的socket的一個(gè)連接 //如果沒有連接請(qǐng)求,就等待到有連接請(qǐng)求--這是accept函數(shù)的特性 //accept函數(shù)返回一個(gè)新的socket,這個(gè)socket(new_server_socket)用于同連接到的客戶的通信 //new_server_socket代表了服務(wù)器和客戶端之間的一個(gè)通信通道 //accept函數(shù)把連接到的客戶端信息填寫到客戶端的socket地址結(jié)構(gòu)client_addr中
new_server_socket = accept(server_socket,(struct sockaddr*)&client_addr,&length); if ( new_server_socket < 0) { printf("Server Accept Failed!\n"); break; }
int child_process_pid = fork(); //fork()后,子進(jìn)程是主進(jìn)程的拷貝 //在主進(jìn)程和子進(jìn)程中的區(qū)別是fork()的返回值不同. if(child_process_pid == 0 )//如果當(dāng)前進(jìn)程是子進(jìn)程,就執(zhí)行與客戶端的交互 { close(server_socket); //子進(jìn)程中不需要被復(fù)制過來的server_socket char buffer[BUFFER_SIZE]; bzero(buffer, BUFFER_SIZE); strcpy(buffer,"Hello,World! 從服務(wù)器來!"); strcat(buffer,"\n"); //C語言字符串連接 //發(fā)送buffer中的字符串到new_server_socket,實(shí)際是給客戶端 send(new_server_socket,buffer,BUFFER_SIZE,0);
bzero(buffer,BUFFER_SIZE); //接收客戶端發(fā)送來的信息到buffer中 length = recv(new_server_socket,buffer,BUFFER_SIZE,0); if (length < 0) { printf("Server Recieve Data Failed!\n"); exit(1); } printf("\n%s",buffer); //關(guān)閉與客戶端的連接 close(new_server_socket);//TCP短連接 exit(0); } else if(child_process_pid > 0) //如果當(dāng)前進(jìn)程是主進(jìn)程 close(new_server_socket); //主進(jìn)程中不需要用于同客戶端交互的new_server_socket } //關(guān)閉監(jiān)聽用的socket close(server_socket); return 0; } 非阻塞的服務(wù)器程序
以上面臨的很多問題,一定程度是 IO 接口的阻塞特性導(dǎo)致的。多線程是一個(gè)解決方案,還一個(gè)方案就是使用非阻塞的接口。非阻塞的接口相比于阻塞型接口的顯著差異在于,在被調(diào)用之后立即返回。使用如下的函數(shù)可以將某句柄 fd 設(shè)為非阻塞狀態(tài)。 fcntl( fd, F_SETFL, O_NONBLOCK );下面將給出只用一個(gè)線程,但能夠同時(shí)從多個(gè)連接中檢測(cè)數(shù)據(jù)是否送達(dá),并且接受數(shù)據(jù)。 ![]() 在非阻塞狀態(tài)下,recv() 接口在被調(diào)用后立即返回,返回值代表了不同的含義。如在本例中,recv() 返回值大于 0,表示接受數(shù)據(jù)完畢,返回值即是接受到的字節(jié)數(shù);recv() 返回 0,表示連接已經(jīng)正常斷開;recv() 返回 -1,且 errno 等于 EAGAIN,表示 recv 操作還沒執(zhí)行完成;recv() 返回 -1,且 errno 不等于 EAGAIN,表示 recv 操作遇到系統(tǒng)錯(cuò)誤 errno。 可以看到服務(wù)器線程可以通過循環(huán)調(diào)用 recv() 接口,可以在單個(gè)線程內(nèi)實(shí)現(xiàn)對(duì)所有連接的數(shù)據(jù)接收工作。 但是上述模型絕不被推薦。因?yàn)?,循環(huán)調(diào)用 recv() 將大幅度推高 CPU 占用率;此外,在這個(gè)方案中,recv() 更多的是起到檢測(cè)“操作是否完成”的作用,實(shí)際操作系統(tǒng)提供了更為高效的檢測(cè)“操作是否完成“作用的接口,例如 select()。 使用 select() 接口的基于事件驅(qū)動(dòng)的服務(wù)器模型 大部分 Unix/Linux 都支持 select 函數(shù),該函數(shù)用于探測(cè)多個(gè)文件句柄的狀態(tài)變化。下面給出 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 nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) 這里,fd_set 類型可以簡(jiǎn)單的理解為按 bit 位標(biāo)記句柄的隊(duì)列,例如要在某 fd_set 中標(biāo)記一個(gè)值為 16 的句柄,則該 fd_set 的第 16 個(gè) bit 位被標(biāo)記為 1。具體的置位、驗(yàn)證可使用 FD_SET、FD_ISSET 等宏實(shí)現(xiàn)。在 select() 函數(shù)中,readfds、writefds 和 exceptfds 同時(shí)作為輸入?yún)?shù)和輸出參數(shù)。如果輸入的 readfds 標(biāo)記了 16 號(hào)句柄,則 select() 將檢測(cè) 16 號(hào)句柄是否可讀。在 select() 返回后,可以通過檢查 readfds 有否標(biāo)記 16 號(hào)句柄,來判斷該“可讀”事件是否發(fā)生。另外,用戶可以設(shè)置 timeout 時(shí)間。 下面將重新模擬上例中從多個(gè)客戶端接收數(shù)據(jù)的模型。 ![]() 上述模型只是描述了使用 select() 接口同時(shí)從多個(gè)客戶端接收數(shù)據(jù)的過程;由于 select() 接口可以同時(shí)對(duì)多個(gè)句柄進(jìn)行讀狀態(tài)、寫狀態(tài)和錯(cuò)誤狀態(tài)的探測(cè),所以可以很容易構(gòu)建為多個(gè)客戶端提供獨(dú)立問答服務(wù)的服務(wù)器系統(tǒng)。 ![]() 這里需要指出的是,客戶端的一個(gè) connect() 操作,將在服務(wù)器端激發(fā)一個(gè)“可讀事件”,所以 select() 也能探測(cè)來自客戶端的 connect() 行為。 上述模型中,最關(guān)鍵的地方是如何動(dòng)態(tài)維護(hù) select() 的三個(gè)參數(shù) readfds、writefds 和 exceptfds。作為輸入?yún)?shù),readfds 應(yīng)該標(biāo)記所有的需要探測(cè)的“可讀事件”的句柄,其中永遠(yuǎn)包括那個(gè)探測(cè) connect() 的那個(gè)“母”句柄;同時(shí),writefds 和 exceptfds 應(yīng)該標(biāo)記所有需要探測(cè)的“可寫事件”和“錯(cuò)誤事件”的句柄 ( 使用 FD_SET() 標(biāo)記 )。 作為輸出參數(shù),readfds、writefds 和 exceptfds 中的保存了 select() 捕捉到的所有事件的句柄值。程序員需要檢查的所有的標(biāo)記位 ( 使用 FD_ISSET() 檢查 ),以確定到底哪些句柄發(fā)生了事件。 上述模型主要模擬的是“一問一答”的服務(wù)流程,所以,如果 select() 發(fā)現(xiàn)某句柄捕捉到了“可讀事件”,服務(wù)器程序應(yīng)及時(shí)做 recv() 操作,并根據(jù)接收到的數(shù)據(jù)準(zhǔn)備好待發(fā)送數(shù)據(jù),并將對(duì)應(yīng)的句柄值加入 writefds,準(zhǔn)備下一次的“可寫事件”的 select() 探測(cè)。同樣,如果 select() 發(fā)現(xiàn)某句柄捕捉到“可寫事件”,則程序應(yīng)及時(shí)做 send() 操作,并準(zhǔn)備好下一次的“可讀事件”探測(cè)準(zhǔn)備。下圖描述的是上述模型中的一個(gè)執(zhí)行周期。 ![]() 這種模型的特征在于每一個(gè)執(zhí)行周期都會(huì)探測(cè)一次或一組事件,一個(gè)特定的事件會(huì)觸發(fā)某個(gè)特定的響應(yīng)。我們可以將這種模型歸類為“事件驅(qū)動(dòng)模型”。 相比其他模型,使用 select() 的事件驅(qū)動(dòng)模型只用單線程(進(jìn)程)執(zhí)行,占用資源少,不消耗太多 CPU,同時(shí)能夠?yàn)槎嗫蛻舳颂峁┓?wù)。如果試圖建立一個(gè)簡(jiǎn)單的事件驅(qū)動(dòng)的服務(wù)器程序,這個(gè)模型有一定的參考價(jià)值。 但這個(gè)模型依舊有著很多問題。 首先,select() 接口并不是實(shí)現(xiàn)“事件驅(qū)動(dòng)”的最好選擇。因?yàn)楫?dāng)需要探測(cè)的句柄值較大時(shí),select() 接口本身需要消耗大量時(shí)間去輪詢各個(gè)句柄。很多操作系統(tǒng)提供了更為高效的接口,如 linux 提供了 epoll,BSD 提供了 kqueue,Solaris 提供了 /dev/poll …。如果需要實(shí)現(xiàn)更高效的服務(wù)器程序,類似 epoll 這樣的接口更被推薦。遺憾的是不同的操作系統(tǒng)特供的 epoll 接口有很大差異,所以使用類似于 epoll 的接口實(shí)現(xiàn)具有較好跨平臺(tái)能力的服務(wù)器會(huì)比較困難。 其次,該模型將事件探測(cè)和事件響應(yīng)夾雜在一起,一旦事件響應(yīng)的執(zhí)行體龐大,則對(duì)整個(gè)模型是災(zāi)難性的。如下例,龐大的執(zhí)行體 1 的將直接導(dǎo)致響應(yīng)事件 2 的執(zhí)行體遲遲得不到執(zhí)行,并在很大程度上降低了事件探測(cè)的及時(shí)性。 圖 7. 龐大的執(zhí)行體對(duì)使用 select() 的事件驅(qū)動(dòng)模型的影響 ![]() 幸運(yùn)的是,有很多高效的事件驅(qū)動(dòng)庫可以屏蔽上述的困難,常見的事件驅(qū)動(dòng)庫有 libevent 庫,還有作為 libevent 替代者的 libev 庫。這些庫會(huì)根據(jù)操作系統(tǒng)的特點(diǎn)選擇最合適的事件探測(cè)接口,并且加入了信號(hào) (signal) 等技術(shù)以支持異步響應(yīng),這使得這些庫成為構(gòu)建事件驅(qū)動(dòng)模型的不二選擇。下章將介紹如何使用 libev 庫替換 select 或 epoll 接口,實(shí)現(xiàn)高效穩(wěn)定的服務(wù)器模型。 使用事件驅(qū)動(dòng)庫 libev 的服務(wù)器模型 Libev 是一種高性能事件循環(huán) / 事件驅(qū)動(dòng)庫。作為 libevent 的替代作品,其第一個(gè)版本發(fā)布與 2007 年 11 月。Libev 的設(shè)計(jì)者聲稱 libev 擁有更快的速度,更小的體積,更多功能等優(yōu)勢(shì),這些優(yōu)勢(shì)在很多測(cè)評(píng)中得到了證明。正因?yàn)槠淞己玫男阅埽芏嘞到y(tǒng)開始使用 libev 庫。本章將介紹如何使用 Libev 實(shí)現(xiàn)提供問答服務(wù)的服務(wù)器。 (事實(shí)上,現(xiàn)存的事件循環(huán) / 事件驅(qū)動(dòng)庫有很多,作者也無意推薦讀者一定使用 libev 庫,而只是為了說明事件驅(qū)動(dòng)模型給網(wǎng)絡(luò)服務(wù)器編程帶來的便利和好處。大部分的事件驅(qū)動(dòng)庫都有著與 libev 庫相類似的接口,只要明白大致的原理,即可靈活挑選合適的庫。) 與前章的模型類似,libev 同樣需要循環(huán)探測(cè)事件是否產(chǎn)生。Libev 的循環(huán)體用 ev_loop 結(jié)構(gòu)來表達(dá),并用 ev_loop( ) 來啟動(dòng)。 void ev_loop( ev_loop* loop, int flags )Libev 支持八種事件類型,其中包括 IO 事件。一個(gè) IO 事件用 ev_io 來表征,并用 ev_io_init() 函數(shù)來初始化: void ev_io_init(ev_io *io, callback, int fd, int events)初始化內(nèi)容包括回調(diào)函數(shù) callback,被探測(cè)的句柄 fd 和需要探測(cè)的事件,EV_READ 表“可讀事件”,EV_WRITE 表“可寫事件”。 現(xiàn)在,用戶需要做的僅僅是在合適的時(shí)候,將某些 ev_io 從 ev_loop 加入或剔除。一旦加入,下個(gè)循環(huán)即會(huì)檢查 ev_io 所指定的事件有否發(fā)生;如果該事件被探測(cè)到,則 ev_loop 會(huì)自動(dòng)執(zhí)行 ev_io 的回調(diào)函數(shù) callback();如果 ev_io 被注銷,則不再檢測(cè)對(duì)應(yīng)事件。 無論某 ev_loop 啟動(dòng)與否,都可以對(duì)其添加或刪除一個(gè)或多個(gè) ev_io,添加刪除的接口是 ev_io_start() 和 ev_io_stop()。 void ev_io_start( ev_loop *loop, ev_io* io ) void ev_io_stop( EV_A_* )由此,我們可以容易得出如下的“一問一答”的服務(wù)器模型。由于沒有考慮服務(wù)器端主動(dòng)終止連接機(jī)制,所以各個(gè)連接可以維持任意時(shí)間,客戶端可以自由選擇退出時(shí)機(jī)。 圖 8. 使用 libev 庫的服務(wù)器模型 ![]() 上述模型可以接受任意多個(gè)連接,且為各個(gè)連接提供完全獨(dú)立的問答服務(wù)。借助 libev 提供的事件循環(huán) / 事件驅(qū)動(dòng)接口,上述模型有機(jī)會(huì)具備其他模型不能提供的高效率、低資源占用、穩(wěn)定性好和編寫簡(jiǎn)單等特點(diǎn)。 由于傳統(tǒng)的 web 服務(wù)器,ftp 服務(wù)器及其他網(wǎng)絡(luò)應(yīng)用程序都具有“一問一答”的通訊邏輯,所以上述使用 libev 庫的“一問一答”模型對(duì)構(gòu)建類似的服務(wù)器程序具有參考價(jià)值;另外,對(duì)于需要實(shí)現(xiàn)遠(yuǎn)程監(jiān)視或遠(yuǎn)程遙控的應(yīng)用程序,上述模型同樣提供了一個(gè)可行的實(shí)現(xiàn)方案。 查看評(píng)論
|
|