15、HTTP协议解析与实现:HTTP请求/响应格式、HTTP状态码、简单的HTTP服务器、CGI程序

说实话,HTTP协议这东西,你天天都在用。浏览器打开网页、手机刷朋友圈、甚至你写的C程序调用API——背后都是HTTP在干活。但很多人写网络程序时,只是调个库就完事了,真让自己手写一个HTTP服务器,立马就懵了。

我个人觉得,搞网络编程的人,至少得亲手解析一次HTTP报文。为什么?因为只有你亲手拆过请求行、拼过响应头,你才能真正理解那些框架背后到底干了什么。今天咱们就把它彻底搞明白。

HTTP协议长什么样?

HTTP协议其实特别简单。它就是个文本协议,说白了就是一堆字符串按固定格式拼起来。你想想看,浏览器和服务器之间,就是通过这种纯文本的格式在交流。

HTTP请求格式

一个标准的HTTP请求,长这样:

GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
Accept: text/html

注意看,它分三部分:

  • 请求行:第一行,包含方法(GET)、路径(/index.html)、协议版本(HTTP/1.1)
  • 请求头:从第二行开始,到空行结束。每行一个键值对,用冒号分隔
  • 请求体:空行之后的内容。GET请求一般没有,POST请求才有
我踩过的坑: 解析HTTP请求时,很多人忘了处理空行。空行是请求头和请求体的分界线,没有它,你就不知道头读到哪结束。我曾经因为这个bug,排查了整整一个下午。

HTTP响应格式

服务器返回的响应,格式也差不多:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 128

<html>
<body><h1>Hello World</h1></body>
</html>

响应也分三部分:

  • 状态行:协议版本 + 状态码 + 状态描述
  • 响应头:和请求头格式一样
  • 响应体:实际返回的数据

HTTP状态码,你得记住这几个

状态码是服务器告诉客户端「这次请求结果如何」的数字。我整理了一张表,把最常用的列出来:

状态码 含义 说明
200 OK 请求成功,正常返回数据
301 Moved Permanently 资源永久重定向
302 Found 临时重定向
400 Bad Request 请求格式错误,服务器看不懂
404 Not Found 资源不存在,最常见
500 Internal Server Error 服务器内部出错
502 Bad Gateway 网关或代理服务器收到无效响应
注意: 写HTTP服务器时,状态码一定要返回正确。我曾经见过有人不管什么情况都返回200,结果客户端那边缓存全乱了。该404就404,该500就500,别糊弄。

手写一个简单的HTTP服务器

好了,理论说完了,咱们直接上代码。下面这个C程序,就是一个最简单的HTTP服务器。它监听8080端口,收到请求后返回一个静态页面。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8080
#define BUFFER_SIZE 4096

void handle_client(int client_fd) {
    char buffer[BUFFER_SIZE];
    char method[16], path[256], protocol[16];
    
    // 读取请求
    read(client_fd, buffer, sizeof(buffer) - 1);
    sscanf(buffer, "%s %s %s", method, path, protocol);
    
    printf("收到请求: %s %s\n", method, path);
    
    // 构造响应
    char response[] = 
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/html\r\n"
        "Content-Length: 68\r\n"
        "Connection: close\r\n"
        "\r\n"
        "<html><body><h1>Hello from C Server!</h1></body></html>";
    
    write(client_fd, response, strlen(response));
    close(client_fd);
}

int main() {
    int server_fd, client_fd;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    
    // 创建socket
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);
    
    // 绑定端口
    bind(server_fd, (struct sockaddr *)&address, sizeof(address));
    listen(server_fd, 3);
    
    printf("服务器启动,监听端口 %d...\n", PORT);
    
    while (1) {
        client_fd = accept(server_fd, (struct sockaddr *)&address, 
                          (socklen_t*)&addrlen);
        handle_client(client_fd);
    }
    
    close(server_fd);
    return 0;
}

这段代码虽然简单,但麻雀虽小五脏俱全。它展示了HTTP服务器的核心流程:

  1. 创建socket,绑定端口,开始监听
  2. 接受客户端连接
  3. 读取HTTP请求,解析请求行
  4. 构造HTTP响应,写回客户端
  5. 关闭连接
核心要点: 注意响应头里的Content-Length必须和响应体实际长度一致。长度不对,浏览器就会一直等,或者显示不全。我刚开始写的时候,经常忘了更新这个值,结果页面总是加载到一半就卡住了。

CGI程序:让服务器动态起来

上面那个服务器只能返回静态页面。但实际应用中,我们需要根据用户请求动态生成内容。这时候就要用到CGI(Common Gateway Interface,通用网关接口)。

CGI的原理其实很简单:服务器收到请求后,把请求数据通过环境变量传给一个外部程序,然后读取这个程序的输出,作为响应返回给客户端。

下面是一个简单的CGI程序,用C语言写的。它读取环境变量QUERY_STRING,然后生成一个动态页面:

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

int main() {
    char *query = getenv("QUERY_STRING");
    char name[256] = "World";
    
    if (query != NULL && strncmp(query, "name=", 5) == 0) {
        strncpy(name, query + 5, sizeof(name) - 1);
    }
    
    printf("Content-Type: text/html\r\n\r\n");
    printf("<html><body>");
    printf("<h1>Hello, %s!</h1>", name);
    printf("<p>这是CGI程序生成的动态页面</p>");
    printf("</body></html>");
    
    return 0;
}

编译后放到服务器的cgi-bin目录下,访问 http://localhost:8080/cgi-bin/hello.cgi?name=Alice,就能看到动态生成的页面了。

经验之谈: CGI虽然简单,但性能不行——每个请求都要启动一个新进程。我在实际项目中,一般用FastCGI或者直接内嵌处理逻辑。但学习阶段,CGI是理解「服务器如何与外部程序交互」的最佳入门方式。

HTTP协议解析的核心流程

最后,我用一张图来总结HTTP协议解析的核心流程。这张图展示了一个HTTP请求从接收到响应的完整生命周期:

HTTP请求处理流程 1. 接收连接 accept() 2. 读取请求 read() 3. 解析请求行 sscanf() 4. 解析请求头 逐行读取 5. 处理请求 静态/动态 6. 构造响应 状态行+头+体 7. 发送响应 write() 8. 关闭连接 close() 等待下一个请求 循环 循环处理下一个连接

这张图把整个流程串起来了。你仔细看,从接收连接到关闭连接,每一步都有明确的函数调用。写HTTP服务器,本质上就是把这个流程用代码实现一遍。

总结一下: HTTP协议没那么神秘。它就是一套约定好的文本格式。你只要会解析字符串、会拼字符串,就能写出一个能用的HTTP服务器。当然,生产环境要考虑并发、性能、安全性,但那是后话了。先把基础打牢,后面的事都好办。

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