libevent 实现一个 TcpRelay

本文介绍了使用libevent库实现TCP中继(TcpRelay)的方法,旨在解决客户端无法直接访问服务端的问题。通过TcpRelay,数据在客户端和服务端之间间接传输。文章详细阐述了TcpRelay处理并发连接、防止消息串话、处理连接建立与断开以及数据堆积等关键问题的策略,并给出了核心代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

libevent 实现一个 TcpRelay

前言

最近用 libevent 实现了一个 TcpRelay,也叫 TCP 中继器,同时连接客户端和服务端来使双方进行间接的通信。一般是为了解决客户端无法直接访问服务端的情况,可以通过 TcpRelay 来替双方转发消息,是比较简单的网络程序,只需把收到的数据交给另一方就行。

在这里插入图片描述

使用 libevent 的目的第一个是熟悉这个网络库,抱着学习的态度来使用,也想和最近正在看的 muduo 库做一些比较;其次是熟悉代理服务器的一些实现方法,所以就先从简单的 tcprelay 来下手。

TcpRelay 的功能说起来简单,但事实上还是有一些细节是需要考虑的(来自《Linux 多线程服务端编程》):

  1. TcpRelay 收到 client 请求后才和 server 建立连接,如果此时收到 client 消息,但 tcprelay 还正在和 server 建立连接,如何处理这部分数据?
  2. TcpRelay 可以服务多个 client,如何管理这些 client,防止消息互相影响造成串话?
  3. 为了模拟出 C/S 连接的模式,TcpRelay 会在其中一端断开的时候,自己同时断开另一端,如果断开 client 的时候有另外的 client 连接进来,复用了该 fd,会不会导致串话的现象?如果双方同时断开,又该如何应对?
  4. 如果两端传输速度不一致,导致 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);
}

这里做了这几件事:

  1. 首先创建 TcpRelay 作为服务端角色的 bufferevent,绑定全局的 event_base 和 listenfd;
  2. 拿到传进来的 server 地址,创建相应的 bufferevent;
  3. 绑定与 server 通信的事件回调方法,用来处理 TcpRelay 作为客户端角色的消息,并且把作为服务端的 bufferevent 传给回调方法。
  4. 与 server 建立连接;
  5. 开启读事件;
  6. 开启 TcpRelay 作为服务端和客户端的读写高水位,防止堆积过多数据(解决问题4)。
  7. 绑定与 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:

  1. 打开 TcpRelay 与客户端通信的读通道;
  2. 获取 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);
    }
}

(全文完)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值