Libevent 学习四:事件状态和事件处理


Libevent 事件状态

Libevent 有 4 种事件状态,分别是:已初始化(initialized)、待决(pending)、激活(active)、持久的(persistent),这 4 种状态的转换关系如下图所示:

在这里插入图片描述

下面比照上图,对这 4 种状态进行说明:

  • 已初始化(initialized):此状态对应图中的 non-pending 状态,表示事件已经新建完成,但是还未添加到 Libevent 队列中。有 4 条路径可以让事件跳转到此状态:

    • 调用 event_new 函数,返回一个事件对象,或调用 event_assign 赋值一个事件对象。这两个函数本质是一样的,一个是返回事件对象,一个是作为参数传递事件对象,一般第一种用的多。这两个函数在 Libevent 中的定义如下

      struct event *event_new(
          struct event_base *, evutil_socket_t, short, event_callback_fn, void *);
      
      int event_assign(struct event *, 
          struct event_base *, evutil_socket_t, short, event_callback_fn, void *);
      
      // 回调函数指针定义
      typedef void (*event_callback_fn)(evutil_socket_t, short, void *);
      
    • pinding 状态的事件调用 event_del 函数后,也会返回到此状态;

    • active 状态的事件调用完成(即执行完事件回调函数),若没有定义持久化,也会返回到此状态。

  • 待决(pending):此状态表示事件已经位于 Libevent 队列中,可以随时准备执行。有 4 条路径可以让事件跳转到此状态:

    • 调用 event_add 之后,事件进入到此状态;
    • active 状态的事件调用完成(即执行完事件回调函数),若定义为持久化,也会返回到此状态。
  • 激活(active):此状态表示事件正在执行中,也包括超时事件。这是由 Libevent 进行调度的,用户也可以手动调用 event_active 函数让事件转为此状态。

  • 持久的(persistent):严格来讲,这应该是一个事件表示,这个标识非常重要,会让事件跳转到不同的状态,所以单独进行说明。从图中可以看出:

    • 若设置为持久化,事件完成后状态会由 active 变为 pending ,可仍由 Libevent 调用,不用做其他操作;
    • 若未设置,则表示事件完成后状态会由 active 变为 non-pending ,即初始化状态,此时需要再次调用 event_add 函数,将事件变为 pending 状态,事件才会再次被执行。

Libevent 事件的状态与操作系统进程的状态类似,可以用于类比,如下图所示

在这里插入图片描述

Libevent 事件处理

本章详细描述 Libevent 事件处理的 API。

event_new 函数

功能:创建一个事件,将事件设置为 non-pending 状态。函数定义如下:

struct event *event_new(
    struct event_base *base, 			// Libevent上下文
    evutil_socket_t fd, 				// 监听的文件描述符
    short what, 						// 事件标志
    event_callback_fn cb, void *arg);	// 事件回调函数和参数
  • struct event_base *base Libevent 上下文;

  • evutil_socket_t fd 监听的文件描述符;

  • short what 事件标志,定义与头文件 event2/event.h ,主要事件标志有:

    • EV_TIMEOUT 超时,默认是忽略的,一般不用,直接设置超时时间即可;
    • EV_READ 读事件;
    • EV_WRITE 写事件;
    • EV_SIGNAL 信号事件;
    • EV_PERSIST 持久化事件;
    • EV_ET 边沿触发,只有在事件变化时才触发,需要底层支持,一般在 epoll 中使用。Libevent 2.0 以上支持,与 EV_READ 和 EV_WRITE 配合使用,如:EV_READ | EV_PERSIST, EV_READ | EV_ET
  • event_callback_fn cb 事件回调函数

    typedef void (*event_callback_fn)(evutil_socket_t fd, short what, void *arg);
    
    • evutil_socket_t fd 发生事件的文件描述符,这里不限于 socket,也可以是文件描述符,定义如下:

      #ifdef _WIN32
      #define evutil_socket_t intptr_t
      #else
      #define evutil_socket_t int
      #endif
      
    • short what 事件标志,定义如上;

    • void *arg 传递的参数,即 event_new 中的最后一个参数。

  • void *arg 传递给回调函数的参数。

event_add 函数

功能:将事件添加到 Libevent 中,事件会变为 pending 状态。若是定时器事件,会添加到优先级队列(小根堆实现)中。函数定义如下

int event_add(struct event *ev, const struct timeval *timeout);
  • struct event *ev event_new 创建的事件;

  • struct timeval *timeout 超时时间,不超时传递 NULL。timeval 中有两个字段,分别表示秒和微秒。定义如下:

    struct timeval {
        long    tv_sec;         /* seconds */
        long    tv_usec;        /* and microseconds */
    };
    

event_add 函数源码

int event_add(struct event *ev, const struct timeval *tv)
{
	int res;

	if (EVUTIL_FAILURE_CHECK(!ev->ev_base)) {
		event_warnx("%s: event has no event_base set.", __func__);
		return -1;
	}

	EVBASE_ACQUIRE_LOCK(ev->ev_base, th_base_lock);
	res = event_add_nolock_(ev, tv, 0);
	EVBASE_RELEASE_LOCK(ev->ev_base, th_base_lock);

	return (res);
}

通过代码可知,主要实现位于函数 event_add_nolock_ 中,此函数比较长,核心就是根据事件标志对不同事件(IO 事件、信号事件和超时事件)进行处理。默认情况有加锁操作 EVBASE_ACQUIRE_LOCK / EVBASE_RELEASE_LOCK,若是单线程调用,可以配置为不加锁,即在 config 中设置 EVENT_BASE_FLAG_NOLOCK ,这样效率会更高一些。

event_del 函数

功能:将事件从 Libevent 中删除,事件会变为 non-pending 状态。函数定义如下

int event_del(struct event *ev);
  • struct event *ev event_new 创建的事件。

注意:如果当前事件不是处于 pending 或 active 状态,则没有影响。

event_free 函数

功能:释放事件资源,事件变为未初始化状态。函数定义如下

void event_free(struct event *ev);
  • struct event *ev event_new 创建的事件。

注意:调用此函数后,事件不再可用,此时 ev 变为野指针,类似与调用 C 的 free 函数。

下面是 Libevent 实现的一个回显服务器的示例,即收到客户端的数据后返回 OK。都是采用最基础的函数实现的,在 Windows/Linux 上都可以运行:

// desc: libevent demo simple server

#include <iostream>
#include <string.h>
#include <errno.h>
#ifndef _WIN32
#include <signal.h>
#endif // !_WIN32

#include "event.h"

#define SERVER_PORT 8000

// 客户端数据读取事件
void client_cb(evutil_socket_t s, short what, void *arg)
{
    // 水平触发LT:只要有数据没有处理,就会一直触发
    // 边沿触发ET:只处理一次,不管数据是否处理完
    event *ev = (event*)arg;

    // 判断超时
    if (what & EV_TIMEOUT) {
        // 需要清理event
        std::cout << "timeout" << std::endl;
        event_free(ev);
        evutil_closesocket(s);
        return;
    }

    // 这是是为了测试,每次只读取4个数据,实际使用中不会这样用的
    char buf[5] = { 0 };
    int len = recv(s, buf, sizeof(buf) - 1, 0);
    if (len > 0) {
        std::cout << buf << std::endl;
        send(s, "OK", 2, 0);
    }
    else {
        // 需要清理event
        std::cout << "event_free" << std::endl;
        event_free(ev);
        evutil_closesocket(s);
    }
}

// 接收连接的回调函数
void listen_cb(evutil_socket_t s, short what, void *arg)
{
    std::cout << "listen_cb" << std::endl;

    // 读取连接信息
    sockaddr_in sin;
    socklen_t len = sizeof(sin);
    evutil_socket_t client = accept(s, (sockaddr*)&sin, &len);

    char ip[16] = { 0 };
    evutil_inet_ntop(AF_INET, &sin.sin_addr, ip, sizeof(ip) - 1);
    std::cout << "client ip: " << ip << ":" << ntohs(sin.sin_port) << std::endl;

    // 客户端数据读取事件
    event_base *base = (event_base*)arg;
    // 水平触发
    //event* ev = event_new(base, client, EV_READ | EV_PERSIST, client_cb, event_self_cbarg());
    // 边沿触发,Windows上无效
    event* ev = event_new(base, client, EV_READ | EV_PERSIST | EV_ET, client_cb, event_self_cbarg());
    timeval tv = { 10, 0 };  // 10秒超时
    event_add(ev, &tv);
}

int main(int argc, char* argv[])
{
#ifdef _WIN32
    // 初始化socket库
    WSADATA wsa;
    WSAStartup(MAKEWORD(2, 2), &wsa);
#else
    // 忽略管道信号,因为发送数据给已关闭的socket会生成SIGPIPE信号,导致进程退出
    if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
        return -1;
#endif

    std::cout << "libevent echo server test" << std::endl;
    // 创建libevent上下文
    event_base *base = event_base_new();
    if (!base) {
        std::cout << "event_base_new failed" << std::endl;
        return -1;
    }
    std::cout << "event_base_new success" << std::endl;

    // 创建 socket
    evutil_socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock <= 0) {
        std::cout << "create socket error: " << strerror(errno) << std::endl;
        return -1;
    }

    // 设置非阻塞和地址复用
    evutil_make_socket_nonblocking(sock);
    evutil_make_listen_socket_reuseable(sock);

    // 绑定IP和端口
    struct sockaddr_in sin;
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_port = htons(SERVER_PORT);
    int ret = ::bind(sock, (sockaddr*)&sin, sizeof(sin));
    if (ret != 0) {
        std::cout << "bind socket error: " << strerror(errno) << std::endl;
        return -1;
    }

    // 开始监听,建立10个连接的缓存
    listen(sock, 10);

    // 创建监听事件,并添加到libevent中
    event* ev = event_new(base, sock, EV_READ | EV_PERSIST, listen_cb, base);
    event_add(ev, 0);

    // 事件分发处理
    event_base_dispatch(base);

    // 释放资源
    if (ev) event_free(ev);
    event_base_free(base);

#ifdef _WIN32
    WSACleanup();
#endif

    return 0;
}

特别说明,上面的代码为了测试,加了很多功能,比如故意将接收缓冲区设置的很小,是为了测试边沿触发。还有,10s 内未收到数据,则服务端自动断开连接,实际使用过程中不用这样处理。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值