《HTTP/2基础教程》协议、特性、详解

《HTTP/2基础教程》协议、特性、详解

前言

  这篇博文讲述笔者在阅读《HTTP/2基础教程》这本书时的读书笔记,同时也会讲述一些个人见解。

第一章 HTTP进化史

  • HTTP/0.9
    HTTP在1989年左右开始进入人们的视野,最初的HTTP/0.9只有一个方法(GET),0.9的规范大概只有一页。
  • HTTP/1.0
    1996年发布了1.0的标准,新增大量内容:首部、响应吗、重定向、更多的请求方法等。对于0.9来讲,1.0已经是一个巨大的飞跃。但是1.0仍存在很多瑕疵,尤其不能让多个请求公用一个链接、缺少强制的Host首部、并且缓存的选择也相当简陋,这三点极大地影响了web可扩展的方式。
  • HTTP/1.1
    1999年发布了1.1的标准。
    【1】强制要求客户端提供Host首部,所以虚拟主机托管成为可能,也就是在一个IP上提供多个Web服务。
    【2】允许在同一个连接上发送多个请求
  • SPDY
    2009年由Google工程师开发,它证明了人们想要更高效的协议。在SPDY之前,人们普遍认为在商业应用中没有必要对HTTP/1.1做出突破性的,不兼容的改变。要兼容浏览器、服务器、网络代理和其他各种各样的中间件,代价极其高昂。
  • HTTP/2
    2012年HTTP小组启动了开发下一个HTTP版本的工作。HTTP/2于2015年5月14日发布。

第二章 HTTP/2 快速入门

这章主要介绍通过简单的方法启动和运行一个h2服务器。主要以下两步

1、获取并安装一个支持h2的Web服务器
2、下载并安装一张TLS证书,让浏览器和服务器通过h2连接。

最简单的方法就是对于步骤1用nghttpd,对于步骤2直接用openssl生成自签名证书。具体过程不做赘述。

第三章 Web优化“黑魔法”的动机与方式

本章主要介绍:

1、HTTP关注的性能指标,比如延迟、首字节时间、内容下载时间等
2、HTTP/1的问题
3、Web性能优化的手段,即为满足关注的性能指标而优化HTTP/1的不足所采用的手段

HTTP/1的问题

  • 队头阻塞
    众所周知,HTTP/1允许同一个连接上处理多个请求,但是一旦某个请求和响应发生阻塞,那后面的请求将会一直处于阻塞状态。这就是队头阻塞,即使HTTP/1优化过程中有了并行连接和管道化连接的手段,但再多的连接有可能存在队头阻塞的问题。
  • 低效的TCP利用
    TCP连接有慢启动过程,另外还有延迟确认、Nagle算法等,每个HTTP连接都会经历慢启动的过程,从而没有最大程度地利用带宽。
  • 臃肿的消息头部
    虽然H1提供了压缩被请求内容的机制,但是消息首部却无法压缩。消息首部可不能忽略,尽管它比响应资源小得多,但它可能占据请求的绝大部分,如果算上cookie,有个几千字节就很正常了。然而很多请求的报文都是重复的。
  • 受限的优先级设置
    H1没有优先级设置,这样可能导致重要的资源排在不重要资源的后面,从而影响用户体验

第四章 HTTP/2迁移

  在本书出版的时候,大约80%的浏览器可以在一定程度上支持h2,常见的Chrome、Firefox、Edge、safari等,这章介绍了网站从h1向h2迁移过程中的注意事项,不必在意。

第五章 HTTP/2协议

  作为一个网络开发者,主要关注的其实也就是这一章。
这章全面探讨了HTTP/2的低层工作原理,深入到数据层传输的帧及其通信方式。这也是本文阐述的重点。书中对连接、流、消息、帧的介绍比较混乱,笔者将统一介绍这些关键内容。

5.1 HTTP/2分层

二进制分帧层
  h2的关键特性之一就是在HTTP与TCP之间增加一个二进制分帧层分帧层这三个字乍看上难以理解,我是这样理解的:分、即分开、划分,帧指的是H2中的帧,层与网络层应用层的层对应。即,在网络层和应用层之间又抽象出了一个层用来划分HTTP的帧,叫做分帧层。\color{red}{}{不知道这样理解对不对,欢迎斧正}
例如:原来的HTTP协议中一个HTTP响应包含一到多个TCP段,而h2中的一个HTTP响应可能包含1到多个HTTP帧,一般一个HTTP帧对应一个TCP段。

主要特性:

  • 二进制协议
    h2的分帧层是基于帧的二进制协议。这方便了机器解析,但是肉眼识别起来比较困难。
  • 首部压缩
    h2的首部会被深度压缩,显著减少传输中的冗余字节。
  • 多路复用
    同一个连接可以被多个请求和响应即多个流复用
  • 加密传输
    线上传输的绝大部分数据都是加密过的,所以在中途读取会更加困难,更具有安全性。

5.2 连接、流、消息、帧

HTTP 2.0中的流、消息和帧

连接:即传统意义上客户端与服务端的一个TCP连接,源IP,源端口,目的IP,目的端口四个元素定义一条独一无二的链接。
流:一个连接上可以有多个流,一个流通常对应一次HTTP请求和响应。
消息:一个流上可以有多个消息,消息分为两种,请求消息和响应消息。
帧:一个消息由一个或多个帧组成,每个帧对应一个流,一个流里有多个帧。

HTTP2中的流

1、各个流之间是相互独立的,没有影响
2、其中一个流阻塞,不会影响其他流
3、流可以有优先级,约定优先级高的流优先处理

HTTP2的帧和流
1、流是一个逻辑上的概念
2、实际上连接里只看到帧,每个帧都有一个流ID用于标识该帧所属的流。

5.3 抓包分析

HTTP/2抓包分析
如图所示,11,13,15,17,19这样的数字就是流ID,每一个帧都拥有一个流ID标识该帧所属的流。
在这几个流中,11,13,15,17,19的请求是按顺序发送的,但是相应确是乱序的,说明各个流之间互不影响。
15这个流中,包含一个请求帧,两个响应帧。

5.4 帧结构

HTTP2帧结构
帧首部字段

名称 长度 描述
Length 3字节 表示帧的负载的长度,取值范围为0~2242^{24}-1字节。请注意,2142^{14}字节是默认的最大帧大小,如果需要更大的帧,必须在SETTINGS帧中设置。
Type 1字节 当前帧的类型,见下表
Flags 1字节 具体帧类型的标识
R 1位 保留位,不要设置,否则会带来严重后果
Stream Identifier 31位 每个流的唯一ID
Frame Payload 长度可变 真实的帧内容,长度是在Length字段中设置的

帧类型

名称 ID 描述
DATA 0X0 传输流的核心内容
HEADERS 0x1 包含HTTP首部,和可选的优先级参数
PRIORITY 0x2 指示或者更改流的优先级和依赖
RST_STREAM 0x3 允许一端停止流(通常是由于错误导致的)
SETTINGS 0x4 协商连接级参数
PUSH_PROMISE 0x5 提示客户端,服务端要推送些东西
PING 0x6 测试连接性和往返时延
GOAWAY 0x7 告诉另一端,目前端已经结束
WINDOW_UPDATE 0x8 协商一端将要接收多少字节(用于流量控制)
CONTINUATION 0x9 用于扩展HEADER数据块

5.5 流量控制

  h2提供了客户端和服务端调整传输速度的能力。WINDOW_UPDATE帧用来指示流量控制信息。WINDOW_UPDATE发送方告诉接收方,发送方能够接收多少字节。当发送方接收并消费接收到的数据时,发送方再发出一个WINDOW——UPDATE帧以支持更新后的处理字节的能力。
例如:

在流建立的时候,窗口的默认大小都是65535(21612^{16}-1)字节。假设客户端A支持该默认值,他的另一端(B)发送了10000字节,B也会关注窗口大小(现在有55535字节了)。现在A花时间处理了5000字节,还剩下5000字节,然后它会发送一个WINDOW_UPDATE帧,说明它现在的窗口大小是60535字节。B收到这个帧后,开始发送了一个大文件(比如4GB大小)。在这个场景下,在B等A准备好接收更多的数据之前,B能发送的数据量就是当前窗口的大小,即60535字节。通过这种方式,A可以控制B发送数据的最大速率。

5.6 优先级

  H2的优先级通过HEADERS帧和PRIORITY帧实现,客户端可以明确地和服务端沟通它需要什么,以及它需要这些资源的顺序。这是通过声明依赖关系树和树立的相对权重实现的。

  • 依赖关系为客户端提供了一种能力,通过指明某些对象对另一些对象有依赖,告知服务器这些对象应该优先传输。
  • 权重让客户端告诉服务器如何确定具有共同依赖关系的的对象的优先级。
* index.html
  - style.css
  	- critical.js
  		- less_critical.js(weight 20)
  		- photo.jpg(weight 8)
  		- hearder.jpg(weight 8)
  		- ad.js(weight 4)

  例如:可以利用依赖关系和权重,实现上述的依赖关系树,不同层级的资源优先级不同。对于同一层级的资源,权重不同,意味着重要程度不同。服务端将会参考依赖关系树中的关系对资源进行处理。
  但是需要注意的是,依赖关系树和权重也只是客户端的建议具体做什么以及如何处理优先级,还是得听服务器的。处理优先级的智能水平,可能会是决定各种支持h2的Web服务器性能优劣的因素。

说了这么多,让我们来看看HEADERS帧和PRIORITY帧是如何实现优先级的。

HEADERS 帧结构

HTTP/2 HEADERS帧结构
其中各字段的含义如下:

命长 长度 描述
Pad Length(填充长度) 1字节 填充字节的长度;帧首部的PADDED标识设置为1时才会有该字段
E 1位 标识流依赖是否为专用;只有设置了PRIORITY标识才会有该字段
Stream Dependency(流依赖) 31位 标识当前流所依赖的流,如果有的话,只有设置了PRIORITY标识才会有该字段
Weight(权重) 1字节 当前流的相对权重;只有设置了PRIORITY标识才会有该字段
Header Block Fragment()首部块片段 长度可变 消息的首部
Padding(填充数据) 长度可变 长度为Pad Length字段的值,所有的字节被设置为0,据说是为了安全

5.7 服务端推送

  提升单个对象性能的最佳方式,就是在它被用到之前就放到浏览器的缓存里面。这正是HTTP/2的服务端推送的目的。推送是服务器能够主动将对象发送给客户端,这可能是因为它知道客户端不久将用到该对象。
  如果服务端要推送一个对象,会构造一个PUSH_PROMISE帧。这个帧有很多重要属性:

  • PUSH_PROMISE帧首部中的流ID用来相应相关联的请求。推送的相应一定会对应到客户端已经发送的某个请求。
  • PUSH_PROMISE帧会指示将要发送的响应所使用的流ID(也就是说,PUSH_PROMISE帧有两个流ID,帧首部中的流ID与客户端请求的流ID对应,帧内容中的流ID指示将要发送的响应所使用的流ID,也就是说,相应将从其他流发送给客户端。)。另外,客户端会从1开始设置流ID,之后每开启一个流,就会增加2,一直是奇数。而服务端设置的流ID从2开始,一直是偶数。
  • :method首部的值必须确保安全。安全的方法就是幂等的那些方法,这是一种不改变任何状态的方法。例如,GET请求被认为是幂等的,因为它通常只是获取对象。而POST请求被认为是非幂等的,因为它可能会改变服务端的状态。
  • 如果客户端对PUSH——PROMISE的任何元素不满意,就可以按照拒收原因选择重置这个流(使用RST_STREAM),或者发送PROTOCOL_ERROR(在GOAWAY帧中)。

5.8首部压缩

  首部压缩算法HPACK非常的复杂。但简单来看就类似查表法,服务端和客户端各保存一张表,当客户端发送请求时,如果首部已经在表中存在,那就只需要发送一个首部索引,服务端拿到索引之后再查表从而得到真正的首部。

参考:
【1】Hypertext Transfer Protocol Version 2 (HTTP/2 )

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章