您现在的位置是:首页 >学无止境 >【网路】-- HTTP协议网站首页学无止境

【网路】-- HTTP协议

川入 2023-05-23 00:00:02
简介【网路】-- HTTP协议

目录

HTTP协议

认识URL

urlencode和urldecode

Http

http的宏观结构

http请求报文格式

http响应报文格式

HTTP的方法

表单

重定向

HTTP常见Header

会话管理

实验证明

Connection选项

工具推荐

Fiddler

原理


应用层:就是程序员基于socket接口之上编写的具体逻辑,都是和文本处理有关的 -- 协议分析与处理。

http协议:具有大量的文本分析和协议处理。

HTTP协议

        虽然我们说,, 应用层协议是我们程序猿自己定的。但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用  ->   HTTP (超文本传输协议) 就是其中之一。

认识URL

        平时我们俗称的 " 网址 " 其实就是说的 URL。

  • 协议方案名:现在大部分更新为https。
  • 登陆信息(认证):大部分URL都是忽略的。
  • 服务器地址:也就是域名。(因为在网路应用当中,ip地址是纯数字,普通人识别成本高,的所以一般都是将对应的网站涵盖域名,域名 = IP地址,只不过多一步域名解析,(向域名解析服务器)去申请域名所匹配的IP地址)
  • 服务器端口号:大部分URL都是省略的。(因为我们所请求的网络服务,对应的端口号都是总所周知的(所有的客户端知道))默认采用的端口号完全对应与我们所使用的服务有关,如http所绑定的端口号为80号端口,https所绑定的端口号为443号端口。
  • 带层次的文件路径:

我们平时的上网(目的):

  1. 我们想获取资源。
  2. 我们想上传资源。
  • 我们想获取资源:

        一张照片,一个视频,一段文字等等 == 资源。

#问:在这个资源没有被我们拿到的时候,在哪里?

        服务器上(Linux上)。一个服务器上,可能存在很多的资源 —> 文件 —> 请求资源拿到我们的本地主机上 —> 服务进程打开我们要访问的文件,读取该文件,通过网络发送给client。

        然后,打开这个文件,就要先找到这个文件。而Linux中标识一个文件,是通过路劲来标识的。于是便有了带层次的文件路径

融汇贯通的理解:

        IP(唯一的机器) + 端口号(所提供的对应进程) + 路径(客户需要的资源) = 全网具有唯一性

唯一的机器 + 唯一的服务 + 唯一的路径:

        定义互联网中唯一的一个资源,url (Uniform Resource Location) 统一资源定位符。

所有的资源:全球范围内,只要找到它的url就能访问该资源。(www:万维网)

urlencode和urldecode

        如果用户想在url中包含url本身用来作为特殊字符的字符,url形式的时候,浏览器会自动给我们进行编码encode。一般服务端收到之后,需要进行转回特殊字符。

        因为:像 / ? : 等这样的字符,已经被url当做特殊意义理解了,因此这些字符不能随意出现。(某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义)

转义的规则:

        将需要转码的字符转为16进制,然后从右到左,取4(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。

        "+" 被转义成了 "%2B"。汉字也是同样的规制。

这里分享一个在线编码工具:

Http

        http协议是应用层的协议,底层采用的叫做TCP。换而言之,就是http在进行正常的request与response之前,已经经历了三次握手的过程。即:建立好了连接,在连接建立好的情况下,双方才才进行正常的通讯。

http的宏观结构

        单纯在报文角度,http可以是基于行的文本协议。

http请求报文格式

  • 版本:

请求:client告知server:client用的是哪一个http版本。

响应:server告知client:server用的是哪一个http版本。

如同微信1.0与微信2.0,老版本没有新版本的东西。

http响应报文格式

建立一个共识:

        考虑协议:需要从该协议是如何封装、解包、向上交付的。

#问:http是如何区分报头和有效载荷的?

        /r/n:通过空行的方式区分报头和有效载荷的。 于是,就一定能够将报头读完,接下来在读就是正文。

#问:如何得知正文的大小?

        报头当中,就涵盖有一种属性Content-Length:正文长度,所以只要读完报头,就能够知道正文有多大了。

写一个简易的TCP模型服务器

Log.hpp

        日志。

#pragma once
#include <cstdarg>
#include <ctime>
#include <cstdio>
#include <unistd.h>

// 日志是有日志级别的
#define DEBUG   0
#define NORMAL  1 // 正常
#define WARNING 2 // 警告 -- 没出错
#define ERROR   3 // 错误 -- 不影响后续执行(一个功能因为条件等,没有执行)
#define FATAL   4 // 致命 -- 代码无法继续向后执行

const char* gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL",
};

#define LOGFILE "./calculator.log"

// 完整的日志功能,至少:日志等级 时间 日志内容 支持用户自定义
void logMessage(int level, const char* format, ...)// level:日志等级; format, ...:用户传参、日志对应的信息等。
{
#ifndef DEBUG_SHOW
    if(level == DEBUG) return;
#endif
    char stdBuffer[1024]; //标准部分
    time_t timestamp = time(nullptr);

    snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);

    char logBuffer[1024]; //自定义部分
    va_list args;
    va_start(args, format);

    // 向缓冲区logBuffer中打印
    vsnprintf(logBuffer, sizeof logBuffer, format, args);
    va_end(args);

    // 向屏幕
    //printf("%s%s
", stdBuffer, logBuffer);

    // 向文件中打印
    FILE* fp = fopen(LOGFILE, "a");
    fprintf(fp, "%s%s
", stdBuffer, logBuffer);
    fclose(fp);
}

Sock.c

        TCP套接字操作的封装。 

#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <memory>
#include "Log.hpp"

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

class Sock
{
private:
    const static int gbacklog = 20; // 一般不能太大也不能太小
public:
    Sock(){}

    int Socket()
    {
        // 创建socket
        int listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock < 0)
        {
            logMessage(FATAL, "create socker error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success, _listensock: %d", listensock); // 验证其是3
        return listensock;
    }

    void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
    {
        // bind
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        //local.sin_addr.s_addr = ip.empty() ? INADDR_ANY : inet_addr(ip.c_str());
        inet_pton(AF_INET, ip.c_str(), &local.sin_addr);
        if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }
    }

    void Listen(int sock)
    {
        if (listen(sock, gbacklog) < 0)
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }

        logMessage(NORMAL, "create server success");
    }

    // 一般而言
    // const std::string &:输入型参数
    // std::string *:输出型参数
    // std::string &:输入输出型参数
    int Accept(int listensock, std::string *ip, uint16_t *port) // 这样既拿出来了新获得的套接字,又将客户端的ip和port拿到了
    {
        struct sockaddr_in src;
        socklen_t len = sizeof(src);
        int servicesock = accept(listensock, (struct sockaddr *)&src, &len);
        if (servicesock < 0)
        {
            logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
            return -1;
        }
        if(port) *port = ntohs(src.sin_port);
        if(ip) *ip = inet_ntoa(src.sin_addr);
        return servicesock;
    }

    bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(server_port);
        server.sin_addr.s_addr = inet_addr(server_ip.c_str());

        if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;
        else return false;
    }

    ~Sock() {}

private:
    int _port;
    std::string _ip;
    int _listensock;
};

HttpServer.hpp

#pragma once

#include <iostream>
#include "Sock.hpp"
class HttpServer
{
public:
    using func_t = std::function<void(int)>;

private:
    int _listensock;
    u_int16_t _port;
    Sock _sock;
    func_t _func;

public:
    HttpServer(const uint16_t &port, func_t func) : _port(port), _func(func)
    {
        _listensock = _sock.Socket();
        _sock.Bind(_listensock, _port);
        _sock.Listen(_listensock);
    }

    void Start()
    {
        while (true)
        {
            std::string clientIp;
            uint16_t clientPort = 0;
            int sockfd = _sock.Accept(_listensock, &clientIp, &clientPort);
            if (sockfd < 0)
                continue;
            if (fork() == 0)
            {
                close(_listensock);
                _func(sockfd);
                close(sockfd);
                
                exit(0);
            }
            close(sockfd);
        }
    }

    ~HttpServer()
    {
        if (_listensock >= 0)
            close(_listensock);
    }
};

HttpServer.cc

#include <iostream>
#include <memory>
#include "HttpServer.hpp"
#include "Usage.hpp"

#define SIZE 1024

void HandlerHttpRequest(int sockfd)
{
    char buffer[SIZE];
    ssize_t s= recv(sockfd, buffer, sizeof(buffer) - 1, 0);
    if(s > 0)
    {
        buffer[s] = 0;
        std::cout << buffer << "------------------------
" << std::endl;
    }
}

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

    std::unique_ptr<HttpServer> httpserver(new HttpServer(atoi(argv[1]), HandlerHttpRequest));
    httpserver->Start();
    return 0;
}

        然后,将服务器在自己的Linux下启动,然后结合ip:port通过浏览器访问。

        我们也可以浏览器进行对应的资源申请。

        如果我们什么都没有写,那么就会如上是根目录。其并不是我们linux的根目录路,而是叫做web根目录,其是我们可以自定义路径的。

#问:如何自定义呢?

        比如说:我们在当前路劲之下创建一个wwwroot的文件夹。如此之后,我们就可以将所有的网页资源放在该路径之下。

        此处我们简易的提取出,访问方想访问的资源。

util.hpp

        是一个工具类,可以使用其进行各种字符串的处理。

#pragma once

#include <string>
#include <vector>

class Util
{
public:
    static void cutString(std::string s, const std::string &sep, std::vector<std::string> *out)
    {
        std::size_t start = 0;
        while(start < s.size())
        {
            auto pos = s.find(sep, start);
            if(pos == std::string::npos) break;
            out->push_back(s.substr(start, pos - start));
            start += pos - start;
            start += sep.size();
        }
        if(start < s.size()) out->push_back(s.substr(start));
    }
};

HttpServer.cc

#include <iostream>
#include <memory>
#include <vector>
#include <string>
#include "HttpServer.hpp"
#include "Usage.hpp"
#include "Util.hpp"

#define SIZE 1024

void HandlerHttpRequest(int sockfd)
{
    char buffer[SIZE];
    ssize_t s= recv(sockfd, buffer, sizeof(buffer) - 1, 0);
    if(s > 0)
    {
        buffer[s] = 0;
        std::cout << buffer << "------------------------
" << std::endl;
    }

    // 2.试着构建一个http的响应
    std::string HttpResponse = "HTTP/1.1 200 OK
";
    HttpResponse += "
";
    HttpResponse += "<html><h3> hello world </h1></html>"; 
    // 此处写的是一个静态的,这个内容其实不应该硬编入代码里,而是应该以网页的形式存放在wwwroot的目录下
    // 然后让对应的用户去请求这个网页
    // 如果当前用户请求的是根目录,默认代表的是请求我们所对应的web服务器的默认首页

    // 提出每一行
    std::vector<std::string> vline;
    Util::cutString(buffer, "
", &vline);

    printf("----------- 每一行 -----------
");
    for(auto &iter : vline)
    {
        std::cout << iter << '
' << std::endl;
    }
    printf("----------- 每一行 -----------
");

    // 提出第一行的' '隔开的数据
    std::vector<std::string> vblock;
    Util::cutString(vline[0], " ", &vblock);
    printf("----------- 第一行 -----------
");
    for(auto &iter : vblock)
    {
        std::cout << iter << '
' << std::endl;
    }
    printf("----------- 第一行 -----------
");


    // 提出第一行中所想的访问路径
    std::string file = vblock[1];
    printf("----------- 第一行 -----------
");
    std::cout << file << '
' << std::endl;
    printf("----------- 第一行 -----------
");

    send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);
}

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

    std::unique_ptr<HttpServer> httpserver(new HttpServer(atoi(argv[1]), HandlerHttpRequest));
    httpserver->Start();
    return 0;
}

进行一个简易的网页路径访问:

        在我们定义的web根目录"./wwwroot"里定义所可以进行请求的资源:

 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>网页测试</title>
</head>
<body>
    <h3>这是一个标题</h3>
    <p> 这是一个文本 </p>
</body>
</html>

 HttpServer.cc

#include <iostream>
#include <memory>
#include <vector>
#include <string>
#include <fstream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "HttpServer.hpp"
#include "Usage.hpp"
#include "Util.hpp"

// 一般http都要有自己的web根目录
#define ROOT "./wwwroot"
// 如果客户端只请求了一个/,我们返回默认首页
#define HOMEPAGE "index.heml"

#define SIZE 1024

void HandlerHttpRequest(int sockfd)
{
    char buffer[SIZE];
    ssize_t s= recv(sockfd, buffer, sizeof(buffer) - 1, 0);
    if(s > 0)
    {
        buffer[s] = 0;
        std::cout << buffer << "------------------------
" << std::endl;
    }

    // // 2.试着构建一个http的响应
    // std::string HttpResponse = "HTTP/1.1 200 OK
";
    // HttpResponse += "
";
    // HttpResponse += "<html><h3> hello world </h1></html>"; 
    // // 此处写的是一个静态的,这个内容其实不应该硬编入代码里,而是应该以网页的形式存放在wwwroot的目录下
    // // 然后让对应的用户去请求这个网页
    // // 如果当前用户请求的是根目录,默认代表的是请求我们所对应的web服务器的默认首页

    // 提出每一行
    std::vector<std::string> vline;
    Util::cutString(buffer, "
", &vline);

    // printf("----------- 每一行 -----------
");
    // for(auto &iter : vline)
    // {
    //     std::cout << iter << '
' << std::endl;
    // }
    // printf("----------- 每一行 -----------
");

    // 提出第一行的' '隔开的数据
    std::vector<std::string> vblock;
    Util::cutString(vline[0], " ", &vblock);
    // printf("----------- 第一行 -----------
");
    // for(auto &iter : vblock)
    // {
    //     std::cout << iter << '
' << std::endl;
    // }
    // printf("----------- 第一行 -----------
");


    // 提出第一行中所想的访问路径
    std::string file = vblock[1];
    // printf("----------- 第一行 -----------
");
    // std::cout << file << '
' << std::endl;
    // printf("----------- 第一行 -----------
");
    std::string target = ROOT;

    if(file == "/") file = "/index.html";
    target += file;
    std::cout << "访问的路径" << target << std::endl;

    std::string content;
    std::ifstream in(target);
    if(in.is_open())
    {
        std::string line;
        while(std::getline(in, line))
        {
            content += line;
        }
        in.close();
    }

    std::string HttpResponse;
    if(content.empty()) HttpResponse = "HTTP/1.1 404 NotFound
";
    else HttpResponse = "HTTP/1.1 200 OK
";
    HttpResponse += "
";
    HttpResponse += content;
    send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);
}

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

    std::unique_ptr<HttpServer> httpserver(new HttpServer(atoi(argv[1]), HandlerHttpRequest));
    httpserver->Start();
    return 0;
}

融会贯通的理解:Http本质上就是文本分析。

HTTP的方法

我们平时的上网行为其实就两种:

  1. 从服务端拿下来资源数据(打开微信、淘宝、拼多多,会得到对应的网站页面)
  2. 把客户端的数据提交到服务器(登录、注册、搜索)

        在Http这里,主要针对的就是资源。资源:通常是一些用户可以直接感知,并理解的东西,比如说我们拿了一张图片、一个视频、一个音频。

需要注意:

        在TCP套接字中就有所提到,我们在进行正常上网的时候,其实并不是我们在上网,而是我们通过我们本地的一些APP / 浏览器来向服务端发起请求。也就是叫做服务器,通常其以守护进程的方式一直在运行。

        而平时我们所上网使用的APP / 浏览器,最终在手机 / 电脑上都叫做一个进程。所以:服务器 -> 服务器进程, 客户端 -> 客户端进程,它们之间进程拿下来数据和将数据提交上去,这个行为的本质就是进程间通讯。只不过咋子Http这里,其通讯需要更具体的通讯方案。

对应的方法:

  1. 从服务端拿下来资源数据  -> 一般都叫做GET方法,也就是Http发起请求时所一般填写的请求方法中所填写的方法。
  2. 把客户端的数据提交到服务器 -> 一般都叫做POST方法 / GET方法。

融会贯通的理解:

        在一般的Http应用场景中,80% ~ 90% 的请求方法就是两个,其他方法要么被服务端禁用,要么服务端不支持。因为web服务器推送的资源是直接推送给客户的,而客户也有好的客户和非法的客户。所以直接应对客户的服务器,其要以最小的接口,最小的成本能给到用户提供最基本的服务之外,还要尽量减少其自身无用服务的暴露,进而减少服务器其自己出现漏洞的可能性。

手动尝试GET方法:

 其下面所显示的一堆就是传说中的html,就是我们在百度中可以查看到的:

        或者可以,以更多工具 -> 开发者工具的形式进行打开:

        之所以我们所看到,我们抓下来的首页是乱的,而我们所查的页面的网页,格式比起来好很多很多。这是因为浏览器给我们做了格式化处理,其实一般的网页真正给我们看的就是我们抓下来的那个样子。因为其是没有空行、回车等的格式控制的,因为对于此些格式控制,它们也是字符,所以一般网页在进行发布的时候都是要对其进行压缩的,说白了就是一个网页里不需要的符号就直接去掉了,用以减少网络传输的成本。


        如果我们想利用我们自己的代码测试一下GET方法拿网页这个过程,我们就必须使用表单来进行测试。

表单

        搜集用户的数据,并把用户数据推送给服务器。我们对应的拿数据 / 提交数据对应的方法,其中这两个方法要提交数据给服务器,就必须依托于网页的表单,而表单的作用就是给用户提供对应的输入框和提交按钮。让用户将信息填充好了后,并且把数据推送给服务器,具体的推送其实就是,对应的表单当中的数据,会被转换成http request的一部分。

        上述图片就叫做input输入框。

index.html

        使用GET方法。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTTP学习测试</title>
</head>

<body>
    <h3>这是一次学习</h3>
    <form name="input" action="/a/b/haha.html" method="get">
        Username: <input type="text" name="user">
        Password: <input type="password" name="pwd">
        <input type="submit" value="登陆">
    </form>
</body>

</html>

我们进行对应的登录可以发现:

跳转地址分析: 

如:在百度上进行搜索。 

        而,当我们一点击对应的登陆的时候,我们最终的参数是直接就会回显到url当中的输入框的,所以GET方法最重大的特性就是:它会将我们所对应的参数直接回显到我们所对应的url向服务端传参。

index.html

        使用POST方法。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTTP学习测试</title>
</head>

<body>
    <h3>这是一次学习</h3>
    <form name="input" action="/a/b/haha.html" method="post">
        Username: <input type="text" name="user">
        Password: <input type="password" name="pwd">
        <input type="submit" value="登陆">
    </form>
</body>

</html>

我们可以发现它将参数放到了正文的部分:

POST通过http的正文方式提交参数。

POST与GET的区别:

    将数据从客户端提交向服务器时,它们两个提交参数是不一样的。

  • GET方法通过url传参,回显输入的私密信息,不够私密。
  • POST方法通过正文提交参数,不会回显,一般私密性是有保证的。

Note:

        私密不是安全,加密和解密才是安全的。

        不成文的规定:当上传的东西过大,使用POST方法传,不建议使用GET方法。主要也是因为:GET方法使用url进行传参,其是按行为单位的,参数里如果包含换行也就不好处理,并且解析起来不方便,还要我们自己解析。而如果使用POST方法,其报头里是有Content-Length字段标识正文长度的,是很方便我们进行整体内容的读取。

HTTP常见的方法

方法 说明 支持的HTTP协议版本 GET 获取资源 1.0、1.1 POST 传输实体主体 1.0、1.1 PUT 传输文件 1.0、1.1 HEAD 获得报文首部(空行之前) 1.0、1.1 DELETE 删除文件 1.0、1.1 OPTIONS 询问支持的方法 1.1 TRACE 追踪路径 1.1 CONNECT 要求用隧道协议连接代理 1.1 LINK

建立和资源之间的联系

1.0 UNLINK 断开连接关系 1.0

HTTP的状态码
类别原因短语
1XXInformational(信息性状态码)接收的请求正在处理
2XXSuccess(成功状态码)请求正常处理完毕
3XXRedirection(重定向状态码)需要进行附加操作以完成请求
4XXClient Error(客户端错误状态码)服务器无法处理请求
5XXServer Error(服务器错误状态码)服务器处理请求出错
        最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向 ), 504(Bad Gateway)

重定向

重定向:
        当我们进行某些网站请求时 / 某些网页请求时,因为功能的要求,要求我们去请求其他网页 / 网站。

永久重定向 (301) VS 临时重定向 (302 / 307):

        有一个网站的访问地址,可以想象为一个物理地址(一家你很喜欢的店铺)。

  • 临时重定向:

        突然有一天,该店铺说:由于装修原因,于是将店铺临时移动到一个附近的新地址。于是当你看到后,会跑去新的地址,但是下回你再来,还是会来到老店铺看一眼,是否开店。

  • 永久重定向:

        突然有一天,该店铺说:由于老旧原因,于是将店铺永久移动到一个附近的新地址。于是当你看到后,会跑去新的地址,并且下回你再也不会到老店铺,而是直接到新店铺。

总结:

  • 临时重定向:不影响用户后续的请求策略。
  • 永久重定向:影响用户的后续的请求策略。

临时重定向:

        当我们对对应的网站地址进行请求之后,会返回302,并告诉我们新的地址,然后浏览器会根据对应的新地址,自行执行跳转。 

临时重定向部分的编写: 

        将不存在的路径,重定向到http://www.qq.com/

    std::string HttpResponse;
    if(content.empty())
    {
        HttpResponse = "HTTP/1.1 302 Found
";
        HttpResponse += "Location: https://www.qq.com/
"; // 临时重定向到qq
    }
    else HttpResponse = "HTTP/1.1 200 OK
";
    HttpResponse += "
";
    HttpResponse += content;
    send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);

        如果我们想实现一个在站内的跳转,也就是说当访问的资源不存在的时候,就返回一个404的页面:

    std::string HttpResponse;
    if(content.empty())
    {
        HttpResponse = "HTTP/1.1 302 Found
";
        HttpResponse += "Location: http://121.36.24.39:8081/a/b/c/404.html
"; 
    }
    else HttpResponse = "HTTP/1.1 200 OK
";
    HttpResponse += "
";
    HttpResponse += content;
    send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);

404.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>不存在</title>
</head>
<body>
    <h2>你访问的页面不存在</h2>
</body>
</html>

        301:相对于302多做一部分的工作,也就是更新一下本地的缓存 / 书签。以此达到后续的访问。

HTTP常见Header

  • Content-Length: 报文的长度。
  • Content-Type: 正文的数据类型(text / html等)。

        Content-Type对照表的查询:Content-Type对照表 - MKLab在线工具

        但是我们可以发现,我们写与不写,好像根本没有什么区别。这是因为现在的浏览器都非常的聪明,当我们在请求的时候,即便不写清Content-Type,浏览器也可以收到正文之后,也可以根据正文的内容,对文件内容做自动识别。注意:这个识别并不是所有的浏览器都可以识别到的。

        如果我们将给予的网络响应,却告诉浏览器是存文本的:

    std::string HttpResponse;
    if(content.empty())
    {
        HttpResponse = "HTTP/1.1 302 Found
";
        HttpResponse += "Location: http://121.36.24.39:8080/a/b/c/404.html
";
    }
    else 
    {
        HttpResponse = "HTTP/1.1 200 OK
";
        HttpResponse += ("content: text/plain
");
        HttpResponse += ("content-Length: " + std::to_string(content.size()) + "
");
    }
    HttpResponse += "
";
    HttpResponse += content;
    send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);

  • Host: 客户端告知服务器,所请求的资源是在哪个主机的哪个端口上。
  • User-Agent: 声明用户的操作系统和浏览器版本信息。

        可以发现,我们用电脑的浏览器所搜索下载出现的推荐,与手机浏览器所搜索下载出现的推荐是不同的。就是因为User-Agent的存在,对应的目标网站会根据我们说发送的请求识别User-Agent知晓我们所使用的操作系统信息。

  • referer: 当前页面是从哪个页面跳转过来的。
  • location: 搭配3xx状态码使用,告诉客户端接下来要去哪里访问。
  • Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能。

会话管理

http的特征:

  • 简单快速

        应用层http协议请求的是在互联网当中的各种资源,底层采用TCP + 套接字帮我们去请求,请求的时候会告诉服务端需要什么资源(通过http的request中的状态行中的url)

  • 无连接

        http协议这一层,http是不维护链接的,是TCP协议来维护的。

  • 无状态

        说白了就是:http协议其并不会记录一个用户,历史上,也就是上一次的请求,不会对用户的行为做任何的记录,不会认为当前用户历史上有没有登录过,有没有直接访问过该网页。

一个理解:

        我们如使用淘宝、哔哩哔哩等网页,我们需要登录,而且登录与主页并不是一个页面。由于http协议的无状态,于是压根就没有登录的记录。

#问:这个无状态的情况就会是个很难堪的局面?

        然而平时的使用告诉我们,并不是这样的。(以网页哔哩哔哩为例)真实情况是我们将网页关掉再重新进入,登录还是存在,甚至将浏览器关掉再开也是登录情况。

#问:这么如何理解?

        http协议确实不记录状态 ,但是并不代表网站服务,不会为我们提供这样的服务。因为我们的web服务器,我们所写的底层套接字,都是我们所写的,我们并没有对用户的状态进行管理,因为这个事本身就不能交给协议去做。其应该是上层的业务逻辑去维护的,所以http协议不管心。它就关系文件大小、文件类别等,负责网络特性。

#问:此状态是怎么记录的?

         为了支持网站进行常规用户的会话管理,报头属性里会有两种属性:

  • 内存级:浏览器进程关掉就没有记录了。
  • 文件级:浏览器进程关掉还有记录。

        并且我们是可以看到cookie文件的:

        现在就有一个问题,浏览器将我们对应的账号和密码都保存起来,如果有一天中了黑客的圈套:木马!

        其直接扫描我们的浏览器的相关的文件,找到我们的cookie文件,盗走文件,然后使用其自身的浏览器打开网站,于是黑客直接使用免密码的方式登录上我们的账号,于是个人信息就严重泄漏了。

        于是在现实生活中,这个方案无法再进行采取,这个方法特别的不安全。

一个方案(现在主流): 

        cookie文件继续用,但是方案进行调整。

        采用将数据存储在服务器端,而返回一个由算法生成的唯一ID(存储私密信息在服务器端文件的文件名为唯一ID:session id)存储在cookie文件中,之后在session id有效期内,http request时就会自动携带cookie->session id自动登录。

#问:但是这样,好像黑客还是可以通过拿到我们的cookie然后进行无密码登录?

        是的,还是可以通过拿到我们的 cookie 然后进行无密码的登录。但是与上次的区别的就是,黑客最多拿到session id,而个人信息并没有做任何的泄漏。而黑客通过此方法进行无密码的登录,是无法避免的了的,这也就是一堆人QQ被盗的原因(没有十全十美的完美防御,只能尽量避免,因为客户太 "小白" 了)

session id的好处:

  • 曾经因为没有session id:黑客得到cookie,直接得到账户密码,服务端没有任何办法,拿着账户密码正确,阻拦也不对。
  • 曾经因为有session id:黑客得到cookie,也就是得到session id,这次服务端站的主权。主要盗取都是外国的,所以ip就会异常,于是对应公司的账户安全团队就会识别到:异地访问,于是直接将session id立即设置为失效,然后向绑定手机发送短信、需要重新登录。

实验证明

    std::string HttpResponse;
    if(content.empty())
    {
        HttpResponse = "HTTP/1.1 302 Found
";
        HttpResponse += "Location: http://121.36.24.39:8080/a/b/c/404.html
";
    }
    else 
    {
        HttpResponse = "HTTP/1.1 200 OK
";
        HttpResponse += ("content-Type: text/plain
");
        HttpResponse += ("content-Length: " + std::to_string(content.size()) + "
");
        HttpResponse += "Set-Cookie: 这是一个cookie
";
    }
    HttpResponse += "
";
    HttpResponse += content;
    send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);

点击登录后:

#问:客户端是如何将信息提交向服务器?服务器又是如何将cookie写给客户端的?

        于是就回到了前面所提的两个选项: 


Connection选项

  • Connection: close,代表在连接的时候,采用短链接。
  • Connection: keep-alive,代表在连接的时候,采用长链接。

一个完整的网页,是由非常多的资源构成的。

  • 短链接:发动十次请求,TCP层面浏览器就要发起十次连接 —— 成本高。
  • 长链接:发动十次请求,一次连接搞定 —— 成本低。

也正因为报头与正文的格式规范化,我们一次读取多个请求也是没有任何问题的。

工具推荐

  • postman:可以让我们的客户端,手动的向目标服务器构建Http请求。
  • Fiddler:是一个进行抓包的工具,我们所有出去的网络请求,最终Fiddler都可以帮我们抓到,主要抓Http。

Fiddler

        Fiddler 4安装与配置博客推荐:Fiddler4安装与配置_偷懒的肥猫的CSDN博客

        Fiddler将我们本主机的http请求全部抓取到。

原理

        原本的浏览器访问服务器。

        当有fidder之后,实际上请求是首先发送给fiddler(相当于截止),fiddler拿到了浏览器的请求,然后fiddler帮浏览器进行请求,响应结果传递原理同理。

        我们通过使用浏览器对我们所写的服务端进行请求,然后使用fiddler进行捕捉会发现:

         fiddler捕捉到的请求方法是不一样的,其url带ip、带端口的,而我们收到的请求是:

        只是GET然后没有带对应的ip地址,就是因为这个http请求,请求的时候已经到对应的主机了(在这个主机上找资源)而fiddler之所以带ip了,是因为fiddler要替我们去请求。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。