应用层协议HTTP

本章内容所有代码请点击这里

1. HTTP协议

  • 虽然我们说, 应用层协议是我们程序猿自己定的. 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议)就是其中之一。
  • 在互联网世界中,HTTP(HyperText Transfer Protocol,超文本传输协议)是一个至关重要的协议。它定义了客户端(如浏览器)与服务器之间如何通信,以交换或传输超文本(如 HTML 文档)。
  • HTTP 协议是客户端与服务器之间通信的基础。客户端通过 HTTP 协议向服务器发送请求,服务器收到请求后处理并返回响应。HTTP 协议是一个无连接、无状态的协议,即每次请求都需要建立新的连接,且服务器不会保存客户端的状态信息。

2. 认识URL

  1. URL介绍

平时我们俗称的 “网址” 其实就是说的 URL——(统一资源定位符
在这里插入图片描述
这里我们那Linux系统来展开:

  • 当我们client客户端向server服务器获取信息时,也就是向服务器获取资源。而在向服务器获取资源的时候,这个资源在哪里呢?当然是在服务器里面,而服务器是部署在linux下的,Linux下皆是文件,也就是说资源是放在文件中的。而我们要获取文件就一定要知道资源的文件路径,这样才能得到对应文件中的资源,在加上我们前面讲解过的ip+port找到唯一主机上的服务器,这样ip + port + 路径 我们就可以在互联网上找到对应主机上的服务器下的一个文件路径下的资源,所以这就是URL的作用,指定服务器下指定路径下的文件获取资源。
    在这里插入图片描述
    注1:这里gitee.com是域名,后面会讲解知识点将域名转化为ip地址。
    注2:还有就是这里为什么没有port端口号呢?这里其实是port一般都是浏览器默认绑定的,因为http服务是知名端口号,它是确定的。http绑定的端口号是80,https绑定的端口号是443(https)后期会进行讲解。
  1. URL中的特殊字符——urlencode (译码)和 urldecode(解码)

/ ? : 等这样的字符, 已经被 url 当做特殊意义理解了. 因此这些字符不能随意出现. 比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义. 转义的规则如下:将需要转码的字符转为 16 进制,然后从右到左,取 4 位(不足 4 位直接处理),每 2 位做一位,前面加上%,编码成%XY 格式.

例如:
在这里插入图片描述
“+” 被转义成了 “%2B” urldecode 就是 urlencode 的逆过程;
urlencode工具

3. HTTP 协议请求与响应格式

3.1.快速获取HTTP请求

直接上代码,看结果在解释。
准备工作:

  1. 将上一期写的Socket.hpp,Log.hpp,InterAddr.hpp,Tcpserver.hpp,直接复用
  2. Tcpserver.hpp稍作修改(修改)
  3. Socket.hpp稍作修改
  4. 创建Http.hpp文件请求(新增)
  5. Main.cc程序入口
  6. Makefile
  7. 这里只将修改和新增的编写出来,其他的文件请查看前面几期
  8. 样式;
    在这里插入图片描述
  9. 从上面我们可能会有疑问,这里为什么没有client客户端呢?从今天开始我们研究的是http协议是现成的协议,这意味着一定有现成的客户端,所以客户端我们不用自己写了,我们只需要服务器就行了。那么谁是客户端呢?只要是支持http协议的客户端都是可以的。(我们这里用浏览器来做测试,浏览器天然的支持http协议)

Tcpserver.hpp修改

  1. 将我们之前自定义协议换成Http协议进行,也就是回调方法 io_service_t 换成 http_t
    即:using io_service_t = std::function<void(socket_sptr sockfd, InterAddr client)>;换成std::function<std::string (std::string request)>;并将类型进行修改即可
  2. 接下来就是对Http.hpp进行编写。

点击连接查看前几期知识点

  1. Socket.hpp部分修改代码:
int Recv(std::string *out) override
{
    char buffer[4096]; // 空间开大点
    // 网路中read读到的是实际读到的字节个数
    ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);
    if (n > 0)
    {
        buffer[n] = 0;
        *out = buffer; // 这里不做+=操作直接带出
    }
    return n;
}

#新增关闭文件操作
#Socket类中添加Close虚方法
virtual void Close() = 0;

#TcpSocket类中添加Close关闭文件实现方法
void Close() override
{
    if (_sockfd > -1)
        ::close(_sockfd);
}
  1. Tcpserver.hpp代码:


#pragma once
#include <iostream>
#include <string>
#include <sys/types.h> /* See NOTES */
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <functional>   
#include <memory>

#include "Log.hpp"
#include "InterAddr.hpp"
#include "Socket.hpp"

using namespace socket_ns;
const static int defaultsockfd = -1;
const static int gbacklog = 16;


using http_t = std::function<std::string (std::string request)>;

enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    USAGE_ERROR,
    LISTEN_ERROR
};

using namespace socket_ns;
class TcpServer;

class ThreadData
{
public:
    ThreadData(socket_sptr fd, InterAddr addr, TcpServer* s): sockfd(fd), clientaddr(addr), self(s)
    {

    }
public:
    socket_sptr sockfd;
    InterAddr clientaddr;
    TcpServer *self;
};

//using task_t = std::function<void()>;

class TcpServer
{
public:
    TcpServer(uint16_t port, http_t httpservice) 
        : _localaddr("0", port),
        _listensock(std::make_unique<TcpSocket>()),
        _http_service(httpservice),
        _isrunning(false)
    {
        std::cout << "BuidListenSocket" << std::endl;
        _listensock->BuidListenSocket(_localaddr);

    }

    static void* HanderSock(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData*>(args);
        
        // 这里虽然我们没有做判断是否读取完整的操作(粘报问题),但是有很大几率读取到完整的请求(测试)
        std::string request, response;
        ssize_t n = td->sockfd->Recv(&request); // 读取请求
        if (n > 0)
        {
            response = td->self->_http_service(request);
            td->sockfd->Send(response);// 发送应答:将读到的请求原封不动的发回去
        }
        td->sockfd->Close();
        delete td;
        return nullptr;
    }
    void Loop()
    {
        _isrunning = true;
        // 4. 不能直接接受数据, 先要获取连接
        while (_isrunning)
        {
            InterAddr peeraddr;
            //listensock只是用来监听的,具体的服务是accept返回的文件描述符来服务的。(服务员案例)
            socket_sptr normalsock = _listensock->Accpeter(&peeraddr);
            if (normalsock == nullptr)
            {
                continue;
            }
            //Version 2 采用多线程 //适合在长服务场景
            pthread_t t;
            ThreadData *td = new ThreadData(normalsock, peeraddr, this);
            pthread_create(&t, nullptr, HanderSock, td); // 将线程设置为分离状态
        }
        _isrunning = false;

    }
    ~TcpServer()
    {
    }

private:
    InterAddr _localaddr;
    std::unique_ptr<Socket> _listensock; 
    bool _isrunning;

    http_t _http_service;
};
  1. Http.hpp代码:
#pragma once
#include <iostream>
#include <string>

// 这里仅是为了做测试,验证HTTP请求,后序会完善功能
class HttpServer
{
public:
	HttpServer(){}
	~HttpServer(){}
    std::string HandlerHttpServer(std::string req) // 处理http的request
    {
        std::cout << "------------------------" << std::endl;
        std::cout << req;

        return std::string();
    }
private:
};
  1. Main.cc代码:
#include <iostream>
#include <string>
#include "Http.hpp"
#include "Tcpserver.hpp"

void Usage(std::string proc)
{
    std::cout << "Usage\n\t" << proc << " local_port\n"
              << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    uint16_t port = std::stoi(argv[1]);

    HttpServer httpserver;
    TcpServer server(port, std::bind(&HttpServer::HandlerHttpServer, &httpserver, std::placeholders::_1));
    server.Loop();
    return 0;
}
  1. Makefile代码:
tcpserver:Main.cc
	g++ -o $@ $^ -std=c++14 -lpthread
.PHONY:clean
clean:
	rm -fr tcpserver

此时我们就可以看现象了:
在这里插入图片描述

3.2 快速解析HTTP请求

从上面我们就可以看到一个完成的http请求,他的基本构成如下:
在这里插入图片描述
所以我们可以发现http请求的格式其实就是一行字符串,其中之所以我们看到的是一行一行的,只是因为请求报文中是以\r\n为分隔符,终端在打印的时候识别了\r\n,所以才呈现出这样的效果

这里我们用浏览器搜索返回的是无响应,这是因为我们返回的是空的响应,现在我们给他添加一点响应。

class HttpServer
{
public:
    std::string HandlerHttpServer(std::string req)
    {
        std::cout << "------------------------" << std::endl;
        std::cout << req;

        // 这里我们添加一些应答——这里后期解释为什么这样做
        std::string response = "HTTP/1.0 200 OK\r\n"; // 报头
        response += "\r\n"; // 空行
        response += "<html><body><h1>hello world<h1><body><html>";

        return response;
    }
private:
};

在这里插入图片描述
其中在浏览器中点击开发者工具就可以看到我们返回的html代码了
在这里插入图片描述

3.3 细致了解HTTP请求与响应

  1. http请求
  • 这是一个标准的HTTP请求报文:
    在这里插入图片描述
  • 首行: [方法] + [url] + [版本]
    • Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\r\n 分隔;遇到空行表示 Header 部分结束
    • Body: 空行后面的内容都是 Body. Body 允许为空字符串. 如果 Body 存在, 则在Header 中会有一个 Content-Length 属性来标识 Body 的长度;

在这里插入图片描述

  1. 请求方式:比如GET,POST, HEAD……
  2. URI:这里我们就看做是URL,后期做解释,作用是指定服务器上的资源
  3. HTTP版本号(client端):比如说http/1.0 http/1.1 http/2.0
  4. 请求报头:是一种key/val形式的字符串
  5. 空行:用于保证报头和有效载荷的分离
  6. 请求正文(可以不携带)
  1. http响应
  • 这是一个标准的http响应
    在这里插入图片描述
  • 首行: [版本号] + [状态码] + [状态码解释]
    • Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\r\n 分隔;遇到空行表示 Header 部分结束
    • Body: 空行后面的内容都是 Body. Body 允许为空字符串. 如果 Body 存在, 则在Header 中会有一个 Content-Length 属性来标识 Body 的长度; 如果服务器返回了一个 html 页面, 那么 html 页面内容就是在 body 中.

在这里插入图片描述

4. 完善Http.hpp代码,实现http协议

4.1 http请求

  1. 将请求进行反序列化出来
    从上面的分析我们知道了,http请求其实一行大的字符串,并且是以\r\n为分隔符进行分离的,所以我们就可以进行验证一下,将这串请求进行拆分,这个工作叫做反序列化,因为前面章节有提到过,这里就不做过多的解释。

准备工作:

  1. HttpRequest类
  2. 根据上面请求报文的格式定义成员属性
  3. 编写对用的功能函数

成员函数

const std::string sep = "\r\n";

// 构建http request请求
class HttpRequest
{
private:
    //提取一行信息
    std::string GetOneLine(std::string &reqstr)
    {
        if (reqstr.empty()) return reqstr;
        auto pos = reqstr.find(sep);
        if (pos == std::string::npos) return std::string();

        //得到以\r\n分割的一行字符串
        std::string line = reqstr.substr(0, pos);
        //将截取到的字符串去除
        reqstr.erase(0, pos + sep.size());
        //这里做特殊处理,如果遇到了空行
        return line.empty() ? sep : line;
    }
public:
    HttpRequest():_blank_line(sep)
    {
    }
    // 序列化
    void Serialize()
    {
    }
    // 反序列化
    void Derialize(std::string &reqstr)
    {
        _req_line = GetOneLine(reqstr);
        while (true)
        {
            std::string line = GetOneLine(reqstr);
            if (line.empty()) break;
            else if (line == sep) //读到空行,后面的全是正文
            {
                _req_text = line;
                break;
            }
            else
            {
                _req_header.emplace_back(line);
            }
        }

    }

    // 打印测试接口
    void Print()
    {
        std::cout << "===" << _req_line << std::endl;
        for (auto& header : _req_header)
        {
            std::cout << "***" << header << std::endl;
        }
        std::cout << _blank_line <<std::endl;
        std::cout << _req_text << std::endl;
    }
    ~HttpRequest() 
    {
    }
// 成员属性
private:
    std::string _req_line;                // 请求行
    std::vector<std::string> _req_header; // 请求报头
    std::string _blank_line;              // 空行
    std::string _req_text;                // 请求正文
};


class HttpServer
{
public:
    std::string HandlerHttpServer(std::string req)
    {
        auto request = Factory::BuildHttpRequest();
        request->Derialize(req);
        request->Print();

        std::string response = "HTTP/1.0 200 OK\r\n"; // 报头
        response += "\r\n";                           // 空行
        response += "<html><body><h1>hello world<h1><body><html>";
        return response;
    }
private:
};

我们可以进行测试以下:
在这里插入图片描述
从面我们确确实实的得到了我们的预期结果,也是将请求反序列化出来了。

  1. 将请求报文按照key/value形式依次分离
    在这里插入图片描述
    从这张途中我们可以看到,请求行是用空格进行分离的,而请求报头是用(: + 空格)进行分离的,我们就可以通过这些分隔符来将其进行key/vlaue形式进行分离出来。

所以我们要做的就是:

  1. 添加kev/value属性
    // 这三个从请求行来
    std::string _method;
    std::string _url;
    std::string _version;
    // 这行从请求报文来
    std::unordered_map<std::string, std::string> _headers;
  2. Derialize中while循环后添加分离函数
    ParseReqLine();
    ParesHeader();
bool PaserHeaderHelper(const std::string &line, std::string *k, std::string *v)
{
    auto pos = line.find(header_sep);
    if (pos == std::string::npos)
        return false;
    *k = line.substr(0, pos);
    *v = line.substr(pos + header_sep.size());
    return true;
}
bool ParseReqLine()
{
    if (_req_line.empty())
        return false;
    // 按照空格进行分离
    std::stringstream ss(_req_line);
    ss >> _method >> _url >> _version;
    return true;
}
bool ParesHeader()
{
    for (auto &header : _req_header)
    {
        std::string k, v;
        if (!PaserHeaderHelper(header, &k, &v)) continue;
        _headers.insert(std::make_pair(k, v));
    }
}
// 打印测试接口
void Print()
{
    std::cout << "===" << _req_line << std::endl;
    for (auto &header : _req_header)
    {
        std::cout << "***" << header << std::endl;
    }
    std::cout << _blank_line << std::endl;
    std::cout << _req_text << std::endl;

    std::cout << "-----------------------------------------------------------" <<std::endl;
    std::cout << "method ###" << _method <<std::endl;
    std::cout << "url ###" << _url <<std::endl;
    std::cout << "version ###" << _version <<std::endl;
    for (auto &header : _headers)
    {
        std::cout << "@@@ " << header.first << " - " << header.second <<std::endl; 
    }
}

在这里插入图片描述

4.2再谈URL

在这里插入图片描述

  • 所以我们都会加上一个保存所有资源的目录一般叫做——wwwroot,并将index.html默认首页放到里面,这样我们再次向上面那样访问的时候,他就会直接访问根目录了,而是到我们指定的index.html目录下访问了。
    在这里插入图片描述
  • 这样,我们还要进行处理,就是让我们访问默认首页的时候访问的是/wwwroot/index.html,也就说我们要将wwwroot/index.html这个路径拼接到/根目录后面。

所以我们可以在ParseReqLine解析请求行的收加上默认的路径。

  1. 在添加一个成员属性std::string _path,根据_rul来判断path是否需要家默认路径
  2. 在HttpRequest函数进行修改操作
static const std::string sep = "\r\n";
static const std::string header_sep = ": ";
static const std::string wwwroot = "wwwroot";
static const std::string homepage = "index.html";

//构造的时候初始化
HttpRequest() : _blank_line(sep),_path(wwwroot)
{}
bool ParseReqLine()
{
    if (_req_line.empty())
        return false;
    // 按照空格进行分离
    std::stringstream ss(_req_line);
    ss >> _method >> _url >> _version;
    _path += _url;

    //判断请求的是否是/,如果是默认加上index.html
    if (_path[_path.size() - 1] == '/')
    {
        _path += homepage;
    }
    return true;
}

这个时候我们就可以看到我们的默认的路劲是/wwwroot了,并且如果申请的是/根目录的化会自动加上index.html。
在这里插入图片描述

4.3构建http应答

这里我们先在linux下直接使用一个命令telnet,它可以远程访问一个网站
在这里插入图片描述

构建http应答准备

  1. 准备HttpResponse类
  2. 根据响应报文设计成员属性
  3. 设置相应函数

这里主要解决的两个问题是,怎么构建出响应,和从哪里拿到资源
首先(这里是测试,并非是完全正式,所以部分代码直接设置)就是设置首行信息,初始化出响应内容。
其次就是根据request得到path路径,然后打开文件获取资源

class HttpResponse
{
public:
    HttpResponse():_version(httpversion), _blank_line(sep)
    {}
    ~HttpResponse()
    {}
    //添加状态行
    void AddStatusLine(int code)
    {
        _code = code;
        _desc = "OK"; // 后面做优化
    }
    void AddHeander(const std::string &k, const std::string &v)
    {
        _headers[k] = v;
    }
    void AddText(const std::string text)
    {
        _resp_text = text;
    }
    std::string Serialize()
    {
        // 状态行
        _status_line = _version + space + std::to_string(_code) + space + _desc + sep;
        // 响应报文
        for (auto &header: _headers)
        {
            _resp_header.emplace_back(header.first + header_sep + header.second + sep);
        }

        // 序列化
        std::string response = _status_line;
        for (auto &header: _resp_header)
        {
            response += header;
        }
        // 添加空行
        response += sep;
        // 添加正文
        response += _resp_text;
        return response;
    }
private:
    // 构建应答的必要字段
    std::string _version;   //版本
    int _code;              //状态码
    std::string _desc;      //状态描述
    std::unordered_map<std::string, std::string> _headers; //响应报头

    // 应答的结构化字段
    std::string _status_line;              // 请求行
    std::vector<std::string> _resp_header; // 响应报头
    std::string _blank_line;               // 空行
    std::string _resp_text;                // 响应正文
};
// 构建http response响应
class HttpServer
{
public:
    HttpServer()
    {}

    std::string ReadFileContent(const std::string &path, int *size)
    {
        // 一定要按照二进制的方式打开
        std::ifstream in;(path, std::ios::binary);
        if (!in.is_open())
            return std::string();

        // 首先获取文件大小
        in.seekg(0, in.end); // 将偏移量设置到文件结尾
        int filesize = in.tellg();
        in.seekg(0, in.beg);// 将偏移量恢复

        std::string content;
        content.resize(filesize);
        in.read((char*)content.c_str(),filesize);
        in.close();
        *size = filesize;
        return content;
    }
    std::string HandlerHttpServer(std::string req)
    {
        auto request = Factory::BuildHttpRequest();
        request->Derialize(req);
        //request->Print();
        
        // 根据path找到资源
        int contentsize = 0;
        std::string text = ReadFileContent(request->Path(), &contentsize);
        // 创建response响应
        auto response = Factory::BuildHttpResponse();
        // 设置状态码
        response->AddStatusLine(200);
        // 设置响应报文
        response->AddHeander("Content-Length", std::to_string(contentsize));
        response->AddHeander("Content-Type", "text/html");
        // 添加正文
        response->AddText(text);

        return response->Serialize();
    }
};

这个时候我们就可以向我们的index.html写点东西了,但是由于我们写的是后端,对前端肯能不太了解,所以我们就可以直接从网上copy一点代码过来。这里推荐一个学些前端的网页链w3cschool

#index.html代码

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>I am title</title>

</head>

<body>
    <h1>这是首页</h1>
    <p>this is my first make http</p>
    <a href="/1.html">点我到内容网页</a>
    <a href="/2.html">点我到登陆网页</a>

</body>

</html>

此时我们在运行以下:
在这里插入图片描述

在这里插入图片描述
如果我们还想访问其他的网页时,只需要添加对应的配置文件。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

#1.html代码

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
</head>

<body>
    <div id="container" style="width:500px">
        <h1>这是内容网页</h1>
        <a href="/">点我回到首页</a>
        <div id="header" style="background-color:#FFA500;">
            <h1 style="margin-bottom:0;">网页的主标题</h1>
        </div>
        <div id="menu" style="background-color:#FFD700;height:200px;width:100px;float:left;">
            <b>菜单</b><br>
            HTML<br>
            CSS<br>
            JavaScript
        </div>
        <div id="content" style="background-color:#EEEEEE;height:200px;width:400px;float:left;">
            内容就在这里</div>
        <div id="footer" style="background-color:#FFA500;clear:both;text-align:center;">
            Copyright © W3Cschools.com</div>
    </div>
</body>

</html>
#2.html代码

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>2</title>
</head>

<body>
    <h1>我是登陆网页</h1>
    <form name="input" action="html_form_action.php" method="get">
        用户名: <input type="text" name="user">
        <input type="submit" value="提交">
    </form>
    <a href="/1.html">点我到内容页面</a>
</body>

</html>
  1. 从上面的实验中,我们可以发现我们的html代码添加了a标签,这样可以使得我们通过点击连接的方式进行页面的跳转,而这种页面的跳转的本质其实就是一个http请求。所以什么是网站?站在程序员的角度:其实就是特定目录和文件构成的目录结构。所以我们写的http demo是一个负责把特定目录下的特定资源(文件)发送个client的IO程序。
  2. 当然,html文件中,还可以添加比如图片标签,这个时候就会有图片显示。所以一张网页,可以包含很多资源。比如获取一张完整的网页,是先获取html——>浏览器渲染html——> < img>——>浏览器会发起二次后者多次请求,后去完整网页所有元素,然后构成一个完成的网页。
  3. 但是我们从上面的实验可以发现,浏览器拿到的正文其实是字符串,那么浏览器到底是怎识别出他是什么格式的文档呢?那些是html的,那些是png的,那些是jpg的呢?所以这里我们还要重点将一些Header中的Content-Typd属性,他是标识了正文的格式。官方格式点击这里

4.4 Content-Type属性

官方格式点击这里

前提准备:

  1. 所以这里HttpServer中还得添加一个属性,用于存放对应的类型字段用于在AddHeander函数中根据path中的后缀来确定正文的类型。
  2. 所以这里还得写一个获取后缀的函数
  3. 这里可以像我们之前写的那个字典的翻译一样,写一个Load函数,将后缀对应的类型写到文件中,在加载出来,这里因为类型过多就直接进行inset操作,具体使用的时候可以自行添加。
  1. 添加上mime_typd属性
#Httpserver类
private:
    std::unordered_map<std::string, std::string> _mime_type;
  1. 在 HttpRequest添加suffix后缀属性,以及后去后缀函数
#HttpRequest类中添加suffix属性
std::string _suffix;    // 获取文件后缀


#后去后缀函数
bool ParseReqLine()
{
    if (_req_line.empty())
        return false;
    // 按照空格进行分离
    std::stringstream ss(_req_line);
    ss >> _method >> _url >> _version;
    _path += _url;

    //判断请求的是否是/,如果是默认加上index.html
    if (_path[_path.size() - 1] == '/')
    {
        _path += homepage;
    }

    auto pos = _path.rfind(filesuffixsep);
    if (pos == std::string::npos)
    {
        _suffix = ".unknown";
    }
    else
    {
        _suffix = _path.substr(pos);
    }

    LOG(INFO, "client want get %s\n", _path.c_str());
    return true;
}

  1. 在Httpserver构造中添加对应的属性
#Httpserver构造
HttpServer()
{
    _mime_type.insert(std::make_pair(".html", "text/html"));
    _mime_type.insert(std::make_pair(".css", "text/css"));
    _mime_type.insert(std::make_pair(".js", "application/x-javascript"));
    _mime_type.insert(std::make_pair(".png", "image/png"));
    _mime_type.insert(std::make_pair(".jpg", "image/jpeg"));
    _mime_type.insert(std::make_pair(".unknown", "text/html"));
}

5.HTTP请求/响应属性

5.1HTTP常见的方法

在这里插入图片描述

  1. GET 方法(重点)
    用途:用于请求 URL 指定的资源。
    示例:GET /index.html HTTP/1.1
    特性:指定资源经服务器端解析后返回响应内容。
    form 表单:https://www.runoob.com/html/html-forms.html
    注:这里我们重新该了我们2.html的代码,具体代买请到gitee里看
    在这里插入图片描述
    在这里插入图片描述
    GET方法会以url的方式,来向服务器提交参数
  1. POST 方法(重点)
    用途:用于传输实体的主体,通常用于提交表单数据。
    示例:POST /submit.cgi HTTP/1.1
    特性:可以发送大量的数据给服务器,并且数据包含在请求体中。
    form 表单:https://www.runoob.com/html/html-forms.html
    在这里插入图片描述
    所以post可以上传参数,以正文形式进行参数上传,以后想拿到参数可以使用content-length + request body拿到。所以post上传参数比get方法传参更私密。但是并不代表post更安全,这两个方法都不安全都可以用抓包工具抓到,看数据安不安全主要看有没有加密,所以就有了https协议。
  1. PUT 方法(不常用)
    用途:用于传输文件,将请求报文主体中的文件保存到请求 URL 指定的位置。
    示例:PUT /example.html HTTP/1.1
    特性:不太常用,但在某些情况下,如 RESTful API 中,用于更新资源。

  2. HEAD 方法
    用途:与 GET 方法类似,但不返回报文主体部分,仅返回响应头。
    示例:HEAD /index.html HTTP/1.1
    特性:用于确认 URL 的有效性及资源更新的日期时间等。

  3. DELETE 方法(不常用)
    用途:用于删除文件,是 PUT 的相反方法。
    示例:DELETE /example.html HTTP/1.1
    特性:按请求 URL 删除指定的资源。

  4. OPTIONS 方法
    用途:用于查询针对请求 URL 指定的资源支持的方法。
    示例:OPTIONS * HTTP/1.1
    特性:返回允许的方法,如 GET、POST 等

5.2HTTP的状态码

在这里插入图片描述
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)

状态码含义应用样例
100Continue上传大文件时,服务器告诉客户端可以继续上传
200OK访问网站首页,服务器返回网页内容
201Created发布新文章,服务器返回文章创建成功的信息
204No Content
301Moved Permanently网站换域名后,自动跳转到新域名;搜索引擎更新网站链接时使用
302Found 或 SeeOther用户登录成功后,重定向到用户首页
304Not Modified浏览器缓存机制,对未修改的资源返回
400Bad Request填写表单时,格式不正确导致提交失败
401Unauthorized访问需要登录的页面时,未登录或认证失败
403Forbidden尝试访问你没有权限查看的页面
404Not Found 访问不存在的网页链接
500Internal ServerError服务器崩溃或数据库错误导致页面无法加载
502Bad Gateway使用代理服务器时,代理服务器无法从上游服务器获取有效响应
503erviceUnavailable服务器维护或过载,暂时无法处理请求

好的,以下是仅包含重定向相关状态码的表格:

状态码含义是否为临时重定向应用样例
301Moved Permanently(永久重定向)
302Found 或 SeeOther(临时重定向)
307TemporaryRedirect(临时重定向)
308PermanentRedirect(永久重定向)
  • 关于重定向的验证,以 301 为代表
    HTTP 状态码 301(永久重定向)和 302(临时重定向)都依赖 Location 选项。以下是关于两者依赖 Location 选项的详细说明:
  1. HTTP 状态码 301(永久重定向)
    当服务器返回 HTTP 301 状态码时,表示请求的资源已经被永久移动到新的位置。
    • 在这种情况下,服务器会在响应中添加一个 Location 头部,用于指定资源的新位置。这个 Location 头部包含了新的 URL 地址,浏览器会自动重定向到该地址。
    • 例如,在 HTTP 响应中,可能会看到类似于以下的头部信息:

C++
HTTP/1.1 301 Moved Permanently\r\n
Location: https://www.new-url.com\r\n

  1. HTTP 状态码 302(临时重定向):
    • 当服务器返回 HTTP 302 状态码时,表示请求的资源临时被移动到新的位置。
    • 同样地,服务器也会在响应中添加一个 Location 头部来指定资源的新位置。浏览
    器会暂时使用新的 URL 进行后续的请求,但不会缓存这个重定向。
    • 例如,在 HTTP 响应中,可能会看到类似于以下的头部信息:

C++
HTTP/1.1 302 Found\r\n
Location: https://www.new-url.com\r\n

  • 总结:无论是 HTTP 301 还是 HTTP 302 重定向,都需要依赖 Location 选项来指定资
    源的新位置。这个 Location 选项是一个标准的 HTTP 响应头部,用于告诉浏览器应该
    将请求重定向到哪个新的 URL 地址。

这里我们测试一下:

  1. 所以这里我们还得在HttpServer中在添加一个属性,用于存放状态码与状态描述的映射
  2. 同时这里我们也只是在HttpServer的构造函数中写几个常见的,具体的可以使用配置文件load加载出来
#HttpServer构造
HttpServer()
{
//…………
    _code_to_desc.insert(std::make_pair(100, "Continue"));
    _code_to_desc.insert(std::make_pair(200, "OK"));
    _code_to_desc.insert(std::make_pair(301, "Moved Permanently"));
    _code_to_desc.insert(std::make_pair(302, "Found "));
    _code_to_desc.insert(std::make_pair(404, "Not Found"));
    _code_to_desc.insert(std::make_pair(500, "Internal ServerError"));
}
#HttpResponse中添加状态码和状态信息
void AddStatusLine(int code, std::string &desc)
{
    _code = code;
    _desc = desc; // 后面做优化
}
#在Httpserver中添加Loaction属性
#这里我们用qq在做重定向
response->AddHeander("Location", "https://www.qq.com/");

在这里插入图片描述
像这种重定向其实本质就是二次http请求。
在这里插入图片描述

  1. HTTP 状态码 404

这里我们还可以在添加一个404.html文件,用于访问路径不存在时出来的界面
在这里插入图片描述
这个html的内容是从网上copy过来的,大家也可去网上找一找自己喜欢的界面。

std::string HandlerHttpServer(std::string req)
{
    std::cout << "------------------------" << std::endl;
    std::cout << req;

    // 这里我们添加一些应答——这里后期解释为什么这样做
    std::string response = "HTTP/1.0 200 OK\r\n"; // 报头
    response += "\r\n";                           // 空行
    response += "<html><body><h1>hello world<h1><body><html>";
    return response;
    auto request = Factory::BuildHttpRequest();
    request->Derialize(req);
    auto response = Factory::BuildHttpResponse();
    std::string newurl = "https://www.qq.com/";
    // request->Print();
    int code = 0;
    if (request->Path() == "wwwroot/redir")
    {
        code = 302;
        response->AddStatusLine(code, _code_to_desc[code]);
        response->AddHeander("Location", newurl);
    }
    else
    {
        code = 200;
        int contentsize = 0;
        std::string text = ReadFileContent(request->Path(), &contentsize);
        if (text.empty())
        {
            code = 404;
            response->AddStatusLine(code, _code_to_desc[code]);
            std::string text404 = ReadFileContent("wwwroot/404.html", &contentsize);
            response->AddHeander("Content-Length", std::to_string(contentsize));
            response->AddHeander("Content-Type", _mime_type[".html"]);
            response->AddText(text404);
        }
        else
        {
            std::string suffix = request->Suffix();
            response->AddStatusLine(code, _code_to_desc[code]);
            response->AddHeander("Content-Length", std::to_string(contentsize));
            response->AddText(text);
            response->AddHeander("Content-Type", _mime_type[suffix]);
        }
    }
    return response->Serialize();
}

在这里插入图片描述

号到这里其实我们的服务器差不都该有的功能都有了,大家可以在网上找到一些网页模板测试,直接把网上写好的网页模板写到wwwroot路径就可以直接使用了,这是是前端和后端分开的好处,只要写好了后端,前端的东西直接拿过来用就行了。

5.3HTTP常见 Header

header作用
Content-Type数据类型(text/html 等)
Content-LengthBody 的长度
Host客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
User-Agent声明用户的操作系统和浏览器版本信息;
referer当前页面是从哪个页面跳转过来的;
Location搭配 3xx 状态码使用, 告诉客户端接下来要去哪里访问;
Cookie用于在客户端存储少量信息. 通常用于实现会话(session)的功能;

关于 connection 报头

  • HTTP 中的 Connection 字段是 HTTP 报文头的一部分,它主要用于控制和管理客户端与服务器之间的连接状态

核心作用

  • 管理持久连接:Connection 字段还用于管理持久连接(也称为长连接)。持久连接允许客户端和服务器在请求/响应完成后不立即关闭 TCP 连接,以便在同一个连接上发送多个请求和接收多个响应。持久连接(长连接)
  • HTTP/1.1:在 HTTP/1.1 协议中,默认使用持久连接。当客户端和服务器都不明确指定关闭连接时,连接将保持打开状态,以便后续的请求和响应可以复用同一个连接。
  • HTTP/1.0:在 HTTP/1.0 协议中,默认连接是非持久的。如果希望在 HTTP/1.0上实现持久连接,需要在请求头中显式设置 Connection: keep-alive。

语法格式

  • Connection: keep-alive:表示希望保持连接以复用 TCP 连接。
  • Connection: close:表示请求/响应完成后,应该关闭 TCP 连接。

下面附上一张关于 HTTP 常见 header 的表格
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.4 处理get方法中的url

从5.1中我们知道了get方法是会将参数拼接到url中的,而post则是放到正文当中的。而像我们在百度搜素关键字的时候,是要将关键字提取出来的
在这里插入图片描述

  1. 所以在我们的HttpRequest中就需要再加上一个参数属性std::string _args
  2. 同时在ParseReqLine函数将参数和路径分离并提取。
  3. 有了这些参数我们就可进比如登陆,查询等操作,这里我们就那登陆测试
  4. 因为服务器是不能进行数据处理的,数据处理是需要在引用层由我们自行处理的,所以这里也就涉及到了function的使用
bool ParseReqLine()
{
    if (_req_line.empty())
        return false;
    // 按照空格进行分离
    std::stringstream ss(_req_line);
    ss >> _method >> _url >> _version;
	
	//判断用的是get还是post方法
    if (strcasecmp("get", _method.c_str()) == 0) // 忽略大小写
    {
        //  /index.html?user=zhangsan&password=123456
        // 分离参数
        auto pos = _url.find(args_sep);
        if (pos != std::string::npos)
        {
            LOG(INFO, "change begin, url: %s\n", _url.c_str());

            _args = _url.substr(pos + args_sep.size());
            _url.resize(pos);
            LOG(INFO, "change down, url: %s, _args: %s\n", _url.c_str(), _args.c_str());
        }
    }
    _path += _url;

    LOG(DEBUG, "url: %s\n", _url.c_str());

    // 判断请求的是否是/,如果是默认加上index.html
    if (_path[_path.size() - 1] == '/')
    {
        _path += homepage;
    }

    auto pos = _path.rfind(filesuffixsep);
    if (pos == std::string::npos)
    {
        _suffix = ".unknown";
    }
    else
    {
        _suffix = _path.substr(pos);
    }

    LOG(INFO, "client want get %s, suffix: %s\n", _path.c_str(), _suffix.c_str());
    return true;
}
# 定义function
using func_t = std::function<std::shared_ptr<HttpResponse>(std::shared_ptr<HttpRequest>)>;

#Httpserver类中添加function属性
std::unordered_map<std::string, func_t> _funcs; 

#添加AddHeader方法进行回调
void AddHeader(const std::string functionname, func_t f)
{
    std::string key = wwwroot + functionname;
    std::cout << key << std::endl;
    _funcs[key] = f;
}

#Httpserver类中进行判断是否传参
auto request = Factory::BuildHttpRequest();
request->Derialize(req);
if (request->IsExec())
{
    LOG(DEBUG,"begin login\n");
    
    auto response = _funcs[request->Path()](request);
    LOG(DEBUG,"begin Serialize\n");

    return response->Serialize();
}
else
{
    auto response = Factory::BuildHttpResponse();
    int code = 200;
    int contentsize = 0;
    std::string text = ReadFileContent(request->Path(), &contentsize);
    if (text.empty())
    {
        code = 404;
        response->AddStatusLine(code, _code_to_desc[code]);
        std::string text404 = ReadFileContent("wwwroot/404.html", &contentsize);
        response->AddHeander("Content-Length", std::to_string(contentsize));
        response->AddHeander("Content-Type", _mime_type[".html"]);
        response->AddText(text404);
    }
    else
    {
        std::string suffix = request->Suffix();
        response->AddStatusLine(code, _code_to_desc[code]);
        response->AddHeander("Content-Length", std::to_string(contentsize));
        response->AddText(text);
        response->AddHeander("Content-Type", _mime_type[suffix]);
    }
    return response->Serialize();
}
#Main.cc中实现回调方法
std::shared_ptr<HttpResponse> Login(std::shared_ptr<HttpRequest> req)
{
    LOG(DEBUG, "=================================================\n");
    std::string userdata;
    if (req->Method() == "GET")
    {
        userdata = req->Args();
    }
    else if (req->Method() == "POST")
    {
        userdata = req->Text();
    }
    else
    {}
	//这里就可以实现拓展方法了
    // 1. 进程间通信,比如pipe!还有环境变量

    // 2. fork();

    // 3. exec();程序替换,执行其他语言代码:python/ php java / ……

    // 处理数据
    LOG(DEBUG, "enter data handle, data is : %s\n", userdata.c_str());
    auto response = Factory::BuildHttpResponse();
    response->AddStatusLine(200, "OK");
    response->AddHeander("Content-Type","text/html");
    response->AddText("<html><h1>handler data done</html><h1>");
    LOG(DEBUG, "=================================================\n");
    return response;
}

在这里插入图片描述

在这里插入图片描述

  • 9
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

初阳hacker

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值