简介
I/O Buffers 是 libevent在常规的事件回调处理机制中封装的机制,被封装的事件称为缓冲事件(bufferevent), 该事件将自动的管理内部的I\O缓冲区。当buffer中的数据量到达一定的水位(由用户设定)时, 通过callback通知用户调用bufferevent_read
从buffer中取数据或是bufferevent_write
写入数据
到buffer中
I/O Buffers
libevent provides an abstraction on top of the regular event callbacks. This abstraction is called a buffered event. A buffered event provides input and output buffers that get filled and drained automatically. The user of a buffered event no longer deals directly with the I/O, but instead is reading from input and writing to output buffers.
Once initialized via bufferevent_new(), the bufferevent structure can be used repeatedly with bufferevent_enable() and bufferevent_disable(). Instead of reading and writing directly to a socket, you would call bufferevent_read() and bufferevent_write().
从基础接口入手
在libevent Documentation中提及了几个常用的接口。
bufferevent_new
bufferevent_read、bufferevent_write
bufferevent_enable、bufferevent_disable
libevent的命名还是挺符合人类直觉的,相似的名称都具有相似的行为。
如 event_new <=> bufferevent_new、
event_base_set <=> bufferevent_base_set。
结合简介以及文档,不难猜测Libevent是如何进行封装的,无非是在event的基础上,管理buffer。
下面从结构体进一步验证猜想。
struct bufferevent {
struct event_base *ev_base;
/*bufferevent底层读写event, 将设置为内部callback
bufferevent_readcb、bufferevent_writecb
*/
struct event ev_read;
struct event ev_write;
/*I/O缓冲区*/
struct evbuffer *input;
struct evbuffer *output;
/*水位*/
struct event_watermark wm_read;
struct event_watermark wm_write;
/*用户定义的回调*/
evbuffercb readcb;
evbuffercb writecb;
everrorcb errorcb;
void *cbarg;
int timeout_read; /* in seconds */
int timeout_write; /* in seconds */
short enabled; /* events that are currently enabled */
};
现在已经可以确认,bufferevent就是将对文件描述符(fd)的读写分别用读写event来控制,并同时通过读写event管理input/out buffer,而buffer涉及水位watermark的管理。
那么实际上如何管理呢?
继续猜测:
当ev_read为active时, evread_cb 将fd中的数据载入到input buffer,直到高水位时,停止载入。若buffer中的数据量超过低水位,则通过readcb通知用户,来拿数据。
当ev_write为active时, evwrite_cb将output buffer中的数据载入到fd中。
因而,可以推出bufferevent的核心处理在于evread_cb 和 evwrite_cb。
bufferevent核心机制
从上面我提到了bufferevent的核心机制应该在evread_cb 和 evwrite_cb?
那么,它们应该在哪里呢? 什么时候被设置呢?
既然bufferevent是通过常规event封装的,那么必然涉及常规事件的初始化。不妨从在bufferevent初始化或是使用的途中寻找有没有涉及到event_set
的调用
在bufferevent_new
中找到了相关的代码:
struct bufferevent *
bufferevent_new(int fd, evbuffercb readcb, evbuffercb writecb,
everrorcb errorcb, void *cbarg)
{
struct bufferevent *bufev;
// 注意: calloc可以说等价于malloc + memset(dest, 0, sz)
if ((bufev = calloc(1, sizeof(struct bufferevent))) == NULL)
return (NULL);
/* 初始化buffer */
if ((bufev->input = evbuffer_new()) == NULL) {
free(bufev);
return (NULL);
}
if ((bufev->output = evbuffer_new()) == NULL) {
evbuffer_free(bufev->input);
free(bufev);
return (NULL);
}
/* 设置event_cb */
event_set(&bufev->ev_read, fd, EV_READ, bufferevent_readcb, bufev);
event_set(&bufev->ev_write, fd, EV_WRITE, bufferevent_writecb, bufev);
bufferevent_setcb(bufev, readcb, writecb, errorcb, cbarg);
/*
* Set to EV_WRITE so that using bufferevent_write is going to
* trigger a callback. Reading needs to be explicitly enabled
* because otherwise no data will be available.
*/
bufev->enabled = EV_WRITE; // 启用读缓冲事件
return (bufev);
}
event_set(&bufev->ev_read, fd, EV_READ, bufferevent_readcb, bufev);
event_set(&bufev->ev_write, fd, EV_WRITE, bufferevent_writecb, bufev);
因此,现在我们将关注点转移到bufferevent_readcb
和bufferevent_writecb
的实现上。
注意:
这里有一段代码
bufev->enabled = EV_WRITE;
,可能现在还不是很清楚这是干嘛的, 但我们的现在应该先理清整体思路,不妨在此处作个标记,待整理完核心部分后,再回头研究。
bufferevent_readcb 的实现
废话不说,直接贴源码
tatic void
bufferevent_readcb(int fd, short event, void *arg)
{
struct bufferevent *bufev = arg;
int res = 0;
short what = EVBUFFER_READ;
size_t len;
int howmuch = -1;
// bufferevent超时,调用用户定义的err callback处理
if (event == EV_TIMEOUT) {
what |= EVBUFFER_TIMEOUT;
goto error;
}
/*
* If we have a high watermark configured then we don't want to
* read more data than would make us reach the watermark.
*/
if (bufev->wm_read.high != 0) {
howmuch = bufev->wm_read.high - EVBUFFER_LENGTH(bufev->input);
/* we might have lowered the watermark, stop reading */
/* 输入缓冲区内容高于限制的水位,需要停止从fd中读取数据 */
if (howmuch <= 0) {
struct evbuffer *buf = bufev->input;
event_del(&bufev->ev_read);
evbuffer_setcb(buf,
bufferevent_read_pressure_cb, bufev);
return;
}
}
// 从fd载入数据到input buffer
res = evbuffer_read(bufev->input, fd, howmuch);
if (res == -1) {
if (errno == EAGAIN || errno == EINTR)
goto reschedule;
/* error case */
what |= EVBUFFER_ERROR;
} else if (res == 0) {
/* eof case */
what |= EVBUFFER_EOF;
}
if (res <= 0)
goto error;
// 由于没有设置EV_为持续事件需要重新添加到base的关注中
bufferevent_add(&bufev->ev_read, bufev->timeout_read);
/* See if this callbacks meets the water marks */
len = EVBUFFER_LENGTH(bufev->input);
/* input buffer中的数据量少于低水位 */
if (bufev->wm_read.low != 0 && len < bufev->wm_read.low)
return;
/* 高于限定的水位,停止载入数据 */
if (bufev->wm_read.high != 0 && len >= bufev->wm_read.high) {
struct evbuffer *buf = bufev->input;
event_del(&bufev->ev_read);
/* Now schedule a callback for us when the buffer changes */
evbuffer_setcb(buf, bufferevent_read_pressure_cb, bufev);
}
// input buffer len > low watermark, 回调readcb
/* Invoke the user callback - must always be called last */
if (bufev->readcb != NULL)
(*bufev->readcb)(bufev, bufev->cbarg);
return;
reschedule:
bufferevent_add(&bufev->ev_read, bufev->timeout_read);
return;
error:
(*bufev->errorcb)(bufev, what, bufev->cbarg);
}
此处,我们可以借用蓄水池和水龙头来描述bufferevent_readcb的主要行为。
这个水龙头(bufferevent
)比较智能,当水龙头中的水管充水时(可读事件active),水龙头根据蓄水池(input buffer
)的水位决定是否打开放水。
当水位越高规定的上限时,水龙头不进水,否则水龙头放水到蓄水池。
当水位较合适时(下限 < 水位 < 上限), 通知用户来取水(bufev->readcb
)
其他细节:
-
bufferevent_readcb是如何停止进水呢? 很简单, 只要水管没水进来。
实际做法就是将读event移出base的关注。bufferevent_enable
/bufferevent_enable
也是同样的原理。但是,数据被取走时,当水平面下降,总得进水吧? 不然,buffer早晚干涸。
此处就是引入buffer设置callback ——bufferevent_read_pressure_cb
的原因。
每当buffer中的水位限制发生改变或是水位发生改变,如果buffer设置了callback,总会尝试调用callback通知base恢复对读event的关注。 -
诡异的"return"
将注意力拉回到开头对高水位的判断。
在数据量不少于高水位时,设置了bufferevent_read_pressure_cb
,却不通知用户来取,直接return了。由于改变水位的操作一般而言都放在随后的bufev->readcb
,这样处理可能导致数据一直停留在buffer中。if (bufev->wm_read.high != 0) { howmuch = bufev->wm_read.high - EVBUFFER_LENGTH(bufev->input); /* we might have lowered the watermark, stop reading */ /* 输入缓冲区内容高于限制的水位,需要停止从fd中读取数据 */ if (howmuch <= 0) { struct evbuffer *buf = bufev->input; event_del(&bufev->ev_read); evbuffer_setcb(buf, bufferevent_read_pressure_cb, bufev); return; } }
然而,我们注意到随后的代码段中,从fd灌水到buffer时,即使水位越过了高水位也会通知用户来取。因此,可以断定,只要在某次蓄水之后,用户未能取走数据使得水位降低到高水位之下时,数据可能将被抛弃在buffer中。只能通过额外的手段来取走了。
/* 高于限定的水位,停止载入数据 */ if (bufev->wm_read.high != 0 && len >= bufev->wm_read.high) { struct evbuffer *buf = bufev->input; event_del(&bufev->ev_read); /* Now schedule a callback for us when the buffer changes */ evbuffer_setcb(buf, bufferevent_read_pressure_cb, bufev); } // input buffer len > low watermark, 回调readcb /* Invoke the user callback - must always be called last */ if (bufev->readcb != NULL) (*bufev->readcb)(bufev, bufev->cbarg); return;
bufferevent_writecb的实现
static void
bufferevent_writecb(int fd, short event, void *arg)
{
struct bufferevent *bufev = arg;
int res = 0;
short what = EVBUFFER_WRITE;
if (event == EV_TIMEOUT) {
what |= EVBUFFER_TIMEOUT;
goto error;
}
// 只要output buffer中有数据就往fd中输出
if (EVBUFFER_LENGTH(bufev->output)) {
res = evbuffer_write(bufev->output, fd);
if (res == -1) {
#ifndef WIN32
/*todo. evbuffer uses WriteFile when WIN32 is set. WIN32 system calls do not
*set errno. thus this error checking is not portable*/
if (errno == EAGAIN ||
errno == EINTR ||
errno == EINPROGRESS)
goto reschedule;
/* error case */
what |= EVBUFFER_ERROR;
#else
goto reschedule;
#endif
} else if (res == 0) {
/* eof case */
what |= EVBUFFER_EOF;
}
if (res <= 0)
goto error;
}
// 只要output buffer中数据未读完就继续关注读事件
if (EVBUFFER_LENGTH(bufev->output) != 0)
bufferevent_add(&bufev->ev_write, bufev->timeout_write);
/*
* Invoke the user callback if our buffer is drained or below the
* low watermark.
*/
// 只要output buffer枯竭或是低于写事件的低水位,则从调用
// write_cb索要数据
if (bufev->writecb != NULL &&
EVBUFFER_LENGTH(bufev->output) <= bufev->wm_write.low)
(*bufev->writecb)(bufev, bufev->cbarg);
return;
reschedule:
if (EVBUFFER_LENGTH(bufev->output) != 0)
bufferevent_add(&bufev->ev_write, bufev->timeout_write);
return;
error:
(*bufev->errorcb)(bufev, what, bufev->cbarg);
}
在bufferevent_writecb
中实现就较简单了,只要有数据就一直往fd中写。