来自公众号:新世界杂货铺
阅读建议
这是HTTP2.0系列的第二篇,所以笔者推荐阅读顺序如下:
本篇主要分为三个部分:数据帧,流控制器以及通过分析源码逐步了解流控制。
本有意将这三个部分拆成三篇文章,但它们之间又有联系,所以最后依旧决定放在一篇文章里面。由于内容较多,笔者认为分三次分别阅读三个部分较佳。
数据帧
HTTP2通信的最小单位是数据帧,每一个帧都包含两部分:帧头和Payload。不同数据流的帧可以交错发送(同一个数据流的帧必须顺序发送),然后再根据每个帧头的数据流标识符重新组装。
由于Payload中为有效数据,故仅对帧头进行分析描述。
帧头
帧头总长度为9个字节,并包含四个部分,分别是:
- Payload的长度,占用三个字节。
- 数据帧类型,占用一个字节。
- 数据帧标识符,占用一个字节。
- 数据流ID,占用四个字节。
用图表示如下:
数据帧的格式和各部分的含义已经清楚了, 那么我们看看代码中怎么读取一个帧头:
func http2readFrameHeader(buf []byte, r io.Reader) (http2FrameHeader, error) {
_, err := io.ReadFull(r, buf[:http2frameHeaderLen])
if err != nil {
return http2FrameHeader{
}, err
}
return http2FrameHeader{
Length: (uint32(buf[0])<<16 | uint32(buf[1])<<8 | uint32(buf[2])),
Type: http2FrameType(buf[3]),
Flags: http2Flags(buf[4]),
StreamID: binary.BigEndian.Uint32(buf[5:]) & (1<<31 - 1),
valid: true,
}, nil
}
在上面的代码中http2frameHeaderLen
是一个常量,其值为9。
从io.Reader中读取9个字节后,将前三个字节和后四个字节均转为uint32
的类型,从而得到Payload长度和数据流ID。另外需要理解的是帧头的前三个字节和后四个字节存储格式为大端(大小端笔者就不在这里解释了,请尚不了解的读者自行百度)。
数据帧类型
根据http://http2.github.io/http2-spec/#rfc.section.11.2描述,数据帧类型总共有10个。在go源码中均有体现:
const (
http2FrameData http2FrameType = 0x0
http2FrameHeaders http2FrameType = 0x1
http2FramePriority http2FrameType = 0x2
http2FrameRSTStream http2FrameType = 0x3
http2FrameSettings http2FrameType = 0x4
http2FramePushPromise http2FrameType = 0x5
http2FramePing http2FrameType = 0x6
http2FrameGoAway http2FrameType = 0x7
http2FrameWindowUpdate http2FrameType = 0x8
http2FrameContinuation http2FrameType = 0x9
)
http2FrameData
:主要用于发送请求body和接收响应的数据帧。
http2FrameHeaders
:主要用于发送请求header和接收响应header的数据帧。
http2FrameSettings
:主要用于client和server交流设置相关的数据帧。
http2FrameWindowUpdate
:主要用于流控制的数据帧。
其他数据帧类型因为本文不涉及,故不做描述。
数据帧标识符
由于数据帧标识符种类较多,笔者在这里仅介绍其中部分标识符,先看源码:
const (
// Data Frame
http2FlagDataEndStream http2Flags = 0x1
// Headers Frame
http2FlagHeadersEndStream http2Flags = 0x1
// Settings Frame
http2FlagSettingsAck http2Flags = 0x1
// 此处省略定义其他数据帧标识符的代码
)
http2FlagDataEndStream
:在前篇中提到,调用(*http2ClientConn).newStream
方法会创建一个数据流,那这个数据流什么时候结束呢,这就是http2FlagDataEndStream
的作用。
当client收到有响应body的响应时(HEAD请求无响应body,301,302等响应也无响应body),一直读到http2FrameData
数据帧的标识符为http2FlagDataEndStream
则意味着本次请求结束可以关闭当前数据流。
http2FlagHeadersEndStream
:如果读到的http2FrameHeaders
数据帧有此标识符也意味着本次请求结束。
http2FlagSettingsAck
:该标示符意味着对方确认收到http2FrameSettings
数据帧。
流控制器
流控制是一种阻止发送方向接收方发送大量数据的机制,以免超出后者的需求或处理能力。Go中HTTP2通过http2flow
结构体进行流控制:
type http2flow struct {
// n is the number of DATA bytes we're allowed to send.
// A flow is kept both on a conn and a per-stream.
n int32
// conn points to the shared connection-level flow that is
// shared by all streams on that conn. It is nil for the flow
// that's on the conn directly.
conn *http2flow
}
字段含义英文注释已经描述的很清楚了,所以笔者不再翻译。下面看一下和流控制有关的方法。
(*http2flow).available
此方法返回当前流控制可发送的最大字节数:
func (f *http2flow) available() int32 {
n := f.n
if f.conn != nil && f.conn.n < n {
n = f.conn.n
}
return n
}
- 如果
f.conn
为nil则意味着此控制器的控制级别为连接,那么可发送的最大字节数就是f.n
。 - 如果
f.conn
不为nil则意味着此控制器的控制级别为数据流,且当前数据流可发送的最大字节数不能超过当前连接可发送的最大字节数。
(*http2flow).take
此方法用于消耗当前流控制器的可发送字节数:
func (f *http2flow) take(n int32) {
if n > f.available() {
panic("internal error: took too much")
}
f.n -= n
if f.conn != nil {
f.conn.n -= n
}
}
通过实际需要传递一个参数,告知当前流控制器想要发送的数据大小。如果发送的大小超过流控制器允许的大小,则panic
,如果未超过流控制器允许的大小,则将当前数据流和当前连接的可发送字节数-n
。
(*http2flow).add
有消耗就有新增,此方法用于增加流控制器可发送的最大字节数:
func (f *http2flow) add(n int32) bool {
sum := f.n + n
if (sum > n) == (f.n > 0) {
f.n = sum
return true
}
return false
}
上面的代码唯一需要注意的地方是,当sum超过int32正数最大值(2^31-1)时会返回false。
回顾:在前篇中提到的(*http2Transport).NewClientConn
方法和(*http2ClientConn).newStream
方法均通过(*http2flow).add
初始化可发送数据窗口大小。
有了帧和流控制器的基本概念,下面我们结合源码来分析总结流控制的具体实现。
(*http2ClientConn).readLoop
前篇分析(*http2Transport).newClientConn
时止步于读循环,那么今天我们就从(*http2ClientConn).readLoop
开始。
func (cc *http2ClientConn) readLoop() {
rl := &http2clientConnReadLoop{
cc: cc}
defer rl.cleanup()
cc.readerErr = rl.run()
if ce, ok := cc.readerErr.(http2ConnectionError); ok {
cc.wmu.Lock()
cc.fr.WriteGoAway(0, http2ErrCode(ce), nil)
cc.wmu.Unlock