文章首发于微信公众号:云舒编程
关注公众号获取: 1、大厂项目分享 2、各种技术原理分享 3、部门内推
前言
想必不少同学在面试过程中,会遇到「在浏览器中输入www.baidu.com后,到网页显示,其间发生了什么 」类似的面试题。
本专栏将从该背景出发,详细介绍数据包从HTTP层->TCP层->IP层->网卡->互联网->目的地服务器 这中间涉及的知识。
本系列文章将采用自底向上的形式讲解每层的工作原理和数据在该层的处理方式。
系列文章
每天5分钟玩转计算机网络-(数据链路层、物理层)工作原理
每天5分钟玩转计算机网络-(网络层ip)工作原理
每天5分钟玩转计算机网络-(传输层tcp)工作原理
通过上一篇文章每天5分钟玩转计算机网络-(传输层tcp)工作原理的介绍,我们知道了传输层的基本工作原理。
本篇将会详解对应用层进行介绍。
通过本文你可学到:
- 什么是超文本传输协议
- HTTP协议格式构成
- HTTP协议的发展历程
-
- 缓存技术
- 长连接
- https
- 多路复用
- 数据压缩
- QUIC协议
- cookie与session
- WebSocket
- GPT打字机输出原理(流式输出)
什么是超文本传输协议
HTTP 全称是HyperText Transfer Protocol,也叫超文本传输协议。
HTTP 于 1991 年提出的,主要用于学术交流,当时的目的也很简单,就是用来在网络之间传递 HTML文本的内容,所以被称为超文本传输协议。
一个简单的HTTP请求流程如下:
- HTTP 都是基于 TCP 协议的,所以客户端先要根据 IP 地址、端口和服务器建立 TCP 连接,而建立连接的过程就是 TCP 协议三次握手的过程。
- 建立好连接之后,会发送一个 GET/POST 请求,如GET /index.html用来获取 index.html。
- 服务器接收请求信息之后,读取对应的 HTML 文件,并将数据以 ASCII 字符流返回给客户端。
- HTML 文档传输完成后,断开连接。
在互联网早期的时候,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协议格式构成】部分标准组成。而是如下:
http
GET /mypage.html
http
<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、提供什么语言的文件以及文件的具体编码。最终发送出来的请求头内容如下:
http
accept: text/html //期望服务器返回 html 类型的文件
accept-encoding: gzip, deflate, br //期望服务器可以采用 gzip、deflate 或者 br 其中的一种压缩方式
accept-Charset: ISO-8859-1,utf-8 //期望返回的文件编码是 UTF-8 或者 ISO-8859-1
accept-language: zh-CN,zh //期望页面的优先语言是中文
服务器接收到浏览器发送过来的请求头信息之后,会根据请求头的信息来准备响应数据。不过有时候会有一些意外情况发生,比如浏览器请求的压缩类型是 gzip,但是服务器不支持 gzip,只支持 br 压缩,那么它会通过响应头中的 content-encoding 字段告诉浏览器最终的压缩类型,也就是说最终浏览器需要根据响应头的信息来处理数据。下面是一段响应头的数据信息:
http
content-encoding: br //服务器采用了 br 的压缩方法
content-type: text/html; charset=UTF-8 //服务器返回的是 html 文件,并且该文件的编码类型是 UTF-8。
HTTP/1.1
优化一:缓存
对于重复性的请求,HTTP会缓存结果,这样当一样的请求发起时直接从本地获取缓存,不需要请求服务端,节省了资源也提高了性能。
HTTP的缓存是通过在HTTP请求头/响应头增加字段实现的,具体又分为两种:
1、强制缓存
强缓存指的是只要浏览器判断缓存没有过期,则直接使用浏览器的本地缓存,主动权在浏览器这边。
类似这样的请求就是使用了强制缓存。
强制缓存由由响应请求设置的,通过:
- Cache-Control:设置相对时间,优先级最高;
- Expires:设置绝对时间;
两个头部字段控制。
流程如下:
- 浏览器请求服务器中时,会先判断是否存在cache,以及相应的Cache-Control 是否过期。如果没有,则使用该缓存,否则重新请求服务器;
- 重新请求服务器后,会再次更新 Response 头部的 Cache-Control。
2、协商缓存
协商缓存就是强制缓存过期后,浏览器继续请求服务器使用缓存的机制,主要分为以下两种情况:
- 协商缓存生效,返回304,代表资源未更新,旧的缓存可以继续使用;
- 协商缓存失效,返回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是资源文件在服务器最后被修改的时间。
客户端请求服务端时会设置如下请求头:
http
If-Modified-Since:Last-Modified
//Last-Modified是客户端第一次请求服务端时,服务端返回的
服务器收到该请求,发现请求头含有If-Modified-Since字段,则会根据If-Modified-Since的字段值与该资源在服务器的最后被修改时间做对比,
- 若服务器的资源最后修改时间大于If-Modified-Since的字段值,则重新返回资源,状态码为200;
- 否则返回304,代表资源无更新,可以继续使用缓存文件。
Etag/If-None-Match
Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成)。
客户端请求服务端时会设置如下请求头:
http
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 持久连接。
http
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 的请求和接收过程如下:
- 浏览器准备好标准HTTP请求体。
- 第一步的数据经过分帧层处理,被转换为一个个带有请求 ID 编号的帧;
- 这些帧被发送给服务器(可以乱序);
- 服务器接收到所有帧之后,会将所有相同 ID 的帧合并为一条完整的请求信息。
- 然后服务器处理该条请求,同样的响应结果也被发送到分帧层进行处理。
- 浏览器接收到响应帧之后,会根据 ID 编号将帧的数据提交给对应的请求。
对比HTTP1.1,数据格式变为:
HTTP2 数据帧格式
image.png
名称 | 长度 | 描述 |
---|---|---|
Length | 3 字节 | 帧的长度 |
Type | 1 字节 | 帧的类型 |
Flags | 1 字节 | 帧的标识 |
R | 1 字节 | 保留位,不需要设置 |
Stream Identifier | 31 位 | 每个流的唯一ID |
Frame Payload | 不固定 | 存放数据 |
Type字段的取值:
帧类型 | 类型编码 | 用途 |
---|---|---|
DATA | 0x0 | 传递HTTP包体 |
HEADERS | 0x1 | 传递HTTP头部 |
PRIORITY | 0x2 | 指定Stream流的优先级 |
RST_STREAM | 0x3 | 终止Stream流 |
SETTINGS | 0x4 | 修改连接或者Stream流的配置 |
PUSH_PROMISE | 0x5 | 服务端推送资源时描述请求的帧 |
PING | 0x6 | 心跳检测,兼具计算RTT往返时间的功能 |
GOAWAY | 0x7 | 优雅的终止连接或者通知错误 |
WINDOW_UPDATE | 0x8 | 实现流量控制 |
CONTINUATION | 0x9 | 传递较大HTTP头部时的持续帧 |
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 中队头阻塞的问题
- 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 :
到目前为止客户端和服务器分别拿到了如下参数:
客户端:
http
- ClientRandom(客户端随机数)
- ServerRandom(服务端随机数)
- Server 椭圆曲线公钥
服务端:
http
- ClientRandom(客户端随机数)
- ServerRandom(服务端随机数)
- Client 椭圆曲线公钥
客户端和服务端分别使用自己的椭圆曲线私钥和对方的椭圆曲线公钥根据 ECDHE 算法一阵算,算出一个新的随机数:Pre-Master(ECDHE可以保证客户端和服务端算出来的Pre-Master值是一样的)
然后客户端和服务端在分别根据ClientRandom、ServerRandom、Pre-Master生成最终的对称加密秘钥:Master Secret。
后续的报文就通过之前协商的对称加密算法和Master Secret对报文进行加密。
TLS RSA加密
其实最开始的TLS握手过程没有那么复杂,以前使用的是RSA传统的加密手段,但是由于无法保证前向安全所以逐渐淘汰了
握手过程如下:
- ClientHello:TLS版本、客户端随机数、一系列加密算法;
- ServerHello:TLS版本、服务端随机数、确定选择的加密算法;
- Server Certificate:服务器把自己的证书发给客户端;
- 客户端验证服务器证书并且提取出公钥,然后生成一个新的随机数,并且使用服务端的公钥加密该随机数,然后将加密后的随机数传递给服务端;
- 服务端收到新随机数后,使用私钥解密;
- 客户端和服务端就分别有了三个随机数:ClientRandom、ServerRandom、Pre-Master,在根据三个随机数生成Master Secret。
- 后续的报文就通过之前协商的对称加密算法和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主要多了以下几个参数。
这里的客户端主要传递几个意图:
- TLS 1.2,1.3我都支持,可以由你决定使用哪个;
- 为了方便我把椭圆曲线参数和对应的公钥都给你,用不用随你;
- 我支持这些类型的椭圆曲线,你要用的话只能用这个几个;
2、ServerHello:服务端收到ClientHello请求后,会响应一个ServerHello请求。
这里的服务端主要传递几个意图:
- 我们确定用TLS1.3吧;
- 我选择了椭圆曲线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可以设置一系列属性。
- 生命周期(超过一定时间就会失效)
go
Expires:用的是绝对时间点,例如上图的:expires=Sat, 10-Aug-2024 09:19:18 GMT (在 2024 年 8 月 10 日 09:19:18之后过期)
Max-Age:优先级更高,用的是相对时间,单位是秒,浏览器用收到报文的时间点再加上 Max-Age,就可以得到失效的绝对时间。
- 作用域(限制cookie只能发送给特定的服务器)
g
Domain:指定了 Cookie 所属的域名
Path:指定了 Cookie 所属的路径。
浏览器在发送 Cookie 前会从 URI 中提取出 host 和 path 部分,对比 Cookie 的属性。如果不满足条件,就不会在请求头里发送 Cookie。
- 安全
go
HttpOnly:该设置会限制Cookie 只能通过浏览器 HTTP 协议传输,禁止其他方式访问。例如document.cookie 等一切相关的 API都将无法操作Cookie,可以避免脚本攻击。
SameSite:
SameSite=Strict可以严格限定 Cookie 不能随着跳转链接跨站发送;
SameSite=Lax允许 GET/HEAD 等安全方法,但禁止 POST 跨站发送;
通过上面两种形式可以防范"跨站请求伪造"(XSRF)攻击。
Secure:表示这个 Cookie 仅能用 HTTPS 协议加密传输,明文的 HTTP 协议会禁止发送。但 Cookie 本身不是加密的,浏览器里还是以明文的形式存在。
缺点
-
安全性问题
虽然可以通过上文提到的属性加强安全,但是增加了复杂度。并且由于还是明文存储在本地依旧有泄露风险。
-
大小受到限制
大多数浏览器对 Cookie 的大小有 4096 字节的限制
-
带宽消耗
每次HTTP请求都会携带Cookie,如果Cookie存放的信息太多,就会造成流量带宽消耗。
-
客户端禁用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 请求,但要带上两个协议升级的专用头字段:
h
Connection: Upgrade(表示要求协议升级)
Upgrade: websocket(表示要升级成 WebSocket 协议)
同时还增加了两个额外的认证用头字段:
h
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报文帧