写点什么

万字图解 | 深入揭秘 HTTP 工作原理

作者:云舒编程
  • 2024-01-25
    广东
  • 本文字数:11282 字

    阅读完需:约 37 分钟

万字图解 | 深入揭秘HTTP工作原理

大家好,我是「云舒编程」,今天我们来聊聊计算机网络面试之-(应用层 HTTP)工作原理。


文章首发于微信公众号:云舒编程

关注公众号获取:1、大厂项目分享 2、各种技术原理分享 3、部门内推

前言

想必不少同学在面试过程中,会遇到「在浏览器中输入 www.baidu.com 后,到网页显示,其间发生了什么」类似的面试题。


本专栏将从该背景出发,详细介绍数据包从 HTTP 层->TCP 层->IP 层->网卡->互联网->目的地服务器 这中间涉及的知识。


本系列文章将采用自底向上的形式讲解每层的工作原理和数据在该层的处理方式。

系列文章

图解 | 深入揭秘数据链路层、物理层工作原理

图解 | 深入揭秘IP层工作原理

图解 | 深入揭秘TCP工作原理

图解 | 深入揭秘HTTP工作原理

图解 | 深入揭秘Linux 接收网络数据包

图解 | 深入揭秘IO多路复用原理


通过上一篇文章图解 | 深入揭秘TCP工作原理的介绍,我们知道了传输层的基本工作原理。


本篇将会详解对应用层进行介绍。

通过本文你可学到:

  1. 什么是超文本传输协议

  2. HTTP 协议格式构成

  3. HTTP 协议的发展历程

  4. 缓存技术

  5. 长连接

  6. https

  7. 多路复用

  8. 数据压缩

  9. QUIC 协议

  10. cookie 与 session

  11. WebSocket

什么是超文本传输协议

HTTP 全称是 HyperText Transfer Protocol,也叫超文本传输协议。


HTTP 于 1991 年提出的,主要用于学术交流,当时的目的也很简单,就是用来在网络之间传递 HTML 文本的内容,所以被称为超文本传输协议。


一个简单的 HTTP 请求流程如下:


  • HTTP 都是基于 TCP 协议的,所以客户端先要根据 IP 地址、端口和服务器建立 TCP 连接,而建立连接的过程就是 TCP 协议三次握手的过程。

  • 建立好连接之后,会发送一个 GET/POST 请求,如 GET /index.html 用来获取 index.html。

  • 服务器接收请求信息之后,读取对应的 HTML 文件,并将数据以 ASCII 字符流返回给客户端。

  • HTML 文档传输完成后,断开连接。


图中断开连接是TCP四次挥手,这里简单画了


在互联网早期的时候,HTTP 传输的数据只是简单的字符文字,但是现在 HTTP 协议经过长时间的发展已经支持了图片、视频、音频等传输,所以【文本】的涵义已经不仅仅是指文字字符了。

HTTP 协议格式构成

HTTP 协议由以下四部分构成:


HTTP request 请求体:


第一部分对应请求行,请求行又由三部分组成:




第二部分对应请求头:请求头由多个 k:v 结构组成


第三部分是空白行:


第四部分是请求体:


        请求体可以接受 form 表单、json、xml、字符串等类型的参数,具体取决于 Content-Type 的设置。

HTTP response 响应体:


第一部分对应响应行,响应行又由三部分组成:



     Version:表示报文使用的 HTTP 协议版本;


     Status Code:一个三位数,用代码的形式表示处理的结果,比如 200 是成功,500 是服务器错误;


     Reason:作为数字状态码补充,是更详细的解释文字,帮助人理解原因。


第二部分对应响应头:请求头由多个 k:v 结构组成


第三部分是空白行:


第四部分是响应体:


        响应体可以接受 form 表单、json、xml、字符串等类型的结果,具体取决于 Content-Type 的设置。

HTTP 优化设计史

在前文【什么是超文本传输协议】我们有提到 HTTP 最开始设计时,只是为了传输简单的字符文本,随着互联网的发展,HTTP 也经过了几次优化设计,满足人们在数据类型传输、安全、性能等多方面的需求。


接下来我们会逐步讲解,HTTP 的几次重大优化设计:

HTTP/0.9

HTTP/0.9 是最开始的 HTTP 协议,就如前面说的,只支持简单的字符文本传输,安全,多样的数据、性能都没有做考虑。


并且他的请求/响应也不是我们前面提到的【HTTP 协议格式构成】部分标准组成。而是如下:

GET /mypage.html
复制代码


<html>  这是一个非常简单的 HTML 页面</html>
复制代码


只支持简单的 GET 请求,响应结果也只包含文档本身。

HTTP/1.0

1994 年底出现了拨号上网服务以及网景推出新浏览器后,人们开始对 HTTP 提出了更多的需求。

需求一、丰富 HTTP 协议格式

定义了前文提到的【HTTP 协议格式构成】,后续的 HTTP 请求都必须按照标准格式请求/响应。

带来如下好处:

  • 引入了更多的请求 Method,例如 POST 命令和 HEAD 命令,丰富了浏览器与服务器的互动手段;

  • 引入了状态码,使浏览器能了解请求执行成功或失败,并相应调整行为(如更新或使用本地缓存);

  • 引入了 HTTP 头部字段的概念,允许传输更多的元数据,使协议变得非常灵活,更具扩展性;

需求二、多文件传输(除了 HTML,还要支持 JS、CSS、图片、音视频等)

得益于 HTTP 标准协议格式的提出,HTTP/1.0 可以通过请求头和响应头来进行协商,在发起请求时候会通过 HTTP 请求头告诉服务器它期待服务器返回:

1、什么类型的文件;

2、采取什么形式的压缩;

3、提供什么语言的文件以及文件的具体编码。最终发送出来的请求头内容如下:

accept: text/html //期望服务器返回 html 类型的文件accept-encoding: gzip, deflate, br //期望服务器可以采用 gzip、deflate 或者 br 其中的一种压缩方式accept-Charset: ISO-8859-1,utf-8 //期望返回的文件编码是 UTF-8 或者 ISO-8859-1accept-language: zh-CN,zh //期望页面的优先语言是中文
复制代码


服务器接收到浏览器发送过来的请求头信息之后,会根据请求头的信息来准备响应数据。不过有时候会有一些意外情况发生,比如浏览器请求的压缩类型是 gzip,但是服务器不支持 gzip,只支持 br 压缩,那么它会通过响应头中的 content-encoding 字段告诉浏览器最终的压缩类型,也就是说最终浏览器需要根据响应头的信息来处理数据。下面是一段响应头的数据信息:

content-encoding: br //服务器采用了 br 的压缩方法content-type: text/html; charset=UTF-8 //服务器返回的是 html 文件,并且该文件的编码类型是 UTF-8。
复制代码

HTTP/1.1

优化一:缓存

对于重复性的请求,HTTP 会缓存结果,这样当一样的请求发起时直接从本地获取缓存,不需要请求服务端,节省了资源也提高了性能。

HTTP 的缓存是通过在 HTTP 请求头/响应头增加字段实现的,具体又分为两种:


1、强制缓存

强缓存指的是只要浏览器判断缓存没有过期,则直接使用浏览器的本地缓存,主动权在浏览器这边。

类似这样的请求就是使用了强制缓存。


强制缓存由由响应请求设置的,通过:

  • Cache-Control:设置相对时间,优先级最高;

  • Expires:设置绝对时间;


两个头部字段控制。

流程如下:

  1. 浏览器请求服务器中时,会先判断是否存在 cache,以及相应的 Cache-Control 是否过期。如果没有,则使用该缓存,否则重新请求服务器;

  2. 重新请求服务器后,会再次更新 Response 头部的 Cache-Control。


2、协商缓存

协商缓存就是强制缓存过期后,浏览器继续请求服务器使用缓存的机制,主要分为以下两种情况:

  1. 协商缓存生效,返回 304,代表资源未更新,旧的缓存可以继续使用;

  2. 协商缓存失效,返回 200 和新的请求结果。


协商缓存主要有两种实现方式:

  • Last-Modified/If-Modified-Since

  • Etag/If-None-Match


其中 Etag/If-None-Match 优先级比 Last-Modified/If-Modified-Since 高。

Last-Modified/If-Modified-Since

Last-Modified 是资源文件在服务器最后被修改的时间。

客户端请求服务端时会设置如下请求头:

If-Modified-Since:Last-Modified//Last-Modified是客户端第一次请求服务端时,服务端返回的
复制代码


服务器收到该请求,发现请求头含有 If-Modified-Since 字段,则会根据 If-Modified-Since 的字段值与该资源在服务器的最后被修改时间做对比,


  • 若服务器的资源最后修改时间大于 If-Modified-Since 的字段值,则重新返回资源,状态码为 200;

  • 否则返回 304,代表资源无更新,可以继续使用缓存文件。

Etag/If-None-Match

Etag 是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成)。


客户端请求服务端时会设置如下请求头:


If-None-Match:Etag//Etag是客户端第一次请求服务端时,服务端返回的
复制代码


服务端收到该请求后,发现该请求含有 If-None-Match,则会根据 If-None-Match 的字段值与该资源在服务器的 Etag 值做对比,


  • 一致则返回 304,代表资源无更新,继续使用缓存文件;

  • 否则重新返回资源,状态码为 200.


Etag/If-None-Match 优先级比 Last-Modified/If-Modified-Since 高的原因就是 Last-Modified/If-Modified-Since 依赖于时间,但是客户端和服务端的时间不一定一致,并且在分布式场景中,服务端各个机器的时间也不一定一致。

优化二:连接复用

HTTP/1.0 每进行一次 HTTP 通信,都需要经历建立 TCP 连接、传输 HTTP 数据和断开 TCP 连接三个阶段。互联网发展到如今,一个页面的渲染会发起十几个 HTTP 请求,如果每个请求都经历三次握手四次挥手,那会增加很多无关的开销。


为了解决这个问题,HTTP/1.1 中增加了持久连接的方法,它的特点是在一个 TCP 连接上可以传输多个 HTTP 请求,只要浏览器或者服务器没有明确断开连接,那么该 TCP 连接会一直保持。



从上图可以看出,HTTP 的持久连接可以有效减少 TCP 建立连接和断开连接的次数,减少了资源的浪费。


  • 持久连接在 HTTP/1.1 中是默认开启的,不需要专门设置。

  • 如果你不想要采用持久连接,可以在 HTTP 请求头中加上 Connection: close。

  • 目前浏览器中对于同一个域名,默认允许同时建立 6 个 TCP 持久连接。


Q:如果同一个域名的HTTP请求超过6个会怎么处理?A: 如果在同一个域名下有超过6个HTTP请求,例如同时有10个请求发生,那么其中4个请求会进入排队等待状态,直至进行中的请求完成。当然,如果当前请求数量少于6,会直接进入下一步,建立TCP连接。
复制代码


在浏览器中可以通过 Connection ID 判断 HTTP 请求是否复用了同一个 TCP 连接。


优化三:管线化技术

HTTP 长连接默认是串行的,也就是后面的请求得等前面的请求响应了才能继续请求,这就会导致如果一个请求响应慢,就会拖累后面的请求,也就是著名的队头阻塞。HTTP/1.1 中试图通过管线化技术来解决队头阻塞的问题。


HTTP/1.1 中的管线化:将多个 HTTP 请求整批提交给服务器的技术,虽然可以整批发送请求,不过服务器依然需要根据请求顺序来回复浏览器的请求。


但是管线化技术依旧存在诸多限制,导致其未流行起来:


  • 响应必须按照请求发过来的顺序,进行发送,如果顺序错乱,客户端就没办法匹配。

  • 不能使用诸如 post 这样对数据有副作用的请求方式。

  • 客户端必须做好连接会在任意时刻关闭的准备,还要准备好重发所有未完成的管道化请求。

  • 会产生 head of line blocking。即前一个请求遇到了阻塞,就算后面请求已经处理完毕也需要等待前一个请求完成,才能发送所有响应,浪费时间。

优化四:响应分块

在设计 HTTP/1.0 时,必须要知道响应数据的大小,浏览器才可以根据设置的数据大小来接收数据。不过随着技术的发展,很多数据都是动态生成的,因此在传输数据之前并不知道最终的数据大小,这就导致了浏览器不知道何时会接收完所有的文件数据。


HTTP/1.1 通过引入 Chunk transfer 机制来解决这个问题,服务器会将数据分割成若干个任意大小的数据块,每个数据块发送时会附上上个数据块的长度,最后使用一个零长度的块作为发送数据完成的标志。

优化五:虚拟主机的支持

在 HTTP/1.0 中,每个域名绑定了一个唯一的 IP 地址,因此一个服务器只能支持一个域名。但是随着虚拟主机技术的发展,需要实现在一台物理主机上绑定多个虚拟主机,每个虚拟主机都有自己的单独的域名,这些单独的域名都公用同一个 IP 地址。


因此,HTTP/1.1 的请求头中增加了 Host 字段,用来表示当前的域名地址,这样服务器就可以根据不同的 Host 值做不同的处理。

HTTP2

HTTP/1.1 的问题:

  • TCP 慢启动

  • 前面 TCP 介绍时,我们有介绍过 TCP 慢启动,当 TCP 连接刚建立时,数据的发送是缓慢的,当网络没有拥塞时才会逐渐加快速度。

  •       这样就会导致页面加载资源的性能会变慢。

  • 队头阻塞

  • HTTP 长连接默认是串行的,也就是后面的请求得等前面的请求响应了才能继续请求,这就会导致如果一个请求响应慢,就会拖累后面的请求,也就是著名的队头阻塞。

HTTP2 的优化:

多路复用

HTTP/2 的设计思路是:一个域名只使用一个 TCP 长连接来传输数据,并且数据传输是并行的,请求之间不存在等待的情况,服务器也可以随时返回响应,不需要保证顺序。

多路复用实现原理

HTTP2 增加了一个 HTTP 分帧层,将上层的 HTTP 请求进行拆分。



HTTP/2 的请求和接收过程如下:


  1. 浏览器准备好标准 HTTP 请求体。

  2. 第一步的数据经过分帧层处理,被转换为一个个带有请求 ID 编号的帧;

  3. 这些帧被发送给服务器(可以乱序);

  4. 服务器接收到所有帧之后,会将所有相同 ID 的帧合并为一条完整的请求信息。

  5. 然后服务器处理该条请求,同样的响应结果也被发送到分帧层进行处理。

  6. 浏览器接收到响应帧之后,会根据 ID 编号将帧的数据提交给对应的请求。


对比 HTTP1.1,数据格式变为:



HTTP2 数据帧格式



Type 字段的取值:


HTTP2 数据传输

HTTP 数据传输主要依赖两个概念:Stream 和 Frame。


一条 TCP 连接上有多个 Stream,一个 Stream 上有多个 Frame。

一个 HTTP 请求与响应对应一个 Stream,请求报文和响应报文会被分割成为多个 Frame。


  • 不同 Stream 的帧是可以并发乱序发送的,因为每个帧的头部会携带 Stream ID 信息,所以接收端可以通过 Stream ID 有序组装成 HTTP 消息。

  • 同一 Stream 内部的帧必须是严格有序的,这样客户端/服务端才能根据达到顺序还原报文。(由于 tcp 可以保证报文的有序,所以只要保证同一 stream 的报文是有序提交到 tcp 层的,就可以保证接收方收到时也是有序的)


例如下图所示:



其中:


  • 客户端向服务端请求的资源,属于客户端建立的 Stream,Stream ID 必须是奇数;

  • 服务端主动向客户端推送的资源,属于服务端建立的 Stream,Stream ID 必须是偶数;

  • 同一个连接中的 Stream ID 是不能复用的,只能顺序递增,所以当 Stream ID 耗尽时,需要发一个控制帧 GOAWAY,用来关闭 TCP 连接。

HTTP3

HTTP2 存在问题:

TCP 的队头阻塞



前面有提到,HTTP2 同一个域名的请求是跑在一个 TCP 上的,不同的 HTTP 请求采用 StreamID 进行区分。这样可以实现并发。


但是 HTTP 请求报文被分割成为 Frame 后,最终还是以 TCP 报文形式发出的。根据前面每天 5 分钟玩转计算机网络-(传输层 tcp)工作原理 我们知道 TCP 会保证报文可靠和顺序重组。


按照图中所示,假设在传输过程中 5 号报文丢失了,即使其余报文已经全部到达了,TCP 依旧不会把 3,2,6 报文提交给 HTTP 层,就会导致请求 1 和请求 3 被请求 2 阻塞了。


随着丢包率的增加,HTTP/2 的传输效率也会越来越差。有测试数据表明,当系统达到了 2% 的丢包率时,HTTP/1.1 的传输效率反而比 HTTP/2 表现得更好。这是因为 HTTP/1.1 对于同一个域名会开启 6 个 TCP 连接,即使一个请求阻塞,其余的 TCP 连接还可以继续使用。


TCP 建立连接的延时


由于 TCP 建立连接必须经历三次握手,并且有慢启动控制,导致初始请求无法弹射起步。

HTTP3 优化:

由于以上问题都是 TCP 的特性导致的,从 HTTP 设计已经无法再产生本质的改变,于是 HTTP3 就把目光放到了 UDP。


UDP 相比 TCP 有如下优点:


  • 无需三次握手,可以直接发送数据;

  • 简单,不存在慢启动、拥塞控制、流量控制;

  • 报头较小,UDP 的包头相对较小,仅包含源端口号和目标端口号等少量信息,这使得 UDP 在传输数据时的包头开销较小。


UDP 的缺点也很明显:


  • 不可靠:UDP 不保证数据传输的可靠性,也不保证数据的完整性;

  • 无拥塞控制:UDP 有数据就发送,不管网络负载和服务器负载,可能导致网络拥挤和服务器过载;


UDP 的有点部分可以解决 HTTP2 遇到的困境,但是简单的将 TCP 替换为 UDP 肯定也是不行的,毕竟没有人会想自己的请求没有任何保障,能不能达到服务端全靠缘分。


于是 Google 提出基于 UDP 设计新的可靠层,去弥补 UDP 的缺点,这个 QUIC,于是整体架构变为:


QUIC 特点:

  • 可靠传输

  • 由于 UDP 不提供可靠性的传输,所以 QUIC 在 UDP 的基础之上增加了一层来保证数据可靠性传输。它提供了数据包重传、拥塞控制以及其他一些 TCP 中存在的特性。

  • 安全保证

  • QUIC 使用 TLS1.3 进行安全加密,相较于早期版本 TLS1.2 有更多的优点,其中最重要的一点是减少了握手所花费的 RTT 个数。

  • 多路复用功能

  • QUIC 实现了在同一 TCP 连接上可以有多个独立的逻辑数据流(如下图)。实现了数据流的单独传输,就解决了 TCP 中队头阻塞的问题


HTTP/3 帧结构


相比于 HTTP2,HTTP3 使用了更加简单的帧结构。

HTTP 的安全机制

HTTP 最初设计时数据是明文在网络上传输的,也就是任何人只要拦截了网络,就可以不费吹灰之力获取到 HTTP 请求/响应内容,从而非法获取信息。


为了解决这个问题,提出了 HTTPS 概念,通过加密的形式去保护请求/响应内容,这样即使报文被劫持,也无法获取其中的内容。

SSL/TLS

HTTP 通过引入 SSL/TLS 层去加解密数据包,如图:



非对称加密:非对称加密

TLS 1.2 运行过程

SSL/TLS 协议的基本思路是采用 非对称加密+对称加密的综合模式。


第一步:客户端向服务端索要公钥(非对称加密) 

第二步:基于非对称加密,生成一个随机秘钥 

第三步:基于随机秘钥(对称加密)加密后续通话的报文


上面是一个粗略的执行过程,具体的执行细节类似 TCP 三次握手:

HTTPS 握手(TLS-ECDHE)


实际抓包 HTTPS 握手过程:



1、ClientHello:TCP 连接建立后,Client 会发出 ClientHello 请求,开始进行 TLS 握手。



2、ServerHello:服务端收到 ClientHello 请求后,会响应一个 ServerHello 请求。



可以看到服务端选择的是【TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384】,这是一系列加密算法的组合写法,含义如下:


  • 密钥协商算法使用 ECDHE;

  • 签名算法使用 RSA;

  • 握手后的通信使用 AES 对称算法,密钥长度 256 位,分组模式是 GCM;

  • 摘要算法使用 SHA384;


​3、Server Certificate:服务器把自己的证书发给客户端



4、Server Key Exchange:由于服务器选择了 ECDHE 算法,所以它会发送 Server Key Exchange 。



这一步服务器做了很多事情:


  • 生成随机数作为服务端的椭圆曲线的私钥,保留在本地,不发送给客户端;

  • 根据椭圆曲线规则和私钥生成公钥;

  • 用 RSA 签名算法给椭圆曲线公钥生成签名,保证其未被篡改;

  • 以上信息发送给客户端;


随后发送 ServeHelloDone 代表服务端消息发送结束。


5、Client Key Exchange:由于选择了 ECDHE 算法,所以客户端会发送 Client Key Exchange


  • 客户端收到服务端的响应后,会验证服务端的证书是否可靠;

  • 生成随机数作为客户端的椭圆曲线的私钥,保留在本地,不发送给服务端;

  • 根据椭圆曲线规则和私钥生成公钥;

  • 以上信息发送给服务端;


6、计算 Pre-Master :


到目前为止客户端和服务器分别拿到了如下参数:


客户端:


  - ClientRandom(客户端随机数)  - ServerRandom(服务端随机数)  - Server 椭圆曲线公钥
复制代码


服务端:


  - ClientRandom(客户端随机数)  - ServerRandom(服务端随机数)  - Client 椭圆曲线公钥
复制代码


客户端和服务端分别使用自己的椭圆曲线私钥和对方的椭圆曲线公钥根据 ECDHE 算法一阵算,算出一个新的随机数:Pre-Master(ECDHE 可以保证客户端和服务端算出来的 Pre-Master 值是一样的)


       然后客户端和服务端在分别根据 ClientRandom、ServerRandom、Pre-Master 生成最终的对称加密秘钥:Master Secret。


       后续的报文就通过之前协商的对称加密算法和 Master Secret 对报文进行加密。

TLS RSA 加密

其实最开始的 TLS 握手过程没有那么复杂,以前使用的是 RSA 传统的加密手段,但是由于无法保证前向安全所以逐渐淘汰了


握手过程如下:


  1. ClientHello:TLS 版本、客户端随机数、一系列加密算法;

  2. ServerHello:TLS 版本、服务端随机数、确定选择的加密算法;

  3. Server Certificate:服务器把自己的证书发给客户端;

  4. 客户端验证服务器证书并且提取出公钥,然后生成一个新的随机数,并且使用服务端的公钥加密该随机数,然后将加密后的随机数传递给服务端;

  5. 服务端收到新随机数后,使用私钥解密;

  6. 客户端和服务端就分别有了三个随机数:ClientRandom、ServerRandom、Pre-Master,在根据三个随机数生成 Master Secret。

  7. 后续的报文就通过之前协商的对称加密算法和 Master Secret 对报文进行加密。

TLS_ECDHE 优势

可以看出 TLS_RSA 跟 TLS_ECDHE 的主要区别在于 Pre-Master 的生成和交换过程:


TLS_RSA 的 Pre_Master 是客户端随机生成,然后服务器公钥加密,私钥解密。那么只要服务器的私钥泄漏了,那么所以的历史报文就有可能被破解。


TLS_ECDHE 的 Pre_Master 是临时生成一对公钥私钥,然后根据 ECDHE 计算出来的,即使被破解了也只影响本次通话,不会影响历史报文。

TLS 1.3 运行过程

TLS 1.2 极大的解决了 HTTP 的安全问题,不过随着互联网的发展,TLS 1.2 慢慢显露出来弊端,主要集中在性能和安全上。


性能问题


TLS 1.2 握手过程中,需要耗费两次往返消息(2-RTT)才能完成加密前置准备。这可能导致几十毫秒甚至上百毫秒的延迟,这对注重性能的程序是影响比较大的。


TLS 1.3 优化:



相比于 TLS 1.2,1.3 只需要一次往返消息(1-RTT)就可以完成加密准备:


1、ClientHello:TCP 连接建立后,Client 会发出 ClientHello 请求,开始进行 TLS 握手。


相比于 1.2,1.3 主要多了以下几个参数。



这里的客户端主要传递几个意图:


  1. TLS 1.2,1.3 我都支持,可以由你决定使用哪个;

  2. 为了方便我把椭圆曲线参数和对应的公钥都给你,用不用随你;

  3. 我支持这些类型的椭圆曲线,你要用的话只能用这个几个;


2、ServerHello:服务端收到 ClientHello 请求后,会响应一个 ServerHello 请求。



这里的服务端主要传递几个意图:


  1. 我们确定用 TLS1.3 吧;

  2. 我选择了椭圆曲线 x25519,对应的参数和公钥也给你了;


通过这样的形式客户端和服务端只需要两条消息就分别拿到了如下数据:


客户端:


  • ClientRandom(客户端随机数)

  • ServerRandom(服务端随机数)

  • Server 椭圆曲线公钥


服务端:


  • ClientRandom(客户端随机数)

  • ServerRandom(服务端随机数)

  • Client 椭圆曲线公钥


接下来就可以计算出 Pre-Master 和 Master Secret。而 TLS.1.2 需要交换 5 条消息才能做到。


安全问题


从 TLS 1.2 运行过程我们知道它支持很多加密算法,但是正是这些加密算法爆出了历史上很多安全漏洞:


  • RSA 密钥传输:不提供前向保密性

  • CBC 模式密码:BEAST 和 Lucky 13 攻击

  • RC4 流密码:在 HTTPS 中使用不安全

  • 任意 Diffie-Hellman 组:CVE-2016-0701

  • 导出密码:FREAK 和 LogJam 攻击


TLS 1.3 优化:


在 TLS 1.3 中对支持的加密算法进行精简,只保留了如下几种:


  • TLS_AES_128_GCM_SHA256

  • TLS_AES_256_GCM_SHA384

  • TLS_CHACHA20_POLY1305_SHA256

  • TLS_AES_128_CCM_SHA256

  • TLS_AES_128_CCM_8_SHA256

HTTP cookie 与 session

HTTP 最初设计时是无状态的,也就是请求之间没有关键性。这样的好处是方便扩展成集群,但是缺点也很明显:对于论坛,电商购物这类网站是需要知道用户是谁的场景,无状态 HTTP 就无法支持。


为了解决这个问题,HTTP 就设计了 Cookie,让 HTTP 有记忆能力。

Cookie

数据格式

我们前面说过 HTTP 头部是可以设置很多 KV 的数据的。Cookie 正是利用了这一点。


在 HTTP 头部中以 key=“cookie”,value=自定义 kv(不同的 kv 使用;分割)的形式存在。如图:


工作原理

当你第一次通过浏览器访问服务端时,服务端处理完业务逻辑后,为了标记你是谁就会生成一些 kv 的数据,然后在响应头里设置“Set-Cookie”=kv,如果存在多组 kv 那么就会有多个“Set-Cookie”=kv。如图:



     然后客户端收到响应后就把“Set-Cookie”里的值全部取出来,并且用;分割组成一条记录然后存储在内存或者本地磁盘。等下次请求的时候就会通过在请求头里设置“Cookie”=kv 再把数据带回去。


     通过这样的形式,就完成了记忆能力。

Cookie 属性

为了增强 Cookie 的能力,围绕 Cookie 可以设置一系列属性。


  1. 生命周期(超过一定时间就会失效)


Expires:用的是绝对时间点,例如上图的:expires=Sat, 10-Aug-2024 09:19:18 GMT (在 2024 年 8 月 10 日 09:19:18之后过期)Max-Age:优先级更高,用的是相对时间,单位是秒,浏览器用收到报文的时间点再加上 Max-Age,就可以得到失效的绝对时间。
复制代码


  1. 作用域(限制 cookie 只能发送给特定的服务器)


Domain:指定了 Cookie 所属的域名Path:指定了 Cookie 所属的路径。
复制代码


浏览器在发送 Cookie 前会从 URI 中提取出 host 和 path 部分,对比 Cookie 的属性。如果不满足条件,就不会在请求头里发送 Cookie。


  1. 安全


HttpOnly:该设置会限制Cookie 只能通过浏览器 HTTP 协议传输,禁止其他方式访问。例如document.cookie 等一切相关的 API都将无法操作Cookie,可以避免脚本攻击。SameSite:    SameSite=Strict可以严格限定 Cookie 不能随着跳转链接跨站发送;    SameSite=Lax允许 GET/HEAD 等安全方法,但禁止 POST 跨站发送;    通过上面两种形式可以防范“跨站请求伪造”(XSRF)攻击。Secure:表示这个 Cookie 仅能用 HTTPS 协议加密传输,明文的 HTTP 协议会禁止发送。但 Cookie 本身不是加密的,浏览器里还是以明文的形式存在。
复制代码

缺点

  1. 安全性问题

  2. 虽然可以通过上文提到的属性加强安全,但是增加了复杂度。并且由于还是明文存储在本地依旧有泄露风险。

  3. 大小受到限制

  4. 大多数浏览器对 Cookie 的大小有 4096 字节的限制

  5. 带宽消耗

  6. 每次 HTTP 请求都会携带 Cookie,如果 Cookie 存放的信息太多,就会造成流量带宽消耗。

  7. 客户端禁用 Cookie


客户端如果禁用了 Cookie 的,那么围绕 Cookie 设计的功能都将无法正常使用。

Session

为了解决 Cookie 的缺点,于是推出了 Session。

数据格式

Session 依旧使用散列表的形式存储数据,例如 Java 中的 HashMap。可以存储多个 kv 数据。

工作原理

与 Cookie 不同,Session 的 kv 数据是存储在服务端的,而 Cookie 的数据则是存储在客户端。


服务端会为每一个 Session 生成一个唯一 id(sessionid),然后把该 id 通过 Cookie 的形式返回给客户端,如图:



等下次再发起请求时,客户端就会通过 Cookie 字段将该 SessionId 带上,服务端就可以根据该 sessionId 找到对应的 kv 数据,从而完成记忆能力。



      如果客户端禁用了 Cookie 能力,依旧可以通过重写 url 的形式将 sessionid 带上(?sessionid=xxx)

WebSocket

HTTP 协议是一种请求 - 应答的通信模式,同时还是一种“被动”通信模式,也就是说服务器只能“被动”响应客户端的请求,无法主动向客户端发送数据。


    但是在互联网中,存在很多需要服务端主动向客户端推送数据的场景:即时消息、网络游戏以及飞书文档的协同编辑等。在没有 WebSocket 之前,只能通过客户端【轮询】的形式去不停地问”服务端是否有数据给我“,在请求量比较少的情况下这么做是没有问题的,但是在高并发的情况下非常容易导致服务端过载。


     为了解决该问题,于是设计了 WebSocket,即允许客户端主动向服务端推送数据,也允许服务端主动向客户端推送数据。

报文结构

RFC 文档中对 WebSocket 报文的格式定义如下所示:



1、FIN :消息结束的标志位,表示数据发送完毕。一个消息可以拆成多个帧,接收方看到 FIN 后,就可以把前面的帧拼起来,组成完整的消息。


2、RSV1、2、3 :三位是保留位,目前没有任何意义,但必须是 0。


3、Opcode :表示帧类型:


1:表示帧内容是纯文本


2:表示帧内容是二进制数据


8:是关闭连接


4、MASK :表示帧内容是否使用异或操作(xor)做简单的加密。目前的 WebSocket 标准规定,客户端发送数据必须使用掩码,而服务器发送则必须不使用掩码。


5、Payload len :表示帧内容的长度。


6、Masking-key :掩码密钥,由上面的标志位 MASK 决定的,如果使用掩码就是 4 个字节的随机数,否则就不存在。


7、Payload Data(continued):真正存放数据的地方。

工作原理

WebSocket 并没有从零开始设计,反而是站在 HTTP 协议的基础上进行设计。WebSocket 也需要进行握手后,才能正式收发数据。

客户端:

WebSocket 的握手是一个标准的 HTTP GET 请求,但要带上两个协议升级的专用头字段:


Connection: Upgrade(表示要求协议升级)Upgrade: websocket(表示要升级成 WebSocket 协议)
复制代码


同时还增加了两个额外的认证用头字段:


Sec-WebSocket-Key:一个 Base64 编码的 16 字节随机数,作为简单的认证密钥;Sec-WebSocket-Version:协议的版本号,当前必须是 13。
复制代码


最终报文如下:


服务端:

服务器收到 HTTP 请求报文,根据上面的四个字段,意识到这是一个 WebSocket 升级请求,于是采用 WebSocket 的方式进行处理。


1、构造一个特殊的 101 Switching Protocols 响应报文;


2、生成 Sec-WebSocket-Accept:取出请求头里 Sec-WebSocket-Key 对应的值,加上专用的 UUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11,然后计算对应的 SHA-1 摘要。


客户端收到响应报文,就可以用同样的算法,比对值是否相等,如果相等,则握手成功。


最终报文如下:


完整握手交互抓包


websocket 报文帧


推荐阅读

1、原来阿里字节员工简历长这样

2、一条SQL差点引发离职

3、MySQL并发插入导致死锁


如果你也觉得我的分享有价值,记得点赞或者收藏哦!你的鼓励与支持,会让我更有动力写出更好的文章哦!

更多精彩内容,请关注公众号「云舒编程」


发布于: 刚刚阅读数: 5
用户头像

云舒编程

关注

公众号 云舒编程,大白话分享技术原理 2020-09-23 加入

字节、阿里资深工程师。 做过营销、支付、百万级Feed流优化、权限系统、网关。 专注于技术原理分享,用最简单的话分享最复杂的技术原理

评论

发布
暂无评论
万字图解 | 深入揭秘HTTP工作原理_多路复用_云舒编程_InfoQ写作社区