HTTP教程

HTTP/1.1的问题

一张图可以很较为全面的概括了HTTP/1.*存在缺陷:

HTTP教程_第1张图片

前端一般采用: - CSS Spriting,小图合并成大图,CSS进行分割成小图显示 - Inlining,使用DataURL方式内嵌Base64编码格式图片 - JS Concatenation,多个JS文件合并成一个,缺陷是一旦有文件修改,需要重新合并 - Sharding,将资源/服务部署到多个机器上,均摊/分享请求压力

HTTP/1.*没有充分利用TCP特性,再加上同一个站点打开多个连接等,导致网络资源利用率不高。

HTTP/2改进点

与HTTP/1相比,主要区别包括:

  1. HTTP/2采用二进制格式而非文本格式
    • 高效紧凑传输解析
    • 更少错误倾向,没有了所谓的空白行、大写、换行符、空连接等
  2. HTTP/2是完全多路复用的,而非有序并阻塞的
    • HTTP/1.1默认情况下单个请求单个连接
    • HTTP/1.1流水线化pipelining方式导致较大/较慢的响应阻塞后续任务
    • HTTP/1.1无法解决线头阻塞的问题
    • 多路复用可以有效避免线头阻塞,多个请求-响应同一个连接内并行处理
  3. 只需一个连接即可实现并行
    • HTTP/1.*请求处理模型导致同一个站点资源客户端需要打开若4-8个连接请求
    • HTTP/1.*多个连接会占用过多网路资源,导致TCP堵塞和数据重传
    • HTTP/2单个连接内多个流(请求-响应)之间并行处理,减少网路资源占用,可避免了TCP频繁的打开、关闭
  4. 使用报头压缩,HTTP/2降低了开销
    • 传统浏览器网路报头一般在80-1400字节大小
    • 压缩头部可让报头更紧凑,更快速传输,有利于移动网络环境等
    • 压缩算法使用HPACK,更为高效、安全
  5. HTTP/2让服务器可以将响应主动“推送”到客户端
    • 传统方式:客户端请求,服务器响应,客户端逐一解析需要后续请求的图片、样式等资源,再次一一发送资源请求
    • HTTP/2服务器根据客户端请求,计算出响应内容所包含的资源,在客户端发起请求之前提前发送给客户端
    • 节省客户端主动发起请求的时间的往返时间

这里有一张图,可以总体上了解HTTP/2:

HTTP教程_第2张图片

HTTP/2的解读

保留/兼容HTTP/1.1的所有语义,但传输语法(或者说传输方式)改变,目的在于更充分利用TCP更高效传输,多路复用是实现途径,低延迟是改进方向。

笔记提纲

以上为简单总体介绍了HTTP/2协议,要想深入其特性,需要阅读器规范。下面为围绕HTTP/2规范的各个方面,列出提纲,便于后面一一填充。

  1. HTTP/2的连接建立
  2. HTTP/2的多路复用和流的属性
  3. HTTP/2的帧定义
  4. HTTP/2的消息交换
  5. HTTP/2的错误处理和安全事项

名词解释

以下名词会在当前或以后笔记中出现,贴出来方便理解。

  1. 中介(intermediation),指代包含代理、企业防火墙、反向代理和CDN等互联网设备
  2. ALPN,Application Layer Protocol Negotiation
  3. 报头,报文头部,请求报文头部,或响应报文头部

小结

HTTP/2相比HTTP/1.1,可以做到更有效的充分利用TCP连接,避免了TCP连接的重复的创建(三次握手)、销毁(四次挥手)的过程。


HTTP/2协议在TCP连接之初进行协商通信,只有协商成功,才会涉及到后续的请求-响应等具体的业务型数据交换。

HTTP版本标识符

  • h2,基于TLS之上构建的HTTP/2,作为ALPN的标识符,两个字节表示,0x68, 0x32,即https
  • h2c,直接在TCP之上构建的HTTP/2,缺乏安全保证,即http
  • 在HTTP/2 RFC文档出现之前,以上版本字段需要添加上草案版本号,类似于h2-11,h2c-17

HTTP/2 请求过程

针对直接建立在标准TCP之上HTTP2,在未知服务器是否提供HTTP/2支持之前,可以依赖现有HTTP/1.1进行试探。

HTTP版本的请求内容

  1. 客户端发起请求,只有请求报头:
    GET / HTTP/1. 1 Host: server. example. com Connection: Upgrade, HTTP2-Settings Upgrade: h2c
    HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>
  2. 服务器若不支持HTTP/2,直接按照HTTP/1.1响应即可
    HTTP/1. 1 200 OK
    Content-Length: 243 Content-Type: text/html
    . . .
  3. 服务器支持HTTP/2,通知客户端一起切换到HTTP/2协议下吧

    HTTP/1. 1 101 Switching Protocols Connection: Upgrade Upgrade: h2c [ HTTP/2 connection . . . 
  4. 101响应空行之后,服务器必须发送的第一个帧为SETTINGS帧(其负载可能为空)作为连接序言
  5. 客户端接收到101响应后,也必须发送一个序言作为响应,其逻辑结构:
    PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n // 纯字符串表示,翻译成字节数为24个字节
    SETTINGS帧                       // 其负载可能为空
    服务器端和客户端所发送的连接序言有所不同。
  6. 客户端可以马上发送请求帧或其它帧过去,不用等待来自服务器端的SETTINGS帧
  7. 任一端接收到SETTINGS帧之后,都需要返回一个包含确认标志位SETTIGN作为确认
  8. 其它帧的正常传输

HTTP/2的直接连接

客户端预先知道服务器提供HTTP/2支持,可以免去101协议切换的流程开销。 具体流程:

  1. 客户端必须首先发送一个连接序言,其逻辑结构:
    PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n // 纯字符串表示,翻译成字节数为24个字节
    SETTINGS帧                       // 其负载可能为空
  2. 发送完毕序言之后,客户端可以不用等待来自服务器端响应,马上发送HTTP/2其它帧
  3. 服务器端接收到客户端的连接序言之后,需要发送一个SETTINGS帧作为连接序言
  4. 任一端接收到SETTINGS帧之后,都需要返回一个包含确认标志位SETTIGN作为确认
  5. 其它帧的正常传输

HTTPS版本建立连接

HTTP/2安全版本在TLS上构建,协商采用的ALPN扩展协议,采用“h2”作为协议标识符(http版本则是“h2c”)。一定程度上可认为不存在试探是否支持或直接连接的烦恼,因为这个过程直接在TLS层协商而成。

流程如下:

  1. 客户端和服务器端TLS层协商
  2. 客户端发送连接序言(同上表示,PRI + SETTINGS)
  3. 接收到客户端连接序言之后,服务器端发送连接序言
  4. 双方各自确认SETTINGS帧
  5. 其它帧的正常传输

HTTPS和HTTP Upgrade方式协商

HTTPS协商是强制,封装在TLS之上ALPN扩展实现,HTTP只有非直接连接方式才会存在通过101 协议切换方式进行升级。

这里有一张图形象说明其流程。

HTTP教程_第3张图片

统一的连接过程

这里不论是HTTP还是HTTPS,在两端成功协商之后(或HTTP的直接连接),其连接过程都是一样的

HTTP教程_第4张图片

注意事项

  1. 客户端发起的HTTP/1.1请求,其流标识符为1,默认优先级;半关闭“half closed”状态,一旦完成HTTP/2的连接,将被应用于响应
  2. 文档提到的客户端可以通过HTTP Alternative Services(简称为[ALT-SVC],类似于CNAME机制)获得通知服务器是否支持HTTP/2,但目前看来仅仅是草案建议而已
  3. 连接序言用于最后两端协商确认双方要使用HTTP/2协议,建立初始化的HTTP/2连接环境
  4. 客户端若知服务器支持HTTP/2,可免去通过HTTP/1.1 101协议切换方式进行升级,在建立连接后即可发送序言,否则只能在接收到服务器端101响应后发送序言
  5. 建立在TLS上的HTTP/2通过ALPN扩展协商机制取代101协议切换
  6. 连接序言所包含的SETTINGS帧其负载可以为空
  7. 针对一个TCP连接,服务器第一个要发送的帧必须是SETTINGS帧
  8. 为了避免不必要延迟,客户端可以在发送完毕序言之后发送帧数据,不用等待来自服务器端的序言SETTINGS帧
  9. 客户端接收到服务器端作为序言的SETTINGS帧,需要遵守其设定
  10. 在一些环境下需要提供一个顺序机制,允许服务器在客户端发送业务帧之前发送SETTINGS,这需要客户端配合
  11. 客户端和服务器端任何一方接收到无效连接序言需要抛出PROTOCOL_ERROR类型连接错误,若收到GOAWAY帧,可忽略

小结

HTTP/2连接的建立协商机制比HTTP/1.1稍微复杂了一些。

对比明文版的HTTP/1.1和HTTP/2完成一次请求-响应:

  1. HTTP/1.1在建立建立之后,只需要发送请求报文数据
  2. HTTP/2客户端需要在连接建立之初马上发送一个连接序言过去,然后才是正常请求
  3. 两端(客户端+服务器端)的两次完整的连接序言+确认的交互流程,多了两次往返过程

在弱网络环境下,会不会加重网络负载,只能拭目一看了。

一。流和多路复用的关系

1. 流的概念

流(Stream),服务器和客户端在HTTP/2连接内用于交换帧数据的独立双向序列,逻辑上可看做一个较为完整的交互处理单元,即表达一次完整的资源请求-响应数据交换流程;一个业务处理单元,在一个流内进行处理完毕,这个流生命周期完结。

特点如下:

  • 一个HTTP/2连接可同时保持多个打开的流,任一端点交换帧
  • 流可被客户端或服务器单独或共享创建和使用
  • 流可被任一端关闭
  • 在流内发送和接收数据都要按照顺序
  • 流的标识符自然数表示,1~2^31-1区间,有创建流的终端分配
  • 流与流之间逻辑上是并行、独立存在

2. 多路复用

流的概念提出是为了实现多路复用,在单个连接上实现同时进行多个业务单元数据的传输。逻辑图如下:

HTTP教程_第5张图片

实际传输可能是这样的:

HTTP教程_第6张图片

只看到帧(Frame),没有流(Stream)嘛。

需要抽象化一些,就好理解了:

  1. 每一个帧可看做是一个学生,流可以认为是组(流标识符为帧的属性值),一个班级(一个连接)内学生被分为若干个小组,每一个小组分配不同的具体任务。
  2. HTTP/1.* 一次请求-响应,建立一个连接,用完关闭;每一个小组任务都需要建立一个班级,多个小组任务多个班级,1:1比例
  3. HTTP/1.1 Pipeling解决方式为,若干个小组任务排队串行化单线程处理,后面小组任务等待前面小组任务完成才能获得执行机会,一旦有任务处理超时等,后续任务只能被阻塞,毫无办法,也就是人们常说的线头阻塞
  4. HTTP/2多个小组任务可同时并行(严格意义上是并发)在班级内执行。一旦某个小组任务耗时严重,但不会影响到其它小组任务正常执行
  5. 针对一个班级资源维护要比多个班级资源维护经济多了,这也是多路复用出现的原因

这样简单梳理,就有些小清晰了。

3. 流的组成

流的概念提出,就是为了实现多路复用。影响因素:

  1. 流的优先级(priority)属性建议终端(客户端+服务器端)需要按照优先级值进行资源合理分配,优先级高的需要首先处理,优先级低的可以稍微排排队,这样的机制可保证重要数据优先处理。
  2. 流的并发数(或者说同一时间存在的流的个数)初始环境下不少于100个
  3. 流量控制阀协调网络带宽资源利用,由接收端提出发送端遵守其规则
  4. 流具有完整的生命周期,从创建到最终关闭,经历不同阶段

流总体组成如下:

HTTP教程_第7张图片

搞清楚了流和多路复用之间关系,下面稍微深入一点,学习流的一些细节。

二。流的属性

1. 流状态/生命周期

帧的行为以及END_STREAM标志位都会对流的状态的产生变化。因为流由各个端独立创建,没有协商,消极后果就是(两端无法匹配的流的状态)导致发送完毕RST_STREAM帧之后“关闭”状态受限,因为帧的传输和接收需要一点时间。

帧的状态列表:

  1. idle,所有流的开始状态值
    • 发送/接收HEADERS帧,进入open状态
    • PUSH_PROMISE帧只能在已有流上发送,导致创建的本地推送流处于"resereved(local)"状态
    • 在已有流上接收PUSH_PORMISE帧,导致本地预留一个流处于"resereved(remote)"状态
    • HEADERS/PUSH_PROMISE帧以及后面的零个或多个CONTINUATION帧,只要携带有END_STREAM标志位,流状态将进入"half closed"状态
    • 只能接收HEADERS和PRIORITY,否则报PROTOCOL_ERROR类型连接错误
  2. reserved,为推送保留一个流稍后使用

    1. reserved (local),服务器端发送完PUSH_PROMISE帧本地预留的一个用于推送流所处于的状态
      • 只能发送HEADERS、RST_STREAM、PRIORITY帧
      • 只能接收RST_STREAM、PRIORITY、WINDOW_UPDATE帧
    2. reserved (remote),客户端接收到PUSH_PROMISE帧,本地预留的一个用于接收推送流所处于的状态

      • 只能发送WINDOW_UPDATE、RST_STREAM、PRIORITY帧
      • 只能接收RST_STREAM、PRIORITY、HEADERS帧

      不满足条件,需要报PROTOCOL_ERROR类型连接错误

  3. open,用于两端发送帧,需要发送数据的对等端需要遵守流量控制的通告。
    • 每一端可以发送包含END_STREAM标志位的帧,导致流进入"half closed"状态
    • 每一端都可以发送RST_STREAM帧,流进入"closed"状态
  4. half closed

    1. half closed (local),发送包含有END_STREAM标志位帧的一端,流进入本地半关闭状态
      • 不能发送WINDOW_UPDATE,PRIORITY和RST_STREAM帧
      • 可以接收到任何类型帧
      • 接收者可以忽略WINDOW_UPDATE帧,后续可能会马上接收到包含有END_STREAM标志位帧
      • 接收到优先级PRIORITY帧,可用来变更依赖流的优先级顺序,有些小复杂了
      • 一旦接收到包含END_STREAM标志位的帧,将进入"closed"状态
    2. half closed (remote),接收到包含有END_STREAM标志位帧的一端,流进入远程半关闭状态

      • 对流量控制窗口可不用维护
      • 只能接收RST_STREAM、PRIORITY、WINDOW_UPDATE帧,否则报STREAM_CLOSED流错误
      • 终端可以发送任何类型帧,但需要遵守对端的当前流的流量控制限制
      • 一旦发送包含END_STREAM标志位的帧,将进入"closed"状态

      一旦接收或发送RST_STREAM帧,流将进入"closed"状态。

  5. closed,流的最终关闭状态
    • 只允许发送PRIORITY帧,对依赖关闭的流进行重排序
    • 终端接收RST_STREAM帧之后,只能接收PRIORITY帧,否则报STREAM_CLOSED流错误
    • 接收的DATA/HEADERS帧包含有END_STREAM标志位,在一个很短的周期内可以接收WINDOW_UPDATE或RST_STREAM帧;超时后需要作为错误对待
    • 终端必须忽略WINDOW_UPDATE或RST_STREAM帧
    • 终端发送RST_STREAM帧之后,必须忽略任何接收到的帧
    • 在RST_STREAM帧被发送之后收到的流量受限DATA帧,转向流量控制窗口连接处理。尽管这些帧可以被忽略,因为他们是在发送端接收到RST_STREAM之前发送的,但发送端会认为这些帧与流量控制窗口不符。
    • 终端在发送RST_STREAM之后接收PUSH_PROMISE帧,尽管相关流已被重置,但推送帧也能使流变成“保留”状态。因此,可用RST_STREAM帧关闭一个不想要的承诺流

要求如下:

  1. 针对具体状态中出现没有允许出现的帧,需要作为协议错误(PROTOCOL_ERROR)类型的连接错误处理
  2. 在流的任何状态下,PRIORITY帧都可以被发送或接收
  3. 未知帧可以被忽略

2. 流标识符

  1. 31个字节表示无符号的整数,1~2^31-1
  2. 客户端创建的流以奇数表示,服务器端创建流以偶数表示
  3. 0x0用来表示连接控制信息流,不能够创建新流
  4. 通过http/1.1 101 协议切换升级切换到HTTP/2,0x1所指代流处于"half closed(local)",不能用于创建新流
  5. 新建流的标识符要大于已有流和预留的流的标识符
  6. 新建流第一次被使用时,低于此标识符的并且处于空闲"idle"状态的流都会被关闭
  7. 已使用的流标识符不能被再次使用
  8. 终端的流标识符若被耗尽的情况下
    • 若是客户端,需要关闭连接,创建新的连接创建新流
    • 若是服务器端,需要发送一个GOAWAY帧通知客户端,强迫其打开一个新连接

3. 流的并发数量

  1. 每一端都可以发送包含有SETTINGS_MAX_CONCURRENT_STREAMS参数的SETTINGS帧限制对等端流的最大并发量
  2. 对等端接收之后遵守终端最大并发量限制约定
  3. 状态为"open"或"half closed"的流需要计入限制总数
  4. 保留态"reserved"流不算入限制总数内
  5. 终端接收到HEADERS帧导致创建的流总数超过限制,需要响应PROTOCOL_ERROR或REFUSED_STREAM错误,具体哪一种错误,需要根据终端是否可以检测得到允许自动重复重试
  6. 终端想降低SETTINGS_MAX_CONCURRENT_STREAMS设置的活动流的上限,若低于当前已经打开流的数值,可以选择光比溢出的流或者允许流继续存在直到完成

4. 流的优先级

流的优先级在于允许终端向对端表达所期待的给予具体流更多资源支持的意见的表达,不能保证对端一定会遵守,非强制性需求建议;默认值16。在资源有限时,可以保证基本数据的传输。

优先级改变:

  1. 终端可在新建的流所传递HEADERS帧中包含优先级priority属性
  2. 可单独通过PRIORITY帧专门设置流的优先级属性

5. 流依赖

  1. 流与流之间存在依赖、被依赖关系。所有流默认依赖流0x0;推送流依赖于传输PUSH_PROMISE的关联流。
  2. 依赖权重值1~256区间,对于依赖同一父级的子节点,应该根据权重比列进行分配资源。
  3. 对于依赖同一个父级流的子节点被指定相关权重值,以及可用资源的分配比重。子节点之间顺序不固定。
    A                 A
       / \      ==>      /|\
      B C B D C 
  4. 一旦设置独家专属标志(exclusive flag)将为现有依赖插入一个水平的依赖关系,其父级流只能被插入的新流所依赖。比如流D设置专属标志并依赖于流A:
    A
        A                 |
       / \      ==> D B C / \
                        B C 
  5. 流的依赖树形模型,底层的流只能等到上层流被关闭或无法正常运转/失效时,才会被分配到资源
  6. 流无法依赖自身,否则为PROTOCOL_ERROR流错误
  7. 在流依赖树形模型中,父节点优先级,以及专属依赖流的加入等,都会导致已有优先级重排序
    ?                ?                ?                 ?
        |               / \               |                 |
        A D A D D / \            /   / \            / \                |
      B C ==>  F   B C ==>    F   A       OR      A
         / \                 |             / \             /|\ D E E B C B C F
        |                                     |             |
        F E E (intermediate)   (non-exclusive)    (exclusive)

6. 流优先级状态管理

  1. 流的依赖树形模型,任一节点被移除,都需要重建优先级顺序,重新分配资源
  2. 终端建议在流关闭一段时间内保留优先级信息,减少潜在的指派错误
  3. 处于"idle"状态流可被指派默认优先级16,这时可以变成其它流的父节点,可以指派新的优先级值
  4. 终端持有的流优先级信息不受SETTINGS_MAX_CONCURRENT_STREAMS限制,但可能会造成终端状态维护负担,其数量可以被限制不多于SETTINGS_MAX_CONCURRENT_STREAMS所定义数量
  5. 优先级状态信息的维持在负载较高时可以被丢弃,以减少资源占用。
  6. 终端若有能力保留足够状态,在接收到PRIORITY帧目的修改已被关闭流的优先级时,可以为其子节点重建优先级顺序

7. 流量控制

多路复用会引入资源竞争,流量控制可以保证流之间不会严重影响到彼此。流量控制通过使用WINDOW_UPDATE帧实现,可作用于单个流以及整个的连接。一些原则如下:

  1. 逐跳,具有方向性
  2. 不能够被禁止
  3. 初始窗口值为65535字节,针对单个流,以及整个连接都有效
  4. 基于WINDOW_UPDATE帧传输实现,接收端通告对端准备在流/连接上接收的字节数
  5. 接收端完全控制权限,接受端可通告针对流/连接的窗口值,发送者需要遵守
  6. 目前只有DATA帧可被流量控制,仅针对其有效负载计算;超出窗口值,其负载可以为空

需要注意事项:

  1. 流量控制是为解决线头阻塞问题,同时在资源约束情况下保护一些操作顺利进行,针对单个连接,某个流可能被阻塞或处理缓慢,但同时不会影响到其它流上正在传输的数据
  2. 虽然流量控制可以用来限制一个对等端消耗的内存,但若在不知道网络带宽延迟乘积的情况下可能未必能够充分利用好网络资源
  3. 流量控制机制很复杂,需要考虑大量的细节,实现很困难

三。小结

HTTP/2规范中所定义的流概念、属性很复杂,在请求量很大以及应对海量并发的情况下,整个连接的流量控制+单个流的流量控制+流的状态+流优先级属性+优先级的状态+流依赖树形模型等一系列新特性,可能会造成:

  1. 服务器端/客户端单个连接内存占用过高,维护一个长连接的成本比以往多了若干倍
  2. 流量控制是一个复杂功能,实现不好会导致一端流量窗口值已被耗尽,需要等待客户端发送新的流控窗口值,若有热数据进行发送,需要等待成本,无形中增加了额外的交互步骤
  3. 流依赖和优先级重排序等,无形中增加了程序的复杂度,处理不好触发潜在BUG
  4. 为了性能和内存考虑,很多知名应用不见得有动力实现全部特性,流的一些高级特性毕竟有些过于理想化,诸如当前实现列表:https://github.com/http2/http2-spec/wiki/Implementations,可以看出一二
  5. 实际非浏览器环境,诸如HTTP API等,实际上仅需要部分关键特性,这属于情理之中的选择
  6. 凡是状态皆需要维护,无论横向还是纵向的扩展都需要倍加注意;无状态才是最有利于扩展

你可能感兴趣的:(HTTP教程)