• 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

     

    bufferevent_sock.c里,一个是bufferevent_socket_new函数调用了freeze,另一个是bufferevent_writecb。根据应用场景分析,目标应该是writecb。分析了一下libevent的流程,遇到socket为可写状态的时候,就会调用bufferevent_writecb,再调用用户设置的writecb。而这个bufferevent_writecb,主要工作就是unfreeze掉buffer,然后调用evbuffer_write_atmost刷出数据到socket里,接着重新freeze该buffer。

     

    这就解释了为什么直接调用evbuffer_write没有作用,因为这个接口似乎就没有打算直接暴露给用户使用。根据这篇文章的分析,libevent对注册事件的检查顺序是超时事件,然后是I/O事件,并将其一一放入链表里,逐个调用回调函数进行处理。回头看应用场景,当一个玩家顶号登录时,首先触发了本轮的IO事件,解包处理逻辑,执行踢人的逻辑处理,向缓冲区写入对客户端的提示信息。注意,本轮对写缓存的修改,并不会立即触发bufferevent_writecb进行发送。因为,其触发的只是用户设置的callback和上一轮留下的deferred callback!正是因为这个原因,本次事件检查循环写入的数据,必须等到下个循环才能被执行。所以,脚本层想要发送完顶号提示信息后,立即断开socket,就会导致socket过早被断开,提示信息无法发送出去了。

     

    update:

    其实底层的write还是有可能没发完,evbuffer_write不提供保证。所以应该是去掉读回调,在写回调里判断是否已写完,写完则断开

  • 相关阅读:
    JDBC 复习4 批量执行SQL
    JDBC 复习3 存取Oracle大数据 clob blob
    Oracle复习
    Linux命令(1)grep
    JDBC 复习2 存取mysql 大数据
    JDBC 复习1 DBUtil
    php 环境搭建问题
    Windows 批处理 bat 开启 WiFi 菜单选项 设置ID PWD
    Bat 批处理启动和停止Oracle 服务
    docker 学习1 WSL docker ,Windows docker
  • 原文地址:https://www.cnblogs.com/hzcya1995/p/13318284.html
Copyright © 2020-2023  润新知