版本:2.1.12-stable

1.libevent的编译与安装

  1. 查看libevent库下的README.md文件
 $ ./configure
 $ make
 $ make verify   # (optional)
 $ sudo make install
  1. 安装成功后,运行一个自带的示例测试:
./ hello-world
nc 127.0.0.1 9995

image.png
image.png

  1. 可能找不到libevent-2.1.so.7共享库,解决办法如下
    image.png
1. 查看libevent-2.1.so.7的位置,结果位于/usr/local/lib下    
locate libevent-2.1.so.7
2. 将该路径放入系统查找路径内
sudo echo "/usr/local/lib" >> /etc/ld.so.conf
3. 使该配置生效
sudo ldconfig

2.使用套路

  1. 使用event_base_new函数创建一个事件处理框架,对应Reactor模式中的Reactor管理器。其Reactor模式内部使用一个IO多路复用接口用于检测注册的事件集合中哪种事件已经就绪。这里的IO多路复用接口对应Reactor模式中的同步事件分发器。
  2. 使用event_new函数创建一个事件,指定事件产生以后的回调。这里的事件对应Reactor模式中的事件源,事件回调对应Reactor模式中的事件处理器。
  3. 使用event_add函数将事件添加到事件处理框架上
  4. 使用event_base_dispatch函数开始事件循环
  5. 使用event_freeevent_base_free函数释放资源

1.事件处理框架event_base的创建

1.event_base
  1. 使用 libevent相关函数之前需要分配一个或者多个event_base 结构体。每个event_base结构体持有一个事件集合,可以检测以确定哪个事件是就绪的。
    1. 相当于epoll红黑树的树根
    2. 每个event_base都使用一个IO多路复用接口用于检测注册的事件集合中哪种事件已经就绪。
      image.png
2.相关函数
  1. event_base的创建
struct event_base* event_base_new(void);
函数返回值:
    失败返回NULL
  1. event_base的释放
event_base_free(struct event_base* base);
  1. 循环监听添加到事件处理框架上的事件,等待条件满足
event_base_dispatch();
  1. 查看event_base封装的后端
#include <stdio.h>
#include <event2/event.h>
int main() {
    const char** event_get_supported_methods(void);
    const char* event_base_get_method(const struct event_base* base);

    // 获取当前OS支持的IO转接方法
    const char **methods = event_get_supported_methods();
    printf("当前libevent版本为:%s\n", event_get_version());
    for (int i = 0; methods[i] != NULL; ++i)
    {
        printf("%s\n", methods[i]);
    }

    // 获取当前libevent使用的IO转接方法
    struct event_base* base = event_base_new();
    printf("当前libevent使用的IO转接方法:%s\n", event_base_get_method(base));
    event_base_free(base);
    
    return 0;
}

// 当前libevent版本为:2.1.12-stable
// epoll
// poll
// select
// 当前libevent使用的IO转接方法:epoll

// g++ test.cpp -levent_core
  1. event_base和fork:子进程创建成功之后,父进程可以继续使用event_base。子进程中需要继续使用event_base需要重新进行初始化。
int event_reinit(struct event_base* base);

2.事件创建

1.创建新事件
  1. 事件的类型
// event_new函数中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   // 边缘模式
  1. 使用event_new函数创建新事件,指定事件发生的回调(事件处理器)
// 事件处理器
typedef void (*event_callback_fn)(evutil_socket_t, short, void *); 
struct event *event_new(
    struct event_base *base, 
    evutil_socket_t fd, // 文件描述符 - int
    short what, 
    event_callback_fn cb, //事件处理器
    void *arg // 事件处理器的第三个参数
);

注意:调用event_new()函数之后,新事件处于已初始化和非未决状态 。 非未决和未决的区别:

非未决:没有资格被处理
未决:有资格被处理,但是还没有处理。激活后再处理
  1. 关于EV_PERSIST标志:默认情况下,每当未决事件激活后,事件将在其事件处理器被执行前成为非未决状态。如果设置了该标志,则即使事件激活导致事件处理器被执行后,事件还是会保持未决状态。
2.释放事件
void event_free(struct event *event);
3.设置未决事件

构造事件之后,在将其添加到 event_base 之前实际上是不能对其做任何操作的,因为此时事件处于非未决状态。使用event_add()将事件添加到event_base, 非未决事件转化为未决事件。

int event_add(
    struct event *ev, 
    const struct timeval *tv
); 
函数参数:
    ev:创建的事件
    tv:
        NULL,事件被触发,对应的回调被调用
        tv = {0, 100}, 如果设置的时间,在该时间段内检测的事件没被触发,
        时间到达之后, 回调函数还是会被调用
函数返回值:
    函数调用成功返回 0,失败返回 -1 。
4.设置非未决事件

对已经初始化的事件调用 event_del() 将使其成为非未决和非激活的。如果事件不是未决的或者激活的,调用将没有效果。

int event_del(struct event *ev); 
函数返回值:
    成功时函数返回 0,失败时返回-1。
5.事件的状态转换

image.png

6.示例:使用libevent读写管道
  1. write_fifo.cpp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#include <event2/event.h>

// 事件处理函数
void write_cb(evutil_socket_t fd, short what, void *arg)
{
    // write管道
    char buf[1024] = {0};
    static int num = 0;
    sprintf(buf, "hello, world == %d\n", num++);
    write(fd, buf, strlen(buf)+1);
}


// 写管道
int main(int argc, const char* argv[])
{
    // open file
    int fd = open("myfifo", O_WRONLY | O_NONBLOCK);
    if(fd == -1)
    {
        perror("open error");
        exit(1);
    }

    // 写管道
    struct event_base* base = NULL;
    base = event_base_new();

    // 创建事件
    struct event* ev = NULL;
    // 检测的写缓冲区是否有空间写
    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);
    close(fd);
    
    return 0;
}

  1. read_fifo.cpp
#include <stdio.h>
#include <unistd.h> // for unlink()
#include <sys/types.h>
#include <sys/stat.h>
#include <event2/event.h>
#include <fcntl.h>  // for open()

void read_fifo(evutil_socket_t fd, short events, void * arg)
{
    char buff[1024] = {0};
    ssize_t read_len = read(fd, buff, sizeof(buff));
    printf("读取长度为:%ld, 数据为:%s\n", read_len, buff);
    printf("read event: %s", events & EV_READ ? "Yes" : "No");
}
int main()
{
    // 文件系统中若存在myfifo这个文件则删除
    unlink("myfifo");
    // 创建有名管道
    mkfifo("myfifo", 0664);
    int fd = open("myfifo", O_RDONLY | O_NONBLOCK);

    // 创建事件处理框架
    struct event_base* base = event_base_new();
    // 创建事件
    struct event* ev = event_new(base, fd, EV_READ | EV_PERSIST, read_fifo, NULL);
    // 添加事件
    event_add(ev, NULL);
    // 循环
    event_base_dispatch(base);

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

    return 0;
}
  1. 编译运行
g++ read_fifo.cpp -levent_core -o read
g++ write_fifo.cpp -levent_core -o write
./read
./write
7.示例:定时器事件
  1. 代码示例如下:每隔五秒打印Hello,World
#include <stdio.h>
#include <event2/event.h>

void timeout_cb(evutil_socket_t fd, short event, void* arg) {
    printf("Hello,World\n");

}
int main() {
    // 创建事件处理框架
    struct event_base* main_base = event_base_new();  

    // 创建定时器事件 
    struct event* timeval;
    timeval = event_new(main_base, -1, EV_PERSIST, timeout_cb, nullptr);
    struct timeval time = {5, 0};
    evtimer_add(timeval, &time);

    event_base_dispatch(main_base);


    event_free(timeval);
    event_base_free(main_base);

    return 0;
}

3.事件循环

1.事件处理方式

一旦有了一个已经注册了某些事件的 event_base, 就需要让 libevent 等待事件并且通知事件的发生。

事件只会被触发一次
事件没有被触发, 阻塞等待
#define EVLOOP_ONCE                  0x01
非阻塞方式去做事件检测
#define EVLOOP_NONBLOCK              0x02
没有事件的时候, 也不退出轮询检测    
#define EVLOOP_NO_EXIT_ON_EMPTY      0x04
2.相关函数
  1. event_base_loop函数
int event_base_loop(struct event_base *base, int flags);
函数参数:
    flags:可以设置一个或者多个标志改变这个函数的行为,具体标志在事件处理方式中已经讲述
函数返回值:
    正常退出返回0, 失败返回-1
  1. event_base_dispatch函数:等同于没有设置标志的event_base_loop(),这个函数将一直运行,直到没有已经注册的事件了,或者调用了event_base_loopbreak()或者event_base_loopexit()为止。
int event_base_dispatch(struct event_base* base);
  1. event_base_loopexit函数:如果 event_base 当前正在执行激活事件的回调,它将在执行完所有激活事件的回调后退出
int event_base_loopexit(
    struct event_base *base,
    const struct timeval *tv
);
返回值:
    成功时返回0,失败返回-1
  1. 让event_base立即退出循环
int event_base_loopbreak(struct event_base *base);
返回值:
    成功时返回0,失败返回-1

4.数据缓冲(bufferevent)

1.bufferevent的理解
  1. bufferevent是libevent为IO缓冲区操作提供的一种通用机制
    bufferevent 由一个底层的传输端口(如套接字 ),一个读取缓冲区和一个写入缓冲区组成。
  2. 与通常的事件在底层传输端口已经就绪,可以读取或者写入的时候执行回调不同的是,bufferevent在读取或者写入了足够量的数据之后调用用户提供的回调
    image.png
2.回调(缓冲区对应的操作)
  1. 每个 bufferevent 有两个数据相关的回调
    1. 一个读取回调
      从底层传输端口读取了任意量的数据之后会调用读取回调(默认)
    2. 一个写入回调
      输出缓冲区中足够量的数据被清空到底层传输端口后写入回调会被调用(默认)
  2. 缓冲区数据存储方式:缓冲区内部数据存储使用队列
  3. event 和 bufferevent:
event --> 没有缓冲区
bufferevent --> 有缓冲区
3.bufferevent的水位
  1. 读取低水位
  2. 读取高水位
  3. 写入低水位
  4. 写入高水位
4.bufferevent的使用
  1. 创建基于套接字的bufferevent: 可以使用 bufferevent_socket_new()创建基于套接字的 bufferevent
struct bufferevent *bufferevent_socket_new(
    struct event_base *base,
    evutil_socket_t fd,
    enum bufferevent_options options
);
参数:
    fd:表示套接字的文件描述符
    options: 
        BEV_OPT_CLOSE_ON_FREE 
    释放 bufferevent 时关闭底层传输端口。这将关闭底层套接字,释放底层 bufferevent 等
    BEV_OPT_THREADSAFE:为bufferevent分配锁,这样可以在多线程环境下使用bufferevent
    BEV_OPT_DEFER_CALLBACKS:延迟回调
    BEV_OPT_UNLOCK_CALLBACKS:在执行回调时不加锁
函数返回值:
    成功时函数返回一个 bufferevent,失败则返回 NULL。
  1. 在bufferevent上启动连接
int bufferevent_socket_connect(
    struct bufferevent *bev,
    struct sockaddr *address, //server ip和port 
    int addrlen
); 
1)address 和 addrlen 参数跟标准调用connect()的参数相同。如果还没有为bufferevent 设置套接字,调用函数将为其分配一个新的流套接字,并且设置为非阻塞的
2)如果已经为 bufferevent 设置套接字,调用bufferevent_socket_connect() 将告知libevent 套接字还未连接,直到连接成功之前不应该对其进行读取或者写入操作。
3)连接完成之前可以向输出缓冲区添加数据。
  1. 释放bufferevent
void bufferevent_free(struct bufferevent *bev); 
  1. bufferevent读写缓冲区回调操作
typedef void (*bufferevent_data_cb)(
    struct bufferevent *bev, 
    void *ctx
);
typedef void (*bufferevent_event_cb)(
    struct bufferevent *bev,
    short events, 
    void *ctx
);
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_read()
    bufferevent_data_cb writecb, //可以是NULL
    bufferevent_event_cb eventcb, //可以是NULL
    void *cbarg
);
  1. 禁用、启用缓冲区:默认情况下,写缓冲区是可用的,读缓冲区是关闭的。
禁用之后, 对应的回调就不会被调用了
void bufferevent_enable(
    struct bufferevent *bufev, 
    short events
); 
void bufferevent_disable(
    struct bufferevent *bufev, 
    short events
); 
short bufferevent_get_enabled(
    struct bufferevent *bufev
); 
可以启用或者禁用 bufferevent 上的 EV_READ、EV_WRITE 或者 EV_READ | EV_WRITE 事件。没有启用读取或者写入事件时, bufferevent 将不会试图进行数据读取或者写入。
  1. 操作bufferevent中的数据
1. 向bufferevent的输出缓冲区添加数据
int bufferevent_write(
    struct bufferevent *bufev,
    const void *data, 
    size_t size
);
int bufferevent_write_buffer(struct bufferevent* bufev, struct evbuffer* buf);
参数:evbuffer实现了从头部移除数据、尾部添加数据而优化的字节队列

2. 从bufferevent的输入缓冲区移除数据
size_t bufferevent_read(
    struct bufferevent *bufev, 
    void *data, 
    size_t size
);

int bufferevent_read_buffer(struct bufferevent *bufev, struct evbuffer *buf);

5.连接监听器(evconnlistener)

evconnlistener机制提供了监听和接收TCP连接的方法

1、创建和释放连接监听器
  1. 创建监听器
typedef void (*evconnlistener_cb)(
    struct evconnlistener *listener,   
    evutil_socket_t sock,  //用于通信的文件描述符
    struct sockaddr *addr, //客户端的IP和端口信息
    int len, 
    void *ptr              //外部传进来的数据
); 

struct evconnlistener * evconnlistener_new(
    struct event_base *base,
    evconnlistener_cb cb, 
    void *ptr, 
    unsigned flags, 
    int backlog,
    evutil_socket_t fd
);
参数flags:
    参考手册 - page99-100 [可识别的标志]
    LEV_OPT_CLOSE_ON_FREE:表示释放监听连接器会关闭底层套接字
    LEV_OPT_REUSEABLE:让libevent标记套接字是可重用的,这样一旦关闭,
    可以立即打开其他套接字在相同端口进行监听。
    
struct evconnlistener *evconnlistener_new_bind(
    struct event_base *base,
    evconnlistener_cb cb,   // 接受连接之后, 用户要做的操作
    void *ptr,              // 给回调传参
    unsigned flags, 
    int backlog,           //-1: 使用默认的最大值
    const struct sockaddr *sa, //服务器的IP和端口信息
    int socklen
);

两个 evconnlistener_new*()函数都分配和返回一个新的连接监听器对象。连接监听器使 用 event_base 来得知什么时候在给定的监听套接字上有新的 TCP 连接。新连接到达时,监听 器调用你给出的回调函数。

evconnlistener_new_bind 函数内部完成的是下面5步:

a. socket - server
b. 创建监听socket
c. 绑定
d. 监听
f. 等待并接收连接
  1. 释放监听器
void evconnlistener_free(struct evconnlistener *lev); 
2、启动和禁用监听器

这两个函数暂时禁止或者重新允许监听新连接。

int evconnlistener_disable(struct evconnlistener *lev);
int evconnlistener_enable(struct evconnlistener *lev);
3.调整 evconnlistener 的回调函数
void evconnlistener_set_cb(
    struct evconnlistener *lev,
    evconnlistener_cb cb, 
    void *arg
); 
4.示例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <event2/event.h>
#include <event2/listener.h>
#include <event2/bufferevent.h>

#define SEVER_PORT 8888

// 读缓冲区回调
void read_cb(struct bufferevent *bev, void *arg)
{
    char buf[1024] = {0};   
    bufferevent_read(bev, buf, sizeof(buf));
    const char* p = "我已经收到了你发送的数据!";
    printf("client say: %s\n", buf);

    // 发数据给客户端
    bufferevent_write(bev, p, strlen(p)+1);
    printf("====== send buf: %s\n", p);
}

// 写缓冲区回调
void write_cb(struct bufferevent *bev, void *arg)
{
    printf("我是写缓冲区的回调函数...\n"); 
}

// 事件
void event_cb(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");
    }
    
    bufferevent_free(bev);    
    printf("buffevent 资源已经被释放...\n"); 
}



void cb_listener(
        struct evconnlistener *listener, 
        evutil_socket_t fd, 
        struct sockaddr *addr, 
        int len, void *ptr)
{
    printf("connect new client\n"); 
    struct event_base* base = (struct event_base*)ptr;
    // 添加新事件
    struct bufferevent *bev;
    bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);

    // 给bufferevent缓冲区设置回调
    bufferevent_setcb(bev, read_cb, write_cb, event_cb, NULL);
    bufferevent_enable(bev, EV_READ);  //默认写enable,读disenable
}


int main(int argc, const char* argv[])
{

    // 初始化服务端监听套接字地址信息 
    struct sockaddr_in serv;
    memset(&serv, 0, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_port = htons(SEVER_PORT);
    serv.sin_addr.s_addr = htonl(INADDR_ANY);

    struct event_base* base = NULL;
    // 创建事件处理框架
    base = event_base_new();

    // 将套接字绑定至IP和端口
    // 接收连接请求
    struct evconnlistener* listener = NULL;
    //第二个base传给了cb_listener的ptr
    listener = evconnlistener_new_bind(base, cb_listener, base, 
                                LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 
                                36, (struct sockaddr*)&serv, sizeof(serv));

    event_base_dispatch(base);

    // 释放资源
    evconnlistener_free(listener);
    event_base_free(base);

    return 0;
}