Linux笔记---基于HTTP协议搭建一个简单的Web服务器

1. 原理

https://blog.csdn.net/2302_80372340/article/details/151611390?spm=1011.2415.3001.5331

上面这篇文章已经详细讨论过HTTP协议是如何运作的了。简单来说,我们要在我们的服务器上做下面几件事:

  1. 接收来自客户端(浏览器)的HTTP请求
  2. 反序列化HTTP请求
  3. 读取请求行的URI字段,读取客户端要求的网页对象文件
  4. 将网页对象文件的数据包装为HTTP响应报文
  5. 序列化响应报文,回送给客户端

网页对象文件通常就是HTML、JavaScript、CSS以及图片、声音、文本等文件。

总之,我们要做的就是用代码来实现HTTP协议。

2. 协议的实现

2.1 报文的定义

首先,我们要根据HTTP协议描述的请求报文与响应报文,定义出结构化的请求报文和响应报文,并实现二者的序列化与反序列化方法。

如果只考虑服务端的话,我们只需要实现请求报文的反序列化方法和响应报文的序列化方法。

我们注意到,请求报文与响应报文最大的区别在于首行,而其他部分均大同小异,所以我们可以首先定义出HttpMessage类作为二者的父类,实现二者共同的部分:

cpp 复制代码
class HttpMessage
{
public:
    // 向_headers中添加键值对
    bool AddHeader(const std::string &header)
    {
        auto pos = header.find(_HeaderSep);
        if (pos == std::string::npos)
        {
            return false;
        }
        std::string key = header.substr(0, pos);
        std::string value = header.substr(pos + _HeaderSep.size());
        _headers[key] = value;
        return true;
    }
    bool AddHeader(const std::string &key, const std::string &value)
    {
        _headers[key] = value;
        return true;
    }
    void SetData(const std::string &data)
    {
        _data = data;
    }
    const std::string &Data() const
    {
        return _data;
    }
    void SetVersion(const std::string &version)
    {
        _version = version;
    }
    bool Headers(const std::string &key, std::string value)
    {
        if(!_headers.count(key))
            return false;
        value = _headers[key];
        return true;
    }

protected:
    static const std::string _Space;                       // 空格
    static const std::string _LineBreak;                   // 换行符
    static const std::string _BlankLine;                   // 空行
    static const std::string _HeaderSep;                   // 报头分割符
    std::string _version;                                  // Http版本
    std::unordered_map<std::string, std::string> _headers; // 请求/响应报头
    std::string _data;                                     // 请求/响应正文
};
const std::string HttpMessage::_Space = " ";
const std::string HttpMessage::_LineBreak = "\r\n";
const std::string HttpMessage::_BlankLine = "\r\n";
const std::string HttpMessage::_HeaderSep = ": ";
2.1.1 请求报文

相比于HttpMessage,请求报文有两个特有的字段:HTTP请求方法和URI。

要完成请求报文的反序列化,我们需要一个函数来帮助我们逐行提取请求的内容:

cpp 复制代码
class Util
{
public:
    static bool ReadLine(std::string &message, const std::string &sep, std::string &one_line)
    {
        auto pos = message.find(sep);
        if (pos == std::string::npos)
        {
            LOG(LogLevel::WARNING) << "Util::ReadLine: 未能提取出一行! ";
            return false;
        }
        one_line = message.substr(0, pos);
        message.erase(0, pos + sep.size());
        return true;
    }
};

当我们遇到空行(即两个连续的换行符)时,就认为收到了一个完整的报头。

在解析完报头之后,我们需要判断是否存在 "Content-Length" 字段,若存在则需继续读取正文部分。

cpp 复制代码
// 请求行: 方法 + 空格 + URI + 空格 + 版本 + 换行符
class HttpRequest : public HttpMessage
{
private:
    void PraseRequestLine(const std::string &request_line)
    {
        std::stringstream buffer(request_line);
        buffer >> _method >> _uri >> _version;
    }
    // 读取报头
    bool GetHead(std::string &request_str)
    {
        static std::string end = _LineBreak + _BlankLine;
        // 判断是否存在一个完整报头
        if (request_str.find(end) == std::string::npos)
        {
            return false;
        }
        // 读取请求行
        std::string request_line;
        Util::ReadLine(request_str, _LineBreak, request_line);
        PraseRequestLine(request_line);
        // 读取请求报头
        std::string header;
        do
        {
            Util::ReadLine(request_str, _LineBreak, header);
            if (header.size() > 0)
            {
                AddHeader(header);
            }
        } while (header.size() > 0); // 读取到空行结束

        // 如果URI包含参数则拆出
        auto pos = _uri.find(_URIArgsSep);
        if (pos != std::string::npos)
        {
            _uri_args = _uri.substr(pos + _URIArgsSep.size());
            _uri = _uri.substr(0, pos);
        }
        return true;
    }

public:
    HttpRequest() {}
    // 序列化
    // std::string Serialize() {}
    // 反序列化
    bool Deserialize(std::string &request_str)
    {
        static bool is_continue = false;
        if (!is_continue)
        {
            if (!GetHead(request_str))
                return false;
        }
        is_continue = true;
        if (_headers.count("Content-Length"))
        {
            int size = std::stoi(_headers["Content-Length"]);
            if (request_str.size() < size)
                return false;
            _data = request_str.substr(0, size);
            request_str.erase(0, size);
        }
        is_continue = false;

        return true;
    }
    const std::string &Uri() const { return _uri; }
    const std::string &Method() const { return _method; }
    ~HttpRequest() {}

private:
    std::string _method;                  // Http请求方法
    std::string _uri;                     // URI
    std::string _uri_args;                // URI中的参数
    static const std::string _URIArgsSep; // URI参数分割符
};
const std::string HttpRequest::_URIArgsSep = "?";

当然,我们只是做一个简单的服务器,虽然请求报头部分我们也做了序列化,但是我们并不打算对这部分做处理。

2.1.2 响应报文

相比于HttpMessage,响应报文有也有两个特有的字段:状态码与状态码描述。除此之外,我们还需要动用HttpMessage的_data字段来存储要发给客户端的对象。

响应报文的序列化看起来是较为简单的,只要按照响应报文的格式拼接字符串即可。

但是要使我们回复给客户端的数据被正确解析,我们还需要两个重要的响应报头字段:"Content-Length"(响应正文的长度)、"Content-Type"(响应正文的数据类型)。

响应报文的长度可以由如下方法进行获取:

cpp 复制代码
class Util
{
public:
    static size_t FileSize(const std::string &path)
    {
        // 以二进制模式打开文件(避免文本模式下的换行符转换影响计算)
        std::ifstream file(path, std::ios::in | std::ios::binary);
        if (!file.is_open())
        {
            LOG(LogLevel::ERROR) << "无法打开文件: " << path << strerror(errno);
            return -1; // 打开失败返回-1
        }

        // 将读指针移动到文件末尾
        file.seekg(0, std::ios::end);
        // 获取当前指针位置(即文件大小)
        size_t size = file.tellg();

        return size;
    }
};

Content-Type可以通过提取文件的后缀名,并通过一个映射集来进行映射的方式获取:

cpp 复制代码
// 状态行: 版本 + 空格 + 状态码 + 空格 + 状态码描述 + 换行符
class HttpResponse : public HttpMessage
{
public:
    HttpResponse()
        : _status(200)
    {
    }
    // 序列化
    std::string Serialize()
    {
        std::string status_line = _version + _Space + std::to_string(_status) + _Space + _StatusDesc[_status] + _LineBreak;
        std::string response_headers;
        for (auto &header : _headers)
        {
            response_headers += header.first + _HeaderSep + header.second + _LineBreak;
        }
        std::string message = status_line + response_headers + _BlankLine + _data;
        return message;
    }
    const std::string &GetMineType(const std::string &extension)
    {
        if (_MineType.count(extension) == 0)
        {
            LOG(LogLevel::ERROR) << "ExtensionToType: 未知的拓展名! [" << extension << "]";
            return _MineType[""];
        }
        return _MineType[extension];
    }
    void SetStatus(int status)
    {
        _status = status;
    }

    // 反序列化
    // bool Deserialize(const std::string request_str) {}
    ~HttpResponse() {}

private:
    int _status;                                                   // 状态码
    static std::unordered_map<int, std::string> _StatusDesc;       // 状态码描述
    static std::unordered_map<std::string, std::string> _MineType; // 后缀转HTTP数据类型
};
std::unordered_map<int, std::string> HttpResponse::_StatusDesc = {
    {200, "OK"},
    {404, "Not Found"},
    {301, "Moved Permanently"},
    {302, "See Other"}};
std::unordered_map<std::string, std::string> HttpResponse::_MineType = {
    {"", "text/plain"},
    {"txt", "text/plain"},
    {"html", "text/html"},
    {"htm", "text/html"},
    {"xml", "text/xml"},
    {"gif", "image/gif"},
    {"jpg", "image/jpeg"},
    {"png", "image/png"}};

2.2 HTTP协议的定义

2.2.1 重定向

如果HTTP响应报文的状态码为3xx,则表明服务端希望客户端跳转到另一个网页(同一个网站或其他网站都有可能)。

此时,我们需要在响应报文的头部加上 "Location: 新网址" 报头。客户端在收到这样的一条响应报文之后,就会重新向这个新网址发起请求。

我们可以设计如下的一个字段来记录重定向关系,即当用户在请求某个URI指向的资源时,我们将其重定向到对应的新的网址:

cpp 复制代码
std::unordered_map<std::string, std::string> _Redirect; // 临时重定向

// 注册重定向
void RegisterRedirect(const std::string &uri, const std::string &dest)
{
    _Redirect[uri] = dest;
}
2.2.2 RESTful API

REST 是 Representational State Transfer的缩写,中文译为 "表现层状态转移",由计算机科学家 Roy Fielding 在 2000 年的博士论文中提出。

它不是一种协议(如 HTTP),而是一套 架构设计原则,用于指导分布式系统(如前后端分离、跨服务通信)的 API 设计。

简单来说,就是将所有数据 / 服务都视为 "资源"(如用户、订单、商品),并通过唯一的 URI(统一资源标识符) 来定位。

除了返回数据,我们还可以为用户提供一些微服务,就比如登录、注册等。

当客户端提供的URI为我们注册了微服务的值时,我们就可以转去执行这个微服务,而不是简单地读取文件数据,然后返回。例如如下这些常见的微服务:

|-------------|----------|---------------|--------------------------------|
| HTTP 方法 | 对应操作 | 语义说明 | 示例 URI |
| GET | 查询 | 获取资源,无副作用(只读) | GET /users(查所有用户) |
| POST | 创建 | 新增资源,会改变服务器状态 | POST /users(新增用户) |
| PUT | 全量更新 | 替换资源的全部属性 | PUT /users/123(更新用户 123) |
| PATCH | 部分更新 | 仅修改资源的部分属性 | PATCH /users/123(改用户 123 的手机号) |
| DELETE | 删除 | 删除资源,会改变服务器状态 | DELETE /users/123(删用户 123) |

同样地,我们可以定义如下的字段来记录URI和微服务之间的映射方法:

cpp 复制代码
using microService_t = std::function<void(const HttpRequest &, HttpResponse &)>;
std::unordered_map<std::string, microService_t> _Route; // 微服务路由

// 注册微服务
void RegisterMicroService(const std::string &uri, microService_t service)
{
    _Route[uri] = service;
}

在制作响应报文的时候,我们就可以按照如下的步骤来完成:

cpp 复制代码
std::string MakeResponse(const HttpRequest &request, HttpResponse &response)
{
    response.SetVersion(_Version);
    std::string uri = request.Uri();
    if (_Redirect.count(uri)) // 重定向
    {
        response.SetStatus(302);
        response.AddHeader("Location", _Redirect[uri]);
    }
    else if (_Route.count(uri)) // uri为微服务
    {
        _Route[uri](request, response);
    }
    else // 获取静态资源
    {
        GetStaticSource(request, response);
    }

    return response.Serialize();
}
2.2.4 微服务参数的传递

既然是调用微服务,那么就免不了需要参数。就比如我们在处理用户登录时,就需要一个微服务,此时我们就需要用户提交的用户名与密码。

那么,HTTP协议是如何传参的呢?

  • GET方法 :在RUI后拼接上 "?参数1=value1&参数2=value2..."

    bash 复制代码
    例如:
    [2025-09-13 16:03:41] [INFO] [146862] [Http.hpp] [270] - 来自[210.41.99.103:53299]的Http请求报文:
    GET /login?username=shishen&password=123456 HTTP/1.1
    Host: 110.41.32.137:8888
    Connection: keep-alive
    Upgrade-Insecure-Requests: 1
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
    Accept-Encoding: gzip, deflate
    Accept-Language: zh-CN,zh;q=0.9
  • POST方法 :将参数存放在正文部分:

    bash 复制代码
    [2025-09-13 16:05:09] [INFO] [146893] [Http.hpp] [270] - 来自[210.41.99.103:53341]的Http请求报文:
    POST /login HTTP/1.1
    Host: 110.41.32.137:8888
    Connection: keep-alive
    Content-Length: 32
    Upgrade-Insecure-Requests: 1
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
    Accept-Encoding: gzip, deflate
    Accept-Language: zh-CN,zh;q=0.9
    
    username=shishen&password=123456
2.2.5 完整实现
cpp 复制代码
class Http
{
private:
    void SetContentLength(HttpResponse &response, const std::string &path)
    {
        size_t filesize = Util::FileSize(path);
        response.AddHeader("Content-Length", std::to_string(filesize));
    }

    void SetContentType(HttpResponse &response, const std::string &path)
    {
        static std::string point = ".";
        auto pos = path.rfind(point);
        std::string extension;
        if (pos == std::string::npos)
        {
            extension = "";
        }
        else
        {
            extension = path.substr(pos + point.size());
        }
        response.AddHeader("Content-Type", response.GetMineType(extension));
    }

    void GetStaticSource(const HttpRequest &request, HttpResponse &response)
    {
        std::string path, data;
        if (request.Uri() == "/")
            path = _HomePage;
        else
            path = _WebRoot + request.Uri();

        if (!Util::ReadFile(path, data))
        {
            response.SetStatus(404);
            LOG(LogLevel::ERROR) << "Http: 获取资源失败! [" << path << "]";
            path = _404Page;
            Util::ReadFile(path, data);
        }
        response.SetData(std::move(data));

        SetContentLength(response, path);
        SetContentType(response, path);
    }

    std::string MakeResponse(HttpRequest &request, HttpResponse &response)
    {
        response.SetVersion(_Version);
        std::string connection;
        if(request.Headers("Connection", connection) && connection == "Keep-alive")
            response.AddHeader("Connection", "Keep-alive");
        std::string uri = request.Uri();
        if (_Redirect.count(uri)) // 重定向
        {
            response.SetStatus(302);
            response.AddHeader("Location", _Redirect[uri]);
        }
        else if (_Route.count(uri)) // uri为微服务
        {
            _Route[uri](request, response);
        }
        else // 获取静态资源
        {
            GetStaticSource(request, response);
        }

        return response.Serialize();
    }

public:
    // TCPServer的回调函数
    void RequestHandler(const std::shared_ptr<TCPConnectSocket> &con_socket)
    {
        std::string cli_message, buffer;
        while (con_socket->Receive(buffer) > 0)
        {
            cli_message += buffer;
            LOG(LogLevel::INFO) << "来自[" << con_socket->addr().Info() << "]的Http请求报文:\n\r" << buffer;
            HttpRequest request;
            if (!request.Deserialize(cli_message))
                continue;
            LOG(LogLevel::DEBUG) << "request 反序列化成功";
            HttpResponse response;
            std::string message = MakeResponse(request, response);

            con_socket->Send(message);
        }
    }

    // 注册重定向
    void RegisterRedirect(const std::string &uri, const std::string &dest)
    {
        _Redirect[uri] = dest;
    }

    // 注册微服务
    using microService_t = std::function<void(const HttpRequest &, HttpResponse &)>;
    void RegisterMicroService(const std::string &uri, microService_t service)
    {
        _Route[uri] = service;
    }

private:
    static const std::string _Version;                             // Http版本
    static const std::string _WebRoot;                             // 网页根目录
    static const std::string _HomePage;                            // 首页
    static const std::string _404Page;                             // 404页面
    std::unordered_map<std::string, std::string> _Redirect; // 临时重定向
    std::unordered_map<std::string, microService_t> _Route; // 微服务路由
};
const std::string Http::_Version = "HTTP/1.1";
const std::string Http::_WebRoot = "/home/shishen/113code/linux-c/Http协议/WebRoot";
const std::string Http::_HomePage = Http::_WebRoot + "/index.html";
const std::string Http::_404Page = Http::_WebRoot + "/404.html";

3. 整个服务端的实现

bash 复制代码
.
├── HttpServer
│   ├── Common.hpp
│   ├── Http.hpp
│   ├── InetAddr.hpp
│   ├── Log.hpp
│   ├── main.cpp
│   ├── Makefile
│   ├── Mutex.hpp
│   ├── Socket.hpp
│   ├── TCPServer.hpp
│   ├── testHttp
│   └── Util.hpp
└── WebRoot
    ├── 404.html
    ├── image
    │   └── 666.jpg
    ├── index.html
    ├── login.html
    ├── login-success.html
    └── register.html

3 directories, 17 files

3.1 main.cpp

cpp 复制代码
#include "Http.hpp"
#include "Common.hpp"
#include <memory>
#include <unistd.h>
#include <signal.h>

int main(int argc, char* args[])
{
    if(argc != 2)
    {
        LOG(LogLevel::FATAL) << "Usage: " << args[0] << " port";
        exit(USAGE_ERROR);
    }
    
    // USE_FILE_STRATEGY();

    Http http;
    http.RegisterRedirect("/qq", "https://qq.com");
    http.RegisterMicroService("/login", [](const HttpRequest& req, HttpResponse& res){
        // 仅作测试,不管req的参数了
        res.SetStatus(302);
        res.AddHeader("Location", "/login-success.html");
    });

    in_port_t port = std::stoi(args[1]);
    TCPServer server(port, [&http](const std::shared_ptr<TCPConnectSocket>& con_socket){
        http.RequestHandler(con_socket);
    });

    // daemon(0, 0);
    signal(SIGCHLD, SIG_IGN);
    server.Run();
    return 0;
}

3.2 Http.hpp

cpp 复制代码
#pragma once
#include "TCPServer.hpp"
#include "Util.hpp"
#include <unordered_map>
#include <memory>
#include <sstream>

class HttpMessage
{
public:
    // 向_headers中添加键值对
    bool AddHeader(const std::string &header)
    {
        auto pos = header.find(_HeaderSep);
        if (pos == std::string::npos)
        {
            return false;
        }
        std::string key = header.substr(0, pos);
        std::string value = header.substr(pos + _HeaderSep.size());
        _headers[key] = value;
        return true;
    }
    bool AddHeader(const std::string &key, const std::string &value)
    {
        _headers[key] = value;
        return true;
    }
    void SetData(const std::string &data)
    {
        _data = data;
    }
    const std::string &Data() const
    {
        return _data;
    }
    void SetVersion(const std::string &version)
    {
        _version = version;
    }
    bool Headers(const std::string &key, std::string value)
    {
        if(!_headers.count(key))
            return false;
        value = _headers[key];
        return true;
    }

protected:
    static const std::string _Space;                       // 空格
    static const std::string _LineBreak;                   // 换行符
    static const std::string _BlankLine;                   // 空行
    static const std::string _HeaderSep;                   // 报头分割符
    std::string _version;                                  // Http版本
    std::unordered_map<std::string, std::string> _headers; // 请求/响应报头
    std::string _data;                                     // 请求/响应正文
};
const std::string HttpMessage::_Space = " ";
const std::string HttpMessage::_LineBreak = "\r\n";
const std::string HttpMessage::_BlankLine = "\r\n";
const std::string HttpMessage::_HeaderSep = ": ";

// 请求行: 方法 + 空格 + URI + 空格 + 版本 + 换行符
class HttpRequest : public HttpMessage
{
private:
    void PraseRequestLine(const std::string &request_line)
    {
        std::stringstream buffer(request_line);
        buffer >> _method >> _uri >> _version;
    }
    // 读取报头
    bool GetHead(std::string &request_str)
    {
        static std::string end = _LineBreak + _BlankLine;
        // 判断是否存在一个完整报头
        if (request_str.find(end) == std::string::npos)
        {
            return false;
        }
        // 读取请求行
        std::string request_line;
        Util::ReadLine(request_str, _LineBreak, request_line);
        PraseRequestLine(request_line);
        // 读取请求报头
        std::string header;
        do
        {
            Util::ReadLine(request_str, _LineBreak, header);
            if (header.size() > 0)
            {
                AddHeader(header);
            }
        } while (header.size() > 0); // 读取到空行结束

        // 如果URI包含参数则拆出
        auto pos = _uri.find(_URIArgsSep);
        if (pos != std::string::npos)
        {
            _uri_args = _uri.substr(pos + _URIArgsSep.size());
            _uri = _uri.substr(0, pos);
        }
        return true;
    }

public:
    HttpRequest() {}
    // 序列化
    // std::string Serialize() {}
    // 反序列化
    bool Deserialize(std::string &request_str)
    {
        static bool is_continue = false;
        if (!is_continue)
        {
            if (!GetHead(request_str))
                return false;
        }
        is_continue = true;
        if (_headers.count("Content-Length"))
        {
            int size = std::stoi(_headers["Content-Length"]);
            if (request_str.size() < size)
                return false;
            _data = request_str.substr(0, size);
            request_str.erase(0, size);
        }
        is_continue = false;

        return true;
    }
    const std::string &Uri() const { return _uri; }
    const std::string &Method() const { return _method; }
    ~HttpRequest() {}

private:
    std::string _method;                  // Http请求方法
    std::string _uri;                     // URI
    std::string _uri_args;                // URI中的参数
    static const std::string _URIArgsSep; // URI参数分割符
};
const std::string HttpRequest::_URIArgsSep = "?";

// 状态行: 版本 + 空格 + 状态码 + 空格 + 状态码描述 + 换行符
class HttpResponse : public HttpMessage
{
public:
    HttpResponse()
        : _status(200)
    {
    }
    // 序列化
    std::string Serialize()
    {
        std::string status_line = _version + _Space + std::to_string(_status) + _Space + _StatusDesc[_status] + _LineBreak;
        std::string response_headers;
        for (auto &header : _headers)
        {
            response_headers += header.first + _HeaderSep + header.second + _LineBreak;
        }
        std::string message = status_line + response_headers + _BlankLine + _data;
        return message;
    }
    const std::string &GetMineType(const std::string &extension)
    {
        if (_MineType.count(extension) == 0)
        {
            LOG(LogLevel::ERROR) << "ExtensionToType: 未知的拓展名! [" << extension << "]";
            return _MineType[""];
        }
        return _MineType[extension];
    }
    void SetStatus(int status)
    {
        _status = status;
    }

    // 反序列化
    // bool Deserialize(const std::string request_str) {}
    ~HttpResponse() {}

private:
    int _status;                                                   // 状态码
    static std::unordered_map<int, std::string> _StatusDesc;       // 状态码描述
    static std::unordered_map<std::string, std::string> _MineType; // 后缀转HTTP数据类型
};
std::unordered_map<int, std::string> HttpResponse::_StatusDesc = {
    {200, "OK"},
    {404, "Not Found"},
    {301, "Moved Permanently"},
    {302, "See Other"}};
std::unordered_map<std::string, std::string> HttpResponse::_MineType = {
    {"", "text/plain"},
    {"txt", "text/plain"},
    {"html", "text/html"},
    {"htm", "text/html"},
    {"xml", "text/xml"},
    {"gif", "image/gif"},
    {"jpg", "image/jpeg"},
    {"png", "image/png"}};

class Http
{
private:
    void SetContentLength(HttpResponse &response, const std::string &path)
    {
        size_t filesize = Util::FileSize(path);
        response.AddHeader("Content-Length", std::to_string(filesize));
    }

    void SetContentType(HttpResponse &response, const std::string &path)
    {
        static std::string point = ".";
        auto pos = path.rfind(point);
        std::string extension;
        if (pos == std::string::npos)
        {
            extension = "";
        }
        else
        {
            extension = path.substr(pos + point.size());
        }
        response.AddHeader("Content-Type", response.GetMineType(extension));
    }

    void GetStaticSource(const HttpRequest &request, HttpResponse &response)
    {
        std::string path, data;
        if (request.Uri() == "/")
            path = _HomePage;
        else
            path = _WebRoot + request.Uri();

        if (!Util::ReadFile(path, data))
        {
            response.SetStatus(404);
            LOG(LogLevel::ERROR) << "Http: 获取资源失败! [" << path << "]";
            path = _404Page;
            Util::ReadFile(path, data);
        }
        response.SetData(std::move(data));

        SetContentLength(response, path);
        SetContentType(response, path);
    }

    std::string MakeResponse(HttpRequest &request, HttpResponse &response)
    {
        response.SetVersion(_Version);
        std::string connection;
        if(request.Headers("Connection", connection) && connection == "Keep-alive")
            response.AddHeader("Connection", "Keep-alive");
        std::string uri = request.Uri();
        if (_Redirect.count(uri)) // 重定向
        {
            response.SetStatus(302);
            response.AddHeader("Location", _Redirect[uri]);
        }
        else if (_Route.count(uri)) // uri为微服务
        {
            _Route[uri](request, response);
        }
        else // 获取静态资源
        {
            GetStaticSource(request, response);
        }

        return response.Serialize();
    }

public:
    // TCPServer的回调函数
    void RequestHandler(const std::shared_ptr<TCPConnectSocket> &con_socket)
    {
        std::string cli_message, buffer;
        while (con_socket->Receive(buffer) > 0)
        {
            cli_message += buffer;
            LOG(LogLevel::INFO) << "来自[" << con_socket->addr().Info() << "]的Http请求报文:\n\r" << buffer;
            HttpRequest request;
            if (!request.Deserialize(cli_message))
                continue;
            LOG(LogLevel::DEBUG) << "request 反序列化成功";
            HttpResponse response;
            std::string message = MakeResponse(request, response);

            con_socket->Send(message);
        }
    }

    // 注册重定向
    void RegisterRedirect(const std::string &uri, const std::string &dest)
    {
        _Redirect[uri] = dest;
    }

    // 注册微服务
    using microService_t = std::function<void(const HttpRequest &, HttpResponse &)>;
    void RegisterMicroService(const std::string &uri, microService_t service)
    {
        _Route[uri] = service;
    }

private:
    static const std::string _Version;                             // Http版本
    static const std::string _WebRoot;                             // 网页根目录
    static const std::string _HomePage;                            // 首页
    static const std::string _404Page;                             // 404页面
    std::unordered_map<std::string, std::string> _Redirect; // 临时重定向
    std::unordered_map<std::string, microService_t> _Route; // 微服务路由
};
const std::string Http::_Version = "HTTP/1.1";
const std::string Http::_WebRoot = "/home/shishen/113code/linux-c/Http协议/WebRoot";
const std::string Http::_HomePage = Http::_WebRoot + "/index.html";
const std::string Http::_404Page = Http::_WebRoot + "/404.html";

3.3 Util.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <fstream>
#include "Log.hpp"
using namespace LogModule;

class Util
{
public:
    static bool ReadFile(const std::string &file_path, std::string &content)
    {
        // 必须要按照二进制的方式读取数据,否则非文本数据无法传输
        std::ifstream file(file_path, std::ios::binary);
        if (!file.is_open())
        {
            LOG(LogLevel::ERROR) << "Util::ReadFile: 无法打开文件[" << file_path << "]! " << strerror(errno);
            return false;
        }

        size_t file_size = FileSize(file_path);
        content.resize(file_size);
        // 5. 读取二进制数据到字符串
        if (!file.read(&content[0], file_size))
        {
            LOG(LogLevel::ERROR) << "Util::ReadFile: 读取文件失败[" << file_path << "]! ";
            return false;
        }
        file.close();
        return true;
    }
    static bool ReadLine(std::string &message, const std::string &sep, std::string &one_line)
    {
        auto pos = message.find(sep);
        if (pos == std::string::npos)
        {
            LOG(LogLevel::WARNING) << "Util::ReadLine: 未能提取出一行! ";
            return false;
        }
        one_line = message.substr(0, pos);
        message.erase(0, pos + sep.size());
        return true;
    }
    static size_t FileSize(const std::string &path)
    {
        // 以二进制模式打开文件(避免文本模式下的换行符转换影响计算)
        std::ifstream file(path, std::ios::in | std::ios::binary);
        if (!file.is_open())
        {
            LOG(LogLevel::ERROR) << "无法打开文件: " << path << strerror(errno);
            return -1; // 打开失败返回-1
        }

        // 将读指针移动到文件末尾
        file.seekg(0, std::ios::end);
        // 获取当前指针位置(即文件大小)
        size_t size = file.tellg();

        return size;
    }
};

其余的hpp文件在以往的文章当中都有,就不再重复贴出了,读者也可以到仓库中找。

https://gitee.com/da-guan-mu-lao-sheng/linux-c/tree/master/Http%E5%8D%8F%E8%AE%AE/HttpServer

至于网页对象,可以让ai帮忙生成几个html对象用于调试。

4. 效果

这样一来,我们的Web服务器基本上就能完成大部分浏览器的请求了。

但是,我们目前还没有对请求方法进行判断处理,对于报头的处理也以忽略为主,总之我们的服务器还是非常简单的版本。

相关推荐
四谎真好看4 小时前
Java 黑马程序员学习笔记(进阶篇6)
java·笔记·学习·学习笔记
mzhan0174 小时前
[笔记] 来到了kernel 5.14
笔记
星梦清河4 小时前
宋红康 JVM 笔记 Day17|垃圾回收器
java·jvm·笔记
一枝小雨4 小时前
【C++】list 容器操作
开发语言·c++·笔记·list·学习笔记
Alice-YUE4 小时前
【css学习笔记8】html5css3新特性
css·笔记·学习
universe_015 小时前
day27|前端框架学习
前端·笔记
沐墨专攻技术5 小时前
二、网页的“化妆师”:从零学习 CSS
css·笔记·学习
帅弟1506 小时前
Day22 用C语言编译应用程序
笔记
g_i_a_o_giao6 小时前
Android8 binder源码学习分析笔记(四)——ServiceManager启动
笔记·学习·binder