信号驱动IO之libevent的使用

5、libevent方法

      libevent是一个事件触发的网络库,适用于windows、linux、bsd等多种平台,内部使用select、epoll、kqueue等系统调用管理事件机制。著名分布式缓存软件memcached也是libevent based,而且libevent在使用上可以做到跨平台,而且根据libevent官方网站上公布的数据统计,似乎也有着非凡的性能。

       libevent 库实际上没有更换 select()poll() 或其他机制的基础。而是使用对于每个平台最高效的高性能解决方案在实现外加上一个包装器。

为了实际处理每个请求,libevent 库提供一种事件机制,它作为底层网络后端的包装器。事件系统让为连接添加处理函数变得非常简便,同时降低了底层 I/O 复杂性。这是 libevent 系统的核心。

        libevent 库的其他组件提供其他功能,包括缓冲的事件系统(用于缓冲发送到客户端/从客户端接收的数据)以及 HTTP、DNS 和 RPC 系统的核心实现。

       

1、libevent有下面一些特点和优势:

    1)事件驱动,高性能;
    2)轻量级,专注于网络; 
    3)  跨平台,支持 Windows、Linux、Mac Os等; 
    4)  支持多种 I/O多路复用技术, epoll、poll、dev/poll、select 和kqueue 等; 
    5)  支持 I/O,定时器和信号等事件


2、libevent部分组成:

     1)event 及 event_base事件管理包括各种IO(socket)、定时器、信号等事件,也是libevent应用最广的模块;

     2 ) evbuffer  event 及 event_base 缓存管理是指evbuffer功能;提供了高效的读写方法

     3)  evdns  DNS是libevent提供的一个异步DNS查询功能;

     4)  evhttp HTTP是libevent的一个轻量级http实现,包括服务器和客户端

     libevent也支持ssl,这对于有安全需求的网络程序非常的重要,但是其支持不是很完善,比如http server的实现就不支持ssl。

3、事件处理框架

        libevent是事件驱动的库,所谓事件驱动,简单地说就是你点什么按钮(即产生什么事件),电脑执行什么操作(即调用什么函数)。

        


      Libevent框架本质上是一个典型的Reactor模式,所以只需要弄懂Reactor模型,libevent就八九不离十了。

      Reactor模式,是一种事件驱动机制。应用程序需要提供相应的接口并注册到Reactor上,如果相应的事件发生,Reactor将主动调用应用程序注册的接口,这些接口又称为回调函数

      在Libevent中也是一样,向Libevent框架注册相应的事件和回调函数;当这些事件发生时,Libevent会调用这些回调函数处理相应的事件(I/O读写、定时和信号)。

      

使用Reactor模型,必备的几个组件:事件源、Reactor框架、多路复用机制和事件处理程序,先来看看Reactor模型的整体框架,接下来再对每个组件做逐一说明。

1) 事件源
      
Linux 上是文件描述符, Windows 上就是 Socket 或者 Handle 了,这里统一称为 句柄集 ;程序在指定的句柄上注册关心的事件,比如 I/O 事件。

1) 2 event demultiplexer——事件多路分发机制
      
由操作系统提供的I/O多路复用机制,比如selectepoll。程序首先将其关心的句柄(事件源)及其事件注册到event demultiplexer上;当有事件到达时,event demultiplexer会发出通知在已经注册的句柄集中,一个或多个句柄的事件已经就绪;程序收到通知后,就可以在非阻塞的情况下对事件进行处理了。

对应到libevent中,依然是selectpollepoll,但是libevent使用结构体eventop进行了封装,以统一的接口来支持这些I/O多路复用机制,达到了对外隐藏底层系统机制的目的。

3
 Reactor——反应器
    Reactor
,是事件管理的接口,内部使用event demultiplexer注册、注销事件;并运行事件循环,当有事件进入就绪状态时,调用注册事件的回调函数处理事件。
对应到libevent中,就是event_base结构体。

 

4) Event Handler——事件处理程序
    事件处理程序提供了一组接口,每个接口对应了一种类型的事件,供Reactor在相应的事件发生时调用,执行相应的事件处理。通常它会绑定一个有效的句柄。

对应到libevent中,就是event结构体。

       

    结合Reactor框架,我们来理一下libevent的事件处理流程,请看下图:

     


event_init() 初始化:
  首先要隆重介绍event_base对象:

  1. struct event_base {
  2. const struct eventop *evsel;
  3. void *evbase;
  4. int event_count; /* counts number of total events */
  5. int event_count_active; /* counts number of active events */
  6. int event_gotterm; /* Set to terminate loop */
  7. /* active event management */
  8. struct event_list **activequeues;
  9. int nactivequeues;
  10. struct event_list eventqueue;
  11. struct timeval event_tv;
  12. RB_HEAD(event_tree, event) timetree;
  13. };

   event_base对象整合了事件处理的一些全局变量,  角色是event对象的"总管家", 他包括了:

     事件引擎函数对象(evsel, evbase), 

    当前入列事件列表(event_count, event_count_active, eventqueue),

   全局终止信号(event_gotterm), 

    活跃事件列表(avtivequeues), 

    事件队列树(timetree)...

     初始化时创建event_base对象, 选择 当前OS支持的事件引擎(epoll, poll, select...)并初始化,    创建全局信号队列(signalqueue), 活跃队列的内存分配( 根据设置的priority个数,默认为1).

 event_set

 event_set来设置event对象,包括所有者event_base对象, fd, 事件(EV_READ| EV_WRITE|EV_PERSIST), 回掉函数和参数,事件优先级是当前event_base的中间级别(current_base->nactivequeues/2)

设置监视事件后,事件处理函数可以只被调用一次或总被调用。

    只调用一次:事件处理函数被调用后,即从事件队列中删除,需要在事件处理函数中再次加入事件,才能在下次事件发生时被调用;

     总被调用:设置为EV_PERSIST,只加入一次,处理函数总被调用,除非采用event_remove显式地删除。




event_add() 事件添加
   int event_add(struct event *ev, struct timeval *tv)
   这个接口有两个参数, 第一个是要添加的事件, 第二个参数作为事件的超时值(timer). 如果该值非NULL, 在添加本事件的同时添加超时事件(EV_TIMEOUT)到时间队列树(timetree), 根据事件类型处理如下:   
   EV_READ  =>  EVLIST_INSERTED  => eventqueue
   EV_WRITE  =>  EVLIST_INSERTED  => eventqueue
   EV_TIMEOUT => EVLIST_TIMEOUT => timetree
  EV_SIGNAL  => EVLIST_SIGNAL => signalqueue


event_base_loop() 事件处理主循环 
   这里是事件的主循环,只要flags不是设置为EVLOOP_NONBLOCK, 该函数就会一直循环监听事件/处理事件.
   每次循环过程中, 都会处理当前触发(活跃)事件:
   (a). 检测当前是否有信号处理(gotterm, gotsig), 这些都是全局参数,不适合多线程
   (b). 时间更新,找到离当前最近的时间事件, 得到相对超时事件tv
   (c). 调用事件引擎的dispatch wait事件触发, 超时值为tv, 触发事件添加到activequeues
   (d). 处理活跃事件, 调用caller的callbacks (event_process_acitve)


典型的libevent的应用大致整体流程:


          创建 libevent 服务器的基本方法是, 注册当发生某一操作(比如接受来自客户端的连接)时应该执行的函数,然后调用主事件循环event_dispatch()。执行过程的控制现在由 libevent 系统处理。注册事件和将调用的函数之后,事件系统开始自治;在应用程序运行时,可以在事件队列中添加(注册)或删除(取消注册)事件。事件注册非常方便,可以通过它添加新事件以处理新打开的连接,从而构建灵活的网络处理系统

    (环境设置)-> (创建event_base) -> (注册event,将此event加入到event_base中) -> (设置event各种属性,事件等) ->(将event加入事件列表 addevent) ->(开始事件监视循环、分发dispatch)。





例子:

        例如,可以打开一个监听套接字,然后注册一个回调函数,每当需要调用 accept() 函数以打开新连接时调用这个回调函数,这样就创建了一个网络服务器。例1如下所示的代码片段说明基本过程:

 例1:打开监听套接字,注册一个回调函数(每当需要调用 accept() 函数以打开新连接时调用它),由此创建网络服务器:

  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <iostream>
  4. #include <sys/socket.h>
  5. #include <netinet/in.h>
  6. #include <arpa/inet.h>
  7. #include <netdb.h>
  8. #include <event.h>
  9. using namespace std;
  10. // 事件base
  11. struct event_base* base;
  12. // 读事件回调函数
  13. void onRead(int iCliFd, short iEvent, void *arg)
  14. {
  15. int iLen;
  16. char buf[ 1500];
  17. iLen = recv(iCliFd, buf, 1500, 0);
  18. if (iLen <= 0) {
  19. cout << "Client Close" << endl;
  20. // 连接结束(=0)或连接错误(<0),将事件删除并释放内存空间
  21. struct event *pEvRead = (struct event*)arg;
  22. event_del(pEvRead);
  23. delete pEvRead;
  24. close(iCliFd);
  25. return;
  26. }
  27. buf[iLen] = 0;
  28. cout << "Client Info:" << buf << endl;
  29. }
  30. // 连接请求事件回调函数
  31. void onAccept(int iSvrFd, short iEvent, void *arg)
  32. {
  33. int iCliFd;
  34. struct sockaddr_in sCliAddr;
  35. socklen_t iSinSize = sizeof(sCliAddr);
  36. iCliFd = accept(iSvrFd, (struct sockaddr*)&sCliAddr, &iSinSize);
  37. // 连接注册为新事件 (EV_PERSIST为事件触发后不默认删除)
  38. struct event *pEvRead = new event;
  39. event_set(pEvRead, iCliFd, EV_READ|EV_PERSIST, onRead, pEvRead);
  40. event_base_set(base, pEvRead);
  41. event_add(pEvRead, NULL);
  42. }
  43. int main()
  44. {
  45. int iSvrFd;
  46. struct sockaddr_in sSvrAddr;
  47. memset(&sSvrAddr, 0, sizeof(sSvrAddr));
  48. sSvrAddr.sin_family = AF_INET;
  49. sSvrAddr.sin_addr.s_addr = inet_addr( "127.0.0.1");
  50. sSvrAddr.sin_port = htons( 8888);
  51. // 创建tcpSocket(iSvrFd),监听本机8888端口
  52. iSvrFd = socket(AF_INET, SOCK_STREAM, 0);
  53. bind(iSvrFd, (struct sockaddr*)&sSvrAddr, sizeof(sSvrAddr));
  54. listen(iSvrFd, 10);
  55. // 初始化base
  56. base = event_base_new();
  57. struct event evListen;
  58. // 设置事件
  59. event_set(&evListen, iSvrFd, EV_READ|EV_PERSIST, onAccept, NULL);
  60. // 设置为base事件
  61. event_base_set(base, &evListen);
  62. // 添加事件
  63. event_add(&evListen, NULL);
  64. // 事件循环
  65. event_base_dispatch(base);
  66. return 0;
  67. }



event_set() 函数创建新的事件结构,

event_add() 在事件队列机制中添加事件。

然后,event_dispatch() 启动事件队列系统,开始监听(并接受)请求。


使用其他语言的实现

尽管 C 语言很适合许多系统应用程序,但是在现代环境中不经常使用 C 语言,脚本语言更灵活、更实用。幸运的是,Perl 和 PHP 等大多数脚本语言是用 C 编写的,所以可以通过扩展模块使用 libevent 等 C 库。


4、libev库

       官方文档:http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod

       与 libevent 一样,libev 系统也是基于事件循环的系统,它在 poll()select() 等机制的本机实现的基础上提供基于事件的循环。

       libev是libevent之后的一个事件驱动的编程框架,其接口和libevent基本类似。据官方介绍,其性能比libevent还要高,bug比libevent还少。

       libev API 比较原始,没有 HTTP 包装器,但是 libev 支持在实现中内置更多事件类型。例如,一种 evstat 实现可以监视多个文件的属性变动,可以在 例4 所示的 HTTP 文件解决方案中使用它。

       但是,libevent 和 libev 的基本过程是相同的。创建所需的网络监听套接字,注册在执行期间要调用的事件,然后启动主事件循环,让 libev 处理过程的其余部分。

      Libev是一个event loop:向libev注册感兴趣的events,比如Socket可读事件,libev会对所注册的事件的源进行管理,并在事件发生时触发相应的程序。

    事件驱动框架:

    定义一个监控器、书写触发动作逻辑、初始化监控器、设置监控器触发条件、将监控器加入大事件驱动器的循环中即可。

     libev的事件驱动过程可以想象成如下的伪代码:

do_some_init()
is_run = True
while is_run:
    t = caculate_loop_time()
    deal_loop(t)
    deal_with_pending_event()
do_some_clear()

首先做一些初始化操作,然后进入到循环中,该循环通过一个状态位来控制是否执行。

在循环中,计算出下一次轮询的时间,这里轮询的实现就采用了系统提供的epoll、kqueue等机制。

再轮询结束后检查有哪些监控器的被触发了,依次执行触发动作。

      Libev 除了提供了基本的三大类事件(IO事件、定时器事件、信号事件)外还提供了周期事件、子进程事件、文件状态改变事件等多个事件。

     libev所实现的功能就是一个强大的reactor,可能notify事件主要包括下面这些:

  • ev_io // IO可读可写
  • ev_stat // 文件属性变化
  • ev_async // 激活线程
  • ev_signal // 信号处理
  • ev_timer // 定时器
  • ev_periodic // 周期任务
  • ev_child // 子进程状态变化
  • ev_fork // 开辟进程
  • ev_cleanup // event loop退出触发事件
  • ev_idle // 每次event loop空闲触发事件
  • ev_embed // TODO(zhangyan04):I have no idea.
  • ev_prepare // 每次event loop之前事件

  • ev_check // 每次event loop之后事件

libev 同样需要循环探测事件是否产生。Libev 的循环体用 ev_loop 结构来表达,并用 ev_loop( ) 来启动。

	 void ev_loop( ev_loop* loop, int flags ) 

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_* ) 

由此,我们可以容易得出如下的“一问一答”的服务器模型。由于没有考虑服务器端主动终止连接机制,所以各个连接可以维持任意时间,客户端可以自由选择退出时机。

IO事件、定时器事件、信号事件:

  1. #include<ev.h>
  2. #include <stdio.h>
  3. #include <signal.h>
  4. #include <sys/unistd.h>
  5. ev_io io_w;
  6. ev_timer timer_w;
  7. ev_signal signal_w;
  8. void io_action(struct ev_loop *main_loop,ev_io *io_w,int e)
  9. {
  10. int rst;
  11. char buf[ 1024] = { ''};
  12. puts("in io cb\n");
  13. read(STDIN_FILENO,buf,sizeof(buf));
  14. buf[1023] = ' ';
  15. printf("Read in a string %s \n",buf);
  16. ev_io_stop(main_loop,io_w);
  17. }
  18. void timer_action(struct ev_loop *main_loop,ev_timer *timer_w,int e)
  19. {
  20. puts("in tiemr cb \n");
  21. ev_timer_stop(main_loop,io_w);
  22. }
  23. void signal_action(struct ev_loop *main_loop,ev_signal signal_w,int e)
  24. {
  25. puts("in signal cb \n");
  26. ev_signal_stop(main_loop,io_w);
  27. ev_break(main_loop,EVBREAK_ALL);
  28. }
  29. int main(int argc ,char *argv[])
  30. {
  31. struct ev_loop *main_loop = ev_default_loop(0);
  32. ev_init(&io_w,io_action);
  33. ev_io_set(&io_w,STDIN_FILENO,EV_READ);
  34. ev_init(&timer_w,timer_action);
  35. ev_timer_set(&timer_w,2,0);
  36. ev_init(&signal_w,signal_action);
  37. ev_signal_set(&signal_w,SIGINT);
  38. ev_io_start(main_loop,&io_w);
  39. ev_timer_start(main_loop,&timer_w);
  40. ev_signal_start(main_loop,&signal_w);
  41. ev_run(main_loop,0);
  42. return 0;
  43. }

这里使用了3种事件监控器,分别监控IO事件、定时器事件以及信号事件。因此定义了3个监控器(watcher),以及触发监控器时要执行动作的回调函数。Libev定义了多种监控器,命名方式为ev_xxx 这里xxx代表监控器类型,其实现是一个结构体,

typedef struct ev_io
{
  ....
} ev_io;

通过宏定义可以简写为 ev_xxx。回调函数的类型为 void cb_name(struct ev_loop *main_loop,ev_xxx *io_w,int event) 。

在main中,首先定义了一个事件驱动器的结构 struct ev_loop *main_loop 这里调用 ev_default_loop(0) 生成一个预制的全局驱动器。这里可以参考Manual中的选择。然后依次初始化各个监控器以及设置监控器的触发条件。

初始化监控器的过程是将相应的回调函数即触发时的动作注册到监控器上。

设置触发条件则是该条件产生时才去执行注册到监控器上的动作。对于IO事件,一般是设置特定fd上的的可读或可写事件,定时器则是多久后触发。这里定时器的触发条件中还有第三参数,表示第一次触发后,是否循环,若为0则吧循环,否则按该值循环。信号触发器则是设置触发的信号。

在初始化并设置好触发条件后,先调用ev_xxx_start 将监控器注册到事件驱动器上。接着调用 ev_run 开始事件驱动器。


 使用libev库的服务器模型

上述模型可以接受任意多个连接,且为各个连接提供完全独立的问答服务。借助 libev 提供的事件循环 / 事件驱动接口,上述模型有机会具备其他模型不能提供的高效率、低资源占用、稳定性好和编写简单等特点。

由于传统的 web 服务器,ftp 服务器及其他网络应用程序都具有“一问一答”的通讯逻辑,所以上述使用 libev 库的“一问一答”模型对构建类似的服务器程序具有参考价值;另外,对于需要实现远程监视或远程遥控的应用程序,上述模型同样提供了一个可行的实现方案。

php-了libev扩展socket:
  1. <?php
  2. /* 使用异步io访问socket Use some async I/O to access a socket */
  3. // `sockets' extension still logs warnings
  4. // for EINPROGRESS, EAGAIN/EWOULDBLOCK etc.
  5. error_reporting(E_ERROR);
  6. $e_nonblocking = array ( /*EAGAIN or EWOULDBLOCK*/ 11, /*EINPROGRESS*/ 115);
  7. // Get the port for the WWW service
  8. $service_port = getservbyname( 'www', 'tcp');
  9. // Get the IP address for the target host
  10. $address = gethostbyname( 'google.co.uk');
  11. // Create a TCP/IP socket
  12. $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
  13. if ($socket === FALSE) {
  14. echo \ "socket_create() failed: reason: \"
  15. .socket_strerror(socket_last_error()) . \"n\";
  16. }
  17. // Set O_NONBLOCK flag
  18. socket_set_nonblock($socket);
  19. // Abort on timeout
  20. $timeout_watcher = new EvTimer(10.0, 0., function () use ($socket) {
  21. socket_close($socket);
  22. Ev::stop(Ev::BREAK_ALL);
  23. });
  24. // Make HEAD request when the socket is writable
  25. $write_watcher = new EvIo($socket, Ev::WRITE, function ($w)
  26. use ($socket, $timeout_watcher, $e_nonblocking) {
  27. // Stop timeout watcher
  28. $timeout_watcher->stop();
  29. // Stop write watcher
  30. $w->stop();
  31. $in = \"HEAD / HTTP/1.1rn\";
  32. $in .= \"Host: google.co.ukrn\";
  33. $in .= \"Connection: Closernrn\";
  34. if (!socket_write($socket, $in, strlen($in))) {
  35. trigger_error(\"Failed writing $in to socket\", E_USER_ERROR);
  36. }
  37. $read_watcher = new EvIo($socket, Ev::READ, function ($w, $re)
  38. use ($socket, $e_nonblocking) {
  39. // Socket is readable. recv() 20 bytes using non-blocking mode
  40. $ret = socket_recv($socket, $out, 20, MSG_DONTWAIT);
  41. if ($ret) {
  42. echo $out;
  43. } elseif ($ret === 0) {
  44. // All read
  45. $w->stop();
  46. socket_close($socket);
  47. return;
  48. }
  49. // Caught EINPROGRESS, EAGAIN, or EWOULDBLOCK
  50. if (in_array(socket_last_error(), $e_nonblocking)) {
  51. return;
  52. }
  53. $w->stop();
  54. socket_close($socket);
  55. });
  56. Ev::run();
  57. });
  58. $result = socket_connect($socket, $address, $service_port);
  59. Ev::run();
  60. ?>


结束语

libevent 和 libev 都提供灵活且强大的环境,支持为处理服务器端或客户端请求实现高性能网络(和其他 I/O)接口。目标是以高效(CPU/RAM 使用量低)的方式支持数千甚至数万个连接。在本文中,您看到了一些示例,包括 libevent 中内置的 HTTP 服务,可以使用这些技术支持基于 IBM Cloud、EC2 或 AJAX 的 web 应用程序。


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值