libevent 库 1.4 升级到 2.0 时无法 flush 的解决办法
libevent 的接口兼容性做的还算不错,基本上替换一下就转到新版本了。但是,强制 flush 数据的时候出了问题。目前的应用场景是,遇到顶号登录这种情形,先用 bufferevent_write 向客户端发送错误信息,然后再断开 socket。用的 flush 是这样的:
void try_flush(bufferevent *bev) {
int size = evbuffer_get_length(bufferevent_get_output(bev));
if (size > 0) {
evbuffer_write(bufferevent_get_output(bev), fd);
}
}
在 1.4 的时候,这段代码工作良好。但是,在 2.0 的 libevent 里,这段代码不能刷出数据。看了下 errno 和 socket_errno,错误码是 EINPROGRESS。百思不得其解下,在脚本层将关闭 socket 放在下一次 libevent 事件循环里完成,结果就好了。
大致猜测下,应该是 libevent 为了效率做的优化。因为 write 是比较昂贵的系统调用,若因为 evbuffer_write 的反复调用,而引起多次 write,会影响性能。翻了一下 libevent 2.0 的代码,evbuffer_write 是调用 evbuffer_write_atmost 的,对比下 1.4 evbuffer_write 和 2.0 的 evbuffer_write_atmost:
int
evbuffer_write(struct evbuffer *buffer, int fd)
{
int n;
#ifndef WIN32
n = write(fd, buffer->buffer, buffer->off);
#else
n = send(fd, buffer->buffer, buffer->off, 0);
#endif
if (n == -1)
return (-1);
if (n == 0)
return (0);
evbuffer_drain(buffer, n);
return (n);
}
int
evbuffer_write_atmost(struct evbuffer *buffer, evutil_socket_t fd,
ev_ssize_t howmuch)
{
int n = -1;
EVBUFFER_LOCK(buffer);
if (buffer->freeze_start) {
goto done;
}
if (howmuch < 0 || (size_t)howmuch > buffer->total_len)
howmuch = buffer->total_len;
if (howmuch > 0) {
#ifdef USE_SENDFILE
struct evbuffer_chain *chain = buffer->first;
if (chain != NULL && (chain->flags & EVBUFFER_SENDFILE))
n = evbuffer_write_sendfile(buffer, fd, howmuch);
else {
#endif
#ifdef USE_IOVEC_IMPL
n = evbuffer_write_iovec(buffer, fd, howmuch);
#elif defined(WIN32)
/* XXX(nickm) Don't disable this code until we know if
* the WSARecv code above works. */
void *p = evbuffer_pullup(buffer, howmuch);
n = send(fd, p, howmuch, 0);
#else
void *p = evbuffer_pullup(buffer, howmuch);
n = write(fd, p, howmuch);
#endif
#ifdef USE_SENDFILE
}
#endif
if (n > 0)
evbuffer_drain(buffer, n);
done:
EVBUFFER_UNLOCK(buffer);
return (n);
}
2.0 有个显眼的 freeze_start 的检查,正是这个检查,让 evbuffer_write 的调用直接跳过了发送阶段。于是,在 flush 的时候加了一个 dirty trick,直接调用了 evbuffer_unfreeze:
void try_flush(bufferevent *bev) {
int size = evbuffer_get_length(bufferevent_get_output(bev));
if (size > 0) {
evbuffer_unfreeze(bufferevent_get_output(bev), 1);
evbuffer_write(bufferevent_get_output(bev), fd);
}
}
问题解决
然后跟了下 libevent 2.0 的文档,里面提到 freeze 的作用:
You can use evbuffer_freeze() to temporarily suspend drains from or adds
to a given evbuffer. This is useful for code that exposes an evbuffer as
part of its public API, but wants users to treat it as a pure source or
sink.
因为是从 1.4 移植到 2.0 的,所以没有理由自己 freeze 掉的。那么剩下的原因只会是 libevent 自己 freeze 了这个 buffer,内部对 freeze 的调用集中在 bufferevent_sock.c 和 bufferevent_pair.c 两个文件里,目前没有使用 socketpair,于是目标只有 bufferevent_sock.c
这就解释了为什么直接调用 evbuffer_write 没有作用,因为这个接口似乎就没有打算直接暴露给用户使用。根据 这篇文章 的分析,libevent 对注册事件的检查顺序是超时事件,然后是 I/O 事件,并将其一一放入链表里,逐个调用回调函数进行处理。回头看应用场景,当一个玩家顶号登录时,首先触发了本轮的 IO 事件,解包处理逻辑,执行踢人的逻辑处理,向缓冲区写入对客户端的提示信息。注意,本轮对写缓存的修改,并不会立即触发 bufferevent_writecb 进行发送。因为,其触发的只是用户设置的 callback 和上一轮留下的 deferred callback!正是因为这个原因,本次事件检查循环写入的数据,必须等到下个循环才能被执行。所以,脚本层想要发送完顶号提示信息后,立即断开 socket,就会导致 socket 过早被断开,提示信息无法发送出去了。
update:
其实底层的 write 还是有可能没发完,evbuffer_write 不提供保证。所以应该是去掉读回调,在写回调里判断是否已写完,写完则断开