【网路】-- HTTP协议

news2025/1/4 17:16:37

目录

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"。汉字也是同样的规制。

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

  • UrlEncode编码/UrlDecode解码 - 站长工具

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\n", stdBuffer, logBuffer);

    // 向文件中打印
    FILE* fp = fopen(LOGFILE, "a");
    fprintf(fp, "%s%s\n", 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 << "------------------------\n" << 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 << "------------------------\n" << std::endl;
    }

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

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

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

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


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

    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 << "------------------------\n" << std::endl;
    }

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

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

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

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


    // 提出第一行中所想的访问路径
    std::string file = vblock[1];
    // printf("----------- 第一行 -----------\n");
    // std::cout << file << '\n' << std::endl;
    // printf("----------- 第一行 -----------\n");
    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\r\n";
    else HttpResponse = "HTTP/1.1 200 OK\r\n";
    HttpResponse += "\r\n";
    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\r\n";
        HttpResponse += "Location: https://www.qq.com/\r\n"; // 临时重定向到qq
    }
    else HttpResponse = "HTTP/1.1 200 OK\r\n";
    HttpResponse += "\r\n";
    HttpResponse += content;
    send(sockfd, HttpResponse.c_str(), HttpResponse.size(), 0);

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

    std::string HttpResponse;
    if(content.empty())
    {
        HttpResponse = "HTTP/1.1 302 Found\r\n";
        HttpResponse += "Location: http://121.36.24.39:8081/a/b/c/404.html\r\n"; 
    }
    else HttpResponse = "HTTP/1.1 200 OK\r\n";
    HttpResponse += "\r\n";
    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\r\n";
        HttpResponse += "Location: http://121.36.24.39:8080/a/b/c/404.html\r\n";
    }
    else 
    {
        HttpResponse = "HTTP/1.1 200 OK\r\n";
        HttpResponse += ("content: text/plain\r\n");
        HttpResponse += ("content-Length: " + std::to_string(content.size()) + "\r\n");
    }
    HttpResponse += "\r\n";
    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\r\n";
        HttpResponse += "Location: http://121.36.24.39:8080/a/b/c/404.html\r\n";
    }
    else 
    {
        HttpResponse = "HTTP/1.1 200 OK\r\n";
        HttpResponse += ("content-Type: text/plain\r\n");
        HttpResponse += ("content-Length: " + std::to_string(content.size()) + "\r\n");
        HttpResponse += "Set-Cookie: 这是一个cookie\r\n";
    }
    HttpResponse += "\r\n";
    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要替我们去请求。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/432100.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何在大厂做好架构演进?

1 架构演进的定义 1.1 定义 通过设计新的系统架构(4R)&#xff0c;来应对业务和技术的发展变化。 1.2 关键点 新架构新的复杂度 1.3 目的 应对业务和技术的发展变化后带来新的复杂度。 案例 淘宝去IOE&#xff0c;是因为业务发展大了后&#xff0c;IOE的成本和可控性难…

51单片机入门

文章目录 一、安装keil5及proteus二、MCS-51单片机结构与原理(一).8051单片机基本组成(二).8051单片机引脚1.电源引脚2.时钟电路引脚3.控制信号引脚4.输入/输出端口 (三) 并行输入/输出端口结构 三、单片机cx51编程基础(一).变量定义(二).数据类型(三).存储类型(四).Cx51语言程…

【Python】逆向解析js代码

目录 1. 打开百度翻译网页&#xff0c;查找翻译结果的网络资源包 2. 获取翻译结果网络资源包的url、请求头、请求体&#xff0c;解析json文件数据 3. 观察请求体字段&#xff0c;发现 query 字段便是我们输入的需要翻译的值 4. ctrl F 快捷键搜索sign值的网络资源包&#x…

自然语言处理:词嵌入简介

动动发财的小手&#xff0c;点个赞吧&#xff01; Word Embeddings 机器学习模型“查看”数据的方式与我们&#xff08;人类&#xff09;的方式不同。例如&#xff0c;我们可以轻松理解“我看到一只猫”这一文本&#xff0c;但我们的模型却不能——它们需要特征向量。此类向量或…

MongoDB 聚合管道的集合关联($lookup)及合并($unionWith)

目前为止&#xff0c;我们已经介绍了一部分聚合管道中的管道参数&#xff1a; $match&#xff1a;文档过滤 $group&#xff1a;文档分组&#xff0c;并介绍了分组中的常用操作&#xff1a;$addToSet&#xff0c;$avg&#xff0c;$sum&#xff0c;$min&#xff0c;$max等。 $add…

python OCR识别验证码

1. 抓取网页验证码图像并保存 import lxml.html, urllib3# 使用urllib3抓取网页数据 http urllib3.PoolManager() html http.request(GET,site).data# 使用lxml解析网页数据 tree lxml.html.fromstring(html) # 解析HTML&#xff0c;补全不完整的格式 fixedhtml lxml.ht…

LeetCode:102. 二叉树的层序遍历

&#x1f34e;道阻且长&#xff0c;行则将至。&#x1f353; &#x1f33b;算法&#xff0c;不如说它是一种思考方式&#x1f340;算法专栏&#xff1a; &#x1f449;&#x1f3fb;123 可以参考&#x1f449;LeetCode&#xff1a;二叉树的前、中、后序遍历——如何创建一棵【二…

机器学习 特征工程

文章目录 一、数据预处理1. 缺失值处理1.1 删除1.2 统计值填充1.3 前后向值填充1.4 直接忽略1.5 模型预测 2. 异常值处理 二、特征提取1. 数值型特征提取1.1 标准化与缩放1.1.1标准化&#xff08;Standardization&#xff09;1.1.2 归一化&#xff08;Normalization&#xff09…

STM32F4_定时器精讲(TIM)

目录 1. 什么是定时器&#xff1f; 2. STM32定时器简介 2.1 高级控制定时器 TIM1和TIM8 2.1.1 TIM1和TIM8简介 2.1.2 时基单元 2.1.3 计数器模式 2.1.4 重复计数器 2.1.5 时钟选择 2.1.6 捕获/比较通道 2.1.7 输入捕获模式 2.1.8 其他功能 2.2 通用定时器 TIM2到TI…

算法----删点成林

题目 给出二叉树的根节点 root&#xff0c;树上每个节点都有一个不同的值。 如果节点值在 to_delete 中出现&#xff0c;我们就把该节点从树上删去&#xff0c;最后得到一个森林&#xff08;一些不相交的树构成的集合&#xff09;。 返回森林中的每棵树。你可以按任意顺序组…

2023年天梯赛模拟赛

//能力有限&#xff0c;只展示一百分代码。前八个题一般是原题&#xff0c;所以不展示题目。 L1-1 嫑废话上代码 #include<bits/stdc.h> using namespace std; int main(){cout<<"Talk is cheap. Show me the code.";return 0; } L1-2 九牛一毛 这是…

Leetcode每日一题——“移除元素”

各位CSDN的uu们你们好呀&#xff0c;小雅兰又来啦&#xff0c;今天&#xff0c;小雅兰的内容是移除元素&#xff0c;下面&#xff0c;让我们进入Leetcode的世界吧 说明: 为什么返回数值是整数&#xff0c;但输出的答案是数组呢? 请注意&#xff0c;输入数组是以「引用」方式…

ChatGPT | 使用new bing的简易教程

1. 教程参考&#xff1a; https://juejin.cn/post/7199557716998078522 2.在参考上述教程遇到的问题与解决 2.1 下载dev浏览器的网址打不开 egde dev下载地址&#xff08;上面网站上的&#xff09;我电脑打不开 换用下面的网址即可 https://www.microsoftedgeinsider.com/z…

在three.js中废置对象

基于three.js子如何废置对象(How to dispose of objects) 前言: 为了提高性能,并避免应用程序中的内存泄露,一个重要的方面是废置未使用的类库实体。 每当创建一个three.js中的实例时,都会分配一定数量的内存。然而,three.js会创建在渲染中所必需的特定对象, 例如几何…

4.11、socket地址

4.11、socket地址1.通用 socket 地址2.专用socket地址1.通用 socket 地址 socket 网络编程接口中表示 socket 地址的是结构体 sockaddr&#xff0c;其定义如下&#xff1a; // socket地址其实是一个结构体&#xff0c;封装端口号和IP等信息。后面的socket相关的api中需要使用…

【c语言】每日一题之汉诺塔类型

目录 前言题目说明描述 题目分析汉诺塔问题 题目代码展示 前言 大佬们&#xff0c;我又回来了&#xff0c;最近也在忙自己的学业&#xff0c;忙着生活对线&#xff0c;也参加了今年的蓝桥杯其他的组&#xff0c;发现今年太难了 &#xff0c;摆烂了。但我想到了读者你们&#x…

前端面试之JavaScript题目,简单全面(持续更新ing...)

数据类型 1.JavaScript有哪些数据类型&#xff0c;它们的区别&#xff1f; 类型&#xff1a;JavaScript共有8种数据类型&#xff0c;undefined&#xff0c;null&#xff0c;Boolean&#xff0c;string&#xff0c;number&#xff0c;bigint&#xff0c;symbol&#xff0c;obj…

K-计算面积

链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 来源&#xff1a;牛客网 题目描述 小w给你三种图形&#xff0c;可能是平行四边形&#xff0c;三角形&#xff0c;梯形&#xff0c;对于给定的TTT个图形&#xff0c;你需要依次回答每个图形的面积&#xff0c;保证答案…

《LeetCode》——LeetCode刷题日记

本期&#xff0c;将给大家带来的是关于 LeetCode 的关于二叉树的题目讲解。 目录 &#xff08;一&#xff09;606. 根据二叉树创建字符串 &#x1f4a5;题意分析 &#x1f4a5;解题思路 &#xff08;二&#xff09;102. 二叉树的层序遍历 &#x1f4a5;题意分析 &#…

docker stats 命令详解

docker stats : 显示容器资源的使用情况&#xff0c;包括&#xff1a;CPU、内存、网络 I/O 等。 docker stats [OPTIONS] [CONTAINER...]OPTIONS 说明&#xff1a; –all , -a :显示所有的容器&#xff0c;包括未运行的。 –format :指定返回值的模板文件。 –no-stream :展…