九、缓冲IO的高级功能

libevent的evbuffer实现了为向后面添加数据和从前面移除数据而优化的字节队列。

evbuffer用于处理缓冲网络IO的“缓冲”部分。它不提供调度IO或者当IO就绪时触发IO的功能:这是bufferevent的工作。

除非特别说明,本章描述的函数都在event2/buffer.h中声明。

创建和释放evbuffer

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

这两个函数的功能很简明:evbuffer_new()分配和返回一个新的空evbuffer;而evbuffer_free()释放evbuffer和其内容。

这两个函数从libevent 0.8版就存在了。

2 evbuffer与线程安全

接口

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

默认情况下,在多个线程中同时访问evbuffer是不安全的。如果需要这样的访问,可以调用evbuffer_enable_locking()。如果lock参数为NULL,libevent会使用evthread_set_lock_creation_callback提供的锁创建函数创建一个锁。否则,libevent将lock参数用作锁。

evbuffer_lock()和evbuffer_unlock()函数分别请求和释放evbuffer上的锁。可以使用这两个函数让一系列操作是原子的。如果evbuffer没有启用锁,这两个函数不做任何操作。

(注意:对于单个操作,不需要调用evbuffer_lock()和evbuffer_unlock():如果evbuffer启用了锁,单个操作就已经是原子的。只有在需要多个操作连续执行,不让其他线程介入的时候,才需要手动锁定evbuffer)

这些函数都在2.0.1-alpha版本中引入。

3 检查evbuffer

接口

size_t evbuffer_get_length(const  struct  evbuffer  * buf);

这个函数返回evbuffer存储的字节数,它在2.0.1-alpha版本中引入。

接口

size_t evbuffer_get_contiguous_space(const  struct  evbuffer  * buf);

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

这个函数在2.0.1-alpha版本引入。

4 向evbuffer添加数据:基础

接口

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

这个函数添加data处的datalen字节到buf的末尾,成功时返回0,失败时返回-1。

接口

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相同。函数返回添加的字节数。

接口

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

这个函数修改缓冲区的最后一块,或者添加一个新的块,使得缓冲区足以容纳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_add()和evbuffer_add_printf()函数在libevent 0.8版本引入;evbuffer_expand()首次出现在0.9版本,而evbuffer_add_printf()首次出现在1.1版本。

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

为提高效率,libevent具有将数据从一个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末尾,尽量少进行复制。如果字节数小于datlen,所有字节被移动。函数返回移动的字节数。

evbuffer_add_buffer()在0.8版本引入;evbuffer_remove_buffer()是2.0.1-alpha版本新增加的。

6 添加数据到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()相同。

使用这些函数时要当心,永远不要对与bufferevent共享的evbuffer使用。这些函数是2.0.1-alpha版本新添加的。

7 重新排列evbuffer的内部布局

有时候需要取出evbuffer前面的N字节,将其看作连续的字节数组。要做到这一点,首先必须确保缓冲区的前面确实是连续的。

接口

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

evbuffer_pullup()函数“线性化”buf前面的size字节,必要时将进行复制或者移动,以保证这些字节是连续的,占据相同的内存块。如果size是负的,函数会线性化整个缓冲区。如果size大于缓冲区中的字节数,函数返回NULL。否则,evbuffer_pullup()返回指向buf中首字节的指针。

调用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_pullup()函数由2.0.1-alpha版本新增加:先前版本的libevent总是保证evbuffer中的数据是连续的,而不计开销。

8 从evbuffer中移除数据

接口

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。

evbuffer_drain()由0.8版引入,evbuffer_remove()首次出现在0.9版。

9 从evbuffer中复制出数据

有时候需要获取缓冲区前面数据的副本,而不清除数据。比如说,可能需要查看某特定类型的记录是否已经完整到达,而不清除任何数据(像evbuffer_remove那样),或者在内部重新排列缓冲区(像evbuffer_pullup那样)。

接口

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处的内存中。如果可用字节少于datlen,函数会复制所有字节。失败时返回-1,否则返回复制的字节数。

如果从缓冲区复制数据太慢,可以使用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 ;
}

10 面向行的输入

接口

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前面取出一行,用一个新分配的空字符结束的字符串返回这一行。如果n_read_out不是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)。这种格式对于解析基于文本的互联网协议很有用,因为标准通常要求\r\n的行结束符,而不遵循标准的客户端有时候只使用\n。

EVBUFFER_EOL_ANY

行尾是任意数量、任意次序的回车和换行符。这种格式不是特别有用。它的存在主要是为了向后兼容。

(注意,如果使用event_se_mem_functions()覆盖默认的malloc,则evbuffer_readln返回的字符串将由你指定的malloc替代函数分配)

示例

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_readln()接口在1.4.14-stable及以后版本中可用。

11 在evbuffer中搜索

evbuffer_ptr结构体指示evbuffer中的一个位置,包含可用于在evbuffer中迭代的数据。

接口

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

pos是唯一的公有字段,用户代码不应该使用其他字段。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()函数在缓冲区中查找含有len个字符的字符串what。函数返回包含字符串位置,或者在没有找到字符串时包含-1的evbuffer_ptr结构体。如果提供了start参数,则从指定的位置开始搜索;否则,从开始处进行搜索。

evbuffer_search_range()函数和evbuffer_search行为相同,只是它只考虑在end之前出现的what。

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函数操作buffer中的位置pos。如果how等于EVBUFFER_PTR_SET,指针被移动到缓冲区中的绝对位置position;如果等于EVBUFFER_PTR_ADD,则向前移动position字节。成功时函数返回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或者其布局的调用都会使得evbuffer_ptr失效,不能再安全地使用。

这些接口是2.0.1-alpha版本新增加的。

12 检测数据而不复制

有时候需要读取evbuffer中的数据而不进行复制(像evbuffer_copyout()那样),也不重新排列内部内存布局(像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);

调用evbuffer_peek()的时候,通过vec_out给定一个evbuffer_iovec数组,数组的长度是n_vec。函数会让每个结构体包含指向evbuffer内部内存块的指针(iov_base)和块中数据长度。

如果len小于0,evbuffer_peek()会试图填充所有evbuffer_iovec结构体。否则,函数会进行填充,直到使用了所有结构体,或者见到len字节为止。如果函数可以给出所有请求的数据,则返回实际使用的结构体个数;否则,函数返回给出所有请求数据所需的结构体个数。

如果ptr为NULL,函数从缓冲区开始处进行搜索。否则,从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_peek()给出的内容之后进行解锁

这个函数是2.0.2-alpha版本新增加的。

13 直接向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_vec是数组的长度。

n_vec的值必须至少是1。如果只提供一个向量,libevent会确保请求的所有连续空间都在单个扩展区中,但是这可能要求重新排列缓冲区,或者浪费内存。为取得更好的性能,应该至少提供2个向量。函数返回提供请求的空间所需的向量数。

写入到向量中的数据不会是缓冲区的一部分,直到调用evbuffer_commit_space(),使得写入的数据进入缓冲区。如果需要提交少于请求的空间,可以减小任何evbuffer_iovec结构体的iov_len字段,也可以提供较少的向量。函数成功时返回0,失败时返回-1。

提示和警告

调用任何重新排列evbuffer或者向其添加数据的函数都将使从evbuffer_reserve_space()获取的指针失效。

当前实现中,不论用户提供多少个向量,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. */
=  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_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 );
}

这个函数及其提出的接口从2.0.2-alpha版本就存在了。

14 使用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末尾。成功时函数返回读取的字节数,0表示EOF,失败时返回-1。注意,错误码可能指示非阻塞操作不能立即成功,应该检查错误码EAGAIN(或者Windows中的WSAWOULDBLOCK)。如果howmuch为负,evbuffer_read()试图猜测要读取多少数据。

evbuffer_write_atmost()函数试图将buffer前面至多howmuch字节写入到套接字fd中。成功时函数返回写入的字节数,失败时返回-1。跟evbuffer_read()一样,应该检查错误码,看是真的错误,还是仅仅指示非阻塞IO不能立即完成。如果为howmuch给出负值,函数会试图写入buffer的所有内容。

调用evbuffer_write()与使用负的howmuch参数调用evbuffer_write_atmost()一样:函数会试图尽量清空buffer的内容。

在Unix中,这些函数应该可以在任何支持read和write的文件描述符上正确工作。在Windows中,仅仅支持套接字。

注意,如果使用bufferevent,则不需要调用这些函数,bufferevent的代码已经为你调用了。

evbuffer_write_atmost()函数在2.0.1-alpha版本中引入。

15 evbuffer和回调

evbuffer的用户常常需要知道什么时候向evbuffer添加了数据,什么时候移除了数据。为支持这个,libevent为evbuffer提高了通用回调机制。

接口

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_cb_info结构体指针,和用户提供的参数。evbuffer_cb_info结构体的orig_size字段指示缓冲区改变大小前的字节数,n_added字段指示向缓冲区添加了多少字节;n_deleted字段指示移除了多少字节。

接口

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

evbuffer_add_cb()函数为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
-> +=  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
-> =   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不会清空其数据,释放evbuffer也不会为回调释放用户提供的数据指针。

如果不想让缓冲区上的回调永远激活,可以移除或者禁用回调:

接口

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

#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_entry来移除回调,也可以通过回调函数和参数指针来移除。成功时函数返回0,失败时返回-1。

evbuffer_cb_set_flags()和evbuffer_cb_clear_flags()函数分别为回调函数设置或者清除给定的标志。当前只有一个标志是用户可见的:EVBUFFER_CB_ENABLED。这个标志默认是打开的。如果清除这个标志,对evbuffer的修改不会调用回调函数。

接口

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

跟bufferevent回调一样,可以让evbuffer回调不在evbuffer被修改时立即运行,而是延迟到某event_base的事件循环中执行。如果有多个evbuffer,它们的回调潜在地让数据添加到evbuffer中,或者从中移除,又要避免栈崩溃,延迟回调是很有用的。

如果回调被延迟,则最终执行时,它可能是多个操作结果的总和。

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

整个回调系统是2.0.1-alpha版本新引入的。evbuffer_cb_(set|clear)_flags()函数从2.0.2-alpha版本开始存在。

16 为基于evbuffer的IO避免数据复制

真正高速的网络编程通常要求尽量少的数据复制,libevent为此提供了一些机制:

接口

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

一些操作系统提供了将文件写入到网络,而不需要将数据复制到用户空间的方法。如果存在,可以使用下述接口访问这种机制:

接口

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

evbuffer_add_file()要求一个打开的可读文件描述符fd(注意:不是套接字)。函数将文件中offset处开始的length字节添加到output末尾。成功时函数返回0,失败时返回-1。

注意

在2.0.2-alpha版中,对于使用这种方式添加的数据的可靠操作只有:通过evbuffer_write*()将其发送到网络、使用evbuffer_drain()清空数据,或者使用evbuffer_*_buffer()将其移动到另一个evbuffer中。不能使用evbuffer_remove()取出数据,使用evbuffer_pullup()进行线性化等。

如果操作系统支持splice()或者sendfile(),则调用evbuffer_write()时libevent会直接使用这些函数来将来自fd的数据发送到网络中,而根本不将数据复制到用户内存中。如果不存在splice()和sendfile(),但是支持mmap(),libevent将进行文件映射,而内核将意识到永远不需要将数据复制到用户空间。否则,libevent会将数据从磁盘读取到内存。

清空数据或者释放evbuffer时文件描述符将被关闭。

这一节描述的函数都在2.0.1-alpha版本中引入。evbuffer_add_referece()则从2.0.2-alpha版本开始存在。

17 让evbuffer只能添加或者只能移除

接口

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

可以使用这些函数暂时禁止修改evbuffer的开头或者末尾。bufferevent的代码在内部使用这些函数阻止对输出缓冲区头部,或者输入缓冲区尾部的意外修改。

evbuffer_freeze()函数是2.0.1-alpha版本引入的。

18 废弃的evbuffer函数

2.0版对evbuffer接口进行了很多修改。在此之前,每个evbuffer实现为一个连续的内存块,访问效率是非常低的。

event.h头文件用于暴露evbuffer结构体的内部,但该结构体已经不可用了,因为对于依赖于它们的代码,1.4和2.0版之间的改动太大了。

要访问evbuffer中的字节数,可以使用EVBUFFER_LENGTH()宏;而实际数据可以通过EVBUFFER_DATA()来访问。这两个宏都在event2/buffer_compat.h中。然而,请注意:EVBUFFER_DATA(b)只是evbuffer_pullup(b,-1)的别名,其开销可能非常大。

其他废弃的接口有:

废弃的接口

char   * evbuffer_readline(struct  evbuffer  * buffer);
unsigned 
char   * evbuffer_find(struct  evbuffer  * buffer,
    
const  unsigned char   * what, size_t len);

evbuffer_readline()函数的工作方式类似于当前的evbuffer_readln(buffer,NULL,EVBUFFER_EOL_ANY)。

evbuffer_find()将在缓冲区中搜索字符串的首次出现,返回其指针。与evbuffer_search()不同的是,它只能找到第一个字符串。为兼容使用这个函数的老代码,这个函数会线性化到被定位字符串末尾的整个缓冲区。

回调函数也有不同:

废弃的接口
typedef  void  ( * evbuffer_cb)( struct  evbuffer  * buffer,
    size_t old_len, size_t new_len, 
void   * arg);
void  evbuffer_setcb( struct  evbuffer  * buffer, evbuffer_cb cb,  void   * cbarg);

evbuffer某时刻只能有一个回调,设置新的回调会禁止先前的回调,设置回调为NULL是最佳的禁止回调的方法。

回调函数不使用evbuffer_cb_info结构体,而是使用evbuffer的原长度和新长度。因此,如果old_len大于new_len,数据被抽取;如果new_len大于old_len,数据被添加。回调不可能延迟,所以添加和删除操作不可能合并到单个回调的执行中。

这里给出的废弃函数依然在event2/buffer_compat.h中,依然可用。

你可能感兴趣的:(九、缓冲IO的高级功能)