第24章 HTTP协议解析:请求与响应格式、基于Socket的HTTP客户端实现

说到网络编程,HTTP协议绝对是绕不开的话题。我刚开始做嵌入式网络开发那会儿,总觉得HTTP是上层应用的事,跟我写C代码没关系。直到有一次要为一个物联网设备写固件升级模块,才发现——嗯,不懂HTTP还真不行。

说白了,HTTP就是客户端和服务器之间约定好的一种对话方式。你问一句,它答一句。问有问的格式,答有答的规矩。今天我们就把它拆开来看。

24.1 HTTP请求长什么样?

一个标准的HTTP请求,由三部分组成:请求行请求头请求体。我习惯把它想象成一张快递单——收件地址、附加说明、包裹内容。

请求行:方法 + 路径 + 协议版本
比如:GET /index.html HTTP/1.1

请求头是键值对,一行一个。常见的像:

  • Host:目标主机名(HTTP/1.1 强制要求)
  • User-Agent:客户端标识
  • Accept:客户端能接收的内容类型
  • Connection:是否保持连接

请求体不是必须的。GET请求就没有,POST请求才有。你想想看,你只是去服务器拿个页面,带什么包裹呢?

24.2 HTTP响应又是什么结构?

响应和请求是对称的。它也有三部分:状态行响应头响应体

状态行:协议版本 + 状态码 + 状态描述
比如:HTTP/1.1 200 OK

状态码是重点。我整理了一个速查表:

状态码 含义 我的经验
200 OK,成功 最常见,看到它就放心了
301/302 重定向 我曾经被这个坑过,没处理重定向导致一直拿不到数据
400 请求格式错误 多半是你拼错了请求行
404 资源不存在 路径写错了,检查一下
500 服务器内部错误 不是你的问题,是服务器炸了

响应头里有个字段特别重要——Content-Length。它告诉你响应体有多长。为什么重要?因为TCP是流式传输,你不知道什么时候读完。有了这个长度,你就知道该读多少字节。

24.3 基于Socket的HTTP客户端实现

理论说完了,咱们动手写代码。用Socket实现HTTP客户端,说白了就是三步:

  1. 建立TCP连接(socket + connect)
  2. 发送HTTP请求(send/write)
  3. 接收HTTP响应(recv/read)

听起来简单吧?但细节里全是坑。我一个个说。

24.3.1 建立连接

先解析域名,拿到IP地址。这里用getaddrinfo,比老式的gethostbyname更安全。我个人习惯用这个:

struct addrinfo hints, *res;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;

int ret = getaddrinfo("example.com", "80", &hints, &res);
if (ret != 0) {
    fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
    return -1;
}

int sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
connect(sockfd, res->ai_addr, res->ai_addrlen);
freeaddrinfo(res);
小提示:记得调用 freeaddrinfo 释放内存。我见过有人忘了这个,程序跑几天内存就爆了。

24.3.2 构造并发送请求

请求的格式必须严格遵循HTTP规范。行尾是\r\n,不是\n。我曾经在这个细节上栽过跟头——在Linux上写\n没问题,但换到某个嵌入式系统上,服务器死活不认。

char request[1024];
snprintf(request, sizeof(request),
    "GET / HTTP/1.1\r\n"
    "Host: example.com\r\n"
    "Connection: close\r\n"
    "\r\n");

send(sockfd, request, strlen(request), 0);

注意最后那个空行——\r\n\r\n,它表示请求头结束。没有它,服务器会一直等你的请求头。

24.3.3 接收响应

接收响应是最容易出问题的环节。为什么?因为TCP是流,你不知道一次recv能收到多少数据。可能一次收完,也可能分好几次。

我的做法是循环接收,直到连接关闭:

char buf[4096];
int bytes;
while ((bytes = recv(sockfd, buf, sizeof(buf) - 1, 0)) > 0) {
    buf[bytes] = '\0';
    printf("%s", buf);
}

这里有个细节:Connection: close。我在请求头里加了这一行,告诉服务器:你发完数据就断开连接吧。这样我就能通过recv返回0来判断数据结束。

注意:如果服务器使用Connection: keep-alive,连接不会自动关闭。这时候你必须解析Content-LengthTransfer-Encoding: chunked来精确控制读取长度。否则程序会一直阻塞在recv上。

24.4 完整示例:一个简单的HTTP客户端

把上面的代码拼起来,就是一个能用的HTTP客户端了。我写了一个精简版,只保留核心逻辑:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netdb.h>
#include <unistd.h>

int main() {
    const char *host = "example.com";
    const char *port = "80";

    // 1. 解析域名
    struct addrinfo hints, *res;
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;

    int ret = getaddrinfo(host, port, &hints, &res);
    if (ret != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
        return 1;
    }

    // 2. 创建socket并连接
    int sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    if (sockfd < 0) {
        perror("socket");
        freeaddrinfo(res);
        return 1;
    }

    if (connect(sockfd, res->ai_addr, res->ai_addrlen) < 0) {
        perror("connect");
        close(sockfd);
        freeaddrinfo(res);
        return 1;
    }

    freeaddrinfo(res);

    // 3. 发送HTTP请求
    char request[] =
        "GET / HTTP/1.1\r\n"
        "Host: example.com\r\n"
        "Connection: close\r\n"
        "\r\n";

    send(sockfd, request, strlen(request), 0);

    // 4. 接收并打印响应
    char buf[4096];
    int bytes;
    while ((bytes = recv(sockfd, buf, sizeof(buf) - 1, 0)) > 0) {
        buf[bytes] = '\0';
        printf("%s", buf);
    }

    close(sockfd);
    return 0;
}

这个程序虽然简单,但已经能完成最基本的HTTP GET请求了。你编译运行一下,就能看到example.com返回的HTML页面。

24.5 知识体系总览

为了让你对整个流程有个直观的认识,我画了一张图:

HTTP客户端请求-响应流程 客户端(Socket) 1. socket() 创建套接字 2. connect() 建立TCP连接 3. send() 发送HTTP请求 4. recv() 接收HTTP响应 服务器(HTTP Server) 1. 解析请求行 2. 解析请求头 3. 处理请求资源 4. 返回HTTP响应 HTTP请求 (GET / HTTP/1.1) HTTP响应 (200 OK + 页面内容) TCP连接建立后,客户端发送请求,服务器解析并返回响应

24.6 避坑指南

做HTTP客户端,有几个坑我踩过,你注意一下:

  • 换行符必须是\r\n——我曾在Windows上开发,换到Linux上部署,结果服务器返回400。查了半天,原来是代码里混用了\n
  • 不要假设一次recv能收完所有数据——TCP是流,不是消息。循环接收是标准做法。
  • 处理HTTP重定向——很多服务器会返回302让你跳转到HTTPS。如果你的客户端不处理重定向,就永远拿不到真正的页面。
  • 注意超时设置——用setsockopt设置SO_RCVTIMEO,防止网络异常时程序卡死。
我的习惯:写HTTP客户端时,先用telnet手动发一次请求,确认服务器行为。比如:telnet example.com 80,然后手动输入GET / HTTP/1.1Host: example.com。这样能快速验证你的请求格式是否正确。

好了,HTTP协议解析和基于Socket的客户端实现就讲到这里。代码虽然简单,但它是很多高级网络应用的基础。你把这个吃透了,后面写HTTPS客户端、WebSocket客户端都会轻松很多。


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