Libevent book 笔记(10) Evbuffers utility functionality for buffered IO

文章目录


Libevent的evbuffer实现了一个字节队列,并对其在尾端添加数据和在前端移除数据的操作做了优化。Evbuffer专注于缓冲网络IO的缓冲部分的通用性,并不提供对IO的调度和IO的触发的操作,这些是bufferevent的工作。

Creating or freeing an evbuffer

evbuffer_new

evbuffer_free

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

evbuffer_new分配并返回一个新的空evbuffer,evbuffer_free删除一个evbuffer及其全部内容。

Evbuffers and Thread-safety

evbuffer_enable_locking

evbuffer_lock

evbuffer_unlock

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函数来分配一个新的锁。否则,就是用所提供的锁。

evbuffer_lock和evbuffer_unlock分别获取和释放锁。如果evbuffer没有启用锁,则这些函数不做任何事情。

注意对于单独的操作并不需要调用锁函数,如果evbuffer的锁启用,那么单独的操作就已经是原子的了。只有在多个操作一起并且不希望其它线程插入的时候才加锁。

Inspecting an evbuffer

evbuffer_get_length

size_t evbuffer_get_length(const struct evbuffer *buf);

返回一个evbuffer中存储的字节数。

evbuffer_get_contiguous_space

size_t evbuffer_get_contiguous_space(const struct evbuffer *buf);

返回一个evbuffer头部连续存储的字节数。evbuffer中的数据可能会存储在多个内存块当中,这个函数返回的是第一个块中当前存储的字节数。

Adding data to an evbuffer: basics

evbuffer_add

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

在buf的尾部添加长度为datlen的数据,这些数据由data指向。成功返回0,失败返回-1。

evbuffer_add_printf

evbuffer_add_vprintf

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

这两个函数向buf的尾部添加格式化的数据,其参数分别与C库函数printf和vprintf类似处理。函数都返回添加的字节数。

evbuffer_expand

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

改变buf中最后一个内存块的大小,或者添加一个新的内存块,使得buf有足够的空间来存放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);

Moving data from one evbuffer to another

出于效率,Libevent优化了在evbuffer之间移动数据的操作。

evbuffer_add_buffer

evbuffer_remove_buffer

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向dst尾端移动datlen那么多字节的数据,如果可移动数据的长度不足datlen,就全部移过去,但是任何时候都不会超过datlen个字节。成功则返回移动的数据长度。

Adding data to the front of an evbuffer

evbuffer_prepend

evbuffer_prepend_buffer

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

这些函数把数据移动到dst的头部,此外分别与evbuffer_add和evbuffer_add_buffer类似。

Rearranging the internal layout of an evbuffer

有时候可能会需要检查evbuffer的头部的N个字节的数据是不是存放在一个连续的数组中,为此,首先要确保缓冲头部确实是连续的。

evbuffer_pullup

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

evbuffer_pullup对buf的最初size个字节进行线性化,需要时通过复制或者移动来确保这些数据是连续的,并且占据同一个内存块。如果size是负数,函数线性化整个缓冲,如果size大于缓冲中已有的字节数,函数返回NULL,否则,返回一个指向buf中第一个字节的指针。

调用这个函数在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_pullup时,如果size取evbuffer_get_contiguous_space返回的值将不会导致任何数据被复制或者移动。

Removing data from an evbuffer

evbuffer_drain

evbuffer_remove

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

evbuffer_remove从buf头部复制然后移除最前面的datlen那么多字节的数据,然后把这些数据放到data所指的内存中。如果可用数据少于datlen,则复制所有的数据,失败返回-1,否则返回所复制的字节数。

evbuffer_drain与evbuffer_remove不同的是并不复制数据,而只是移除缓冲头部指定长度的数据。成功返回0,失败返回-1。

Copying data out from an evbuffer

有时候可能会需要从缓冲的头部复制但又不需要移除其中的数据。比如,可能只是想看看是否一个某类型的结构完整的到达了,这时候并不打算移除(比如evbuffer_remove),也不打算重新布局缓冲(比如evbuffer_pullup)

evbuffer_copyout

evbuffer_copyout_from

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不同只是在于前者不从缓冲中移除任何数据。也就是说,只是把buf头部最前面的datlen长度的字节复制到data中去。如果buf中可用数据少于datlen字节,则复制全部数据。失败返回-1,否则返回复制的字节数。

evbuffer_copyout_from从buf中pos所指的位置复制数据,这点是与evbuffer_copyout的主要区别。关于evbuffer_ptr结构参见下文"Searching within an 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;
}

Line-oriented input

evbuffer_readln

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);

许多Internet协议都是基于行的格式。evbuffer_readln从evbuffer的头部获取一行的数据,为其分配一个NULL结尾的字符串,然后返回这个字符串。如果n_read_out不为NULL,则设置其为返回字符串的长度。如果未能读取一整行,函数返回NULL。行结束符不复制到返回的字符串中,因为返回的字符串已经带了一个了。

evbuffer_readln可以识别4种行结束格式:

  • EVBUFFER_EOL_LF

    行以一个单独的换行符结束,也就是\n,ASCII值为0x0A。

  • EVBUFFER_EOL_CRLF_STRICT

    行以一个回车加一个换行结束,也就是\r\n,ASCII值为0x0D 0x0A。

  • EVBUFFER_EOL_CRLF

    行结束以一个可选的回车加一个换行符结束,也就是\r\n或者\n。这个格式在解析基于文本的Internet协议时很有用,因为尽管标准通常要求是\r\n,但一些客户端可能只用\n

  • EVBUFFER_EOL_ANY

    行结束可能是任意数量的回车和任意数量的换行。这个格式不是很有用,主要只是出于后向兼容性的考虑。

  • EVBUFFER_EOL_NUL

    行结束于一个单独字节的0,也就是ASCII NUL。

注意如果用event_set_mem_functions覆盖了缺省的malloc,evbuffer_readln返回的字符串会由所指定的替换分配函数分配。

例子

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);
}

Searching within an evbuffer

evbuffer_ptr结构指向evbuffer中的一个位置,可以从那里开始迭代evbuffer中的数据。

struct evbuffer_ptr

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

pos域是唯一的公开域,其它部分不应该被用户代码使用。pos表示距evbuffer起始的一个偏移量。

evbuffer_search

struct evbuffer_ptr evbuffer_search(struct evbuffer *buffer,
    const char *what, size_t len, const struct evbuffer_ptr *start);

函数扫描缓冲,寻找长度为len的字符串*what,如果存在,则返回包含这个字符串位置的evbuffer_ptr ,否则返回-1。

如果提供了start参数,则从start开始查找,否则,从头开始查找,注意,指的是buffer的开头,不是what字符串的开头,原文这里用string是有些误导的。

evbuffer_search_range

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);

实际上evbuffer_search是直接调用了evbuffer_search_range,只是把end置为NULL。所以两个函数的区别只是在于evbuffer_search_range可以指定一个结束位置,而evbuffer_search要查找到buffer最后。

evbuffer_search_eol

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_eol检测行结束,但不会复制行,只是返回行结束符的起始位置。如果eol_len_out不为NULL,则设置为EOL字符串的长度,指的是组成行结束符的字符串的长度,不是整个字符串的长度。

evbuffer_ptr_set

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 调整buffer中pos的位置。如果how是EVBUFFER_PTR_SET,则指针在buffer中移动到一个绝对的位置,如果how是EVBUFFER_PTR_ADD,则指针向前移动。成功返回0,失败返回-1。

如下面例子所示,这个函数也可以用来把一个buffer和一个pos关联起来,或者说给一个pos赋个初值。

例子

#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的内容或者布局的函数调用都会使得evbuffer_ptr 失效,从而对其的使用将变得不安全。

Inspecting data without copying it

有时候,可能需要从evbuffer中读数据,但不需要复制出来(比如evbuffer_copyout会复制数据),或者不重新安排evbuffer的内部存储布局(比如evbuffer_pullup)。有时候也可能想看看evbuffer中间部分的数据。这时候就可以使用:

evbuffer_peek

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);

调用evbuffer_peek的时候,要提供一个长度为n_vec的evbuffer_iovec结构数组vec_out。结构中的iov_base域指向evbuffer内部的存储块,iov_len则是对应内存块的长度。

如果len小于0,evbuffer_peek会尽可能地填写所提供的所有evbuffer_iovec结构,如果evbuffer内部内存块的数量大于n_vec就填n_vec个,否则有多少就填多少,返回的是实际上填写的evbuffer_iovec的数量。

如果len大于0,则所填写的evbuffer_iovec结构的个数取决于这些evbuffer_iovec对应的内存块的数据总量,实际上获取到的全部内存块数据的总量可能会大于len。如果函数能够获取所要求的所有数据,那么就会返回实际上使用的evbuffer_iovec结构的数量。如果函数不能够获取所要求的的数据量,则返回所需要提供的evbuffer_iovec结构的数量,也就是说,如果len长度那么多的数据不足以通过n_vec个evbuffer_iovec结构来获取,那么就返回为了满足这个len长度要求而提供的evbuffer_iovec数量,换句话说就是,这个时候在evbuffer内部有这么多个内存块,它们的数据总量满足len,但是n_vec小于这个数字。

如果start_at为空,则evbuffer_peek从缓冲的起始开始,否则,就从start_at所指的位置开始,不过这时候所返回的第一个evbuffer_iovec的iov_base指向的不一定是对应内存块的起始位置,可能会有一个偏移,iov_len也不是完整的内存块的长度,而是从iov_base到末尾的长度。注意一下,原文这里是ptr而不是start_at,从上下文看,应该是start_at。

例子

{
    /* 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_peek返回的结果之后解锁。话说虽然evbuffer_peek内部也会锁定一次evbuffer,但是显然两次锁定的粒度是不同的。

Adding data to an evbuffer directly

有时候可能会需要把数据直接插入到evbuffer中,而不是把数据先写入一个字符数组再用evbuffer_add把数组复制过去。evbuffer_reserve_space和evbuffer_commit_space借助evbuffer_peek可以通过evbuffer_iovec实现这个目的。

evbuffer_reserve_space

evbuffer_commit_space

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的要求。这些指针以及各自长度会存放在evbuffer_iovec结构数组vec中,n_vecs则是这个数组的大小。

n_vecs必须至少为1,如果n_vecs真的为1,则Libevent会确保提供在一个单独的内存块内的满足要求的连续空间,但这可能会造成缓冲的重新布局或者内存浪费。为了更好的性能,至少提供两个evbuffer_iovec。函数会返回为了满足空间要求所需要的evbuffer_iovec的数量。

向这些内存块写入的数据只有在evbuffer_commit_space调用之后才真正加入到evbuffer。如果提交的数据少于之前请求的空间,可以减少evbuffer_iovec中iov_len的值,当然所提交的evbuffer_iovec的数量也可以少于之前给出的。evbuffer_commit_space函数成功返回0,失败返回-1。

注意

  • 对evbuffer重新布局或者添加数据都会使evbuffer_reserve_space得到的指针失效。
  • 原文说不论n_vecs是多少,当时版本只使用两个evbuffer_iovec,不过目前版本的源码并没有看到这个限制。
  • 不论调用evbuffer_reserve_space多少次都是安全的。
  • 如果要在多线程中使用evbuffer,那么在调用evbuffer_reserve_space前加锁,在evbuffer_commit_space之后解锁。

例子

/* 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. */
/*
* 注意一下,这里的循环没有修改n_to_add,
* 因为要写多少数据已经在返回的evbuffer_iovec的iov_len域设置好了
* 至于调整len,只是为了in case,或者只是举个例子
*/
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);
}

Network IO with evbuffers

evbuffer_read

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

函数从socket fd中读取howmuch字节的数据到buffer的尾端。返回成功读取的数据量,EOF返回0,错误返回-1。注意错误可能表示一个非堵塞的操作不成功,这时候需要检查错误码是不是EAGAIN,或者在Windows下的WSAEWOULDBLOCK 。如果howmuch为负数,evbuffer_read会猜测一个需要读取的数字,当然不是瞎猜,而是调用一个函数看看fd有多少就绪的数据。

evbuffer_write_atmost

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

将buffer头部最多howmuch字节的数据写入socket fd。返回写成果的数据量,失败返回-1。与evbuffer_read一样,需要检查错误是不是由于非堵塞引起的。如果howmuch为负值,则尝试把buffer的全部数据写入fd。

evbuffer_write

int evbuffer_write(struct evbuffer *buffer, evutil_socket_t fd);

与调用evbuffer_write_atmost时howmuch为负值是一样的。

在Unix,这些函数支持所有可以读写的文件描述符,在Windows,只支持socket。

注意当使用bufferevent时不需要直接调用这些函数,bufferevent本身就会调用这些函数。

Evbuffers and callbacks

为了知道数据什么时候添加到evbuffer或者从evbuffer中移除,Libevent为evbuffer提供了一套通用的回调机制

evbuffer_cb_func

struct evbuffer_cb_info {
        size_t orig_size;
        size_t n_added;
        size_t n_deleted;
};

typedef void (*evbuffer_cb_func)(struct evbuffer *buffer,
    const struct evbuffer_cb_info *info, void *arg);

evbuffer回调在数据添加到evbuffer或者从evbuffer中移除时调用。evbuffer_cb_info 结构的orit_size域记录buffer原始的字节数,n_added记录向buffer添加的字节数,n_deleted 记录移除的字节数。

evbuffer_add_cb

struct evbuffer_cb_entry;
struct evbuffer_cb_entry *evbuffer_add_cb(struct evbuffer *buffer,
    evbuffer_cb_func cb, void *cbarg);

函数用来给evbuffer添加一个回调,并返回一个指向这个回调函数实例的指针以备后用。cb和cbarg的用途跟以前的类似就不说明了。

可以为单独一个evbuffer添加多个回调,添加新的回调不会移除旧的回调。

例子

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

/* Here's a callback that remembers how many bytes we have drained in
   total from the buffer, and prints a dot every time we hit a
   megabyte. */
struct total_processed {
    size_t n;
};
void count_megabytes_cb(struct evbuffer *buffer,
    const struct evbuffer_cb_info *info, void *arg)
{
    struct total_processed *tp = arg;
    size_t old_n = tp->n;
    int megabytes, i;
    tp->n += info->n_deleted;
    megabytes = ((tp->n) >> 20) - (old_n >> 20);
    for (i=0; i<megabytes; ++i)
        putc('.', stdout);
}

void operation_with_counted_bytes(void)
{
    struct total_processed *tp = malloc(sizeof(*tp));
    struct evbuffer *buf = evbuffer_new();
    tp->n = 0;
    evbuffer_add_cb(buf, count_megabytes_cb, tp);

    /* Use the evbuffer for a while.  When we're done: */
    evbuffer_free(buf);
    free(tp);
}

注意释放一个非空的evbuffer时不会视为从中排空数据,因此释放时不会调用回调,也不会释放传入回调的用户提供的数据指针,比方说上面的tp。

evbuffer_remove_cb_entry

int evbuffer_remove_cb_entry(struct evbuffer *buffer,
    struct evbuffer_cb_entry *ent);

通过添加回调时返回的回调项指针来移除一个回调。成功返回0,失败返回-1。

evbuffer_remove_cb

int evbuffer_remove_cb(struct evbuffer *buffer, evbuffer_cb_func cb,
    void *cbarg);

通过一个回调函数及传入的参数来移除一个回调。成功返回0,失败返回-1。

evbuffer_cb_set_flags

evbuffer_cb_clear_flags

#define EVBUFFER_CB_ENABLED 1
int evbuffer_cb_set_flags(struct evbuffer *buffer,
                          struct evbuffer_cb_entry *cb,
                          ev_uint32_t flags);
int evbuffer_cb_clear_flags(struct evbuffer *buffer,
                          struct evbuffer_cb_entry *cb,
                          ev_uint32_t flags);

设置或者清除一个回调的标记。当前只支持一个用户可见的标记EVBUFFER_CB_ENABLED。这个标记是缺省设置的,如果清除,则表示该回调不被调用,或者时候暂停使用直到下次设置。

evbuffer_defer_callbacks

int evbuffer_defer_callbacks(struct evbuffer *buffer, struct event_base *base);

也可以延迟调用evbuffer的回调,也就是说可以和bufferevent的回调一样,不在事件触发时马上调用,而由event base的事件循环来调用。理由也是一样的,避免多个evbuffer相互之间多次交换数据造成的栈压平。

当evbuffer的回调延迟调用时,最终会累记多次操作的结果。

与bufferevent一样,evbuffer内部也是引用计数的,所以即使一个延迟回调还没有执行释放一个evbuffer也是安全的。

Avoiding data copies with evbuffer-based IO

为了尽可能的减少数据复制,Libevent提供了一些机制。

evbuffer_add_reference

typedef void (*evbuffer_ref_cleanup_cb)(const void *data,
    size_t datalen, void *extra);

int evbuffer_add_reference(struct evbuffer *outbuf,
    const void *data, size_t datlen,
    evbuffer_ref_cleanup_cb cleanupfn, void *extra);

函数把一块数据的引用添加到evbuffer的尾端,没有复制,evbuffer只是存储了一个指针data,指向长度为datlen的一片数据区。因而,这个指针在被evbuffer使用期间必须保持有效。当evbuffer不需要数据时,会调用所提供的cleanupfn函数来清理数据,其参数为data指针,datlen值及extra指针。成功返回0,失败返回-1。

例子

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

/* In this example, we have a bunch of evbuffers that we want to use to
   spool a one-megabyte resource out to the network.  We do this
   without keeping any more copies of the resource in memory than
   necessary. */

#define HUGE_RESOURCE_SIZE (1024*1024)
struct huge_resource {
    /* We keep a count of the references that exist to this structure,
       so that we know when we can free it. */
    int reference_count;
    char data[HUGE_RESOURCE_SIZE];
};

struct huge_resource *new_resource(void) {
    struct huge_resource *hr = malloc(sizeof(struct huge_resource));
    hr->reference_count = 1;
    /* Here we should fill hr->data with something.  In real life,
       we'd probably load something or do a complex calculation.
       Here, we'll just fill it with EEs. */
    memset(hr->data, 0xEE, sizeof(hr->data));
    return hr;
}

void free_resource(struct huge_resource *hr) {
    --hr->reference_count;
    if (hr->reference_count == 0)
        free(hr);
}

static void cleanup(const void *data, size_t len, void *arg) {
    free_resource(arg);
}

/* This is the function that actually adds the resource to the
   buffer. */
void spool_resource_to_evbuffer(struct evbuffer *buf,
    struct huge_resource *hr)
{
    ++hr->reference_count;
    evbuffer_add_reference(buf, hr->data, HUGE_RESOURCE_SIZE,
        cleanup, hr);
}

Adding a file to an evbuffer

一些操作系统可以把文件直接写入到网络上,而不需要从用户空间复制数据。

evbuffer_add_file

int evbuffer_add_file(struct evbuffer *output, int fd, ev_off_t offset,
    size_t length);

函数假定fd是一个已经打开的文件的描述符,不能是socket,fd要可读。从文件的offset开始,读取length长度的内容,添加到output的尾端。成功返回0,失败返回-1。

警告

如果操作系统支持splice()或者sendfile(),Libevent就会在调用evbuffer_write时直接从fd向网络发送数据,而不需要把数据复制到用户RAM中。如果不存在这样的函数,但是有mmap(),Libevent就会mmap文件,然后操作系统可能就会认为不需要把数据复制到用户空间。除此之外,Libevent就只能把数据从磁盘复制到RAM。

当数据被从evbuffer中刷新后或者evbuffer被释放后,就会关闭文件。如果这不是所需要的,或者需要更精细的控制,就往下看。

Fine-grained control with file segments

evbuffer_add_file在对同一个文件多次操作时效率不高,因为其需要拿走文件的所有权。当然,这里的意思是独占文件或者说需要打开文件的全部内容,文件的属性显然不能随意变更的。

evbuffer_file_segment_new

struct evbuffer_file_segment;
struct evbuffer_file_segment *evbuffer_file_segment_new(
        int fd, ev_off_t offset, ev_off_t length, unsigned flags);

函数创建并返回一个新的evbuffer_file_segment 对象,该对象代表一个底层文件的片段,fd是这个底层文件的文件描述符,这个片段从文件的offset开始,长度为length字节。如果出错,函数返回NULL。

文件片段是由 sendfile, splice, mmap, CreateFileMapping, 或者malloc-and-read实现的,具体哪一个要具体而定,选择的依据是从最轻量的支持机制到最重量级的支持机制。比方说,如果操作系统支持sendfile和mmap,那么文件片段就会只通过sendfile来实现,直到你打算检查文件的内容,这时候就会由mmap来实现。

可以通过以下标记来精细控制文件片段的行为:

  • EVBUF_FS_CLOSE_ON_FREE

    调用evbuffer_file_segment_free释放文件片段时关闭底层文件。

  • EVBUF_FS_DISABLE_MMAP

    文件片段将不会使用内存映射类型的后端来实现,如CreateFileMapping和mmap,即使它们是合适的。

  • EVBUF_FS_DISABLE_SENDFILE

    文件片段将不会使用sendfile类型的后端来实现,比附sendfile、splice,即使它们是合适的。

  • EVBUF_FS_DISABLE_LOCKING

    将不会为文件片段分配锁,也就意味着在多线程下使用文件片段是不安全的。

evbuffer_file_segment_free

void evbuffer_file_segment_free(struct evbuffer_file_segment *seg);

当不再需要使用一个文件片段时,可以调用evbuffer_file_segment_free来释放。不过只有到了没有别的evbuffer引用该文件片段时,才会真正释放空间。

evbuffer_add_file_segment

int evbuffer_add_file_segment(struct evbuffer *buf,
    struct evbuffer_file_segment *seg, ev_off_t offset, ev_off_t length);

使用evbuffer_add_file_segment来添加一个evbuffer_file_segment,可以是全部内容也可以是部分内容,offset指的是文件片段的偏移,而不是文件本身的偏移。

evbuffer_file_segment_add_cleanup_cb

typedef void (*evbuffer_file_segment_cleanup_cb)(
    struct evbuffer_file_segment const *seg, int flags, void *arg);

void evbuffer_file_segment_add_cleanup_cb(struct evbuffer_file_segment *seg,
        evbuffer_file_segment_cleanup_cb cb, void *arg);

evbuffer_file_segment_add_cleanup_cb用来设置一个回调函数,以供当对文件片段的最后一个引用被释放,随即要释放文件片段时调用。这个回调函数不能试图再激活该文件片段,不能将其添加到任意的缓冲上,等等。

Adding an evbuffer to another by reference

可以把一个evbuffer的引用添加到另一个evbuffer上,而不是将一个evbuffer的内容添加到另一个evbuffer,并将原evbuffer的内容抹掉,这时候在另一个evbuffer上使用这个引用,就好像把原evbuffer的数据全部复制过来一样。

evbuffer_add_buffer_reference

int evbuffer_add_buffer_reference(struct evbuffer *outbuf,
    struct evbuffer *inbuf);

函数把outbuf的引用添加到inbuf上,不会进行任何不必要的复制。成功返回0,失败返回-1。

注意之后改变inbuf的内容的不会反射回outbuf,函数只是添加了对evbuffer的内容的引用,而不是evbuffer本身。

也要注意不能嵌套引用缓冲,一个缓存已经作为evbuffer_add_buffer_reference的outbuf后,就不能再成为其他的inbuf。

实际上,看源码似乎文档这里把方向搞反了。在源码里边是把inbuf的引用添加到outbuf的附加链上,并且被设置为不可更改。所以,实际上这个函数并没有人真正使用过?比较前面的evbuffer_add_buffer和evbuffer_remove_buffer也可以发现,源和目标参数在evbuffer各个函数里出现的顺序或者说位置是蛮混乱的。

Making an evbuffer add- or remove-only

evbuffer_freeze

evbuffer_unfreeze

int evbuffer_freeze(struct evbuffer *buf, int at_front);
int evbuffer_unfreeze(struct evbuffer *buf, int at_front);

用来临时禁用对evbuffer头部或者尾部的变更。bufferevent内部使用来避免对输出缓冲的头部或者输入缓冲的尾部偶然的变更。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值