HTTP协议

Posted 小倪同学 -_-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HTTP协议相关的知识,希望对你有一定的参考价值。

文章目录

再谈 “协议”

我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层

协议是一种 “约定”. socket api的接口, 在读写数据时, 都是按 “字符串” 的方式来发送接收的. 如果我们要传输一些"结构化的数据" 怎么办呢?

模拟实现网络版计算器

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

实现程序前我们先做约定

  • 客户端依次输入 数字1,数字2,操作符
  • 操作符只能为 加 减 乘 除 取模
  • 除零错误码为1,模零错误码为2,输入错误操作符错误码为3

我们所做的约定就可以理解为协议

协议一定和具体使用场景有关,一般比较简单,鲁棒性(健壮性),可扩展性不强。

互联网应用中,有很多场景是特别高频的,就有一些程序员前辈为我们做好了很多应用层协议,这些场景我们就不用自己设计协议了。我们作为学习者,需要了解别人的协议,适度的学别人的协议细节甚至自己编写一小部分!不排除有些场景,需要自己定义协议如游戏领域,通信领域…

协议文件 protocol.hpp

#pragma once

#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<strings.h>

namespace ns_protocol

        // request
        struct Request
                int x;
                int y;
                char op; 

                Request():x(0),y(0),op('+')
        ;  

        // response
        struct Response
                int code;//状态码,0,success;!0
                int result;

                Response():code(0),result(0)
        ;  

服务端

server.hpp

#pragma once

#include"protocol.hpp"
#include<pthread.h>

namespace ns_server

        class Server
                private:
                        uint16_t port;
                        int listen_sock;
                public:
                        Server(uint16_t _port):port(_port),listen_sock(-1)
                        

                        void InitServer()
                        
                                listen_sock=socket(AF_INET,SOCK_STREAM,0);
                                if(listen_sock<0)
                                        std::cerr<<"socket error"<<std::endl;
                                        exit(2);
                                

                                struct sockaddr_in local;
                                bzero(&local,sizeof(local));
                                local.sin_family=AF_INET;
                                local.sin_port=htons(port);
                                local.sin_addr.s_addr=INADDR_ANY;

                                if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0)
                                        std::cerr<<"bind error"<<std::endl;
                                        exit(3);
                                

                                if(listen(listen_sock,5)<0)
                                        std::cerr<<"listen error"<<std::endl;
                                        exit(4);
                               
                        

                        static void *calc(void *args)
                        
                            int sock = *(int*)args;
                            delete (int*)args;

                            while(true)
                                ns_protocol::Request req;
                                ssize_t s = recv(sock, &req, sizeof(req), 0); //read
                                //少了一个反序列化的过程,暂时
                                if(s > 0)
                                    ns_protocol::Response resp;
                                    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(0 == req.y) resp.code = 1; //除0
                                            else resp.result = req.x / req.y;
                                            break;
                                        case '%':
                                            if(0 == req.y) resp.code = 2; //模0
                                            else resp.result = req.x % req.y;
                                            break;
                                        default:
                                            resp.code = 3;
                                            break;
                                    
                                    //需要一个序列化的过程, 暂时不管
                                    send(sock, &resp, sizeof(resp), 0); //write
                                
                                else if(s == 0)
                                    std::cout << "client quit, me too" << std::endl;
                                    break;
                                
                                else
                                    std::cerr << "client quit, me too" << std::endl;
                                    break;
                                
                            

                            close(sock);
                            return nullptr;
                        


                        void Loop()
                        
                                while(true)
                                        struct sockaddr_in peer;
                                        socklen_t len=sizeof(peer);
                                        int sock=accept(listen_sock,(struct sockaddr*)&peer,&len);
                                        if(sock<0)
                                                std::cerr<<"accept error"<<std::endl;
                                                continue;
                                        

                                        //hander sock
                                        pthread_t tid;
                                        int *p=new int(sock);
                                        pthread_create(&tid,nullptr,calc,p);
                                        //close(sock);
                                
                        
                        ~Server()
                        
                                if(listen_sock>=0)
                                        close(listen_sock);
                        
        ;

server.cc

#include"server.hpp"

void Usage(std::string proc)

        std::cerr<<"Usage: "<<"\\n\\t"<<proc<<" port"<<std::endl;

int main(int argc,char *argv[])

        if(argc!=2)
                Usage(argv[0]);
                exit(1);
           

        uint16_t port=atoi(argv[1]);
        ns_server::Server svr(port);

        svr.InitServer();

        svr.Loop();

        return 0;

用户端

client.hpp

#pragma once

#include "protocol.hpp"
#include <string>

namespace ns_client

    class Client
        private:
            std::string svr_ip;
            uint16_t svr_port;
            int sock;
        public:
            Client(const std::string &_ip, uint16_t _port): svr_ip(_ip), svr_port(_port), sock(-1)
            

            void InitClient()
            
                sock = socket(AF_INET, SOCK_STREAM, 0);
                if(sock < 0)
                    std::cerr << "socket error" << std::endl;
                    exit(2);
                
                //不需要bind,listen,accept
            
            void Run()
            
                struct sockaddr_in peer;
                bzero(&peer, sizeof(peer));

                peer.sin_family = AF_INET;
                peer.sin_port = htons(svr_port);
                peer.sin_addr.s_addr = inet_addr(svr_ip.c_str());

                int ret = connect(sock, (struct sockaddr*)&peer, sizeof(peer));
                if(ret < 0) 
                    std::cerr << "connect error" << std::endl;
                    exit(3);
                

                while(true)
                    ns_protocol::Request req;
                    std::cout << "请输入你的第一个数据# ";
                    std::cin >> req.x;
                    std::cout << "请输入你的第二个数据# ";
                    std::cin >> req.y;
                    std::cout << "请输入要进行的计算<+-*/%># ";
                    std::cin >> req.op;

                    //req是一个结构化的数据,其实也需要进行序列化,暂时忽略
                    send(sock, &req, sizeof(req), 0);

                    ns_protocol::Response resp;
                    ssize_t s = recv(sock, &resp, sizeof(resp), 0);
                    if(s > 0)
                        //你读到的一定是一个曾经被server序列化的字符串,需要你进行反序列化,今天暂时忽略
                        std::cout << "code: " << resp.code << std::endl;
                        std::cout << "result: " << resp.result << std::endl;
                    
                
            

            ~Client()
            
                if(sock >= 0)close(sock);
            
    ;

client.cc

#include "client.hpp"

static void Usage(std::string proc)

    std::cerr << "Usage: " << "\\n\\t" << proc << " svr_ip svr_port" << std::endl;

// ./client svr_ip svr_port
int main(int argc, char *argv[])

    if( argc != 3 )
        Usage(argv[0]);
        exit(1);
       

    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);

    ns_client::Client cli(ip, port);

    cli.InitClient();

    cli.Run();

    return 0;

运行结果

HTTP协议概念

  • HTTP(HyperText Transfer Protocol,超⽂本传输协 )的协议。
  • HTTP是⽆连接, ⽆状态, ⼯作在应⽤层的协议。
  • ⽆连接理解为: http协议本身是没有维护连接信息的, http的数据会交给⽹络协议栈传输层的TCP协议, ⽽TCP是⾯向连接的。
  • ⽆状态: HTTP 协议⾃身不对请求和响应之间的通信状态进⾏保存。也就是说在 HTTP 这个级别,协议对于发送过的请求或响应都不做持久化处理。

认识URL

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

urlencode和urldecode

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

转义的规则如下:

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

例如C++就会被转义成C%2B%2B,这里的"+" 被转义成了 “%2B”

urldecode就是urlencode的逆过程

urldeconde工具

HTTP协议格式

HTTP请求

HTTP 协议规定,请求从客户端发出,最后服务器端响应该请求并返回。换句话说,肯定是先从客户端开始建⽴通信的,服务器端在没有接收到请求之前不会发送响应。

  • 首行: [方法] + [url] + [版本]
  • Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\\n分隔;遇到空行表示Header部分结束
  • Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度;

http的请求为什么会存在空行?

作为一个特殊符号来表示http请求报头的结束

每一层协议都必须解决两个问题:
1.将报头和有效载荷进行分离!(解包)特殊字符的方案
2.将自己的有效载荷交付给上层协议!(分用)

HTTP响应

  • 首行: [版本号] + [状态码] + [状态码解释]
  • Header: 请求的属性, 冒号分割的键值对;每组属性之间使用\\n分隔;遇到空行表示Header部分结束
  • Body: 空行后面的内容都是Body. Body允许为空字符串. 如果Body存在, 则在Header中会有一个Content-Length属性来标识Body的长度; 如果服务器返回了一个html页面, 那么html页面内容就是在body中

HTTP协议请求方法


其中最常用的就是GET方法和POST方法

GET vs POST

  1. GET和POST方法都不安全,因为默认都没有加密
  2. Get通常用来进行获取网页等资源,不过也可以提交参数:百度搜索
  3. POST通常用来提交数据

HTTP的状态码

HTTP常见Header

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

HTTP的特点

  1. HTTP本身是无连接的(

虽然底层是基于TCP协议的,但是http要发起所谓的http request的时候,不会先在http层建立链接,而是直接通过底层的TCP进行传输.

  1. HTTP本身是无状态的

http不会记录自己的发起http请求的上下文,不会对历史请求有任何记忆能力!

给用户造成非常的不方便:比如:当vip用户登录上某些vip类的视频网站的时候,点播某些vip视频,因为http是无状态的,那么也就意味着,新打开的网页,以及里面的vip网页,都要重新登录

我们可以给http添加一些其他的技术特点(cookie+ session),让http具有保存状态和会话能力!

cookie和session

当我们在网页中成功输入账户和密码后,浏览器会提示我们是否保存账户和密码。页面保存是将自己的私密信息保存到浏览器的默认文件中,这里的默认文件就是cookie
浏览器每次发起请求的时候,http请求都会自动给你携带你曾经写到临时文件(默认文件)中的内容,发送给服务器端!

浏览器使用cookie的时候

  • 每一个http request都会携带cookie的信息,发给服务器!一定需要服务器也支持cookie技术(其中认证的环节,不是http完成的,程序员自己完成认证的)
  • cookie"文件":有些浏览器配置的时候,cookie有内存级的,也有文件级别的!

内存级: 当浏览器不关闭时,访问都会直接登录。一旦关闭浏览器,再打开访问就需要输入密码。
文件级: 较长的一段时间内访问都会直接登录,无论是否关闭浏览器。

只用cookie有什么风险!

cookie里面保存的是用户的私密信息,而且有可能是基于文件的,万一我的电脑被注入木马等恶意程序,盗取浏览器的所有cookie信息!比如:拿到了你的访问某个网页的所有cookie信息!,不法分子拿着你的cookie拷贝到自己的浏览器cookie路径下,然后自己访问该网页,就是以你的身份访问了。

所以存在如下风险

1.个人私密信息泄漏的风险
2.盗用个人的身份从事一些不好的事情

那么如何解决上述问题呢?这里我们引入了session文件

session文件是通过复杂的算法将cookie文件转换成唯一的session文件。

session文件保存在用户端,用户的个人私密信息保存在服务端(比较安全),我们在访问时,会用session文件去在服务端找对应的个人信息进行访问。

cookie 和 session 本身都是有时间和空间限制的,比如说:上午在北京访问,下午在南京访问,session会失效,这时就要重新输入用户信息。

http VS https

  1. http请求无论是get或者post都是不安全的,只有经过加密的数据,才能保证在理论上是安全的!


SSL (Secure Sockets Layer,安全套接字层 )

TLS (Transport Layer Security,传输层安全)

这里所谓的加密解密的软件层,本质是在用户层!
ssl也是分层,分为两层第一层为封装加密解密 , 第一层为https握手。

  1. 加密分为对称加密(只有一个秘钥)和非对称加密(公钥和私钥),非对称加密中,一般公钥是公开的,私钥必须保存!对称加密效率高,非对称加密效率低(需要不断的加密解密操作)

只要加密了,就一定是安全的吗?

所有的密码学加密算法,都是和CPI的算力有关。如果想破解一个加密的数据,只要我们的算力足够强,就大概率可以破解!强大的算力是有成本的,所谓的安全本质:破解的成本>>破解的收益

  1. MD5信息摘要算法,一种被广泛使用的密码散列函数

MD5是将文本提取出一部分内容,进行不可逆算法,形成一串字符串,该字符串我们称之为摘要,接着再对摘要进行加密,形成一串复杂的字符,该字符被称为数据指纹

指纹技术: 凡是对原始文本做任何修改,在进行摘要形成指纹之后,得到的指纹数据,差别会非常大!

用户的密码信息是必须经过加密的!保存在对一个公司的mysql数据库中

用户输入密码的流程

加密

  1. 选择什么种类的加密算法?对称的,还是非对称的?

http刚开始通信的时候,绝对不能使用对称加密,第一次交换秘钥时会出现无法解密的问题。但我们可以使用一对(2个)公钥和私钥,做到安全通信,我们需要所有的client和server提前内置公钥和私钥。所以解决方法是前期ssl握手期间,使用非对称加密,来交换对称加密的秘钥,通信期间,采用对称加密来保证数据安全。

加密过程可以简单的总结为: 非对称加密解密算法(秘钥协商阶段)+对称加密解密算法(加密通信阶段)

秘钥协商阶段

这样做有没有什么问题呢?

可能有黑客截获服务端发来的公钥,将自己的公钥发送给客户端。客户端不考虑接收的公钥是否合法,直接通过公钥加密对称秘钥并发送回去。这样黑客就能以客户端的身份和用户端进行交流了。

如何确认对方发来的公钥是合法的且没有被篡改呢?

这就需要确认对方的主机Server是合法的(有权威机构对网站或者服务器进行合法认证:CA认证)

生成证书的过程

如何证明证书是合法的

client只要能够用CA的公钥成功解密,证书就是合法的!

如何证明证书没有被篡改过

通过对比原文摘要和指纹解密之后的携带的摘要是否相等,来判定是否被篡改过!

中间人是否可以只对证书明文内容进行修改

可以

为什么携带了指纹的证书,根本就不怕被中间人修改呢?

根本原因是,任何中间人,都没有曾经形成指纹的时候,CA的私钥!在client认证阶段,client只会只用CA的公钥来进行证书认证,不相信任何人的其他公钥信息!

结论:世界上的任何一份证书=数据+指纹,没有任何人可以修改因为大部分人,没有CA的私钥,无法二次重新生成指纹

为何我们要先认证证书呢?
不是目的,目的是为了相信server发过来的公钥是合法的!

client是如何得知CA的公钥呢?
操作系统or浏览器出厂的时候,内置了各种权威的根认证机构的各种证书(公钥)

对称加密解密算法(加密通信阶段)

简单的HTTP服务器

http_server.hpp文件

#include <iostream>
#include 以上是关于HTTP协议的主要内容,如果未能解决你的问题,请参考以下文章

计算机网络HTTP协议(格式报头请求响应)

osi七层原理概念

http中的净荷 payload(有效载荷有效负载)是什么?

Teensy 3.2 传出数据包有效载荷限制 8 个字节

防火墙基础之安全策略和GRE隧道

基于FTP的载荷投递