项目实战:基于回调的HTTP请求解析器

好了,到了这一章,咱们要把前面学的函数指针和回调机制,真正用到一个实际项目里。我个人觉得,这是整个课程里最有意思的部分之一。你想想看,学了一堆语法,最后能做出一个能跑的东西,那种成就感是不一样的。

HTTP请求解析,说白了就是把你浏览器发出去的那些文本,拆成计算机能理解的结构体。比如请求方法、路径、头部字段、消息体等等。我当年刚入行时,第一个任务就是写一个嵌入式Web服务器,那时候对HTTP协议一知半解,踩了不少坑。今天咱们就用回调的方式,实现一个轻量级的解析器。

为什么用回调?

传统的解析器,通常是一个大循环,里面塞满了if-else。比如:

if (strncmp(line, "GET", 3) == 0) {
    // 处理GET
} else if (strncmp(line, "POST", 4) == 0) {
    // 处理POST
} else if (strncmp(line, "PUT", 3) == 0) {
    // 处理PUT
}
// ... 几十个else if

这种写法有什么问题?我遇到过最头疼的情况:客户突然说要支持一个新方法,比如PATCH。你得去改那个核心解析函数,一不小心就把别的逻辑搞坏了。而且代码越长,维护成本越高。

回调机制能解决这个问题。我们把每种请求方法的处理逻辑,抽象成一个函数指针。解析器只负责拆解文本,然后调用对应的回调。新增一种方法,只需要注册一个新的回调函数,核心代码一行都不用改。

核心思想: 解析器只做「拆解」,不做「处理」。处理逻辑由外部通过回调注入。

整体架构设计

先画一张图,让你对整个结构有个直观印象。我习惯在写代码之前,先把框架想清楚。

基于回调的HTTP请求解析器架构 原始HTTP请求文本 解析器核心 拆解方法、路径、头部、消息体 回调函数:处理GET 用户注册 回调函数:处理POST 用户注册 更多回调... 可扩展 解析完成,响应返回

从图上可以看得很清楚:原始文本进入解析器核心,核心拆解出请求方法,然后根据方法名去查找对应的回调函数,最后执行回调。整个过程,解析器不需要知道回调内部怎么实现,它只负责「分发」。

定义回调接口

首先,我们需要定义回调函数的类型。我习惯用typedef,这样写起来更清爽。

// 回调函数类型定义
// 参数:请求路径、头部链表、消息体、用户自定义数据
typedef int (*http_method_handler_t)(
    const char *path,
    struct http_header *headers,
    const char *body,
    void *user_data
);

// 注册结构体
typedef struct {
    const char *method;                // 方法名,如 "GET"
    http_method_handler_t handler;     // 对应的回调函数
    void *user_data;                   // 用户自定义数据,透传给回调
} http_method_entry_t;

这里有个细节:user_data 参数。为什么要有它?我早期写回调时,经常遇到一个问题——回调函数里需要访问一些外部状态,比如数据库连接、配置信息等。如果没有这个参数,你只能靠全局变量,那代码就乱套了。所以,留一个 void * 的透传参数,是很有必要的。

解析器核心实现

解析器的核心函数,负责拆解HTTP请求文本。我们只实现最核心的部分,不追求完整HTTP协议支持,但思路是一样的。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define MAX_METHODS 32
#define MAX_HEADERS 64

// 头部链表节点
struct http_header {
    char *name;
    char *value;
    struct http_header *next;
};

// 解析器上下文
typedef struct {
    http_method_entry_t methods[MAX_METHODS];
    int method_count;
} http_parser_t;

// 初始化解析器
void http_parser_init(http_parser_t *parser) {
    parser->method_count = 0;
    memset(parser->methods, 0, sizeof(parser->methods));
}

// 注册回调
int http_parser_register(http_parser_t *parser, 
                         const char *method,
                         http_method_handler_t handler,
                         void *user_data) {
    if (parser->method_count >= MAX_METHODS) {
        return -1;  // 满了
    }
    int idx = parser->method_count++;
    parser->methods[idx].method = strdup(method);
    parser->methods[idx].handler = handler;
    parser->methods[idx].user_data = user_data;
    return 0;
}

// 查找回调
http_method_handler_t find_handler(http_parser_t *parser,
                                   const char *method,
                                   void **user_data) {
    for (int i = 0; i < parser->method_count; i++) {
        if (strcmp(parser->methods[i].method, method) == 0) {
            *user_data = parser->methods[i].user_data;
            return parser->methods[i].handler;
        }
    }
    return NULL;  // 没找到
}
小技巧: 注册回调时用 strdup 复制字符串,避免外部字符串被释放后指针悬空。但记得在清理时 free 掉。

解析请求行

HTTP请求的第一行长这样:GET /index.html HTTP/1.1。我们需要从中提取出方法和路径。

// 解析请求行,返回方法名和路径
// 返回值:0成功,-1失败
int parse_request_line(const char *line, 
                       char *method, int method_size,
                       char *path, int path_size) {
    // 跳过前导空白
    while (*line == ' ' || *line == '\t') line++;
    
    // 提取方法
    const char *start = line;
    while (*line && *line != ' ' && *line != '\t') line++;
    int len = line - start;
    if (len >= method_size) len = method_size - 1;
    strncpy(method, start, len);
    method[len] = '\0';
    
    // 跳过空白
    while (*line == ' ' || *line == '\t') line++;
    
    // 提取路径
    start = line;
    while (*line && *line != ' ' && *line != '\t') line++;
    len = line - start;
    if (len >= path_size) len = path_size - 1;
    strncpy(path, start, len);
    path[len] = '\0';
    
    return 0;
}

嗯,这里要注意:实际HTTP协议中,路径后面可能还有查询参数(?key=value),我们这里简化处理了。你在实际项目中,可以根据需要再扩展。

主解析函数

现在把上面这些串起来,实现主解析函数。

// 主解析函数
// 输入:raw_request - 完整的HTTP请求文本
// 返回:0成功,-1失败
int http_parse(http_parser_t *parser, const char *raw_request) {
    char method[32], path[256];
    struct http_header *headers = NULL;
    const char *body = NULL;
    
    // 用 strtok 按行分割
    char *request_copy = strdup(raw_request);
    char *line = strtok(request_copy, "\r\n");
    if (!line) {
        free(request_copy);
        return -1;
    }
    
    // 解析请求行
    if (parse_request_line(line, method, sizeof(method), 
                           path, sizeof(path)) != 0) {
        free(request_copy);
        return -1;
    }
    
    // 解析头部(简化:只演示思路)
    // 实际项目中需要循环解析每一行,直到空行
    // 这里省略头部解析细节
    
    // 查找消息体(空行之后的内容)
    const char *body_start = strstr(raw_request, "\r\n\r\n");
    if (body_start) {
        body = body_start + 4;  // 跳过空行
    }
    
    // 查找并调用回调
    void *user_data = NULL;
    http_method_handler_t handler = find_handler(parser, method, &user_data);
    
    if (handler) {
        // 调用回调,把解析结果传进去
        int ret = handler(path, headers, body, user_data);
        free(request_copy);
        // 这里应该释放头部链表,省略
        return ret;
    } else {
        // 没有注册对应方法的回调
        printf("未支持的方法: %s\n", method);
        free(request_copy);
        return -1;
    }
}
注意: 上面的代码为了演示清晰,省略了头部链表的内存管理。实际项目中,解析完头部后一定要释放链表,否则内存泄漏。我曾经在一个嵌入式项目里吃过这个亏——连续处理几千个请求后,内存直接爆了。

使用示例

来看看怎么用这个解析器。假设我们要处理GET和POST请求。

// 用户自定义数据
typedef struct {
    int server_id;
    const char *root_dir;
} server_config_t;

// GET请求的回调
int on_get_request(const char *path, 
                   struct http_header *headers,
                   const char *body,
                   void *user_data) {
    server_config_t *config = (server_config_t *)user_data;
    printf("[服务器%d] 收到GET请求: %s\n", config->server_id, path);
    printf("根目录: %s\n", config->root_dir);
    // 这里可以读取文件、返回内容...
    return 0;
}

// POST请求的回调
int on_post_request(const char *path,
                    struct http_header *headers,
                    const char *body,
                    void *user_data) {
    server_config_t *config = (server_config_t *)user_data;
    printf("[服务器%d] 收到POST请求: %s\n", config->server_id, path);
    printf("消息体: %s\n", body ? body : "(空)");
    // 这里可以处理表单数据...
    return 0;
}

int main() {
    http_parser_t parser;
    http_parser_init(&parser);
    
    // 配置信息
    server_config_t config = {.server_id = 1, .root_dir = "/var/www"};
    
    // 注册回调
    http_parser_register(&parser, "GET", on_get_request, &config);
    http_parser_register(&parser, "POST", on_post_request, &config);
    
    // 模拟一个HTTP请求
    const char *request = 
        "GET /index.html HTTP/1.1\r\n"
        "Host: example.com\r\n"
        "User-Agent: Mozilla\r\n"
        "\r\n";
    
    http_parse(&parser, request);
    
    // 再模拟一个POST请求
    const char *post_request = 
        "POST /api/login HTTP/1.1\r\n"
        "Host: example.com\r\n"
        "Content-Type: application/json\r\n"
        "\r\n"
        "{\"user\":\"admin\",\"pass\":\"123\"}";
    
    http_parse(&parser, post_request);
    
    return 0;
}

运行结果:

[服务器1] 收到GET请求: /index.html
根目录: /var/www
[服务器1] 收到POST请求: /api/login
消息体: {"user":"admin","pass":"123"}

扩展性演示

现在,如果客户说要支持DELETE方法,我们只需要新增一个回调函数,然后注册一下:

int on_delete_request(const char *path,
                      struct http_header *headers,
                      const char *body,
                      void *user_data) {
    printf("收到DELETE请求: %s\n", path);
    // 处理删除逻辑
    return 0;
}

// 在main函数中加一行:
http_parser_register(&parser, "DELETE", on_delete_request, &config);

看到了吗?核心解析函数 http_parse 一行代码都没改。这就是回调的魅力——开闭原则:对扩展开放,对修改关闭。

避坑指南

最后,分享几个我踩过的坑:

  • 回调函数不要阻塞太久。 我曾经在一个回调里做了文件I/O,结果整个解析器卡住了。回调应该尽快返回,耗时操作放到线程池里去做。
  • 注意线程安全。 如果解析器在多线程环境下使用,注册回调时最好加锁,或者一次性注册完再启动服务。
  • user_data 的生命周期。 确保 user_data 指向的内存在回调执行期间是有效的。我见过有人传了栈上变量的地址,函数返回后指针就悬空了。
  • 回调的返回值要有意义。 定义清晰的错误码,让解析器知道回调执行是否成功,以便决定是否继续处理或返回错误响应。

好了,这一章的内容就到这里。你可以在自己的项目里试试这个模式,把那些臃肿的if-else替换成回调,代码会清爽很多。


公众号:蓝海资料掘金营,微信deep3321