项目实战:基于回调的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。你得去改那个核心解析函数,一不小心就把别的逻辑搞坏了。而且代码越长,维护成本越高。
回调机制能解决这个问题。我们把每种请求方法的处理逻辑,抽象成一个函数指针。解析器只负责拆解文本,然后调用对应的回调。新增一种方法,只需要注册一个新的回调函数,核心代码一行都不用改。
整体架构设计
先画一张图,让你对整个结构有个直观印象。我习惯在写代码之前,先把框架想清楚。
从图上可以看得很清楚:原始文本进入解析器核心,核心拆解出请求方法,然后根据方法名去查找对应的回调函数,最后执行回调。整个过程,解析器不需要知道回调内部怎么实现,它只负责「分发」。
定义回调接口
首先,我们需要定义回调函数的类型。我习惯用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