libevent をシンプルかつ詳細に理解する - 20,000 語の要約

概要

libevent、libev、libuv はすべて C で実装された非同期イベント ライブラリです。これらは、非同期イベントを登録し、非同期イベントを検出し、対応するコールバック関数を呼び出して、次のとおりにイベントを処理します。イベントがトリガーされる順序。処理されるイベントには次のものがあります: 网络 io 事件、定时事件以及信号事件。これら 3 つのイベントは、 サーバーの動作を推進します。

  1. 网络io事件:
    linux:epoll、poll、select
    mac:kqueue
    window:iocp
  2. 時間指定イベント:
    赤黒ツリー
    最小ヒープ: バイナリ ツリー、クアッドツリー
    スキップ リスト a> a>
    タイムホイール
  3. 信号事件

libevent と libev は主に、非同期イベント ライブラリとオペレーティング システムの間の対話のための単純なイベント管理インターフェイスをカプセル化しているため、ユーザーはイベントの検出と処理に関するプラットフォームのメカニズムの違いに注意を払う必要はなく、料金を支払うだけで済みます。イベントの特定の処理に注意してください。

libev は、設計コンセプトから始めて、libevent のいくつかのアーキテクチャ上の決定を改善することを目的としています。たとえば、グローバル変数の使用により、マルチスレッド環境で libevent を安全に使用することが困難になります。イベントのデータ構造設計が大きすぎ、次のものが含まれます。 io、時間、および信号処理は構造内に完全にカプセル化されています。http、dns、openssl などの追加コンポーネントの実装品質は低く (セキュリティ上の問題が発生しやすい)、タイマーは不正確で、時間イベントを適切に処理できません。

libev はグローバル変数の使用を完全に排除しますが、コールバック パラメーターを介してコンテキストを渡します (libevent も後でこれを行います)。また、イベントの結合を減らすために、さまざまなイベント タイプに応じてさまざまなデータ構造を構築します。タイマーは最小限に使用されます。 libev は小さく効率的であり、イベント処理のみに重点を置いています。

Libevent と libev は Windows のサポートが不十分であるため、libuv ライブラリが作成されました。libuv は libev に基づいており、ウィンドウ プラットフォームでの iocp のカプセル化が向上しています。node.js は libuv に基づいています。

リベベントのメリット

もちろん、最初に賞賛しなければなりません。Libevent には、いくつかの注目すべき点があります。
  => イベント駆動型、高いパフォーマンス。
  => ; 軽量でネットワークに重点が置かれており、ACE ほど肥大化していません;
  => ソース コードは非常に簡潔で読みやすいです;
  = > クロス-プラットフォーム、Windows、Linux、*BSD、Mac OS をサポート。
  => さまざまな I/O 多重化テクノロジ、epoll、poll、dev/poll、select、kqueue などをサポート。
  => I/O、タイマー、信号などのイベントをサポート;
  => イベントの優先順位を登録;

Libevent は、memcached、Vomit、Nylon、Netchat などの基盤となるネットワーク ライブラリとして広く使用されています。

Libevent の最新の安定バージョンは 1.4.13 で、これはこの記事で参照されているバージョンでもあります。

作業フローチャート:
libevent のパッケージ化階層

 自分たちで IO イベントを操作したくない場合は、IO 読み取りおよび書き込み操作を管理用の libevent に引き渡し、境界問題の対処に役立ててもらいます。 libevent をより高いカプセル化レベルから使用するには、libevent が読み取りおよび書き込み I/O 処理を完了した後、バッファーからデータを読み取り、イベントの論理処理を完了するだけで済みます。境界の問題については、心配する必要はありません。それについて。詳しい紹介は以下になります
 

IOイベント検出のカプセル化とAPIの導入


Libevent は、イベント検出とイベント操作の 2 つのレベルをカプセル化します。イベント検出は低レベルのカプセル化であり、Libevent がイベント検出を完了し、呼び出し元が自分で IO 操作を完了します。これは、基礎となる epoll、select、および Paul の詳細を非表示にするのと似ています。この層は、イベント マネージャーの操作とイベント自体の操作インターフェイスをカプセル化します。

イベントマネージャーevent_base
イベントマネージャーのビルドevent_base_new

libevent 関数を使用する前に、1 つ以上のevent_base 構造体を割り当てる必要があります。各event_base構造体にはイベントのコレクションが保持されており、これを検出してどのイベントがアクティブであるかを判断できます。event_base構造体は、epoll redのルートノードに相当します。 blacktree. 各event_baseには、特定のイベントの準備ができたことを検出するための「メソッド」があります。
 

struct event_base *event_base_new(void); 
函数说明: 获得event_base结构,当于epoll红黑树的树根节点
参数说明: 无
返回值: 
	成功返回event_base结构体指针;
	失败返回NULL;
無料イベントマネージャーevent_base_free
void event_base_free(struct event_base *);
函数说明: 释放event_base指针
イベント再初期化
int event_reinit(struct event_base *base);
函数说明: 如果有子进程, 且子进程也要使用base, 则子进程需要对event_base重新初始化, 
此时需要调用event_reinit函数.
函数参数: 由event_base_new返回的执行event_base结构的指针
返回值: 成功返回0, 失败返回-1
对于不同系统而言, event_base就是调用不同的多路IO接口去判断事件是否已经被激活, 
对于linux系统而言, 核心调用的就是epoll, 同时支持poll和select.
event_get_supported_methods
const char **event_get_supported_methods(void);
函数说明: 获得当前系统(或者称为平台)支持的方法有哪些
参数: 无
返回值: 返回二维数组, 类似与main函数的第二个参数**argv.
イベントベース_get_メソッド
const char * event_base_get_method(const struct event_base *base);
函数说明: 获得当前base节点使用的多路io方法
函数参数: event_base结构的base指针.
返回值: 获得当前base节点使用的多路io方法的指针
 イベントセット()
void event_set(struct event *ev, int fd, short events, void (*callback)(int, short, void *), void *arg)
event_set 初始化事件event,设置回调函数和关注的事件。
参数说明:
ev:执行要初始化的event对象;
fd:该event绑定的“句柄”,对于信号事件,它就是关注的信号;
events:在该fd上关注的事件类型,它可以是EV_READ, EV_WRITE, EV_SIGNAL;
callback:这是一个函数指针,当fd上的事件event发生时,调用该函数执行处理,它有三个参数,调用时由event_base负责传入,按顺序,实际上就是event_set时的fd, event和arg;
arg:传递给callback函数指针的参数;

定时事件说明:evtimer_set(&ev, timer_cb, NULL) = event_set(&ev, -1, 0, timer_cb, NULL)
由于定时事件不需要fd,并且定时事件是根据添加时(event_add)的超时值设定的,因此这里event也不需要设置。
这一步相当于初始化一个event handler,在libevent中事件类型保存在event结构体中。
注意:libevent并不会管理event事件集合,这需要应用程序自行管理;

#include <event.h>
#include <stdio.h>
#include <string.h>
int main()
{
   const char** p = event_get_supported_methods();
   //获取当前系统支持的方法有哪些
  
   int i = 0;
   while(p[i] != NULL)
   {
     printf("[%s] ",p[i]);
   }
   printf("\n");

   struct event_base* base = event_base_new();
   if(base == NULL) printf("event_base_new error\n");

  printf("[%s]\n",event_base_get_method(base));

  event_base_free(base);
  return 0;
}
 struct イベント構造解析
struct event {
	TAILQ_ENTRY (event) ev_next;
	TAILQ_ENTRY (event) ev_active_next;
	TAILQ_ENTRY (event) ev_signal_next;
	unsigned int min_heap_idx;	/* for managing timeouts 用于管理超时默认是-1 */

	struct event_base *ev_base; //属于哪个一event_base

	int ev_fd; //设置事件监听对象,也就是监听句柄
	short ev_events; //设置监听对象触发的动作:EV_READ, EV_WRITE, EV_SIGNAL,EV_TIMEOUT,EV_PERSIST  
	short ev_ncalls; //事件被调用了几次
	short *ev_pncalls;	/* Allows deletes in callback */

	struct timeval ev_timeout;

	int ev_pri;		/* smaller numbers are higher priority */
    
    //设置事件的回调函数
	void (*ev_callback)(int, short, void *arg);

    //设置事件的回调函数的参数
	void *ev_arg;

	int ev_res;		/* result passed to event callback */
	int ev_flags; //事件的状态,EVLIST_INIT,EVLIST_INTERNAL,EVLIST_ACTIVE,EVLIST_SIGNAL,EVLIST_INSERTED,EVLIST_TIMEOUT
};
イベントループevent_base_dispatchおよびevent_base_loop


event_base_new が完了した後、libevent はイベントが生成されるまで待機する必要があります。つまり、イベントがアクティブ化されるまで待機する必要があるため、プログラムは終了できません。epoll の場合は、ループを自分で制御する必要があり、libevent は次の機能も提供します。 where( 1) 関数に似た API インターフェイス。
 

//这个函数一般不用, 而大多数都调用libevent给我们提供的另外一个API:
int event_base_loop(struct event_base *base, int flags); 
函数说明: 进入循环等待事件
参数说明:
	base: 由event_base_new函数返回的指向event_base结构的指针
	flags的取值:
	#define EVLOOP_ONCE	0x01
	只触发一次, 如果事件没有被触发, 阻塞等待
	#define EVLOOP_NONBLOCK	0x02
	非阻塞方式检测事件是否被触发, 不管事件触发与否, 都会立即返回.
int event_base_dispatch(struct event_base *base);
函数说明: 进入循环等待事件
参数说明:由event_base_new函数返回的指向event_base结构的指针
调用该函数, 相当于没有设置标志位的event_base_loop。程序将会一直运行, 
直到没有需要检测的事件了, 或者被结束循环的API终止。
 イベント ループには、event_base_loopbreak とevent_base_loopexit が導入されています。
int event_base_loopexit(struct event_base *base, const struct timeval *tv);
int event_base_loopbreak(struct event_base *base);
struct timeval {
	long    tv_sec;                    
	long    tv_usec;            
};

2 つの関数の違いは如果正在执行激活事件的回调函数、event_base_loopexit はイベント コールバックの実行後にループを終了します (tv 時間が NULL でない場合、ループは直後に終了します)。テレビで設定された時間を待ちます )、event_base_loopbreak会立即终止循环。

イベントプロセス_アクティブ

主にアクティベーションキュー内のデータを処理します

static void
event_process_active(struct event_base *base)
{
	struct event *ev;
	struct event_list *activeq = NULL;
	int i;
	short ncalls;

    获得就绪链表中有就绪事件并且高优先级的表头
	for (i = 0; i < base->nactivequeues; ++i) {
		if (TAILQ_FIRST(base->activequeues[i]) != NULL) {
			activeq = base->activequeues[i];
			break;
		}
	}

	assert(activeq != NULL);

	for (ev = TAILQ_FIRST(activeq); ev; ev = TAILQ_FIRST(activeq)) {
		if (ev->ev_events & EV_PERSIST)
			event_queue_remove(base, ev, EVLIST_ACTIVE);
		else
			event_del(ev);//如果不是永久事件则需要进行一系统的删除工作,包括移除注册在事件链表的事件等

		/* Allows deletes to work */
		ncalls = ev->ev_ncalls;
		ev->ev_pncalls = &ncalls;
		while (ncalls) {
			ncalls--;
			ev->ev_ncalls = ncalls;
            //根据回调次数调用回调函数
			(*ev->ev_callback)((int)ev->ev_fd, ev->ev_res, ev->ev_arg);
			if (event_gotsig || base->event_break)
				return;
		}
	}
}
イベントオブジェクト
typedef void (*event_callback_fn)(evutil_socket_t fd, short events, void *arg);
struct event *event_new(struct event_base *base, evutil_socket_t fd, 
short events, event_callback_fn cb, void *arg);

#define evsignal_new(b, x, cb, arg)   event_new((b), (x), EV_SIGNAL|EV_PERSIST, (cb), (arg))
函数说明: event_new负责创建event结构指针, 同时指定对应的base(epfd), 还有对应的文件描述符
    , 事件, 以及回调函数和回调函数的参数。
参数说明:
	base: 对应的根节点--epfd
	fd: 要监听的文件描述符
	events:要监听的事件
	  	#define  EV_TIMEOUT    0x01   //超时事件
		#define  EV_READ       0x02    //读事件
		#define  EV_WRITE      0x04    //写事件
		#define  EV_SIGNAL     0x08    //信号事件
		#define  EV_PERSIST     0x10    //周期性触发
		#define  EV_ET         0x20    //边缘触发, 如果底层模型支持设置										 则有效, 若不支持则无效.
		若要想设置持续的读事件则: EV_READ | EV_PERSIST
		
	cb: 回调函数, 原型如下:
	typedef void (*event_callback_fn)(evutil_socket_t fd, short events, void *arg);
	注意: 回调函数的参数就对应于event_new函数的fd, event和arg

イベントオブジェクトevent_freeを破棄します。
void event_free(struct event *ev);
函数说明: 释放由event_new申请的event节点。
イベントevent_addを登録します(epoll_ctlと同様)
int event_add(struct event *ev, const struct timeval *timeout);
函数说明: 将非未决态事件转为未决态, 相当于调用epoll_ctl函数(EPOLL_CTL_ADD), 
开始监听事件是否产生, 相当于epoll的上树操作.
参数说明:
	ev: 调用event_new创建的事件
	timeout: 限时等待事件的产生(定时事件使用), 也可以设置为NULL, 没有限时。
ログアウト イベントevent_del (epoll の del に似ています)
int event_del(struct event *ev);
函数说明: 将事件从未决态变为非未决态, 相当于epoll的下树(epoll_ctl调用EPOLL_CTL_DEL操作)操作。
参数说明: ev指的是由event_new创建的事件.
イベント駆動型イベントの概要
イベント駆動はlibeventの中核となる考え方です

それはもっと重要ですが、epoll とactor を学んだことがあれば、比較的簡単に学ぶことができます。

主な州:

無効なポインタ: 現時点では struct イベント *ptr のみが定義されています。
保留されていません : 同等イベントを作成しますが、イベントはまだ監視状態ではありません。epoll を使用する場合と同様に、struct epoll_event ev を定義し、ev の 2 つのフィールドに値を割り当てますが、epoll_ctl はまだ呼び出されず、
保留中: イベントの監視を開始しますが、まだイベントは生成されていません。これは、epoll_ctl を呼び出して監視対象のイベントのツリーを登るのと同じですが、イベントは生成されません。
アクティブ化: は、監視対象のイベントがすでにアクティブ化されていることを意味します。 epoll_wait 関数の呼び出しからの戻りがあります。イベントがアクティブ化されると、libevent はイベントに対応するコールバック関数を呼び出します。

libevent イベント検出のみを使用し、io 操作が単独でデモを処理します。

memcached と同様に、このレベルを使用します (検出に libevent のみを使用し、io 操作をそれ自体で書き込みます)。以下のデモからわかるように、libevent の使用はリアクターを操作するのと同じで、コールバック関数を渡して、コールバック関数内に io 操作のロジックを記述するだけです。

#include <event.h>
#include <event2/listener.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
//为什么events是short是因为库里定义过了
// #define  EV_TIMEOUT    0x01   //超时事件
// #define  EV_READ       0x02    //读事件
// #define  EV_WRITE      0x04    //写事件
// #define  EV_SIGNAL     0x08    //信号事件
// #define  EV_PERSIST     0x10    //周期性触发
// #define  EV_ET         0x20    //边缘触发, 如果底层模型支持设置	


// int event_assign(struct event *ev, struct event_base *base, evutil_socket_t fd, short events, event_callback_fn callback, void *arg);
// ev:要进行初始化的事件结构体指针。
// base:事件所关联的事件基础。
// fd:文件描述符或套接字,表示这个事件与哪个描述符相关联。
// events:指定事件类型,比如读取、写入等。可以使用 EV_READ、EV_WRITE 等常量。
// callback:是事件触发时调用的函数。
// arg:是传递给回调函数 callback 的可选参数。
void socket_read_cb(int fd,short events,void* arg);
void socket_accept_cb(int fd,short events,void* arg)
{
  sleep(1);
  struct sockaddr_in addr;
  socklen_t len = sizeof(addr);
  int clientfd = accept(fd, (struct sockaddr *) &addr, &len);
  evutil_make_socket_nonblocking(clientfd);
  //将该fd设置成非阻塞的
  
  printf("client_fd:%d",clientfd);
  
  struct event_base* base = (struct event_base*)arg;//这个是reactor对象
  struct event *ev = event_new(NULL, -1, 0, NULL, NULL);
  //建立一个event

  //注册这个事件,写到base里
  event_assign(ev,base,clientfd,EV_READ | EV_PERSIST,socket_read_cb,(void*)ev);//只是写入了数据
  //这次的arg我们传的是事件结构体

  event_add(ev,NULL);//跟epoll_ctl一样,但是也不是很相同
  
}

void socket_read_cb(int fd,short events,void* arg)
{
   char msg[1024];
   struct event* ev = (struct event*)arg;
   int len = read(fd,msg,sizeof(msg) - 1);
   if (len <= 0) {
        printf("client fd:%d disconnect\n", fd);
        event_free(ev);
        close(fd);
        return;
    }
   

   msg[len] = '\0';
   printf("recv the client msg: %s",msg);
   char reply_msg[1024] = "recvieced msg: ";
   strcat(reply_msg + strlen(reply_msg), msg);
   write(fd, reply_msg, strlen(reply_msg));
}
int main()
{
   int listenfd = socket(AF_INET,SOCK_STREAM,0);
   if(listenfd  == -1) printf("socket error\n");
   struct sockaddr_in addr;
   memset(&addr, 0, sizeof(addr)); 
   addr.sin_family = AF_INET;
   addr.sin_port = htons(8080);
   addr.sin_addr.s_addr = htonl(INADDR_ANY);

   if(bind(listenfd ,(struct sockaddr*)&addr,sizeof(addr)) == -1)printf("bind error\n");
   listen(listenfd ,3);
   struct event_base* base  = event_base_new();
   printf("create base\n");
   struct event* ev_listen = event_new(base,listenfd,EV_READ | EV_PERSIST,socket_accept_cb,base);
    //返回是struct event*得到这个事件结构体的指针
        /*
    event_new 等价于
    struct event ev_listen;
    event_set(&ev_listen, listenfd, EV_READ | EV_PERSIST, socket_accept_cb, base);
    event_base_set(base, &ev_listen);
    */
   event_add(ev_listen,NULL);//相当于epoll_ctl

   event_base_dispatch(base);//循环等待事件
    return 0;
}

IOイベント操作(主にevconnlistenerとbufferevent)のカプセル化とAPIの導入

 組み込みのbuffer-bufferevent を使用したイベント


Bufferevent は実際にはイベントですが、通常のイベントよりも高度で、2 つの内部バッファーと 1 つのファイル記述子 (ネットワーク ソケット) を備えています。ネットワーク ソケットには読み取りと書き込みの 2 つのバッファがあります。Buffereven にも 2 つのバッファと、libevent イベントによって駆動されるコア コールバック関数があります。4 つのバッファとトリガー コールバックの関係は次のとおりです:

図からわかるように、1 つのバッファイベントは 2 つのバッファと 3 つのコールバック関数 (書き込みコールバック、読み取りコールバック、イベント コールバック) に対応します。

Bufferevent には 3 つのコールバック関数があります。

読み取りコールバック– 読み取りイベント コールバックは、buffereven が基礎となる読み取りバッファから独自の読み取りバッファにデータを読み取るときにトリガーされます。
書き込みコールバック < /span> – バッファイベントによってバインドされたソケット接続が切断されたとき、または異常なときイベント コールバックがトリガーされます。
イベント コールバック – 書き込みイベント コールバックは、bufferevent が独自の書き込みバッファ データを基礎となる書き込みバッファに書き込むときにトリガーされます。データは最終的にカーネルの書き込みバッファに書き込まれるため、アプリケーションはそれを制御できません。このイベントは基本的にアプリケーションにとって役に立たず、単なる通知機能です。
 

バッファイベントオブジェクトを構築する 
struct bufferevent *bufferevent_socket_new(struct event_base *base, evutil_socket_t fd, int options);
函数说明: bufferevent_socket_new 对已经存在socket创建bufferevent事件, 可用于
后面讲到的连接监听器的回调函数中.
参数说明:
	base :对应根节点
	fd   :文件描述符
	options : bufferevent的选项
		BEV_OPT_CLOSE_ON_FREE  -- 释放bufferevent自动关闭底层接口
		(当bufferevent被释放以后, 文件描述符也随之被close)    
		BEV_OPT_THREADSAFE  -- 使bufferevent能够在多线程下是安全的
バッファイベントオブジェクトを破棄します
void bufferevent_free(struct bufferevent *bufev);
函数说明: 释放bufferevent
接続操作bufferevent_socket_connect
int bufferevent_socket_connect(struct bufferevent *bev, struct sockaddr *serv, int socklen);
函数说明: 该函数封装了底层的socket与connect接口, 通过调用此函数, 可以将bufferevent事件与通信的socket进行绑定, 参数如下:
	bev – 需要提前初始化的bufferevent事件
	serv – 对端(一般指服务端)的ip地址, 端口, 协议的结构指针
	socklen – 描述serv的长度
说明: 调用此函数以后, 通信的socket与bufferevent缓冲区做了绑定, 后面调用了bufferevent_setcb函数以后, 会对bufferevent缓冲区的读写操作的事件设置回调函数, 当往缓冲区中写数据的时候会触发写回调函数, 当数据从socket的内核缓冲区读到bufferevent读缓冲区中的时候会触发读回调函数.
buffereventコールバックとbufferevent_setcbを設定する
void bufferevent_setcb(struct bufferevent *bufev,
	bufferevent_data_cb readcb,
	bufferevent_data_cb writecb,
	bufferevent_event_cb eventcb,
	void *cbarg
);
函数说明: bufferevent_setcb用于设置bufferevent的回调函数, 
readcb, writecb,eventcb分别对应了读回调, 写回调, 事件回调, 
cbarg代表回调函数的参数。
コールバック関数のプロトタイプ:
typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void *ctx);
typedef void (*bufferevent_event_cb)(struct bufferevent *bev, short what, void *ctx);
What 代表 对应的事件
BEV_EVENT_EOF--遇到文件结束指示
BEV_EVENT_ERROR--发生错误
BEV_EVENT_TIMEOUT--发生超时
BEV_EVENT_CONNECTED--请求的过程中连接已经完成
書き込みバッファへのデータの書き込みbufferevent_write
int bufferevent_write(struct bufferevent *bufev, const void *data, size_t size);
int bufferevent_write_buffer(struct bufferevent *bufev, struct evbuffer *buf);
bufferevent_write是将data的数据写到bufferevent的写缓冲区,bufferevent_write_buffer 
是将数据写到写缓冲区另外一个写法, 实际上bufferevent的内部的两个缓冲区结构就是struct evbuffer。
読み取りバッファからデータを読み取ります。
size_t bufferevent_read(struct bufferevent *bufev, void *data, size_t size);
int bufferevent_read_buffer(struct bufferevent *bufev, struct evbuffer *buf);
bufferevent_read 是将bufferevent的读缓冲区数据读到data中, 同时将读到的数据从
bufferevent的读缓冲清除。
bufferevent_read_buffer 将bufferevent读缓冲数据读到buf中, 接口的另外一种。
登録および登録解除のイベント タイプbufferevent_enable/disable
int bufferevent_enable(struct bufferevent *bufev, short event);
int bufferevent_disable(struct bufferevent *bufev, short event);
bufferevent_enable与bufferevent_disable是设置事件是否生效, 如果设置为disable, 
事件回调将不会被触发。
読み取りおよび書き込みバッファーの取得buffereven_get_input およびbufferevent_get_oupput
struct evbuffer *bufferevent_get_input(struct bufferevent *bufev)
struct evbuffer *bufferevent_get_output(struct bufferevent *bufev)
获取bufferevent的读缓冲区和写缓冲区
分割文字読み取り evbuffer_readln と固定長読み取り evbuffer_remove
char *evbuffer_readln(struct evbuffer *buffer, size_t *n_read_out, enum evbuffer_eol_style eol_style);
int evbuffer_remove(struct evbuffer *buf, void *data, size_t datlen);
分割字符读evbuffer_readln
固定长度读evbuffer_remove
バッファイベントの概要


bufferevent には、ファイル記述子、2 つのバッファ、および 3 つのコールバック関数があります。ファイル記述子は、クライアントとの通信に使用される通信ファイル記述子であり、リスニング ファイル記述子ではありません。

2 つのバッファとは次のことを指します。 バッファイベントには読み取りバッファと書き込みバッファが含まれます。
3 つのコールバック関数とは、 読み取りコールバック関数、書き込みコールバック関数、イベント コールバック関数
読み取りコールバック関数のトリガー時間:

ソケットのカーネル ソケット読み取りバッファにデータがある場合、bufferevent はカーネル バッファのデータを独自の読み取りバッファに読み取り、bufferevent の読み取り操作をトリガーします。このとき、bufferevent の読み取りコールバック関数が呼び出されます。

コールバック関数のトリガー時間を書き込みます。

buffereventの書き込みバッファにデータを書き込むと、buffereventの最下位層はバッファ内のデータをカーネルソケットの書き込みバッファに書き込みます。このとき、buffereventの書き込みコールバック関数がトリガーされ、最終的にカーネルドライバが実行されます。データを送信します。

イベント(例外)コールバック関数のトリガータイミング:

クライアントが接続を閉じるか、プロセスがシグナルによって終了すると、イベント コールバック関数がトリガーされます。

接続リスナー-evconnlistener


リンク リスナーは、ソケット、バインド、リッスン、受け入れ関数など、基礎となるソケット通信関連の関数をカプセル化します。リンク リスナーの作成後、実際にはソケット、バインド、リッスンの呼び出しと同じになります。このとき、新しいクライアント接続の到着を待ちます。新しいクライアント接続がある場合、最初に受け入れプロセスが内部で呼び出されます。 、その後、ユーザー指定のコールバック関数が呼び出されます。まず関数のプロトタイプを見て、それがどのように機能するかを理解します。
 

接続リスナー evconnlistener_new_bind を構築する
struct evconnlistener *evconnlistener_new_bind(
	struct event_base *base,evconnlistener_cb cb, 
	void *ptr, unsigned flags, int backlog,
	const struct sockaddr *sa, int socklen
);
函数说明: 
是在当前没有套接字的情况下对链接监听器进行初始化, 看最后2个参数实际上就是bind使用的关键参数, 
backlog是listen函数的关键参数(略有不同的是, 如果backlog是-1, 那么监听器会自动选择一个合适的值, 
如果填0, 那么监听器会认为listen函数已经被调用过了), ptr是回调函数的参数, cb是有新连接之后的回调函数, 
但是注意这个回调函数触发的时候, 链接器已经处理好新连接了, 并将与新连接通信的描述符交给回调函数。

flags 需要参考几个值:
	LEV_OPT_LEAVE_SOCKETS_BLOCKING   文件描述符为阻塞的
	LEV_OPT_CLOSE_ON_FREE            关闭时自动释放
	LEV_OPT_REUSEABLE                端口复用
	LEV_OPT_THREADSAFE               分配锁, 线程安全
struct evconnlistener *evconnlistener_new(
	struct event_base *base,
	evconnlistener_cb cb, void *ptr, 
	unsigned flags, int backlog,
	evutil_socket_t fd
);

evconnlistener_new 関数と前の関数の違いは、最後の 2 つのパラメータにあります。この関数を使用すると、ソケットが初期化され、バインドが完了し、リスニングも完了するとみなされるため、ほとんどの今度は、最初の 1 つの関数を使用できます。

コールバック関数 evconnlistener_cb を受け入れる
typedef void (*evconnlistener_cb)(struct evconnlistener *evl, evutil_socket_t fd, struct sockaddr *cliaddr, int socklen, void *ptr);

コールバック関数fd参数是与客户端通信的描述符 は接続の監視を待機する記述子ではないため、cliaddr は accept によって処理された新しい接続のピア アドレス情報にも対応します。

接続リスナーevconnlistener_freeを破棄します。
void evconnlistener_free(struct evconnlistener *lev);

libeventを使用したイベント検出とイベント操作のデモ 

#include <netinet/in.h>
#include <sys/socket.h>
#include <stdio.h>
#include <signal.h>
#include <event.h>
#include <event2/buffer.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
#include <event2/bufferevent.h>
#include <event2/listener.h>
void socket_read_callback(struct bufferevent* bev,void* arg)
{
    //操作读缓冲当中过的数据,通过该函数得到读缓冲的地址
    struct evbuffer* evbuf = bufferevent_get_input(bev);

    char* msg = evbuffer_readln(evbuf,NULL,EVBUFFER_EOL_LF);
    //我们的bufferevent里肯定有读缓冲

    // 也可以直接用 bufferevent_read 读数据
    // bufferevent_read(struct bufferevent *bufev, void *data, size_t size)
    if(!msg) return;

    printf("server read the data: %s\n",msg);

    char reply[1024] = {0};
    sprintf(reply, "recvieced msg: %s\n", msg);//echo
    //需要自己释放资源,很重要
    free(msg);
    bufferevent_write(bev,reply,strlen(reply));
}
void socket_event_callback(struct bufferevent *bev, short events, void *arg)
{
     if(events & BEV_EVENT_EOF) 
        printf("connection closed\n");
     else if (events & BEV_EVENT_ERROR)
        printf("some other error\n");
     else if (events & BEV_EVENT_TIMEOUT)
        printf("timeout\n");

    bufferevent_free(bev);

}
void listener_callback(struct evconnlistener *listener,evutil_socket_t fd,
                      struct sockaddr *sock,int socklen, void *arg)
{
      char ip[32] = {0};
      evutil_inet_ntop(AF_INET,sock,ip,sizeof(ip) - 1);
      //该函数的作用是将网络字节序表示的 IPv4 地址转换为可读的字符串格式,并将结果存储在提供的缓冲区中。
      //这样可以方便地将 IP 地址以人可读的形式输出,比如在日志中记录连接的来源。
      printf("accept a client fd:%d ip:%s\n",fd,ip);
      //也就是说,监听到之后,触发回调,然后会自动把新连接的fd传进来吗
      //也就是相当于我们前面设置了一个监听套接字的bufferevent,当内核中的socket有链接到来的时候
      //也就是内核中的读缓冲有数据的时候,那么就会触发bufferevent回调,写到bufferevent的读缓冲区
      //然后把数据传到我们用户层,就不需要我们自己去事件处理了

      struct event_base* base = (struct event_base*)arg;//把reactor对象传进来了

      //创建一个bufferevent,构建bufferevent对象
      struct bufferevent* bev = bufferevent_socket_new(base,fd,BEV_OPT_CLOSE_ON_FREE);
      //函数说明: bufferevent_socket_new 对已经存在socket创建bufferevent事件, 可用于
      //后面讲到的连接监听器的回调函数中.我这里的fd是accept是用于和客户端交互的fd
      //所以这个fd是我们设置到bufferevent中,让bufferevent对象帮我们处理
      //选了这个选项会当bufferevent释放后,里面的fd什么的也会自动关闭和释放
      
      // 设置读、写、以及异常时的回调函数
      bufferevent_setcb(bev,socket_read_callback,NULL,socket_event_callback,NULL);

      bufferevent_enable(bev,EV_READ | EV_PERSIST);//注册事件 
}
void stdin_callback(struct bufferevent* bev,void* arg)
{
     struct evbuffer* evbuf = bufferevent_get_input(bev);
     struct event_base* base =  (struct event_base*)arg;//这个就是上下文
     char* msg = evbuffer_readln(evbuf,NULL,EVBUFFER_EOL_LF);
     if(!msg) return;

    if (strcmp(msg, "quit") == 0) {
        printf("safe exit!!!\n");
        event_base_loopbreak(arg);//中断事件循环
    }

    printf("stdio read the data: %s\n", msg);
}
void do_timer(int fd,short events,void* arg)
{
    struct event *timer = (struct event *) arg;
    time_t now = time(NULL);
    printf("do_timer %s", (char *) ctime(&now));
}
void do_sig_int(int fd,short events,void* arg)
{
    struct event *si = (struct event *)arg;
    event_del(si);
    printf("do_sig_int SIGINT\n");//CTRL + C
}
int main()
{
    struct sockaddr_in sin;
    memset(&sin,0,sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(8088);
    sin.sin_addr.s_addr = htonl(INADDR_ANY);
    struct event_base* base = event_base_new();
    
    //链接监听器封装了底层的socket通信相关函数, 比如socket, bind, listen, accept这几个函数。
    struct evconnlistener* listener = evconnlistener_new_bind(base,listener_callback,
    base,LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE,10,(struct sockaddr*)&sin,sizeof(sin));
    //: 创建一个监听器对象,该监听器将监听指定的地址和端口,
    //并在有新连接时调用 listener_callback 函数进行处理。参数说明如下:
    // base: 事件基础结构,将监听器与此关联。
    // listener_callback: 当有新连接建立时将调用的回调函数。
    // base: 将传递给回调函数的参数,这里是事件基础结构。
    // LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE: 一组标志,指定监听器的行为,包括启用地址重用和在释放时关闭底层文件描述符。
    // 10: 允许在套接字上排队等待连接的最大数量。
    // (struct sockaddr*)&sin: 要监听的地址和端口信息。
    // sizeof(sin): 提供地址结构体的大小。
    
    //对stdin的io事件进行处理
    //stdin的文件描述符是0
    struct bufferevent* ioev = bufferevent_socket_new(base,0,BEV_OPT_CLOSE_ON_FREE);
    bufferevent_setcb(ioev,stdin_callback,NULL,NULL,base);//设置读写事件回调
    bufferevent_enable(ioev,EV_READ | EV_PERSIST);//设置事件属性并开启

    //定时事件
    struct event evtimer;
    struct timeval tv = {1,0};
    event_set(&evtimer,-1,EV_PERSIST,do_timer,&evtimer);//初始化
    // &evtimer: 是一个指向 struct event 结构体的指针,表示你要设置的事件对象。
    // -1: 是事件的文件描述符,定时器事件不需要一个真实的文件描述符,因此通常设置为 -1。
    // EV_PERSIST: 是事件的标志,表示这是一个持久性事件,即它会在每次触发后自动重新添加到事件循环中,使得它可以周期性地触发。
    // do_timer: 是事件触发时执行的回调函数。
    // &evtimer: 是传递给回调函数的用户数据。
    event_base_set(base,&evtimer);
    //设置event从属的event_base,这一步相当于指明event要注册到哪个event_base实例上。
    event_add(&evtimer,&tv);

    //信号事件
    struct event ev_sig_int;
    event_set(&ev_sig_int,-1,EV_PERSIST,do_sig_int,&ev_sig_int);
    event_base_set(base,&ev_sig_int);
    event_add(&ev_sig_int,NULL);

    //开启事件主循环
    event_base_dispatch(base);
    /* 结束释放资源 */
    evconnlistener_free(listener);
    event_base_free(base);
    return 0;
}

libevent イベント原理の分析 

信号イベントの分析 (これについてはまだ説明しません)

スケジュールされたイベントとネットワーク イベントの分析

タイマーの小さなルートヒープ


libevent タイマーのメカニズムは最小ヒープ + epoll_wait メカニズムです。Event_base_dispatch は内部でevent_base_loop を呼び出します。メイン ループに入ると、最初に最小ヒープに移動してタイムアウト パラメーターを見つけ、その後 epoll_wait を実行します。その後、すべてのタイムアウト タスクが timeout_process から取得され、レディ キューに配置され、ネットワーク イベントとスケジュールされたイベントがレディ キューに追加され、優先度に従って処理され、対応するコールバック関数が呼び出されることがわかります。

while (!done) {
	......
	tv_p = &tv;
	if (!N_ACTIVE_CALLBACKS(base) && !(flags & EVLOOP_NONBLOCK)) {
		timeout_next(base, &tv_p);	// 返回的 tv_p 即是 最小堆实现的定时器中第一个事件的剩余等待时间
	} 
	......
	clear_time_cache(base);

	res = evsel->dispatch(base, tv_p);	// 以tv_p作为 epoll_wait 的超时时间。这里相当于epoll_wait(),收集网络事件
	......
	update_time_cache(base);	// 更新 time_cache,time_cache的作用在于不必每次都从系统调用获取时间值
	......
	timeout_process(base);	// 将所有已超时的任务从最小堆中取出,插入到就绪队列(有优先级)收集定时事件

	if (N_ACTIVE_CALLBACKS(base)) {
		int n = event_process_active(base);	// 处理这些就绪的任务,调用其回调函数
		......
	} 
}

/* Activate every event whose timeout has elapsed. */
static void timeout_process(struct event_base *base)
{
	/* Caller must hold lock. */
	struct timeval now;
	struct event *ev;
	if (min_heap_empty_(&base->timeheap)) {
		return;
	}

	gettime(base, &now);
	while ((ev = min_heap_top_(&base->timeheap))) {
		if (evutil_timercmp(&ev->ev_timeout, &now, >))	// 从堆中取出所有 ev_timeout 已达到 now 的事件
			break;

		/* delete this event from the I/O queues */
		event_del_nolock_(ev, EVENT_DEL_NOBLOCK);	// 从所在的 event_base 中删除该事件
		event_active_nolock_(ev, EV_TIMEOUT, 1);	//  激活该事件,即 插入到就绪队列
	}
}

 event_active_nolock_() の最下層は、event_queue_insert_active() を呼び出して、event_base の下のレディ キュー activequeues にイベントを挿入します。このレディ キューは実際には、nactivequeues 要素を持つキュー配列です。配列の添え字が小さいほど、キューの優先順位が高くなります。毎回新しいイベントを作成するとき、デフォルトの優先度 ev_pri は nactivequeues / 2 です (デフォルトでは、新しいイベントは中間の優先度に設定されます)。イベントをevent_baseに登録する前に、この関数を使用して優先度を手動で設定できます。
 

/* イベントの優先順位を設定します - イベントがすでにスケジュールされている場合
 * 優先順位の変更は失敗します。 */
int events_priority_set(struct event *ev, int pri)
 

読み取りおよび書き込みバッファー evbuffer の実装 (重要な理解)


ネットワーク IO の読み取りと書き込みを行う場合、1 回の読み取りが完全なデータ パケットであることを保証することはできません。たとえば、size と書いても、実際には n

バッファを修正: char rbuf[16 * 1024 * 1024]; char wbuf[16 * 1024 * 1024] しかし、これにより 2 つの新たな問題が発生します。1. スペースの無駄が発生する 2. 頻繁なデータ移動< / a> evbuffer は libevent によって実装されたチェーン バッファです。bufferevent を使用してイベントを管理する場合、各イベントの evbuffer からデータを読み書きします。各 evbuffer は本質的にバッファにリンクされたリストであり、その中の各要素は struct evbuffer_chain です。 struct evbuffer の主要なメンバーは次のように定義されます。 evbuffer。まずはevbufferの紹介から始めましょう。
Ringbuffer: リング バッファ。頻繁なデータ移動の問題は解決されますが、データ スペースの無駄の問題は解決されません。 libevent の


 

struct evbuffer {
	/** The first chain in this buffer's linked list of chains. */
	struct evbuffer_chain *first;
	/** The last chain in this buffer's linked list of chains. */
	struct evbuffer_chain *last;
	/** Pointer to the next pointer pointing at the 'last_with_data' chain. */
	struct evbuffer_chain **last_with_datap;	// 指针指向最后一个可写的 chain
	/** Total amount of bytes stored in all chains.*/
	size_t total_len;
	...... // 以上为关键成员
}

それぞれevbuffer_chainは次のように定義されます:

/** A single item in an evbuffer. */
struct evbuffer_chain {
	/** points to next buffer in the chain */
	struct evbuffer_chain *next;	// 指向下一个 evbuffer_chain 

	/** total allocation available in the buffer field. */
	size_t buffer_len;				// buffer 的长度

	/** unused space at the beginning of buffer or an offset into a file for sendfile buffers. */
	ev_misalign_t misalign;			// 实际数据在 buffer 中的偏移

	/** Offset into buffer + misalign at which to start writing.
	 * In other words, the total number of bytes actually stored in buffer. */
	size_t off;						// buffer 中有效数据的末尾,接下来的数据从这个位置开始填入(该位置即 buffer + misalign + off)

	/** number of references to this chain */
	int refcnt;						// 这个 buffer的引用计数

	/** Usually points to the read-write memory belonging to this buffer allocated as part of the evbuffer_chain allocation.
	 * For mmap, this can be a read-only buffer and EVBUFFER_IMMUTABLE will be set in flags.  For sendfile, it may point to NULL. */
	unsigned char *buffer;			// 指向实际数据存储的位置,这是真正的 buffer
};

ミサリギンとはどういう意味ですか?読み取られたデータです。次の有効データは、[buffer+misaligin,buffer+misaligin +off] の off セクションの長さで、取得したい有効データです。 [buffer,buffer+misaligin]セクションは以前に読み込まれているため、これは無効なデータです。したがって、ミサリギンは頻繁なデータ移動の問題を解決します。私たちの evbuffer_chain は リンク リスト の形式であるため、 データ スペースの無駄の問題を解決します。したがって、evbuffer の設計は非常に巧妙です。

bufferevent_write
書き込みバッファにデータを書き込むためにbufferevent_write を呼び出すとき、実際には evbuffer_add を呼び出します。書き込み後、libevent は自動的にカーネル バッファに書き込み、書き込みコールバックをトリガーします。関数。

この evbuffer にデータを書き込むことができるチェーンがない場合は、書き込まれたデータのサイズに応じて新しいチェーンを申請し、リンクされたリストの最後にぶら下げてから、このチェーンにデータを書き込む必要があります。各チェーンのバッファサイズは可変です。コメントに詳細を書きましたので、読者は自分で読んでください。

int evbuffer_add(struct evbuffer *buf, const void *data_in, size_t datlen) {
    //...
    //如果大于限定的容量
    if (datlen > EV_SIZE_MAX - buf->total_len) {
        goto done;
    }

    //使chain指向之后一个链表
    if (*buf->last_with_datap == NULL) {
        chain = buf->last;
    }
    else {
        chain = *buf->last_with_datap;
    }

    //...
    //如果没有chain,那么创建一个datlen大小的返回即可
    if (chain == NULL) {
        chain = evbuffer_chain_insert_new(buf, datlen);
        if (!chain)
            goto done;
    }

    if ((chain->flags & EVBUFFER_IMMUTABLE) == 0) {
        //...
        //remain为当前可用剩余空间还有多少
        remain = chain->buffer_len - (size_t) chain->misalign - chain->off;
        //如果剩余空间大于需求,那么直接分配即可
        if (remain >= datlen) {
            /* there's enough space to hold all the data in the
             * current last chain */
            memcpy(chain->buffer + chain->misalign + chain->off,
                   data, datlen);
            chain->off += datlen;
            buf->total_len += datlen;
            buf->n_add_for_cb += datlen;
            goto out;
        }
            //否则看一看剩余空间+misalign是否大于需求,大于则移动off数据
        else if (!CHAIN_PINNED(chain) &&
                 //里面涉及别的一些细节,这里不展开
                 evbuffer_chain_should_realign(chain, datlen)) {
            /* we can fit the data into the misalignment */
            evbuffer_chain_align(chain);

            memcpy(chain->buffer + chain->off, data, datlen);
            chain->off += datlen;
            buf->total_len += datlen;
            buf->n_add_for_cb += datlen;
            goto out;
        }
    }
    else {
        /* we cannot write any data to the last chain */
        remain = 0;
    }
    //走到这里代表一个chain不能满足datlen,那么预分配一个tmp chain
    /* we need to add another chain */
    to_alloc = chain->buffer_len;
    if (to_alloc <= EVBUFFER_CHAIN_MAX_AUTO_SIZE / 2)
        to_alloc <<= 1;
    if (datlen > to_alloc)
        to_alloc = datlen;
    tmp = evbuffer_chain_new_membuf(to_alloc);
    if (tmp == NULL)
        goto done;
    //把当前chain给分配完
    if (remain) {
        memcpy(chain->buffer + chain->misalign + chain->off,
               data, remain);
        chain->off += remain;
        buf->total_len += remain;
        buf->n_add_for_cb += remain;
    }
    //还需要多少大小从新的tmp里面分配
    data += remain;
    datlen -= remain;

    memcpy(tmp->buffer, data, datlen);
    tmp->off = datlen;
    evbuffer_chain_insert(buf, tmp);
    buf->n_add_for_cb += datlen;

    out:
    evbuffer_invoke_callbacks_(buf);
    result = 0;
    done:
    EVBUFFER_UNLOCK(buf);
    return result;
}

バッファイベント_読み取り

bufferevent_read()基になる呼び出しevbuffer_remove は、指定された長に従って読み取り、evbuffer_copyout_from を呼び出すことを意味します。詳細には触れませんが、書き方がわかれば、読み方もわかります。

/* Reads data from an event buffer and drains the bytes read */
int evbuffer_remove(struct evbuffer *buf, void *data_out, size_t datlen)
{
	ev_ssize_t n;
	EVBUFFER_LOCK(buf);
	n = evbuffer_copyout_from(buf, NULL, data_out, datlen);	// 拷贝数据
	if (n > 0) {
		if (evbuffer_drain(buf, n)<0)	// drain 就是丢弃已读走的数据,即 调整当前 chain 的 misalign 或 直接释放数据已全部读走的 chain
			n = -1;
	}
	EVBUFFER_UNLOCK(buf);
	return (int)n;
}

evbufferの欠点


evbuffer の利点については上で説明しましたが、evbuffer の欠点についてはどうでしょうか。実際、データが不連続メモリに格納されていることも明らかです (たとえば、20B を読み取った結果、20B が 2 つのチェーンに存在します)。不連続メモリでは複数の IO が発生し、複数の IO が必要になる場合があります。データを完全に読み取ることができます。不連続メモリの問題に対して、Linux カーネルは、メモリの不連続読み取りおよび書き込みの問題を解決するためのインターフェイス、readv および writev を提供します。

readv: 読み取りバッファ内のデータを不連続メモリに読み取ります
writev: 不連続メモリ データを書き込みバッファに書き込みます
 

man 2 readv
# 第二个参数是数组,第三个参数是数组的长度
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

struct iovec {
    void  *iov_base;    /* Starting address 起始地址*/
    size_t iov_len;     /* Number of bytes to transfer 长度*/
};

 写真は陵生教育より抜粋

これは、カーネル バッファーが連続的であり、libevent のバッファーイベント バッファーがチェーン バッファーを使用しているためです。

 データが大きく、複数のチェーンを配置できる場合、書き込みと読み取りを引き続き使用すると、チェーンに複数回割り当てられるため、必然的にカーネル スイッチが多すぎます。

解決:

readv と writev を使用して、bufferevent のすべてのチェーン アドレスを渡し、それらを各チェーンに割り当てます。

このとき、ユーザー モードとカーネル モードを切り替える必要があるのは 1 回だけであり、効率が大幅に向上します。

ネットワーク プログラミングの問題点は解決されましたか?

効率的なネットワークバッファ

io機能の使用法とネットワーク原理、

マルチスレッド環境では、バッファがロックされている場合、読み取りの場合は完全なパケットを読み出す必要があり、書き込みの場合も同様です。

おすすめ

転載: blog.csdn.net/txh1873749380/article/details/134571392