文章目录
- 引言
- [二、第一步:编写 HTTP 层核心 ------Http.hpp](#二、第一步:编写 HTTP 层核心 ——Http.hpp)
-
- [2.1 Http.hpp 完整代码与解析](#2.1 Http.hpp 完整代码与解析)
- [2.2 Http.hpp 核心逻辑说明](#2.2 Http.hpp 核心逻辑说明)
- [三、第二步:修改 TCP 层代码,适配 HTTP 逻辑](#三、第二步:修改 TCP 层代码,适配 HTTP 逻辑)
-
- [3.1 修改 TcpServer.hpp(关键适配点)](#3.1 修改 TcpServer.hpp(关键适配点))
-
- [3.1.1 调整 func_t 回调签名](#3.1.1 调整 func_t 回调签名)
- [3.1.2 重写 HandleClient 函数](#3.1.2 重写 HandleClient 函数)
- [3.1.3 修改 Start 函数的任务封装](#3.1.3 修改 Start 函数的任务封装)
- [3.2 TcpServer.cc](#3.2 TcpServer.cc)
- 四、第三步:准备静态资源与编译测试
-
- [4.1 准备 index.html 示例文件](#4.1 准备 index.html 示例文件)
- [4.2 编译与启动(Makefile)](#4.2 编译与启动(Makefile))
- [4.3 测试验证](#4.3 测试验证)
-
- [4.3.1 访问首页(200 OK)](#4.3.1 访问首页(200 OK))
- [4.3.2 访问不存在的路径(404 Not Found)](#4.3.2 访问不存在的路径(404 Not Found))
- 五、常见问题排查
- 六、扩展方向
- 总结
引言
在前面的博客中,我们实现了一个通用的 TCP 线程池服务器 ------ 通过 ThreadPool 管理并发任务,用 Lock 保证线程安全,核心逻辑聚焦于 TCP 连接的监听、接收与任务调度。但 TCP 层仅负责 "传输数据",无法处理 HTTP 这类应用层协议的语义(如请求行解析、响应构造)。
本文将基于前文的 TCP 线程池服务器代码,新增 HTTP 应用层封装 (核心是 Http.hpp),并通过最小化修改 TCP 层代码实现适配,最终打造一个能处理静态资源(如 index.html)的 HTTP 服务器。全程遵循 "复用优先、分层解耦" 原则,不破坏原有 TCP 层的稳定性。
- TCP 层(复用) :负责底层连接管理(监听端口、
accept连接、线程池任务调度、连接关闭),不关心数据内容; - HTTP 层(新增):基于 TCP 层提供的 "客户端 FD"(文件描述符),实现 HTTP 协议的核心逻辑(解析请求、构造响应、读取静态资源);
- 适配关键:调整 TCP 层的 "业务处理回调",让其从 "收发字符串" 转为 "调用 HTTP 处理逻辑",仅需修改回调签名和少量逻辑,无需重构 TCP 核心。
二、第一步:编写 HTTP 层核心 ------Http.hpp
Http.hpp 是 HTTP 服务器的灵魂,需包含请求解析、响应构造、业务入口三个核心模块。为了简化使用,我们将类的声明与实现合并在头文件中(实战中可拆分,但入门阶段优先降低依赖复杂度)。
2.1 Http.hpp 完整代码与解析
cpp
#ifndef __HTTP_HPP__
#define __HTTP_HPP__
// 依赖头文件:系统基础库+STL,无第三方依赖
#include <cstdio> // 读取静态文件(index.html)
#include <cstring> // 缓冲区操作
#include <unistd.h> // recv/send/close系统调用
#include <string> // 存储请求/响应数据
#include <sstream> // 解析HTTP请求行
#include <iostream> // 日志输出
// 1. HTTP请求解析类:从客户端FD读取数据,提取请求方法和路径
class HttpParser {
public:
// 输入:客户端FD;输出:请求方法(如GET)、请求路径(如/index.html)
// 返回值:true=解析成功,false=解析失败(客户端断开/数据异常)
static bool Parse(int client_fd, std::string& method, std::string& path) {
// 读取HTTP请求(缓冲区设4096字节,覆盖多数场景)
char req_buf[4096] = {0};
ssize_t recv_len = recv(client_fd, req_buf, sizeof(req_buf)-1, 0);
// 处理读取失败(如客户端断连)
if (recv_len <= 0) {
std::cerr << "[HttpParser] 读取请求失败,FD: " << client_fd << std::endl;
return false;
}
// 转换为字符串,解析请求行(HTTP请求行格式:METHOD PATH VERSION\r\n)
std::string http_req(req_buf, recv_len);
std::istringstream req_stream(http_req);
req_stream >> method >> path; // 提取前两个关键字段(忽略版本)
// 处理默认路径:请求"/"时,自动映射到首页/index.html
if (path.empty() || path == "/") {
path = "/index.html";
}
std::cout << "[HttpParser] 解析成功 | FD: " << client_fd
<< " | 方法: " << method
<< " | 路径: " << path << std::endl;
return true;
}
};
// 2. HTTP响应构造类:根据请求路径生成符合HTTP协议的响应
class HttpResponder {
public:
// 输入:请求路径(如/index.html);输出:完整HTTP响应字符串
static std::string Build(const std::string& path) {
std::string resp; // 最终响应
std::string resp_body; // 响应体(HTML内容)
std::string status_line; // 状态行(如HTTP/1.1 200 OK)
// 响应头:告诉客户端响应类型是HTML,编码为UTF-8
std::string content_type = "text/html; charset=utf-8";
// 步骤1:读取静态资源(如index.html),生成响应体
// path.substr(1):去掉路径开头的"/",如"/index.html"→"index.html"(匹配实际文件名)
if (ReadStaticFile(path.substr(1), resp_body)) {
status_line = "HTTP/1.1 200 OK\r\n"; // 文件存在:返回200成功
} else {
// 文件不存在:返回404页面(硬编码简单HTML)
resp_body = "<!DOCTYPE html>"
"<html lang='zh-CN'><head><meta charset='UTF-8'><title>404 Not Found</title></head>"
"<body style='text-align:center; margin-top:50px;'>"
"<h1 style='color:#e74c3c;'>404 页面不存在</h1>"
"<p style='color:#7f8c8d;'>请求路径: " + path + " 对应的文件未找到</p></body></html>";
status_line = "HTTP/1.1 404 Not Found\r\n";
}
// 步骤2:构造HTTP响应头(必须包含Content-Type和Content-Length)
resp += status_line;
resp += "Content-Type: " + content_type + "\r\n"; // 响应类型
resp += "Content-Length: " + std::to_string(resp_body.size()) + "\r\n"; // 响应体长度(避免客户端断连)
resp += "Connection: close\r\n"; // 短连接:处理完关闭连接(入门友好)
resp += "\r\n"; // 响应头与响应体的分隔符(HTTP协议强制要求,缺一不可)
// 步骤3:拼接响应体
resp += resp_body;
return resp;
}
private:
// 辅助函数:读取静态文件内容(输入:文件名;输出:文件内容;返回值:true=读取成功)
static bool ReadStaticFile(const std::string& filename, std::string& content) {
// 以只读方式打开文件(相对路径:与服务器可执行文件同目录)
FILE* file = fopen(filename.c_str(), "r");
if (!file) {
std::cerr << "[HttpResponder] 文件不存在/无法打开:" << filename << std::endl;
return false;
}
// 循环读取文件(每次1024字节,避免大文件内存溢出)
char file_buf[1024] = {0};
size_t read_len = 0;
while ((read_len = fread(file_buf, 1, sizeof(file_buf), file)) > 0) {
content.append(file_buf, read_len);
memset(file_buf, 0, sizeof(file_buf)); // 清空缓冲区,避免残留数据
}
// 检查文件读取是否正常结束(排除读取出错的情况)
if (ferror(file)) {
std::cerr << "[HttpResponder] 读取文件失败:" << filename << std::endl;
fclose(file);
return false;
}
fclose(file); // 关闭文件,释放资源
return true;
}
};
// 3. HTTP处理入口类:对外提供统一接口,整合解析与响应逻辑
class HttpHandler {
public:
// 核心接口:处理单个客户端的HTTP请求(输入:客户端FD、客户端IP)
static void Process(int client_fd, const std::string& client_ip) {
std::string method; // HTTP请求方法(如GET)
std::string path; // HTTP请求路径(如/index.html)
// 步骤1:解析HTTP请求
if (!HttpParser::Parse(client_fd, method, path)) {
close(client_fd); // 解析失败,关闭连接
return;
}
// 步骤2:仅处理GET方法(入门阶段聚焦静态资源,其他方法返回405)
if (method != "GET") {
std::string resp = "HTTP/1.1 405 Method Not Allowed\r\nContent-Length: 0\r\n\r\n";
send(client_fd, resp.c_str(), resp.size(), 0);
std::cerr << "[HttpHandler] 不支持的方法 | FD: " << client_fd << " | 方法: " << method << std::endl;
close(client_fd);
return;
}
// 步骤3:构造HTTP响应
std::string resp = HttpResponder::Build(path);
// 步骤4:发送响应给客户端
ssize_t send_len = send(client_fd, resp.c_str(), resp.size(), 0);
if (send_len <= 0) {
std::cerr << "[HttpHandler] 发送响应失败 | FD: " << client_fd << " | IP: " << client_ip << std::endl;
} else {
std::cout << "[HttpHandler] 处理完成 | FD: " << client_fd << " | IP: " << client_ip
<< " | 响应长度: " << send_len << "字节" << std::endl;
}
// 注:无需手动关闭FD,TCP层会在回调结束后处理
}
};
#endif // __HTTP_HPP__
2.2 Http.hpp 核心逻辑说明
HttpParser:负责 "读数据 + 提关键信息"------ 从客户端 FD 读取 HTTP 请求,解析出method(请求方法)和path(请求路径),并处理默认路径映射(/→/index.html);HttpResponder:负责 "造响应"------ 根据请求路径读取静态文件(如index.html),生成包含 "状态行、响应头、响应体" 的完整 HTTP 响应,同时处理 "文件不存在"(返回 404);HttpHandler:对外提供统一入口(Process静态函数),整合解析与响应逻辑,隐藏内部细节,方便 TCP 层调用。
三、第二步:修改 TCP 层代码,适配 HTTP 逻辑
上一篇博客的 TcpServer 用 func_t(std::function)定义业务回调,但原回调是 "字符串入参、字符串出参"(适配简单 TCP 通信),无法满足 HTTP 层 "需要客户端 FD" 的需求。我们需要最小化修改 TCP 层,让其能传递 FD 和 IP 给 HTTP 处理逻辑。
3.1 修改 TcpServer.hpp(关键适配点)
需修改 3 处:func_t 回调签名、HandleClient 业务逻辑、Start 函数的任务封装。
3.1.1 调整 func_t 回调签名
原func_t是 "字符串入参、字符串出参",改为传递 HTTP 需要的 "客户端 FD" 和 "客户端 IP",且无返回值(HTTP 层直接通过 FD 发送响应):
cpp
// TcpServer.hpp 中,替换原func_t定义
// 原代码:using func_t = std::function<std::string(const std::string&)>;
// 新代码:传递FD和IP,无返回值
using func_t = std::function<void(int, const std::string&)>;
3.1.2 重写 HandleClient 函数
原 HandleClient 是 "循环收发字符串",HTTP 是 "单次请求 - 响应 - 关闭",需删掉循环和字符串处理,直接调用 HTTP 层的 HttpHandler::Process:
cpp
// TcpServer.hpp 中,修改HandleClient函数
private:
void HandleClient(int client_fd, const std::string& client_ip) {
std::cout << "子线程(tid: " << pthread_self() << ")开始处理客户端[" << client_ip << "]" << std::endl;
// 核心:调用HTTP处理逻辑(传递FD和IP)
_data_handler(client_fd, client_ip);
// HTTP处理完成后,关闭客户端连接(短连接)
close(client_fd);
std::cout << "子线程(tid: " << pthread_self() << ")处理完毕,关闭客户端[" << client_ip << "]连接" << std::endl;
}
3.1.3 修改 Start 函数的任务封装
Start 函数中,原逻辑会解析 client_port 并传递给 HandleClient,现在删掉冗余的 client_port,仅捕获 client_fd 和 client_ip:
cpp
// TcpServer.hpp 中,修改Start函数的任务封装
void Start() {
if (!_is_running || _listen_fd == -1) {
perror("服务器未初始化,无法启动");
return;
}
// 主线程循环:仅接收连接,不处理业务
while (_is_running) {
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
// 1. 接收客户端连接
int client_fd = accept(_listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_fd == -1) {
perror("accept 失败!");
continue;
}
// 2. 解析客户端IP(删掉冗余的client_port解析)
std::string client_ip = inet_ntoa(client_addr.sin_addr);
std::cout << "\n客户端连接成功:[" << client_ip << "],client_fd: " << client_fd << std::endl;
// 3. 封装任务:仅捕获client_fd和client_ip,传递给HandleClient
Task task = [this, client_fd, client_ip]() {
this->HandleClient(client_fd, client_ip);
};
// 提交任务到线程池
if (!_thread_pool.AddTask(task)) {
std::cerr << "任务提交失败,关闭客户端连接:[" << client_ip << "]" << std::endl;
close(client_fd);
}
}
}
3.2 TcpServer.cc
TcpServer.cc 是服务器入口,只需引入 Http.hpp,并在创建 TcpServer 实例时传入 HttpHandler::Process 即可:
cpp
#include <memory>
#include "TcpServer.hpp"
#include "Http.hpp" // 新增:引入HTTP层头文件
// 打印用法
void Usage(const std::string& proc) {
std::cerr << "Usage: " << proc << " <listen_port> <thread_num>" << std::endl;
std::cerr << "示例:" << proc << " 8080 4" << std::endl;
}
int main(int argc, char* argv[]) {
// 1. 检查参数
if (argc != 3) {
Usage(argv[0]);
return 1;
}
// 2. 解析端口和线程数
uint16_t listen_port = std::stoi(argv[1]);
size_t thread_num = std::stoi(argv[2]);
if (listen_port < 1024 || listen_port > 65535) {
std::cerr << "端口无效!需在 1024~65535 之间" << std::endl;
return 2;
}
if (thread_num < 1 || thread_num > 1024) {
std::cerr << "线程数无效!需在 1~1024 之间" << std::endl;
return 3;
}
// 3. 创建TCP服务器实例:传入HTTP处理逻辑(HttpHandler::Process)
std::unique_ptr<TcpServer> tcp_server =
std::make_unique<TcpServer>(listen_port, HttpHandler::Process, thread_num);
// 4. 初始化并启动服务器
if (!tcp_server->Init()) {
std::cerr << "HTTP服务器初始化失败" << std::endl;
return 4;
}
tcp_server->Start();
return 0;
}
四、第三步:准备静态资源与编译测试
4.1 准备 index.html 示例文件
在服务器可执行文件的同级目录下,新建 index.html(作为默认首页):
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>HTTP线程池服务器</title>
<style>
h1 { color: #27ae60; text-align: center; margin-top: 80px; }
p { color: #34495e; text-align: center; font-size: 18px; }
</style>
</head>
<body>
<h1>✅ HTTP服务器运行成功!</h1>
<p>这是来自 index.html 的静态资源</p>
<p>底层基于TCP线程池实现,支持并发处理请求</p>
</body>
</html>
4.2 编译与启动(Makefile)
沿用之前的 Makefile,只需确保链接 pthread 库(线程池依赖):
编译并启动服务器:
bash
# 编译
make
# 启动(端口8888,线程数4)
./httpserver 8888 4
4.3 测试验证
4.3.1 访问首页(200 OK)
用浏览器打开 http://localhost:8888 或 http://你的服务器IP:8888,会看到 index.html 的内容:
- 服务器日志会输出:

4.3.2 访问不存在的路径(404 Not Found)
浏览器打开 http://localhost:8080/test.html,会看到 404 页面,服务器日志输出:

五、常见问题排查
- 端口占用报错 :启动时提示 "bind 失败",用
netstat -tuln | grep 8888查看端口是否被占用,换一个未占用的端口(如 8889); - 静态资源找不到 :确保
index.html与服务器可执行文件在同一目录,或在HttpResponder::ReadStaticFile中修改文件路径(如改为绝对路径/var/www/index.html); - 浏览器白屏 :检查 HTTP 响应格式 ------ 必须保证 "响应头与响应体之间有一个空行(
\r\n)",且Content-Length与响应体长度一致。
六、扩展方向
当前实现是基础的 HTTP 1.0 服务器,后续可基于此扩展:
- 支持 HTTP 长连接 :将
Connection: close改为Connection: keep-alive,让一个连接处理多个请求; - 增加动态资源支持:集成 CGI 或 FastCGI,处理 C++/python 后端接口;
- 静态资源缓存 :添加
Cache-Control响应头,减少重复请求; - 请求超时控制 :给
recv设置超时(SO_RCVTIMEO),避免线程因客户端断连长期阻塞。
总结
本文的核心是 "复用与分层"------ 基于原有 TCP 线程池服务器,仅通过调整回调签名和少量逻辑,就能快速封装 HTTP 层能力。Http.hpp 封装了 HTTP 协议的所有细节,TCP 层专注于底层连接管理,两者解耦清晰,既保护了原有代码的稳定性,又实现了新的业务需求。