select、poll、epoll使用小結

參考鏈接:http://c4fun.cn/blog/2013/11/19/linux-io-reuse-interface/

http://blog.chinaunix.net/uid-17299695-id-3059110.html

http://blog.csdn.net/kkxgx/article/details/7717125

I/O複用是Linux中的I/O模型之一。所謂I/O複用,指的是進程預先告訴內核,使得內核一旦發現進程指定的一個或多個I/O條件就緒,就通知進程進行處理,從而不會在單個I/O上導致阻塞。

在Linux中,提供了select、poll、epoll三類接口來實現I/O複用。

select函數接口

select中主要就是一個select函數,用於監聽指定事件的發生,原型如下:

1
2
3
4
5
#include<sys/select.h>
#include<sys/time.h>
int select(int maxfd, fd_set *rset, fd_set *wset, fd_set *eset,
const struct timeval *timeout);
//返回:若有就緒描述符則返回其數目,超時返回0,出錯返回-1

其中各參數的含義如下:
maxfd:最大文件描述符加1,比它小的從0開始的描述符都將被監視,它的值不能超過系統中定義的FD_SETSIZE(通常是1024)。

rset,wset,eset:分別表示監視的讀、寫、錯誤的描述符位數組,通常是一個整數數組,每一個整數可以表示32個描述符是否被監視。需要注意的是這幾個參數都是值-結果參數,在調用select後這幾個參數將表示哪些描述符就緒了。通過以下幾個宏可以很方便的操作fset數組:

1
2
3
4
5
6
7
8
void FD_ZERO(fd_set *fdset);
//將一個fdset清空
void FD_SET(int fd, fd_set *fdset);
//將某個fd對應在該fd_set裏的那一位打開
void FD_CLR(int fd, fd_set *fdset);
//將某個fd對應在該fd_set裏的那一位關閉
void FD_ISSET(int fd, fd_set *fdset);
//檢測某個fd_set裏對應fd的那一位是否打開

timeout:超時時間,即select最長等待多久就返回,爲NULL時表示等到有操作符準備就緒後才返回。該時間可以精確到微秒,其結構如下:

1
2
3
4
struct timeval{
long tv_sec;//秒數
long tv_usec;//微妙數
}

描述符就緒條件
對於普通數據的讀寫,描述符就緒顯而易見,但仍有一些特殊情況時描述符會讀寫就緒,UNP中對描述符的讀寫就緒條件進行了說明。

1)滿足以下4個條件時,描述符準備好讀
a)套接字接收緩衝區中的數據字節數大於套接字接收緩衝區低水位標記的當前大小(默認爲1),讀將會返回大於0的數。
b)該連接的讀半部關閉,讀將會返回0。
c)套接字上有一個錯誤待處理,讀將返回-1。
d)該套接字是一個監聽套接字並且已完成連接數不爲0。

2)滿足以下4個條件時,描述符準備好寫
a)套接字發送緩衝區中的可用空間字節數大於等於套接字發送緩衝區低水位標記的當前大小(默認2048),寫將會返回大於0的數。
b)該連接的寫半部關閉,寫將會返回EPIPE。
c)套接字上有一個錯誤待處理,寫將返回-1。
d)使用非阻塞式connect的套接字建立有結果返回。

poll函數接口

poll中的主要函數也只有一個poll,與select作用類似,但參數有所不同,函數原型如下:

1
2
3
#include<poll.h>
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
//返回:若有就緒描述符則返回其數目,超時返回0,出錯返回-1

其中各參數的含義如下:
fdarray:是一個指向pollfd結構數組的指針,維護着描述符以及事件信息,該結構體是poll裏比較核心的結構體,結構如下:

1
2
3
4
5
struct pollfd{
int fd; //描述符
short events; //關注的事件
short revents; //發生的事件
}

該結構體通過兩個變量區分關注的事件和發生的事件,從而避免了使用值-結果參數。events和revents可選的標誌位如下:

1
2
3
4
5
6
7
8
9
10
11
12
POLLIN //普通或優先級帶數據可讀
POLLRDNORM //普通數據可讀
POLLRDBAND //優先級帶數據可讀
POLLPRI //高優先級數據可讀
POLLOUT //普通數據可寫
POLLWRNORM //普通數據可寫
POLLWRBAND //優先級帶數據可寫
POLLERR //發生錯誤
POLLHUP //發生掛起
POLLINVAL //描述符不是一個打開的文件
//其中POLLERR,POLLHUP,POLLINVAL僅作爲reventes的標誌位
//優先級帶數據主要是指TCP的帶外數據,其它大部分數據都是普通數據。

nfds:指定結構體數組中元素的個數。
timeout:每次調用poll最大等待的毫秒數,負值代表等待到直到有事件觸發。

epoll函數接口

epoll主要有三個函數,函數原型如下:

1
2
3
4
5
6
7
#include <sys/epoll.h>
int epoll_create(int size);
//創建一個epoll句柄
int epoll_ctl(int efd, int op, int fd, struct epoll_event *event);
//註冊一個epoll事件
int epoll_wait(int efd, struct epoll_event *events, int maxevents, int timeout);
//等待事件發生

epoll_create(int size)

size:能監聽多少個描述符,返回一個epoll描述符。注意使用完epoll後要關閉該描述符。

epoll_ctl(int efd, int op, int fd, struct epoll_event *event)

efd:epoll_create返回的epoll描述符
op:表示動作,可以在以下三個宏裏選擇一個

1
2
3
EPOLL_CTL_ADD //註冊新的fd到epoll中
EPOLL_CTL_MOD //修改已經註冊的fd的監聽事件
EPOLL_CTL_DEL //從epoll中刪除一個fd

fd:要監聽的fd
event:告訴內核要監聽什麼事件,其結構如下:

1
2
3
4
5
6
7
8
9
10
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; //epoll事件
epoll_data_t data; //用於儲存epoll描述符信息
};

其中events表示epoll事件,可選的標誌位如下:

1
2
3
4
5
6
7
8
EPOLLIN //描述符可以讀
EPOLLOUT //描述符可以寫
EPOLLPRI //描述符有優先數據可讀(帶外數據)
EPOLLERR //文件描述符發生錯誤;
EPOLLHUP //文件描述符被掛斷;
EPOLLET //將EPOLL設爲邊緣觸發(Edge Triggered)模式,默認是水平觸發(Level Triggered)。
EPOLLRDHUP //對端關閉連接
EPOLLONESHOT//只監聽一次事件,事件發生後該描述符的其他信息將不被提示。

而epoll_data_t使用了union來存儲數據,用戶可以使用data來存放一些關於該fd的額外內容。

標誌位中比較特殊的是EPOLLET這個選項,這個選項將EPOLL設置爲邊緣觸發模式,EPOLL有EPOLLET和EPOLLLT兩種工作模式。
EPOLLLT(Level Triggered,水平觸發模式):默認工作模式,支持block和no-block socket,內核通知你描述符事件後,如果不進行操作,會一直通知。
EPOLLET(Edge Triggered,邊緣觸發模式):高速工作模式,只支持no-block socket,只會在描述符狀態由未就緒轉爲就緒時會通知一次,使用該模式時,如果程序編寫的不夠健全,是很容易出現問題的。

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

該函數與select和poll函數的功能類似,監視指定事件的發生並返回給用戶。
efd:epoll_create返回的opoll描述符。
events:用來從內核得到事件的集合。
maxevents:用來告知內核events數組的大小。
timeout:超時時間,-1將阻塞直到有事件發生,否則表示最多等待多少毫秒後函數就返回。

select,poll,epoll比較

select

  • select能監控的描述符個數由內核中的FD_SETSIZE限制,僅爲1024,這也是select最大的缺點,因爲現在的服務器併發量遠遠不止1024。即使能重新編譯內核改變FD_SETSIZE的值,但這並不能提高select的性能。
  • 每次調用select都會線性掃描所有描述符的狀態,在select結束後,用戶也要線性掃描fd_set數組才知道哪些描述符準備就緒,等於說每次調用複雜度都是O(n)的,在併發量大的情況下,每次掃描都是相當耗時的,很有可能有未處理的連接等待超時。
  • 每次調用select都要在用戶空間和內核空間裏進行內存複製fd描述符等信息。

poll

  • poll使用pollfd結構來存儲fd,突破了select中描述符數目的限制。
  • 與select的後兩點類似,poll仍然需要將pollfd數組拷貝到內核空間,之後依次掃描fd的狀態,整體複雜度依然是O(n)的,在併發量大的情況下服務器性能會快速下降。

epoll

  • epoll維護的描述符數目不受到限制,而且性能不會隨着描述符數目的增加而下降。
  • 服務器的特點是經常維護着大量連接,但其中某一時刻讀寫的操作符數量卻不多。epoll先通過epoll_ctl註冊一個描述符到內核中,並一直維護着而不像poll每次操作都將所有要監控的描述符傳遞給內核;在描述符讀寫就緒時,通過回掉函數將自己加入就緒隊列中,之後epoll_wait返回該就緒隊列。也就是說,epoll基本不做無用的操作,時間複雜度僅與活躍的客戶端數有關,而不會隨着描述符數目的增加而下降。
  • epoll在傳遞內核與用戶空間的消息時使用了內存共享,而不是內存拷貝,這也使得epoll的效率比poll和select更高。
epoll高效的實現

  

  epoll使用起來很清晰,首先要調用epoll_create建立一個epoll對象。參數size是內核保證能夠正確處理的最大句柄數,多於這個最大數時內核可不保證效果。

epoll_ctl可以操作上面建立的epoll,例如,將剛建立的socket加入到epoll中讓其監控,或者把 epoll正在監控的某個socket句柄移出epoll,不再監控它等等。

epoll_wait在調用時,在給定的timeout時間內,當在監控的所有句柄中有事件發生時,就返回用戶態的進程。

從上面的調用方式就可以看到epoll比select/poll的優越之處:因爲後者每次調用時都要傳遞你所要監控的所有socket給select/poll系統調用,這意味着需要將用戶態的socket列表copy到內核態,如果以萬計的句柄會導致每次都要copy幾十幾百KB的內存到內核態,非常低效。而我們調用epoll_wait時就相當於以往調用select/poll,但是這時卻不用傳遞socket句柄給內核,因爲內核已經在epoll_ctl中拿到了要監控的句柄列表。

所以,實際上在你調用epoll_create後,內核就已經在內核態開始準備幫你存儲要監控的句柄了,每次調用epoll_ctl只是在往內核的數據結構裏塞入新的socket句柄。

在內核裏,一切皆文件。所以,epoll向內核註冊了一個文件系統,用於存儲上述的被監控socket。當你調用epoll_create時,就會在這個虛擬的epoll文件系統裏創建一個file結點。當然這個file不是普通文件,它只服務於epoll。

epoll在被內核初始化時(操作系統啓動),同時會開闢出epoll自己的內核高速cache區,用於安置每一個我們想監控的socket,這些socket會以紅黑樹的形式保存在內核cache裏,以支持快速的查找、插入、刪除。這個內核高速cache區,就是建立連續的物理內存頁,然後在之上建立slab層,簡單的說,就是物理上分配好你想要的size的內存對象,每次使用時都是使用空閒的已分配好的對象。

   epoll的高效就在於,當我們調用epoll_ctl往裏塞入百萬個句柄時,epoll_wait仍然可以飛快的返回,並有效的將發生事件的句柄給我們用戶。這是由於我們在調用epoll_create時,內核除了幫我們在epoll文件系統裏建了個file結點,在內核cache裏建了個紅黑樹用於存儲以後epoll_ctl傳來的socket外,還會再建立一個list鏈表,用於存儲準備就緒的事件,當epoll_wait調用時,僅僅觀察這個list鏈表裏有沒有數據即可。有數據就返回,沒有數據就sleep,等到timeout時間到後即使鏈表沒數據也返回。所以,epoll_wait非常高效。

   而且,通常情況下即使我們要監控百萬計的句柄,大多一次也只返回很少量的準備就緒句柄而已,所以,epoll_wait僅需要從內核態copy少量的句柄到用戶態而已,如何能不高效?!

   那麼,這個準備就緒list鏈表是怎麼維護的呢?當我們執行epoll_ctl時,除了把socket放到epoll文件系統裏file對象對應的紅黑樹上之外,還會給內核中斷處理程序註冊一個回調函數,告訴內核,如果這個句柄的中斷到了,就把它放到準備就緒list鏈表裏。所以,當一個socket上有數據到了,內核在把網卡上的數據copy到內核中後就來把socket插入到準備就緒鏈表裏了。

   如此,一顆紅黑樹,一張準備就緒句柄鏈表,少量的內核cache,就幫我們解決了大併發下的socket處理問題。執行epoll_create時,創建了紅黑樹和就緒鏈表,執行epoll_ctl時,如果增加socket句柄,則檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹幹上,然後向內核註冊回調函數,用於當中斷事件來臨時向準備就緒鏈表中插入數據。執行epoll_wait時立刻返回準備就緒鏈表裏的數據即可。

   最後看看epoll獨有的兩種模式LT和ET。無論是LT和ET模式,都適用於以上所說的流程。區別是,LT模式下,只要一個句柄上的事件一次沒有處理完,會在以後調用epoll_wait時次次返回這個句柄,而ET模式僅在第一次返回。

   這件事怎麼做到的呢?當一個socket句柄上有事件時,內核會把該句柄插入上面所說的準備就緒list鏈表,這時我們調用epoll_wait,會把準備就緒的socket拷貝到用戶態內存,然後清空準備就緒list鏈表,最後,epoll_wait幹了件事,就是檢查這些socket,如果不是ET模式(就是LT模式的句柄了),並且這些socket上確實有未處理的事件時,又把該句柄放回到剛剛清空的準備就緒鏈表了。所以,非ET的句柄,只要它上面還有事件,epoll_wait每次都會返回。而ET模式的句柄,除非有新中斷到,即使socket上的事件沒有處理完,也是不會次次從epoll_wait返回的。

程序示例

分別使用select,poll和epoll實現了簡單的回顯服務器程序,客戶端使用select來實現。其中select和poll程序主要參考unp的實現,只是Demo程序,對一些異常情況沒有進行處理。

客戶端程序

使用select來監聽終端輸入和連接服務器的流輸入,這樣可以保證客戶端不在某一個輸入流上死等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#define MAXLINE 4096
in_port_t SERV_PORT = 8888;
//char *addr = "192.168.0.231";
char *addr = "127.0.0.1";
void str_cli(FILE *fp, int sockfd);
int main(int argc ,char *argv[]) {
int sockfd;
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof servaddr);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, addr, &servaddr.sin_addr);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
connect(sockfd, (const struct sockaddr *)&servaddr, sizeof servaddr);
str_cli(stdin, sockfd);
}
void str_cli(FILE *fp, int sockfd) {
int maxfd, stdineof, n;
fd_set rset;
char buf[MAXLINE];
FD_ZERO(&rset);
stdineof = 0;
for (;;) {
//如果不是已經輸入結束,就繼續監聽終端輸入
if (stdineof == 0) FD_SET(fileno(fp), &rset);
//監聽來自服務器的信息
FD_SET(sockfd, &rset);
//maxfd設置爲sockfd和stdin中較大的一個加1
maxfd = (fileno(fp) > sockfd ? fileno(fp) : sockfd) + 1;
//只關心是否有描述符讀就緒,其他幾個直接傳NULL即可
select(maxfd, &rset, NULL, NULL, NULL);
//如果有來自服務器的信息可讀
if (FD_ISSET(sockfd, &rset)) {
if ((n = read(sockfd, buf, MAXLINE)) == 0) {
//如果這邊輸入了EOF之後服務器close掉連接說明正常結束,否則爲異常結束
if (stdineof == 1)
return;
else
perror("terminated error\n");
}
//輸出到終端
write(fileno(stdout), buf, n);
}
//如果有來自終端的輸入
if (FD_ISSET(fileno(fp), &rset)) {
//終端這邊輸入了結束符
if ((n = read(fileno(fp), buf, MAXLINE)) == 0) {
//標記已經輸入完畢,並只單端關閉寫,因爲可能還有消息在來客戶端的路上尚未處理
stdineof = 1;
shutdown(sockfd, SHUT_WR);
//不再監聽終端輸入
FD_CLR(fileno(fp), &rset);
continue;
}
//將輸入信息發送給服務器
write(sockfd, buf, n);
}
}
}

select服務器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#define MAXLINE 4096
in_port_t SERV_PORT = 8888;
int main(int argc ,char *argv[]){
int i;
int listenfd, connfd, sockfd;
int maxfd, maxi, nready, client[FD_SETSIZE];
char buf[MAXLINE];
struct sockaddr_in cliaddr, servaddr;
socklen_t clilen;
ssize_t n;
fd_set rset, allset;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&servaddr, 0, sizeof servaddr);
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof servaddr))
printf("bind error\n");
listen(listenfd, 1024);
//客戶端描述符存儲在client中,maxi表示該數組最大的存有客戶端描述符的數組下標
maxfd = listenfd;
maxi = -1;
memset(client, -1, sizeof client);
//初始化讀就緒的fd_set數組,並監聽listen描述符
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
for (;;) {
//allset是監控的描述符列表,rset是可讀描述符列表
rset = allset;
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
//如果listen描述符可讀,說明有客戶端連接
if (FD_ISSET(listenfd, &rset)) {
clilen = sizeof cliaddr;
connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);
if (connfd == -1) perror("accept error\n");
else printf("%d accepted!\n", connfd);
//掃描client數組,找到下標最小的未用的來存客戶端描述符
for (i = 0; i < FD_SETSIZE; i++) if (client[i] < 0) {
client[i] = connfd;
break;
}
if (i == FD_SETSIZE) perror("too many clients\n");
//將客戶端描述符放到監視的fd_set中,並更新maxfd和maxi
FD_SET(connfd, &allset);
if (connfd > maxfd) maxfd = connfd;
if (i > maxi) maxi = i;
if (--nready <= 0) continue;
}
//掃描所有的客戶端,查看是否有描述符讀就緒
for (i = 0; i <= maxi; i++) {
if ((sockfd = client[i]) < 0) continue;
if (FD_ISSET(sockfd, &rset)) {
//讀到EOF或錯誤,清除該描述符
if ((n = read(sockfd, buf, MAXLINE)) <= 0) {
close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
if (n < 0) perror("read error\n");
//回顯給客戶端
} else {
write(sockfd, buf, n);
}
if (--nready <= 0) break;
}
}
}
return 0;
}

poll服務器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <poll.h>
#include <errno.h>
#define MAXLINE 4096
#ifndef OPEN_MAX
#define OPEN_MAX 1024
#endif
in_port_t SERV_PORT = 8888;
int main(int argc ,char *argv[]){
int i, maxi;
int listenfd, connfd, sockfd;
int nready;
char buf[MAXLINE];
struct pollfd client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
socklen_t clilen;
ssize_t n;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&servaddr, 0, sizeof servaddr);
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof servaddr))
perror("bind error\n");
listen(listenfd, 1024);
//client保存了pull監聽的描述符,其中client[0]是給listen描述符的
client[0].fd = listenfd;
client[0].events = POLLRDNORM;
for (i = 1; i < OPEN_MAX; i++)
client[i].fd = -1;
maxi = 0;
for (;;) {
nready = poll(client, maxi+1, -1);
//如果是監聽描述符可讀,說明有客戶端連入
if (client[0].revents & POLLRDNORM) {
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);
if (connfd == -1) perror("accept error\n");
else printf("%d accepted!\n", connfd);
//掃描clien數組,找到下標最小的未用的來存客戶端描述符
for (i = 1; i < OPEN_MAX; i++) {
if (client[i].fd < 0) {
client[i].fd = connfd;
break;
}
}
if (i == OPEN_MAX) perror("too many clients.\n");
client[i].events = POLLRDNORM;
if (i > maxi) maxi = i;
if (--nready <= 0) continue;
}
//掃描所有的客戶端描述符
for (i = 1; i <= maxi; i++) {
if ((sockfd = client[i].fd) < 0) continue;
//POLLERR不需要監聽,如果有錯誤的話poll返回時會自動加上
if (client[i].revents & (POLLRDNORM | POLLERR)) {
//讀到EOF或錯誤關閉描述符
if ((n = read(sockfd, buf, MAXLINE)) <= 0) {
close(sockfd);
client[i].fd = -1;
if (n < 0) perror("read error\n");
//回顯給客戶端
} else {
write(sockfd, buf, n);
}
if (--nready <= 0) break;
}
}
}
return 0;
}

epoll服務器

回顯服務器使用了ET高速模式。在該模式下,最好所有的操作都是非阻塞的,程序中套接字都設置爲了non-socket,並且使用了緩衝區,在讀到數據時先將數據存到緩衝區中,下次可寫時纔將數據從緩衝區寫回客戶端。
另外,在ET模式下,accept、read、write時都要使用循環直到讀到EAGAIN才能說明沒有數據了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#ifndef OPEN_MAX
#define OPEN_MAX 1024
#endif
#define SERV_PORT 8888
#define MAXLINE 4096
#define EVENT_MAX 20
struct event_data{
int fd;
int offset;
char buf[MAXLINE];
}event_d[OPEN_MAX];
void set_event_d(int fd, struct epoll_event *evt, struct event_data *met){
met->fd = fd;
met->offset = 0;
memset(&met->buf, 0, sizeof met->buf);
evt->data.ptr = met;
}
int main(int argc, char *argv[]){
int listenfd, connfd, epfd;
char buf[MAXLINE];
struct sockaddr_in servaddr, cliaddr;
int i, j, nready;
socklen_t clilen;
ssize_t n, wpos;
struct epoll_event evt, evts[EVENT_MAX];
listenfd = socket(AF_INET, SOCK_STREAM, 0);
//設置lisenfd非阻塞
fcntl(listenfd, F_SETFL, O_NONBLOCK);
memset(&servaddr, 0, sizeof servaddr);
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr*)&servaddr, sizeof servaddr);
listen(listenfd, 1024);
//創建epoll描述符
epfd = epoll_create(OPEN_MAX);
//將listen描述符加入到epoll中
set_event_d(listenfd, &evt, &event_d[0]);
evt.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &evt);
//event_d中的fd爲-1表示沒有使用,0用來存listenfd,其它用來存客戶端fd
for (i = 1; i < OPEN_MAX; i++) event_d[i].fd = -1;
for (;;) {
nready = epoll_wait(epfd, evts, EVENT_MAX, -1);
for (i = 0; i < nready; i++) {
//讀取存儲描述符信息的指針
struct event_data *ed = (struct event_data*)evts[i].data.ptr;
//accept
if (ed->fd == listenfd) {
//ET模式下存在多個client connect只通知一次的情況,需要循環accept直到讀到EAGAIN
for(;;) {
clilen = sizeof cliaddr;
connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);
if (connfd == -1) {
if (errno == EAGAIN) break;
else perror("accept error");
} else {
printf("a client connected! fd: %d\n", connfd);
}
//找到可用的event_d來存放event.data
for (j = 1; j < OPEN_MAX; j++) {
if (event_d[j].fd == -1) break;
}
if (j == OPEN_MAX) {
perror("too many clients");
break;
}
//設置客戶端fd非阻塞
fcntl(connfd, F_SETFL, O_NONBLOCK);
set_event_d(connfd, &evt, &event_d[j]);
evt.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &evt);
}
//read
} else if(evts[i].events & EPOLLIN){
//ET模式,重複讀直到EAGAIN說明無數據可讀或者讀到錯誤及EOF
for (;ed->offset < MAXLINE;) {
n = read(ed->fd, ed->buf + ed->offset, MAXLINE - ed->offset);
if (n <= 0) {
if (errno == EAGAIN) break;
if (errno == EINTR) continue;
close(ed->fd);
ed->fd = -1;
break;
}
ed->offset += n;
}
//修改爲監聽描述符寫就緒
evt.events = EPOLLOUT | EPOLLET;
evt.data.ptr = ed;
epoll_ctl(epfd, EPOLL_CTL_MOD, ed->fd, &evt);
//write
} else if(evts[i].events & EPOLLOUT){
wpos = 0;
//ET模式下,重複寫直到無數據可發或者EAGAIN
for (;wpos < ed->offset;) {
n = write(ed->fd, ed->buf + wpos, ed->offset - wpos);
if (n < 0) {
if (errno == EAGAIN) break;
if (errno == EINTR) continue;
close(ed->fd);
ed->fd = -1;
break;
}
wpos += n;
}
ed->offset = 0;
//修改爲監聽描述符讀就緒
evt.events = EPOLLIN | EPOLLET;
evt.data.ptr = ed;
epoll_ctl(epfd, EPOLL_CTL_MOD, ed->fd, &evt);
}
}
}
return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章