当前位置: 首页 > news >正文

基于 TCP 线程池服务器封装 HTTP 服务器:从协议解析到适配落地

文章目录

  • 引言
  • 二、第一步:编写 HTTP 层核心 ——Http.hpp
    • 2.1 Http.hpp 完整代码与解析
    • 2.2 Http.hpp 核心逻辑说明
  • 三、第二步:修改 TCP 层代码,适配 HTTP 逻辑
    • 3.1 修改 TcpServer.hpp(关键适配点)
      • 3.1.1 调整 func_t 回调签名
      • 3.1.2 重写 HandleClient 函数
      • 3.1.3 修改 Start 函数的任务封装
    • 3.2 TcpServer.cc
  • 四、第三步:准备静态资源与编译测试
    • 4.1 准备 index.html 示例文件
    • 4.2 编译与启动(Makefile)
    • 4.3 测试验证
      • 4.3.1 访问首页(200 OK)
      • 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 完整代码与解析

#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.htmlif (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-8std::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 逻辑

上一篇博客的 TcpServerfunc_tstd::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 发送响应):

// 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

// 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_fdclient_ip

// 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,传递给HandleClientTask 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 即可:

#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(作为默认首页):

<!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 库(线程池依赖):

编译并启动服务器:

# 编译
make# 启动(端口8888,线程数4)
./httpserver 8888 4

4.3 测试验证

4.3.1 访问首页(200 OK)

用浏览器打开 http://localhost:8888http://你的服务器IP:8888,会看到 index.html 的内容:

  • 服务器日志会输出:
    在这里插入图片描述

4.3.2 访问不存在的路径(404 Not Found)

浏览器打开 http://localhost:8080/test.html,会看到 404 页面,服务器日志输出:
在这里插入图片描述

五、常见问题排查

  1. 端口占用报错:启动时提示 “bind 失败”,用 netstat -tuln | grep 8888 查看端口是否被占用,换一个未占用的端口(如 8889);
  2. 静态资源找不到:确保 index.html 与服务器可执行文件在同一目录,或在 HttpResponder::ReadStaticFile 中修改文件路径(如改为绝对路径 /var/www/index.html);
  3. 浏览器白屏:检查 HTTP 响应格式 —— 必须保证 “响应头与响应体之间有一个空行(\r\n)”,且 Content-Length 与响应体长度一致。

六、扩展方向

当前实现是基础的 HTTP 1.0 服务器,后续可基于此扩展:

  1. 支持 HTTP 长连接:将 Connection: close 改为 Connection: keep-alive,让一个连接处理多个请求;
  2. 增加动态资源支持:集成 CGI 或 FastCGI,处理 C++/python 后端接口;
  3. 静态资源缓存:添加 Cache-Control 响应头,减少重复请求;
  4. 请求超时控制:给 recv 设置超时(SO_RCVTIMEO),避免线程因客户端断连长期阻塞。

总结

本文的核心是 “复用与分层”—— 基于原有 TCP 线程池服务器,仅通过调整回调签名和少量逻辑,就能快速封装 HTTP 层能力。Http.hpp 封装了 HTTP 协议的所有细节,TCP 层专注于底层连接管理,两者解耦清晰,既保护了原有代码的稳定性,又实现了新的业务需求。

http://www.dtcms.com/a/568890.html

相关文章:

  • xargs
  • 据库事务是数据库管理系统 ACID 四大特性
  • 宜昌市住房和城乡建设局网站wordpress后台慢
  • SSM基于HTML5的流浪动物领养平台yww0b(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • 网站栏目分类网站开发市场
  • word转Pdf,在window正常,放在linux服务器上就转出来中文是空白
  • 攻防世界-Misc-pdf
  • 开启RN之旅——前端基础
  • 【LeetCode】99. 恢复二叉搜索树
  • 【rhcsa第一次作业】
  • 哪个网站做图找图片上海网络推广公司排名
  • 订单支付后库存不扣减,如何用RabbitMQ来优化?
  • Qt对话框设计
  • 解决 contents have differences only in line separators
  • 无锡建站方案深圳百度总部
  • Docker中安装 redis、rabbitmq、MySQL、es、 mongodb设置用户名密码
  • SAP EXCEL模板下载导入
  • 动态贝叶斯网络物联网应用方式
  • Oracle OCP认证:深度解析与实战指南
  • 帝国建设网站wordpress迅雷插件下载
  • HTTP 请求与数据交互全景指南:Request、GET、POST、JSON 及 curl
  • 如何进一步推动淘宝商品详情API的安全强化与生态协同创新?
  • Flutter | 基础环境配置和创建flutter项目
  • 58同城网站建设排名wordpress页面生成二维码
  • 怎么在子域名建立一个不同的网站怎么通过ip查看自己做的网站
  • UVa 11027 Palindromic Permutation
  • Python模板注入漏洞
  • 【SMTP】在线配置测试工具,如何配置接口?
  • 黑马JAVAWeb-01 Maven依赖管理-生命周期-单元测试
  • 第12讲:入门级状态管理方案 - Provider详解