網絡編程中,使用多路IO複用的典型場合:
1.當客戶處理多個描述字時(交互式輸入以及網絡接口),必須使用IO複用。
2.一個客戶同時處理多個套接口。
3.一個tcp服務程序既要處理監聽套接口,又要處理鏈接套接口,通常須要用到IO複用。
4.若是一個服務器既要處理TCP,又要處理UDP,通常也須要用到IO複用。
5.若是一個服務器要處理多個服務或者多個協議,通常須要用到IO複用。
linux提供了select、poll、epoll等方法來實現IO複用,三者的原型以下:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); int poll(struct pollfd *fds, nfds_t nfds, int timeout); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
函數參數說明:linux
select | slect的第一個參數nfds爲fdset集合中最大描述符值加1,fdset是一個位數組,其大小限制爲 __FD_SETSIZE(1024),位數組的每一位表明其對應的描述符是否須要被檢查。編程
select的第二三四個參數表示須要關注讀、寫、錯誤事件的文件描述符位數組,這些參數既是輸入參數也是輸出數組 參數,可能會被內核修改用於標示哪些描述符上發生了關注的事件。因此每次調用select前都需從新初始化服務器 fdset。網絡
timeout參數爲超時時間,該結構會被內核修改,其值爲超時剩餘的時間。數據結構 |
poll | poll與select不一樣,經過一個pollfd數組向內核傳遞須要關注的事件,故沒有描述符個數的限制,pollfd中的 events字段和revents分別用於標示關注的事件和發生的事件,故pollfd數組只須要被初始化一次。socket
poll的實現機制與select相似,其對應內核中的sys_poll,只不過poll向內核傳遞pollfd數組,而後對pollfd中的tcp 每一個描述符進行poll,相比處理fdset來講,poll效率更高。函數
poll返回後,須要對pollfd中的每一個元素檢查其revents值,來得指事件是否發生。性能 |
epoll | epoll經過epoll_create建立一個用於epoll輪詢的描述符,經過epoll_ctl添加/修改/刪除事件,經過epoll_wait 檢查事件,epoll_wait的第二個參數用於存放結果。
epoll與select、poll不一樣,首先,其不用每次調用都向內核拷貝事件描述信息,在第一次調用後,事件信息就會 與對應的epoll描述符關聯起來。另外epoll不是經過輪詢,而是經過在等待的描述符上註冊回調函數,當事件發 生時,回調函數負責把發生的事件存儲在就緒事件鏈表中,最後寫到用戶空間。
epoll返回後,該參數指向的緩衝區中即爲發生的事件,對緩衝區中每一個元素進行處理便可,而不須要像poll、 select那樣進行輪詢檢查。 |
select、poll、epoll比較:
select | select本質上是經過設置或者檢查存放fd標誌位的數據結構來進行下一步處理。這樣所帶來的缺點是: 1.單個進程可監視的fd數量被限制。 2.須要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時複製開銷大。 3.對socket進行掃描時是線性掃描。 |
poll | poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,而後查詢每一個fd對應的設備狀態,若是設備就緒則在設備等待隊列中加入一項並繼續遍歷,若是遍歷完全部fd後沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒後它又要再次遍歷fd。這個過程經歷了屢次無謂的遍歷。 它沒有最大鏈接數的限制,緣由是它是基於鏈表來存儲的,可是一樣有一個缺點: 大量的fd的數組被總體複製於用戶態和內核地址空間之間,而無論這樣的複製是否是有意義。 poll還有一個特色是「水平觸發」,若是報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd。 |
epoll | epoll支持水平觸發和邊緣觸發,最大的特色在於邊緣觸發,它只告訴進程哪些fd剛剛變爲就需態,而且只會通知一次。 在前面說到的複製問題上,epoll使用mmap減小複製開銷。 還有一個特色是,epoll使用「事件」的就緒通知方式,經過epoll_ctl註冊fd,一旦該fd就緒,內核就會採用相似callback的回調機制來激活該fd,epoll_wait即可以收到通知。 |
進程所能打開的最大鏈接數:
select | 單個進程所能打開的最大鏈接數有FD_SETSIZE宏定義,其大小是32個整數的大小(在32位的機器上,大小就是32*32,同理64位機器上FD_SETSIZE爲32*64),固然咱們能夠對進行修改,而後從新編譯內核,可是性能可能會受到影響,這須要進一步的測試。 |
poll | poll本質上和select沒有區別,可是它沒有最大鏈接數的限制,緣由是它是基於鏈表來存儲的。 |
epoll | 雖然鏈接數有上限,可是很大,1G內存的機器上能夠打開10萬左右的鏈接,2G內存的機器能夠打開20萬左右的鏈接 |
FD劇增後帶來的IO效率問題:
select | 由於每次調用時都會對鏈接進行線性遍歷,因此隨着FD的增長會形成遍歷速度慢的「線性降低性能問題」。 |
poll | 同上 |
epoll | 由於epoll內核中實現是根據每一個fd上的callback函數來實現的,只有活躍的socket纔會主動調用callback,因此在活躍socket較少的狀況下,使用epoll沒有前面二者的線性降低的性能問題,可是全部socket都很活躍的狀況下,可能會有性能問題。 |
消息傳遞方式
select | 內核須要將消息傳遞到用戶空間,都須要內核拷貝動做 |
poll | 同上 |
epoll | epoll經過內核和用戶空間共享一塊內存來實現的。 |
Linux網絡編程(五)中用select實現了多路IO複用,若是用poll來實現的話。代碼以下:
服務器端功能:
使用單進程爲多個客戶端服務,接收到客戶端發來的一條消息後,將該消息原樣返回給客戶端。首先,創建一個監聽套接字來接收來自客戶端的鏈接。每當接收到一個鏈接後,將該鏈接套接字加入客戶端套接字數組,經過poll實現多路複用。每當poll返回時,檢查pollfd數組的狀態。並進行相應操做,若是是新的鏈接到來,則將新的鏈接套接字登記到pollfd數組,若是是已有客戶端鏈接套接字變爲可讀,則對相應客戶端進行響應。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <sys/types.h> #include <netdb.h> #include <errno.h> #include <poll.h>
#define OPEN_MAX 1113
#define SERV_PORT 2048
#define LISTENQ 32
#define MAXLINE 1024
int main(int argc, char **argv) { int i, maxi,listenfd, connfd, sockfd; int nready; ssize_t n; char buf[MAXLINE]; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; struct pollfd client[OPEN_MAX]; if((listenfd = socket(AF_INET, SOCK_STREAM,0))==-1){ fprintf(stderr,"Socket error:%s\n\a",strerror(errno)); exit(1); } /* 服務器端填充 sockaddr結構*/ memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); /* 捆綁listenfd描述符 */
if(bind(listenfd,(struct sockaddr*)(&servaddr),sizeof(struct sockaddr))==-1){ fprintf(stderr,"Bind error:%s\n\a",strerror(errno)); exit(1); } /* 監聽listenfd描述符*/
if(listen(listenfd,LISTENQ)==-1){ fprintf(stderr,"Listen error:%s\n\a",strerror(errno)); exit(1); } client[0].fd=listenfd; client[0].events=POLLRDNORM;/*等待普通數據可讀*/ maxi = 0; /*client數組索引*/
for (i = 1; i < FD_SETSIZE; i++) client[i].fd = -1; /* -1表明未使用*/
for ( ; ; ) { if((nready = poll(client, maxi+1, -1))<0){/*永遠等待*/ fprintf(stderr,"poll Error\n"); exit(1); } if (client[0].revents & POLLRDNORM){/*有新的客戶端鏈接到來*/ clilen = sizeof(cliaddr); if((connfd = accept(listenfd, (struct sockaddr *)&cliaddr,&clilen))<0){ fprintf(stderr,"accept Error\n"); continue; } char des[sizeof(cliaddr)]; inet_ntop(AF_INET, &cliaddr.sin_addr, des, sizeof(cliaddr)); printf("new client: %s, port %d\n",des,ntohs(cliaddr.sin_port)); for (i = 0; i < OPEN_MAX; i++) if (client[i].fd < 0) { client[i].fd = connfd; /*保存新的鏈接套接字*/
break; } if (i == OPEN_MAX){ fprintf(stderr,"too many clients"); exit(1); } client[i].events=POLLRDNORM; /*設置新套接字的普通數據可讀事件*/
if (i > maxi) maxi = i; /*當前client數組最大下標值*/
if (--nready <= 0) continue; /*可讀的套接字所有處理完了*/ } for (i = 1; i <= maxi; i++) { /*檢查全部已經鏈接的客戶端是否有數據可讀*/
if ( (sockfd = client[i].fd) < 0) continue; if (client[i].revents & (POLLRDNORM|POLLERR)){ if ((n = read(sockfd, buf, MAXLINE)) == 0){/*客戶端主動斷開了鏈接*/ close(sockfd); client[i].fd = -1;/*設置爲-1,表示未使用*/ } else if(n<0){/*小於0,是出錯的節奏*/
if(errno==ECONNRESET){/*客戶端發送了reset分節*/ close(sockfd); client[i].fd = -1; } else{ fprintf(stderr,"read error"); exit(1); } } else write(sockfd, buf, n); if (--nready <= 0) break; /*可讀的套接字所有處理完了*/ } } } }
由於客戶端代碼只須要同時處理來標準輸入是否可讀以及socket是否可讀兩路IO,所以仍然使用select時的客戶端程序。
本程序(客戶端)功能:
1.向服務器發起鏈接請求,並從標準輸入stdin獲取字符串,將字符串發往服務器。
2.從服務器中接收字符串,並將接收到的字符串輸出到標準輸出stdout.
=========================================================================
問題:因爲既要從標準輸入獲取數據,又要從鏈接套接字中讀取服務器發來的數據。
爲避免當套接字上發生了某些事件時,程序阻塞於fgets()調用,因爲這兩隻
須要處理兩路IO,所以程序客戶端程序仍然使用select實現多路IO複用,或等
待標準輸入,或等待套接口可讀。這樣一來,若服務器進程終止,客戶端能立刻獲得通知。
=========================================================================
對於客戶端套接口,須要處理如下三種狀況:
1.服務器端發送了數據過來,套接口變爲可讀,且read返回值大於0
2.服務器端發送了一個FIN(服務器進程終止),套接口變爲可讀,且read返回值等於0
3.服務器端發送了一個RST(服務器進程崩潰,且從新啓動,此時服務器程序已經不認
識以前創建好了的鏈接,因此發送一個RST給客戶端),套接口變爲可讀,且read返回-1
錯誤碼存放在了errno
代碼以下:
//使用多路複用select的客戶端程序
#include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <sys/types.h> #include <netdb.h>
#define SERV_PORT 2048
#define MAXLINE 1024
#define max(x,y) (x)>(y) ? (x):(y)
void str_cli(FILE *fp, int sockfd); int main(int argc, char **argv) { int sockfd; struct sockaddr_in servaddr; if (argc != 2){ fprintf(stderr,"usage: tcpcli <IPaddress>\n\a"); exit(0); } if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1){ fprintf(stderr,"Socket error:%s\n\a",strerror(errno)); exit(1); } /*客戶程序填充服務端的資料*/ memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family=AF_INET; servaddr.sin_port=htons(SERV_PORT); if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){ fprintf(stderr,"inet_pton Error:%s\a\n",strerror(errno)); exit(1); } /* 客戶程序發起鏈接請求*/
if(connect(sockfd,(struct sockaddr *)(&servaddr),sizeof(struct sockaddr))==-1){ fprintf(stderr,"connect Error:%s\a\n",strerror(errno)); exit(1); } str_cli(stdin, sockfd); /*重點工做都在此函數*/ exit(0); } void str_cli(FILE *fp, int sockfd) { int maxfdp1, stdineof; fd_set rset;/*用於存放可讀文件描述符集合*/
char buf[MAXLINE]; int n; stdineof = 0;/*用於標識是否結束了標準輸入*/ FD_ZERO(&rset); while(1){ if (stdineof == 0) FD_SET(fileno(fp), &rset); FD_SET(sockfd, &rset); maxfdp1 = max(fileno(fp), sockfd) + 1; if(select(maxfdp1, &rset, NULL, NULL, NULL)<0){/*阻塞,直到有數據可讀或出錯*/ fprintf(stderr,"select Error\n"); exit(1); } if (FD_ISSET(sockfd, &rset)) { /*套接口有數據可讀*/
if ( (n = read(sockfd, buf, MAXLINE)) == 0) { if (stdineof == 1) return; /*標準輸入正常結束*/
else fprintf(stderr,"str_cli: server terminated prematurely"); } write(fileno(stdout), buf, n);/*將收到的數據寫到標準輸出*/ } if (FD_ISSET(fileno(fp), &rset)) { /*標準輸入可讀*/
if ( (n = read(fileno(fp), buf, MAXLINE)) == 0) { stdineof = 1; /*向服務器發送FIN,告訴它,後續已經沒有數據發送了,但仍爲讀而開放套接口,注意這裏使用了shutdown,而不是close*/
if(-1==shutdown(sockfd, SHUT_WR)){ fprintf(stderr,"shutdown Error\n"); } FD_CLR(fileno(fp), &rset); continue; } write(sockfd, buf, n); } } }
關於close與shutdown的區別:
close()將描述字的訪問計數減1,僅在訪問計數爲0時才關閉套接字。shutdown()能夠激發TCP的正常鏈接終止系列,而無論訪問計數。 close()終止了數據傳輸的兩個方向:讀、寫。