42

HTTP/2 简介

 5 years ago
source link: https://jiajunhuang.com/articles/2018_10_13-http2.md.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

最近阅读了一下RFC7540和一部分HTTP/2的Go语言支持实现,故作此记录。

HTTP/1 的问题在哪

回想一下作为一个浏览器,请求HTTP/1网站的过程。浏览器经过一系列操作之后,和服务器建立了通信,并且发送请求,例如:

GET / HTTP/1.1
Host: jiajunhuang.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate

此时服务器收到请求,并且返回 / 对应的内容,此处,是一个网页。网页中包含了一堆的图片,css和js的链接,浏览器解析出来之后, 为了获取这些内容,浏览器必须新建一些和服务器的连接,请求图片,css和js等内容。例如请求 common.min.css ,会发起一个这样的 报文:

GET /static/css/common.min.css HTTP/1.1
Host: jiajunhuang.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate

可以发现,如果有n个这样的请求,请求中header里大部分的内容都是相同的例如 Host , User-Agent 等。多次请求中包含同样的内容, 是一种资源浪费,而且,如果有多个资源需要请求,则需要建立多个TCP连接,请求完之后这个连接就废掉了,没法重复利用,对于TCP连接 的利用率实在是很低。

HTTP/1.1 中有 request pipelining,但是有缺陷,例如只能对GET请求进行pipelinning,详见: https://en.wikipedia.org/wiki/HTTP_pipelining

而HTTP/2就是为了解决上述问题而存在的。

HTTP/2

首先得说明,HTTP/2不再像HTTP/1那样是明文协议,HTTP/2是二进制协议,也就意味着,我们没法再简单的通过 telnet jiajunhuang.com 80 这样去请求一些内容。为什么HTTP/2要做成二进制协议呢?主要原因在 https://http2.github.io/faq/#why-is-http2-binary:

  • 使用TLS之后,也没法直接telnet这样进行调试
  • 相比明文协议(可以手工输入),二进制协议必须使用库或者工具(当然也可以自己写),相对来说更不容易出错
  • 二进制协议解析起来更加高效(再也不用逐个字符去判断哪里是头部结束了,直接偏移多少就可以知道对应的字节表示什么意思)

frame(帧)

binary_framing_layer01.svg

frame 是HTTP/2中最小的传输单位。HTTP/2 相对HTTP/1来说,把原来的头部和body分开来了,统一都丢到frame里,头部对应的frame的类型 是HEADERS,而body对应的frame的类型是DATA。除此之外,frame的类型还有例如:GOAWAY, WINDOW_UPDATE等等。可以这么理解,HTTP/2 连接开始之后,所有的数据都是一个frame的内容,直到开始下一个frame。因此,Go语言gRPC实现中有这么一段代码:

func readFrameHeader(buf []byte, r io.Reader) (FrameHeader, error) {
	_, err := io.ReadFull(r, buf[:frameHeaderLen])
	if err != nil {
		return FrameHeader{}, err
	}
	return FrameHeader{
		Length:   (uint32(buf[0])<<16 | uint32(buf[1])<<8 | uint32(buf[2])),
		Type:     FrameType(buf[3]),
		Flags:    Flags(buf[4]),
		StreamID: binary.BigEndian.Uint32(buf[5:]) & (1<<31 - 1),
		valid:    true,
	}, nil
}

第一行,frameHeaderLen 的定义是 const frameHeaderLen = 9 ,为什么呢?我们来看看RFC中对frame的格式的定义:

+-----------------------------------------------+
|                 Length (24)                   |
+---------------+---------------+---------------+
|   Type (8)    |   Flags (8)   |
+-+-------------+---------------+-------------------------------+
|R|                 Stream Identifier (31)                      |
+=+=============================================================+
|                   Frame Payload (0...)                      ...
+---------------------------------------------------------------+

数一下 frame payload 之前一共是多少个bit, 24 + 8 + 8 + 1 + 31 = 72 ,72 bit,也就是9byte了。既然提到了frame的格式, 那我们还是要看看frame中各个字段的用处:

  • Length: The length of the frame payload expressed as an unsigned 24-bit integer. Values greater than 2^14 (16,384) MUST NOT be sent unless the receiver has set a larger value for SETTINGS_MAX_FRAME_SIZE. 这24bit是一个24bit的无符号整数, 因此最大可以表示2^14=16384,所以通常来说,frame的最大长度是16384 byte, 也就是16k。当接收者设置了 SETTINGS_MAX_FRAME_SIZE 之后,才可以传输超过这个大小的frame。

  • Type: The 8-bit type of the frame. The frame type determines the format and semantics of the frame. 这8bit用来明确 所在frame的类型。

  • Flags: An 8-bit field reserved for boolean flags specific to the frame type. 这8bit用来标志当前类型的frame的一些其他 特征,例如DATA这种类型的frame有定义 END_STREAM(0x1) 表示当前frame是这个stream中最后的一个frame。

  • R: A reserved 1-bit field. The semantics of this bit are undefined, and the bit MUST remain unset (0x0) when sending and MUST be ignored when receiving. 保留位,没用。

  • Stream Identifier: A stream identifier (see Section 5.1.1) expressed as an unsigned 31-bit integer. The value 0x0 is reserved for frames that are associated with the connection as a whole as opposed to an individual stream. 当前frame 所在的stream的id。stream的概念在下一小节会介绍。

stream(流)

上一节我们看到了每个frame的定义中都有31bit用来标识当前frame所在stream的id。那么stream是个什么鬼?stream是一个抽象概念, 因为HTTP/2把原本HTTP/1中一个请求中的头部和body打散了,分成了HEADERS和DATA两个frame。那当服务器收到一堆的frame之后,他如何 知道哪个frame和哪个frame是一起的,组合起来是一个完整的请求呢?所以我们需要stream这个抽象概念,而且由于一个HTTP/2连接可以 同时传输多个stream,所以我们可以通过下面的图片来理解stream:

  • 这是stream 1的内容:

stream1.jpg

  • 这是stream 2的内容:

stream2.jpg

  • 这是实际传输流程中的样子:

stream12.jpg

也就是这样:

stream_multiplexing.svg

从HTTP/1升级

接下来我们看看,由于现实世界中大部分网站还是HTTP/1的,HTTP/2在实现细节上与HTTP/1相差太大,是如何做到兼容的。

首先可以肯定的是, http://https:// 这两个scheme不能改,要不然估计HTTP/2别想推广开来,不信可以看看ipv6今天的覆盖 程度,ipv6可是推出二十好几年了呢。

那我们怎么判断服务器是不是支持HTTP/2呢?HTTP/1中有一个状态码,叫做 101 Switching Protocols 。从HTTP/1升级到HTTP/2连接,就靠它了。

https://tools.ietf.org/html/rfc7540#section-3.2

首先发起HTTP/1请求,并且给出如下报文:

GET / HTTP/1.1
Host: jiajunhuang.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>

如果服务器不支持HTTP/2,就该怎么返回怎么返回,例如返回:

HTTP/1.1 200 OK
Content-Length: 243
Content-Type: text/html

...

但是如果服务器支持HTTP/2,那就返回101状态码,然后随即开始的内容就是HTTP/2的二进制内容了:

HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c

[ HTTP/2 connection ...

没有讲到的东西

  • HPACK HTTP/2 头部压缩
  • Stream prioritization
  • Server Push
  • Flow Control

说穿了,HTTP/2就是把HTTP/1建立在多个TCP之上的这一整套流程,建立在一个TCP之上,所以有这么一坨的概念,例如multiplexing, 优先级等等。

头部压缩我准备在读完 RFC 7541 之后再单独介绍。

参考


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK