C事件驱动网络库libevent的http详解
libevent的http
- 释放 Libevent 异步 HTTP 的威力:深入解析 `event2/http.h`
- 核心服务器设置与管理
- 服务器配置选项
- 服务器请求处理回调
- 虚拟主机和别名
- 服务器响应函数
- 客户端连接设置
- 客户端连接配置
- 发出客户端请求
- 访问请求/响应数据
- 头部操作工具
- URI 解析和编码工具
- 高级 RFC3986 URI 解析
- 结论
由于现有服务器大部分都使用http所以对libevent的http使用需要了解
也是为我下面这个服务器做铺垫
20250428_080643
释放 Libevent 异步 HTTP 的威力:深入解析 event2/http.h
Libevent 是构建高性能异步 I/O 应用的基石库。虽然其核心处理通用事件,但它的 event2/http.h
模块提供了一个健壮且高效的框架,用于构建 HTTP 服务器和客户端。忘掉阻塞调用和复杂的线程模型吧;Libevent 的 HTTP 层让你能够优雅、高速地并发处理大量连接。
但是,浏览一个强大库的 API 有时感觉像是在探索茂密的森林。本指南旨在成为你探索 event2/http.h
的地图和指南针,用解释和实用示例照亮每一个函数。无论你是在构建轻量级 Web 服务器、REST API 后端,还是高效的 HTTP 客户端,理解这些函数都是关键。
让我们开始这次探索之旅吧!
核心服务器设置与管理
这些函数是创建和管理你的 HTTP 服务器实例的基础构建块。
-
struct evhttp *evhttp_new(struct event_base *base);
- 目的: 创建一个新的、空的 HTTP 服务器实例。这通常是设置 HTTP 服务器的第一步。
- 参数:
base
: 一个可选的event_base
,用于与此服务器关联。如果提供,服务器的事件(如接受连接)将由这个事件循环管理。如果为NULL
,可能会使用一个默认的内部event_base
,但通常建议提供你自己的以获得更好的控制。
- 返回值: 指向新创建的
evhttp
结构的指针,失败时返回NULL
。 - 场景联想: 这就像为你的数字场馆打下地基。你需要一个空间(
evhttp
)和一个引擎(event_base
)来驱动其中的事件。 - 示例:
#include <event2/event.h> #include <event2/http.h> #include <stdio.h>int main() {// 创建事件基础(事件循环)struct event_base *base = event_base_new();if (!base) {fprintf(stderr, "无法创建 event_base\n");return 1;}// 创建一个新的 evhttp 实例struct evhttp *http = evhttp_new(base);if (!http) {fprintf(stderr, "无法创建 evhttp\n");event_base_free(base); // 清理 basereturn 1;}printf("evhttp 服务器创建成功!\n");// ... 服务器配置和绑定 ...evhttp_free(http); // 稍后清理 httpevent_base_free(base); // 稍后清理 basereturn 0; }
-
int evhttp_bind_socket(struct evhttp *http, const char *address, ev_uint16_t port);
- 目的: 将 HTTP 服务器绑定到指定的 IP 地址和端口进行监听。这使得服务器准备好接受传入连接。
- 参数:
http
:evhttp
服务器实例。address
: 要绑定的 IP 地址字符串(例如,“0.0.0.0” 表示所有接口,“127.0.0.1” 表示本地主机)。port
: 要监听的端口号。
- 返回值: 成功时返回 0,失败时返回 -1(例如,端口已被使用,地址无效)。
- 场景联想: 你已经建好了场馆;现在是时候在特定的地址(
address
,port
)打开大门(bind_socket
),让客人可以到达。 - 示例(接上文):
// ... 在 evhttp_new 之后 ...// 尝试将服务器绑定到 0.0.0.0 的 8080 端口 if (evhttp_bind_socket(http, "0.0.0.0", 8080) != 0) {fprintf(stderr, "无法将套接字绑定到端口 8080\n");evhttp_free(http);event_base_free(base);return 1; }printf("服务器已绑定到 0.0.0.0:8080\n");// ... 设置回调并启动事件循环 ...
-
struct evhttp_bound_socket *evhttp_bind_socket_with_handle(struct evhttp *http, const char *address, ev_uint16_t port);
- 目的: 类似于
evhttp_bind_socket
,但返回一个代表监听套接字的句柄(evhttp_bound_socket
)。这个句柄可以在之后使用,例如与evhttp_del_accept_socket
配合。 - 返回值: 成功时返回指向
evhttp_bound_socket
的指针,失败时返回NULL
。 - 示例:
// ... 在 evhttp_new 之后 ... struct evhttp_bound_socket *handle = evhttp_bind_socket_with_handle(http, "127.0.0.1", 8081); if (!handle) {fprintf(stderr, "无法通过句柄将套接字绑定到端口 8081\n");// ... 清理 ...return 1; } printf("服务器已绑定(带句柄)到 127.0.0.1:8081\n");// 如果之后需要引用这个特定的监听器,保留 'handle' // evhttp_del_accept_socket(http, handle); // 之后使用的示例
- 目的: 类似于
-
int evhttp_accept_socket(struct evhttp *http, evutil_socket_t fd);
- 目的: 让服务器在一个已经存在的监听套接字文件描述符(
fd
)上接受连接。这在诸如从父进程继承套接字或权限阻止直接绑定端口的场景中很有用。 - 返回值: 成功时返回 0,失败时返回 -1。
- 场景联想: 别人已经设置好了入口(
fd
),你只是接管了它的主持职责。 - 示例(概念性 - 需要先获取
listening_fd
):// evutil_socket_t listening_fd = /* 通过某种方式获取已存在的监听套接字 */; // if (evhttp_accept_socket(http, listening_fd) != 0) { // fprintf(stderr, "无法在已存在的套接字 fd %d 上接受连接\n", listening_fd); // // ... 清理 ... // return 1; // } // printf("服务器正在预先存在的套接字 fd %d 上接受连接\n", listening_fd);
- 目的: 让服务器在一个已经存在的监听套接字文件描述符(
-
struct evhttp_bound_socket *evhttp_accept_socket_with_handle(struct evhttp *http, evutil_socket_t fd);
- 目的: 类似于
evhttp_accept_socket
,但返回所接受套接字的句柄。 - 返回值: 成功时返回句柄,失败时返回
NULL
。 - 示例(概念性):
// evutil_socket_t listening_fd = /* 通过某种方式获取已存在的监听套接字 */; // struct evhttp_bound_socket *handle = evhttp_accept_socket_with_handle(http, listening_fd); // if (!handle) { // fprintf(stderr, "无法通过句柄在已存在的套接字 fd %d 上接受连接\n", listening_fd); // // ... 清理 ... // return 1; // } // printf("服务器正在预先存在的套接字 fd %d 上接受连接(带句柄)\n", listening_fd);
- 目的: 类似于
-
struct evhttp_bound_socket *evhttp_bind_listener(struct evhttp *http, struct evconnlistener *listener);
- 目的: 最底层的绑定/接受方法。接受一个现有的
evconnlistener
(Libevent 的通用连接监听器)并将其与evhttp
服务器集成。如果你需要在将监听器交给evhttp
之前对其选项进行精细控制,这提供了最大的灵活性。evhttp
服务器获得监听器的所有权,并在绑定套接字被移除或服务器被释放时释放它。 - 返回值: 成功时返回句柄,失败时返回
NULL
。 - 示例(概念性 - 假设
listener
已创建和配置):// struct evconnlistener *listener = /* 创建和配置 evconnlistener */; // struct evhttp_bound_socket *handle = evhttp_bind_listener(http, listener); // if (!handle) { // fprintf(stderr, "无法绑定 evconnlistener\n"); // // evconnlistener_free(listener); // 如果绑定成功,则不要手动释放 // // ... 清理 ... // return 1; // } // printf("服务器已使用现有的 evconnlistener 进行绑定\n");
- 目的: 最底层的绑定/接受方法。接受一个现有的
-
struct evconnlistener *evhttp_bound_socket_get_listener(struct evhttp_bound_socket *bound);
- 目的: 检索与绑定套接字句柄关联的底层
evconnlistener
。 - 示例:
// struct evhttp_bound_socket *handle = /* 来自 bind/accept_with_handle 的句柄 */; // struct evconnlistener *listener = evhttp_bound_socket_get_listener(handle); // if (listener) { // // 你可以检查监听器,但不要在这里手动释放它。 // printf("已检索到底层监听器。\n"); // }
- 目的: 检索与绑定套接字句柄关联的底层
-
void evhttp_bound_set_bevcb(struct evhttp_bound_socket *bound, struct bufferevent* (*cb)(struct event_base *, void *), void *cbarg);
- 目的: 为特定监听器设置一个回调函数(
cb
),该回调函数为通过该特定监听器传入的连接创建bufferevent
对象。这会覆盖该bound
套接字上的全局bevcb
(通过evhttp_set_bevcb
设置)。这对于在同一个evhttp
服务器内为不同的监听器应用不同的传输层(如 SSL)非常有用。 - 示例(概念性回调):
#include <event2/bufferevent_ssl.h> // SSL 示例需要 #include <openssl/ssl.h>// 创建 SSL bufferevent 的回调函数 struct bufferevent* create_ssl_bufferevent(struct event_base *base, void *arg) {SSL_CTX *ctx = (SSL_CTX *)arg; // 获取 SSL 上下文SSL *ssl = SSL_new(ctx);// BEV_OPT_CLOSE_ON_FREE 很重要,确保 bufferevent 释放时 SSL 也被释放return bufferevent_openssl_socket_new(base, -1, ssl,BUFFEREVENT_SSL_ACCEPTING, // 服务器端接受状态BEV_OPT_CLOSE_ON_FREE); }// ... 在 main 或设置函数中 ... // SSL_CTX *my_ssl_context = /* 初始化 OpenSSL 上下文 */; // // 绑定一个用于 SSL 的端口,并获取句柄 // struct evhttp_bound_socket *ssl_handle = evhttp_bind_socket_with_handle(http, "0.0.0.0", 8443); // if (ssl_handle) { // // 为这个特定的监听器设置 bufferevent 创建回调 // evhttp_bound_set_bevcb(ssl_handle, create_ssl_bufferevent, my_ssl_context); // printf("已为端口 8443 上的监听器设置 SSL bufferevent 创建回调\n"); // }
- 目的: 为特定监听器设置一个回调函数(
-
void evhttp_foreach_bound_socket(struct evhttp *http, evhttp_bound_socket_foreach_fn *function, void *argument);
- 目的: 遍历当前绑定到
evhttp
服务器的所有监听套接字,并为每个套接字调用提供的function
回调。 - 参数:
http
: 服务器实例。function
: 要执行的回调函数 (void (*fn)(struct evhttp_bound_socket *, void *)
)。argument
: 传递给回调函数的任意指针参数。
- 示例:
#include <event2/util.h> // 需要 evutil_socket_t// 遍历函数:打印每个绑定套接字的信息 void print_socket_info(struct evhttp_bound_socket *bound, void *arg) {evutil_socket_t fd = evhttp_bound_socket_get_fd(bound);const char *prefix = (const char *)arg;printf("%s 发现绑定套接字,fd: %d\n", prefix, (int)fd);// 也可以获取监听器等信息 }// ... 在代码稍后,绑定套接字之后 ... // 调用遍历函数,传入 "[信息]" 作为参数 evhttp_foreach_bound_socket(http, print_socket_info, (void *)"[信息]");
- 目的: 遍历当前绑定到
-
void evhttp_del_accept_socket(struct evhttp *http, struct evhttp_bound_socket *bound_socket);
- 目的: 停止服务器在由
bound_socket
标识的特定监听器上接受新连接。它还会清理与该监听器相关的资源(如果是由evhttp_bind/accept_socket_with_handle
创建的,则关闭 FD;如果是由evhttp_bind_listener
创建的,则释放evconnlistener
)。此调用后,bound_socket
句柄变为无效。 - 示例:
// struct evhttp_bound_socket *handle = /* 来自 evhttp_bind_socket_with_handle 的句柄 */; // printf("停止与句柄 %p 关联的监听器\n", (void*)handle); // evhttp_del_accept_socket(http, handle); // handle = NULL; // 良好的实践是之后将指针置 NULL
- 目的: 停止服务器在由
-
evutil_socket_t evhttp_bound_socket_get_fd(struct evhttp_bound_socket *bound_socket);
- 目的: 检索与绑定套接字句柄关联的原始监听套接字文件描述符。
- 示例: (见上面的
evhttp_foreach_bound_socket
示例)
-
void evhttp_free(struct evhttp* http);
- 目的: 释放与
evhttp
服务器实例相关的所有资源,包括任何内部管理的监听器和连接(如果未显式分离)。重要提示:仅当当前没有请求正在处理时才应调用此函数。它不会释放关联的event_base
。 - 场景联想: 打烊时间!关闭场馆并释放所有资源。
- 示例:
// ... 在服务器生命周期的末尾 ... printf("正在关闭 HTTP 服务器...\n"); evhttp_free(http); // 释放 evhttp 结构 event_base_free(base); // 释放事件循环 printf("服务器已关闭。\n");
- 目的: 释放与
服务器配置选项
这些函数允许你调整 HTTP 服务器的行为和限制。
-
void evhttp_set_max_headers_size(struct evhttp* http, ev_ssize_t max_headers_size);
- 目的: 设置请求头允许的最大总大小(以字节为单位)。有助于防止使用过大头部的拒绝服务攻击。
- 示例:
// 设置最大头部大小为 16 KB evhttp_set_max_headers_size(http, 16 * 1024);
-
void evhttp_set_max_body_size(struct evhttp* http, ev_ssize_t max_body_size);
- 目的: 设置请求体允许的最大大小(以字节为单位)。防止客户端在非预期情况下用巨大的上传压垮服务器。
- 示例:
// 设置最大请求体大小为 1 MB evhttp_set_max_body_size(http, 1 * 1024 * 1024);
-
void evhttp_set_max_connections(struct evhttp* http, int max_connections);
- 目的: 限制服务器将处理的最大并发客户端连接数。后续传入的连接可能会根据底层监听器的配置排队或被丢弃。值 <= 0 通常表示无限制。
- 示例:
// 允许最多 500 个并发连接 evhttp_set_max_connections(http, 500);
-
int evhttp_get_connection_count(struct evhttp* http);
- 目的: 返回服务器当前正在处理的活动客户端连接数。用于监控。
- 示例:
// int current_connections = evhttp_get_connection_count(http); // printf("当前活动连接数: %d\n", current_connections);
-
void evhttp_set_default_content_type(struct evhttp *http, const char *content_type);
- 目的: 设置一个默认的
Content-Type
头部值,如果响应处理程序没有显式设置它,则会自动添加到响应中。如果content_type
为NULL
,则不添加默认值。 - 示例:
// 设置默认内容类型 evhttp_set_default_content_type(http, "text/plain; charset=utf-8");
- 目的: 设置一个默认的
-
void evhttp_set_allowed_methods(struct evhttp* http, ev_uint32_t methods);
- 目的: 指定服务器应接受并传递给处理程序的 HTTP 方法(GET、POST、PUT 等)。使用其他方法的请求将自动收到 “405 Method Not Allowed” 响应。方法使用按位或(
|
)组合。 - 示例:
// 仅允许 GET 和 POST 请求 evhttp_set_allowed_methods(http, EVHTTP_REQ_GET | EVHTTP_REQ_POST);
- 目的: 指定服务器应接受并传递给处理程序的 HTTP 方法(GET、POST、PUT 等)。使用其他方法的请求将自动收到 “405 Method Not Allowed” 响应。方法使用按位或(
-
void evhttp_set_ext_method_cmp(struct evhttp *http, evhttp_ext_method_cb cmp);
- 目的: 注册一个回调函数以处理非标准或扩展的 HTTP 方法。回调函数(
cmp
)将方法字符串(如 “COPY”, “MOVE”)映射到内部的evhttp_cmd_type
值和标志(如EVHTTP_METHOD_HAS_BODY
)。这允许 Libevent 正确解析带有这些方法的请求。你仍然需要使用evhttp_set_allowed_methods
允许相应的type
。 - 示例(概念性回调):
#include <string.h> // 需要 strcmp// 虚构的类 WEBDAV 方法类型 #define EVHTTP_REQ_COPY_CUSTOM (1 << 16) #define EVHTTP_REQ_MOVE_CUSTOM (1 << 17)// 自定义扩展方法处理函数 int my_ext_method_handler(struct evhttp_ext_method *ext) {if (ext->method == NULL) { // Libevent 问:这个类型是什么字符串?if (ext->type == EVHTTP_REQ_COPY_CUSTOM) {ext->method = "COPY"; return 0;} else if (ext->type == EVHTTP_REQ_MOVE_CUSTOM) {ext->method = "MOVE"; return 0;}} else { // Libevent 问:这个字符串是什么类型/标志?if (strcmp(ext->method, "COPY") == 0) {ext->type = EVHTTP_REQ_COPY_CUSTOM;ext->flags = 0; // COPY 通常可能没有主体return 0;} else if (strcmp(ext->method, "MOVE") == 0) {ext->type = EVHTTP_REQ_MOVE_CUSTOM;ext->flags = 0;return 0;}}return -1; // 未知方法/类型 }// ... 在设置阶段 ... // // 注册扩展方法处理器 // evhttp_set_ext_method_cmp(http, my_ext_method_handler); // // 允许这些自定义类型 // evhttp_set_allowed_methods(http, EVHTTP_REQ_GET | EVHTTP_REQ_POST | // EVHTTP_REQ_COPY_CUSTOM | EVHTTP_REQ_MOVE_CUSTOM);
- 目的: 注册一个回调函数以处理非标准或扩展的 HTTP 方法。回调函数(
-
void evhttp_set_timeout(struct evhttp *http, int timeout);
/void evhttp_set_timeout_tv(struct evhttp *http, const struct timeval* tv);
- 目的: 为服务器管理的客户端连接设置一个通用超时(以秒或
struct timeval
为单位)。这通常同时适用于读和写的不活动状态。如果客户端连接空闲时间超过此持续时间,它可能会被关闭。 - 示例:
// 设置 60 秒超时 evhttp_set_timeout(http, 60);// 或者使用 timeval 以获得更精确的时间(例如,30.5 秒) // struct timeval tv = {30, 500000}; // 30 秒, 500000 微秒 // evhttp_set_timeout_tv(http, &tv);
- 目的: 为服务器管理的客户端连接设置一个通用超时(以秒或
-
void evhttp_set_read_timeout_tv(struct evhttp *http, const struct timeval* tv);
/void evhttp_set_write_timeout_tv(struct evhttp *http, const struct timeval* tv);
- 目的: 为服务器端客户端连接上的读不活动和写不活动设置特定的超时(
struct timeval
)。提供比通用evhttp_set_timeout_tv
更精细的控制。NULL
可能会禁用特定超时或恢复为默认值。 - 示例:
// struct timeval read_tv = {60, 0}; // 60 秒读超时 // struct timeval write_tv = {30, 0}; // 30 秒写超时 // evhttp_set_read_timeout_tv(http, &read_tv); // evhttp_set_write_timeout_tv(http, &write_tv);
- 目的: 为服务器端客户端连接上的读不活动和写不活动设置特定的超时(
-
int evhttp_set_flags(struct evhttp *http, int flags);
- 目的: 设置服务器范围的操作标志。当前定义的主要标志是:
EVHTTP_SERVER_LINGERING_CLOSE
:如果客户端发送的请求体大于max_body_size
,服务器将尝试读取(并丢弃)整个超大主体,然后再发送错误响应并关闭连接。这对某些客户端可能更友好,但会消耗更多资源。没有此标志,连接可能在检测到超大时立即关闭。
- 返回值: 成功时返回 0,如果标志不支持则返回非零值。
- 示例:
// if (evhttp_set_flags(http, EVHTTP_SERVER_LINGERING_CLOSE) != 0) { // fprintf(stderr, "警告:无法设置 EVHTTP_SERVER_LINGERING_CLOSE\n"); // }
- 目的: 设置服务器范围的操作标志。当前定义的主要标志是:
服务器请求处理回调
这些函数定义了你的服务器如何响应传入的请求。
-
int evhttp_set_cb(struct evhttp *http, const char *path, void (*cb)(struct evhttp_request *, void *), void *cb_arg);
- 目的: 注册一个回调函数(
cb
)来处理特定 URI 路径(path
)的请求。这是定义服务器端点的核心机制。 - 参数:
http
: 服务器实例。path
: URI 路径(例如,“/users”, “/status”)。精确匹配。cb
: 当请求匹配路径时调用的函数。签名:void (*cb)(struct evhttp_request *req, void *arg)
cb_arg
: 传递给回调函数cb
的任意指针参数。
- 返回值: 成功时返回 0,如果此路径的回调已存在则返回 -1,其他失败返回 -2。
- 场景联想: 设置特定的接待台(
path
),配备专门的工作人员(cb
),以处理不同类型的客人问询(requests
)。 - 示例:
#include <event2/buffer.h> #include <event2/keyvalq_struct.h> // 需要 evkeyvalq// 处理 /hello 请求的回调函数 void handle_hello(struct evhttp_request *req, void *arg) {const char *message = (const char *)arg; // 获取传递的参数struct evbuffer *buf = evbuffer_new(); // 创建响应缓冲区if (!buf) {evhttp_send_error(req, HTTP_INTERNAL, "无法创建缓冲区");return;}printf("正在处理 /hello 请求\n");// 添加响应头evhttp_add_header(evhttp_request_get_output_headers(req), "Content-Type", "text/plain; charset=utf-8");// 向缓冲区添加响应内容evbuffer_add_printf(buf, "你好,世界!消息:%s\n", message);// 发送 OK (200) 响应evhttp_send_reply(req, HTTP_OK, "OK", buf);// 释放缓冲区(内容已被 Libevent 发送)evbuffer_free(buf); }// ... 在设置阶段,evhttp_new 之后 ... const char *my_message = "来自 cb_arg 的欢迎!"; // 注册 /hello 路径的处理函数 if (evhttp_set_cb(http, "/hello", handle_hello, (void *)my_message) != 0) {fprintf(stderr, "无法为 /hello 设置回调\n");// ... 清理 ...return 1; } printf("已为 /hello 注册处理函数\n");
- 目的: 注册一个回调函数(
-
int evhttp_del_cb(struct evhttp *, const char *);
- 目的: 移除先前为特定路径注册的回调。
- 返回值: 成功时返回 0,如果未找到该路径的回调则返回 -1。
- 示例:
// if (evhttp_del_cb(http, "/hello") == 0) { // printf("已移除 /hello 的处理函数\n"); // }
-
void evhttp_set_gencb(struct evhttp *http, void (*cb)(struct evhttp_request *, void *), void *arg);
- 目的: 注册一个通用回调函数(
cb
),用于处理任何其路径与通过evhttp_set_cb
设置的任何特定路径都不匹配的请求。对于处理 404 或作为默认路由器至关重要。 - 示例:
// 通用请求处理函数(通常用于 404) void handle_generic(struct evhttp_request *req, void *arg) {const char *uri = evhttp_request_get_uri(req);printf("正在处理通用请求,URI:%s\n", uri);// 发送简单的 404 Not Found 响应evhttp_send_error(req, HTTP_NOTFOUND, "Not Found"); }// ... 在设置阶段 ... // 注册通用回调函数 evhttp_set_gencb(http, handle_generic, NULL); printf("已注册通用 (404) 处理函数\n");
- 目的: 注册一个通用回调函数(
-
void evhttp_set_bevcb(struct evhttp *http, struct bufferevent *(*cb)(struct event_base *, void *), void *arg);
- 目的: 设置一个全局回调函数(
cb
),用于为所有传入到此evhttp
服务器的连接创建bufferevent
对象,除非被通过evhttp_bound_set_bevcb
设置的特定于监听器的bevcb
覆盖。主要用于通过返回启用 SSL 的 bufferevent 来为整个服务器启用 SSL/TLS。 - 示例(概念性 - 使用之前的
create_ssl_bufferevent
):// SSL_CTX *global_ssl_context = /* 初始化 OpenSSL 上下文 */; // // 设置全局的 bufferevent 创建回调,使用 SSL // evhttp_set_bevcb(http, create_ssl_bufferevent, global_ssl_context); // printf("已为服务器设置全局 SSL bufferevent 创建回调\n"); // 注意:这通常适用于在此调用*之后*绑定的套接字。
- 目的: 设置一个全局回调函数(
-
void evhttp_set_newreqcb(struct evhttp *http, int (*cb)(struct evhttp_request*, void *), void *arg);
- 目的: 设置一个回调函数(
cb
),它在连接上刚开始一个新请求时被调用,通常在头部完全解析之前。传递的evhttp_request
对象可能包含的信息很少。如果此回调返回 -1,则立即终止连接。这对于在投入资源进行完整的 HTTP 解析之前进行早期节流、IP 黑名单检查或连接级别的检查很有用。 - 示例(概念性):
// 检查新请求的回调函数 int check_new_request(struct evhttp_request *req, void *arg) {struct evhttp_connection *conn = evhttp_request_get_connection(req);const char *peer_ip = NULL;ev_uint16_t peer_port = 0;if (conn) {// 获取对端 IP 和端口evhttp_connection_get_peer(conn, &peer_ip, &peer_port);}// // 示例:检查 IP 是否在黑名单中// if (is_blacklisted(peer_ip)) {// printf("拒绝来自黑名单 IP 的连接:%s\n", peer_ip ? peer_ip : "未知");// return -1; // 终止连接// }printf("新请求开始,来自 %s:%d\n", peer_ip ? peer_ip : "未知", peer_port);return 0; // 允许连接继续 }// ... 在设置阶段 ... // evhttp_set_newreqcb(http, check_new_request, NULL);
- 目的: 设置一个回调函数(
-
void evhttp_set_errorcb(struct evhttp *http, int (*cb)(struct evhttp_request *req, struct evbuffer *buffer, int error, const char *reason, void *cbarg), void *cbarg);
- 目的: 注册一个回调函数(
cb
),用于为evhttp_send_error
通常会生成的错误页面(如 404 Not Found, 500 Internal Server Error)生成自定义的 HTML/内容。回调接收请求、一个输出缓冲区、错误代码、原因字符串和用户参数。它应该用自定义错误页面的内容填充buffer
。如果回调返回 < 0 或使缓冲区为空,则会发送默认的 Libevent 错误页面。 - 示例:
// 自定义错误页面生成函数 int custom_error_page(struct evhttp_request *req, struct evbuffer *buffer,int error, const char *reason, void *cbarg) {const char *custom_message = (const char *)cbarg; // 获取用户参数printf("正在为 %d %s 生成自定义错误页面\n", error, reason);// 向缓冲区添加 HTML 内容evbuffer_add_printf(buffer, "<!DOCTYPE html><html><head><title>%d %s</title></head>", error, reason);evbuffer_add_printf(buffer, "<body><h1>哎呀!错误 %d</h1>", error);evbuffer_add_printf(buffer, "<p>出错了:%s</p>", reason);if (custom_message) {evbuffer_add_printf(buffer, "<p><i>%s</i></p>", custom_message);}// 确保内容足够大以避免 IE 覆盖它(>= 512 字节)while (evbuffer_get_length(buffer) < 512) {evbuffer_add(buffer, " ", 1);}evbuffer_add_printf(buffer, "</body></html>");return 0; // 表示我们已经填充了缓冲区 }// ... 在设置阶段 ... // const char *footer_msg = "如果问题持续存在,请联系支持。"; // // 设置错误回调 // evhttp_set_errorcb(http, custom_error_page, (void*)footer_msg);
- 目的: 注册一个回调函数(
虚拟主机和别名
用于从同一服务器实例提供多个域或主机名的服务。
-
int evhttp_add_virtual_host(struct evhttp* http, const char *pattern, struct evhttp* vhost);
- 目的: 将一个辅助
evhttp
实例(vhost
)与主服务器(http
)关联起来。其Host:
头部匹配 globpattern
(不区分大小写,例如 “*.example.com”, “api.example.org”)的请求将被路由到vhost
实例上定义的处理程序,而不是主http
实例。vhost
不应拥有自己的监听套接字;它只定义请求处理程序。主http
服务器管理vhost
的生命周期。 - 返回值: 成功时返回 0,失败时返回 -1。
- 示例:
// 为 vhost 创建一个单独的 evhttp 实例 struct evhttp *vhost_api = evhttp_new(base); if (!vhost_api) { /* 处理错误 */ }// 为 vhost 设置特定的回调 // evhttp_set_cb(vhost_api, "/status", handle_api_status, NULL); // evhttp_set_gencb(vhost_api, handle_api_generic, NULL);// 将其添加到主服务器 if (evhttp_add_virtual_host(http, "api.mydomain.com", vhost_api) != 0) {fprintf(stderr, "添加虚拟主机 api.mydomain.com 失败\n");evhttp_free(vhost_api); // 如果添加失败,则清理// ... 更多清理 ...return 1; } printf("已为 api.mydomain.com 添加虚拟主机\n"); // 注意:现在不要直接调用 evhttp_free(vhost_api), // evhttp_free(http) 会处理它。
- 目的: 将一个辅助
-
int evhttp_remove_virtual_host(struct evhttp* http, struct evhttp* vhost);
- 目的: 移除先前添加的虚拟主机关联。调用者之后负责在必要时释放
vhost
。 - 返回值: 成功时返回 0,如果未找到
vhost
则返回 -1。 - 示例:
// if (evhttp_remove_virtual_host(http, vhost_api) == 0) { // printf("已移除虚拟主机 api.mydomain.com\n"); // evhttp_free(vhost_api); // 现在我们需要释放它 // vhost_api = NULL; // }
- 目的: 移除先前添加的虚拟主机关联。调用者之后负责在必要时释放
-
int evhttp_add_server_alias(struct evhttp *http, const char *alias);
- 目的: 为服务器(或虚拟主机)
http
添加一个其应响应的备用主机名(alias
)。这允许单个evhttp
实例(主实例或 vhost)被多个Host:
头部值识别,而无需为简单的别名设置完整的虚拟主机。 - 返回值: 成功时返回 0,失败时返回 -1。
- 示例:
// 让主服务器也响应 "www.mydomain.com" // if (evhttp_add_server_alias(http, "www.mydomain.com") != 0) { // fprintf(stderr, "添加服务器别名 www.mydomain.com 失败\n"); // }// 让 API vhost 也响应 "api.mydomain.org" // if (vhost_api) { // evhttp_add_server_alias(vhost_api, "api.mydomain.org"); // }
- 目的: 为服务器(或虚拟主机)
-
int evhttp_remove_server_alias(struct evhttp *http, const char *alias);
- 目的: 移除先前添加的服务器别名。
- 返回值: 成功时返回 0,如果未找到别名则返回 -1。
- 示例:
// if (evhttp_remove_server_alias(http, "www.mydomain.com") == 0) { // printf("已移除服务器别名 www.mydomain.com\n"); // }
服务器响应函数
在你的请求处理程序(cb
)中使用,用于向客户端发送回数据。
-
void evhttp_send_error(struct evhttp_request *req, int error, const char *reason);
- 目的: 向客户端发送标准的 HTTP 错误响应(例如 404、500)。Libevent 会生成一个简单的 HTML 主体。如果设置了
errorcb
,则会使用它来代替。 - 参数:
req
: 请求对象。error
: HTTP 状态码(例如HTTP_NOTFOUND
,HTTP_INTERNAL
)。reason
: 简短的、人类可读的解释(例如 “Not Found”, “Internal Server Error”)。如果为NULL
,则使用该代码的标准原因短语。
- 示例: (见上面的
handle_generic
示例)
- 目的: 向客户端发送标准的 HTTP 错误响应(例如 404、500)。Libevent 会生成一个简单的 HTML 主体。如果设置了
-
void evhttp_send_reply(struct evhttp_request *req, int code, const char *reason, struct evbuffer *databuf);
- 目的: 发送一个完整的 HTTP 响应,包括状态码、原因短语、头部(先前添加到请求的输出头部)以及包含在
databuf
中的响应主体。Libevent 会负责处理Content-Length
(除非使用分块编码)。重要: Libevent 在发送时会排空databuf
;缓冲区本身仍然由调用者拥有,如果必要,在调用后应由调用者释放。 - 参数:
req
: 请求对象。code
: HTTP 状态码(例如HTTP_OK
)。reason
: 原因短语(例如 “OK”)。databuf
: 包含响应主体的evbuffer
。对于没有主体的响应(如 204 No Content),可以为NULL
。
- 示例: (见上面的
handle_hello
示例)
- 目的: 发送一个完整的 HTTP 响应,包括状态码、原因短语、头部(先前添加到请求的输出头部)以及包含在
-
void evhttp_send_reply_start(struct evhttp_request *req, int code, const char *reason);
- 目的: 启动一个使用
Transfer-Encoding: chunked
的响应。发送状态行和头部,但保持连接打开以供后续数据块使用。当总响应大小预先未知或需要流式传输大量数据时使用此方法。 - 场景联想: 为多幕剧拉开序幕。你宣布开始(
_start
),但主要表演(_chunk
)还在后面。 - 示例(分块处理程序的开始):
// 处理流式请求的回调 void handle_stream(struct evhttp_request *req, void *arg) {printf("开始分块流响应\n");// 在调用 start 之前设置头部evhttp_add_header(evhttp_request_get_output_headers(req), "Content-Type", "text/plain; charset=utf-8");// 启动分块响应evhttp_send_reply_start(req, HTTP_OK, "OK");// 现在安排发送块,例如使用定时器或其他事件// send_next_chunk(req); // 虚构的函数 }
- 目的: 启动一个使用
-
void evhttp_send_reply_chunk(struct evhttp_request *req, struct evbuffer *databuf);
- 目的: 作为由
evhttp_send_reply_start
启动的分块响应的一部分,发送单个数据块。Libevent 处理分块帧(<chunk-size>\r\n<data>\r\n
)。与evhttp_send_reply
类似,它会排空databuf
,该缓冲区仍由调用者拥有。 - 示例(发送一个块):
// 发送一个数据块的函数 void send_a_chunk(struct evhttp_request *req, const char* data) {struct evbuffer *chunk_buf = evbuffer_new(); // 创建块缓冲区if (!chunk_buf) return; // 处理错误evbuffer_add_printf(chunk_buf, "%s", data); // 添加数据printf("正在发送块:%s", data);evhttp_send_reply_chunk(req, chunk_buf); // 发送块evbuffer_free(chunk_buf); // 释放缓冲区// 安排下一个块或结束... }
- 目的: 作为由
-
void evhttp_send_reply_chunk_with_cb(struct evhttp_request *req, struct evbuffer *databuf, void (*cb)(struct evhttp_connection *, void *), void *arg);
- 目的: 类似于
evhttp_send_reply_chunk
,但包含一个回调函数(cb
),该回调在这个特定的块成功写入底层连接的缓冲区之后被调用。这对于与特定块发送相关的流控制或资源清理很有用。 - 示例(概念性回调):
// 块发送完成的回调 void chunk_sent_cb(struct evhttp_connection *conn, void *arg) {int *chunks_left = (int*)arg; // 获取剩余块数(*chunks_left)--;printf("块发送成功。剩余 %d 块。\n", *chunks_left);// 也许基于此触发发送下一个块? }// ... 在发送逻辑内部 ... // static int remaining = 5; // 假设总共 5 块 // struct evbuffer *chunk_buf = evbuffer_new(); // evbuffer_add_printf(chunk_buf, "数据块 %d\n", 6 - remaining); // // 发送带回调的块 // evhttp_send_reply_chunk_with_cb(req, chunk_buf, chunk_sent_cb, &remaining); // evbuffer_free(chunk_buf);
- 目的: 类似于
-
void evhttp_send_reply_end(struct evhttp_request *req);
- 目的: 完成一个分块响应。发送终止的零长度块(
0\r\n\r\n
)并清理请求对象(除非调用了evhttp_request_own
)。必须调用此函数以正确完成由evhttp_send_reply_start
启动的响应。 - 场景联想: 演出结束后的最后谢幕。标志着表演的结束。
- 示例(结束流):
// 完成流式响应的函数 void finish_stream(struct evhttp_request *req) {printf("结束分块流响应。\n");evhttp_send_reply_end(req);// 此后 'req' 对象很可能无效,除非被拥有。 }
- 目的: 完成一个分块响应。发送终止的零长度块(
哇,这已经涵盖了服务器端的函数!我们进展很顺利。仅这一部分可能就超过了 2000 字,但让我们保持势头,继续处理客户端和工具函数。
客户端连接设置
用于建立到远程 HTTP 服务器的连接的函数。
-
struct evhttp_connection *evhttp_connection_base_bufferevent_new(struct event_base *base, struct evdns_base *dnsbase, struct bufferevent* bev, const char *address, ev_uint16_t port);
- 目的: 创建一个用于发出客户端请求的
evhttp_connection
对象,使用提供的bufferevent
(bev
) 进行底层传输。如果bev
为NULL
,将在内部创建一个标准的基于套接字的bufferevent
。允许为客户端连接使用自定义传输(如 SSL bufferevent)。evhttp_connection
获得所提供的bev
的所有权。 - 参数:
base
: 用于处理连接事件的event_base
。dnsbase
: 可选的evdns_base
用于异步 DNS 解析。如果为NULL
,DNS 查找可能会阻塞。bev
: 可选的、预先配置的bufferevent
(必须未设置 FD)。如果为NULL
,则创建一个。address
: 要连接的服务器的主机名或 IP 地址。port
: 服务器的端口号。
- 返回值: 一个新的
evhttp_connection
,失败时返回NULL
。 - 示例(客户端使用 SSL):
#include <event2/dns.h> #include <event2/bufferevent_ssl.h> #include <openssl/ssl.h>// ... 假设 base, dns_base, 和 SSL_CTX* client_ctx 已存在 ...SSL *ssl = SSL_new(client_ctx); // 创建 SSL 对象 // 注意:状态是 BUFFEREVENT_SSL_CONNECTING // 创建 OpenSSL 套接字 bufferevent struct bufferevent *bev = bufferevent_openssl_socket_new(base, -1, ssl,BUFFEREVENT_SSL_CONNECTING, // 客户端连接状态BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); if (!bev) { /* 处理错误 */ }// 重要:如果需要,启用主机验证! // bufferevent_openssl_set_allow_dirty_shutdown(bev, 1); // 可选:允许不干净关闭// 使用提供的 SSL bufferevent 创建 evhttp 连接 struct evhttp_connection *evcon = evhttp_connection_base_bufferevent_new(base, dns_base, bev, "encrypted.example.com", 443);if (!evcon) {fprintf(stderr, "创建 SSL evhttp 连接失败\n");// bufferevent_free(bev); // 仅当 evhttp_connection_... 失败时才释放 bev// ... 更多清理 ...return 1; } printf("已创建 evhttp 客户端连接 (SSL) 句柄。\n"); // 'evcon' 现在拥有 'bev'。不要直接释放 'bev'。
- 目的: 创建一个用于发出客户端请求的
-
struct evhttp_connection *evhttp_connection_base_bufferevent_unix_new(struct event_base *base, struct bufferevent* bev, const char *path);
- 目的: 创建一个
evhttp_connection
以连接到监听 Unix 域套接字的 HTTP 服务器。与上面类似,但接受套接字path
而不是地址/端口。 - 返回值: 一个新的
evhttp_connection
,失败时返回NULL
。 - 示例:
// ... 假设 base 存在 ... const char *socket_path = "/tmp/my_http_server.sock"; // Unix 套接字路径 // 'bev' 可以为 NULL,让 libevent 创建一个标准的 // 创建连接到 Unix 域套接字的 evhttp 连接 struct evhttp_connection *evcon_unix = evhttp_connection_base_bufferevent_unix_new(base, NULL, socket_path);if (!evcon_unix) {fprintf(stderr, "创建到 %s 的 Unix 套接字 evhttp 连接失败\n", socket_path);// ... 清理 ...return 1; } printf("已创建 evhttp 客户端连接 (Unix 套接字) 句柄。\n");
- 目的: 创建一个
-
struct evhttp_connection *evhttp_connection_base_bufferevent_reuse_new(struct event_base *base, struct evdns_base *dnsbase, struct bufferevent* bev);
- 目的: 在一个已经连接的
bufferevent
(bev
) 周围创建一个evhttp_connection
包装器。用于在现有的、已建立的连接之上分层 HTTP。在这种情况下,evhttp_connection
不获得bev
的所有权。 - 返回值: 一个新的
evhttp_connection
,失败时返回NULL
。 - 示例(概念性):
// struct bufferevent *already_connected_bev = /* 获取已连接的 bev */; // // 重用已连接的 bufferevent 创建 evhttp 连接 // struct evhttp_connection *evcon_reuse = evhttp_connection_base_bufferevent_reuse_new( // base, dns_base, already_connected_bev); // if (!evcon_reuse) { /* 错误 */ } // printf("已创建重用现有 bufferevent 的 evhttp 连接。\n"); // 你仍然负责稍后释放 'already_connected_bev'。
- 目的: 在一个已经连接的
-
struct evhttp_connection *evhttp_connection_base_new(struct event_base *base, struct evdns_base *dnsbase, const char *address, ev_uint16_t port);
- 目的: 创建标准 TCP 客户端连接的最常用方法。它等效于使用
bev = NULL
调用evhttp_connection_base_bufferevent_new
。Libevent 创建并管理底层的套接字 bufferevent。 - 返回值: 一个新的
evhttp_connection
,失败时返回NULL
。 - 示例:
// ... 假设 base 和 可选的 dns_base 存在 ... // 创建一个标准的到 www.google.com:80 的 evhttp 连接 struct evhttp_connection *evcon = evhttp_connection_base_new(base, dns_base, "www.google.com", 80);if (!evcon) {fprintf(stderr, "创建标准 evhttp 连接失败\n");// ... 清理 ...return 1; } printf("已创建标准 evhttp 客户端连接句柄。\n");
- 目的: 创建标准 TCP 客户端连接的最常用方法。它等效于使用
-
void evhttp_connection_free(struct evhttp_connection *evcon);
- 目的: 释放一个
evhttp_connection
对象及其关联资源(包括底层的bufferevent
,如果它是内部创建的或通过..._bufferevent_new
创建的)。当不再需要连接时应调用此函数。此连接上的任何待处理请求可能会被取消/失败。 - 示例:
// ... 完成连接使用后 ... // evhttp_connection_free(evcon); // evcon = NULL;
- 目的: 释放一个
客户端连接配置
调整传出客户端连接的行为。
-
struct bufferevent* evhttp_connection_get_bufferevent(struct evhttp_connection *evcon);
- 目的: 检索与客户端连接关联的底层
bufferevent
。用于访问较低级别的传输细节或直接在bufferevent
上配置选项(例如,如果在bev
最初为 NULL 后设置 SSL 选项,尽管使用..._bufferevent_new
创建通常更清晰)。 - 示例:
// // 获取底层 bufferevent // struct bufferevent *bev = evhttp_connection_get_bufferevent(evcon); // if (bev) { // // 检查或配置 bev(小心!) // // 示例:获取底层 fd(如果是基于套接字的) // // evutil_socket_t fd = bufferevent_getfd(bev); // }
- 目的: 检索与客户端连接关联的底层
-
struct evhttp *evhttp_connection_get_server(struct evhttp_connection *evcon);
- 目的: 主要用于服务器端。当在服务器回调中获取的连接句柄(
evhttp_request_get_connection
)上调用时,它返回处理该连接的evhttp
服务器实例。对于使用evhttp_connection_*_new
创建的客户端连接,这通常返回NULL
。
- 目的: 主要用于服务器端。当在服务器回调中获取的连接句柄(
-
void evhttp_connection_set_family(struct evhttp_connection *evcon, int family);
- 目的: 向连接提示在解析主机名时是偏好 IPv4 (
AF_INET
)、IPv6 (AF_INET6
),还是允许两者 (AF_UNSPEC
- 默认)。 - 示例:
// // 为此连接偏好 IPv4 // evhttp_connection_set_family(evcon, AF_INET);
- 目的: 向连接提示在解析主机名时是偏好 IPv4 (
-
int evhttp_connection_set_flags(struct evhttp_connection *evcon, int flags);
- 目的: 设置影响客户端连接行为的标志。相关标志包括:
EVHTTP_CON_REUSE_CONNECTED_ADDR
:如果连接失败并且启用了重试,则重用它先前成功连接到的特定 IP 地址(如果有),而不是重新解析主机名。EVHTTP_CON_READ_ON_WRITE_ERROR
:如果发生写错误(例如,发送请求体),在声明失败之前尝试从服务器读取任何挂起的数据(如错误响应)。EVHTTP_CON_LINGERING_CLOSE
:与服务器标志类似,可能在出错时等待服务器数据,然后再完全关闭。
- 返回值: 成功时返回 0,如果标志未知则返回非零值。
- 示例:
// // 设置在写错误时尝试读取 // evhttp_connection_set_flags(evcon, EVHTTP_CON_READ_ON_WRITE_ERROR);
- 目的: 设置影响客户端连接行为的标志。相关标志包括:
-
void evhttp_connection_set_ext_method_cmp(struct evhttp_connection *evcon, evhttp_ext_method_cb cmp);
- 目的: 专门为此客户端连接设置扩展方法映射回调(类似于服务器的
evhttp_set_ext_method_cmp
)。允许使用此连接发送带有自定义方法的请求。
- 目的: 专门为此客户端连接设置扩展方法映射回调(类似于服务器的
-
struct event_base *evhttp_connection_get_base(struct evhttp_connection *evcon);
- 目的: 返回与此客户端连接关联的
event_base
。
- 目的: 返回与此客户端连接关联的
-
void evhttp_connection_set_max_headers_size(struct evhttp_connection *evcon, ev_ssize_t new_max_headers_size);
- 目的: 设置在此连接上接收的响应头的最大总大小。
-
void evhttp_connection_set_max_body_size(struct evhttp_connection* evcon, ev_ssize_t new_max_body_size);
- 目的: 设置在此连接上接收的响应体的最大大小。
-
void evhttp_connection_free_on_completion(struct evhttp_connection *evcon);
- 目的: 告知 Libevent 在其上当前未完成的请求完成(成功或出错)之后自动释放此
evhttp_connection
。对于不需要持久连接的“一次性”连接很有用。调用此函数后,你不应手动释放evcon
。 - 场景联想: 为连接设置一个自毁序列,在其任务(请求)完成后执行。
- 目的: 告知 Libevent 在其上当前未完成的请求完成(成功或出错)之后自动释放此
-
void evhttp_connection_set_local_address(struct evhttp_connection *evcon, const char *address);
- 目的: 在连接之前将连接的客户端端绑定到特定的本地 IP 地址。如果客户端机器有多个网络接口,这很有用。注意:可能会重置底层的 bufferevent/FD。
- 示例:
// // 强制连接源自本地 IP 192.168.1.100 // evhttp_connection_set_local_address(evcon, "192.168.1.100");
-
void evhttp_connection_set_local_port(struct evhttp_connection *evcon, ev_uint16_t port);
- 目的: 将客户端端绑定到特定的本地端口。比设置地址的需求少见。
-
void evhttp_connection_set_timeout(struct evhttp_connection *evcon, int timeout);
/void evhttp_connection_set_timeout_tv(struct evhttp_connection *evcon, const struct timeval *tv);
- 目的: 设置客户端连接的整体不活动超时(秒或
timeval
)。默认情况下适用于读和写不活动,但不适用于初始连接超时(历史原因)。使用下面的特定函数进行更精细的控制。 - 示例:
// // 设置连接的 30 秒超时 // evhttp_connection_set_timeout(evcon, 30);
- 目的: 设置客户端连接的整体不活动超时(秒或
-
void evhttp_connection_set_connect_timeout_tv(struct evhttp_connection *evcon, const struct timeval *tv);
- 目的: 仅为初始 TCP 连接建立阶段设置特定的超时(
timeval
)。 - 示例:
// struct timeval connect_tv = {5, 0}; // 5 秒连接超时 // evhttp_connection_set_connect_timeout_tv(evcon, &connect_tv);
- 目的: 仅为初始 TCP 连接建立阶段设置特定的超时(
-
void evhttp_connection_set_read_timeout_tv(struct evhttp_connection *evcon, const struct timeval *tv);
/void evhttp_connection_set_write_timeout_tv(struct evhttp_connection *evcon, const struct timeval *tv);
- 目的: 为连接建立后的客户端连接上的读或写不活动设置特定的超时(
timeval
)。
- 目的: 为连接建立后的客户端连接上的读或写不活动设置特定的超时(
-
void evhttp_connection_set_initial_retry_tv(struct evhttp_connection *evcon, const struct timeval *tv);
- 目的: 设置在第一次重试尝试之前的初始延迟(
timeval
),前提是evhttp_connection_set_retries
> 0。后续重试通常使用指数退避(将延迟加倍)。默认值通常约为 2 秒。
- 目的: 设置在第一次重试尝试之前的初始延迟(
-
void evhttp_connection_set_retries(struct evhttp_connection *evcon, int retry_max);
- 目的: 设置 Libevent 在此连接上自动重试请求的最大次数,如果请求因连接错误或超时而失败(在调用请求的回调函数报告错误之前)。
0
表示不重试(默认)。-1
表示无限重试(谨慎使用!)。 - 示例:
// // 失败时最多重试 3 次 // evhttp_connection_set_retries(evcon, 3); // struct timeval retry_delay = {1, 0}; // 初始延迟 1 秒 // evhttp_connection_set_initial_retry_tv(evcon, &retry_delay);
- 目的: 设置 Libevent 在此连接上自动重试请求的最大次数,如果请求因连接错误或超时而失败(在调用请求的回调函数报告错误之前)。
-
void evhttp_connection_set_closecb(struct evhttp_connection *evcon, void (*)(struct evhttp_connection *, void *), void *);
- 目的: 注册一个回调函数,如果底层连接意外关闭(例如,服务器关闭连接,网络错误)并且此时没有活动请求正在处理,则调用该回调。它不在正常请求完成或
evhttp_connection_free
期间调用。用于检测持久连接的关闭。 - 示例:
// 客户端连接关闭的回调 void client_conn_close_cb(struct evhttp_connection *closed_evcon, void *arg) {printf("客户端连接 %p 意外关闭。\n", (void*)closed_evcon);// 可能尝试重新连接或清理关联状态?// 小心:'closed_evcon' 可能在此回调后不久被释放。 }// ... 创建 evcon 之后 ... // evhttp_connection_set_closecb(evcon, client_conn_close_cb, NULL);
- 目的: 注册一个回调函数,如果底层连接意外关闭(例如,服务器关闭连接,网络错误)并且此时没有活动请求正在处理,则调用该回调。它不在正常请求完成或
-
void evhttp_connection_get_peer(struct evhttp_connection *evcon, const char **address, ev_uint16_t *port);
- 目的: 获取连接建立的(或配置为连接的)远程对端的 IP 地址字符串和端口号。地址字符串指针在连接被释放之前有效。
- 示例:
// const char *peer_addr; // ev_uint16_t peer_port; // evhttp_connection_get_peer(evcon, &peer_addr, &peer_port); // if (peer_addr) { // printf("连接对端:%s:%u\n", peer_addr, peer_port); // }
-
const struct sockaddr* evhttp_connection_get_addr(struct evhttp_connection *evcon);
- 目的: 获取远程对端的原始
struct sockaddr
。比get_peer
更详细,但需要了解套接字地址结构。如果未连接或地址不可用,则返回NULL
。
- 目的: 获取远程对端的原始
发出客户端请求
使用客户端连接创建、配置和发送 HTTP 请求。
-
struct evhttp_request *evhttp_request_new(void (*cb)(struct evhttp_request *, void *), void *arg);
- 目的: 创建一个新的、空的
evhttp_request
对象。此对象将保存请求详情(头部、主体)并最终保存响应详情。 - 参数:
cb
: 完成回调函数。当整个 HTTP 请求-响应周期完成时(无论是成功还是出错),将调用此函数。签名:void (*cb)(struct evhttp_request *req, void *arg)
arg
: 传递给完成回调cb
的任意指针参数。
- 返回值: 一个新的
evhttp_request
对象,失败时返回NULL
。 - 场景联想: 准备一个消息卷轴(
evhttp_request
),你将在上面写下你的请求,稍后接收回复。回调cb
是那个将向你宣读最终卷轴(请求+回复)的人。 - 示例(客户端请求回调):
#include <event2/buffer.h> #include <event2/keyvalq_struct.h>// 客户端请求完成的回调函数 void client_request_done_cb(struct evhttp_request *req, void *arg) {long user_id = (long)arg; // 示例:检索用户数据printf("用户 %ld 的请求已完成。\n", user_id);if (req == NULL) {// req 为 NULL 可能是请求被取消或发生严重错误printf(" (请求对象为 NULL - 可能已被取消或发生严重错误)\n");return;}// 获取响应状态码int response_code = evhttp_request_get_response_code(req);printf(" 响应码:%d (%s)\n", response_code, evhttp_request_get_response_code_line(req));if (response_code != HTTP_OK) {fprintf(stderr, " 请求失败!\n");// 如果没有设置错误回调,可以考虑检查 evhttp_request_get_response_error()return; // 或者以不同方式处理错误}// 获取响应体缓冲区struct evbuffer *response_body = evhttp_request_get_input_buffer(req);size_t body_len = evbuffer_get_length(response_body);printf(" 响应体长度:%zu\n", body_len);// 处理响应体// char *data = malloc(body_len + 1);// if (data) {// evbuffer_copyout(response_body, data, body_len); // 从 evbuffer 复制数据// data[body_len] = '\0'; // 添加 null 终止符// printf(" 响应体:\n%s\n", data);// free(data);// }// 注意:此回调返回后,'req' 通常会自动释放,// 除非使用了 evhttp_request_own() 或 make_request 失败。 }// ... 在发出请求的代码部分 ... // long current_user = 12345; // // 创建新的请求对象,并指定完成回调和参数 // struct evhttp_request *request = evhttp_request_new(client_request_done_cb, (void*)current_user); // if (!request) { // fprintf(stderr, "创建请求对象失败\n"); // // ... 清理 ... // return 1; // }
- 目的: 创建一个新的、空的
-
void evhttp_request_free(struct evhttp_request *req);
- 目的: 手动释放一个
evhttp_request
对象。重要提示: 如果evhttp_make_request
成功,你通常不直接调用此函数,因为 Libevent 在这种情况下会管理请求的生命周期。你会在evhttp_request_new
成功但evhttp_make_request
立即失败时,或者在服务器端使用了evhttp_request_own
时调用它。
- 目的: 手动释放一个
-
void evhttp_request_set_chunked_cb(struct evhttp_request *, void (*cb)(struct evhttp_request *, void *));
- 目的: 设置一个回调函数(
cb
),当响应体的块从服务器到达时(如果服务器发送分块响应),该回调会被重复调用。允许在不缓冲整个主体的情况下增量处理大型响应。回调接收req
对象(使用evhttp_request_get_input_buffer
获取当前块的数据)和来自evhttp_request_new
的原始用户参数。回调返回后,输入缓冲区会自动排空。对于零长度响应,不会调用此回调。 - 示例(块回调):
// 客户端接收响应块的回调 void client_chunk_cb(struct evhttp_request *req, void *arg) {struct evbuffer *chunk_data = evhttp_request_get_input_buffer(req); // 获取当前块的数据size_t chunk_len = evbuffer_get_length(chunk_data);printf("收到大小为 %zu 的响应块\n", chunk_len);// 处理这个块(例如,写入文件,增量解析)// fwrite(evbuffer_pullup(chunk_data, -1), 1, chunk_len, outfile); }// ... 在调用 evhttp_make_request 之前 ... // evhttp_request_set_chunked_cb(request, client_chunk_cb);
- 目的: 设置一个回调函数(
-
void evhttp_request_set_header_cb(struct evhttp_request *, int (*cb)(struct evhttp_request *, void *));
- 目的: 设置一个回调函数(
cb
),在所有响应头都已接收并解析之后,但在响应体(或第一个块)开始到达之前调用。允许及早检查头部(例如Content-Type
,Content-Length
)。如果回调返回 < 0 的值,则关闭连接,请求处理停止(主完成回调可能仍会因错误而被调用)。 - 示例(头部回调):
// 客户端接收响应头部的回调 int client_header_cb(struct evhttp_request *req, void *arg) {printf("收到响应头部。\n");struct evkeyvalq *headers = evhttp_request_get_input_headers(req); // 获取输入头部const char *ctype = evhttp_find_header(headers, "Content-Type"); // 查找 Content-Typeconst char *clen = evhttp_find_header(headers, "Content-Length"); // 查找 Content-Lengthprintf(" Content-Type: %s\n", ctype ? ctype : "N/A");printf(" Content-Length: %s\n", clen ? clen : "N/A (或分块)");// 示例:及早拒绝非 JSON 响应// if (!ctype || strstr(ctype, "application/json") == NULL) {// fprintf(stderr, " 错误:期望 JSON 响应,但收到 %s\n", ctype ? ctype : "未知");// return -1; // 终止处理// }return 0; // 继续处理 }// ... 在调用 evhttp_make_request 之前 ... // evhttp_request_set_header_cb(request, client_header_cb);
- 目的: 设置一个回调函数(
-
void evhttp_request_set_error_cb(struct evhttp_request *, void (*)(enum evhttp_request_error, void *));
- 目的: 专门为处理请求生命周期中的客户端错误设置回调。当发生错误时,它在主完成回调(来自
evhttp_request_new
的cb
)之前被调用。 - 参数:
- 回调接收一个
enum evhttp_request_error
来指示错误类型(例如EVREQ_HTTP_TIMEOUT
,EVREQ_HTTP_EOF
,EVREQ_HTTP_INVALID_HEADER
,EVREQ_HTTP_BUFFER_ERROR
,EVREQ_HTTP_REQUEST_CANCEL
,EVREQ_HTTP_DATA_TOO_LONG
)和来自evhttp_request_new
的用户参数。
- 回调接收一个
- 示例:
// 客户端请求错误的回调 void client_error_cb(enum evhttp_request_error error_code, void *arg) {fprintf(stderr, "请求遇到错误:");switch (error_code) {case EVREQ_HTTP_TIMEOUT: fprintf(stderr, "超时\n"); break;case EVREQ_HTTP_EOF: fprintf(stderr, "EOF / 连接过早关闭\n"); break;case EVREQ_HTTP_INVALID_HEADER: fprintf(stderr, "响应中存在无效头部\n"); break;case EVREQ_HTTP_BUFFER_ERROR: fprintf(stderr, "缓冲区错误\n"); break;case EVREQ_HTTP_REQUEST_CANCEL: fprintf(stderr, "请求已被取消\n"); break;case EVREQ_HTTP_DATA_TOO_LONG: fprintf(stderr, "响应数据过长\n"); break;default: fprintf(stderr, "未知错误 (%d)\n", error_code); break;} }// ... 在调用 evhttp_make_request 之前 ... // evhttp_request_set_error_cb(request, client_error_cb);
- 目的: 专门为处理请求生命周期中的客户端错误设置回调。当发生错误时,它在主完成回调(来自
-
void evhttp_request_set_on_complete_cb(struct evhttp_request *req, void (*cb)(struct evhttp_request *, void *), void *cb_arg);
- 目的: 设置一个回调,在请求完全完成(响应已发送/接收,主回调可能已完成)之后,但在
evhttp_request
对象可能被 Libevent 释放之前立即调用。用于最终的清理、日志记录或与请求对象生命周期特别相关的度量,独立于主要结果处理。 - 示例(概念性):
// // 假设有一个结构来存储请求计时信息 // struct RequestTimers { ev_uint64_t start_time; /* ... */ };// 请求最终清理回调 void request_final_cleanup_cb(struct evhttp_request *req, void *arg) {// RequestTimers *timers = (RequestTimers*)arg;// ev_uint64_t end_time = get_monotonic_time_usec(); // 获取当前时间// printf("请求在 %llu 微秒内完成。\n", end_time - timers->start_time);// free(timers); // 释放计时器结构printf("请求 %p 最终清理。\n", (void*)req); }// ... 创建请求时 ... // RequestTimers *req_timers = malloc(sizeof(RequestTimers)); // req_timers->start_time = get_monotonic_time_usec(); // 记录开始时间 // struct evhttp_request *request = evhttp_request_new(client_request_done_cb, some_arg); // // 设置请求完成时的最终回调 // evhttp_request_set_on_complete_cb(request, request_final_cleanup_cb, req_timers);
- 目的: 设置一个回调,在请求完全完成(响应已发送/接收,主回调可能已完成)之后,但在
-
void evhttp_request_own(struct evhttp_request *req);
- 目的: 告知 Libevent 应用程序正在接管
evhttp_request
对象的生命周期所有权。Libevent 在请求完成后(例如,服务器处理程序返回后,或客户端回调完成后)不会自动释放它。应用程序必须稍后调用evhttp_request_free(req)
。主要用于需要在发送最终回复之前执行异步操作的服务器处理程序。 - 场景联想: 告诉系统,“先别清理这个,我还在用它!”
- 目的: 告知 Libevent 应用程序正在接管
-
int evhttp_request_is_owned(struct evhttp_request *req);
- 目的: 检查是否已对此请求对象调用
evhttp_request_own()
。如果由应用程序拥有则返回 1,否则返回 0。
- 目的: 检查是否已对此请求对象调用
-
struct evhttp_connection *evhttp_request_get_connection(struct evhttp_request *req);
- 目的: 返回与此请求关联的
evhttp_connection
。在回调内部用于获取连接详情(对端地址等)或可能重用连接以进行后续请求(如果是持久连接)很有用。
- 目的: 返回与此请求关联的
-
int evhttp_make_request(struct evhttp_connection *evcon, struct evhttp_request *req, enum evhttp_cmd_type type, const char *uri);
- 目的: 提交客户端请求(
req
),使其通过指定的连接(evcon
)发送。这是实际启动客户端请求网络通信的函数。 - 参数:
evcon
: 客户端连接句柄(来自evhttp_connection_*_new
)。req
: 请求对象(来自evhttp_request_new
),已配置头部(输出头部)和可能有的请求体(输出缓冲区)。type
: HTTP 方法(例如EVHTTP_REQ_GET
,EVHTTP_REQ_POST
)。uri
: 请求 URI 的路径和查询字符串部分(例如 “/search?q=libevent”, “/users/123”)。
- 返回值: 成功时返回 0(表示请求已成功排队等待发送)。立即失败时返回 -1(例如,无效参数,连接已关闭)。重要提示: 此处成功仅表示它已排队;实际请求稍后仍可能失败(超时、连接错误),从而触发错误/完成回调。如果此函数成功,
evcon
获得req
的所有权,你不应自己调用evhttp_request_free(req)
(除非你之后调用evhttp_request_own
)。 - 场景联想: 将准备好的消息卷轴(
req
)交给信使(evcon
),并附上指令(type
,uri
),让其递送。 - 示例(将客户端部分组合在一起):
// 提交客户端请求的函数 int submit_client_request(struct event_base *base, struct evdns_base *dns_base) {// 创建到 httpbin.org 的连接struct evhttp_connection *evcon = evhttp_connection_base_new(base, dns_base, "httpbin.org", 80);if (!evcon) return -1; // 错误// 可选:配置连接(超时、重试等)// evhttp_connection_set_timeout(evcon, 10); // 10 秒超时long user_data = 42; // 示例参数// 创建请求对象struct evhttp_request *req = evhttp_request_new(client_request_done_cb, (void*)user_data);if (!req) {evhttp_connection_free(evcon);return -1; // 错误}// 可选:设置特定回调// evhttp_request_set_error_cb(req, client_error_cb);// evhttp_request_set_chunked_cb(req, client_chunk_cb); // 如果期望分块响应// 添加请求头部(请求的输出头部)struct evkeyvalq *req_headers = evhttp_request_get_output_headers(req);evhttp_add_header(req_headers, "Host", "httpbin.org"); // Host 头部通常是必需的evhttp_add_header(req_headers, "User-Agent", "LibeventClient/1.0");evhttp_add_header(req_headers, "Connection", "close"); // 或 Keep-Alive// 添加请求体(如果是 POST, PUT 等)- 请求的输出缓冲区// if (method == EVHTTP_REQ_POST) {// struct evbuffer *req_body = evhttp_request_get_output_buffer(req);// evbuffer_add_printf(req_body, "param1=value1¶m2=value2");// // 如果有请求体,通常需要设置 Content-Type// evhttp_add_header(req_headers, "Content-Type", "application/x-www-form-urlencoded");// }// 发出请求(GET httpbin.org 的 /get 端点)if (evhttp_make_request(evcon, req, EVHTTP_REQ_GET, "/get") != 0) {fprintf(stderr, "立即发出请求失败\n");// evcon 没有获得 req 的所有权,我们必须释放它evhttp_request_free(req);evhttp_connection_free(evcon);return -1; // 错误}printf("请求提交成功。\n");// 现在 evcon 拥有 req。回调将稍后通过事件循环调用。// 此处不要释放 req。在完成连接使用时释放 evcon(或使用 free_on_completion)。// 对于单个请求,通常最好在完成时释放连接:evhttp_connection_free_on_completion(evcon);return 0; // 成功(已排队) }
- 目的: 提交客户端请求(
-
void evhttp_cancel_request(struct evhttp_request *req);
- 目的: 尝试取消一个已通过
evhttp_make_request
提交但尚未完成的挂起客户端请求(req
)。如果成功取消,请求的完成回调通常不会被调用(但错误回调可能会以EVREQ_HTTP_REQUEST_CANCEL
被调用)。此调用会释放req
对象。如果请求正在被主动发送/接收,这可能会导致底层连接被重置。无法取消其回调已开始执行的请求。 - 场景联想: 派遣一个快速信使,在消息完全送达或收到回复之前召回信使。
- 目的: 尝试取消一个已通过
访问请求/响应数据
(主要在回调中使用的)函数,用于检查请求或响应的详细信息。
-
const char *evhttp_request_get_uri(const struct evhttp_request *req);
- 目的: 获取从客户端接收到的(服务器端)或客户端发送的(客户端,来自
evhttp_make_request
)完整请求 URI 字符串。
- 目的: 获取从客户端接收到的(服务器端)或客户端发送的(客户端,来自
-
const struct evhttp_uri *evhttp_request_get_evhttp_uri(const struct evhttp_request *req);
- 目的: 获取请求 URI 的已解析表示(
struct evhttp_uri *
)。如果你需要特定组件(主机、路径、查询),这比手动解析字符串更方便。如果解析失败或不可用,则返回NULL
。
- 目的: 获取请求 URI 的已解析表示(
-
enum evhttp_cmd_type evhttp_request_get_command(const struct evhttp_request *req);
- 目的: 获取请求的 HTTP 方法类型(
EVHTTP_REQ_GET
,EVHTTP_REQ_POST
等)。
- 目的: 获取请求的 HTTP 方法类型(
-
int evhttp_request_get_response_code(const struct evhttp_request *req);
- 目的: (客户端)获取在服务器响应中收到的 HTTP 状态码。
-
const char * evhttp_request_get_response_code_line(const struct evhttp_request *req);
- 目的: (客户端)获取从服务器收到的带有状态码的完整原因短语(例如 “OK”, “Not Found”)。
-
struct evkeyvalq *evhttp_request_get_input_headers(struct evhttp_request *req);
- 目的: 获取输入头部。
- 服务器端:从客户端在请求中收到的头部。
- 客户端:从服务器在响应中收到的头部。
- 返回值: 指向
evkeyvalq
结构(键值对队列/列表)的指针。
- 目的: 获取输入头部。
-
struct evkeyvalq *evhttp_request_get_output_headers(struct evhttp_request *req);
- 目的: 获取输出头部。
- 服务器端:要在响应中发送给客户端的头部(在
evhttp_send_reply
/_start
之前在此处添加头部)。 - 客户端:要在请求中发送给服务器的头部(在
evhttp_make_request
之前在此处添加头部)。
- 服务器端:要在响应中发送给客户端的头部(在
- 返回值: 指向
evkeyvalq
结构的指针。
- 目的: 获取输出头部。
-
struct evbuffer *evhttp_request_get_input_buffer(struct evhttp_request *req);
- 目的: 获取输入数据缓冲区。
- 服务器端:包含从客户端收到的请求体。
- 客户端:包含从服务器收到的响应体。
- 返回值: 指向
evbuffer
的指针。从此缓冲区读取数据。
- 目的: 获取输入数据缓冲区。
-
struct evbuffer *evhttp_request_get_output_buffer(struct evhttp_request *req);
- 目的: 获取输出数据缓冲区。
- 服务器端:在调用
evhttp_send_reply
之前在此处添加响应体数据(如果不使用分块)。 - 客户端:在调用
evhttp_make_request
之前在此处添加请求体数据(对于 POST, PUT 等)。
- 服务器端:在调用
- 返回值: 指向
evbuffer
的指针。向此缓冲区添加数据。
- 目的: 获取输出数据缓冲区。
-
const char *evhttp_request_get_host(struct evhttp_request *req);
- 目的: (服务器端)获取客户端请求的主机名。它优先使用客户端提供的绝对 URI 的主机部分,否则查找
Host:
头部。如果两者都不存在,则返回NULL
。
- 目的: (服务器端)获取客户端请求的主机名。它优先使用客户端提供的绝对 URI 的主机部分,否则查找
头部操作工具
用于处理 evkeyvalq
头部结构的函数。
-
const char *evhttp_find_header(const struct evkeyvalq *headers, const char *key);
- 目的: 在头部队列中搜索与
key
匹配的第一个头部(不区分大小写)并返回其值。 - 返回值: 指向值字符串的指针,如果未找到则返回
NULL
。该字符串由evkeyvalq
拥有。 - 示例:
// struct evkeyvalq *hdrs = evhttp_request_get_input_headers(req); // 获取输入头部 // const char *user_agent = evhttp_find_header(hdrs, "User-Agent"); // 查找 User-Agent // if (user_agent) { // printf("客户端 User-Agent:%s\n", user_agent); // }
- 目的: 在头部队列中搜索与
-
int evhttp_remove_header(struct evkeyvalq *headers, const char *key);
- 目的: 从队列中移除与
key
匹配的所有头部(不区分大小写)。 - 返回值: 如果至少移除了一个头部则返回 0,如果未找到该键则返回 -1。
- 示例:
// struct evkeyvalq *output_headers = evhttp_request_get_output_headers(req); // 获取输出头部 // // 移除我们之前可能添加的任何默认 "Server" 头部 // evhttp_remove_header(output_headers, "Server");
- 目的: 从队列中移除与
-
int evhttp_add_header(struct evkeyvalq *headers, const char *key, const char *value);
- 目的: 向头部队列的末尾添加一个新的头部(键值对)。Libevent 会复制键和值字符串。允许重复的键(例如,多个
Set-Cookie
头部)。 - 返回值: 成功时返回 0,失败时返回 -1(例如,内存分配失败)。
- 示例: (见上面的
handle_hello
和客户端请求示例)
- 目的: 向头部队列的末尾添加一个新的头部(键值对)。Libevent 会复制键和值字符串。允许重复的键(例如,多个
-
void evhttp_clear_headers(struct evkeyvalq *headers);
- 目的: 从队列中移除所有头部。
URI 解析和编码工具
用于处理 URI 和相关编码的辅助函数。
-
char *evhttp_encode_uri(const char *str);
- 目的: 对字符串执行 URI 百分号编码。编码除
-
,.
,_
,~
之外的大多数非字母数字字符。 - 返回值: 一个新分配的、编码后的字符串(调用者必须
free
),失败时返回NULL
。
- 目的: 对字符串执行 URI 百分号编码。编码除
-
char *evhttp_uriencode(const char *str, ev_ssize_t size, int space_to_plus);
- 目的: 更通用的 URI 编码。接受显式
size
(-1 表示空字符终止)。如果space_to_plus
为 true,空格被编码为+
(如在application/x-www-form-urlencoded
中),否则编码为%20
。 - 返回值: 新分配的编码字符串(必须
free
),或NULL
。 - 示例:
// char *query_part = "带 空 格 的 查 询"; // // 为查询字符串编码,将空格编码为 + // char *encoded = evhttp_uriencode(query_part, -1, 1); // if (encoded) { // printf("编码后:%s\n", encoded); // 输出:编码后:带+空+格+的+查+询 // free(encoded); // }
- 目的: 更通用的 URI 编码。接受显式
-
char *evhttp_decode_uri(const char *uri);
- 目的: 解码一个百分号编码的 URI 字符串。已弃用: 对
+
的行为不寻常(仅在第一个?
之后将+
解码为空格)。请改用evhttp_uridecode
。 - 返回值: 新分配的解码字符串(必须
free
),或NULL
。
- 目的: 解码一个百分号编码的 URI 字符串。已弃用: 对
-
char *evhttp_uridecode(const char *uri, int decode_plus, size_t *size_out);
- 目的: 解码百分号编码字符串的首选函数。如果
decode_plus
为 true,它还会将+
解码为空格(适用于查询字符串参数)。如果size_out
不为NULL
,它会被填充为解码后字符串的长度(对二进制数据有用)。 - 返回值: 新分配的解码字符串(必须
free
),或NULL
。 - 示例:
const char *encoded_query = "name=John+Doe&city=New%20York"; size_t decoded_len; // 解码查询字符串,将 + 解码为空格 char *decoded = evhttp_uridecode(encoded_query, 1, &decoded_len); if (decoded) {printf("解码后 (%zu 字节):%s\n", decoded_len, decoded); // 输出:解码后 (... 字节):name=John Doe&city=New Yorkfree(decoded); }
- 目的: 解码百分号编码字符串的首选函数。如果
-
int evhttp_parse_query(const char *uri, struct evkeyvalq *headers);
- 目的: 将完整 URI 的查询字符串部分解析为
headers
队列中的键值对。已弃用: 请改用evhttp_uri_parse
和evhttp_parse_query_str
以获得更健壮的解析。 - 返回值: 成功时返回 0,失败时返回 -1。
- 目的: 将完整 URI 的查询字符串部分解析为
-
int evhttp_parse_query_str(const char *query_string, struct evkeyvalq *headers);
- 目的: 仅将查询字符串部分(例如 “a=1&b=2”)解析为键值对。解码百分号编码和
+
为空格。 - 返回值: 成功时返回 0,失败时返回 -1。
- 示例:
struct evkeyvalq query_params; TAILQ_INIT(&query_params); // 初始化队列const char *query = "name=Alice&topic=Libevent%20HTTP&debug=true"; // 解析查询字符串 if (evhttp_parse_query_str(query, &query_params) == 0) {const char *name = evhttp_find_header(&query_params, "name");const char *topic = evhttp_find_header(&query_params, "topic");printf("解析的查询 - Name:%s, Topic:%s\n", name ? name : "N/A", topic ? topic : "N/A"); } else {fprintf(stderr, "解析查询字符串失败。\n"); }// 重要:清理分配的头部 evhttp_clear_headers(&query_params);
- 目的: 仅将查询字符串部分(例如 “a=1&b=2”)解析为键值对。解码百分号编码和
-
int evhttp_parse_query_str_flags(const char *uri, struct evkeyvalq *headers, unsigned flags);
- 目的: 类似于
evhttp_parse_query_str
但带有附加标志:EVHTTP_URI_QUERY_NONCONFORMANT
:容忍轻微格式错误的查询(例如a=1&b
,a=1&&b=2
)。EVHTTP_URI_QUERY_LAST_VAL
:如果一个键出现多次(例如a=1&a=2
),只保留遇到的最后一个值。默认是保留第一个。
- 返回值: 成功时返回 0,失败时返回 -1。
- 目的: 类似于
-
char *evhttp_htmlescape(const char *html);
- 目的: 将字符
< > " ' &
转义为其 HTML 实体等效项(<
,>
,"
,'
,&
),以在 HTML 输出中嵌入用户提供的文本时防止跨站脚本(XSS)攻击。 - 返回值: 新分配的转义字符串(必须
free
),或NULL
。 - 示例:
const char *user_input = "<script>alert('XSS');</script>"; char *escaped = evhttp_htmlescape(user_input); // 转义 HTML 特殊字符 if (escaped) {// struct evbuffer *output = evhttp_request_get_output_buffer(req);// // 在 HTML 中安全地输出用户评论// evbuffer_add_printf(output, "<p>用户评论:%s</p>", escaped);printf("转义后:%s\n", escaped);// 输出:转义后:<script>alert('XSS');</script>free(escaped); }
- 目的: 将字符
高级 RFC3986 URI 解析
一种更现代、更健壮的将 URI 解析为组件的方法。
-
struct evhttp_uri *evhttp_uri_new(void);
- 目的: 分配一个新的、空的
evhttp_uri
结构。 - 返回值: 指向新结构的指针,或
NULL
。
- 目的: 分配一个新的、空的
-
void evhttp_uri_free(struct evhttp_uri *uri);
- 目的: 释放一个
evhttp_uri
结构以及由evhttp_uri_parse*
分配的所有组件字符串。
- 目的: 释放一个
-
struct evhttp_uri *evhttp_uri_parse(const char *source_uri);
/struct evhttp_uri *evhttp_uri_parse_with_flags(const char *source_uri, unsigned flags);
- 目的: 根据 RFC3986 将完整的 URI-reference(绝对 URI 或相对路径)解析为其组件(scheme, authority, path, query, fragment)。不解码百分号编码的部分。
- 标志 (用于
_with_flags
):EVHTTP_URI_NONCONFORMANT
:容忍一些与严格 RFC3986 的偏差。EVHTTP_URI_HOST_STRIP_BRACKETS
:如果主机是 IPv6 字面量(例如[::1]
),evhttp_uri_get_host
将返回不带括号的地址(对getaddrinfo
有用)。evhttp_uri_join
仍然会加回它们。EVHTTP_URI_UNIX_SOCKET
:允许解析像http://unix:/path/to/socket:/resource/path
这样的 URI,其中“主机”是套接字路径。
- 返回值: 一个新分配的
evhttp_uri
结构,包含指向原始source_uri
字符串内部组件的指针(或对于某些复杂情况是新分配的副本),解析失败时返回NULL
。 - 示例:
const char *uri_string = "https://user:pass@example.com:8080/path/to/resource?q=test#fragment"; // 解析 URI 字符串 struct evhttp_uri *parsed_uri = evhttp_uri_parse(uri_string);if (parsed_uri) {printf("URI 解析结果:\n");printf(" Scheme: %s\n", evhttp_uri_get_scheme(parsed_uri)); // httpsprintf(" UserInfo: %s\n", evhttp_uri_get_userinfo(parsed_uri)); // user:passprintf(" Host: %s\n", evhttp_uri_get_host(parsed_uri)); // example.comprintf(" Port: %d\n", evhttp_uri_get_port(parsed_uri)); // 8080printf(" Path: %s\n", evhttp_uri_get_path(parsed_uri)); // /path/to/resourceprintf(" Query: %s\n", evhttp_uri_get_query(parsed_uri)); // q=test (不含 '?')printf(" Fragment: %s\n", evhttp_uri_get_fragment(parsed_uri));// fragment (不含 '#')// 记得释放!evhttp_uri_free(parsed_uri); } else {fprintf(stderr, "解析 URI 失败:%s\n", uri_string); }
-
void evhttp_uri_set_flags(struct evhttp_uri *uri, unsigned flags);
- 目的: 在一个已存在的已解析
evhttp_uri
对象上设置标志(主要影响evhttp_uri_join
和evhttp_uri_get_host
关于括号的行为)。
- 目的: 在一个已存在的已解析
-
const char *evhttp_uri_get_scheme(const struct evhttp_uri *uri);
/..._get_userinfo(...)
/..._get_host(...)
/..._get_unixsocket(...)
/..._get_path(...)
/..._get_query(...)
/..._get_fragment(...)
- 目的: 访问器函数,用于检索已解析
evhttp_uri
的各个组件。如果组件不存在,则返回NULL
。
- 目的: 访问器函数,用于检索已解析
-
int evhttp_uri_get_port(const struct evhttp_uri *uri);
- 目的: 从已解析的 URI 获取端口号。如果没有指定端口,则返回 -1。
-
int evhttp_uri_set_scheme(struct evhttp_uri *uri, const char *scheme);
/..._set_userinfo(...)
/..._set_host(...)
/..._set_unixsocket(...)
/..._set_path(...)
/..._set_query(...)
/..._set_fragment(...)
- 目的: 以编程方式设置或替换
evhttp_uri
结构的组件的函数。它们获得或复制提供的字符串。成功时返回 0,失败时返回 -1(例如,组件格式无效)。
- 目的: 以编程方式设置或替换
-
int evhttp_uri_set_port(struct evhttp_uri *uri, int port);
- 目的: 设置或清除端口号(-1 表示清除)。成功时返回 0,失败时返回 -1(例如,无效端口号)。
-
char *evhttp_uri_join(const struct evhttp_uri *uri, char *buf, size_t limit);
- 目的: 将
evhttp_uri
结构的组件重新构造成一个 URI-reference 字符串,放入提供的缓冲区buf
中(最多limit
字节)。添加必要的分隔符(://
,@
,:
,/
,?
,#
)。不对组件本身执行百分号编码。 - 返回值: 成功时返回指向
buf
的指针,失败时返回NULL
(例如,缓冲区太小)。 - 示例:
// 假设 parsed_uri 来自 evhttp_uri_parse // struct evhttp_uri *parsed_uri = evhttp_uri_parse(...); // if (parsed_uri) { // // 修改一个组件 // evhttp_uri_set_query(parsed_uri, "new_query=updated"); // 设置新的查询字符串 // // char joined_uri_buf[1024]; // 准备一个足够大的缓冲区 // // 将修改后的 URI 组件连接回字符串 // if (evhttp_uri_join(parsed_uri, joined_uri_buf, sizeof(joined_uri_buf))) { // printf("重新连接后的 URI:%s\n", joined_uri_buf); // } else { // fprintf(stderr, "连接 URI 失败(缓冲区太小?)\n"); // } // evhttp_uri_free(parsed_uri); // 释放解析后的 URI 结构 // }
- 目的: 将
结论
呼!我们已经遍历了 Libevent 的 event2/http.h
的全部内容。从设置健壮的服务器和多功能的客户端,到操作头部、处理分块传输,以及精确解析 URI,这个头文件为异步 HTTP 通信提供了一个强大的工具箱。
这里提供的示例是起点。请记住 Libevent 的异步特性:回调是你处理事件和响应的主要方式。构建复杂的应用程序通常涉及在这些回调之间管理状态,也许可以使用 void *arg
参数来传递上下文指针。
不要害怕实验!设置一个简单的回显服务器,编写一个客户端来获取网页,尝试实现分块响应,或者探索虚拟主机。真正的理解来自于编写代码并看到这些函数在实践中如何工作。Libevent 的 HTTP 模块虽然详细,但一旦掌握,就能提供显著的性能优势和灵活性。
编码愉快,愿你的 HTTP 交互快速且非阻塞!