0%

基于libevent 的回声服务器

引言

计算机四大基础:

  • 计算机组成与体系结构
  • 操作系统
  • 计算机网络
  • 编译原理

计算机网络在实际中的体现就是计算机网络理论与网络编程。但工作中,我们终究不可能使用最基础的socket进行网络编译。因而学习一个高效的网络库是十分有必要的。为初步了解这个libevent就从最简单的回声服务器开始吧

回声服务器

本身libevent也是基于socket的编程,所以创建Tcp连接的过程同样与是需要遵从创建连接的函数调用顺序,即如下所示:
server_tcp_function_invoke.png

同样的,对于客户端同样有着一样的调用顺序的要求:
client_tcp_function_invoke.png
libevent 中则相对的简化了这样的繁杂的创建连接的过程,他将这些操作进行了封闭。对于用户而言,我们只需要在传入已实现的回调函数,剩下的即可交由libevent去完成。这也是与libevent设计理念是一致的,即用户无需关心内存事件的检测与处理,只需要去关注事件的具体处理即可
在使用上,有许多的网络库可以使用的:

  1. libev
  2. libuv
  3. libevent

libev是一个linux极为优秀的网络库,Python的协程库gevent就是一个基于libev的优秀作品。libuv是同样作为nodejs的核心组件,也是功能强大的。但是libev对于Windows平台的支持太差了,而libuv的优秀之处的异步IO与方便的nodejs集成特性,我都不需要,因此最后选择了libevent,可以利用libevent高性能的特性,对性能做到极致的追求!

服务器

回声服务器的功能是: 客户端将消息发送给服务器后,服务器将消息完整的返回给客户端
首先需要先初始化libevent是基础的结构: event_base

1
2
struct event_base* base;
base = event_base_new()

这时event_base_new()会对event_base中的参数进行初始化,也就相当于创建了一个socket。那么接下来,我们就应该为这个socket 绑定端口与IP了。
这里可使用libevent提供的:evconnlistener_new_bind的接口。其内部的大致的实现流程是:

  • evconnlistener_new_bind
  • 使用eventlistener_event 存储连接信息
  • 将连接到来时的回调函数实现设置下去
  • event_assign 将事件(event)与基本事件(event_base)绑定

为处理连接到来,我们还需要提供一个回调函数,根据listen.h中回调函数的定义如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**@file event2/listener.h

@brief A callback that we invoke when a listener has a new connection.

@param listener The evconnlistener
@param fd The new file descriptor
@param addr The source address of the connection
@param socklen The length of addr
@param user_arg the pointer passed to evconnlistener_new()
*/
typedef void (*evconnlistener_cb)(struct evconnlistener *,
evutil_socket_t,
struct sockaddr *,
int socklen, void *);

这个回调函数中,我们需要处理两件事件

  1. 读取客户端发送来的数据
  2. 将数据原样返回给客户端

依照前言所述,每个client都会分配一个bufferevent去处理连接,因此我们需要使用bufferevent_socket_new()去创建,但需要先获取我们当前客户端的event_base,因此代码如下:

1
2
3
4
5
6
7
void on_accpet_cb(struct evconnlistener *listener,
evutil_socket_t fd, struct sockaddr *address,
int socklen, void *ctx){
struct event_base* base = evconnlistener_get_base(listener);
struct bufferevent* bev =
bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
}

接下来就是设置为读回调与事件回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void EchoServer::on_read_cb(struct bufferevent *bev, void *ctx) {
struct evbuffer *input = bufferevent_get_input(bev);
struct evbuffer *output = bufferevent_get_output(bev);

// 从输入缓冲区读取数据
size_t len = evbuffer_get_length(input);
char *data = (char *)malloc(len);
evbuffer_copyout(input, data, len);

printf("The message from client is : %s", data);

// 将数据写入输出缓冲区
evbuffer_add_buffer(output, input);

// 释放内存
free(data);
}

void EchoServer::echo_event_cb(struct bufferevent *bev, short events,
void *ctx) {
if (events & BEV_EVENT_ERROR) {
perror("Error");
}
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
bufferevent_free(bev);
}
}

那么回调已经提供了,就可以回到on_accpet_cb中,将回调函数设置下去

1
2
3
4
5
6
// 设置读回调函数和事件回调函数
bufferevent_setcb(bev, on_read_cb, NULL, echo_event_cb, NULL);

// 开始监听读事件
bufferevent_enable(bev, EV_READ | EV_WRITE);

参数与回调都已提供后,我们就可以将event绑定到指定的event_base上了

1
2
3
4
5
6
7
8
9
struct sockaddr_in sin;
int port = 8888;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = htonl(INADDR_ANY);
sin.sin_port = htons(port);
listener = evconnlistener_new_bind(base, on_accept, NULL,
LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSABLE,
-1, (struct sockaddr*)&sin, sizeof(sin));

最后就是启动事件循环,等循环结束后,将listener与event_base清除即可

1
2
3
4
5
event_base_dispatch(base);

evconnlistener_free(listener);
event_base_free(base);

服务器完整的代码在:EchoServer.cpp

客户端

回声客户端所需的功能只有两个:

  1. 输入字符并将字符发送给服务器
  2. 获取服务器返回的字符

那么围绕这两个功能,我们还与服务不同的一点的是。我们是客户端并不需要像服务器那般,需要为每个连接都创建一个bufferevent。因此在客户端处,我们仅需要创建一个公用的bufferevent即可。
之后再通过bufferevent_socket_connect()来创建一个基于socket的buffer event,内部的实现上就是创建了一个bufferevent对象,用于处理底层socket的事件、数据的读出与写入。因此读取数据就需要传入一个回调函数的实现给bufferevent,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void read_cb(struct bufferevent *bev, void *ctx){
struct evbuffer *input = bufferevent_get_input(bev);
size_t len = evbuffer_get_length(input);
char *data = new char[len + 1];
evbuffer_copyout(input, data, len);
data[len] = '\0';

// 清空输入缓冲区
evbuffer_drain(input, len);

std::cout << "Received: " << data << std::endl;
delete[] data;
}

struct event_base *base = event_base_new();
if (!base) {
std::cerr << "Could not initialize event base." << std::endl;
return;
}

struct bufferevent *bev = bufferevent_socket_new(
base, -1, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS);
bufferevent_setcb(bev, read_cb, nullptr, event_cb,
base); // 设置读取回调函数和事件回调函数

struct sockaddr_in sin;
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = inet_addr(Ip);
sin.sin_port = htons(atoi(port));

if (bufferevent_socket_connect(bev, reinterpret_cast<struct sockaddr *>(&sin),
sizeof(sin)) < 0) {
printf("My errno is %d", errno);
perror("Error connecting to server ");
bufferevent_free(bev);
exit(1);
}

解决完读取与客户端与服务器连接问题,我们还需要去处理一下写入事件,即处理用户输入。这里我们就需要在原有的event中添加一个stdin_event事件,通过event_add添加到event_base中。具体实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void stdin_read_cb(evutil_socket_t fd, short event, void *arg) {
char message[1024];
struct bufferevent *bev = static_cast<struct bufferevent *>(arg);
struct evbuffer *output = bufferevent_get_output(bev);

if (bufferevent_getfd(bev) == -1) {
// 如果尚未连接到服务器,则不发送数据
return;
}

if (fgets(message, sizeof(message), stdin) != nullptr) {
evbuffer_add(output, message, strlen(message));
bufferevent_flush(bev, EV_WRITE, BEV_FLUSH);
} else {
event_base_loopbreak(event_get_base(static_cast<struct event *>(arg)));
}
}

struct event *stdin_event =
event_new(base, fileno(stdin), EV_READ | EV_PERSIST, stdin_read_cb, bev);
event_add(stdin_event, nullptr);

客户端设计到这里基本上是完成了,但是我们还需要开启bev的可读事件。如果不开启,那么绑定到bufferevent上的read_cb也就不会生效了。代码如下

1
2
3
4
5
6
7
8
9
10
11
// 启用bufferevent读写事件
bufferevent_enable(bev, EV_READ);

// 启动事件循环
event_base_dispatch(base);

// 释放事件与连接
bufferevent_free(bev);
event_base_free(base);
event_free(stdin_event);

服务器完整的代码在:Client.cpp

总结

总体来说,在熟悉libevent的基本流程后,写出这样一个demo还是很轻松的。写这样一个demo时,要明确的知道bufferevent的使用,以及对于 socket创建连接的流程。