libev事件驱动网络库从安装到使用

上次说到的select/poll模型(传送门)的特征在于每一个执行周期都会探测一次或一组事件,一个特定的事件会触发某个特定的响应。我们可以将这种模型归类为“事件驱动模型”。
相比其他模型,使用 select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。

但这个模型依旧有着很多问题。

首先,select() 接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select() 接口本身需要消耗大量时间去轮询各个句柄。很多操作系统提供了更为高效的接口,如 linux 提供了 epoll,BSD 提供了 kqueue,Solaris 提供了 /dev/poll …。如果需要实现更高效的服务器程序,类似 epoll 这样的接口更被推荐。遗憾的是不同的操作系统特供的 epoll 接口有很大差异,所以使用类似于 epoll 的接口实现具有较好跨平台能力的服务器会比较困难

其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的,在很大程度上降低了事件探测的及时性。

幸运的是,有很多高效的事件驱动库可以屏蔽上述的困难,常见的事件驱动库有 libevent 库,还有作为 libevent 替代者的 libev 库。这些库会根据操作系统的特点选择最合适的事件探测接口,并且加入了信号 (signal) 等技术以支持异步响应,这使得这些库成为构建事件驱动模型的不二选择。下面将介绍如何使用 libev 库替换 select 或 epoll 接口,实现高效稳定的服务器模型。
Libev是一个基于Reactor模式的事件库,效率较高、代码精简(4.15版本8000多行,c语言编写),是一个值得学习的轻量级事件驱动库。
一、安装
首先下载官方源码http://dist.schmorp.de/libev/libev-4.15.tar.gz
也可以从github上下载,一样的,地址https://github.com/daidaotian/libev
安装过程如下:

wget http://dist.schmorp.de/libev/libev-4.15.tar.gz
tar -zxf libev-4.15.tar.gz
cd libev-4.15
./configure
make
make install

下面是安装信息:

Libraries have been installed in:
   /usr/local/lib

If you ever happen to want to link against installed libraries
in a given directory, LIBDIR, you must either use libtool, and
specify the full pathname of the library, or use the `-LLIBDIR'
flag during linking and do at least one of the following:
   - add LIBDIR to the `LD_LIBRARY_PATH' environment variable
     during execution
   - add LIBDIR to the `LD_RUN_PATH' environment variable
     during linking
   - use the `-Wl,-rpath -Wl,LIBDIR' linker flag
   - have your system administrator add LIBDIR to `/etc/ld.so.conf'

See any operating system documentation about shared libraries for
more information, such as the ld(1) and ld.so(8) manual pages.
----------------------------------------------------------------------
 /bin/mkdir -p '/usr/local/include'
 /usr/bin/install -c -m 644 ev.h ev++.h event.h '/usr/local/include'
 /bin/mkdir -p '/usr/local/share/man/man3'
 /usr/bin/install -c -m 644 ev.3 '/usr/local/share/man/man3'

下一步按要求设置环境变量:

//这种只对当前shell本次有效,想要永久生效去在/etc/profile文件中添加变量
export LIBDIR=/usr/local/lib
export LD_LIBRARY_PATH=/usr/local/lib
export LD_RUN_PATH=/usr/local/lib

二、使用
libev 跟select一样,同样需要循环探测事件是否产生。Libev 的循环体用 ev_loop 结构来表达,并用 ev_loop( ) 来启动。Libev通过一个struct ev_loop结构表示一个事件驱动的框架。

 void ev_loop( ev_loop* loop, int flags ) 

Libev 支持多种事件类型,在ev_loop框架里面通过ev_xxx结构,ev_init、ev_xxx_set、ev_xxx_start接口向这个事件驱动的框架里面注册事件监控器,当相应的事件监控器的事件出现时,便会触发该事件监控器的处理逻辑,去处理该事件。处理完之后,这些监控器进入到下一轮的监控中。符合一个标准的事件驱动状态的模型。
  Libev 除了提供了基本的三大类事件(IO事件、定时器事件、信号事件)外还提供了周期事件、子进程事件、文件状态改变事件等多个事件。
例如一个 IO 事件:
用 ev_io 来表征,并用 ev_io_init() 函数来初始化:

 void ev_io_init(ev_io *io, callback, int fd, int events) 

初始化内容包括回调函数 callback,被探测的句柄 fd 和需要探测的事件,EV_READ 表“可读事件”,EV_WRITE 表“可写事件”。

现在,用户需要做的仅仅是在合适的时候,将某些 ev_io 从 ev_loop 加入或剔除。一旦加入,下个循环即会检查 ev_io 所指定的事件有否发生;如果该事件被探测到,则 ev_loop 会自动执行 ev_io 的回调函数 callback();如果 ev_io 被注销,则不再检测对应事件。

无论某 ev_loop 启动与否,都可以对其添加或删除一个或多个 ev_io,添加和删除的接口是 ev_io_start() 和 ev_io_stop()。

 void ev_io_start( ev_loop *loop, ev_io* io ) 
 void ev_io_stop( EV_A_* ) 

下面是一个官方给出的例子:

#include <stdio.h>
#include <ev.h> //ev库头文件

//定义一个ev_TYPE 的结构体
ev_io stdin_watcher;//定义一个stdin的观测者
ev_timer timeout_watcher;


//所有的watcher的回调函数都有相似的特点
//当stdin有可读的数据时,将会调用下面这个回调函数
static void stdin_cb(EV_P_ ev_io *w,int revents)
{
    puts("stdin ready");

    //每一次时间都必须用对应的停止函数,手动的停止其watcher
    ev_io_stop(EV_A_ w);
    //这将导致所有嵌套执行的ev_run停止监听
    ev_break(EV_A_ EVBREAK_ALL);
}

//这是一个回调函数,用于定时器回调
static void timeout_cb(EV_P_ ev_timer *w,int revents)
{
    puts("timeout");
    //这将导致最早运行的ev_run停止监听
    ev_break(EV_A_ EVBREAK_ONE);
}

int main(int argc,char **args)
{
    //使用一般默认的事件循环
    struct ev_loop *loop = EV_DEFAULT;

    //初始化一个I/O watcher,然后启动它
    ev_io_init(&stdin_watcher,stdin_cb,0,EV_READ);
    ev_io_start(loop,&stdin_watcher);

    //初始化一个定时器watcher,然后启动它,只有一次,没有重复的5.5秒定时
    ev_timer_init(&timeout_watcher,timeout_cb,5.5,0);
    ev_timer_start(loop,&timeout_watcher);

    //这里等待时间出发
    ev_run(loop,0);//0代表只循环一次

    //撤销监听退出程序
    return 0;
}

libev大致框架

上个例子没什么好讲的,下面看另一个例子:

#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <sys/unistd.h>
#include <ev.h>


void io_action(struct ev_loop *main_loop,ev_io *io_w,int e)
{
    int rst;
    char buf[1024];
    memset(buf,0,sizeof(buf));
    puts("In IO action");
    read(STDIN_FILENO,buf,sizeof(buf));
    buf[1023]='\0';
    printf("String: %s\n",buf);
    ev_io_stop(main_loop,io_w);
}

void timer_action(struct ev_loop *main_loop,ev_timer *time_w,int e)
{
    puts("In Time action");
    ev_timer_stop(main_loop,time_w);
}

void signal_action(struct ev_loop *main_loop,ev_signal *signal_w,int e)
{
    puts("In Signal action");
    ev_signal_stop(main_loop,signal_w);
    ev_break(main_loop,EVBREAK_ALL);
}

int main(int argc,char **argv)
{
    ev_io io_w;
    ev_timer timer_w;
    ev_signal signal_w;
    struct ev_loop *main_loop = ev_default_loop(0);

    ev_init(&io_w,io_action);
    ev_io_set(&io_w,STDIN_FILENO,EV_READ);

    ev_init(&timer_w,timer_action);
    ev_timer_set(&timer_w,2,0);

    ev_init(&signal_w,signal_action);
    ev_signal_set(&signal_w,SIGINT);

    ev_io_start(main_loop,&io_w);
    ev_timer_start(main_loop,&timer_w);
    ev_signal_start(main_loop,&signal_w);

    ev_run(main_loop,0);
    return 0;
}

该程序一直处于监听状态,直到有调用信号然后回调signal_w函数,该函数会调用ev_break函数退出ev_run的调用,如果注释掉第30行的代码,那么程序会在调用三个回调函数后才会结束(外包引用计数为0),否则一直监听着。
具体ev_run和ev_break的参数说明如下:

void ev_run (EV_P_ int flags);
void ev_break (EV_P_ int how);

flags:
0:默认值。一直循环进行处理,直到外部引用计数==0或者是显示退出
EVRUN_NOWAIT:运行一次,poll时候不会等待。如果有pending事件则进行处理,否则立即返回。
EVRUN_ONCE:运行一次,poll时候会等待至少一个event发生,处理完成之后返回。

how:
EVBREAK_ONE:只是退出一次ev_run这个调用。通常来说使用这个就可以了。
EVBREAK_ALL:退出所有的ev_run调用。这种情况存在于ev_run在pending处理时候会递归调用。

创建一个struct ev_loop *结构体,上面我们给出 ev_default_loop(0) 进行创建。使用libev的核心是事件循环,可以用 ev_default_loop 或 ev_loop_new 函数创建循环,或者直接使用 EV_DEFAULT 宏。

创建子进程后,且想要使用事件循环时,需要先在子进程中调用 ev_default_forkev_loop_fork 来重新初始化后端的内核状态,它们分别对应 ev_default_loop 和 ev_loop_new 来使用。

ev_run 启动事件循环。它的第二个参数为0时,将持续运行并处理循环直到没有活动的事件观察器或者调用了 ev_break 。另外两个取值是 EVRUN_NOWAIT 和 EVRUN_ONCE 。

ev_break 跳出事件循环(在全部已发生的事件处理完之后)。第二个参数为 EVBREAK_ONE 或 EVBREAK_ALL 来指定跳出最内层的 ev_run 或者全部嵌套的 ev_run 。

ev_suspend 和 ev_resume 用来暂停和重启事件循环,比如在程序挂起的时候。

创建watcher,主要包括类型、触发条件和回调函数。将它注册到事件循环上,在满足注册的条件时,会触发观察器,调用它的回调函数(callback)。上面的例子中已经包含了IO观察器和计时观察器、信号观察器,此外还有周期观察器、文件状态观察器等等。
初始化和设置观察器使用 ev_init 和 ev_TYPE_set也可以直接使用 ev_TYPE_init
在特定事件循环上启动观察器使用 ev_TYPE_start 。 ev_TYPE_stop 停止观察器,并且会释放内存。
libev中将观察器分为4种状态:初始化、启动/活动、等待、停止。libev中的观察器还支持优先级。

观察器(watcher):

typedef void (*)(struct ev_loop *loop, ev_TYPE *watcher, int revents) callback; // callback都是这种类型
ev_init (ev_TYPE *watcher, callback); // 初始化watcher
ev_TYPE_set (ev_TYPE *watcher, [args]); // 设置watcher
ev_TYPE_init (ev_TYPE *watcher, callback, [args]); // 通常使用这个函数最方便,初始化和设置都在这里
ev_TYPE_start (loop, ev_TYPE *watcher); // 注册watcher
ev_TYPE_stop (loop, ev_TYPE *watcher); // 注销watcher
ev_set_priority (ev_TYPE *watcher, int priority); // 设置优先级
ev_feed_event (loop, ev_TYPE *watcher, int revents); // 这个做跨线程通知非常有用,相当于触发了某个事件。
bool ev_is_active (ev_TYPE *watcher); // watcher是否active.
bool ev_is_pending (ev_TYPE *watcher); // watcher是否pending.
int ev_clear_pending (loop, ev_TYPE *watcher); // 清除watcher pending状态并且返回事件

watcher的4种状态:

  (1) initialiased.调用init函数初始化
  (2) active.调用start进行注册
  (3) pending.已经触发事件但是没有处理
  (4) inactive.调用stop注销。这个状态等同于initialised这个状态。
  
典型watcher有:
ev_io(IO可读可写观察器),ev_signal(信号处理观察器),ev_timer(定时器),
ev_periodic(周期任务处理),ev_child(子进程状态变化观察器),ev_stat(文件属性变化观察器)。
ev_fork(创建的进程时的观察器),ev_async(异步调用观察器),ev_cleanup(event loop退出时触发事件),ev_prepare(每次event loop之前事件),ev_check(每次event loop之后事件),ev_idle(每次event loop空闲触发事件)
下面举几个栗子:
1、ev_io

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <ev.h>


static void stdin_callback(struct ev_loop *loop,ev_io *w,int revents)
{
    char str[1024];
    if(revents & EV_READ)
    {
        //stdin might have data for us
        printf("有数据可读\n");
        scanf("%s",str);
        ev_io_stop(loop,w);
    }
    else if(revents & EV_WRITE)
    {
        //stdout might have data for us
        printf("有数据输出\n");
        //ev_break(loop,EVBREAK_ONE);
    }
    printf("water:%d\n",ev_is_active(w));
}

int main(int argc,char **argv)
{
    struct ev_loop * main_loop = ev_default_loop(0);
    /*这里的ev_default_loop可以使用ev_loop_new动态分配一个,然后使用ev_loop_destroy销毁。
    struct ev_loop * epoller = ev_loop_new(EVBACKEND_EPOLL | EVFLAG_NOENV);
    这里一般是使用EVBACKEND_EPOLL模型,同样的还有EVBACKEND_SELECT EVBACKEND_POLL EVBACKEND_KQUEUE EVBACKEND_DEVPOLL EVBACKEND_PORT 如果默认,那么ev会自动判断系统环境,选择最适合的模型,Linux一般为epoll */
    ev_io stdin_watcher;//定义一个watcher
    ev_init(&stdin_watcher,stdin_callback);//初始化watcher,参数是回调函数
    ev_io_set(&stdin_watcher,STDIN_FILENO,EV_READ|EV_WRITE);//设置watcher,参数为要监听的描述符及事件
    ev_io_start(main_loop,&stdin_watcher);//注册watcher,参数为要注册到的循环

    //ev_run(main_loop,EVRUN_ONCE);

    //void ev_set_io_collect_interval (EV_P_ ev_tstamp interval);//这个是设置轮询的时间
    //typedef double ev_tstamp
    ev_set_io_collect_interval(main_loop,2.);//2秒
    ev_run(main_loop,0);
    //ev_is_active(ev_TYPE * watcher);//用于判断watcher是否为active
    printf("main:%d\n",ev_is_active(&stdin_watcher));

    //initialiased.调用init函数初始化
    //active.调用start进行注册
    //pending.已经触发事件但是没有处理
    //inactive.调用stop注销。这个状态等同于initialised这个状态

    return 0;
}

2.ev_signal

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <ev.h>

static void sigint_callback(struct ev_loop * loop,ev_signal *w,int revents)
{
    if(revents & EV_SIGNAL)//用这个可以判断这次进来的是不是ev_signal 如果一个callback回调函数复用的话,就可以用这个来区分
    {
        printf("signal SIGINT\n");
        ev_break(loop, EVBREAK_ALL);
    }
}

static void sigquit_callback(struct ev_loop * loop,ev_signal *w,int revents)
{
    printf("signal SIGQUIT\n");
    ev_break(loop, EVBREAK_ALL);
}

int main(int argc, char **args)
{
    struct ev_loop * main_loop=ev_default_loop(0);

    ev_signal sigint_watcher;
    ev_signal sigquit_watcher;

    ev_init(&sigint_watcher,sigint_callback);
    ev_signal_set(&sigint_watcher,SIGINT/*Other want to catch*/);//这里多个信号不能用或符号| 连接起来
    ev_signal_start(main_loop,&sigint_watcher);

    ev_init(&sigquit_watcher,sigquit_callback);
    ev_signal_set(&sigquit_watcher,SIGQUIT/*Other want to catch*/);
    ev_signal_start(main_loop,&sigquit_watcher);

    ev_run(main_loop,0);

    return 0;
}

运行程序,输入Ctrl-C(中断)或Ctrl-\(quit)都是可以捕获的。
3、ev_child

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <ev.h>

static void child_callback(struct ev_loop *loop,ev_child *w,int revents)
{
    ev_child_stop(loop,w);
    printf("Process %d exited with status %d\n",w->rpid,w->rstatus);
}

int main(int argc, char **args)
{
    struct ev_loop * main_loop=ev_default_loop(0);
    pid_t pid;

    ev_child child_watcher;

    pid=fork();
    if(pid<0)
    {
        printf("Fork Error\n");
        return -1;
    }
    else if(pid==0)//child
    {
        printf("child doing..\n");
        return 0;
    }
    else //father
    {
        sleep(2);//即使让子进程先执行,最后还是可以捕获到。
        ev_init(&child_watcher,child_callback);
        ev_child_set(&child_watcher,pid,0);
        //ev_child_start(EV_DEFAULT_ &child_watcher);
        ev_child_start(main_loop,&child_watcher);
        ev_run(main_loop,0);
    }

    //waitpid(pid,0,0);
    return 0;
}

主进程通过pid将子进程绑定到了child_callback事件中,当子进程挂掉后,主进程就能捕捉的信号,然后调用child_callback函数。
4、ev_stat

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <ev.h>

static void stat_callback(struct ev_loop *loop,ev_stat *w, int revents)
{
    if(w->attr.st_nlink)
    {
        printf("The file size %ld\n",(long)w->attr.st_size);
    }
    else
    {
        printf("文件不存在\n");
    }
}

int main(int argc, char **args)
{
    struct ev_loop *main_loop=ev_default_loop(0);

    ev_stat stat_watcher;
    ev_init(&stat_watcher,stat_callback);
    ev_stat_set(&stat_watcher,"/home/myuser/hello.txt",0);
    ev_stat_start(main_loop,&stat_watcher);

    ev_run(main_loop,0);
    return 0;
}

如果文件有一点修改,无论是什么属性,都将触发这个回调函数。这个attr文件在这里可以获取到的属性成员。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值