• 【Network】网络基础@应用层 —— 协议 | http | https


    “like you do when you lie and I know it’s my imagination” @_zing_photograph

    1. 认识协议

    一条QQ消息经历了如下过程:结构化数据 → 长“字符串” → 结构化数据

    struct message
    {
     昵称:小边小边不秃头
     头像:我愚蠢的理想主义.png
     消息:GN ST
     时间:2022-08-26 15:02:55
    }msg;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    为什么要进行序列化和反序列化

    • 为了应用层网络通信的方便。因为这种结构化的数据是不便于网络传输的,而字符串便于网络传输。
    • 这种结构化的数据方便上层使用内部成员,例如图片界面显示。将应用和网络进行解耦。

    之前我们做的udp、tcp通信,并没有进行任何序列化和反序列化,只是因为我们没去定义结构化数据。

    事实上,这个结构化数据就是协议的表现。

    怎么做到呢?我们可以自己造轮子,但其实真挺麻烦的;今天我们用轮子,就是用别人写好的组件(xml/json/protobuff)

    网络计算器

    例如, 我们需要实现一个服务器版的加法器. 我们需要客户端把要计算的两个加数发过去, 然后由服务器进行计算, 最后再把结果返回给客户端.

    约定方案

    客户端发送一个形如1+1的结构体; 这个字符串中有两个操作数, 都是整形; 两个数字之间会有一个字符是运算符, 运算符只能是 + ;

    数字和运算符之间没有空格; 定义结构体来表示我们需要交互的信息; 发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体…

    订制协议

    目前就是订制数据化结构的过程

    1. 原生版本

    没有明显的序列化/反序列化的过程的版本

    Protocol.hpp
    #pragma once
    
    #include
    #include
    
    using namespace std;
    
    //订制协议
    
    //请求格式
    typedef struct request
    {
        int x;
        int y;
        char op;  //"+-*/%"
    }request_t;
    
    //响应格式
    typedef struct response
    {
        int code;   //server运算完毕的计算状态:code(0:success) code(-1:div 0)...
        int result; //计算结果,你能否区分是正常的计算结果 还是 异常的退出结果
    }response_t;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    Sock.hpp
    //对套接字接口做简单封装
    #pragma once
    
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    
    using namespace std;
    
    class Sock 
    {
    public:
        //创建套接字
        static int Socket()
        {
            int sock = socket(AF_INET, SOCK_STREAM, 0);
            if(sock < 0)
            {
                cerr << "socket error" << endl;
                exit(2);
            }
            return sock;
        }
    
        //绑定
        static void Bind(int sock, uint16_t port)
        {
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(port);
            local.sin_addr.s_addr = INADDR_ANY;
    
            if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
            {
                cerr << "bind error" << endl;
                exit(3);
            }
        }
    
        //监听
        static void Listen(int sock)
        {
            if(listen(sock, 5) < 0)
            {
                cerr << "listen error" << endl;
                exit(4);
            }
        }
    
        static int Accept(int sock)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int fd = accept(sock, (struct sockaddr*)&peer, &len);
            if(fd >= 0)
            {
                return fd;
            }
            else
            {
                return -1;
            }
        }
    
        static void Connect(int sock, string ip, uint16_t port)
        {
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_addr.s_addr = inet_addr(ip.c_str());
            server.sin_port = htons(port);
    
            if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
            {
                cout << "Connect Success!" << endl;
            }
            else
            {
                cout << "Connect Failed!" << endl;
                exit(5);
            }
        }    
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    CalServer.cc
    #include"Protocol.hpp"
    #include"Sock.hpp"
    #include
    
    // ./CalServer port
    static void Usage(string proc)
    {
        cout << "Usage: " << proc << " port" << endl;
        exit(1);
    }
    
    void* HandlerRequest(void* args)
    {
        int sock = *(int*)args;
        delete (int*)args;
    
        pthread_detach(pthread_self());
    
        //业务逻辑 - 做一个短服务
        // request → 分析处理 → 构建response → send(response) → close(sock)
        //1.读取请求
        request_t req;
        ssize_t s = read(sock, &req, sizeof(req));
        if(s == sizeof(req))
        {
            //读取到了完整的请求
            // req.x, req.y, req.op
            //2.分析请求 3.计算结果
            //4.构建响应,并进行返回
            response_t resp = {0, 0};
            switch (req.op)
            {
                case '+':
                    resp.result = req.x + req.y;
                    break;
    
                case '-':
                    resp.result = req.x - req.y;
                    break;
    
                case '*':
                    resp.result = req.x * req.y;
                    break;
    
                case '/':
                    if(req.y == 0)
                        resp.code = -1; //代表除0
                    else 
                        resp.result = req.x / req.y;
                    break;
    
                case '%':
                    if(req.y == 0)
                        resp.code = -2; //代表模0
                    else 
                        resp.result = req.x % req.y;
                    break;
    
                default:
                    resp.code = -3; //代表请求方法异常
                    break;  
            }
            cout << req.x << req.op << req.y << endl;
            write(sock, &resp, sizeof(resp));
            cout << "服务结束" << endl;
        }
        //5.关闭连接
        close(sock);
    }
    
    int main(int argc, char* argv[])
    {
        if(argc != 2)
        {
            Usage(argv[0]);
        }
        uint16_t port = atoi(argv[1]);
    
        int listen_sock = Sock::Socket();
        Sock::Bind(listen_sock, port);
        Sock::Listen(listen_sock);
    
        for(;;)
        {
            int sock = Sock::Accept(listen_sock);
            if(sock >= 0)
            {
                cout << "get a new client..." << endl;
                int* pram = new int(sock);
                pthread_t tid;
                pthread_create(&tid, nullptr, HandlerRequest, pram);
            }
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    CalClient.cc
    #include "Protocol.hpp"
    #include "Sock.hpp"
    
    void Usage(string proc)
    {
        cout << "Usage: " << proc << " server_ip server_port" << endl;
    }
    
    // ./CalClient server_ip server_port
    int main(int argc, char *argv[])
    {
        if (argc != 3)
        {
            Usage(argv[0]);
            exit(1);
        }
    
        int sock = Sock::Socket();
        Sock::Connect(sock, argv[1], atoi(argv[2]));
    
        //业务逻辑
        request_t req;
        memset(&req, 0, sizeof(req));
        cout << "Please Enter Data One# ";
        cin >> req.x;
        cout << "Please Enter Data Two# ";
        cin >> req.y;
        cout << "Please Enter operator# ";
        cin >> req.op;
    
        ssize_t s = write(sock, &req, sizeof(req));
    
        response_t resp;
        s = read(sock, &resp, sizeof(resp));
        if (s == sizeof(resp))
        {
            cout << "code[0:success]: " << resp.code << endl;
            cout << "result: " << resp.result << endl;
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    Makefile
    .PHONY:all
    all:CalClient CalServer
    
    CalClient:CalClient.cc 
    	g++ -o $@ $^ -std=c++11
    
    CalServer:CalServer.cc 
    	g++ -o $@ $^ -std=c++11 -lpthread
    
    .PHONY:clean
    clean:
    	rm -rf CalClient CalServer 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    我们自己定的协议,client 和 server都必须遵守,这就叫做自定义协议 ——

    image-20220831213408779

    但这个方案很cuo

    2. 序列化反序列化版本

    理解json.cpp

    安装,本质上是在/usr/include/jsoncpp/json中添加头文件,在/usr/lib64/下添加libjson库 ——

    sudo yum install -y jsoncpp-devel 
    
    • 1

    编译时,需要带-ljsoncpp~

    仅仅是了解,我们目的是解释序列化和反序列化。

    Protocal.hpp
    #pragma once
    
    #include
    #include
    #include
    
    using namespace std;
    
    //订制协议
    
    //请求格式
    typedef struct request
    {
        int x;
        int y;
        char op;  //"+-*/%"
    }request_t;
    
    //响应格式
    typedef struct response
    {
        int code;   //server运算完毕的计算状态:code(0:success) code(-1:div 0)...
        int result; //计算结果,你能否区分是正常的计算结果 还是 异常的退出结果
    }response_t;
    
    //序列化 - 结构化数据→字符串
    std::string SerializeRequest(const request_t& req)
    {
        Json::Value root; //万金油对象,可以橙装任何对象,json是一种kv式的序列化方案
        // 盛装
        root["datax"] = req.x;
        root["datay"] = req.y;
        root["operator"] = req.op;
    
        //FastWriter, StyledWriter
        // Json::StyledWriter writer;
        Json::FastWriter writer;
    
        //返回值为string,即序列化后的结果
        return writer.write(root); 
    
    }
    
    //反序列化 - 字符串→结构化数据
    void DeserializeRequest(const std::string& json_string, request_t& out)
    {
        Json::Reader reader;
        Json::Value root;
    
        reader.parse(json_string, root);
        out.x = root["datax"].asInt();
        out.y = root["datay"].asInt();
        out.op = (char)root["operator"].asUInt();
    }
    
    //序列化 
    std::string SerializeReponse(const response_t& resp)
    {
        Json::Value root; //万金油对象,可以橙装任何对象,json是一种kv式的序列化方案
        // 盛装
        root["code"] = resp.code;
        root["result"] = resp.result;
    
        //FastWriter, StyledWriter
        // Json::StyledWriter writer;
        Json::FastWriter writer;
    
        //返回值为string,即序列化后的结果
        return writer.write(root); 
    }
    
    //反序列化
    void DeserializeReponse(const std::string& json_string, response_t& out)
    {
        Json::Reader reader;
        Json::Value root;
    
        reader.parse(json_string, root);
        out.code = root["code"].asInt();
        out.result = root["result"].asInt();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    CalServer.cc
    #include "Protocol.hpp"
    #include "Sock.hpp"
    #include 
    
    // ./CalServer port
    static void Usage(string proc)
    {
        cout << "Usage: " << proc << " port" << endl;
        exit(1);
    }
    
    void *HandlerRequest(void *args)
    {
        int sock = *(int *)args;
        delete (int *)args;
    
        pthread_detach(pthread_self());
    
        //业务逻辑 - 做一个短服务
        // request → 分析处理 → 构建response → send(response) → close(sock)
        // 1.读取请求
        char buffer[1024];
        request_t req;
        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "get a new request:" << buffer << endl;
            std::string str = buffer;
            DeserializeRequest(str, req); //反序列化请求
        }
    
        response_t resp = {0, 0};
        switch (req.op)
        {
        case '+':
            resp.result = req.x + req.y;
            break;
    
        case '-':
            resp.result = req.x - req.y;
            break;
    
        case '*':
            resp.result = req.x * req.y;
            break;
    
        case '/':
            if (req.y == 0)
                resp.code = -1; //代表除0
            else
                resp.result = req.x / req.y;
            break;
    
        case '%':
            if (req.y == 0)
                resp.code = -2; //代表模0
            else
                resp.result = req.x % req.y;
            break;
    
        default:
            resp.code = -3; //代表请求方法异常
            break;
        }
        cout << req.x << req.op << req.y << endl;
    
        //序列化响应
        std::string send_string = SerializeReponse(resp); //序列化之后的字符串
        write(sock, send_string.c_str(), send_string.size());
        cout << "服务结束" << send_string << endl;
    
        // 5.关闭连接
        close(sock);
    }
    
    int main(int argc, char *argv[])
    {
        if (argc != 2)
        {
            Usage(argv[0]);
        }
        uint16_t port = atoi(argv[1]);
    
        int listen_sock = Sock::Socket();
        Sock::Bind(listen_sock, port);
        Sock::Listen(listen_sock);
    
        for (;;)
        {
            int sock = Sock::Accept(listen_sock);
            if (sock >= 0)
            {
                cout << "get a new client..." << endl;
                int *pram = new int(sock);
                pthread_t tid;
                pthread_create(&tid, nullptr, HandlerRequest, pram);
            }
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    CalClient.cc
    #include "Protocol.hpp"
    #include "Sock.hpp"
    
    void Usage(string proc)
    {
        cout << "Usage: " << proc << " server_ip server_port" << endl;
    }
    
    // ./CalClient server_ip server_port
    int main(int argc, char *argv[])
    {
        if (argc != 3)
        {
            Usage(argv[0]);
            exit(1);
        }
    
        int sock = Sock::Socket();
        Sock::Connect(sock, argv[1], atoi(argv[2]));
    
        //业务逻辑
        request_t req;
        memset(&req, 0, sizeof(req));
        cout << "Please Enter Data One# ";
        cin >> req.x;
        cout << "Please Enter Data Two# ";
        cin >> req.y;
        cout << "Please Enter operator# ";
        cin >> req.op;
    
        // 序列化请求
        std::string json_string = SerializeRequest(req);
        ssize_t s = write(sock, json_string.c_str(), json_string.size());
        // ssize_t s = write(sock, &req, sizeof(req));
    
        char buffer[1024];
        s = read(sock, buffer,sizeof(buffer)-1);
        if(s > 0)
        {
            response_t resp;
            buffer[s] = 0;
            std::string str = buffer;
            //反序列化响应 至resp
            DeserializeReponse(str, resp);
            cout << "code[0:success]: " << resp.code << endl;
            cout << "result: " << resp.result << endl;
        }
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    Makefile
    .PHONY:all
    all:CalClient CalServer
    
    CalClient:CalClient.cc 
    	g++ -o $@ $^ -std=c++11 -ljsoncpp
    
    CalServer:CalServer.cc 
    	g++ -o $@ $^ -std=c++11 -lpthread -ljsoncpp
    
    .PHONY:clean
    clean:
    	rm -rf CalClient CalServer 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    小总结

    如上重点在于演示在网络中是互发字符串 ——

    image-20220903203245736

    我们所写的cs模式的在线版本服务器,本质就是一个应用层网络服务 —— 基本通信代码;序列和反序列化(借助组件);业务逻辑是我们自己定的、请求结果格式 & code含义等约定是我们自己做的。

    我们干的这些事儿,就完美对应了五层协议中的应用层 ——

    image-20220903204542363

    2. http

    虽然说, 上面的应用层协议是我们自己定的. 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议)就是其中之一. 但本质上和我们刚刚写的网络计算器没有区别,都是应用层协议

    • 网络通信
    • 序列化和反序列化
    • 协议细节

    在http内部都已实现。

    2.1 认识url - 网址

    我们请求的图片、html、css、jx、视频、音频、标签、文档等这些都称之为“资源”

    我们可以用IP+Port确认一个进程,但是无法唯一确认一个资源,公网IP地址是唯一一台主机的,而网络“资源”是存在于网络中的一台Linux机器上。Linux或者传统的操作系统,都是以文件的方式保存资源的。单Linux系统,表示一个唯一资源的方式是通过路径的。

    所以,IP + Linux路径,就可以唯一的确认一个网络资源

    • IP通常是以域名方式呈现的

    • 路径可以通过目录名+/确认

    image-20220903212124794

    urlencode和urldecode

    像 / ? : 等这样的字符, 已经被url当做特殊意义理解了,因此这些字符不能随意出现.

    如果某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义 —— 转义的规则如下

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

    encode编码:

    image-20220903213000604

    还需要decode解码。

    2.2 http格式

    简化认识:

    • 无论请求还是响应,http都是按照为单位(\n)构建请求或者响应的。

    • 无论是请求还是响应,几乎都是由3或4部分组成

    image-20220904100339231

    如何理解普通用户的上网行为 —— IO

    • 从目标拿到你要的资源
    • 向目标服务器上传你的数据

    思考 —— http request 和 http response被如何看待~

    我们可以把请求/响应看做一个大的字符串

    • http如何解包?如何封装?如何分用?

      • 解包/封装:用空行\n这个特殊字符,将请求和协议一分为二
      • 分用:不是http解决的,是用具体的应用代码解决的,http需要有接口来帮助上层获取参数 //TODO 等待详谈
    • http请求/响应,是如何被发送的?是如何被读取的?

    我们接下来的demo中用recv来读,这种读法是不正确的,只不过现在没有被暴露出来罢了。。

    我们需要保证 ——

    • 每次读取都是读取完整的一个http request
      • 如何判定我们将报头部分读完了呢?读到空行,分离报头和有效载荷(解包)。就能提取报头中的各种属性,包括**Content-Length**,自描述字段。
      • 决定后面还有没有正文?这和请求方法有关(我们接下来会验证);如果有正文,如何保证把正文全部读取完成呢? 上面那个字段表明正文部分有多少字节,帮助我们完整的读取http请求/响应。
      • 会有不存在Content-Length的情况,就是没有正文
      • 如上所说,是规定、协议!
    • 每次读取都不要将下一个http请求的一部分读到(后面详谈)

    HTTP请求

    • 首行:[方法] + [url] + [版本]

    • Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束

    • Body:空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度;

    HTTP响应

    • 首行: [版本号] + [状态码] + [状态码描述]

    • Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束

    • Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中.

    2.3 http的操作

    • 我想看看请求报头
    • 我想发送一个响应

    为了方便测试,我们引入这两个为tcp订制的接口,面向字节流读取的函数 ——

    #include 
    #include 
    
    ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    
    ssize_t send(int sockfd, const void *buf, size_t len, int flags);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    对比read/write函数,它们几乎没有差别,我们也不关心flag默认为0

    #include 
    
    ssize_t read(int fd, void *buf, size_t count);
    
    ssize_t write(int fd, const void *buf, size_t count);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    http.cc

    #include"Sock.hpp"
    #include
    
    void Usage(std::string proc)
    {
        std::cout << "Usage: " << proc << "port" << std::endl; 
    }
    
    void* HandlerHttpRequest(void* args)
    {
        int sock = *(int*)args;
        delete (int*)args;
        pthread_detach(pthread_self());
        
    #define SIZE 1024*10
        char buffer[SIZE]; //一个大字符串
        memset(buffer, 0, sizeof(buffer));
    
        //接收请求响应
        ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << buffer; //查看http的请求格式 - for test
    
            std::string http_response = "http/1.0 200 OK\n";//状态行
            http_response +="Content-Type: text/plain\n"; //有效载荷的类型 - 正文是普通的文本
            http_response += "\n"; //传说中的空行,用来区分报头和有效载荷
            http_response += "hello beatles";
            send(sock, http_response.c_str(), http_response.size(), 0); 
        }
        close(sock);
        return nullptr;
    }
    
    int main(int argc, char* argv[])
    {
        if(argc != 2)
        {
            Usage(argv[0]);
        }
    
        uint16_t port = atoi(argv[1]);
        int listen_sock = Sock::Socket();
        Sock::Bind(listen_sock, port);
        Sock::Listen(listen_sock);
    
        for(;;)
        {
            int sock = Sock::Accept(listen_sock);
            if(sock > 0)
            {
                std::cout << "a new link" << std::endl;
                pthread_t tid;
                int* parm = new int(sock);
                pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
            }
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    这样就实实在在的看到了请求报头 ——

    image-20220906164927622 image-20220906165016139

    2.4 http的方法

    早期http版本是短链接,一个请求,一次响应,close socket,一个请求一般就是请求一个资源,链接自动关闭。

    image-20220906194601674

    大部分服务器为了安全只会暴露 GET,POST,HEAD方法

    http请求中的/不是根目录,而叫做web根目录

    image-20220906202628869

    准备工作 ——

    http.cc

    • Content-Type类型:正文类型

    • Content-Length:获取文件大小,我们今天用stat获取文件特定属性 - 大小

      #include 
      #include 
      #include 
      
      int stat(const char *path, struct stat *buf);
      
      //buf -输出型参数
      struct stat {
             dev_t     st_dev;     /* ID of device containing file */
             ino_t     st_ino;     /* inode number */
             mode_t    st_mode;    /* protection */
             nlink_t   st_nlink;   /* number of hard links */
             uid_t     st_uid;     /* user ID of owner */
             gid_t     st_gid;     /* group ID of owner */
             dev_t     st_rdev;    /* device ID (if special file) */
             off_t     st_size;    /* total size, in bytes */
             blksize_t st_blksize; /* blocksize for file system I/O */
             blkcnt_t  st_blocks;  /* number of 512B blocks allocated */
             time_t    st_atime;   /* time of last access */
             time_t    st_mtime;   /* time of last modification */
             time_t    st_ctime;   /* time of last status change */
         };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22

    http.cc

    #include"Sock.hpp"
    #include
    #include 
    #include 
    #include 
    #include
    
    #define WWWPORT "./wwwroot/"
    #define HOME_PAGE "index.html"
    
    void Usage(std::string proc)
    {
        std::cout << "Usage: " << proc << "port" << std::endl; 
    }
    
    void* HandlerHttpRequest(void* args)
    {
        int sock = *(int*)args;
        delete (int*)args;
        pthread_detach(pthread_self());
        
    #define SIZE 1024*10
        char buffer[SIZE]; //一个大字符串
        memset(buffer, 0, sizeof(buffer));
    
        //接收请求响应
        ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << buffer; //查看http的请求格式 - for test
    
            //响应 - 返回首页
            std::string html_file = WWWPORT;
            html_file += HOME_PAGE;
            struct stat st;
            stat(html_file.c_str(), &st);
    
            //返回时,不仅要返回正文的网页信息,还要包括http请求
            //1.状态行
            std::string http_response = "http/1.0 200 OK\n";
            //2.响应报头
            http_response +="Content-Type: text/html; charset=utf8\n"; //正文(有效载荷)的类型 - 
            http_response += "Content-Length: ";
            http_response += std::to_string(st.st_size);
            http_response += "\n";
            //3.空行
            http_response += "\n";
            // 接下来才是正文
            std::ifstream in(html_file);
            if(!in.is_open())
            {
                std::cerr << "open html error" << std::endl;
            }
            else
            {
                std::string content;
                std::string line;
                while(std::getline(in, line))
                {
                    content += line;
                }
                http_response += content;
                in.close();
                send(sock, http_response.c_str(), http_response.size(), 0); 
            }
    
        }
        close(sock);
        return nullptr;
    }
    
    int main(int argc, char* argv[])
    {
        if(argc != 2)
        {
            Usage(argv[0]);
        }
    
        uint16_t port = atoi(argv[1]);
        int listen_sock = Sock::Socket();
        Sock::Bind(listen_sock, port);
        Sock::Listen(listen_sock);
    
        for(;;)
        {
            int sock = Sock::Accept(listen_sock);
            if(sock > 0)
            {
                pthread_t tid;
                int* parm = new int(sock);
                pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
            }
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    image-20220906214930105

    Makefile 同上。如上只是把我们简陋的字符串版本改成了文件版。

    wwwroot/index.html

    • wwwroot称为web根目录,wwwroot下放置的内容都叫做资源
    • wwwroot目录下的index.html就叫做网站的首页
    <!DOCTYPE html>
    
    <html>
        <head>
            <meta charset="utf-8">
        </head>
        <body>
            <h3>小边's Home</h3>
        </body>
    
    
    </html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    接下来,就要验证GET和POST方法啦~

    2.4.1 GET

    HTML 教程 (w3school.com.cn)

    index.html

    <!DOCTYPE html>
    
    <html>
        <head>
            <meta charset="utf-8">
        </head>
        <body>
            <h5>long time no see!! I am missing you~</h5>
            <h5>我是表单!您是谁</h5>
            <!-- /a/b/handler_form 这个路径并不存在,我们目前也不处理,因为我们今天的重点在GET -->
            <form action="/a/b/handler_form" method="GET">
                姓名: <input type="text" name="name">
                密码: <input type="password" name="password">
                <input type="submit" value="提交">
            </form>
        </body>
    </html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    会把表单内容拼接到url后面,这样前端的数据就被后端C++程序拿到了 ——

    image-20220907191024541

    2.4.2 POST

    index.html

    只需将表单中的GET方法改为POST方法 ——

    <!DOCTYPE html>
    
    <html>
        <head>
            <meta charset="utf-8">
        </head>
        <body>
            <h5>long time no see!! I missed you~</h5>
            <h5>我是表单!您是谁</h5>
            <!-- /a/b/handler_form 这个路径并不存在,我们目前也不处理,因为我们今天的重点在GET -->
            <form action="/a/b/handler_form" method="POST">
                姓名: <input type="text" name="name">
                密码: <input type="password" name="password">
                <input type="submit" value="提交">
            </form>
        </body>
    </html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    image-20220907191756843

    2.4.3 小总结

    概念

    • GET:获取方法,一般所有的网页默认的都是GET方法,但是GET也能提交参数 - 通过url参数拼接从而提交给server端。
    • POST:推送方法,以比较常用的参数提交方法,但是一般是通过正文部分提交的,Content-Length表示参数的长度。

    区别

    参数提交的位置不同

    • GET:GET方法不私密,会回显到浏览器url输入框,增加了被盗取的风险。GET通过url传参,而url是有大小限制的,这和具体的浏览器有关。
    • POST:POST方法比较私密(!=安全),因为不会回显到浏览器url输入框。POST由正文部分传参,一般大小没有限制。

    如何选择

    GET:提交的参数不敏感,数量非常少;否则用POST

    http协议处理,本质是文本分析 ——

    • http协议本身的字段
    • 提取参数,如果有的话。GET和POST其实是前后端交互的一个重要方式。

    2.5 http的状态码

    应用层是人要参与的,这里的人“水平”参差不齐,很多人根本就不清楚如何使用http的状态码,又因为浏览器种类太多,导致大家对状态码的支持比较混乱。这样类似于404的状态码,对浏览器没什么指导意义,它就是“正常”显示网页。

    image-20220907195929467

    1XX:100, 101表示请求正在被处理,服务器收到你的请求,但是处理你的请求需要很多时间,给你返回一个响应
    2XX:200请求正常处理完毕 - OK,我们的刚刚自己构建的就是200
    3XX:重定向。有301, 302, 303, 307, 308不同的浏览器,不同的版本对于这种重定向的处理机制是不一样的,我们接下来会测试
    4XX:403 (Forbidden) 禁止访问;404 (Not Found)你访问的资源不存在,属于客户端问题.
    5XX:服务器错误。比如来了一个请求,创建线程或者进程失败;处理请求时,做字符串分析时出现问题,程序崩溃。比如,500, 503,504(Bad Gateway)

    #include "Sock.hpp"
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define WWWPORT "./wwwroot/"
    #define HOME_PAGE "index.html-bak"
    
    void Usage(std::string proc)
    {
        std::cout << "Usage: " << proc << "port" << std::endl;
    }
    
    void *HandlerHttpRequest(void *args)
    {
        int sock = *(int *)args;
        delete (int *)args;
        pthread_detach(pthread_self());
    
    #define SIZE 1024 * 10
        char buffer[SIZE]; //一个大字符串
        memset(buffer, 0, sizeof(buffer));
    
        //接收请求响应
        ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << buffer; //查看http的请求格式 - for test
    
            //响应 - 返回首页
            std::string html_file = WWWPORT;
            html_file += HOME_PAGE;
    
            // 接下来才是正文
            std::ifstream in(html_file);
            if (!in.is_open())
            {
                std::cerr << "open html error" << std::endl;
                // 1.状态行
                std::string http_response = "http/1.0 404 not Found\n";
                // 2.响应报头
                http_response += "Content-Type: text/html; charset=utf8\n"; //正文(有效载荷)的类型
                // 3.空行
                http_response += "\n";
    
                http_response += "

    你访问的资源走丢了~~

    "
    ; send(sock, http_response.c_str(), http_response.size(), 0); } else { struct stat st; stat(html_file.c_str(), &st); //返回时,不仅要返回正文的网页信息,还要包括http请求 // 1.状态行 std::string http_response = "http/1.0 200 ok\n"; // 2.响应报头 http_response += "Content-Type: text/html; charset=utf8\n"; //正文(有效载荷)的类型 - http_response += "Content-Length: "; http_response += std::to_string(st.st_size); http_response += "\n"; // 3.空行 http_response += "\n"; std::string content; std::string line; while (std::getline(in, line)) { content += line; } http_response += content; in.close(); send(sock, http_response.c_str(), http_response.size(), 0); } } close(sock); return nullptr; } int main(int argc, char *argv[]) { if (argc != 2) { Usage(argv[0]); } uint16_t port = atoi(argv[1]); int listen_sock = Sock::Socket(); Sock::Bind(listen_sock, port); Sock::Listen(listen_sock); for (;;) { int sock = Sock::Accept(listen_sock); if (sock > 0) { pthread_t tid; int *parm = new int(sock); pthread_create(&tid, nullptr, HandlerHttpRequest, parm); } } return 0; }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    image-20220907203717353

    我们重点来谈3xx的状态码 —— 重定向

    • 永久重定向:301、
    • 临时重定向:302、307

    有时,我们访问某一个网站,可能会跳转到另一个网址:比如当我访问某种资源时,提示我登陆,于是我便跳转到了登录页面;当我输完密码,会自动再跳转回来。这种现象,都叫做重定向。

    所谓永久和临时,永久重定向通常用于网站搬迁、域名更换;临时重定向,每次都要经历跳转,属于业务环节。

    模拟301

    注意,重定向时需要浏览器给我们提供支持的:必须能识别301/302/307,server要告诉浏览器(客户端),接下来应该去哪里?

    报头属性Location:新的地址

    http.cc

    #include "Sock.hpp"
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define WWWPORT "./wwwroot/"
    #define HOME_PAGE "index.html"
    
    void Usage(std::string proc)
    {
        std::cout << "Usage: " << proc << "port" << std::endl;
    }
    
    void *HandlerHttpRequest(void *args)
    {
        int sock = *(int *)args;
        delete (int *)args;
        pthread_detach(pthread_self());
    
    #define SIZE 1024 * 10
        char buffer[SIZE]; //一个大字符串
        memset(buffer, 0, sizeof(buffer));
    
        //接收请求响应
        ssize_t s = recv(sock, buffer, sizeof(buffer), 0);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << buffer; //查看http的请求格式 
            std::string response = "http/1.1 301 Permantly moved\n";
            response += "Location: https://new.qq.com/\n"; 
            response += "\n";
            send(sock, response.c_str(), response.size(), 0);
        }
        close(sock);
        return nullptr;
    }
    
    int main(int argc, char *argv[])
    {
        if (argc != 2)
        {
            Usage(argv[0]);
        }
    
        uint16_t port = atoi(argv[1]);
        int listen_sock = Sock::Socket();
        Sock::Bind(listen_sock, port);
        Sock::Listen(listen_sock);
    
        for (;;)
        {
            int sock = Sock::Accept(listen_sock);
            if (sock > 0)
            {
                pthread_t tid;
                int *parm = new int(sock);
                pthread_create(&tid, nullptr, HandlerHttpRequest, parm);
            }
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    于是我们自动跳转到了腾讯网 ——

    image-20220907214432417

    2.6 http的常见header

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

    我们之前所有的实验,全部都是请求→响应→断开连接 ——
    http/1.0采用的网络请求方案是短链接,当我们访问一个由多个元素构成的一个大型的网页资源时,就要发起多次http请求(基于短链接),http协议是基于tcp协议的,所以每一次的http request都要执行 建立链接→ 传送数据→断开连接,但是这样效率较低。

    这样http/1.1后支持长链接 ,需要设置Connection属性:keep-alive,通过减少频繁建立tcp链接,来达到提高效率的目的。

    2.7 cookie 和 session

    我们有这样的经验,我们网站中,各种页面跳转时,本质就是进行各种http请求,网站照样认识我。但是http协议本身是一种无状态的协议(因为简单),并不记录请求的历史信息。这样看似是矛盾的,但是“让网站认识我”并不是http协议本身要解决的问题,而是做网络资源获取,但是http可以提供一些技术支持来保证网站具有“会话保持”的功能 —— cookie & session。

    cookie

    会话保持

    • 浏览器角度:cookie其实是一个文件,该文件保存的是我们用户的私密信息
    • http协议角度:一旦该网站对应有cookie,在发起任何请求的时候,都会自动在request中携带该cookie信息

    这个cookie就是在浏览器(客户端)中,首次登陆时浏览器会自动保存登录相关信息到cookie文件,后续的请求,浏览器会将每一个请求都会请求报头属性中,自动携带对应的cookie。

    cookie有两种形式 ——

    • 文件版
    • 内存版

    如果别人盗取了我的cookie文件,他就可以以我的身份进行认证,访问特定资源如果保存的是用户名和密码,那就很糟糕了~~

    所以单纯使用cookie是有安全隐患的 —— session,但是不代表我们不用cookie~

    session

    将用户的私密信息,保存在服务端。

    image-20220908203119158

    后序所有的http请求,都会由浏览器自动携带cookie内容 —— 当前用户的session_id,server依旧可以做到认识client,这也是一种会话保持的功能。

    由于客户端的cookie文件中,不再直接保存用户私密信息,就不会直接泄漏啦~ 但是的确还有cookie文件被泄漏的可能,如果别人也去访问我们对应的网址,还是会去访问我们对应的网址,但我们也没办法啦~ 但是也衍生了很多的防御方案 —— 比如异地登录重新生成session_id、短信认证等等

    cookie+session本质是提高用户访问网站或者平台的体验。

    3. https (了解)

    3.1 “加密”

    http = http +TLS/SSL (数据的加密解密层)

    image-20220909204632170

    加密方式

    • 对称加密 - 秘钥(只有一个)

      用X加密,也要用X解密

    • 非对称加密

      有一对秘钥:公钥和私钥。可以用公钥加密,但是只能用私钥解密;或者用私钥加密,只能用公钥解密。例如RSA。

      一般而言,公钥是群世界众所周知的,私钥是必须自己进行私有保存的。

    如何识别/防止文本中的内容被篡改?

    回忆起了上学期一门写字儿就给分儿的课 —— 信息安全概论,第几章来着,密码学。。幸好逃离了。。言晨而应该懂这个,不过好像挺难?!

    image-20220909213034077

    秘钥协商,采用对称的方式是有安全隐患的!

    3.2 https通信过程

    如何选择加密算法?

    • 对称
    • 非对称
    image-20220910090222954

    看起来有两对非对称秘钥,就能保证数据双向的安全,但事实并非如此 ——

    • 依旧有被非法窃取的风险
    • 非对称较密算法,特别费时间

    实际上,采取的是非对称 + 对称方案 ——

    • 用非对称的方式交换对称秘钥

    • 用对称方案进行数据通信

    image-20220910094250383

    事实上,第一次把公钥S给client会不会出现问题呢?

    image-20220910105411890

    这样以来,中间人就拿到了就拿到了接下来用于通信的对称秘钥的私钥X. 然而client无法判断秘钥协商报文是不是从合法的服务方发来的。

    那怎么办呢?于是有CA证书机构(Certificate Authority) —— 只有一个服务商经过权威机构认证,该机构才合法。

    • 申请证书:提供企业信息;域名;公钥
    • 创建证书:企业基本信息(域名,公钥);由这段文本(hash散列形成数据指纹)形成的数据签名
      • 权威
      • 有自己的公钥A 和 私钥A`
    image-20220910112646568
    • 要求client必须知道CA机构的公钥信息用来解密数字签名。那它如何得知呢?这个一般是内置的。另外,证书颁发具有“传递性”,所以也有一部分在访问网址是,浏览器会提示用户进行安装

    因为CA的公钥是全世界众所周知的,但是CA的私钥只有CA自己知道,换言之,只有CA机构能重新形成对应的数字签名,因此即便中间人可以改内容,但,无法更改数字签名,因为他没有CA机构的私钥,用公钥解密再更改也无法再加密。

    那如果中间人也是一个合法的服务方呢。。不行~ 因为基本信息中有“域名”,你请求的域名和“合法中间人”的域名那一定是会变化而被察觉到的。。

    今天才知道证书是这么回事儿啊~~

  • 相关阅读:
    基于b/s架构搭建一个支持多路摄像头的实时处理系统(2) ---- 使用yolo v5 模型基于GPU 多进程处理视频流
    Logstash filter grok正则的使用及介绍
    Framwork入门のPiex 6P源码(下载/编译/刷机)
    JAVA学习实战(十二)分库分表学习
    CDR插件开发之Addon插件007 - Addon插件简介和案例演示
    建模示范视频EA-027智慧公寓系统试看片段-视频+图片版
    【校招VIP】前端算法考点之智力分析
    Ubuntu Server CLI专业提示
    Session的基本使用 [JavaWeb][Servlet]
    零零信安-D&D数据泄露报警日报【第41期】
  • 原文地址:https://blog.csdn.net/qq_54851255/article/details/126795252