博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Evbuffer
阅读量:4036 次
发布时间:2019-05-24

本文共 13920 字,大约阅读时间需要 46 分钟。

Evbuffer

  Evbuffer是用一个队列实现的,这个队列具有高效的尾插和头删。Evbuffer是用来提高网络 I/O的效率的一个工具,它的源码在 event2/buffer.h中。

创建或者释放一个evbuffer
struct evbuffer *evbuffer_new(void);void evbuffer_free(struct evbuffer *buf);
Evbuffer 与线程安全

  默认的情况下操作Evbuffer不是线程安全的,如果你想线程安全可以调用函数evbuffer_enable_locking在该evbuffer上,如果lock参数是NULL,Libevent会使用锁创建函数去创建一个新的lock,这个锁创建函数是由 evthread_set_lock_creation_callback 来设置的相应函数,否则如果传了就使用你传的锁。

  evbuffer_lock() and evbuffer_unlock() 函数用来去为一个 evbuffer加锁与解锁,你可以使用这俩个函数来做一系列的原子操作,如果这个evbuffer没有先调用evbuffer_enable_locking开启锁的话,那么直接使用 evbuffer_lock 与 evbuffer_unlock是无效的。
  如果我们只做一个原子操作的时候是不需要调用上面的lock和unlock函数的,因为evbuffer_enable_locking函数保证了evbuffer单一操作的原子性。

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

检测evbuffer的字节数

v 2.0.1size_t evbuffer_get_length(const struct evbuffer *buf);size_t evbuffer_get_contiguous_space(const struct evbuffer *buf);

  第一个函数返回存在该evbuffer存储的字节数

  第二个函数的返回值是这样的,因为evbuffer底层是队列,所以evbuffer底层也许是是由多个内存块构成的,那么该函数就是返回evbuffer的队头处的第一个内存块的字节数

向evbuffer中插入

向evbuffer中尾插
int evbuffer_add(struct evbuffer *buf, const void *data, size_t datlen);int evbuffer_add_printf(struct evbuffer *buf, const char *fmt, ...)int evbuffer_add_vprintf(struct evbuffer *buf, const char *fmt, va_list ap);

  这三个函数不用多讲,看参数和函数名字就知道了它们都是追加数据(相当于Append操作)。

/* 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的途中,由于不断地扩容而导致更多的系统调用所造成性能上的开销。

int evbuffer_expand(struct evbuffer *buf, size_t datlen);
尾部move数据

  Evbuffer也有移动数据的接口,比如我们想把源Evbuffer的所有或者部分数据全部都追加到目标Evbuffer的末尾上,当该函数成功调用后源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); v2.0.1

  第二个函数中会移动datlen个字节大小的数据。如果datlen值大于源Evbuffer的大小,就移动所有的Evbuffer的数据追加到目标Evbuffer队尾处。

头插数据
v 2.0.1 int evbuffer_prepend(struct evbuffer *buf, const void *data, size_t size);int evbuffer_prepend_buffer(struct evbuffer *dst, struct evbuffer* src);

  由名字可以看到含义,小心使用

从evbuffer中读取数据

线性化队头数据

  线性化指把链式数据变成顺序式数据,这样可以增加缓存局部性。下面的这个函数可以做预读操作,因为队列是FIFO所以队头的数据也是即将要输出的数据,即在Input缓冲区中是将要读走的数据,在Output缓冲区中是将要write到底层的数据。

  evbuffer_pullup函数是用来使 evbuffer 线性化的一个函数,它返回该evbuffer前size个被线性化的数据

调用成功返回一个有效地址如果 size 大于 buf大小 调用失败返回NULL如果 size 为 负数则会线性化整个buf unsigned char *evbuffer_pullup(struct evbuffer *buf, ev_ssize_t size);
move队头数据

  注意注意是队头数据哦,第一个函数就只是单单的删除,而第二个函数由一点copy的作用,但还是会删除原evbuffer的数据

int evbuffer_drain(struct evbuffer *buf, size_t len); 成功 0 失败 -1int evbuffer_remove(struct evbuffer *buf, void *data, size_t datlen);失败 -1 成功字节数
Copy队头数据

  如果我们只想从队头简简单单的copy数据,不删除原有数据可以调用以下接口

。并且evbuffer_remove 就是拿 evbuffer_copyout封装的,所以俩个功能相似,只是一个删除一个不删除而已

v 2.0.5ev_ssize_t evbuffer_copyout(struct evbuffer *buf, void *data, size_t datlen);v 2.1.1ev_ssize_t evbuffer_copyout_from(struct evbuffer *buf,const struct evbuffer_ptr *pos,void *data_out, size_t datlen);
demo
#include 
#include
#include
#include
int get_record(struct evbuffer *buf, size_t *size_out, char **record_out){/* Let’s assume that we’re speaking some protocol where recordscontain a 4-byte size field in network order, followed by thatnumber of bytes. We will return 1 and set the ’out’ fields if wehave 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 onthe 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;}
按行读取

  Libevent给我们提供了很方便的按行读取的函数,我们可以以多种规则来读取一行

enum evbuffer_eol_style {
EVBUFFER_EOL_ANY, // 以 \r 和 \n结尾的任意序列 EVBUFFER_EOL_CRLF, // \r\n 或者 \n EVBUFFER_EOL_CRLF_STRICT, //严格的 \r\n EVBUFFER_EOL_LF, // \n EVBUFFER_EOL_NUL // 以 0 作为结尾};

  下面的这个是以上面这几种方式来按行读取的一个函数。调用成功,这个函数就会在堆上为我们申请一堆空间,把当前行内容copy到这个string空间处,n_read_out是这个string有效字符的个数它的大小不包括\0,这个string是以 \0结尾的一个string,调用失败返回NULL。

  注意

  1. 下个这个函数是在v1.4.14加入的,但是 EVBUFFER_EOL_NUL 是 2.1.1才加入进来的一个函数
  2. 这个字符串是这个函数在堆上为我们申请的,我们使用完这个字符串后需要自己去释放它,否则内存泄漏
char *evbuffer_readln(struct evbuffer *buffer, size_t *n_read_out,   enum evbuffer_eol_style eol_style);
demo
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的,它就像一个c++中的迭代器

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

  这个evbuffer_ptr结构体中,pos是我们可以在代码中使用的成员变量,_internal是这个结构体的内部的实现,我们不应该来使用它。

查找函数

  以下查找函数,当查找成功这个evbuffer_ptr结构体的 pos 变量指向相应的位置,失败pos值为-1,如果我们start 传为NULL,则从buffer的起始处开始查找。

查找长度为len的字符串在evbuffer中
struct evbuffer_ptr evbuffer_search(struct evbuffer *buffer,const char *what, size_t len, const struct evbuffer_ptr *start);
在一个range范围查找长度为len的字符串
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);跟上面一样只不过是在begin 和 end的这个范围内寻找what字符串
查找以eol风格结尾的字符串

  eol_len_out是输出型参数,当函数成功调用,它保存了分隔符\结尾符的长度。该函数返回值:

  1. 调用成功,evbuffer_ptr中的pos变量置为eol的第一个字符的位置
  2. 调用失败,evbuffer_ptr中的pos变量置为-1
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_ptr的位置
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指的是设置为当前位置,ADD指的是向后移指定位置。注意任何修改evbuffer的函数,都会使evbuffer_ptr失效,这个跟迭代器失效一个道理

demo
#include 
#include
/* 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和move,我们只是想简简单单的知道evbuffer中保存了什么,所以我们可以用预读的方式来达到这个目的,下面是Libevent为我们提供的接口

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); v2.0.2

  evbuffer_iovec是用来保存evbuffer内部块的一个结构体。

  这个函数的len参数指我们要读多少个字节,start_at参数指从哪个位置预读,vec_out指我们提供给这个函数的数组,n_vec指的是我们提供的数组大小。
  该函数的细节

  1. 如果 len 为负数,该函数就会尝试填充满你给它的数组
  2. 如果为正数,那么该函数就会要么填充满你的数组要么填充至少len个字节的内容
  3. 对于返回值,如果你提供的数组足够它去填充你请求的数据的时候,它就会它填充了多个iovec
  4. 对于返回值,如果你提供的数组不足够的话,它会返回它需要的iovec的数量
从evbuffer的起始处处开始读取2个chunk的内容{/* 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
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 putsus over the limit. */ written += len;} free(v);}从evbuffer中读取16kb的数据去调用consume函数消费{/* Let’s get the first 16K of data after the first occurrence of thestring "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; }}
  1. 修改data指向的内容是未定义行为
  2. 调用修改evbuffer的函数会造成 evbuffer_iovec失效
  3. 如果你的evbuffer使用在多线程环境下,要确保加锁。比如在调用evbuffer_peek的时候一定要使用evbuffer_lock / evbuffer_unlock确保线程安全
  4. 如果我们想知道我们需要提前申请多少个iovec,可以通过 n = evbuffer_peek(buf, 4096, NULL, NULL, 0)的方式获得,这个时候n就是需要提前准备的数量

高级追加函数

  一般如果我们想要追加数据是通过先申请一段字符串数组,然后再调用evbuffer_add去追加它。这样会有俩次copy,第一次生成字符串数组,第二个调用evbuffer_add函数。如果我们想直接把数据追加到evbuffer中,我们可以调用下面来个高级函数

v 2.0.2int 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_iovec数组也就是我们即将要追加的数据,第四个参数是我们提供的这个数组的大小。

  对于evbuffer_reserve_space 中的第四个参数来说,它至少为1。并且如果设置为1,Libevent就会认为我们想要一个线性化的空间去扩展,这样会造成一些额外的性能开销比如浪费内存,为了性能至少传递2个。
  当我们调用完evbuffer_reserve_space函数后,evbuffer_iovec内部的data就会被置为指向 evbuffer 中新申请的空间。当我们在iovec中填充完数据后,其内容并没有被累计到evbuffer中,只有我们调用了evbuffer_commit_space函数,这些数据才会被提交到evbuffer中。
  如果我们reserve的时候size很大,当填充完数据后,我们并不想提交这么多的数据。第一个方法可以通过减少iovec内的iov_len的值,第二个方法可以通过减少提交的iovec数组的数量。
  返回值

  1. evbuffer_reserve_space 返回你申请size个空间时所需的Vec数量
  2. evbuffer_commit_space 成功0 失败-1

  注意的细节点

  1. 调用任何添加数据的函数 或 evbuffer_pullup 这种线性化的函数都会导致iovec保存的指针失效
  2. 目前实现evbuffer_reserve_space函数最多只接受2个vec
  3. 多线程使用的时候,请确保加锁
通过 generate_data 函数构造出2048字节的数据填充到evbuffer中/* Suppose we want to fill a buffer with 2048 bytes of output from agenerate_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);//只填充2048if (n<=0)     return; /* Unable to reserve the space for some reason. */for (i=0; i
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 wedon’t commit too much. */ v[i].iov_len = len;}/* We commit the space here. Note that we give it ’i’ (the number ofvectors we actually used) rather than ’n’ (the number of vectors wehad available. */if (evbuffer_commit_space(buf, v, i) < 0) return; /* Error committing */
evbuffer在网络I/O的应用

  evbuffer最多使用的场景就是网络I/O,下面这些是网络I/O的接口,在unix平台只要是支持读写的文件描述符都可以调用以下函数。虽然Libevent给我们提供了这些接口,但是如果我们使用了bufferevent,那么我们是不需要去使用这些接口的,bufferevent会自动帮助我们I/O

v 2.0.1int 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函数的howmuch参数表示帮我们从fd中最多读取多少字节的数据追加到evbuffer的尾部。如果howmuch是个负数,evbuffer_read函数会自己判断从fd中读多少数据追加到evbuffer的尾部

  evbuffer_write_atmost函数中howmuch参数表示我们要从evbuffer的队头移动多少字节数据到fd中。如果howmuch为负数就是表示把evbuffer的所有数据都刷新到fd中。evbuffer_write函数就是上面这个函数howmuch为负数的情况
  返回值如read/write系统调用一样,需要自己判断error

evbuffer的回调函数

  为了让使用evbuffer的用户知道何时有数据在evbuffer上新增或者移除,Libevent提供了一个基本的evbuffer的回调机制

struct evbuffer_cb_info {size_t orig_size;//原先的evbuffer数据量大小,在它新增或者移出前,它的大小有多少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上add或者remove数据的时候,evbuffer的回调函数就会被调用。这个回调函数如上所示,evbuffer_cb_info的字段解释也如上所示。

struct evbuffer_cb_entry;struct evbuffer_cb_entry *evbuffer_add_cb(struct evbuffer *buffer,evbuffer_cb_func cb, void *cbarg);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 1int 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_add_cb的时候就会注册成功并会返回一个evbuffer_cb_entry的一个指针,当我们想要移除该callback函数的时候,我们可以把它传给evbuffer_remove_cb_entry。第二个移除的方法,就是调用evbuffer_cb_clear_flags函数去传递 EVBUFFER_CB_ENABLED 标志,这样就会导致该callback函数不会再有效,但是还可以再次开启通过调evbuffer_cb_set_flags函数。默认当前只支持这一个flag标志。

  需要注意的每个evbuffer可以有无数的回调函数,添加新的不会删除旧的。还有如果我们调用evbuffer_free释放了一个evbuffer,就不要再去统计来自于这个evbuffer上的数据消耗了,并且evbuffer_free函数是不会帮我们释放用户自己申请的arg参数对应的空间,我们需要手动自己释放。

零拷贝evbuffer I/O

  在网络程序中,我们要避免更多的copy来提高网络程序的处理效率。Libevent 给我们提供了一个零拷贝的机制,也就是我们向往 evbuffer 中追加数据可以不使用copy的方式追加,而是给evbuffer传递一个指针,evbuffer内部去直接使用这个指针来进行I/O

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); v2.0.2   成功0  失败 -1

  当我们提供的数组没有数据可消费的时候,它就会调用cleanup回调函数去进行清理工作。

总结evbuffer

  这个玩意就是一个带callback函数的buffer,无论read消费还是写消费,buffer的数据都会越来越少,read消费以为着被应用层读走,write消费意味着把这些数据发送出去。所以无论怎么看,buffer都是只是起一个缓冲作用,最后还是别用单独使用evbuffer,因为当我们使用bufferevent的时候它会使用evbuffer帮助我们去I/O

转载地址:http://omjdi.baihongyu.com/

你可能感兴趣的文章
Uboot 中的hush shell
查看>>
从zImage中提取出Image的方法
查看>>
zImage构成图解
查看>>
arm-linux启动过程中的内存布局
查看>>
ARM LINUX内核如何确定自己的实际物理地址
查看>>
Kernel low-level debugging functions linux汇编的调试方法
查看>>
LINUX内核代码在线阅读网址
查看>>
Linux芯片级移植与底层驱动(基于3.7.4内核)
查看>>
Linux CCF框架简要分析和API调用
查看>>
Linux common clock framework(1)_概述
查看>>
Linux common clock framework(2)_clock provider
查看>>
Linux common clock framework(3)_实现逻辑分析
查看>>
Common Clock Framework系统结构
查看>>
Linux时间子系统之:软件架构
查看>>
Linux时间子系统之:Tick Device layer综述
查看>>
git 下载跟踪远程分支
查看>>
制作jffs2根文件系统
查看>>
u-boot从内存启动命令 bootz
查看>>
Device Tree:代码分析
查看>>
gpio子系统和pinctrl子系统(一)
查看>>