HTTP 协议详解 & HTTPS

1. 网络模型

  说HTTP协议之前,先简单说一下网络模型。为了简化网络的复杂度,网络通信的不同方面被分解为多层次结构,每一层只与紧挨着的上层或者下层进行交互。将网络进行分层,就可以修改甚至替换某一层的软件,只要层与层之间的接口保持不变,就不会影响到其他层。

  在网络分层有两种模型

  开放式系统互联通信参考模型(英语:Open System Interconnection Reference Model,缩写为 OSI),简称为OSI模型(OSI model),一种概念模型,由国际标准化组织提出,一个试图使各种计算机在世界范围内互连为网络的标准框架。这个是分了七层。

  TCP/IP协议族: 分为了五层。

如下图:

 2. HTTP 协议

  超文本传输协议(Hyper Text Transfer Protocol)。是一个应用层协议。HTTP是一个基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。

  是一种无状态的以请求应答方式运行的协议,它使用可扩展的语义和自描述消息格式,与基于网络的超文本信息系统互动。

  无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。

1. 报文格式

1. 请求报文格式如下:

  客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。 请求数据也是请求体,这个是非必须的,前面的三个是必须的。

  对于Tomcat:请求行中的数据被Tomcat 封装到request的相关属性中,请求头的信息被封装在request的headersMap 中,请求体在request的inputstream 中。

 2. 响应报文格式如下:

HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。

 2. 关于请求头和响应头

  头部字段是Key-value的形式,key和value 之间用 : 分隔,最后用CRLF换行符表示字段结束。比如前后端分离时传送的"Content-Type: application/json"。HTTP头字段非常灵活,不仅可以使用标准的Host,Connection等已有的头,也可以添加任意自定义头,这就给HTTP协议扩展带来了可能性。

  头字段注意事项:

(1) 字段名不区分大小写,字段名不允许出现空格,可以使用连字符"-",但不能使用下划线(这里其实HEADER 字段名可以可由可打印的 ASCII 字符组成,也就是十进制值在 33 和 126 之间的字符,不含冒号。 不能使用下划线是因为有的服务器不会解析下划线,所以非常不建议使用下划线)。字段名后面必须紧接着":" 冒号,不能有空格,而":" 冒号后面可以有多个空格。

(2) 字段的顺序是没有意义的, 可以任意排列而不影响语义。

(3) 字段原则上不允许重复,除非这个字段本身的语义允许,例如 Set-Cookie

3. 常用头

HTTP 协议的头部字段非常多,但基本可以分为四类。

请求字段:HOST、REFERER、User-Agent(发送请求的来源,一般是浏览器信息; curl 脚本是 curl/版本号)

响应字段:Server(标记服务器使用的服务器,比如Nginx等)

通用字段:在请求头和响应头都可以出现,比如Content-Type、Connection

自定义请求头

3. 请求方法

请求方法(所有方法全为大写)有多种,各个方法的解释如下:

GET 请求获取Request-URI所标识的资源

POST 在Request-URI所标识的资源后附加新的数据

HEAD 请求获取由Request-URI所标识的资源的响应消息报头

PUT 请求服务器存储一个资源,并用Request-URI作为其标识

DELETE 请求服务器删除Request-URI所标识的资源

TRACE 请求服务器回送收到的请求信息,主要用于测试或诊断

CONNECT 保留将来使用

OPTIONS 请求查询服务器的性能,或者查询与资源相关的选项和需求

4. HTTP请求的完整过程

  当用虎仔浏览器输入网址回车之后,网络协议做的工作如下:

1. 首先工作的是浏览器,要解析URL中的域名

2. 根据域名获取对应的IP地址。 首先从浏览器缓存中查看,如下可以查看浏览器缓存的域名: 也可以导出到本地,导出后可以看到会记录域名、IP地址、失效时间等

chrome://net-internals/#events

 如果没有则从本地的hosts 文件查看,如果还没有就从DNS服务器解析

3. 拿到IP之后就会发起与服务器的三次握手

4. 握手建立之后,开启组装HTTP 请求报文并发送报文

5. 服务器收到请求之后开始解析报文,生成响应数据且发送给客户端

6. 浏览器收到响应之后开始渲染页面,如果页面还有其他类似于图片等资源的请求会继续发送其他请求(HTTP1.1 的长连接,一次连接、多次请求)

7. 所有请求完成后进行四次挥手

4. 测试

使用Java Socket 编程分析Http 协议。 用JavaSocket 接收HTTP 请求, 并按照上面的格式解析请求行、头、体等信息

 服务器端代码:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


public class PlainTest {

    private static final ExecutorService executorService = Executors.newFixedThreadPool(20);

    public static void main(String[] args) throws IOException {

        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("服务启动,等待监听~~~");
        while (true) {
            Socket accept = serverSocket.accept();
            System.out.println(" 接收到一个请求" + accept.getRemoteSocketAddress());
            executorService.execute(new SocketHandler(accept));
        }

    }

    static class SocketHandler implements Runnable {

        private Socket socket;

        SocketHandler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            try {
                InputStream inputStream = socket.getInputStream();
                // 缓冲区设置比较小,便于测试
                byte[] bytes = new byte[1024];
                int length;
                byte[] allBytesArray = new byte[]{};

                // 核心代码,需要判断什么时候客户端请求发送结束
                while ((length = inputStream.read(bytes)) > 0) {
                    System.out.println(length);
                    // 数组合并,使用字节累加,直接转String的话中文会被分割,导致乱码
                    byte[] joinedArray = new byte[allBytesArray.length + length];
                    System.arraycopy(allBytesArray, 0, joinedArray, 0, allBytesArray.length);
                    System.arraycopy(bytes, 0, joinedArray, allBytesArray.length, length);
                    allBytesArray = joinedArray;

                    // 没有请求体
                    String readedData = new String(allBytesArray, "UTF-8");
                    // 如果包含Content-Length 证明有请求体
                    boolean existBody = readedData.contains("Content-Length");
                    // 没有请求体且以

 结尾表示传输结束
                    if (!existBody && readedData.endsWith("

")) {
                        break;
                    }

                    // 有请求体并且http已经接收完成
                    if (existBody && readedData.contains("

")) {
                        System.out.println("-------------------存在body");
                        // 先等http头传输完成,截取头内容
                        String header = readedData.substring(0, readedData.indexOf("

"));
                        System.out.println("-------------------header:" + header);

                        // 读取Content-Length的长度
                        int startIndex = header.indexOf("Content-Length");
                        int start = header.indexOf(" ", startIndex);
                        int end = header.indexOf("
", startIndex);
                        // 如果没有证明Content-Length 正好是最后一个头
                        if (end == -1) {
                            end = header.length();
                        }
                        // 截取请求体长度
                        int contentLength = Integer.valueOf(header.substring(start + 1, end));
                        System.out.println("-------------------contentLength:" + contentLength);
                        int total = readedData.getBytes().length;
                        // 当前字节长度
                        int currentBodyLength = total - header.getBytes().length - "

".getBytes().length;
                        System.out.println("-------------------currentBodyLength:" + currentBodyLength);
                        if (currentBodyLength == contentLength) {
                            //当前长度达到Content-Length表示接收完成
                            break;
                        }
                    }
                }

                // 组装响应数据
                OutputStream outputStream = socket.getOutputStream();
                outputStream.
                        write(("HTTP/1.1 200 OK
" +  //响应头第一行
                                "Content-Type: text/html; charset=utf-8
" +  //简单放一个头部信息
                                "
" +  //这个空行是来分隔请求头与请求体的
                                "<h1>hello world</h1>").getBytes());

                outputStream.close();
                inputStream.close();
                socket.close();
            } catch (Exception e) {
                // ignore
            }


        }
    }

}

1. 简单的测试Get 请求

curl http://localhost:6666/XXXsss?w=fsfs

服务器端收到的数据如下:

 可以看到满足上面的请求报文的格式,这个是没有请求体的格式,没有请求体的时候以空行结束。

2. 测试POST 传递带参数的请求体

curl -X POST --header 'Content-Type: application/json' -d '{"username": "zs"}' http://localhost:6666/XXXsss?w=fsfs
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- 100    40    0    22  100    18    687    562 --:--:-- --:--:-- --:--:-- 22000<h1>hello world</h1>

服务器端收到的数据如下:

可以看到空行后面拼装的请求体。并且回传的也满足上面的响应头的格式。(如果响应数据格式不满足上面的格式响应会异常)

  两个请求都添加了一些默认的请求头。这个是HTTP在处理的时候添加的默认的请求头。

  这里还有一个待完善的问题就是如何判断请求体读取的数据已经结束,以及关于长链接和短链接的测试。这个在学习了Tomcat 相关源码之后进行研究。

5. HTTPS 协议

  超文本传输安全协议(英语:Hypertext Transfer Protocol Secure,缩写:HTTPS,常称为HTTP over TLS,HTTP over SSL或HTTP Secure)是一种通过计算机网络进行安全通信的传输协议。

  HTTPS经由HTTP进行通信,但利用SSL/TLS来加密数据包。

  HTTPS开发的主要目的,是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。

1. HTTP是明文传输,整个传输透明,任何人都能在链路中截获、修改或者伪造请求/响应报文,数据不具有可靠性。因此就诞生了为安全而生的HTTPS协议。

 使用HTTPS时,所有的HTTP请求和响应在发送到网络之前,都要进行加密; 另一个重要作用是对网站服务器进行真实身份认证。

 2. SSL/TLS

  SSL 即安全套接层(Secure Sockets Layer), 后改名为TLS(Transport Layer Security), 正式标准化。采用的主要算法有:

(1) 摘要算法:

摘要算法能够把任意长度的数据压缩成固定长度, 而且独一无二的摘要字符串。 比如MD5、SHA1、SHA2、SHA256

(2) 对称加密: XOR  异或操作 - 相同为0, 不相同为1。 编码解码使用相同密钥的算法。如AES,RC4等。

比如:

11010100 原文

01001101 密钥

10011001 密文

再用密文和密钥做异或得到原文:

01001101 密钥

10011001 密文

11010100 原文

(3) 非对称加密

它有一个密钥, 一个叫公钥、一个叫私钥。两个密钥不同,公钥公开任何人都能使用,私钥必须严格保密。非对称加密可以解决"密钥交换" 的问题。网站秘密保管私钥,在网上任意发布公钥,登陆网站用公钥加密,密文只能由私钥解密。非对象加密需要大量的数学运算,通常比较慢。(DHDSARSA)等。

  TLS 使用混合的加密方式。对称加密和非对称加密都用。大致过程如下:

1. 通信开始使用非对称加密如RSA等解决密钥交换的问题。

 2. 然后用对称加密算法进行数据通信

 TCP三次握手之后:

(1) 客户端将自己支持的算法发送给服务器端

(2) 服务器端从客户端支持的加密算法中选择一个算法告知客户端,并且将公钥数字证书传给客户端。证书包括CA信息、公钥用户信息、公钥等

(3) 客户端随机生成一个密钥(用于后面对称加密), 并且用上面发送的公钥进行加密。服务器端拿到之后用私钥解密,得到客户端生成的密钥(这时候客户端和服务器端都有相同的密钥)。

  上面三步称为TLS握手。

(4) 接下来就可以进行对称加密双向传输, 客户端和服务端使用对称加密算法进行通信,使用的是客户端随机生成的秘钥。

3. 身份验证

(1) CA信息、公钥用户信息、公钥、权威机构的签名、有效期

(2) 数字证书作用:

1》 通过数字证书向浏览器证明身份

(2) 数字证书里面包含了公钥(用于后面加密客户端随机生成的秘钥)

(3) 数字证书的申请和验证

申请:

1》 生成自己的公钥和私钥, 服务器自己保留私钥

2》 向CA提交公钥、公司、域名等信息

3》 CA通过线上、线下等放肆验证提交信息的真实性和合法性

4》 审核通过,CA机构签发认证的数字证书,包含公钥、公司等信息、CA信息、有效时间、证书序列号,同时生成一个签名

签名步骤: hash(申请证书提交的信息) = 信息摘要, CA再使用私钥对信息摘要进行加密,密文就是证书的数字签名

浏览器验证过程:

当浏览器访问服务器时,服务器会返回数字证书链给浏览器,浏览器收到会对数字证书进行验证。

证书是一个证书链,不是单个的证书。也就是证书的上级保证下级的合法性。 根证书一定是浏览器或者操作系统自己能验证的。比如查看掘金网的:

证书链:

 证书的详细信息:

【当你用心写完每一篇博客之后,你会发现它比你用代码实现功能更有成就感!】
原文地址:https://www.cnblogs.com/qlqwjy/p/14561910.html