20250428_080643
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);
base
: 一个可选的 event_base
,用于与此服务器关联。如果提供,服务器的事件(如接受连接)将由这个事件循环管理。如果为 NULL
,可能会使用一个默认的内部 event_base
,但通常建议提供你自己的以获得更好的控制。evhttp
结构的指针,失败时返回 NULL
。evhttp
)和一个引擎(event_base
)来驱动其中的事件。#include
#include
#include
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); // 清理 base
return 1;
}
printf("evhttp 服务器创建成功!\n");
// ... 服务器配置和绑定 ...
evhttp_free(http); // 稍后清理 http
event_base_free(base); // 稍后清理 base
return 0;
}
int evhttp_bind_socket(struct evhttp *http, const char *address, ev_uint16_t port);
http
: evhttp
服务器实例。address
: 要绑定的 IP 地址字符串(例如,“0.0.0.0” 表示所有接口,“127.0.0.1” 表示本地主机)。port
: 要监听的端口号。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
)上接受连接。这在诸如从父进程继承套接字或权限阻止直接绑定端口的场景中很有用。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 // SSL 示例需要
#include
// 创建 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 // 需要 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);
// 允许最多 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);
|
)组合。// 仅允许 GET 和 POST 请求
evhttp_set_allowed_methods(http, EVHTTP_REQ_GET | EVHTTP_REQ_POST);
void evhttp_set_ext_method_cmp(struct evhttp *http, evhttp_ext_method_cb cmp);
cmp
)将方法字符串(如 “COPY”, “MOVE”)映射到内部的 evhttp_cmd_type
值和标志(如 EVHTTP_METHOD_HAS_BODY
)。这允许 Libevent 正确解析带有这些方法的请求。你仍然需要使用 evhttp_set_allowed_methods
允许相应的 type
。#include // 需要 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);
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
,服务器将尝试读取(并丢弃)整个超大主体,然后再发送错误响应并关闭连接。这对某些客户端可能更友好,但会消耗更多资源。没有此标志,连接可能在检测到超大时立即关闭。// 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
的任意指针参数。path
),配备专门的工作人员(cb
),以处理不同类型的客人问询(requests
)。#include
#include // 需要 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 *);
// 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, "%d %s ", error, reason);
evbuffer_add_printf(buffer, "哎呀!错误 %d
", error);
evbuffer_add_printf(buffer, "出错了:%s
", reason);
if (custom_message) {
evbuffer_add_printf(buffer, "%s
", custom_message);
}
// 确保内容足够大以避免 IE 覆盖它(>= 512 字节)
while (evbuffer_get_length(buffer) < 512) {
evbuffer_add(buffer, " ", 1);
}
evbuffer_add_printf(buffer, "");
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:
头部匹配 glob pattern
(不区分大小写,例如 “*.example.com”, “api.example.org”)的请求将被路由到 vhost
实例上定义的处理程序,而不是主 http
实例。vhost
不应拥有自己的监听套接字;它只定义请求处理程序。主 http
服务器管理 vhost
的生命周期。// 为 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
。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:
头部值识别,而无需为简单的别名设置完整的虚拟主机。// 让主服务器也响应 "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);
// 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);
errorcb
,则会使用它来代替。req
: 请求对象。error
: HTTP 状态码(例如 HTTP_NOTFOUND
, HTTP_INTERNAL
)。reason
: 简短的、人类可读的解释(例如 “Not Found”, “Internal Server Error”)。如果为 NULL
,则使用该代码的标准原因短语。handle_generic
示例)void evhttp_send_reply(struct evhttp_request *req, int code, const char *reason, struct evbuffer *databuf);
databuf
中的响应主体。Libevent 会负责处理 Content-Length
(除非使用分块编码)。重要: Libevent 在发送时会排空 databuf
;缓冲区本身仍然由调用者拥有,如果必要,在调用后应由调用者释放。req
: 请求对象。code
: HTTP 状态码(例如 HTTP_OK
)。reason
: 原因短语(例如 “OK”)。databuf
: 包含响应主体的 evbuffer
。对于没有主体的响应(如 204 No Content),可以为 NULL
。handle_hello
示例)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 处理分块帧(\r\n\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
。#include
#include
#include
// ... 假设 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);
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");
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);
AF_INET
)、IPv6 (AF_INET6
),还是允许两者 (AF_UNSPEC
- 默认)。// // 为此连接偏好 IPv4
// evhttp_connection_set_family(evcon, AF_INET);
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
:与服务器标志类似,可能在出错时等待服务器数据,然后再完全关闭。// // 设置在写错误时尝试读取
// 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);
evhttp_connection
。对于不需要持久连接的“一次性”连接很有用。调用此函数后,你不应手动释放 evcon
。void evhttp_connection_set_local_address(struct evhttp_connection *evcon, const char *address);
// // 强制连接源自本地 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);
timeval
)。// struct timeval connect_tv = {5, 0}; // 5 秒连接超时
// evhttp_connection_set_connect_timeout_tv(evcon, &connect_tv);
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);
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);
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);
// 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
#include
// 客户端请求完成的回调函数
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-Type
const char *clen = evhttp_find_header(headers, "Content-Length"); // 查找 Content-Length
printf(" 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);
evhttp_request
对象的生命周期所有权。Libevent 在请求完成后(例如,服务器处理程序返回后,或客户端回调完成后)不会自动释放它。应用程序必须稍后调用 evhttp_request_free(req)
。主要用于需要在发送最终回复之前执行异步操作的服务器处理程序。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”)。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);
struct evhttp_uri *
)。如果你需要特定组件(主机、路径、查询),这比手动解析字符串更方便。如果解析失败或不可用,则返回 NULL
。enum evhttp_cmd_type evhttp_request_get_command(const struct evhttp_request *req);
EVHTTP_REQ_GET
, EVHTTP_REQ_POST
等)。int evhttp_request_get_response_code(const struct evhttp_request *req);
const char * evhttp_request_get_response_code_line(const struct evhttp_request *req);
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);
Host:
头部。如果两者都不存在,则返回 NULL
。用于处理 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
匹配的所有头部(不区分大小写)。// 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);
Set-Cookie
头部)。handle_hello
和客户端请求示例)void evhttp_clear_headers(struct evkeyvalq *headers);
用于处理 URI 和相关编码的辅助函数。
char *evhttp_encode_uri(const char *str);
-
, .
, _
, ~
之外的大多数非字母数字字符。free
),失败时返回 NULL
。char *evhttp_uriencode(const char *str, ev_ssize_t size, int space_to_plus);
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);
// }
char *evhttp_decode_uri(const char *uri);
+
的行为不寻常(仅在第一个 ?
之后将 +
解码为空格)。请改用 evhttp_uridecode
。free
),或 NULL
。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 York
free(decoded);
}
int evhttp_parse_query(const char *uri, struct evkeyvalq *headers);
headers
队列中的键值对。已弃用: 请改用 evhttp_uri_parse
和 evhttp_parse_query_str
以获得更健壮的解析。int evhttp_parse_query_str(const char *query_string, struct evkeyvalq *headers);
+
为空格。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);
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
),只保留遇到的最后一个值。默认是保留第一个。char *evhttp_htmlescape(const char *html);
< > " ' &
转义为其 HTML 实体等效项(<
, >
, "
, '
, &
),以在 HTML 输出中嵌入用户提供的文本时防止跨站脚本(XSS)攻击。free
),或 NULL
。const char *user_input = "";
char *escaped = evhttp_htmlescape(user_input); // 转义 HTML 特殊字符
if (escaped) {
// struct evbuffer *output = evhttp_request_get_output_buffer(req);
// // 在 HTML 中安全地输出用户评论
// evbuffer_add_printf(output, "用户评论:%s
", escaped);
printf("转义后:%s\n", escaped);
// 输出:转义后:<script>alert('XSS');</script>
free(escaped);
}
一种更现代、更健壮的将 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);
_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:[email protected]: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)); // https
printf(" UserInfo: %s\n", evhttp_uri_get_userinfo(parsed_uri)); // user:pass
printf(" Host: %s\n", evhttp_uri_get_host(parsed_uri)); // example.com
printf(" Port: %d\n", evhttp_uri_get_port(parsed_uri)); // 8080
printf(" Path: %s\n", evhttp_uri_get_path(parsed_uri)); // /path/to/resource
printf(" 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);
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);
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 交互快速且非阻塞!