socket異步編程--libevent的使用

這篇文章介紹下libevent在socket異步編程中的應用。在一些對性能要求較高的網絡應用程序中,爲了防止程序阻塞在socket I/O操做上形成程序性能的降低,須要使用異步編程,即程序準備好讀寫的函數(或接口)並向系統註冊,而後在須要的時候只向系統提交讀寫的請求以後就繼續作本身的事情,實際的讀寫操做由系統在合適的時候調用咱們程序註冊的接口進行。異步編程會給一些程序猿帶來一些理解和編寫上的困難,由於咱們一般寫的一些簡單的程序都是順序執行的,而異步編程將程序的執行順序打亂了,有些代碼什麼狀況下執行每每不是太清晰,所以也使得編程的複雜度大大增長。linux

    Note:這裏系統這個詞使用的不許確,實際上能夠是本身封裝的異步調用機制,更常見的是一些可用的庫,好比libevent,ACE等算法

 

    想了解libevent的工做原理能夠自行查詢資料,網上相關的介紹一大堆,也能夠本身閱讀源碼進行分析,本文僅從使用的角度作一個簡單的介紹,看如何快速的將libevent引入咱們的程序中。任何應用都免不了須要承載其功能的底層OS,libevent也不例外,其內部是經過封裝操做系統的IO複用機制實現的,在linux系統上多是epoll、kqueu之類的,取決於具體的OS所支持的IO複用方式,在個人系統上是epoll,所以能夠理解爲libevent提供了一個比epoll更爲友好的操做接口,將程序猿從網絡IO處理的細節中解放出來,使其能夠專一於目標問題的處理上。編程

 

    首先,安裝libevent到任意目錄下安全

wget http://monkey.org/~provos/libevent-1.4.13-stable.tar.gz
tar –xzvf libevent-1.4.13-stable.tar.gz
cd libevent-1.4.13-stable
./configure --prefix=/home/mydir/libevent
make && make install

 

    如今假定咱們要設計一個服務器程序,用於接收客戶端的數據,並將接收的數據回寫給客戶端。下面來構造該程序,因爲本僅僅是展現一個Demo,所以程序中將不對錯誤進行處理,假設全部的調用都成功服務器

複製代碼
2 #define PORT 25341
3 #define BACKLOG 5
4 #define MEM_SIZE 1024

6 struct event_base* base;

8 int main(int argc, char* argv[])
9 {
10     struct sockaddr_in my_addr;
11     int sock;
12 
13     sock = socket(AF_INET, SOCK_STREAM, 0); 
14     int yes = 1;
15     setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));
16     memset(&my_addr, 0, sizeof(my_addr));
17     my_addr.sin_family = AF_INET;
18     my_addr.sin_port = htons(PORT);
19     my_addr.sin_addr.s_addr = INADDR_ANY;
20     bind(sock, (struct sockaddr*)&my_addr, sizeof(struct sockaddr));
21     listen(sock, BACKLOG);
22 
23     struct event listen_ev;
24     base = event_base_new();
25     event_set(&listen_ev, sock, EV_READ|EV_PERSIST, on_accept, NULL);
26     event_base_set(base, &listen_ev);
27     event_add(&listen_ev, NULL);
28     event_base_dispatch(base);
29 
30     return 0;
31 }
複製代碼
 

    第13行說明建立的是一個TCP socket。第15行是服務器程序的一般作法,設置了該選項後,在父子進程模型中,當子進程爲客戶服務的時候若是父進程退出,能夠從新啓動程序完成服務的無縫升級,不然在全部父子進程徹底退出前再啓動程序會在該端口上綁定失敗,也即不能完成無縫升級的操做(更多信息能夠參考該函數說明或Steven先生的<網絡編程>)。第24行用於建立一個事件處理的全局變量,能夠理解爲這是一個負責集中處理各類出入IO事件的總管家,它負責接收和派發全部輸入輸出IO事件的信息,這裏調用的是函數event_base_new(), 不少程序裏這裏用的是event_init(),區別就是前者是線程安全的、然後者是非線程安全的,後者在其官方說明中已經被標誌爲過期的函數、且建議用前者代替,libevent中還有不少相似的函數,好比建議用event_base_dispatch代替event_dispatch,用event_assign代替event_set和event_base_set等,關於libevent接口的詳細說明見其官方說明libevent_doc. 第25行說明在listen_en這個事件監聽sock這個描述字的讀操做,當讀消息到達是調用on_accept函數,EV_PERSIST參數告訴系統持續的監聽sock上的讀事件,若是不加該參數,每次要監聽該事件時就要重複的調用26行的event_add函數,從前面的代碼可知,sock這個描述字是bind到本地的socket端口上,所以其對應的可讀事件天然就是來自客戶端的鏈接到達,咱們就能夠調用accept無阻塞的返回客戶的鏈接了。第26行將listen_ev註冊到base這個事件中,至關於告訴處理IO的管家請留意個人listen_ev上的事件。第27行至關於告訴處理IO的管家,當有個人事件到達時你發給我(調用on_accept函數),至此對listen_ev的初始化完畢。第28行正式啓動libevent的事件處理機制,使系統運行起來,運行程序的話會發現event_base_dispatch是一個無限循環。網絡

 

下面是on_accept函數的內容app

   1: void on_accept(int sock, short event, void* arg)
   2: {
   3:     struct sockaddr_in cli_addr;
   4:     int newfd, sin_size;
   5:     // read_ev must allocate from heap memory, otherwise the program would crash from segmant fault
   6:     struct event* read_ev = (struct event*)malloc(sizeof(struct event));;
   7:     sin_size = sizeof(struct sockaddr_in);
   8:     newfd = accept(sock, (struct sockaddr*)&cli_addr, &sin_size);
   9:     event_set(read_ev, newfd, EV_READ|EV_PERSIST, on_read, read_ev);
  10:     event_base_set(base, read_ev);
  11:     event_add(read_ev, NULL);
  12: } 

    第9-12與前面main函數的24-26相同,即在表明客戶的描述字newfd上監聽可讀事件,當有數據到達是調用on_read函數。這裏有亮點須要注意,一是read_ev須要從堆裏malloc出來,若是是在棧上分配,那麼當函數返回時變量佔用的內存會被釋放,所以事件主循環event_base_dispatch會訪問無效的內存而致使進程崩潰(即crash);第二個要注意的是第9行read_ev做爲參數傳遞給了on_read函數。異步

 

下面是on_read函數的內容socket

   1: void on_read(int sock, short event, void* arg)
   2: {
   3:     struct event* write_ev;
   4:     int size;
   5:     char* buffer = (char*)malloc(MEM_SIZE);
   6:     bzero(buffer, MEM_SIZE);
   7:     size = recv(sock, buffer, MEM_SIZE, 0);
   8:     printf("receive data:%s, size:%d\n", buffer, size);
   9:     if (size == 0) {
  10:         event_del((struct event*)arg);
  11:         free((struct event*)arg);
  12:         close(sock);
  13:         return;
  14:     }
  15:     write_ev = (struct event*) malloc(sizeof(struct event));;
  16:     event_set(write_ev, sock, EV_WRITE, on_write, buffer);
  17:     event_base_set(base, write_ev);
  18:     event_add(write_ev, NULL);
  19: }

    第9行,當從socket讀返回0標誌對方已經關閉了鏈接,所以這個時候就不必繼續監聽該套接口上的事件,因爲EV_READ在on_accept函數裏是用EV_PERSIST參數註冊的,所以要顯示的調用event_del函數取消對該事件的監聽。第18-21行與on_accept函數的6-11行相似,當可寫時調用on_write函數,注意第19行將buffer做爲參數傳遞給了on_write。這段程序還有比較嚴重的問題,後面進行說明。異步編程

 

on_write函數的實現

複製代碼
1 void on_write(int sock, short event, void* arg)
2 {
3     char* buffer = (char*)arg;
4     send(sock, buffer, strlen(buffer), 0); 

6     free(buffer);
複製代碼

7 } 

     on_write函數中向客戶端回寫數據,而後釋放on_read函數中malloc出來的buffer。在不少書合編程指導中都很強調資源的全部權,常常要求誰分配資源、就由誰釋放資源,這樣對資源的管理指責就更明確,不容易出問題,可是經過該例子咱們發如今異步編程中資源的分配與釋放每每是由不一樣的全部者操做的,所以也是比較容易出問題的地方。

 

    其實在on_read函數中從socket讀取數據後程序就能夠直接調用write/send接口向客戶回寫數據了,由於寫事件已經知足,不存在異步不異步的問題,這裏進行on_write的異步操做僅僅是爲了說明異步編程中資源的管理與釋放的問題,另一方面,直接調用write/send函數向客戶端寫數據可能致使程序較長時間阻塞在IO操做上,好比socket的輸出緩衝區已滿,則write/send操做阻塞到有可用的緩衝區以後才能進行實際的寫操做,而經過向寫事件註冊on_accept函數,那麼libevent會在合適的時間調用咱們的callback函數,(好比對於會引發IO阻塞的狀況好比socket輸出緩衝區滿,則由libevent設計算法來處理,如此當回調on_accept函數時咱們在調用IO操做就不會發生真正的IO以外的阻塞)。注:前面括號中是我我的認爲一個庫應該實現的功能,至於libevent是否是實現這樣的功能並不清楚也無心深究。

 

    再來看看前面提到的on_read函數中存在的問題,首先write_ev是動態分配的內存,可是沒有釋放,所以存在內存泄漏,另外,on_read中進行malloc操做,那麼當屢次調用該函數的時候就會形成內存的屢次泄漏。這裏的解決方法是對socket的描述字能夠封裝一個結構體來保護讀、寫的事件以及數據緩衝區,整理後的完整代碼以下

複製代碼
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>

#include <event.h>


#define PORT        25341
#define BACKLOG     5
#define MEM_SIZE    1024

struct event_base* base;
struct sock_ev {
    struct event* read_ev;
    struct event* write_ev;
    char* buffer;
};

void release_sock_event(struct sock_ev* ev)
{
    event_del(ev->read_ev);
    free(ev->read_ev);
    free(ev->write_ev);
    free(ev->buffer);
    free(ev);
}

void on_write(int sock, short event, void* arg)
{
    char* buffer = (char*)arg;
    send(sock, buffer, strlen(buffer), 0);

    free(buffer);
}

void on_read(int sock, short event, void* arg)
{
    struct event* write_ev;
    int size;
    struct sock_ev* ev = (struct sock_ev*)arg;
    ev->buffer = (char*)malloc(MEM_SIZE);
    bzero(ev->buffer, MEM_SIZE);
    size = recv(sock, ev->buffer, MEM_SIZE, 0);
    printf("receive data:%s, size:%d\n", ev->buffer, size);
    if (size == 0) {
        release_sock_event(ev);
        close(sock);
        return;
    }
    event_set(ev->write_ev, sock, EV_WRITE, on_write, ev->buffer);
    event_base_set(base, ev->write_ev);
    event_add(ev->write_ev, NULL);
}

void on_accept(int sock, short event, void* arg)
{
    struct sockaddr_in cli_addr;
    int newfd, sin_size;
    struct sock_ev* ev = (struct sock_ev*)malloc(sizeof(struct sock_ev));
    ev->read_ev = (struct event*)malloc(sizeof(struct event));
    ev->write_ev = (struct event*)malloc(sizeof(struct event));
    sin_size = sizeof(struct sockaddr_in);
    newfd = accept(sock, (struct sockaddr*)&cli_addr, &sin_size);
    event_set(ev->read_ev, newfd, EV_READ|EV_PERSIST, on_read, ev);
    event_base_set(base, ev->read_ev);
    event_add(ev->read_ev, NULL);
}

int main(int argc, char* argv[])
{
    struct sockaddr_in my_addr;
    int sock;

    sock = socket(AF_INET, SOCK_STREAM, 0);
    int yes = 1;
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));
    memset(&my_addr, 0, sizeof(my_addr));
    my_addr.sin_family = AF_INET;
    my_addr.sin_port = htons(PORT);
    my_addr.sin_addr.s_addr = INADDR_ANY;
    bind(sock, (struct sockaddr*)&my_addr, sizeof(struct sockaddr));
    listen(sock, BACKLOG);

    struct event listen_ev;
    base = event_base_new();
    event_set(&listen_ev, sock, EV_READ|EV_PERSIST, on_accept, NULL);
    event_base_set(base, &listen_ev);
    event_add(&listen_ev, NULL);
    event_base_dispatch(base);

    return 0;
複製代碼

}

 

    程序編譯的時候要加 -levent 鏈接選項,以鏈接libevent的共享庫,可是執行的時候依然爆出以下錯誤:error while loading shared libraries: libevent-1.4.so.2: cannot open shared object file: No such file or directory, 這個是程序找不到共享庫的位置,經過執行echo $LD_LIBRARY_PATH能夠看到系統庫的環境變量裏沒有咱們安裝的路徑,即由--prefix制定的路徑,執行export LD_LIBRARY_PATH=/home/mydir/libevent/lib/:$LD_LIBRARY_PATH將該路徑加入系統環境變量裏,再執行程序就能夠了。

 

測試結果

相關文章
相關標籤/搜索