libevent 实现一个 TcpRelay
前言
最近用 libevent 实现了一个 TcpRelay,也叫 TCP 中继器,同时连接客户端和服务端来使双方进行间接的通信。一般是为了解决客户端无法直接访问服务端的情况,可以通过 TcpRelay 来替双方转发消息,是比较简单的网络程序,只需把收到的数据交给另一方就行。
使用 libevent 的目的第一个是熟悉这个网络库,抱着学习的态度来使用,也想和最近正在看的 muduo 库做一些比较;其次是熟悉代理服务器的一些实现方法,所以就先从简单的 tcprelay 来下手。
TcpRelay 的功能说起来简单,但事实上还是有一些细节是需要考虑的(来自《Linux 多线程服务端编程》):
- TcpRelay 收到 client 请求后才和 server 建立连接,如果此时收到 client 消息,但 tcprelay 还正在和 server 建立连接,如何处理这部分数据?
- TcpRelay 可以服务多个 client,如何管理这些 client,防止消息互相影响造成串话?
- 为了模拟出 C/S 连接的模式,TcpRelay 会在其中一端断开的时候,自己同时断开另一端,如果断开 client 的时候有另外的 client 连接进来,复用了该 fd,会不会导致串话的现象?如果双方同时断开,又该如何应对?
- 如果两端传输速度不一致,导致 TcpRelay 堆积消息过多怎么处理?
作为服务端的角色
首先 TcpRelay 自身是作为服务端的角色,需要来接收 client 的请求,所以需要开启监听。
struct evconnlistener* listener = evconnlistener_new_bind(g_base, server_connection_cb, (void*)&serv_addr, LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE, -1, (struct sockaddr*)&sin, sizeof(sin));
其中 sin 是 TcpRelay 的监听地址,server_connection_cb
是连接到来事件,用来处理 client 连接,serv_addr 是 server 的地址,通过参数的形式传到回调函数中,为了和 server 建立连接。
连接建立
TcpRelay 处理新连接是在 server_connection_cb
中,如下:
void server_connection_cb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *sa, int socklen, void *ctx)
{
printf("Client connected!\n");
struct bufferevent *server_bev = bufferevent_socket_new(g_base, fd, BEV_OPT_CLOSE_ON_FREE); // 1
if (!server_bev) {
perror("bufferevent_socket_new error");
return;
}
struct sockaddr_in *serv_addr = (struct sockaddr_in *)ctx; // 2
struct bufferevent *client_bev = bufferevent_socket_new(g_base, -1, BEV_OPT_CLOSE_ON_FREE); // 2
bufferevent_setcb(client_bev, client_message_cb, NULL, client_event_cb, (void*)server_bev); // 3
if (bufferevent_socket_connect(client_bev, (struct sockaddr*)serv_addr, sizeof(struct sockaddr_in)) < 0) { // 4
perror("connect error");
bufferevent_free(client_bev);
}
bufferevent_enable(client_bev, EV_READ | EV_PERSIST); // 5
bufferevent_setwatermark(client_bev, EV_WRITE | EV_READ, 0, 1024*1024); // 6
bufferevent_setwatermark(server_bev, EV_WRITE | EV_READ, 0, 1024*1024); // 6
bufferevent_setcb(server_bev, server_message_cb, NULL, server_event_cb, (void*)client_bev); // 7
bufferevent_enable(server_bev, EV_PERSIST);
bufferevent_disable(server_bev, EV_READ);
}
这里做了这几件事:
- 首先创建 TcpRelay 作为服务端角色的 bufferevent,绑定全局的 event_base 和 listenfd;
- 拿到传进来的 server 地址,创建相应的 bufferevent;
- 绑定与 server 通信的事件回调方法,用来处理 TcpRelay 作为客户端角色的消息,并且把作为服务端的 bufferevent 传给回调方法。
- 与 server 建立连接;
- 开启读事件;
- 开启 TcpRelay 作为服务端和客户端的读写高水位,防止堆积过多数据(解决问题4)。
- 绑定与 client 通信的事件回调方法,用来处理 TcpRelay 作为服务端角色的消息,并且把作为客户端的 bufferevent 传给回调方法。(解决问题 2);
这里解决了开头提出的两个问题,问题 2 是通过函数间传递 client_bev 的方式使得 TcpRelay 收到 server 的消息后与 client 通信, 每个连接都会创建唯一的一个 client_bev,fd 由 bufferevent 内部管理,不会造成串话的情况;问题 4 是通过设置两端高水位,防止数据堆积过多的情况,比较简单的处理。因此这些问题是可以利用网络库的特性解决。
消息处理
void server_message_cb(struct bufferevent *server_bev, void *ctx)
{
char buffer[BUFLEN] = {0};
struct evbuffer* input = bufferevent_get_input(server_bev);
int read_bytes = evbuffer_remove(input, buffer, sizeof(buffer));
if (ctx == NULL) {
perror("ctx error");
return;
}
printf("recv from client data: %s", buffer);
struct bufferevent *client_bev = (struct bufferevent *)ctx;
// 发给 server
if (bufferevent_write(client_bev, buffer, read_bytes) < 0) {
printf("bufferevent_write error\n");
}
}
TcpRelay 作为服务端处理 client 的消息很简单,拿到数据直接发给 server 即可,这里要注意 buffer 的初始化。
void server_event_cb(struct bufferevent *server_bev, short events, void *ctx)
{
if (events & (BEV_EVENT_ERROR|BEV_EVENT_EOF)) {
printf("Client disconnected!\n");
if (ctx == NULL) {
perror("ctx error");
return;
}
struct bufferevent *client_bev = (struct bufferevent *)ctx;
bufferevent_free(client_bev);
bufferevent_free(server_bev);
}
}
处理 client 连接关闭的时候同时会断开 server,通过调用 bufferevent_free,因为设置了 BEV_OPT_CLOSE_ON_FREE,所以释放后会关闭对应 fd,从而断开连接。
作为客户端的角色
连接建立
处理 client 成功建立的回调和 client 关闭的回调都在 client_event_cb 中。
void client_event_cb(struct bufferevent *client_bev, short events, void *ctx)
{
if (events & BEV_EVENT_CONNECTED) {
printf("Server connected!\n");
struct bufferevent *server_bev = (struct bufferevent *)ctx;
bufferevent_enable(server_bev, EV_READ); // 1
struct evbuffer* input = bufferevent_get_input(server_bev);
// 如果接收缓冲区有数据(来自客户端),发给服务端
if (evbuffer_get_length(input) > 0) {
char buffer[BUFLEN] = {0};
int read_bytes = evbuffer_remove(input, buffer, sizeof(buffer));
bufferevent_write(client_bev, buffer, read_bytes); // 2
}
}
else if (events & (BEV_EVENT_ERROR|BEV_EVENT_EOF)) {
printf("Server disconnected!\n");
if (ctx == NULL) {
perror("ctx error");
return;
}
struct bufferevent *server_bev = (struct bufferevent *)ctx;
bufferevent_free(server_bev);
bufferevent_free(client_bev);
}
}
首先如果是 TcpRelay 成功连接到 server:
- 打开 TcpRelay 与客户端通信的读通道;
- 获取 TcpRelay 与客户端通信的读缓冲区的数据,如果有则发给服务端(解决问题1);
其次处理 client 关闭事件,直接释放双端的 bufferevent,关闭双方连接。
这里通过 TcpRelay 与 server 建立连接后才打开读通道,然后检测读缓冲区,试探是否有数据可以发送,解决了问题1的情况,利用读缓冲区把来自 client 的数据先暂存了起来,等到连接建立后才读取数据发往 server。
处理消息
void client_message_cb(struct bufferevent *client_bev, void *ctx)
{
char buffer[BUFLEN] = {0};
struct bufferevent *server_bev = (struct bufferevent *)ctx;
struct evbuffer* input = bufferevent_get_input(client_bev);
int read_bytes = evbuffer_remove(input, buffer, sizeof(buffer));
printf("recv from server data: %s", buffer);
// 发给 client
bufferevent_write(server_bev, buffer, read_bytes);
}
TcpRelay 作为客户端的角色处理消息也同 server_message_cb 一样,拿到数据直接发送给 client。
总结
TcpRelay 的业务并不复杂,但有些细节需要处理。其中业务主要是两个部分,一个是作为服务端的部分,一个是作为客户端的部分。利用网络库可以解决其中的问题,包括解决双端并发、速度不匹配、连接的建立和断开所导致的问题。
本次实现这个程序时间有限,可能还会有一些漏洞。全部代码如下:
#include <event2/bufferevent.h>
#include <event2/buffer.h>
#include <event2/listener.h>
#include <event2/util.h>
#include <event2/event.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#define BUFLEN 2048
struct bufferevent *g_clients[1024];
struct event_base* g_base;
void client_message_cb(struct bufferevent *bev, void *ctx);
void client_event_cb(struct bufferevent *bev, short events, void *ctx);
void server_message_cb(struct bufferevent *bev, void *ctx);
void server_event_cb(struct bufferevent *bev, short events, void *ctx);
void server_connection_cb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *sa, int socklen, void *ctx);
void client_message_cb(struct bufferevent *client_bev, void *ctx)
{
char buffer[BUFLEN] = {0};
struct bufferevent *server_bev = (struct bufferevent *)ctx;
struct evbuffer* input = bufferevent_get_input(client_bev);
int read_bytes = evbuffer_remove(input, buffer, sizeof(buffer));
printf("recv from server data: %s", buffer);
bufferevent_write(server_bev, buffer, read_bytes);
}
void client_event_cb(struct bufferevent *client_bev, short events, void *ctx)
{
if (events & BEV_EVENT_CONNECTED) {
printf("Server connected!\n");
struct bufferevent *server_bev = (struct bufferevent *)ctx;
bufferevent_enable(server_bev, EV_READ);
struct evbuffer* input = bufferevent_get_input(server_bev);
// 如果接收缓冲区有数据(来自客户端),发给服务端
if (evbuffer_get_length(input) > 0) {
char buffer[BUFLEN] = {0};
int read_bytes = evbuffer_remove(input, buffer, sizeof(buffer));
bufferevent_write(client_bev, buffer, read_bytes);
}
}
else if (events & (BEV_EVENT_ERROR|BEV_EVENT_EOF)) {
printf("Server disconnected!\n");
struct bufferevent *server_bev = (struct bufferevent *)ctx;
if (ctx == NULL) {
perror("ctx error");
return;
}
bufferevent_free(server_bev);
bufferevent_free(client_bev);
}
}
void server_message_cb(struct bufferevent *server_bev, void *ctx)
{
char buffer[BUFLEN] = {0};
struct evbuffer* input = bufferevent_get_input(server_bev);
int read_bytes = evbuffer_remove(input, buffer, sizeof(buffer));
if (ctx == NULL) {
perror("ctx error");
return;
}
printf("recv from client data: %s", buffer);
struct bufferevent *client_bev = (struct bufferevent *)ctx;
if (bufferevent_write(client_bev, buffer, read_bytes) < 0) {
printf("bufferevent_write error\n");
}
}
void server_event_cb(struct bufferevent *server_bev, short events, void *ctx)
{
if (events & (BEV_EVENT_ERROR|BEV_EVENT_EOF)) {
printf("Client disconnected!\n");
if (ctx == NULL) {
perror("ctx error");
return;
}
struct bufferevent *client_bev = (struct bufferevent *)ctx;
bufferevent_free(client_bev);
bufferevent_free(server_bev);
}
}
void server_connection_cb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *sa, int socklen, void *ctx)
{
printf("Client connected!\n");
struct bufferevent *server_bev = bufferevent_socket_new(g_base, fd, BEV_OPT_CLOSE_ON_FREE);
if (!server_bev) {
perror("bufferevent_socket_new error");
return;
}
struct sockaddr_in *serv_addr = (struct sockaddr_in *)ctx;
struct bufferevent *client_bev = bufferevent_socket_new(g_base, -1, BEV_OPT_CLOSE_ON_FREE);
bufferevent_setcb(client_bev, client_message_cb, NULL, client_event_cb, (void*)server_bev);
if (bufferevent_socket_connect(client_bev, (struct sockaddr*)serv_addr, sizeof(struct sockaddr_in)) < 0) {
perror("connect error");
bufferevent_free(client_bev);
}
bufferevent_enable(client_bev, EV_READ | EV_PERSIST);
bufferevent_setwatermark(client_bev, EV_WRITE | EV_READ, 0, 1024*1024);
bufferevent_setwatermark(server_bev, EV_WRITE | EV_READ, 0, 1024*1024);
bufferevent_setcb(server_bev, server_message_cb, NULL, server_event_cb, (void*)client_bev);
bufferevent_enable(server_bev, EV_PERSIST);
}
int
main(int argc, char **argv)
{
if (argc < 4) {
fprintf(stderr, "Usage: %s <host_ip> <port> <listen_port>\n", argv[0]);
} else {
signal(SIGPIPE, SIG_IGN);
char* server_ip = argv[1];
uint16_t server_port = (uint16_t)atoi(argv[2]);
uint16_t listen_port = (uint16_t)atoi(argv[3]);
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(server_port);
inet_pton(AF_INET, server_ip, &serv_addr.sin_addr.s_addr);
g_base = event_base_new();
struct sockaddr_in sin = {0};
sin.sin_family = AF_INET;
sin.sin_port = htons(listen_port);
struct evconnlistener* listener = evconnlistener_new_bind(g_base, server_connection_cb, (void*)&serv_addr,
LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE, -1, (struct sockaddr*)&sin, sizeof(sin));
event_base_dispatch(g_base);
evconnlistener_free(listener);
event_base_free(g_base);
}
}
(全文完)