HTTP权威指南读书笔记
2018-06-25 17:57:44 阿炯

本站赞助商链接,请多关照。 HTTP 简介

HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web)服务器传输超文本到本地浏览器的传送协议。HTTP是一个基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)。

HTTP 工作原理

HTTP协议工作于客户端-服务端架构为上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。

Web服务器有:Apache服务器,IIS服务器(Internet Information Services)等。

Web服务器根据接收到的请求后,向客户端发送响应信息。

HTTP默认端口号为80,但是你也可以改为8080或者其他端口。

HTTP三点注意事项:
1、HTTP是无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
2、HTTP是媒体独立的:这意味着,只要客户端和服务器知道如何处理的数据内容,任何类型的数据都可以通过HTTP发送。客户端以及服务器指定使用适合的MIME-type内容类型。
3、HTTP是无状态:HTTP协议是无状态协议,无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

以下图表展示了HTTP协议通信流程:


HTTP 消息结构

HTTP是基于客户端/服务端(C/S)的架构模型,通过一个可靠的链接来交换信息,是一个无状态的请求/响应协议。

一个HTTP"客户端"是一个应用程序(Web浏览器或其他任何客户端),通过连接到服务器达到向服务器发送一个或多个HTTP的请求的目的。

一个HTTP"服务器"同样也是一个应用程序(通常是一个Web服务,如Apache Web服务器或IIS服务器等),通过接收客户端的请求并向客户端发送HTTP响应数据。

HTTP使用统一资源标识符(Uniform Resource Identifiers, URI)来传输数据和建立连接。

一旦建立连接后,数据消息就通过类似Internet邮件所使用的格式[RFC 5322]和多用途Internet邮件扩展(MIME)[RFC 2045]来传送。

客户端请求消息

客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据4个部分组成,下图给出了请求报文的一般格式。


服务器响应消息

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


URL组成

大多数的URL方案的URL语法都建立在这个由9个部分构成的通用格式上:
<scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<query>#<frag>,几乎没有哪个URL包含了所有这些组件。URL最重要的3个方案(scheme)、主机(host)和路径(path)


URI
每一个web服务器资源都有一个名字,这个名字叫资源标识符。他就像一个邮政地址一样,在世界范围内唯一标识并定位信息资源。URI有两种形式,分别为URL和URN。

URL
统一资源定位符(URL,英语UniformResourceLocator的缩写)也被称为网页地址,是因特网上标准的资源的地址。
URL的格式由下列三部分组成:
第一部分是协议(或称为服务方式);
第二部分是存有该资源的主机IP地址(有时也包括端口号);
第三部分是主机资源的具体地址,如目录和文件名等。
第一部分和第二部分之间用"://"符号隔开,第二部分和第三部分用“/”符号隔开。第一部分和第二部分是不可缺少的,第三部分有时可以省略。现在几乎所有的URI都是URL

URN
统一资源名称 (Uniform Resource Name, URN),唯一标识一个实体的标识符,但是不能给出实体的位置。系统可以先在本地寻找一个实体,在它试着在Web上找到该实体之前。它也允许Web位置改变,然而这个实体却还是能够被找到。URN 可以提供一种机制,用于查找和检索定义特定命名空间的架构文件。尽管普通的 URL 可以提供类似的功能,但是在这方面,URN 更加强大并且更容易管理,因为 URN 可以引用多个 URL。与 URL 不同,URN 与地址无关。URN 和 URL 都属于 URI。URN在web中主要应用是下拉菜单的制作。使用URN时下拉菜单的易扩展性将会得到很大的提高。


HTTP如何使用TCP连接

世界上几乎所有的HTTP通信都是有TCP/IP承载的,它是一种常用的分组交换网络分层协议集。HTTP连接实际就是TCP连接及其使用规则。web浏览器与服务器通过TCP连接的交互如下图:


TCP流是通过分段、由IP分组传送

TCP数据是通过IP分组(或IP数据报)的小数据块来发送的。HTTP就是“HTTP OVER TCP OVER IP”这个协议栈中的最顶层。其安全版本HTTPS就是在HTTP和TCP之间插入了一个密码加密层(称为TLS或SSL)。HTTP要传送一条报文的时候,会以流的形式将报文数据的内容通过一条打开的TCP连接按照顺序传输。TCP收到数据流后,会将数据流分成被称之为段的小数据块,并将段封装在ip分组中,通过因特网进行传输。


每个IP分组都包括:
一个IP分组收首部(包含源和目的IP地址、长度等)
一个TCP段首部((包含TCP端口号、控制标志等)
一个TCP数据块

保持TCP连接持续不断地运行

 一个TCP连接是由4个值来识别的,<源ip地址:源端口    目的ip地址:目的端口> ,这4个值唯一地定义了一条连接。两条不同的TCP连接不能拥有4个完全相同的地址组件值(但不同连接的部分组件可以拥有相同的值),TCP是通过端口号来保持所有这些连接的正确运行的。


操作系统提供了一套操作TCP连接的工具,TCP客户端和服务器是通过tcp套接字接口来进行通讯的,如下图所示

 
如上图,web服务器等待连接(S4),客户端根据URL判定出IP地址和端口号,并建立一条到服务器的TCP连接C3,(建立连接要花费一些时间,时间长短取决于服务器距离的远近、服务器的负载情况,以及因特网的拥挤程度),一旦建立了连接,客户端发送http请求C5,服务器读取请求S6。一旦服务器获取了整条请求报文,就会对请求进行处理,执行请求的动作S7,并将数据写回客户端,客户端读取数据C6,客户端读取数据,并对响应数据进行处理。

对TCP性能的考虑

HTTP紧挨着TCP,位于其上层,所以HTTP事务的性能在很大程度上取决于底层TCP通道的性能。

1.HTTP事务处理时的延时

下图描绘了HTTP事务主要的连接、传输以及处理时延:


从上图可以看出整个HTTP事务的延时主要有以下:

1).解析时延   DNS解析与DNS缓存

客户端首先需要根据URL确定Web服务器的IP地址和端口号,如果最近没有对URL中的主机名进行访问,通过DNS将URL中的主机名转换为IP地址可能会花费数十秒的时间。如果是近期访问过的主机名,那么在HTTP客户端的DNS缓存中,就会保存该主机名对应的IP地址。

2).连接时延   TCP连接的建立

接下来,客户端会向服务器发送一条TCP连接请求,并等待服务器回送一个请求接受应答。每条新的TCP连接都会有连接新建时延,这个时间通常最多只有一两秒钟,但是如果数百个HTTP事务的话,这个值会快速叠加上去。

3).传输时延   HTTP请求发送    HTTP响应返回

一旦连接建立起来之后,客户端就会通过新建立的TCP管道来发送HTTP请求,数据到达时,web服务器会从TCP链接中读取请求报文,并对其进行处理。因特网传输请求报文,以及服务器处理请求报文都需要时间。

4).处理时延   HTTP报文处理

服务器会回送HTTP响应,这也需要花费时间。

2.TCP网络时延、瓶颈

1).TCP连接的握手时延

建立一条新的TCP连接时,甚至是在发送任意数据前,TCP软件之间会交换一系列的IP分组,对连接的有关参数进行沟通。如果连接只用来传送少量的数据,这些交换过程就会严重降低HTTP的性能。

在发送数据之前,TCP要传送两个分组来建立连接:


TCP连接握手需要经过以下几个步骤:
1)请求新的TCP连接时,客户端要服务器发送一个小的TCP分组,这个分组中设置了一个特殊的SYN标记,说明这是一个连接请求。

2)如果服务器接收了连接,就会对一些连接参数进行计算,并向客户端回送一个TCP分组,这个分组中的SYN和ACK标记都被置位,说明连接请求已被接受。

3)最后,客户端向服务器回送一条确认信息,通知它连接已成功建立。现在的TCP栈都允许客户端在这个确认分组中发送数据。

通常HTTP事务的交换数据量都不会太多,所以SYN/SYN+ACK握手会产生一个可测量的时延。最后可能的结果是:小的HTTP事务可能会在TCP建立上花费50%,或更多的时间。后面会介绍如何通过重用现存连接来减小这种TCP建立时延所造成的影响。

2).延迟确认机制:保证数据传输的成功

由于因特网自身无法确保可靠的分组传输(路由器超负荷的话,可以随意丢弃分组),所以TCP实现了自己的确认机制来确保数据的成功传输。

每个TCP段都有一个序列号和一个数据完整性校验和。每个段的接收者收到完好的段时,都会向发送者回送一个小的确认分组。如果发送者没有在指定的窗口时间内收到确认信息,发送者就会认为分组已被破坏或损毁,并重发数据。

由于确认报文很小,所以TCP允许服务器在发往客户端的或者是客户端发往服务器的数据分组中队其进行“捎带”,将返回的确认信息和输出的数据分组结合在一起,更有效地利用网络。HTTP的双峰特征-请求应答行为降低了捎带信息的可能性,通常,延迟确认算法会引入相当大的时延,根据操作系统的不同,可以调整或禁止延迟确认算法。

为了增加确认报文找到同向传输数据分组的可能性,很多TCP栈都实现了一种“延迟确认”的算法。延迟确认算法会在一个特定的窗口时间(通常是100~200ms)内将输出确认放在缓冲区中,以寻找能够捎带它的输出分组。如果在时间段内没有输出分组符合条件,那么确认信息就放到单独的分组中进行传送。

3).TCP慢启动

TCP连接会随着时间进行自我调谐,起初会限制连接的最大速度,如果数据成功传输,会随着时间的推移提高传输的速度。这种调谐被称为TCP慢启动,用于防止因特网的突然过载和拥塞。

TCP慢启动限制了一个TCP端点在任意时刻可以传输的分组数。简单来说,每成功接收一个分组,发送端就有了发送另外两个分组的权限。当一个HTTP事务由大量数据要发送的时候,是不能一次性将所有分组都发送出去的,必须先发送一个分组,等待确认,然后可以发送两个分组,每个分组都必须被确认,这样就可以发送4个分组了,以此类推。这种方法被称为“打开拥塞窗口”。由于已调谐的连接要更快一些,可以通过“持久连接”重用现存的连接。

4).数据聚集的Nagle算法

TCP发送大量包含数据的分组,会严重影响网络性能。Nagle算法试图在发送分组之前,绑定大量TCP数据,鼓励发送全尺寸的段,将数据缓存直至其他分组都被确认或者缓存中已足够全尺寸的段才会发送。这样就引入了一些性能问题。

(1).小的HTTP报文可能无法填满一个分组,可能会因为等待不会到来的数据产生时延。[小的报文产生延迟]

(2).与延迟确认算法交互存在问题。Nagle算法阻止数据发送,直到有确认分组抵达,但确认分组自身会被延迟确认算法延迟,因为它在等待捎带它的数据包。[与延迟确认算法交互存在问题]

5).TIME_WAIT累积与端口耗尽

当某个TCP端点关闭TCP连接时,会在内存中维护一个小的控制块,用来记录所关闭的连接的IP地址和端口号,这个信息通常只能存在一个小时间段。这个算法可以防止在短时间内创建、关闭具有相同IP和端口号的连接。

TIME_WAIT的作用:允许老的重复分组在网络中消失,防止最后ACK的丢失,可靠地实现TCP全双工通信的终止。

即使没有遇到端口耗尽问题,也要特别小心有大量连接处于打开状态的情况,在有大量打开连接或控制块的情况下,有些操作系统的速度会严重减缓。

持久连接(keep-alive)

Web客户端经常会打开到同一个站点的连接,而且相当一部分指向其他对象的超链通常都指向同一个站点。因此,初始化对某服务器HTTP请求的应用程序很可能会在不久的将来对那台服务器发起更多的请求。

因此允许HTTP设备在事务处理结束之后将TCP连接保持在打开状态,以便为将来的HTTP请求重用现存的连接。如果服务器愿意为下一条请求保持打开状态,就在响应中包含相同的首部,如果响应中没有Connection:Keep-alive首部,客户端就认为服务器不支持Keep-alive,会在发回响应报文后关闭连接。

同时可以在响应首部中指定timeout(服务器希望将连接保持在活跃状态的时间)与max(服务器还希望为多少个事务保持此连接的活跃状态).Keep-alive首部是可选的,但只有在提供了Connection:Keep-alive时才能使用它,以下是一个Keep-alive响应首部的例子。

Connection:Keep-Alive

Keep-Alive:max=5,timeout=120

注意:在HTTP/1.0中,keep-alive并不是默认使用的。客户端必须发送一个Connection:Keep-alive请求首部来激活Keep-alive连接。通过检测响应中是否包含Connection:Keep-alive响应首部,客户端可以判断服务器是否会在发出响应之后关闭连接。


HTTP 1.1 的分块传输编码


HTTP 1.1引入分块传输编码提供了以下几点好处:

HTTP分块传输编码允许服务器为动态生成的内容维持HTTP持久连接。通常,持久链接需要服务器在开始发送消息体前发送Content-Length消息头字段,但是对于动态生成的内容来说,在内容创建完之前是不可知的。[动态内容,content-length无法预知]

分块传输编码允许服务器在最后发送消息头字段。对于那些头字段值在内容被生成之前无法知道的情形非常重要,例如消息的内容要使用散列进行签名,散列的结果通过HTTP消息头字段进行传输。没有分块传输编码时,服务器必须缓冲内容直到完成后计算头字段的值并在发送内容前发送这些头字段的值。[散列签名,需缓冲完成才能计算]

HTTP服务器有时使用压缩(gzip或deflate)以缩短传输花费的时间。分块传输编码可以用来分隔压缩对象的多个部分。在这种情况下,块不是分别压缩的,而是整个负载进行压缩,压缩的输出使用本文描述的方案进行分块传输。在压缩的情形中,分块编码有利于一边进行压缩一边发送数据,而不是先完成压缩过程以得知压缩后数据的大小。[gzip压缩,压缩与传输同时进行]

后附电子书籍下载:

HTTP权威指南.HTTP.The.Definitive.Guide.英文原版.pdf


HTTP权威指南.HTTP.The.Definitive.Guide.中文简版.pdf



下文目标是帮助读者理清 HTTP 的演化过程,说说 HTTP 变化的那些事,可做为上文的补充。转自又拍云

HTTP 的起源

HTTP 最初是 Tim BernersLee 1989 年在欧洲核子研究组织(CERN)所发起的。Tim BernersLee 提出了一种能让远隔两地的研究者们共享知识的设想,这个设想的基本理念是:借助多文档之间相互关联形成的超文本(HyperText),连成可相互参阅的 WWW(World Wide Web,万维网)。用于传输的超文本传输协议(HyperText Transfer Protocol),即 HTTP 由此诞生,WWW 这一名称是 Web 浏览器当年用来浏览超文本的客户端应用程序时的名称。现在则用来表示这一系列的集合,也可简称为 Web。

HTTP 本身是一个简单的请求-响应协议,它通常运行在 TCP 之上。从整个网络模型来看,HTTP 是应用层的一个协议。在 OSI 七层模型中,HTTP 位于最上层。它并不涉及数据包的传输,只是规定了客户端和服务器之间的通信格式。定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。请求和响应消息的头以 ASCII 码形式给出。

HTTP 采用 BS 架构,也就是浏览器到服务器的架构,客户端通过浏览器发送 HTTP 请求给服务器,服务器经过解析响应客户端的请求。就是这个简单实用的模型,使得 HTTP 这个基于 TCP/IP 的协议迅速推广。

HTTP/0.9 到 HTTP/1.1

HTTP 的演化并不是一蹴而就的。当年 HTTP 的出现主要是为了解决文本传输的难题。由于协议本身非常简单,于是在此基础上设想了很多应用方法并投入了实际使用。现在 HTTP 已经超出了 Web 这个框架的局限,被运用到了各种场景里。

HTTP/0.9

HTTP 协议最早的一个版本是 1990 年发布的 HTTP/0.9。

前面说到,HTTP 于 1989 年问世,那时的 HTTP 并没有作为正式的标准被建立。这时的 HTTP 其实含有 HTTP/1.0 之前版本的意思,因此被称为 HTTP/0.9。这个版本只有一个命令:GET。通过 GET 可以获取服务器的资源,比如请求服务器根目录下的 index.html 文件。这个版本的协议规定,服务器只能回应 HTML 格式的字符串,不能回应其它格式,也就是说图像、视频等多媒体资源,在 HTTP/0.9 这个版本上是无法进行传输的。

HTTP/1.0

HTTP 正式作为标准被公布是在 1996 年的 5 月,版本被命名为 HTTP/1.0,并记载于 RFC1945。虽说是初期标准,但该协议标准至今仍被广泛使用在服务器端。

HTTP/1.0 版本发布,增加了 POST 命令和 HEAD 命令,丰富了浏览器与服务器的互动手段。这个版本的 HTTP 协议可以发送任何格式的内容,包括传输文字、图像、视频、文件等,这为互联网的大发展奠定了基础。HTTP/1.0 除了增加了请求方法以及对发送文件的支持之外,还增加了格式的改变。除了数据部分,每次通信都必须包括头信息(HTTP header),用来描述一些元数据。另外还增加了状态码、多字符集支持、多部分发送(multi-part type)、权限(authorization)、缓存(cache)、内容编码(content encoding)等等。

HTTP/1.1

HTTP/1.0 版也并不是完美的,它的主要缺点是,每一次建立 TCP 连接只能发送一个请求。发送数据完毕,连接就关闭,如果还要请求其他资源,就必须再新建一个连接。如果多次请求,势必就会对服务器产生较大的资源性能损耗。

1997 年 1 月公布的 HTTP/1.1 是目前主流的 HTTP 协议版本。当初的标准是 RFC2068,之后发布的修订版 RFC2616 就是当前的最新版本。

其中最著名的是 1999 年 6 月公布的 RFC 2616,定义了 HTTP 协议中现今广泛使用的一个版本——HTTP/1.1。这个版本最大的变化就是将持久化连接加入了 HTTP 标准,即 TCP 连接默认不关闭,可以被多个请求复用。此外 HTTP/1.1 版还新增了许多方法,例如:PUT、PATCH、HEAD、OPTIONS、DELETE。得到进一步完善的HTTP/1.1 版本,一直沿用至今。

HTTP 协议简单介绍

请求

客户端发送一个 HTTP 请求到服务器,请求消息包括以下格式:

请求行(request line)、请求头部(header)、空行和请求数据四个部分组成。

Get 请求例子

1 GET / HTTP/1.1
2 Host: www.freeoa.net
3 User-Agent: curl/7.52.1
4 Accept: /

第一部分:请求行,用来说明请求类型,要访问的资源以及所使用的 HTTP 版本。

第二部分:请求头部,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息

从第二行起为请求头部,HOST 将指出请求的目的地。User-Agent,服务器端和客户端脚本都能访问它,它是浏览器类型检测逻辑的重要基础。该信息由你的浏览器来定义,并且在每个请求中自动发送等等。

第三部分:空行,请求头部后面的空行是必须的

即使第四部分的请求数据为空,也必须有空行。

第四部分:请求数据也叫主体,可以添加任意的其他数据。

这个例子的请求数据为空。

响应消息

一般情况下,服务器接收并处理客户端发过来的请求后,会返回一个 HTTP 的响应消息,HTTP 响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。

状态码

状态代码有三位数字组成,第一个数字定义了响应的类别,共分五种类别:
1xx:指示信息–表示请求已接收,继续处理
2xx:成功–表示请求已被成功接收、理解、接受
3xx:重定向–要完成请求必须进行更进一步的操作
4xx:客户端错误–请求有语法错误或请求无法实现
5xx:服务器端错误–服务器未能实现合法的请求

安全性与HTTPS

HTTP 的诞生是为了解决信息传递和共享的问题,并没有考虑到互联网高速发展后面临的安全问题。

一般来说 HTTP 从 TCP 三次握手后,便开始了数据传输。由于 HTTP 本身以明文形式来传输数据,并不具备任何数据加密、身份校验的机制。同时下层协议并不对数据安全性、保密性提供保证。所以在网络传输的过程中,任意节点的第三方都可以随意劫持流量、篡改数据或窃取信息。

HTTP 无法确保数据的保密性、完整性和真实性,已经不能适应现代互联网应用的安全需求。

随着 Web 的日益壮大,HTTP 的使用呈巨额增长趋势,对信息安全的需求也愈来愈迫切,SSL(Secure SocketsLayer ,安全套接层)应运而生。

当对于安全需求,首先想到的就是对信息进行加密。SSL ,安全套接层,顾名思义是在 TCP 上提供的安全套接字层。其位于应用层和传输层之间,应用层数据不再直接传递给传输层而是传递给 SSL 层,SSL 层对从应用层收到的数据进行加密,利用数据加密、身份验证和消息完整性验证机制,为网络上数据的传输提供安全性保证。HTTPS 便是指 Hyper Text Transfer Protocol over SecureSocket Layer。

谈到具体实施上,业内通常采用的一般有对称加密和非对称加密。采用何种方式进行加密?如何判断服务器未被篡改?如何传递加密密钥?带着这样的问题,我们来看看 HTTPS 的工作流程。

1、客户端发起 HTTPS 请求

这个没什么好说的,就是用户在浏览器里输入一个 HTTPS 网址,然后连接到 server 的 443 端口。

2、服务端的配置

采用 HTTPS 协议的服务器必须要有一套数字证书,可以自己制作,也可以向组织申请,区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面(Let's Encrypt 就是个不错的选择,免费的 SSL 证书)。

这套证书其实就是一对公钥和私钥,如果对公钥和私钥不太理解,可以想象成一把钥匙和一个锁头,只是全世界只有你一个人有这把钥匙,你可以把锁头给别人,别人可以用这个锁把重要的东西锁起来,然后发给你,因为只有你一个人有这把钥匙,所以只有你才能看到被这把锁锁起来的东西。

3、传送证书

这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等等。

4、客户端解析证书

这部分工作是有客户端的 TLS 来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等等,如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随机值,然后用证书对该随机值进行加密,就好像上面说的,把随机值用锁头锁起来,这样除非有钥匙,不然看不到被锁住的内容。

5、传送加密信息

这部分传送的是用证书加密后的随机值,目的就是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。

6、服务段解密信息

服务端用私钥解密后,得到了客户端传过来的随机值(私钥),然后把内容通过该值进行对称加密,所谓对称加密就是,将信息和私钥通过某种算法混合在一起,这样除非知道私钥,不然无法获取内容,而正好客户端和服务端都知道这个私钥,所以只要加密算法够彪悍,私钥够复杂,数据就够安全。

7、传输加密后的信息

这部分信息是服务段用私钥加密后的信息,可以在客户端被还原。

8、客户端解密信息

客户端用之前生成的私钥解密服务段传过来的信息,于是获取了解密后的内容,整个过程第三方即使监听到了数据,也束手无策。简单说完了 HTTPS 的工作流程,让我们再将注意力放在 SSL 的演化上。

1994年,Netscape 创建了 SSL 协议的原始规范并逐步发布协议改进版本。1995 年发布 SSL 2.0。1996年,Netscape 和 Paul Kocher 共同设计发布 SSL 3.0 协议,获得互联网广泛认可和支持。因特网工程任务组(IETF)接手负责该协议,并将其重命名为 TLS(传输层安全)协议。

我们看到,SSL 2.0 规范是在 1995 年左右发布的,而 SSL 3.0 是在 1996 年 11 月发布的。有趣的是,SSL 3.0 是在 RFC 6101 中描述的,该 RFC 于 2011 年 8 月发布。它位于历史类别中,该类别通常是被考虑和被丢弃的文档想法,或者是在决定记录它们时已经具有历史意义的协议(根据 IETF 说明)。在这种情况下,有一个描述 SSL 3.0 的 IETF 文档是很有必要的,因为在其可以被用作规范参考。

再来看看,SSL 是如何激发 TLS 的发展的。后者在 1996 年 11 月以 draft-ietf-tls-protocol-00 宣告开始。它经历了六个草案版本,并于 1999 年初作为 RFC 2246 - TLS 1.0 正式发布。

在 1995 和 1999 年间,SSL 和 TLS 协议用于保护互联网上的 HTTP 通信,这作为事实上的标准运行良好。直到 1998 年 1 月,随着 draft-ietf-tls-HTTPs-00 的发布,HTTPS 的正式标准化过程才开始。该工作于 2000 年 5 月以 RFC 2616 - HTTP 上的 TLS 的发布结束。

TLS 在 2000 到 2007 年间继续发展,标准化为 TLS 1.1 和 1.2。直至七年后,TLS 的下一个版本开始进行,该版本在 2014 年四月被采纳为 draft-ietf-tls-tls13-00,并在 28 份草稿后,于 2018 年八月出了完成版本 RFC 8446 - TLS 1.3。

改进与HTTP2

回到 HTTP 本身。在很长一段时间里,HTTP/1.1 已经足够好了(确实是,现在仍应用最为广泛),但是,Web 不断变化的需求使得我们需要一个更好更合适的协议。

HTTP/1.1 自从 1997 年发布以来,我们已经使用 HTTP/1.x 相当长一段时间了。但随着互联网近十年爆炸式的发展,从当初网页内容以文本为主,到现在以富媒体(如图片、声音、视频)为主,而且对页面内容实时性高要求的应用越来越多(比如聊天、视频直播),所以当时协议规定的某些特性,已经逐渐无法满足现代网络的需求了。


如果你有仔细观察,那些最流行的网站首页所需要下载资源的话,会发现一个非常明显的趋势。近年来加载网站首页需要下载的数据量在逐渐增加,并已经超过了 2100K。但在这里我们更关心的是:平均每个页面为了完成显示与渲染所需要下载的资源数也已经超过了 100 个。

基于此,在 2010 年到 2015 年,谷歌通过实践一个实验性的 SPDY 协议,证明了一个在客户端和服务器端交换数据的另类方式。其收集了浏览器和服务器端的开发者的焦点问题,明确了响应数量的增加和解决复杂的数据传输。在启动 SPDY 这个项目时预设的目标是:
页面加载时间 (PLT) 减少 50%。
无需网站作者修改任何内容。
将部署复杂性降至最低,无需变更网络基础设施。
与开源社区合作开发这个新协议。
收集真实性能数据,验证这个实验性协议是否有效。为了达到降低目标,减少页面加载时间的目标,SPDY 引入了一个新的二进制分帧数据层,以实现多向请求和响应、优先次序、最小化及消除不必要的网络延迟,目的是更有效地利用底层 TCP 连接。

HTTP/1.1 有两个主要的缺点:安全不足和性能不高,由于背负着 HTTP/1.x 庞大的历史包袱,所以协议的修改,兼容性是首要考虑的目标,否则就会破坏互联网上无数现有的资产。


而如上图所示,SPDY 位于 HTTP 之下,TCP 和 SSL 之上,这样可以轻松兼容老版本的 HTTP 协议同时可以使用已有的 SSL 功能。

SPDY 协议在 Chrome 浏览器上证明可行以后,就被当作 HTTP/2 的基础,主要特性都在 HTTP/2 之中得到继承。

于是时间来到 2015 年,HTTP/2.0 问世。

HTTP/2 相比 HTTP/1.1 的修改并不会破坏现有程序的工作,但是新的程序可以借由新特性得到更好的速度。HTTP/2 保留了 HTTP/1.1 的大部分语义,例如请求方法、状态码、乃至 URI 和绝大多数 HTTP 头部字段一致。而 HTTP/2 采用了新的方法来编码、传输客户端和服务器间的数据。

来看看 HTTP/2 的具体特点:

二进制分帧层:在应用层与传输层之间增加一个二进制分帧层,以此达到在不改动 HTTP 的语义,HTTP 方法、状态码、URI 及首部字段的情况下,突破 HTTP/1.1 的性能限制,改进传输性能,实现低延迟和高吞吐量。在二进制分帧层上,HTTP/2.0 会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码,其中 HTTP1.x 的首部信息会被封装到 Headers 帧,而我们的 request body 则封装到 Data 帧里面。

多路复用:对于 HTTP/1.x,即使开启了长连接,请求的发送也是串行发送的,在带宽足够的情况下,对带宽的利用率不够,HTTP/2.0 采用了多路复用的方式,可以并行发送多个请求,提高对带宽的利用率。

数据流优先级:由于请求可以并发发送了,那么如果出现了浏览器在等待关键的 CSS 或者 JS 文件完成对页面的渲染时,服务器却在专注的发送图片资源的情况怎么办呢?HTTP/2.0 对数据流可以设置优先值,这个优先值决定了客户端和服务端处理不同的流采用不同的优先级策略。

服务端推送:在 HTTP/2.0 中,服务器可以向客户发送请求之外的内容,比如正在请求一个页面时,服务器会把页面相关的 logo,CSS 等文件直接推送到客户端,而不会等到请求来的时候再发送,因为服务器认为客户端会用到这些东西。这相当于在一个 HTML 文档内集合了所有的资源。

头部压缩:使用首部表来跟踪和存储之前发送的键值对,对于相同的内容,不会再每次请求和响应时发送。

HTTP/2.0 支持明文 HTTP 传输,而 SPDY 强制使用 HTTPS。HTTP/2.0 消息头的压缩算法采用 HPACK,而非 SPDY 采用的 DEFLATE。

QUIC 和 HTTP3

虽然 HTTP/2 提高了网页的性能,但是并不代表它已经是完美的了,HTTP/3 就是为了解决 HTTP/2 所存在的一些问题而被推出来的。随着时间的演进,越来越多的流量都往手机端移动,手机的网络环境会遇到的问题像是封包丢失机率较高、较长的 Round Trip Time (RTT)和连接迁移等问题,都让主要是为了有线网路设计的HTTP/TCP协议遇到贫颈。

我们可以看两个典型的问题。

第一握手带来的消耗。HTTP/2 使用 TCP 协议来传输的,而如果使用 HTTPS 的话,还需要使用 TLS 协议进行安全传输,而使用 TLS 也需要一个握手过程,这样就需要有两个握手延迟过程:
在建立 TCP 连接的时候,需要和服务器进行三次握手来确认连接成功,也就是说需要在消耗完 1.5 个 RTT 之后才能进行数据传输。

进行 TLS 连接,TLS 有两个版本——TLS 1.2 和 TLS 1.3,每个版本建立连接所花的时间不同,大致是需要1~2个 RTT。

总之,在传输数据之前,我们需要花掉 3~4 个 RTT。

第二,TCP 的队头阻塞并没有得到彻底解决。我们知道,为了实现多路复用,在 HTTP/2 中多个请求是跑在一个 TCP 管道中的。但当出现了丢包时,HTTP/2 的表现反倒不如 HTTP/1.X 了。因为 TCP 为了保证可靠传输,有个特别的丢包重传机制,丢失的包必须要等待重新传输确认,HTTP/2 出现丢包时,整个 TCP 都要开始等待重传,那么就会阻塞该 TCP 连接中的所有请求。而对于 HTTP/1.1 来说,可以开启多个 TCP 连接,出现这种情况反到只会影响其中一个连接,剩余的 TCP 连接还可以正常传输数据。

至此,我们很容易就会想到,为什么不直接去修改 TCP 协议?其实这已经是一件不可能完成的任务了。因为 TCP 存在的时间实在太长,已经充斥在各种设备中,并且这个协议是由操作系统实现的,更新起来非常麻烦,不具备显示操作性。

HTTP/3 乘着 QUIC 来了。


HTTP3 是基于 QUIC 的协议,如上图。先说 QUIC,QUIC 协议是 Google 提出的一套开源协议,基于 UDP 来实现,直接竞争对手是 TCP 协议。QUIC 协议的性能非常好,甚至在某些场景下可以实现 0-RTT 的加密通信。

在 Google 关于 QUIC 的文件中提到,与 HTTP/2 相比,QUIC 主要具有下列优势:
Reduce connection establishment latency (减少连接建立时间)

Improved congestion control (改进拥塞控制)

Multiplexing without head-of-line blocking (没有队头阻塞的多路复用)

Forward error correction (修复之前的错误)

Connection migration(支持网络迁移)

多路复用,避免队头阻塞

这句话说起来很容易,但理解起来并不那么显然,要想理解 QUIC 协议到底做了什么以及这么做的必要性,我想还是从最基础的 HTTP/1.0 聊起比较合适。

Pipiline

根据谷歌的调查, 现在请求一个网页,平均涉及到 80 个资源,30 多个域名。考虑最原始的情况,每请求一个资源都需要建立一次 TCP 请求,显然不可接受。HTTP 协议规定了一个字段 Connection,不过默认的值是 close,也就是不开启。

早在 1999 年提出的 HTTP 1.1 协议中就把 Connection 的默认值改成了Keep-Alive,这样同一个域名下的多个 HTTP 请求就可以复用同一个 TCP 连接。这种做法被称为 HTTP Pipeline,优点是显著的减少了建立连接的次数,也就是大幅度减少了 RTT。

以上面的数据为例,如果 80 个资源都要走一次 HTTP 1.0,那么需要建立 80 个 TCP 连接,握手 80 次,也就是 80 个 RTT。如果采用了 HTTP 1.1 的 Pipeline,只需要建立 30 个 TCP 连接,也就是 30 个 RTT,提高了 62.5% 的效率。

Pipeline 解决了 TCP 连接浪费的问题,但它自己还存在一些不足之处,也就是所有管道模型都难以避免的队头阻塞问题。

队头阻塞

我们再举个简单而且直观的例子,假设加载一个 HTML 一共要请求 10 个资源,那么请求的总时间是每一个资源请求时间的总和。最直观的体验就是,网速越快请求时间越短。然而如果某一个资源的请求被阻塞了(比如 SQL 语句执行非常慢)。但对于客户端来说所有后续的请求都会因此而被阻塞。


队头阻塞(Head of line blocking,下文简称 HOC)说的是当有多个串行请求执行时,如果第一个请求不执行完,后续的请求也无法执行。比如上图中,如果第四个资源的传输花了很久,后面的资源都得等着,平白浪费了很多时间,带宽资源没有得到充分利用。

因此,HTTP 协议允许客户端发起多个并行请求,比如在笔者的机器上最多支持六个并发请求。并发请求主要是用于解决 HOC 问题,当有三个并发请求时,情况会变成这样:


可见虽然第四个资源的请求被阻塞了,但是其他的资源请求并不一定会被阻塞,这样总的来说网络的平均利用率得到了提升。

支持并发请求是解决 HOC 问题的一种方案,这句话没有错。但是我们要理解到:“并发请求并非是直接解决了 HOC 的问题,而是尽可能减少 HOC 造成的影响”,以上图为例,HOC 的问题依然存在,只是不会太浪费带宽而已。

有读者可能会好奇,为什么不多搞几个并发的 HTTP 请求呢?刚刚说过笔者的电脑最多支持 6 个并发请求,谷歌曾经做过实验,把 6 改成 10,然后尝试访问了三千多个网页,发现平均访问时间竟然还增加了 5% 左右。这是因为一次请求涉及的域名有限,再多的并发 HTTP 请求并不能显著提高带宽利用率,反而会消耗性能。

SPDY 的做法

有没有办法解决队头阻塞呢?

答案是肯定的。SPDY 协议的做法很值得借鉴,它采用了多路复用(Multiplexing)技术,允许多个 HTTP 请求共享同一个 TCP 连接。我们假设每个资源被分为多个包传递,在 HTTP 1.1 中只有前面一个资源的所有数据包传输完毕后,后面资源的包才能开始传递(HOC 问题),而 SPDY 并不这么要求,大家可以一起传输。这么做的代价是数据会略微有一些冗余,每一个资源的数据包都要带上标记,用来指明自己属于哪个资源,这样客户端最后才能把他们正确的拼接起来。不同的标记可以理解为图中不同的颜色,每一个小方格可以理解为资源的某一个包。

TCP 窗口

是不是觉得 SPDY 的多路复用已经够厉害了,解决了队头阻塞问题?很遗憾的是,并没有,而且我可以很肯定的说,只要你还在用 TCP 链接,HOC 就是逃不掉的噩梦,不信我们来看看 TCP 的实现细节。我们知道 TCP 协议会保证数据的可达性,如果发生了丢包或者错包,数据就会被重传。于是问题来了,如果一个包丢了,那么后面的包就得停下来等这个包重新传输,也就是发生了队头阻塞。当然 TCP 协议的设计者们也不傻,他们发明了滑动窗口的概念:


这样的好处是在第一个数据包(1-1000) 发出后,不必等到 ACK 返回就可以立刻发送第二个数据包。可以看出图中的 TCP 窗口大小是 4,所以第四个包发送后就会开始等待,直到第一个包的 ACK 返回。这样窗口可以向后滑动一位,第五个包被发送。

如果第一、二、三个的包都丢失了也没有关系,当发送方收到第四个包时,它可以确信一定是前三个 ACK 丢了而不是数据包丢了,否则不会收到 4001 的 ACK,所以发送方可以大胆的把窗口向后滑动四位。

滑动窗口的概念大幅度提高了 TCP 传输数据时抗干扰的能力,一般丢失一两个 ACK 根本没关系。但如果是发送的包丢失,或者出错,窗口就无法向前滑动,出现了队头阻塞的现象。

QUIC 是如何做的


QUIC 协议基于 UDP 实现,我们知道 UDP 协议只负责发送数据,并不保证数据可达性。这一方面为 QUIC 的多路复用提供了基础,另一方面也要求 QUIC 协议自己保证数据可达性。

SPDY 为各个数据包做好标记,指明他们属于哪个 HTTP 请求,至于这些包能不能到达客户端,SPDY 并不关心,因为数据可达性由 TCP 协议保证。既然客户端一定能收到包,那就只要排序、拼接就行了。QUIC 协议采用了多路复用的思想,但同时还得自己保证数据的可达性。

TCP 协议的丢包重传并不是一个好想法,因为一旦有了前后顺序,队头阻塞问题将不可避免。而无序的数据发送给接受者以后,如何保证不丢包,不错包呢?这看起来是个不可能完成的任务,不过如果把要求降低成:最多丢一个包,或者错一个包。事情就简单多了,操作系统中有一种存储方式叫 RAID 5,采用的是异或运算加上数据冗余的方式来保证前向纠错(FEC: Forward Error Correcting)。QUIC 协议也是采用这样的思想,这里不再赘述。

利用冗余数据的思想,QUIC 协议基本上避免了重发数据的情况。当然 QUIC 协议还是支持重传的,比如某些非常重要的数据或者丢失两个包的情况。

少RTT,请求更快速

前面说到,一次 HTTPS 请求,它的基本流程是三次 TCP 握手外加四次 SSL/TLS 握手,也就是需要三个 RTT。但是 QUIC 在某些场景下,甚至能够做到 0RTT。

首先介绍下什么是 0RTT。所谓的 0RTT 就是通信双方发起通信连接时,第一个数据包便可以携带有效的业务数据。而我们知道,这个使用传统的TCP是完全不可能的,除非你使能了 TCP 快速打开特性,而这个很难,因为几乎没人愿意为了这个收益去对操作系统的网络协议栈大动手脚。未使能 TCP 快速打开特性的TCP传输第一笔数据前,至少要等1个RTT。

我们这里再说说 HTTP2。对于 HTTP2 来说,本来需要一个额外的 RTT 来进行协商,判断客户端与服务器是不是都支持 HTTP2,不过好在它可以和 SSL 握手的请求合并。这也导致了一个现象,就是大多数主流浏览器仅支持 HTTPS2 而不单独支持 HTTP2。因为 HTTP2 需要一个额外的 RTT,HTTPS2 需要两个额外的 RTT,仅仅是增加一个 RTT 就能获得数据安全性,还是很划算的。

TCP 快速打开

何谓 TCP 快速打开,即客户端可以在发送第一个 SYN 握手包时携带数据,但是 TCP 协议的实现者不允许将把这个数据包上传给应用层。这主要是为了防止 TCP 泛洪攻击

因为如果 SYN 握手的包能被传输到应用层,那么现有的防护措施都无法防御泛洪攻击,而且服务端也会因为这些攻击而耗尽内存和 CPU。当然 TCP 快速打开并不是完全不可行的,人们设计了 TFO (TCP Fast Open),这是对 TCP 的拓展,不仅可以在发送 SYN 时携带数据,还可以保证安全性。

TFO 设计了一个 Cookie,它在第一次握手时由 server 生成,Cookie 主要是用来标识客户端的身份,以及保存上次会话的配置信息。因此在后续重新建立 TCP 连接时,客户端会携带 SYN + Cookie + 请求数据,然后不等 ACK 返回就直接开始发送数据。


服务端收到 SYN 后会验证 Cookie 是否有效,如果无效则会退回到三次握手的步骤,如下图所示:


同时为了安全起见,服务端为每个端口记录了一个值 PendingFastOpenRequests,用来表示有多少请求利用了 TFO,如果超过预设上限就不再接受。

关于 TFO 的优化,可以总结出三点内容:
TFO 设计的 Cookie 思想和 SSL 恢复握手时的 Session Ticket 很像,都是由服务端生成一段 Cookie 交给客户端保存,从而避免后续的握手,有利于快速恢复。

第一次请求绝对不会触发 TFO,因为服务器会在接收到 SYN 请求后把 Cookie 和 ACK 一起返回。后续客户端如果要重新连接,才有可能使用这个 Cookie 进行 TFO

TFO 并不考虑在 TCP 层过滤重复请求,以前也有类似的提案想要做过滤,但因为无法保证安全性而被拒绝。所以 TFO 仅仅是避免了泛洪攻击(类似于 backlog),但客户端接收到的,和 SYN 包一起发来的数据,依然有可能重复。不过也只有可能是 SYN 数据重复,所以 TFO 并不处理这种情况,要求服务端程序自行解决。这也就是说,不仅仅要操作系统的支持,更要求应用程序(比如 MySQL)也支持 TFO。

TFO 使得 TCP 协议有可能变成 0-RTT,核心思想和 Session Ticket 的概念类似:将当前会话的上下文缓存在客户端。如果以后需要恢复对话,只需要将缓存发给服务器校验,而不必花费一个 RTT 去等待。结合 TFO 和 Session Ticket 技术,一个本来需要花费 3 个 RTT 才能完成的请求可以被优化到一个 RTT。如果使用 QUIC 协议,我们甚至可以更进一步,将 Session Ticket 也放到 TFO 中一起发送,这样就实现了 0-RTT 的对话恢复。

QUIC 是怎么做的


让我们看看 QUIC 是怎么做的。

首先声明一点,如果一对使用 QUIC 进行加密通信的双方此前从来没有通信过,那么 0-RTT 是不可能的,即便是 QUIC 也是不可能的。

QUIC 握手的过程需要一次数据交互,0-RTT 时延即可完成握手过程中的密钥协商,比 TLS 相比效率提高了 5 倍,且具有更高的安全性。在握手过程中使用 Diffie-Hellman 算法协商初始密钥,初始密钥依赖于服务器存储的一组配置参数,该参数会周期性的更新。初始密钥协商成功后,服务器会提供一个临时随机数,双方根据这个数再生成会话密钥。

具体握手过程如下:
(1) 客户端判断本地是否已有服务器的全部配置参数,如果有则直接跳转到(5),否则继续

(2) 客户端向服务器发送 inchoate client hello(CHLO) 消息,请求服务器传输配置参数

(3) 服务器收到 CHLO,回复 rejection(REJ) 消息,其中包含服务器的部分配置参数

(4) 客户端收到 REJ,提取并存储服务器配置参数,跳回到(1)

(5) 客户端向服务器发送 full client hello 消息,开始正式握手,消息中包括客户端选择的公开数。此时客户端根据获取的服务器配置参数和自己选择的公开数,可以计算出初始密钥。

(6) 服务器收到 full client hello,如果不同意连接就回复 REJ,同(3);如果同意连接,根据客户端的公开数计算出初始密钥,回复 server hello(SHLO)消息,SHLO 用初始密钥加密,并且其中包含服务器选择的一个临时公开数。

(7) 客户端收到服务器的回复,如果是 REJ 则情况同(4);如果是 SHLO,则尝试用初始密钥解密,提取出临时公开数

(8) 客户端和服务器根据临时公开数和初始密钥,各自基于 SHA-256 算法推导出会话密钥

(9) 双方更换为使用会话密钥通信,初始密钥此时已无用,QUIC 握手过程完毕。之后会话密钥更新的流程与以上过程类似,只是数据包中的某些字段略有不同。


IETF 将 QUIC 发布为 RFC 9000

2021年5月消息,IETF(互联网工程任务组)已将 QUIC 发布为 RFC 9000,并由 RFC 9001, RFC 9002 和 RFC 8999 提供支持。这意味着 QUIC 第一版已正式确定,并且部署 QUIC 所使用的版本也已经从临时草稿版本转变为新的第一版(HTTP/3 作为运行于 QUIC 之上的 HTTP 也即将发布)。RFC Editor 页面显示,目前 RFC 9000 处于“提案标准 (PROPOSED STANDARD)”状态,尚未成为正式标准。

QUIC(Quick UDP Internet Connections 的缩写,读作“quick”)是降低延迟、可靠且安全的新一代互联网传输协议,被认为可取代当今最常用的传输协议 TCP。最初由 Google 的 Jim Roskind 设计,2012年实现并部署,2013年随着实验范围的扩大而公开发布,并向 IETF 提交。2015年6月,QUIC 规范的互联网草案提交给 IETF 进行标准化。2016年,QUIC 工作组成立。2018年10月,IETF 的 HTTP 工作组和 QUIC 工作组共同决定将 QUIC 上的 HTTP 映射称为 "HTTP/3",以提前使其成为全球标准。QUIC 旨在提供几乎等同于 TCP 连接的可靠性,但延迟大大减少。原因之一是它在连接创建期间大大减少开销,QUIC 使用 UDP 协议作为其基础,不包括丢失恢复。



写在最后

想起有一个名言:计算机领域没有什么问题是加一层解决不了的,如果有,就再加一层。网络模型本来就是层层累加,到了 Web 得以快速生动的展现给人们以丰富的内容。从 HTTP 的演变过程中,我们可以看到中间又累加了若干层。不知道以后,又会是怎么样呢?

大家会发现,笔者在文中不止一次提到了演变这个词。是的,这是来自达尔文进化论中的理论。在笔者看来,“物竞天择,适者生存”的演变理论和计算机领域的技术变化是很类似的,只不过在这里,不是天择,而是人择。由市场,由用户来选择。不知道接下来,作为选择者的我们,又将怎样主导技术的走向?