bufferevent 服务端实例
本章使用 bufferevent 实现一个服务端实例:
- 调用
evconnlistener_new_bind
建立一个 socket 监听; - 在监听回调
listen_cb
中使用 bufferevent 建立与客户端的连接,并指定读、写、事件回调,并设置客户端的超时时间; - 在读取回调
read_cb
中接受数据,接收一次就向客户端返回一个 OK 字符串,当接受到 quit 时,断开与客户端的连接; - 在事件回调
event_cb
中处理超时事件BEV_EVENT_TIMEOUT
,读取缓冲区中的剩余数据,断开与客户端的连接。其他事件直接断开连接。
整个服务端的代码如下:
#include <iostream>
#include <string.h>
#include <errno.h>
#ifndef _WIN32
#include <signal.h>
#endif // !_WIN32
#include "event.h"
#include "event2/listener.h"
#define SERVER_PORT 8000
char recv_buf[10240] = { 0 };
size_t recv_len = 0;
// 读取回调
void read_cb(struct bufferevent *bev, void *ctx) {
std::cout << "[R]" << std::endl;
// 读取输入缓冲数据
char data[1024] = { 0 };
size_t len = bufferevent_read(bev, data, sizeof(data) - 1);
std::cout << "read[" << len << "]: " << data << std::endl;
if (len <= 0) return;
strncat(recv_buf, data, sizeof(recv_buf) - recv_len - 1);
recv_len += len;
if (strstr(data, "quit") != NULL) {
std::cout << "quti" << std::endl;
// 退出并关闭socket,因为bufferevent设置了BEV_OPT_CLOSE_ON_FREE属性
bufferevent_free(bev);
}
// 发送OK到输出缓冲区
bufferevent_write(bev, "OK", 3);
}
// 写入回调
void write_cb(struct bufferevent *bev, void *ctx) {
std::cout << "[W]" << std::endl;
}
// 事件回调,错误,超时,连接断开会进入
void event_cb(struct bufferevent *bev, short what, void *ctx) {
std::cout << "[E]:" << what << std::endl;
if ((what & BEV_EVENT_READING) && (what & BEV_EVENT_TIMEOUT)) {
// 若是超时事件,读取数据会停止,即EV_READ不可读
std::cout << "BEV_EVENT_READING & BEV_EVENT_TIMEOUT" << std::endl;
// 因为设置了最低水位,此时缓存中的数据可能没有读完,需要继续读取
char data[1024] = { 0 };
size_t len = bufferevent_read(bev, data, sizeof(data) - 1);
if (len > 0) {
std::cout << "read[" << len << "]: " << data << std::endl;
strncat(recv_buf, data, sizeof(recv_buf) - recv_len - 1);
recv_len += len;
}
std::cout << "recv[" << recv_len << "]: " << recv_buf << std::endl;
// 可以重新设置可读,一般直接断开连接
// bufferevent_enable(bev, EV_READ);
bufferevent_free(bev);
}
else if (what & BEV_EVENT_EOF) {
// 客户端连接断开事件
std::cout << "BEV_EVENT_EOF" << std::endl;
bufferevent_free(bev);
}
else if (what & BEV_EVENT_ERROR) {
// 错误,直接断开连接
std::cout << "BEV_EVENT_ERROR" << std::endl;
bufferevent_free(bev);
}
else {
std::cout << "OTHERS" << std::endl;
}
}
// 监听回调
void listen_cb(struct evconnlistener *ev, evutil_socket_t fd, struct sockaddr *sin, int slen, void *arg) {
std::cout << "listen_cb" << std::endl;
event_base *base = (event_base*)arg;
// 创建 bufferevent 上下文,BEV_OPT_CLOSE_ON_FREE 清理bufferevent时关闭socket
bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
// 添加监控事件
bufferevent_enable(bev, EV_READ | EV_WRITE);
// 设置读取水位
bufferevent_setwatermark(bev, EV_READ,
5, // 低水位,默认0,即无限制,最低读取数量
10); // 高水位,默认0,即无限制,最高读取数量
// 设置写入水位(目前还不太清楚具体应用,以后可能会补上)
/*bufferevent_setwatermark(bev, EV_WRITE,
5, // 低水位,默认0,即无限制,缓冲区低于设置时,回调函数被调用
0); // 高水位无效*/
// 设置超时时间
timeval tv = { 3, 0 };
bufferevent_set_timeouts(bev, &tv, NULL);
// 设置回调函数
bufferevent_setcb(bev, read_cb, write_cb, event_cb, base);
}
int main(int argc, char* argv[])
{
#ifdef _WIN32
// 初始化socket库
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
#else
// 忽略管道信号,因为发送数据给已关闭的socket会生成SIGPIPE信号,导致进程退出
if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
return -1;
#endif
std::cout << "libevent bufferevent test" << std::endl;
// 创建libevent上下文
event_base *base = event_base_new();
if (!base) {
std::cout << "event_base_new failed" << std::endl;
return -1;
}
std::cout << "event_base_new success" << std::endl;
// 绑定IP和端口
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(SERVER_PORT);
// 创建监听事件
evconnlistener *ev = evconnlistener_new_bind(base,
listen_cb, // 回调函数
base, // 回调函数参数
LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE, // 设置地址复用和关闭时释放资源
10, // listen back
(sockaddr*)&sin, sizeof(sin)); // 监听地址
// 进入事件主循环
event_base_dispatch(base);
if (ev) evconnlistener_free(ev);
event_base_free(base);
#ifdef _WIN32
WSACleanup();
#endif
return 0;
}
为了测试,在93行设置了读取水位(实际项目中不会这样用)。在104行设置了读取超时,若发生超时事件,而因为有低水位限制,缓冲区中仍会有一部分数据没有读取,此时需要调用 read 函数将缓冲区中的数据读完,见代码52 ~ 59 行。
下图是使用 netassist 工具进行测试的结果,服务端设置了读取水位:满足5个才读取,一次最多读取10个字符数据。从图中可知:
- 第一次发送 aaa ,此时服务端数据不足5个,没有达到最低水位,此时不会有任何操作,netassist 也没有收到 OK 的回复;
- 第二次发送 aaa ,此时服务端有6个数据(加上第一次发送的3个字符),已达到最低水位,此时会调用读回调函数,读取到6个字符(aaaaaa),netassist 会收到 OK 的回复;
- 第三次发送 123456,服务端数据达已到最低水位,会调用读回调函数,读取到6个字符(123456),netassist 会收到 OK 的回复;
- 第四次发送 1234567890123456,服务端收到16个字符数据,已超过最高水位,会调用两次读回调函数,第一次读取10个字符(1234567890),第二次读取6个字符(123456),netassist 会收到两个 OK 的回复。
这里使用了网络调试助手充当客户端程序,这个工具很好用,强烈推荐。下面介绍一个客户端实例。
bufferevent 客户端实例
本章使用 bufferevent 实现一个客户端实例:
- 调用
bufferevent_socket_connect
连接指定的服务端,并指定读、写、事件回调; - 在事件回调
client_event_cb
中处理连接事件BEV_EVENT_CONNECTED
,标识连接成功,并触发 write 事件回调; - 在写入回调
client_write_cb
中读取指定的文件,并将文件发送给服务端。
整个客户端的代码如下:
#include <iostream>
#include <string.h>
#include <errno.h>
#ifndef _WIN32
#include <signal.h>
#endif // !_WIN32
#include "event.h"
#define SERVER_PORT 8000
size_t send_len = 0;
// 读取回调
void client_read_cb(struct bufferevent *bev, void *ctx)
{
std::cout << "[CLIENT R]" << std::endl;
}
// 写入回调
void client_write_cb(struct bufferevent *bev, void *ctx)
{
std::cout << "[CLIENT W]" << std::endl;
FILE *fp = (FILE*)ctx;
char data[1024] = { 0 };
size_t len = fread(data, 1, sizeof(data) - 1, fp);
std::cout << "len=" << len << ", data=" << data << std::endl;
send_len += len;
if (len <= 0) {
// 读到结尾或者文件出错
fclose(fp);
// 立刻清理,可能会造成缓冲区数据发送没有结束
// bufferevent_free(bev);
// 可以将写入禁用,因为当缓冲区可写入的时候是会再次进入
bufferevent_disable(bev, EV_WRITE);
std::cout << "send total len=" << send_len << std::endl;
return;
}
// 写入bufferevent
bufferevent_write(bev, data, len);
}
// 事件回调,错误,超时,连接断开会进入
void client_event_cb(struct bufferevent *bev, short what, void *ctx)
{
std::cout << "[CLIENT E]:" << what << std::endl;
if ((what & BEV_EVENT_READING) && (what & BEV_EVENT_TIMEOUT)) {
// 若是超时事件,读取数据会停止,即EV_READ不可读
std::cout << "BEV_EVENT_READING & BEV_EVENT_TIMEOUT" << std::endl;
bufferevent_free(bev);
}
else if (what & BEV_EVENT_EOF) {
// 服务端连接断开事件
std::cout << "BEV_EVENT_EOF" << std::endl;
bufferevent_free(bev);
}
else if (what & BEV_EVENT_ERROR) {
// 错误,直接断开连接
std::cout << "BEV_EVENT_ERROR" << std::endl;
bufferevent_free(bev);
}
else if (what & BEV_EVENT_CONNECTED) {
// 连接成功
std::cout << "BEV_EVENT_CONNECTED" << std::endl;
// 触发 write 事件
bufferevent_trigger(bev, EV_WRITE, 0);
}
else {
std::cout << "OTHERS" << std::endl;
}
}
int main(int argc, char* argv[])
{
if (argc < 2) {
std::cout << "argument error\n";
std::cout << "usage:\n\t" << argv[0] << " [file]" << std::endl;
return -1;
}
#ifdef _WIN32
// 初始化socket库
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
#else
// 忽略管道信号,因为发送数据给已关闭的socket会生成SIGPIPE信号,导致进程退出
if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
return -1;
#endif
std::cout << "libevent bufferevent test" << std::endl;
// 创建libevent上下文
event_base *base = event_base_new();
if (!base) {
std::cout << "event_base_new failed" << std::endl;
return -1;
}
std::cout << "event_base_new success" << std::endl;
// 客户端bufferevent,-1表示内部创建socket
bufferevent *bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
// 指定服务端的IP和端口
struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(SERVER_PORT);
evutil_inet_pton(AF_INET, "127.0.0.1", &sin.sin_addr.s_addr);
FILE *fp = fopen(argv[1], "rb");
if (!fp) {
std::cout << "fopen error!" << std::endl;
return -1;
}
// 设置回调函数
bufferevent_setcb(bev, client_read_cb, client_write_cb, client_event_cb, fp);
bufferevent_enable(bev, EV_READ | EV_WRITE);
int ret = bufferevent_socket_connect(bev, (sockaddr*)&sin, sizeof(sin));
if (ret == 0)
// 进入事件主循环
event_base_dispatch(base);
event_base_free(base);
#ifdef _WIN32
WSACleanup();
#endif
return 0;
}
注意:客户端不用主动断开,因为服务端程序检测到客户端超时后,会主动断开与客户端的连接,客户端只需要被动接受服务端的断开事件 BEV_EVENT_EOF
,即代码第 55 ~ 59 行。这是为了演示效果,实际编码时不能这样用,即 实际使用时,客户端处理完后应立刻断开连接 。