(十六)evbuffers緩衝區(下)

前言

在上節中,我們介紹了evbuffers的部分知識,在本小節中,我們將對剩餘的部分一起分析總結。

evbuffer_drain

在上節中分析的evbuffer_add_buffer中,如果添加源緩衝區到目的緩衝區結尾成功,則會調用evbuffer_drain清除源緩衝區,但是它的功能除了清除整個有效緩衝區以外,還可以清除一部分,接下來我們來看看它的源碼:

void
evbuffer_drain(struct evbuffer *buf, size_t len)
{
    size_t oldoff = buf->off;
    /* 要清除的緩衝區長度大於了有效緩衝區的長度
     * 則將有效緩衝區全部清除
     * 否則只清除len長度的有效緩衝區
     */
    if (len >= buf->off) {
        buf->off = 0;
        buf->buffer = buf->orig_buffer;
        buf->misalign = 0;
        /* 難得一見的goto語句
         * 但是這裏好像可以用if-else替代
         */
        goto done;
    }
    /* 縮小有效緩衝區的大小 */
    buf->buffer += len;
    buf->misalign += len;

    buf->off -= len;

 done:
    /* Tell someone about changes in this buffer */
    /* 隨着上面的一頓操作,緩衝區的大小如果已經改變,則回調處理函數 */
    if (buf->off != oldoff && buf->cb != NULL)
        (*buf->cb)(buf, oldoff, buf->off, buf->cbarg);

}

在當時調用evbuffer_add_buffer時,傳遞給evbuffer_drainlen是等與buf->off的,所以會清除整個緩衝區的大小,但是其實這個函數的作用是清除指定大小的有效緩衝區。邏輯也很簡單:

  1. len的長度是否大於等於當前有效緩衝區的長度
  2. 如果大於等於,則清除整個有效緩衝區
  3. 否則清除指定大小的有效緩衝區(將buffer後移,調整off)
  4. 如果滿足緩衝區大小被改變的條件,則調用回調函數

下面介紹一下evbuffer_remove,它和evbuffer_drain關係密切(畢竟裏面就調用了evbuffer_drain),只不過會將該緩衝區指定大小的內容讀出來,然後再消除緩衝區。代碼如下:

/* Reads data from an event buffer and drains the bytes read */
int
evbuffer_remove(struct evbuffer *buf, void *data, size_t datlen)
{
    size_t nread = datlen;
    //這裏也是處理了一下指定大小大於有效緩衝區大小的情況
    if (nread >= buf->off)    
        nread = buf->off;
    /* 將數據拷貝到data
     * 然後清除已讀的數據
     * 最後返回讀取的大小
     */
    memcpy(data, buf->buffer, nread);
    evbuffer_drain(buf, nread);    

    return (nread);
}

添加格式化字符串到緩衝區

除了普通的數據,將格式化的字符串添加到緩衝區需要特殊的處理。這部分主要是由evbuffer_add_printf以及evbuffer_add_vprintf完成。

int
evbuffer_add_printf(struct evbuffer *buf, const char *fmt, ...)  
{
    int res = -1;
    va_list ap;

    va_start(ap, fmt);
    res = evbuffer_add_vprintf(buf, fmt, ap);   //調用該函數實現
    va_end(ap);

    return (res);
}

這個函數涉及到C語言中可變參數的用法,具體過程有時間我再整理出來。
裏面主要調用的了evbuffer_add_vprintf函數,代碼如下:

int
evbuffer_add_vprintf(struct evbuffer *buf, const char *fmt, va_list ap)
{
    char *buffer;
    size_t space;
    size_t oldoff = buf->off;
    int sz;
    va_list aq;

    /* make sure that at least some space is available */
    //檢測有無64字節的有效空間,如果沒有,才分配
    evbuffer_expand(buf, 64);
    for (;;) {
        size_t used = buf->misalign + buf->off;
        buffer = (char *)buf->buffer + buf->off;
        assert(buf->totallen >= used);
        space = buf->totallen - used;

#ifndef va_copy
#define va_copy(dst, src)   memcpy(&(dst), &(src), sizeof(va_list))
#endif
        va_copy(aq, ap);

        //evutil_vsnprintf裏面主要就是調用vsnprintf  
        sz = evutil_vsnprintf(buffer, space, fmt, aq);

        va_end(aq);
        /* 將數據寫入緩衝區之後
         * 調整緩衝區大小
         * 調用相應的回調函數
         */
        if (sz < 0)
            return (-1);
        if ((size_t)sz < space) {
            buf->off += sz;
            if (buf->cb != NULL)
                (*buf->cb)(buf, oldoff, buf->off, buf->cbarg);
            return (sz);
        }
        //如果前面空間不夠,則嘗試擴展
        if (evbuffer_expand(buf, sz + 1) == -1)
            return (-1);

    }
    /* NOTREACHED */
}

這部分主要就是需要理解可變參數的用法,其餘的跟evbuffer_add的操作大同小異。

緩衝區的讀寫操作

接下來我們介紹緩衝區的讀寫操作,從簡到繁,先介紹寫相關的操作。

緩衝區的寫出操作(將緩衝區數據寫出)

緩衝區的寫操作主要由evbuffer_write實現,代碼如下

int
evbuffer_write(struct evbuffer *buffer, int fd)
{
        int n;
        //將緩衝區數據寫入fd
#ifndef WIN32
        n = write(fd, buffer->buffer, buffer->off);
#else
        n = send(fd, buffer->buffer, buffer->off, 0);
#endif
        if (n == -1)
                return (-1);
        if (n == 0)
                return (0);
        //數據被讀出之後,從緩衝區中清空讀取的n字節
        evbuffer_drain(buffer, n);

        return (n);
}

寫操作很簡單,就是將數據寫出去,然後將緩衝區對應的數據清除了就行了。

緩衝區的讀入操作(將指定的數據大小讀入緩衝區)

讀操作有幾個函數,首先先介紹evbuffer_read:

//最多一次性讀取4096個字節
#define EVBUFFER_MAX_READ       4096

int
evbuffer_read(struct evbuffer *buf, int fd, int howmuch)
{
        u_char *p;
        size_t oldoff = buf->off;
        int n = EVBUFFER_MAX_READ;

        /* 下面有個條件編譯嵌套,需要注意一下
         * #if defined(...)
         *    #ifdef WIN32
         *    ...
         *    #else
         *    ...
         *    #endif
         *    ...
         * #endif   
         */
         /* ioctl(fd, FIONREAD, &n)的作用是得到緩衝區爲有多少字節要被讀取,並將n賦值爲該數據
         * 下面的邏輯是:當獲取緩衝區大小失敗時,將大小賦值爲EVBUFFER_MAX_READ;獲取成功時,判斷其大小,如果過於大了,
         */
#if defined(FIONREAD)
#ifdef WIN32
        long lng = n;
        if (ioctlsocket(fd, FIONREAD, &lng) == -1 || (n=lng) <= 0) {
#else
        if (ioctl(fd, FIONREAD, &n) == -1 || n <= 0) {
#endif
                //該條語句如果執行是因爲ioctl函數調用失敗
                n = EVBUFFER_MAX_READ;
        } else if (n > EVBUFFER_MAX_READ && n > howmuch) {
                /*
                 * It's possible that a lot of data is available for
                 * reading.  We do not want to exhaust resources
                 * before the reader has a chance to do something
                 * about it.  If the reader does not tell us how much
                 * data we should read, we artifically limit it.
                 */
                /* 如果可讀緩衝區的大小比我們自己設置的緩衝區的4倍還大,那麼就將其設置爲我們的緩衝區的4倍大小*/
                if ((size_t)n > buf->totallen << 2)
                        n = buf->totallen << 2;
                if (n < EVBUFFER_MAX_READ)
                        n = EVBUFFER_MAX_READ;
        }
#endif  
        //要讀的字節數大於n或者小於0,將其限制爲n
        if (howmuch < 0 || howmuch > n)
                howmuch = n;

        /* If we don't have FIONREAD, we might waste some space here */
        if (evbuffer_expand(buf, howmuch) == -1)
                return (-1);

        /* We can append new data at this point */
        p = buf->buffer + buf->off;
        /* 將fd中的howmuch的數據讀入p中
         * p是有效緩衝區尾部
         */
#ifndef WIN32
        n = read(fd, p, howmuch);
#else
        n = recv(fd, p, howmuch, 0);
#endif
        if (n == -1)
                return (-1);
        if (n == 0)
                return (0);
        //收尾工作
        buf->off += n;

        /* Tell someone about changes in this buffer */
        if (buf->off != oldoff && buf->cb != NULL)
                (*buf->cb)(buf, oldoff, buf->off, buf->cbarg);
        //返回讀入了多少字節
        return (n);
}

需要注意,ioctlFIONREAD是用於得知當前有多少數據可讀,和我們自己構建的緩衝區是無關的,而是系統的緩衝區。以及這個讀操作,是將數據往緩衝區裏面讀,而不是讀出去。

緩衝區的讀出一行操作(將一行數據讀出緩衝區)

接下來是讀取一行的函數evbuffer_readline:

/*
 * Reads a line terminated by either '\r\n', '\n\r' or '\r' or '\n'.
 * The returned buffer needs to be freed by the called.
*/
//讀取一行直到遇到\r\n,\n\r,\r,\n這個幾個特殊的回車換行符
char *
evbuffer_readline(struct evbuffer *buffer)
{
 u_char *data = EVBUFFER_DATA(buffer);  //一個簡單的宏函數,即buffer->buffer
 size_t len = EVBUFFER_LENGTH(buffer);  //一個簡單的宏函數,即buffer->off
 char *line;
 unsigned int i;
 //獲取到一行的末尾位置
 for (i = 0; i < len; i++) {
   if (data[i] == '\r' || data[i] == '\n')
     break;
 }
 //掃描完了整個緩衝區,該行還沒結束,直接返回NULL
 if (i == len)
   return (NULL);
 //申請內存
 if ((line = malloc(i + 1)) == NULL) {
   fprintf(stderr, "%s: out of memory\n", __func__);
   return (NULL);
 }
 //將有效緩衝區的i大小的字節拷貝給line
 memcpy(line, data, i);
 //注意不要忘了結束符
 line[i] = '\0';

 /*
  * Some protocols terminate a line with '\r\n', so check for
  * that, too.
  */
 /* 如果判度是否是一行的符號是\r\n或\n\r,需要把後面的那個\n或\r讀取了,以防下次讀取數據的時候造成讀空行
  * 除非是兩個連續的\n或\r,證明是下一行是空行
  */
 if ( i < len - 1 ) {
   char fch = data[i], sch = data[i+1];

   /* Drain one more character if needed */
   if ( (sch == '\r' || sch == '\n') && sch != fch )
     i += 1;
 }
 /* 清除有效緩衝區中i+1字節大小,這裏加了一個1是因爲要把換行字符讀取了
  * 但是注意前面寫數據的時候,並未將換行字符一起寫入
  * 換行字符相當於被捨棄掉了
  */
 evbuffer_drain(buffer, i + 1);

 return (line);
}

evbuffer_readlineevbuffer_read這兩個函數雖然都是read,但是evbuffer_readline是將一行數據從緩衝區讀出,而evbuffer_read是將數據讀入,這點需要特別注意,不要被名字所迷惑了。而將數據從緩衝區讀出的操作其實是文章開頭我們講到的evbuffer_remove
evbuffer_readline函數沒有什麼特別需要注意的地方,唯一的就是需要將換行字符捨棄掉,如果是回車和換行符一起做爲換行字符,則這兩個都要捨棄。如果是連續兩個相同的換行字符,證明下一個是個空行,無需處理。

除了evbuffer_readline函數,還有一個evbuffer_readln函數的功能與其類似,其實evbuffer_readline在現今的libevent版本中已經不推薦使用了,而是使用evbuffer_readln,不過兩者的思想很類似。
在這裏,我只將你可能不理解的地方做個解釋。
enum evbuffer_eol_style定義如下:

enum evbuffer_eol_style {
        EVBUFFER_EOL_ANY, //任意數量的回車和換行字符中的任何序列
        EVBUFFER_EOL_CRLF, // \r\n或\n
        EVBUFFER_EOL_CRLF_STRICT, // \r\n
        EVBUFFER_EOL_LF // \n
};

這4個變量分別代表了不同的行結束方式。
其他地方應該沒什麼特殊的地方。
evbuffer_readln代碼如下:

char *
evbuffer_readln(struct evbuffer *buffer, size_t *n_read_out,
        enum evbuffer_eol_style eol_style)
{
    u_char *data = EVBUFFER_DATA(buffer);
    u_char *start_of_eol, *end_of_eol;
    size_t len = EVBUFFER_LENGTH(buffer);
    char *line;
    unsigned int i, n_to_copy, n_to_drain;

    if (n_read_out)
        *n_read_out = 0;

    /* depending on eol_style, set start_of_eol to the first character
     * in the newline, and end_of_eol to one after the last character. */
    switch (eol_style) {
    case EVBUFFER_EOL_ANY:
        for (i = 0; i < len; i++) {
            if (data[i] == '\r' || data[i] == '\n')
                break;
        }
        if (i == len)
            return (NULL);
        start_of_eol = data+i;
        ++i;
        for ( ; i < len; i++) {
            if (data[i] != '\r' && data[i] != '\n')
                break;
        }
        end_of_eol = data+i;
        break;
    case EVBUFFER_EOL_CRLF:
        end_of_eol = memchr(data, '\n', len);
        if (!end_of_eol)
            return (NULL);
        if (end_of_eol > data && *(end_of_eol-1) == '\r')
            start_of_eol = end_of_eol - 1;
        else
            start_of_eol = end_of_eol;
        end_of_eol++; /*point to one after the LF. */
        break;
    case EVBUFFER_EOL_CRLF_STRICT: {
        u_char *cp = data;
        while ((cp = memchr(cp, '\r', len-(cp-data)))) {
            if (cp < data+len-1 && *(cp+1) == '\n')
                break;
            if (++cp >= data+len) {
                cp = NULL;
                break;
            }
        }
        if (!cp)
            return (NULL);
        start_of_eol = cp;
        end_of_eol = cp+2;
        break;
    }
    case EVBUFFER_EOL_LF:
        start_of_eol = memchr(data, '\n', len);
        if (!start_of_eol)
            return (NULL);
        end_of_eol = start_of_eol + 1;
        break;
    default:
        return (NULL);
    }

    n_to_copy = start_of_eol - data;
    n_to_drain = end_of_eol - data;

    if ((line = malloc(n_to_copy+1)) == NULL) {
        event_warn("%s: out of memory\n", __func__);
        return (NULL);
    }

    memcpy(line, data, n_to_copy);
    line[n_to_copy] = '\0';

    evbuffer_drain(buffer, n_to_drain);
    if (n_read_out)
        *n_read_out = (size_t)n_to_copy;

    return (line);
}

最後還有一個evbuffer_find函數,它的作用是在緩衝區中搜索字符串的第一個匹配項,並返回一個指向它的指針,主要使用了memechr函數來查找第一個匹配項,沒什麼好說的。以及還有一個evbuffer_setcb,就是改變函數指針指向的函數,以及參數。

小結

在本小節中,我們將libevent緩衝區的剩餘部分進行了分析,主要就是緩衝區數據的讀入、讀出一行、寫出等操作。在下一小節中,我們將介紹bufferevent相關的知識,用來自動管理緩衝區。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章