HTTP/2之旅 (翻译)

Posted zhangrunhao

tags:

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

Journey to HTTP/2
HTTP/2

距离我上一次通过博客写作以来, 经过了很长的一段安静的时间. 因为一直没有足够的时间投入其中. 直到现在有了一些空闲的时间, 我想利用他们写一些HTTP相关的文章.

HTTP是一种协议, 每一个web开发者都应该知道他是如何推进整个网络的 并应该清楚的知道他是如何帮助你开发更好的应用.

什么是HTTP

首先, 什么是HTTP? HTTP是一种基于TCP/IP的应用层的传输协议, 规定了客户端和服务器端如何进行通信的. 定义了在物联网中是请求和传送的内容. 对于应用层的协议, 我理解的只是一层抽象的协议, 让主机(客户端和服务器)之间的交流标准化, 并且依赖于TCP/IP来完成客户端之间的请求和响应.TCP默认使用80端口, 也可以使用其他的端口. HTTPS使用过的是443端口.

HTTP/0.9 一个班机(开始的协议)(1991)

第一个HTTP的版本是HTTP/0.9在1991年之前推出. 那是一种非常简单的协议, 含有一个简单的被称为GET的方法. 如果一个客户端通过访问服务器上的一些网页, 他会发出一个下面这种的简单请求.

GET /index.html

服务器返回的内容如下面展示的

(response body)
(connection closed)

这就是服务器获得的请求, 在响应中返回一个HTML, 只要内容开始传输, 那响应就会关闭. 他是

  • 无头响应
  • GET只是一个请求方法
  • 响应一个HTML

    正如你看到的, 协议真的没什么, 除了作为未来发展的一个踏板.

HTTP/1.0-1996

在1996年, 下一个HTTP版本, 即HTTP/1.0版本被开发, 大大超过了上一个版本.

不同于HTTP/0.9只能定义HTML响应, HTTP/1.0能够定义其他响应格式, 即图片, 视频文件, 普通文本和其他任何的内容类型. 他增加了更多的方法(即, HEADPOST), 请求和响应的格式没有改变, HTTP头部可以在请求和响应都增加, 定义额外的状态码, 引入字符集的支持, 多部分类型, 作者, 缓存, 内容格式化并且支持更多

下面是一个简单的HTTP/1.0的请求和响应看起来大概如此:

GET / HTTP/1.0
Host: kamranahmed.info
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS)
Accept: */*

正如你看到的, 通过这个请求, 客户端也可以发送他的个人信息, 支持的响应类型内容. 在HTTP/0.9中客户端并没有办法发送这些信息, 因为没有头部.

对于上面的请求可能有如下的示例响应

HTTP / 1.0 200 OK
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84

(response body)
(connection closed)

每一个响应的开始都是HTTP/1.0(HTTP后面跟着的是版本号), 然后是状态码200, 后面跟着的事原因短语(如果你需要的话, 也可以是对于状态码的描述)

在这个新的版本中, 请求和响应头, 依旧都是使用ASCII进行编码, 但是响应主体可以使用任何类型, 即, 图片, 视频, 普通文本和其他的任何内容类型. 所以, 现在服务器可以发送任何类型的响应给客户端; 在HTTP引入后不久, "超文本"一词, 在HTTP中变得并不适用. HMTP或者超媒体传输协议也许更加适用于场景, 但是, 我想, 我们还是坚持使用生命这个名字.

HTTP/1.0一个主要的缺点是在每一个链接中使用不同的请求方式. 结果就是, 无论什么时候, 客户端都是为了从服务器获得一些东西, 他会打开一个新的TCP链接, 稍后一个简单的请求会完全使用这个链接. 然后这个链接就关闭了. 无论下一个请求是什么, 都会打开一个新的链接. 为什么坏呢? 很好, 让我们假设, 你浏览的网站有10张图片, 5个样式文件, 和5javascript文件, 当网页打开的时候, 需要一共进行20次请求. 因为请求一旦被满足, 服务器就会关闭链接. 将会有一系列独立的20个链接, 每个链接都在自己独立的链接上提供服务. 大量的链接导致严重的性能损失, 因为建立一个新的TCP造成明显的性能损失, 因为三次握手的建立启动非常慢.

三次握手 (Three-way Handshake)

三次握手的简单建立过程: 所有的TCP链接都是通过三次握手开始的, 也就是客户端和服务器在发送应用数据之前, 发送一系列的数据包.

  • SYN - 客户端挑选一个随机数, 我们称之为x, 然后发送给服务器端.
  • SYN ACK - 服务器接收到请求之后, 发送一个ACK包返回给客户端, 也是一个随机的数字, 我们把服务器选出的数字称之为y, 并且和x+1, 这里的x是通过客户端发送给服务器的.
  • ACK - 客户端将从服务器收到的数字y增加, 返回一个ACK的数据包, 包含一个数字y+1.

    一次完整的三次握手的过程就完成了, 客户端和服务器端的数据就可以开始传输了. 需要注意的是, 客户端一旦发送完最后一个ACK数据包, 就立即开始发送应用数据, 但是服务器端需要等到最后一个ACK包接受完成才会去响应请求.

    技术图片

需要注意, 这张图片有一个严重的问题, 最后一次通过客户端发送的数据包ACK, 这次握手应该只包括y+1, 也就是, 应该使用ACK: Y+1替代ACK: x+1, y+1

然而, HTTP / 1.0的一些方案尝试通过增加一个请求头Connection: keep-alive去解决这个问题. 那意味着告诉服务器: "你好, 服务器, 不要关闭这个链接, 我还需要它", 但因为没有广泛使用, 所以这个问题依旧存在.

除了无连接, HTTP也是一个无状态的协议, 也就是服务器不会存储有关客户端的信息, 所以每一个请求都必须含有服务器能够完成请求的独立信息, 和任何老的请求没有关系. 导致了, 大量分开的请求在客户端打卡的时候, 需要发送一些多余数据, 增加了网络带宽的使用.

HTTP / 1.1 - 1999

仅仅3年, 就在1999年发布了下一个版本, HTTP / 1.1, 对上个版本进行了大幅改进. 对于HTTP/1.0的主要改进包括:

  • 新的HTTP方法增加了PUT, PATCH, ‘OPTIONS‘, ‘DELETE‘
  • 主机名表示, 在HTTP/1.0中请求头中的Host并不是必须的, 但在HTTP/1.1就是必须的了.
  • 上面提到的持久链接: 在HTTP/1.0中, 每个链接是唯一一个请求, 只要请求完成了, 就关闭了, 导致了严重的性能浪费和一些潜在的问题. HTTP/1.1引入了持久链接, 也就是链接默认是不关闭的, 会一直保持打开, 允许多个连续的请求. 可以利用请求头上的Connection: close关闭链接. 客户端通常在最后一次请求中发送这个请求头来关闭连接状态.
  • 开始支持管道流Pipelining, 就是客户端可以发送多个请求到服务端, 不需要等待服务器在同一个连接上的响应, 并且服务器端在街道请求之后, 会遵循一样的顺序返回响应. 但是客户端如何知道这是第一个响应下载完成的点. 和下一个响应何时开始. 为了解决这个问题, 必须在头部使用Content -Length, 那可以让客户端区分出相应结束的地方, 并且可以开始等待下一个响应.
  • 应该注意的是, 为了从持续连接和管道流中获利, 响应中的Content-Length必须是可用的, 因为这可以让客户端知道传输完成, 并可以继续开始下一次请求(普通连续的请求方式)或者开始等待下一次响应(当管道流可以使用的时候)
  • 当这种方式依旧有个问题: 若数据是动态的, 服务器没有办法提前知道内容大小. 这种请求, 你的确不能使用持续连接. 为了解决这个问题, HTTP/1.1动态引入了动态编码. 在这种情况下, 服务器并不能通过分开编码省略内容长度. 然而, 如果这些方法都不能使用, 链接在最后一次请求后必须关闭.
  • 当服务器并不能在传输开始的时候计算出Content-Length, 会对内容进行分块传输, 那意味着一块一块的发送数据, 并且对发送的每一个快添加一个Content-Length, 当所有数据块发送完成的时候, 也就是这次传输完成了, 就会发送一个空的数据块, 就是一个Content-Lenght是0的数据块, 标志这客户端的这次传输完成了. 为了标志出客户端的传输, 服务器端应该在请求头上添加一个Transfer-Encoding: chunked.
  • 不像HTTP/1.0中只有一个基础的认证, HTTP/1.1A还包括了摘要和代理认证.
  • 缓存
  • 字节范围
  • 字符集合
  • 语言谈判(Language negotiation? 这特么是什么啊?)
  • 客户端cookies
  • 支持增强压缩
  • 新的状态码
  • 等等

    我并不准备在这篇文章里面, 完全展开素有HTTP/1.1的功能. 你可以通过自己去了解更多. 我推荐你阅读Key differences between HTTP/1.0 and HTTP/1.1, 还还有一个不错的original RFC

    HTTP/1.1在1999年发布后, 已经存在很多年了. 即使它能够不错的提高性能, 但网络世界每天都在变化, 它有些力不从心. 如今加载一个网页特别消耗资源. 一个简单的网页至少打开30个链接. 即使HTTP/1.1引入了持久连接, 为什么这么连接呢? 因为在HTTP/1.1中任何时间, 都只能有一个未完成的链接. 在HTTP/1.1尝试通过管道流水线操作(pipelining)去解决, 但因为**线头阻塞(head-of-line-blocking)**的原因没能解决问题, 指的是, 如果缓慢和繁重的请求可能会阻塞后面的请求, 一但某个管道中的请求被阻塞了, 那就不能不等到下一次请求被满足. (TODO: 这里没有很好的理解管道流的概念). 为了解决在HTTP/1.1`中的这些缺点, 开发者开始实行变通的方法. 例如, 使用精灵图, 在对CSS中的图片进行编码, 唯一一个极大的CSS/JavaScript文件, 主域分割(domain sharding)等

SPDY - 2009

Google带头开始尝试新的协议, 来提高web速度, 提升web的安全性, 降低网页加载延迟. 在2009年, 他们发布了SPDY.

SPDY, 是谷歌的商标, 并非是首字母缩写

协议提出, 我们可以通过提高带宽的方式提高网络的性能, 但是有一个点, 过了这个点, 就没有办法大量的提升性能了. 但如果你对延迟也这么做, 就是我们继续降低延迟, 延迟是性能提高的常数, 也就是降低延迟, 就可以提高西性能. 在SPDY之后, 关于性能提升有一个重要理念, 降低延迟, 以此提高网站的性能.

当我们并不请求其中的不同, 延迟就是延迟, 也就是, 数据在服务器和客户端之间需要的传递时间(使用毫秒计算.) 带宽是指每秒钟数据传输的总量(bit/每秒)

SPDY的功能包括: 多路优化, 压缩, 优先级划分, 安全性等. 在这里并不会深入讲解SPDY, 因为下面的HTTP/2协议大部分都受到了SPDY的启发.

SPDY并没有尝试取代HTTP; 它是HTTP所在应用数据层之上的传输层, 在请求发送到网络之前对其进行修改. 它开始在实际中投入使用, 大部分的浏览器开始使用它.

2015年, Google并不希望出现竞争的两种协议, 他们决定把它合并到HTTP中, 产生HTTP/2, 不再使用SPDY.

HTTP/2 - 2015

现在, 你一定确信, 我们需要另一个增强版的HTTP协议. HTTP/2是为了降低内容的延迟传输而设计的. 和HTTP/1.1主要区别或者功能, 包括:

  • 使用二进制替代文本
  • 多路复用(Multiplexing) - 多个异步HTTP请求使用同一个链接.
  • 使用HPACK压缩头部
  • 服务端推送 - 对同一个请求的多个响应
  • 请求优化
  • 安全性

    技术图片

名词解释

这里参考: HTTP/2

  • Message: 逻辑上的request, response.
  • Frame: 数据传输中的最小单位. 每个Frame都属于一个特定的stream或者整个链接.
    • Length: Frame的长度, 默认最大16kb, 如果要更大需要设置max frame size
    • Type: Frame的类型, 有DATA, HEADRES, PRIORITY等
    • Flag 和 R: 保留位
    • Stream identifier: 标识所属于的stream, 如果为0, 表示这个frame属于整条链接.
    • Frame Payload: 不同的type, 有不同的格式.
  • Stream: 一个双向流, 一条链接可以有多个stream.
    • HTTP/2依靠streams实现了多路复用, 提高了链接的利用率.
    • 一条连接可以包含多个streams, 多个streams发送的数据互不影响
    • Stream可以被client和server单方面使用, 也可以共享使用
    • Stream会确定好发送frame的顺序, 另一端按照接收到的顺序处理
    • Stream会有唯一的标识.
      • 如果是客户端创建的stream, ID是奇数.如果是server创建的, ID就是偶数. ID 0x00和0x01都有特定用途.
      • Stream不可能被重复利用, 如果一条链接的ID分配完了, client会新建一条连接. 而server则会给clent发送一个GOAWAY frame强制client新建一条链接.
      • 为了更大的一条连接上面的stream并发, 可以考虑调大SETTING_MAX_CONCURRENT_STREAMS

1. 二进制协议

HTTP/2尝试通过二进制协议的方法解决,现在HTTP/1.1的延迟问题. 作为一个二进制协议, 他更容易被解析, 但不容易被人眼所辨识. HTTP/2主要使用Frames和Streams进行构建.

介绍下: Frames和Streams

HTTP中的信息, 现在能够被压缩成为一个或多个frames. HEADRSframe为了元数据(meta data), DATAframe为了有效荷载(payload), 还有存在其他集中类型的frames(HEADRS, DATA, RST_STREAM, SETTINGS, PRIORITY等), 你可以查看技术图片

每一个HTTP/2的请求和响应都会生成一个唯一的streamID并分配给frames. Frames只是二进制的数据. 一个frames的链接被称为Stream. 每一个frame都有stream id. 用来标记他所属于的stream, 每一个frame都有相同的头部. 此外, 除了Stream ID 是唯一的以外, 值得一提的是, 客户端发定义的任何一个请求中的streamID都使用奇数, 服务器的每一个响应中的streamID都是用偶数.

除了HEADERSDATA两种类型的frame, 其他类型的frame中, 我想提下, RST_STREAM, 这是一种特殊的类型, 用来中断stream, 即, 客户端发送了这种frame就是告诉服务器, 我再也不需要这种stream了. 在HTTP/1.1中, 唯一一种可以让服务器端停止发送数据的方法, 就是响应的时候告诉客户端关闭这条连接. 导致了延迟增加, 因为一个新的链接需要非常多次的请求进行打开. 在HTTP/2中, 客户端可以使用RST_STREAM, 去停止接受一个特殊的Stream, 这个链接会一直保持着打开, 另一个stream会继续使用.

2. 多路复用(Multiplexing)

因为HTTP/2现在是一个二进制的协议, 正如前面所说的, 他使用frames和stream进行请求和响应, 一个TCP链接一旦被打开, 所有的stream都可以使用相同的链接进行异步的发送, 不需要再增加任何链接. 相反, 服务器也可以进行相同的异步响应方法, 即, 响应没有顺序, 客户度使用streamID来进行特殊数据包的区分. 这样就可以解决一个头部阻塞问题(head-of-line-blocking)的问题. 客户端不需要话费时间一直等待请求, 其他请求仍然被正常处理.

3. HPACK头部压缩(HPACK Header Compression)

这是RFC中单独的一部分, 这是RFC针对优化发送的报文头部. 当同一个客户端不断访问着服务器的时候, 会带着很多多余的数据. 我们一遍又一遍的发送着报文头部, 有时候, 会有cookies增加报文头部的大小, 导致的带宽的使用增加了时间延迟. 为了解决这个问题, HTTP/2使用了头部压缩.

技术图片

与请求响应不同的是, 头部信息无法通过gzip或者compress等格式压缩, 这里的头部压缩使用了一种完全不同的机制. 文字值使用Huffman编码机, 头部信息表通过客户端和服务端, 并且在客户端和服务端都省略了请求队列中重复的头部信息. 比如: 用户信息等. 使用两者都在维护的头部表进行引用.

当我们讨论标题的时候, 补充一点, 头部仍然和HTTP/1.1保持相同, 除了添加的一些伪标题, 即::method, :scheme, :host:path.

4. 服务器推送

服务器推送是另一个在服务器端强大的功能, 都知道当客户端请求一个确定的资源的时候, 服务器能用把这个资源推送给客户端, 甚至不需要客户端推送. 举个例子: 当浏览器加载一个web页面, 他会格式化整个页面, 找出需要从服务器段获取的内容, 后随之发送请求给服务器获取内容.

服务器端推送, 允许当服务器知道客户端需要的数据时, 通过推送的数据, 来减少往返次数. 他是如何完成的, 服务器发送一个特殊的数据帧, 命名为PUSH_PROMISE通知客户端, "嗨, 我将会把整个资源发送给你, 不需要再询问我了". 这个PUSH_PROMISE数据帧与导致推送发生的流相关, 他其中包括了流ID, 即, 整个数据流就是服务器端将要推送的数据.

5. 请求优化

当stream数据流打开的时候, 客户端向HEADERS数据帧中注入一个优化信息, 来对stream进行优化. 在任何时候, 客户度均可以发送一个PRIORITY数据帧来改变steam的优化.

如果不含有优化信息, 服务器异步响应请求. 也就是没有顺序. 如果给stream分配一个优化, 其中至少含有优化信息, 服务器能够决定, 针对整个请求, 需要执行返回多少资源.

6. 安全性

是否应该在HTTP/2中强制使用TLS, 引起了广泛讨论. 最后决定不会强制使用. 然而, 大部分的厂商表示, 他们只会在TLS层面上支持HTTP\2. 所以, 即使HTTP/2规范中不需要加密, 但是已经成为了一种默认的选项. HTTP\2通过TLS实现中的确有一些要求. 必须使用1.2或者更高版本的TLS, 必须含有一定级别的最小秘钥, 需要含有临时秘钥.
HTTP/2在兼容性方面, 已经渐渐超过SPDY. 在许多方面提供了性能优势, 不用多久, 我们就可以开始用了.

HTTP/2的详细细节感兴趣的人, 可以访问link to specsdemonstrating the performance benefits of HTTP/2.. 欢迎在评论中提出疑问,希望指出在阅读过程中遇到的错误点. 下次见.

以上是关于HTTP/2之旅 (翻译)的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin学习之旅解决错误:kotlin.NotImplementedError: An operation is not implemented: Not yet implemented(代码片段

如何将此 JavaScript 代码片段翻译成 Parenscript?

我的C语言学习进阶之旅解决 Visual Studio 2019 报错:错误 C4996 ‘fscanf‘: This function or variable may be unsafe.(代码片段

我的C语言学习进阶之旅解决 Visual Studio 2019 报错:错误 C4996 ‘fscanf‘: This function or variable may be unsafe.(代码片段

我的Android进阶之旅关于Android平台获取文件的mime类型:为啥不传小写后缀名就获取不到mimeType?为啥android 4.4系统获取不到webp格式的mimeType呢?(代码片段

我的Android进阶之旅关于Android平台获取文件的mime类型:为啥不传小写后缀名就获取不到mimeType?为啥android 4.4系统获取不到webp格式的mimeType呢?(代码片段