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(帧)
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 2的内容:
- 这是实际传输流程中的样子:
也就是这样:
从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连接,就靠它了。
首先发起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 之后再单独介绍。
参考
- https://developers.google.com/web/fundamentals/performance/http2/
- https://tools.ietf.org/html/rfc7540
- https://http2-explained.haxx.se/content/en/part6.html
更多文章
- socks5 协议详解
- zerotier简明教程
- 搞定面试中的系统设计题
- frp 源码阅读与分析(一):流程和概念
- 用peewee代替SQLAlchemy
- Golang(Go语言)中实现典型的fork调用
- DNSCrypt简明教程
- 一个Gunicorn worker数量引发的血案
- Golang validator使用教程
- Docker组件介绍(一):runc和containerd
- Docker组件介绍(二):shim, docker-init和docker-proxy
- 使用Go语言实现一个异步任务框架
- 协程(coroutine)简介 - 什么是协程?
- SQLAlchemy简明教程
- Go Module 简明教程