如何在 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 來判斷,那麼就能反覆讀取來進行判斷了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章