目录
[3.3、defer callback](#3.3、defer callback)
C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.htmlVC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/131405795开源组件及数据库技术(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_2276111.html
1、libevent介绍
1.1、什么是libevent?
libevent是一个用C语言实现的、基于事件驱动(event-driven)的轻量级高性能开源网络库,适用于Windows、Linux、bsd等多种平台,内部使用select、epoll、kqueue等系统调用管理事件机制。著名的用于apache的php缓存库memcached也是基于libevent实现的。
如果你将要开发的应用程序需要支持以上所列出的平台中的两个以上,那么建议你采用这个库,即使你的应用程序只需要支持一个平台,选择libevent也是有好处的,因为它可以根据编译/运行环境切换底层的事件驱动机制,这既能充分发挥系统的性能,又增加了软件的可移植性。
它封装并且隔离了事件驱动的底层机制,除了一般的文件描述符读写操作外,它还提供有读写超时、定时器和信号回调,另外,它还允许为事件设定不同的优先级,当前版本的libevent还提供dns和http协议的异步封装,这一切都让这个库尤其适合于事件驱动应用程序的开发。
1.2、libevent特点
libevent有几个显著的特点:
- 事件驱动(event-driven),高性能;
- 轻量级,专注于网络,不如 NGINX 那么臃肿庞大;
- 源代码相当精炼、易读;
- 跨平台,支持 Windows、Linux、*BSD和Mac OS,虽说Windows支持不怎么好;
- 支持多种I/O多路复用技术,select、epoll、poll、dev/poll、select、kqueue、evports等;
- 支持I/O,定时器和信号等事件;
- 采用Reactor设计模式;
- 支持HTTP(S),DNS解析。
libevent是用于编写高速可移植非阻塞IO应用的库,其设计目标是:
- 可移植性:使用libevent编写的程序应该可以在libevent支持的所有平台上工作。即使没有好的方式进行非阻塞IO,libevent也支持一般的方式,让程序可以在受限的环境中运行。
- 高性能:libevent尝试使用每个平台上最高速的非阻塞IO实现,并且不引入太多的额外开销。
- 便捷:无论何时,最自然的使用libevent编写程序的方式应该是稳定的、可移植的。
- 可扩展性:libevent被设计为程序即使需要上万个活动套接字的时候也可以良好工作。
1.3、网络连接管理模块bufferevent
一般通过libevent进行网络编程,都是将一个socket的fd与一个event进行绑定,并自行维护一个buffer用于存储从socket上接收的数据,同时可能也用于待发送数据的缓存。然后通过可读可写事件从socket上收取数据写入缓存并进行相应处理,或者将缓存中的数据通过socket发送。
libevent为这种带缓存的IO模式提供了一种通用的机制,那就是bufferevent。bufferevent主要用于管理基于流的网络连接,提供了缓冲、超时、流控等功能。一个bufferevent包含了一个底层传输的fd(通常为socket),一个输入buffer和一个输出buffer,并且bufferevent已经帮我们完成了从socket上接收数据写入输入buffer,同时从输出buffer中取出数据通过socket发送,当输入输出缓存中的数据达到一定量时调用我们设置的回调函数。这样使得我们可以更加关注数据的处理。
2、bufferevent有什么用?
bufferevent的主要作用有:
- 读写缓冲区管理:bufferevent为我们提供了缓冲区管理的功能,可以帮助我们处理读写缓冲区的分配、管理以及数据传输的同步与异步操作。
- 数据处理:bufferevent可以将底层的数据读写操作转化为事件的回调函数,从而使得数据的读写操作可以被更加灵活的处理。
- 操作简单:bufferevent的使用非常简单,只需要注册事件回调函数,就可以开始读写操作,无需进行额外的操作。
总的来说 ,有了bufferevent用户就可以不用处理底层的I/O操作,直接在bufferevent中读或者写数据就行。
在这里,给大家重点推荐一下我的几个热门畅销专栏:
专栏1:(该专栏订阅量已达到420多个,有很强的实战参考价值,广受好评!专栏文章持续更新中,预计更新到200篇以上!)
C++软件调试与异常排查从入门到精通系列文章汇总https://blog.csdn.net/chenlycly/article/details/125529931
本专栏根据近几年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法,详细讲述了C++软件的调试方法与手段,以图文并茂的方式给出具体的实战问题分析实例,带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!
专栏中的文章均是通过项目实战总结出来的(通过项目实战积累了大量的异常排查素材和案例),有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到200篇以上!
专栏2:
C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html
以多年的开发实战为基础,总结并讲解一些的C/C++基础与进阶内容,以图文并茂的方式对相关知识点进行详细地展开与阐述!专栏涉及了C/C++领域的多个方面的内容,同时给出C/C++及网络方面的常见笔试面试题,并详细讲述Visual Studio常用调试手段与技巧!
专栏3:
开源组件及数据库技术https://blog.csdn.net/chenlycly/category_12458859.html
以多年的开发实战为基础,分享一些开源组件及数据库技术!
3、bufferevent的整体设计与实现细节
3.1、整体概况
bufferevent的结构体定义如下:
从bufferevent结构体的构成可以看出,bufferevent中包含了读,写两个事件,这两个事件的回调函数分别为bufferevent_readcb和bufferevent_writecb。
bufferevent同时还包含了输入输出两个缓存区,以及读、写、事件回调函数的指针,高低水位的设置,事件驱动的句柄等。当触发可读可写事件后,回调bufferevent_readcb和bufferevent_writecb,在这里完成从fd上的数据收发,然后根据收发结果及高低水位的设置等来进行不同回调处理。
3.2、evbuffer与bufferevent
bufferevent采用evbuffer作为输入输出缓存,evbuffer的实现如下所示:
evbuffer像是一个字节队列,在队列的末尾写入数据,在队列的头取数据。evbuffer具体实现则是一个链表,链表中的每个节点都是一块连续的内存块。往evbuffer写数据时(evbuffer_add等函数),evbuffer内部动态创建链表节点,并紧凑的写入数据(一个节点写满,再写另外一个节点),从evbuffer中删除数据时(evbuffer_remove/evbuffer_drain),从链表头部节点开始读取,当一个节点的数据被全部读取后删除该节点,如果未读取完,则用标示记录数据已读取(删除)的部分。对于这种头部有数据被标识为读取(删除)的节点,再次写入数据时,可能会进行调整,即将数据部分整体往前拷贝移动,然后再继续写入数据。
3.3、defer callback
在创建bufferevent时,可以设置不同的选项,其中一个是BEV_OPT_DEFER_CALLBACKS,这意味着延迟进行回调。
所谓延迟回调,是将该事件延迟等到本次事件循环中,所有active事件都处理完成后再进行该事件的处理。在event_base中,有一个active事件队列,一个defer事件队列。事件循环时,遍历active事件队列并进行相应的处理,当发现某个事件时需要延迟处理时,将该事件放到defer事件队列中,继续后续active事件的处理,等active事件队列中的事件都处理完成后,再处理defer队列中的事件。
对于bufferevent来说,当fd上有数据可读时,其实是先进行了一次回调(bufferevent_readcb),这个回调函数中判断是否需要延迟处理,如果不需要延迟则直接调用我们设置的回调函数,如果需要延迟,则等libevent处理完其他的active事件后再次调用bufferevent的回调函数,再由该回调函数调用我们设置的回调函数。
4、bufferevent的使用方法
使用bufferevent主要有以下几个步骤。
4.1、创建和销毁bufferevent
使用libevent创建bufferevent非常简单。首先,创建一个event_base对象和一个套接字描述符,然后使用bev_socket_new或bev_bufferevent_new函数创建一个新的bufferevent。在不再需要时,可以使用bev_free函数释放bufferevent。
cpp
struct event_base *base = event_base_new();
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct bufferevent *bev = bufferevent_socket_new(base, sockfd, BEV_OPT_CLOSE_ON_FREE);
//释放
bufferevent_free(bev);
event_base_free(base);
4.2、设置bufferevent事件回调函数
bufferevent需要处理不同类型的事件,例如读取数据、写入数据、错误、超时等。使用bufferevent_setcb函数设置回调函数来处理这些事件。在回调函数中,可以读取或写入数据,或者关闭bufferevent。
cpp
void my_read_cb(struct bufferevent *bev, void *ctx)
{
struct evbuffer *input = bufferevent_get_input(bev);
// 从输入读取数据
}
void my_write_cb(struct bufferevent *bev, void *ctx)
{
struct evbuffer *output = bufferevent_get_output(bev);
// 将数据写入到输出
}
void my_event_cb(struct bufferevent *bev, short events, void *ctx)
{
if (events & BEV_EVENT_ERROR)
{
// 处理错误事件
bufferevent_free(bev);
}
}
bufferevent_setcb(bev, my_read_cb, my_write_cb, my_event_cb, NULL);
4.3、启用或禁用bufferevent
可以使用bufferevent_enable和bufferevent_disable函数启用或禁用bufferevent的读、写或事件操作。如果需要暂停读或写数据,则可以使用BEV_SUSPEND读或写操作,然后使用BEV_RESUME恢复它们。
cpp
// 禁用读取操作
bufferevent_disable(bev, EV_READ);
// 启用写入操作
bufferevent_enable(bev, EV_WRITE);
4.4、读写数据
使用bufferevent_read和bufferevent_write函数读取和写入数据。bufferevent可以在内部缓冲区中缓存数据,也可以直接读取或写入套接字。
cpp
// 读数据
bufferevent_read(bev, buffer, buffer_size);
// 写数据
bufferevent_write(bev, buffer, buffer_size);
4.5、设置bufferevent选项
可以使用bufferevent_setwatermark和bufferevent_settimeout函数设置bufferevent的选项。其中,bufferevent_setwatermark设置读取和写入缓冲区的低水位和高水位。bufferevent_settimeout设置超时时间。
cpp
// 设置watermark
bufferevent_setwatermark(bev, EV_READ, lowmark, highmark);
bufferevent_setwatermark(bev, EV_WRITE, lowmark, highmark);
// 设置 timeout
struct timeval tv = {5, 0}; // 5秒
bufferevent_set_timeouts(bev, &tv, NULL);
这里详细说一下bufferevent_setwatermark函数。在该函数中,第二个参数是 events,它表示在何时触发回调函数。具体来说,EV_READ 和 EV_WRITE 是事件标志,它们分别代表读事件和写事件。
EV_READ 用于在读操作时触发回调函数。
EV_WRITE 用于在写操作时触发回调函数。
在这里,我们可以理解为:lowmark 和 highmark 是在读取数据时缓冲区的低水位和高水位。
EV_READ 表示在缓冲区的读取操作中触发回调函数。因此,bufferevent_setwatermark(bev, EV_READ, lowmark, highmark) 的作用是:当读取缓冲区的数据量达到 lowmark 或 highmark 时,触发读操作的回调函数。
关于低水位和高水位:当写入缓冲区的数据量超过了高水位时,bufferevent 将停止发送数据,等待缓冲区中的数据被消费。当写入缓冲区中的数据量低于低水位时,bufferevent 又会恢复写入数据,向下游发送数据,以保证高效的数据传输。
5、使用bufferevent时的细节问题
5.1、tcp连接断开处理
对于客户端来说,如果仅有bufferevent这么一个事件,那么当tcp连接断开时,调用回调函数后会退出事件循环(event_base_loop)。因为bufferevent感知tcp连接断开后会删除相关的事件,这个时候事件驱动中没有任何事件,于是退出循环。
在官网的教程中看到可以对event_base设置选项EVLOOP_NO_EXIT_ON_EMPTY保证没有等待事件时也不会退出事件循环,但是在最新稳定版本中(libevent-2.0.21-stable)没有该选项设置,在2.1.x-alpha中才有该选项。我们可以采用增加定时器事件的方式来处理断链后不退出事件循环,甚至进一步实现断链重连的功能, 这个定时器事件可以是断链后在回调函数中动态增加,也可以一开始就增加一个持久的定时器事件,检测连接状态并触发向服务器重连。例如:
cpp
int g_nState;
//定时器事件回调函数
void handle_timeout(int nSock, short sWhat, void * pArg)
{
if( 0 == g_nState )
{
struct bufferevent * pBufferEvent = (struct bufferevent *)pArg;
struct sockaddr_in tSockAddr;
memset(&tSockAddr, 0, sizeof(tSockAddr));
tSockAddr.sin_family = AF_INET;
tSockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
tSockAddr.sin_port = htons(50000);
bufferevent_socket_connect(pBufferEvent, (struct sockaddr*)&tSockAddr, sizeof(tSockAddr));
}
}
void event_callback(struct bufferevent * pBufEv, short sEvent, void * pArg)
{
//成功连接 状态变更
if( BEV_EVENT_CONNECTED == sEvent )
{
bufferevent_enable( pBufEv, EV_READ );
g_nState = 1;
}
//出现错误
if( 0 != (sEvent & (BEV_EVENT_ERROR)) )
{
//关闭fd并更改状态
int fd = bufferevent_getfd(pBufEv);
if( fd > 0 )
{
evutil_closesocket(fd);
}
bufferevent_setfd(pBufEv, -1);
g_nState = 0;
}
}
int main( void )
{
...
//增加PERSIST的定时器事件
struct event eTimeout;
struct timeval tTimeout = {10, 0};
//回调函数的参数为bufferevent
event_assign(&eTimeout, pEventBase, -1, EV_PERSIST, handle_timeout, pBufferEvent);
evtimer_add(&eTimeout, &tTimeout);
...
}
这里需要注意的是,重连之前最好先关闭bufferevent中fd,或者直接对bufferevent进行释放并重新创建一个新的bufferevent。如果是直接释放bufferevent再次新建,那么在创建bufferevent时记得设置BEV_OPT_CLOSE_ON_FREE参数,这样在释放bufferevent时会对fd进行关闭,从而不会出现fd泄漏。
不设置该参数,通过bufferevent_setfd传入fd,释放bufferevent后自动关闭fd也是一种处理方式。
5.2、心跳处理
通常,客户端与服务端之间都有心跳检测,以检测tcp链路是否正常,那么通过bufferevent开发的客户端或者服务端完成心跳检测功能可以有这么几种实现方式。
5.2.1、增加定时器事件
前面提到了可以增加持久的定时器事件来检测状态并触发断链重连,当然我们也可以利用这个定时器事件来完成定时发送心跳包的功能。个人觉得这种方式不太好的一点是:需要有一种机制让定时器事件的回调处理函数获取bufferevent的句柄,例如作为定时器事件回调函数的参数,这样才能将心跳包的数据写入该bufferevent并通过fd发送,但两种事件搅合在一起感觉会有些混乱。
5.2.2、利用bufferevent的超时机制
bufferevent可以为读写设置超时时间,我们可以设置读超时来完成定时发送心跳包的功能。在事件回调处理函数中处理BEV_EVENT_TIMEOUT|BEV_EVENT_READING事件,然后将心跳包写入输出缓存。这种方式有一点需要注意:bufferevent触发超时事件后会将对应的可读/可写事件删除,我们在处理完超时事件后需要重新注册一下对应的事件(bufferevent_enable)。例如:
cpp
{
if( BEV_EVENT_CONNECTED == sEvent )
{
bufferevent_enable( pBufEv, EV_READ );
//设置读超时时间 10s
struct timeval tTimeout = {10, 0};
bufferevent_set_timeouts( pBufEv, &tTimeout, NULL);
}
if(0 != (sEvent & (BEV_EVENT_TIMEOUT|BEV_EVENT_READING)) )
{
//发送心跳包
...
//重新注册可读事件
bufferevent_enable(pBufEv, EV_READ);
}
...
return;
}
5.3、高低水位的使用
默认情况下,bufferevent从fd上接收到任何数据并写入输入缓存区时,就会回调交给我们进行处理。而我们的客户端和服务端通信时都会遵循一定的格式(数据包协议),比如固定长度的包头,然后从包头中获取包体的数据长度,然后等包体的数据都接收完成后再进行实际处理。在这种情况下,我们可以设置读的低水位减少回调的次数,bufferevent会等输入缓存区中的数据长度超过最低水位后,才回调我们的函数进行业务处理。
同样,设置写的低水位,表示只有输出缓存区的数据低于最低水位后,调用写回调函数进行相应处理。高水位的设置可用于进行传输速率的控制,例如设置读的高水位时,当输入缓存区中的数据长度超过高水位时,会处于"挂起"状态,即不再从fd上读取数据,直到输入缓存区中的数据低于高水位。