Libevent 的 evbuffer 功能实现了一个字节队列,针对将数据添加到末尾和从前面删除数据进行了优化。

Evbuffers 通常用于执行缓冲网络 IO 的“缓冲”部分。它们不提供调度 IO 或在 IO 准备好时触发 IO 的功能:这是 bufferevents 所做的功能。

除非特别说明,本章的所有函数声明在event2/buffer.h头文件中。

创建或释放evbuffer

struct evbuffer *evbuffer_new(void);
void evbuffer_free(struct evbuffer *buf);

evbuffer_new()分配内存并且返回一个新的空的evbuffer,evbuffer_free()释放并且清除evbuffer的内容。

evbuffer与线程安全

int evbuffer_enable_locking(struct evbuffer *buf, void *lock);
void evbuffer_lock(struct evbuffer *buf);
void evbuffer_unlock(struct evbuffer *buf);

默认情况下,同时在多个线程中访问evbuffer并不是安全的。如果需要在多线程中访问,你需要在evbuffer上调用evbuffer_enable_locking(),如果lock参数为NULL,libevent使用使用evthread_set_lock_creation_callback函数的锁创建函数来分配一个新的锁。如果lock参数不为空的话,则用之作为锁。

evbuffer_lock()evbuffer_unlock()函数分别获取和释放锁。你可以使用它们来进行一系列的原子操作。如果evbuffer没有启用加锁,则这些函数调用无效。

(注意,单个操作无须调用evbuffer_lock()evbuffer_unlock(),如果evbuffer启用了加锁,单个操作已经是原子的了。只有在多线程同时有多个操作的情况下手动锁定evbuffer。)

检查evbuffer

size_t evbuffer_get_length(const struct evbuffer *buf);

该函数返回存储在evbuffer中的字节数。

size_t evbuffer_get_contiguous_space(const struct evbuffer *buf);

该函数返回evbuffer中前面连续的字节数。evbuffer的字节可能存储在多个分散的内存块中,这个函数返回第一个内存块中连续的字节数。

向evbuffer添加数据

int evbuffer_add(struct evbuffer *buf, const void *data, size_t datlen);

该函数将以data起始的datlen长度的数据添加到buf的尾部。

int evbuffer_add_printf(struct evbuffer *buf, const char *fmt, ...)
int evbuffer_add_vprintf(struct evbuffer *buf, const char *fmt, va_list ap);

上述两个函数向buf的尾部添加格式化的数据,fmt参数及后续的参数同C函数的printfvprintf,这两个函数均返回添加的字节数。

int evbuffer_expand(struct evbuffer *buf, size_t datlen);

此函数修改缓冲区的最后一个内存块,或者新增一个内存块,使evbuffer不用再进行分配内存便可以存入datlen长度的数据。

/* Here are two ways to add "Hello world 2.0.1" to a buffer. */
/* Directly: */
evbuffer_add(buf, "Hello world 2.0.1", 17);

/* Via printf: */
evbuffer_add_printf(buf, "Hello %s %d.%d.%d", "world", 2, 0, 1);

从一个evbuffer移动数据到另一个evbuffer

为了提高效率,libevent优化了从一个evbuffer移动数据到另一个evbuffer的功能。

int evbuffer_add_buffer(struct evbuffer *dst, struct evbuffer *src);
int evbuffer_remove_buffer(struct evbuffer *src, struct evbuffer *dst,
    size_t datlen);

evbuffer_add_buffer()函数把src中的所有数据移动到dst中。成功时返回0,失败时返回-1.

evbuffer_remove_buffer()函数把src中datlen长度字节的数据移动到dst中,尽可能少的减少复制。如果src中的数据少于datlen,则将src中的数据全部移动到dst中。该函数返回移动的字节数。

向evbuffer的头部添加数据

int evbuffer_prepend(struct evbuffer *buf, const void *data, size_t size);
int evbuffer_prepend_buffer(struct evbuffer *dst, struct evbuffer* src);

这两个函数和evbuffer_add()evbuffer_add_buffer()类似,不过这两个函数向evbuffer的头部添加数据。

重新调整evbuffer的内部布局

有时你想要检查evbuffer的前N个字节的数据,并将其视为连续的字节数组。为了达到此目的,你首先要确保evbuffer的前端确实是连续的。

unsigned char *evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size);

evbuffer_pullup()函数线性化buf的前size字节,如果需要的话,进行数据的拷贝和移动以确保它们在同一块连续的内存块上。如果size参数为负数,此函数线性化整个buf,如果size大于buf的长度,则返回NULL。正常情况下,evbuffer_pullup()函数返回第一个字节的指针。

当给size指定很大的值的时候调用此函数,可能会很慢,因为可能需要拷贝整个缓冲区的内容。

#include <event2/buffer.h>
#include <event2/util.h>

#include <string.h>

int parse_socks4(struct evbuffer *buf, ev_uint16_t *port, ev_uint32_t *addr)
{
    /* Let's parse the start of a SOCKS4 request!  The format is easy:
     * 1 byte of version, 1 byte of command, 2 bytes destport, 4 bytes of
     * destip. */
    unsigned char *mem;

    mem = evbuffer_pullup(buf, 8);

    if (mem == NULL) {
        /* Not enough data in the buffer */
        return 0;
    } else if (mem[0] != 4 || mem[1] != 1) {
        /* Unrecognized protocol or command */
        return -1;
    } else {
        memcpy(port, mem+2, 2);
        memcpy(addr, mem+4, 4);
        *port = ntohs(*port);
        *addr = ntohl(*addr);
        /* Actually remove the data from the buffer now that we know we
           like it. */
        evbuffer_drain(buf, 8);
        return 1;
    }
}

注意:以evbuffer_get_contiguous_space()返回值为参数调用evbuffer_pullup()并不会导致任何数据的移动和拷贝。

从evbuffer中移除数据

int evbuffer_drain(struct evbuffer *buf, size_t len);
int evbuffer_remove(struct evbuffer *buf, void *data, size_t datlen);

evbuffer_remove()函数拷贝datlen字节的数据值data中,并且移除原来的数据。如果可用的长度小于datlen,则此函数拷贝所有的字节。该函数失败时返回-1,成功时返回拷贝的字节数。

evbuffer_drain()evbuffer_remove()函数类似。但是它只是移除数据,并不进行拷贝。该函数在成功时返回0,失败时返回-1.

从evbuffer中拷贝数据

有时你只想从evbuffer的起始位置拷贝数据,但是并不想移除。例如,你只想看看是否到了一个完整的记录,而不想移除或者重新排列缓冲区。

ev_ssize_t evbuffer_copyout(struct evbuffer *buf, void *data, size_t datlen);
ev_ssize_t evbuffer_copyout_from(struct evbuffer *buf,
     const struct evbuffer_ptr *pos,
     void *data_out, size_t datlen);

evbuffer_copyout()evbuffer_remove()类似,但是并不从缓冲区中移除数据。他将datlen字节的数据拷贝到data所在的内存中,如果buf中长度小于datlen,该函数拷贝所有的字节数。该函数在成功时返回拷贝的字节数,在失败时返回-1.

evbuffer_copyout_from()函数和evbuffer_copyout()函数类似,但是它不是从buf的起始位置拷贝数据,而是从pos开始的位置进行拷贝。有关evbuffer_ptr的更多信息,请参考“搜索evbuffer”小节。

如果从evbuffer中拷贝数据太慢了,请使用evbuffer_peek()函数代替。

#include <event2/buffer.h>
#include <event2/util.h>
#include <stdlib.h>
#include <stdlib.h>

int get_record(struct evbuffer *buf, size_t *size_out, char **record_out)
{
    /* Let's assume that we're speaking some protocol where records
       contain a 4-byte size field in network order, followed by that
       number of bytes.  We will return 1 and set the 'out' fields if we
       have a whole record, return 0 if the record isn't here yet, and
       -1 on error.  */
    size_t buffer_len = evbuffer_get_length(buf);
    ev_uint32_t record_len;
    char *record;

    if (buffer_len < 4)
       return 0; /* The size field hasn't arrived. */

   /* We use evbuffer_copyout here so that the size field will stay on
       the buffer for now. */
    evbuffer_copyout(buf, &record_len, 4);
    /* Convert len_buf into host order. */
    record_len = ntohl(record_len);
    if (buffer_len < record_len + 4)
        return 0; /* The record hasn't arrived */

    /* Okay, _now_ we can remove the record. */
    record = malloc(record_len);
    if (record == NULL)
        return -1;

    evbuffer_drain(buf, 4);
    evbuffer_remove(buf, record, record_len);

    *record_out = record;
    *size_out = record_len;
    return 1;
}

面向行的输入

enum evbuffer_eol_style {
        EVBUFFER_EOL_ANY,
        EVBUFFER_EOL_CRLF,
        EVBUFFER_EOL_CRLF_STRICT,
        EVBUFFER_EOL_LF,
        EVBUFFER_EOL_NUL
};
char *evbuffer_readln(struct evbuffer *buffer, size_t *n_read_out,
    enum evbuffer_eol_style eol_style);

许多互联网协议使用了基于行的协议。evbuffer_readln()函数同evbuffer的头部提取一行,并且返回新的以NUL结尾的字符串。如果n_read_out非空,*n_read_out被设置为返回的字符串的长度。如果没有一整行可以读取,该函数返回NULL,行结束符不包括在返回的字符串中。

该函数理解4种类型的行结束符:

  • EVBUFFER_EOL_LF

    单个的回车符(\n,ASCII码为0x0A)

  • EVBUFFER_EOL_CRLF_STRICT

    回车换行(\r\n,ASCII码为0x0D 0x0A)

  • EVBUFFER_EOL_CRLF

    可选的回车,然后时换行(\r\n或者\n),这种格式在解析互联网协议的时候很有用,因为标准通常规定了\r\n为行结束符,但是有些非标准使用的是\n。

  • EVBUFFER_EOL_ANY

    任意数量、任意序列的回车换行符,这种格式不太常用,主要是为了向后兼容

  • EVBUFFER_EOL_NUL

    单字节的0,ASCII为NUL。

注意:如果您使用 event_set_mem_functions() 覆盖默认 malloc,则 evbuffer_readln 返回的字符串将由您指定的 malloc-replacement 分配。

char *request_line;
size_t len;

request_line = evbuffer_readln(buf, &len, EVBUFFER_EOL_CRLF);
if (!request_line) {
    /* The first line has not arrived yet. */
} else {
    if (!strncmp(request_line, "HTTP/1.0 ", 9)) {
        /* HTTP 1.0 detected ... */
    }
    free(request_line);
}

搜索evbuffer

evbuffer_ptr 结构指向 evbuffer 中的一个位置,并包含可用于遍历 evbuffer 的数据。

struct evbuffer_ptr {
        ev_ssize_t pos;
        struct {
                /* internal fields */
        } _internal;
};

pos字段是唯一的公开字段,其余的字段使用者不应该调用,它代表着一个evbuffer从起始位置的偏移量。

struct evbuffer_ptr evbuffer_search(struct evbuffer *buffer,
    const char *what, size_t len, const struct evbuffer_ptr *start);
struct evbuffer_ptr evbuffer_search_range(struct evbuffer *buffer,
    const char *what, size_t len, const struct evbuffer_ptr *start,
    const struct evbuffer_ptr *end);
struct evbuffer_ptr evbuffer_search_eol(struct evbuffer *buffer,
    struct evbuffer_ptr *start, size_t *eol_len_out,
    enum evbuffer_eol_style eol_style);

evbuffer_search()函数在evbuffer中搜索长度为len的what字符串。如果找到了,则返回evbuffer_ptr,包含了字符串所在的位置,如果没找到的话,返回-1。如果指定了start参数,则从此start参数所在的位置开始搜索,否则从evbuffer的起始位置开始搜索。

evbuffer_search_range()函数和evbuffer_search()一样,除了只搜索end之前的位置。

evbuffer_search_eol()函数想evbuffer_readln()函数一样检测行结束符。但是并不拷贝行数据,仅仅返回evbuffer_ptr指向行结束符的起始位置,如果eol_len_out参数为非空,则将其设置为EOL字符串的长度。

enum evbuffer_ptr_how {
        EVBUFFER_PTR_SET,
        EVBUFFER_PTR_ADD
};
int evbuffer_ptr_set(struct evbuffer *buffer, struct evbuffer_ptr *pos,
    size_t position, enum evbuffer_ptr_how how);

evbuffer_ptr_set 函数操作缓冲区内 evbuffer_ptr pos 的位置。如果是 EVBUFFER_PTR_SET,则指针移动到缓冲区内的绝对位置。如果是 EVBUFFER_PTR_ADD,则指针向前移动位置字节。此函数在成功时返回 0,在失败时返回 -1。

#include <event2/buffer.h>
#include <string.h>

/* Count the total occurrences of 'str' in 'buf'. */
int count_instances(struct evbuffer *buf, const char *str)
{
    size_t len = strlen(str);
    int total = 0;
    struct evbuffer_ptr p;

    if (!len)
        /* Don't try to count the occurrences of a 0-length string. */
        return -1;

    evbuffer_ptr_set(buf, &p, 0, EVBUFFER_PTR_SET);

    while (1) {
         p = evbuffer_search(buf, str, len, &p);
         if (p.pos < 0)
             break;
         total++;
         evbuffer_ptr_set(buf, &p, 1, EVBUFFER_PTR_ADD);
    }

    return total;
}

检测数据,而不复制数据

有时你想读取数据,但是并不进行数据的读取(evbuffer_copy()),也不进行数据的重组(evbuffer_pullup()),有时你只想查看evbuffer中间的一些数据。

struct evbuffer_iovec {
        void *iov_base;
        size_t iov_len;
};

int evbuffer_peek(struct evbuffer *buffer, ev_ssize_t len,
    struct evbuffer_ptr *start_at,
    struct evbuffer_iovec *vec_out, int n_vec);

当你调用此函数时,vec_out参数代表一个数组,n_vec代表数组中元素的个数。该函数填充此数组,使数组元素中的iov_base指向evbuffer中内存块的指针,iov_len代表内存块的长度。

如果len参数小于0,evbuffer_peek()尝试填充你提供的数组的所有元素。如果len参数不为0,则数组中元素所指向的内存块至少包含len字节。如果你提供了数组及长度字段,该函数返回所填充的数组的个数,否则返回需要的数组元素的个数。

当ptr参数为NULL时,evbuffer_peek()从buffer的起始位置开始遍历,否则从ptr指向的位置开始遍历。

{
    /* Let's look at the first two chunks of buf, and write them to stderr. */
    int n, i;
    struct evbuffer_iovec v[2];
    n = evbuffer_peek(buf, -1, NULL, v, 2);
    for (i=0; i<n; ++i) { /* There might be less than two chunks available. */
        fwrite(v[i].iov_base, 1, v[i].iov_len, stderr);
    }
}

{
    /* Let's send the first 4906 bytes to stdout via write. */
    int n, i, r;
    struct evbuffer_iovec *v;
    size_t written = 0;

    /* determine how many chunks we need. */
    n = evbuffer_peek(buf, 4096, NULL, NULL, 0);
    /* Allocate space for the chunks.  This would be a good time to use
       alloca() if you have it. */
    v = malloc(sizeof(struct evbuffer_iovec)*n);
    /* Actually fill up v. */
    n = evbuffer_peek(buf, 4096, NULL, v, n);
    for (i=0; i<n; ++i) {
        size_t len = v[i].iov_len;
        if (written + len > 4096)
            len = 4096 - written;
        r = write(1 /* stdout */, v[i].iov_base, len);
        if (r<=0)
            break;
        /* We keep track of the bytes written separately; if we don't,
           we may write more than 4096 bytes if the last chunk puts
           us over the limit. */
        written += len;
    }
    free(v);
}

{
    /* Let's get the first 16K of data after the first occurrence of the
       string "start\n", and pass it to a consume() function. */
    struct evbuffer_ptr ptr;
    struct evbuffer_iovec v[1];
    const char s[] = "start\n";
    int n_written;

    ptr = evbuffer_search(buf, s, strlen(s), NULL);
    if (ptr.pos == -1)
        return; /* no start string found. */

    /* Advance the pointer past the start string. */
    if (evbuffer_ptr_set(buf, &ptr, strlen(s), EVBUFFER_PTR_ADD) < 0)
        return; /* off the end of the string. */

    while (n_written < 16*1024) {
        /* Peek at a single chunk. */
        if (evbuffer_peek(buf, -1, &ptr, v, 1) < 1)
            break;
        /* Pass the data to some user-defined consume function */
        consume(v[0].iov_base, v[0].iov_len);
        n_written += v[0].iov_len;

        /* Advance the pointer so we see the next chunk next time. */
        if (evbuffer_ptr_set(buf, &ptr, v[0].iov_len, EVBUFFER_PTR_ADD)<0)
            break;
    }
}

注意

  • 修改evbuffer_iovec指向的数据将导致未定义的行为。
  • 如果调用任何修改 evbuffer 的函数,则 evbuffer_peek() 产生的指针可能会变得无效。
  • 如果evbuffer需要在多线程中使用,确保调用evbuffer_peek()函数之前先调用evbuffer_lock(),并在调用结束时调用evbuffer_unlock()

向evbuffer中直接添加数据

有时你想直接往evbuffer中直接添加数据,无需先将其写入字符数组,然后再调用evbuffer_add()进行拷贝。你可以调用一对高级的函数来达到此功能:

  • evbuffer_reserve_space()
  • evbuffer_commit_space()

就像evbuffer_peek()一样,这些函数使用evbuffer_iovec结构,来提供对evbuffer中的内存进行直接的访问。

int evbuffer_reserve_space(struct evbuffer *buf, ev_ssize_t size,
    struct evbuffer_iovec *vec, int n_vecs);
int evbuffer_commit_space(struct evbuffer *buf,
    struct evbuffer_iovec *vec, int n_vecs);

evbuffer_reserve_space()函数返回evbuffer内部的指针,它根据需要决定是否扩展缓冲区,以便足以容纳size字节。vec中的指针指向这些扩展,n_vecs为数组的长度。

n_vecs的值最小必须为1,如果你只提供了一个vector,libevent将确保你所请求的空间分配在单个连续的扩展中,但是为了达到此目的,可能会重新排列内存、内存浪费。为了更好的性能,请最少提供两个vector,该函数返回你请求的空间所需的提供vector的数量。

在你调用 evbuffer_commit_space() 之前,您写入这些vector的数据不是缓冲区的一部分,这实际上使你写入的数据算作在缓冲区中。如果你想提交比你要求的更少的空间,你可以减少任何给定的 evbuffer_iovec 结构中的 iov_len 字段。您还可以传回比给定的更少的vector。 evbuffer_commit_space() 函数在成功时返回 0,在失败时返回 -1。

注意

  • 调用任何重新排列 evbuffer 或向evbuffer 添加数据的函数,将使您从 evbuffer_reserve_space() 获得的指针无效。
  • 在当前实现中,evbuffer_reserve_space()从不使用两个以上的vector,而不论用户提供了多少。这可能会在未来的版本中改变。
  • 不论调用多少次evbuffer_reserve_space(),总是安全的。
  • 如果你的evbuffer可能在多线程中使用,确保在调用evbuffer_reserve_space()函数之前调用evbuffer_lock(),并在提交之后解锁。
/* Suppose we want to fill a buffer with 2048 bytes of output from a
   generate_data() function, without copying. */
struct evbuffer_iovec v[2];
int n, i;
size_t n_to_add = 2048;

/* Reserve 2048 bytes.*/
n = evbuffer_reserve_space(buf, n_to_add, v, 2);
if (n<=0)
   return; /* Unable to reserve the space for some reason. */

for (i=0; i<n && n_to_add > 0; ++i) {
   size_t len = v[i].iov_len;
   if (len > n_to_add) /* Don't write more than n_to_add bytes. */
      len = n_to_add;
   if (generate_data(v[i].iov_base, len) < 0) {
      /* If there was a problem during data generation, we can just stop
         here; no data will be committed to the buffer. */
      return;
   }
   /* Set iov_len to the number of bytes we actually wrote, so we
      don't commit too much. */
   v[i].iov_len = len;
}

/* We commit the space here.  Note that we give it 'i' (the number of
   vectors we actually used) rather than 'n' (the number of vectors we
   had available. */
if (evbuffer_commit_space(buf, v, i) < 0)
   return; /* Error committing */

反例

/* Here are some mistakes you can make with evbuffer_reserve().
   DO NOT IMITATE THIS CODE. */
struct evbuffer_iovec v[2];

{
  /* Do not use the pointers from evbuffer_reserve_space() after
     calling any functions that modify the buffer. */
  evbuffer_reserve_space(buf, 1024, v, 2);
  evbuffer_add(buf, "X", 1);
  /* WRONG: This next line won't work if evbuffer_add needed to rearrange
     the buffer's contents.  It might even crash your program. Instead,
     you add the data before calling evbuffer_reserve_space. */
  memset(v[0].iov_base, 'Y', v[0].iov_len-1);
  evbuffer_commit_space(buf, v, 1);
}

{
  /* Do not modify the iov_base pointers. */
  const char *data = "Here is some data";
  evbuffer_reserve_space(buf, strlen(data), v, 1);
  /* WRONG: The next line will not do what you want.  Instead, you
     should _copy_ the contents of data into v[0].iov_base. */
  v[0].iov_base = (char*) data;
  v[0].iov_len = strlen(data);
  /* In this case, evbuffer_commit_space might give an error if you're
     lucky */
  evbuffer_commit_space(buf, v, 1);
}

evbuffer与网路IO

libevent中evbuffer最常见用例是网络IO,在evbuffer上的网络IO的接口是:

int evbuffer_write(struct evbuffer *buffer, evutil_socket_t fd);
int evbuffer_write_atmost(struct evbuffer *buffer, evutil_socket_t fd,
        ev_ssize_t howmuch);
int evbuffer_read(struct evbuffer *buffer, evutil_socket_t fd, int howmuch);

evbuffer_read()函数在指定的fd上读取howmuch字节的数据到buffer的尾部,如果成功的话,返回读取的字节数,如果遇到EOF返回0,如果遇到错误的话,返回-1。请注意,该错误可能表明非阻塞操作不会成功,你可能需要检查EAGAIN的错误码(在windows上位WSAEWOULDBLOCK),如果howmuch是负值,evbuffer_read()函数将自己猜测需要读取多少数据。

evbuffer_write_atmost()函数尝试从buffer的头部,取出howmuch字节长度的数据,写入fd,在成功时返回实际写入的字节数,在失败时返回-1。和evbuffer_read()函数一样,你需要检查检查错误代码以检查错误是否真实,或者只是表示无法立即完成非阻塞IO,如果howmuch传入了一个负值,则尝试写入整个buffer。

调用evbuffer_write()函数和调用evbuffer_write_atmost()函数时以负值传入howmuch,效果是一样的,它尽可能多的写入数据。

在unix上,这些函数在任何支持读写的文件描述符上都可以工作。在Windows上,仅仅支持套接字。

请注意,当你使用bufferevent的时候,不要调用这些IO函数,bufferevent会自动帮你做这些。

evbuffer和回调

标签: libevent

添加新评论