MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

libevent 的 bufferevent 缓冲区操作技巧

2023-12-117.7k 阅读

libevent库简介

libevent 是一个用 C 语言编写的、轻量级的开源高性能事件通知库,它提供了一个跨平台的事件驱动的 I/O 框架。libevent 支持多种事件通知机制,如 epoll、kqueue、select 等,能够在不同的操作系统上提供高效的事件处理能力。它常用于网络编程、服务器开发等领域,帮助开发者处理各种 I/O 事件,如套接字可读、可写事件等。

bufferevent 概述

bufferevent 的定义

bufferevent 是 libevent 库中用于处理带缓冲的 I/O 操作的结构体。它封装了底层的套接字操作,并提供了缓冲区管理功能,使得开发者可以更加方便地处理数据的读写。bufferevent 结合了读缓冲区和写缓冲区,允许开发者在不直接操作套接字的情况下进行数据的收发。

bufferevent 的作用

在网络编程中,直接操作套接字进行读写可能会面临许多问题,比如数据的部分读取、写入失败等。bufferevent 通过缓冲区机制,简化了这些操作。读缓冲区可以缓存从套接字接收到的数据,直到应用程序准备好读取;写缓冲区则可以暂存应用程序要发送的数据,当套接字可写时再将数据发送出去。这种机制减少了对套接字的直接操作次数,提高了程序的效率和稳定性。

bufferevent 的创建与初始化

创建 bufferevent

在 libevent 中,可以使用 bufferevent_socket_new 函数来创建一个 bufferevent。该函数的原型如下:

struct bufferevent *bufferevent_socket_new(struct event_base *base, evutil_socket_t fd, enum bufferevent_options options);
  • base:指向 event_base 结构体的指针,event_base 是 libevent 事件处理的核心,用于管理所有的事件。
  • fd:要关联的套接字描述符。
  • options:一些选项,例如 BEV_OPT_CLOSE_ON_FREE 表示当 bufferevent 被释放时,自动关闭关联的套接字。

示例代码:

#include <event2/bufferevent.h>
#include <event2/event.h>
#include <event2/util.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define PORT 8888
#define MAX_BUFFER_SIZE 1024

void read_cb(struct bufferevent *bev, void *ctx) {
    char buffer[MAX_BUFFER_SIZE];
    size_t len = bufferevent_read(bev, buffer, MAX_BUFFER_SIZE - 1);
    if (len > 0) {
        buffer[len] = '\0';
        printf("Received: %s", buffer);
    }
}

void event_cb(struct bufferevent *bev, short events, void *ctx) {
    if (events & BEV_EVENT_EOF) {
        printf("Connection closed.\n");
    } else if (events & BEV_EVENT_ERROR) {
        printf("Some error occurred.\n");
    }
    bufferevent_free(bev);
}

int main() {
    struct event_base *base = event_base_new();
    if (!base) {
        perror("event_base_new");
        return 1;
    }

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket");
        event_base_free(base);
        return 1;
    }

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("connect");
        close(sockfd);
        event_base_free(base);
        return 1;
    }

    struct bufferevent *bev = bufferevent_socket_new(base, sockfd, BEV_OPT_CLOSE_ON_FREE);
    if (!bev) {
        perror("bufferevent_socket_new");
        close(sockfd);
        event_base_free(base);
        return 1;
    }

    bufferevent_setcb(bev, read_cb, NULL, event_cb, NULL);
    bufferevent_enable(bev, EV_READ | EV_WRITE);

    event_base_dispatch(base);

    event_base_free(base);
    return 0;
}

在上述代码中,首先创建了一个 event_base,然后创建套接字并连接到服务器。接着使用 bufferevent_socket_new 创建了一个 bufferevent,并将其与套接字关联。最后设置了读回调函数 read_cb 和事件回调函数 event_cb,并启用了读和写事件。

初始化 bufferevent

在创建 bufferevent 后,通常需要对其进行一些初始化操作,比如设置回调函数。可以使用 bufferevent_setcb 函数来设置读回调、写回调和事件回调函数。

void bufferevent_setcb(struct bufferevent *bufev, bufferevent_data_cb readcb, bufferevent_data_cb writecb, bufferevent_event_cb eventcb, void *cbarg);
  • bufev:要设置回调的 bufferevent。
  • readcb:读回调函数,当读缓冲区有数据可读时会调用此函数。
  • writecb:写回调函数,当写缓冲区有空间可写时会调用此函数(通常在应用层数据写入写缓冲区后,当套接字可写时触发)。
  • eventcb:事件回调函数,当发生一些事件,如连接关闭、错误等时会调用此函数。
  • cbarg:传递给回调函数的参数。

读缓冲区操作技巧

读取数据

从 bufferevent 的读缓冲区读取数据可以使用 bufferevent_read 函数。其原型为:

size_t bufferevent_read(struct bufferevent *bufev, void *dst, size_t size);
  • bufev:目标 bufferevent。
  • dst:存放读取数据的缓冲区。
  • size:要读取的最大字节数。

该函数会从读缓冲区中读取数据到 dst 指向的缓冲区,返回实际读取的字节数。如果读缓冲区中没有足够的数据,函数可能不会读取到 size 字节的数据。

示例代码:

void read_cb(struct bufferevent *bev, void *ctx) {
    char buffer[MAX_BUFFER_SIZE];
    size_t len = bufferevent_read(bev, buffer, MAX_BUFFER_SIZE - 1);
    if (len > 0) {
        buffer[len] = '\0';
        printf("Received: %s", buffer);
    }
}

在这个读回调函数中,使用 bufferevent_read 从读缓冲区读取数据到 buffer 中,并将其打印出来。

查看读缓冲区数据

有时候需要查看读缓冲区中是否有数据,或者查看数据的长度等信息。可以使用 bufferevent_get_input 函数获取读缓冲区的指针,然后通过缓冲区相关的函数来操作。

struct evbuffer *bufferevent_get_input(const struct bufferevent *bufev);

evbuffer 结构体提供了一些函数来操作缓冲区,比如 evbuffer_get_length 可以获取缓冲区中数据的长度。

示例代码:

void read_cb(struct bufferevent *bev, void *ctx) {
    struct evbuffer *input = bufferevent_get_input(bev);
    size_t len = evbuffer_get_length(input);
    if (len > 0) {
        printf("There are %zu bytes in the read buffer.\n", len);
    }
}

此代码片段在读回调函数中获取读缓冲区的长度并打印。

清理读缓冲区

在某些情况下,可能需要清理读缓冲区中的数据,比如在处理完数据后,确保缓冲区为空,以便接收新的数据。可以使用 evbuffer_drain 函数来从读缓冲区中移除一定数量的数据。

void evbuffer_drain(struct evbuffer *buf, size_t len);
  • buf:要操作的 evbuffer(通过 bufferevent_get_input 获取)。
  • len:要移除的数据长度。

示例代码:

void read_cb(struct bufferevent *bev, void *ctx) {
    struct evbuffer *input = bufferevent_get_input(bev);
    size_t len = evbuffer_get_length(input);
    evbuffer_drain(input, len);
    printf("Read buffer has been cleared.\n");
}

在这个例子中,读回调函数获取读缓冲区的长度,并移除所有数据,从而清理了读缓冲区。

写缓冲区操作技巧

写入数据

向 bufferevent 的写缓冲区写入数据可以使用 bufferevent_write 函数。其原型为:

int bufferevent_write(struct bufferevent *bufev, const void *data, size_t size);
  • bufev:目标 bufferevent。
  • data:要写入的数据。
  • size:数据的长度。

该函数将 data 指向的数据写入写缓冲区,返回实际写入的字节数。如果返回值小于 size,可能表示写缓冲区空间不足,需要等待写回调函数被触发以继续写入。

示例代码:

void send_message(struct bufferevent *bev, const char *message) {
    int len = strlen(message);
    int written = bufferevent_write(bev, message, len);
    if (written < len) {
        printf("Not all data was written to the write buffer.\n");
    }
}

在这个函数中,将 message 写入 bufferevent 的写缓冲区,并检查是否所有数据都成功写入。

查看写缓冲区状态

可以使用 bufferevent_get_output 函数获取写缓冲区的指针,进而查看写缓冲区的状态,比如剩余空间等。

struct evbuffer *bufferevent_get_output(const struct bufferevent *bufev);

evbuffer 提供了 evbuffer_spaceleft 函数来获取写缓冲区剩余的空间大小。

示例代码:

void write_cb(struct bufferevent *bev, void *ctx) {
    struct evbuffer *output = bufferevent_get_output(bev);
    size_t space_left = evbuffer_spaceleft(output);
    printf("There are %zu bytes of space left in the write buffer.\n", space_left);
}

在写回调函数中,获取写缓冲区的剩余空间并打印。

刷新写缓冲区

当应用程序向写缓冲区写入数据后,数据并不会立即发送出去,而是等待套接字可写时才会发送。有时候需要强制将写缓冲区中的数据发送出去,可以使用 bufferevent_flush 函数。

void bufferevent_flush(struct bufferevent *bufev, enum bufferevent_flush_mode mode, short what);
  • bufev:目标 bufferevent。
  • mode:刷新模式,例如 BEV_FLUSH_NORMAL 表示正常刷新,BEV_FLUSH_EOF 表示在刷新后关闭连接。
  • what:指定刷新读缓冲区、写缓冲区还是两者都刷新,如 EV_READEV_WRITEEV_READ|EV_WRITE

示例代码:

void send_and_flush(struct bufferevent *bev, const char *message) {
    int len = strlen(message);
    int written = bufferevent_write(bev, message, len);
    if (written < len) {
        printf("Not all data was written to the write buffer.\n");
    }
    bufferevent_flush(bev, BEV_FLUSH_NORMAL, EV_WRITE);
}

在这个函数中,先将消息写入写缓冲区,然后使用 bufferevent_flush 强制将写缓冲区的数据发送出去。

bufferevent 的事件处理

连接关闭事件

当连接关闭时,事件回调函数会被触发,并且 events 参数会包含 BEV_EVENT_EOF 标志。在事件回调函数中可以进行一些清理操作,比如释放 bufferevent。

示例代码:

void event_cb(struct bufferevent *bev, short events, void *ctx) {
    if (events & BEV_EVENT_EOF) {
        printf("Connection closed.\n");
    } else if (events & BEV_EVENT_ERROR) {
        printf("Some error occurred.\n");
    }
    bufferevent_free(bev);
}

在这个事件回调函数中,检查 events 是否包含 BEV_EVENT_EOFBEV_EVENT_ERROR 标志,并在相应情况下打印信息,最后释放 bufferevent。

错误事件

如果在 bufferevent 的操作过程中发生错误,事件回调函数会被触发,并且 events 参数会包含 BEV_EVENT_ERROR 标志。可以通过 evutil_socket_geterror 函数获取具体的错误信息。

示例代码:

void event_cb(struct bufferevent *bev, short events, void *ctx) {
    if (events & BEV_EVENT_ERROR) {
        int err = evutil_socket_geterror(bufferevent_getfd(bev));
        printf("Error occurred: %s\n", evutil_strerror(err));
    }
    bufferevent_free(bev);
}

在这个事件回调函数中,当发生错误时,获取套接字的错误信息并打印,然后释放 bufferevent。

性能优化与注意事项

缓冲区大小调整

合理调整读缓冲区和写缓冲区的大小对于性能至关重要。如果缓冲区过小,可能会导致频繁的读写操作,增加系统开销;如果缓冲区过大,可能会浪费内存。可以根据应用场景和预计的数据量来调整缓冲区大小。在创建 bufferevent 时,可以通过 evbuffer 的相关函数来设置缓冲区的初始大小。

避免阻塞操作

在回调函数中应避免执行阻塞操作,因为 libevent 是基于事件驱动的框架,阻塞操作会影响整个事件循环的运行,导致其他事件无法及时处理。如果需要进行一些耗时操作,应考虑将其放在单独的线程或进程中执行。

内存管理

正确管理 bufferevent 和相关缓冲区的内存是很重要的。当不再需要 bufferevent 时,应及时调用 bufferevent_free 函数释放其占用的资源,包括关联的套接字(如果设置了 BEV_OPT_CLOSE_ON_FREE)和缓冲区。同时,在操作缓冲区时,要注意避免内存泄漏和缓冲区溢出等问题。

通过合理运用 bufferevent 的缓冲区操作技巧,开发者可以在网络编程中更加高效、稳定地处理数据的读写,利用 libevent 提供的强大功能构建高性能的网络应用程序。在实际开发中,需要根据具体的需求和场景,灵活调整和优化 bufferevent 的使用,以达到最佳的性能和稳定性。