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协议解析的核心流程

为了让你更直观地理解整个过程,我画了一张流程图:

收到原始数据 解析请求行(方法 + URI + 版本) 逐行解析头部(键: 值) 遇到空行? 读取剩余数据作为请求体 否,继续 构造HTTP响应并发送

这个流程看起来简单,但实际开发中,你可能会遇到各种“不守规矩”的HTTP请求。比如:

  • 头部键名大小写混用(content-type vs Content-Type
  • 请求行末尾没有回车换行
  • Content-Length 和实际体长度不一致

核心要点:

  • 解析时不要假设输入是规范的,做好容错
  • Content-Length 必须精确计算,多一个字节少一个字节都不行
  • 空行是头部和体的分界线,这个不能搞错
  • 构造响应时,先写头部再写体,顺序不能乱

避坑指南

我曾经在做一个物联网项目时,设备上报的数据里包含二进制内容。我直接用 strlen 算长度,结果数据里有个 \0 字节,长度直接算错了。从那以后,我处理HTTP体时一律用 Content-Length 指定的长度,绝不依赖字符串函数。

还有一个坑:分块传输编码。有些服务器返回数据时不用 Content-Length,而是用 Transfer-Encoding: chunked。这种情况下,你需要按块读取,每个块前面有长度标识。这个我们后面章节会细讲。

总结一下

HTTP协议解析,说白了就是“按格式拆,按格式拼”。你只要记住:

  1. 请求行三个字段:方法、URI、版本
  2. 头部是键值对,空行结束
  3. 体是原始数据,长度由 Content-Length 决定
  4. 响应和请求结构一样,只是第一行变成了状态行

掌握了这些,你就能自己写一个简单的HTTP服务器了。嗯,下一章我们聊聊更高级的话题——不过那是后话了。


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