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

使用 libevent 库实现惊人的高并发 C++ 服务器

2022-08-297.4k 阅读

1. 高并发服务器背景及 libevent 库概述

在当今互联网应用爆发式增长的时代,后端服务器面临着前所未有的高并发挑战。想象一下,像大型电商平台在促销活动期间,瞬间会有成千上万的用户同时访问商品页面、下单购买;又如热门直播平台,海量观众同时观看直播并发送弹幕互动。传统的单线程或简单多线程服务器模型在应对如此大规模的并发请求时,性能会急剧下降,甚至出现响应迟缓、服务崩溃等问题。

为了应对这些挑战,开发高性能的高并发服务器成为后端开发的关键任务。其中,事件驱动编程模型因其高效的 I/O 处理能力而备受青睐。而 libevent 库,作为事件驱动编程的优秀代表,为开发者提供了一个简洁且强大的框架,能够轻松实现高并发服务器。

libevent 库是一个跨平台的高性能事件通知库,它支持多种事件多路复用机制,如 selectpollepoll(Linux 系统)、kqueue(FreeBSD、Mac OS X 等系统)等。这些机制可以高效地监控多个文件描述符(如套接字)的状态变化,一旦有事件发生,libevent 库就能迅速通知应用程序进行相应处理。同时,libevent 库还提供了对定时器、信号等其他事件类型的支持,使得开发者可以在一个统一的框架下处理各种不同类型的事件。

2. 环境搭建

在开始使用 libevent 库编写高并发服务器之前,我们需要先搭建好开发环境。不同的操作系统安装方式略有不同,以下以 Ubuntu 系统为例介绍安装步骤。

2.1 安装依赖

在 Ubuntu 系统中,libevent 库可以通过 apt 包管理器进行安装。首先更新软件包列表:

sudo apt update

然后安装 libevent 开发包:

sudo apt install libevent-dev

这个命令会安装 libevent 库及其头文件,为我们后续的开发提供必要的支持。

2.2 编译器及 IDE 选择

对于 C++ 开发,GCC(GNU Compiler Collection)是一个常用且功能强大的编译器。在 Ubuntu 系统中,可以通过以下命令安装:

sudo apt install build-essential

这会安装 GCC 编译器以及一些开发工具。

至于 IDE(Integrated Development Environment),有很多选择,如 CLion、Eclipse CDT、VS Code 等。这里以 VS Code 为例,它是一个轻量级且功能丰富的跨平台代码编辑器。安装 VS Code 后,再安装 C/C++ 扩展插件,即可方便地进行 C++ 代码的编写、调试等操作。

3. libevent 库基础概念

在深入使用 libevent 库编写高并发服务器之前,我们需要先了解一些基本概念。

3.1 事件基(Event Base)

事件基是 libevent 库的核心概念之一,它可以看作是一个事件循环的容器。所有的事件都注册到事件基中,事件基负责监听这些事件的发生,并在事件发生时调用相应的回调函数进行处理。可以把事件基想象成一个调度中心,它统筹管理着所有的事件。

在代码中,我们通过 event_base_new() 函数来创建一个事件基对象:

#include <event2/event.h>

// 创建事件基
struct event_base* base = event_base_new();
if (!base) {
    std::cerr << "Failed to create event base" << std::endl;
    return 1;
}

在上述代码中,我们调用 event_base_new() 函数创建了一个事件基对象,并检查是否创建成功。如果创建失败,输出错误信息并退出程序。

3.2 事件(Event)

事件是 libevent 库中表示特定发生情况的对象,比如套接字上有数据可读、定时器超时、信号到达等。每个事件都关联到一个事件基,并且有一个回调函数,当事件发生时,libevent 库会调用这个回调函数来处理事件。

事件可以分为两种类型:I/O 事件和信号事件。I/O 事件用于监听文件描述符(如套接字)的读写状态变化,信号事件用于监听系统信号(如 SIGINT、SIGTERM 等)。

以创建一个简单的 I/O 事件为例,假设我们有一个套接字 fd,想要监听其可读事件:

#include <event2/event.h>
#include <iostream>

// 事件回调函数
void read_callback(evutil_socket_t fd, short events, void* arg) {
    std::cout << "Socket is readable, fd: " << fd << std::endl;
}

// 创建并添加事件
struct event* ev = event_new(base, fd, EV_READ | EV_PERSIST, read_callback, nullptr);
if (!ev) {
    std::cerr << "Failed to create event" << std::endl;
    event_base_free(base);
    return 1;
}

if (event_add(ev, nullptr) == -1) {
    std::cerr << "Failed to add event" << std::endl;
    event_free(ev);
    event_base_free(base);
    return 1;
}

在上述代码中,我们首先定义了一个回调函数 read_callback,当套接字可读事件发生时,这个函数会被调用并输出相应信息。然后通过 event_new() 函数创建一个事件对象,该事件监听 fd 套接字的可读事件,并关联到之前创建的事件基 base 上。EV_READ 表示监听读事件,EV_PERSIST 表示事件触发后不会自动删除,会继续监听。接着通过 event_add() 函数将事件添加到事件基中进行监听。如果创建或添加事件失败,会输出错误信息并释放相关资源。

3.3 事件驱动循环(Event Loop)

事件驱动循环是 libevent 库的运行核心,它在事件基中不断地循环检查是否有事件发生。当有事件发生时,事件驱动循环会调用相应事件的回调函数进行处理。这个循环会一直运行,直到通过调用 event_base_loopbreak()event_base_loopexit() 函数来终止。

在代码中,我们通过 event_base_dispatch() 函数来启动事件驱动循环:

// 启动事件驱动循环
int result = event_base_dispatch(base);
if (result != 0) {
    std::cerr << "Event base dispatch failed" << std::endl;
}
event_base_free(base);

在上述代码中,调用 event_base_dispatch() 函数启动事件驱动循环,该函数会阻塞当前线程,直到事件驱动循环被终止(例如通过调用 event_base_loopbreak()event_base_loopexit())。如果 event_base_dispatch() 函数返回非零值,表示循环过程中发生了错误,输出错误信息并释放事件基资源。

4. 构建简单的 libevent 高并发服务器

现在我们基于 libevent 库来构建一个简单的高并发服务器,这个服务器能够接收多个客户端的连接,并处理客户端发送的数据。

4.1 创建服务器套接字

首先,我们需要创建一个服务器套接字,用于监听客户端的连接请求。在 Unix 网络编程中,我们通常使用 socket() 函数来创建套接字,bind() 函数来绑定地址和端口,listen() 函数来开始监听。

#include <iostream>
#include <event2/event.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>

// 创建服务器套接字
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == -1) {
    std::cerr << "Failed to create socket" << std::endl;
    return 1;
}

struct sockaddr_in server_addr;
std::memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = INADDR_ANY;

if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
    std::cerr << "Failed to bind socket" << std::endl;
    close(server_socket);
    return 1;
}

if (listen(server_socket, 10) == -1) {
    std::cerr << "Failed to listen on socket" << std::endl;
    close(server_socket);
    return 1;
}

在上述代码中,我们首先使用 socket() 函数创建一个 TCP 套接字(AF_INET 表示 IPv4 地址族,SOCK_STREAM 表示 TCP 协议)。然后设置服务器地址结构体 server_addr,将端口号设置为 8080,并绑定到本地任意地址(INADDR_ANY)。接着使用 bind() 函数将套接字绑定到指定的地址和端口,使用 listen() 函数开始监听客户端连接,最大连接数设置为 10。如果在创建、绑定或监听过程中出现错误,输出错误信息并关闭套接字,程序退出。

4.2 处理客户端连接事件

接下来,我们需要监听服务器套接字上的连接事件。当有客户端连接到来时,接受连接并为每个客户端创建一个新的事件来处理后续的数据读写。

// 连接事件回调函数
void accept_callback(evutil_socket_t listen_fd, short events, void* arg) {
    struct event_base* base = (struct event_base*)arg;
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    int client_socket = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
    if (client_socket == -1) {
        std::cerr << "Failed to accept client connection" << std::endl;
        return;
    }

    std::cout << "Accepted client connection, socket: " << client_socket << std::endl;

    // 为客户端套接字创建读事件
    struct event* client_ev = event_new(base, client_socket, EV_READ | EV_PERSIST, read_callback, nullptr);
    if (!client_ev) {
        std::cerr << "Failed to create client event" << std::endl;
        close(client_socket);
        return;
    }

    if (event_add(client_ev, nullptr) == -1) {
        std::cerr << "Failed to add client event" << std::endl;
        event_free(client_ev);
        close(client_socket);
        return;
    }
}

// 创建并添加连接事件
struct event* listen_ev = event_new(base, server_socket, EV_READ | EV_PERSIST, accept_callback, base);
if (!listen_ev) {
    std::cerr << "Failed to create listen event" << std::endl;
    close(server_socket);
    event_base_free(base);
    return 1;
}

if (event_add(listen_ev, nullptr) == -1) {
    std::cerr << "Failed to add listen event" << std::endl;
    event_free(listen_ev);
    close(server_socket);
    event_base_free(base);
    return 1;
}

在上述代码中,我们定义了一个 accept_callback 回调函数,当服务器套接字有可读事件(即有客户端连接到来)时,这个函数会被调用。在函数中,使用 accept() 函数接受客户端连接,得到客户端套接字 client_socket。然后为这个客户端套接字创建一个读事件,关联到事件基 base 上,并设置回调函数为 read_callback(这个函数稍后定义)。接着将客户端读事件添加到事件基中进行监听。如果在接受连接、创建或添加客户端事件过程中出现错误,输出错误信息并关闭相关套接字和释放事件资源。

最后,我们为服务器套接字创建一个连接事件,关联到事件基 base 上,并设置回调函数为 accept_callback,将这个连接事件添加到事件基中监听服务器套接字的可读事件(即客户端连接事件)。如果创建或添加连接事件失败,输出错误信息并关闭服务器套接字,释放事件基资源,程序退出。

4.3 处理客户端数据读写事件

现在我们需要定义 read_callback 函数来处理客户端发送的数据,并可以选择回显数据给客户端。

// 数据读回调函数
void read_callback(evutil_socket_t client_fd, short events, void* arg) {
    char buffer[1024];
    ssize_t bytes_read = recv(client_fd, buffer, sizeof(buffer), 0);
    if (bytes_read == -1) {
        std::cerr << "Failed to read from client" << std::endl;
        event_del((struct event*)arg);
        close(client_fd);
        return;
    } else if (bytes_read == 0) {
        std::cout << "Client disconnected, socket: " << client_fd << std::endl;
        event_del((struct event*)arg);
        close(client_fd);
        return;
    }

    buffer[bytes_read] = '\0';
    std::cout << "Received from client: " << buffer << std::endl;

    // 回显数据给客户端
    if (send(client_fd, buffer, bytes_read, 0) != bytes_read) {
        std::cerr << "Failed to send data to client" << std::endl;
    }
}

在上述代码中,read_callback 函数在客户端套接字有可读事件发生时被调用。函数中使用 recv() 函数从客户端套接字接收数据到缓冲区 buffer 中。如果接收数据失败(bytes_read 为 -1),输出错误信息,删除该客户端套接字的事件(通过 event_del() 函数),关闭客户端套接字并返回。如果 bytes_read 为 0,表示客户端断开连接,输出相应信息,同样删除事件并关闭套接字。如果成功接收到数据,在缓冲区末尾添加字符串结束符 '\0',输出接收到的数据,并使用 send() 函数将接收到的数据回显给客户端。如果回显数据失败,输出错误信息。

4.4 完整代码示例

下面是一个完整的使用 libevent 库实现简单高并发服务器的代码示例:

#include <iostream>
#include <event2/event.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>

// 数据读回调函数
void read_callback(evutil_socket_t client_fd, short events, void* arg) {
    char buffer[1024];
    ssize_t bytes_read = recv(client_fd, buffer, sizeof(buffer), 0);
    if (bytes_read == -1) {
        std::cerr << "Failed to read from client" << std::endl;
        event_del((struct event*)arg);
        close(client_fd);
        return;
    } else if (bytes_read == 0) {
        std::cout << "Client disconnected, socket: " << client_fd << std::endl;
        event_del((struct event*)arg);
        close(client_fd);
        return;
    }

    buffer[bytes_read] = '\0';
    std::cout << "Received from client: " << buffer << std::endl;

    // 回显数据给客户端
    if (send(client_fd, buffer, bytes_read, 0) != bytes_read) {
        std::cerr << "Failed to send data to client" << std::endl;
    }
}

// 连接事件回调函数
void accept_callback(evutil_socket_t listen_fd, short events, void* arg) {
    struct event_base* base = (struct event_base*)arg;
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    int client_socket = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
    if (client_socket == -1) {
        std::cerr << "Failed to accept client connection" << std::endl;
        return;
    }

    std::cout << "Accepted client connection, socket: " << client_socket << std::endl;

    // 为客户端套接字创建读事件
    struct event* client_ev = event_new(base, client_socket, EV_READ | EV_PERSIST, read_callback, nullptr);
    if (!client_ev) {
        std::cerr << "Failed to create client event" << std::endl;
        close(client_socket);
        return;
    }

    if (event_add(client_ev, nullptr) == -1) {
        std::cerr << "Failed to add client event" << std::endl;
        event_free(client_ev);
        close(client_socket);
        return;
    }
}

int main() {
    // 创建事件基
    struct event_base* base = event_base_new();
    if (!base) {
        std::cerr << "Failed to create event base" << std::endl;
        return 1;
    }

    // 创建服务器套接字
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        std::cerr << "Failed to create socket" << std::endl;
        event_base_free(base);
        return 1;
    }

    struct sockaddr_in server_addr;
    std::memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        std::cerr << "Failed to bind socket" << std::endl;
        close(server_socket);
        event_base_free(base);
        return 1;
    }

    if (listen(server_socket, 10) == -1) {
        std::cerr << "Failed to listen on socket" << std::endl;
        close(server_socket);
        event_base_free(base);
        return 1;
    }

    // 创建并添加连接事件
    struct event* listen_ev = event_new(base, server_socket, EV_READ | EV_PERSIST, accept_callback, base);
    if (!listen_ev) {
        std::cerr << "Failed to create listen event" << std::endl;
        close(server_socket);
        event_base_free(base);
        return 1;
    }

    if (event_add(listen_ev, nullptr) == -1) {
        std::cerr << "Failed to add listen event" << std::endl;
        event_free(listen_ev);
        close(server_socket);
        event_base_free(base);
        return 1;
    }

    // 启动事件驱动循环
    int result = event_base_dispatch(base);
    if (result != 0) {
        std::cerr << "Event base dispatch failed" << std::endl;
    }

    event_free(listen_ev);
    close(server_socket);
    event_base_free(base);

    return 0;
}

上述代码完整地展示了如何使用 libevent 库创建一个简单的高并发服务器,它能够接受多个客户端连接,并处理客户端发送的数据,同时将数据回显给客户端。

5. 优化与扩展

虽然上述简单的服务器示例已经能够实现基本的高并发功能,但在实际应用中,还需要进行一些优化和扩展。

5.1 内存管理优化

在处理大量客户端连接时,合理的内存管理至关重要。例如,在上述代码中,每次接收和发送数据都使用了固定大小的缓冲区 buffer。在实际场景中,可以考虑使用动态内存分配来根据实际数据大小调整缓冲区,避免内存浪费或缓冲区溢出。

// 数据读回调函数优化
void read_callback(evutil_socket_t client_fd, short events, void* arg) {
    // 动态分配缓冲区
    char* buffer = new char[1024];
    ssize_t bytes_read = recv(client_fd, buffer, 1024, 0);
    if (bytes_read == -1) {
        std::cerr << "Failed to read from client" << std::endl;
        event_del((struct event*)arg);
        close(client_fd);
        delete[] buffer;
        return;
    } else if (bytes_read == 0) {
        std::cout << "Client disconnected, socket: " << client_fd << std::endl;
        event_del((struct event*)arg);
        close(client_fd);
        delete[] buffer;
        return;
    }

    buffer[bytes_read] = '\0';
    std::cout << "Received from client: " << buffer << std::endl;

    // 回显数据给客户端
    if (send(client_fd, buffer, bytes_read, 0) != bytes_read) {
        std::cerr << "Failed to send data to client" << std::endl;
    }

    delete[] buffer;
}

在上述优化后的代码中,使用 new 动态分配内存创建缓冲区 buffer,在使用完后通过 delete[] 释放内存,以更好地管理内存。

5.2 错误处理增强

在实际运行中,服务器可能会遇到各种错误情况,如系统资源不足、网络故障等。增强错误处理机制可以提高服务器的稳定性和可靠性。例如,在 accept() 函数调用时,除了检查返回值为 -1 的情况,还可以根据 errno 的具体值进行更详细的错误处理。

// 连接事件回调函数错误处理增强
void accept_callback(evutil_socket_t listen_fd, short events, void* arg) {
    struct event_base* base = (struct event_base*)arg;
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    int client_socket = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
    if (client_socket == -1) {
        int err = errno;
        if (err == EAGAIN || err == EWOULDBLOCK) {
            // 暂时没有新连接,不做处理,等待下一次事件触发
        } else {
            std::cerr << "Failed to accept client connection, errno: " << err << std::endl;
        }
        return;
    }

    std::cout << "Accepted client connection, socket: " << client_socket << std::endl;

    // 为客户端套接字创建读事件
    struct event* client_ev = event_new(base, client_socket, EV_READ | EV_PERSIST, read_callback, nullptr);
    if (!client_ev) {
        std::cerr << "Failed to create client event" << std::endl;
        close(client_socket);
        return;
    }

    if (event_add(client_ev, nullptr) == -1) {
        std::cerr << "Failed to add client event" << std::endl;
        event_free(client_ev);
        close(client_socket);
        return;
    }
}

在上述代码中,当 accept() 函数返回 -1 时,获取 errno 的值。如果 errnoEAGAINEWOULDBLOCK,表示暂时没有新连接,这是一种正常的情况,不做额外处理,等待下一次事件触发。对于其他错误值,输出详细的错误信息。

5.3 功能扩展

除了基本的连接处理和数据读写,还可以根据实际需求对服务器进行功能扩展。例如,添加身份验证功能,在客户端连接后要求客户端发送认证信息,验证通过后才进行后续的数据交互;或者添加日志记录功能,记录服务器的运行状态、客户端连接和数据传输等信息,方便调试和监控。

以添加日志记录功能为例,我们可以使用 glog 库(Google Logging Library)。首先安装 glog 库(在 Ubuntu 系统中可以通过 sudo apt install libgoogle-glog-dev 安装)。

#include <glog/logging.h>
// 数据读回调函数添加日志记录
void read_callback(evutil_socket_t client_fd, short events, void* arg) {
    char buffer[1024];
    ssize_t bytes_read = recv(client_fd, buffer, sizeof(buffer), 0);
    if (bytes_read == -1) {
        LOG(ERROR) << "Failed to read from client, socket: " << client_fd;
        event_del((struct event*)arg);
        close(client_fd);
        return;
    } else if (bytes_read == 0) {
        LOG(INFO) << "Client disconnected, socket: " << client_fd;
        event_del((struct event*)arg);
        close(client_fd);
        return;
    }

    buffer[bytes_read] = '\0';
    LOG(INFO) << "Received from client: " << buffer << ", socket: " << client_fd;

    // 回显数据给客户端
    if (send(client_fd, buffer, bytes_read, 0) != bytes_read) {
        LOG(ERROR) << "Failed to send data to client, socket: " << client_fd;
    }
}

在上述代码中,使用 glog 库的 LOG(ERROR)LOG(INFO) 宏来记录错误和普通信息。这样在服务器运行过程中,可以通过查看日志文件了解服务器的运行情况。

6. 性能测试与分析

为了评估使用 libevent 库实现的高并发服务器的性能,我们需要进行性能测试,并对测试结果进行分析。

6.1 性能测试工具选择

常用的性能测试工具如 ab(Apache Benchmark)、wrk 等都可以用于测试服务器的性能。这里以 wrk 为例,它是一个高性能的 HTTP 基准测试工具,也可以用于测试 TCP 服务器。在 Ubuntu 系统中,可以通过编译安装 wrk

git clone https://github.com/wg/wrk.git
cd wrk
make
sudo cp wrk /usr/local/bin/

安装完成后,就可以使用 wrk 来测试我们的服务器性能。

6.2 测试场景设置

假设我们的服务器运行在本地,监听端口为 8080。我们设置不同的并发连接数和请求总数来测试服务器的性能。例如,使用以下命令测试 100 个并发连接,总共发送 10000 个请求:

wrk -c 100 -t 4 -d 10s --latency http://127.0.0.1:8080

上述命令中,-c 100 表示并发连接数为 100,-t 4 表示使用 4 个线程进行测试,-d 10s 表示测试持续时间为 10 秒,--latency 表示输出延迟信息。

6.3 性能分析

通过 wrk 的测试结果,我们可以得到服务器的吞吐量(Requests per second)、平均延迟(Average latency)等关键性能指标。例如,如果在测试中发现吞吐量较低,可能是因为服务器在处理数据时存在性能瓶颈,如内存读写速度慢、算法复杂度高;如果平均延迟较高,可能是因为事件处理过程中存在阻塞操作,或者网络带宽不足等原因。

根据分析结果,我们可以进一步优化服务器代码,如调整事件处理逻辑、优化数据结构、提升硬件性能等,以提高服务器的整体性能。

7. 实际应用场景

使用 libevent 库实现的高并发服务器在很多实际应用场景中都有广泛的应用。

7.1 即时通讯(IM)服务器

在即时通讯系统中,服务器需要同时处理大量用户的连接,实时接收和发送消息。使用 libevent 库可以高效地管理这些连接,确保消息的及时传递。例如,像微信、QQ 这样的大型即时通讯平台,其服务器端部分就可以采用类似的高并发架构来处理海量用户的消息交互。

7.2 游戏服务器

游戏服务器需要处理大量玩家的实时连接,处理玩家的操作指令、同步游戏状态等。高并发处理能力对于游戏服务器至关重要,以保证游戏的流畅运行和玩家的良好体验。例如,大型多人在线游戏(MMO)的服务器,需要在瞬间处理成千上万玩家的登录、移动、技能释放等操作,libevent 库提供的高效事件驱动机制可以很好地满足这种需求。

7.3 物联网(IoT)后端服务器

随着物联网设备的大量普及,物联网后端服务器需要同时与众多设备进行通信,接收设备上传的数据,并下发控制指令。这些设备数量庞大且连接频繁,对服务器的高并发处理能力要求极高。使用 libevent 库实现的高并发服务器可以有效地管理这些设备连接,确保数据的可靠传输和及时处理。

通过以上详细的介绍、代码示例、优化扩展、性能测试及实际应用场景分析,相信读者对使用 libevent 库实现高并发 C++ 服务器有了全面深入的理解,可以在实际项目中灵活运用这一技术来构建高性能的后端服务器。