基于 C++ 手写 HTTP 服务器:从请求解析到响应构建全流程解析

基于 C++ 手写 HTTP 服务器:从请求解析到响应构建全流程解析

在上一篇博客中,我们了解到TCP是面向字节流式的进行网络通信的,所以不具备消息边界的功能,所以我们要实现一个完整的网络通信,就必须设计应用层协议,那么要是我们每次都要像上一篇博客那样定义如此麻烦的协议,确实很棘手,因此为了方便,其实已经有大佬定义了一些现成的,非常好用的应用层协议,可以让我们直接使用,不再需要我们自己去定义了,比如HTTP(超文本传输协议就是其中之一)。

协议名称协议全称默认端口传输层协议说明
HTTP超文本传输协议80TCP网页访问(明文)
HTTPS安全超文本传输协议443TCP加密网页
FTP文件传输协议21 / 20TCP文件上传下载
TFTP简单文件传输协议69UDP简单传输
SMTP邮件发送协议25TCP发送邮件
POP3邮件接收协议110TCP下载邮件
IMAP邮件访问协议143TCP管理邮件
DNS域名系统53UDP/TCP域名解析
DHCP动态主机配置协议67 / 68UDP自动分配IP
Telnet远程登录协议23TCP不安全远程登录
SSH安全远程登录22TCP加密远程连接
SNMP网络管理协议161UDP网络设备管理
NTP网络时间协议123UDP时间同步

这些就是常见的一些应用层协议。

认识URL

https://www.baidu.com/

现在,大家有问题一般都是问豆包等AI软件,以前没有AI的时候,大家有问题都是问度娘,这个URL就是我们访问百度的域名

当我们在浏览器中通过域名访问网站时,本质上其实并不是直接访问这个字符串,而是需要先将这个域名解析为对应的 IP 地址,然后进行访问

具体流程是:

  • 首先,客户端(浏览器或操作系统)会向本地 DNS 服务器发送解析请求,这个服务器提供者就是三大运营商。
  • 如果本地 DNS 服务器没有缓存结果,它会向更高层级的 DNS 服务器发起查询:
    • 根域名服务器
    • 顶级域名服务器
    • 权威域名服务器
  • 最终找到该域名对应的 IP 地址,并将结果返回给客户端

拿到 IP 地址之后,客户端就可以通过:

👉 IP地址 + 端口号

但是又仔细的人就会看到上图中我们使用IP地址进行访问的时候,并没有看到端口号这个东西,难道只需要通过IP地址就可以访问到具体的网站了吗?其实并不是,其实是因为我们使用的是HTTPS协议,这个协议就代表着它的端口号就是443(这就像我们在生活中110就是报警电话,119就是火警电话一样),所以我们就不需要在显现的写出来,浏览器替我们隐式补全了端口号,而不是端口号不存在。

IP 地址负责定位主机,端口号负责定位主机上的具体服务,二者缺一不可

HTTP协议

这张图其实展示的是一次完整的 HTTP 通信过程中,请求报文和响应报文的整体结构。从客户端角度来看,当浏览器发起请求时,会先构造一段符合 HTTP 协议规范的文本数据发送给服务器,这段数据最开始是请求行,包含请求方法(如 GET、POST)、资源路径以及 HTTP 版本,用来明确“我要做什么、访问哪里、用什么协议版本”。紧接着是多行请求报头,每一行都是 key:value 的形式,用于携带额外信息,比如目标主机、数据类型、客户端信息等,这些内容全部以 \r\n 作为分隔。

在请求报头结束后,会有一个非常关键的空行,它的作用是明确告诉服务器:报头部分已经结束,接下来如果还有内容,就是请求的有效载荷。对于 GET 请求来说通常没有正文,而 POST 请求则会在这里携带数据,比如表单或 JSON,这部分就是图中标注的“请求正文”。

服务器接收到请求并处理后,会按照同样的协议格式返回响应。响应报文的开头是状态行,包含 HTTP 版本、状态码以及对应的描述信息,例如 200 OK 表示请求成功。随后是多行响应报头,同样以 key:value 的形式存在,用来说明返回数据的类型、长度以及服务器相关信息等。报头之后同样会有一个空行,用于分隔元数据和真正的数据内容,最后的响应正文才是客户端真正关心的部分,可能是 HTML 页面、JSON 数据,或者 JS、CSS 等资源。

从整体来看,这张图的核心就是在说明:HTTP 通信本质上是在 TCP 之上,按照固定格式传输的一段“结构化字符串”,通过请求行/状态行、报头、空行和正文这几个部分进行组织,而 \r\n 则充当了天然的分隔符,使客户端和服务器能够准确地解析彼此发送的数据。

HTTP请求

  • 首行:方法+url+版本
  • Header:请求的属性,冒号分割的键值对;每一组属性之间用\r\n进行分割,遇到空行则表示Header部分结束
  • Body:空行后面的内容就是Body,Body的内容可以是空字符串。

HTTP响应

  • 首行:版本号+状态码+状态码解释
  • Header:请求的属性,冒号分割的键值对;每一组属性之间用\r\n进行分割,遇到空行则表示Header部分结束
  • Body:
  • 空行后面的内容就是Body,Body的内容可以是空字符串。如果服务器返回了一个html页面, 那么html页面内容就是在 body中。

现在我们就来简单实现一个能被浏览器访问的 HTTP 服务器

HTTP服务器

一、实现效果

启动服务器后,在浏览器输入:

http://ip:port/

即可访问我们自己写的网页:

  • 支持返回 index.html
  • 支持简单页面跳转

 二、基本原理

HTTP 服务器本质还是一个 TCP 服务器,只不过多了一步:

👉 解析请求 + 返回符合 HTTP 协议的数据

整体流程:

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

三、核心代码思路

1️⃣ 读取浏览器请求

char buffer[1024]; ssize_t s = read(sockfd, buffer, sizeof(buffer) - 1);

浏览器发来的请求大致是:

GET /index.html HTTP/1.1 Host: xxx

2️⃣ 解析请求行

std::stringstream ss(req_header[0]); std::string method, url, version; ss >> method >> url >> version;

我们只关心:

  • 请求方法(GET)
  • 访问路径(url)

3️⃣ 拼接文件路径

std::string path; if (url == "/" || url == "/index.html") path = "./wwwroot/index.html"; else path = "./wwwroot" + url;

4️⃣ 读取网页内容

std::ifstream in(path); while (std::getline(in, line)) { content += line + "\n"; }

5️⃣ 构造 HTTP 响应

std::string response; response = "HTTP/1.0 200 OK\r\n"; response += "Content-Length: " + std::to_string(content.size()) + "\r\n"; response += "Content-Type: text/html\r\n"; response += "\r\n"; response += content;

然后发送给浏览器:

write(sockfd, response.c_str(), response.size());

6️⃣ 简单处理 404

if (!in.is_open()) { 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; }

HTTP服务器完整代码

Socket.hpp

#include <iostream> #include <string> #include <unistd.h> #include <sys/socket.h> #include <sys/stat.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); } } void Bind(uint16_t port, std::string ip) { struct sockaddr_in server; memset(&server, 0, sizeof server); server.sin_family = AF_INET; server.sin_port = htons(port); inet_pton(AF_INET, ip.c_str(), &server.sin_addr); if (bind(sockfd_, (struct sockaddr *)&server, sizeof server) < 0) { std::cout << "bind fail" << std::endl; exit(BindErr); } } void Listen() { if (listen(sockfd_, 10) < 0) { std::cout << "listen fail" << std::endl; exit(ListenErr); } } 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; } bool Connect(uint16_t server_port, std::string server_ip) { struct sockaddr_in server; bzero(&server, sizeof server); server.sin_family = AF_INET; server.sin_port = htons(server_port); inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr); if (connect(sockfd_, (struct sockaddr *)&server, sizeof(server)) < 0) { return false; } return true; } ~Socket() { close(sockfd_); } private: int sockfd_; };

HttpServer.cc

#include <iostream> #include <unistd.h> #include "Socket.hpp" #include <fstream> #include <vector> #include <sstream> const std::string wwwroot = "./wwwroot"; class HttpServer; class ThreadData { public: ThreadData(int sockfd, HttpServer *ts) : sockfd_(sockfd), ts_(ts) { } public: int sockfd_; HttpServer *ts_; }; 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; } std::string ReadHtmlContent(std::string &htmlpath) { std::string content; std::ifstream in(htmlpath.c_str()); if (!in.is_open()) { return "404"; } std::string line; while (std::getline(in, line)) { content += line; } in.close(); return content; } 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); } std::stringstream ss(req_header[0]); std::string method; std::string url; std::string 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); } ~HttpServer() { } private: Socket listenfd_; uint16_t port_; std::string ip_; }; int main(int argc, char *argv[]) { uint16_t port = std::stoi(argv[2]); std::string ip = argv[1]; HttpServer *svr = new HttpServer(port, ip); svr->Init(); svr->start(); return 0; }

index.html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>index</title> </head> <body> <h1>你是一个好人</h1> <a href="http://49.232.193.163:8080/a/b/hello.html">下一页</a> </body> </html>

hello.html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Hello World</title> </head> <body> <h1>祝你幸福</h1> <a href="http://49.232.193.163:8080/index.html">回到首页</a> </body> </html> 

HTTP的方法

其中最常用的就是GET和POST方法

现在我们就来通过现象来看看GET和POST方法有什么不同。

GET方法

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>index</title> </head> <body> <h1>你是一个好人</h1> <form action="http://49.232.193.163:8080/a/b/hello.html" method="GET"> 银行卡账号:<input type="text" name="user"><br> 银行卡密码:<input type="password" name="pass"><br> <input type="submit" value="登录"> </form> <a href="http://49.232.193.163:8080/a/b/hello.html">下一页</a> </body> </html>

通过现象我们可以看到,我们在输入银行卡账号和密码之后,点击登录之后,并没有跳转到我们想要的界面,而是返回给我们404响应码,这是因为我们在使用GET方法之后,会将我们提交的参数拼接到我们的url之后,这就使得我们的url中就有了我们提交的参数信息。

POST方法

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>index</title> </head> <body> <h1>你是一个好人</h1> <form action="http://49.232.193.163:8080/a/b/hello.html" method="POST"> 银行卡账号:<input type="text" name="user"><br> 银行卡密码:<input type="password" name="pass"><br> <input type="submit" value="登录"> </form> <a href="http://49.232.193.163:8080/a/b/hello.html">下一页</a> </body> </html>

从结果来看,我们可以看到使用POST的方法的时候,POST的方法会将我们提交的参数显示到http请求信息中的正文部分,我们可以从正文部分看到我们提交的参数信息。

从之前博客中的 TCP 通信,到现在我们亲手实现一个 HTTP 服务器,其实我们已经了解了

  • 一个请求是怎么被服务器解析的?
  • 浏览器和服务器到底在“说什么”?
  • GET 和 POST 的区别

总而言之就是:

👉 HTTP 不再是黑盒,而只是一个“有规则的字符串协议”

Read more

【实用工具】无需安装!JupyterLab Online 在线运行 Python 代码(附完整教程)

【实用工具】无需安装!JupyterLab Online 在线运行 Python 代码(附完整教程)

【实用工具】无需安装!JupyterLab Online 在线运行 Python 代码(附完整教程) 文章目录 * 【实用工具】无需安装!JupyterLab Online 在线运行 Python 代码(附完整教程) * 一、为什么推荐JupyterLab Online? * 二、JupyterLab Online 完整使用教程(以运行matplotlib绘图代码为例) * 1. 进入在线环境 * 2. 创建Python文件 * 3. 运行代码(以绘图代码为例) * 4. 保存/下载文件(关键!) * 5. 关闭/退出 * 三、适用场景 & 注意事项 * ✅ 适用场景 * ❗ 注意事项 * 四、总结 一、为什么推荐JupyterLab Online?

By Ne0inhk
用 Python 搭建本地 AI 问答系统:避开 90% 新手都会踩的环境坑

用 Python 搭建本地 AI 问答系统:避开 90% 新手都会踩的环境坑

欢迎文末添加好友交流,共同进步! “ 俺はモンキー・D・ルフィ。海贼王になる男だ!” * 前言 * 一、整体架构概览 * 二、新手踩坑分布图 * 三、环境搭建:最容易翻车的第一步 * 3.1 用虚拟环境隔离,别污染全局 * 3.2 PyTorch 安装:版本对齐是关键 * 3.3 依赖管理:用 requirements.txt 锁定版本 * 四、模型下载:别让网络毁了你的心情 * 4.1 使用 Ollama 管理本地模型(强烈推荐) * 4.2 用 Python 调用 Ollama * 五、搭建 RAG 问答系统 * 5.

By Ne0inhk

【C++ 硬核】摆脱开发板:用 Google Test + Mock 构建嵌入式 TDD (测试驱动开发) 体系

摘要:嵌入式软件质量往往依赖于手工测试,回归测试成本极高。一旦底层硬件没就位,软件开发就得停滞。本文将介绍如何通过 接口抽象 和 依赖注入,将业务逻辑与硬件驱动解耦。利用 Google Mock 模拟硬件行为(如模拟 Flash 写入失败、模拟传感器数据),在 PC 上实现自动化的单元测试。 一、 痛点:被硬件“绑架”的软件开发 假设你要写一个 “数据记录器” 的逻辑: 1. 每隔 1 秒读取传感器。 2. 如果数据超过阈值,写入 Flash。 3. 如果 Flash 写满了,擦除最旧的一个扇区。 典型的“耦合”代码 // DataLogger.cpp #include "stm32f4xx_

By Ne0inhk
【C++】手搓AVL树

【C++】手搓AVL树

手搓AVL树 * 手搓AVL树 * github地址 * 0. 前言 * 1. 二叉搜索树的缺陷 * 性能分析 * 2. 什么是AVL树 * 概念与定义 * 平衡因子 * 基本性质 * 为什么AVL树不要求左右子树的高度为0呢? * 3. AVL树的实现 * 整体架构设计 * AVL树的结点定义 * AVL树设计 * AVL树的操作实现 * 插入 * 1. 本质 * 2. 思路简述 * 3. 二叉搜索树的插入逻辑 * 4. 更新平衡因子 * 1. 插入后父节点的平衡因子变化分析 * 2. 平衡因子更新后的三种情况: * 3. 更新平衡因子的最坏情况 * 4. 更新平衡因子的代码实现 * 旋转操作 * 旋转的目的 * 一、左单旋 * 触发条件 * 左单旋原理与核心操作 * 代码实现

By Ne0inhk