epoll有兩種模式,Edge Triggered(簡(jiǎn)稱ET) 和 Level
Triggered(簡(jiǎn)稱LT).在采用這兩種模式時(shí)要注意的是,如果采用ET模式,那么僅當(dāng)狀態(tài)發(fā)生變化時(shí)才會(huì)通知,而采用LT模式類似于原來的
select/poll操作,只要還有沒有處理的事件就會(huì)一直通知.
以代碼來說明問題:
首先給出server的代碼,需要說明的是每次accept的連接,加入可讀集的時(shí)候采用的都是ET模式,而且接收緩沖區(qū)是5字節(jié)的,也就是每次只接收5字節(jié)的數(shù)據(jù):
#include
<
iostream
>
#include
<
sys
/
socket.h
>
#include
<
sys
/
epoll.h
>
#include
<
netinet
/
in.h
>
#include
<
arpa
/
inet.h
>
#include
<
fcntl.h
>
#include
<
unistd.h
>
#include
<
stdio.h
>
#include
<
errno.h
>
using namespace std;
#define MAXLINE
5
#define OPEN_MAX
100
#define LISTENQ
20
#define SERV_PORT
5000
#define INFTIM
1000
void setnonblocking(
int
sock)
{
int
opts;
opts
=
fcntl(sock,F_GETFL);
if
(opts
<
0
)
{
perror(
"
fcntl(sock,GETFL)
"
);
exit
(
1
);
}
opts
=
opts|O_NONBLOCK;
if
(fcntl(sock,F_SETFL,opts)
<
0
)
{
perror(
"
fcntl(sock,SETFL,opts)
"
);
exit
(
1
);
}
}
int
main()
{
int
i, maxi, listenfd, connfd, sockfd,epfd,nfds;
ssize_t n;
char line[MAXLINE];
socklen_t clilen;
//
聲明epoll_event結(jié)構(gòu)體的變量,ev用于注冊(cè)事件,數(shù)組用于回傳要處理的事件
struct epoll_event ev,events[
20
];
//
生成用于處理accept的epoll專用的文件描述符
epfd
=
epoll_create(
256
);
struct sockaddr_in clientaddr;
struct sockaddr_in serveraddr;
listenfd
=
socket(AF_INET, SOCK_STREAM,
0
);
//
把socket設(shè)置為非阻塞方式
//
setnonblocking(listenfd);
//
設(shè)置與要處理的事件相關(guān)的文件描述符
ev.data.fd
=
listenfd;
//
設(shè)置要處理的事件類型
ev.events
=
EPOLLIN|EPOLLET;
//
ev.events
=
EPOLLIN;
//
注冊(cè)epoll事件
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,
&
ev);
bzero(
&
serveraddr, sizeof(serveraddr));
serveraddr.sin_family
=
AF_INET;
char
*
local_addr
=
"
127.0.0.1
"
;
inet_aton(local_addr,
&
(serveraddr.sin_addr));
//
htons(SERV_PORT);
serveraddr.sin_port
=
htons(SERV_PORT);
bind(listenfd,(sockaddr
*
)
&
serveraddr, sizeof(serveraddr));
listen(listenfd, LISTENQ);
maxi
=
0
;
for
( ; ; ) {
//
等待epoll事件的發(fā)生
nfds
=
epoll_wait(epfd,events,
20
,
500
);
//
處理所發(fā)生的所有事件
for
(i
=
0
;i
<
nfds;
++
i)
{
if
(events[i].data.fd
==
listenfd)
{
clilen=sizeof(struct sockaddr);
connfd
=
accept(listenfd,(struct sockaddr
*
)
&
clientaddr,
&
clilen);
if
(connfd
<
0
){
perror(
"
connfd<0
"
);
exit
(
1
);
}
//
setnonblocking(connfd);
char
*
str
=
inet_ntoa(clientaddr.sin_addr);
cout
<<
"
accapt a connection from
"
<<
str
<<
endl;
//
設(shè)置用于讀操作的文件描述符
ev.data.fd
=
connfd;
//
設(shè)置用于注測(cè)的讀操作事件
ev.events
=
EPOLLIN|EPOLLET;
//
ev.events
=
EPOLLIN;
//
注冊(cè)ev
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,
&
ev);
}
else
if
(events[i].events
&
EPOLLIN)
{
cout
<<
"
EPOLLIN
"
<<
endl;
if
( (sockfd
=
events[i].data.fd)
<
0
)
continue;
if
( (n
=
read(sockfd, line, MAXLINE))
<
0
) {
if
(errno
==
ECONNRESET) {
close(sockfd);
events[i].data.fd
=
-
1
;
}
else
std::cout
<<
"
readline error
"
<<
std::endl;
}
else
if
(n
==
0
) {
close(sockfd);
events[i].data.fd
=
-
1
;
}
line[n]
=
'
\0';
cout
<<
"
read
"
<<
line
<<
endl;
//
設(shè)置用于寫操作的文件描述符
ev.data.fd
=
sockfd;
//
設(shè)置用于注測(cè)的寫操作事件
ev.events
=
EPOLLOUT|EPOLLET;
//
修改sockfd上要處理的事件為EPOLLOUT
//
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,
&
ev);
}
else
if
(events[i].events
&
EPOLLOUT)
{
sockfd
=
events[i].data.fd;
write(sockfd, line, n);
//
設(shè)置用于讀操作的文件描述符
ev.data.fd
=
sockfd;
//
設(shè)置用于注測(cè)的讀操作事件
ev.events
=
EPOLLIN|EPOLLET;
//
修改sockfd上要處理的事件為EPOLIN
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,
&
ev);
}
}
}
return
0
;
}
下面給出測(cè)試所用的Perl寫的client端,在client中發(fā)送10字節(jié)的數(shù)據(jù),同時(shí)讓client在發(fā)送完數(shù)據(jù)之后進(jìn)入死循環(huán), 也就是在發(fā)送完之后連接的狀態(tài)不發(fā)生改變--既不再發(fā)送數(shù)據(jù), 也不關(guān)閉連接,這樣才能觀察出server的狀態(tài):
#!
/
usr
/
bin
/
perl
use IO::Socket;
my $host
=
"
127.0.0.1
"
;
my $port
=
5000
;
my $socket
=
IO::Socket::INET
->
new
(
"
$host:$port
"
)
or
die
"
create socket error $@
"
;
my $msg_out
=
"
1234567890
"
;
print $socket $msg_out;
print
"
now send over, go to sleep
\n
"
;
while
(
1
)
{
sleep(
1
);
}
運(yùn)行server和client發(fā)現(xiàn),server僅僅讀取了5字節(jié)的數(shù)據(jù),而client其實(shí)發(fā)送了10字節(jié)的數(shù)據(jù),也就是說,server僅當(dāng)?shù)谝淮?
監(jiān)聽到了EPOLLIN事件,由于沒有讀取完數(shù)據(jù),而且采用的是ET模式,狀態(tài)在此之后不發(fā)生變化,因此server再也接收不到EPOLLIN事件了.
(友情提示:上面的這個(gè)測(cè)試客戶端,當(dāng)你關(guān)閉它的時(shí)候會(huì)再次出發(fā)IO可讀事件給server,此時(shí)server就會(huì)去讀取剩下的5字節(jié)數(shù)據(jù)了,但是這一事件與前面描述的ET性質(zhì)并不矛盾.)
如果我們把client改為這樣:
#!
/
usr
/
bin
/
perl
use IO::Socket;
my $host
=
"
127.0.0.1
"
;
my $port
=
5000
;
my $socket
=
IO::Socket::INET
->
new
(
"
$host:$port
"
)
or
die
"
create socket error $@
"
;
my $msg_out
=
"
1234567890
"
;
print $socket $msg_out;
print
"
now send over, go to sleep
\n
"
;
sleep(
5
);
print
"
5 second gone
send another line\n
"
;
print $socket $msg_out;
while
(
1
)
{
sleep(
1
);
}
可以發(fā)現(xiàn),在server接收完5字節(jié)的數(shù)據(jù)之后一直監(jiān)聽不到client的事件,而當(dāng)client休眠5秒之后重新發(fā)送數(shù)據(jù),server再次監(jiān)聽到了變化,只不過因?yàn)橹皇亲x取了5個(gè)字節(jié),仍然有10個(gè)字節(jié)的數(shù)據(jù)(client第二次發(fā)送的數(shù)據(jù))沒有接收完.
如果上面的實(shí)驗(yàn)中,對(duì)accept的socket都采用的是LT模式,那么只要還有數(shù)據(jù)留在buffer中,server就會(huì)繼續(xù)得到通知,讀者可以自行改動(dòng)代碼進(jìn)行實(shí)驗(yàn).
基
于這兩個(gè)實(shí)驗(yàn),可以得出這樣的結(jié)論:ET模式僅當(dāng)狀態(tài)發(fā)生變化的時(shí)候才獲得通知,這里所謂的狀態(tài)的變化并不包括緩沖區(qū)中還有未處理的數(shù)據(jù),也就是說,如果
要采用ET模式,需要一直read/write直到出錯(cuò)為止,很多人反映為什么采用ET模式只接收了一部分?jǐn)?shù)據(jù)就再也得不到通知了,大多因?yàn)檫@樣;而LT
模式是只要有數(shù)據(jù)沒有處理就會(huì)一直通知下去的.
補(bǔ)充說明一下這里一直強(qiáng)調(diào)的"狀態(tài)變化"是什么:
1)對(duì)于監(jiān)聽可讀事件時(shí),如果是socket是監(jiān)聽socket,那么當(dāng)有新的主動(dòng)連接到來為狀態(tài)發(fā)生變化;對(duì)一般的socket而言,協(xié)議棧中相應(yīng)的緩
沖區(qū)有新的數(shù)據(jù)為狀態(tài)發(fā)生變化.但是,如果在一個(gè)時(shí)間同時(shí)接收了N個(gè)連接(N>1),但是監(jiān)聽socket只accept了一個(gè)連接,那么其它未
accept的連接將不會(huì)在ET模式下給監(jiān)聽socket發(fā)出通知,此時(shí)狀態(tài)不發(fā)生變化;對(duì)于一般的socket,就如例子中而言,如果對(duì)應(yīng)的緩沖區(qū)本身
已經(jīng)有了N字節(jié)的數(shù)據(jù),而只取出了小于N字節(jié)的數(shù)據(jù),那么殘存的數(shù)據(jù)不會(huì)造成狀態(tài)發(fā)生變化.
2)對(duì)于監(jiān)聽可寫事件時(shí),同理可推,不再詳述.
而不論是監(jiān)聽可讀還是可寫,對(duì)方關(guān)閉socket連接都將造成狀態(tài)發(fā)生變化,比如在例子中,如果強(qiáng)行中斷client腳本,也就是主動(dòng)中斷了socket連接,那么都將造成server端發(fā)生狀態(tài)的變化,從而server得到通知,將已經(jīng)在本方緩沖區(qū)中的數(shù)據(jù)讀出.
把前面的描述可以總結(jié)如下:僅當(dāng)對(duì)方的動(dòng)作(發(fā)出數(shù)據(jù),關(guān)閉連接等)造成的事件才能導(dǎo)致狀態(tài)發(fā)生變化,而本方協(xié)議棧中已經(jīng)處理的事件(包括接收了對(duì)方的數(shù)
據(jù),接收了對(duì)方的主動(dòng)連接請(qǐng)求)并不是造成狀態(tài)發(fā)生變化的必要條件,狀態(tài)變化一定是對(duì)方造成的.所以在ET模式下的,必須一直處理到出錯(cuò)或者完全處理完
畢,才能進(jìn)行下一個(gè)動(dòng)作,否則可能會(huì)發(fā)生錯(cuò)誤.
另外,從這個(gè)例子中,也可以闡述一些基本的網(wǎng)絡(luò)編程概念.首先,連接的兩端中,一端發(fā)送成功并不代表著對(duì)方上層應(yīng)用程序接收成功,
就拿上面的client測(cè)試程序來說,10字節(jié)的數(shù)據(jù)已經(jīng)發(fā)送成功,但是上層的server并沒有調(diào)用read讀取數(shù)據(jù),因此發(fā)送成功僅僅說明了數(shù)據(jù)被對(duì)
方的協(xié)議棧接收存放在了相應(yīng)的buffer中,而上層的應(yīng)用程序是否接收了這部分?jǐn)?shù)據(jù)不得而知;同樣的,讀取數(shù)據(jù)時(shí)也只代表著本方協(xié)議棧的對(duì)應(yīng)
buffer中有數(shù)據(jù)可讀,而此時(shí)時(shí)候在對(duì)端是否在發(fā)送數(shù)據(jù)也不得而知.