网络编程之libevent

安装libevent

官方仓库 下载源码,按照Cmake方式安装,可能会缺少一些依赖包

sudo apt install libssl-dev
sudo apt install libmbedtls-dev libmbedtls10

编译程序的时候发现还是找不到,将libevent目录下的include文件复制到/usr/include 仍然不行

直接 sudo apt-get install libevent-dev 哈哈哈

通过gdb core dump方法查看程序异常时的堆栈信息

当发生"内存越界"等错误时,会产生"Segmention fault",并生成Core文件

Core文件默认是没有打开的,可以通过 ulimit -a 查看,用 ulimit -c unlimited 设置(只在当前窗口有效)

再执行 gdb program_name core 就可以查看出错时的堆栈信息啦

参考通过gdb core dump方法查看程序异常时的堆栈信息

初识libevent

libevent提供的simple有点劝退,这里用《Linux高性能服务器编程》中Libevent源码分析里面的demo

#include <sys/signal.h>
#include <event.h>

void signal_cb( int fd, short event, void* argc )
{
    struct event_base* base =(struct event_base*)argc;
    struct timeval delay = { 2, 0 };
    printf( "Caught an interrupt signal; exiting cleanly in two seconds...\n" );
    event_base_loopexit( base, &delay );
}  

void timeout_cb( int fd, short event, void* argc )
{
    printf( "timeout\n" );
}

int main()  
{  
    struct event_base* base = event_init();

    struct event* signal_event = evsignal_new( base, SIGINT, signal_cb, base ); // 事件处理器
    event_add( signal_event, NULL ); // 添加到事件队列

    struct timeval tv = { 1, 0 };
    struct event* timeout_event = evtimer_new( base, timeout_cb, NULL );
    event_add( timeout_event, &tv );

    event_base_dispatch( base );  // 执行事件循环

    event_free( timeout_event );
    event_free( signal_event );
    event_base_free( base );
}  

Output:

gcc -o libevent_test libevent_test.c -levent
./libevent_test
timeout
^CCaught an interrupt signal; exiting cleanly in two seconds...

Demo简单,但是却描述了LibEvent库的主要逻辑

libevent开发流程

零、socket编程

基本的socket编程是阻塞/同步的,每个操作除非已经完成或者出错才会返回,这样对于每一个请求,要使用一个线程或者单独的进程去处理,系统资源没法支撑大量的请求(所谓c10k problem?),例如内存:默认情况下每个线程需要占用2~8M的栈空间。posix定义了可以使用异步的select系统调用,但是因为其采用了轮询的方式来判断某个fd是否变成active,效率不高[O(n)],连接数一多,也还是撑不住。于是各系统分别提出了基于异步/callback的系统调用,例如Linux的epoll,BSD的kqueue,Windows的IOCP。由于在内核层面做了支持,所以可以用O(1)的效率查找到active的fd。基本上,libevent就是对这些高效IO的封装,提供统一的API,简化开发

libevent大概是这样的:
默认情况下是单线程的(可以配置成多线程,如果有需要的话),每个线程有且只有一个event_base,对应一个struct event_base结构体(以及附于其上的事件管理器),用来schedule托管给它的一系列event,可以和操作系统的进程管理类比,当然,要更简单一点。当一个事件发生后,event_base会在合适的时间(不一定是立即)去调用绑定在这个事件上的函数(传入一些预定义的参数,以及在绑定时指定的一个参数),直到这个函数执行完,再返回schedule其他事件。

一、创建event_base对象

调用event_init函数创建event_base对象。一个event_base相当于一个rector实例.

struct event_base* base = event_init();

二、创建具体的事件处理器

创建具体的事件处理器,并设置从属的Rector实例,evsignal_newevtimer_new分别用于创建信号事件处理器和定时事件处理器。

struct event* signal_event = evsignal_new( base, SIGINT, signal_cb, base );//创建信号事件处理器并设置从属的Rector实例
struct event* timeout_event = evtimer_new( base, timeout_cb, NULL );//创建定时事件处理器并设置从属的Rector实例

这两个函数有一个统一入口函数event_new:

struct event* event_new(struct event_base* base,evutil_socket_t  fd,short what,event_callback_fn  cb,void* arg);

参数1:base 指定新创建的事件处理器从属的 Rector,
参数2:fd指定与该事件处理器关联的句柄
参数3:events需要监控的事件

EV_TIMEOUT      0x01	定时事件
EV_READ			0x02	可读事件
EV_WRITE		0x04	可写事件
EV_SIGNAL		0x08	信号事件
EV_PERSIST		0x10	永久事件
/*边沿触发事件,需要I/O复用系统调用支持*/
EV_ET			0x20

参数4:callback指定目标事件的回调函数
参数5:callback_arg 传递给回调函数的参数
函数返回值:成功返回一个event类型对象,

三、将事件处理器添加到注册事件队列

调用event_add函数将事件处理器添加到注册事件队列中去

四、执行事件循环

调用event_base_dispatch函数执行事件循环

五、释放资源

事件循环之后free掉资源

libevent里面的一些函数

设置端口重用

evutil_make_listen_socket_reuseable(server_socketfd); 

设置无阻赛 实体在evutil.c中,是对fcntl操作

evutil_make_socket_nonblocking(server_socketfd); 

返回一个字符串,标识内核事件机制(kqueue的,epoll的,等等)

const char *x =  event_base_get_method(base); //查看用了哪个IO多路复用模型,linux一下用epoll 

程序进入无限循环,等待就绪事件并执行事件处理

int event_base_dispatch(struct event_base *);

属性获取示例

点击查看代码
#include<stdio.h>
#include<event2/event.h>

int main() {

    // libevent version
    printf("LIBEVENT_VERSION_NUMBER:%d\n", LIBEVENT_VERSION_NUMBER);
    printf("version: %s\n", event_get_version());
    struct event_base *base;
    base = event_base_new();
    // return current the method of multi-thread io
    const char *x =  event_base_get_method(base);
    printf("METHOD:%s\n", x);

    // return base support bit mask
    int t = event_base_get_features(base);
    if(t & EV_FEATURE_ET) {  // 支持边沿触发的后端
        printf("EV_FEATURE_ET\n");
    } 
    if(t & EV_FEATURE_O1) {  // 添加、删除单个事件,或者确定哪个事件激活的操作是 O(1)复杂度的后端
        printf("EV_FEATURE_O1\n");
    }
    if(t & EV_FEATURE_FDS) {  // /要求支持任意文件描述符,而不仅仅是套接字的后端
        printf("EV_FEATURE_FDS\n");
    }
 

    int y = event_base_dispatch(base);
    event_base_free(base);
    return 1;
}

Output:

点击查看代码
LIBEVENT_VERSION_NUMBER:33622016
version: 2.1.8-stable
METHOD:epoll
EV_FEATURE_ET
EV_FEATURE_O1

事件循环 event_loop

一旦创建好事件根基event_base,并且在根基上安插好事件之后,需要对事件循环监控(换句话说就是等待事件的到来,触发事件的回调函数),有两种方式可以达到上面描述的功能,即:event_base_dispatch和event_base_loop

int event_base_dispatch(struct event_base *);	//程序进入无限循环,等待就绪事件并执行事件处理
int event_base_loop(struct event_base *base, int flags);	//

参数flags

  • EVLOOP_ONCE:相当于epoll_wait阻塞方式&&只调用一次 ⇒ 当没有事件到来时,程序将一直阻塞在event_base_loop函数;直到有任意一个事件到来时,程序才不会阻塞在event_base_loop,将会继续向下执行。
  • EVLOOP_NONBLOCK:相当于epoll_wait非阻塞方式&&只调用一次 ⇒ 即使没有事件到来,程序也不会阻塞在event_base_loop
  • EVLOOP_NO_EXIT_ON_EMPTY:等价于event_base_dispatch ⇒ 将一直循环监控事件 ⇒ 直到没有已经注册的事件 || 调用了event_base_loopbreak()或 event_base_loopexit()为止

事件循环的退出的情况
引起循环退出的情况:

  • event_base中没有事件了
  • 调用event_base_loopbreak 事件循环会停止 (立即停止)
  • 调用event_base_loopexit (等待所有事件结束后停止)
  • 程序错误

libevent简易聊天室

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <sys/types.h>      
#include <sys/socket.h>      
#include <netinet/in.h>      
#include <arpa/inet.h>     
#include <string.h>  
#include <fcntl.h>   
  
#include <event2/event.h>  
#include <event2/bufferevent.h> 

int cli_socket[1024]; 
int cli_index = 0;

//读取客户端  
void do_read(evutil_socket_t fd, short event, void *arg) {  
    //继续等待接收数据    
    char buf[1024];  //数据传送的缓冲区      
    int len;    
    if ((len = recv(fd, buf, 1024, 0)) > 0)  {    
        buf[len] = '\0';      
        printf("%s", buf); 
		for(int index = 0;index<cli_index;index++){
			if (send(cli_socket[index], buf, len, 0) < 0) {    //将接受到的数据写回每一个客户端  
				perror("write");      
			}
		}
    }
	if(len == 0)
	{
		printf("fd:%d close\n",fd);
		close(fd);
	}
	if(len <0)
	{
		perror("recv");     
	}
} 

//回调函数,用于监听连接进来的客户端socket  
void do_accept(evutil_socket_t fd, short event, void *arg) {  
    int client_socketfd;//客户端套接字      
    struct sockaddr_in client_addr; //客户端网络地址结构体     
    int in_size = sizeof(struct sockaddr_in);    
    //客户端socket    
    client_socketfd = accept(fd, (struct sockaddr *) &client_addr, &in_size); //等待接受请求,这边是阻塞式的    
    if (client_socketfd < 0) {    
        puts("accpet error");    
        exit(1);  
    } 
	cli_socket[cli_index++] = client_socketfd;
	printf("Connect from %s:%u ...!\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port)); 
    //类型转换  
    struct event_base *base_ev = (struct event_base *) arg;  
  
    //socket发送欢迎信息    
    char * msg = "Welcome to Libevent socket\n";    
    int size = send(client_socketfd, msg, strlen(msg), 0);    
  
    //创建一个事件,这个事件主要用于监听和读取客户端传递过来的数据  
    //持久类型,并且将base_ev传递到do_read回调函数中去  
    struct event *ev;  
    ev = event_new(base_ev, client_socketfd, EV_TIMEOUT|EV_READ|EV_PERSIST, do_read, base_ev);  
    event_add(ev, NULL);  
} 

//入口主函数  
int main() {  
  
    int server_socketfd; //服务端socket    
    struct sockaddr_in server_addr;   //服务器网络地址结构体      
    memset(&server_addr,0,sizeof(server_addr)); //数据初始化--清零      
    server_addr.sin_family = AF_INET; //设置为IP通信      
    server_addr.sin_addr.s_addr = INADDR_ANY;//服务器IP地址--允许连接到所有本地地址上      
    server_addr.sin_port = htons(5555); //服务器端口号      
    
    //创建服务端套接字    
    server_socketfd = socket(PF_INET,SOCK_STREAM,0);    
    if (server_socketfd < 0) {    
        puts("socket error");    
        return 0;    
    }    
  
    evutil_make_listen_socket_reuseable(server_socketfd); //设置端口重用  
    evutil_make_socket_nonblocking(server_socketfd); //设置无阻赛  
    
    //绑定IP    
    if (bind(server_socketfd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr))<0) {    
        puts("bind error");    
        return 0;    
    }  

    //监听,监听队列长度 5    
    listen(server_socketfd, 10);    
    printf("listen port:%d\n",5555);
    //创建event_base 事件的集合,多线程的话 每个线程都要初始化一个event_base  
    struct event_base *base_ev;  
    base_ev = event_base_new();   
    const char *x =  event_base_get_method(base_ev); //获取IO多路复用的模型,linux一般为epoll  
    printf("METHOD:%s\n", x);  
  
    //创建一个事件,类型为持久性EV_PERSIST,回调函数为do_accept(主要用于监听连接进来的客户端)  
    //将base_ev传递到do_accept中的arg参数  
    struct event *ev;  
    ev = event_new(base_ev, server_socketfd, EV_TIMEOUT|EV_READ|EV_PERSIST, do_accept, base_ev);  
  
    //注册事件,使事件处于 pending的等待状态  
    event_add(ev, NULL);  
  
    //事件循环  
    event_base_dispatch(base_ev);  
  
    //销毁event_base  
    event_base_free(base_ev);    
    return 1;  
}


gcc simple_chat_room.c -o simple_chat_room -levent
./simple_chat_room
listen port:5555
METHOD:epoll

效果:一个客户端的消息会发送到其他所有客户端

参考链接

  1. 基于Libevent的简易聊天室设计----从0开始
  2. libevent入门教程:Echo Server based on libevent
posted @ 2021-12-17 17:39  Rogn  阅读(73)  评论(0编辑  收藏  举报