Linux与深入HTTP序列化和反序列化

发布于:2025-03-17 ⋅ 阅读:(15) ⋅ 点赞:(0)

深入HTTP序列化和反序列化

本篇介绍

在上一节已经完成了客户端和服务端基本的HTTP通信,但是前面的传递并没有完全体现出HTTP的序列化和反序列化,为了更好得理解其工作流程,在本节会以更加具体的方式分析到HTTP序列化和反序列化

本节会在介绍HTTP协议基本结构与基本实现HTTPServer的基础之上继续完善HTTP服务器,所以需要有对应的知识作为铺垫才可以开始本节

基本实现思路

本次实现的HttpServer类主要完成接收客户端发送的HTTP请求,也就是说,服务器需要根据客户端的HTTP请求回复一个HTTP响应,所以必须要有的方法就是处理请求方法,但是前面已经提到过,HTTP的请求属于结构化的数据,并且这个数据在传递给服务器时就已经做了序列化,服务器需要处理的就是将结构化的数据进行反序列化;同样,服务器处理完毕后还需要发给客户端,所以此处就需要服务器对处理的结果填充到HTTP响应结构对象中再返回给客户端,此处就需要进行序列化

基于上面的原因,与前面序列化和反序列化与网络计算器一样,需要实现一个协议,包含HttpRequestHttpResponse类,用于处理序列化和反序列化

本次为了更好得理解序列化和反序列化,以HTTP请求为例,首先以请求行、请求报头和请求体三个整体做序列化和反序列化,接着再深入请求行、请求报头和请求体中的字段

根据这个两个阶段,需要实现的目标如下:

  1. 第一阶段:打印出反序列化和序列化的结果
  2. 第二阶段:向客户端返回具体的静态HTML文件

第一阶段

创建HttpRequest

根据前面的基本思路,实现HttpRequest类就需要实现对应的反序列化。因为HTTP请求中带有三种数据:

  1. 请求行
  2. 请求报头
  3. 请求体

所以需要定义三个成员分别存放从请求获取到的内容,所以基本结构如下:

class HttpRequest
{
public:
    HttpRequest()
    {

    }

    // 反序列化
    bool deserialize(std::string& in_str)
    {
        
    }

    ~HttpRequest()
    {

    }
private:
    std::string _req_line;              // 请求行
    std::vector<std::string> _req_head; // 请求报头
    std::string _req_body;              // 请求体
};

实现HttpRequest反序列化接口

在HTTP中的反序列化本质就是根据基本的格式去除掉多余的部分,从而提取出有效的数据放在相应的字段中,所以根据这个思路依次进行提取

注意,前面提到过,HTTP是基于TCP的,而TCP是面向字节流的,这就导致可能服务器接收到的HTTP请求不完整,对此还需要对接收到的HTTP请求进行完整性判断,但是本次不考虑这一步

截取请求行

首先提取请求行中的数据,根据前面对HTTP请求结构的描述可以知道HTTP请求的请求行以\r\n结尾,所以只需要找到第一个\r\n,就说明找到了请求行,这里定义一个成员函数用来处理这个逻辑:

// 获取请求行
bool parseReqLineFromTotal(std::string& in_str)
{
    auto pos = in_str.find(default_sep);
    if(pos == std::string::npos)
    {
        LOG(LogLevel::WARNING) << "未找到请求行";
        return false;
    }

    // 获取到请求行
    _req_line = in_str.substr(0, pos);

    // 从原始字符串中移除请求头和第一个分隔符
    in_str.erase(0, pos + default_sep.size());

    LOG(LogLevel::DEBUG) << "请求行处理后:" << in_str;

    return true;
}

这里考虑到后面的请求体也是以\r\n结尾,所以考虑将该函数更改为更通用的版本:

// 截取以\r\n结尾的数据
bool parseOneLineFromTotal(std::string& in_str, std::string& out_str)
{
    auto pos = in_str.find(default_sep);
    if(pos == std::string::npos)
        return false;

    // 获取到截取数据
    out_str = in_str.substr(0, pos);

    // 从原始字符串中移除截取的字符串和对应的分隔符
    in_str.erase(0, pos + default_sep.size());

    return true;
}

接着完善反序列化接口:

// 反序列化
bool deserialize(std::string& in_str)
{
    bool getReqLineFlag = parseOneLineFromTotal(in_str, _req_line);
    if(!getReqLineFlag)
    {
        LOG(LogLevel::WARNING) << "反序列化获取请求行失败";
        return false;
    }
    LOG(LogLevel::INFO) << "截取的请求行为:" << _req_line;
    
    // 未完...
}
截取请求报头

截取请求报头的方式与请求行非常类似,无非就是需要多次截取,但是需要考虑截取何时结束。根据HTTP请求体的特点,最后一行就是一个空行,即\r\n,所以可以考虑利用这个空行进行处理,具体思路如下:

因为每一次截取都会截取到分隔符之前的内容,所以可以考虑定义一个变量用于接收请求报头的结果,那么根据截取一行的函数的逻辑,只有成功找到了\r\n时才会进行截取,而截取的结果不会包含\r\n,那么一旦截取的结果是空且找到了\r\n,就说明找到了最后一行

根据这个思路,将获取请求报头数据的逻辑放在一个单独的函数中,如下:

// 获取请求报头
bool getReqHeadFromTotal(std::string &in_str)
{
    // 保存以\r\n结尾的一行数据
    std::string oneLine;
    while (true)
    {
        bool getOneLineFlag = parseOneLineFromTotal(in_str, oneLine);

        // 如果getOneLineFlag为true并且oneLine不为空,说明当前行有效,否则代表已经找到了结尾
        if(getOneLineFlag && !oneLine.empty())
        {
            _req_head.push_back(oneLine);
        }
        else if(getOneLineFlag && oneLine.empty())
        {
            break;
        }
        else
        {
            return false;
        }
    }
    
    return true;
}

继续完善反序列化接口:

// 反序列化
bool deserialize(std::string &in_str)
{
    // ...

    bool getReqHeadLine = getReqHeadFromTotal(in_str);
    if (!getReqHeadLine)
    {
        LOG(LogLevel::WARNING) << "反序列化获取请求报头失败";
        return false;
    }

    LOG(LogLevel::INFO) << "获取到的请求行为:";
    std::for_each(_req_head.begin(), _req_head.end(), [&](int i){
        std::cout << _req_head[i] << std::endl;
    });

    // 未完...
}
截取请求体

因为在前面截取请求行和截取请求报头时已经修改了HTTP请求字符串,所以剩下的就是请求体,直接赋值即可:

// 反序列化
bool deserialize(std::string &in_str)
{
    // ...

    // 获取请求体
    _req_body = in_str;

    return true;
}

创建HttpResponse

服务器需要给客户端返回内容,所以在这之前必须对整个HttpResponse结构进行序列化。同样,HTTP响应也有对应的三种数据:

  1. 请求行
  2. 请求报头
  3. 请求体

所以需要定义三个成员分别存放从请求获取到的内容,所以基本结构如下:

// HTTP响应
class HttpResponse
{
public: 
    HttpResponse(std::string &rl, std::vector<std::string> &rq, std::string &rb)
        : _resp_line(rl), _resp_head(rq), _resp_body(rb)
    {
    }

    HttpResponse(std::string &rl, std::string &rb)
        : _resp_line(rl), _resp_body(rb)
    {
    }

    // 序列化
    bool serialize(std::string &out_str)
    {

    }

    ~HttpResponse()
    {

    }
private:
    std::string _resp_line;              // 响应头
    std::vector<std::string> _resp_head; // 响应报头
    std::string _resp_body;             // 响应体
};

实现HttpResponse序列化接口

实现serialize就会比实现deserialize接口简单,只需要根据对应的字段加上\r\n即可,所以基本代码如下:

// 序列化
bool serialize(std::string &out_str)
{
    // 给请求行添加\r\n
    _resp_line += default_sep;
    out_str += _resp_line;

    // 给请求报头的每一个字段加上\r\n
    std::for_each(_resp_head.begin(), _resp_head.end(), [&](std::string &str)
                    {
        str += default_sep;
        out_str += str; });

    // 添加空行
    out_str += default_sep;

    out_str += _resp_body;

    return true;
}

修改HttpServer

只需要改变HttpServer类中的请求处理函数,但是如果要打印反序列的结果就必须提供对应的接口或者在HttpRequest类内提供打印函数,本次考虑后者:

void print()
{
    // 请求行
    LOG(LogLevel::INFO) << "请求行:" << _req_line;
    // 请求报头
    std::for_each(_req_head.begin(), _req_head.end(), [&](std::string& str)
                    { LOG(LogLevel::INFO) << "请求报头:" << str; });
    // 请求体
    LOG(LogLevel::INFO) << "请求体:" << _req_body;
}

接着修改HttpServerhandleHttpRequest函数:

void handleHttpRequest(SockAddrIn sock_addr_in, int ac_socketfd)
{
    LOG(LogLevel::INFO) << "收到来自:" << sock_addr_in.getIp() << ":" << sock_addr_in.getPort() << "的连接";

    // 获取客户端传来的HTTP请求
    base_socket_ptr bs = _tp->getSocketPtr();
    std::string in_str;
    bs->recvData(in_str, ac_socketfd);
    // 反序列化
    HttpRequest req;
    req.deserialize(in_str);
    // 打印反序列结果
    req.print();

    // 构建HttpResponse返回
    std::string line = "HTTP 1.1 200 OK";
    std::string body = "<h1>Build HttpResponse success</h1>";

    HttpResponse resp(line, body);
    std::string out_str;
    // 序列化
    resp.serialize(out_str);

    LOG(LogLevel::INFO) << out_str;

    bs->sendData(out_str, ac_socketfd);
}

测试

主函数与上一节一样,测试结果如下:

在这里插入图片描述

在这里插入图片描述

从上图可以看到可以成功获取到HTTP请求结果并且正常回复HTTP响应,第一阶段目标完成

第二阶段

在第一阶段的基础之上,现在需要对HTTP请求和HTTP响应的每一个字段进行细化,本次不考虑某个字段或者属性是什么含义,只需要将其进行提取即可

修改HttpRequest

提取HTTP请求行中的字段

因为HTTP请求行中的每个字段是根据空格进行分隔的,回忆C/C++的输入和输出,默认也是以空白字符进行分隔,所以就可以利用这一点,可以使用C语言的sscanf()进行读取,也可以考虑使用C++的stringstream进行

因为需要读取到每个字段,所以需要对应的成员进行接收,这里就使用三个成员_req_method_req_uri_req_ver作为补充成员:

class HttpRequest
{
public:
    // ...

private:
    std::string _req_method;            // 请求方法
    std::string _req_uri;               // 请求资源路径
    std::string _req_ver;               // HTTP请求版本
    // ...
};

接着就是实现一个函数用于从req_line中提取对应的字段填充_req_method_req_uri_req_ver三个成员:

=== “sscanf版本”

// sscanf版本
bool getContentFromReqLine()
{
    char method[1024] = {0};
    char uri[1024] = {0};
    char ver[1024] = {0};
    sscanf(_req_line.c_str(), "%s%s%s", method, uri, ver);
    LOG(LogLevel::INFO) << "请求行:" << method << "-" << uri << "-" << ver;
    _req_method = method;
    _req_uri = uri;
    _req_ver = ver;

    return true;
}

=== “stringstream版本”

 // stringstream版本
 bool getContentFromReqLine()
 {
     std::stringstream ss;
     // 读取到stringstream中
     ss << _req_line;
     // 输出到成员中
     ss >> _req_method >> _req_uri >> _req_ver;
     LOG(LogLevel::INFO) << "请求行:" << _req_method << "-" << _req_uri << "-" << _req_ver;
 
     return true;
 }

接下来修改deserialize的逻辑:

bool deserialize(std::string &in_str)
{
    // 截取请求行
    bool getReqLineFlag = parseOneLineFromTotal(in_str, _req_line);
    if (!getReqLineFlag)
    {
        LOG(LogLevel::WARNING) << "反序列化获取请求行失败";
        return false;
    }
    LOG(LogLevel::INFO) << "截取的请求头为:" << _req_line;

    // 填充请求行的字段
    getContentFromReqLine();

    // ...

    return true;
}
提取HTTP请求报头中的字段

前面完成了获取到HTTP请求报头中的每一条数据,但是请求报头实际上是key-value结构的数据,服务器需要拿到其中的key以及value进行后续的处理,所以这里就需要分别取出key和对应的value

为了存储对应的keyvalue,可以考虑使用一个哈希表。这里,因为处理每一个每一条报头数据不需要经过_req_head过渡,所以可以考虑直接将分割出的字符串传递给处理分隔的函数,在该函数中直接将对应的键值对添加到哈希表即可

每一个键值对字符串以: 分隔,而不是:

首先完成分割逻辑:

bool getPairFromReqHead(std::string& oneLine)
{
    // 找到分隔符
    auto pos = oneLine.find(default_head_sep);
    // 左侧即为key
    std::string key = oneLine.substr(0, pos);
    // 右侧即为value
    std::string value = oneLine.substr(pos + default_head_sep.size());

    // 插入到哈希表中
    _kv.insert({key, value});

    return true;
}

接下来修改deserializegetReqHeadFromTotal的逻辑:

=== “getReqHeadFromTotal

// 获取请求报头
bool getReqHeadFromTotal(std::string &in_str)
{
    // 保存以\r\n结尾的一行数据
    std::string oneLine;
    while (true)
    {
        bool getOneLineFlag = parseOneLineFromTotal(in_str, oneLine);

        // 如果getOneLineFlag为true并且oneLine不为空,说明当前行有效,否则代表已经找到了结尾
        if (getOneLineFlag && !oneLine.empty())
        {
            getPairFromReqHead(oneLine);
        }
        // ...
    }

    return true;
}

=== “deserialize

// 反序列化
bool deserialize(std::string &in_str)
{
    // ...

    // 截取请求报头
    bool getReqHeadLine = getReqHeadFromTotal(in_str);
    if (!getReqHeadLine)
    {
        LOG(LogLevel::WARNING) << "反序列化获取请求报头失败";
        return false;
    }

    LOG(LogLevel::INFO) << "获取到的请求报头为:";
    std::for_each(_kv.begin(), _kv.end(), [&](std::pair<std::string, std::string> kv){
        LOG(LogLevel::INFO) << kv.first << "-" << kv.second;
    });

    // ...

    return true;
}
提取HTTP请求体中的字段

保持和第一阶段的处理方式一样

修改HttpResponse

第一阶段的HttpResponse类只是使用一个固定的字符串进行序列化再发送给客户端,这个做法明显是不妥的。实际上,对于HTTP响应来说,应该处理下面几点:

  1. 允许根据请求内容是否合法自动构建响应状态码,并且根据状态码自动生成状态码描述
  2. 允许外部添加响应报头中的属性
  3. 根据客户端请求的内容读取服务端存在的对应文件并返回给客户端,没有时返回404页面
HTTP状态码

根据上面的思路,首先需要处理的就是状态码,在介绍HTTP协议基本结构与基本实现HTTPServer一节提到,HTTP协议规定任何客户端的请求都必须得到响应,而区分响应情况就是通过状态码

在HTTP中,状态码有以下几种:

类别 原因短语
1XX Informational(信息性状态码)接收的请求正在处理
2XX Success(成功状态码)请求正常处理完毕
3XX Redirection(重定向状态码)需要进行附加操作以完成请求
4XX Client Error(客户端错误状态码)服务器无法处理请求
5XX Server Error(服务器错误状态码)服务器处理请求出错

但是,仅有开头还不足以说明具体的问题,所以每一种类别下还有具体的状态码和对应描述,因为状态码太多,所以下面仅仅展示常见的状态码:

状态码 类别 描述 示例场景
100 信息响应 请求已接收,客户端应继续发送请求 客户端询问服务器是否支持某些功能
101 信息响应 切换协议 客户端请求切换到WebSocket协议
200 成功响应 请求成功 页面加载成功
201 成功响应 资源创建成功 创建新用户或上传文件
204 成功响应 请求成功但无内容返回 删除操作后不返回任何内容
301 重定向 永久重定向 网站迁移至新域名
302 重定向 临时重定向 用户登录后跳转到主页
304 重定向 资源未修改,使用缓存 浏览器缓存的资源未过期
400 客户端错误 请求无效或无法被服务器理解 请求参数缺失或格式错误
401 客户端错误 未授权访问 用户未提供身份验证
403 客户端错误 禁止访问 用户权限不足
404 客户端错误 资源未找到 请求的页面或API不存在
405 客户端错误 方法不允许 使用了不支持的HTTP方法(如POST代替GET)
429 客户端错误 请求过多 超过API速率限制
500 服务器错误 内部服务器错误 服务器代码逻辑出错
502 服务器错误 错误网关 服务器作为网关时收到无效响应
503 服务器错误 服务不可用 服务器过载或维护中
504 服务器错误 网关超时 服务器作为网关时未能及时从上游获取响应

其中,更为常见的就是200(OK)、404(Not Found)、403(Forbidden)、302(Redirect)和504(Bad Gateway)

本次优先考虑200(OK)和404(Not Found),对于重定向将在后面的章节介绍,此处不具体描述

处理响应行

首先是HTTP响应版本,这个字段可以设置为一个固定值,因为一般情况下只会在升级的时候更改,所以可以考虑使用一个字符串指定

接着是HTTP响应状态码和状态码描述,因为本次只考虑200和404,所以只有两种情况:

  1. 用户请求的资源存在
  2. 用户请求的资源不存在

根据这两种情况,需要考虑的问题就是如何判断用户请求的资源是否存在。前面提到,URI就是资源路径,也就是说,用户想要拿到的资源就在URI中。根据这一点得出「判断用户请求的资源是否存在」只需要「在当前服务器的资源目录中找到对应的文件是否存在」即可。现在的问题就转变为「如何判断一个文件是否存在」,这里可以使用文件流读取对应的文件,如果文件不存在就给用户返回一个空字符串,否则就将读取到的文件添加到结果字符串即可

根据上面的思路,首先就是要获取到URI,这一点其实在HttpRequest中已经做到了,所以在HttpResponse中需要获取一下即可,这里可以考虑让读取内容的函数接收一个URI字符串,所以获取文件内容函数的基本逻辑如下:

// 获取文件内容
std::string getFileContent(std::string& uri)
{
    // 当前uri中即为用户需要的文件,使用文件流打开文件
    std::fstream f(uri);

    // 如果文件为空,直接返回空字符串
    if(!f.is_open())
        return std::string();
    
    // 否则就读取文件内容
    std::string content;
    std::string line;

    while (std::getline(f, line))
        content += line;

    f.close();
    
    return content;
}

接着,在创建一个函数用于处理获取URI以及构建文件内容,前面提到可以使用HttpResponse对象获取到对应的URI,所以当前函数需要接收一个HttpRequest对象,并且在HttpRequest类中需要提供获取URI的函数:

=== “获取URI函数”

 // 获取URI
 std::string getReqUri()
 {
     return _req_uri;
 }

=== “处理URI以及构建文件内容函数”

void buildHttpResponse(HttpRequest& req)
{
    // 获取uri
    std::string req_uri = req.getReqUri();
    // 根据uri获取文件内容
    std::string content = getFileContent(req_uri);
}

现在已经解决了文件问题,也就是说现在可以根据文件是否存在决定状态码的值和描述,根据前面的思路可以得出文件存在会返回空字符串,那么此时就说明状态码应该是404,否则就是200,所以这里就可以通过是否为空设置对应的状态码,而状态码描述可以通过状态码进行匹配,例如:

// 根据状态码得到状态码描述
std::string setStatusCodeDesc(int status_code)
{
    switch (status_code)
    {
    case 200:
        return "OK";
    case 404:
        return "Not Found";
    default:
        break;
    }

    return std::string();
}

接下来继续完善构建函数buildHttpResponse,因为要设置状态码和状态码描述,所以需要两个成员接收这两个值,便于后面构建HTTP响应结构:

void buildHttpResponse(HttpRequest &req)
{
    // 获取uri
    std::string req_uri = req.getReqUri();
    // 根据uri获取文件内容
    std::string content = getFileContent(req_uri);

    if (content.empty())
    {
        // 如果为真,说明文件不存在
        // 设置状态码为404并设置状态码描述
        _status_code = 404;
        _status_code_desc = setStatusCodeDesc(_status_code);
    }
    else
    {
        // 文件存在
        _status_code = 200;
        _status_code_desc = setStatusCodeDesc(_status_code);
    }
}

处理完状态码和状态码描述之后,接下来就是将HTTP版本、状态码和状态码描述构建出一个HTTP响应行,首先修改原有的构造函数,删除不需要的字段:

// 默认HTTP版本
const std::string default_http_ver = "HTTP/1.1";
class HttpResponse
{
public:
    HttpResponse()
        : _http_ver(default_http_ver)
    {
    }

private:
    std::string _http_ver;               // HTTP版本
    // ...
};

接着,在buildHttpResponse函数中添加构建请求行的逻辑:

void buildHttpResponse(HttpRequest &req)
{
    // ...

    // 构建请求行
    _resp_line = _http_ver + std::to_string(_status_code) + _status_code_desc;
}
处理响应行

根据前面的要求「允许外部添加响应报头中的属性」,需要提供一个哈希表存储keyvalue,所以首先需要创建一个类成员,接着添加一个函数用于执行添加逻辑:

class HttpResponse
{
public:
    // ...

    void insertRespHead(std::string& key, std::string& value)
    {
        _kv[key] = value;
    }
    // ...

private:
    // ...

    std::unordered_map<std::string, std::string> _kv;
    // ...
};
处理响应体

根据客户端请求的内容读取服务端存在的对应文件并返回给客户端,没有时返回404页面,所以只需要在buildHttpResponse中根据是否有文件内容给定具体文件即可,对于存在对应的文件的,因为getFileContent返回的就是读取到的文件内容,所以直接将结果给响应头即可,但是对于404页面,并没有读取到一个实际的文件内容,这里可以考虑直接给getFileContent写入一个固定文件,即404文件,这样该函数获取到的文件内容就是404文件

有了基本思路,现在就是缺少这类文件。在添加文件之前,先仔细了解一下不带参数的URI,在介绍HTTP协议基本结构与基本实现HTTPServer中提到了/开始不一定是系统根目录,而是Web应用根目录,这个Web应用根目录实际上就是当前服务器程序所在目录下的一个文件夹,这个文件夹下放着一些静态资源,例如HTML、图片、视频、CSS、JavaScript等,所以客户端想访问资源本质就是让服务器在这个文件夹中找到对应的文件并将其中的内容返回给客户端

有了上面的概念,下面就是在当前服务器程序所在的目录创建一个Web应用根目录,基本目录结构如下:

主程序目录
    - Web应用根目录
        - src
            - HTML文件
        - assets
            - stylesheets
                - CSS文件
            - JavaScripts
                - JavaScript文件
            - public
                - images
                    - 图片文件
                - videos
                    - 视频文件
    - HttpServer程序

上面的目录只是一个参考目录,并不是固定的,可以根据自己或者其他地方的规定进行调整,下面将以这个目录结构为例演示,当前创建的目录结构如下:

HttpServer
    - wwwroot
        - src
        - assets
            - js
            - style
            - public
                - images
                - videos
    - HttpServer_main

接着,为了演示出客户端正常接收到服务器存在的文件以及404文件,需要在src目录下创建两个HTML文件,此处不给出具体的HTML文件代码

接着,修改buildHttpResponse逻辑,确保可以读取到文件并将文件内容存储到响应体中:

// 404页面固定路径
std::string default_404_page = "wwwroot/src/404.html";

void buildHttpResponse(HttpRequest &req)
{
    // 获取uri
    std::string req_uri = req.getReqUri();
    // 根据uri获取文件内容
    std::string content = getFileContent(req_uri);

    if (content.empty())
    {
        // 如果为真,说明文件不存在
        // 设置状态码为404并设置状态码描述
        _status_code = 404;
        _status_code_desc = setStatusCodeDesc(_status_code);

        // 读取404页面并设置响应体
        _resp_body = getFileContent(default_404_page);
    }
    else
    {
        // 文件存在
        _status_code = 200;
        _status_code_desc = setStatusCodeDesc(_status_code);

        // 设置响应体
        _resp_body = content;
    }

    // 构建请求行
    _resp_line = _http_ver + std::to_string(_status_code) + _status_code_desc;
}

修改HttpServer

因为HttpServer需要调用sendData函数,该函数需要传入一个字符串作为发送数据,而HTTP响应中存在一个序列化函数,可以调用这个函数可以传入一个字符串,并将序列化后的字符串存储到参数的字符串中,这样就可以实现发送。所以整体handleHttpRequest逻辑修改如下:

void handleHttpRequest(SockAddrIn sock_addr_in, int ac_socketfd)
{
    // ...
    
    // 构建HTTP响应
    HttpResponse resq;
    resq.buildHttpResponse(req);
    // 序列化
    std::string out_str;
    resq.serialize(out_str);

    // 发送给客户端
    bs->sendData(out_str, ac_socketfd);
}

测试

主函数与上一节一样,测试结果如下:

在这里插入图片描述

从上面的测试结果可以发现,的确可以接收到数据,如果将地址栏的内容修改为localhost:8080/index.html,结果如下:

在这里插入图片描述

这个测试结果并不是像预期的那样显示主页的内容,而是显示404页面。那么明明存在index.html文件,为什么会出现无法读取到index.html?这是因为在解析URI时并没有考虑到URI起始的/,实际上getFileContent函数得到的uri字符串内容是/index.html,而已有的index.html文件路径为wwwroot/src/index.html,所以还需要在已有的uri上还需要加入默认的Web应用目录wwwroot/src,修改如下:

// Web应用路径
std::string default_webapp_dir = "wwwroot/";

// HTML文件路径
std::string default_html_dir = "src";

// 404页面固定路径
std::string default_404_page = default_webapp_dir + default_html_dir + "/404.html";

void buildHttpResponse(HttpRequest &req)
{
    // 获取uri
    // ...

    // 补充uri
    std::string real_uri = default_webapp_dir + default_html_dir + req_uri;
    // 根据uri获取文件内容
    std::string content = getFileContent(real_uri);

    // ...
}

再次测试,结果如下:

在这里插入图片描述

优化

从上面的测试可以发现,如果想要访问index.html文件还需要手动加上/index.html,但是访问一个实际的网站时,尽管没有携带/index.html,依旧可以访问到网站的index.html文件,例如访问百度的首页:

默认访问:

在这里插入图片描述

在网址后添加/index.html

在这里插入图片描述

因为直接输入网址,浏览器请求的默认就是Web应用根目录下的某一个文件,只是默认情况下隐藏了IP地址+端口号后的/,实现这个效果的方式很简单,只需要在getFileContent函数的开始判断是否是/,如果是就直接返回index.html的内容即可。思路的确如此,但是在上面先处理了传递给getFileContent的参数为添加了wwwroot/src的字符串,所以实际上如果只有一个/,那么传递给getFileContent函数的参数为wwwroot/src/,所以修改如下:

// 获取文件内容
std::string getFileContent(std::string &uri)
{
    // 默认访问index.html文件
    if(uri == "wwwroot/src/")
        uri = "wwwroot/src/index.html";

    // ...
}

再次测试,观察结果:

在这里插入图片描述

可以发现已经默认到了index.html的内容

虽然上面的思路的确可以实现问题,但是如果默认以/结尾,那么只要判断最后字符串是否是/即可,这样不论前面的内容是什么,只要是以/结尾,都可以访问到主页,所以还可以修改为:

// 获取文件内容
std::string getFileContent(std::string &uri)
{
    // 默认访问index.html文件
    if(uri.back() == '/')
        uri = "wwwroot/src/index.html";

    // ...
}

现在,不论前面是什么都可以访问到index.html,哪怕是不存在的目录:

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到