HTTP服务器实现请求解析与响应构建:从基础架构到动态交互

目录

一、Http.hpp

模块1:头文件和命名空间

模块2:全局常量定义

模块3:HttpRequest类(HTTP请求解析)

模块4:HttpResponse类(HTTP响应构建)

模块5:函数指针定义和Http类

代码总结

[1. 架构设计](#1. 架构设计)

[2. 功能特点](#2. 功能特点)

[3. 主要流程](#3. 主要流程)

[4. 待改进点](#4. 待改进点)

1、总体结构

2、详细解析

[1. 常量和头文件](#1. 常量和头文件)

[2. HttpRequest类 - 请求解析](#2. HttpRequest类 - 请求解析)

主要功能:

关键方法:

路径处理逻辑:

[3. HttpResponse类 - 响应构建](#3. HttpResponse类 - 响应构建)

主要功能:

关键方法:

文件类型识别:

[4. Http类 - 主服务器](#4. Http类 - 主服务器)

核心架构:

工作流程:

路由注册机制:

[5. 请求处理流程](#5. 请求处理流程)

3、特色功能

[1. 静态文件服务](#1. 静态文件服务)

[2. 动态请求支持](#2. 动态请求支持)

[3. 特殊处理](#3. 特殊处理)

4、代码优缺点

优点:

局限性:

5、使用示例

二、Util.hpp

1、代码总结与注意事项

[1. 类设计特点](#1. 类设计特点)

[2. 方法详细说明](#2. 方法详细说明)

ReadFileContent方法

ReadOneLine方法

FileSize方法

[3. 设计模式](#3. 设计模式)

[4. 使用示例](#4. 使用示例)

[5. 改进建议](#5. 改进建议)

三、引入之前实现过的相关头文件

Common.hpp

InetAddr.hpp

Log.hpp

Mutex.hpp

Socket.hpp

TcpServer.hpp

四、相关的HTML文件

404.html

Login.html

Register.html

index.html

test.html

五、Main.cc

1、代码架构分析

[1. 处理函数设计模式](#1. 处理函数设计模式)

[2. 路由注册机制](#2. 路由注册机制)

[3. 请求处理流程](#3. 请求处理流程)

2、重要技术细节

[1. 查询参数处理](#1. 查询参数处理)

[2. HTTP响应构建](#2. HTTP响应构建)

[3. 日志记录](#3. 日志记录)

3、运行服务器

编译和运行:

测试请求:

总结


一、Http.hpp

模块1:头文件和命名空间

cpp 复制代码
// 防止头文件重复包含
#pragma once

// 包含自定义网络库的头文件
#include "Socket.hpp"      // Socket相关类
#include "TcpServer.hpp"   // TCP服务器类
#include "Util.hpp"        // 工具函数
#include "Log.hpp"         // 日志模块

// 包含标准库头文件
#include <iostream>        // 输入输出流
#include <string>          // 字符串类
#include <memory>          // 智能指针
#include <sstream>         // 字符串流
#include <functional>      // 函数对象
#include <unordered_map>   // 哈希表

// 使用命名空间
using namespace SocketModule;  // Socket模块命名空间
using namespace LogModule;     // 日志模块命名空间

模块2:全局常量定义

cpp 复制代码
// HTTP协议相关的常量定义
const std::string gspace = " ";       // 空格
const std::string glinespace = "\r\n"; // 行分隔符(回车换行)
const std::string glinesep = ": ";    // 头部键值分隔符

// 服务器文件路径相关常量
const std::string webroot = "./wwwroot";  // 网站根目录
const std::string homepage = "index.html"; // 默认首页文件名
const std::string page_404 = "/404.html";  // 404错误页面路径

模块3:HttpRequest类(HTTP请求解析)

cpp 复制代码
// HTTP请求类,用于解析客户端发来的HTTP请求
class HttpRequest
{
public:
    // 构造函数,初始化_is_interact为false(默认不是交互请求)
    HttpRequest() : _is_interact(false)
    {
    }
    
    // 序列化方法(当前未实现,返回空字符串)
    std::string Serialize()
    {
        return std::string();
    }
    
    // 解析请求行:例如 "GET / HTTP/1.1"
    void ParseReqLine(std::string &reqline)
    {
        // GET / HTTP/1.1
        std::stringstream ss(reqline);  // 创建字符串流
        ss >> _method >> _uri >> _version;  // 按空格分割,分别存入_method、_uri、_version
    }
    
    // 反序列化:将HTTP请求字符串解析为HttpRequest对象
    // reqstr是一个完整的http request string
    bool Deserialize(std::string &reqstr)
    {
        // 1. 提取请求行
        std::string reqline;  // 用于存储请求行
        // 使用Util工具读取一行(以glinespace为分隔符)
        bool res = Util::ReadOneLine(reqstr, &reqline, glinespace);
        // 记录调试日志
        LOG(LogLevel::DEBUG) << reqline;

        // 2. 对请求行进行反序列化
        ParseReqLine(reqline);  // 调用上面方法解析请求行

        // 处理URI:如果请求根目录,添加默认首页
        if (_uri == "/")
            _uri = webroot + _uri + homepage; // 转换为 ./wwwroot/index.html
        else
            _uri = webroot + _uri; // 转换为 ./wwwroot/a/b/c.html

        // 记录解析结果到日志
        LOG(LogLevel::DEBUG) << "_method: " << _method;
        LOG(LogLevel::DEBUG) << "_uri: " << _uri;
        LOG(LogLevel::DEBUG) << "_version: " << _version;
        
        // 检查URI中是否包含查询参数(?后面的部分)
        const std::string temp = "?";  // 查询参数分隔符
        auto pos = _uri.find(temp);    // 查找?的位置
        if (pos == std::string::npos)  // 如果没有找到?
        {
            return true;  // 直接返回,没有查询参数
        }
        
        // 如果有查询参数,进行分割处理
        // _uri示例: ./wwwroot/login?username=zhangsan&password=123456
        _args = _uri.substr(pos + temp.size());  // 提取查询参数部分
        _uri = _uri.substr(0, pos);              // 提取URI部分(去掉查询参数)
        _is_interact = true;                     // 标记为交互请求

        // 最终_uri格式: ./wwwroot/XXX.YYY
        return true;
    }
    
    // 获取URI的getter方法
    std::string Uri()
    {
        return _uri;
    }
    
    // 判断是否为交互请求的getter方法
    bool isInteract()
    { 
        return _is_interact;
    }
    
    // 获取查询参数的getter方法
    std::string Args()
    {
        return _args;
    }
    
    // 析构函数
    ~HttpRequest()
    {
    }

private:
    // HTTP请求行部分
    std::string _method;   // 请求方法(GET/POST等)
    std::string _uri;      // 请求的资源路径
    std::string _version;  // HTTP协议版本
    
    // HTTP头部字段(未完全实现)
    std::unordered_map<std::string, std::string> _headers;
    std::string _blankline;  // 空行
    std::string _text;       // 请求体(如果有)
    
    // 自定义字段
    std::string _args;       // 查询参数
    bool _is_interact;       // 是否为交互请求的标志
};

基本用法:

cpp 复制代码
#include <iostream>
#include <string>

// 假设有相关的依赖文件
#include "httpreq.h"

// 全局变量定义(实际应该在合适的地方定义)
std::string webroot = "./wwwroot/";
std::string homepage = "index.html";

int main() {
    // 创建一个HttpRequest对象
    HttpRequest req;
    
    // 准备一个HTTP请求字符串
    std::string http_request = 
        "GET / HTTP/1.1\r\n"
        "Host: localhost:8080\r\n"
        "User-Agent: Mozilla/5.0\r\n"
        "\r\n";
    
    // 解析HTTP请求
    bool success = req.Deserialize(http_request);
    
    if (success) {
        // 获取解析后的URI
        std::cout << "请求URI: " << req.Uri() << std::endl;
        
        // 检查是否是交互请求
        if (req.isInteract()) {
            std::cout << "这是交互请求" << std::endl;
            std::cout << "查询参数: " << req.Args() << std::endl;
        } else {
            std::cout << "这是静态文件请求" << std::endl;
        }
    }
    
    return 0;
}

模块4:HttpResponse类(HTTP响应构建)

cpp 复制代码
// HTTP响应类,用于构建发送给客户端的HTTP响应
class HttpResponse
{
public:
    // 构造函数,初始化空行和HTTP版本
    HttpResponse() : _blankline(glinespace), _version("HTTP/1.0")
    {
    }
    
    // 序列化:将HttpResponse对象转换为HTTP响应字符串
    std::string Serialize()
    {
        // 构建状态行:HTTP/1.0 200 OK\r\n
        std::string status_line = _version + gspace + std::to_string(_code) + gspace + _desc + glinespace;
        
        // 构建响应头
        std::string resp_header;
        for (auto &header : _headers)
        {
            // 每个头部格式:key: value\r\n
            std::string line = header.first + glinesep + header.second + glinespace;
            resp_header += line;
        }

        // 组合:状态行 + 响应头 + 空行 + 响应体
        return status_line + resp_header + _blankline + _text;
    }
    
    // 设置目标文件路径
    void SetTargetFile(const std::string &target)
    {
        _targetfile = target;
    }
    
    // 设置HTTP状态码和对应的描述
    void SetCode(int code)
    {
        _code = code;
        switch (_code)
        {
        case 200:
            _desc = "OK";
            break;
        case 404:
            _desc = "Not Found";
            break;
        case 301:
            _desc = "Moved Permanently";
            break;
        case 302:
            _desc = "See Other";
            break;
        default:
            break;
        }
    }
    
    // 设置HTTP响应头(如果已存在则不设置)
    void SetHeader(const std::string &key, const std::string &value)
    {
        auto iter = _headers.find(key);
        if (iter != _headers.end())  // 如果key已存在
            return;                   // 直接返回,不覆盖
        _headers.insert(std::make_pair(key, value));  // 插入新的键值对
    }
    
    // 根据文件后缀名确定Content-Type
    std::string Uri2Suffix(const std::string &targetfile)
    {
        // ./wwwroot/a/b/c.html
        auto pos = targetfile.rfind(".");  // 从后往前查找最后一个.
        if (pos == std::string::npos)      // 如果没有找到后缀名
        {
            return "text/html";            // 默认返回text/html
        }

        std::string suffix = targetfile.substr(pos);  // 提取后缀名
        if (suffix == ".html" || suffix == ".htm")
            return "text/html";
        else if (suffix == ".jpg")
            return "image/jpeg";
        else if (suffix == "png")
            return "image/png";
        else
            return "";
    }

    // 构建HTTP响应
    bool MakeResponse()
    {
        // 特殊处理:忽略favicon.ico请求
        if (_targetfile == "./wwwroot/favicon.ico")
        {
            LOG(LogLevel::DEBUG) << "用户请求: " << _targetfile << "忽略它";
            return false;  // 返回false表示不发送响应
        }
        
        // 重定向测试:如果请求redir_test,返回301重定向
        if (_targetfile == "./wwwroot/redir_test")
        {
            SetCode(301);                            // 设置状态码301
            SetHeader("Location", "https://www.qq.com/");  // 设置重定向地址
            return true;
        }
        
        // 读取文件内容
        int filesize = 0;
        bool res = Util::ReadFileContent(_targetfile, &_text);  // 读取目标文件
        
        if (!res)  // 如果文件读取失败(文件不存在)
        {
            _text = "";
            LOG(LogLevel::WARNING) << "client want get : " << _targetfile << " but not found";
            
            // 设置404状态
            SetCode(404);
            
            // 加载404错误页面
            _targetfile = webroot + page_404;              // 设置404页面路径
            filesize = Util::FileSize(_targetfile);        // 获取404页面大小
            Util::ReadFileContent(_targetfile, &_text);    // 读取404页面内容
            
            // 设置响应头
            std::string suffix = Uri2Suffix(_targetfile);  // 根据后缀确定Content-Type
            SetHeader("Content-Type", suffix);
            SetHeader("Content-Length", std::to_string(filesize));
            
            // 注释掉的302重定向示例
            // SetCode(302);
            // SetHeader("Location", "http://8.137.19.140:8080/404.html");
            // return true;
        }
        else  // 文件读取成功
        {
            LOG(LogLevel::DEBUG) << "读取文件: " << _targetfile;
            
            // 设置200状态
            SetCode(200);
            
            // 设置响应头
            filesize = Util::FileSize(_targetfile);
            std::string suffix = Uri2Suffix(_targetfile);
            SetHeader("Conent-Type", suffix);
            SetHeader("Content-Length", std::to_string(filesize));
            SetHeader("Set-Cookie", "username=zhangsan;");  // 设置Cookie
            // SetHeader("Set-Cookie", "passwd=123456;");
        }
        return true;
    }
    
    // 设置响应体的文本内容
    void SetText(const std::string &t)
    {
        _text = t;
    }
    
    // 反序列化方法(当前未实现,直接返回true)
    bool Deserialize(std::string &reqstr)
    {
        return true;
    }
    
    // 析构函数
    ~HttpResponse() {}

    // 响应字段(为了方便调试,设为public,实际应该为private)
public:
    std::string _version;  // HTTP版本
    int _code;             // 状态码,如404
    std::string _desc;     // 状态描述,如"Not Found"

    std::unordered_map<std::string, std::string> _headers;  // 响应头
    std::vector<std::string> cookie;                       // Cookie列表(未使用)
    std::string _blankline;                                // 空行
    std::string _text;                                     // 响应体

    // 其他属性
    std::string _targetfile;  // 目标文件路径
};

基本使用(返回静态文件):

cpp 复制代码
#include <iostream>
#include <string>

// 假设有相关的依赖文件
#include "httpres.h"

// 全局变量定义
std::string webroot = "./wwwroot/";
std::string homepage = "index.html";
std::string page_404 = "404.html";

int main() {
    // 创建一个HttpResponse对象
    HttpResponse response;
    
    // 设置要返回的目标文件
    response.SetTargetFile("./wwwroot/about.html");
    
    // 构建HTTP响应(会自动设置状态码、Content-Type等)
    bool shouldSend = response.MakeResponse();
    
    if (shouldSend) {
        // 序列化为HTTP响应字符串
        std::string http_response = response.Serialize();
        
        std::cout << "生成的HTTP响应:" << std::endl;
        std::cout << http_response << std::endl;
        
        // 在实际服务器中,可以通过socket发送这个字符串
        // send(client_socket, http_response.c_str(), http_response.size(), 0);
    } else {
        std::cout << "不需要发送响应(如favicon.ico)" << std::endl;
    }
    
    return 0;
}

模块5:函数指针定义和Http类

cpp 复制代码
// 定义HTTP处理函数的类型:接受HttpRequest和HttpResponse的引用
using http_func_t = std::function<void(HttpRequest &req, HttpResponse &resp)>;

// Http服务器主类
// 功能:
// 1. 返回静态资源
// 2. 提供动态交互的能力
class Http
{
public:
    // 构造函数,创建TCP服务器
    Http(uint16_t port) : tsvrp(std::make_unique<TcpServer>(port))
    {
    }
    
    // 处理HTTP请求的核心方法
    void HandlerHttpRquest(std::shared_ptr<Socket> &sock, InetAddr &client)
    {
        // 收到请求
        std::string httpreqstr;
        
        // 假设:概率大,读到了完整的请求(实际可能有bug)
        // tcp是面向字节流的,这里假设一次recv就读取到完整请求
        int n = sock->Recv(&httpreqstr);  // 从socket接收HTTP请求字符串
        
        if (n > 0)  // 如果接收到了数据
        {
            // 打印原始请求(调试用)
            std::cout << "##########################" << std::endl;
            std::cout << httpreqstr;
            std::cout << "##########################" << std::endl;
            
            // 对报文完整性进行审核 -- 缺(TODO:需要实现)
            
            // 创建请求和响应对象
            HttpRequest req;
            HttpResponse resp;
            
            // 解析HTTP请求
            req.Deserialize(httpreqstr);
            
            if (req.isInteract())  // 如果是交互请求(带查询参数)
            {
                // _uri: ./wwwroot/login
                if (_route.find(req.Uri()) == _route.end())  // 路由不存在
                {
                    // 处理路由不存在的情况(TODO:实现302重定向等)
                    // SetCode(302)
                }
                else  // 路由存在
                {
                    // 调用注册的处理函数
                    _route[req.Uri()](req, resp);
                    
                    // 序列化响应并发送
                    std::string response_str = resp.Serialize();
                    sock->Send(response_str);
                }
            }
            else  // 静态资源请求
            {
                // 设置目标文件路径
                resp.SetTargetFile(req.Uri());
                
                // 构建响应
                if (resp.MakeResponse())
                {
                    // 序列化并发送响应
                    std::string response_str = resp.Serialize();
                    sock->Send(response_str);
                }
            }
        }

// 调试模式代码块
// #ifndef DEBUG
// #define DEBUG
#ifdef DEBUG
        // 收到请求
        std::string httpreqstr;
        // 假设:概率大,读到了完整的请求
        sock->Recv(&httpreqstr);  // 接收请求
        
        std::cout << httpreqstr;  // 打印请求

        // 直接构建http应答. 内存级别+固定
        HttpResponse resp;
        resp._version = "HTTP/1.1";
        resp._code = 200;  // success
        resp._desc = "OK";

        std::string filename = webroot + homepage;  // "./wwwroot/index.html";
        
        // 读取首页文件内容
        bool res = Util::ReadFileContent(filename, &(resp._text));
        (void)res;  // 避免未使用警告
        
        // 序列化并发送响应
        std::string response_str = resp.Serialize();
        sock->Send(response_str);
#endif
        // 对请求字符串,进行反序列化(已在上面的if块中处理)
    }
    
    // 启动HTTP服务器
    void Start()
    {
        // 启动TCP服务器,并绑定请求处理函数
        tsvrp->Start([this](std::shared_ptr<Socket> &sock, InetAddr &client)
                     { this->HandlerHttpRquest(sock, client); });
    }
    
    // 注册服务(动态请求处理)
    void RegisterService(const std::string name, http_func_t h)
    {
        // 构建完整路径键:./wwwroot/login
        std::string key = webroot + name;
        
        // 检查路由是否已存在
        auto iter = _route.find(key);
        if (iter == _route.end())  // 如果不存在
        {
            _route.insert(std::make_pair(key, h));  // 插入路由
        }
    }
    
    // 析构函数
    ~Http()
    {
    }

private:
    std::unique_ptr<TcpServer> tsvrp;  // TCP服务器实例(智能指针管理)
    std::unordered_map<std::string, http_func_t> _route;  // 路由表
};
  • 完整读取请求报头后,识别空行作为分隔符

  • 对报头进行解析,提取关键属性:Content-Length(表示有效载荷的数据长度)

  • 根据获取的Content-Length值,从后续内容中准确截取指定长度的有效载荷数据

基本用法(启动HTTP服务器)

cpp 复制代码
#include <iostream>
#include "http.h"  // 包含Http类头文件

int main() {
    // 1. 创建HTTP服务器,监听8080端口
    Http server(8080);
    
    // 2. 启动服务器
    server.Start();
    
    // 服务器开始运行,监听连接并处理请求
    return 0;
}

注册动态路由(处理表单提交)

cpp 复制代码
#include <iostream>
#include "http.h"

// 登录处理函数
void HandleLogin(HttpRequest &req, HttpResponse &resp) {
    std::cout << "处理登录请求" << std::endl;
    
    // 获取查询参数(如:username=zhangsan&password=123456)
    std::string args = req.Args();
    
    // 这里可以解析参数,验证用户名密码...
    // 简单示例:直接返回欢迎信息
    resp.SetCode(200);
    resp.SetHeader("Content-Type", "text/html");
    
    std::string response_body = R"(
        <html>
            <head><title>登录成功</title></head>
            <body>
                <h1>欢迎登录!</h1>
                <p>参数: )" + args + R"(</p>
            </body>
        </html>
    )";
    
    resp.SetText(response_body);
}

// 搜索处理函数
void HandleSearch(HttpRequest &req, HttpResponse &resp) {
    resp.SetCode(200);
    resp.SetHeader("Content-Type", "text/html");
    resp.SetText("<h1>搜索结果页面</h1><p>查询参数: " + req.Args() + "</p>");
}

int main() {
    // 创建HTTP服务器
    Http server(8080);
    
    // 注册动态路由(注意:路径不要带webroot前缀)
    server.RegisterService("/login", HandleLogin);      // 对应 ./wwwroot/login
    server.RegisterService("/search", HandleSearch);    // 对应 ./wwwroot/search
    
    // 启动服务器
    server.Start();
    
    return 0;
}

代码总结

这是一个简单的HTTP服务器实现,主要特点包括:

1. 架构设计

  • 使用面向对象设计,分离请求解析(HttpRequest)和响应构建(HttpResponse)

  • 支持静态文件服务和动态路由处理

  • 基于TcpServer实现底层网络通信

2. 功能特点

  • 静态文件服务:自动处理HTML、图片等静态资源

  • 动态路由:支持注册自定义处理函数

  • 错误处理:自动处理404错误,返回错误页面

  • 重定向支持:支持301/302重定向

  • Cookie支持:可设置Cookie

3. 主要流程

  1. 接收HTTP请求字符串

  2. 解析请求行和URI

  3. 判断是否为交互请求(带查询参数)

  4. 交互请求:查找路由并调用对应处理函数

  5. 静态请求:读取文件并构建响应

  6. 发送HTTP响应

4. 待改进点

  • 请求完整性验证缺失

  • 请求头解析不完整

  • 并发处理能力有限

  • 安全性考虑不足

  • POST请求体处理未实现

这个实现是一个教学级别的HTTP服务器,适合理解HTTP协议基本原理,但在生产环境中需要更完善的实现。这份代码是一个简单的HTTP服务器实现,支持静态资源服务和基本的动态请求处理。

1、总体结构

这个HTTP服务器由以下几个核心类组成:

  1. HttpRequest - 解析HTTP请求

  2. HttpResponse - 构建HTTP响应

  3. Http - 主服务器类,协调请求和响应

2、详细解析

1. 常量和头文件

cpp 复制代码
const std::string webroot = "./wwwroot";      // 网站根目录
const std::string homepage = "index.html";    // 默认首页
const std::string page_404 = "/404.html";     // 404错误页面
  • webroot:所有静态文件的存储目录

  • 使用#pragma once防止头文件重复包含

2. HttpRequest类 - 请求解析

主要功能:
  • 解析HTTP请求行 :从GET / HTTP/1.1中提取方法、URI、版本

  • 处理查询参数 :支持URL中的?参数

  • 路径补全:将相对路径转换为绝对路径

关键方法:
cpp 复制代码
// 示例:解析请求行
// 输入:"GET /login?user=admin HTTP/1.1"
// 输出:_method="GET", _uri="./wwwroot/login", _args="user=admin"

bool Deserialize(std::string &reqstr) {
    // 1. 读取请求行
    // 2. 解析方法、URI、版本
    // 3. 处理默认页面(/ -> /index.html)
    // 4. 分离查询参数
}
路径处理逻辑:
  • 请求/./wwwroot/index.html

  • 请求/about.html./wwwroot/about.html

  • 请求/login?user=admin./wwwroot/login + args="user=admin"

3. HttpResponse类 - 响应构建

主要功能:
  • 状态码管理:自动设置状态描述

  • 头部管理:添加HTTP头部

  • 文件服务:读取并返回静态文件

  • 重定向支持:301/302重定向

关键方法:
cpp 复制代码
std::string Serialize() {
    // 构建完整的HTTP响应:
    // 状态行: HTTP/1.0 200 OK\r\n
    // 头部: Content-Type: text/html\r\n
    //        Content-Length: 1234\r\n
    // 空行: \r\n
    // 正文: <html>...</html>
}

bool MakeResponse() {
    // 特殊文件处理(如favicon.ico)
    // 重定向测试页面
    // 文件存在性检查
    // 自动设置Content-Type和Content-Length
}
文件类型识别:
cpp 复制代码
// 根据文件后缀设置Content-Type
.html/.htm → "text/html"
.jpg → "image/jpeg"
.png → "image/png"

4. Http类 - 主服务器

核心架构:
cpp 复制代码
class Http {
private:
    std::unique_ptr<TcpServer> tsvrp;  // TCP服务器
    std::unordered_map<std::string, http_func_t> _route;  // 路由表
};
工作流程:
  1. 接收请求:通过TcpServer接收HTTP原始字符串

  2. 解析请求:使用HttpRequest解析

  3. 路由分发

    • 静态资源 → 直接返回文件

    • 动态请求 → 调用注册的处理函数

  4. 构建响应:使用HttpResponse构建HTTP响应

  5. 发送响应:通过socket发送给客户端

路由注册机制:
cpp 复制代码
// 注册动态处理函数
void RegisterService(const std::string name, http_func_t h) {
    // 示例:RegisterService("/login", LoginHandler)
    // 内部存储:"./wwwroot/login" -> LoginHandler
}

5. 请求处理流程

cpp 复制代码
void HandlerHttpRquest(std::shared_ptr<Socket> &sock, InetAddr &client) {
    // 1. 接收HTTP请求字符串
    // 2. 创建HttpRequest和HttpResponse对象
    // 3. 解析请求
    
    if (req.isInteract()) {  // 是否有查询参数(动态请求)
        // 查找路由表,调用对应的处理函数
        _route[req.Uri()](req, resp);
    } else {  // 静态资源请求
        // 设置目标文件,生成响应
        resp.SetTargetFile(req.Uri());
        resp.MakeResponse();
    }
    
    // 4. 序列化并发送响应
    sock->Send(resp.Serialize());
}

3、特色功能

1. 静态文件服务

  • 自动服务./wwwroot目录下的所有文件

  • 自动识别文件类型

  • 404错误处理

2. 动态请求支持

cpp 复制代码
// 可以注册这样的处理函数:
void LoginHandler(HttpRequest &req, HttpResponse &resp) {
    std::string args = req.Args();  // "username=admin&password=123"
    // 处理登录逻辑...
    resp.SetText("Login Success!");
    resp.SetCode(200);
}

// 注册路由
server.RegisterService("/login", LoginHandler);

3. 特殊处理

  • 忽略favicon.ico:避免浏览器重复请求

  • 重定向测试/redir_test重定向到QQ官网

  • Cookie设置 :示例中设置了Set-Cookie头部

4、代码优缺点

优点:

  1. 模块化设计:请求、响应、服务器分离

  2. 易于扩展:通过路由表支持动态处理

  3. 错误处理:基本的404页面支持

  4. 调试友好:详细的日志输出

局限性:

  1. HTTP协议不完整:只实现了基本功能

  2. 请求解析简单:未完整解析HTTP头部

  3. 内存问题:可能处理大文件时内存占用高

  4. 性能问题:单线程,未使用epoll等高效I/O

5、使用示例

cpp 复制代码
int main() {
    Http server(8080);  // 监听8080端口
    
    // 注册动态路由
    server.RegisterService("/api/test", [](HttpRequest &req, HttpResponse &resp){
        resp.SetText("Dynamic Response");
        resp.SetCode(200);
    });
    
    server.Start();  // 启动服务器
    return 0;
}

这是一个教学级别的HTTP服务器实现,适合学习HTTP协议和网络编程的基本原理。在生产环境中,建议使用更成熟的开源方案(如Nginx、Apache)。


二、Util.hpp

Util工具类代码详细讲解,这个工具类提供了文件读取和处理的常用功能,我将逐行详细讲解:

cpp 复制代码
// 防止头文件重复包含
#pragma once 

// 包含标准库头文件
#include <iostream>    // 输入输出流
#include <fstream>     // 文件流操作
#include <string>      // 字符串类

// 工具类 - 提供文件操作相关的静态方法
class Util
{
public:
    // 读取文件内容到字符串中
    // 参数:
    //   filename - 要读取的文件路径
    //   out      - 输出参数,用于存储读取的文件内容
    // 返回值:读取成功返回true,失败返回false
    static bool ReadFileContent(const std::string &filename /*std::vector<char>*/, std::string *out)
    {
        // version1: 原始版本,以文本方式读取文件(注释掉的代码)
        // 问题:对于二进制文件(如图片)处理不正确
        // std::ifstream in(filename);  // 创建输入文件流
        // if (!in.is_open())           // 检查文件是否成功打开
        // {
        //     return false;            // 打开失败返回false
        // }
        // std::string line;            // 临时存储每行内容
        // while(std::getline(in, line)) // 逐行读取文件
        // {
        //     *out += line;            // 将每行内容添加到输出字符串
        // }
        // in.close();                  // 关闭文件流

        // version2 : 以二进制方式进行读取(当前使用的版本)
        // 优点:可以正确读取文本文件和二进制文件
        
        // 首先获取文件大小
        int filesize = FileSize(filename);  // 调用FileSize方法获取文件大小
        
        // 如果文件大小大于0,说明文件存在且有内容
        if(filesize > 0)
        {
            std::ifstream in(filename);     // 创建输入文件流(默认文本模式)
            if(!in.is_open())               // 检查文件是否成功打开
                return false;               // 打开失败返回false
            
            // 调整输出字符串的大小为文件大小
            // 注意:这里有一个潜在问题,c_str()返回的是const char*,不应该被修改
            // 实际上应该使用:out->resize(filesize); 然后使用&(*out)[0]获取可写指针
            out->resize(filesize);          // 调整字符串大小
            
            // 读取整个文件内容到字符串中
            // 这里使用了c_str()获取字符串的内部缓冲区指针,然后强制转换为char*
            // 更安全的写法:in.read(&(*out)[0], filesize);
            in.read((char*)(out->c_str()), filesize);  // 读取filesize字节到字符串中
            
            in.close();                     // 关闭文件流
        }
        else  // 文件大小为0或负数(文件不存在或为空)
        {
            return false;                   // 返回false表示读取失败
        }

        return true;  // 读取成功返回true
    }
    
    // 从大字符串中读取一行(以指定分隔符为界)
    // 参数:
    //   bigstr - 输入的大字符串,读取后会被修改(移除已读取的部分)
    //   out    - 输出参数,存储读取的一行内容
    //   sep    - 行分隔符,通常是"\r\n"
    // 返回值:成功读取一行返回true,否则返回false
    static bool ReadOneLine(std::string &bigstr, std::string *out, const std::string &sep/*\r\n*/)
    {
        // 在bigstr中查找分隔符的位置
        auto pos = bigstr.find(sep);
        
        // 如果没有找到分隔符,说明没有完整的一行
        if(pos == std::string::npos)
            return false;  // 返回false

        // 从开始到分隔符位置就是一行内容
        *out = bigstr.substr(0, pos);
        
        // 从bigstr中删除已经读取的部分(包括分隔符)
        // 这样可以继续读取下一行
        bigstr.erase(0, pos + sep.size());
        
        return true;  // 成功读取一行返回true
    }
    
    // 获取文件大小(以字节为单位)
    // 参数:
    //   filename - 要检查的文件路径
    // 返回值:文件大小(字节数),文件不存在或无法打开返回-1
    static int FileSize(const std::string &filename)
    {
        // 以二进制模式打开文件
        // std::ios::binary 确保以二进制方式读取,避免文本模式下的转换
        std::ifstream in(filename, std::ios::binary);
        
        // 检查文件是否成功打开
        if(!in.is_open())
            return -1;  // 打开失败返回-1
        
        // 将文件指针移动到文件末尾
        // 参数说明:
        //   0 - 偏移量
        //   in.end - 相对于文件末尾
        in.seekg(0, in.end);
        
        // 获取当前位置(即文件大小)
        // tellg()返回当前读取位置
        int filesize = in.tellg();
        
        // 将文件指针移回文件开头
        // 这样不会影响后续的读取操作(如果有的话)
        in.seekg(0, in.beg);
        
        in.close();  // 关闭文件流
        
        return filesize;  // 返回文件大小
    }
};

1、代码总结与注意事项

1. 类设计特点

  • 静态工具类:所有方法都是静态的,不需要创建Util对象

  • 功能聚焦:专注于文件操作相关的工具函数

  • 错误处理:每个方法都有明确的成功/失败返回值

2. 方法详细说明

ReadFileContent方法
  • 功能:读取整个文件内容到字符串中

  • 改进:从文本模式改为二进制模式,支持图片等二进制文件

  • 潜在问题

    cpp 复制代码
    // 不安全的写法:
    in.read((char*)(out->c_str()), filesize);
    
    // 更安全的写法:
    out->resize(filesize);
    in.read(&(*out)[0], filesize);  // 或者 in.read(out->data(), filesize);

    因为c_str()返回的是const char*,不应该被写入。虽然在某些实现中可能工作,但不是标准做法。

ReadOneLine方法
  • 功能:从大字符串中提取一行(基于自定义分隔符)

  • 应用场景:HTTP协议解析中读取请求行和头部

  • 副作用 :会修改输入字符串bigstr,移除已读取的部分

FileSize方法
  • 功能:获取文件大小

  • 实现原理 :使用seekgtellg组合

  • 二进制模式 :使用std::ios::binary确保正确计算大小

3. 设计模式

  • 工具类模式:提供一组相关的静态方法

  • 无状态:不维护任何成员变量,所有方法都是纯函数

  • 参数设计

    • 使用std::string*作为输出参数(C++风格)

    • 使用引用参数std::string&允许修改原字符串

4. 使用示例

cpp 复制代码
// 读取文件内容
std::string content;
if (Util::ReadFileContent("test.txt", &content)) {
    std::cout << "文件内容:" << content << std::endl;
}

// 获取文件大小
int size = Util::FileSize("test.txt");
if (size > 0) {
    std::cout << "文件大小:" << size << "字节" << std::endl;
}

// 逐行读取
std::string text = "第一行\r\n第二行\r\n第三行";
std::string line;
while (Util::ReadOneLine(text, &line, "\r\n")) {
    std::cout << "行内容:" << line << std::endl;
}

5. 改进建议

  1. 异常处理:考虑添加异常处理机制

  2. 性能优化:大文件读取可以考虑分块读取

  3. 跨平台 :路径分隔符处理(Windows使用\,Unix使用/

  4. 编码处理:考虑不同编码格式的文件读取

  5. 内存安全 :修正ReadFileContent中的潜在问题

这个工具类是HTTP服务器的重要组成部分,为文件操作提供了基础支持。


三、引入之前实现过的相关头文件

Common.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <functional>
#include <unistd.h>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

enum ExitCode
{
    OK = 0,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR,
    FORK_ERR,
    OPEN_ERR
};

class NoCopy
{
public:
    NoCopy(){}
    ~NoCopy(){}
    NoCopy(const NoCopy &) = delete;
    const NoCopy &operator = (const NoCopy&) = delete;
};

#define CONV(addr) ((struct sockaddr*)&addr)

InetAddr.hpp

cpp 复制代码
#pragma once
#include "Common.hpp"
// 网络地址和主机地址之间进行转换的类

class InetAddr
{
public:
    InetAddr() {}
    InetAddr(struct sockaddr_in &addr)
    {
        SetAddr(addr);
    }
    InetAddr(const std::string &ip, uint16_t port) : _ip(ip), _port(port)
    {
        // 主机转网络
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
        _addr.sin_port = htons(_port);
        // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
    }
    InetAddr(uint16_t port) : _port(port), _ip()
    {
        // 主机转网络
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_addr.s_addr = INADDR_ANY;
        _addr.sin_port = htons(_port);
    }
    void SetAddr(struct sockaddr_in &addr)
    {
        _addr = addr;
        // 网络转主机
        _port = ntohs(_addr.sin_port); // 从网络中拿到的!网络序列
        // _ip = inet_ntoa(_addr.sin_addr); // 4字节网络风格的IP -> 点分十进制的字符串风格的IP
        char ipbuffer[64];
        inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(_addr));
        _ip = ipbuffer;
    }
    uint16_t Port() { return _port; }
    std::string Ip() { return _ip; }
    const struct sockaddr_in &NetAddr() { return _addr; }
    const struct sockaddr *NetAddrPtr()
    {
        return CONV(_addr);
    }
    socklen_t NetAddrLen()
    {
        return sizeof(_addr);
    }
    bool operator==(const InetAddr &addr)
    {
        return addr._ip == _ip && addr._port == _port;
    }
    std::string StringAddr()
    {
        return _ip + ":" + std::to_string(_port);
    }
    ~InetAddr()
    {
    }

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

Log.hpp

cpp 复制代码
#ifndef __LOG_HPP__
#define __LOG_HPP__

#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> //C++17
#include <sstream>
#include <fstream>
#include <memory>
#include <ctime>
#include <unistd.h>
#include "Mutex.hpp"

namespace LogModule
{
    using namespace MutexModule;

    const std::string gsep = "\r\n";
    // 策略模式,C++多态特性
    // 2. 刷新策略 a: 显示器打印 b:向指定的文件写入
    //  刷新策略基类
    class LogStrategy
    {
    public:
        ~LogStrategy() = default;
        virtual void SyncLog(const std::string &message) = 0;
    };

    // 显示器打印日志的策略 : 子类
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        ConsoleLogStrategy()
        {
        }
        void SyncLog(const std::string &message) override
        {
            LockGuard lockguard(_mutex);
            std::cout << message << gsep;
        }
        ~ConsoleLogStrategy()
        {
        }

    private:
        Mutex _mutex;
    };

    // 文件打印日志的策略 : 子类
    const std::string defaultpath = "/var/log/";
    const std::string defaultfile = "my.log";
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile)
            : _path(path),
              _file(file)
        {
            LockGuard lockguard(_mutex);
            if (std::filesystem::exists(_path))
            {
                return;
            }
            try
            {
                std::filesystem::create_directories(_path);
            }
            catch (const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << '\n';
            }
        }
        void SyncLog(const std::string &message) override
        {
            LockGuard lockguard(_mutex);

            std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file; // "./log/" + "my.log"
            std::ofstream out(filename, std::ios::app);                              // 追加写入的 方式打开
            if (!out.is_open())
            {
                return;
            }
            out << message << gsep;
            out.close();
        }
        ~FileLogStrategy()
        {
        }

    private:
        std::string _path; // 日志文件所在路径
        std::string _file; // 日志文件本身
        Mutex _mutex;
    };

    // 形成一条完整的日志&&根据上面的策略,选择不同的刷新方式

    // 1. 形成日志等级
    enum class LogLevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };
    std::string Level2Str(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }
    std::string GetTimeStamp()
    {
        time_t curr = time(nullptr);
        struct tm curr_tm;
        localtime_r(&curr, &curr_tm);
        char timebuffer[128];
        snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",
            curr_tm.tm_year+1900,
            curr_tm.tm_mon+1,
            curr_tm.tm_mday,
            curr_tm.tm_hour,
            curr_tm.tm_min,
            curr_tm.tm_sec
        );
        return timebuffer;
    }

    // 1. 形成日志 && 2. 根据不同的策略,完成刷新
    class Logger
    {
    public:
        Logger()
        {
            EnableConsoleLogStrategy();
        }
        void EnableFileLogStrategy()
        {
            _fflush_strategy = std::make_unique<FileLogStrategy>();
        }
        void EnableConsoleLogStrategy()
        {
            _fflush_strategy = std::make_unique<ConsoleLogStrategy>();
        }

        // 表示的是未来的一条日志
        class LogMessage
        {
        public:
            LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
                : _curr_time(GetTimeStamp()),
                  _level(level),
                  _pid(getpid()),
                  _src_name(src_name),
                  _line_number(line_number),
                  _logger(logger)
            {
                // 日志的左边部分,合并起来
                std::stringstream ss;
                ss << "[" << _curr_time << "] "
                   << "[" << Level2Str(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _src_name << "] "
                   << "[" << _line_number << "] "
                   << "- ";
                _loginfo = ss.str();
            }
            // LogMessage() << "hell world" << "XXXX" << 3.14 << 1234
            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                // a = b = c =d;
                // 日志的右半部分,可变的
                std::stringstream ss;
                ss << info;
                _loginfo += ss.str();
                return *this;
            }

            ~LogMessage()
            {
                if (_logger._fflush_strategy)
                {
                    _logger._fflush_strategy->SyncLog(_loginfo);
                }
            }

        private:
            std::string _curr_time;
            LogLevel _level;
            pid_t _pid;
            std::string _src_name;
            int _line_number;
            std::string _loginfo; // 合并之后,一条完整的信息
            Logger &_logger;
        };

        // 这里故意写成返回临时对象
        LogMessage operator()(LogLevel level, std::string name, int line)
        {
            return LogMessage(level, name, line, *this);
        }
        ~Logger()
        {
        }

    private:
        std::unique_ptr<LogStrategy> _fflush_strategy;
    };

    // 全局日志对象
    Logger logger;

    // 使用宏,简化用户操作,获取文件名和行号
    #define LOG(level) logger(level, __FILE__, __LINE__)
    #define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
    #define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}

#endif

Mutex.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <pthread.h>

namespace MutexModule
{
    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(&_mutex, nullptr);
        }
        void Lock()
        {
            int n = pthread_mutex_lock(&_mutex);
            (void)n;
        }
        void Unlock()
        {
            int n = pthread_mutex_unlock(&_mutex);
            (void)n;
        }
        ~Mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }
        pthread_mutex_t *Get()
        {
            return &_mutex;
        }
    private:
        pthread_mutex_t _mutex;
    };

    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex):_mutex(mutex)
        {
            _mutex.Lock();
        }
        ~LockGuard()
        {
            _mutex.Unlock();
        }
    private:
        Mutex &_mutex;
    };
}

Socket.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

namespace SocketModule
{
    using namespace LogModule;
    const static int gbacklog = 16;
    // 模版方法模式
    // 基类socket, 大部分方法,都是纯虚方法
    class Socket
    {
    public:
        virtual ~Socket() {}
        virtual void SocketOrDie() = 0;
        virtual void BindOrDie(uint16_t port) = 0;
        virtual void ListenOrDie(int backlog) = 0;
        virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
        virtual void Close() = 0;
        virtual int Recv(std::string *out) = 0;
        virtual int Send(const std::string &message) = 0;
        virtual int Connect(const std::string &server_ip, uint16_t port) = 0;

    public:
        void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog)
        {
            SocketOrDie();
            BindOrDie(port);
            ListenOrDie(backlog);
        }
        void BuildTcpClientSocketMethod()
        {
            SocketOrDie();
        }
        // void BuildUdpSocketMethod()
        // {
        //     SocketOrDie();
        //     BindOrDie();
        // }
    };

    const static int defaultfd = -1;

    class TcpSocket : public Socket
    {
    public:
        TcpSocket() : _sockfd(defaultfd)
        {
        }
        TcpSocket(int fd) : _sockfd(fd)
        {
        }
        ~TcpSocket() {}
        void SocketOrDie() override
        {
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                LOG(LogLevel::FATAL) << "socket error";
                exit(SOCKET_ERR);
            }
            LOG(LogLevel::INFO) << "socket success";
        }
        void BindOrDie(uint16_t port) override
        {
            InetAddr localaddr(port);
            int n = ::bind(_sockfd, localaddr.NetAddrPtr(), localaddr.NetAddrLen());
            if (n < 0)
            {
                LOG(LogLevel::FATAL) << "bind error";
                exit(BIND_ERR);
            }
            LOG(LogLevel::INFO) << "bind success";
        }
        void ListenOrDie(int backlog) override
        {
            int n = ::listen(_sockfd, backlog);
            if (n < 0)
            {
                LOG(LogLevel::FATAL) << "listen error";
                exit(LISTEN_ERR);
            }
            LOG(LogLevel::INFO) << "listen success";
        }
        std::shared_ptr<Socket> Accept(InetAddr *client) override
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int fd = ::accept(_sockfd, CONV(peer), &len);
            if (fd < 0)
            {
                LOG(LogLevel::WARNING) << "accept warning ...";
                return nullptr; // TODO
            }
            client->SetAddr(peer);
            return std::make_shared<TcpSocket>(fd);
        }
        // n == read的返回值
        int Recv(std::string *out) override
        {
            // 流式读取,不关心读到的是什么
            char buffer[4096*2];
            ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);
            if (n > 0)
            {
                buffer[n] = 0;
                *out += buffer; // 故意
            }
            return n;
        }
        int Send(const std::string &message) override
        {
            return send(_sockfd, message.c_str(), message.size(), 0);
        }
        void Close() override //??
        {
            if (_sockfd >= 0)
                ::close(_sockfd);
        }
        int Connect(const std::string &server_ip, uint16_t port) override
        {
            InetAddr server(server_ip, port);
            return ::connect(_sockfd, server.NetAddrPtr(), server.NetAddrLen());
        }

    private:
        int _sockfd; // _sockfd , listensockfd, sockfd;
    };

    // class UdpSocket : public Socket
    // {
    // };
}

TcpServer.hpp

cpp 复制代码
#include "Socket.hpp"
#include <iostream>
#include <memory>
#include <sys/wait.h>
#include <functional>

using namespace SocketModule;
using namespace LogModule;

using ioservice_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>;

class TcpServer
{
public:
    TcpServer(uint16_t port) : _port(port),
                               _listensockptr(std::make_unique<TcpSocket>()),
                               _isrunning(false)
    {
        _listensockptr->BuildTcpSocketMethod(_port);
    }
    void Start(ioservice_t callback)
    {
        _isrunning = true;
        while (_isrunning)
        {
            InetAddr client;
            auto sock = _listensockptr->Accept(&client); // 1. 和client通信sockfd 2. client 网络地址
            if (sock == nullptr)
            {
                continue;
            }
            LOG(LogLevel::DEBUG) << "accept success ..." << client.StringAddr();

            // sock && client
            pid_t id = fork();
            if (id < 0)
            {
                LOG(LogLevel::FATAL) << "fork error ...";
                // excepter(sock); // 
                exit(FORK_ERR);
            }
            else if (id == 0)
            {
                // 子进程 -> listensock
                _listensockptr->Close();
                if (fork() > 0)
                    exit(OK);
                // 孙子进程在执行任务,已经是孤儿了
                callback(sock, client);
                sock->Close();
                exit(OK);
            }
            else
            {
                // 父进程 -> sock
                sock->Close();
                pid_t rid = ::waitpid(id, nullptr, 0);
                (void)rid;
            }
        }
        _isrunning = false;
    }
    ~TcpServer() {}

private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensockptr;
    bool _isrunning;

    //func_t excepter; // 服务器异常的回调
};

四、相关的HTML文件

404.html

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>404 - 页面未找到</title>
    <style>
        body {
            font-family: 'Arial', sans-serif;
            background-color: #f5f5f5;
            color: #333;
            text-align: center;
            padding: 50px 0;
            margin: 0;
            line-height: 1.6;
        }
        .container {
            max-width: 600px;
            margin: 0 auto;
            padding: 20px;
            background: white;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h1 {
            font-size: 5em;
            margin: 0;
            color: #e74c3c;
        }
        h2 {
            margin-top: 0;
            color: #333;
        }
        p {
            margin-bottom: 30px;
        }
        a {
            color: #3498db;
            text-decoration: none;
            font-weight: bold;
        }
        a:hover {
            text-decoration: underline;
        }
        .emoji {
            font-size: 3em;
            margin-bottom: 20px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="emoji">😕</div>
        <h1>404</h1>
        <h2>页面未找到</h2>
        <p>抱歉,您访问的页面不存在或已被移除。</p>
        <p>您可以返回<a href="/">首页</a>或检查URL是否正确。</p>
    </div>
</body>
</html>

Login.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login Page</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f4f4f4;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
        }
        .login-container {
            background-color: #fff;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            width: 300px;
            text-align: center;
        }
        .login-container h2 {
            margin-bottom: 20px;
        }
        .form-group {
            margin-bottom: 15px;
        }
        .form-group label {
            display: block;
            margin-bottom: 5px;
            text-align: left;
        }
        .form-group input {
            width: 100%;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }
        .form-group input[type="submit"] {
            background-color: #007bff;
            color: #fff;
            border: none;
            cursor: pointer;
        }
        .form-group input[type="submit"]:hover {
            background-color: #0056b3;
        }
    </style>
</head>
<body>
    <div class="login-container">
        <h2>Login</h2>
        <form action="/login" method="GET">
            <div class="form-group">
                <label for="username">Username</label>
                <input type="text" id="username" name="username" required>
            </div>
            <div class="form-group">
                <label for="password">Password</label>
                <input type="password" id="password" name="password" required>
            </div>
            <div class="form-group">
                <input type="submit" value="Login">
            </div>
            <a href="Register.html">Login</a> <!-- 跳转到登录页面 -->
            <a href="index.html">Register</a> <!-- 跳转到注册页面 -->
        </form>
    </div>
</body>
</html>

Register.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Register Page</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f4f4f4;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
        }
        .register-container {
            background-color: #fff;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            width: 350px;
            text-align: center;
        }
        .register-container h2 {
            margin-bottom: 20px;
        }
        .form-group {
            margin-bottom: 15px;
        }
        .form-group label {
            display: block;
            margin-bottom: 5px;
            text-align: left;
        }
        .form-group input {
            width: 100%;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }
        .form-group input[type="submit"] {
            background-color: #28a745;
            color: #fff;
            border: none;
            cursor: pointer;
        }
        .form-group input[type="submit"]:hover {
            background-color: #218838;
        }
    </style>
</head>
<body>
    <div class="register-container">
        <h2>Register</h2>
        <form action="/register" method="GET">
            <div class="form-group">
                <label for="username">Username</label>
                <input type="text" id="username" name="username" required>
            </div>
            <div class="form-group">
                <label for="email">Email</label>
                <input type="email" id="email" name="email" required>
            </div>
            <div class="form-group">
                <label for="password">Password</label>
                <input type="password" id="password" name="password" required>
            </div>
            <div class="form-group">
                <label for="confirm-password">Confirm Password</label>
                <input type="password" id="confirm-password" name="confirm-password" required>
            </div>
            <div class="form-group">
                <input type="submit" value="Register">
            </div>
            <a href="Login.html">Login</a> <!-- 跳转到登录页面 -->
            <a href="index.html">Register</a> <!-- 跳转到注册页面 -->
        </form>
    </div>
</body>
</html>

index.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Default Home Page</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            background-color: #f4f4f4;
            color: #333;
        }
        header {
            background-color: #007bff;
            color: #fff;
            padding: 10px 20px;
            text-align: center;
        }
        nav {
            background-color: #343a40;
            padding: 10px 0;
        }
        nav a {
            color: #fff;
            text-decoration: none;
            padding: 10px 20px;
            display: inline-block;
        }
        nav a:hover {
            background-color: #5a6268;
        }
        .container {
            padding: 20px;
        }
        .welcome {
            text-align: center;
            margin-bottom: 20px;
        }
        .welcome h1 {
            margin: 0;
        }
        .content {
            background-color: #fff;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        }
        footer {
            background-color: #343a40;
            color: #fff;
            text-align: center;
            padding: 10px 0;
            position: fixed;
            width: 100%;
            bottom: 0;
        }
    </style>
</head>
<body>
    <header>
        <h1>Welcome to Our Website</h1>
    </header>
    <nav>
        <a href="#">Home</a>
        <a href="Login.html">Login</a> <!-- 跳转到登录页面 -->
        <a href="Register.html">Register</a> <!-- 跳转到注册页面 -->
        <a href="#">About</a>
        <a href="#">Contact</a>
    </nav>
    <div class="container">
        <div class="welcome">
            <h1>Welcome to Our Default Home Page</h1>
            <p>This is a simple default home page template.</p>
        </div>
        <div class="content">
            <h2>Introduction</h2>
            <p>This is a basic HTML template for a default home page. It includes a header, navigation bar, a welcome section, and a content area. You can customize this template to suit your needs.</p>
        </div>

        <!-- <a href="http://8.137.19.140:8081/image/1.png">板书1</a>
        <a href="http://8.137.19.140:8081/image/2.png">板书2</a>
        <a href="http://8.137.19.140:8081/image/3.png">板书3</a>
        <a href="http://8.137.19.140:8081/image/4.png">板书4</a>
        <a href="http://8.137.19.140:8081/image/5.jpg">图片5</a> -->

        <img src="/image/5.jpg"/>
        <!-- <img src="/image/1.png"/> -->
        <a href="/board1.html">查看板书</a>
    </div>
    <footer>
        <p>&copy; 2025 Your Company Name. All rights reserved.</p>
    </footer>
</body>
</html>

test.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <a href="https://www.qq.com/">点我</a>
</body>
</html>

五、Main.cc

HTTP服务器主程序代码详细讲解,这是HTTP服务器的主程序文件,包含入口函数和示例处理函数:

cpp 复制代码
// 包含HTTP服务器主类的头文件
#include "Http.hpp"

// Login函数:处理用户登录请求
// 参数:
//   req - HTTP请求对象,包含客户端请求信息
//   resp - HTTP响应对象,用于构建返回给客户端的响应
void Login(HttpRequest &req, HttpResponse &resp)
{
    // 从请求中获取查询参数(args()返回查询字符串,如"username=zhangsan&passwd=123456")
    // 注意:这里只是打印日志,实际上应该解析参数
    LOG(LogLevel::DEBUG) << req.Args() << ", 我们成功进入到了处理数据的逻辑";
    
    // 构建简单的响应文本
    // 这里直接拼接查询参数,实际应用中应该解析参数并进行验证
    std::string text = "hello: " + req.Args(); // 例如: "hello: username=zhangsan&password=123456"

    // TODO: 实际登录认证逻辑应该在这里实现
    // 1. 解析查询参数,获取用户名和密码
    // 2. 验证用户名密码是否正确
    // 3. 根据验证结果返回不同响应

    // 设置HTTP响应状态码为200(成功)
    resp.SetCode(200);
    
    // 设置响应头:Content-Type指定响应体类型为纯文本
    resp.SetHeader("Content-Type","text/plain");
    
    // 设置响应头:Content-Length指定响应体大小
    resp.SetHeader("Content-Length", std::to_string(text.size()));
    
    // 设置响应体内容
    resp.SetText(text);
}

// 以下是被注释掉的示例处理函数,展示了如何添加更多业务逻辑

// void Register(HttpRequest &req, HttpResponse &resp)
// {
//     // 注册功能处理函数
//     LOG(LogLevel::DEBUG) << req.Args() << ", 我们成功进入到了处理数据的逻辑";
//     std::string text = "hello: " + req.Args();
//
//     resp.SetCode(200);
//     resp.SetHeader("Content-Type","text/plain");
//     resp.SetHeader("Content-Length", std::to_string(text.size()));
//     resp.SetText(text);
// }

// void VipCheck(HttpRequest &req, HttpResponse &resp)
// {
//     // VIP检查功能处理函数
//     LOG(LogLevel::DEBUG) << req.Args() << ", 我们成功进入到了处理数据的逻辑";
//     std::string text = "hello: " + req.Args();
//     resp.SetCode(200);
//     resp.SetHeader("Content-Type","text/plain");
//     resp.SetHeader("Content-Length", std::to_string(text.size()));
//     resp.SetText(text);
// }

// void Search(HttpRequest &req, HttpResponse &resp)
// {
//     // 搜索功能处理函数
//     // 这里没有实现具体逻辑
// }

// HTTP服务器主入口函数
// 参数:
//   argc - 命令行参数个数
//   argv - 命令行参数数组
int main(int argc, char *argv[])
{
    // 检查命令行参数数量
    // 程序期望接收一个参数:端口号
    // 正确用法:./server 8080
    if(argc != 2)  // 如果不是2个参数(程序名+端口号)
    {
        // 打印使用说明
        std::cout << "Usage: " << argv[0] << " port" << std::endl;
        // 退出程序,使用预定义的错误码USAGE_ERR(可能在其他头文件中定义)
        exit(USAGE_ERR);
    }
    
    // 将命令行参数转换为端口号
    // argv[0] 是程序名,argv[1] 是端口号字符串
    uint16_t port = std::stoi(argv[1]);  // stoi将字符串转换为整数

    // 创建HTTP服务器实例
    // 使用智能指针管理,避免内存泄漏
    // std::make_unique是C++14特性,创建unique_ptr智能指针
    std::unique_ptr<Http> httpsvr = std::make_unique<Http>(port);
    
    // 注册服务(即注册动态请求处理函数)
    // 将URI路径与处理函数绑定
    // 当用户访问"/login"时,会调用Login函数处理
    httpsvr->RegisterService("/login", Login);
    
    // 以下是其他示例服务的注册(被注释掉了)
    // httpsvr->RegisterService("/register", Register);
    // httpsvr->RegisterService("/vip_check", VipCheck);
    // httpsvr->RegisterService("/s", Search);
    // httpsvr->RegisterService("/", Login);  // 根路径也可以绑定处理函数

    // 启动HTTP服务器
    // Start()方法会启动服务器并进入事件循环,等待客户端连接
    httpsvr->Start();
    
    // 程序结束,返回0表示正常退出
    // 注意:由于Start()方法可能会阻塞,这里的return只有在服务器停止后才会执行
    return 0;
}

1、代码架构分析

1. 处理函数设计模式

cpp 复制代码
// 处理函数的标准签名
void HandlerName(HttpRequest &req, HttpResponse &resp)
{
    // 1. 从req中提取参数
    // 2. 执行业务逻辑
    // 3. 设置resp的各个部分
}

2. 路由注册机制

cpp 复制代码
// 路由表结构
std::unordered_map<std::string, http_func_t> _route;

// 注册路由
httpsvr->RegisterService("/login", Login);
// 内部实现:将"/login"映射到Login函数

3. 请求处理流程

客户端请求 → 服务器接收 → 解析请求 → 路由匹配 → 调用处理函数 → 构建响应 → 发送响应

2、重要技术细节

1. 查询参数处理

cpp 复制代码
// 请求示例:GET /login?username=zhangsan&password=123456
// req.Args() 返回:"username=zhangsan&password=123456"

// 实际应用中需要解析这个字符串:
// std::string args = req.Args();
// 然后按'&'分割键值对,再按'='分割键和值

2. HTTP响应构建

cpp 复制代码
// 一个完整的HTTP响应包含:
resp.SetCode(200);                    // 状态码
resp.SetHeader("Content-Type", ...);  // 内容类型
resp.SetHeader("Content-Length", ...);// 内容长度
resp.SetText(...);                    // 响应体

3. 日志记录

cpp 复制代码
// 使用日志系统记录调试信息
LOG(LogLevel::DEBUG) << "日志信息";
// 有助于调试和监控服务器运行状态

3、运行服务器

使用tree命令查看当前项目的全局结构:

可以看出,在浏览器端发起请求时,首页作为网站的入口点,整个站点结构呈现为多叉树形态。当用户点击链接时,浏览器会生成新的访问地址并触发二次请求。所有请求的资源都通过HTTP请求的URI进行标识。

编译和运行:

cpp 复制代码
# 编译(假设使用g++)
g++ -std=c++14 -o httpserver main.cpp Http.cpp Util.cpp Socket.cpp TcpServer.cpp -lpthread

# 运行(在8080端口启动服务器)
./httpserver 8080

这里我们使用makefile文件来编译:

bash 复制代码
myhttp:Main.cc
	g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
	rm -f myhttp

编译完成后,我们运行服务端:

测试请求:

bash 复制代码
# 访问静态文件
curl http://113.45.79.2:8080/index.html

我们也可以直接通过浏览器来访问,这样更直观并符合HTML的演示:

bash 复制代码
# 访问动态接口
curl http://113.45.79.2:8080/login?username=zhangsan&password=123456

总结

这个HTTP服务器示例展示了:

  1. 基于TCP的HTTP服务器基本架构

  2. 静态文件服务和动态API的统一处理

  3. 简单的路由注册机制

  4. 请求/响应的完整处理流程

可以作为学习HTTP协议和服务器开发的入门示例,理解请求处理、响应构建的基本原理。

相关推荐
Crazy________1 小时前
45Ansible Roles:标准化部署的终极利器
linux·运维·服务器
翼龙云_cloud1 小时前
腾讯云渠道商:腾讯云轻量服务器和CVM有什么差异?
运维·服务器·云计算·php·腾讯云
汤愈韬1 小时前
知识点4:Nat Server的Server-map 跟ASPF中的server map区别与联系
网络协议·网络安全·security·huawei
qq_296544651 小时前
驱动精灵、驱动人生、NVIDIA专业显卡驱动、360驱动大师、联想乐驱动,电脑驱动修复工具大全
网络·电脑·负载均衡
wanhengidc1 小时前
服务器受到病毒攻击该怎么办
运维·服务器·科技·云计算
wadesir1 小时前
Linux网络优化服务配置(从零开始提升服务器网络性能)
linux·服务器·网络
小李独爱秋1 小时前
计算机网络经典问题透视:简述一下TCP拥塞控制算法中的拥塞避免算法
服务器·网络·tcp/ip·计算机网络·php
tianyuanwo1 小时前
从PAM到零信任:Linux密码认证体系的深度解析与演进
linux·运维·服务器·pam·密码认证
シ風箏2 小时前
Shell【脚本 06】监测文件数据量并压缩及查看远程服务器状态并删除文件脚本分享
linux·运维·服务器·github·shell