HTTP2-协议

HTTP2-缘起

Layers of HTTP/2

HTTP/2可以分为两个部分:framing层和数据或者HTTP层。,framing层多路复用能力的核心。
framing层是通用的,可重复使用的结构,用来传输HTTP。
数据层用来跟HTTP/1.1兼容。

  • Binary protocol:h2的framing层是二进制协议。
  • Header compression
  • Multiplexed:请求和响应交织在一起
  • Encrypted:大部分数据被加密了。

The Connection

任何HTTP/2 session的基本元素是连接。它是由客户端初始化的TCP/IP socket,发送HTTP请求的实体。
和h1没区别。但是,h1是无状态的,h2把所有连接级的元素(连接级的设置和头表header table)捆绑在一起,帧和流都运行在其上。

协议发现

HTTP/2支持两种办法,知道对端支持什么协议:
如果连接没加密,客户端发送Upgrade header,说它希望h2.如果服务器支持h2,返回“101 Switching Protocols”响应。
如果连接在TLS之上,客户端在ClientHello里设置程序级协议握手扩展(Application-Layer Protocol Negotiation(ALPN) extension),SPDY和早期版本的h2使用的是Next Protocol Negotiation (NPN) 扩展协商h2。

在连接上,客户端首先发送一个magic八字节流,这主要适用于客户端从HTTP/1.1升级而来:
0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a
解码成ASCII:
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n

这看起来像一个h1消息,h1服务器收到它,将返回错误。
magic字符串后面,是一个SETTINGS帧。支持h2的服务器确认客户端的SETTINGS帧,回复一个它自己的SETTINGS帧。
这样就可以开始h2了。如果客户端在收到SETTINGS之前,收到了其他东西,协商失败。

Frames

Framing是一个方法,Framing把所有重要的材料包装到一起,使消费者很容易读取、分析和增加。
对比一下,h1不是framed,而是用文字分割的。例如:

GET / HTTP/1.1 <crlf>
Host: www.example.com <crlf>
Connection: keep-alive <crlf>
Accept: text/html,application/xhtml+xml,application/xml;q=0.9... <crlf>
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4)... <crlf>
Accept-Encoding: gzip, deflate, sdch <crlf>
Accept-Language: en-US,en;q=0.8 <crlf>
Cookie: pfy_cbc_lb=p-browse-w; customerZipCode=99912|N; ltc=%20;... <crlf>
<crlf>

分析这样的结构,慢而且容易出错。分析h1的请求/响应容易碰到的问题是:

  • 一次只有一个请求/响应。接收完才能分析
  • 不知道分析会用多少内存。这导致下列问题
    • 你把一行读到什么缓存
    • 行太长怎么办
    • 你重新分配缓存,还是返回400错误

Frames,能让消费者提前知道会发生什么。HTTP/2的帧看上去像这样:

HTTP/2 frame header

HTTP/2 frame header fields

NameLengthDescription
Length3 bytes帧的载荷(payload)的长度。214到224-1之间。214是默认长度,再大需要发送一个SETTINGS帧
Type1 bytes帧的类型
R1 bit保留位,不要设置
Stream Identifier31 bits流的唯一标识
Frame PayloadVariable帧的实际内容

HTTP/2 frame types

NameIDDescription
DATA0x0携带流的核心内容
HEADERS0x1HTTP头和任选的优先级
PRIORITY0x2说明或者修改流的优先级和依赖项
RST_STREAM0x3允许一端结束流(通常在发生错误时)
SETTINGS0x4连接级参数
PUSH_PROMISE0x5告诉客户端,服务器将发消息
PING0x6连接测试,测量round-trip时间(RTT)
GOAWAY0x7告诉对端,自己已经开始接受新流
WINDOW_UPDATE0x8告诉对方,自己将接收多少数据(用于流量控制)
CONTINUATION0x9用于扩展HEADER

extension frames

H2可以增加新的帧类型。新类型不应该影响核心协议。

Streams

流(stream)就是HTTP/2连接内的客户端和服务器端之间的独立的、双向的帧序列。可以理解成一个连接上的独立的请求/响应对的一系列帧。客户端想发送请求时,初始化一个新的流。服务器在同一个流上响应。和H1的请求/响应流(flow)类似,但是,H2的多个请求/响应交织在一起,不会互相阻塞。Stream Identifier(帧头的6-9字节)表示帧属于哪个流。
客户端建立H2连接以后,通过发送一个HEADERS帧,开始一个新的stream,如果头需要多帧,就跟着CONTINUATION帧。HEADERS通常包含请求或者响应。随后的streams初始化一个新的,Stream Identifier增加了的HEADERS帧。

CONTINUATION FRAMES

如果HEADERS帧没有更多的头,就设置帧的Flags属性的END_HEADERS位。CONTINUATION帧是一种特殊的HEADERS帧。HEADERS和CONTINUATION必须是连续的,减少了多路复用的收益。要尽量少用CONTINUATION帧。

Messages

GET请求可能是这样的:
GET Request message and response message

POST请求可能是这样的:
POST request message and response message

HTTP/1和HTTP/2消息有几点明显的不同:

  • Everything is a header:h1把消息分成request/status行和头。h2把这些行打包成pseudo头。

    HTTP/1.1的请求/响应可能是这样的:

GET / HTTP/1.1
Host: www.example.com
User-agent: Next-Great-h2-browser-1.0.0
Accept-Encoding: compress, gzip
HTTP/1.1 200 OK
Content-type: text/plain
Content-length: 2
...
对应的HTTP/2版是:
:scheme: https
:method: GET
:path: /
:authority: www.example.com
User-agent: Next-Great-h2-browser-1.0.0
Accept-Encoding: compress, gzip
:status: 200
content-type: text/plain
现在,请求和状态行被分成了:scheme、:method、:path和:status头。
  • No chunked encoding:H2使用frames分块
  • No more 101 responses:H1一般用来把协议升级到WebSocket。ALPN提供更明确的协议协商路径,具有更少的往返开销。

Flow Control

H1,客户端消耗多快,服务器就发多快。H2提供了客户端/服务器控制速度的能力。流量控制信息是WINDOW_UPDATE帧,告诉对方,它将接收多少字节。收到WINDOW_UPDATE帧的,也发送WINDOWS_UPDATE帧,说明自己已经更新消费数据的能力。

Priority

使用HEADERS和PRIORITY帧,客户端能说明它需要什么资源,以及期望这些资源以什么顺序发送。它会声明一个依赖树和权重:

  • Dependencies:客户端可以告诉服务器,应该优先交付某对象,因为其他对象依赖它。
  • Weights:客户端可以告诉服务器,拥有相同依赖关系的对象的优先级。

举一个简单的web站点的例子:

  • index.html
    • header.jpg
    • critical.js
    • less_critical.js
    • style.css
    • ad.js
    • photo.jpg

接收完基本的HTML页面,客户端分析生成依赖树,并分配权重。这棵树可能是这个样子的:

  • index.html
    • style.css
      • critical.js
        • less_critical.js (weight 20)
        • photo.jpg (weight 8)
        • header.jpg (weight 8)
        • ad.js (weight 4)

Server Push

允许服务器向客户端发送数据,容易导致性能和安全问题。

Pushing an Object

服务器决定推送对象时,构造PUSH_PROMISE帧。这种帧的重要属性有:

  • PUSH_PROMISE帧头里的流ID是与响应相关联的请求的流ID。一个推送的响应总是关联到客户端已经发送的一个请求。例如,如果浏览器请求一个基本的HTML页,服务器会为页面上的JS对象构造一个基于该请求的流ID的PUSH_PROMISE。
  • PUSH_PROMISE帧的头
  • 发送的对象是可缓存的
  • :method头属性被认为是安全的。安全的方法是幂等的,不能修改状态。比如GET请求是幂等的,不改变状态,而POST就不是幂等的
  • 理想情况下,PUSH_PROMISE应该先被推到客户端,然后再接收引用推送对象的DATA帧。如果服务器在发送PUSH_PROMISE之前,发送了完整的HTML,比如客户端在收到PUSH_PROMISE之前,已经请求了某对象,H2协议足够强大,可以处理这种情况,但是浪费了精力。

如果客户端不能安全地处理PUSH_PROMISE的对象,会发送复位新流(RST_STREAM)或者发送一个PROTOCOL_ERROR(在GOAWAY帧内)。
Server Push message processing

Choosing What to Push

当服务器收到请求,它需要决定推送页面上的对象,还是等客户端请求这些对象。决策应该考虑:

  • 对象是否在浏览器缓存中
  • 从客户端的角度,假设对象的优先级
  • 客户端接受推送对象的能力,受带宽和类似资源的影响

Header Compression (HPACK)

HPACK是一种表查找(table lookup)压缩方案,使用Huffman编码,接近GZIP的压缩率。
一个web页依赖的对象需要多次请求,他们的头都是类似的。
当客户端发送请求时,在header block里说明需要的头和索引。应该增加这样一个表格:

IndexNameValue
62Header1foo
63Header2bar
64Header3bat

在服务器侧,读这些头,生成同样的表格。下次请求时,如果头相同,发送的头就是这样的:62 63 64。
实际的HPACK更复杂:

  • 在请求和响应侧,实际上各维护两张表。一个是动态表,很像上面描述的例子。另一个是静态表,由最常用的61个头的名称和值的组成。因为静态表有61个实体,所以例子的索引从62开始
  • 头怎么被索引,有很多控制
    • 发送文字值和索引
    • 发送文字值,不索引(一次性的或者敏感的)
    • 发送文字值的索引的头名,不索引(比如:path: /foo.html,内容总变)
    • 发送索引的头和值
  • 整数压缩
  • 利用霍夫曼编码表进一步压缩字符串

On the Wire

h2是二进制,被压缩的。

A Simple GET

HTTP/2 GET请求是这样的:

:authority: www.akamai.com
:method: GET
:path: /
:scheme: https
accept: text/html,application/xhtml+xml,...
accept-language: en-US,en;q=0.8
cookie: sidebar_collapsed=0; _mkto_trk=...
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Macintosh;...

HTTP/2 GET响应头:

:status: 200
cache-control: max-age=600
content-encoding: gzip
content-type: text/html;charset=UTF-8
date: Tue, 31 May 2016 23:38:47 GMT
etag: "08c024491eb772547850bf157abb6c430-gzip"
expires: Tue, 31 May 2016 23:48:47 GMT
link: <https://c.go-mpulse.net>;rel=preconnect
set-cookie: ak_bmsc=8DEA673F92AC...
vary: Accept-Encoding, User-Agent
x-akamai-transformed: 9c 237807 0 pmb=mRUM,1
x-frame-options: SAMEORIGIN
<DATA Frames follow here>

使用nghttp工具,可以看H2的细节:

nghttp -v -n --no-dep -w 14 -a -H "Header1: Foo" https://www.akamai.com

该命令行设置的窗口大小是16 KB(214),加了一个头,请求下载该页的关键资产。输出如下:

[ 0.047] Connected
The negotiated protocol: h2     [^1]
[ 0.164] send SETTINGS frame <length=12, flags=0x00, stream_id=0>       [^2]
        (niv=2)
        [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
        [SETTINGS_INITIAL_WINDOW_SIZE(0x04):16383]      [^3]
[ 0.164] send HEADERS frame <length=45, flags=0x05, stream_id=1>
        ; END_STREAM | END_HEADERS      [^4]
        (padlen=0)
        ; Open new stream
        :method: GET
        :path: /
        :scheme: https
        :authority: www.akamai.com
        accept: */*
        accept-encoding: gzip, deflate
        user-agent: nghttp2/1.9.2
        header1: Foo        [^5]
[ 0.171] recv SETTINGS frame <length=30, flags=0x00, stream_id=0>       [^6]
        (niv=5)
        [SETTINGS_HEADER_TABLE_SIZE(0x01):4096]
        [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
        [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
        [SETTINGS_MAX_FRAME_SIZE(0x05):16384]
        [SETTINGS_MAX_HEADER_LIST_SIZE(0x06):16384]
[ 0.171] send SETTINGS frame <length=0, flags=0x01, stream_id=0>        [^7]
        ; ACK
        (niv=0)
[ 0.197] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
        ; ACK
        (niv=0)        
[ 0.278] recv (stream_id=1, sensitive) :status: 200     [^8][^9]
[ 0.279] recv (stream_id=1, sensitive) last-modified: Wed, 01 Jun 2016 ...
[ 0.279] recv (stream_id=1, sensitive) content-type: text/html;charset=UTF-8
[ 0.279] recv (stream_id=1, sensitive) etag: "0265cc232654508d14d13deb...gzip"
[ 0.279] recv (stream_id=1, sensitive) x-frame-options: SAMEORIGIN
[ 0.279] recv (stream_id=1, sensitive) vary: Accept-Encoding, User-Agent
[ 0.279] recv (stream_id=1, sensitive) x-akamai-transformed: 9 - 0 pmb=mRUM,1
[ 0.279] recv (stream_id=1, sensitive) content-encoding: gzip
[ 0.279] recv (stream_id=1, sensitive) expires: Wed, 01 Jun 2016 22:01:01 GMT
[ 0.279] recv (stream_id=1, sensitive) date: Wed, 01 Jun 2016 22:01:01 GMT
[ 0.279] recv (stream_id=1, sensitive) set-cookie: ak_bmsc=70A833EB...
[ 0.279] recv HEADERS frame <length=458, flags=0x04, stream_id=1>       [^10]
        ; END_HEADERS
        (padlen=0)
        ; First response heade
[ 0.346] recv DATA frame <length=2771, flags=0x00, stream_id=1>     [^11]
[ 0.346] recv DATA frame <length=4072, flags=0x00, stream_id=1>
[ 0.346] recv DATA frame <length=4072, flags=0x00, stream_id=1>
[ 0.348] recv DATA frame <length=4072, flags=0x00, stream_id=1>
[ 0.348] recv DATA frame <length=1396, flags=0x00, stream_id=1>
[ 0.348] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=1>

[ 0.348] send HEADERS frame <length=39, flags=0x25, stream_id=15>       [^12]
        :path: /styles/screen.1462424759000.css
[ 0.348] send HEADERS frame <length=31, flags=0x25, stream_id=17>
        :path: /styles/fonts--full.css
[ 0.348] send HEADERS frame <length=45, flags=0x25, stream_id=19>
        :path: /images/favicons/favicon.ico?v=XBBK2PxW74

[ 0.378] recv DATA frame <length=2676, flags=0x00, stream_id=1>
[ 0.378] recv DATA frame <length=4072, flags=0x00, stream_id=1>
[ 0.378] recv DATA frame <length=1445, flags=0x00, stream_id=1>
[ 0.378] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=13>
        (window_size_increment=12216)
[ 0.379] recv HEADERS frame <length=164, flags=0x04, stream_id=17>      [^13]
[ 0.379] recv DATA frame <length=175, flags=0x00, stream_id=17>
[ 0.379] recv DATA frame <length=0, flags=0x01, stream_id=17>
        ; END_STREAM
[ 0.380] recv DATA frame <length=2627, flags=0x00, stream_id=1>
[ 0.380] recv DATA frame <length=95, flags=0x00, stream_id=1>
[ 0.385] recv HEADERS frame <length=170, flags=0x04, stream_id=19>      [^13]
[ 0.387] recv DATA frame <length=1615, flags=0x00, stream_id=19>
[ 0.387] recv DATA frame <length=0, flags=0x01, stream_id=19>
        ; END_STREAM
[ 0.389] recv HEADERS frame <length=166, flags=0x04, stream_id=15>      [^13]
[ 0.390] recv DATA frame <length=2954, flags=0x00, stream_id=15>
[ 0.390] recv DATA frame <length=1213, flags=0x00, stream_id=15>
[ 0.390] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
        (window_size_increment=36114)
[ 0.390] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=15>      [^14]
        (window_size_increment=11098)
[ 0.410] recv DATA frame <length=3977, flags=0x00, stream_id=1>
[ 0.410] recv DATA frame <length=4072, flags=0x00, stream_id=1>
[ 0.410] recv DATA frame <length=1589, flags=0x00, stream_id=1>     [^15]
[ 0.410] recv DATA frame <length=0, flags=0x01, stream_id=1>
[ 0.410] recv DATA frame <length=0, flags=0x01, stream_id=15>
[ 0.457] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
        (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[])

[^1]:协商好的协议
[^2]:发送SETTINGS帧
[^3]:窗口大小设置成16 KB
[^4]:客户端(nghttp)发送END_HEADERS和END_STREAM标记。告诉服务器没有头了,也不发数据。如果是POST,现在不发送END_STREAM
[^5]:nghttp命令行增加的头
[^6]:客户端接收到的服务器的SETTINGS帧
[^7]:发送和接收SETTINGS帧的确认信息
[^8]:stream_id说明相关的请求
[^9]:200状态码,成功了
[^10]:没有END_STREAM,因为DATA来了
[^11]:最后,开始获取数据流,WINDOW_UPDATE帧后面是5个DATA帧。客户端告诉服务器,已经读了10,915字节的DATA帧,并且准备好读取更多数据。请注意,此流还没完成
[^12]:客户端已经有了一些基本HTML,开始请求页面上的对象。新增加了三个流,15、16和19,准备接收css文件和图标
[^13]:流15、16和19的HEADERS帧
[^14]:窗口更新。包括连接级(connection-level)的更新(流0)
[^15]:流1的最后的DATA帧

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值