跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

C++ 手写 HTTP 服务器:从请求解析到响应构建

基于 C++ 实现简易 HTTP 服务器,深入解析 TCP 流式通信下的应用层协议设计。内容涵盖 URL 与 DNS 解析原理、HTTP 报文结构(请求行、报头、正文)、Socket 网络编程流程以及 GET 与 POST 方法差异对比。通过完整代码示例展示如何接收请求、解析路径、读取文件并构造标准响应,帮助理解 Web 服务背后的底层逻辑。

城市逃兵发布于 2026/3/29更新于 2026/10/291 浏览
C++ 手写 HTTP 服务器:从请求解析到响应构建

C++ 手写 HTTP 服务器:从请求解析到响应构建

TCP 是面向字节流的通信方式,本身不具备消息边界。要实现完整的网络通信,必须设计应用层协议来界定数据范围。虽然我们可以自定义协议,但为了效率与兼容性,直接使用成熟的现成协议更为明智,HTTP(超文本传输协议)就是其中之一。

常见应用层协议概览

协议名称默认端口说明
HTTP80网页访问(明文)
HTTPS443加密网页
FTP21/20文件传输
DNS53域名解析
SSH22安全远程登录

URL 与域名解析

当我们通过浏览器输入 https://www.baidu.com/ 时,本质上并不是直接访问这个字符串,而是需要先将域名解析为对应的 IP 地址。

具体流程如下:

  1. 客户端发起请求:向本地 DNS 服务器发送解析请求。
  2. 层级查询:若本地无缓存,依次向根域名、顶级域名、权威域名服务器查询。
  3. 返回结果:最终获取 IP 地址并返回给客户端。

拿到 IP 后,客户端通过 IP 地址 + 端口号 建立连接。使用 HTTPS 协议时,默认端口为 443,浏览器会自动补全,因此地址栏中不显示端口号。IP 负责定位主机,端口负责定位服务,二者缺一不可。

HTTP 协议结构

HTTP 通信本质是在 TCP 之上,按照固定格式传输的'结构化字符串'。一次完整的交互包含请求报文和响应报文。

请求报文结构

  • 请求行:包含请求方法(GET/POST)、资源路径及 HTTP 版本。
  • 请求报头:多行 key:value 形式,携带额外信息(如 Host、Content-Type),以 \r\n 分隔。
  • 空行:标识报头结束。
  • 请求正文:可选,POST 请求通常在此携带表单或 JSON 数据。

响应报文结构

  • 状态行:包含 HTTP 版本、状态码(如 200 OK)及描述。
  • 响应报头:说明返回数据的类型、长度等元数据。
  • 空行:分隔元数据与内容。
  • 响应正文:客户端真正关心的部分,如 HTML 页面或 JSON 数据。

实现一个简易 HTTP 服务器

我们的目标是启动一个服务器,在浏览器输入 http://ip:port/ 即可访问自定义网页。

核心流程

HTTP 服务器底层仍是 TCP 服务器,关键在于增加了一层协议解析逻辑:

socket → bind → listen → accept ↓ 创建线程 ↓ read → 解析 → write

代码实现思路

1. Socket 封装

为了方便管理,我们封装了一个 Socket 类,处理 socket 创建、绑定、监听和接受连接。

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>

enum Err { SocketErr = 1, BindErr, ListenErr };

class Socket {
public:
    Socket() {}
    void Init() {
        sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd_ < 0) {
            std::cout << "socket fail!!!" << std::endl;
            exit(SocketErr);
        }
    }
    // ... 省略部分辅助函数 ...
    int Accept(uint16_t *client_port, std::string *client_ip) {
        struct sockaddr_in client;
        bzero(&client, sizeof client);
        socklen_t len = sizeof(client);
        int newfd = accept(sockfd_, (struct sockaddr *)&client, &len);
        if (newfd < 0) return -1;
        char ip[64];
        inet_ntop(AF_INET, &client.sin_addr, ip, sizeof ip);
        *client_port = ntohs(client.sin_port);
        *client_ip = ip;
        return newfd;
    }
    ~Socket() { close(sockfd_); }
private:
    int sockfd_;
};
2. 主服务器逻辑

服务器启动后进入循环,接受连接并创建新线程处理请求。

class HttpServer {
public:
    HttpServer(uint16_t port, std::string ip) : port_(port), ip_(ip) {}
    void Init() {
        listenfd_.Init();
        listenfd_.Bind(port_, ip_);
        listenfd_.Listen();
    }
    void start() {
        while (1) {
            uint16_t client_port;
            std::string client_ip;
            int sockfd = listenfd_.Accept(&client_port, &client_ip);
            if (sockfd < 0) continue;
            ThreadData *td = new ThreadData(sockfd, this);
            pthread_t tid;
            pthread_create(&tid, nullptr, ThreadRun, td);
        }
    }
    static void *ThreadRun(void *arg) {
        pthread_detach(pthread_self());
        ThreadData *td = (ThreadData *)arg;
        td->ts_->HttpHandler(td->sockfd_);
        delete td;
        return nullptr;
    }
    // ... 省略构造函数等 ...
};
3. 请求解析与响应构造

这是最关键的部分。我们需要读取缓冲区,按 \r\n 分割请求行,提取 URL,然后查找对应文件。

void HttpHandler(int sockfd) {
    char buffer[1024];
    ssize_t s = read(sockfd, buffer, sizeof buffer - 1);
    std::string request;
    if (s > 0) {
        buffer[s] = 0;
        std::cout << buffer << std::endl;
        request = buffer;
        
        // 分割请求行
        std::vector<std::string> req_header;
        while (!request.empty()) {
            ssize_t pos = request.find("\r\n", 0);
            if (pos == std::string::npos) break;
            std::string line = request.substr(0, pos);
            if (line.empty()) break;
            req_header.push_back(line);
            request.erase(0, pos + 2);
        }

        // 解析首行:Method URL Version
        std::stringstream ss(req_header[0]);
        std::string method, url, http_version;
        ss >> method >> url >> http_version;

        // 拼接文件路径
        std::string path = url;
        if (path == "/" || path == "/index.html") {
            path = "./wwwroot/index.html";
        } else {
            path = "./wwwroot" + url;
        }

        // 读取文件内容
        std::string text = ReadHtmlContent(path);
        std::string response;

        // 构造响应
        if (text == "404") {
            std::string body = "<h1>404 Not Found</h1>";
            response = "HTTP/1.0 404 Not Found\r\n";
            response += "Content-Length: " + std::to_string(body.size()) + "\r\n";
            response += "Content-Type: text/html\r\n";
            response += "\r\n";
            response += body;
        } else {
            response = "HTTP/1.0 200 OK\r\n";
            response += "Content-Length: " + std::to_string(text.size()) + "\r\n";
            response += "Content-Type: text/html\r\n";
            response += "\r\n";
            response += text;
        }
        write(sockfd, response.c_str(), response.size());
    }
    close(sockfd);
}

完整示例文件

index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
<h1>你是一个好人</h1>
<a href="http://127.0.0.1:8080/a/b/hello.html">下一页</a>
</body>
</html>

hello.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello World</title>
</head>
<body>
<h1>祝你幸福</h1>
<a href="http://127.0.0.1:8080/index.html">回到首页</a>
</body>
</html>

GET 与 POST 方法对比

HTTP 中最常用的两种方法是 GET 和 POST,它们的区别主要体现在参数传递的位置上。

GET 方法

GET 请求会将表单参数拼接到 URL 之后。这意味着敏感信息(如密码)会暴露在地址栏中,且 URL 长度有限制。

<form action="http://127.0.0.1:8080/login" method="GET">
    账号:<input type="text" name="user"><br>
    密码:<input type="password" name="pass"><br>
    <input type="submit" value="登录">
</form>

POST 方法

POST 请求将参数放在请求正文(Body)中,URL 保持不变。这种方式更安全,适合传输大量数据或敏感信息。

<form action="http://127.0.0.1:8080/login" method="POST">
    账号:<input type="text" name="user"><br>
    密码:<input type="password" name="pass"><br>
    <input type="submit" value="登录">
</form>

总结

从 TCP 通信基础到手写 HTTP 服务器,我们理清了以下关键点:

  1. 请求解析:服务器如何识别请求行、报头和正文。
  2. 报文交互:浏览器与服务器之间交换的具体数据结构。
  3. 方法差异:GET 与 POST 在参数传递上的本质区别。

HTTP 不再是一个黑盒,它只是遵循特定规则的字符串协议。理解这些底层细节,有助于我们在开发更复杂的网络服务时做出更合理的架构决策。

目录

  1. C++ 手写 HTTP 服务器:从请求解析到响应构建
  2. 常见应用层协议概览
  3. URL 与域名解析
  4. HTTP 协议结构
  5. 请求报文结构
  6. 响应报文结构
  7. 实现一个简易 HTTP 服务器
  8. 核心流程
  9. 代码实现思路
  10. 1. Socket 封装
  11. 2. 主服务器逻辑
  12. 3. 请求解析与响应构造
  13. 完整示例文件
  14. GET 与 POST 方法对比
  15. GET 方法
  16. POST 方法
  17. 总结

更多推荐文章

查看全部
  • Python 缓存过期清理实战指南:5 大核心内存管理策略
  • 浏览器 WebRTC 隐私泄露风险及关闭方法
  • Python 基于 Flask 的小区停车场管理系统设计与实现
  • AXURE 11 结合 AI 的智能原型设计体验
  • AMD 显卡部署 llama.cpp 兼容性解决方案与部署指南
  • VR、具身智能与人形机器人:通往现实世界的智能接口
  • 基于 GFPGAN 的低质量人脸图像超分辨率恢复实战
  • QQ群JM漫画下载机器人部署指南
  • 特斯联获 20 亿融资,聚焦 AI+IoT 与模型系统落地路径
  • 从 0 到 1 打造 RISC-V 智能家居中控:硬件、固件与通信全链路实战
  • Git 快速入门指南:从基础概念到分支管理
  • 知网 AIGC 检测原理与降重实操指南
  • 提升英文写作质量以应对 Turnitin AIGC 检测
  • HTML5 Web Workers 详解:提升网页性能的关键技术
  • Python Web 框架对比与实战:Django vs Flask vs FastAPI
  • FPGA 调试:PCIe XDMA 链路不通使用 LTSSM 定位问题
  • Spring 4.3 新增专用请求注解,简化@RequestMapping 配置
  • macOS 完整卸载 OpenClaw 指南(含深度清理)
  • 20 个提升 Python 开发效率的 PyCharm 常用插件推荐
  • Linux 网络传输层详解:UDP 与 TCP 协议原理

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online