Go发起HTTP2.0请求流程分析(中篇)——数据帧&流控制

本文深入分析了Go发起HTTP2.0请求时的数据帧和流控制过程。从数据帧的帧头、类型、标识符到流控制器的运作,包括`(*http2ClientConn).readLoop`、`(*http2flow).available`、`(*http2flow).take`等关键函数,探讨了HTTP2.0流控制如何防止数据过载。此外,还介绍了客户端和服务器之间的流控制交互,如收到不同数据帧后的处理逻辑。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

来自公众号:新世界杂货铺

阅读建议

这是HTTP2.0系列的第二篇,所以笔者推荐阅读顺序如下:

  1. Go中的HTTP请求之——HTTP1.1请求流程分析
  2. Go发起HTTP2.0请求流程分析(前篇)

本篇主要分为三个部分:数据帧,流控制器以及通过分析源码逐步了解流控制。

本有意将这三个部分拆成三篇文章,但它们之间又有联系,所以最后依旧决定放在一篇文章里面。由于内容较多,笔者认为分三次分别阅读三个部分较佳。

数据帧

HTTP2通信的最小单位是数据帧,每一个帧都包含两部分:帧头Payload。不同数据流的帧可以交错发送(同一个数据流的帧必须顺序发送),然后再根据每个帧头的数据流标识符重新组装。

由于Payload中为有效数据,故仅对帧头进行分析描述。

帧头

帧头总长度为9个字节,并包含四个部分,分别是:

  1. Payload的长度,占用三个字节。
  2. 数据帧类型,占用一个字节。
  3. 数据帧标识符,占用一个字节。
  4. 数据流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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值