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服务器基本上就能完成大部分浏览器的请求了。

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

相关推荐
Peace & Love4872 小时前
C++初阶 -- 模拟实现list
开发语言·c++·笔记
摇滚侠2 小时前
Spring Boot3零基础教程,云服务停机不收费,笔记71
java·spring boot·笔记
丰锋ff2 小时前
英一2013年真题学习笔记
笔记·学习
摇滚侠2 小时前
Spring Boot3零基础教程,监听 Kafka 消息,笔记78
spring boot·笔记·kafka
能不能别报错3 小时前
K8s学习笔记(二十二) 网络组件 Flannel与Calico
笔记·学习·kubernetes
摇滚侠3 小时前
Spring Boot3零基础教程,RedisTemplate 定制化,笔记70
spring boot·笔记·后端
阿民不加班3 小时前
【React】打卡笔记,入门学习01:点击事件
笔记·学习·react.js
我命由我123454 小时前
PDFBox - PDF 页面坐标系、PDF 页面尺寸获取、PDF 页面位置计算
java·服务器·开发语言·笔记·后端·java-ee·pdf
做一道光4 小时前
2、SVPWM原理及实现学习笔记
笔记·学习·嵌入式·电机控制
繁花与尘埃4 小时前
CSS简介(本文为个人学习笔记,内容整理自哔哩哔哩UP主【非学者勿扰】的公开课程。 > 所有知识点归属原作者,仅作非商业用途分享)
css·笔记·学习