HTTP/2 简介 - Jiajun的编程随想

首页

/

友情链接

/

我的Github

/

关于我


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:

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中各个字段的用处:

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

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 ...

没有讲到的东西

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

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

参考