高性能 I/O框架库 libevent 的使用

之前已经把高性能 I/O框架库 libevent 的安装 讲过了,还请详见我的博客:I/O框架库 libevent 的安装

今天就把这个库具体使用一下:(这一部分详见《Linux高性能服务器编程》第12章)

I/O框架库

Linux 服务器程序必须处理的三类事件:I/O事假、信号和定时事件。但是在处理这三类事件的时候需要考虑如下问题:

  • 统一事件源。统一处理这三类问题可以使代码简单易懂,又能避免潜在的逻辑问题。实现统一事件源的一般办法:利用I/O复用系统调用来管理所有事件。
  • 可移植性。不同的操作系统具有不同的 I/O 复用方式,Linux里面的epoll系列 系统调用。
  • 对并发编程的支持。在多进程和多线程情况下,需要考虑各执行实体如何协同处理客户连接、信号和定时器,以避免竞态条件。

而很多优秀的I/O框架库,可以很好的解决上面问题,且让开发人员把精力都放在程序的逻辑上,此外 它们稳定性、性能等个方面都非常出色。我们接下来就详细介绍 轻量级的libevent框架库。

I/O框架库是以库函数的形式,封装了较为底层的系统调用,给应用程序提供的一组更便于使用的接口。 各种I/O框架库的实现原理基本类似:以Reactor模式实现、以Proactor模式实现和同时用Reactor和Proactor两种模式实现。这两种模式差异如下:

  • Reactor模式,即反应器模式,是一种高效的异步I/O模式,特征是 回调,当I/O完成时,回调对应的函数进行处理。这种模式并非是真正的异步,而是运用了异步的思想,当I/O事件触发时,通知应用程序作出I/O处理。模式本身并不调用系统的异步I/O函数。
  • Proactor模式,即前摄器模式,也是一种高效的异步I/O模式,特征也是回调,当I/O事件完成时,回调对应的函数对完成事件作出处理。这种模式是真正意义上的异步,属于系统级的异步,通常要调用系统提供的异步I/O函数进行I/O处理。
  • Reacor模式不调用系统异步I/O函数,是一种仿异步;Proactor是系统层面上的真正的异步,调用系统提供的异步I/O函数。

我们接下来重点解释一下基于Reactor模式的I/O框架库 的几个组件:
它们之间的关系如下图所示:在这里插入图片描述

  1. 句柄(Handle)。I/O框架库要处理的对象,即I/O事件、信号和定时事件,统一称为事件源。 一个事件源通常和一个句柄绑定在一起。 句柄的作用是:当内核检测到就绪事件时,它将通过句柄来通知应用程序这一事件。在Linux环境下,I/O事件对应的句柄为文件描述符,信号事件对应的句柄为信号值。 (说那么干嘛,可以理解为 文件描述符或者信号值)
  2. 事件多路分发器(EventDemultiplexer)。事件的到来是随机的、异步的。 我们无法预知程序何时收到一个客户连接请求,又亦或收到一个暂停信号。所以程序需要循环地等待并处理事件,这就是事件循环。 在事件循环中,等待事件一般使用I/O复用技术来实现。而我们的I/O 框架库一般将系统支持的各种I/O复用系统调用封装成统一的接口,称为事件多路分发器。 事件多路分发器的demultiplex方法是等待事件的核心函数,其内部调用的是select、 poll、 epoll_wait 等函数。(所以说,这个什么事件多路分发器,就是各种I/O复用系统调用的一层封装)。此外,事件多路分发器还需要实现 register_event 和 remove_event 方法,以供调用者向事件多路分发器中添加事件和从事件多路分发器中删除事件。
  3. 事件处理器和具体事件处理器(EventHandle and ConcreteEventHandle)。事件处理器执行事件对应的业务逻辑。它通常包含一个或多个 handle_event 回调函数,这些回调函数在事件循环中被执行。I/O 框架库提供的事件处理器通常是一个接口, 用户需要继承它来实现自己的事件处理器,即具体事件处理器。因此,事件处理器中的回调函数一般被声明为虚函数,以支持用户的扩展。(总之,这个回调函数是我们自己书写,然后交由程序调用) 此外,事件处理器一般还提供一个 get_handle 方法,它返回与该事件处理器关联的句柄。 那么,事件处理器和句柄有什么关系? 当事件多路分发器检测到有事件发生时,它是通过句柄来通知应用程序的。因此,我们必须将事件处理器和句柄绑定,才能在事件发生时获取到正确的事件处理器。
  4. Reactor。Reactor是I/O框架库的核心。它提供的几个主要方法是:
    一、handle_events 该方法执行事件循环。它重复如下过程:等待事件,然后依次处理所有就绪事件对应的事件处理器。
    二、register_handler 该方法调用事件多路分发器的 register_event 方法来往事件多路分发器中注册一个事件。
    三、remove_handler 该方法调用事件多路分发器的 remove_ event 方法来删除事件多路分发器中的一个事件。

I/O框架库的工作时序图如下:
在这里插入图片描述

Libevent的使用

Libevent的简介

作为一款高性能的I/O框架库,Lievent具有以下的特点

  1. 跨平台支持,Lievent支持Linux、UNIX和Windows.
  2. 统一事件源。Libevent对I/O事件、信号和定时事件提供统一的处理
  3. 线程安全。Libevent使用libevent_pthreads库来提高线程安全支持
  4. 基于Reactor模式的研究

Libevent库的主要逻辑如下:
在这里插入图片描述
Libevent支持的事件类型:
在这里插入图片描述

Libevent的实例使用

实例1,源代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <signal.h>
#include <event.h>

//信号处理的回调函数
void sig_fun(int fd,short ev,void *arg)
{
    if(ev & EV_SIGNAL)
    {
        printf("sig=%d\n",fd);
    }
}

//定时器的回调函数
void timeout_cb(int fd,short ev,void *arg)
{
    if(ev & EV_TIMEOUT)
    {
        printf("timeout\n");
    }
}

int main()
{
	//下面的两行都是可以的,第二行是基于多线程下的考量。
    //struct event_base* base=event_init();//创建libevent实例
    struct event_base* base=event_base_new();//创建libevent实例
    assert(base != NULL);
    
    //定义信号事件
    //struct event* sig_ev=evsignal_new(base,SIGINT,sig_fun,NULL);
    struct event* sig_ev=event_new(base,SIGINT,EV_SIGNAL,sig_fun,NULL);
	//上面的两行是等价的,只是下面是底层的调用而已。EV_SIGNAL,没有永久事件
	
    assert(sig_ev != NULL);
    event_add(sig_ev,NULL);

    //定义 定时器事件
    //struct event* time_ev =evtimer_new(base,timeout_cb,NULL);    
    //event_new 参数:base实例,文描(信号用代号,定时器-1),事件类型,回调函数,传给回调函数参数 
    struct event* time_ev=event_new(base,-1,
    						EV_TIMEOUT | EV_PERSIST,timeout_cb,NULL);
    struct timeval tv={5,0};//设置定时器超时时间
    event_add(time_ev,&tv);//注册定时器事件到base 里面
    
    //事件循环
    event_base_dispatch(base);

    //////////////////////////////////
    event_free(sig_ev);
    event_free(time_ev);
    event_base_free(base);

    exit(0);
}

运行效果如下:
在这里插入图片描述
分析定义信号事件那里,只有EV_SIGNAL,没有永久事件。所以说第一次 Ctrl+c是可以触发的,但是第二次就相当于关闭这个进程。当然 可以类似于下面的定时器事件,增加一个EV_PERSIST永久事件,就可以一直触发了,如下:在这里插入图片描述
此时运行结果:
在这里插入图片描述
实例2,实现的服务器 连接多客户端。源代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <event.h>

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

//句柄(文件描述符)值上限
#define MAXFD 100

//数组存放事件处理器,以文件描述符作为下标
struct event* map_arr[MAXFD];

void map_arr_init()
{
    int i=0;
    for(;i<MAXFD;++i)
    {
        map_arr[i]=NULL;
    }
}
//添加事件处理器到数组中,将事件处理器和句柄(文件描述符)绑定
void map_arr_add(int fd,struct event* ev)
{
    if(fd<0 || fd>=MAXFD)
    {
        return;
    }
    map_arr[fd]=ev;
}

//从数组中删除句柄(文件描述符fd)所指定的事件处理器,
//并返回该事件处理器供处理函数从libevent中将事件处理器删除
struct event*map_arr_find(int fd)
{
    if(fd<0 || fd>=MAXFD)
    {
        return NULL;
    }
    return map_arr[fd];
}

void recv_cb(int fd,short ev,void*arg)
{
    if(ev & EV_READ)//处理读事件
    {
        char buff[128]={0};
        int n=recv(fd,buff,127,0);
        //若没有从libevent中删除,则会反复循环,
        //因此,我们必须处理当客户端关闭时,从libevent中将事件删除
        if(n<=0)
        {
        	//从上面的数组里面得到这个 指针然后去释放
            event_free(map_arr_find(fd));
            map_arr[fd]=NULL;
            close(fd);
            printf("one client over\n");
            return;
        }
        printf("buff=%s\n",buff);
        send(fd,"received!!!",11,0);
    }
}
int create_socket()
{
    //创建监听套接字(socket描述符),指定协议族ipv4,字节流服务传输
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd == -1)
        return -1;

    //socket专用地址信息设置
    struct sockaddr_in saddr;

    memset(&saddr,0,sizeof(saddr));

    saddr.sin_family=AF_INET;
    saddr.sin_port=htons(6000);
    saddr.sin_addr.s_addr=inet_addr("127.0.0.1");

    //命名套接字,将socket专用地址绑定到socket描述符上
    int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
    if(res==-1)
        return -1;
    if(listen(sockfd,5)==-1)//创建监听队列
        return -1;
    return sockfd;
}

//处理新连接,每次当有新连接到来时,就会调用accept_cb
void accept_cb(int fd,short ev,void *arg)
{
    struct event_base* base=(struct event_base*)arg;
    if(ev & EV_READ)
    {
        struct sockaddr_in caddr;
        int len=sizeof(caddr);
		
		//获得连接套接字
        int c=accept(fd,(struct sockaddr*)&caddr,&len);
        if(c<0)
        {
            return;
        }
        printf("accept c=%d\n",c);

        //创建I/O事件
        /* 
		**	调用 event_new 创建连接事件。事件触发之后,
		**	自动重新对这个event调用event_add函数(永久性事件)
		**  recv_cb为指定的回调函数
		 */	
        struct event*ev_c=
        			event_new(base,c,EV_READ|EV_PERSIST,recv_cb,NULL);
		/*
		** 	调用event_add函数,将事件处理器添加到注册
		**	事件队列中,并将该事件处理器对应的事件添加
		**	到事件多路分发器中。
		 */
        event_add(ev_c,NULL);
        
        //调用map_arr_add函数,将事件处理器和句柄(文件描述符)绑定
        map_arr_add(c,ev_c);
    }
}

int main()
{
    map_arr_init();//把这个全局的数组给初始化一下
    int sockfd=create_socket();
    assert(sockfd != -1);
	
	//创建libevent实例
    struct event_base *base=event_base_new();
    assert(base != NULL);

    //创建一个事件,accept_cb为回调函数
    struct event* sock_ev=event_new(base,sockfd,
    						EV_READ | EV_PERSIST,accept_cb,base);
    event_add(sock_ev,NULL);

    //下面是事件循环
    event_base_dispatch(base);
    
    event_free(sock_ev);//里面已经调用了delete
    event_base_free(base);

    exit(0);
}

运行效果如下:在这里插入图片描述
总结:我们可以借助于 Libevent 框架库来实现之前我们使用I/O复用系统调用实现的多客户服务器。由于Libevent 框架库封装了较为底层的系统调用(select、 poll、 epoll_wait 等),所以这就会大大减少程序编写的工作量,可以使开发人员将注意力集中到功能实现上,不用过多的关注底层的系统调用。

此外event_new函数的参数详解:

struct event*ev_c=
        			event_new(base,c,EV_READ|EV_PERSIST,recv_cb,NULL);
----------------------------------------------------------------------
//event_new 参数:
//base实例,文描(信号用代号,定时器-1),事件类型,回调函数,传给回调函数参数 

对于上面的event_new函数,其中的回调函数是我们写的recv_cb函数。这个回调函数的形式如下:void recv_cb(int fd,short ev,void*arg)。它的fd,ev,arg就是分别地对应着event_new函数的文描,事假类型,传给回调函数参数。这就是回调函数recv_cb的参数由来。

2019年11月1日01:27:10

猜你喜欢

转载自blog.csdn.net/weixin_43949535/article/details/102845075