IO模型演進(jìn)變化史:使用libevent和libev

構(gòu)建現(xiàn)代的server應(yīng)用程序須要以某種方法同一時(shí)候接收數(shù)百、數(shù)千甚至數(shù)萬(wàn)個(gè)事件,不管它們是內(nèi)部請(qǐng)求還是網(wǎng)絡(luò)連接,都要有效地處理它們的操作。有很多解決方式,但事件驅(qū)動(dòng)也被廣泛應(yīng)用到網(wǎng)絡(luò)編程中。并大規(guī)模部署在高連接數(shù)高吞吐量的server程序中,如http server程序、ftp server程序等。

相比于傳統(tǒng)的網(wǎng)絡(luò)編程方式,事件驅(qū)動(dòng)可以極大的減少資源占用,增大服務(wù)接待能力,并提高網(wǎng)絡(luò)傳輸效率。這些事件驅(qū)動(dòng)模型中,libevent庫(kù)和libev庫(kù)可以大大提高性能和事件處理能力。

在討論libev和libevent之前,我們看看I/O模型演進(jìn)變化歷史。

一、堵塞網(wǎng)絡(luò)接口:處理單個(gè)client

我們第一次接觸到的網(wǎng)絡(luò)編程一般都是從socket()、listen()、bind()、send()、recv()、close()等接口開始的。使用這些接口能夠非常方便的構(gòu)建server/client的模型。

堵塞I/O模型圖:在調(diào)用recv()函數(shù)時(shí),發(fā)生在內(nèi)核中等待數(shù)據(jù)和復(fù)制數(shù)據(jù)的過程。

當(dāng)調(diào)用recv()函數(shù)時(shí):系統(tǒng)首先查是否有準(zhǔn)備好的數(shù)據(jù);假設(shè)數(shù)據(jù)沒有準(zhǔn)備好,那么系統(tǒng)就處于等待狀態(tài);當(dāng)數(shù)據(jù)準(zhǔn)備好后,將數(shù)據(jù)從系統(tǒng)緩沖區(qū)拷貝到用戶空間,然后該函數(shù)才返回。在套接應(yīng)用程序中,當(dāng)調(diào)用recv()函數(shù)時(shí),未必用戶空間就已經(jīng)存在數(shù)據(jù),那么此時(shí)recv()函數(shù)就會(huì)處于等待狀態(tài)。

我們注意到。大部分的socket接口都是堵塞型的。所謂堵塞型接口是指系統(tǒng)調(diào)用(通常是IO接口)不返回調(diào)用結(jié)果并讓當(dāng)前線程一直堵塞,僅僅有當(dāng)該系統(tǒng)調(diào)用獲得結(jié)果或者超時(shí)出錯(cuò)時(shí)才返回。

實(shí)際上,除非特別指定,幾乎全部的IO接口(包含socket接口)都是堵塞型的。這給網(wǎng)絡(luò)編程帶來了一個(gè)非常大的問題:如在調(diào)用send()的同一時(shí)候,線程將被堵塞,而在此期間,線程將無法運(yùn)行不論什么運(yùn)算、響應(yīng),還是網(wǎng)絡(luò)請(qǐng)求。這給多客戶機(jī)、多業(yè)務(wù)邏輯的網(wǎng)絡(luò)編程帶來了挑戰(zhàn)。這時(shí),非常多程序猿可能會(huì)選擇多線程的方式來解決問題。

使用堵塞模式的套接字,開發(fā)網(wǎng)絡(luò)程序比較簡(jiǎn)單,也容易實(shí)現(xiàn)。

當(dāng)希望可以馬上發(fā)送和接收數(shù)據(jù)。且處理的套接字?jǐn)?shù)量比較少的情況下。即一個(gè)一個(gè)處理client,server沒什么壓力。使用堵塞模式來開發(fā)網(wǎng)絡(luò)程序比較合適。

假設(shè)非常多client同一時(shí)候訪問server,server就不能同一時(shí)候處理這些請(qǐng)求。這時(shí),我們可能會(huì)選擇多線程的方式來解決問題。

二、多線程/進(jìn)程處理多個(gè)client

應(yīng)對(duì)多客戶機(jī)的網(wǎng)絡(luò)應(yīng)用,最簡(jiǎn)單的解決方案是在server端使用多線程(或多進(jìn)程)。多線程(或多進(jìn)程)的目的是讓每一個(gè)連接都擁有獨(dú)立的線程(或進(jìn)程),這樣不論什么一個(gè)連接的堵塞都不會(huì)影響其它的連接。

詳細(xì)使用多進(jìn)程還是多線程,并沒有一個(gè)特定的模式。傳統(tǒng)意義上,進(jìn)程的開銷要遠(yuǎn)遠(yuǎn)大于線程,所以,假設(shè)須要同一時(shí)候?yàn)檩^多的客戶機(jī)提供服務(wù)。則不推薦使用多進(jìn)程;假設(shè)單個(gè)服務(wù)運(yùn)行體須要消耗較多的CPU資源,譬如須要進(jìn)行大規(guī)模或長(zhǎng)時(shí)間的數(shù)據(jù)運(yùn)算或文件訪問,則進(jìn)程較為安全。通常,使用pthread_create()創(chuàng)建新線程,fork() 創(chuàng)建新進(jìn)程:

  • 當(dāng)新的connection進(jìn)來,用fork()創(chuàng)建一個(gè)新的process處理業(yè)務(wù);
  • 當(dāng)新的connection進(jìn)來,用pthread_create()產(chǎn)生一個(gè)Thread處理;

多線程/進(jìn)程server同一時(shí)候?yàn)槎鄠€(gè)客戶機(jī)提供應(yīng)答服務(wù)。模型交互過程如下圖所示:

image

主線程持續(xù)等待client的連接請(qǐng)求,假設(shè)有連接,則創(chuàng)建新線程,并在新線程中提供為前例相同的問答服務(wù)。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>                 
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>  

void do_service(int conn);
void err_log(string err, int sockfd) {
    perror("binding");  close(sockfd);      exit(-1);
}

int main(int argc, char *argv[])
{
    unsigned short port = 8000;
    int sockfd;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);// 創(chuàng)建通信端點(diǎn):套接字
    if(sockfd < 0) {
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in my_addr;
    bzero(&my_addr, sizeof(my_addr));
    my_addr.sin_family = AF_INET;
    my_addr.sin_port   = htons(port);
    my_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    int err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));
    if( err_log != 0)   err_log("binding");
    err_log = listen(sockfd, 10);
    if(err_log != 0) err_log("listen");

    struct sockaddr_in peeraddr; //傳出參數(shù)
    socklen_t peerlen = sizeof(peeraddr); //傳入傳出參數(shù)。必須有初始值
    int conn; // 已連接套接字(變?yōu)橹鲃?dòng)套接字,即能夠主動(dòng)connect)
    pid_t pid;
    while (1) {
        if ((conn = accept(sockfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0) //3次握手完畢的序列
            err_log("accept error");
        printf("recv connect ip=%s port=%d/n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

        pid = fork();
        if (pid == -1)
            err_log("fork error");
        if (pid == 0) {// 子進(jìn)程
            close(listenfd);
            do_service(conn);
            exit(EXIT_SUCCESS);
        }
        else
            close(conn); //父進(jìn)程
    }
    return 0;
}

void do_service(int conn) {
    char recvbuf[1024];
    while (1)  {
        memset(recvbuf, 0, sizeof(recvbuf));
        int ret = read(conn, recvbuf, sizeof(recvbuf));
        if (ret == 0)    { //客戶端關(guān)閉了
            printf("client close/n");
            break;
        }
        else if (ret == -1)
            ERR_EXIT("read error");
        fputs(recvbuf, stdout);
        write(conn, recvbuf, ret);
    }
}

非常多剛開始學(xué)習(xí)的人可能不明確為何一個(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句柄值。運(yùn)行完bind()和listen()后,操作系統(tǒng)已經(jīng)開始在指定的port處監(jiān)聽全部的連接請(qǐng)求。假設(shè)有請(qǐng)求,則將該連接請(qǐng)求增加到請(qǐng)求隊(duì)列。調(diào)用accept()接口正是從socket的請(qǐng)求隊(duì)列抽取第一個(gè)連接信息,創(chuàng)建一個(gè)與s同類的新的socket句柄并返回。新的socket句柄即是與client調(diào)用read()和recv()的輸入?yún)?shù)。假設(shè)請(qǐng)求隊(duì)列當(dāng)前沒有請(qǐng)求。則accept()將進(jìn)入堵塞狀態(tài)直到有請(qǐng)求進(jìn)入隊(duì)列。

上述多進(jìn)程的server模型似乎完美的攻克了為多個(gè)客戶機(jī)提供問答服務(wù)的要求,但事實(shí)上并不盡然。假設(shè)要同一時(shí)候響應(yīng)成百上千路的連接請(qǐng)求,則不管多線程還是多進(jìn)程都會(huì)嚴(yán)重占領(lǐng)系統(tǒng)資源,減少系統(tǒng)對(duì)外界響應(yīng)效率。而且線程與進(jìn)程本身也更easy進(jìn)入假死狀態(tài)。

因此其缺點(diǎn)也很明顯:

  1. 用fork()的問題在于每個(gè)Connection進(jìn)來時(shí)的成本太高,假設(shè)同一時(shí)候接入的并發(fā)連接數(shù)太多easy進(jìn)程數(shù)量非常多,進(jìn)程之間的切換開銷會(huì)非常大,同一時(shí)候?qū)τ诶系膬?nèi)核(Linux)會(huì)產(chǎn)生雪崩效應(yīng)。
  2. 用Multi-thread的問題在于Thread-safe與Deadlock問題難以解決。另外有Memory-leak的問題也要處理,這個(gè)問題對(duì)于非常多程序猿來說無異于惡夢(mèng),尤其是對(duì)于連續(xù)server的server程序更是不能夠接受。且在多CPU的系統(tǒng)上沒有辦法使用到全部的CPU resource。

由此可能會(huì)考慮使用“線程池”或“連接池”。

“線程池”旨在降低創(chuàng)建和銷毀線程的頻率,其維持一定合理數(shù)量的線程。并讓空暇的線程又一次承擔(dān)新的運(yùn)行任務(wù)。“連接池”維持連接的緩存池,盡量重用已有的連接、降低創(chuàng)建和關(guān)閉連接的頻率。這兩種技術(shù)都能夠非常好的降低系統(tǒng)開銷,都被廣泛應(yīng)用非常多大型系統(tǒng),如apache,mysql數(shù)據(jù)庫(kù)等。

可是,“線程池”和“連接池”技術(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)整“池”的大小。

相應(yīng)上例中的所面臨的可能同一時(shí)候出現(xiàn)的上千甚至上萬(wàn)次的client請(qǐng)求,“線程池”或“連接池”也許能夠緩解部分壓力,可是不能解決全部問題。并且由于多線程/進(jìn)程還會(huì)導(dǎo)致過多的占用內(nèi)存或CPU等系統(tǒng)資源。

三、非堵塞的server模型

以上面臨的非常多問題,一定程度是IO接口的堵塞特性導(dǎo)致的。多線程是一個(gè)解決方式,還有一個(gè)方案就是使用非堵塞的接口。非堵塞的接口相比于堵塞型接口的顯著差異在于:在被調(diào)用之后馬上返回。

使用例如以下的函數(shù)能夠?qū)⒛尘浔鷉d設(shè)為非堵塞狀態(tài)。我們能夠使用fcntl(fd, F_SETFL, flag | O_NONBLOCK)將套接字標(biāo)志變成非堵塞:

fcntl(fd, F_SETFL, O_NONBLOCK);

以下將給出僅僅用一個(gè)線程。但可以同一時(shí)候從多個(gè)連接中檢測(cè)數(shù)據(jù)是否送達(dá),且接受數(shù)據(jù)。下圖是使用非堵塞接收數(shù)據(jù)的模型:

image

在非堵塞狀態(tài)下,recv()接口在被調(diào)用后馬上返回,返回值代表了不同的含義:比如,假設(shè)設(shè)備臨時(shí)沒有數(shù)據(jù)可讀就返回-1,同時(shí)設(shè)置errno為EWOULDBLOCK(或者EAGAIN,這兩個(gè)宏定義的值同樣)。

  • recv() 返回值大于0,表示接受數(shù)據(jù)完成,返回值即是接受到的字節(jié)數(shù);
  • recv() 返回0,表示連接已經(jīng)正常斷開;
  • recv() 返回-1,且errno等于EAGAIN,表示recv操作還沒運(yùn)行完畢;
  • recv() 返回-1,且errno不等于EAGAIN,表示recv操作遇到系統(tǒng)錯(cuò)誤errno。

這樣的行為方式稱為輪詢(Poll):調(diào)用者僅僅是查詢一下,而不是堵塞在這里死等。這樣能夠同一時(shí)候監(jiān)視多個(gè)設(shè)備:

while(1){

  非堵塞read(設(shè)備1);
  if(設(shè)備1有數(shù)據(jù)到達(dá))
      處理數(shù)據(jù);
  非堵塞read(設(shè)備2);
  if(設(shè)備2有數(shù)據(jù)到達(dá))
     處理數(shù)據(jù);
   ......
}

假設(shè)read(設(shè)備1)是堵塞的,那么僅僅要設(shè)備1沒有數(shù)據(jù)到達(dá)就會(huì)一直堵塞在設(shè)備1的read調(diào)用上,即使設(shè)備2有數(shù)據(jù)到達(dá)也不能處理,使用非堵塞I/O就能夠避免設(shè)備2得不到及時(shí)處理。

非堵塞I/O有一個(gè)缺點(diǎn),假設(shè)全部設(shè)備都一直沒有數(shù)據(jù)到達(dá),調(diào)用者須要重復(fù)查詢做無用功;假設(shè)能堵塞在那里,操作系統(tǒng)就能夠調(diào)度別的進(jìn)程運(yùn)行,就不會(huì)做無用功了。在實(shí)際應(yīng)用中非堵塞I/O模型比較少用。

能夠看到server線程能夠通過循環(huán)調(diào)用recv()接口,在單個(gè)線程內(nèi)實(shí)現(xiàn)對(duì)全部連接的數(shù)據(jù)接收工作。

可是上述模型絕不被推薦。因?yàn)椋h(huán)調(diào)用recv()將大幅度推高CPU占用率。此外。在這個(gè)方法中,recv() 很多其它的是起到檢測(cè)“操作是否完畢”的作用,實(shí)際操作系統(tǒng)提供了更為高效的檢測(cè)“操作是否完畢”作用的接口,比如 select()、poll()、epool()、kqueue()等。

四、IO復(fù)用事件驅(qū)動(dòng)server模型

簡(jiǎn)單介紹:主要是select和epoll,對(duì)一個(gè)IO端口,兩次調(diào)用,兩次返回,對(duì)比堵塞IO并沒有什么優(yōu)越性;關(guān)鍵是select和epoll能實(shí)現(xiàn)同一時(shí)候?qū)Χ鄠€(gè)IO端口進(jìn)行監(jiān)聽

I/O復(fù)用模型會(huì)用到select、poll、epoll函數(shù),這幾個(gè)函數(shù)也會(huì)使進(jìn)程堵塞,可是和堵塞I/O所不同的是:這兩個(gè)函數(shù)能夠同一時(shí)候堵塞多個(gè)I/O操作。并且能夠同一時(shí)候?qū)Χ鄠€(gè)讀操作,多個(gè)寫操作的I/O函數(shù)進(jìn)行檢測(cè),直到有數(shù)據(jù)可讀或可寫時(shí),才真正調(diào)用I/O操作函數(shù)。

我們先具體解釋select。SELECT函數(shù)進(jìn)行IO復(fù)用server模型的原理是:當(dāng)一個(gè)client連接上server時(shí)。server就將其連接的fd增加到fd_set集合,等到這個(gè)連接準(zhǔn)備好讀或?qū)懙臅r(shí)候,就通知程序進(jìn)行IO操作,與client進(jìn)行數(shù)據(jù)通信。大部分Unix/Linux都支持select函數(shù),該函數(shù)用于探測(cè)多個(gè)文件句柄的狀態(tài)變化。

4.1 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 maxfdp, //Winsock中此參數(shù)無意義
     fd_set* readfds, //進(jìn)行可讀檢測(cè)的Socket
     fd_set* writefds, //進(jìn)行可寫檢測(cè)的Socket
     fd_set* exceptfds, //進(jìn)行異常檢測(cè)的Socket
     const struct timeval* timeout //非堵塞模式中設(shè)置最大等待時(shí)間
)

下面對(duì)select函數(shù)的參數(shù)詳細(xì)介紹。

  1. maxfdp

是一個(gè)整數(shù)值,意思是“最大fd加1(max fd plus 1)。在三個(gè)描寫敘述符集(readfds, writefds, exceptfds)中找出最大描寫敘述符。它是一個(gè)編號(hào)值,也可將maxfdp設(shè)置為FD_SETSIZE,這是一個(gè)< sys/types.h >中的常數(shù),它說明了最大的描寫敘述符數(shù)(常常是256或1024),可是對(duì)大多數(shù)應(yīng)用程序而言,此值太大了。

確實(shí),大多數(shù)應(yīng)用程序僅僅應(yīng)用3 ~ 10個(gè)描寫敘述符。假設(shè)將第三個(gè)參數(shù)設(shè)置為最高描寫敘述符編號(hào)值加 1,內(nèi)核就僅僅需在此范圍內(nèi)尋找打開的位,而不必在數(shù)百位的大范圍內(nèi)搜索。

  1. readfds

是指向fd_set結(jié)構(gòu)的指針,這個(gè)集合中應(yīng)該包含文件描寫敘述符,表示我們要監(jiān)視這些文件描寫敘述符的讀變化的。即我們關(guān)心能否夠從這些文件里讀取數(shù)據(jù)了,假設(shè)這個(gè)集合中有一個(gè)文件可讀,select就會(huì)返回一個(gè)大于0的值。表示有文件可讀,假設(shè)沒有可讀的文件。則依據(jù)timeout參數(shù)再推斷是否超時(shí),若超出timeout的時(shí)間,select返回0,若錯(cuò)誤發(fā)生返回負(fù)值。能夠傳入NULL值;表示不關(guān)心不論什么文件的讀變化。

  1. writefds

是指向fd_set結(jié)構(gòu)的指針,這個(gè)集合中應(yīng)該包含文件描寫敘述符,表示我們要監(jiān)視這些文件描寫敘述符的寫變化的,即我們關(guān)心能否夠向這些文件里寫入數(shù)據(jù)了,假設(shè)這個(gè)集合中有一個(gè)文件可寫,select就會(huì)返回一個(gè)大于0的值,表示有文件可寫,假設(shè)沒有可寫的文件,則依據(jù)timeout參數(shù)再推斷是否超時(shí),若超出timeout的時(shí)間,select返回0,若錯(cuò)誤發(fā)生返回負(fù)值。能夠傳入NULL值,表示不關(guān)心不論什么文件的寫變化。

  1. exceptfds

同上面兩個(gè)參數(shù)的意圖,用來監(jiān)視文件錯(cuò)誤異常。readfds,writefds, exceptfds每一個(gè)描寫敘述符集存放在一個(gè)fd_set數(shù)據(jù)類型中。

  1. timeout

是select的超時(shí)時(shí)間,這個(gè)參數(shù)至關(guān)重要。它能夠使select處于三種狀態(tài):

  • 第一,若將NULL以形參傳入,即不傳入時(shí)間結(jié)構(gòu),就是將select置于堵塞狀態(tài),一定等到監(jiān)視文件描寫敘述符集合中某個(gè)文件描寫敘述符發(fā)生變化為止;
  • 第二,若將時(shí)間值設(shè)為0秒0毫秒,就變成一個(gè)純粹的非堵塞函數(shù),無論文件描寫敘述符是否有變化,都立馬返回繼續(xù)運(yùn)行;文件無變化返回0,有變化返回一個(gè)正值;
  • 第三,timeout的值大于0,這就是等待的超時(shí)時(shí)間,即select在timeout時(shí)間內(nèi)堵塞。超時(shí)時(shí)間之內(nèi)有事件到來就返回了,否則在超時(shí)后無論如何一定返回,返回值同上述。

4.2 使用select的步驟

  1. 創(chuàng)建所關(guān)注的事件的描寫敘述符集合(fd_set)。對(duì)于一個(gè)描寫敘述符,能夠關(guān)注其上面的讀(read)、寫(write)、異常(exception)事件。所以通常,要?jiǎng)?chuàng)建三個(gè)fd_set:一個(gè)用來收集關(guān)注讀事件的描寫敘述符,一個(gè)用來收集關(guān)注寫事件的描寫敘述符,另外一個(gè)用來收集關(guān)注異常事件的描寫敘述符集合;
  2. 調(diào)用select(),等待事件發(fā)生;這里須要注意的一點(diǎn)是,select的堵塞與是否設(shè)置非堵塞I/O是沒有關(guān)系的;
  3. 輪詢?nèi)縡d_set中的每個(gè)fd,檢查是否有對(duì)應(yīng)的事件發(fā)生;假設(shè)有,就進(jìn)行處理。
/* 可讀、可寫、異常三種文件描寫敘述符集的申明和初始化 */
fd_set readfds, writefds, exceptionfds;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&exceptionfds);

int max_fd;

/* socket配置和監(jiān)聽 */
sock = socket(...);
bind(sock, ...);
listen(sock, ...);

/* 對(duì)socket描寫敘述符上發(fā)生關(guān)心的事件進(jìn)行注冊(cè) */
FD_SET(&readfds, sock);
max_fd = sock; 

while(1) {
    int i;
    fd_set r,w,e;

    /* 為了反復(fù)使用readfds 、writefds、exceptionfds,將它們復(fù)制到暫時(shí)變量?jī)?nèi)。*/
    memcpy(&r, &readfds, sizeof(fd_set));
    memcpy(&w, &writefds, sizeof(fd_set));
    memcpy(&e, &exceptionfds, sizeof(fd_set));

    /* 利用暫時(shí)變量調(diào)用select()堵塞等待。timeout=null表示等待時(shí)間為永遠(yuǎn)等待直到發(fā)生事件。*/
    select(max_fd + 1, &r, &w, &e, NULL);

    /* 測(cè)試是否有client發(fā)起連接請(qǐng)求,假設(shè)有則接受并把新建的描寫敘述符增加監(jiān)控。*/
    if(FD_ISSET(&r, sock)){
        new_sock = accept(sock, ...);
        FD_SET(&readfds, new_sock);
        FD_SET(&writefds, new_sock);
        max_fd = MAX(max_fd, new_sock);
    }

    /* 對(duì)其他描寫敘述符發(fā)生的事件進(jìn)行適當(dāng)處理。描寫敘述符依次遞增,
       最大值各系統(tǒng)有所不同(比方在作者系統(tǒng)上最大為1024)。
       在linux能夠用命令ulimit -a查看(用ulimit命令也對(duì)該值進(jìn)行改動(dòng));
       在freebsd下,用sysctl -a | grep kern.maxfilesperproc來查詢和改動(dòng)。 */
    for(i= sock+1; i <max_fd+1; ++i) {
        if(FD_ISSET(&r, i))
            doReadAction(i);
        if(FD_ISSET(&w, i))
            doWriteAction(i);
    }
}

4.3 select相關(guān)的四個(gè)宏

FD_ZERO(int fd, fd_set* fds)  //清除其全部位
FD_SET(int fd, fd_set* fds)  //在某 fd_set 中標(biāo)記一個(gè)fd的相應(yīng)位為1
FD_ISSET(int fd, fd_set* fds) // 測(cè)試該集中的一個(gè)給定位是否仍舊設(shè)置
FD_CLR(int fd, fd_set* fds)  //刪除相應(yīng)位

這里,fd_set類型能夠簡(jiǎn)單的理解為按bit位標(biāo)記句柄的隊(duì)列,比如要在某fd_set中標(biāo)記一個(gè)值為16的句柄,則該fd_set的第16個(gè)bit位被標(biāo)記為1。詳細(xì)的置位、驗(yàn)證可使用FD_SET、FD_ISSET等宏實(shí)現(xiàn)。

[圖片上傳失敗...(image-1877de-1558955750837)]

比如,編寫如下代碼:

fd_setreadset,writeset;
FD_ZERO(&readset);
FD_ZERO(&writeset);
FD_SET(0,&readset);
FD_SET(3,&readset);
FD_SET(1,&writeset);
FD_SET(2,&writeset);
select(4,&readset,&writeset,NULL,NULL);

下圖顯示了這兩個(gè)描寫敘述符集的情況:

image

由于描寫敘述符編號(hào)從0開始,所以要在最大描寫敘述符編號(hào)值上加1。第一個(gè)參數(shù)實(shí)際上是要檢查的描寫敘述符數(shù)(從描寫敘述符0開始)。

4.4 select的返回值

select的返回值有三種可能:

  1. 返回值-1表示出錯(cuò);是可能發(fā)生的,比如在所指定的描寫敘述符都沒有準(zhǔn)備好時(shí)捕捉到一個(gè)信號(hào);
  2. 返回值0表示沒有描寫敘述符準(zhǔn)備好;若指定的描寫敘述符都沒有準(zhǔn)備好,并且指定的時(shí)間已經(jīng)超過,則發(fā)生這樣的情況。
  3. 返回一個(gè)正值說明了已經(jīng)準(zhǔn)備好的描寫敘述符數(shù);在這樣的情況下,三個(gè)描寫敘述符集中仍舊打開的位是相應(yīng)于已準(zhǔn)備好的描寫敘述符位。

4.5 select接收數(shù)據(jù)模型

下圖是使用select多路復(fù)用技術(shù)的數(shù)據(jù)接收模型:

image

上述模型僅僅是描寫敘述了使用select()接口同一時(shí)候從多個(gè)client接收數(shù)據(jù)的過程;因?yàn)閟elect()接口能夠同一時(shí)候?qū)Χ鄠€(gè)句柄進(jìn)行讀狀態(tài)、寫狀態(tài)和錯(cuò)誤狀態(tài)的探測(cè),所以能夠非常easy構(gòu)建為多個(gè)client提供獨(dú)立問答服務(wù)的server系統(tǒng)。下面是使用select()接口事件驅(qū)動(dòng)的server模型。

image

上述模型中,最關(guān)鍵的地方是怎樣動(dòng)態(tài)維護(hù)select()的三個(gè)參數(shù)readfds、writefds和exceptfds:

  • 作為輸入?yún)?shù),readfds應(yīng)該標(biāo)記全部的須要探測(cè)的“可讀事件”的句柄,當(dāng)中永遠(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ù)流程。所以,假設(shè)select()發(fā)現(xiàn)某句柄捕捉到了“可讀事件”,server程序應(yīng)及時(shí)做recv()操作。并依據(jù)接收到的數(shù)據(jù)準(zhǔn)備好待發(fā)送數(shù)據(jù)。并將相應(yīng)的句柄值增加writefds,準(zhǔn)備下一次的“可寫事件”的select()探測(cè)。類似的,假設(shè)select()發(fā)現(xiàn)某句柄捕捉到“可寫事件”,則程序應(yīng)及時(shí)做send()操作,并準(zhǔn)備好下一次的“可寫事件”探測(cè)準(zhǔn)備。下圖描寫敘述的是上述模型中的一個(gè)運(yùn)行周期:

image

這樣的模型的特征在于每個(gè)運(yùn)行周期都會(huì)探測(cè)一次或一組事件。一個(gè)特定的事件會(huì)觸發(fā)某個(gè)特定的響應(yīng)。我們能夠?qū)⑦@樣的模型歸類為“事件驅(qū)動(dòng)模型”。

4.6 select的優(yōu)缺點(diǎn)

相比其它模型,使用select()的事件驅(qū)動(dòng)模型僅僅用單線程(進(jìn)程)運(yùn)行,占用資源少,不消耗太多 CPU,同一時(shí)候可以為多client提供服務(wù)。假設(shè)試圖建立一個(gè)簡(jiǎn)單的事件驅(qū)動(dòng)的server程序,這個(gè)模型有一定的參考價(jià)值。但這個(gè)模型依然有著非常多問題。

select的缺點(diǎn):

  1. 單個(gè)進(jìn)程可以監(jiān)視的文件描寫敘述符的數(shù)量存在最大限制;
  2. select須要復(fù)制大量的句柄數(shù)據(jù)結(jié)構(gòu),產(chǎn)生巨大的開銷;
  3. select返回的是含有整個(gè)句柄的列表,應(yīng)用程序須要消耗大量時(shí)間去輪詢各個(gè)句柄才干發(fā)現(xiàn)哪些句柄發(fā)生了事件;
  4. select的觸發(fā)方式是水平觸發(fā),應(yīng)用程序假設(shè)沒有完畢對(duì)一個(gè)已經(jīng)就緒的文件描寫敘述符進(jìn)行IO操作,那么之后每次select調(diào)用還是會(huì)將這些文件描寫敘述符通知進(jìn)程;與之對(duì)應(yīng)方式的是邊緣觸發(fā);
  5. 該模型將事件探測(cè)和事件響應(yīng)夾雜在一起,一旦事件響應(yīng)的運(yùn)行體龐大,則對(duì)整個(gè)模型是災(zāi)難性的。

例如以下例:龐大的運(yùn)行體1的將直接導(dǎo)致響應(yīng)事件2的運(yùn)行體遲遲得不到運(yùn)行,并在非常大程度上減少了事件探測(cè)的及時(shí)性。

image

龐大的運(yùn)行體對(duì)使用select()的事件驅(qū)動(dòng)模型的影響。

非常多操作系統(tǒng)提供了更為高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll等。假設(shè)須要實(shí)現(xiàn)更高效的server程序,類似epoll這種接口更被推薦。

4.7 poll事件模型

poll庫(kù)是在linux 2.1.23中引入的,windows平臺(tái)不支持poll。poll與select的基本方式同樣。都是先創(chuàng)建一個(gè)關(guān)注事件的描寫敘述符的集合,然后再去等待這些事件發(fā)生;然后再輪詢描寫敘述符集合,檢查有沒有事件發(fā)生,假設(shè)有,就進(jìn)行處理。

因此。poll有著與select相似的處理流程:

  1. 創(chuàng)建描寫敘述符集合,設(shè)置關(guān)注的事件;
  2. 調(diào)用poll(),等待事件發(fā)生,以下是poll的原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

類似select,poll也能夠設(shè)置等待時(shí)間,效果與select一樣。

  1. 輪詢描寫敘述符集合,檢查事件,處理事件。

在這里要說明的是:poll與select的主要差別在與,select須要為讀、寫、異常事件分別創(chuàng)建一個(gè)描寫敘述符集合,最后輪詢的時(shí)候,須要分別輪詢這三個(gè)集合;而poll僅僅須要一個(gè)集合,在每一個(gè)描寫敘述符相應(yīng)的結(jié)構(gòu)上分別設(shè)置讀、寫、異常事件,最后輪詢的時(shí)候,能夠同時(shí)檢查三種事件。

4.8 epoll事件模型

epoll是和上面的poll和select不同的一個(gè)事件驅(qū)動(dòng)庫(kù),它是在linux-2.5.44中引入的,它屬于poll的一個(gè)變種。

poll和select庫(kù)的最大的問題就在于效率,它們的處理方式都是創(chuàng)建一個(gè)事件列表,然后把這個(gè)列表發(fā)給內(nèi)核,返回的時(shí)候,再去輪詢檢查這個(gè)列表,這樣在描寫敘述符比較多的應(yīng)用中,效率就顯得比較低下了。

epoll是一種比較好的做法,它把描寫敘述符列表交給內(nèi)核,一旦有事件發(fā)生,內(nèi)核把發(fā)生事件的描寫敘述符列表通知給進(jìn)程,這樣就避免了輪詢整個(gè)描寫敘述符列表。以下對(duì)epoll的使用進(jìn)行說明:

  1. 創(chuàng)建一個(gè)epoll描寫敘述符,調(diào)用epoll_create()來完畢,epoll_create()有一個(gè)整型的參數(shù)size,用來告訴內(nèi)核,要?jiǎng)?chuàng)建一個(gè)有size個(gè)描寫敘述符的事件列表(集合);
int epoll_create(int size);
  1. 給描寫敘述符設(shè)置所關(guān)注的事件,并把它加入到內(nèi)核的事件列表中去,這里須要調(diào)用epoll_ctl()來完成:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

這里op參數(shù)有三種,分別代表三種操作:

  • EPOLL_CTL_ADD, 把要關(guān)注的描寫敘述符和對(duì)其關(guān)注的事件的結(jié)構(gòu)。加入到內(nèi)核的事件列表中去;
  • EPOLL_CTL_DEL,把先前加入的描寫敘述符和對(duì)其關(guān)注的事件的結(jié)構(gòu),從內(nèi)核的事件列表中去除;
  • EPOLL_CTL_MOD,改動(dòng)先前加入到內(nèi)核的事件列表中的描寫敘述符的關(guān)注的事件;
  1. 等待內(nèi)核通知事件發(fā)生,得到發(fā)生事件的描寫敘述符的結(jié)構(gòu)列表;該過程由epoll_wait()完畢。得到事件列表后,就能夠進(jìn)行事件處理了。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)

在使用epoll的時(shí)候,有一個(gè)須要特別注意的地方,那就是epoll觸發(fā)事件的文件有兩種方式:

  1. Edge Triggered(ET),在這樣的情況下,事件是由數(shù)據(jù)到達(dá)邊界觸發(fā)的;所以要在處理讀、寫的時(shí)候,要不斷的調(diào)用read/write,直到它們返回EAGAIN,然后再去epoll_wait(),等待下次事件的發(fā)生。這樣的方式適用要遵從以下的原則:
  • 使用非堵塞的I/O;
  • 直到read/write返回EAGAIN時(shí),才去等待下一次事件的發(fā)生。
  1. Level Triggered(LT), 在這樣的情況下,epoll和poll類似,但處理速度上比poll更快;在這樣的情況下,僅僅要有數(shù)據(jù)沒有讀/寫完,調(diào)用epoll_wait()的時(shí)候,就會(huì)有事件被觸發(fā)。
/* 新建并初始化文件描寫敘述符集 */
struct epoll_event ev;
struct epoll_event events[MAX_EVENTS];

/* 創(chuàng)建epoll句柄。*/
int epfd = epoll_create(MAX_EVENTS);

/* socket配置和監(jiān)聽。*/
sock = socket(...);
bind(sock, ...);
listen(sock, ...);

/* 對(duì)socket描寫敘述符上發(fā)生關(guān)心的事件進(jìn)行注冊(cè)。*/
ev.events = EPOLLIN;
ev.data.fd = sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);

while(1) {
    int i;
    /*調(diào)用epoll_wait()堵塞等待。等待時(shí)間為永遠(yuǎn)等待直到發(fā)生事件。*/
    int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
    for(i=0; i <n; ++i) {
        /* 測(cè)試是否有client發(fā)起連接請(qǐng)求,假設(shè)有則接受并把新建的描寫敘述符增加監(jiān)控。*/
        if(events.data.fd == sock) {
            if(events.events & POLLIN){
                new_sock = accept(sock, ...);
                ev.events = EPOLLIN | POLLOUT;
                ev.data.fd = new_sock;
                epoll_ctl(epfd, EPOLL_CTL_ADD, new_sock, &ev);
            }
        }else{
        /* 對(duì)其他描寫敘述符發(fā)生的事件進(jìn)行適當(dāng)處理。*/
            if(events.events & POLLIN)
                doReadAction(i);
            if(events.events & POLLOUT)
                doWriteAction(i);
        }
    }
}

epoll支持水平觸發(fā)和邊緣觸發(fā),理論上來說邊緣觸發(fā)性能更高。可是使用更加復(fù)雜,由于不論什么意外的丟失事件都會(huì)造成請(qǐng)求處理錯(cuò)誤。Nginx就使用了epoll的邊緣觸發(fā)模型。

這里提一下水平觸發(fā)和邊緣觸發(fā)就緒通知的差別,這兩個(gè)詞來源于計(jì)算機(jī)硬件設(shè)計(jì)。

它們的差別是:水平觸發(fā)僅僅要句柄滿足某種狀態(tài),就會(huì)發(fā)出通知;邊緣觸發(fā)則只有當(dāng)句柄狀態(tài)改變時(shí),才會(huì)發(fā)出通知。比如一個(gè)socket經(jīng)過長(zhǎng)時(shí)間等待后接收到一段100k的數(shù)據(jù),兩種觸發(fā)方式都會(huì)向程序發(fā)出就緒通知。如果程序從這個(gè)socket中讀取了50k數(shù)據(jù),并再次調(diào)用監(jiān)聽函數(shù),水平觸發(fā)依舊會(huì)發(fā)出就緒通知,而邊緣觸發(fā)會(huì)由于socket“有數(shù)據(jù)可讀”這個(gè)狀態(tài)沒有發(fā)生變化而不發(fā)出通知且陷入長(zhǎng)時(shí)間的等待。
因此在使用邊緣觸發(fā)的API時(shí),要注意每次都要讀到socket返回EWOULDBLOCK為止。

遺憾的是不同的操作系統(tǒng)特供的epoll接口有非常大差異,所以使用類似于epoll的接口實(shí)現(xiàn)具有較好跨平臺(tái)能力的server會(huì)比較困難。然而幸運(yùn)的是,有非常多高效的事件驅(qū)動(dòng)庫(kù)能夠屏蔽上述的困難,常見的事件驅(qū)動(dòng)庫(kù)有l(wèi)ibevent庫(kù),還有作為libevent替代者的libev庫(kù)。

這些庫(kù)會(huì)依據(jù)操作系統(tǒng)的特點(diǎn)選擇最合適的事件探測(cè)接口,而且增加了信號(hào)(signal)等技術(shù)以支持異步響應(yīng),這使得這些庫(kù)成為構(gòu)建事件驅(qū)動(dòng)模型的不二選擇。

下章將介紹怎樣使用libev庫(kù)替換select或epoll接口,實(shí)現(xiàn)高效穩(wěn)定的server模型。

五、libevent方法

libevent是一個(gè)事件觸發(fā)的網(wǎng)絡(luò)庫(kù),適用于windows、linux、bsd等多種平臺(tái),內(nèi)部使用select、epoll、kqueue等系統(tǒng)調(diào)用管理事件機(jī)制。著名分布式緩存軟件memcached也是libevent-based,并且libevent在使用上能夠做到跨平臺(tái)。并且依據(jù)libevent官方站點(diǎn)上發(fā)布的數(shù)據(jù)統(tǒng)計(jì),似乎也有著非凡的性能。

libevent庫(kù)實(shí)際上沒有更換select()、poll()或其它機(jī)制的基礎(chǔ),而是使用對(duì)于每一個(gè)平臺(tái)最高效的高性能解決方式在實(shí)現(xiàn)外加上一個(gè)包裝器。

為了實(shí)際處理每一個(gè)請(qǐng)求,libevent庫(kù)提供一種事件機(jī)制,它作為底層網(wǎng)絡(luò)后端的包裝器。事件系統(tǒng)讓為連接加入處理函數(shù)變得很簡(jiǎn)便,同一時(shí)候減少了底層I/O復(fù)雜性。這是libevent系統(tǒng)的核心。

libevent庫(kù)的其它組件提供其它功能:包含緩沖的事件系統(tǒng)(用于緩沖發(fā)送到client/從client接收的數(shù)據(jù))以及 HTTP、DNS 和RPC系統(tǒng)的核心實(shí)現(xiàn)。

5.1 libevent的特點(diǎn)與優(yōu)勢(shì)

  1. 事件驅(qū)動(dòng),高性能;
  2. 輕量級(jí),專注于網(wǎng)絡(luò);
  3. 跨平臺(tái),支持 Windows、Linux、Mac Os等;
  4. 支持多種I/O多路復(fù)用技術(shù),epoll、poll、dev/poll、select和kqueue等;
  5. 支持I/O、定時(shí)器和信號(hào)等事件。

5.2 libevent組成部分

  1. event及event_base事件管理包含各種IO(socket)、定時(shí)器、信號(hào)等事件,也是libevent應(yīng)用最廣的模塊;
  2. evbuffer event及event_base緩存管理是指evbuffer功能,提供了高效的讀寫方法;
  3. evdns DNS是libevent提供的一個(gè)異步DNS查詢功能;
  4. evhttp HTTP是libevent的一個(gè)輕量級(jí)http實(shí)現(xiàn),包含server和client。

libevent也支持ssl,這對(duì)于有安全需求的網(wǎng)絡(luò)程序非常的重要。可是其支持不是非常完好,比方http server的實(shí)現(xiàn)就不支持ssl。

5.3 事件處理框架

libevent是事件驅(qū)動(dòng)的庫(kù),所謂事件驅(qū)動(dòng),簡(jiǎn)單地說就是你點(diǎn)什么button(即產(chǎn)生什么事件),電腦運(yùn)行什么操作(即調(diào)用什么函數(shù))。

image

Libevent框架本質(zhì)上是一個(gè)典型的Reactor模式,所以僅僅須要弄懂Reactor模型,libevent就八九不離十了。Reactor模式,也是一種事件驅(qū)動(dòng)機(jī)制。

應(yīng)用程序須要提供對(duì)應(yīng)的接口并注冊(cè)到Reactor上,假設(shè)對(duì)應(yīng)的事件發(fā)生,Reactor將主動(dòng)調(diào)用應(yīng)用程序注冊(cè)的接口,這些接口又稱為“回調(diào)函數(shù)”。在Libevent中也是一樣,向Libevent框架注冊(cè)對(duì)應(yīng)的事件和回調(diào)函數(shù);當(dāng)這些事件發(fā)生時(shí),Libevent會(huì)調(diào)用這些回調(diào)函數(shù)處理對(duì)應(yīng)的事件(I/O讀寫、定時(shí)和信號(hào))。

使用Reactor模型,必備的幾個(gè)組件:事件源、Reactor框架、多路復(fù)用機(jī)制和事件處理程序。先來看看Reactor模型的總體框架,接下來再對(duì)每一個(gè)組件做逐一說明。

image
  1. 事件源:Linux上是文件描寫敘述符,Windows上就是Socket或者Handle了,這里統(tǒng)一稱為“句柄集”;程序在指定的句柄上注冊(cè)關(guān)心的事件,比方I/O事件;
  2. event demultiplexer(事件多路分發(fā)機(jī)制):由操作系統(tǒng)提供的I/O多路復(fù)用機(jī)制,比方select和epoll;程序首先將其關(guān)心的句柄(事件源)及其事件注冊(cè)到event demultiplexer上,當(dāng)有事件到達(dá)時(shí),event demultiplexer會(huì)發(fā)出通知“在已經(jīng)注冊(cè)的句柄集中,一個(gè)或多個(gè)句柄的事件已經(jīng)就緒”;程序收到通知后,就能夠在非堵塞的情況下對(duì)事件進(jìn)行處理了;在libevent中,依舊是select、poll、epoll等,可是libevent使用結(jié)構(gòu)體eventop進(jìn)行了封裝,以統(tǒng)一的接口來支持這些I/O多路復(fù)用機(jī)制,達(dá)到了對(duì)外隱藏底層系統(tǒng)機(jī)制的目的。
  3. Reactor(反應(yīng)器):Reactor,是事件管理的接口,內(nèi)部使用event demultiplexer注冊(cè)、注銷事件;并執(zhí)行事件循環(huán);當(dāng)有事件進(jìn)入“就緒”狀態(tài)時(shí),調(diào)用注冊(cè)事件的回調(diào)函數(shù)處理事件;在libevent中,就是event_base結(jié)構(gòu)體。
  4. Event Handler(事件處理程序):事件處理程序提供了一組接口,每一個(gè)接口對(duì)應(yīng)了一種類型的事件;供Reactor在對(duì)應(yīng)的事件發(fā)生時(shí)調(diào)用,運(yùn)行對(duì)應(yīng)的事件處理,通常它會(huì)綁定一個(gè)有效的句柄;對(duì)應(yīng)libevent中,就是event結(jié)構(gòu)體。

結(jié)合Reactor框架,我們來理一下libevent的事件處理流程,如下圖所示:

image
  1. event_init()初始化

首先要隆重介紹event_base對(duì)象(結(jié)構(gòu)體):

struct event_base {
    const struct eventop *evsel;
    void *evbase;
    int event_count;        /* counts number of total events */
    int event_count_active; /* counts number of active events */
    
    int event_gotterm;      /* Set to terminate loop */
        
    /* active event management */
    struct event_list **activequeues;
    int nactivequeues;
    struct event_list eventqueue;
    struct timeval event_tv;
    RB_HEAD(event_tree, event) timetree;
};

event_base對(duì)象整合了事件處理的一些全局變量,角色是event對(duì)象的"總管家", 他包含了:

  • 事件引擎函數(shù)對(duì)象(evsel, evbase);
  • 當(dāng)前入列事件列表(event_count, event_count_active, eventqueue);
  • 全局終止信號(hào)(event_gotterm);
  • 活躍事件列表(avtivequeues);
  • 事件隊(duì)列樹(timetree)...。

初始化時(shí)創(chuàng)建event_base對(duì)象,選擇當(dāng)前OS支持的事件引擎(epoll, poll, select...)并初始化,創(chuàng)建全局信號(hào)隊(duì)列(signalqueue),活躍隊(duì)列的內(nèi)存分配(依據(jù)設(shè)置的priority個(gè)數(shù),默覺得1)。

  1. event_set()

event_set來設(shè)置event對(duì)象,包含全部者event_base對(duì)象、fd、事件(EV_READ|EV_WRITE|EV_PERSIST),回掉函數(shù)和參數(shù),事件優(yōu)先級(jí)是當(dāng)前event_base的中間級(jí)別(current_base->nactivequeues/2)。

設(shè)置監(jiān)視事件后,事件處理函數(shù)能夠僅僅被調(diào)用一次或總被調(diào)用。

  • 僅僅調(diào)用一次:事件處理函數(shù)被調(diào)用后,即從事件隊(duì)列中刪除。須要在事件處理函數(shù)中再次增加事件,才干在下次事件發(fā)生時(shí)被調(diào)用;
  • 總被調(diào)用:設(shè)置為EV_PERSIST,僅僅增加一次,處理函數(shù)總被調(diào)用,除非採(cǎi)用event_remove顯式地刪除。
  1. event_add()
int event_add(struct event *ev, struct timeval *tv)

這個(gè)接口有兩個(gè)參數(shù),第一個(gè)是要加入的事件,第二個(gè)參數(shù)作為事件的超時(shí)值(timer)。假設(shè)該值非NULL,在加入本事件的同一時(shí)候加入超時(shí)事件(EV_TIMEOUT)到時(shí)間隊(duì)列樹(timetree),依據(jù)事件類型處理例如以下:

  • EV_READ => EVLIST_INSERTED => eventqueue
  • EV_WRITE => EVLIST_INSERTED => eventqueue
  • EV_TIMEOUT => EVLIST_TIMEOUT => timetree
  • EV_SIGNAL => EVLIST_SIGNAL => signalqueue
  1. event_base_loop()

這里是事件的主循環(huán),僅僅要flags不是設(shè)置為EVLOOP_NONBLOCK,該函數(shù)就會(huì)一直循環(huán)監(jiān)聽事件/處理事件。每次循環(huán)過程中,都會(huì)處理當(dāng)前觸發(fā)(活躍)事件:

  1. 檢測(cè)當(dāng)前是否有信號(hào)處理(gotterm、gotsig),這些都是全局參數(shù),不適合多線程;
  2. 時(shí)間更新,找到離當(dāng)前近期的時(shí)間事件,得到相對(duì)超時(shí)事件tv;
  3. 調(diào)用事件引擎的dispatch wait事件觸發(fā),超時(shí)值為tv,觸發(fā)事件加入到activequeues;
  4. 處理活躍事件,調(diào)用caller的callbacks (event_process_acitve)。

5.4 libevent典型應(yīng)用的大致流程

創(chuàng)建libevent server的基本方法是,注冊(cè)當(dāng)發(fā)生某一操作(比方接受來自client的連接)時(shí)應(yīng)該運(yùn)行的函數(shù),然后調(diào)用主事件循環(huán)event_dispatch()。運(yùn)行過程的控制如今由libevent系統(tǒng)處理。

注冊(cè)事件和將調(diào)用的函數(shù)之后,事件系統(tǒng)開始自治。在應(yīng)用程序執(zhí)行時(shí),能夠在事件隊(duì)列中加入(注冊(cè))或刪除(取消注冊(cè))事件。事件注冊(cè)很方便,能夠通過它加入新事件以處理新打開的連接,從而構(gòu)建靈活的網(wǎng)絡(luò)處理系統(tǒng):

(環(huán)境設(shè)置)-> (創(chuàng)建event_base) -> (注冊(cè)event,將此event增加到event_base中) -> (設(shè)置event各種屬性、事件等) -> (將event增加事件列表 addevent) -> (開始事件監(jiān)視循環(huán)、分發(fā)dispatch)

image

5.5 示例

打開一個(gè)監(jiān)聽套接字,然后注冊(cè)一個(gè)回調(diào)函數(shù),每當(dāng)須要調(diào)用accept()函數(shù)以打開新連接時(shí)調(diào)用這個(gè)回調(diào)函數(shù),這樣就創(chuàng)建了一個(gè)網(wǎng)絡(luò)server。例如以下所看到的的代碼片段說明基本過程。

#include <stdio.h>
#include <string.h>
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <event.h>
using namespace std;

// 事件base
struct event_base* base;

// 讀事件回調(diào)函數(shù)
void onRead(int iCliFd, short iEvent, void *arg)
{
    int iLen;
    char buf[1500];
    iLen = recv(iCliFd, buf, 1500, 0);
    if (iLen <= 0) {
        cout << "Client Close" << endl;
        // 連接結(jié)束(=0)或連接錯(cuò)誤(<0)。將事件刪除并釋放內(nèi)存空間
        struct event *pEvRead = (struct event*)arg;
        event_del(pEvRead);
        delete pEvRead;
        close(iCliFd);
        return;
    }
    buf[iLen] = 0;
    cout << "Client Info:" << buf << endl;
}

// 連接請(qǐng)求事件回調(diào)函數(shù)
void onAccept(int iSvrFd, short iEvent, void *arg)
{
    int iCliFd;
    struct sockaddr_in sCliAddr;
    socklen_t iSinSize = sizeof(sCliAddr);
    iCliFd = accept(iSvrFd, (struct sockaddr*)&sCliAddr, &iSinSize);
    // 連接注冊(cè)為新事件 (EV_PERSIST為事件觸發(fā)后不默認(rèn)刪除)
    struct event *pEvRead = new event;
    event_set(pEvRead, iCliFd, EV_READ|EV_PERSIST, onRead, pEvRead);
    event_base_set(base, pEvRead);
    event_add(pEvRead, NULL);
}

int main()
{
    int iSvrFd;
    struct sockaddr_in sSvrAddr;
    memset(&sSvrAddr, 0, sizeof(sSvrAddr));
    sSvrAddr.sin_family = AF_INET;
    sSvrAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    sSvrAddr.sin_port = htons(8888);

    // 創(chuàng)建tcpSocket(iSvrFd),監(jiān)聽本機(jī)8888端口
    iSvrFd = socket(AF_INET, SOCK_STREAM, 0);
    bind(iSvrFd, (struct sockaddr*)&sSvrAddr, sizeof(sSvrAddr));
    listen(iSvrFd, 10);

    // 初始化base
    base = event_base_new();
    struct event evListen;
    // 設(shè)置事件
    event_set(&evListen, iSvrFd, EV_READ|EV_PERSIST, onAccept, NULL);
    // 設(shè)置為base事件
    event_base_set(base, &evListen);
    // 加入事件
    event_add(&evListen, NULL);

    // 事件循環(huán)
    event_base_dispatch(base);
    return 0;
}

event_set()函數(shù)創(chuàng)建新的事件結(jié)構(gòu);event_add()在事件隊(duì)列機(jī)制中加入事件;然后,event_dispatch()啟動(dòng)事件隊(duì)列系統(tǒng),開始監(jiān)聽(并接收)請(qǐng)求。

六、libev庫(kù)

官方文檔:http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod

與libevent一樣,libev系統(tǒng)也是基于事件循環(huán)的系統(tǒng),它在poll()、select()等機(jī)制的本機(jī)實(shí)現(xiàn)的基礎(chǔ)上提供基于事件的循環(huán)。

libev是libevent之后的一個(gè)事件驅(qū)動(dòng)的編程框架。其接口和libevent基本類似。據(jù)官方介紹,其性能比libevent還要高,bug比libevent還少。

libev API比較原始,沒有HTTP包裝器,可是libev支持在實(shí)現(xiàn)中內(nèi)置很多其它事件類型。比如,一種evstat實(shí)現(xiàn)能夠監(jiān)視多個(gè)文件的屬性變動(dòng),能夠在例4所看到的HTTP文件解決方式中使用它。

可是,libevent和libev的基本過程是同樣的。創(chuàng)建所需的網(wǎng)絡(luò)監(jiān)聽套接字,注冊(cè)在運(yùn)行期間要調(diào)用的事件。然后啟動(dòng)主事件循環(huán),讓libev處理過程的其余部分。

Libev是一個(gè)event loop:向libev注冊(cè)感興趣的events,比如Socket可讀事件,libev會(huì)對(duì)所注冊(cè)的事件的源進(jìn)行管理,并在事件發(fā)生時(shí)觸發(fā)對(duì)應(yīng)的程序。

事件驅(qū)動(dòng)框架:定義一個(gè)監(jiān)控器、書寫觸發(fā)動(dòng)作邏輯、初始化監(jiān)控器、設(shè)置監(jiān)控器觸發(fā)條件、將監(jiān)控器增加大事件驅(qū)動(dòng)器的循環(huán)中就可以。

libev的事件驅(qū)動(dòng)過程能夠想象成例如以下的偽代碼:

do_some_init()
is_run = True
while is_run:
    t = caculate_loop_time()
    deal_loop(t)
    deal_with_pending_event()
do_some_clear()

首先做一些初始化操作,然后進(jìn)入到循環(huán)中,該循環(huán)通過一個(gè)狀態(tài)位來控制是否運(yùn)行。在循環(huán)中。計(jì)算出下一次輪詢的時(shí)間,這里輪詢的實(shí)現(xiàn)就採(cǎi)用了系統(tǒng)提供的epoll、kqueue等機(jī)制。再輪詢結(jié)束后檢查有哪些監(jiān)控器的被觸發(fā)了,依次運(yùn)行觸發(fā)動(dòng)作。

Libev除了提供了主要的三大類事件(IO事件、定時(shí)器事件、信號(hào)事件)外還提供了周期事件、子進(jìn)程事件、文件狀態(tài)改變事件等多個(gè)事件。libev所實(shí)現(xiàn)的功能就是一個(gè)強(qiáng)大的reactor,可能notify事件主要包含以下這些:

  • ev_io // IO可讀可寫
  • ev_stat // 文件屬性變化
  • ev_async // 激活線程
  • ev_signal // 信號(hào)處理
  • ev_timer // 定時(shí)器
  • ev_periodic // 周期任務(wù)
  • ev_child // 子進(jìn)程狀態(tài)變化
  • ev_fork // 開辟進(jìn)程
  • ev_cleanup // event loop退出觸發(fā)事件
  • ev_idle // 每次event loop空暇觸發(fā)事件
  • ev_embed // TODO(zhangyan04):I have no idea.
  • ev_prepare // 每次event loop之前事件
  • ev_check // 每次event loop之后事件

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支持八種事件類型,當(dāng)中包含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 表“可寫事件”。如今,用戶須要做的不過在合適的時(shí)候,將某些ev_io從ev_loop增加或剔除。一旦增加,下個(gè)循環(huán)即會(huì)檢查ev_io所指定的事件有否發(fā)生;假設(shè)該事件被探測(cè)到,則ev_loop會(huì)自己主動(dòng)運(yùn)行ev_io的回調(diào)函數(shù)callback();假設(shè)ev_io被注銷,則不再檢測(cè)相應(yīng)事件。

不管某ev_loop啟動(dòng)與否,都能夠?qū)ζ浼尤牖騽h除一個(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_* );

由此,我們能夠easy得出例如以下的“一問一答”的server模型。因?yàn)闆]有考慮server端主動(dòng)終止連接機(jī)制,所以各個(gè)連接能夠維持隨意時(shí)間,client能夠自由選擇退出時(shí)機(jī)。IO事件、定時(shí)器事件、信號(hào)事件:

#include<ev.h>
#include <stdio.h>
#include <signal.h>
#include <sys/unistd.h>

ev_io io_w;
ev_timer timer_w;
ev_signal signal_w;

void io_action(struct ev_loop *main_loop,ev_io *io_w,int e)
{
        int rst;
        char buf[1024] = {''};
        puts("in io cb\n");
        read(STDIN_FILENO,buf,sizeof(buf));
        buf[1023] = '';
        printf("Read in a string %s \n",buf);
        ev_io_stop(main_loop,io_w);
}

void timer_action(struct ev_loop *main_loop,ev_timer *timer_w,int e)
{
        puts("in tiemr cb \n");
        ev_timer_stop(main_loop,io_w);
}

void signal_action(struct ev_loop *main_loop,ev_signal signal_w,int e)
{
        puts("in signal cb \n");
        ev_signal_stop(main_loop,io_w);
        ev_break(main_loop,EVBREAK_ALL);
}

int main(int argc ,char *argv[])
{
        struct ev_loop *main_loop = ev_default_loop(0);
        ev_init(&io_w,io_action);
        ev_io_set(&io_w,STDIN_FILENO,EV_READ);
        ev_init(&timer_w,timer_action);
        ev_timer_set(&timer_w,2,0);
        ev_init(&signal_w,signal_action);
        ev_signal_set(&signal_w,SIGINT);
        ev_io_start(main_loop,&io_w);
        ev_timer_start(main_loop,&timer_w);
        ev_signal_start(main_loop,&signal_w);
        ev_run(main_loop,0);
        return 0;
}

這里使用了3種事件監(jiān)控器,分別監(jiān)控IO事件、定時(shí)器事件以及信號(hào)事件。因此定義了3個(gè)監(jiān)控器(watcher),以及觸發(fā)監(jiān)控器時(shí)要運(yùn)行動(dòng)作的回調(diào)函數(shù)。Libev定義了多種監(jiān)控器,命名方式為ev_xxx這里xxx代表監(jiān)控器類型,事實(shí)上現(xiàn)是一個(gè)結(jié)構(gòu)體。

typedef struct ev_io
{
  ....
} ev_io;

通過宏定義能夠簡(jiǎn)寫為ev_xxx。回調(diào)函數(shù)的類型為:

void cb_name(struct ev_loop *main_loop,ev_xxx *io_w,int event);

在main中,首先定義了一個(gè)事件驅(qū)動(dòng)器的結(jié)構(gòu)struct ev_loop *main_loop這里調(diào)用ev_default_loop(0)生成一個(gè)預(yù)制的全局驅(qū)動(dòng)器,這里能夠參考Manual中的選擇。

然后依次初始化各個(gè)監(jiān)控器以及設(shè)置監(jiān)控器的觸發(fā)條件,初始化監(jiān)控器的過程是將對(duì)應(yīng)的回調(diào)函數(shù)即觸發(fā)時(shí)的動(dòng)作注冊(cè)到監(jiān)控器上。

設(shè)置觸發(fā)條件則是該條件產(chǎn)生時(shí)才去運(yùn)行注冊(cè)到監(jiān)控器上的動(dòng)作。對(duì)于IO事件,通常是設(shè)置特定fd上的可讀或可寫事件,定時(shí)器則是多久后觸發(fā)。這里定時(shí)器的觸發(fā)條件中還有第三參數(shù),表示第一次觸發(fā)后,是否循環(huán):若為0則不循環(huán),否則按該值循環(huán)。信號(hào)觸發(fā)器則是設(shè)置觸發(fā)的信號(hào)。

在初始化并設(shè)置好觸發(fā)條件后,先調(diào)用ev_xxx_start將監(jiān)控器注冊(cè)到事件驅(qū)動(dòng)器上,接著調(diào)用ev_run開始事件驅(qū)動(dòng)器。

image

上述模型能夠接受隨意多個(gè)連接,且為各個(gè)連接提供全然獨(dú)立的問答服務(wù)。借助libev提供的事件循環(huán)/事件驅(qū)動(dòng)接口,上述模型有機(jī)會(huì)具備其它模型不能提供的高效率、低資源占用、穩(wěn)定性好和編寫簡(jiǎn)單等特點(diǎn)。

因?yàn)閭鹘y(tǒng)的web server、ftp server及其它網(wǎng)絡(luò)應(yīng)用程序都具有“一問一答”的通訊邏輯,所以上述使用libev庫(kù)的“一問一答”模型對(duì)構(gòu)建類似的server程序具有參考價(jià)值;另外,對(duì)于須要實(shí)現(xiàn)遠(yuǎn)程監(jiān)視或遠(yuǎn)程遙控的應(yīng)用程序,上述模型相同提供了一個(gè)可行的實(shí)現(xiàn)方案。

PHP使用了libev擴(kuò)展的socket:

<?php
/* 使用異步io訪問socket Use some async I/O to access a socket */

// `sockets' extension still logs warnings
// for EINPROGRESS, EAGAIN/EWOULDBLOCK etc.
error_reporting(E_ERROR);

$e_nonblocking = array (/*EAGAIN or EWOULDBLOCK*/11, /*EINPROGRESS*/115);

// Get the port for the WWW service
$service_port = getservbyname('www', 'tcp');

// Get the IP address for the target host
$address = gethostbyname('google.co.uk');

// Create a TCP/IP socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socket === FALSE) {
    echo \"socket_create() failed: reason: \"
        .socket_strerror(socket_last_error()) . \"n\";
}

// Set O_NONBLOCK flag
socket_set_nonblock($socket);

// Abort on timeout
$timeout_watcher = new EvTimer(10.0, 0., function () use ($socket) {
    socket_close($socket);
    Ev::stop(Ev::BREAK_ALL);
});

// Make HEAD request when the socket is writable
$write_watcher = new EvIo($socket, Ev::WRITE, function ($w)
    use ($socket, $timeout_watcher, $e_nonblocking) {
    // Stop timeout watcher
    $timeout_watcher->stop();
    // Stop write watcher
    $w->stop();

    $in = \"HEAD / HTTP/1.1rn\";
    $in .= \"Host: google.co.ukrn\";
    $in .= \"Connection: Closernrn\";

    if (!socket_write($socket, $in, strlen($in))) {
        trigger_error(\"Failed writing $in to socket\", E_USER_ERROR);
    }

    $read_watcher = new EvIo($socket, Ev::READ, function ($w, $re)
        use ($socket, $e_nonblocking) {
        // Socket is readable. recv() 20 bytes using non-blocking mode
        $ret = socket_recv($socket, $out, 20, MSG_DONTWAIT);

        if ($ret) {
            echo $out;
        } elseif ($ret === 0) {
            // All read
            $w->stop();
            socket_close($socket);
            return;
        }

        // Caught EINPROGRESS, EAGAIN, or EWOULDBLOCK
        if (in_array(socket_last_error(), $e_nonblocking)) {
            return;
        }

        $w->stop();
        socket_close($socket);
    });

    Ev::run();
});

$result = socket_connect($socket, $address, $service_port);

Ev::run();
?>

七、總結(jié)

libevent和libev都提供靈活且強(qiáng)大的環(huán)境,支持為處理server端或client請(qǐng)求實(shí)現(xiàn)高性能網(wǎng)絡(luò)(和其它 I/O)接口。目標(biāo)是以高效(CPU/RAM使用量低)的方式支持?jǐn)?shù)千甚至數(shù)萬(wàn)個(gè)連接。

在本文中,您看到了一些演示樣例,包含libevent中內(nèi)置的HTTP服務(wù),能夠使用這些技術(shù)支持基于IBM Cloud、EC2或AJAX的web應(yīng)用程序。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,570評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評(píng)論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,786評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,219評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,438評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,971評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,796評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,995評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,230評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評(píng)論 1 286
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,697評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容