第六章、[特殊字符] HTTP 深度进阶:报文格式 + 服务器实现(从理论到代码)

一、HTTP 报文格式("快递包裹" 的说明书)

HTTP 报文是浏览器和服务器的 "沟通载体",就像你寄快递时的 "包裹 + 快递单"------ 既要有 "收件信息"(沟通规则),也要有 "包裹内容"(实际数据)。整体分为请求报文 (客户端→服务器)和响应报文(服务器→客户端),结构统一是 "开始行 + 首部行 + 实体主体"。

1. 报文整体结构(通用规则)

【是什么】

所有 HTTP 报文都遵循 "三段式" 结构,缺一不可(部分字段可省略):

plaintext

复制代码
开始行(核心信息)→ 首部行(附加说明)→ 空行(分隔符)→ 实体主体(实际数据)
  • 开始行:一句话说清 "干什么"(请求 / 响应的核心目的);
  • 首部行:补充说明(比如 "包裹重量""接收要求"),可有多行;
  • 空行:必须有!用来区分首部和主体,少了会解析失败;
  • 实体主体:实际要传输的数据(比如表单数据、HTML 页面)。
【为什么】

没有统一格式,服务器和浏览器就 "看不懂对方的话"------ 比如浏览器发个 "我要首页",服务器不知道是要 HTML 还是图片,也不知道浏览器支持什么格式,报文格式就是统一的 "沟通语言规范"。

【怎么用】

举个直观例子(请求报文 + 响应报文):

plaintext

复制代码
// 请求报文(客户端→服务器)
GET /index.html HTTP/1.1  // 开始行(请求行)
Host: 192.168.1.100:8080  // 首部行(服务器地址)
Connection: Keep-Alive    // 首部行(长连接)
Accept: text/html         // 首部行(接收HTML格式)

                         // 空行(必须有)
username=admin&password=123  // 实体主体(POST请求的数据)

// 响应报文(服务器→客户端)
HTTP/1.1 200 OK           // 开始行(状态行)
Content-Type: text/html   // 首部行(返回HTML)
Content-Length: 1024      // 首部行(数据长度1024字节)
Date: Thu, 01 Jan 2025 08:00:00 GMT  // 首部行(时间)

                         // 空行(必须有)
<html><body>首页内容</body></html>  // 实体主体(HTML页面)
【坑在哪】
  • 缺少空行:服务器会把实体主体当首部行解析,直接报错;
  • 首部行格式错:必须是 "字段名:值"(冒号后有空格),少空格会解析失败;
  • 实体主体和 Content-Length 不匹配:服务器会提前断开连接或接收不全数据。

2. 请求报文(客户端发的 "需求单")

【是什么】

客户端发给服务器的 "需求申请",核心是 "我要什么资源,用什么方式要"。结构:

  • 开始行(请求行):请求方法 + URL + HTTP版本(比如GET / HTTP/1.1);
  • 首部行:补充请求信息(如 Host、Accept、Cookie 等);
  • 实体主体:POST 请求时存放数据(如表单、文件),GET 请求一般为空。
【为什么】

服务器需要知道三个核心问题:"做什么操作"(请求方法)、"要什么资源"(URL)、"用什么规则沟通"(HTTP 版本),首部行补充额外要求(比如 "我只接受 HTML""保持连接")。

【怎么用】
  • 请求方法:常用 GET(查资源)、POST(提交数据),资料里还提了 HEAD、PUT 等;
  • URL:要访问的资源路径(比如/index.html);
  • 首部行常用字段:
    • Host:服务器 IP + 端口(必填,比如Host: 192.168.1.100:8080);
    • Connection: Keep-Alive:要求长连接(HTTP 1.1 默认);
    • Content-Length:POST 请求时说明实体主体长度;
  • 实体主体:POST 请求的表单数据(比如username=admin&password=123)。
【坑在哪】
  • GET 请求带大量数据:GET 的参数在 URL 里,长度有限制(约 2KB),大数据要用 POST;
  • 请求方法用错:比如用 GET 提交密码(明文显示在 URL),不安全;
  • 缺少 Host 字段:HTTP 1.1 要求必须带,否则服务器返回 400 错误。

3. 响应报文(服务器发的 "回执单")

【是什么】

服务器回复客户端的 "处理结果",核心是 "请求处理得怎么样,给你什么数据"。结构:

  • 开始行(状态行):HTTP版本 + 状态码 + 短语(比如HTTP/1.1 200 OK);
  • 首部行:补充响应信息(如 Content-Type、Content-Length);
  • 实体主体:要返回给客户端的数据(比如 HTML 页面、JSON 数据)。
【为什么】

客户端需要知道 "请求成功了吗"(状态码)、"返回的是什么类型数据"(Content-Type)、"数据有多大"(Content-Length),这样才能正确显示内容。

【怎么用】
  • 状态码(核心中的核心):
    • 2XX:成功(200 OK:请求成功);
    • 3XX:重定向(304 Not Modified:缓存可用);
    • 4XX:客户端错(404 Not Found:资源不存在);
    • 5XX:服务器错(500 Internal Server Error:服务器崩溃);
  • 首部行常用字段:
    • Content-Type:返回数据类型(text/html=HTML,application/json=JSON);
    • Content-Length:数据长度(帮助客户端判断是否接收完);
    • Set-Cookie:服务器给客户端设置 Cookie(记录登录状态);
  • 实体主体:比如 HTML 页面、求和结果(1+2=3)。
【坑在哪】
  • 状态码用错:比如明明资源存在却返回 404,客户端会误以为没找到;
  • Content-Type 写错:比如返回 JSON 却标text/html,浏览器会显示乱码;
  • 实体主体为空却带 Content-Length:客户端会一直等待数据,超时断开。

二、HTTP 服务器("快递驿站" 的运作流程)

HTTP 服务器本质是 "监听端口→接收请求→解析请求→处理业务→返回响应" 的程序,资料里给了完整的 C++ 实现(多线程并发),我们按 "代码文件 + 核心流程" 拆解,就像拆解驿站的运作步骤。

1. 服务器核心原理

【是什么】

一个 "永远在线的快递驿站":

  • 监听端口:驿站开在固定地址(IP + 端口),等待客户(客户端)上门;
  • 接收连接:客户到店(客户端连接),驿站分配工作人员(线程)接待;
  • 解析请求:工作人员问清客户需求(解析 HTTP 报文);
  • 处理业务:按需求找包裹(读取 HTML 文件)或办业务(求和、登录);
  • 返回响应:把包裹 + 回执(响应报文)交给客户。
【为什么】

没有服务器,客户端的请求就 "没人接"------ 比如你在浏览器输http://localhost:8080,没有服务器监听 8080 端口,浏览器会直接提示 "无法连接"。服务器是 Web 通信的 "核心枢纽"。

【怎么用】(按代码实例讲解)

资料里的服务器由 5 个文件组成:main.cpp(入口)、http.cpp(核心处理)、custom_handle.cpp(业务逻辑)、http.h(头文件)、custom_handle.h(头文件),还有CMakeLists.txt(编译配置)。

核心文件拆解(逐个击破)
(1)入口文件:main.cpp(驿站大门)

cpp

运行

复制代码
#include <myhead.h>
#include "http.h"

// 线程处理函数(每个客户分配一个线程)
void *msg_request(void *arg) {
    int sock = *(int *)arg;
    delete (int *)arg;
    handler_msg(sock);  // 调用核心处理函数
    close(sock);
    return NULL;
}

int main(int argc, const char *argv[]) {
    int port = 80;  // 默认端口80(HTTP默认端口)
    if (argc > 1) port = atoi(argv[1]);  // 允许手动传端口(比如./thttpd.out 8080)

    int lis_socket = init_server(port);  // 初始化服务器(创建套接字、绑定、监听)

    // 循环接收客户端连接(多线程并发)
    while (1) {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int sock = accept(lis_socket, (struct sockaddr *)&peer, &len);  // 接收连接
        if (sock == -1) { perror("accept error"); return -1; }
        printf("新客户端连接(fd=%d)\n", sock);

        // 创建线程处理该客户端,防止阻塞其他客户
        pthread_t tid;
        if (pthread_create(&tid, NULL, msg_request, new int(sock)) > 0) {
            printf("pthread_create error\n");
            return -1;
        }
        pthread_detach(tid);  // 线程分离,自动回收资源
    }

    close(lis_socket);
    return 0;
}
【是什么】

服务器的 "入口大门":初始化服务器、监听端口、多线程接收客户端连接,给每个客户端分配独立线程处理(避免一个客户卡住所有人)。

【为什么】
  • 多线程:单线程只能接待一个客户,多线程能同时接待多个(比如宿舍多个人同时访问);
  • 端口可配置:默认 80 端口(不用输端口),也能手动改(比如 8080,避免端口占用)。
【怎么用】
  1. 编译:按CMakeLists.txt配置(add_executable(thttpd.out main.cpp http.cpp custom_handle.cpp)),链接 pthread 库;
  2. 运行:./thttpd.out(默认 80 端口)或./thttpd.out 8080(自定义端口);
  3. 访问:浏览器输http://服务器IP:端口(比如http://192.168.1.100:8080)。
【坑在哪】
  • 端口占用:80 端口可能被其他程序占用,换 8080、9090 等端口;
  • 线程创建失败:检查是否链接 pthread 库(target_link_libraries(thttpd.out pthread));
  • 内存泄漏:new int(sock)后要在线程里delete,否则内存越用越多。
(2)核心处理:http.cpp(驿站工作人员)

核心函数handler_msg(解析请求 + 响应):

cpp

运行

复制代码
int handler_msg(int sock) {
    char del_buf[SIZE] = "";
    recv(sock, del_buf, SIZE, MSG_PEEK);  // 偷看请求数据(不读走)
    printf("收到请求:\n%s\n", del_buf);

    // 1. 解析请求行(方法、URL、版本)
    char buf[SIZE] = "";
    int count = get_line(sock, buf);  // 读取请求行
    char method[32] = "";
    int k=0, i=0;
    // 提取请求方法(GET/POST)
    while (i<count && !isspace(buf[i])) {
        method[k++] = buf[i++];
    }
    method[k] = '\0';

    // 跳过空格,提取URL
    while (isspace(buf[i]) && i<SIZE) i++;
    char url[SIZE] = "";
    char *querry_string = NULL;  // 存储URL中的参数(比如?data=123)
    int t=0;
    while (i<SIZE && !isspace(buf[i])) {
        if (buf[i] == '?') {  // 有参数(GET请求)
            querry_string = &url[t];
            querry_string++;
            url[t] = '\0';
        } else {
            url[t++] = buf[i];
        }
        i++;
    }
    url[t] = '\0';
    printf("方法:%s,URL:%s,参数:%s\n", method, url, querry_string);

    // 2. 确定资源路径(wwwroot下的文件)
    char path[SIZE] = "";
    sprintf(path, "../wwwroot%s", url);
    if (path[strlen(path)-1] == '/') strcat(path, "index.html");  // 默认首页

    // 3. 判断是否需要处理业务(POST或GET带参数)
    int need_handle = 0;
    if (strcasecmp(method, "POST") == 0 || (strcasecmp(method, "GET")==0 && querry_string!=NULL)) {
        need_handle = 1;
    }

    // 4. 处理请求
    struct stat st;
    if (stat(path, &st) == -1) {  // 资源不存在
        echo_error(sock, 404);  // 返回404页面
        close(sock);
        return -1;
    }
    if (need_handle == 1) {
        handle_request(sock, method, path, querry_string);  // 处理业务(求和、登录)
    } else {
        clear_header(sock);  // 清空首部
        echo_www(sock, path, st.st_size);  // 返回静态页面(比如HTML)
    }

    close(sock);
    return 0;
}
【是什么】

服务器的 "核心大脑":解析请求行(方法、URL)、确定资源路径、判断是否需要业务处理(求和、登录),要么返回静态页面,要么调用业务逻辑。

【为什么】
  • 解析 URL:把客户端请求的/index.html转换成服务器本地的../wwwroot/index.html
  • 区分请求类型:静态请求(直接返回 HTML)和动态请求(求和、登录)分开处理;
  • 错误处理:资源不存在时返回 404 页面,提升用户体验。
【怎么用】
  • 静态页面:把 HTML 文件放在../wwwroot目录下(比如index.html404.html),访问http://IP/会默认返回index.html
  • 动态请求:POST 请求提交求和 / 登录数据,服务器调用custom_handle.cpp的业务函数。
【坑在哪】
  • 路径错误:sprintf(path, "../wwwroot%s", url)要确保wwwroot目录在正确位置(比如服务器程序在bin目录,wwwroot在上级目录);
  • 解析 URL 失败:URL 中有特殊字符(比如中文)要编码,否则解析错乱;
  • 404 页面缺失:要在wwwroot下创建404.html,否则echo_www会报错。
(3)业务逻辑:custom_handle.cpp(驿站专项窗口)

处理求和、登录等业务,核心函数parse_and_process

cpp

运行

复制代码
int parse_and_process(int sock, const char *querry_string, char * req_buf) {
    // 处理求和请求(data1=1&data2=2)
    if (strstr(req_buf, "data1=") && strstr(req_buf, "data2=")) {
        return handle_add(sock, req_buf);
    }
    // 处理登录请求(username=admin&password=123)
    else if (strstr(req_buf, "username=") && strstr(req_buf, "password=")) {
        return handle_login(sock, req_buf);
    } else {
        return 0;
    }
}

// 求和逻辑
static int handle_add(int sock, const char * req_buf) {
    int num1, num2;
    sscanf(req_buf, "data1=%d&data2=%d", &num1, &num2);  // 解析参数
    printf("求和:%d+%d=%d\n", num1, num2, num1+num2);
    char reply_buf[HTML_SIZE] = "";
    sprintf(reply_buf, "%d", num1+num2);  // 结果转字符串
    send(sock, reply_buf, strlen(reply_buf), 0);  // 返回结果
    return 0;
}

// 登录逻辑
int handle_login(int sock, char *req_buf) {
    char *uname = strstr(req_buf, "username=");
    uname += strlen("username=");
    char *ptr = strstr(req_buf, "password=");
    *(ptr-1) = '\0';  // 分割用户名和密码
    char *passwd = ptr + strlen("password=");
    printf("登录:账号=%s,密码=%s\n", uname, passwd);

    // 账号密码相等则登录成功,跳转首页
    if (strcmp(uname, passwd) == 0) {
        char reply_buf[HTML_SIZE] = "";
        sprintf(reply_buf, "<script>localStorage.setItem('usr_user_name', '%s'); window.location.href='/index.html';</script>", uname);
        send(sock, reply_buf, strlen(reply_buf), 0);
    }
    return 0;
}
【是什么】

服务器的 "专项业务窗口":处理动态请求(求和、登录),按业务逻辑处理后返回结果(比如求和结果、登录跳转脚本)。

【为什么】

静态页面只能展示内容,动态业务(比如用户登录、数据计算)需要专门的逻辑处理,这部分代码和核心解析分离,便于维护。

【怎么用】
  • 求和功能:用 POST 请求提交data1=1&data2=2,服务器返回3
  • 登录功能:提交username=admin&password=admin(账号密码相等),服务器返回跳转脚本,跳转到index.html并存储用户名到 localStorage。
【坑在哪】
  • 解析参数失败:sscanf格式要和请求数据匹配(比如data1=%d&data2=%d对应data1=1&data2=2);
  • 登录跳转失败:确保index.html存在,脚本语法正确(引号闭合);
  • 业务逻辑耦合:如果要加新业务(比如注册),直接在parse_and_process中加分支即可,不用改核心解析代码。

2. 服务器整体流程(从连接到响应)

plaintext

复制代码
客户端发起请求 → main.cpp的accept接收连接 → 创建线程 → 线程调用handler_msg(http.cpp)→ 解析请求行/URL → 判断静态/动态请求 → 静态请求返回HTML(echo_www)→ 动态请求调用custom_handle.cpp的业务函数 → 返回响应 → 关闭连接

三、快速复习查找表

知识点 核心内容 怎么用 坑在哪
HTTP 报文整体结构 开始行 + 首部行 + 空行 + 实体主体 按格式构造请求 / 响应,空行不可少 缺少空行导致解析失败;首部行格式错误(无冒号 / 空格)
请求报文 - 请求行 方法(GET/POST)+ URL + 版本(HTTP/1.1) GET 用于查资源,POST 用于提交数据;URL 是资源路径 GET 带大量数据;请求方法用错(如 GET 提交密码)
请求报文 - 首部行 Host(必填)、Connection(长连接)、Content-Length(POST 数据长度) 按需求添加字段,比如要长连接加Connection: Keep-Alive 缺少 Host 字段;Content-Length 与实体主体长度不匹配
响应报文 - 状态行 版本 + 状态码 + 短语(200 = 成功,404 = 资源不存在,500 = 服务器错) 按处理结果返回对应状态码 状态码用错(如资源存在返回 404)
响应报文 - 首部行 Content-Type(数据类型)、Content-Length(数据长度)、Set-Cookie(Cookie) 返回 HTML 标text/html,JSON 标application/json Content-Type 写错导致乱码;Content-Length 缺失导致客户端超时
服务器 - main.cpp 初始化服务器、多线程接收连接 编译链接 pthread 库;运行时指定端口(./thttpd.out 8080) 端口占用;线程创建失败;内存泄漏(new 后未 delete)
服务器 - http.cpp 解析请求行 / URL、判断静态 / 动态请求、响应 静态页面放 wwwroot 目录;URL 解析为本地路径 路径错误(wwwroot 位置不对);404 页面缺失;解析 URL 失败(特殊字符)
服务器 - custom_handle.cpp 业务逻辑(求和、登录) POST 提交对应数据(data1=1&data2=2);登录账号密码相等即可成功 解析参数格式不匹配;登录跳转脚本语法错误;业务逻辑耦合

总结

HTTP 报文是 "沟通规则",按 "三段式" 格式构造就能让服务器和浏览器看懂;HTTP 服务器是 "规则执行者",核心是 "接收 - 解析 - 响应",通过多线程支持并发,分离核心解析和业务逻辑便于维护。跟着代码实例跑一遍,就能理解从客户端请求到服务器响应的完整链路,遇到问题对照复习表找坑,很快就能上手!

相关推荐
水天需0101 小时前
VS Code C++ 环境配置及 HelloWorld 程序
c++
Boop_wu1 小时前
[Java EE] 网络原理(1)
java·网络·java-ee
永远都不秃头的程序员(互关)1 小时前
查找算法深入分析与实践:从线性查找到二分查找
数据结构·c++·算法
zl0_00_01 小时前
isctf2025 部分wp
linux·前端·javascript
Sunsets_Red1 小时前
二项式定理
java·c++·python·算法·数学建模·c#
qq_479875431 小时前
std::true_type {}
java·linux·服务器
爱跑步的程序员~1 小时前
TCP三次握手
网络·网络协议·tcp/ip
2401_853448231 小时前
U-boot引导Linux内核启动
linux·uboot·nfs·mmc·tftp·系统移植
谷粒.1 小时前
云原生时代的测试策略:Kubernetes环境下的测试实践
运维·网络·云原生·容器·kubernetes