一篇文章搞懂 Libevent 網絡庫的原理與應用

1. Libevent 介紹

Libevent 是一個用 C 語言編寫的、輕量級的開源高性能事件通知庫,主要有以下幾個亮點:

1.1 安裝 Libevent

下載地址: http://http://libevent.org/

安裝步驟 -> 源碼安裝的方式(終端打開下載目錄)

      1. 可執行程序: configure
          ./configure         // 檢測安裝環境, 並且生成一個makefile文件
 
      2. 根據makefile中的構建規則編譯源代碼
          make     
 
      3. 安裝
          sudo make install   //將得到可執行程序/動態庫/靜態庫/頭文件拷貝到系統目錄

動態庫找不到問題解決:

  1. 通過 find 命令查找對應的庫的位置

find 搜索目錄 -name "http://libevent.so"

得到結果: /usr/local/lib/libevent.so

  1. 通過 vi 打開 / etc/ld.so.conf 文件

sudo /etc/ld.so.conf

將 / usr/local/lib / 放到文件的最後一行, 保存

  1. 執行命令: sudo ldconfig

編譯要加上動態庫 event hello.c

gcc hello.c -o hello -levent

  1. 事件處理框架 - event_base

使用 libevent 函數之前需要分配一個或者多個 event_base 結構體。每個 event_base 結構體持有一個事件集合,可以檢測以確定哪個事件是激活的。每個 event_base 都有一種用於檢測哪種事件已經就緒的 “方法”。

2.1 event_base API 函數

// 頭文件
  #include <event2/event.h>
  // 操作函數
  struct event_base * event_base_new(void);          //創建事件處理框架
  void event_base_free(struct event_base * base);    //釋放事件處理框架
  
  // 檢查event_base的後端方法
  const char** event_get_supported_methods(void);
  const char *event_base_get_method(const struct event_base *base);

event_base 和 fork(進程)關係:

  1. 子進程創建成功之後, 父進程可以繼續使用 event_base

  2. 子進程中需要繼續使用 event_base 需要用下面函數,重新初始化

int event_reinit(struct event_base* base);

例子:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <event2/event.h>
 
int main()
{
    // 1. 創建事件處理框架
    struct event_base* base = event_base_new();
    
    // 打印支持的IO轉接函數
    const char** method = event_get_supported_methods();
    for(int i=0; method[i] != NULL; ++i)
    {
        printf("%s\n", method[i]);
    }
    printf("current method: %s\n", event_base_get_method(base));
 
    // 創建子進程
    pid_t pid = fork();
    if(pid == 0)
    {
        // 子進程中event_base也會被複制,在使用這個base時候要重新初始化
        event_reinit(base); 
    }
 
    // 2. 釋放資源
    event_base_free(base);
    return 0;
}

libevent 實戰:實現用戶登錄系統

  1. 事件循環

event_base 不停的檢測委託的檢測是實際是不是發生了, 如果發生了, event_base 會調用對應的回調函數, 這個回調函數的用戶委託檢測事件的時候給的.

3.1 設置事件循環

如果委託了 event_base 檢測某些事件, 不停的進行循環檢測
結束檢測時間: 所有要檢測的事件都被觸發, 並且處理完畢。

// 頭文件
  #include <event2/event.h>
  
  // 操作函數
  #define EVLOOP_ONCE 			0x01
  #define EVLOOP_NONBLOCK 		0x02
  #define EVLOOP_NO_EXIT_ON_EMPTY 0x04
 
  int event_base_loop(struct event_base *base, int flags);
  	參數:
  		- base: 通過 event_base_new(void)得到的
  		- flags:
  			- EVLOOP_ONCE: 一直檢測某個事件, 當事件被觸發了, 停止事件循環
  			- EVLOOP_NONBLOCK: 非阻塞的方式檢測, 當事件被觸發了, 停止事件循環
  			- EVLOOP_NO_EXIT_ON_EMPTY: 一直進行事件檢測, 如果沒有要檢測的事件, 不退出
 
  int event_base_dispatch(struct event_base* base); 	// 一般使用這個函數
  	參數:
  		- base: 通過 event_base_new(void)得到的

3.2 終止事件循環

// 頭文件
  #include <event2/event.h>
  
  struct timeval {
  	long    tv_sec;                    
  	long    tv_usec;    // 微秒        
  };
 
  // 在 tv 時長之後退出循環, 如果這個參數爲空NULL, 直接退出事件循環
  // 事件循環: 檢測對應的事件是否被觸發了
  // 如果事件處理函數正在被執行, 執行完畢之後才終止
  int event_base_loopexit(struct event_base * base, const struct timeval * tv);
  
// 馬上終止
  int event_base_loopbreak(struct event_base * base);
  1. 事件

4.1 事件基本操作

  #include <event2/event.h>
  
//要檢測事件   what:
  #define EV_TIMEOUT 	0x01
  #define EV_READ 	    0x02
  #define EV_WRITE 	    0x04
  #define EV_SIGNAL 	0x08
  #define EV_PERSIST 	0x10	// 修飾某個事件是持續觸發的
  #define EV_ET 		0x20	// 邊沿模式
 
//回調函數格式:
  typedef void (*event_callback_fn)(evutil_socket_t,short,void *);
  	參數:
  		- 第一個參數: event_new的第二個參數
  		- 第二個參數: 實際觸發的事件
  		- 第三個參數: event_new的最後一個參數
 
// 創建事件
  struct event* event_new(struct event_base * base,evutil_socket_t fd,
       					 short what,event_callback_fn cb,void * arg);
  	參數:
  		- base: event_base_new得到的
  		- fd: 文件描述符, 檢測這個fd對應的事件
  		- what: 監測fd的什麼事件 
  		- cb: 回調函數, 當前檢測的事件被觸發, 這個函數被調用
  		- arg: 給回調函數傳參
#include <event2/event.h>
// 釋放事件資源
  void event_free(struct event * event);

事件被 new 出之後, 不能直接被 event_base 進行檢測 event_add 之後 event_base 就可以對事件進行檢測

#include <event2/event.h>
int  event_add(struct event * ev,const  struct timeval * tv);
  	參數: tv-> 超時時間, 
          如果這個值> 0, 比如 == 3
          檢測fd的讀事件, 在三秒之內沒有觸發該事件 -> 超時-> 超時之後, 事件對應的回調函數會被強制調用
          如果該參數爲NULL, 不會做超時檢測
  
// 刪除要檢測的事件
  int  event_del(struct event * ev);

4.2 事件的優先級設置

// 頭文件
  #include <event2/event.h>
 
  // EVENT_MAX_PRIORITIES == 256     最大的初始化事件優先級
 
  int event_base_priority_init(struct event_base * base,int n_priorities);
  	參數:
  		- n_priorities: 等級的個數, 假設 == 6
             也就是說有6個等級: 0,1,2,3,4,5, 0優先級最高
  
  // 獲取當前可用的等的個數
  int event_base_get_npriorities(struct event_base * base);
  // 給事件設置等級
  int event_priority_set(struct event *event, int priority);
  	參數:
  		- event: 創建的事件
  		- priority: 要設置的等級

例子:使用 event 實現有名管道的進程間通信 myfifo

myfifo 管道文件要提前創造好

讀端:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <event2/event.h>
#include <fcntl.h>
 
//讀的回調函數
void read_cb(evutil_socket_t fd, short what, void* arg)
{
    char buf[128];
    int count = read(fd, buf, sizeof(buf)+1);
    printf("read data: %s, %d\n", buf, count);
    printf("read event: %s\n", what & EV_READ ? "yes" : "no");
    printf("what: %d\n\n", what);
}
 
int main()
{
    int fd = open("myfifo", O_RDONLY);
    if (fd == -1)
    {
        perror("open");
        exit(0);
    }
    
    struct event_base* base = event_base_new();
    //創建事件
    struct event* ev = event_new(base, fd, EV_READ|EV_PERSIST, read_cb, NULL);
    //添加事件
    event_add(ev, NULL);
    //事件循環檢測
    event_base_dispatch(base);
 
    //釋放資源
    event_free(ev);
    event_base_free(base);
    return 0;
}

寫端:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <event2/event.h>
#include <fcntl.h>
 
//讀的回調函數
void write_cb(evutil_socket_t fd, short what, void* arg)
{
    char buf[128];
    static int num = 0;
    sprintf(buf, "hello, %d\n", num++);
    int count = write(fd, buf, sizeof(buf)+1);
    printf("wirte data: %s, %d\n", buf, count);
    printf("wirte event: %s\n", what & EV_WRITE ? "yes" : "no");
    printf("what: %d\n\n", what);
    sleep(3);
}
 
int main()
{
    int fd = open("myfifo", O_WRONLY);
    if (fd == -1)
    {
        perror("open");
        exit(0);
    }
    
    struct event_base* base = event_base_new();
    //創建事件
    struct event* ev = event_new(base, fd, EV_WRITE|EV_PERSIST, write_cb, NULL);
    //添加事件
    event_add(ev, NULL);
    //事件循環檢測
    event_base_dispatch(base);
 
    //釋放資源
    event_free(ev);
    event_base_free(base);
    return 0;
}
  1. 帶緩衝區的事件

概念理解:

  1. bufferevent 理解:
    • 每個 bufferevent 有兩個數據相關的回調

創建 / 釋放基於套接字的 bufferevent bufferevent_socket_new

主要應用於網絡套接字通信 -> socket()

struct bufferevent *bufferevent_socket_new(
  	struct event_base *base,
  	evutil_socket_t fd,
  	enum bufferevent_options options
  ); 
  	參數:
  		- base: 處理事件的
  		- fd: 通信的文件描述符
  		- options: BEV_OPT_CLOSE_ON_FREE -> 自動釋放底層資源
  	返回值: 得到帶緩衝區的事件變量
  
// 釋放資源
  void bufferevent_free(struct bufferevent *bev);

在 bufferevent 上啓動連接服務器函數 bufferevent_socket_connect

  1. 如果還沒有爲 bufferevent 設置套接字, 調用函數將爲其分配一個新的流套接字, 並且設置爲非阻塞的

例子:bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);

  1. 如果已經爲 bufferevent 設置套接字, 調用 bufferevent_socket_connect() 將告知 libevent 套接字還未連接, 直到連接成功之前不應該對其進行讀取或者寫入操作。

  2. 連接完成之前可以向輸出緩衝區添加數據。

int bufferevent_socket_connect(struct bufferevent *bev, struct sockaddr *address, int addrlen); 
  	參數:
  		- bev: 帶緩衝區的事件, 裏邊封裝 fd
  		- address: 要連接的服務器的IP和端口
  		- addrlen: address結構體的內存大小

bufferevent 讀寫緩衝區回調操作 bufferevent_setcb

//讀、寫事件觸發之後的回調函數格式
typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void *ctx);
  	參數:
  		- bev: 從bufferevent_setcb函數中的第一個參數傳入的
  		- ctx: 從bufferevent_setcb函數中的最後第一個參數傳入的
 
//特殊事件的回調函數格式 		
typedef void (*bufferevent_event_cb)(struct bufferevent *bev, short events, void *ctx);
  	參數:
  		- bev: 從bufferevent_setcb函數中的第一個參數傳入的
  		- events: 可以檢測到的事件
  			EV_EVENT_READING:讀取操作時發生某事件,具體是哪種事件請看其他標誌。
  			BEV_EVENT_WRITING:寫入操作時發生某事件,具體是哪種事件請看其他標誌。
  			BEV_EVENT_ERROR:操作時發生錯誤。關於錯誤的更多信息,請調用 EVUTIL_SOCKET_ERROR()。
  			BEV_EVENT_TIMEOUT:發生超時。
  			BEV_EVENT_EOF:遇到文件結束指示。
  			BEV_EVENT_CONNECTED:請求的連接過程已經完成 
 
void bufferevent_setcb(struct bufferevent *bufev, 
                       bufferevent_data_cb readcb, 		
                       bufferevent_data_cb writecb, 
                       bufferevent_event_cb eventcb, void *cbarg
);
  	參數:
  		- bufev: 帶緩衝區的事件
  		- readcb: 讀事件觸發之後的回調函數
  		- writecb: 寫事件觸發之後的回調函數
  		- eventcb: 特殊事件的回調函數
  		- cbarg: 給回調函數傳參

禁用、啓用緩衝區

可以啓用或者禁用 bufferevent 上的 EV_READ、EV_WRITE 或者 EV_READ | EV_WRITE 事件。
沒有啓用讀取或者寫入事件時, bufferevent 將不會試圖進行數據讀取或者寫入。

寫緩衝區默認是有效的,讀緩衝區默認無效

// 設置某個事件有效
  void bufferevent_enable(struct bufferevent *bufev, short events); 
  
// 設置某個事件無效
  void bufferevent_disable(struct bufferevent *bufev, short events);
  
// 獲取緩衝區對應的有效事件
  short bufferevent_get_enabled(struct bufferevent *bufev);

操作 bufferevent 中的數據 bufferevent_write bufferevent_read

// 向bufferevent的輸出緩衝區添加數據
  int bufferevent_write(struct bufferevent *bufev, const void *data, size_t size);
  
// 從bufferevent的輸入緩衝區移除數據
  size_t bufferevent_read(struct bufferevent *bufev, void *data, size_t size);
  1. 鏈接監聽器

#include <event2/listener.h> 
 
//回調函數格式
  typedef void (*evconnlistener_cb)(
  			struct evconnlistener *listener,   
  			evutil_socket_t sock,   
  			struct sockaddr *addr, 
  			int len, 
  			void *ptr
  ); 
  	參數:
  		- listener: evconnlistener_new_bind 返回的地址
  		- sock: 用於通信的fd
  		- addr: 客戶端的地址信息
  		- ptr: 外部傳進來的參數, evconnlistener_new_bind的第三個參數
  		
 
// 創建監聽的套接字, 綁定, 設置監聽, 等待並接受連接請求
  struct evconnlistener *evconnlistener_new_bind(
  			struct event_base *base,    
  			evconnlistener_cb cb,	        // 接受新連接之後的回調函數
  			void *ptr,                      // 回調函數參數
  			unsigned flags, 
  			int backlog,                   // listen()中的第二參數,最多的監聽數量,小於128的整數
  			const struct sockaddr *sa,     // 本地的IP和端口
  			int socklen				   // struct sockaddr結構體大小
  );
  	參數:
  		- flags:
  			LEV_OPT_CLOSE_ON_FREE: 自動關閉底層套接字
  			LEV_OPT_REUSEABLE: 設置端口複用
// 釋放
  void evconnlistener_free(struct evconnlistener *lev);

設置無效之後, 就不監聽連接請求了

  #include <event2/listener.h> 
 
  int evconnlistener_disable(struct evconnlistener *lev);
  int evconnlistener_enable(struct evconnlistener *lev);
#include <event2/listener.h>
 
void evconnlistener_set_cb(struct evconnlistener *lev, evconnlistener_cb cb, void *arg);
  1. 例子:用 event 實現服務器和客戶端 tcp 通信

服務器使用鏈接監聽器、帶緩衝區的事件

客戶端使用帶緩衝區的事件

服務器 server 代碼:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <event2/event.h>
#include <event2/bufferevent.h>
#include <event2/listener.h>
 
// read緩衝區的回調
void read_cb(struct bufferevent* bev, void* arg)
{
    // 讀緩衝區的數據
    char buf[128];
    int len = bufferevent_read(bev, buf, sizeof(buf));
    printf("read data: len = %d, str = %s\n", len, buf);
 
    // 回覆數據
    bufferevent_write(bev, buf, len);
    printf("數據發送完畢...\n");
}
 
// 寫緩衝區的回調
// 調用的時機: 寫緩衝區中的數據被髮送出去之後, 該函數被調用
void write_cb(struct bufferevent* bev, void* arg)
{
 
    printf("arg value: %s\n", (char*)arg);
    printf("數據已經發送完畢...xxxxxxxxxxxx\n");
}
 
// 事件回調
void events_cb(struct bufferevent* bev, short event, void* arg)
{
    if(event & BEV_EVENT_ERROR)
    {
        printf("some error happened ...\n");
    }
    else if(event & BEV_EVENT_EOF)
    {
        printf("server disconnect ...\n");
    }
    // 終止連接
    bufferevent_free(bev);
}
 
// 接收連接請求之後的回調
void listener_cb(struct evconnlistener *listener,   
                evutil_socket_t sock,   
                struct sockaddr *addr, 
                int len, 
                void *ptr)
{
    // 通信
    // 使用帶緩衝區的事件對sock進行包裝
    struct event_base* base = (struct event_base*)ptr;
    struct bufferevent* bev = bufferevent_socket_new(base, sock, BEV_OPT_CLOSE_ON_FREE);
    // 設置回調
    bufferevent_setcb(bev, read_cb, write_cb, events_cb, NULL);
    bufferevent_enable(bev, EV_READ);
}
 
int main()
{
    struct event_base * base = event_base_new();
    // 1. 創建監聽的套接字, 綁定, 設置監聽, 等待並接受連接請求
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9898);    // 服務器監聽的端口
    addr.sin_addr.s_addr = INADDR_ANY;
    struct evconnlistener* listener = evconnlistener_new_bind(base, listener_cb, base, 
                                                               LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE,
                                                               100, (struct sockaddr*)&addr, sizeof(addr)); 
 
    event_base_dispatch(base);
    evconnlistener_free(listener);
    event_base_free(base);
 
 
    return 0;
}

客戶端 client 代碼:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <event2/event.h>
#include <event2/bufferevent.h>
 
// read緩衝區的回調
void read_cb(struct bufferevent* bev, void* arg)
{
    printf("arg value: %s\n", (char*)arg);
    // 讀緩衝區的數據
    char buf[128];
    int len = bufferevent_read(bev, buf, sizeof(buf));
    printf("read data: len = %d, str = %s\n", len, buf);
 
    // 回覆數據
    bufferevent_write(bev, buf, len);
    printf("數據發送完畢...\n");
}
 
// 寫緩衝區的回調
// 調用的時機: 寫緩衝區中的數據被髮送出去之後, 該函數被調用
void write_cb(struct bufferevent* bev, void* arg)
{
 
    printf("arg value: %s\n", (char*)arg);
    printf("數據已經發送完畢...xxxxxxxxxxxx\n");
}
 
// 事件回調
void events_cb(struct bufferevent* bev, short event, void* arg)
{
    if(event & BEV_EVENT_ERROR)
    {
        printf("some error happened ...\n");
    }
    else if(event & BEV_EVENT_EOF)
    {
        printf("server disconnect ...\n");
    }
    // 終止連接
    bufferevent_free(bev);
}
 
void send_msg(evutil_socket_t fd, short ev, void * arg)
{
    // 將寫入到終端的數據讀出
    char buf[128];
    int len = read(fd, buf, sizeof(buf));
    // 發送給服務器
    struct bufferevent* bev = (struct bufferevent*)arg;
    bufferevent_write(bev, buf, len);
}
 
int main()
{
    struct event_base * base = event_base_new();
    // 1. 創建通信的套接字
    struct bufferevent* bufev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
    
    // 2. 連接服務器
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(9898);    // 服務器監聽的端口
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);
    // 這個函數調用成功, == 服務器已經成功連接
    bufferevent_socket_connect(bufev, (struct sockaddr*)&addr, sizeof(addr));
    
    // 3. 通信
    // 給bufferevent的緩衝區設置回調
    bufferevent_setcb(bufev, read_cb, write_cb, events_cb, (void*)"hello, world");
    bufferevent_enable(bufev, EV_READ);
 
    // 創建一個普通的輸入事件
    struct event* myev = event_new(base, STDIN_FILENO, EV_READ|EV_PERSIST, send_msg, bufev);
    event_add(myev, NULL);
    
    
    event_base_dispatch(base);
    event_free(myev);
    event_base_free(base);
 
 
    return 0;
}
  1. 總結:

處理不帶緩衝區的事件:

  1. 創建事件處理框架 event_base event_base_new()

  2. 創建新事件 event event_new()

  3. 將事件添加到事件處理框架 event_base 上 event_add()

  4. 啓動事件循環檢測 event_base_dispatch()

  5. 循環結束之後釋放資源 event_base_free() 、event_free()

處理帶緩衝區的事件:

1、創建事件處理框架 event_base event_base_new()

2、服務器端:

  1. 創建連接監聽器(在回調函數得到 fd) evconnlistener_new_bind()

  2. 將通信 fd 包裝 bufferevent_socket_new()

  3. 使用 bufferevent 通信:給 bufferevent 讀寫緩衝區設置回調函數 bufferevent_setcb()

  4. 設置讀緩衝區可用 bufferevent_enable()

  5. 對緩衝區數據操作 bufferevent_write()、bufferevent_rea()

3、客戶端:

  1. 創建通信用的 fd 並且使用 bufferevent 包裝 bufferevent_socket_new()

  2. 連接服務器 bufferevent_socket_connect()

  3. 使用 bufferevent 通信:給 bufferevent 讀寫緩衝區設置回調函數 bufferevent_setcb()

  4. 設置讀緩衝區可用 bufferevent_enable()

  5. 對緩衝區數據操作 bufferevent_write()、bufferevent_read()

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/0dyNQtJhJmgkmoQ6cjeD5Q