很多开发者初学 HTTP 时,总觉得它是一个抽象的“高层”协议。但实际上,HTTP 底层完全基于 TCP,本质就是一套基于套接字的文本约定。本文将从零开始,手搓一个可运行的 HTTP 服务器,带你彻底搞懂请求解析、响应构建、URI 映射等核心原理。
1. HTTP 底层依托 TCP:从云服务到本地代码
HTTP 属于应用层协议,底层完全建立在 TCP 协议之上。无论是云平台上的高并发服务,还是本地开发测试,其本质都是 TCP 套接字通信。浏览器作为天然 HTTP 客户端,自动发送标准 HTTP 请求,而服务端只需解析字节流即可。
代码复用与解耦是关键。我们可以直接复用原有 TCP 组件:
TcpServer.hpp
Socket.hpp
InetAddr.hpp
Logger.hpp(日志)
Mutex.hpp(互斥锁)
与网络计算器项目对比,仅上层应用协议不同,网络通信层完全一致。这种解耦设计让协议替换变得简单,也便于后续向云原生架构迁移。整体代码模块如下:
HttpProtocol.hpp:定义 HttpRequest、HttpResponse,负责反序列化(解析请求)与序列化(构建响应)HttpServer.hpp:HTTP 服务器主类,基于 TCP 封装,提供请求处理入口Main.cc:程序入口,完成服务器初始化、端口绑定、服务启动
2. HttpServer 实现与 TCP 联动
类结构定义是核心。TcpServer 仅负责网络 IO——创建套接字、绑定、监听、收发字节流;HttpServer 负责协议处理——解析请求、构建响应。二者通过回调函数联动,TCP 收到的字节流直接交给 HandlerHttpRequest 处理。
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include "TcpServer.hpp"
#include "Logger.hpp"
using namespace LogModule;
class HttpServer {
public:
HttpServer(uint16_t port)
: _port(port), _tsvr(std::make_unique<TcpServer>(port))
{}
std::string HandlerHttpRequest(std::string &streamstr);
void Run() {
_tsvr->Run([this](std::string &streamstr) -> std::string {
return this->HandlerHttpRequest(streamstr);
});
}
~HttpServer(){}
private:
uint16_t _port;
std::unique_ptr<TcpServer> _tsvr;
};
Main.cc 入口实现简洁明了:
#include "HttpServer.hpp"
void Usage(std::string procname) {
std::cout << "Usage:" << procname << " ServerPort" << std::endl;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
Usage(argv[0]);
exit(1);
}
ENABLE_CONSOLE_LOG_STRATEGY();
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<HttpServer> hsvr = std::make_unique<HttpServer>(port);
hsvr->Run();
return 0;
}
这种分层设计非常适用于云部署场景:你可以在云服务器上运行 TcpServer,而业务逻辑放在 HttpServer 中,便于独立扩展和运维。
3. HTTP 请求报文格式与解析
请求报文完整结构如下:
请求行\r\n
请求报头 Key: Value\r\n
请求报头 Key: Value\r\n
...\r\n
空行\r\n
请求正文 DATA
请求行格式为:请求方法 空格 URI 空格 HTTP 版本 \r\n,示例:GET / HTTP/1.1。请求报头是多行 Key: Value 结构,描述客户端信息、连接方式等。空行用于分隔报头与正文,是判断报头是否完整的标志。请求正文可选,长度由报头中的 Content-Length 字段决定。
HTTP 请求本质是一串按固定规则组织的字节流,分行显示仅因 \r\n 分隔符。请求行 + 请求报头 = 报头部分;请求正文 = 有效载荷部分。验证代码示例如下:
需要现场基于历史代码,先架构处⼀个基本的HTTP服务器,然后⽤浏览器进⾏验证
需要验证前端内容,直接AI即可
HttpRequest 类定义:
#ifndef _HTTP_PROTOCOL_HPP
#define _HTTP_PROTOCOL_HPP
#include <iostream>
#include <string>
#include <unordered_map>
class HttpRequest {
public:
HttpRequest(){}
~HttpRequest(){}
void Deserialize(std::string &streamstr);
void ParseReqLine(std::string &status_line);
int ReadOneline(std::string &streamstr, std::string *line);
void SplitString(std::string &line, std::string *key, std::string *value, std::string sep);
void PrintDebug();
std::string operator[](const std::string &key) const;
private:
std::string _method;
std::string _uri;
std::string _http_version;
std::unordered_map<std::string, std::string> _headerkv;
std::string _blankline;
std::string _body;
std::string _path;
};
class HttpResponse {
public:
HttpResponse(){}
~HttpResponse(){}
void Serialize(std::string *outstr);
private:
std::string _http_version;
int _status_code;
std::string _status_code_desc;
std::unordered_map<std::string, std::string> _resp_headerkv;
std::string _blankline;
std::string _body;
};
#endif
反序列化流程:
- 按行读取:以
\r\n为分隔符 - 解析请求行:通过
stringstream按空格拆分出方法、URI、HTTP 版本 - 解析请求报头:循环读取非空行,以 : 拆分
Key-Value存入unordered_map - 识别空行:读取到空行标志报头读取完毕
- 读取正文:根据
Content-Length截取剩余字符串 - 路径映射:将 URI 映射为本地 wwwroot 目录下的路径,
_path = webroot + _uri
operator [] 重载实现:
std::string HttpRequest::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
{
auto iter = _headerkv.find(key);
if (iter != _headerkv.end()) return iter->second;
}
return std::string();
}
未来在 Web 就可以访问 Request 的内容了(以 [] 的方式)。
4. HTTP 响应报文格式与构建
响应报文完整结构:
状态行\r\n
响应报头 Key: Value\r\n
响应报头 Key: Value\r\n
...\r\n
空行\r\n
响应正文 DATA
状态行格式:HTTP 版本 空格 状态码 空格 状态描述 \r\n,示例:HTTP/1.0 200 OK\r\n。响应报头必带 Content-Length 字段,标识正文长度。空行分隔报头与正文。响应正文是浏览器展示的内容。
最简响应构建示例:
std::string resp_status = "HTTP/1.0 200 OK\r\n";
std::string resp_content = "hello world, hello http!";
std::string cl = "Content-Length: " + std::to_string(resp_content.size()) + "\r\n";
std::string blankLine = "\r\n";
return resp_status + cl + blankLine + resp_content;
响应序列化流程:将 HttpResponse 中的版本、状态码、状态描述、响应报头、空行、响应正文,按 HTTP 响应格式拼接为字符串,返回给 TCP 层发送至客户端。这种模式非常适合云平台上的微服务架构,可以快速构建轻量级 HTTP 网关。
5. 报文完整性与连接模式
报文完整性判断:
- 报头完整:读取到 \r\n 构成的空行
- 正文完整:通过报头中 Content-Length 字段确定长度
- TCP 粘包:本文做了简化处理,默认读取完整报文。粘包问题确实存在,但属于小概率事件,后续可通过
Unpack解包解决
连接模式默认采用短连接,一次请求-响应完成后,服务端主动关闭 TCP 连接。将 while(true) 和判断条件中的 break 去掉即可支持长连接。对于云原生应用,建议后续扩展 keep-alive 长连接以提升性能。
6. 服务运行、测试与现象
启动方式:编译后执行 ./httpserver 端口号(如 8080),日志输出 socket 创建成功 → bind 成功 → listen 成功。测试方式包括浏览器访问 服务器 IP: 端口 自动发送 GET 请求,或使用 telnet 手动模拟 HTTP 请求。
测试现象:服务端打印完整 HTTP 请求报文;未返回响应时浏览器提示 ERR EMPTY RESPONSE;正常返回时浏览器展示响应正文。注意浏览器会自动请求 /favicon.ico 图标文件。HTTP 版本协商中,请求携带客户端支持的版本,响应携带服务端支持的版本,完成握手。
HTTP 方法中最常用的是 GET 和 POST。核心本质是:HTTP 是纯文本格式的应用层协议,请求与响应均为格式化字节流;核心作用是远程资源本地化访问,请求 URI 对应服务器本地文件,通过网络传输给浏览器。
总结与后续完善
本文从底层原理到代码实现,完整还原了 HTTP 基于 TCP 的运行模型。我们不仅看懂了 HTTP 报文,更用 C++ 实现了解析、封装、回调、IO 处理的全套流程。补充知识点:浏览器的回退就是 cd ..,默认首页命名固定为 index.html。
已经可以跑的代码:
- HttpProtocol.hpp:
#ifndef __HTTP_PROTOCOL_HPP #define __HTTP_PROTOCOL_HPP #include <iostream> #include <string> #include <unordered_map> // key_value #include <sstream> // 日志 #include "Logger.hpp" // 怎么提取?分隔符 --> 我要把分隔符暴露出来 const std::string linesep = "\r\n"; // 我需要一个报头的分隔符 const std::string headersep = ": "; // tuturoot const std::string webroot = "tuturoot"; // 命名空间 using namespace LogModule; // 示例:协议格式 // 前3行:带长度前缀的自定义协议格式("长度"\r\nJSON内容) // "40"\r\n{"float":3.14, "int": 100, "s1": "helloworld", "s2": "hello bit"} // "40"\r\n{"float":3.14, "int": 100, "s1": "helloworld", "s2": "hello bit"} // "40"\r\n{"float":3.14, "int": 100, "s1": "helloworld", "s2": "hello bit"} // 最后1行:纯JSON格式,没有长度前缀 // {"float":3.14, "int": 100, "s1": "helloworld", "s2": "hello bit"} // HTTP协议底层就是TCP协议 // 也需要请求和应答 class HttpRequest { private: // 把请求行读出来 int ReadOneLine(std::string &streamstr,std::string *line) // \r\n { // 1. 在输入字节流中查找行分隔符 "\r\n" 的位置 auto pos = streamstr.find(linesep); // 2. 如果没找到行分隔符,说明数据不完整,返回空字符串 if(pos == std::string::npos) // 整个字符串里没有分隔符 // return std::string(); // 返回值字符串是空串 return -1; // 参数要匹配 // 3. 从字节流开头截取到分隔符之前,得到一行完整内容 *line = streamstr.substr(0,pos); // 截止到pos位置 // 4. 从字节流中删除已经读取的这一行(包括 "\r\n" 分隔符), // 方便下次调用时读取下一行 streamstr.erase(0,pos + linesep.size()); // 删的时候删到换行符 // // 5. 返回读取到的这一行 // return line; return line->size(); // >=(表示截取成功;等于0的时候说明读到空串) } // 把stringstream以文件的形式写到网络 void ParseReqLine(std::string &status_line) { std::stringstream ss(status_line); ss >> _method >> _uri >> _http_version; // 比如说资源是:/a/b/c.html _path = webroot + _uri; // tuturoot/a/b/c.html } // 打印 void PrintDebug() { std::cout << _method << "#" << _uri << "#" << _http_version << std::endl; // 遍历headerkv for(auto &item : _header_kv) { std::cout << item.first << "->" << item.second << std::endl; } // 把空行打印出来 std::cout << "blankline: " << _blankline << std::endl; // 空行这里再加个换行 // 我的body现在没什么东西,加点东西 std::cout << "body: " << _body << std::endl; } // 单独设计一个分割字符串的接口 void SplitString(std::string &line,std::string *key,std::string *value,std::string sep = headersep) { // headersep设置为“:”,就是以:为分隔符 auto pos = line.find(sep); if(pos == std::string::npos) // 没有返回空串 return; *key = line.substr(0,pos); *value = line.substr(pos + sep.size()); } public: HttpRequest() {} // 提供一个反序列化的接口 void Deserialize(std::string &streamstr) { // 从网络当中传递一个字节流信息,然后转换成结构化信息 // 1.读取第一行 std::string status_line; int n = ReadOneLine(streamstr,&status_line); (void)n; // GET /a/b/c/d/e.html HTTP/1.1 // GET / HTTP/1.1 // 2.解析请求行 ParseReqLine(status_line); PrintDebug(); // 3.提取请求报头 n = 0; // 第一行已经被移除了 do { std::string line; n = ReadOneLine(streamstr,&line); if(n > 0) { std::string key,value; SplitString(line,&key,&value,headersep); // 都不为空说明取到了合法的key_value值 if(!key.empty() && !value.empty()) { _header_kv[key] = value; } } else if(n == 0) { _blankline = "\r\n"; break; } else { LOG(LogLevel::DEBUG) << "bug???"; break; } }while(n > 0); // 请求的正文是否存在 if(_header_kv.find("Content-Length") != _header_kv.end()) { // 不存在就什么都不做 int len = std::stoi(_header_kv["Content-Length"]); // 前面都已经去掉了,这里就读到body了 _body = streamstr.substr(0,len); // _body = streamstr; } PrintDebug(); } // 在Request里面做一个运算符重载(只读),重载一个中括号,因为原来获取URI的方式不太优雅 // 这里引用不能引用常量字符串,把const和&去掉 std::string operator [](const std::string &key) const // 未来提供一个key就行 { // 把一堆GET方法,封装从接口,以只读方式访问 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 == "uri") // return _uri; // 不要请求uri了,在当前代码中还存在像/a/b/c.html这样的特殊情况 else if(key == "path") return _path; else{ auto iter = _header_kv.find(key); if(iter != _header_kv.end()) return iter->second; } return std::string(); // 返回一个空串 } ~HttpRequest() {} private: // 请求方法、URI、HTTP版本、Key:[空格]Value,还有空行 std::string _method; std::string _uri; // /a/b/c.html /(特殊情况) std::string _http_version; std::unordered_map<std::string,std::string> _header_kv; std::string _blankline; // 除了请求还要有一个请求的正文 std::string _body; private: std::string _path; }; // 处理请求,构建应答 class HttpResponse { public: HttpResponse() {} ~HttpResponse() {} void Serialize(std::string *outstr) {} private: // 结构化字段 std::string _http_version; // HTTP协议版本,如 "HTTP/1.1" int _status_code; // 状态码,如 200(成功)、404(未找到)、500(服务器错误) std::string _status_code_desc; // 状态码描述,如 "OK"、"Not Found"、"Internal Server Error" std::unordered_map<std::string,std::string> _resp_headerkv; // 响应头键值对,如 Content-Type、Content-Length std::string _blankline; // 空行,用于分隔响应头和响应体 std::string _body; // 正文:响应体,如网页内容、JSON数据等 }; #endif - HttpServer.hpp:
#ifndef __HTTPSERVER_HPP #define __HTTPSERVER_HPP // Http就不用写客户端了,因为是和浏览器连接,浏览器就是客户端 #include <iostream> #include <string> #include <memory> #include "TcpServer.hpp" #include "Logger.hpp" #include "HttpProtocol.hpp" // 命名空间包一下 using namespace LogModule; class HttpServer { public: // ===== 原来的代码(已注释)===== HttpServer(uint16_t port) : _port(port), _tsvr(std::make_unique<TcpServer>(port)) {} // 不想这样写就在类里面写这样一个接口 std::string HandlerHttpRequest(std::string &streamstr) { // 1.报文完整性这里不管啦,不是不存在粘包问题,粘包问题是个小概率事件,这里先不考虑,默认报文是完整的 // 2.反序列化 HttpRequest httpreq; // 服务端接收到的是一个请求 httpreq.Deserialize(streamstr); std::cout << "HandlerHttpRequest: " << httpreq["method"] << std::endl; std::cout << "HandlerHttpRequest: " << httpreq["uri"] << std::endl; std::cout << "HandlerHttpRequest: " << httpreq["httpversion"] << std::endl; std::string target = httpreq["path"]; // /a/b/c.html std::cout << "http req: " << target << std::endl; // 3. 处理请求,构建应答:httpreq --> httpresp HttpResponse httpresp; // 4.应答序列化:把结构化的字段变成字符串 std::string httprespstr; httpresp.Serialize(&httprespstr); // 5.返回:返回应答的序列化字段 return httprespstr; // 函数会回调到这里 // 这样main函数就简单了,到时候直接包一下头文件HttpServer.hpp // 打印日志 // LOG(LogLevel::DEBUG) << "http request:\r\n"; // 将来把回调的HTTP请求从哪来的? // // LOG(LogLevel::DEBUG) << "http request:\r\n" << streamstr; // LOG(LogLevel::DEBUG) << "##################################\r\n"; // LOG(LogLevel::DEBUG) << streamstr; // LOG(LogLevel::DEBUG) << "##################################\r\n"; // =============> 上面第5步返回:就是把这些结构化字段按照这种格式发回去,回到TcpServer <============= // // ----> 这个应答测试可以,但是太草率了 <---- // // 状态行 // std::string resp_status = "HTTP/1.0 200 OK\r\n"; // HTTP版本 状态码 状态码描述 // // 响应正文 // std::string resp_content = "hello world,hello http!"; // // 响应报头 // std::string c1 = "Content-Length: " + std::to_string(resp_content.size()) + "\r\n"; // // 空行 // std::string blankline = "\r\n"; // // 怎么返回应答报文,直接拼接一个字符串 // return resp_status + c1 + blankline + resp_content; // 返回之后就会回到TcpServer的回调当中 } // 回调有了 void Run() { // _tsvr->Run(); // 写一个lambda _tsvr->Run([this](std::string &streamstr)->std::string{ return this->HandlerHttpRequest(streamstr); }); } ~HttpServer() {} private: // 端口号 uint16_t _port; // 这里要有一个协议字段 // std::unique_ptr<HttpProtocol> _protocol; // 不想这样写就在类里面写一个接口 std::unique_ptr<TcpServer> _tsvr; }; #endif - TcpServer.hpp(短连接):
#ifndef __TcpServer_HPP #define __TcpServer_HPP #include <iostream> #include <string> #include "Socket.hpp" #include <memory> #include <unistd.h> #include <signal.h> #include "Logger.hpp" #include <functional> // 处理客户端请求的回调函数类型 // 参数: 接收到的客户端数据 (inbuffer) // 返回值: 要发送给客户端的响应数据 (outbuffer) using handler_t = std::function<std::string(std::string &)>; // TCP服务器类 - 提供多进程并发的TCP网络服务 // 核心功能: // 1. 在指定端口创建并监听套接字 // 2. 接收客户端连接请求 // 3. 为每个客户端fork子进程进行独立处理 // 4. 通过回调函数实现业务逻辑与网络层的解耦 class TcpServer { public: TcpServer(uint16_t port, handler_t handler = nullptr) : _port(port), _listensockfd(std::make_unique<TcpSocket>()), _isrunning(false), _handlerstream(handler) { _listensockfd->BuildSocketMethod(_port); // 模版方法模式 } // ================= // 启动TCP服务器,开始监听并接受客户端连接 // 功能: // 1. 进入无限循环,持续等待客户端连接 // 2. 每次接收到新连接时,fork子进程处理该客户端的IO // 3. 父进程继续监听下一个连接 void Run(handler_t handler) { _handlerstream = handler; _isrunning = true; signal(SIGCHLD,SIG_IGN); while(_isrunning) { InetAddr clientaddr; auto sockfd = _listensockfd->Accept(&clientaddr); // 1. clientaddress 2. 得到socket if(!sockfd) { LOG(LogLevel::WARNING) << "Accepter error"; continue; } LOG(LogLevel::DEBUG) << "client addr: " << clientaddr.StringAddress() << " socket: " << sockfd->Socketfd(); // 处理链接的问题 if(fork() == 0) { _listensockfd->Close(); // 子进程 HanderIO(sockfd, clientaddr); exit(0); } // 父进程 sockfd->Close(); } } // 处理已连接客户端的IO操作 // 参数1: 与客户端建立连接的socket对象 // 参数2: 客户端的地址信息 // 功能: 循环接收客户端数据 void HanderIO(std::shared_ptr<Socket> sockfd, InetAddr &clientaddr) { std::string inbuffer; // 改成短连接 int n = sockfd->Recv(&inbuffer); // 假设我们默认读到的就是完整的报文 if(n < 0) { LOG(LogLevel::WARNING) << "recv error"; } if(n == 0) { LOG(LogLevel::INFO) << "client quit: " << clientaddr.StringAddress() << " sockfd: " << sockfd->Socketfd(); } LOG(LogLevel::DEBUG) << inbuffer; // 1. 分析inbuffer保证报文的完整性 // 2. a. 完整: 提取,处理 b.不完整:什么都不做 // 3:这个工作谁做?协议来做! // 谁不做??TcpServer std::string outbuffer; if(_handlerstream) outbuffer = _handlerstream(inbuffer); // 回调函数会调出去,也会再回来! if(!outbuffer.empty()) sockfd->Send(outbuffer); sockfd->Close(); } ~TcpServer() { // 析构 } private: int _port; std::unique_ptr<Socket> _listensockfd; bool _isrunning; handler_t _handlerstream; }; #endif - Main.cc:
#include "HttpServer.hpp" // 先把头文件包好,头文件只要包含这个就可以了,日志在这个头文件那里就已经包含了 void Usage(std::string procname) { std::cout << "Usage: " << procname << " Serverport" << std::endl; } int main(int argc,char *argv[]) { // 输出一个手册 if(argc != 2) { Usage(argv[0]); exit(0); } // 控制台策略 ENABLE_CONSOLE_LOG_STRATEGY(); // 解析命令行参数,将字符串端口号转换为uint16_t整数类型 uint16_t port = std::stoi(argv[1]); // 创建HttpServer智能指针对象,初始化服务器监听端口 std::unique_ptr<HttpServer> hsvr = std::make_unique<HttpServer>(port); // 启动HTTP服务器,开始接收并处理客户端请求 hsvr->Run(); return 0; } - 客户端 telnet 连接:
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/HTTP/HttpServer_v2$ telnet 127.0.0.1 8088 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. aaaa Connection closed by foreign host. - 服务器端:
alice@VM-4-17-ubuntu:~/Alice/bit_118_ubuntu/HTTP/HttpServer_v2$ ./httpserver 8088 [2026-05-16 01:19:53][INFO][2221403][Socket.hpp][92]- create socket success [2026-05-16 01:19:53][INFO][2221403][Socket.hpp][104]- bind socket success [2026-05-16 01:19:53][INFO][2221403][Socket.hpp][115]- listen socket success [2026-05-16 01:20:24][DEBUG][2221403][TcpServer.hpp][68]- client addr: [127.0.0.1:39758] socket: 4 [2026-05-16 01:28:07][DEBUG][2222050][TcpServer.hpp][101]- aaaa aaaa## blankline: body: [2026-05-16 01:28:07][DEBUG][2222050][HttpProtocol.hpp][132]- bug??? aaaa## blankline: body: HandlerHttpRequest: aaaa HandlerHttpRequest: HandlerHttpRequest: http req: tuturoot
后续完善方向:实现完整 Unpack 解包逻辑解决 TCP 粘包问题;完善 200、404、403、500 等状态码;实现文件读取功能,根据 URI 返回 wwwroot 目录下的真实资源;支持 POST、PUT 等方法;支持长连接和高并发优化;将端口、webroot 等配置参数文件化,便于云迁移和云部署。
最终你会发现,HTTP 协议并不神秘——它只是 TCP 上的一层文本约定。掌握它,你就掌握了云服务通信的基础。希望本文对学习 Linux 网络编程的读者有所帮助,欢迎一键四连支持!
浙公网安备 33010602011771号