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

文章目录

  • 引言
  • [二、第一步:编写 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 逻辑

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

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_fdclient_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: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 层专注于底层连接管理,两者解耦清晰,既保护了原有代码的稳定性,又实现了新的业务需求。

相关推荐
吉普赛的歌3 小时前
【阿里云】ECS服务器重启需要注意的事项
运维·服务器·阿里云
草莓熊Lotso4 小时前
Linux 权限管理进阶:从 umask 到粘滞位的深度解析
linux·运维·服务器·人工智能·ubuntu·centos·unix
iCxhust6 小时前
windows环境下在Bochs中运行Linux0.12系统
linux·运维·服务器·windows·minix
七七七七079 小时前
【计算机网络】深入理解ARP协议:工作原理、报文格式与安全防护
linux·服务器·网络·计算机网络·安全
qq_54702617910 小时前
Flowable 工作流引擎
java·服务器·前端
奋斗的蛋黄11 小时前
网络卡顿运维排查方案:从客户端到服务器的全链路处理
运维·服务器·网络
wanhengidc12 小时前
云手机搬砖 尤弥尔传奇自动化操作
运维·服务器·arm开发·安全·智能手机·自动化
进击的圆儿12 小时前
TCP可靠传输的秘密:从滑动窗口到拥塞控制
网络·网络协议·tcp/ip
图图图图爱睡觉12 小时前
主机跟虚拟机ip一直Ping不通,并且虚拟机使用ifconfig命令时,ens33没有ipv4地址,只有ipv6地址
服务器·网络·tcp/ip