1. 程式人生 > >Libevent原始碼分析-----evbuffer結構與基本操作

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。在連結串列尾部插入資料,分下面幾種情況:

  1. 該連結串列為空,即這是第一次插入資料。這是最簡單的,直接把新建的evbuffer_chain插入到連結串列中,通過呼叫evbuffer_chain_insert。
  2. 連結串列的最後一個節點(即evbuffer_chain)還有一些空餘的空間,放得下本次要插入的資料。此時直接把資料追加到最後一個節點即可。
  3. 連結串列的最後一個節點並不能放得下本次要插入的資料,那麼就需要把本次要插入的資料分開由兩個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