epoll學習筆記

epoll學習筆記

epoll有兩種模式,Edge Triggered(簡稱ET) 和 Level Triggered(簡稱LT).在採用這兩種模式時要注意的是,如果採用ET模式,那麼僅當狀態發生變化時纔會通知,而採用LT模式類似於原來的select/poll操作,只要還有沒有處理的事件就會一直通知.

以代碼來說明問題:
首先給出server的代碼,需要說明的是每次accept的連接,加入可讀集的時候採用的都是ET模式,而且接收緩衝區是5字節的,也就是每次只接收5字節的數據:
#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結構體的變量,ev用於註冊事件,數組用於回傳要處理的事件
    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設置爲非阻塞方式
    
//setnonblocking(listenfd);
    
//設置與要處理的事件相關的文件描述符
    ev.data.fd
=listenfd;
    
//設置要處理的事件類型
    ev.events
=EPOLLIN|EPOLLET;
    
//ev.events=EPOLLIN;
    
//註冊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事件的發生
        nfds
=epoll_wait(epfd,events,20,500);
        
//處理所發生的所有事件     
        
for(i=0;i<nfds;++i)
        {
            
if(events[i].data.fd==listenfd)
            {
                connfd 
= accept(listenfd,(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;
                
//設置用於讀操作的文件描述符
                ev.data.fd
=connfd;
                
//設置用於注測的讀操作事件
                ev.events
=EPOLLIN|EPOLLET;
                
//ev.events=EPOLLIN;
                
//註冊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;
                
//設置用於寫操作的文件描述符
                ev.data.fd
=sockfd;
                
//設置用於注測的寫操作事件
                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);
                
//設置用於讀操作的文件描述符
                ev.data.fd
=sockfd;
                
//設置用於注測的讀操作事件
                ev.events
=EPOLLIN|EPOLLET;
                
//修改sockfd上要處理的事件爲EPOLIN
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,
&ev);
            }
        }
    }
    return 
0;
}


下面給出測試所用的Perl寫的client端,在client中發送10字節的數據,同時讓client在發送完數據之後進入死循環, 也就是在發送完之後連接的狀態不發生改變--既不再發送數據, 也不關閉連接,這樣才能觀察出server的狀態:
#!/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);
}
運行server和client發現,server僅僅讀取了5字節的數據,而client其實發送了10字節的數據,也就是說,server僅當第一次監聽到了EPOLLIN事件,由於沒有讀取完數據,而且採用的是ET模式,狀態在此之後不發生變化,因此server再也接收不到EPOLLIN事件了.
(友情提示:上面的這個測試客戶端,當你關閉它的時候會再次出發IO可讀事件給server,此時server就會去讀取剩下的5字節數據了,但是這一事件與前面描述的ET性質並不矛盾.)

如果我們把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 gonesend another line\n";
print $socket $msg_out;

while (1)
{
    sleep(
1);
}

可以發現,在server接收完5字節的數據之後一直監聽不到client的事件,而當client休眠5秒之後重新發送數據,server再次監聽到了變化,只不過因爲只是讀取了5個字節,仍然有10個字節的數據(client第二次發送的數據)沒有接收完.

如果上面的實驗中,對accept的socket都採用的是LT模式,那麼只要還有數據留在buffer中,server就會繼續得到通知,讀者可以自行改動代碼進行實驗.

基於這兩個實驗,可以得出這樣的結論:ET模式僅當狀態發生變化的時候才獲得通知,這裏所謂的狀態的變化並不包括緩衝區中還有未處理的數據,也就是說,如果要採用ET模式,需要一直read/write直到出錯爲止,很多人反映爲什麼採用ET模式只接收了一部分數據就再也得不到通知了,大多因爲這樣;而LT模式是只要有數據沒有處理就會一直通知下去的.

補充說明一下這裏一直強調的"狀態變化"是什麼:

1)對於監聽可讀事件時,如果是socket是監聽socket,那麼當有新的主動連接到來爲狀態發生變化;對一般的socket而言,協議棧中相應的緩衝區有新的數據爲狀態發生變化.但是,如果在一個時間同時接收了N個連接(N>1),但是監聽socket只accept了一個連接,那麼其它未accept的連接將不會在ET模式下給監聽socket發出通知,此時狀態不發生變化;對於一般的socket,就如例子中而言,如果對應的緩衝區本身已經有了N字節的數據,而只取出了小於N字節的數據,那麼殘存的數據不會造成狀態發生變化.

2)對於監聽可寫事件時,同理可推,不再詳述.

而不論是監聽可讀還是可寫,對方關閉socket連接都將造成狀態發生變化,比如在例子中,如果強行中斷client腳本,也就是主動中斷了socket連接,那麼都將造成server端發生狀態的變化,從而server得到通知,將已經在本方緩衝區中的數據讀出.

把前面的描述可以總結如下:僅當對方的動作(發出數據,關閉連接等)造成的事件才能導致狀態發生變化,而本方協議棧中已經處理的事件(包括接收了對方的數據,接收了對方的主動連接請求)並不是造成狀態發生變化的必要條件,狀態變化一定是對方造成的.所以在ET模式下的,必須一直處理到出錯或者完全處理完畢,才能進行下一個動作,否則可能會發生錯誤.

另外,從這個例子中,也可以闡述一些基本的網絡編程概念.首先,連接的兩端中,一端發送成功並不代表着對方上層應用程序接收成功, 就拿上面的client測試程序來說,10字節的數據已經發送成功,但是上層的server並沒有調用read讀取數據,因此發送成功僅僅說明了數據被對方的協議棧接收存放在了相應的buffer中,而上層的應用程序是否接收了這部分數據不得而知;同樣的,讀取數據時也只代表着本方協議棧的對應buffer中有數據可讀,而此時時候在對端是否在發送數據也不得而知.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章