从零开始实现 C++ TinyWebServer 构建响应 HttpResponse类详解

文章目录

  • [HTTP 请求报文](#HTTP 请求报文)
  • [HttpResponse 类](#HttpResponse 类)
  • 实现构造和析构函数
  • [实现 Init() 函数](#实现 Init() 函数)
  • [实现 UnmapFile() 函数](#实现 UnmapFile() 函数)
  • [实现 ErrorHtml() 函数](#实现 ErrorHtml() 函数)
  • [实现 AddStateLine() 函数](#实现 AddStateLine() 函数)
  • [实现 AddHeader() 函数](#实现 AddHeader() 函数)
  • [实现 GetFileType() 函数](#实现 GetFileType() 函数)
  • [实现 AddContent() 函数](#实现 AddContent() 函数)
  • [实现 ErrorContent() 函数](#实现 ErrorContent() 函数)
  • [实现 MakeResponse() 函数](#实现 MakeResponse() 函数)
  • [HttpResponse 代码](#HttpResponse 代码)
  • [HttpResponse 测试](#HttpResponse 测试)

从零开始实现 C++ TinyWebServer 项目总览
项目源码

HTTP 请求报文

一个典型的 HTTP 响应包含以下部分:

  • 状态行 :例如 HTTP/1.1 200 OK
  • 响应头 :包含各种元数据,如Content-TypeContent-LengthConnection等。
  • 响应体:实际传输的数据,如 HTML 文件内容。

HttpResponse 类

在 Web 服务器中,HttpResponse 类起着至关重要的作用,主要负责处理服务器向客户端发送的 HTTP 响应。具体来说,它承担了以下几个关键任务:

  1. 响应构建与内容处理 :依据服务器对客户端请求的处理结果,生成符合 HTTP 协议规范的响应报文。该过程涵盖设置合适的 HTTP 状态码及对应的状态描述,添加必要的响应头信息(如 ConnectionContent-typeContent-length 等),同时处理响应体内容。若请求的是文件资源,会将文件内容映射到内存并作为响应体;若出现错误,则生成包含错误信息的 HTML 页面作为响应体。
  2. 资源管理:负责管理与响应相关的资源,如文件映射的内存。在不需要这些资源时,会及时释放,避免资源泄漏,确保服务器的稳定运行。
  3. 与客户端交互:将生成好的完整 HTTP 响应发送给客户端,使得客户端能够根据响应内容进行相应的处理,如显示网页、下载文件等,从而实现服务器与客户端之间的有效通信。
cpp 复制代码
class HttpResponse {
public:
    HttpResponse();
    ~HttpResponse();

    void Init(const std::string& path, const std::string& src_dir, int code = -1, bool is_keep_alive = false);
    void UnmapFile();

    void MakeResponse(Buffer& buff);
    void ErrorContent(Buffer& buff, const std::string& message);

    char* File() { return mmfile_; }
    size_t FileLen() const { return mmfile_stat_.st_size; }
    int Code() const { return code_; }

private:
    void AddStateLine(Buffer& buff);
    void AddHeader(Buffer& buff);
    void AddContent(Buffer& buff);

    void ErrorHtml();
    std::string GetFileType();

    static const std::unordered_map<int, std::string> CODE_STATUS;          // 编码状态集
    static const std::unordered_map<int, std::string> CODE_PATH;            // 编码路径集
    static const std::unordered_map<std::string, std::string> SUFFIX_TYPE;  // 后缀状态集

    int code_; // 响应状态码
    bool is_keep_alive_;

    std::string path_;
    std::string src_dir_;

    char* mmfile_;
    struct stat mmfile_stat_;
};

实现构造和析构函数

cpp 复制代码
HttpResponse::HttpResponse() 
    : code_(-1), is_keep_alive_(false),
      path_(""), src_dir_(""), mmfile_(nullptr) {
        memset(&mmfile_stat_, 0, sizeof(mmfile_stat_));
    }

HttpResponse::~HttpResponse() {
    UnmapFile();
}

实现 Init() 函数

cpp 复制代码
void HttpResponse::Init(const std::string& path, const std::string& src_dir, 
                        int code, bool is_keep_alive) {
    assert(src_dir != "");
    code_ = code;
    is_keep_alive_ = is_keep_alive;
    path_ = path;
    src_dir_ = src_dir;
    mmfile_ = nullptr;
    memset(&mmfile_stat_, 0, sizeof(mmfile_stat_));
}

实现 UnmapFile() 函数

cpp 复制代码
// 释放文件映射的内存
void HttpResponse::UnmapFile() {
    if (mmfile_) {
        munmap(mmfile_, mmfile_stat_.st_size); // 解除内存映射
        mmfile_ = nullptr;
    }
}

实现 ErrorHtml() 函数

  • 当 HTTP 状态码表示出现错误时,根据状态码更新 path_
  • 使用 stat 函数获取错误页面文件的状态信息,存储在 mmfile_stat_ 中。
cpp 复制代码
// 状态出错,更新路径,以及文件状态
void HttpResponse::ErrorHtml() {
    if (CODE_PATH.count(code_) == 1) {
        path_ = CODE_PATH.find(code_)->second;
        stat((src_dir_ + path_).c_str(), &mmfile_stat_);
    }
}

实现 AddStateLine() 函数

  • 根据 code_添加 HTTP 响应的状态行到buff中。
cpp 复制代码
void HttpResponse::AddStateLine(Buffer& buff) {
    std::string status;
    if (CODE_STATUS.count(code_) == 1) {
        status = CODE_STATUS.find(code_)->second;
    } else {
        code_ = 400;
        status = CODE_STATUS.find(code_)->second;
    }
    buff.Append("HTTP/1.1 " + std::to_string(code_) + " " + status + "\r\n");
}

实现 AddHeader() 函数

  • 添加 HTTP 响应的状态行到buff中。
cpp 复制代码
void HttpResponse::AddHeader(Buffer& buff) {
    buff.Append("Connection: ");
    if (is_keep_alive_) {
        buff.Append("keep-alive\r\n");
        buff.Append("keep-alive: max=6 timeout=120\r\n");
    } else {
        buff.Append("close\r\n");
    }
    buff.Append("Content-type: " + GetFileType() + "\r\n");
}

实现 GetFileType() 函数

  • 查找文件路径中最后一个 . 的位置,若未找到则返回默认的 MIME 类型 text/plain
  • 提取后缀后在 SUFFIX_TYPE 中查找对应的 MIME 类型,若找到则返回该类型;否则返回默认的 text/plain
cpp 复制代码
std::string HttpResponse::GetFileType() {
    std::string::size_type idx = path_.find_last_of(".");
    if (idx == std::string::npos)
        return "text/plain"; // 文本类型
    std::string suffix = path_.substr(idx);
    if (SUFFIX_TYPE.count(suffix) == 1)
        return SUFFIX_TYPE.find(suffix)->second;
    return "text/plain";
}

实现 AddContent() 函数

  • 拼接完整的文件路径,使用 open 函数以只读模式打开文件。若打开失败,则调用 ErrorContent 函数生成错误响应内容。
  • 使用 mmap 函数将文件映射到内存中,若映射失败则关闭文件描述符,并调用 ErrorContent 函数。
  • mmfile_ 指向映射的内存区域,关闭文件描述符,并添加 Content-length 头部。
cpp 复制代码
void HttpResponse::AddContent(Buffer& buff) {
    std::string path = src_dir_ + path_;
    int src_fd = open(path.c_str(), O_RDONLY);  
    if (src_fd < 0) {
        ErrorContent(buff, "File Not Found!");
        return;
    }

    LOG_DEBUG("file path %s", path.c_str());
    void* mmret = mmap(0, mmfile_stat_.st_size, PROT_READ, MAP_PRIVATE, src_fd, 0);
    if (mmret == MAP_FAILED) {
        close(src_fd);
        ErrorContent(buff, "mmap fail!");
        return;
    }
    mmfile_ = (char*)mmret;
    close(src_fd);
    buff.Append("Content-length: " + std::to_string(mmfile_stat_.st_size) + "\r\n\r\n");
}

实现 ErrorContent() 函数

  • 构建一个 HTML 格式的错误页面,包含状态码、状态描述和错误消息。
  • 添加 Content-length 头部,后面跟着两个换行符表示头部结束,最后将错误页面响应报文追加到 buff 中。
cpp 复制代码
void HttpResponse::ErrorContent(Buffer& buff, const std::string& message) {
    std::string body;
    std::string status;
    body += "<html><title>Error</title>";
    body += "<body bgcolor=\"fffff\">";
    if (CODE_STATUS.count(code_) == 1)
        status = CODE_STATUS.find(code_)->second;
    else
        status = "Bad Request";
    body += std::to_string(code_) + " : " + status + "\n";
    body += "<p>" + message + "</p>";
    body += "<hr><em>TinyWebServer</em><body></html>";

    buff.Append("Content-length: " + std::to_string(body.size()) + "\r\n\r\n");
    buff.Append(body);
}

实现 MakeResponse() 函数

  • 使用 stat 函数获取文件的状态信息,根据不同情况设置状态码。
  • 调用 ErrorHtml 函数处理错误页面,依次调用 AddStateLineAddHeaderAddContent 函数,分别添加状态行、头部信息和响应内容到buff中。
cpp 复制代码
void HttpResponse::MakeResponse(Buffer& buff) {
    if (stat((src_dir_ + path_).c_str(), &mmfile_stat_) < 0) {
        LOG_WARN("stat fail: error: %s", strerror(errno));
        code_ = 404;
    } else if (S_ISDIR(mmfile_stat_.st_mode)) {
        code_ = 404;
    } else if (!(mmfile_stat_.st_mode & S_IROTH)) {
        code_ = 403;
    } else if (code_ == -1) {
        code_ = 200;
    }
        
    ErrorHtml();
    AddStateLine(buff);
    AddHeader(buff);
    AddContent(buff);
}

HttpResponse 代码

http_response.h

cpp 复制代码
#ifndef HTTP_RESPONSE_H
#define HTTP_RESPONSE_H

#include <sys/stat.h> // stat
#include <sys/mman.h> // mmap, munmap
#include <fcntl.h>    // open
#include <unistd.h>   // close

#include <cstring>    // memset
#include <cassert>
#include <string>
#include <unordered_map>

#include "../buffer/buffer.h"
#include "../log/log.h"

class HttpResponse {
public:
    HttpResponse();
    ~HttpResponse();

    void Init(const std::string& path, const std::string& src_dir, int code = -1, bool is_keep_alive = false);
    void UnmapFile();

    void MakeResponse(Buffer& buff);
    void ErrorContent(Buffer& buff, const std::string& message);

    char* File() { return mmfile_; }
    size_t FileLen() const { return mmfile_stat_.st_size; }
    int Code() const { return code_; }

private:
    void AddStateLine(Buffer& buff);
    void AddHeader(Buffer& buff);
    void AddContent(Buffer& buff);

    void ErrorHtml();
    std::string GetFileType();

    static const std::unordered_map<int, std::string> CODE_STATUS;          // 编码状态集
    static const std::unordered_map<int, std::string> CODE_PATH;            // 编码路径集
    static const std::unordered_map<std::string, std::string> SUFFIX_TYPE;  // 后缀状态集

    int code_; // 响应状态码
    bool is_keep_alive_;

    std::string path_;
    std::string src_dir_;

    char* mmfile_;
    struct stat mmfile_stat_;
};

#endif // HTTP_RESPONSE_H

http_response.cc

cpp 复制代码
#include "http_response.h"

const std::unordered_map<int, std::string> HttpResponse::CODE_STATUS = {
    {200, "OK"},
    {400, "Bad Requeset"},
    {403, "Forbidden"},
    {404, "Not Found"},
};

const std::unordered_map<int, std::string> HttpResponse::CODE_PATH = {
    {400, "/400.html"},
    {403, "/403.html"},
    {404, "/404.html"},
};

const std::unordered_map<std::string, std::string> HttpResponse::SUFFIX_TYPE = {
    {".html",  "text/html"},
    {".xml",   "text/xml"},
    {".xhtml", "application/xhtml+xml"},
    {".txt",   "text/plain"},
    {".rtf",   "application/rtf"},
    {".pdf",   "application/pdf"},
    {".word",  "application/nsword"},
    {".png",   "image/png"},
    {".gif",   "image/gif"},
    {".jpg",   "image/jpeg"},
    {".jpeg",  "image/jpeg"},
    {".au",    "audio/basic"},
    {".mpeg",  "video/mpeg"},
    {".mpg",   "video/mpeg"},
    {".avi",   "video/x-msvideo"},
    {".gz",    "application/x-gzip"},
    {".tar",   "application/x-tar"},
    {".css",   "text/css"},
    {".js",    "text/javascript"},
};

HttpResponse::HttpResponse() 
    : code_(-1), is_keep_alive_(false),
      path_(""), src_dir_(""), mmfile_(nullptr) {
        memset(&mmfile_stat_, 0, sizeof(mmfile_stat_));
    }

HttpResponse::~HttpResponse() {
    UnmapFile();
}

void HttpResponse::Init(const std::string& path, const std::string& src_dir, 
                        int code, bool is_keep_alive) {
    assert(src_dir != "");
    code_ = code;
    is_keep_alive_ = is_keep_alive;
    path_ = path;
    src_dir_ = src_dir;
    mmfile_ = nullptr;
    memset(&mmfile_stat_, 0, sizeof(mmfile_stat_));
}

// 释放文件映射的内存
void HttpResponse::UnmapFile() {
    if (mmfile_) {
        munmap(mmfile_, mmfile_stat_.st_size); // 解除内存映射
        mmfile_ = nullptr;
    }
}

// 状态出错,更新路径,以及文件状态
void HttpResponse::ErrorHtml() {
    if (CODE_PATH.count(code_) == 1) {
        path_ = CODE_PATH.find(code_)->second;
        stat((src_dir_ + path_).c_str(), &mmfile_stat_);
    }
}

void HttpResponse::AddStateLine(Buffer& buff) {
    std::string status;
    if (CODE_STATUS.count(code_) == 1) {
        status = CODE_STATUS.find(code_)->second;
    } else {
        code_ = 400;
        status = CODE_STATUS.find(code_)->second;
    }
    buff.Append("HTTP/1.1 " + std::to_string(code_) + " " + status + "\r\n");
}

void HttpResponse::AddHeader(Buffer& buff) {
    buff.Append("Connection: ");
    if (is_keep_alive_) {
        buff.Append("keep-alive\r\n");
        buff.Append("keep-alive: max=6 timeout=120\r\n");
    } else {
        buff.Append("close\r\n");
    }
    buff.Append("Content-type: " + GetFileType() + "\r\n");
}

std::string HttpResponse::GetFileType() {
    std::string::size_type idx = path_.find_last_of(".");
    if (idx == std::string::npos)
        return "text/plain"; // 文本类型
    std::string suffix = path_.substr(idx);
    if (SUFFIX_TYPE.count(suffix) == 1)
        return SUFFIX_TYPE.find(suffix)->second;
    return "text/plain";
}

void HttpResponse::AddContent(Buffer& buff) {
    std::string path = src_dir_ + path_;
    int src_fd = open(path.c_str(), O_RDONLY);  
    if (src_fd < 0) {
        ErrorContent(buff, "File Not Found!");
        return;
    }

    LOG_DEBUG("file path %s", path.c_str());
    void* mmret = mmap(0, mmfile_stat_.st_size, PROT_READ, MAP_PRIVATE, src_fd, 0);
    if (mmret == MAP_FAILED) {
        close(src_fd);
        ErrorContent(buff, "mmap fail!");
        return;
    }
    mmfile_ = (char*)mmret;
    close(src_fd);
    buff.Append("Content-length: " + std::to_string(mmfile_stat_.st_size) + "\r\n\r\n");
}

void HttpResponse::ErrorContent(Buffer& buff, const std::string& message) {
    std::string body;
    std::string status;
    body += "<html><title>Error</title>";
    body += "<body bgcolor=\"fffff\">";
    if (CODE_STATUS.count(code_) == 1)
        status = CODE_STATUS.find(code_)->second;
    else
        status = "Bad Request";
    body += std::to_string(code_) + " : " + status + "\n";
    body += "<p>" + message + "</p>";
    body += "<hr><em>TinyWebServer</em><body></html>";

    buff.Append("Content-length: " + std::to_string(body.size()) + "\r\n\r\n");
    buff.Append(body);
}

void HttpResponse::MakeResponse(Buffer& buff) {
    if (stat((src_dir_ + path_).c_str(), &mmfile_stat_) < 0) {
        LOG_WARN("stat fail: error: %s", strerror(errno));
        code_ = 404;
    } else if (S_ISDIR(mmfile_stat_.st_mode)) {
        code_ = 404;
    } else if (!(mmfile_stat_.st_mode & S_IROTH)) {
        code_ = 403;
    } else if (code_ == -1) {
        code_ = 200;
    }
        
    ErrorHtml();
    AddStateLine(buff);
    AddHeader(buff);
    AddContent(buff);
}

HttpResponse 测试

测试请求报文的构建

cpp 复制代码
#include "../code/http/http_response.h"
#include <iostream>
#include <cassert>


void TestHttpResponse() {
    Log* logger = Log::GetInstance();
    logger->Init(0, "./logs/", ".log", 1024);

    // 正常情况的测试
    HttpResponse response;
    std::string path = "/welcome.html";
    std::string src_dir = "/home/Tian/tiny_web_server/resources";
    int code = 200;
    bool isKeepAlive = true;

    response.Init(path, src_dir, code, isKeepAlive);

    // 测试构建响应
    Buffer buff;
    response.MakeResponse(buff);
    assert(response.Code() == 200);

    std::string str = buff.RetrieveAllAsString();
    LOG_DEBUG("http response:\n%s", str.c_str());

    // 测试错误路径
    src_dir = "/error/for/test";
    response.Init(path, src_dir, code, isKeepAlive);
    response.MakeResponse(buff);
    assert(response.Code() == 404);

    str = buff.RetrieveAllAsString();
    LOG_DEBUG("http response:\n%s", str.c_str());
}

int main() {
    TestHttpResponse();
    return 0;
}

CMakeLists.txt

cmake 复制代码
cmake_minimum_required(VERSION 3.10)
project(tests)

# 设置 C++ 标准和编译器选项
set(CMAKE_BUILD_TYPE Debug)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")

# 定义公共源文件和特定文件
set(COMMON ../code/buffer/buffer.cc ../code/log/log.cc)
set(HTTP_REQUEST ../code/http/http_request.cc)
set(HTTP_RESPONSE ../code/http/http_response.cc)
set(SQL_POOL ../code/pool/sql_connect_pool.cc)

add_executable(http_response_test http_response_test.cc ${COMMON} ${HTTP_RESPONSE})
相关推荐
Peter·Pan爱编程6 分钟前
Docker在Linux中安装与使用教程
linux·docker·eureka
倚栏听风雨7 分钟前
CompletableFuture 延时执行任务
后端
舒一笑20 分钟前
MySQL中模糊匹配like的一个坑
后端·mysql
BingoGo29 分钟前
PHP 集成 FFmpeg 处理音视频处理完整指南
后端·php
数字人直播36 分钟前
稳了!青否数字人分享3大精细化AI直播搭建方案!
前端·后端
听风的码41 分钟前
Vue2封装Axios
开发语言·前端·javascript·vue.js
掘金一周1 小时前
被老板逼出来的“表格生成器”:一个前端的自救之路| 掘金一周 8.21
前端·人工智能·后端
卷卷卷土重来1 小时前
C++单例模式
javascript·c++·单例模式
kunge20131 小时前
Ubuntu22.04 安装virtualbox7.1
linux·virtualbox
清溪5491 小时前
DVWA中级
linux