当前位置: 首页 > news >正文

c/c++的Libevent 和OpenSSL构建HTTPS客户端详解(附带源码)

使用 Libevent 和 OpenSSL 构建 HTTPS 客户端详解

在现代网络应用中,HTTPS 协议的普及使得安全通信成为标配。Libevent 是一个功能强大且广泛应用的事件通知库,能够帮助开发者编写高性能、可移植的网络程序。然而,libevent 本身并不直接处理 SSL/TLS 加密,这需要借助 OpenSSL 这样的库来完成。本文将详细介绍如何结合 libevent 和 OpenSSL 构建一个异步的 HTTPS 客户端,实现对 HTTPS 网站的访问,并打印服务器的响应内容。

字数统计: 约 4000+字
预计阅读时间: 25-35 分钟

1. 核心概念与准备工作

1.1 Libevent 简介

Libevent 是一个轻量级的开源高性能事件通知库,它提供了一组API,用于在发生特定事件(如套接字可读/可写、超时、信号等)时执行回调函数。其核心优势在于跨平台性和对多种I/O多路复用技术的封装(如epoll, kqueue, select, poll),使得开发者无需关心底层细节。

关键组件:

  • event_base: 事件循环的上下文,管理所有事件。
  • event: 代表一个具体的事件。
  • bufferevent: 封装了带缓冲的I/O操作,非常适合TCP流式数据。它支持普通套接字、SSL套接字以及过滤类型。

1.2 OpenSSL 简介

OpenSSL 是一个强大的、开源的密码学工具包,提供了丰富的加密算法、密钥和证书管理功能,以及SSL/TLS协议的实现。在我们的HTTPS客户端中,OpenSSL将负责处理TLS握手、数据加解密等任务。

1.3 HTTPS 工作流程回顾

一个简化的HTTPS GET请求流程如下:

  1. DNS解析: 客户端解析目标服务器的域名,获取IP地址。
  2. TCP连接: 客户端与服务器在特定端口(默认为443)建立TCP连接。
  3. TLS握手:
    • 客户端发送 ClientHello,包含支持的TLS版本、加密套件等。
    • 服务器回应 ServerHello,确定TLS版本和加密套件,并发送其数字证书。
    • 客户端验证服务器证书的有效性(颁发机构、有效期、域名匹配等)。
    • (可选)服务器可能请求客户端证书。
    • 客户端生成预主密钥(Pre-Master Secret),用服务器证书中的公钥加密后发送给服务器。
    • 双方各自使用预主密钥、客户端随机数、服务器随机数生成主密钥(Master Secret),进而生成会话密钥(对称密钥)。
    • 客户端发送 ChangeCipherSpecFinished(加密的握手摘要)。
    • 服务器发送 ChangeCipherSpecFinished
  4. 安全通信: TLS握手完成,双方使用协商好的会话密钥对应用数据(HTTP请求/响应)进行加密传输。
  5. HTTP请求与响应: 客户端发送加密的HTTP请求,服务器返回加密的HTTP响应。
  6. 关闭连接: 通信结束,TLS连接关闭,TCP连接关闭。

1.4 准备工作

确保你的系统安装了 libevent 和 OpenSSL 的开发库。

  • Debian/Ubuntu:
    sudo apt-get update
    sudo apt-get install libevent-dev libssl-dev
    
  • CentOS/RHEL/Fedora:
    sudo yum install libevent-devel openssl-devel # CentOS/RHEL
    sudo dnf install libevent-devel openssl-devel # Fedora
    
  • macOS (using Homebrew):
    brew install libevent openssl
    
    (macOS 自带的 libressl 可能与某些 OpenSSL 特定 API 不完全兼容,使用 brew 安装的 OpenSSL 可能需要指定头文件和库路径进行编译)

2. HTTPS 客户端实现步骤

我们将逐步构建一个能够执行以下操作的客户端:

  1. 初始化 OpenSSL 和 libevent。
  2. 创建 SSL_CTX (SSL 上下文对象)。
  3. 创建 event_base (事件循环) 和 evdns_base (DNS 解析器)。
  4. 使用 bufferevent_openssl_socket_new 创建一个 SSL bufferevent。
  5. 配置 bufferevent 的回调函数(读、写、事件)。
  6. 发起连接到目标 HTTPS 服务器。
  7. 连接成功后,发送 HTTP GET 请求。
  8. 接收并打印服务器响应。
  9. 清理资源。

2.1 包含头文件与全局定义

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>#include <event2/event.h>
#include <event2/bufferevent.h>
#include <event2/bufferevent_ssl.h>
#include <event2/dns.h>
#include <event2/buffer.h>
#include <event2/util.h>#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/rand.h>// 用于存储回调函数所需数据的结构体
typedef struct {SSL_CTX *ssl_ctx;struct event_base *base;struct evdns_base *dns_base;const char *hostname;unsigned short port;int request_sent; // 标志是否已发送请求
} client_context_t;// 目标服务器和请求信息
#define TARGET_HOSTNAME "[www.example.com](https://www.example.com)" // 替换为你要访问的域名
#define TARGET_PORT 443
#define HTTP_REQUEST_FORMAT "GET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n"

这里我们定义了一个 client_context_t 结构体,方便在回调函数之间传递共享数据。

2.2 OpenSSL 初始化与 SSL_CTX 创建

在使用 OpenSSL 的任何功能之前,需要对其进行初始化。同时,我们需要创建一个 SSL_CTX 对象,它是 SSL/TLS 连接的配置工厂。

SSL_CTX *create_ssl_context() {SSL_CTX *ctx;// 初始化 OpenSSL 库// SSL_library_init(); // 在 OpenSSL 1.1.0 及更高版本中已弃用,会自动初始化// SSL_load_error_strings(); // 同上// OpenSSL_add_all_algorithms(); // 同上// RAND_poll(); // 确保随机数生成器已播种,对于某些旧版本是必要的// 使用 TLS 方法 (通用,推荐)// const SSL_METHOD *method = TLS_client_method(); // OpenSSL 1.1.0+// 对于旧版本 OpenSSL (如1.0.x)// SSL_METHOD *method = SSLv23_client_method();const SSL_METHOD *method = TLS_client_method();if (!method) {fprintf(stderr, "Could not create SSL/TLS method: %s\n", ERR_error_string(ERR_get_error(), NULL));return NULL;}ctx = SSL_CTX_new(method);if (!ctx) {fprintf(stderr, "Could not create SSL_CTX: %s\n", ERR_error_string(ERR_get_error(), NULL));return NULL;}// 配置 SSL_CTX (重要!)// 禁用不安全的 SSLv2 和 SSLv3 协议SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);// 设置证书验证 (生产环境中至关重要)// 对于本示例,为简化,我们先跳过严格验证或使用系统默认CA证书// 要正确验证服务器,你需要加载CA证书:// if (!SSL_CTX_load_verify_locations(ctx, "/path/to/ca-bundle.crt", NULL)) { // 或使用 SSL_CTX_set_default_verify_paths(ctx)//    fprintf(stderr, "Failed to load CA certificates: %s\n", ERR_error_string(ERR_get_error(), NULL));//    SSL_CTX_free(ctx);//    return NULL;// }// SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); // 设置验证回调(这里用NULL表示使用默认)// 对于此示例,我们可以暂时设置为不验证,或尝试加载默认CA路径// 警告:SSL_VERIFY_NONE 在生产中是不安全的!// SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL); if (!SSL_CTX_set_default_verify_paths(ctx)) {fprintf(stderr, "Failed to set default CA verify paths: %s\n", ERR_error_string(ERR_get_error(), NULL));// 可以选择在这种情况下失败,或者继续进行,但连接可能因证书验证失败而失败}SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); // 启用服务器证书验证// 更多选项,例如设置密码套件:// SSL_CTX_set_cipher_list(ctx, "HIGH:!aNULL:!MD5");return ctx;
}

重要提示:

  • OpenSSL 版本差异: OpenSSL 1.1.0 及更高版本简化了初始化过程,许多旧的初始化函数(如 SSL_library_init)已不再需要显式调用。代码中注释了这些。
  • 证书验证: 上述代码中,SSL_CTX_set_default_verify_paths(ctx) 尝试加载系统默认的CA证书路径。这在许多系统上可以工作。如果失败,或你想使用特定的CA证书包,应使用 SSL_CTX_load_verify_locations()在生产环境中,SSL_VERIFY_PEER 必须启用,并且必须正确配置CA证书,否则HTTPS没有安全性可言。 如果 SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL); 被使用,它会禁用服务器证书验证,这是非常不安全的,仅用于非常受限的测试。

2.3 Libevent 回调函数

我们需要为 bufferevent 定义几个回调函数:

  • read_cb: 当接收到数据时调用。
  • event_cb: 当连接状态发生变化或发生错误时调用。
// 读取回调函数:当 bufferevent 的输入缓冲区中有数据时调用
void read_cb(struct bufferevent *bev, void *arg) {client_context_t *ctx = (client_context_t *)arg;struct evbuffer *input = bufferevent_get_input(bev);size_t len = evbuffer_get_length(input);char *data;if (len > 0) {data = malloc(len + 1);if (!data) {perror("malloc failed");// 严重错误,可能需要关闭连接或停止事件循环bufferevent_free(bev);event_base_loopexit(ctx->base, NULL);return;}evbuffer_remove(input, data, len);data[len] = '\0';printf("---------- Server Response ----------\n%s\n", data);printf("---------- End of Response Chunk (length: %zu) ----------\n", len);free(data);}
}// 事件回调函数:当 bufferevent 上发生特定事件(如连接成功、EOF、错误)时调用
void event_cb(struct bufferevent *bev, short events, void *arg) {client_context_t *client_ctx = (client_context_t *)arg;char request_buffer[512];if (events & BEV_EVENT_CONNECTED) {printf("Connected to %s:%d\n", client_ctx->hostname, client_ctx->port);// SSL 连接已建立,可以获取 SSL 对象并进行检查(可选)SSL *ssl = bufferevent_openssl_get_ssl(bev);if (ssl) {printf("SSL connection established using %s\n", SSL_get_cipher_name(ssl));// 验证服务器证书的结果(如果启用了验证)long verify_result = SSL_get_verify_result(ssl);if (verify_result != X509_V_OK) {fprintf(stderr, "Server certificate verification failed: %s\n",X509_verify_cert_error_string(verify_result));// 可以选择在此处关闭连接// bufferevent_free(bev);// event_base_loopexit(client_ctx->base, NULL);// return;} else {printf("Server certificate verified successfully.\n");}} else {fprintf(stderr, "Could not get SSL object from bufferevent.\n");// 可能是非SSL bufferevent,或配置错误}// 发送 HTTP GET 请求if (!client_ctx->request_sent) {snprintf(request_buffer, sizeof(request_buffer), HTTP_REQUEST_FORMAT, client_ctx->hostname);printf("Sending HTTP Request:\n%s", request_buffer);bufferevent_write(bev, request_buffer, strlen(request_buffer));// bufferevent_flush(bev, EV_WRITE, BEV_FLUSH); // 可选,通常libevent会自动处理client_ctx->request_sent = 1;}return; // 保持连接以接收数据}if (events & BEV_EVENT_EOF) {printf("Connection closed by peer (EOF).\n");} else if (events & BEV_EVENT_ERROR) {fprintf(stderr, "Bufferevent error: ");unsigned long err;while ((err = bufferevent_get_openssl_error(bev)) != 0) {fprintf(stderr, "%s; ", ERR_reason_error_string(err));}// 如果没有 OpenSSL 特定错误,可能是套接字错误if (errno != 0) {fprintf(stderr, "System error: %s (%d)", evutil_socket_error_to_string(EVUTIL_SOCKET_ERROR()), EVUTIL_SOCKET_ERROR());}fprintf(stderr, "\n");} else if (events & BEV_EVENT_TIMEOUT) {printf("Bufferevent timeout.\n");} else {printf("Unhandled bufferevent event: 0x%hx\n", events);}// 发生EOF或错误后,释放资源并退出事件循环bufferevent_free(bev); // 这也会关闭底层套接字和释放SSL对象bev = NULL;// 如果事件循环是因为这个bev的错误而需要终止,则调用loopexit// 注意:如果有多个并发连接,不应轻易退出整个事件循环event_base_loopexit(client_ctx->base, NULL);
}

event_cb 中:

  • BEV_EVENT_CONNECTED: 表示TCP连接和TLS握手均已成功。此时,我们发送HTTP GET请求。这里还添加了获取SSL*对象并检查加密套件和证书验证结果的逻辑。
  • BEV_EVENT_EOF: 对端关闭了连接。
  • BEV_EVENT_ERROR: 发生错误。我们尝试获取 OpenSSL 的特定错误信息,如果获取不到,则可能是底层的套接字错误。
  • request_sent 标志确保请求只发送一次。

打印服务器响应:
read_cb 函数负责从 bufferevent 的输入缓冲区读取数据并打印。这里简单地将接收到的数据块打印到标准输出。对于实际应用,你需要实现一个HTTP响应解析器来处理HTTP头和主体。

2.4 主函数 main()

main 函数将所有部分串联起来:

int main(int argc, char **argv) {struct event_base *base;struct evdns_base *dns_base;struct bufferevent *bev;SSL_CTX *ssl_ctx;client_context_t client_ctx;const char *hostname = TARGET_HOSTNAME;unsigned short port = TARGET_PORT;if (argc > 1) {hostname = argv[1];}if (argc > 2) {port = (unsigned short)atoi(argv[2]);if (port == 0) {fprintf(stderr, "Invalid port number: %s\n", argv[2]);return 1;}}printf("Target: %s:%d\n", hostname, port);// 1. 初始化 OpenSSL 并创建 SSL_CTX// OpenSSL 1.1.0+ 自动初始化,但显式调用兼容旧版,且无害SSL_library_init(); // 可安全调用,即使在1.1.0+也是no-op或别名SSL_load_error_strings();OpenSSL_add_all_algorithms(); // 对于某些应用可能仍然需要ssl_ctx = create_ssl_context();if (!ssl_ctx) {fprintf(stderr, "Failed to create SSL_CTX.\n");return 1;}// 2. 创建 event_base 和 evdns_basebase = event_base_new();if (!base) {fprintf(stderr, "Could not initialize libevent!\n");SSL_CTX_free(ssl_ctx);return 1;}dns_base = evdns_base_new(base, 1); // 1表示使用系统默认的DNS配置if (!dns_base) {fprintf(stderr, "Could not create evdns_base!\n");event_base_free(base);SSL_CTX_free(ssl_ctx);return 1;}// 填充 client_contextclient_ctx.ssl_ctx = ssl_ctx;client_ctx.base = base;client_ctx.dns_base = dns_base;client_ctx.hostname = hostname;client_ctx.port = port;client_ctx.request_sent = 0;// 3. 创建 SSL bufferevent// 参数:event_base, 底层bufferevent(通常为NULL让libevent创建), SSL对象, SSL状态, 选项// SSL对象可以为NULL,让bufferevent从SSL_CTX创建。// BUFFEREVENT_SSL_OPENING 表示 bufferevent 处于客户端模式,将发起SSL握手。SSL *ssl = SSL_new(ssl_ctx); // 从CTX创建一个SSL对象实例if (!ssl) {fprintf(stderr, "Failed to create SSL object from SSL_CTX.\n");evdns_base_free(dns_base, 0); // 0表示不等待未完成的请求event_base_free(base);SSL_CTX_free(ssl_ctx);return 1;}// 重要:为 SNI (Server Name Indication) 设置主机名// 许多现代服务器(特别是共享IP的)依赖SNI来提供正确的证书if (!SSL_set_tlsext_host_name(ssl, hostname)) {fprintf(stderr, "Failed to set SNI hostname: %s\n", ERR_error_string(ERR_get_error(), NULL));// 这通常不是致命错误,但可能导致握手失败或收到错误的证书}bev = bufferevent_openssl_socket_new(base,-1, // -1 表示让 libevent 创建新的套接字ssl, // 传入创建的 SSL 对象BUFFEREVENT_SSL_OPENING, // 状态:客户端,将启动握手BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS);if (!bev) {fprintf(stderr, "Could not create SSL bufferevent!\n");// SSL_free(ssl); // bufferevent_openssl_socket_new 失败时是否需要手动释放 ssl 取决于具体实现和失败点,// 但通常如果 bev 创建失败,传入的 ssl 仍需调用者管理。// 然而,如果 bev 创建成功,它会接管 ssl 对象的生命周期。// 在此场景,由于 bev 未成功创建,ssl 需手动释放。SSL_free(ssl);evdns_base_free(dns_base, 0);event_base_free(base);SSL_CTX_free(ssl_ctx);return 1;}// bufferevent 创建成功,它现在拥有 SSL 对象。// 不需要再手动 SSL_free(ssl);当 bev 被 free 时它会被处理。// 允许在"脏"关闭后进行SSL清理,某些服务器可能不发送SSL close_notifybufferevent_openssl_set_allow_dirty_shutdown(bev, 1);// 4. 设置回调函数bufferevent_setcb(bev, read_cb, NULL, event_cb, &client_ctx); // 写回调这里设为NULL,因为我们是一次性写入请求// 5. 发起连接 (异步)// 使用 bufferevent_socket_connect_hostname 进行DNS解析和连接// 注意:它需要 evdns_baseif (bufferevent_socket_connect_hostname(bev, dns_base, AF_UNSPEC, hostname, port) < 0) {fprintf(stderr, "Could not connect to %s:%d\n", hostname, port);bufferevent_free(bev); // 这会释放内部的 SSL 对象evdns_base_free(dns_base, 0);event_base_free(base);SSL_CTX_free(ssl_ctx);return 1;}printf("Connection initiated to %s:%d...\n", hostname, port);// 启用读写事件(通常在连接后自动启用,但显式调用无害)bufferevent_enable(bev, EV_READ | EV_WRITE);// 6. 启动事件循环printf("Starting event loop...\n");event_base_dispatch(base);printf("Event loop finished.\n");// 7. 清理 (bufferevent 已在回调中或连接失败时释放)// bev 应该在 event_cb 中被释放,或者如果连接从未成功则在上面释放evdns_base_free(dns_base, 0); // 0: don't wait for outstanding requestsevent_base_free(base);SSL_CTX_free(ssl_ctx);// OpenSSL 全局清理 (可选,某些版本的OpenSSL可能需要)// EVP_cleanup();// CRYPTO_cleanup_all_ex_data();// ERR_free_strings();// 在 OpenSSL 1.1.0+ 中,这些通常由库自动处理,或通过atexit处理程序printf("Exiting.\n");return 0;
}

关键点解释:

  • SSL_library_init(), SSL_load_error_strings(), OpenSSL_add_all_algorithms(): 这些是传统的 OpenSSL 初始化函数。在 OpenSSL 1.1.0+ 中,它们大部分是空操作或别名,因为库会自动进行初始化。显式调用它们通常是安全的,并能保持对旧版本 OpenSSL 的兼容性。
  • bufferevent_openssl_socket_new(): 这是创建 SSL bufferevent 的核心函数。
    • 第一个参数是 event_base
    • 第二个参数是底层套接字的文件描述符。传递 -1 表示让 libevent 自动创建一个新的套接字。
    • 第三个参数是 SSL * 对象。我们先通过 SSL_new(ssl_ctx) 创建一个 SSL 实例。重要的是,通过 SSL_set_tlsext_host_name(ssl, hostname) 来设置 SNI(服务器名称指示),这对于访问托管在共享IP上的多个HTTPS站点至关重要。
    • 第四个参数 state 设置为 BUFFEREVENT_SSL_OPENING,表示这是一个客户端 bufferevent,它将在连接后主动发起TLS握手。
    • 第五个参数 options 可以包含 BEV_OPT_CLOSE_ON_FREE(当 bufferevent 被释放时关闭底层套接字和SSL连接)和 BEV_OPT_DEFER_CALLBACKS(延迟回调执行,避免递归)。
  • bufferevent_socket_connect_hostname(): 这个函数非常方便,它会使用提供的 evdns_base 进行异步DNS解析,然后尝试连接到解析出的IP地址和指定端口。
  • bufferevent_enable(bev, EV_READ | EV_WRITE): 确保 bufferevent 监听读写事件。连接成功后,通常会自动启用,但显式调用是好的实践。
  • 资源释放: SSL_CTXevent_baseevdns_base 都需要在不再使用时通过对应的 _free 函数释放。bufferevent 通常在 event_cb 中检测到EOF或错误后释放,或者在连接建立失败时释放。SSL 对象在传递给 bufferevent_openssl_socket_new 成功后,其生命周期由 bufferevent 管理,会在 bufferevent 释放时一同释放。

2.5 编译和运行

将以上代码保存为 https_client.c。编译时需要链接 libevent, libevent_openssl, OpenSSL 的 crypto 和 ssl 库,以及 pthreads (libevent可能依赖)。

gcc https_client.c -o https_client \-I/usr/include/openssl \-L/usr/lib \-levent -levent_openssl -lssl -lcrypto \-pthread $(pkg-config --cflags --libs libevent_openssl libevent) # 使用 pkg-config 更佳# 在 macOS 上使用 brew 安装的 openssl 可能需要类似:
# OPENSSL_PREFIX=$(brew --prefix openssl@1.1) # 或 openssl@3
# gcc https_client.c -o https_client \
#    -I${OPENSSL_PREFIX}/include -L${OPENSSL_PREFIX}/lib \
#    -levent -levent_openssl -lssl -lcrypto -pthread

如果 pkg-config 配置正确,以下命令通常更简洁且跨平台性更好:

gcc https_client.c -o https_client $(pkg-config --cflags --libs libevent libevent_openssl openssl) -pthread

如果 libevent_openssl 没有单独的 .pc 文件,通常 libeventopenssl 的就够了,因为 libevent_openssl 库本身会依赖 libeventopenssl

运行程序:

./https_client
# 或者指定域名和端口
./https_client [www.google.com](https://www.google.com) 443

你将在控制台看到连接过程、发送的HTTP请求(如果你打印了它)以及服务器返回的原始HTTPS响应内容(包含HTTP头和HTML/JSON等主体)。

3. 调用流程图 (Mermaid 格式)

下面是客户端操作的简化流程图:

graph TDA[main(): 开始] --> B{初始化 OpenSSL};B --> C[创建 SSL_CTX (ssl_ctx)];C --> D[创建 event_base (base)];D --> E[创建 evdns_base (dns_base)];E --> F[创建 SSL 对象 (ssl) 从 ssl_ctx];F --> G[设置 SNI: SSL_set_tlsext_host_name(ssl, hostname)];G --> H[创建 SSL Bufferevent (bev = bufferevent_openssl_socket_new)];H --> I[设置 Bufferevent 回调: event_cb, read_cb];I --> J[发起异步连接: bufferevent_socket_connect_hostname];J --> K[启动事件循环: event_base_dispatch()];subgraph Asynchronous Events & CallbacksL[event_base 处理事件] -.-> M{连接事件?};M -- BEV_EVENT_CONNECTED --> N[event_cb: 连接成功];N --> O[打印连接信息, 检查SSL];O --> P[发送 HTTP GET 请求: bufferevent_write];M -- BEV_EVENT_ERROR/EOF --> Q[event_cb: 错误或EOF];Q --> R[打印错误/EOF信息];R --> S[释放 bufferevent: bufferevent_free];S --> T[退出事件循环: event_base_loopexit];L -.-> U{读取事件?};U -- 有数据 --> V[read_cb: 接收数据];V --> W[打印服务器响应数据];endK --> X[event_base_dispatch() 返回];X --> Y[清理全局资源: dns_base, base, ssl_ctx];Y --> Z[main(): 结束];J -. 失败 .-> AA[打印连接失败信息];AA --> S;

流程图解释:

  1. main 函数执行初始化步骤(OpenSSL, SSL_CTX, event_base, evdns_base, SSL 对象, SNI, bufferevent)。
  2. 设置好回调后,调用 bufferevent_socket_connect_hostname 发起异步连接,然后启动 event_base_dispatch() 进入事件循环。
  3. 事件循环 event_base 等待并分派事件。
    • 当连接成功建立(TCP连接和TLS握手都完成),event_cb 会收到 BEV_EVENT_CONNECTED 事件。在此回调中,我们发送HTTP GET请求。
    • 当服务器发送数据过来,read_cb 会被触发,读取并打印响应。
    • 如果发生错误或连接被对方关闭(EOF),event_cb 会收到相应的事件,进行错误处理并释放 bufferevent,最终可能导致事件循环退出。
  4. 事件循环结束后(通常是调用了 event_base_loopexit 或没有更多活动事件),main 函数继续执行,清理剩余的全局资源。

4. 深入理解与高级主题

4.1 错误处理

健壮的错误处理至关重要。

  • OpenSSL错误栈: 当OpenSSL函数失败时,可以使用 ERR_get_error() 配合 ERR_error_string()ERR_reason_error_string() 来获取详细错误信息。在event_cb中,bufferevent_get_openssl_error() 专门用于获取与bufferevent相关的OpenSSL错误。
  • Libevent错误: evutil_socket_error_to_string(EVUTIL_SOCKET_ERROR()) 可以将套接字相关的错误码转换为可读字符串。
  • 回调中的错误: 在回调函数中遇到不可恢复的错误时,通常需要释放相关的 bufferevent 并可能决定是否终止整个事件循环。

4.2 HTTP 协议处理

本文示例仅发送了一个硬编码的GET请求并打印原始响应。实际应用中:

  • 请求构建: 需要根据需求动态构建HTTP请求(方法、路径、头部、主体)。
  • 响应解析: 实现一个HTTP响应解析器来分离状态行、头部和主体。处理分块编码(Chunked Transfer Encoding)、内容编码(gzip, deflate)等。
  • 状态码处理: 根据HTTP状态码执行不同逻辑(例如,处理重定向3xx,客户端错误4xx,服务器错误5xx)。

4.3 连接管理与超时

  • 超时: bufferevent_set_timeouts() 可以为读写操作设置超时。超时事件会在 event_cb 中以 BEV_EVENT_TIMEOUT 形式报告。
  • Keep-Alive: HTTP/1.1 默认使用持久连接(Keep-Alive)。如果服务器支持,可以在一次连接中发送多个请求。这需要更复杂的请求/响应管理逻辑。当前示例使用 Connection: close,表示请求完成后服务器应关闭连接。

4.4 证书固定 (Certificate Pinning)

为了增强安全性,防止中间人攻击(即使CA系统被攻破),可以使用证书固定技术,即客户端只信任特定服务器的预设证书或公钥。这可以通过在TLS握手后检查服务器证书链来实现。

4.5 客户端证书认证

某些服务器可能要求客户端提供证书进行身份验证。这需要在 SSL_CTX 中配置客户端证书和私钥 (SSL_CTX_use_certificate_file, SSL_CTX_use_PrivateKey_file)。

4.6 非阻塞DNS

bufferevent_socket_connect_hostname 配合 evdns_base 实现了非阻塞DNS查询。如果直接使用 bufferevent_socket_connect 并传入 sockaddr_in 结构,你需要自己预先完成DNS解析(可能是阻塞的 getaddrinfo 或非阻塞的自定义实现)。

5. 总结

本文详细介绍了如何使用 libevent 和 OpenSSL 从头开始构建一个能够访问 HTTPS 网站的C语言客户端程序。我们涵盖了从环境准备、核心概念理解、代码实现细节到编译运行的整个过程,并提供了一个调用流程图来帮助理解其异步工作模式。

关键点回顾:

  • 正确初始化 OpenSSL 和创建配置 SSL_CTX (特别是证书验证部分)。
  • 使用 bufferevent_openssl_socket_new 创建支持SSL的 bufferevent。
  • bufferevent 设置正确的异步回调函数来处理连接事件、数据读写和错误。
  • 通过 SSL_set_tlsext_host_name() 实现 SNI,确保能访问现代 HTTPS 服务器。
  • 使用 bufferevent_socket_connect_hostname 进行异步DNS解析和连接。
  • BEV_EVENT_CONNECTED 事件回调中发送HTTP请求。
  • 在读回调中处理服务器响应。
  • 细致的错误处理和资源管理是程序稳定性的基石。

虽然示例相对基础,但它为构建更复杂、功能更丰富的 HTTPS 客户端应用(如网络爬虫、API客户端等)打下了坚实的基础。希望本文能为你使用 libevent 进行安全网络编程提供有价值的参考。

相关文章:

  • Python毕业设计219—基于python+Django+vue的房屋租赁系统(源代码+数据库+万字论文)
  • 如何制作网站?制作网站的流程。
  • C++ 观察者模式详解
  • k8s之ingress
  • 电路研究9.3.4——合宙Air780EP中的AT开发指南:HTTPS示例
  • 具身智能数据集解析
  • n8n系列(4):生产环境最佳实践
  • 数据库基础:概念、原理与实战示例
  • 云轴科技ZStack入选赛迪顾问2025AI Infra平台市场发展报告代表厂商
  • 万兴PDF-PDFelement v11.4.13.3417
  • 对遗传算法思想的理解与实例详解
  • odoo-049 Pycharm 中 git stash 后有pyc 文件,如何删除pyc文件
  • python打卡day20
  • LeetCode 热题 100_编辑距离(94_72_中等_C++)(动态规划)
  • 并发设计模式实战系列(19):监视器(Monitor)
  • C#参数数组全解析
  • 人工智能之数学基础:二次型
  • H5 移动端适配最佳实践落地指南。
  • Java如何获取电脑分辨率?
  • 前端学习(1)—— 使用HTML编写一个简单的个人简历展示页面
  • 上海“电子支付费率成本为0”背后:金融服务不仅“快”和“省”,更有“稳”和“准”
  • 游客称在网红雪山勒多曼因峰需救援被开价2.8万,康定文旅:封闭整改
  • 远离军事前线的另一面暗斗:除了“断水”,印度还试图牵制对巴国际援助
  • 外卖员投资失败负疚离家流浪,经民警劝回后泣不成声给父母下跪
  • 商务部:中方愿同各国一道加强合作,促进跨境电商健康可持续发展
  • 加力、攻坚、借力、问效,上海为优化营商环境推出增量举措