Libevent原始碼分析-----evbuffer結構與基本操作
對於非阻塞IO的網路庫來說,buffer幾乎是必須的。Libevent在1.0版本之前就提供了buffer功能。現在來看一下Libevent的buffer。
buffer相關結構體:
Libevent為buffer定義了下面的結構體:
//evbuffer-internal.h檔案 struct evbuffer_chain; struct evbuffer { struct evbuffer_chain *first; struct evbuffer_chain *last; //這是一個二級指標。使用*last_with_datap時,指向的是連結串列中最後一個有資料的evbuffer_chain。 //所以last_with_datap儲存的是倒數第二個evbuffer_chain的next成員地址。 //一開始buffer->last_with_datap = &buffer->first;此時first為NULL。所以當連結串列沒有節點時 //*last_with_datap為NULL。當只有一個節點時*last_with_datap就是first。 struct evbuffer_chain **last_with_datap; size_t total_len;//連結串列中所有chain的總位元組數 ... }; struct evbuffer_chain { struct evbuffer_chain *next; size_t buffer_len;//buffer的大小 //錯開不使用的空間。該成員的值一般等於0 ev_off_t misalign; //evbuffer_chain已存資料的位元組數 //所以要從buffer + misalign + off的位置開始寫入資料 size_t off; ... unsigned char *buffer; };
這兩個結構體配合工作得到下圖所示的儲存結構:
因為last_with_datap成員比較特殊,上圖只是展示了一種情況。後面還有一張圖,展示另外一種情況。
Libevent將緩衝資料都存放到buffer中。通過一個個的evbuffer_chain連成的連結串列可以存放很多的緩衝資料。
這是一個很常見的連結串列形式。但Libevent有一個很獨特的地方,就是那個evbuffer_chain結構體。
首先,該結構體有misalign成員。該成員表示錯開不用的buffer空間。也就是說buffer中真正的資料是從buffer + misalign開始。
第二,evbuffer_chain結構體buffer是一個指標,按道理來說,應該單獨呼叫malloc分配一個堆記憶體並讓buffer指向之。但實際上buffer指向的記憶體和evbuffer_chain結構體本身的儲存記憶體是一起分配的。下面程式碼展示了這一點:
//evbuffer-internal.h檔案 #define EVBUFFER_CHAIN_SIZE sizeof(struct evbuffer_chain) #if _EVENT_SIZEOF_VOID_P < 8 #define MIN_BUFFER_SIZE 512 #else #define MIN_BUFFER_SIZE 1024 #endif //巨集的作用就是返回,chain + sizeof(evbuffer_chain) 的記憶體地址。 #define EVBUFFER_CHAIN_EXTRA(t, c) (t *)((struct evbuffer_chain *)(c) + 1) //buffer.c檔案 static struct evbuffer_chain * evbuffer_chain_new(size_t size)//size是buffer所需的大小 { struct evbuffer_chain *chain; size_t to_alloc; //所需的大小size 再 加上evbuffer_chain結構體本身所需 //的記憶體大小。這樣做的原因是,evbuffer_chain本身是管理 //buffer的結構體。但buffer記憶體就分配在evbuffer_chain結構體儲存 //記憶體的後面。所以要申請多一些記憶體。 size += EVBUFFER_CHAIN_SIZE;//evbuffer_chain結構體本身的大小 to_alloc = MIN_BUFFER_SIZE; //記憶體塊的最小值 while (to_alloc < size) to_alloc <<= 1; //從分配的記憶體大小可以知道,evbuffer_chain結構體和buffer是一起分配的 //也就是說他們是存放在同一塊記憶體中 if ((chain = mm_malloc(to_alloc)) == NULL) return (NULL); //只需初始化最前面的結構體部分即可 memset(chain, 0, EVBUFFER_CHAIN_SIZE); //buffer_len儲存的是buffer的大小 chain->buffer_len = to_alloc - EVBUFFER_CHAIN_SIZE; //巨集的作用就是返回,chain + sizeof(evbuffer_chain) 的記憶體地址。 //其效果就是buffer指向的記憶體剛好是在evbuffer_chain的後面。 chain->buffer = EVBUFFER_CHAIN_EXTRA(u_char, chain); return (chain); }
前面的圖中,buffer記憶體區域(藍色區域)連在next的後面也是基於這一點的。在程式碼的while迴圈中也可以看到申請的空間大小是512的倍數,也就是說evbuffer_chain申請的空間大小是512、1024、2048、4096……
上面貼出了函式evbuffer_chain_new,該函式是用來建立一個evbuffer_chain。現在貼出另外一個函式evbuffer_new,它是用來建立一個evbuffer的。
//buffer.c
struct evbuffer *
evbuffer_new(void)
{
struct evbuffer *buffer;
buffer = mm_calloc(1, sizeof(struct evbuffer));
if (buffer == NULL)
return (NULL);
buffer->refcnt = 1;
buffer->last_with_datap = &buffer->first;
return (buffer);
}
Buffer的資料操作:
在連結串列尾新增資料:
Libevent提供給使用者的新增資料介面是evbuffer_add,現在就通過這個函式看一下是怎麼將資料插入到buffer中的。該函式是在連結串列的尾部新增資料,如果想在連結串列的前面新增資料可以使用evbuffer_prepend。在連結串列尾部插入資料,分下面幾種情況:
- 該連結串列為空,即這是第一次插入資料。這是最簡單的,直接把新建的evbuffer_chain插入到連結串列中,通過呼叫evbuffer_chain_insert。
- 連結串列的最後一個節點(即evbuffer_chain)還有一些空餘的空間,放得下本次要插入的資料。此時直接把資料追加到最後一個節點即可。
- 連結串列的最後一個節點並不能放得下本次要插入的資料,那麼就需要把本次要插入的資料分開由兩個evbuffer_chain存放。
具體的實現如下面所示:
//buffer.c檔案
int
evbuffer_add(struct evbuffer *buf, const void *data_in, size_t datlen)
{
struct evbuffer_chain *chain, *tmp;
const unsigned char *data = data_in;
size_t remain, to_alloc;
int result = -1;
EVBUFFER_LOCK(buf);//加鎖,執行緒安全
//凍結緩衝區尾部,禁止追加資料
if (buf->freeze_end) {
goto done;
}
//找到最後一個evbuffer_chain。
chain = buf->last;
//第一次插入資料時,buf->last為NULL
if (chain == NULL) {
chain = evbuffer_chain_new(datlen);
if (!chain)
goto done;
evbuffer_chain_insert(buf, chain);
}
//EVBUFFER_IMMUTABLE 是 read-only chain
if ((chain->flags & EVBUFFER_IMMUTABLE) == 0) {//等於0說明是可以寫的
//最後那個chain可以放的位元組數
remain = (size_t)(chain->buffer_len - chain->misalign - chain->off);
if (remain >= datlen) {//最後那個chain可以放下本次要插入的資料
memcpy(chain->buffer + chain->misalign + chain->off,
data, datlen);
chain->off += datlen;//偏移量,方便下次插入資料
buf->total_len += datlen;//buffer的總位元組數
goto out;
} else if (!CHAIN_PINNED(chain) &&//該evbuffer_chain可以修改
evbuffer_chain_should_realign(chain, datlen)) {
//通過調整後,也可以放得下本次要插入的資料
//通過使用chain->misalign這個錯位空間而插入資料
evbuffer_chain_align(chain);
memcpy(chain->buffer + chain->off, data, datlen);
chain->off += datlen;
buf->total_len += datlen;
goto out;
}
} else {
remain = 0; //最後一個節點是隻寫evbuffer_chain
}
//當這個evbuffer_chain是一個read-only buffer或者最後那個chain
//放不下本次要插入的資料時才會執行下面程式碼
//此時需要新建一個evbuffer_chain
to_alloc = chain->buffer_len;
//當最後evbuffer_chain的緩衝區小於等於2048時,那麼新建的evbuffer_chain的
//大小將是最後一個節點緩衝區的2倍。
if (to_alloc <= EVBUFFER_CHAIN_MAX_AUTO_SIZE/2)//4096/2
to_alloc <<= 1;
//最後的大小還是有要插入的資料決定。要注意的是雖然to_alloc最後的值可能為
//datlen。但在evbuffer_chain_new中,實際分配的記憶體大小必然是512的倍數。
if (datlen > to_alloc)
to_alloc = datlen;
//此時需要new一個chain才能儲存本次要插入的資料
tmp = evbuffer_chain_new(to_alloc);
if (tmp == NULL)
goto done;
//連結串列最後那個節點還是可以放下一些資料的。那麼就先填滿連結串列最後那個節點
if (remain) {
memcpy(chain->buffer + chain->misalign + chain->off,
data, remain);
chain->off += remain;
buf->total_len += remain;
buf->n_add_for_cb += remain;
}
data += remain;//要插入的資料指標
datlen -= remain;
//把要插入的資料複製到新建一個chain中。
memcpy(tmp->buffer, data, datlen);
tmp->off = datlen;
//將這個chain插入到evbuffer中
evbuffer_chain_insert(buf, tmp);
buf->n_add_for_cb += datlen;
out:
evbuffer_invoke_callbacks(buf);//呼叫回撥函式
result = 0;
done:
EVBUFFER_UNLOCK(buf);//解鎖
return result;
}
可以看到,evbuffer_add函式是複製一份資料,儲存在連結串列中。這樣做的好處是,使用者呼叫該函式後,就可以丟棄該資料。讀者比較熟知的函式bufferevent_write就是直接呼叫這個函式。當用戶呼叫bufferevent_write後,就可以馬上把資料丟棄,無需等到Libevent把這份資料寫到socket的快取區中。
前面的程式碼是把資料存放到evbuffer_chain中,至於怎麼把evbuffer_chain插入到連結串列中,則是由函式evbuffer_chain_insert完成。
//buffer.c檔案
static void
evbuffer_chain_insert(struct evbuffer *buf,
struct evbuffer_chain *chain)
{
//新建evbuffer時是把整個evbuffer結構體都賦值0,
//並有buffer->last_with_datap = &buffer->first;
//所以*buf->last_with_datap就是first的值,所以一開始為NULL
if (*buf->last_with_datap == NULL) {
buf->first = buf->last = chain;
} else {
struct evbuffer_chain **ch = buf->last_with_datap;
/* Find the first victim chain. It might be *last_with_datap */
//(*ch)->off != 0表示該evbuffer_chain有資料了
//CHAIN_PINNED(*ch)則表示該evbuffer_chain不能被修改
//在連結串列中尋找到一個可以使用的evbuffer_chain.
//可以使用是指該chain沒有資料並且可以修改。
while ((*ch) && ((*ch)->off != 0 || CHAIN_PINNED(*ch)))
ch = &(*ch)->next;//取的還是next地址。 這樣看&((*ch)->next)更清晰
//在已有的連結串列中找不到一個滿足條件的evbuffer_chain。一般都是這種情況
if (*ch == NULL) {
/* There is no victim; just append this new chain. */
//此時buf->last指向的chain不再是最後了。因為last->next被賦值了
buf->last->next = chain;
if (chain->off)//要插入的這個chain是有資料的
buf->last_with_datap = &buf->last->next;//last_with_datap指向的是倒數第二個有資料的chain的next
} else {//這種情況得到的連結串列可以參考下圖
/* Replace all victim chains with this chain. */
//斷言,從這個節點開始,後面的說有節點都是沒有資料的
EVUTIL_ASSERT(evbuffer_chains_all_empty(*ch));
//釋放從這個節點開始的餘下連結串列節點
evbuffer_free_all_chains(*ch);
//把這個chain插入到最後
*ch = chain;
}
buf->last = chain;//重新設定last指標,讓它指向最後一個chain
}
buf->total_len += chain->off;
}
static void
evbuffer_free_all_chains(struct evbuffer_chain *chain)
{
struct evbuffer_chain *next;
for (; chain; chain = next) {//遍歷餘下的連結串列,刪除之
next = chain->next;
evbuffer_chain_free(chain);
}
}
static inline void
evbuffer_chain_free(struct evbuffer_chain *chain)
{
...//特殊buffer緩衝資料。一般的不用這些操作。直接釋放記憶體即可
mm_free(chain);
}
可以看到,evbuffer_chain_insert的插入並不是已經一個簡單的連結串列插入,還要檢測連結串列裡面是否有沒有資料(off為0)的節點。但這個buffer連結串列裡面會有這樣的節點嗎?其實是有這樣節點,這種節點一般是用於預留空間的。預留空間這個概念在STL中是很常見的,它的主要作用是使得當下次新增資料時,無需額外申請空間就能儲存資料。
預留buffer空間:
其中一個擴大預留空間的函式是evbuffer_expand。在講evbuffer_expand前,看一下如果存在沒有資料(off為0)的節點,連結串列又會是怎麼樣的。這涉及到last_with_data指標的指向,如下圖所示:
好了,現在來說一下evbuffer_expand。
//buffer.c檔案
int
evbuffer_expand(struct evbuffer *buf, size_t datlen)
{
struct evbuffer_chain *chain;
EVBUFFER_LOCK(buf);//加鎖
chain = evbuffer_expand_singlechain(buf, datlen);
EVBUFFER_UNLOCK(buf);//解釋
return chain ? 0 : -1;
}
該函式的作用是擴大連結串列的buffer空間,使得下次add一個長度為datlen的資料時,無需動態申請記憶體。
由於確保的是無需動態申請記憶體,所以假如這個連結串列本身還有大於datlen的空閒空間,那麼這個evbuffer_expand函式將不做任何操作。
如果這個連結串列的所有buffer空間都被用完了,那麼解決需要建立一個buffer為datlen的evbuffer_chain,然後把這個evbuffer_chain插入到連結串列最後面即可。此時這個evbuffer_chain的off就等於0了,也就出現了前面說的的那個問題。
如果連結串列的最後一個有資料chain還有一些空閒空間,但小於datlen。那麼就有點麻煩。evbuffer_expand 是呼叫evbuffer_expand_singlechain實現擴大空間的。而evbuffer_expand_singlechain函式有一個特點,預留空間datlen必須是在一個evbuffer_chain中,不能跨chain。該函式的返回值就指明瞭哪個chain預留了datlen空間。不能跨chain也就導致了一些麻煩事。
由於不能跨chain,但最後一個chain確實又還有一些空閒空間。前面的evbuffer_add函式會把連結串列的所有節點的buffer都填得滿滿的。這說明所有節點的buffer還是用完的好,比較統一。要明確的是,此種情況下,肯定是要新建一個evbuffer_chain插入到後面。
Libevent還是想把所有節點的buffer都填滿。如果最後一個chain的資料比較少,那麼就直接不要那個chain。當然chain上的資料還是要的。Libevent新建一個比datlen更大的chain,把最後一個chain上的資料遷移到這個新建的chain上。這樣就既能保證該chain節點也能填滿,也保證了預留空間datlen必須在是一個chain的。如果最後一個chain的資料比較多,Libevent就認為遷移不划算,那麼Libevent就讓這個chain最後留有一些空間不使用。
下面是該函式的程式碼展示了上面所說的:
//buffer.c檔案
#define MAX_TO_COPY_IN_EXPAND 4096
//計算evbuffer_chain的可用空間是多少
#define CHAIN_SPACE_LEN(ch) ((ch)->flags & EVBUFFER_IMMUTABLE ? \
0 : (ch)->buffer_len - ((ch)->misalign + (ch)->off))
static struct evbuffer_chain *
evbuffer_expand_singlechain(struct evbuffer *buf, size_t datlen)
{
struct evbuffer_chain *chain, **chainp;
struct evbuffer_chain *result = NULL;
ASSERT_EVBUFFER_LOCKED(buf);
chainp = buf->last_with_datap;
//*chainp指向最後一個有資料的evbuffer_chain或者為NULL
if (*chainp && CHAIN_SPACE_LEN(*chainp) == 0)//CHAIN_SPACE_LEN該chain可用空間的大小
chainp = &(*chainp)->next;
//經過上面的那個if後,當最後一個有資料的evbuffer_chain還有空閒空間時
//*chainp就指向之。否則*chainp指向最後一個有資料的evbuffer_chain的next。
chain = *chainp;
if (chain == NULL ||//這個chain是不可修改的,那麼就只能插入一個新的chain了
(chain->flags & (EVBUFFER_IMMUTABLE|EVBUFFER_MEM_PINNED_ANY))) {
goto insert_new;
}
if (CHAIN_SPACE_LEN(chain) >= datlen) {//這個chain的可用空間大於擴充套件空間
result = chain;
//這種情況,Libevent並不會擴大buffer空間.因為Libevent認為現在的可用空間可以用作使用者提出的預留空間
goto ok;
}
if (chain->off == 0) {//當前一個chain存滿了時,就會出現這種情況
goto insert_new;//插入一個新的chain
}
//通過使用misalign錯位空間,也能使得可用空間大於等於預留空間,那麼也不用
//擴大buffer空間
if (evbuffer_chain_should_realign(chain, datlen)) {
evbuffer_chain_align(chain);
result = chain;
goto ok;
}
//空閒空間小於總空間的1/8 或者 已有的資料量大於MAX_TO_COPY_IN_EXPAND(4096)
if (CHAIN_SPACE_LEN(chain) < chain->buffer_len / 8 ||
chain->off > MAX_TO_COPY_IN_EXPAND) {//4096
//本chain有比較多的資料,將這些資料遷移到另外一個chain是不划算的
//此時,將不會改變這個chain。
//下一個chain是否可以有足夠的空閒空間.有則直接用之
if (chain->next && CHAIN_SPACE_LEN(chain->next) >= datlen) {
result = chain->next;
goto ok;
} else {
goto insert_new;
}
} else {
//由於本chain的資料量比較小,所以把這個chain的資料遷移到另外一個
//chain上是值得的。
size_t length = chain->off + datlen;
struct evbuffer_chain *tmp = evbuffer_chain_new(length);
if (tmp == NULL)
goto err;
tmp->off = chain->off;
//進行資料遷移
memcpy(tmp->buffer, chain->buffer + chain->misalign,
chain->off);
EVUTIL_ASSERT(*chainp == chain);
result = *chainp = tmp;
if (buf->last == chain)
buf->last = tmp;
tmp->next = chain->next;
evbuffer_chain_free(chain);
goto ok;
}
insert_new:
result = evbuffer_chain_insert_new(buf, datlen);
if (!result)
goto err;
ok:
EVUTIL_ASSERT(result);
EVUTIL_ASSERT(CHAIN_SPACE_LEN(result) >= datlen);
err:
return result;
}
static inline struct evbuffer_chain *
evbuffer_chain_insert_new(struct evbuffer *buf, size_t datlen)
{
struct evbuffer_chain *chain;
if ((chain = evbuffer_chain_new(datlen)) == NULL)
return NULL;
evbuffer_chain_insert(buf, chain);
return chain;
}
上面程式碼中evbuffer_expand_singlechain函式的第一個if語句,可以聯合前面的兩張圖一起看,更容易看懂。
evbuffer_expand_singlechain函式是要求一個節點就能提供大小為datlen的可用空間。其實Libevent還提供了_evbuffer_expand_fast函式,該函式還有一個整型的引數n,用來表示使用不超過n個節點的前提下,提供datlen的可用空間。不過這個函式只留給Libevent內部使用,使用者不能使用之。
//buffer.c檔案
int//用最多不超過n個節點就提供datlen大小的空閒空間。連結串列過長是不好的
_evbuffer_expand_fast(struct evbuffer *buf, size_t datlen, int n)
{
struct evbuffer_chain *chain = buf->last, *tmp, *next;
size_t avail;
int used;
EVUTIL_ASSERT(n >= 2); //n必須大於等於2
//最後一個節點是不可用的
if (chain == NULL || (chain->flags & EVBUFFER_IMMUTABLE)) {
//這種情況下,直接新建一個足夠大的evbuffer_chain即可
chain = evbuffer_chain_new(datlen);
if (chain == NULL)
return (-1);
evbuffer_chain_insert(buf, chain);
return (0);
}
used = 0; /* number of chains we're using space in. */
avail = 0; /* how much space they have. */
for (chain = *buf->last_with_datap; chain; chain = chain->next) {
if (chain->off) {//最後一個有資料的節點的可用空間也是要被使用
size_t space = (size_t) CHAIN_SPACE_LEN(chain);
EVUTIL_ASSERT(chain == *buf->last_with_datap);
if (space) {
avail += space;
++used;
}
} else {//連結串列中off為0的空buffer統統使用
/* No data in chain; realign it. */
chain->misalign = 0;
avail += chain->buffer_len;
++used;
}
if (avail >= datlen) {//連結串列中的節點的可用空間已經足夠了
return (0);
}
if (used == n)//到達了最大可以忍受的連結串列長度
break;
}
//前面的for迴圈,如果找夠了空閒空間,那麼是直接return。所以
//執行到這裡時,就說明還沒找到空閒空間。一般是因為連結串列後面的off等於0
//的節點已經被用完了都還不能滿足datlen
if (used < n) {
EVUTIL_ASSERT(chain == NULL);
//申請一個足夠大的evbuffer_chain,把空間補足
tmp = evbuffer_chain_new(datlen - avail);
if (tmp == NULL)
return (-1);
buf->last->next = tmp;
buf->last = tmp;
return (0);
} else { //used == n。把後面的n個節點都用了還是不夠datlen空間
//連結串列後面的n個節點都用上了,這個n個節點中,至少有n-1個節點的off等於
//0。n個節點都不夠,Libevent就認為這些節點都是飯桶,Libevent會統統刪除
//然後新建一個足夠大的evbuffer_chain。
//用來標誌該連結串列的所有節點都是off為0的。在這種情況下,將刪除所有的節點
int rmv_all = 0; /* True iff we removed last_with_data. */
chain = *buf->last_with_datap;
if (!chain->off) {
//這說明連結串列中的節點都是沒有資料的evbuffer_chain
EVUTIL_ASSERT(chain == buf->first);
rmv_all = 1;//標誌之
avail = 0;
} else {
//最後一個有資料的chain的可用空間的大小。這個空間是可以用上的
avail = (size_t) CHAIN_SPACE_LEN(chain);
chain = chain->next;
}
//chain指向第一個off等於0的evbuffer_chain 或者等於NULL
//將這些off等於0的evbuffer_chain統統free掉,不要了。
//然後new一個足夠大的evbuffer_chain即可。這能降低連結串列的長度
for (; chain; chain = next) {
next = chain->next;
EVUTIL_ASSERT(chain->off == 0);
evbuffer_chain_free(chain);
}
//new一個足夠大的evbuffer_chain
tmp = evbuffer_chain_new(datlen - avail);
if (tmp == NULL) {//new失敗
if (rmv_all) {//這種情況下,該連結串列就根本沒有節點了
ZERO_CHAIN(buf);//相當於初始化evbuffer的連結串列
} else {
buf->last = *buf->last_with_datap;
(*buf->last_with_datap)->next = NULL;
}
return (-1);
}
if (rmv_all) {//這種情況下,該連結串列就只有一個節點了
buf->first = buf->last = tmp;
buf->last_with_datap = &buf->first;
} else {
(*buf->last_with_datap)->next = tmp;
buf->last = tmp;
}
return (0);
}
}
在連結串列頭新增資料:
前面的evbuffer_add是在連結串列尾部追加資料,Libevent提供了另外一個函式evbuffer_prepend可以在連結串列頭部新增資料。在這個函式裡面可以看到evbuffer_chain結構體成員misalign的一些使用,也能知道為什麼會有這個成員。
evbuffer_prepend函式並不複雜,只需弄懂misalign的作用就很容易明白該函式的實現。考慮這種情況:要在連結串列頭插入資料,那麼應該new一個新的evbuffer_chain,然後把要插入的資料放到這個新建個的evbuffer_chain中。但evbuffer_chain_new申請到的buffer空間可能會大於要插入的資料長度。插入資料後,buffer就必然會剩下一些空閒空間。那麼這個空閒空間放在buffer的前面好還是後面好呢?Libevent認為放在前面會好些,此時misalign就有用了。它表示錯開不用的空間,也就是空閒空間。如果再次在連結串列頭插入資料,就可以使用到這些空閒空間了。所以,misalign也可以認為是空閒空間,可以隨時使用。
//buffer.c檔案
int
evbuffer_prepend(struct evbuffer *buf, const void *data, size_t datlen)
{
struct evbuffer_chain *chain, *tmp;
int result = -1;
EVBUFFER_LOCK(buf);
//凍結緩衝區頭部,禁止在頭部新增資料
if (buf->freeze_start) {
goto done;
}
chain = buf->first;
//該連結串列暫時還沒有節點
if (chain == NULL) {
chain = evbuffer_chain_new(datlen);
if (!chain)
goto done;
evbuffer_chain_insert(buf, chain);
}
if ((chain->flags & EVBUFFER_IMMUTABLE) == 0) {//該chain可以修改
/* If this chain is empty, we can treat it as
* 'empty at the beginning' rather than 'empty at the end' */
if (chain->off == 0)
chain->misalign = chain->buffer_len;
//考慮這種情況:一開始chain->off等於0,之後呼叫evbuffer_prepend插入
//一些資料(還沒填滿這個chain),之後再次呼叫evbuffer_prepend插入一些
//資料。這樣就能分別進入下面的if else了
if ((size_t)chain->misalign >= datlen) {//空閒空間足夠大
memcpy(chain->buffer + chain->misalign - datlen,
data, datlen);
chain->off += datlen;
chain->misalign -= datlen;
buf->total_len += datlen;
buf->n_add_for_cb += datlen;
goto out;
} else if (chain->misalign) {//不夠大,但也要用
memcpy(chain->buffer,//用完這個chain,所以從頭開始
(char*)data + datlen - chain->misalign,
(size_t)chain->misalign);
chain->off += (size_t)chain->misalign;
buf->total_len += (size_t)chain->misalign;
buf->n_add_for_cb += (size_t)chain->misalign;
datlen -= (size_t)chain->misalign;
chain->misalign = 0;
}
}
//為datlen申請一個evbuffer_chain。把datlen長的資料放到這個新建的chain
if ((tmp = evbuffer_chain_new(datlen)) == NULL)
goto done;
buf->first = tmp;
if (buf->last_with_datap == &buf->first)
buf->last_with_datap = &tmp->next;
tmp->next = chain;
tmp->off = datlen;
tmp->misalign = tmp->buffer_len - datlen;
memcpy(tmp->buffer + tmp->misalign, data, datlen);
buf->total_len += datlen;
buf->n_add_for_cb += (size_t)chain->misalign;
out:
evbuffer_invoke_callbacks(buf);//呼叫回撥函式
result = 0;
done:
EVBUFFER_UNLOCK(buf);
return result;
}
讀取資料:
現在來看一下怎麼從evbuffer中複製一些資料。Libevent提供了函式evbuffer_copyout用來複制evbuffer的資料。當然是從連結串列的前面開始複製。
//buffer.c檔案
ev_ssize_t
evbuffer_copyout(struct evbuffer *buf, void *data_out, size_t datlen)
{
struct evbuffer_chain *chain;
char *data = data_out;
size_t nread;
ev_ssize_t result = 0;
EVBUFFER_LOCK(buf);
chain = buf->first;
if (datlen >= buf->total_len)
datlen = buf->total_len;//最大能提供的資料
if (datlen == 0)
goto done;
//凍結緩衝區頭部,禁止讀取緩衝區的資料
if (buf->freeze_start) {
result = -1;
goto done;
}
nread = datlen;
while (datlen && datlen >= chain->off) {
memcpy(data, chain->buffer + chain->misalign, chain->off);
data += chain->off;
datlen -= chain->off;
chain = chain->next;
}
if (datlen) {
memcpy(data, chain->buffer + chain->misalign, datlen);
}
result = nread;
done:
EVBUFFER_UNLOCK(buf);
return result;
}
這個函式邏輯比較簡單,這裡就不多講了。
有時我們不僅僅想複製資料,還想刪除資料,或者是複製後就刪除資料。這些操作在socket程式設計中還是很常見的。
//buffer.c檔案
int
evbuffer_drain(struct evbuffer *buf, size_t len)
{
struct evbuffer_chain *chain, *next;
size_t remaining, old_len;
int result = 0;
EVBUFFER_LOCK(buf);
old_len = buf->total_len;
if (old_len == 0)
goto done;
//凍結緩衝區頭部,禁止刪除頭部資料
if (buf->freeze_start) {
result = -1;
goto done;
}
//要刪除的資料量大於等於已有的資料量。並且這個evbuffer是可以刪除的
if (len >= old_len && !HAS_PINNED_R(buf)) {
len = old_len;
for (chain = buf->first; chain != NULL; chain = next) {
next = chain->next;
evbuffer_chain_free(chain);
}
ZERO_CHAIN(buf);//相當於初試化evbuffer的連結串列
} else {
if (len >= old_len)
len = old_len;
buf->total_len -= len;
remaining = len;
for (chain = buf->first;
remaining >= chain->off;
chain = next) {
next = chain->next;
remaining -= chain->off;
//已經刪除到最後一個有資料的evbuffer_chain了
if (chain == *buf->last_with_datap) {
buf->last_with_datap = &buf->first;
}
//刪除到倒數第二個有資料的evbuffer_chain
if (&chain->next == buf->last_with_datap)
buf->last_with_datap = &buf->first;
//這個chain被固定了,不能刪除
if (CHAIN_PINNED_R(chain)) {
EVUTIL_ASSERT(remaining == 0);
chain->misalign += chain->off;
chain->off = 0;
break;//後面的evbuffer_chain也是固定的
} else
evbuffer_chain_free(chain);
}
buf->first = chain;
if (chain) {
chain->misalign += remaining;
chain->off -= remaining;
}
}
evbuffer_invoke_callbacks(buf);//因為刪除資料,所以也要呼叫回撥函式
done:
EVBUFFER_UNLOCK(buf);
return result;
}
int
evbuffer_remove(struct evbuffer *buf, void *data_out, size_t datlen)
{
ev_ssize_t n;
EVBUFFER_LOCK(buf);
n = evbuffer_copyout(buf, data_out, datlen);
if (n > 0) {
if (evbuffer_drain(buf, n)<0)
n = -1;
}
EVBUFFER_UNLOCK(buf);
return (int)n;
}
可以看到evbuffer_remove是先複製資料,然後才刪除evbuffer的資料。而evbuffer_drain則直接刪除evbuffer的資料,而不會複製。
本文來自 luotuo44 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/luotuo44/article/details/39290721?utm_source=copy