50、HTTP协议解析:HTTP请求行/头部/体解析、HTTP响应构造
说实话,HTTP协议这东西,你天天用,但真要你手写解析器,很多人就懵了。我记得刚入行那会儿,总觉得HTTP很简单——不就是GET、POST嘛。直到有一次我需要为一个嵌入式设备写一个极简的HTTP客户端,才发现里面门道不少。
今天我们就来聊聊,怎么用C语言把HTTP请求拆开看明白,再自己动手拼一个响应回去。
HTTP协议长什么样?
先看一个最普通的HTTP请求:
GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
Accept: text/html
嗯,结构其实很简单,就三部分:
- 请求行:方法 + 空格 + URI + 空格 + 协议版本 + 回车换行
- 请求头部:一堆“键: 值”对,每行一个,最后以一个空行结束
- 请求体:空行之后的内容,GET请求一般没有,POST请求才有
响应也是类似的套路:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 13
Hello, World!
说白了,就是状态行 + 头部 + 空行 + 体。
手写一个HTTP请求解析器
我个人习惯,解析HTTP请求时,先按行读,再按空格或冒号切分。这样逻辑清晰,不容易出错。
来看一个简单的解析函数:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct {
char method[8];
char uri[256];
char version[16];
char headers[64][2][256];
int header_count;
char body[1024];
} http_request;
int parse_http_request(const char *raw, http_request *req) {
char line[1024];
int pos = 0, line_idx = 0;
// 解析请求行
sscanf(raw, "%s %s %s", req->method, req->uri, req->version);
// 跳过请求行
while (raw[pos] != '\n') pos++;
pos++; // 跳过 \n
// 解析头部
req->header_count = 0;
while (1) {
int j = 0;
while (raw[pos] != '\n' && raw[pos] != '\0') {
line[j++] = raw[pos++];
}
line[j] = '\0';
pos++; // 跳过 \n
if (strlen(line) == 0) break; // 空行,头部结束
char *colon = strchr(line, ':');
if (colon) {
*colon = '\0';
strcpy(req->headers[req->header_count][0], line);
strcpy(req->headers[req->header_count][1], colon + 2);
req->header_count++;
}
}
// 解析体
strcpy(req->body, raw + pos);
return 0;
}
这段代码我故意写得比较“直白”,方便你理解。实际项目中,我会用状态机来解析,效率更高。但作为教学,这样一步步来,你更容易看懂。
构造HTTP响应
构造响应比解析简单多了。说白了就是拼字符串。但要注意一点:Content-Length 必须准确。我曾经遇到过一个坑,响应体里有个中文字符,长度算错了,浏览器一直转圈不显示。
来看一个构造响应的例子:
void build_http_response(const char *body, const char *content_type, char *out, int out_size) {
char header[512];
int body_len = strlen(body);
snprintf(header, sizeof(header),
"HTTP/1.1 200 OK\r\n"
"Content-Type: %s\r\n"
"Content-Length: %d\r\n"
"Connection: close\r\n"
"\r\n",
content_type, body_len);
// 检查缓冲区是否够用
if (strlen(header) + body_len >= out_size) {
fprintf(stderr, "缓冲区太小了!\n");
return;
}
strcpy(out, header);
strcat(out, body);
}
嗯,这里有个细节:Content-Length 是字节数,不是字符数。如果你返回的是二进制数据,记得用 sizeof 而不是 strlen。
一个完整的解析+响应示例
把上面两段合起来,就是一个简单的HTTP服务器核心逻辑:
void handle_client(const char *raw_request, char *response, int resp_size) {
http_request req;
parse_http_request(raw_request, &req);
printf("收到请求: %s %s\n", req.method, req.uri);
// 根据URI返回不同内容
if (strcmp(req.uri, "/hello") == 0) {
build_http_response("<h1>Hello, World!</h1>", "text/html", response, resp_size);
} else if (strcmp(req.uri, "/data") == 0) {
build_http_response("{\"status\":\"ok\"}", "application/json", response, resp_size);
} else {
build_http_response("<h1>404 Not Found</h1>", "text/html", response, resp_size);
}
}
strcat 拼响应。我见过有人因为缓冲区溢出导致服务器被攻击。建议用 snprintf 或者自己维护一个缓冲区指针。
HTTP协议解析的核心流程
为了让你更直观地理解整个过程,我画了一张流程图:
这个流程看起来简单,但实际开发中,你可能会遇到各种“不守规矩”的HTTP请求。比如:
- 头部键名大小写混用(
content-typevsContent-Type) - 请求行末尾没有回车换行
- Content-Length 和实际体长度不一致
核心要点:
- 解析时不要假设输入是规范的,做好容错
- Content-Length 必须精确计算,多一个字节少一个字节都不行
- 空行是头部和体的分界线,这个不能搞错
- 构造响应时,先写头部再写体,顺序不能乱
避坑指南
我曾经在做一个物联网项目时,设备上报的数据里包含二进制内容。我直接用 strlen 算长度,结果数据里有个 \0 字节,长度直接算错了。从那以后,我处理HTTP体时一律用 Content-Length 指定的长度,绝不依赖字符串函数。
还有一个坑:分块传输编码。有些服务器返回数据时不用 Content-Length,而是用 Transfer-Encoding: chunked。这种情况下,你需要按块读取,每个块前面有长度标识。这个我们后面章节会细讲。
总结一下
HTTP协议解析,说白了就是“按格式拆,按格式拼”。你只要记住:
- 请求行三个字段:方法、URI、版本
- 头部是键值对,空行结束
- 体是原始数据,长度由 Content-Length 决定
- 响应和请求结构一样,只是第一行变成了状态行
掌握了这些,你就能自己写一个简单的HTTP服务器了。嗯,下一章我们聊聊更高级的话题——不过那是后话了。
公众号:蓝海资料掘金营,微信deep3321