HTTP/2 简介

2018.11.09,可以看更详细的:https://github.com/jiajunhuang/http2-illustrated

最近阅读了一下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 frame

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的内容:

stream 1

  • 这是stream 2的内容:

stream 2

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

stream 1 and 2

也就是这样:

http 2 stream

从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 之后再单独介绍。

参考


更多文章
  • Android SwipeRefreshLayout左右滑动冲突的解决
  • Android调用gRPC的两个小工具函数
  • Android上结合kotlin使用coroutine
  • gRPC错误处理
  • Java collection的结构
  • 为啥Redis使用pipelining会更快?
  • 通过阳台种菜实现蔬菜自由
  • 从GORM里学习到的panic处理方式
  • Go使用闭包简化数据库操作代码
  • TCMalloc设计文档学习
  • Flask和requests做一个简单的请求代理
  • Linux常用命令(四):xargs
  • Linux常用命令(二):htop
  • Linux常用命令(三):watch
  • Linux常用命令(一):netcat