【Linux网络】深入理解 HTTP 协议(四):完善 C++ HTTP 服务器:从协议原理到生产级实现
HTTP 协议作为 Web 世界的基石,支撑着我们每天浏览的每一个网页、调用的每一个 API。虽然 Nginx、Apache 等成熟服务器已经足够强大,但亲手实现一个 HTTP 服务器,能让我们穿透抽象层,真正理解 “浏览器输入 URL 按下回车” 背后发生的一切。本文将从 HTTP 协议最核心的 GET/POST 请求讲起,一步步拆解 HTTP 服务器的分层架构,实现一个支持静态资源托管、动态路

🎬 博主简介:

文章目录
前言:
HTTP 协议作为 Web 世界的基石,支撑着我们每天浏览的每一个网页、调用的每一个 API。虽然 Nginx、Apache 等成熟服务器已经足够强大,但亲手实现一个 HTTP 服务器,能让我们穿透抽象层,真正理解 “浏览器输入 URL 按下回车” 背后发生的一切。本文将从 HTTP 协议最核心的 GET/POST 请求讲起,一步步拆解 HTTP 服务器的分层架构,实现一个支持静态资源托管、动态路由注册、完整协议解析的 C++ HTTP 服务器。然后我们会深入讲解Nginx 生产级部署的最佳实践,最后对比自研实现与业界主流库 cpp-httplib 的差异,让你既能懂原理,又能快速落地。
一. HTTP 协议核心:GET 与 POST 的本质区别
1.1 表单提交:HTTP 请求的起点
所有 Web 交互的本质都是浏览器构造 HTTP 请求发送给服务器,服务器处理后返回响应。最常见的交互方式就是 HTML 表单:
<form action="/login" method="GET">
用户名:<input type="text" name="username"><br><br>
密码:<input type="password" name="passwd"><br><br>
<input type="submit" value="登录">
</form>
这里有两个关键属性:
action:指定数据提交的目标 URLmethod:指定 HTTP 请求方法
1.2 GET 与 POST:参数传递的两种方式
这是 HTTP 最基础也最容易混淆的知识点,我们从报文结构层面彻底搞清楚:
| 特性 | GET 方法 | POST 方法 |
|---|---|---|
| 参数位置 | 拼接在 URL 中,格式为 ?key1=value1&key2=value2 |
放在 HTTP 请求体中 |
| 可见性 | 参数直接显示在浏览器地址栏 | 参数不可见(但抓包仍可获取) |
| 长度限制 | 受浏览器 URL 长度限制(通常 2KB 左右) | 理论上无限制,受服务器配置影响 |
| 缓存 | 可被浏览器缓存 | 默认不缓存 |
| 适用场景 | 获取资源、搜索、分页等无副作用操作 | 登录、注册、提交数据等有副作用操作 |
报文示例对比:
// GET请求
GET /login?username=zhangsan&passwd=123456 HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
// POST请求
POST /login HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: application/x-www-form-urlencoded
Content-Length: 31
username=zhangsan&passwd=123456


1.3 其他 6 种 HTTP 方法详解
HTTP/1.1 协议共定义了 8 种请求方法,除了最常用的 GET 和 POST,还有以下 6 种:
| 方法 | 说明 | 支持的 HTTP 版本 | 生产环境状态 |
|---|---|---|---|
| PUT | 传输文件,将请求体中的内容保存到指定 URL | 1.0、1.1 | 通常禁用,防止用户随意上传文件 |
| HEAD | 与 GET 类似,但只返回响应头,不返回响应体 | 1.0、1.1 | 常用,用于检查资源是否存在、获取资源大小 |
| DELETE | 删除指定 URL 的资源 | 1.0、1.1 | 通常禁用,防止用户随意删除文件 |
| OPTIONS | 查询服务器对指定资源支持的 HTTP 方法 | 1.1 | 通常禁用,防止攻击者探测服务器信息 |
| TRACE | 追踪请求 - 响应的传输路径,用于调试 | 1.1 | 几乎不用,存在安全风险 |
| CONNECT | 要求用隧道协议连接代理,用于 HTTPS | 1.1 | 仅代理服务器使用 |
| LINK/UNLINK | 建立 / 断开资源之间的链接关系 | 1.0 | 已废弃,几乎不使用 |
关于这里更详细的解析可以看,包括后面对常见Header的补充也有
各方法实战演示
# 1. HEAD方法:只获取响应头
curl --head http://www.baidu.com
# 输出:
# HTTP/1.1 200 OK
# Content-Length: 0
# Content-Type: text/html
# Date: Mon, 18 May 2026 12:55:44 GMT
# 2. OPTIONS方法:查询支持的方法
curl -X OPTIONS -i http://127.0.0.1/
# 生产环境Nginx通常返回:
# HTTP/1.1 405 Not Allowed
# Server: nginx/1.24.0 (Ubuntu)
# 3. PUT方法:上传文件(生产环境禁用)
curl -X PUT -T test.txt http://127.0.0.1/test.txt
- 只开放必要的 GET 和 POST 方法
- 禁用 PUT、DELETE、OPTIONS、TRACE 等方法
- 对于 RESTful API,可以有选择地开放 PUT 和 DELETE,但必须做好权限控制
二. HTTP 服务器架构设计:分层解耦的艺术
一个健壮的 HTTP 服务器应该采用分层架构,每一层只负责单一职责:
┌─────────────────────────────────┐
│ 业务逻辑层 │
│ (登录、注册、搜索等具体服务) │
├─────────────────────────────────┤
│ HTTP协议层 │
│ (请求解析、响应构造、路由分发) │
├─────────────────────────────────┤
│ TCP传输层 │
│ (连接管理、数据收发) │
└─────────────────────────────────┘
2.1 底层:TCP 服务器封装(简版)
我们先封装一个基础的 TCP 服务器,负责处理连接建立和数据收发,这个更详细的封装实现,我们在前面讲过,这里是个简单版的,实际使用的是之前那个
// TcpServer.hpp 核心代码
using handler_t = std::function<std::string(std::string &)>;
class TcpServer
{
public:
TcpServer(int port) : _port(port), _listensockfd(std::make_unique<TcpSocket>())
{
_listensockfd->BulidSocketMethod(port); // 封装socket、bind、listen
}
void Run(handler_t handler)
{
_handlerstream = handler;
_isrunning = true;
signal(SIGCHLD, SIG_IGN); // 自动回收子进程
while(_isrunning)
{
InetAddr clientaddr;
auto sockfd = _listensockfd->Accepter(&clientaddr); // 接受连接
if(fork() == 0)
{ // 多进程处理每个连接
_listensockfd->Close();
HandlerIo(sockfd, clientaddr);
exit(0);
}
sockfd->Close();
}
}
private:
void HandlerIo(std::shared_ptr<Socket> sockfd, InetAddr clientaddr)
{
std::string inbuffer;
int n = sockfd->Recv(&inbuffer); // 接收数据
if(n > 0 && _handlerstream)
{
std::string outbuffer = _handlerstream(inbuffer); // 回调上层处理
if(!outbuffer.empty()) sockfd->Send(outbuffer); // 发送响应
}
}
int _port;
std::unique_ptr<Socket> _listensockfd;
handler_t _handlerstream;
};
这里采用多进程模型,每个连接由一个子进程处理,实现简单且稳定,适合入门学习。



2.2 中间层:HTTP 协议处理
这是整个服务器的核心,负责将 TCP 收到的字节流解析成结构化的 HTTP 请求,再将结构化的响应序列化成字节流发送出去。
三. 核心模块实现:从字节流到 HTTP 请求(有的上篇博客里有)
3.1 HTTP 请求反序列
我们需要将原始的 HTTP 字节流解析成HttpRequest对象:
// HttpProtocol.hpp HttpRequest类核心方法
void Deserialize(std::string &streamstr)
{
// 1. 读取并解析请求行
std::string request_line;
ReadOneLine(streamstr, &request_line);
ParseLine(request_line);
// 2. 解析请求头
std::string line;
while(ReadOneLine(streamstr, &line) > 0)
{
std::string key, value;
SplitString(line, &key, &value);
if(!key.empty() && !value.empty())
{
_request_headerkv[key] = value;
}
}
// 3. 解析请求体(如果有)
if(_request_headerkv.count("Content-Length"))
{
int len = std::stoi(_request_headerkv["Content-Length"]);
_body = streamstr.substr(0, len);
// POST方法参数在请求体中
if(_method == "POST" || _method == "post")
{
_args = _body;
}
}
}
// 解析请求行,提取方法、URI、版本,并处理参数
void ParseLine(std::string &request_line)
{
std::stringstream ss(request_line);
ss >> _method >> _uri >> _http_version;
// 处理根路径,默认返回index.html
if(_uri == "/") {
_path = webroot + _uri + homepage;
} else {
_path = webroot + _uri;
}
// GET方法参数在URL中
if(_method == "GET" || _method == "get")
{
auto pos = _path.find(argsep); // argsep = "?"
if(pos != std::string::npos) {
_args = _path.substr(pos + argsep.size());
_path = _path.substr(0, pos); // 分离路径和参数
}
}
// 提取文件后缀,用于设置Content-Type
auto pos = _path.rfind(suffixsep); // suffixsep = "."
_suffix = (pos == std::string::npos) ? ".html" : _path.substr(pos);
}
关键设计:
- 统一了 GET 和 POST 的参数获取方式,上层通过
req["args"]即可获取所有参数(为啥可以获得看过之前的博客就懂了) - 自动处理根路径重定向到首页
- 提取文件后缀,为后续静态资源服务做准备
3.2 HTTP 响应序列化
将HttpResponse对象序列化成符合 HTTP 协议的字节流:
// HttpProtocol.hpp HttpResponse类核心方法
void Serialize(std::string *outstr)
{
// 1. 构造状态行
std::string status_line = _http_version + gspace +
std::to_string(_status_code) + gspace +
_status_code_desc + linesep;
// 2. 构造响应头
std::string response_header;
for(auto& it : _response_headerkv)
{
response_header += it.first + headersep + it.second + linesep;
}
// 3. 拼接完整响应:状态行 + 响应头 + 空行 + 响应体
*outstr = status_line + response_header + _blankline + _body;
}
3.3 静态资源服务
静态资源(HTML、CSS、JS、图片等)是 Web 服务器最基础的功能:
// HttpServer.hpp 静态资源处理核心代码
std::string GetFileContentHelper(const std::string &fileurl)
{
std::ifstream in(fileurl, std::ios::binary); // 二进制方式打开,避免图片损坏
if(!in.is_open()) return std::string();
// 获取文件大小并一次性读取
in.seekg(0, in.end);
int filesize = in.tellg();
in.seekg(0, in.beg);
std::string content;
content.resize(filesize);
in.read(content.data(), filesize);
in.close();
return content;
}
// 文件后缀到MIME类型的映射
std::string Suffix2Type(const std::string &suffix)
{
static const std::unordered_map<std::string, std::string> mime_map = {
{".html", "text/html"}, {".css", "text/css"},
{".js", "application/javascript"}, {".jpg", "image/jpeg"},
{".png", "image/png"}, {".json", "application/json"},
// 更多类型省略...
};
auto it = mime_map.find(suffix);
return (it != mime_map.end()) ? it->second : "application/octet-stream";
}
3.4 动态路由与服务注册
这是让 HTTP 服务器具备动态交互能力的关键。我们通过一个哈希表维护路由映射:
// HttpServer.hpp 路由相关代码
using route_t = std::function<void(const HttpRequest &req, HttpResponse &resp)>;
class HttpServer
{
public:
// 注册服务接口
void Register(std::string uri, route_t handler)
{
std::string key = webroot + uri; // 统一加上webroot前缀
_route[key] = handler;
}
std::string HandlerHttpRequest(std::string &streamstr)
{
HttpRequest httpreq;
httpreq.Deserialize(streamstr);
HttpResponse httpresp;
// 判断是否是动态路由请求
if(IsNeedRoute(httpreq["path"]))
{
_route[httpreq["path"]](httpreq, httpresp); // 调用注册的处理函数
}
else
{
// 处理静态资源
std::string filecontent = GetFileContentHelper(httpreq["path"]);
std::string suffix = httpreq["suffix"];
if(filecontent.empty())
{
// 返回404页面
std::string file404 = GetFileContentHelper("wwwroot/404.html");
httpresp.SetCode(404);
httpresp.SetHeader("Content-Length", file404.size());
httpresp.SetHeader("Content-Type", "text/html");
httpresp.SetBody(file404);
}
else
{
httpresp.SetCode(200);
httpresp.SetHeader("Content-Length", filecontent.size());
httpresp.SetHeader("Content-Type", Suffix2Type(suffix));
httpresp.SetBody(filecontent);
}
}
httpresp.SetHeader("Connection", "close"); // 短连接模式
std::string httprespstr;
httpresp.Serialize(&httprespstr);
return httprespstr;
}
private:
bool IsNeedRoute(const std::string &key)
{
return _route.find(key) != _route.end();
}
std::unordered_map<std::string, route_t> _route; // 路由表
};
四. 实战:搭建一个完整的 Web 服务(附加所有完整代码,前面不想看的可以直接看这里)
现在我们可以用自己写的 HTTP 服务器搭建一个包含登录、注册功能的 Web 服务,声明一下,我们这里就不展示其中我们封装好的组件代码了,这些之前都有讲过,前端代码这里也不展示了。
Main.cpp
#include "HttpServer.hpp"
#include <memory>
// @brief 打印程序的使用说明
// @param procname 当前运行的程序名称(通常是 argv[0])
void Usage(std::string procname)
{
// 提示用户运行程序时需要附带端口号参数
std::cout << "Usage: " << procname << " ServerPort" << std::endl;
}
// =========================================================
// 以下是具体的业务逻辑处理函数 (Callback Handlers)
// 当底层的 HttpServer 匹配到对应的路由路径时,会回调这些函数
// =========================================================
// @brief 处理登录请求的业务函数
void Login(const HttpRequest &req, HttpResponse &resp)
{
// 1. 从 HTTP 请求对象中获取由前端传递过来的参数信息 (通常在 URL 或 Body 中解析得出)
std::string args = req["args"];
std::cout << "Login service: haha: " << args << std::endl;
// 2. 准备要返回给客户端的 HTTP 响应体数据
std::string body = "Login success!\n";
// 3. 构建 HTTP 响应 (HttpResponse)
resp.SetCode(200); // 设置 HTTP 状态码为 200 OK,表示请求成功
resp.SetHeader("Content-Type", "text/plain"); // 告诉客户端返回的内容类型是纯文本
resp.SetHeader("Content-Length", body.size()); // 设置响应体的长度,这是 HTTP 协议标准要求
resp.SetHeader("Connection", "close"); // 告知客户端本次交互结束后关闭 TCP 连接 (短连接模式)
resp.SetBody(body); // 将具体数据写入响应体
}
// @brief 处理注册请求的业务函数
void Register(const HttpRequest &req, HttpResponse &resp)
{
// 提取请求参数并打印日志,便于服务端调试
std::string args = req["args"];
std::cout << "-----> Register service, args: " << args << std::endl;
// 注册通常代表在服务端创建了新资源,按 RESTful 规范返回 201 Created 状态码
resp.SetCode(201);
}
// @brief 处理搜索请求的业务函数
void Search(const HttpRequest &req, HttpResponse &resp)
{
std::string args = req["args"];
std::cout << "-----> Search service, args: " << args << std::endl;
// 提示:此处目前仅做了日志打印,实际开发中需要补充构建 HttpResponse 的逻辑 (例如查询数据库并返回结果)
}
// @brief 处理调用大模型请求的业务函数
void CallBigModel(const HttpRequest &req, HttpResponse &resp)
{
std::string args = req["args"];
std::cout << "-----> CallBigModel service, args: " << args << std::endl;
// 提示:此处实际开发中可能会涉及网络请求其他大模型 API,并处理异步/等待逻辑
}
// =========================================================
// 主函数:服务器程序的入口点
// 负责参数校验、环境初始化、路由注册以及启动服务
// =========================================================
int main(int argc, char *argv[])
{
// 1. 命令行参数校验:强制要求运行时必须提供端口号
if (argc != 2)
{
Usage(argv[0]); // 如果参数不对,打印正确用法并退出程序
exit(1);
}
// 2. 初始化环境
ENABLE_CONSOLE_LOG_STRATEGY(); // 宏调用:启用向控制台输出日志的策略
uint16_t port = std::stoi(argv[1]); // 将命令行传入的端口号字符串转换为 uint16_t 整数
// 3. 实例化 HTTP 服务器
// 使用 std::unique_ptr 管理 HttpServer 对象的生命周期,防止内存泄漏
// 我们现在这里不用lambda了,在HttpServer中实现了
std::unique_ptr<HttpServer> hsvr = std::make_unique<HttpServer>(port);
// 4. 路由注册 (URL Mapping)
// 将特定的 URI 路径与上面定义的业务逻辑函数绑定起来
// 当客户端请求这些路径时,HttpServer 内部会自动调用对应的函数
hsvr->Register("/app/login", Login);
hsvr->Register("/app/register", Register);
hsvr->Register("/app/search", Search);
hsvr->Register("/app/model", CallBigModel);
// 5. 启动服务器,进入事件循环,开始监听和处理网络请求
hsvr->Run();
return 0;
}
HttpServer.hpp
#ifndef __HTTPSERVER__HPP
#define __HTTPSERVER__HPP
#include <cstdint>
#include <fstream>
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
#include "TcpServer.hpp"
#include "Logger.hpp"
#include "HttpProtocol.hpp"
using namespace LogModule;
// @brief 路由回调函数类型定义
// 使用 std::function 定义了一个统一的接口,任何业务逻辑(如 Login、Search 等)
// 只要满足参数是 (HttpRequest, HttpResponse),就可以被注册到路由表中
using route_t = std::function<void(const HttpRequest &req, HttpResponse &resp)>;
class HttpServer
{
public:
// @brief 构造函数,初始化 HTTP 服务器
// 在这里实例化了底层的 TcpServer,HTTP 服务完全依赖这个 TCP 实例进行网络数据的收发
HttpServer(uint16_t port)
: _port(port)
, _tsvr(std::make_unique<TcpServer>(port))
{}
// @brief 核心处理器:将 TCP 收到的纯字节流,转化为 HTTP 请求,并生成响应字节流
std::string HandlerHttpRequest(std::string &streamstr)
{
// 1. 检查报文完整性 -- 我们今天默认报文是完整的, 这里就不处理了
// 2. 对收到的请求进行反序列化
HttpRequest httpreq;
// @brief 反序列化:将网络中收到的扁平字符串(streamstr),解析成有结构的 HttpRequest 对象
// 这样后续就可以通过键值对的方式方便地获取 method、path 等信息
httpreq.Deserialize(streamstr);
std::cout << "method: " << httpreq["method"]<< std::endl;
std::cout << "path: " << httpreq["path"]<< std::endl;
std::cout << "args: " << httpreq["args"]<< std::endl;
// 3. httpreq -> httpresp
HttpResponse httpresp;
// 我们想测试重定向的话 -- 下面我们404页面的方法2其实也测试到了
// httpresp.SetCode(302);
// httpresp.SetHeader("Location", "https://www.qq.com/");
// 处理动态资源
// @brief 路由分发:如果客户端请求的路径(path)在我们的注册表(_route)中存在,说明这是一个后端业务接口
if (IsNeedRoute(httpreq["path"]))
{
// 直接调用对应的业务回调函数(例如上一份代码里的 Login/Search),将控制权交给上层
_route[httpreq["path"]](httpreq, httpresp);
}
else
{
// 处理静态资源
// @brief 如果不在路由表中,说明客户端只是想要请求一个普通的网页文件、图片或 CSS 等
std::string filecontent = GetFileContentHelper(httpreq["path"]);
std::string suffix = httpreq["suffix"];
if (filecontent.empty()) {
// 如果为空, 返回404页面
// @brief 找不到文件时的异常处理(Not Found)
std::string page404 = "wwwroot/404.html";
httpresp.SetCode(404);
std::string file404 = GetFileContentHelper(page404);
suffix = ".html";
// 构建 404 响应的 Header 报头
httpresp.SetHeader("Content-Length", file404.size());
httpresp.SetHeader("Content-Type", Suffix2Type(suffix));
httpresp.SetHeader("Connection", "close");
httpresp.SetBody(file404);
}
else
{
// @brief 文件成功找到,构建标准的 200 OK 响应
httpresp.SetCode(200);
httpresp.SetHeader("Content-Length", filecontent.size());
httpresp.SetHeader("Content-Type", Suffix2Type(suffix)); // 根据文件后缀自动推导 MIME 类型
httpresp.SetHeader("Connection", "close"); // 应答告诉浏览器我是短链接
httpresp.SetBody(filecontent);
}
}
// 4. 应答进行序列化
std::string httprespstr;
// @brief 序列化:将我们刚刚填充好的 HttpResponse 结构体对象,重新打包成符合 HTTP 标准规范的纯字符串格式
httpresp.Serialize(&httprespstr); // 带出来
// 5. 返回
// @brief 将序列化后的字符串返回,这串数据最终会被底层的 TcpServer 发送给客户端(浏览器)
return httprespstr;
}
// @brief 启动服务器
void Run()
{
// @brief 经典的回调闭包:TcpServer 收到数据后,会调用传入的这个 lambda 表达式。
// 而这个 lambda 内部又调用了本类的 HandlerHttpRequest 方法,实现了 TCP 层到 HTTP 层的联动。
_tsvr->Run([this](std::string &streamstr){
return this->HandlerHttpRequest(streamstr);
});
}
// @brief 路由注册接口,供 Main 函数调用,用于挂载后端业务功能
void Register(std::string uri, route_t handler)
{
// @note 注意:这里的 webroot 变量需要在前面有定义(可能是全局或宏),用于拼接完整的映射路径
std::string key = webroot + uri;
_route[key] = handler;
}
~HttpServer()
{}
private:
// @brief 检查当前请求路径是否需要走动态路由逻辑
bool IsNeedRoute(const std::string &key)
{
return _route.find(key) != _route.end();
}
// @brief 辅助函数:根据文件路径读取整个文件的内容到字符串中
std::string GetFileContentHelper(const std::string &fileurl)
{
std::ifstream in(fileurl);
if(!in.is_open())
{
return std::string();
}
// 获取文件长度
// @brief C++ 常见的文件大小测算方式:将文件读写指针移动到末尾(end),获取当前偏移量即为大小,再移回头部(beg)
in.seekg(0, in.end);
int filesize = in.tellg();
in.seekg(0, in.beg);
// 把文件内容全部读进Content
std::string content;
content.resize(filesize); // 提前开辟好空间,避免读取时频繁扩容
in.read(content.data(), filesize); // 用data,cpp17 (C++17 允许直接往 string 的 data 指针写入数据)
in.close();
return content;
}
// suffix: .html
// return: text/html
// @brief MIME 类型映射转换函数,用于告诉浏览器返回的正文内容到底是什么类型的数据
std::string Suffix2Type(const std::string &suffix)
{
// 使用静态映射表提高效率,避免每次调用都重新创建
// @brief static 关键字确保这巨大的哈希表在整个程序运行期间只会被初始化一次,极大地节省了性能开销
static const std::unordered_map<std::string, std::string> mime_map = {
// 文本类型
{".html", "text/html"},
{".htm", "text/html"},
{".css", "text/css"},
{".js", "application/javascript"},
{".mjs", "application/javascript"},
{".json", "application/json"},
{".xml", "application/xml"},
{".txt", "text/plain"},
{".csv", "text/csv"},
{".md", "text/markdown"},
// 图片类型
{".jpg", "image/jpeg"},
{".jpeg", "image/jpeg"},
{".png", "image/png"},
{".gif", "image/gif"},
{".bmp", "image/bmp"},
{".webp", "image/webp"},
{".svg", "image/svg+xml"},
{".ico", "image/x-icon"},
{".tiff", "image/tiff"},
{".tif", "image/tiff"},
// 字体类型
{".woff", "font/woff"},
{".woff2", "font/woff2"},
{".ttf", "font/ttf"},
{".otf", "font/otf"},
// 视频类型
{".mp4", "video/mp4"},
{".webm", "video/webm"},
{".ogv", "video/ogg"},
{".avi", "video/x-msvideo"},
{".mov", "video/quicktime"},
{".mpeg", "video/mpeg"},
{".mpg", "video/mpeg"},
// 音频类型
{".mp3", "audio/mpeg"},
{".wav", "audio/wav"},
{".ogg", "audio/ogg"},
{".flac", "audio/flac"},
{".m4a", "audio/mp4"},
{".aac", "audio/aac"},
// 应用类型
{".pdf", "application/pdf"},
{".zip", "application/zip"},
{".rar", "application/vnd.rar"},
{".7z", "application/x-7z-compressed"},
{".tar", "application/x-tar"},
{".gz", "application/gzip"},
{".exe", "application/vnd.microsoft.portable-executable"},
{".dll", "application/x-msdownload"},
{".msi", "application/x-msi"},
{".doc", "application/msword"},
{".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
{".xls", "application/vnd.ms-excel"},
{".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
{".ppt", "application/vnd.ms-powerpoint"},
{".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"},
{".wasm", "application/wasm"},
// 其他常见类型
{".jsonld", "application/ld+json"},
{".rss", "application/rss+xml"},
{".atom", "application/atom+xml"},
{".manifest", "text/cache-manifest"},
{".map", "application/json"}, // source maps
{".ts", "video/mp2t"}, // MPEG transport stream
{".m3u8", "application/vnd.apple.mpegurl"}
};
auto it = mime_map.find(suffix);
if (it != mime_map.end())
{
return it->second;
}
// 默认返回二进制流,或根据需求返回其他默认值
// @brief 当遇到不认识的文件后缀时,作为“通用二进制数据”返回,此时浏览器通常会直接触发下载行为
return "application/octet-stream";
}
private:
uint16_t _port; // 服务器绑定的端口
std::unique_ptr<TcpServer> _tsvr; // 独占底层的 TCP 服务器指针,负责真正的网络通信
std::unordered_map<std::string, route_t> _route; // 注册服务 (哈希表:实现 URI 到回调函数的映射)
};
#endif
HttpProtocol.hpp
#ifndef __HTTPPROTOCOL__HPP
#define __HTTPPROTOCOL__HPP
#include <functional>
#include <iostream>
#include <string>
#include <unordered_map>
#include <sstream>
#include "Logger.hpp"
using namespace LogModule;
// =========================================================
// @brief 定义 HTTP 协议中常用的一些标准分隔符和默认常量
// =========================================================
const std::string linesep = "\r\n"; // HTTP 报文的标准换行符 (CRLF)
const std::string headersep = ": "; // HTTP 报头中键值对的分隔符
const std::string webroot = "wwwroot"; // Web 服务器的根目录,存放 HTML/CSS/图片等静态资源
const std::string homepage = "index.html"; // 默认的主页文件名 (访问 / 时自动替换)
const std::string gdefaulthttpversion = "HTTP/1.0"; // 默认响应的 HTTP 版本
const std::string gspace = " "; // 状态行/请求行中的空格分隔符
const std::string suffixsep = "."; // 文件后缀的分隔符 (用于后续推导 Content-Type)
const std::string argsep = "?"; // URL 中携带 GET 传参的分隔符
// =========================================================
// @brief HTTP 请求类:负责将网络收到的原始字节流,反序列化解析成结构化数据
// =========================================================
class HttpRequest
{
private:
// @brief 工具函数:从网络数据流中提取出一整行
// @param streamstr 原始的网络数据流 (传引用,提取完一行后会从原始流中把该行删掉)
// @param line 用于存放提取出来的一行字符串 (不包含 \r\n)
int ReadOneLine(std::string &streamstr, std::string *line)
{
// 我们先去找 linesep
auto pos = streamstr.find(linesep);
if(pos == std::string::npos)
return -1; // 出现了问题
// 走到这里就读到了一行 -- 我们需要处理一下
// a. 先拿到这行
*line = streamstr.substr(0, pos);
// b. 删掉原始字符串中的这一行, 加上分隔符号
streamstr = streamstr.erase(0, pos + linesep.size());
// n == 0 证明读到了空行
return line->size();
}
// @brief 解析 HTTP 请求行 (即报文的第一行),例如: "GET /index.html?name=zhangsan HTTP/1.1"
void ParseLine(std::string &request_line)
{
// 使用 stringstream 按空格自动拆分出 方法、URI 和 版本号
std::stringstream ss(request_line);
ss >> _method >> _uri >> _http_version;
// @brief 路径映射逻辑:将前端请求的 URI 映射到服务器本地磁盘的真实路径
if(_uri == "/")
{
_path = webroot + _uri + homepage; // 如果只访问 /,默认拼接出 wwwroot/index.html
}
else
{
_path = webroot + _uri;
}
// /content.html?username=zhangsan&passwd=123456
// @brief GET 方法参数提取:GET 的参数拼接在 URI 里面,以 ? 作为分隔
if(_method == "GET" || _method == "get")
{
auto pos = _path.find(argsep);
if(pos != std::string::npos)
{
_args = _path.substr(pos + argsep.size()); // 提取 ? 后面的所有内容作为参数
_path = _path.substr(0, pos); // 截断 ? 之前的内容,保留纯净的文件路径
}
}
// 解析后缀
// 分析请求的资源的后缀! // wwwroot + / + index.html wwwroot/image/th.jpg wwwroot/css/XXX.css
auto pos = _path.rfind(suffixsep); // 从右向左找点号 "."
if(pos == std::string::npos)
{
_suffix = ".html"; // 如果没找到后缀,默认当成 html 网页处理
}
else
{
_suffix = _path.substr(pos); // .css .jpg ....
}
std::cout << "=======: PATH: " << _path << std::endl;
}
// @brief 将 HTTP 报头的一行按 ": " 拆解为 key 和 value (例如 "Content-Length: 100")
void SplitString(std::string &line, std::string *key, std::string *value, const std::string sep = headersep)
{
// a. 先找分隔符号
auto pos = line.find(sep);
if(pos == std::string::npos)
return;
*key = line.substr(0, pos);
*value = line.substr(pos + sep.size());
}
void PrintDebug()
{
// 打印进行观察
std::cout << "reqline:" << _method << "#" << _uri << "#" << _http_version << std::endl;
for (auto &item : _request_headerkv)
{
std::cout << item.first << "->" << item.second << std::endl;
}
std::cout << "blankline: " << _blankline << std::endl;
std::cout << "body: " << _body << std::endl;
std::cout << "suffix: " << _suffix << std::endl;
}
public:
HttpRequest(){}
// @brief 核心方法:反序列化
// 严格按照 HTTP 报文格式解析: 1.请求行 -> 2.请求报头 -> 3.空行 -> 4.请求正文
void Deserialize(std::string &streamstr)
{
// 1. 我们先读取第一行 -- 请求行
std::string request_line;
// streamstr给的引用, 是因为里面要找到一行就删掉这一行
// request_line给的是指针, 我们可以通过这个拿到读取到的
int n = ReadOneLine(streamstr, &request_line);
// 可以进行简单的判断,我这里就不处理了
(void)n;
// 2. 解析请求行 (拆解方法、路径、参数等)
ParseLine(request_line);
// 3. 继续去解析其他行 (循环读取 HTTP 请求头,直到遇到空行)
n = 0;
do
{
std::string line;
n = ReadOneLine(streamstr, &line);
if(n > 0)
{
// 这里就需要处理
std::string key, value;
SplitString(line, &key, &value, headersep);
// 这里判断一下
if(!key.empty() && !value.empty())
{
_request_headerkv[key] = value; // 存进去 (存入哈希表方便后续按 Key 查找)
}
}
else if(n < 0)
{
LOG(LogLevel::DEBUG) << "ReadOneLine, bug?";
break;
}
else
{
// @brief 读到长度为 0 的行,说明碰到了 HTTP 协议规定的空行(\r\n)
// 空行意味着 Header 结束,后面就是 Body(如果有的话)
_blankline = "\r\n";
break;
}
}while (n > 0);
// 4. 判断并提取请求正文 (Body)
if(_request_headerkv.find("Content-Length") != _request_headerkv.end())
{
int len = std::stoi(_request_headerkv["Content-Length"]); // 去哈希表里面找这个属性看看有没有 -- 判断有没有正文
_body = streamstr.substr(0, len); // 可以这样找, 是因为前面我们都是找一行删一行
// @brief 如果是 POST 方法,说明数据放在了正文里,将其提取到 _args 变量中统一处理
if(_method == "POST" || _method == "post")
{
_args = _body;
}
}
PrintDebug();
}
// 方便外部可以获取我请求里面的东西
// @brief 运算符重载,提供一个统一、优雅的接口给外界获取各种请求参数
std::string operator[](const std::string& key) const
{
if(key == "method")
return _method;
else if(key == "uri")
return _uri;
else if(key == "httpversion")
return _http_version;
else if(key == "body")
return _body;
else if(key == "path")
return _path;
else if(key == "suffix")
return _suffix;
else if(key == "args")
return _args;
else
// 如果不是基础属性,则去 Header 的哈希表里找 (例如提取 User-Agent, Host 等)
{
auto iter = _request_headerkv.find(key);
if(iter != _request_headerkv.end())
return iter->second;
}
// 再就是直接返回空串了
return std::string();
}
~HttpRequest(){}
private:
std::string _method;
std::string _uri; // URL -> a/b/c/index.html
std::string _http_version;
std::unordered_map<std::string,std::string> _request_headerkv; // 存放 Headers 的哈希表
std::string _blankline; // 协议规定的空行
std::string _body; // 报文正文
private:
std::string _path; // 映射到服务端的真实文件路径
std::string _suffix; // 后缀
std::string _args; // 提参 (无论是 GET 还是 POST 的参数最终都提取到这里)
};
// =========================================================
// @brief HTTP 响应类:提供接口供业务代码设置返回内容,并负责最终序列化成字符串发往网络
// =========================================================
class HttpResponse
{
public:
// 构造时默认使用 HTTP/1.0 并且设置好标准的空行
HttpResponse(): _http_version(gdefaulthttpversion), _blankline(linesep)
{}
// @brief 核心方法:序列化
// 严格按照 HTTP 响应报文格式拼接: 1.状态行 -> 2.响应头 -> 3.空行 -> 4.响应正文
void Serialize(std::string *outstr)
{
// 1. 拼接状态行 (例如: "HTTP/1.0 200 OK\r\n")
std::string status_line = _http_version + gspace + std::to_string(_status_code) + gspace + _status_code_desc + linesep;
// 2. 拼接响应报头
std::string response_header;
for(auto& it: _response_headerkv)
{
std::string header = it.first + headersep + it.second + linesep;
response_header += header;
}
// 3. 最终整体拼接 (带上空行和正文)
*outstr = status_line + response_header + _blankline + _body;
}
// @brief 设置响应正文内容
void SetBody(const std::string &content)
{
_body = content;
}
// @brief 设置 HTTP 状态码,并自动映射其对应的英文状态描述
void SetCode(int code)
{
_status_code = code;
_status_code_desc = Code2Desc(code);
}
// @brief 设置响应 Header (字符串值)
void SetHeader(const std::string &key, const std::string &value)
{
_response_headerkv[key] = value;
}
// @brief 设置响应 Header (整型值重载,例如用于设置 Content-Length: 1024)
void SetHeader(const std::string &key, int value)
{
_response_headerkv[key] = std::to_string(value);
}
~HttpResponse(){}
private:
// @brief HTTP 状态码字典:根据状态码返回对应的英文标准描述短语
std::string Code2Desc(int code)
{
switch (code)
{
// 1xx: 信息响应
case 100:
return "Continue";
case 101:
return "Switching Protocols";
case 102:
return "Processing"; // WebDAV
case 103:
return "Early Hints";
// 2xx: 成功
case 200:
return "OK";
case 201:
return "Created";
case 202:
return "Accepted";
case 203:
return "Non-Authoritative Information";
case 204:
return "No Content";
case 205:
return "Reset Content";
case 206:
return "Partial Content";
case 207:
return "Multi-Status"; // WebDAV
case 208:
return "Already Reported";
case 226:
return "IM Used";
// 3xx: 重定向
case 300:
return "Multiple Choices";
case 301:
return "Moved Permanently";
case 302:
return "Found";
case 303:
return "See Other";
case 304:
return "Not Modified";
case 305:
return "Use Proxy";
case 306:
return "Switch Proxy"; // 已废弃,但仍保留
case 307:
return "Temporary Redirect";
case 308:
return "Permanent Redirect";
// 4xx: 客户端错误
case 400:
return "Bad Request";
case 401:
return "Unauthorized";
case 402:
return "Payment Required";
case 403:
return "Forbidden";
case 404:
return "Not Found";
case 405:
return "Method Not Allowed";
case 406:
return "Not Acceptable";
case 407:
return "Proxy Authentication Required";
case 408:
return "Request Timeout";
case 409:
return "Conflict";
case 410:
return "Gone";
case 411:
return "Length Required";
case 412:
return "Precondition Failed";
case 413:
return "Payload Too Large";
case 414:
return "URI Too Long";
case 415:
return "Unsupported Media Type";
case 416:
return "Range Not Satisfiable";
case 417:
return "Expectation Failed";
case 418:
return "I'm a teapot"; // 愚人节笑话,但常被实现
case 421:
return "Misdirected Request";
case 422:
return "Unprocessable Entity"; // WebDAV
case 423:
return "Locked";
case 424:
return "Failed Dependency";
case 425:
return "Too Early";
case 426:
return "Upgrade Required";
case 428:
return "Precondition Required";
case 429:
return "Too Many Requests";
case 431:
return "Request Header Fields Too Large";
case 451:
return "Unavailable For Legal Reasons";
// 5xx: 服务端错误
case 500:
return "Internal Server Error";
case 501:
return "Not Implemented";
case 502:
return "Bad Gateway";
case 503:
return "Service Unavailable";
case 504:
return "Gateway Timeout";
case 505:
return "HTTP Version Not Supported";
case 506:
return "Variant Also Negotiates";
case 507:
return "Insufficient Storage"; // WebDAV
case 508:
return "Loop Detected";
case 510:
return "Not Extended";
case 511:
return "Network Authentication Required";
// 未知状态码
default:
return "Unknown";
}
}
private:
std::string _http_version;
int _status_code;
std::string _status_code_desc;
std::unordered_map<std::string,std::string> _response_headerkv;
std::string _blankline;
std::string _body;
};
#endif
五. 进阶:HTTP 常见 Header 全解析
HTTP 头是客户端和服务器之间传递元数据的重要方式,分为请求头和响应头两大类。以下是生产环境中最常用的 10 个 Header:
5.1 通用请求头
| Header | 作用 | 示例 | 应用场景 |
|---|---|---|---|
| Host | 指定请求的主机和端口,用于虚拟主机 | Host: www.baidu.com:80 |
一台服务器部署多个网站 |
| User-Agent | 客户端信息,包括操作系统、浏览器版本 | User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) |
反爬、适配不同设备 |
| Referer | 表示当前请求来自哪个页面 | Referer: http://127.0.0.1:8080/index.html |
防盗链、统计用户来源、CSRF 防护 |
| Accept | 客户端能接受的媒体类型 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 |
内容协商 |
| Accept-Encoding | 客户端支持的压缩编码 | Accept-Encoding: gzip, deflate |
减少传输数据量 |
| Accept-Language | 客户端偏好的语言 | Accept-Language: zh-CN,zh;q=0.9 |
多语言网站 |
| Cookie | 客户端存储的会话信息 | Cookie: JSESSIONID=D628A75845A74D29D991D47461E4FC |
会话管理、用户登录状态 |
5.2 通用响应头
| Header | 作用 | 示例 | 应用场景 |
|---|---|---|---|
| Content-Type | 响应体的 MIME 类型 | Content-Type: application/json |
告诉客户端如何解析响应体 |
| Content-Length | 响应体的字节长度 | Content-Length: 1024 |
客户端知道何时接收完成 |
| Location | 配合 3xx 状态码使用,指定重定向地址 | Location: https://www.qq.com/ |
页面跳转、登录后重定向 |
| Connection | 连接管理 | Connection: keep-alive |
长连接 / 短连接控制 |



5.3 关键 Header 实战应用
Referer 防盗链
防止其他网站直接引用你的图片、视频等资源:
// 在静态资源处理前添加Referer检查
std::string referer = httpreq["Referer"];
if(!referer.empty() && referer.find("yourdomain.com") == std::string::npos) {
// 返回默认防盗链图片
std::string default_img = GetFileContentHelper("wwwroot/default.jpg");
httpresp.SetCode(200);
httpresp.SetHeader("Content-Type", "image/jpeg");
httpresp.SetBody(default_img);
return;
}
Location 重定向
实现登录后跳转到首页:
void Login(const HttpRequest &req, HttpResponse &resp) {
std::string args = req["args"];
// 验证用户名密码成功
resp.SetCode(302); // 临时重定向
resp.SetHeader("Location", "/index.html");
}
Cookie 会话管理
实现用户登录状态保持:
void Login(const HttpRequest &req, HttpResponse &resp) {
std::string args = req["args"];
// 验证用户名密码成功
std::string session_id = GenerateSessionId(); // 生成唯一会话ID
// 设置Cookie,有效期1小时
resp.SetHeader("Set-Cookie", "session_id=" + session_id + "; Max-Age=3600; Path=/");
resp.SetCode(302);
resp.SetHeader("Location", "/index.html");
}
5.4 长连接 vs 短连接
- 短连接(HTTP/1.0 默认):每次请求都建立一个新的 TCP 连接,请求完成后立即关闭。优点是实现简单,缺点是频繁建立连接开销大。
- 长连接(HTTP/1.1 默认):一个 TCP 连接可以处理多个请求,减少了连接建立和关闭的开销。
实现长连接的关键:
- 服务器返回
Connection: keep-alive头 - 服务器不立即关闭 socket,继续等待下一个请求
- 正确处理多个请求的边界(通过
Content-Length或分块传输)
六. 生产级部署:Nginx 静态资源服务与反向代理
自己实现的 HTTP 服务器适合学习和小型项目,但在生产环境中,我们几乎都会使用Nginx作为前端服务器。Nginx 是一个高性能的 HTTP 和反向代理服务器,以高并发、低内存消耗著称。
6.1 Nginx 核心特点
| 特性 | 说明 |
|---|---|
| 高并发 | 采用异步非阻塞事件驱动架构,单机可处理数万甚至数十万并发连接 |
| 低内存消耗 | 相比 Apache 等传统服务器,内存占用极低 |
| 反向代理 | 可将请求分发到后端多个应用服务器(如 Tomcat、Node.js、我们自己写的 C++ 服务器) |
| 负载均衡 | 支持轮询、最少连接、IP 哈希等多种负载均衡算法 |
| 静态文件服务 | 处理静态资源(HTML/CSS/JS/ 图片)性能极高 |
| HTTPS 支持 | 可配置 SSL 证书,提供安全的 HTTPS 服务 |
6.2 Nginx 安装与基本操作
在 Ubuntu 系统中,我们可以通过 apt 包管理器快速安装:
# 安装Nginx
sudo apt install nginx
# 启动Nginx
sudo nginx
# 停止Nginx
sudo nginx -s stop
# 重新加载配置(不中断服务)
sudo nginx -s reload
# 查看Nginx进程
ps ajx | grep nginx
6.3 Nginx 核心文件路径
不同安装方式的 Nginx 文件路径略有不同,以下是 apt 安装的默认路径:
# 主配置文件
/etc/nginx/nginx.conf
# 站点配置目录
/etc/nginx/conf.d/
/etc/nginx/sites-enabled/
# 二进制文件
/usr/sbin/nginx
# 日志文件
/var/log/nginx/access.log # 访问日志
/var/log/nginx/error.log # 错误日志
# 默认静态资源根目录
/var/www/html/
现在访问http://你的服务器IP,就能看到我们自己写的页面了。
6.5 Nginx 安全配置:禁用不安全的 HTTP 方法
生产环境中,为了安全起见,我们通常会禁用除 GET 和 POST 之外的所有 HTTP 方法:
测试当前支持的方法:
curl -X OPTIONS -i http://127.0.0.1/
如果返回405 Not Allowed,说明已经禁用;如果返回200 OK和Allow头,说明支持。
禁用不安全方法的配置:
编辑/etc/nginx/nginx.conf,在http块中添加以下内容:
server {
listen 80;
server_name localhost;
# 只允许GET和POST方法
if ($request_method !~ ^(GET|POST)$ ) {
return 405;
}
location / {
root /var/www/html;
index index.html index.htm;
}
}
保存后重新加载配置:
sudo nginx -s reload
6.6 Nginx 反向代理:转发动态请求
我们可以将 Nginx 作为反向代理,将动态请求转发到我们自己写的 C++ HTTP 服务器:
server {
listen 80;
server_name localhost;
# 静态资源由Nginx直接处理
location / {
root /var/www/html;
index index.html index.htm;
}
# 动态请求转发到后端C++服务器
location /api/ {
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
这样所有以/api/开头的请求都会被转发到运行在 8080 端口的 C++ 服务器,而静态资源则由 Nginx 直接处理,充分发挥 Nginx 的性能优势。



七. 生产级实践:使用 cpp-httplib 快速开发
虽然自己实现 HTTP 服务器能深入理解原理,但在实际项目中,我们更推荐使用成熟的第三方库。cpp-httplib是一个仅头文件、无依赖、接口优雅的 C++ HTTP 库,非常适合快速开发。
#include "httplib.h" // 引入 cpp-httplib 库,这是一个非常优秀的轻量级 C++ HTTP 框架 (Header-only)
int main()
{
// @brief 实例化一个 HTTP 服务器核心对象
httplib::Server svr;
// =========================================================
// 模块一:静态资源托管 (取代了你以前手写读取文件的逻辑)
// =========================================================
// 1. 托管静态资源:将 ./static 目录下的文件作为静态资源服务
// 访问 http://localhost:8080/index.html 会返回 ./static/index.html
// @note 补充说明:你的代码中实际将 URL 的根路径 "/" 映射到了上一级目录的 "../wwwroot"。
// 这意味着,只要浏览器请求的不是动态路由(如 /search),httplib 就会自动去 ../wwwroot 文件夹下找同名文件并返回给前端,极大简化了开发。
svr.set_mount_point("/", "../wwwroot");
// =========================================================
// 模块二:动态路由与业务回调
// =========================================================
// 2. 提供搜索接口(回调路由)
// 访问 http://localhost:8080/search?q=关键字
// @brief 注册 GET 请求路由。当匹配到 "/search" 路径时,自动触发后面的 Lambda 匿名函数。
// - req (Request): 框架已经帮你反序列化好的请求对象,里面有前端传来的参数、报头等。
// - res (Response): 你需要将处理结果装填到这个对象里,框架最后会自动将其序列化发给浏览器。
svr.Get("/search", [](const httplib::Request& req, httplib::Response& res) {
// --- ⬇️ 以下是你注释掉的完整业务逻辑演示(提取参数 -> 校验 -> 构建JSON返回) ⬇️ ---
// 获取查询参数 q
// std::string query = req.get_param_value("q");
// if (query.empty()) {
// res.set_content(R"({"error": "请提供搜索关键字 q"})", "application/json");
// return;
// }
// // 模拟搜索结果(实际可从数据库查询)
// std::string results = R"([
// {"title": "关于 )" + query + R"( 的第一条结果", "url": "/page1"},
// {"title": "关于 )" + query + R"( 的第二条结果", "url": "/page2"},
// {"title": "关于 )" + query + R"( 的第三条结果", "url": "/page3"}
// ])";
// res.set_content(results, "application/json");
// --- ⬆️ 注释结束 ⬆️ ---
// @brief 当前生效的代码:无论前端查什么,直接暴力返回纯文本 "hello result"
// 参数2 "text/plain" 设置了 Content-Type,告诉浏览器不用解析,直接显示文字即可。
res.set_content("hello result", "text/plain");
});
// =========================================================
// 模块三:启动服务器
// =========================================================
// 启动服务
std::cout << "Server started at http://localhost:9090" << std::endl;
// @brief 绑定 IP 和端口并启动监听
// "0.0.0.0" 是一个特殊 IP,代表监听本机的所有网卡(这意味着无论是本地的 127.0.0.1,还是别人通过局域网/公网 IP 都能访问你的服务)。
// 注意:listen() 是一个阻塞函数 (死循环),程序运行到这里就会停住,专门处理不断进来的网络请求,直到你按下 Ctrl+C 才会退出。
svr.listen("0.0.0.0", 9090);
return 0;
}
可以看到,cpp-httplib 的接口比我们自己实现的更加简洁和强大,支持路径参数、文件上传、HTTPS 等高级功能。




结尾:
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!
结语:
✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)