Apache Serf 构建并运行示例应用指南
Apache Serf 1.3.10 构建并运行示例应用指南,从零编写最小 C 语言 HTTP GET 客户端:给出完整示例源码、gcc 编译链接命令、本机 python3 http.server 服务搭建、真实运行回显(HTTP 200 与响应体打印),并沉淀连接回调必填、acceptor 响应桶包装、handler 返回契约三大实测要点与段错误排障。
Apache Serf 构建并运行示例应用指南
目标:从零构建并运行一个基于 Apache Serf 1.3.10 的最小 C 语言 HTTP GET 客户端示例:
serf_context_create建上下文 →serf_connection_create2建连接 →serf_connection_request_create排队 GET 请求 →serf_context_run事件循环驱动响应处理,实测返回HTTP status: 200并完整打印响应体。所有命令与输出均在 Ubuntu 22.04.5 + GCC 11.4.0 环境实测通过。
一、环境信息
- 操作系统:Ubuntu 22.04.5 LTS
- 架构:x86_64
- 编译器:GCC 11.4.0
- Serf 版本:1.3.10(安装前缀
/usr/local/serf,含libserf-1.so.1与头文件) - APR:1.7.0;APR-util:1.6.1(
libapr1-dev/libaprutil1-dev) - 本机 HTTP 服务:python3
http.server(监听 8000 端口) - 示例目录:
/opt/(源码/opt/serf-demo.c,产物/opt/serf-demo) - 演示账号:root
二、前置条件
-
已按《Apache Serf 安装指南》篇完成 Serf 1.3.10 源码编译安装,并注册动态库:
ldconfig -p | grep serf预期回显:
libserf-1.so.1 (libc6,x86-64) => /usr/local/serf/lib/libserf-1.so.1 libserf-1.so (libc6,x86-64) => /usr/local/serf/lib/libserf-1.so -
已安装编译工具链(
build-essential libapr1-dev libaprutil1-dev libssl-dev)。 -
已安装 python3(Ubuntu 22.04 自带),用于在 8000 端口起一个本机 HTTP 服务。
三、示例工作流程
Serf 是事件驱动 + 回调风格的异步库,一次 GET 请求在程序里分五步:
1) serf_context_create 创建事件循环上下文
2) apr_uri_parse + serf_connection_create2 解析 URL 并建连接(挂 conn_setup 回调)
3) serf_connection_request_create 排队请求(挂 setup / acceptor / handler 三个请求级回调)
4) serf_context_run 事件循环:连接建立 → 发请求 → 收响应 → 逐次回调 handler
5) serf_connection_close 清理
三个回调的分工:
| 回调 | 时机 | 职责 |
|---|---|---|
| conn_setup | TCP 连接建立后 | 把 socket 包装成输入 bucket(HTTPS 再包 ssl_decrypt 层),1.3.10 必填 |
| acceptor | 收到响应字节流时 | 把裸字节流包装成响应 bucket(barrier + response) |
| handler | 响应数据处理期间 | 读状态行与响应体;读尽必须返回 APR_EOF,暂无数据返回 APR_EAGAIN |
四、准备本机 HTTP 服务
为让 GET 有目标,先在本机 8000 端口起一个静态文件服务并放置测试页面:
mkdir -p /var/www/demo
echo "<h1>Hello from Apache Serf demo</h1><p>Serf 1.3.10 async HTTP client test page.</p>" > /var/www/demo/index.html
cd /var/www/demo
nohup python3 -m http.server 8000 >/tmp/httpd.log 2>&1 &
确认服务可访问:
wget -q -O- http://127.0.0.1:8000/ | head -2
实测回显:
<h1>Hello from Apache Serf demo</h1><p>Serf 1.3.10 async HTTP client test page.</p>
五、完整示例源码
创建 /opt/serf-demo.c,内容如下(逐行实测编译运行通过):
/* serf_get_demo.c —— 基于 Apache Serf 1.3.10 的最小 HTTP GET 客户端示例 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <apr.h>
#include <apr_pools.h>
#include <apr_uri.h>
#include "serf.h"
/* 应用上下文:贯穿 setup/acceptor/handler 三个回调 */
typedef struct {
int done; /* 响应处理完成标志 */
int status_code; /* HTTP 状态码 */
char body[65536]; /* 响应体缓冲 */
size_t blen; /* 已接收字节数 */
serf_bucket_alloc_t *bkt_alloc; /* 应用级 bucket 分配器 */
} app_ctx_t;
/* conn_setup(连接级回调,1.3.10 必填):把 socket 包装成输入 bucket。
* 明文 HTTP 直接返回 socket bucket;如需 HTTPS,先在此包一层 ssl_decrypt。 */
static apr_status_t conn_setup(apr_socket_t *skt,
serf_bucket_t **input_bkt,
serf_bucket_t **output_bkt,
void *setup_baton,
apr_pool_t *pool)
{
app_ctx_t *ctx = setup_baton;
*input_bkt = serf_bucket_socket_create(skt, ctx->bkt_alloc);
return APR_SUCCESS;
}
/* acceptor:把服务器字节流包装成响应 bucket(barrier 防止响应桶读穿底层流) */
static serf_bucket_t *accept_response(serf_request_t *request,
serf_bucket_t *stream,
void *acceptor_baton,
apr_pool_t *pool)
{
serf_bucket_t *barrier;
serf_bucket_alloc_t *alloc = serf_request_get_alloc(request);
/* Create a barrier so the response doesn't eat us! */
barrier = serf_bucket_barrier_create(stream, alloc);
return serf_bucket_response_create(barrier, alloc);
}
/* handler:读取状态行 + 循环读取响应体直到 EOF */
static apr_status_t handle_response(serf_request_t *request,
serf_bucket_t *response,
void *handler_baton,
apr_pool_t *pool)
{
app_ctx_t *ctx = handler_baton;
serf_status_line sl;
apr_status_t status;
if (response == NULL) { /* 连接中断,未收到响应 */
fprintf(stderr, "[demo] connection closed without response\n");
ctx->done = 1;
return APR_SUCCESS;
}
status = serf_bucket_response_status(response, &sl);
if (status) {
return status;
}
ctx->status_code = sl.code;
while (1) {
const char *data;
apr_size_t len;
status = serf_bucket_read(response, 4096, &data, &len);
if (SERF_BUCKET_READ_ERROR(status)) {
return status;
}
if (len > 0 && ctx->blen + len < sizeof(ctx->body)) {
memcpy(ctx->body + ctx->blen, data, len);
ctx->blen += len;
ctx->body[ctx->blen] = '\0';
}
if (APR_STATUS_IS_EOF(status)) {
ctx->done = 1;
return APR_EOF; /* 契约:响应读取完成必须返回 APR_EOF,
返回 APR_SUCCESS 会被 serf 再次回调导致自旋 */
}
if (APR_STATUS_IS_EAGAIN(status)) {
return status; /* 暂无更多数据,交还事件循环 */
}
}
ctx->done = 1;
return APR_SUCCESS;
}
/* request setup:构造 GET 请求 bucket 并挂接 acceptor/handler */
static apr_status_t setup_request(serf_request_t *request,
void *setup_baton,
serf_bucket_t **req_bkt,
serf_response_acceptor_t *acceptor,
void **acceptor_baton,
serf_response_handler_t *handler,
void **handler_baton,
apr_pool_t *pool)
{
app_ctx_t *ctx = setup_baton;
serf_bucket_alloc_t *alloc = serf_request_get_alloc(request);
/* 构造 "GET / HTTP/1.1" 请求 bucket,无请求体 */
*req_bkt = serf_request_bucket_request_create(request, "GET", "/", NULL, alloc);
*acceptor = accept_response;
*acceptor_baton = ctx;
*handler = handle_response;
*handler_baton = ctx;
return APR_SUCCESS;
}
int main(int argc, char **argv)
{
apr_status_t status;
apr_pool_t *pool;
serf_context_t *ctx;
serf_connection_t *conn;
apr_uri_t host_info;
app_ctx_t app;
const char *url = (argc > 1) ? argv[1] : "http://127.0.0.1:8000";
int loops = 0;
memset(&app, 0, sizeof(app));
/* 1) 初始化 APR(Serf 依赖 APR 内存池与网络抽象) */
status = apr_initialize();
if (status != APR_SUCCESS) {
fprintf(stderr, "[demo] apr_initialize failed: %d\n", status);
return 1;
}
atexit(apr_terminate);
apr_pool_create(&pool, NULL);
/* 2) 创建 serf 上下文(事件循环核心对象)与 bucket 分配器 */
ctx = serf_context_create(pool);
app.bkt_alloc = serf_bucket_allocator_create(pool, NULL, NULL);
/* 3) 解析目标 URL 并按主机信息创建连接(host_url 完整,认证路径安全) */
status = apr_uri_parse(pool, url, &host_info);
if (status != APR_SUCCESS) {
fprintf(stderr, "[demo] apr_uri_parse failed: %d\n", status);
return 1;
}
status = serf_connection_create2(&conn, ctx, host_info,
conn_setup, &app,
NULL, NULL, pool);
if (status != APR_SUCCESS) {
fprintf(stderr, "[demo] serf_connection_create2 failed: %d\n", status);
return 1;
}
/* 4) 排队一个 GET / 请求(setup 回调在事件循环内被调用) */
serf_connection_request_create(conn, setup_request, &app);
/* 5) 事件循环:驱动请求发送与响应接收 */
while (!app.done && loops < 200) {
status = serf_context_run(ctx, 100000, pool); /* 单次最多等 100ms */
if (status && !APR_STATUS_IS_TIMEUP(status)) {
fprintf(stderr, "[demo] serf_context_run error: %d\n", status);
return 1;
}
loops++;
}
/* 6) 打印结果并清理 */
printf("HTTP status: %d\n", app.status_code);
printf("Body bytes : %zu\n", app.blen);
printf("---- body ----\n%s\n--------------\n", app.body);
serf_connection_close(conn);
apr_pool_destroy(pool);
return app.done ? 0 : 2;
}
💡 为什么用
serf_connection_create2而不是serf_connection_create:1.3.10 的认证模块在建连阶段会以conn->host_url为键查询哈希表,而serf_connection_create(sockaddr 版)不设置host_url(为 NULL),运行即段错误;serf_connection_create2按 URL 解析生成完整host_url(详见「九、常见问题」Q2)。
六、编译与链接
一条 gcc 命令完成编译链接(头文件目录、APR/APU 旗标、Serf 库路径均显式给出):
cd /opt
gcc serf-demo.c -o serf-demo \
-I/usr/local/serf/include/serf-1 \
$(apr-1-config --cflags --includes) \
$(apu-1-config --includes) \
-L/usr/local/serf/lib -lserf-1 -lapr-1 -laprutil-1
实测:无输出(编译链接成功),产物 /opt/serf-demo(约 17KB)。
链接核对(ldd):
ldd /opt/serf-demo | grep -E "serf|apr"
libserf-1.so.1 => /usr/local/serf/lib/libserf-1.so.1 (0x00007ef621ee4000)
libapr-1.so.0 => /lib/x86_64-linux-gnu/libapr-1.so.0 (0x00007ef621ea7000)
libaprutil-1.so.0 => /usr/lib/x86_64-linux-gnu/libaprutil-1.so.0 (0x00007ef621749000)
💡 参数速记:
-I指向 serf 头文件目录;apr-1-config --cflags --includes与apu-1-config --includes展开 APR/APU 头文件与编译旗标;-L/usr/local/serf/lib -lserf-1链接 Serf;-lapr-1 -laprutil-1链接 APR/APU。OpenSSL 与 zlib 已由 libserf-1.so 传递依赖,无需显式列出。
七、运行与真实输出
/opt/serf-demo http://127.0.0.1:8000
echo "demo-exit=$?"
实测完整输出:
HTTP status: 200
Body bytes : 84
---- body ----
<h1>Hello from Apache Serf demo</h1><p>Serf 1.3.10 async HTTP client test page.</p>
--------------
demo-exit=0
结果解读:
- HTTP status: 200:
handle_response中serf_bucket_response_status()解析状态行得到sl.code,python3 http.server 对存在的 index.html 返回 200。 - Body bytes : 84:事件循环内 handler 分批
serf_bucket_read()累计的响应体字节数,与 index.html 内容长度一致。 - demo-exit=0:
app.done已置位,程序正常收尾;若 20 秒内未收到完整响应则退出码为 2(循环 200 次、每次 100ms 上限)。
八、开发要点解读
⚠️ 以下三点是实测踩坑后沉淀的 1.3.10 编程契约,违背任何一点都会导致段错误或空转:
- conn_setup 必填:
serf_connection_create2的 setup 参数不能传 NULL。连接建立后 serf 直接调用conn->setup(...)生成输入桶,NULL 即空函数指针调用(栈顶#0 0x0,位于prepare_conn_streams)。- acceptor 必须包装响应桶:官方模式是
serf_bucket_barrier_create(stream, alloc)再serf_bucket_response_create(barrier, alloc)。直接返回裸流会让 handler 对非响应桶调用serf_bucket_response_status,行为未定义。- handler 返回值契约:serf 会反复调用 handler 直到其返回非 APR_SUCCESS。响应读尽(
serf_bucket_read返回 EOF)必须return APR_EOF,暂无数据返回 APR_EAGAIN;返回 APR_SUCCESS 会被立即再次回调,形成 100% CPU 空转。
💡 事件循环时长参数:
serf_context_run(ctx, duration, pool)的 duration 单位为微秒;头文件提供SERF_DURATION_NOBLOCK(0,不阻塞)与SERF_DURATION_FOREVER。示例用 100ms 超时轮询并在应用层判断app.done,便于程序体面退出。
九、常见问题(FAQ)
Q1:链接时报 undefined reference to serf_context_create 等符号错误?
-lserf-1 没找到库或顺序不对。确认 -L/usr/local/serf/lib 与 -lserf-1 成对出现且放在源文件之后(gcc 链接顺序敏感);ldconfig -p | grep serf 确认库已注册。
Q2:运行段错误 Segmentation fault(退出码 139)?
两类已实测复现的根因:
- 用了
serf_connection_create(sockaddr 版):其不设置host_url,认证模块apr_hash_get(ctx->server_authn_info, conn->host_url, ...)对 NULL 键解引用崩溃(ASan 栈:apr_hash_get ← serf__get_authn_info_for_server ← serf__open_connections ← serf_context_prerun)。改用serf_connection_create2+apr_uri_parse。 serf_connection_create2传了 NULL setup:建连后空函数指针调用(gdb 栈:prepare_conn_streams → serf__open_connections → serf_context_prerun)。按第五节实现conn_setup。
Q3:程序不退出且 CPU 100%?
handler 返回值契约被破坏:读到响应末尾后返回了 APR_SUCCESS,serf 立即再次回调形成空转(采样栈:serf__process_connection → serf_event_trigger → serf_context_run)。把 EOF 分支改为 return APR_EOF,EAGAIN 分支原样返回 status。
Q4:请求发出去了但一直收不到响应?
检查三点:本机服务是否存活(wget -q -O- http://127.0.0.1:8000/ 有输出);conn_setup 是否真的创建了 socket 桶;acceptor 是否正确包装(未包装时 handler 第一步 serf_bucket_response_status 会返回异常)。
Q5:如何改成 HTTPS?
在 conn_setup 中先用 serf_ssl_context_create 创建 SSL 上下文,把 socket 桶包上 serf_bucket_ssl_encrypt_create(输出方向)与 serf_bucket_ssl_decrypt_create(输入方向),并对 serf_ssl_context_t 设置校验回调;请求 URL 换成 https:// 即可。
十、参考链接
- Serf 官方仓库源码内官方示例:源码包
test/serf_get.c(acceptor/handler 标准写法出处) - Serf 项目主页:
https://serf.apache.org/ - 头文件 API 文档:
/usr/local/serf/include/serf-1/serf.h、serf_bucket_types.h - 安装与环境准备:见《Apache Serf 安装指南》篇;API 速查:见《Apache Serf 常用工具与 API 详解》篇