• HTTP/HTTPS协议


    背景概念

    应用层

    我们写的各种日常的程序,都在应用层

    “协议”

    协议的本质是一种约定,我们用的socket api接口都是按”字符串“方式发送接收的,如果要传送一些”结构化的数据“怎么办?

    序列化与反序列化

    比如我们使用的聊天软件,我发送的内容可能是这样的:

    struct message
    {
    	我的昵称:xxxxx
    	我的头像:xxxxx
    	我的消息:xxxxx
    	消息时间:xxxxx
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这就是结构化的数据,那我们发的时候并不是直接把这些消息传输到网络中发出去,因为这些消息大家的大小,长度可能都是不一样的,所以我们发送的时候不方便统一发送,所以我们要把这里的**结构化信息转化成一个长的”字符串“(字节流/数据包)**然后发出去。为什么要转?假设是一个结构体,结构体有内存对齐,我对端用一个message接收,我的message的大小和你的不一定一样,因为机器可能不同,所以不能直接把信息传过去。

    如果把这四个字符串分别传过去?那就不是一个独立的结构化数据了,如果同时有很多人一起发消息,那我还要区分哪些消息是组合在一起的,成本太高。所以一般都是把它转化成长字符串(打包)。然后把长字符串传递给对方,对方用一个结构体接收,然后根据这个长字符串依次填补自己的结构体,把数据由一个字符串转化成一个结构化的数据。
    由结构化的数据转成长字符串数据的过程叫做序列化的过程。
    用分析算法把字符串里的内容一个个分析出来,填入到结构体当中,再形成一个新的结构话的数据,这个过程叫做反序列化的过程。
    上面的两个过程是网络通信中必须要做的

    为什么要序列化与反序列化
    1.结构化的数据是不便于网络传输的,但是字符串便于网路传输。为了应用层网络通信的方便。
    2.通信的两个人上层可能还有别的应用,比如图形界面显示,如果是结构话的数据,那上层应用就可以比较简单的使用数据,否则上层自己要完成对字符串的解析工作。为了方便上层进行使用内部成员,本质是将应用和网络进行了解耦。(应用不再关心网络发送)

    你要有序列化于反序列化的过程,就要有结构化的数据,有了~数据,就需要有一套对应的机制。

    所谓的结构化 的数据本质就是协议的表现。

    怎么序列化与反序列化
    可以自己写一个(造轮子)也可以用别人写好的组件(用轮子比如:xml,json,protobuff)
    下面我们使用json演示一下:

    场景:网络版本的计算器

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

    约定方案一:
    客户端发送一个形如"1+1"的字符串;
    这个字符串中有两个操作数, 都是整形;
    两个数字之间会有一个字符是运算符, 运算符只能是 + ;
    数字和运算符之间没有空格;

    非常麻烦,工作都要由我们自己做。

    约定方案二:
    定义结构体来表示我们需要交互的信息;(结构化数据)
    发送数据时将这个结构体按照一个规则转换成字符串, 接收到数据的时候再按照相同的规则把字符串转化回结构体;
    这个过程叫做 “序列化” 和 “反序列化”

    首先.定制一下基本的协议
    Protocol.hpp:

    //客户端和服务端通信的协议内容
    #pragma once
    #include 
    #include 
    using namespace std;
    
    //定制协议的过程,可以初步理解为定制结构化数据的过程
    
    //请求格式
    typedef struct request
    {
        int x;//10
        int y;//0
        char op;//支持"+-*/%"
    }request_t;//   10/0?判断
    
    //相应格式
    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

    基本套接字的封装
    Sock.hpp

    #pragma once
    #include 
    #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)
            {
                cout<<"bind error"<<endl;
                exit(3);
            }
        }
        static void Listen(int sock)
        {
            if(listen(sock,5)<0)
            {
                cerr<<"listem 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;
            return -1;
        }
        static void Connect(int sock,std::string ip,uint16_t port)
        {
            struct sockaddr_in server;
            memset(&server,0,sizeof(server));
            server.sin_family=AF_INET;
            server.sin_port=htons(port);
            server.sin_addr.s_addr=inet_addr(ip.c_str());
            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

    服务端:
    直接使用封装好的套接字进行正常的业务逻辑
    用封装好的套接字接口创建套接字,绑定,监听,然后不断的获取链接,拿到链接以后创建线程实现业务逻辑,为了实现并行,进程不等待线程,在线程内部进行分离。

    先实现一个没有明显序列化和反序列化的原生版本
    假设业务逻辑是短服务,即客户端request->分析处理->构建response->send(response)->close(sock);
    实现过程一共可以分为5步:
    1.读取请求
    2.分析请求&&3.计算结果
    4.构建响应并返回
    5.关闭链接

    以下是服务端代码

    #include "Protocol.hpp"
    #include "Sock.hpp"
    #include 
    
    static void Usage(string proc)
    {
        cout<<"Usage: "<<proc<<" port"<<endl;
        exit(1);
    }
    void* HanderRequest(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<<"requst"<<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,HanderRequest,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

    客户端:

    #include "Protocol.hpp"
    #include "Sock.hpp"
    
    void Usage(string proc)
    {
        cout << "Usage: " << proc << " server_ip server_port" << endl;
    }
    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 << "请输入x: ";
        cin >> req.x;
        cout << "请输入y: ";
        cin >> req.y;
        cout << "请输入要执行的操作: ";
        cin >> req.op;
        write(sock, &req, sizeof(req));
        response_t resp;
        ssize_t 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

    运行结果:
    在这里插入图片描述
    为什么我们和客户端都知道x,y,op,code,result知道这些都是什么意思?这就叫做约定。我们用结构化的数据结合自己所作的约定就定义了一个简单的协议

    但是客户和服务器如果直接使用这种原生结构体的方式发送二进制的内容,也只能满足部分情况,这种方式对内存对齐的方式,每个字段的大小都要求必须有相同的标准。所以这样的方案并不好,也不利于后续大型业务的处理。所以这种方式虽然目前可行,但是不建议这样做,还是要加上序列化和反序列化

    使用的是jsoncpp,这是C++使用比较多的一个组件,有两大操作方法
    云服务器安装JsonCpp的命令

    sudo yum install -y jsoncpp-devel
    
    • 1

    安装开发包

    ls /usr/include/jsoncpp/json
    
    • 1

    (安装一个库就是安装了一堆头文件)

    ls /usr/lib/lib64/libjson
    
    • 1

    (把对应的库也安装下来)

    头文件

    #include 
    
    • 1

    下面是一个序列化的测试代码:

    #include 
    #include 
    #include 
    
    typedef struct request
    {
        int x;//10
        int y;//0
        char op;//支持"+-*/%"
    }request_t;//   10/0?判断
    
    int main()
    {
        request_t req={10,20,'*'};//结构话的数据
        Json::Value root;//万金油对象,可以承装任何对象,json是一种kv式的序列化方案
        //承装到一个Value对象
        root["datax"]=req.x;
        root["datay"]=req.y;
        root["operator"]=req.op;
    
        //有两种writer方法: FastWriter,StyledWriter
        //Json::FastWriter writer;
        Json::StyledWriter writer;
        std::string json_string=writer.write(root);//完成序列化
        std::cout<<json_string.c_str()<<std::endl;
    }
    
    • 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

    在这里插入图片描述
    在序列化之前,我们先要创建一个中间对象Json::Value,它可以接收任何类型的对象,是以一个KV结构,把我们需要序列化的结构化数据填充到这个对象中,然后就可以创建Json对象进行序列化

    有Json中序列化的对象有两种,对应两种方法,分别是StyledWriter和FastWriter,下面我们验证一下两种方法的区别。

    因为这是一个第三方库,所以编译的时候要加-ljsoncpp

    可以看到我们的程序是依赖这个库的。
    在这里插入图片描述
    StylendWriter方法的序列化结果:
    在这里插入图片描述
    FastWriter方法的序列化的结果:
    在这里插入图片描述
    反序列化

    使用R"(####)"格式,可以把括号里面的内容当作原始字符串,这样里面就可以不用转义。
    使用这个方法编译需要加-std=c++11
    在这里插入图片描述
    下面是运行结果:
    在这里插入图片描述

    所以现在就可以在自定义协议文件里面分别定制request和response的序列化方法和反序列化方法。

    //Request序列化,需要结构化的数据request_t->string
    std::string SerializeRequest(const request_t &req)
    {
        Json::Value root;
        root["datax"]=req.x;
        root["datay"]=req.y;
        root["op"]=req.op;
        Json::FastWriter writer;
        return writer.write(root);
    }
    
    //Request反序列化 string->request_t
    void DeserializeRequest(std::string& json_string,request_t& out)
    {
        Json::Value root;
        Json::Reader reader;
        reader.parse(json_string,root);
        out.x=root["datax"].asInt();
        out.y=root["datay"].asInt();
        out.op=(char)root["op"].asUInt();
    }
    
    //Response序列化
    std::string SerializeResponse(const response_t &resp)
    {
        Json::Value root;
        root["code"]=resp.code;
        root["result"]=resp.result;
        Json::FastWriter writer;
        return writer.write(root);
    }
    
    //Response反序列化
    void DeserializeResponse(std::string& json_string,response_t& out)
    {
        Json::Value root;
        Json::Reader reader;
        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

    服务端直接调用封装好的序列化与反序列化接口即可:

    #include "Protocol.hpp"
    #include "Sock.hpp"
    #include 
    
    static void Usage(string proc)
    {
        cout << "Usage: " << proc << " port" << endl;
        exit(1);
    }
    void *HanderRequest(void *args)
    {
        int sock = *(int *)args;
        delete (int *)args;
        pthread_detach(pthread_self());
    
        request_t req;
        char buffer[1024];
        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
             反序列化请求 ///
            buffer[s] = 0;
            std::string str = buffer;
            cout<<"get a new request: "<<str.c_str()<<endl;
            DeserializeRequest(str, req);
    
            /// 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 << "requst" << req.x << req.op << req.y << endl;
            /// 序列化结果响应 ///
            std::string send_string=SerializeResponse(resp);
            write(sock,send_string.c_str(),send_string.size());
            cout << "服务结束" <<send_string.c_str()<< endl;
        }
        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, HanderRequest, 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

    客户端:

    #include "Protocol.hpp"
    #include "Sock.hpp"
    
    void Usage(string proc)
    {
        cout << "Usage: " << proc << " server_ip server_port" << endl;
    }
    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 << "请输入x: ";
        cin >> req.x;
        cout << "请输入y: ";
        cin >> req.y;
        cout << "请输入要执行的操作: ";
        cin >> req.op;
       
         序列化后写 ///
        std::string json_string=SerializeRequest(req);
        write(sock, json_string.c_str(), json_string.size());
    
         反序列化后读 /
        char buffer[1024];
        response_t resp;
        ssize_t s=read(sock,buffer,sizeof(buffer)-1);
        if(s>0)
        {
            buffer[s]=0;
            std::string str=buffer;
            DeserializeResponse(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

    复盘:
    在客户端,我们就输入了一些数据,然后把构建的请求序列化成字符串,然后把这个字符串发送给服务器,收到响应之后,把读取的响应字符串进行反序列化,得到对应的结果并打印
    服务端,把收到的请求先进行反序列化,然后再继续分析,分析完成后得到响应,把响应序列化之后再发给客户端。

    运行结果:
    在这里插入图片描述
    刚刚的代码,总结起来可以说一共完成了4件事:
    1.基本的通信代码
    2.用组件完成序列和反序列化
    3.自定义业务逻辑
    4.自定义请求,结果格式,code含义等约定。
    这就是一个自定义的应用层网络服务!!

    所以如何理解OSI的上三层:应用层,表示层,会话层。
    会话层:通信管理,负责建立和断开通信连接。就是第一部分,能够进行基本的网络通信。
    表示层:设备固有格式和网络标准数据格式的转换。就是序列化和反序列化
    应用层:针对特定应用的协议。就是我们定义的业务逻辑和第四部分的内容的各种约定

    这就是OSI定义的上三层,但是我们可以发现完成上面代码的过程中这三次的操作很难完全独立分隔开也很难在内核中实现,所以在TCP/IP协议实现时把他们都划分到应用层,也就是说系统不实现,程序员自己实现。
    所以才有了基本套接字,序列与反序列的各种组件,和特定应用场景的各种协议的出现。

    在网络当中肯定有很多的应用场景是经常被使用的,已经有很多大佬把这些常用的协议都写好了,其中最典型的就是http协议

    http协议

    HTTP协议本质上,定位上和我们刚刚写的网络计算器没有区别,都是应用层协议
    但是http的网络通信,其中的序列化,反序列化,协议细节等内容会在http协议内部自己实现

    认识URL

    URL就是我们俗称的网址

    什么是URL(Uniform Resource Locator): 统一资源定位符。就是通过网址去确认哪一个资源在哪一个服务器上。

    网址是定位网络资源的一种方式。我们请求的图片,html,cass,js,视频,音频,标签,文档这些都称之为“资源”。服务器后台是用Linux做的,我们可以用IP+端口号唯一的确定一个进程,但是我们无法唯一的确认一个资源!公网IP地址是唯一的确认一台主机的,而我们所谓的网络资源,都一定存在于网络中的一台linux机器上。而linux或者传统的操作系统保存资源的方式都是以文件的方式保存的。单Linux系统标识一个唯一资源的方式是通过路径进行的!所以,IP+Linux路径就可以唯一的确认一个网络资源!!(这就是URL存在意义
    而IP通常是以域名的方式呈现的,路径可以通过目录名+分隔符(/)确认。

    ping www.baidu.com//用域名查看IP地址
    
    • 1

    我们用找到的IP地址直接访问,也是可以的。
    在这里插入图片描述
    说明域名与IP地址是映射的

    而被大家广泛使用的协议名端口号关系是非常紧密的常见的比如:http:80、https:443、mysql:3306
    总之,协议名和端口号之间的关系就好比报警电话和110的关系。所以一般只要指明了协议类型,端口号是省略的

    带层次的文件路径后面可能会有一个’?',后面是他的参数,叫做资源的片段标识符。

    所以URL就是:协议+域名+资源路径(可能会带参)

    urlencode和urldecode

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

    搜索C++得到的URL

    https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd=C%2B%2B&fenlei=256&rsv_pq=c57a40c30003a6ae&rsv_t=a9edNWvIKKcUXEzI7XTceSf4pvIKXeEv0qzIgtDgICIZgThBffbT2p2kbBQ&rqlang=cn&rsv_enter=1&rsv_dl=tb&rsv_sug3=4&rsv_sug1=4&rsv_sug7=101&rsv_sug2=0&rsv_btype=i&prefixsug=%2526lt%253B%252B%252B&rsp=6&inputT=2255&rsv_sug4=3416

    可以看到C++的++被处理成了%2B%2B,说明在进行URL处理时有些特殊符号是需要被特殊处理的。比如’+’ '?'由原始的字面意义转化成16进制方案,称为encode(编码)对字符进行转码。
    16进制数转化成字面意义称为decode(解码)
    。编码由浏览器自动完成,服务器收到之后可能要自己解码。

    转码规则:把字符就是一个整数,一个字符由8个bit位,要把字符转成16进制,那就每4位(不足四位的直接处理掉)组成一个十六进制数,8个bit位一共转成2个16进制数,前面再加上%转成%xy的格式(16进制)。

    HTTP协议的格式

    HTTP的设计是本着简单做的

    1.无论是请求还是响应,基本上http都是按照行为单位(可以理解为\n),进行构建请求或者响应的。
    2.但是无论是请求还是响应,几乎都是由3或者4部分组成

    http请求:
    在这里插入图片描述

    1.首行:请求行
    格式:请求方法+url(去掉域名之后的内容)+http版本(常见的有http/1.1)+\n
    这三部分都以字符串的形式构成一个请求行,中间以空格作为分隔符分成三个区域,最后的内容以\n结束

    2.请求报头Header
    有多个请求报头,每个请求报头也是以行为单位陈列的,构建成多行内容,每一行都是由一个一个kv形式的属性构成

    3.空行
    在报头与报文之间,分隔报头与报文

    4.请求正文Body
    主要是用户提交的数据。这部分不一定有,因为有可能一个请求没有报文。

    http响应
    和请求在宏观上的构成一样
    在这里插入图片描述

    1.首行:状态行
    格式:http协议版本(http/1.1)+状态码(比如404)+状态码描述(与状态码相对的描述)+\n

    2.响应报头Header

    3.空行

    4.响应正文Body
    用户请求的资源比如:html,css,js,音乐,视频,图片等。

    http请求的整个报头和报文以空行作为分隔符,整体按行为单位,这也算是一种序列化,写入的时候一行一行写,读取的时候一行一行读,本质就是一种序列与反序列化的过程,只是没有显示的显示出来

    首行:[方法]+[URL]+[版本]
    Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\n分隔;遇到空行表示Header部分结束
    Body: 空行后面的内容都是Body. Body允许为空字符串.

    请求中会包含请求方法,请求url和http版本。
    HTTP协议的版本最典型的是1.0版,而现在我们广泛使用的是1.1版,最新的还有2.0版,这里就以1.0为主。
    1.0和1.1最大的区别是是否支持长链接
    有长链接就有短链接,什么是短链接?
    短链接:一个请求一般就请求一个资源,请求完链接自动关闭。长链接就是连接上之后不断开,请求资源都复用这个连接,访问结束才关闭链接。

    早期的1.0使用短链接是因为那时候的网络资源并不丰富,主要以文本,图片为主,其次,当时的服务器压力不大,请求的资源都比较短小,并且和网络也有关系,短链接的优势就是简单。

    无论是请求还是响应,最终都是以行呈现的。空行以前包括空行整体我们称之为http的报头,下面的部分称为http的有效载荷,响应也是一样。

    那么就可以把http的请求和响应看成一个大的字符串,文本解释的时候可以看成一行一行,所以读取和发送都是按照字符串读取和发送的,越靠近头部的地方一定是越先被发送的,越靠近尾部的地方一定是越后被发送。

    要对http报文进行解包时,就可以通过空行,找到报头结束的位置,从而拿出http报头。

    编写代码查看http请求与构建响应

    编写一个简单的http查看请求
    http底层使用的是TCP协议,所以可以使用TCP的套接字接口实现,可以使用我们上面封装好的Sock接口。
    和上面的计算器服务一样,先创建套接字,然后绑定,监听,不断获取连接,得到连接之后就创建新线程,给线程传入套接字,让现在处理请求,然后分离线程,避免串行,线程首先读取从网络中读取数据,之前我们用的是read,这里可以用一个专门用来在网络中读取数据的函数recv

    man recv
    
    • 1

    在这里插入图片描述
    与read的对比
    在这里插入图片描述
    返回值都是读到的字节数,0表示对方连接关闭,小于0表示读失败

    参数:从哪个套接字读,读到哪里,期望读多大。
    但是recv多了一个参数flags:可以让我们以阻塞非阻塞读取一些之类的东西,一般默认的设为0.
    下面是线程处理函数的代码,其他部分和之前的代码一样。

    void* HanderHttpRequest(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
        }
        close(sock);
        return nullptr;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    运行起来以后,可以通过浏览器给我们的云服务器发送请求,发送方法是用浏览器访问我们云服务器的IP+我们刚刚运行服务的端口号,然后浏览器就会给我们发送请求,而我们的服务就会把请求打印出来,(端口需要要开放)。

    下面是运行结果:
    在这里插入图片描述
    可以看到一共有三部分内容:请求行,请求报头,空行

    URL是根目录,请求方法是GET方法,http版本是1.1,
    这里的http版本是客户端版本,客户端和服务端每一个都有自己的版本。在使用时会统一使用一套服务,然后服务端会判断对方的版本,从而提供对应的功能。

    剩下的是请求报头里的各种请求属性,其含义如下:
    host:你想访问谁?
    Connection:连接类型
    Cache-Control:双方通信的缓存信息
    Upgrade-Insecure-Requests:协议升级的情况
    User-Agent:代表浏览器访问时客户端的信息
    Accept-Encoding:可接受的编码类型
    Accept-Language:可接受的语言类型

    http的属性

    有时候我们用浏览器下载东西,为什么他们可以自动给我们推荐我们系统的版本?就是因为我们的访问里的User-Agent属性包含了我们的设备信息。当我们用不同的设备访问网站时,http会根据浏览器会自动根据你的本地平台识别主机,客户端,服务端收到请求后可以识别这些信息,然后根据客户的平台去推荐你现在的入网设备适合的版本。

    除此之外,想要查看请求,还可以通过以下方法:

    可以下载一个telnet

    sudo yum install -y telnet
    
    • 1

    输入以下内容,可以手动给百度构建一个请求

    telnet www.baidu.com 80
    
    • 1

    输入

    ctrl+]
    
    • 1

    输入

    GET / HTTP/1.0
    
    • 1

    两次回车,就可以看到。

    常见的请求属性有以下几种:

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

    Content-Type属性
    是响应报头的属性之一,Content指内容 Type指类型,其用来描述正文的数据类型
    代表我的请求或者响应如果携带了正文,正文的类型就是Content_Type。可以搜索Content_Type对应表,查看每种类型是用什么字段表示。

    为了验证Content-Type,我们可以编写代码,通过上面接收的请求,构建一个简单的响应。

    功能:无论你给我发送什么,我都以文本方式返回Hello World(Content-Type的text/plain就是表示正文是普通的文本)

    可以使用用write,但是这里我们使用一个专门为TCP设计的接口

    man 2 send
    
    • 1

    在这里插入图片描述
    在这里插入图片描述
    这两个接口的参数,返回值含义一模一样,send只是多了一个flags,和recv一样,也设置为0。

    我们给客户端返回消息时,不可以直接把字符串返回去,我们要模拟http的行为,所以要发的应该是一个响应,响应就要有状态行,响应报头等等,因为这是协议!

    构建响应:

    void* HanderHttpRequest(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 World!";//正文内容
            //std::string str("Hello Woeld");
            send(sock,http_response.c_str(),http_response.size(),0);
        }
        close(sock);
        return nullptr;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    这时候我们再运行服务端代码,用浏览器给我的的服务器发送访问请求时,得到的就是一个Hello World!文本。

    在Microsoft Edge浏览器选择更多工具->开发者工具->元素 就可以看到网页。

    在这里插入图片描述

    这就是响应出来的文本内容,浏览器构建出来的文本内容,网页内容,就是我们抓到的下面那部分响应正文内容,只不过我们抓到的把\n都去掉了,为了节省网络传输效率,所以看起来很乱

    在服务端我们可以看到请求后面还会跟一部分:GET /favicon HTTP/1.1… 这里的东西我们不用管,可以理解为我们请求一个网站时的图标,或者是上面那个小小的图标那个内容,因为我们服务器上没有这个资源,所以下面那个请求我们是没有给他响应的

    结论:HTTP协议如果自己写的话,本质是我们要根据协议内容进行文本分析,得到对应的响应,把响应构建成字符串响应给别人

    Content-Length属性
    如果请求的body部分存在, 则在Header中会有一个Content-Length属性来标识Body的长度

    上面打印http请求的代码我们直接用recv读的方式是不正确的,因为无论是从网络里读还是从TCP里读,一定是TCP协议给我们的数据,而我们定义的空间大小是1024*10字节,而实际上在给我们发送请求时不一定是一个一个发的,有可能http客户端会以某种方式给我们发送多个请求,而我们在读取的时候有可能会因为缓冲区设置的问题读完一个报文以后多读了下一个报文的一部分,这时候上面的报文就多了一块数据,底下的报文就是残缺的报文,两个报文都不正确,所以有以下两点
    第一:我们要保证每次读取的都是一个完整的http请求。
    第二:保证每次读取都不要将下一个http请求的一部分读到。

    我们可以通过按行读取,判断是否读到空行,来确定报头读取完成。

    报头读完了,怎么判断有没有正文,如果有正文,这么确保能够把正文读完并且不会读到下一个报文的数据?
    有没有正文,我们是不确定的,这和请求方法有关,所以我们无法确定下面有没有报文。但是我们把报头读完了,那我们就可以提取报头中的各种属性,而报头的属性包含一个字段Content-Length:len,如果有正文,报头部分的这个属性的len表示的就是正文部分有多少字节!!我们之前使用的套接字是通过文件描述符读的,而文件描述符是一个黑盒,只有读上来了我才知道内容是什么,所以我先无脑读到空行,那我就能把http协议的报头部分全读上来,其中就包括了Content-Length,如果有正文的话,对我来讲,我就知道正文部分有多少字节,所以我们正常读的时候,一旦读取到空行,那我们就要回来分析他的属性,提取到Content-Length,然后再决定读取到多少个len字节的正文,所以Content-Length表示的就是如果报文携带有效载荷,他的有效载荷有多长,这就是报头里面描述了报文的长度,这就是自描述字段

    Content-Length的作用:
    1.能够帮助我们读取 到完整的http请求和响应
    2.让我们能够根据空行能够做到将报头和有效载荷进行分离(解包)

    Content-Length如果不存在,则表示没有正文。

    http的请求方法

    请求方法说明支持的HTTP协议版本
    GET获取资源1.0、1.1
    PSOT传输实体主体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

    GET和POST方法是最重要的两个请求,他们都可以获得网络资源GET的语义上是想获取资源,POST的语义上是想上传资源,但是实际上GET也可以上传资源,POST也可以获取资源

    这里的很多方法虽然http协议是支持的,但是使用的人不一定打开了这些方法,他们有可能会禁掉一些方法,一般暴漏出来的方法有GET、POST、HEAD。这么做主要是为了防止一些恶意用户,所以只能爆露出一些有限的方法。

    GET方法 vs POST方法

    我们向一个网站发起请求时,一般是输入该网站的域名,后面不带资源路径,但是如果带上也可以访问到该网站的主页,根据URL的结构,我们知道域名后面的内容就是资源的地址,服务器怎么看待我们请求的’/'的?
    因为/在linux中代表根目录,那这里的/是不是就代表根目录?

    其实不是的,这里的/叫做web根目录
    请求的时候如果什么都没写,那我们打印的请求GET后面就是一个/,如果请求的时候带了路径,那么那个路径就会被显示在/的位置。
    这个/叫做资源所在的目录,如果我请求的什么也没写,就是一个根目录?
    要保证我们请求的一定是一个具体的资源,网页、图片…但如果请求是/,意味着我们要请求该网站的首页!!首页一般叫index.html或index.htm。
    在这里插入图片描述
    在这里插入图片描述
    也就是说我们我们请求的内容是/,服务器不会把web根目录下的所有文件给你返回,而是返回的是首页。所以一般所有的网站,都要有默认的首页(index.html)

    可以在我们服务的目录下创建一个目录,当作当前HTTP服务的web根目录,因为需要一个首页,所以我们在该目录下创建一个index.html文件,这就是我这个http的首页信息。
    现在我要想办法把这个首页信息给你返回

    http请求就是通过我们的请求把当前web根目录下的对应网页信息返回,那我们把我们刚刚的代码进行一下修改i,不再返回我们自己构建的响应,而是返回我们刚刚写的首页。

    怎么填Content-length?

    man 2 stat
    
    • 1

    根据文件的路径,解析出文件的属性
    在这里插入图片描述
    属性有很多,我们要的只有size
    在这里插入图片描述
    只要我们能返回这一个,那么也就是说我们也能返回该目录下的别的网页
    在这里插入图片描述

    服务运行起来后,可以用发起一下请求,然后就可以看到我们刚刚编写的网页内容,再浏览器的开发者模式也可以看到其代码

    wwwroot就叫做web根目录,wwwroot目录下存放的内容就叫做资源,我们访问网站域名后面的第一个/一定代表它的服务器下的web根目录,而后面的目录就是存放的网页信息的目录。
    web理论上可以拷贝到linux的任何目录下,只要你的服务器能找到就可以。
    可以理解为如果你访问的资源是/,他就会把你的资源改成首页信息。

    我们这里是直接写死返回首页信息,实际上在写的时候,是要根据分析请求的url,提取出要访问的资源路径,然后构建出对应的响应返回

    简单的网页:

    DOCTYPE html>
    
    <html>
        <head>
            <meta charset="utf-8">
        head>
        <body>
            <h3>你好啊!h3>
        body>
    
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    搞一个输入框,输入框就是网页,上面的都叫做标签,是被浏览器解释的,浏览器会根据标签给他解释成我们想要的样子,
    w3cschool,有一些教程,可以找到html的表单,然后做一个简单的密码登录表单

    DOCTYPE html>
    
    <html>
        <head>
            <meta charset="utf-8">
        head>
        <body>
            <h3>你好啊!h3>
    
            <h5>这里是表单!h5>
            
            
            <form action="/a/b/hander_from" method="GET">
                姓名: <input type="text" name="name"><br/>
                密码: <input type="password" name="passwd"><br/>
                <input type="submit" value="登录"><br/>
            form>
        body>
    
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述
    在这里插入图片描述

    查看一下请求

    在这里插入图片描述

    使用GET方法,输入的参数是拼接在url上面的,以?作为分隔符。有name和password,两个参数之间用&分隔

    GET方法如果提交参数,是通过URL方式进行提交的。
    也就是说,GET方法服务器提交参数时,浏览器会自动把表单拼接在URL后面,而URL会包含在HTTP请求,这样后端就可以分析拿到的HTTP请求,可以进行分割等操作,这样前端的数据就被后端拿到了,然后在服务器后端做一些比如连接数据库的操作,与输入的表单的用户名和密码做对比,从而实现登录过程。

    现在我们改成POST方法
    在这里插入图片描述
    在这里插入图片描述
    因为我们是直接读的,所以正文部分就和下一个请求混在一起了,但是不影响,现在的现象就是当我们是POST请求时,它请求访问的资源还是在url中,但是参数是会在正文中。

    特点:POST方法是通过正文提交参数的。

    GET方法与POST方法请求的资源都会在url中,只是如果传参,参数的位置不同

    总结:
    GET方法叫做获取,是最常用的方法,默认一般获取所有的网页,都是GET方法**,但是如果GET要提交参数是通过URL进行参数拼接从而提交给server的
    POST方法叫做推送,与GET相比是提交参数比较常用的方法,但是如果
    提交参数,一般是通过正文部分提交的**,Content-Length:xxx表示参数的长度。

    参数提交的位置不同,导致POST方法比较私密,但是不等于安全。它只是不会把参数回显到浏览器url输入框,而get方法不私秘,回把重要信息回显到url的输入框中,增加了被盗取的风险,但是也代表POST方法就不会被盗取了,用POST方法该盗取还是盗取。所有在网络中传输的内容,只要没有经过加密,都是不安全的。

    GET是通过url传参的,而url是有大小限制的!不可能一个url有几个兆吧,所以有大小限制,而这个限制和具体的浏览器有关。而POST方法是由正文部分传参的,所以一般大小没有限制

    如何选择使用什么方法?
    GET:如果提交的参数不敏感,数量非常少,可以采用GET,否则就使用POST方法。

    而无论是GET方法吧前端数据从url提交还是POST方法在正文中,最终都会保存在服务端读取到的响应中,所有http协议处理的本质是文本分析,而所谓的文本分析,就是两方面:
    1.http协议本身的字段(报头部分)
    2.提取参数,如果有的话(前端的数据交付给了后端语言)

    GET或者POST方法,其实是前后端交互的一个重要方式

    http响应

    我们在上面构建响应的时候,第一行设置了两个参数
    “http/1.0 200 OK\n”
    状态码和状态码描述

    而http响应的四部分:状态行,响应报头,空行,正文
    状态行的第一个字段:版本
    第二个字段:状态码
    第三个字段:状态码描述
    我们知道的状态码:404,状态码描述:not found

    http常见的状态码

    应用层是要人参与的,那就会出先的问题是:http的状态码很多人不清楚状态码如何使用,又因为浏览器种类太多了,就会导致前端的各种标准一直在做妥协,并且网页,前端有关的没有一个非常强力的组织或公司说浏览器只有我来做,那最终的结果就是导致大家对可能对状态码的支持不是特别好

    类似于404的状态码,对浏览器没有任何指导意义,浏览器就是正常显示内容。那我们平时看见的not found页面不是浏览器显示的,而是服务端显示的。

    但是也有一些状态码是有显示意义的

    类别原因短语
    1XXInformational(信息类状态码)请求正在被处理
    2XXSuccess(成功状态码)请求正常处理完毕
    3XXRedirection(重定向状态码)需要进行附加操作完成请求
    4XXClient Error(客户端错误状态码)服务器无法处理请求
    5XXServer Error(服务器错误状态码)服务器处理请求出错

    1XX:表示请求正在被处理,用于处理时间比较长的情况,比较少见
    2XX:表示请求处理成功,最常见的就是200,描述就是OK
    3XX:这里比较凌乱,有301 302 303 307 308,其中不同浏览器的不同版本对重定向的处理机制还不一样。协议规定的一些方法转化比如去掉正文这些操作在有些浏览器里就根本没做。协议有了,但是你不一定遵守,这就是靠近前端回出现的问题
    4XX:客户端错误,最典型的403禁止访问,使用的不多,一般在内网 404访问资源不存在。
    404错误属于客户端问题?你去京东看抖音?谁的问题
    5XX:服务器错误,服务器因为各种原因崩了,典型的有500 503 504。

    重定向状态码

    3XX的状态码是有特殊含义的,叫做重定向,重定向分为永久重定向(301)临时重定向(302、307)

    什么叫做重定向
    当访问某一个网站时,可能回自动跳转到另一个网址。
    当我登录访问某种资源时,让我登录,就跳转到登录页面,登录完又自动跳转回来
    以上就是重定向

    什么叫永久?什么叫零时?
    网站更新了,换了一个域名,老用户不知道,那老网站就可以设置永久重定向,老的服务端不给用户提供服务,会返回一个永久重定向,浏览器会向新网址重新发起请求,然后重新得到响应给用户提供服务,浏览器会保存下这个新网站,以后用户再访问老域名时,浏览器就会直接访问新网站。
    永久性重定向通常用于网站搬迁,域名更换

    支付页面各种跳转,每个用户都要跳转,什么时候都要跳转,是为了完成某一种业务的需求。属于业务的的一种环节,这就是零时重定向

    注意:重定向是需要浏览器给我们提供支持的,前提是浏览器必须待能识别这些状态码,无论是那种重定向,server都应该告诉浏览器我应该去哪里,这就涉及到http的另一个报头属性Location。设置时在后面加上新地址就可以。

    在这里插入图片描述
    这时候访问我们自己的网站,直接就跳转到了新的网站

    connection

    代表长短连接
    我们前面 的实验,都是请求->响应->断开连接,而实际的服务器上有很多资源,一个大型的网页内部是由非常多个资源组成的,一堆图片,标题,视频构成一个网页。最原始的情况下每个资源都要发起http请求,这就是短链接的策略。在http/1.1之后,引入了长连接,客户端发起请求要设置一个字段keep-alive,长连接,这样只要在底层把连接建立好,双方进行正常的通信时复用同一个连接,就不用大量的发起连接。

    结论:一般而言,一个大网页是由多个元素组成的,
    http/1.0采用的网络请求的方案是短链接
    request->respsonse->close
    这个过程其实就是把客户访问的资源打开,一次http请求就是返回一个资源,当我们访问由多个元素构成的网页时,使用http/1.0就需要进行多次http请求。http协议是基于tcp协议的,tcp协议要通信,要建立链接,传输数据,断开链接,也就是说每一次请求都要执行这个过程,效率较低。
    客户端在服务器请求一张网页时,如果这个网页里面还有很多其他资源,当一个客户端发起请求时,请求到这个网页,然后网页被返回给它,客户端拿到网页之后发现里面包含很多第三方资源,链接等,他会不断地进行请求和响应,得到多份资源,最终构建一个完整的网页,这就是短链接。而长链就是建立好链接之后双方在进行请求和响应时都使用这一条链接,把网站中的各种资源获取到的时候该链接始终不关闭,这种技术就叫做长链接

    长链接主要解决的是每一次请求都要建立链接而导致的频繁建立链接的过程,长链接通过减少频繁建立tcp链接达到提高效率的目的!!

    双方有可能一方支持长链接一方不支持,就可以通过Connection这个选项,如果携带了keep-alive,表示支持长链接,如果没有Connection选项或者该字段是close,则表示不支持长链接

    session和cookie

    我们可以发现在我们访问一些网站比如CSDN,B站等一些需要登录的场景时,我们第一次访问的时候,可能需要我们输入账号密码登录,当我们一次访问完成,把对应的网页关掉之后,后面我再次登录的时候它可以直接自动登录,不需要我们再重新输入账号密码,直接就处于登陆状态。

    我看电影人怎么知道我不是vip然后就给我推广告,经验:在网站中,网站式认识我的。技术:我打开一部电影,它也会跳转到一个网站中,它怎么知道我是不是vip从而限定我的权限?也就是说,在网站中当我们进行各种页面跳转时,本质就是进行各种http请求,而网站依然认识我

    http协议本身是一种无状态的协议,无状态就是一个客户发起了两次http请求,对客户端和服务器来讲,它不知道曾经是否发起过请求,他也不关心以后即将发起的请求,他也不关心上一次干了什么,下一次干了什么,我只负责当前请求,也就是说http协议并不记录发起http请求的上下文信息

    那为什么我们可以感觉到网站好像认识我?
    "网站认识我"并不是http协议本身要解决的问题,http可以提供一些技术支持,保证网站具有"会话保持"的功能,网站要做到这样的功能,主要用到的技术就是cookie

    cookie:主要用来做“会话保持”,也叫会话管理——登录网站,网站记录你的个人信息,从而让网站可以根据你的权限进行各种资源访问。

    http主要是帮我们解决网络资源获取的问题,

    打开浏览器可以在URL的左边看见一个锁的图标,点开就可以看见一个Cookie,点开就可以看见网站下保存的Cookie信息,把里面的信息移除之后就可以发现“网页不认识我了”。

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

    当客户端向服务器发起一个登录请求时,服务端要通过http的请求方法得到用户的信息,然后服务端经过认证登录成功,就会给客户端返回一个http响应,浏览器内部会形成一个cookie文件,这里保存的就是用户相关的一些信息,从此以后,后续的所有请求,每一次请求的报头属性会自动携带对应的cookie(由浏览器添加),服务端就可以根据cookie的信息进行认证,然后响应。

    Set-Cookie选项:服务器向浏览器设置一个cookie,浏览器会把后面的内容写在自己的cookie文件里。可以设置多个。

    如果你把cookie移除了,再刷新网页就会发现“网页不认识我了”,而如果一直保留着cookie,浏览器每次发送请求时都会带着cookie字段,从而支持服务后端对用户的身份进行认证。

    cookie文件通常有两种保存形式,
    1.文件版:在浏览器的安装目录下或使用的某些用户级目录下的文件里,这种信息电脑关了也可以保存(可以在电脑上找到)。
    2.内存版:cookie属于内存版信息,浏览器只要一关闭,就没了。

    有时候cookie里面不仅要保存账号密码,还要保存浏览痕迹,总之就是一些私密信息,账号密码,暂且忽略http是否安全,如果我不小心点击了一些恶意网址,向我的浏览器注入了一些恶意程序,盗取了我的cookie文件,然后用我的cookie访问网址,别人就可以以我的身份进行认证访问网站了。这也就是我们的各种账号被盗的重要理论。其次,如果cookie里明文保存了我的用户名和密码,那就相当于泄露了用户的私人信息。所有,如果单独使用cookie,是具有一定的安全隐患的。这个问题现在依然避免不了,只要用cookie,这个信息就有被泄漏的风险。

    市面上现在主流的使用方式是cookie+session。什么是session?
    session的字面意思是会话,其核心思路就是将用户的私密信息保存在服务器端

    客户端向服务端发起http登录请求后,服务端首先进行认证,认证通过以后会给该用户形成session文件,这个文件会有一个文件名,然后被存放在服务端的磁盘上,这个文件里面保存的就是该用户的私密信息,之后服务端就会构建响应,响应的时候依旧会设置cookie值,但是这个cookie值里设置的就不再是用户的信息了,而是在服务端存放用户信息的session文件的文件名,这个信息我们就称之为会话ID。因为同时可能有很多人在访问服务端,每个人都要有一个对应额session文件,那这个session文件的文件名就必须具有唯一性,所有sessionID在服务器是一个具有唯一性的值,每个人的sessionID都不一样,这个可以通过算法实现。后续用户发起的所有http请求都会由浏览器自动携带cookie文件中的当前用户的会话ID,那这时候服务端对用户的验证就不需要用户名和密码了,只需要验证会话ID,就可以找到用户的私密信息,从而自动给用户认证,这也是一种会话保持的功能。

    因为客户端的cookie不再保存用户任何的私密信息,即便客户不小心泄漏了自己的cookie文件,也不会导致用户的私人信息泄漏。但是cookie文件被泄露的风险依然存在,别人如果拿到了我的cookie文件还是可以使用我的信息访问网站,并且这个问题无法杜绝,因为cookie文件是在用户的电脑上,用户的防范意识不够,就有被盗的可能。

    虽然不能完全解决,但是可以有一些衍生的防御方案,比如当你的登录地点跨地域时,有些app会提示异地登录,让用户重新登录,重新认证。这是通过IP归属做到,不同的IP属于不同的IP片区的,所有可以通过它确定位置,而用户登录的时候就可以对通过IP地址判断用户的登录地点,如果识别到用户有异常的登录行为,就会让用户重新登录,其目的就是废弃掉之前的sessionID,重新生成新的sessionID,一旦重新登录,对端的sessionID也就失效了。这只是一种方案,还有很多别的方案,比如不法分子想修改你的密码,导致账号直接被盗走,但是这种情况现在不会再发生了,因为我们有一个重要的设备诞生了,就是手机,以前账号绑定使用的是邮箱,而现在如果你想修改密码,首先会让你输入旧密码,其次如果你的登录地址可能有异常,还需要进行短信认证,即使数据层面上你的cookie丢了,但是你的手机也没丢,即便是手机和cookie都丢了,但是也不一定是同一个人拿走了你的手机和cookie,即便是手机和cookie被同一个人拿到,手机本身也有密码,所以有短信方面的认证,即使你的信息被盗取的,但是对方也无法修改你的密码。我们个人的账号被盗取,去申诉,就是让服务端重新认证,因为你是账号的拥有者,绑定的是你的手机账号,所以服务端可以通过手机对你进行二次认证,使别人盗取到的cookie信息失效,所以只要session ID是服务端去指派的,那么他的session ID的管理工作就有服务端做了,虽然客户端保留了一个session ID但是我随时都可以让这个session ID失效,别人盗取也没有意义,所以就可以有各种各样的策略。比如异地登陆,短信验证,账户的异常行为等等

    为什么网站需要认识用户?
    http无状态,那你登录以后如果网站不认识你,那你只能访问该页面,一旦点击一个新的页面,那你就待输入一次用户名和密码,手动进行一次验证。所以引入cookie+session的本质是为了提高用户访问网站或者平台的体验

    https协议

    https的层级

    http的信息在互联网中传送的时候基本上相当于数据在裸奔,别人随时随地都可以抓取你的数据。所以我们现在的所有网站基本上都是https,并且浏览器在访问网站时,也会默认的使用https。

    https=http+TLS/SSL
    TLS/SSL统称为http数据的加密解密层,其也是一个软件层,位于http的下层,但是也属于应用层。

    如果我们直接使用http,向下访问的时候不直接方法系统调用接口,而是访问安全相关的接口,再把数据发出去,因为http的相关请求和响应都要贯穿体系结构,所以请求时会要贯穿,完成了加密,响应要从这里拿,响应时完成了解密。这两层整体就称为https。因为TLS和SSL属于应用层,也就使得网络中的应用层的数据总是被加密的。下层的信息是没有被加密的,也不需要,因为我们主要保护的是用户的隐私。所以被加密的数据实际上是整个http请求和有效载荷(也有版本只对有效载荷加密)

    加密方式

    1.对称加密:密钥,只有一个,对称加密就是用一个密钥加密,也要用同一个密钥解密。就像是钥匙:
    data^x=result;
    result^x=data;
    就是使用同一个密钥进行加密和解密,就类似于异或

    2.非对称加密
    有一对密钥,分别为公钥和私钥
    可以用公钥加密,但是只能用私钥解密或者用私钥加密,只能用公钥解密,不能用公钥加密公钥解密,也不能用私钥加密私钥解密,必须是用一个加密,用另一个解密,这就是非对称加密。
    典型的非对称加密算法有常见的RSA。
    公钥私钥其实就是一套加密算法

    一般而言,公钥是全世界公开的,私钥是必须自己进行私有保存的,不能暴漏。既然公钥和私钥可以互相加密解密,那也就是说他们其实没有区别,只不过被爆露出去的那个就叫做公钥,没有公开的就叫做私钥

    数据摘要&&数据指纹&&数字签名

    我有一篇文章发到网上,如何防止文本中的内容是否被篡改,以及如何识别是否被篡改?
    因为文本的大小可大可笑,那就针对该文本做一下基本的哈希散列,有一些哈希散列的方式比如MD5,就可以形成一个固定长度且不重复的唯一字符序列,这种哈希散列的一个算法特征就是对文本做任何改变,哪怕是一个标点符号,都会形成差异非常大的哈希结果。我们把这个固定长度且唯一的字符序列称为数据摘要数据指纹,一个文本具有这样一个唯一的序列来标记这个文本。再采用特定的加密算法,再对这个数据摘要进行加密,得到加密结果,这个加密结果我们称为数字签名
    在这里插入图片描述
    所以通信端要发送一串文本,除了发送原始文本,就可以再文本的尾部加上数据签名,在网络里发送时,就把他们两个作为一个整体发送出去,当对端收到数据时,那它要确认的就是文本是否被篡改了——校验。

    校验的过程:
    首先把文本内容和数据签名分出来,对原始文本采用相同的哈希散列重新形成数据摘要,根据解密算法对分出来的数据签名进行解密,得到对应的数据摘要,然后对比两份数据摘要,如果相等,说明没有被篡改,如果不同,就有问题了。
    在这里插入图片描述

    https通信如何加密

    双方要进行通信,数据是必须要被加密的,除了加密,还要考虑双方解密,如何选择加密算法,也就是对称还是非对称。如果是对称,如果一方使用了一个密钥进行加密,那另一端如何得知该密钥进行解密?两种方案
    1.预装,给世界上所有的服务器把世界上所有的对称密钥信息都预装好,部署软件时天然就有,但是这种情况成本太高了,并且如果我没有这个密钥要下载,那还是会在网络上传输,再其次,密钥可能很多,算法也很多,即使不用协商密钥,也要协商算法,而且既然这个信息你可以预装,按别人也可以预装。所以这是一个不靠谱的方案。

    2.协商,双方通信的时候,协商密钥,假设我发送的数据用一个我自己生成的密钥加密了,那对方要想解密,我还带把这个密钥也发送给对方,但是第一次发送的时候我加不加密?如果我想让对方知道这个密钥,那我必须明文的把密钥发送过去,如果这个密钥信息被盗取,以后双方的加密通信就没有任何意义了。所以密钥协商,采用对称的方式是根本不可能的

    服务器采用非对称加密,那它就有公钥和私钥假设公钥为G私钥为S,当客户端请求我时,我的服务端首先要进行密钥协商,把我的公钥G给客户端,接者客户端拿到公钥后,用公钥G对数据进行加密,发送回服务端。因为只有服务端具有私钥,也就是说只有该服务端能进行解密,至此就拿到了通信的数据。也就意味着,公钥被暴漏给全世界,所以客户端都可以拿到公钥,一旦使用公钥加密,那世界上就只有它能进行解密,经过这样的方案,就能够保证数据客户端到服务端的安全。
    在这里插入图片描述
    但是从服务端到客户端如何加密,肯定不能用私钥加密,如果用私钥,客户端是有公钥可以进行解密,但是别人也有公钥,他们也能解密,就说明如果只有一对公钥私钥,只能保证单向的数据安全,而另一个方向是不安全的。

    既然一对非对称密钥能保证数据的单项安全,那那么两对非对称密钥就可以保证数据的双向安全了嘛?给客户端也配一对公钥私钥,在通信阶段,提前交换双方的公钥不就可以,双方通信时各自使用自己的私钥进行加密,接收到消息后使用对方的公钥进行解密。这个方案看起来很好,但事实并非如此

    1.依旧有被非法窃取的风险
    2.非对称加密算法特别耗时(相对对称加密),因为这一点,这种方法就不可能被采纳了。

    所以我们在进行http通信时,采用的加密方式不是采用纯粹的对称也不是纯粹的非对称,纯的对称有安全隐患,纯的非对称有效率问题,所以实际上采用的是非对称+对称方案
    客户端发起请求,服务端正常吧自己的公钥(假设为S)给客户端,客户端接下来会形成对称密钥的私钥(假设为X),客户端用服务端的公钥S对X加密(形成XS),客户端再把经过加密的对称密钥X+发送给服务端,因为这个消息是用服务端的公钥加密的,所以只有服务端能解密,解密后就得到了服务端的对称密钥。服务端以安全的方式拿到了客户端发来的对称密钥的私钥信息,那以后的通信就可以采用对称加密的方式进行通信了。相当于最开始双方通过非对称的方式交换对称密钥,然后采用对称方案进行加密通信
    在这里插入图片描述

    交换对称密钥的阶段我们称为密钥协商阶段,采用的是非对称算法
    之后的通信我们称为数据通信阶段,采用的是对称加密

    中间人攻击

    端和端之间的数据被人篡改,这种攻击方法称为中间人攻击,上面的方法可以说是滴水不漏,是如何发起攻击的?因为客户端发起第一次请求,服务端给客户端返回公钥的时候,这是没有任何加密的,所以是有风险的。

    在网络环节中,随时都可能存在中间人偷窥,修改我们的数据,因为无论数据是否加密,别人都是可以拿到你的数据的,只要你的数据要在网络上走,就可以拿到,我们现在做的无非就是让别人拿到的是加密的还是未加密的,服务端给客户端返回的公钥,这也是一段数据,假设此时出现了一个“中间人”,它自己也有一对加密算法,即公钥和私钥,服务端发送公钥时,这个报文在网络中是大家都可以获取的,对大家是公开的,而如果这个中间人可以随时随地的接收服务端发送的信息,而服务端最开始发公钥时,第一条消息是没有被加密的,那中间人拿到这个报文,把服务端的公钥信息替换出来,填上了自己的公钥信息,修改了包含服务端公钥的报文,然后把这个被修改过的报文发送给客户端。客户端并不知道服务端发给自己的报文被修改了,所有客户端会直接使用中间人的公钥对自己的对称私钥进行加密,这时候再发这个报文发送给客户端,因为加密使用的是中间人的公钥,所以就算服务端拿到这个报文也无法解密,这时候中间人又截到了这个报文,然后用自己的私钥对报文进行解密,这时中间人就拿到了客户端的对称私钥,这时候再用当时拿到的服务端的公钥对其进行重新加密,然后再返还给服务端。服务端再解密就拿到报文后再进行解密,这时候就拿到了客户端的密钥,这时客户端和服务端都认为自己成功的拿到了对方的密钥,而中间人则通过“狸猫换太子”的方法拿到了客户端与服务端接下来要进行通信使用的对称加密的私钥。接下来两端进行对称加密通信时,中间人都可以监测到对应的数据,甚至是篡改
    在这里插入图片描述

    证书机制

    中间人问题的本质就是因为客户端无法判定发来的密钥协商报文是不是从合法的服务放发来的!!!当人们意识到这种攻击手法无法解决,网络中就出现了一个非常重要的机构,叫做CA证书机构

    我怎么证明你是一个大学生?你有学位证,我们为什么信任这些高校?因为高校有国家教育部支持,可以证明这个服务是合法的,也就是说只要一个服务商经过权威机构认证,该机构就是合法的

    CA机构属性:1.权威(全世界都认识它,最顶端的)2.有自己的公钥和私钥,假设公钥是A,私钥是A’

    所以一般的一个服务方,即一个常规的正规公司的网站,想要,它首先要向CA机构申请证书,一般申请证书肯定要提供材料,比如企业的基本信息,域名,公钥等。而这些信息其实都是文本,CA机构拿到了你的这些信息,他会给你创建证书,这个证书由两部分构成:1.基本内容域名+服务端公钥(一段文本)、2.根据上面这段文本形成的数据摘要,再用CA公司自己的私钥加密形成的数字签名。然后把这个证书颁发给企业。

    当我们有了这个证书,客户端发起请求时,服务器首先进入密钥协商阶段,以前是把自己的公钥返回给客户端,而现在服务端直接可以把证书信息返回给客户端,这里面就有服务端对应的公钥,而这个证书信息照样截取到你的服务端的证书,这时候中间人无法修改服务端的公钥了,虽然这时候的公钥信息是明文传送,你可以随便修改,但是这样为了保证内容和签名一致,你还要修改数字签名,但是因为这是用CA的私钥加密的,中间人只有公钥,解密可以,但是除了CA机构谁都无法使用CA机构的私钥继续加密形成新的数字签名,而大家统一都是使用CA公司的公钥继续解密的,所以中间人实际上是无法对内容进行修改的。服务端接收到文本内容时,他会把内容和数字签名拆出来,使用相同的散列算法对该文本形成摘要,然后用CA机构的公钥进行解密,然后对比是否相等。

    因为CA机构的公钥是全世界都知道的,但是CA机构的私钥只有CA机构自己知道,换言之,世界上只有CA机构能够重新形成对应的数字签名,这就保证了中间人对内容做的任何修改都是徒劳无功的,也就保证了客户端拿到的信息是没有被修改的

    如果中间人也向CA机构申请,成为一个合法的服务方,它为了拿到客户端的对称私钥直接把服务端的整个证书都替换掉,然后给客户端发送自己的证书,这样能否盗取客户端的对称私钥信息?也是不可以的,因为一个合法的服务商的证书里还包含了他的域名,这个域名和真正服务端的域名一定是不一样的,和密钥一样,除了CA机构,任何人无法对证书内容做修改。只要有证书,无论中间人怎么改证书,都改不了。

    所以证书机制就保证了服务端与客户端之间通信的安全,但是这就要求客户端必须知道CA机构的公钥信息,这个公钥信息一般是内置的

    CA机构不止有一家,如果是几个人互相信任,那他们就可以互相颁发证书,我们有时候访问一些网站时,可能会弹出说当前网站是不受信任的,是否颁发对应的证书信息,这就属于在信任证书层面让你安装一些其他信任链的证书信息,但是这个信任出现后果是需要用户自己承担的。所以也有一部分证书是访问网址时浏览器可能会提示用户进行安装

    以上就是本篇的全部内容。

  • 相关阅读:
    「聊设计模式」之模板方法模式(Template Method)
    Redis缓存穿透和缓存击穿和缓存雪崩
    【Hack The Box】windows练习-- SecNotes
    教你找回MySQL管理员root密码的3个妙招
    算法第七天:leetcode之209.长度最小的子数组
    【Spring】Bean加载控制
    Java之方法
    bytebuffer 内部结构
    大语言模型LLM推理加速:Hugging Face Transformers优化LLM推理技术(LLM系列12)
    理想中的接口自动化项目
  • 原文地址:https://blog.csdn.net/qq_45967533/article/details/125710625