应用层协议HTTP

目录

一、背景知识

[二、http demo与http的理解](#二、http demo与http的理解)

请求行

状态码

重定向

再谈GET、POST

其他方法:

报头中的Connection字段

cookie和session简单了解

无连接解释:

无状态解释:

cookie

session


应用层协议可以自主实现,其中有一个很好的协议,HTTP协议:超文本传输协议,定义了客户端(例如浏览器)和服务端之间的通信,以交换或者传输超文本(例如HTML)。它是一个无连接、无状态的协议,也就是每一次通信都要重新建立连接并且服务端不会保留客户端的状态信息。

一、背景知识

url(统一资源定位符)

例如**http://www.example.com/docs/guide/tutorial.html**

首先http表示通信使用的应用层协议,这些广为使用的协议都有自己的专属端口号,http的是80,再url中省略了

www.example.com是域名,域名实际上就是ip地址,标识了客户端要获取哪一个主机的数据;通过DNS进行域名解析,找到域名对应的ip地址。不直接使用ip地址,是因为域名的可读性更好,一眼知道访问的是什么

后面用'/'分割组成的实际上就是linux下的文件路径。

知道ip地址以及端口号,就可以找到唯一的一台主机以及想要访问的进程,然后url获取具体路径下的资源,这就是超文本传输协议,至于超文本的体现就是获取的资源可以是视频音频等超文本内容

在看一个比较详细的url

一般有使用&进行分割的都是动态网站资源,&分割的是一个个key:value字段(后面提及);%20这种格式的字段是因为输入给浏览器的内容具有特殊字符(都具有特殊功能),为了避免url解析错误,浏览器会自动将这些特殊字符进行encode编码;对应的服务器会进行解码。浏览器和服务器之间的交互简称为B/S。

二、http demo与http的理解

http协议的请求以及响应都是格式化的,先看请求

首行是请求行:包括方法+url+版本,通过空格分割;

中间部分是请求报头Header,都是由key:value的键值对组成,每个键值对之间使用\r\n进行分割;

请求报头结束会有一个空行,遇见这个空行表示请求报头结束,下面部分是请求报文的数据部分,这个数据部分可以是空,不为空的时候Header中会有一个Content-Length来标识数据的长度。

我们可以将其看作为一个很大的字符串,这个字符串是以行为单位的。http使用空行来对报头和数据部分进行分离,使用特殊字符来区分区域,这其实就是HTTP协议的约定,那么这个协议的序列化实现就是使用特殊字符进行字符串的拼接,反序列化也是对这些特殊字符进行解析,HTTP协议的序列化和反序列化没有依赖类似于json的第三方库。

代码引入理解HTTP

复用网络版本计算器的Socket.hpp以及TcpServer.hpp,因为HTTP底层使用的依旧是TCP协议;现在实现一个Http.hpp,包含三个类,Http类用于实现服务器,功能是接收到客户端发送的请求对请求进行反序列化,然后构建响应报文,序列化之后将响应发送给客户端,这个函数设计成一个回调函数,交给底层的TcpServer.hpp去处理,因此还需要有两个类HttpRequest和HttpResponse,这两个类的成员属性按照HTTP的请求和响应报文格式中的属性来设计,序列化和反序列化也是按照HTTP报文的格式设计(用特殊字符来进行序列化和反序列化)

HttpRequest类:

需要有成员变量:

请求方法:请求方法例如POST以及GET对应的就是发送和获取;

URI:URI就是统一资源标识符,服务端解析这个URI知道路径然后找到响应资源返回给客户端,到服务端的web(http)根目录下,其实也是文件,它不是linux根目录,具体的体现就是wwwroot目录,这个目录下面有很多客户端想要的资源,例如html文件,uri是/的时候凭借wwwroot和首页(默认拼接的html),uri是一个具体路径的时候拼接wwwroot和这个路径,找到html文件之后序列化文件内容进入response的请求正文部分返回给客户端;

HTTP版本:实际上client和server是两款软件,不同的客户端版本可能不一样,server需要知道当前发送请求的客户端的版本,根据版本来进行响应,所以需要知道请求报文的http版本。

还需要有请求报头的keyvalue键值对,这个用一个unordered_map定义属性

最终是请求正文

HttpResponse类:

这个类是获取请求之后的响应,它通过处理之后的请求设置自己的成员属性,例如正文就是具体的wwwroot路径下的html文件,写进响应报文的data中,然后序列化发送给客户端,客户端浏览器会自动解析,然后显示对应页面在浏览器上

同样需要有对应的成员属性:HTTP版本、状态码(就是响应的类型,后面详细说)、状态码描述(对状态码进行描述方便客户端用户读懂)、报头是哈希表,响应正文是html文件内容

请求行

HttpRequest中的反序列化,先读取第一行内容(通过\r\n知道这是一行,解析都是通过HTTP设置的格式也就是特殊字符来进行的),实例化一个HttpRequest对象然后将第一行内容按照空格特殊字符分割,获取并且设置相应属性,尤其是uri,将uri和wwwroot进行拼接,得到文件路径(反序列化);设计一个URI函数给外部获取文件路径

这是在回调函数读到内容之后的第一步(这里默认可以读到完整请求),之后实例化一个HttpResponse对象,这里先手动设置状态行,之后URI读取路径下的文件进入这个对象的正文字符串中,然后通过HttpResponse中的方法序列化,也就是按照特殊字符分割拼接成为字符串,之后发送给客户端即可,客户端会显示网页信息

初始代码:(后序会修改)

Http.hpp

cpp 复制代码
#pragma once

#include "Log.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Util.hpp"
#include <unordered_map>
#include <sstream>
#include <string>

using namespace LogModule;
using namespace SocketModule;

const std::string gspace = " ";
const std::string glinespace = "\r\n";
const std::string glinesep = ": ";

const std::string webroot = "./wwwroot";
const std::string homepage = "index.html";

class HttpRequest
{
private:   
    std::string _method;
    std::string _uri;
    std::string _version;

    std::unordered_map<std::string, std::string> _headers;
    std::string _blankline;
    std::string _text;
public:
    HttpRequest()
    {}
    ~HttpRequest()
    {}
    void ParseReqLine(std::string reqline)
    {
        std::stringstream ss(reqline);
        ss >> _method >> _uri >> _version;
    }
    std::string Serialize()
    {
        return std::string();
    }
    bool Deserialize(std::string &reqstr)
    {
        // 读取第一行
        std::string reqline;
        bool n = Util::ReadOneLine(reqstr, &reqline, glinespace);
        if (!n) return false;
        logger(LogLevel::DEBUG) << reqline;

        // 读取成功将第一行的内容反序列化
        ParseReqLine(reqline);
        logger(LogLevel::INFO) << "method: " << _method;
        logger(LogLevel::INFO) << "uri: " << _uri;
        logger(LogLevel::INFO) << "version: " << _version;
        // 获取uri
        if (_uri == "/")
            _uri = webroot + _uri + homepage;
        else
            _uri = webroot + _uri;
        return true;
    }
    std::string URI()
    {
        return _uri;
    }
};

class HttpResponse
{
public:
    std::string _version;
    int _code;         
    std::string _desc;

    std::unordered_map<std::string, std::string> _headers;
    std::string _blankline;
    std::string _text;

public:
    HttpResponse()
    :_blankline(glinespace)
    {}
    ~HttpResponse()
    {}

    std::string Serialize()
    {
        // 状态行
        std::string statusline;
        statusline = _version + gspace + std::to_string(_code) + gspace + _desc + _blankline;
        // 响应报头
        std::string headerstr;
        for (auto &[k, v] : _headers)
        {
            std::string line = k + glinesep + v + _blankline;
            headerstr += line;
        }
        // 加上换行以及响应正文返回
        return statusline + headerstr + _blankline + _text;
    }
    void Deserialize(std::string &respstr)
    {

    }
};

class Http
{
private:
    std::unique_ptr<TcpServer> _tsvrp;
public:
    Http(uint16_t port)
    :_tsvrp(std::make_unique<TcpServer>(port))
    {}
    ~Http(){}
    void HandleHttpRequest(std::shared_ptr<Socket> &sock, InetAddr& client)
    {
        // 读取
        std::string httprequest;
        int n = sock->Recv(&httprequest);
        if (n > 0)
        {
            // 反序列化读到的请求字符串
            HttpRequest req;
            bool ret = req.Deserialize(httprequest);
            (void)ret;
            // 构建响应报文,这里的请求行先手动设置(内存级别,后面改)
            // 通过Util中的读取文件工具将URI路径下的html内容读到响应报文的正文处,然后序列化返回
            HttpResponse resp;
            resp._version = "HTTP/1.1";
            resp._code = 200; 
            resp._desc = "OK";
            ret = Util::ReadFileContent(req.URI(), &resp._text);
            std::string respstr = resp.Serialize();
            sock->Send(respstr);
        }
    }
    void Start()
    {
        _tsvrp->Start([this](std::shared_ptr<Socket> &sock, InetAddr& client){
            this->HandleHttpRequest(sock, client);
        });
    }
};

Util.hpp

cpp 复制代码
#pragma once
// 这是一个工具类

#include <iostream>
#include <string>
#include <fstream>

class Util
{
public:
    static bool ReadFileContent(std::string filename, std::string *out)
    {
        std::ifstream in(filename);
        if (!in.is_open())
        {
            return false;
        }
        std::string line;
        while (std::getline(in, line))
        {
            *out += line;
        }
        in.close();
        return true;
    }
    static bool ReadOneLine(std::string &bigstr, std::string *out, const std::string& sep)
    {
        int pos = bigstr.find(sep); // sep是\r\n,找不到说明读取内容不够
        if (pos == std::string::npos)
            return false;
        // 找到了将第一行写入out
        *out = bigstr.substr(0, pos); // 写入的内容不包括换行符
        // 之后将读取到的内容的这一行直接删除
        bigstr.erase(0, pos + sep.size());
        return true;
    }
};

测试结果:可以正常响应,浏览器正常解析了

状态码

但是请求的资源不一定存在,需要处理,这时候需要状态码的使用,看一下常见的状态码

当请求资源存在的时候,响应报文中的状态码设置为200,表示响应成功,状态码描述设置为ok;不存在设置为404,并且给响应报文的正文部分拼接一个404的页面,状态码描述设置为not found(一般服务器出现问题状态码应该设置为5开头的,但是这样会暴漏服务器的缺点,可能出现安全问题)

在HttpResponse中添加方法,SetTargeFile,用户响应类中资源路径的初始化,这样在响应类里面就可以通过路径获取资源;

定义一个SetCode类,用于设置状态码以及对应的状态描述;

客户端以及服务器如何保证自己获得到的报文是完整的?通过判断有没有读到空行得知报头是否完整,若是报头里面存在Content-Length(这个keyvalue)是描述报文正文部分的长度的,不为0说明有正文,此时判断正文长度和这个字段的value显示的长度是否相同即可判断;代码中响应报文需要设置这个字段;

还有一个字段Content-Type表示响应正文的类型,html结尾文件是一个类型,图片又是一个类型;当浏览器请求资源的时候可能在请求到的网页中再次请求图片资源,此时就需要服务端响应的时候谁知类型,方便浏览器进行解析然后显示图片

所以响应类里面还要设置一个方法SetHeader,设置报头中的keyvalue字段;为了获取正响应文长度以及类型,需要进行处理:获取正文长度,在Util.hpp中定义一个方法,计算文件大小(响应文件的大小);在响应类里面定义一个方法,用来获取uri的相应文件的后缀;

代码

Util.hpp

cpp 复制代码
#pragma once
// 这是一个工具类

#include <iostream>
#include <string>
#include <fstream>

class Util
{
public:
    static bool ReadFileContent(std::string filename, std::string *out)
    {
        // version1:以文本形式读,这种方式不能读图片一类的,读的是字符串
        // std::ifstream in(filename);
        // if (!in.is_open())
        // {
        //     return false;
        // }
        // std::string line;
        // while (std::getline(in, line))
        // {
        //     *out += line;
        // }
        // in.close();
        // return true;

        // version2:以二进制的方式读,可以忽略字符处理,可以读图片
        int filesize = FileSize(filename);
        std::ifstream in(filename);
        if (!in.is_open())
        {
            return false;
        }
        out->resize(filesize);
        in.read((char*)out->c_str(), filesize);
        in.close();
        return true;
    }
    static bool ReadOneLine(std::string &bigstr, std::string *out, const std::string& sep)
    {
        int pos = bigstr.find(sep); // sep是\r\n,找不到说明读取内容不够
        if (pos == std::string::npos)
            return false;
        // 找到了将第一行写入out
        *out = bigstr.substr(0, pos); // 写入的内容不包括换行符
        // 之后将读取到的内容的这一行直接删除
        bigstr.erase(0, pos + sep.size());
        return true;
    }
    static int FileSize(const std::string &filename)
    {
        std::ifstream in(filename, std::ios::binary);
        if (!in.is_open())
            return -1;
        in.seekg(0, in.end);
        int filesize = in.tellg();
        in.seekg(0, in.beg);
        in.close();
        return filesize;
    }
};

Http.hpp

cpp 复制代码
#pragma once

#include "Log.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Util.hpp"
#include <unordered_map>
#include <sstream>
#include <string>

using namespace LogModule;
using namespace SocketModule;

const std::string gspace = " ";
const std::string glinespace = "\r\n";
const std::string glinesep = ": ";

const std::string webroot = "./wwwroot";
const std::string homepage = "index.html";
const std::string notfoundpage = "/404.html";

class HttpRequest
{
private:   
    std::string _method;
    std::string _uri;
    std::string _version;

    std::unordered_map<std::string, std::string> _headers;
    std::string _blankline;
    std::string _text;
public:
    HttpRequest()
    {}
    ~HttpRequest()
    {}
    void ParseReqLine(std::string reqline)
    {
        std::stringstream ss(reqline);
        ss >> _method >> _uri >> _version;
    }
    std::string Serialize()
    {
        return std::string();
    }
    bool Deserialize(std::string &reqstr)
    {
        // 读取第一行
        std::string reqline;
        bool n = Util::ReadOneLine(reqstr, &reqline, glinespace);
        if (!n) return false;
        logger(LogLevel::DEBUG) << reqline;

        // 读取成功将第一行的内容反序列化
        ParseReqLine(reqline);
        logger(LogLevel::INFO) << "method: " << _method;
        logger(LogLevel::INFO) << "uri: " << _uri;
        logger(LogLevel::INFO) << "version: " << _version;
        // 获取uri
        if (_uri == "/")
            _uri = webroot + _uri + homepage;
        else
            _uri = webroot + _uri;
        return true;
    }
    std::string URI()
    {
        return _uri;
    }
};

class HttpResponse
{
public:
    std::string _version;
    int _code;         
    std::string _desc;

    std::unordered_map<std::string, std::string> _headers;
    std::string _blankline;
    std::string _text;

    std::string _targetfile;
public:
    HttpResponse()
    :_blankline(glinespace), _version("HTTP/1.0")
    {}
    ~HttpResponse()
    {}
    void SetTargetFile(const std::string &uri)
    {
        _targetfile = uri;
    }
    void SetCode(int code)
    {
        _code = code;
        switch (_code)
        {
            case 404:
                _desc = "not found";
                break;
            case 200:
                _desc = "ok";
                break;
            default:
                break;
        }
    }
    void SetHeader(const std::string key, const std::string value)
    {
        auto iter = _headers.find(key);
        if (iter != _headers.end())
            return; // 说明已经有这个报头了,不需要添加
        _headers.insert(make_pair(key, value));
    }
    std::string Uri2Suffix(const std::string &filename)
    {
        int pos = filename.rfind('.');
        if (pos == std::string::npos)
        {
            return "text/html";
        }
        std::string suffix = filename.substr(pos);
        if(suffix == ".html" || suffix == ".htm")
            return "text/html";
        else if(suffix == ".jpg")
            return "image/jpeg";
        else if(suffix == "png")
            return "image/png";
        else
            return "";
    }
    bool MakeResponse()
    {
        // 忽略客户端的favicon.ico请求
        if (_targetfile == "./wwwroot/favicon.ico")
        {
            logger(LogLevel::WARNING) << "user want uri: " << _targetfile << "忽略";
            return false;
        }
        bool ret = Util::ReadFileContent(_targetfile, &_text);
        int filesize = 0;
        if (ret == false) // 说明文件读取失败,返回404页面
        {
            _text = "";
            logger(LogLevel::WARNING) << "user want uri: " << _targetfile << " but not found";
            SetCode(404);
            _targetfile = webroot + notfoundpage;
            Util::ReadFileContent(_targetfile, &_text); // 读取失败重新读取
            // 获取响应文件大小进行报头设置
            filesize = Util::FileSize(_targetfile);
            SetHeader("Content-Length", std::to_string(filesize));
            // 获取uri后缀
            std::string suffix = Uri2Suffix(_targetfile);
            SetHeader("Content-Type", suffix);
            return false;
        }
        else
        {
            logger(LogLevel::DEBUG) << "user want uri: " << _targetfile;
            SetCode(200);
            filesize = Util::FileSize(_targetfile);
            SetHeader("Content-Length", std::to_string(filesize));
            std::string suffix = Uri2Suffix(_targetfile);
            SetHeader("Content-Type", suffix);
            return true;
        }
        return false;
    }
    std::string Serialize()
    {
        // 状态行
        std::string statusline;
        statusline = _version + gspace + std::to_string(_code) + gspace + _desc + _blankline;
        // 响应报头
        std::string headerstr;
        for (auto &[k, v] : _headers)
        {
            std::string line = k + glinesep + v + _blankline;
            headerstr += line;
        }
        // 加上换行以及响应正文返回
        return statusline + headerstr + _blankline + _text;
    }
    bool Deserialize(std::string &respstr)
    {
        return true;
    }
};

class Http
{
private:
    std::unique_ptr<TcpServer> _tsvrp;
public:
    Http(uint16_t port)
    :_tsvrp(std::make_unique<TcpServer>(port))
    {}
    ~Http(){}
    void HandleHttpRequest(std::shared_ptr<Socket> &sock, InetAddr& client)
    {
        // 读取
        std::string httprequest;
        int n = sock->Recv(&httprequest);
        std::cout << "httprequest :" << std::endl; // 打印读取的请求
        std::cout << httprequest;
        if (n > 0)
        {
            // 反序列化读到的请求字符串
            HttpRequest req;
            req.Deserialize(httprequest);
            HttpResponse resp;
            resp.SetTargetFile(req.URI());            
            // 进行相应的构建
            if (resp.MakeResponse());
            {
                // 成功之后序列化发送
                std::string respstr = resp.Serialize();
                sock->Send(respstr);
            }
        }
    }
    void Start()
    {
        _tsvrp->Start([this](std::shared_ptr<Socket> &sock, InetAddr& client){
            this->HandleHttpRequest(sock, client);
        });
    }
};

测试

Host表示请求的主机;

User-Agent表示请求的客户端浏览器信息;一般是根据有没有这个信息拦截爬虫,没有说明不安全;

Referer表示当前请求是从哪里跳转的。

使用telnet命令可以看到客户端接收到的响应信息

可以看到正文大小以及类型都被返回

重定向

301表示永久重定向,状态码描述为Moved Permanently,例如:网站更换域名之后,自动跳转到新域名,搜索引擎更新网站链接的时候使用。具体地,当服务器响应报文返回状态码301的时候,说明请求的资源已经被永久转移到了新的位置,此时会在响应报头中添加一个头部,Location,包含新位置的URI,客户端浏览器拿到这个URI去访问资源。

302表示临时重定向,状态码描述为Found。例如:用户登录之后,跳转到网站首页。服务器报文中状态码为302,表示请求资源临时被转移到别的位置,需要浏览器拿到Location中的URI去访问,临时重定向时浏览器不会缓存Location中的URI。

代码示例:

先重定向到新的网址,例如www.qq.com,看一下效果:

输入http://8/129.16.214:8080/redir 浏览器跳转到腾讯网了

telnet显示响应报文:

重定向到我们自己的资源:例如读取资源文件失败要返回404页面,当我们输入一个不存在的URI的时候,服务器不做读取404.html操作,而是设置状态码和报头分别为301和Location(value是我们服务器地址+端口+404.html的路径),浏览器接收到,重新发起请求,请求的是404页面,此次请求可以视为正常请求,因此走读取资源文件成功的逻辑,看代码:

再谈GET、POST

之前说到GET是获取资源的方法,那么如何上传资源呢?

POST是上传资源的方法,GET也可以上传资源,GET一般获取静态资源,但是若是动态资源,需要交互,也就是客户端需要上传信息给服务端,此时可以用GET方法;

拿登录页面说明:使用GET方法上传的时候,先将Login.html中的方法改为GET,URI中会包含登录的用户名和密码;类似于

先写代码再理解:服务端在反序列化请求报文的时候,解析URI,login是HTTP提供的一种服务,后面的就是使用GET上传资源的体现,服务端拿到这两部分并且分别设置为HttpRequest中的两个成员属性_uri和_args(反序列化);并且设置一个参数_is_interact,bool类型的,为真说明是上传;其实这是一种RESTful风格的设计,通过URI中的服务设计不同的方法,例如这里是login,那么可以给这个login服务设计一种方法,需要在HTTP中定义一个哈希表构建例如login和方法的映射,通过URI中的HTTP服务来调用方法,这是一种微服务。这里方法就将args打印出来并且设置响应报文发送

看代码:

cpp 复制代码
#pragma once

#include "Log.hpp"
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Util.hpp"
#include <unordered_map>
#include <sstream>
#include <string>
#include <memory>

using namespace LogModule;
using namespace SocketModule;

const std::string gspace = " ";
const std::string glinespace = "\r\n";
const std::string glinesep = ": ";

const std::string webroot = "./wwwroot";
const std::string homepage = "index.html";
const std::string notfoundpage = "/404.html";

class HttpRequest
{
private:   
    std::string _method;
    std::string _uri;
    std::string _version;

    std::unordered_map<std::string, std::string> _headers;
    std::string _blankline;
    std::string _text;

    std::string _args;
    bool _is_interact;
public:
    HttpRequest(): _is_interact(false)
    {}
    ~HttpRequest()
    {}
    void ParseReqLine(std::string reqline)
    {
        std::stringstream ss(reqline);
        ss >> _method >> _uri >> _version;
    }
    std::string Serialize()
    {
        return std::string();
    }
    bool Deserialize(std::string &reqstr)
    {
        // 读取第一行
        std::string reqline;
        bool n = Util::ReadOneLine(reqstr, &reqline, glinespace);
        if (!n) return false;
        logger(LogLevel::DEBUG) << reqline;

        // 读取成功将第一行的内容反序列化
        ParseReqLine(reqline);
        // 获取uri
        if (_uri == "/")
            _uri = webroot + _uri + homepage;
        else
            _uri = webroot + _uri;

        logger(LogLevel::INFO) << "method: " << _method;
        logger(LogLevel::INFO) << "uri: " << _uri;
        logger(LogLevel::INFO) << "version: " << _version;

        const std::string tmp = "?";
        int pos = _uri.find(tmp);
        if (pos == std::string::npos)
        {
            return true;
        }
        _args =  _uri.substr(pos + tmp.size());
        _uri = _uri.substr(0, pos);
        _is_interact = true;
        return true;
    }
    std::string URI()
    {
        return _uri;
    }
    std::string Args()
    {
        return _args;
    }
    bool IsInteract()
    {
        return _is_interact;
    }
};

class HttpResponse
{
public:
    std::string _version;
    int _code;         
    std::string _desc;

    std::unordered_map<std::string, std::string> _headers;
    std::string _blankline;
    std::string _text;

    std::string _targetfile;

public:
    HttpResponse()
    :_blankline(glinespace), _version("HTTP/1.0")
    {}
    ~HttpResponse()
    {}
    void SetTargetFile(const std::string &uri)
    {
        _targetfile = uri;
    }
    void SetText(const std::string &t)
    {
        _text = t;
    }
    void SetCode(int code)
    {
        _code = code;
        switch (_code)
        {
            case 404:
                _desc = "not found";
                break;
            case 200:
                _desc = "ok";
                break;
            case 301:
                _desc = "Moved Permanently";
                break;
            case 302:
            _desc = "Found";
                break;
            default:
                break;
        }
    }
    void SetHeader(const std::string key, const std::string value)
    {
        auto iter = _headers.find(key);
        if (iter != _headers.end())
            return; // 说明已经有这个报头了,不需要添加
        _headers.insert(make_pair(key, value));
    }
    std::string Uri2Suffix(const std::string &filename)
    {
        int pos = filename.rfind('.');
        if (pos == std::string::npos)
        {
            return "text/html";
        }
        std::string suffix = filename.substr(pos);
        if(suffix == ".html" || suffix == ".htm")
            return "text/html";
        else if(suffix == ".jpg")
            return "image/jpeg";
        else if(suffix == "png")
            return "image/png";
        else
            return "";
    }
    bool MakeResponse()
    {
        // 忽略客户端的favicon.ico请求
        if (_targetfile == "./wwwroot/favicon.ico")
        {
            logger(LogLevel::WARNING) << "user want uri: " << _targetfile << "忽略";
            return false;
        }
        if (_targetfile == "./wwwroot/redir")
        {
            SetCode(301);
            SetHeader("Location", "https://www.qq.com/");
            return true;
        }
        bool ret = Util::ReadFileContent(_targetfile, &_text);
        int filesize = 0;
        if (ret == false) // 说明文件读取失败,返回404页面
        {
            _text = "";
            logger(LogLevel::WARNING) << "user want uri: " << _targetfile << " but not found";
            SetCode(404);
            _targetfile = webroot + notfoundpage;
            Util::ReadFileContent(_targetfile, &_text); // 读取失败重新读取
            // 获取响应文件大小进行报头设置
            filesize = Util::FileSize(_targetfile);
            SetHeader("Content-Length", std::to_string(filesize));
            // 获取uri后缀
            std::string suffix = Uri2Suffix(_targetfile);
            SetHeader("Content-Type", suffix);
            return true;
            // SetCode(302);
            // SetHeader("Location", "http://8.129.16.214:8080/404.html");
            // return true;
        }
        else
        {
            logger(LogLevel::DEBUG) << "user want uri: " << _targetfile;
            SetCode(200);
            filesize = Util::FileSize(_targetfile);
            SetHeader("Content-Length", std::to_string(filesize));
            std::string suffix = Uri2Suffix(_targetfile);
            SetHeader("Content-Type", suffix);
            return true;
        }
        return false;
    }
    std::string Serialize()
    {
        // 状态行
        std::string statusline;
        statusline = _version + gspace + std::to_string(_code) + gspace + _desc + _blankline;
        // 响应报头
        std::string headerstr;
        for (auto &[k, v] : _headers)
        {
            std::string line = k + glinesep + v + _blankline;
            headerstr += line;
        }
        // 加上换行以及响应正文返回
        return statusline + headerstr + _blankline + _text;
    }
    bool Deserialize(std::string &respstr)
    {
        return true;
    }
};

using http_func_t = std::function<void(HttpRequest& req, HttpResponse& resp)>;

class Http
{
private:
    std::unique_ptr<TcpServer> _tsvrp;
    std::unordered_map<std::string, http_func_t> _service;
public:
    Http(uint16_t port)
    :_tsvrp(std::make_unique<TcpServer>(port))
    {}
    ~Http(){}
    void HandleHttpRequest(std::shared_ptr<Socket> &sock, InetAddr& client)
    {
        // 读取
        std::string httprequest;
        int n = sock->Recv(&httprequest);
        std::cout << "httprequest :" << std::endl; // 打印读取的请求
        std::cout << httprequest;
        if (n > 0)
        {
            // 反序列化读到的请求字符串
            HttpRequest req;
            req.Deserialize(httprequest);
            HttpResponse resp;
            if (req.IsInteract())
            {
                if (_service.find(req.URI()) == _service.end())
                {}
                else
                {
                    _service[req.URI()](req, resp);
                    std::string respstr = resp.Serialize();
                    sock->Send(respstr);
                }
            }
            else
            {
                resp.SetTargetFile(req.URI());            
                // 进行相应的构建
                if (resp.MakeResponse());
                {
                    // 成功之后序列化发送
                    std::string respstr = resp.Serialize();
                    sock->Send(respstr);
                }
            }
        }
    }
    void Start()
    {
        _tsvrp->Start([this](std::shared_ptr<Socket> &sock, InetAddr& client){
            this->HandleHttpRequest(sock, client);
        });
    }
    void RegisterService(const std::string name, http_func_t h)
    {
        std::string key = webroot + name;
        if (_service.find(key) == _service.end())
        {
            _service.insert(make_pair(key, h));
        }
    }
};
cpp 复制代码
#include "Http.hpp"
#include "Log.hpp"

using namespace LogModule;

void loginService(HttpRequest &req, HttpResponse &resp)
{
    logger(LogLevel::INFO) << "现在是http中login服务对应的服务";
    std::string text = "hello: " + req.Args();
    
    // 这里可以写登录认证的逻辑

    // 
    resp.SetCode(200);
    resp.SetHeader("Content-Type", "text/plain");
    resp.SetHeader("Content-Length", std::to_string(text.size()));
    resp.SetText(text);
}

// /main port
int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage : " << argv[0] << " port" << std::endl;
        exit(USAGE_ERR);
    }
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<Http> httpsvr = std::make_unique<Http>(port);
    
    httpsvr->RegisterService("/login", loginService);

    httpsvr->Start();
    return 0;
}

POST则是将数据设置到请求报文的正文部分

总结

使用HTTP协议传送的报文可以通过fiddler抓取,想要安全就要使用HTTPS协议

其他方法:

PUT:用于传输文件,将请求报文中的主体部分传递给服务器中URI的指定位置;不太常用,但在某些情况下,用于RESTful API中的资源更新。

HEAD:与GET方法类似,但是只返回响应头部,不包含响应报文主体部分。

OPTIONS:查询针对指定URI资源支持的请求方法。

报头中的Connection字段

先说HTTP/1.0这个版本,这是之前广泛使用的版本,当时每次请求资源就先建立TCP连接(三次握手),然后才可以发送请求,服务器受理之后返回响应;并且是短连接的,也就是每一次请求都要这样。这个时候每一个用户上网请求资源,因为那个时候一张网页上面资源很少,所以即使使用短连接服务器也能cover住;

但是现在,一张网页动辄几十张图片,甚至还有视频,这个时候使用短连接是不行的,因为资源太多,要建立很多次连接,服务器受理过载,所以使用HTTP/1.1,这是支持长连接的;也就是通信前只需要建立一次连接,之后传送数据在这条连接上面可以传输很多条报文(发送请求报文,返回多个应答)

查看HTTP报文可以看到支不支持长连接:

这是请求报文,Connection显示的是keep-alive,也就是客户端支持长连接

这一个就不支持了,显示的是close,表示只会处理一次请求只会就关闭

cookie和session简单了解

回归http,之前说过HTTP是一种无连接、无状态的协议,每次请求都需要建立新的连接,并且服务器不会保存客户端的用户状态信息。

无连接解释:

刚刚说到HTTP/1.0、HTTP/1.1的短、长连接,为什么说是无连接的呢?实际上HTTP根本没有连接的概念,这里的长短连接指的是下层TCP的连接,HTTP至始至终只关心请求和响应报文的处理,回顾之前的代码可以发现:HTTP处理的都是请求和响应报文,和连接不着边,至于每次请求都要建立新的连接其实是站在HTTP的角度去看TCP的,所以说是无连接的。这也是为什么写代码的时候要将HTTP.hpp和TcpServer.hpp分开的原因。

无状态解释:

祟拜你访问一个需要登陆认证的应用,会发现在一段时间内,登录之后,退出应用,再次打开应用就不需要再次登录输入账号密码了,这是因为服务端保留了账号密码状态信息。所以无状态就是使用HTTP协议的时候,服务器不会保存状态信息。

但是这样会给用户造成很多困扰,就比如b站,无状态的时候,打开一个视频就要登录认证一次,这样很麻烦,用户也不可能使用这样的软件;

那么就需要引入cookie了,这也是HTTP报头中的一个字段,它的工作原理是这样的:用户第一次登录,服务器受理返回响应报文,此时报文报头中有一个Set-Cookie字段,这个字段记录了用户登录的状态信息,例如用户名、密码等,客户端浏览器会接收这个字段并且写入在文件或者内存中,下一次登录,用户不需要输入账号密码了,因为此时浏览器拿出了之前文件或者内存中保存的的cookie,那么请求报文中的cookie字段中包含之前登录的状态信息,服务器直接查询这个字段就知道是你了,完成登录。

所谓保存在文件中就是对cookie的保存做了持久化处理,即使关闭浏览器,再次打开登录软件也不需要输入账号密码;保存在内存中就不一样了,关闭应用或者浏览器之后都需要重新登录;

持久化写入到文件中的实验:在我的代码中的响应字段添加cookie信息,之后查看浏览器的cookie文件:

session

但是cookie会有这样的问题:黑客可以在客户端植入木马,在通过cookie通信的时候,黑客会通过木马程序获取到cookie信息,这样一来就引发两个严重错误:1是黑客会通过你的cookie请求服务器资源了;2是黑客知道了你的状态信息,用户密码账号等等,也就是私密信息泄露了,这样就造成了安全问题。

如何解决,session引入,这是会话的意思。客户端和服务器通信,首次登录服务器会在自己内部为你的状态信息创建一个单独的session(内部会先描述再组织),然后为这个session设置一个id,通过哈希表构建映射,响应时,响应报文中会包含这个session id,下一次请求,用户只需要拿着这个id发送就行了,服务器会自动找到你的session来验证。

那么黑客还是可以获取session id来进行非法行动啊,是的。但是现在解决了一个问题就是你的私密信息不会泄露了,因为通信时不传转状态信息了,只传session id,session保存在服务器中,二服务器往往是强大的,不会轻易被破坏;还是可以拿着你的id登录访问资源。

针对于此虽说不能完全解决(这也正常,攻击与防御总是一直相互螺旋更新进化的,现在防好了,新技术出现,攻击手段更多了,以前的防御又不行了),但是引入了许多辅助方案,例如:ip溯源(服务器那边会对ip进行检查,发现你这个ip上次访问服务器的时候还在湖北,5s后再次访问就变成云南了,此时服务器会free掉这个session,保护资源)。

cookie+session:会话管理与会话保持

相关推荐
齐潇宇4 小时前
文件共享服务器
linux·运维·网络·文件共享
@insist1234 小时前
网络工程师-虚拟专用网技术(二):高级架构详解
网络·网络工程师·软考·软件水平考试
添砖java‘’4 小时前
数据链路层
服务器·网络·数据链路层
xiaoyaohou115 小时前
012、骨干网络改进(三):CSPNet与跨阶段局部网络的深度优化
网络
lew-yu5 小时前
websocket后端实现心跳检测,并定时清理异常的连接
websocket·网络协议·vim
a1117765 小时前
网络安全检查表 docx 附文件
网络·安全·web安全
aaa最北边5 小时前
计算机网络传输层-TCP三次握手底层详情
网络·tcp/ip·计算机网络
kyle~5 小时前
FANUC 机械臂 --- 配置字
网络·c++·机器人·ros2
达不溜的日记5 小时前
CAN总线网络传输层CanTp详解
网络·stm32·嵌入式硬件·网络协议·网络安全·信息与通信·信号处理