Apache Serf 构建并运行示例应用指南

Apache Serf 1.3.10 构建并运行示例应用指南,从零编写最小 C 语言 HTTP GET 客户端:给出完整示例源码、gcc 编译链接命令、本机 python3 http.server 服务搭建、真实运行回显(HTTP 200 与响应体打印),并沉淀连接回调必填、acceptor 响应桶包装、handler 返回契约三大实测要点与段错误排障。

Apache SerfApache Serf2026/10/12

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_setupTCP 连接建立后把 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 编程契约,违背任何一点都会导致段错误或空转:

  1. conn_setup 必填:serf_connection_create2 的 setup 参数不能传 NULL。连接建立后 serf 直接调用 conn->setup(...) 生成输入桶,NULL 即空函数指针调用(栈顶 #0 0x0,位于 prepare_conn_streams)。
  2. acceptor 必须包装响应桶:官方模式是 serf_bucket_barrier_create(stream, alloc) 再 serf_bucket_response_create(barrier, alloc)。直接返回裸流会让 handler 对非响应桶调用 serf_bucket_response_status,行为未定义。
  3. 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 详解》篇