如何在 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 內,先讀取一個包頭,將整個包的大小解析出來
- 如果 evbuffer 中的數據大小大於等於 Len 時,直接將所有的數據讀取出來,並且要將 watermask 設置爲 0(下次讀取不受影響)
- 如果 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 接收比較大的數據
- 優先結合 watermask 來一次性讀取完所有的數據
- 如果 watermask 沒有準確的值,比如數據包沒有包含長度的元數據,只能通過固定的一些 MAGIC 來判斷,那麼就能反覆讀取來進行判斷了。