A Libevent Reference Manual
- Reference Link
- R5: Utility and portability functions
- R6: Bufferevents: concepts and basics
- R6a: Bufferevents: advanced topics
- Paired bufferevents
- Filtering bufferevents
- Limiting maximum single read/write size
- Bufferevents and Rate-limiting
- The rate-limiting model
- Setting a rate limit on a bufferevent
- Interface
- Setting a rate limit on a group of bufferevents
- Interface
- Inspecting current rate-limit values
- Interface
- Interface
- Interface
- Manually adjusting rate limits
- Interface
- Setting the smallest share possible in a rate-limited group
- Interface
- Limitations of the rate-limiting implementation
- Bufferevents and SSL
Reference Link
Fast portable non-blocking network programming with Libevent
R5: Utility and portability functions
Helper functions and types for Libevent
`evutil_socket_t
除了 Windows 之外,大多数地方的套接字都是一个 int
,操作系统按数字顺序分发它们。然而,使用 Windows 套接字 API
,套接字的类型为 SOCKET,它实际上是一个类似指针的操作系统句柄,并且您接收它们的顺序是未定义的。我们将 evutil_socket_t
类型定义为一个整数,它可以保存 socket()
或 accept()
的输出,而不会冒在 Windows 上指针截断的风险。
Definition
#ifdef WIN32
#define evutil_socket_t intptr_t
#else
#define evutil_socket_t int
#endif
这种类型是在 Libevent 2.0.1-alpha 中引入的。
Standard integer types
通常,您会发现自己使用的 C 系统错过了 21 世纪,因此没有实现标准的 C99 stdint.h
头文件。对于这种情况,Libevent 定义了它自己版本的 stdint.h
中特定于位宽的整数:
Type | Width | Signed | Maximum | Minimum |
---|---|---|---|---|
ev_uint64_t | 64 | No | EV_UINT64_MAX | 0 |
ev_int64_t | 64 | Yes | EV_INT64_MAX | EV_INT64_MIN |
ev_uint32_t | 32 | No | EV_UINT32_MAX | 0 |
ev_int32_t | 32 | Yes | EV_INT32_MAX | EV_INT32_MIN |
ev_uint16_t | 16 | No | EV_UINT16_MAX | 0 |
ev_int16_t | 16 | Yes | EV_INT16_MAX | EV_INT16_MIN |
ev_uint8_t | 8 | No | EV_UINT8_MAX | 0 |
ev_int8_t | 8 | Yes | EV_INT8_MAX | EV_INT8_MIN |
与 C99 标准一样,每种类型都具有指定的宽度(以位为单位)。
这些类型是在 Libevent 1.4.0-beta 中引入的。MAX/MIN 常量首次出现在 Libevent 2.0.4-alpha 中。
Miscellaneous compatibility types
ev_ssize_t
类型在具有 1 的平台上定义为 ssize_t
(签名 size_t
),在没有的平台上定义为合理的默认值。ev_ssize_t
的最大可能值为 EV_SSIZE_MAX
;最小的是 EV_SSIZE_MIN
。(size_t
的最大可能值是 EV_SIZE_MAX
,以防您的平台没有为您定义 SIZE_MAX
。)
ev_off_t
类型用于表示文件或内存块的偏移量。它在具有合理 off_t
定义的平台上被定义为 off_t
,在 Windows 上被定义为 ev_int64_t
。
套接字 API 的某些实现提供了长度类型 socklen_t
,而有些则不提供。ev_socklen_t
在它存在的地方定义为这种类型,否则为合理的默认值。
ev_intptr_t
类型是一个有符号整数,它大到足以容纳一个指针而不会丢失位。ev_uintptr_t
类型是一个无符号整数,大到足以容纳一个指针而不会丢失位。
ev_ssize_t
类型是在 Libevent 2.0.2-alpha 中添加的。ev_socklen_t
类型是 Libevent 2.0.3-alpha 中的新类型。ev_intptr_t
和 ev_uintptr_t
类型以及 EV_SSIZE_MAX/MIN
宏是在 Libevent 2.0.4-alpha 中添加的。ev_off_t
类型首次出现在 Libevent 2.0.9-rc 中。
Timer portability functions
并非每个平台都定义了标准的 timeval
操作函数,因此我们提供了自己的实现。
Interface
#define evutil_timeradd(tvp, uvp, vvp) /* ... */
#define evutil_timersub(tvp, uvp, vvp) /* ... */
这些宏分别添加或减去它们的前两个参数,并将结果存储在第三个参数中。
Interface
#define evutil_timerclear(tvp) /* ... */
#define evutil_timerisset(tvp) /* ... */
清除 timeval
会将其值设置为零。如果它是非零的,则检查它是否被设置返回真,否则返回假。
Interface
#define evutil_timercmp(tvp, uvp, cmp)
evutil_timercmp
宏比较两个时间值,如果它们处于关系运算符cmp指定的关系中,则返回 true 。例如,evutil_timercmp(t1, t2, ⇐) 的意思是,“是 t1 ⇐ t2 吗?” 请注意,与某些操作系统的版本不同,Libevent 的 timercmp
支持所有 C 关系操作(即 <、>、==、!=、⇐ 和 >=)。
Interface
int evutil_gettimeofday(struct timeval *tv, struct timezone *tz);
evutil_gettimeofday
函数将tv
设置为当前时间。未使用 tz
参数。
Example
struct timeval tv1, tv2, tv3;
/* Set tv1 = 5.5 seconds */
tv1.tv_sec = 5; tv1.tv_usec = 500*1000;
/* Set tv2 = now */
evutil_gettimeofday(&tv2, NULL);
/* Set tv3 = 5.5 seconds in the future */
evutil_timeradd(&tv1, &tv2, &tv3);
/* all 3 should print true */
if (evutil_timercmp(&tv1, &tv1, ==)) /* == "If tv1 == tv1" */
puts("5.5 sec == 5.5 sec");
if (evutil_timercmp(&tv3, &tv2, >=)) /* == "If tv3 >= tv2" */
puts("The future is after the present.");
if (evutil_timercmp(&tv1, &tv2, <)) /* == "If tv1 < tv2" */
puts("It is no longer the past.");
这些函数是在 Libevent 1.4.0-beta 中引入的,除了 evutil_gettimeofday()
,它是在 Libevent 2.0 中引入的。
笔记
在 Libevent 1.4.4 之前,将 ⇐ 或 >= 与 timercmp 一起使用是不安全的。
Socket API compatibility
本节之所以存在,是因为由于历史原因,Windows 从未真正以良好兼容(并且非常兼容)的方式实现 Berkeley
套接字 API。以下是您可以用来假装它具有的一些功能。
Interface
int evutil_closesocket(evutil_socket_t s);
#define EVUTIL_CLOSESOCKET(s) evutil_closesocket(s)
这个函数关闭一个套接字。在 Unix 上,它是 close()
的别名;在 Windows 上,它调用 closesocket()
。(您不能在 Windows 上的套接字上使用 close()
,并且没有其他人定义了 closesocket()
。)
evutil_closesocket
函数是在 Libevent 2.0.5-alpha 中引入的。在此之前,您需要调用 EVUTIL_CLOSESOCKET
宏。
Interface
#define EVUTIL_SOCKET_ERROR()
#define EVUTIL_SET_SOCKET_ERROR(errcode)
#define evutil_socket_geterror(sock)
#define evutil_socket_error_to_string(errcode)
这些宏访问和操作套接字错误代码。EVUTIL_SOCKET_ERROR()
返回来自该线程的最后一个套接字操作的全局错误代码,evutil_socket_geterror()
为特定套接字这样做。(两者在类 Unix 系统上都是 errno
。) EVUTIL_SET_SOCKET_ERROR()
更改当前套接字错误代码(如在 Unix 上设置 errno
),而 evutil_socket_error_to_string()
返回给定套接字错误代码的字符串表示(如 Unix 上的 strerror()
) .
(我们需要这些函数是因为 Windows 不使用 errno 来处理来自套接字函数的错误,而是使用 WSAGetLastError()
。)
Note
Windows 套接字错误与您在 errno 中看到的标准 C 错误不同;小心。
Interface
int evutil_make_socket_nonblocking(evutil_socket_t sock);
即使您需要在套接字上执行非阻塞 IO 的调用也不能移植到 Windows。evutil_make_socket_nonblocking()
函数接受一个新套接字(来自 socket()
或 accept()
)并将其转换为非阻塞套接字。(它在 Unix 上设置 O_NONBLOCK
,在 Windows 上设置 FIONBIO
。)
Interface
int evutil_make_listen_socket_reuseable(evutil_socket_t sock);
此函数确保侦听器套接字使用的地址在套接字关闭后立即可用于另一个套接字。(它在 Unix 上设置 SO_REUSEADDR
而在 Windows 上什么也不做。你不想在 Windows 上使用 SO_REUSEADDR
;它在那里意味着不同的东西。)
Interface
int evutil_make_socket_closeonexec(evutil_socket_t sock);
这个调用告诉操作系统如果我们调用 exec()
就应该关闭这个套接字。它在 Unix 上设置 FD_CLOEXEC
标志,而在 Windows 上什么也不做。
Interface
int evutil_socketpair(int family, int type, int protocol,
evutil_socket_t sv[2]);
这个函数的行为就像 Unix socketpair()
调用:它创建两个相互连接的套接字,可以与普通的套接字 IO 调用一起使用。它将两个套接字存储在 sv[0]
和 sv[1]
中,成功返回 0,失败返回 -1。
在 Windows 上,这仅支持家族 AF_INET
、类型 SOCK_STREAM
和协议 0
。请注意,这可能会在某些 Windows 主机上失败,其中防火墙软件巧妙地对 127.0.0.1 设置了防火墙以防止主机与自身通信。
这些函数是在 Libevent 1.4.0-beta 中引入的,除了 evutil_make_socket_closeonexec()
,它是 Libevent 2.0.4-alpha 中的新功能。
Portable string manipulation functions
Interface
ev_int64_t evutil_strtoll(const char *s, char **endptr, int base);
此函数的行为与 strtol
相同,但处理 64 位整数。在某些平台上,它仅支持 Base 10
。
Interface
int evutil_snprintf(char *buf, size_t buflen, const char *format, ...);
int evutil_vsnprintf(char *buf, size_t buflen, const char *format, va_list ap);
这些 snprintf
替换函数的行为与标准 snprintf
和 vsnprintf
接口相同。如果缓冲区足够长,它们返回将写入缓冲区的字节数,不计算终止的 NUL 字节。(此行为符合 C99 snprintf()
标准,与 Windows _snprintf()
形成对比,后者在字符串不适合缓冲区时返回负数。)
evutil_strtoll()
函数从 1.4.2-rc 开始就在 Libevent 中。这些其他功能首次出现在 1.4.5 版中。
Locale-independent string manipulation functions
有时,在实现基于 ASCII 的协议时,您希望根据 ASCII 的字符类型概念来操作字符串,而不管您当前的语言环境如何。Libevent 提供了一些函数来帮助解决这个问题:
Interface
int evutil_ascii_strcasecmp(const char *str1, const char *str2);
int evutil_ascii_strncasecmp(const char *str1, const char *str2, size_t n);
这些函数的行为与 strcasecmp()
和 strncasecmp()
相同,不同之处在于它们总是使用 ASCII 字符集进行比较,而不管当前的语言环境如何。evutil_ascii_str[n]casecmp()
函数首先在 Libevent 2.0.3-alpha 中公开。
IPv6 helper and portability functions
Interface
const char *evutil_inet_ntop(int af, const void *src, char *dst, size_t len);
int evutil_inet_pton(int af, const char *src, void *dst);
这些函数的行为与标准 inet_ntop()
和 inet_pton()
函数相同,用于解析和格式化 IPv4 和 IPv6 地址,如 RFC3493 中所述。也就是说,要格式化 IPv4 地址,您调用 evutil_inet_ntop()
并将af
设置为 AF_INET
,src
指向结构 in_addr
,dst
指向大小为len
的字符缓冲区。对于 IPv6 地址,af
是 AF_INET6
,src
是结构 in6_addr
。要解析 IPv4 地址,请调用 evutil_inet_pton()
,并将af
设置为 AF_INET
或 AF_INET6
,要在src
中解析的字符串,以及dst适当地指向 in_addr
或 in_addr6
。
evutil_inet_ntop()
的返回值在失败时为 NULL
,否则指向 dst
。evutil_inet_pton()
的返回值在成功时为 0,在失败时为 -1。
Interface
int evutil_parse_sockaddr_port(const char *str, struct sockaddr *out,
int *outlen);
此函数从str
解析地址并将结果写入 out
。所述outlen
参数必须指向一个整数保持在可用的字节数出; 它被更改以保存实际使用的字节数。此函数在成功时返回 0,在失败时返回 -1。它识别以下地址格式:
-
[ipv6] :端口(如“[ffff::]:80”)
-
ipv6(如“ffff ::”)
-
[ipv6](如“[ffff::]”)
-
ipv4:port(如“1.2.3.4:80”)
-
ipv4(如“1.2.3.4”)
如果未给出端口,则生成的 sockaddr
中的端口设置为 0。
Interface
int evutil_sockaddr_cmp(const struct sockaddr *sa1,
const struct sockaddr *sa2, int include_port);
evutil_sockaddr_cmp()
函数比较两个地址,如果sa1
在sa2
之前返回负数,如果它们相等则返回0,如果sa2 在sa1 之前返回正数。它适用于 AF_INET
和 AF_INET6
地址,并为其他地址返回未定义的输出。保证给出这些地址的总顺序,但顺序可能会在 Libevent 版本之间发生变化。
如果include_port
参数为 false
,则两个 sockaddr
仅在端口不同时被视为相等。否则,具有不同端口的 sockaddrs
将被视为不相等。
这些函数是在 Libevent 2.0.1-alpha 中引入的,除了 evutil_sockaddr_cmp()
,它在 2.0.3-alpha 中引入。
Structure macro portability functions
Interface
#define evutil_offsetof(type, field) /* ... */
如offsetof
宏,这个宏产生从field所出现的type开始的地方的字节数。
这个宏是在 Libevent 2.0.1-alpha 中引入的。在 Libevent 2.0.3-alpha 之前的每个版本中它都有问题。
Secure random number generator
许多应用程序(包括 evdns
)需要一个难以预测的随机数来源以确保其安全性。
Interface
void evutil_secure_rng_get_bytes(void *buf, size_t n);
此函数用n字节的随机数据填充buf
处的 n 字节缓冲区。
如果您的平台提供 arc4random()
函数,Libevent 会使用它。否则,它使用自己的 arc4random()
实现,由操作系统的熵池(Windows 上的 CryptGenRandom
,其他地方的 /dev/urandom
)作为种子。
Interface
int evutil_secure_rng_init(void);
void evutil_secure_rng_add_bytes(const char *dat, size_t datlen);
您不需要手动初始化安全随机数生成器,但是如果您想确保它成功初始化,您可以通过调用 evutil_secure_rng_init()
来实现。它播种 RNG
(如果尚未播种)并在成功时返回 0。如果它返回 -1,则 Libevent 无法在您的操作系统上找到良好的熵源,并且您不能在不自行初始化的情况下安全地使用 RNG。
如果您在程序可能会放弃特权的环境中运行(例如,通过运行 chroot()
),您应该在这样做之前调用 evutil_secure_rng_init()
。
您可以通过调用 evutil_secure_rng_add_bytes()
自己向熵池添加更多随机字节;这在典型使用中不是必需的。
这些函数是 Libevent 2.0.4-alpha 中的新功能。
R6: Bufferevents: concepts and basics
大多数时候,除了响应事件之外,应用程序还希望执行一些数据缓冲。例如,当我们想要写入数据时,通常的模式运行如下:
-
决定我们要向连接写入一些数据;将该数据放入缓冲区。
-
等待连接变为可写
-
尽可能多地写入数据
-
记住我们写了多少,如果我们还有更多的数据要写,等待连接再次变为可写。
这种缓冲 IO 模式很常见,以至于 Libevent 为其提供了通用机制。“bufferevent
”由底层传输(如套接字)、读取缓冲区和写入缓冲区组成。与在底层传输准备好读取或写入时提供回调的常规事件不同,bufferevent
在读取或写入足够的数据时调用其用户提供的回调。
有多种类型的缓冲区事件,它们都共享一个公共接口。在撰写本文时,存在以下类型:
-
基于套接字的缓冲事件
一个缓冲区事件,它使用event_*
接口作为后端,从底层流套接字发送和接收数据。 -
异步 IO 缓冲事件
使用Windows IOCP
接口向底层流套接字发送和接收数据的缓冲区事件。(仅限 Windows;实验性的。) -
过滤缓冲事件
在将传入和传出数据传递给底层bufferevent
对象之前处理传入和传出数据的缓冲区事件 - 例如,压缩或转换数据。 -
成对的缓冲事件
两个缓冲区事件相互传输数据。
NOTE
从 Libevent 2.0.2-alpha 开始,这里的 bufferevents
接口在所有 bufferevent
类型中仍然不是完全正交的。换句话说,并非下面描述的每个接口都适用于所有 bufferevent
类型。Libevent 开发人员打算在未来版本中更正此问题。
NOTE ALSO
Bufferevents
目前仅适用于面向流的协议,如 TCP。将来可能会支持面向数据报的协议,如 UDP。
本节中的所有函数和类型都在 event2/bufferevent.h
中声明。与 evbuffers
相关的函数在 event2/buffer.h
中声明;有关这些的信息,请参阅下一章。
Bufferevents and evbuffers
每个缓冲区事件都有一个输入缓冲区和一个输出缓冲区。这些是struct evbuffer
类型。当您有数据要写入 bufferevent
时,将其添加到输出缓冲区;当 bufferevent
有数据供您读取时,您将其从输入缓冲区中排出。
evbuffer
接口支持多种操作;我们将在后面的部分讨论它们。
Callbacks and watermarks
每个缓冲区事件都有两个与数据相关的回调:读取回调和写入回调。默认情况下,只要从底层传输读取任何数据,就会调用读取回调,并且只要将输出缓冲区中的足够数据清空到底层传输,就会调用写入回调。您可以通过调整缓冲区事件的读写“水印”来覆盖这些函数的行为。
每个缓冲区事件都有四个水印:
-
Read low-water mark
每当发生将bufferevent
的输入缓冲区保留在此级别或更高级别的读取时,将调用bufferevent
的读取回调。默认为 0,因此每次读取都会导致调用读取回调。 -
Read high-water mark
如果 bufferevent 的输入缓冲区达到此级别,则 bufferevent 将停止读取,直到从输入缓冲区中排出足够的数据使我们再次低于它。默认为无限制,因此我们永远不会因为输入缓冲区的大小而停止读取。 -
Write low-water mark
每当发生将我们带到此级别或更低级别的写操作时,我们都会调用写回调。默认为 0,因此除非清空输出缓冲区,否则不会调用写入回调。 -
Write high-water mark
不直接由缓冲区事件使用,当缓冲区事件用作另一个缓冲区事件的底层传输时,此水印可能具有特殊含义。请参阅下面有关过滤缓冲区事件的说明。
bufferevent
也有一个“错误
”或“事件
”回调,它被调用以告诉应用程序有关非面向数据的事件,例如连接关闭或发生错误时。定义了以下事件标志:
-
BEV_EVENT_READING
在对bufferevent
执行读取操作期间发生了一个事件。查看它是哪个事件的其他标志。 -
BEV_EVENT_WRITING
在对bufferevent
执行写操作期间发生了一个事件。查看它是哪个事件的其他标志。 -
BEV_EVENT_ERROR
缓冲区事件操作期间发生错误。有关错误的详细信息,请调用EVUTIL_SOCKET_ERROR()
。 -
BEV_EVENT_TIMEOUT
缓冲区事件超时。 -
BEV_EVENT_EOF
我们在bufferevent
上得到了文件结束指示。 -
BEV_EVENT_CONNECTED
我们在bufferevent
上完成了请求的连接。
(以上事件名称是 Libevent 2.0.2-alpha 中的新名称。)
Deferred callbacks
默认情况下,当相应的条件发生时,会立即执行 bufferevent
回调。(这也适用于 evbuffer
回调;我们稍后会讨论这些。)当依赖关系变得复杂时,这种立即调用可能会带来麻烦。例如,假设有一个回调在 evbuffer A
变空时将数据移动到 evbuffer A
中,而另一个回调在 evbuffer A
变满时将数据从 evbuffer A
中处理出来。由于这些调用都发生在堆栈上,如果依赖关系变得足够糟糕,您可能会面临堆栈溢出的风险。
为了解决这个问题,你可以告诉一个 bufferevent(
或一个 evbuffer
)它的回调应该是deferred
。当满足延迟回调的条件时,它不会立即调用它,而是作为 event_loop()
调用的一部分排队,并在常规事件的回调之后调用。
(延迟回调是在 Libevent 2.0.1-alpha 中引入的。)
Option flags for bufferevents
您可以在创建缓冲区事件时使用一个或多个标志来改变其行为。公认的标志是:
-
BEV_OPT_CLOSE_ON_FREE
当bufferevent
被释放时,关闭底层传输。这将关闭底层套接字,释放底层缓冲区事件等。 -
BEV_OPT_THREADSAFE
自动为bufferevent
分配锁,以便从多个线程使用它是安全的。 -
BEV_OPT_DEFER_CALLBACKS
设置此标志后,bufferevent
会推迟其所有回调,如上所述。 -
BEV_OPT_UNLOCK_CALLBACKS
默认情况下,当bufferevent
设置为线程安全时,只要调用任何用户提供的回调,就会持有bufferevent
的锁。设置此选项会使 Libevent 在调用回调时释放bufferevent
的锁。
(Libevent 2.0.5-beta 引入了 BEV_OPT_UNLOCK_CALLBACKS
。上面的其他选项是 Libevent 2.0.1-alpha 中的新选项。)
Working with socket-based bufferevents
最简单的缓冲事件是基于套接字的类型。基于套接字的 bufferevent
使用 Libevent 的底层事件机制来检测底层网络套接字何时准备好进行读取和/或写入操作,并使用底层网络调用(如 readv、writev、WSASend 或 WSARecv)来传输和接收数据。
Creating a socket-based bufferevent
您可以使用 bufferevent_socket_new() 创建一个基于套接字的 bufferevent:
Interface
struct bufferevent *bufferevent_socket_new(
struct event_base *base,
evutil_socket_t fd,
enum bufferevent_options options);
所述base
是event_base,和options
是bufferevent选项(BEV_OPT_CLOSE_ON_FREE等)的位掩码。该FD参数是一个套接字的可选文件描述符。如果您想稍后设置文件描述符,您可以将 fd 设置为 -1。
Tip
[确保您提供给 bufferevent_socket_new 的套接字处于非阻塞模式。Libevent 为此提供了方便的方法 evutil_make_socket_nonblocking。]
此函数在成功时返回一个缓冲区事件,在失败时返回 NULL。
bufferevent_socket_new() 函数是在 Libevent 2.0.1-alpha 中引入的。
Launching connections on socket-based bufferevents
如果缓冲区事件的套接字尚未连接,您可以启动一个新连接。
Interface
int bufferevent_socket_connect(struct bufferevent *bev,
struct sockaddr *address, int addrlen);
address
和 addrlen
参数与标准调用 connect()
相同。如果 bufferevent
还没有设置套接字,则调用此函数会为其分配一个新的流套接字,并使其成为非阻塞的。
如果 bufferevent
已经有一个套接字,调用 bufferevent_socket_connect()
会告诉 Libevent 套接字未连接,并且在连接操作成功之前不应在套接字上进行读取或写入操作。
在连接完成之前将数据添加到输出缓冲区是可以的。
如果连接成功启动,则此函数返回 0,如果发生错误,则返回 -1。
Example
#include <event2/event.h>
#include <event2/bufferevent.h>
#include <sys/socket.h>
#include <string.h>
void eventcb(struct bufferevent *bev, short events, void *ptr)
{
if (events & BEV_EVENT_CONNECTED) {
/* We're connected to 127.0.0.1:8080. Ordinarily we'd do
something here, like start reading or writing. */
} else if (events & BEV_EVENT_ERROR) {
/* An error occured while connecting. */
}
}
int main_loop(void)
{
struct event_base *base;
struct bufferevent *bev;
struct sockaddr_in sin;
base = event_base_new();
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = htonl(0x7f000001); /* 127.0.0.1 */
sin.sin_port = htons(8080); /* Port 8080 */
bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
bufferevent_setcb(bev, NULL, NULL, eventcb, NULL);
if (bufferevent_socket_connect(bev,
(struct sockaddr *)&sin, sizeof(sin)) < 0) {
/* Error starting connection */
bufferevent_free(bev);
return -1;
}
event_base_dispatch(base);
return 0;
}
bufferevent_base_connect()
函数是在 Libevent-2.0.2-alpha 中引入的。在此之前,您必须自己在套接字上手动调用 connect()
,当连接完成时,bufferevent
会将其报告为写入。
Note
如果您使用 bufferevent_socket_connect()
启动 connect()
尝试,您只会收到 BEV_EVENT_CONNECTED
事件。如果您自己调用 connect()
,连接将被报告为写入。
如果您想自己调用connect()
,但在连接成功时仍然收到BEV_EVENT_CONNECTED
事件,请在connect()
返回-1 且errno
等于EAGAIN
或EINPROGRESS
后调用bufferevent_socket_connect(bev, NULL, 0)
。
这个函数是在 Libevent 2.0.2-alpha 中引入的。
Launching connections by hostname
很多时候,您希望将解析主机名和连接到它的操作合并为一个操作。有一个接口:
Interface
int bufferevent_socket_connect_hostname(struct bufferevent *bev,
struct evdns_base *dns_base, int family, const char *hostname,
int port);
int bufferevent_socket_get_dns_error(struct bufferevent *bev);
此函数解析 DNS
名称hostname
,查找类型family
的地址 。(允许的系列类型是 AF_INET
、AF_INET6
和 AF_UNSPEC
。)如果名称解析失败,它会调用带有错误事件的事件回调。如果成功,它会像 bufferevent_connect
一样启动连接尝试。
dns_base
参数是可选的。如果它是 NULL
,则 Libevent 会在等待名称查找完成时阻塞,这通常不是您想要的。如果提供,则 Libevent 使用它来异步查找主机名。有关DNS 的更多信息,请参阅第 R9 章。
与 bufferevent_socket_connect()
一样,该函数告诉 Libevent 缓冲区事件上的任何现有套接字都未连接,并且在解析完成且连接操作成功之前不应在套接字上进行读取或写入操作。
如果发生错误,则可能是 DNS 主机名查找错误。您可以通过调用 bufferevent_socket_get_dns_error()
找出最近发生的错误。如果返回的错误代码为 0,则表示未检测到 DNS 错误。
Example: Trivial HTTP v0 client.
/* Don't actually copy this code: it is a poor way to implement an
HTTP client. Have a look at evhttp instead.
*/
#include <event2/dns.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <event2/util.h>
#include <event2/event.h>
#include <stdio.h>
void readcb(struct bufferevent *bev, void *ptr)
{
char buf[1024];
int n;
struct evbuffer *input = bufferevent_get_input(bev);
while ((n = evbuffer_remove(input, buf, sizeof(buf))) > 0) {
fwrite(buf, 1, n, stdout);
}
}
void eventcb(struct bufferevent *bev, short events, void *ptr)
{
if (events & BEV_EVENT_CONNECTED) {
printf("Connect okay.\n");
} else if (events & (BEV_EVENT_ERROR|BEV_EVENT_EOF)) {
struct event_base *base = ptr;
if (events & BEV_EVENT_ERROR) {
int err = bufferevent_socket_get_dns_error(bev);
if (err)
printf("DNS error: %s\n", evutil_gai_strerror(err));
}
printf("Closing\n");
bufferevent_free(bev);
event_base_loopexit(base, NULL);
}
}
int main(int argc, char **argv)
{
struct event_base *base;
struct evdns_base *dns_base;
struct bufferevent *bev;
if (argc != 3) {
printf("Trivial HTTP 0.x client\n"
"Syntax: %s [hostname] [resource]\n"
"Example: %s www.google.com /\n",argv[0],argv[0]);
return 1;
}
base = event_base_new();
dns_base = evdns_base_new(base, 1);
bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
bufferevent_setcb(bev, readcb, NULL, eventcb, base);
bufferevent_enable(bev, EV_READ|EV_WRITE);
evbuffer_add_printf(bufferevent_get_output(bev), "GET %s\r\n", argv[2]);
bufferevent_socket_connect_hostname(
bev, dns_base, AF_UNSPEC, argv[1], 80);
event_base_dispatch(base);
return 0;
}
bufferevent_socket_connect_hostname()
函数是 Libevent 2.0.3-alpha 中的新函数;bufferevent_socket_get_dns_error()
是 2.0.5-beta 中的新内容。
Generic bufferevent operations
本节中的函数适用于多个 bufferevent
实现。
Freeing a bufferevent
Interface
void bufferevent_free(struct bufferevent *bev);
这个函数释放一个bufferevent
。Bufferevents
是内部引用计数的,所以如果 bufferevent
在你释放它时有挂起的延迟回调,它在回调完成之前不会被删除。
然而,bufferevent_free()
函数会尝试尽快释放bufferevent
。如果缓冲区事件上有待写入的数据,则在释放缓冲区事件之前它可能不会被刷新。
如果设置了 BEV_OPT_CLOSE_ON_FREE
标志,并且此 bufferevent
有一个与之关联的套接字或底层 bufferevent
作为其传输,则当您释放 bufferevent
时,该传输将关闭。
这个函数是在 Libevent 0.8 中引入的。
Manipulating callbacks, watermarks, and enabled operations
Interface
typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void *ctx);
typedef void (*bufferevent_event_cb)(struct bufferevent *bev,
short events, void *ctx);
void bufferevent_setcb(struct bufferevent *bufev,
bufferevent_data_cb readcb, bufferevent_data_cb writecb,
bufferevent_event_cb eventcb, void *cbarg);
void bufferevent_getcb(struct bufferevent *bufev,
bufferevent_data_cb *readcb_ptr,
bufferevent_data_cb *writecb_ptr,
bufferevent_event_cb *eventcb_ptr,
void **cbarg_ptr);
bufferevent_setcb()
函数更改一个或多个 bufferevent
的回调。readcb
、writecb
和 eventcb
函数在读取足够的数据、写入足够的数据或发生事件时(分别)调用。每个的第一个参数是发生事件的缓冲区事件。最后一个参数是用户在bufferevent_callcb()
的cbarg
参数中提供的值:您可以使用它来将数据传递给您的回调。事件回调的events
参数是事件标志的位掩码:请参阅上面的callbacks, watermarks
。
您可以通过传递 NULL 而不是回调函数来禁用回调。请注意,bufferevent
上的所有回调函数共享一个cbarg
值,因此更改它会影响所有回调函数。
您可以通过将指针传递给 bufferevent_getcb()
来检索当前设置的 bufferevent
回调,它设置 *readcb_ptr
为当前读取回调,*writecb_ptr
为当前写入回调,*eventcb_ptr
为当前事件回调,以及 *cbarg_ptr
为当前回调参数字段。任何设置为 NULL 的指针都将被忽略。
bufferevent_setcb()
函数是在 Libevent 1.4.4 中引入的。类型名称bufferevent_data_cb
和bufferevent_event_cb
是Libevent 2.0.2-alpha中的新内容。bufferevent_getcb()
函数是在 2.1.1-alpha 中添加的。
Interface
void bufferevent_enable(struct bufferevent *bufev, short events);
void bufferevent_disable(struct bufferevent *bufev, short events);
short bufferevent_get_enabled(struct bufferevent *bufev);
您可以在缓冲区事件上启用或禁用事件 EV_READ
、EV_WRITE
或 EV_READ|EV_WRITE
。未启用读取或写入时,bufferevent
不会尝试读取或写入数据。
输出缓冲区为空时无需禁用写入:bufferevent
自动停止写入,并在有数据写入时重新启动。
类似地,当输入缓冲区达到其高水位线时,无需禁用读取:bufferevent
自动停止读取,并在有空间读取时再次重新启动。
默认情况下,新创建的 bufferevent
已启用写入,但未启用读取。
您可以调用 bufferevent_get_enabled()
以查看当前在 bufferevent
上启用了哪些事件。
这些函数是在 Libevent 0.8 中引入的,但 bufferevent_get_enabled()
是在 2.0.3-alpha 版本中引入的。
Interface
void bufferevent_setwatermark(struct bufferevent *bufev, short events,
size_t lowmark, size_t highmark);
bufferevent_setwatermark()
函数调整单个缓冲区事件的读取水印、写入水印或两者。(如果events
字段设置EV_READ
,则调整读取水印。如果event
s字段设置EV_WRITE
,则调整写入水印。)
0 的高水位线相当于“无限制
”。
该函数在 Libevent 1.4.4 中首次公开。
Example
#include <event2/event.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <event2/util.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
struct info {
const char *name;
size_t total_drained;
};
void read_callback(struct bufferevent *bev, void *ctx)
{
struct info *inf = ctx;
struct evbuffer *input = bufferevent_get_input(bev);
size_t len = evbuffer_get_length(input);
if (len) {
inf->total_drained += len;
evbuffer_drain(input, len);
printf("Drained %lu bytes from %s\n",
(unsigned long) len, inf->name);
}
}
void event_callback(struct bufferevent *bev, short events, void *ctx)
{
struct info *inf = ctx;
struct evbuffer *input = bufferevent_get_input(bev);
int finished = 0;
if (events & BEV_EVENT_EOF) {
size_t len = evbuffer_get_length(input);
printf("Got a close from %s. We drained %lu bytes from it, "
"and have %lu left.\n", inf->name,
(unsigned long)inf->total_drained, (unsigned long)len);
finished = 1;
}
if (events & BEV_EVENT_ERROR) {
printf("Got an error from %s: %s\n",
inf->name, evutil_socket_error_to_string(EVUTIL_SOCKET_ERROR()));
finished = 1;
}
if (finished) {
free(ctx);
bufferevent_free(bev);
}
}
struct bufferevent *setup_bufferevent(void)
{
struct bufferevent *b1 = NULL;
struct info *info1;
info1 = malloc(sizeof(struct info));
info1->name = "buffer 1";
info1->total_drained = 0;
/* ... Here we should set up the bufferevent and make sure it gets
connected... */
/* Trigger the read callback only whenever there is at least 128 bytes
of data in the buffer. */
bufferevent_setwatermark(b1, EV_READ, 128, 0);
bufferevent_setcb(b1, read_callback, NULL, event_callback, info1);
bufferevent_enable(b1, EV_READ); /* Start reading. */
return b1;
}
Manipulating data in a bufferevent
如果你看不到它,从网络读取和写入数据对你没有好处。Bufferevents
为您提供了这些方法来为它们提供要写入的数据以及要读取的数据:
Interface
struct evbuffer *bufferevent_get_input(struct bufferevent *bufev);
struct evbuffer *bufferevent_get_output(struct bufferevent *bufev);
这两个函数是非常强大的基础:它们分别返回输入和输出缓冲区。有关您可以对 evbuffer
类型执行的所有操作的完整信息,请参阅下一章。
Note
应用程序只能删除(而不是添加)输入缓冲区上的数据,并且只能添加(而不是删除)输出缓冲区中的数据。
如果缓冲区事件上的写入由于数据太少而停止(或者如果读取因数据过多而停止),则将数据添加到输出缓冲区(或从输入缓冲区中删除数据)将自动重新启动它。
这些函数是在 Libevent 2.0.1-alpha 中引入的。
Interface
int bufferevent_write(struct bufferevent *bufev,
const void *data, size_t size);
int bufferevent_write_buffer(struct bufferevent *bufev,
struct evbuffer *buf);
这些函数将数据添加到 bufferevent
的输出缓冲区。调用 bufferevent_write()
将size字节从内存中的data
添加到输出缓冲区的末尾。调用 bufferevent_write_buffer()
删除buf的全部内容并将它们放在输出缓冲区的末尾。如果成功,两者都返回 0,如果发生错误,则返回 -1。
这些函数从 Libevent 0.8 开始就存在了。
Interface
size_t bufferevent_read(struct bufferevent *bufev, void *data, size_t size);
int bufferevent_read_buffer(struct bufferevent *bufev,
struct evbuffer *buf);
这些函数从 bufferevent
的输入缓冲区中删除数据。bufferevent_read()
函数从输入缓冲区中删除最多size个字节,将它们存储到内存中的data。它返回实际删除的字节数。bufferevent_read_buffer()
函数清空输入缓冲区的全部内容并将它们放入 buf
;成功时返回 0,失败时返回 -1。
Note
使用 bufferevent_read()
时,data
处的内存块实际上必须有足够的空间来保存size字节的数据。
bufferevent_read()
函数从 Libevent 0.8 开始就存在了;bufferevent_read_buffer()
是在 Libevent 2.0.1-alpha 中引入的。
Example
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <ctype.h>
void
read_callback_uppercase(struct bufferevent *bev, void *ctx)
{
/* This callback removes the data from bev's input buffer 128
bytes at a time, uppercases it, and starts sending it
back.
(Watch out! In practice, you shouldn't use toupper to implement
a network protocol, unless you know for a fact that the current
locale is the one you want to be using.)
*/
char tmp[128];
size_t n;
int i;
while (1) {
n = bufferevent_read(bev, tmp, sizeof(tmp));
if (n <= 0)
break; /* No more data. */
for (i=0; i<n; ++i)
tmp[i] = toupper(tmp[i]);
bufferevent_write(bev, tmp, n);
}
}
struct proxy_info {
struct bufferevent *other_bev;
};
void
read_callback_proxy(struct bufferevent *bev, void *ctx)
{
/* You might use a function like this if you're implementing
a simple proxy: it will take data from one connection (on
bev), and write it to another, copying as little as
possible. */
struct proxy_info *inf = ctx;
bufferevent_read_buffer(bev,
bufferevent_get_output(inf->other_bev));
}
struct count {
unsigned long last_fib[2];
};
void
write_callback_fibonacci(struct bufferevent *bev, void *ctx)
{
/* Here's a callback that adds some Fibonacci numbers to the
output buffer of bev. It stops once we have added 1k of
data; once this data is drained, we'll add more. */
struct count *c = ctx;
struct evbuffer *tmp = evbuffer_new();
while (evbuffer_get_length(tmp) < 1024) {
unsigned long next = c->last_fib[0] + c->last_fib[1];
c->last_fib[0] = c->last_fib[1];
c->last_fib[1] = next;
evbuffer_add_printf(tmp, "%lu", next);
}
/* Now we add the whole contents of tmp to bev. */
bufferevent_write_buffer(bev, tmp);
/* We don't need tmp any longer. */
evbuffer_free(tmp);
}
Read- and write timeouts
与其他事件一样,如果经过一定时间后缓冲区事件没有成功写入或读取任何数据,则可以调用超时。
Interface
void bufferevent_set_timeouts(struct bufferevent *bufev,
const struct timeval *timeout_read, const struct timeval *timeout_write);
将超时设置为 NULL
应该可以删除它;但是在 Libevent 2.1.2-alpha 之前,这不适用于所有事件类型。(作为旧版本的解决方法,您可以尝试将超时设置为多天间隔和/或让您的 eventcb
函数在您不想要它们时忽略 BEV_TIMEOUT
事件。)
如果 bufferevent
在尝试读取读取时等待至少timeout_read
秒,则读取超时将触发 。如果 bufferevent
在 尝试写入数据时等待至少timeout_write
秒,则写入超时将触发。
Note
超时仅在 bufferevent
想要读取或写入时计数。换句话说,如果在 bufferevent
上禁用读取,或者如果输入缓冲区已满(在其高水位标记处),则不会启用读取超时。同样,如果写入被禁用,或者如果没有数据要写入,则不会启用写入超时。
当发生读或写超时时,相应的读或写操作在 bufferevent 上
被禁用。然后使用 BEV_EVENT_TIMEOUT|BEV_EVENT_READING
或 BEV_EVENT_TIMEOUT|BEV_EVENT_WRITING
调用事件回调。
这个函数从 Libevent 2.0.1-alpha 开始就存在了。在 Libevent 2.0.4-alpha 之前,它在 bufferevent
类型之间的行为不一致。
Initiating a flush on a bufferevent
Interface
int bufferevent_flush(struct bufferevent *bufev,
short iotype, enum bufferevent_flush_mode state);
刷新 bufferevent
告诉 bufferevent
强制从底层传输读取或写入尽可能多的字节,忽略其他可能阻止写入的限制。它的详细功能取决于bufferevent
的类型。
iotype
参数应为 EV_READ
、EV_WRITE
或 EV_READ|EV_WRITE
以指示是否应处理读取、写入或两者的字节。状态参数可以是 BEV_NORMAL
、BEV_FLUSH
或 BEV_FINISHED
之一。BEV_FINISHED
表示应该告诉对方不再发送数据;BEV_NORMAL
和 BEV_FLUSH
之间的区别取决于缓冲事件的类型。
bufferevent_flush()
函数在失败时返回 -1,如果没有数据被刷新则返回 0,如果某些数据被刷新则返回 1。
当前(从 Libevent 2.0.5-beta 开始),bufferevent_flush()
仅针对某些 bufferevent
类型实现。特别是,基于套接字的缓冲区事件没有它。
Type-specific bufferevent functions
并非所有 bufferevent
类型都支持这些 bufferevent
函数。
Interface
int bufferevent_priority_set(struct bufferevent *bufev, int pri);
int bufferevent_get_priority(struct bufferevent *bufev);
该函数将用于实现bufev
的事件的优先级调整 为pri
。有关优先级的更多信息,请参阅 event_priority_set()
。
此函数成功时返回 0,失败时返回 -1。它仅适用于基于套接字的缓冲区事件。
bufferevent_priority_set()
函数是在 Libevent 1.0 中引入的;bufferevent_get_priority()
直到 Libevent 2.1.2-alpha 才出现。
Interface
int bufferevent_setfd(struct bufferevent *bufev, evutil_socket_t fd);
evutil_socket_t bufferevent_getfd(struct bufferevent *bufev);
这些函数设置或返回基于 fd
的事件的文件描述符。只有基于套接字的缓冲区事件支持 setfd()
。两者都在失败时返回 -1;setfd()
成功返回 0。
bufferevent_setfd()
函数是在 Libevent 1.4.4 中引入的;bufferevent_getfd()
函数是在 Libevent 2.0.2-alpha 中引入的。
Interface
struct event_base *bufferevent_get_base(struct bufferevent *bev);
此函数返回缓冲区事件的 event_base
。它是在 2.0.9-rc 中引入的。
Interface
struct bufferevent *bufferevent_get_underlying(struct bufferevent *bufev);
此函数返回另一个缓冲区事件用作传输的缓冲区事件(如果有)。有关何时会发生这种情况的信息,请参阅有关过滤缓冲区事件的说明。
这个函数是在 Libevent 2.0.2-alpha 中引入的。
Manually locking and unlocking a bufferevent
与 evbuffers
一样,有时您希望确保对 bufferevent
的许多操作都以原子方式执行。Libevent 公开了可用于手动锁定和解锁缓冲区事件的函数。
Interface
void bufferevent_lock(struct bufferevent *bufev);
void bufferevent_unlock(struct bufferevent *bufev);
Note
如果缓冲区事件在创建时未赋予 BEV_OPT_THREADSAFE
线程,或者未激活 Libevent 的线程支持,则锁定缓冲区事件将不起作用。
使用此函数锁定 bufferevent
也将锁定其关联的 evbuffers
。这些函数是递归的:锁定一个你已经持有锁的缓冲区事件是安全的。当然,每次锁定 bufferevent
时都必须调用 unlock
一次。
这些函数是在 Libevent 2.0.6-rc 中引入的。
Obsolete bufferevent functionality
bufferevent
后端代码在 Libevent 1.4 和 Libevent 2.0 之间进行了大量修订。在旧接口中,有时通过访问 struct bufferevent
的内部结构来构建并使用依赖于这种访问的宏是正常的。
为了使事情变得混乱,旧代码有时使用以evbuffer
为前缀的 bufferevent
功能名称。
以下是关于在 Libevent 2.0 之前曾经被调用的事物的简要指南:
Current name | Old name |
---|---|
bufferevent_data_cb | evbuffercb |
bufferevent_event_cb | everrorcb |
BEV_EVENT_READING | EVBUFFER_READ |
BEV_EVENT_WRITE | EVBUFFER_WRITE |
BEV_EVENT_EOF | EVBUFFER_EOF |
BEV_EVENT_ERROR | EVBUFFER_ERROR |
BEV_EVENT_TIMEOUT | EVBUFFER_TIMEOUT |
bufferevent_get_input(b) | EVBUFFER_INPUT(b) |
bufferevent_get_output(b) | EVBUFFER_OUTPUT(b) |
旧函数是在 event.h
中定义的,而不是在 event2/bufferevent.h
中。
如果您仍然需要访问 bufferevent
结构的公共部分的内部结构,您可以包含 event2/bufferevent_struct.h
。我们建议不要这样做:struct bufferevent
的内容将在 Libevent 版本之间发生变化。如果包含 event2/bufferevent_compat.h
,则本节中的宏和名称可用。
设置缓冲区事件的接口在旧版本中有所不同:
Interface
struct bufferevent *bufferevent_new(evutil_socket_t fd,
evbuffercb readcb, evbuffercb writecb, everrorcb errorcb, void *cbarg);
int bufferevent_base_set(struct event_base *base, struct bufferevent *bufev);
bufferevent_new()
函数仅创建套接字缓冲区事件,并在已弃用的“默认” event_base
上执行此操作。调用 bufferevent_base_set
仅调整套接字缓冲区事件的 event_base
。
不是将超时设置为 struct timeval
,而是将它们设置为秒数:
Interface
void bufferevent_settimeout(struct bufferevent *bufev,
int timeout_read, int timeout_write);
最后,请注意 2.0 之前的 Libevent 版本的底层 evbuffer
实现非常低效,以至于将 bufferevents
用于高性能应用程序有点值得怀疑。
R6a: Bufferevents: advanced topics
本章描述了 Libevent 的 bufferevent
实现的一些典型用途不需要的高级功能。如果你刚刚学习如何使用 bufferevents
,你现在应该跳过这一章,继续阅读evbuffer 章节。
Paired bufferevents
有时,您有一个需要与自身对话的网络程序。例如,您可以编写一个程序来通过某个协议建立隧道用户连接,而该程序有时也希望通过该协议建立自己的连接隧道。当然,您可以通过打开与您自己的侦听端口的连接并让您的程序使用自己来实现这一点,但是,让您的程序通过网络堆栈与自己对话会浪费资源。
相反,您可以创建一对成对的缓冲区事件,以便写入一个的所有字节都被另一个接收(反之亦然),但不使用实际的平台套接字。
Interface
int bufferevent_pair_new(struct event_base *base, int options,
struct bufferevent *pair[2]);
调用 bufferevent_pair_new()
将 pair[0]
和 pair[1]
设置为一对 bufferevent
,每个都连接到另一个。支持所有常用选项,但 BEV_OPT_CLOSE_ON_FREE
无效,BEV_OPT_DEFER_CALLBACKS
始终打开。
为什么 bufferevent
对需要在回调延迟的情况下运行?对一对元素中的一个元素的操作调用改变 bufferevent
的回调是很常见的,从而调用另一个 bufferevent
的回调,等等通过许多步骤。当回调没有延迟时,这个调用链会经常溢出堆栈,使其他连接饿死,并要求所有回调都是可重入的。
成对的缓冲事件支持刷新;将 mode 参数设置为 BEV_NORMAL
或 BEV_FLUSH
强制所有相关数据从一对缓冲区事件传输到另一个缓冲区事件,忽略否则会限制它的水印。将 mode 设置为 BEV_FINISHED
还会在相反的 bufferevent
上生成一个 EOF 事件。
释放对中的任何一个成员不会自动释放另一个或生成 EOF 事件;它只会使配对中的另一个成员断开连接。一旦 bufferevent
被取消链接,它将不再成功读取或写入数据或生成任何事件。
界面
Interface
struct bufferevent *bufferevent_pair_get_partner(struct bufferevent *bev)
有时您可能需要在仅给定一个成员的情况下获取 bufferevent
对的另一个成员。为此,您可以调用 bufferevent_pair_get_partner()
函数。如果bev
是一对的成员,并且另一个成员仍然存在,它将返回该对的另一个成员。否则,它返回 NULL。
Bufferevent
对是 Libevent 2.0.1-alpha 中的新内容;bufferevent_pair_get_partner()
函数是在 Libevent 2.0.6 中引入的。
Filtering bufferevents
有时您想转换通过 bufferevent
对象的所有数据。您可以这样做以添加压缩层,或将协议包装在另一个协议中以进行传输。
Interface
enum bufferevent_filter_result {
BEV_OK = 0,
BEV_NEED_MORE = 1,
BEV_ERROR = 2
};
typedef enum bufferevent_filter_result (*bufferevent_filter_cb)(
struct evbuffer *source, struct evbuffer *destination, ev_ssize_t dst_limit,
enum bufferevent_flush_mode mode, void *ctx);
struct bufferevent *bufferevent_filter_new(struct bufferevent *underlying,
bufferevent_filter_cb input_filter,
bufferevent_filter_cb output_filter,
int options,
void (*free_context)(void *),
void *ctx);
bufferevent_filter_new()
函数创建一个新的过滤缓冲区事件,包裹在现有的“基础”缓冲区事件周围。通过底层bufferevent
接收的所有数据在到达过滤bufferevent
之前用“输入”过滤器进行转换,通过过滤bufferevent
发送的所有数据在发送到底层bufferevent
之前用“输出”过滤器进行转换。
将过滤器添加到底层 bufferevent
会替换底层 bufferevent
上的回调。您仍然可以向底层 bufferevent
的 evbuffers
添加回调,但如果您希望过滤器仍然工作,则不能在 bufferevent
本身上设置回调。
input_filter
和output_filter
的功能描述如下。options
支持所有常用选项。如果设置了 BEV_OPT_CLOSE_ON_FREE
,那么释放过滤缓冲区事件也会释放底层缓冲区事件。CTX
字段是传递给过滤器的功能的任意的指针; 如果提供了free_context
函数,它会在过滤缓冲区事件关闭之前在ctx上调用。
只要底层输入缓冲区上有新的可读数据,就会调用输入过滤器函数。只要过滤器的输出缓冲区中有新的可写数据,就会调用输出过滤器函数。每个人都会收到一对 evbuffer:一个用于读取数据的源evbuffer 和一个用于写入数据的 目标evbuffer。所述dst_limit参数所描述的上界的字节添加到目的地。过滤器函数可以忽略这个值,但这样做可能会违反高水位线或速率限制。如果dst_limit为 -1,则没有限制。该模式 参数告诉过滤器在写作中的积极性。如果是BEV_NORMAL,那么它应该尽可能多地写,可以方便地转换。BEV_FLUSH 值意味着尽可能多地写入,而 BEV_FINISHED 意味着过滤函数应该在流的末尾额外进行任何必要的清理。最后,过滤器函数的ctx 参数是一个空指针,提供给 bufferevent_filter_new()
构造函数。
如果任何数据成功写入目标缓冲区,过滤器函数必须返回 BEV_OK
,如果没有更多的数据可以写入目标缓冲区而没有更多的输入或使用不同的刷新模式,则返回 BEV_NEED_MORE
,如果出现不可恢复的错误,则返回 BEV_ERROR
过滤器。
创建过滤器可以对底层缓冲区事件进行读取和写入。您不需要自己管理读/写:过滤器将在不想读取时为您挂起对底层 bufferevent
的读取。对于 2.0.8-rc 及更高版本,允许启用/禁用独立于过滤器的底层 bufferevent
的读取和写入。但是,如果您这样做,您可能会阻止过滤器成功获取它想要的数据。
您不需要同时指定输入过滤器和输出过滤器:您省略的任何过滤器都将替换为传递数据而不进行转换的过滤器。
Limiting maximum single read/write size
默认情况下,bufferevents
不会在每次调用事件循环时读取或写入最大可能的字节数;这样做会导致奇怪的不公平行为和资源匮乏。另一方面,默认值可能不适用于所有情况。
Interface
int bufferevent_set_max_single_read(struct bufferevent *bev, size_t size);
int bufferevent_set_max_single_write(struct bufferevent *bev, size_t size);
ev_ssize_t bufferevent_get_max_single_read(struct bufferevent *bev);
ev_ssize_t bufferevent_get_max_single_write(struct bufferevent *bev);
这两个“设置”函数分别替换了当前的读写最大值。如果大小值为 0 或高于 EV_SSIZE_MAX
,则将最大值设置为默认值。这些函数在成功时返回 0,在失败时返回 -1。
两个“get”函数分别返回当前每个循环的读取和写入最大值。
这些功能是在 2.1.1-alpha 中添加的。
Bufferevents and Rate-limiting
一些程序想要限制用于任何单个缓冲区事件或一组缓冲区事件的带宽量。Libevent 2.0.4-alpha 和 Libevent 2.0.5-alpha 添加了一个基本工具来设置单个缓冲区事件的上限,或者将缓冲区事件分配给一个速率受限的组。
The rate-limiting model
Libevent 的速率限制使用令牌桶算法来决定一次读取或写入多少字节。每个受速率限制的对象在任何给定时间都有一个**“读桶”和一个“写桶”**,它们的大小决定了允许对象立即读取或写入的字节数。每个桶都有一个重新填充率、一个最大突发大小和一个计时单位或“滴答”。每当计时单位结束时,桶会根据重新填充率按比例重新填充——但如果它变得比其突发大小更满,则任何多余的字节都会丢失。
因此,重新填充速率决定了对象发送或接收字节的最大平均速率,而突发大小决定了在单个突发中将发送或接收的最大字节数。计时单位决定了交通的顺畅程度。
Setting a rate limit on a bufferevent
Interface
#define EV_RATE_LIMIT_MAX EV_SSIZE_MAX
struct ev_token_bucket_cfg;
struct ev_token_bucket_cfg *ev_token_bucket_cfg_new(
size_t read_rate, size_t read_burst,
size_t write_rate, size_t write_burst,
const struct timeval *tick_len);
void ev_token_bucket_cfg_free(struct ev_token_bucket_cfg *cfg);
int bufferevent_set_rate_limit(struct bufferevent *bev,
struct ev_token_bucket_cfg *cfg);
一个ev_token_bucket_cfg
结构表示为一对用于限制读写上bufferevents
的单个bufferevent
或组令牌桶的配置值。要创建一个,调用 ev_token_bucket_cfg_new
函数并提供最大平均读取速率、最大读取突发、最大写入速率、最大写入突发和滴答的长度。如果tick_len
参数为 NULL,则刻度的长度默认为一秒。该函数可能会在出错时返回 NULL。
Note
read_rate
和write_rate
参数以每个滴答的字节为单位进行缩放。也就是说,如果滴答是十分之一秒,而 read_rate
是 300,那么最大平均读取速率是每秒 3000 字节。不支持超过 EV_RATE_LIMIT_MAX
的速率和突发值。
要限制缓冲区事件的传输速率,请使用 ev_token_bucket_cfg
对其调用 bufferevent_set_rate_limit()
。该函数在成功时返回 0,在失败时返回 -1。您可以为任意数量的缓冲区事件提供相同的 ev_token_bucket_cfg
。要删除缓冲区事件的速率限制,请调用 bufferevent_set_rate_limit()
,为cfg参数传递 NULL 。
要释放 ev_token_bucket_cfg
,请调用 ev_token_bucket_cfg_free()
。请注意,在没有缓冲区事件使用 ev_token_bucket_cfg
之前,目前这样做是不安全的。
Setting a rate limit on a group of bufferevents
如果您想限制它们的总带宽使用,您可以将 bufferevents
分配给速率限制组。
Interface
struct bufferevent_rate_limit_group;
struct bufferevent_rate_limit_group *bufferevent_rate_limit_group_new(
struct event_base *base,
const struct ev_token_bucket_cfg *cfg);
int bufferevent_rate_limit_group_set_cfg(
struct bufferevent_rate_limit_group *group,
const struct ev_token_bucket_cfg *cfg);
void bufferevent_rate_limit_group_free(struct bufferevent_rate_limit_group *);
int bufferevent_add_to_rate_limit_group(struct bufferevent *bev,
struct bufferevent_rate_limit_group *g);
int bufferevent_remove_from_rate_limit_group(struct bufferevent *bev);
要构建速率限制组,请使用 event_base
和初始 ev_token_bucket_cfg
调用 bufferevent_rate_limit_group()
。您可以使用 bufferevent_add_to_rate_limit_group()
和 bufferevent_remove_from_rate_limit_group()
将 bufferevents
添加到组中;这些函数在成功时返回 0,在错误时返回 -1。
单个缓冲区事件一次只能是一个速率限制组的成员。一个bufferevent
可以有一个单独的速率限制(用bufferevent_set_rate_limit()
设置)和一个组速率限制。当两个限制都设置时,每个缓冲事件的下限适用。
您可以通过调用 bufferevent_rate_limit_group_set_cfg()
来更改现有组的速率限制。成功时返回 0,失败时返回 -1。bufferevent_rate_limit_group_free()
函数释放速率限制组并删除其所有成员。
从 2.0 版本开始,Libevent 的组速率限制尝试在总体上公平,但在非常小的时间尺度上实现可能是不公平的。如果您非常关心日程安排的公平性,请帮助提供未来版本的补丁。
Inspecting current rate-limit values
有时您的代码可能想要检查适用于给定缓冲区事件或组的当前速率限制。libevent 提供了一些函数来做到这一点。
Interface
ev_ssize_t bufferevent_get_read_limit(struct bufferevent *bev);
ev_ssize_t bufferevent_get_write_limit(struct bufferevent *bev);
ev_ssize_t bufferevent_rate_limit_group_get_read_limit(
struct bufferevent_rate_limit_group *);
ev_ssize_t bufferevent_rate_limit_group_get_write_limit(
struct bufferevent_rate_limit_group *);
上述函数返回缓冲区事件或组的读取或写入令牌桶的当前大小(以字节为单位)。请注意,如果缓冲区事件已被强制超过其分配,则这些值可能为负。(刷新 bufferevent
可以做到这一点。)
Interface
ev_ssize_t bufferevent_get_max_to_read(struct bufferevent *bev);
ev_ssize_t bufferevent_get_max_to_write(struct bufferevent *bev);
这些函数返回缓冲区事件现在愿意读取或写入的字节数,考虑适用于缓冲区事件的任何速率限制、其速率限制组(如果有)和任何最大读取/写入Libevent 作为一个整体强加的一次值。
Interface
void bufferevent_rate_limit_group_get_totals(
struct bufferevent_rate_limit_group *grp,
ev_uint64_t *total_read_out, ev_uint64_t *total_written_out);
void bufferevent_rate_limit_group_reset_totals(
struct bufferevent_rate_limit_group *grp);
每个 bufferevent_rate_limit_group
跟踪通过它发送的总字节数。您可以使用它来跟踪组中多个缓冲区事件的总使用量。对组调用 bufferevent_rate_limit_group_get_totals()
将 * total_read_out
和 * total_written_out
分别设置为在缓冲区事件组上读取和写入的总字节数。这些总数在创建组时从 0 开始,并在对组调用 bufferevent_rate_limit_group_reset_totals()
时重置为 0。
Manually adjusting rate limits
对于需求非常复杂的程序,您可能需要调整令牌桶的当前值。例如,如果您的程序以某种不通过缓冲事件的方式产生流量,您可能想要这样做。
Interface
int bufferevent_decrement_read_limit(struct bufferevent *bev, ev_ssize_t decr);
int bufferevent_decrement_write_limit(struct bufferevent *bev, ev_ssize_t decr);
int bufferevent_rate_limit_group_decrement_read(
struct bufferevent_rate_limit_group *grp, ev_ssize_t decr);
int bufferevent_rate_limit_group_decrement_write(
struct bufferevent_rate_limit_group *grp, ev_ssize_t decr);
这些函数递减缓冲区事件或速率限制组中的当前读或写桶。请注意,减量是有符号的:如果要增加存储桶,请传递一个负值。
Setting the smallest share possible in a rate-limited group
通常,您不想将速率限制组中的可用字节平均分配给每个滴答中的所有缓冲区事件。例如,如果您在一个速率限制组中有 10,000 个活动的缓冲区事件,并且每个滴答有 10,000 个字节可用于写入,那么由于系统调用和 TCP 的开销,让每个缓冲区事件每个滴答只写入 1 个字节是没有效率的标题。
为了解决这个问题,每个限速组都有一个“最小份额”的概念。在上面的情况下,不是每个bufferevent
每个tick允许写入1个字节,而是每个tick允许10,000/SHARE bufferevents
每个tick写入SHARE字节,其余的不允许写入任何内容。允许首先写入的缓冲区事件在每个滴答声中随机选择。
选择默认的最小共享以提供不错的性能,当前(从 2.0.6-rc 开始)设置为 64。您可以使用以下函数调整此值:
Interface
int bufferevent_rate_limit_group_set_min_share(
struct bufferevent_rate_limit_group *group, size_t min_share);
将 min_share
设置为 0 将完全禁用最小共享代码。
自首次推出以来,Libevent 的速率限制一直具有最低份额。更改它们的函数首先在 Libevent 2.0.6-rc 中公开。
Limitations of the rate-limiting implementation
从 Libevent 2.0 开始,您应该了解速率限制实现的一些限制。
-
并非每个 bufferevent 类型都支持速率限制,或者根本不支持。
-
Bufferevent 限速组不能嵌套,一个bufferevent一次只能在一个限速组中。
-
速率限制实现仅将 TCP 数据包中传输的字节数计为数据,不包括 TCP 标头。
-
读取限制实现依赖于 TCP 堆栈,注意到应用程序仅以特定速率消耗数据,并在其缓冲区已满时推回 TCP 连接的另一端。
-
bufferevents 的一些实现(特别是
windows IOCP
实现)可能会过度提交。 -
存储桶从一个完整滴答的流量开始。这意味着 bufferevent 可以立即开始读取或写入,而不是等到一个完整的滴答过去。然而,这也意味着,一个被速率限制为 N.1 滴答的缓冲事件可能会传输 N+1 滴答的流量。
-
刻度不能小于 1 毫秒,并且忽略毫秒的所有小数部分。
/// TODO: 写一个限速示例
Bufferevents and SSL
Bufferevents 可以使用 OpenSSL
库来实现 SSL/TLS 安全传输层。由于许多应用程序不需要或不想链接 OpenSSL,因此此功能在安装为libevent_openssl
的单独库中实现。Libevent 的未来版本可能会添加对其他 SSL/TLS 库(例如 NSS 或 GnuTLS)的支持,但现在只有 OpenSSL。
OpenSSL 功能是在 Libevent 2.0.3-alpha 中引入的,尽管它在 Libevent 2.0.5-beta 或 Libevent 2.0.6-rc 之前效果不佳。
本节不是关于 OpenSSL、SSL/TLS 或一般密码学的教程。
这些函数都在头文件event2/bufferevent_ssl.h
中声明。
Setting up and using an OpenSSL-based bufferevent
Interface
enum bufferevent_ssl_state {
BUFFEREVENT_SSL_OPEN = 0,
BUFFEREVENT_SSL_CONNECTING = 1,
BUFFEREVENT_SSL_ACCEPTING = 2
};
struct bufferevent *
bufferevent_openssl_filter_new(struct event_base *base,
struct bufferevent *underlying,
SSL *ssl,
enum bufferevent_ssl_state state,
int options);
struct bufferevent *
bufferevent_openssl_socket_new(struct event_base *base,
evutil_socket_t fd,
SSL *ssl,
enum bufferevent_ssl_state state,
int options);
您可以创建两种 SSL 缓冲区事件:通过另一个底层缓冲区事件进行通信的基于过滤器的缓冲区事件,或者告诉 OpenSSL
直接通过网络与网络通信的基于套接字的缓冲区事件。无论哪种情况,您都必须提供 SSL 对象和 SSL 对象状态的描述。如果 SSL 当前作为客户端执行协商,状态应该是 BUFFEREVENT_SSL_CONNECTING
,如果 SSL 当前作为服务器执行协商,则状态应该是 BUFFEREVENT_SSL_ACCEPTING
,或者如果 SSL 握手完成,状态应该是 BUFFEREVENT_SSL_OPEN
。
接受通常的选项;当 openssl bufferevent
本身关闭时,BEV_OPT_CLOSE_ON_FREE
使 SSL 对象和底层 fd
或 bufferevent
关闭。
握手完成后,新的 bufferevent
的事件回调将通过标志中的 BEV_EVENT_CONNECTED
调用。
如果您正在创建一个基于套接字的缓冲区事件并且 SSL 对象已经有一个套接字集,则您不需要自己提供套接字:只需传递 -1。您也可以稍后使用 bufferevent_setfd()
设置 fd。
/// 待办事项:一旦 bufferevent_shutdown() API
完成就删除它。
Note
当在 SSL 缓冲区事件上设置 BEV_OPT_CLOSE_ON_FREE
时,将不会对 SSL 连接执行干净关闭。这有两个问题:首先,连接似乎已经被对方“破坏”了,而不是被干净地关闭了:对方将无法判断你是否关闭了连接,或者它是否被对方破坏了。攻击者或第三方。其次,OpenSSL 会将会话视为“坏的”,并从会话缓存中删除。这可能会导致负载下 SSL 应用程序的性能显着下降。
目前唯一的解决方法是手动关闭 SSL。虽然这违反了 TLS RFC
,但它会确保会话在关闭后将保留在缓存中。以下代码实现了此解决方法。
Example
SSL *ctx = bufferevent_openssl_get_ssl(bev);
/*
* SSL_RECEIVED_SHUTDOWN tells SSL_shutdown to act as if we had already
* received a close notify from the other end. SSL_shutdown will then
* send the final close notify in reply. The other end will receive the
* close notify and send theirs. By this time, we will have already
* closed the socket and the other end's real close notify will never be
* received. In effect, both sides will think that they have completed a
* clean shutdown and keep their sessions valid. This strategy will fail
* if the socket is not ready for writing, in which case this hack will
* lead to an unclean shutdown and lost session on the other end.
*/
SSL_set_shutdown(ctx, SSL_RECEIVED_SHUTDOWN);
SSL_shutdown(ctx);
bufferevent_free(bev);
Interface
SSL *bufferevent_openssl_get_ssl(struct bufferevent *bev);
此函数返回 OpenSSL 缓冲区事件使用的 SSL 对象,如果bev
不是基于OpenSSL 的缓冲区事件,则返回 NULL 。
Interface
unsigned long bufferevent_get_openssl_error(struct bufferevent *bev);
此函数返回给定缓冲区事件操作的第一个未决 OpenSSL 错误,如果没有未决错误,则返回 0。错误格式由 openssl 库中的 ERR_get_error()
返回。
Interface
int bufferevent_ssl_renegotiate(struct bufferevent *bev);
调用此函数会告诉 SSL 重新协商,并告诉 bufferevent
调用适当的回调。这是一个高级话题;您通常应该避免使用它,除非您真的知道自己在做什么,尤其是因为许多 SSL 版本都存在与重新协商相关的已知安全问题。
Interface
int bufferevent_openssl_get_allow_dirty_shutdown(struct bufferevent *bev);
void bufferevent_openssl_set_allow_dirty_shutdown(struct bufferevent *bev,
int allow_dirty_shutdown);
SSL 协议的所有良好版本(即 SSLv3 和所有 TLS 版本)都支持经过身份验证的关闭操作,使各方能够将有意关闭与底层缓冲区中的意外或恶意引发的终止区分开来。默认情况下,除了正确关闭之外,我们将任何事情都视为连接错误。但是,如果 allow_dirty_shutdown
标志设置为 1,我们会将连接中的关闭视为 BEV_EVENT_EOF
。
在 Libevent 2.1.1-alpha 中添加了 allow_dirty_shutdown
函数。
Example: A simple SSL-based echo server
/* Simple echo server using OpenSSL bufferevents */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/rand.h>
#include <event.h>
#include <event2/listener.h>
#include <event2/bufferevent_ssl.h>
static void
ssl_readcb(struct bufferevent * bev, void * arg)
{
struct evbuffer *in = bufferevent_get_input(bev);
printf("Received %zu bytes\n", evbuffer_get_length(in));
printf("----- data ----\n");
printf("%.*s\n", (int)evbuffer_get_length(in), evbuffer_pullup(in, -1));
bufferevent_write_buffer(bev, in);
}
static void
ssl_acceptcb(struct evconnlistener *serv, int sock, struct sockaddr *sa,
int sa_len, void *arg)
{
struct event_base *evbase;
struct bufferevent *bev;
SSL_CTX *server_ctx;
SSL *client_ctx;
server_ctx = (SSL_CTX *)arg;
client_ctx = SSL_new(server_ctx);
evbase = evconnlistener_get_base(serv);
bev = bufferevent_openssl_socket_new(evbase, sock, client_ctx,
BUFFEREVENT_SSL_ACCEPTING,
BEV_OPT_CLOSE_ON_FREE);
bufferevent_enable(bev, EV_READ);
bufferevent_setcb(bev, ssl_readcb, NULL, NULL, NULL);
}
static SSL_CTX *
evssl_init(void)
{
SSL_CTX *server_ctx;
/* Initialize the OpenSSL library */
SSL_load_error_strings();
SSL_library_init();
/* We MUST have entropy, or else there's no point to crypto. */
if (!RAND_poll())
return NULL;
server_ctx = SSL_CTX_new(SSLv23_server_method());
if (! SSL_CTX_use_certificate_chain_file(server_ctx, "cert") ||
! SSL_CTX_use_PrivateKey_file(server_ctx, "pkey", SSL_FILETYPE_PEM)) {
puts("Couldn't read 'pkey' or 'cert' file. To generate a key\n"
"and self-signed certificate, run:\n"
" openssl genrsa -out pkey 2048\n"
" openssl req -new -key pkey -out cert.req\n"
" openssl x509 -req -days 365 -in cert.req -signkey pkey -out cert");
return NULL;
}
SSL_CTX_set_options(server_ctx, SSL_OP_NO_SSLv2);
return server_ctx;
}
int
main(int argc, char **argv)
{
SSL_CTX *ctx;
struct evconnlistener *listener;
struct event_base *evbase;
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(9999);
sin.sin_addr.s_addr = htonl(0x7f000001); /* 127.0.0.1 */
ctx = evssl_init();
if (ctx == NULL)
return 1;
evbase = event_base_new();
listener = evconnlistener_new_bind(
evbase, ssl_acceptcb, (void *)ctx,
LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 1024,
(struct sockaddr *)&sin, sizeof(sin));
event_base_loop(evbase, 0);
evconnlistener_free(listener);
SSL_CTX_free(ctx);
return 0;
}
Some
Some notes on threading and OpenSSL
Libevent 的内置线程机制不包括 OpenSSL 锁定。由于 OpenSSL 使用了无数的全局变量,因此您仍然必须将 OpenSSL 配置为线程安全的。虽然这个过程超出了 Libevent 的范围,但这个话题已经足够值得讨论了。
Example: A very simple example of how to enable thread safe OpenSSL
/*
* Please refer to OpenSSL documentation to verify you are doing this correctly,
* Libevent does not guarantee this code is the complete picture, but to be used
* only as an example.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <openssl/ssl.h>
#include <openssl/crypto.h>
pthread_mutex_t * ssl_locks;
int ssl_num_locks;
/* Implements a thread-ID function as requied by openssl */
static unsigned long
get_thread_id_cb(void)
{
return (unsigned long)pthread_self();
}
static void
thread_lock_cb(int mode, int which, const char * f, int l)
{
if (which < ssl_num_locks) {
if (mode & CRYPTO_LOCK) {
pthread_mutex_lock(&(ssl_locks[which]));
} else {
pthread_mutex_unlock(&(ssl_locks[which]));
}
}
}
int
init_ssl_locking(void)
{
int i;
ssl_num_locks = CRYPTO_num_locks();
ssl_locks = malloc(ssl_num_locks * sizeof(pthread_mutex_t));
if (ssl_locks == NULL)
return -1;
for (i = 0; i < ssl_num_locks; i++) {
pthread_mutex_init(&(ssl_locks[i]), NULL);
}
CRYPTO_set_id_callback(get_thread_id_cb);
CRYPTO_set_locking_callback(thread_lock_cb);
return 0;
}