• 如何在 libevent 中读取超过 4096 字节的数据


    如何在 libevent 中读取超过 4096 字节的数据

    bufferevent 是 libevent 中相对高层的封装,较 event 使用起来方便很多。

    之前有一个需求,需要从服务端读取数据进行操作,为了防止数据过大,在 bufferevent 的 read_callback 中循环调用 bufferevent_read,期望多次通过调用来读完所有的数据。

    很显然,这个方法不行,第二次调用 bufferevent_read 会被阻塞,不符合预期,不能够像调用 read(2) 那样来使用。

    实际上,bufferevent 内有可读数据并且大于水位 watermask 才会调用 read_callback,在 read_callback 只能调用一次 bufferevent_read 来读出缓冲区内的数据。

    当一次 bufferevent_read 不能全部读取完数据怎么办,网上有人通过骚操作去修改 EVBUFFER_MAX_READ 来调大单次读取的值,然后再进行编译。不可取的行为!!!

    buffervent 中的 watermask 是触发 read_callback 的关键,我们只要 bufferevent 内的数据大于设置的 watermask 即可,这样再次触发直接在 read_callback 内一次性读完。

    watermask 如何设置

    网络服务对数据的处理,肯定是要分包进行处理的,关闭 TCP_NODELAY 选项又会对性能造成影响。一般的解决方案是增加一个包头

    bytes:     4         1          4
      +----------------+----+----------------+---------------------+
      |      Magic     |Type|       Len      |      Payload        |
      +----------------+----+----------------+---------------------+
    
    • Magic 为标志魔数,4字节
    • Type 为包类型,1字节
    • Len 为 Header + Payload 长度,4字节

    用代码来实现这个头部的也非常简单

    static const uint32_t kMessageHeaderLen = 9;
    static const uint32_t kMessageHeaderMagic = 0x00114514;
    
    enum MessageType : uint8_t {
      kMessageTypeNULL = 0,
      // ...
    };
    
    struct Message {
      uint32_t magic;
      MessageType type;
      uint32_t len;
    
      Message() : type(kMessageTypeNULL) {}
    
      Message(MessageType type, uint32_t len) : magic(kMessageHeaderMagic), type(type), len(len) {}
    
      Message(char data[kMessageHeaderLen]) { decode(data); }
    
      void decode(const char data[kMessageHeaderLen]) {
        magic = ntohl(*(uint32_t *)data);
        type = *(MessageType *)(data + sizeof(magic));
        len = ntohl(*(uint32_t *)(data + sizeof(magic) + sizeof(type))) < kMessageHeaderLen
                  ? 0
                  : ntohl(*(uint32_t *)(data + sizeof(magic) + sizeof(type))) - kMessageHeaderLen;
      }
    
      void encode(char data[kMessageHeaderLen]) {
        *(uint32_t *)data = htonl(magic);
        *(uint8_t *)(data + sizeof(magic)) = type;
        *(uint32_t *)(data + sizeof(magic) + sizeof(type)) = htonl(len + kMessageHeaderLen);
      }
    };
    

    watermask 就是这个包头中的 Len,在代码中的值为 Message::len + sizeof(Message).

    如何利用 watermask 读取大于 4096 大小的数据

    在第一次的 read_callback 内,先读取一个包头,将整个包的大小解析出来

    1. 如果 evbuffer 中的数据大小大于等于 Len 时,直接将所有的数据读取出来,并且要将 watermask 设置为 0(下次读取不受影响)
    2. 如果 evbuffer 中的数据大小小于 Len 时,所有还有数据没有从内核缓冲区读取到 bufferevent 内的 evbuffer 中,这个时候设置水位为 Len,在下次 read_callback 调用的时候,所有的数据都在 evbuffer 中。

    代码如下:

    static void read_callback(struct bufferevent *bev, void *arg) {
      struct evbuffer *evbuf = bufferevent_get_input(bev);
      size_t len = evbuffer_get_length(evbuf);
      if (len < 9)
        return;
    
      char head[9];
      evbuffer_copyout(evbuf, head, sizeof(head));
      Message msg(head);
      if (msg.magic != kMessageHeaderMagic) {
        evbuffer_drain(evbuf, len);
        return;
      }
    
      if (msg.len + 9 <= len) {
        std::vector<char> buf(msg.len, 0);
        evbuffer_remove(evbuf, head, sizeof(head));
        evbuffer_remove(evbuf, buf.data(), buf.capacity());
        bufferevent_setwatermark(bev, EV_READ, 0, 0);
        // handle data...
      } else if (msg.len + 9 > len) {
        bufferevent_setwatermark(bev, EV_READ, msg.len + 9, 0);
      }
    }
    

    结论

    libevent 每次读取 4096 个字节的确性能一般,在做代理的情况下更明显,毕竟 read/epoll_ctl 的次数都要更多一些,如果是为了提升性能,直接使用 event 来直接操作 fd,比修改 EVBUFFER_MAX_READ 靠谱很多。

    如果是使用 bufferevent 接收比较大的数据

    1. 优先结合 watermask 来一次性读取完所有的数据
    2. 如果 watermask 没有准确的值,比如数据包没有包含长度的元数据,只能通过固定的一些 MAGIC 来判断,那么就能反复读取来进行判断了。
  • 相关阅读:
    2023服务端测试开发必备技能:Mock测试
    networking /etc/network/interfaces 笔记221104
    word如何实现不同章节显示不同页眉
    Flutter.源码分析.flutter/packages/flutter/lib/src/widgets/scroll_view.dart/GridView
    十大排序算法C++实现
    山东二本计算机数据科学与大数据技术专业,目前大二求就业思路
    阅读 | 003《知识图谱发展报告2022》(三)实体抽取
    vue轮播图使用swiper插件
    浅谈制药企业安全供电系统的设计与应用
    MATLAB: 埃米
  • 原文地址:https://www.cnblogs.com/shuqin/p/18025975