二进制分帧

一、帧(Frame)

  • HTTP/2 把所有通信数据都切分成帧(Frame),是最小的通信单位。

  • 每一帧由 9 字节固定帧头 + 可变长度负载 组成:

http2

二、帧头数据结构

// 常量固定 9 字节
const http2frameHeaderLen = 9
// 帧头结构
type http2FrameHeader struct {
    valid    bool           // 帧是否有效(读完后失效,放入池中复用)
    Type     http2FrameType // 帧类型(1 字节,10 种标准类型)
    Flags    http2Flags     // 标志位(1 字节,8 个 bit)
    Length   uint32         // 负载长度(3 字节,最大 16MB-1)
    StreamID uint32         // 流 ID(4 字节,高位保留,实际 31 bit)
}

三、帧类型(FrameType)

  • 不同类型的数据用不同的帧来发送
// 帧类型定义,就是整数
type http2FrameType uint8

const (
  // 数据帧:传输请求/响应体
  http2FrameData     http2FrameType = 0x0  
  // 头部帧:传输 HTTP 头部(开启新流)
  http2FrameHeaders    http2FrameType = 0x1 
  // 优先级帧:指定流的优先级
  http2FramePriority   http2FrameType = 0x2  
  // 重置流帧:异常终止一个流
  http2FrameRSTStream   http2FrameType = 0x3  
  // 设置帧:传输连接级别配置参数
  http2FrameSettings   http2FrameType = 0x4  
  // 推送承诺帧:服务端推送
  http2FramePushPromise  http2FrameType = 0x5 
  // 心跳帧:检测连接活性和 RTT
  http2FramePing     http2FrameType = 0x6  
  // 离开帧:优雅关闭连接
  http2FrameGoAway    http2FrameType = 0x7  
  // 窗口更新帧:流量控制
  http2FrameWindowUpdate http2FrameType = 0x8  
  // 延续帧:续传大头部块
  http2FrameContinuation http2FrameType = 0x9  
)

四、帧标志位(Flags)

每一种帧都有一个 1 字节(8 bit)的 Flags 字段,每一位代表一个开关,表示此帧不同的含义和状态

  • 0 = 关闭
  • 1 = 开启

4.1 标志位类型

// 标志位定义
type http2Flags uint8

// http2Flags 类型的方法:判断该位开关是否打开
// 		假如当前标志:0x05 → 二进制 00000101 ,说明同时开启了:EndStream(0x01) + EndHeaders(0x04)
func (f http2Flags) Has(v http2Flags) bool {
    // 检查是否包含 EndStream 标志
    return (f & v) == v
    // 运算过程:(00000101 & 00000001) = 00000001 (等于v) → 返回 true
}

4.2 各帧的标志位定义

const (
    // DATA 帧标志
    http2FlagDataEndStream http2Flags = 0x1  // 标记流结束(不再发送数据)
    http2FlagDataPadded    http2Flags = 0x8  // 数据有填充,如果带了 PADDED,payload 会变成:[Pad Length 1字节][真实数据][Padding若干字节],需要先读出填充长度

    // HEADERS 帧标志
    http2FlagHeadersEndStream  http2Flags = 0x1  // 流结束
    http2FlagHeadersEndHeaders http2Flags = 0x4  // 头部块完整(没有分片,不需要 CONTINUATION)
    http2FlagHeadersPadded     http2Flags = 0x8  // 有填充
    http2FlagHeadersPriority   http2Flags = 0x20 // 包含优先级信息

    // SETTINGS 帧标志
    http2FlagSettingsAck http2Flags = 0x1  // ACK 确认对端的 SETTINGS

    // PING 帧标志
    http2FlagPingAck http2Flags = 0x1  // ACK 确认 PING 响应

    // CONTINUATION 帧标志
    http2FlagContinuationEndHeaders http2Flags = 0x4  // 头部块完整

    // PUSH_PROMISE 帧标志
    http2FlagPushPromiseEndHeaders http2Flags = 0x4
    http2FlagPushPromisePadded     http2Flags = 0x8
)

五、Frame 接口与解析器

5.1 Frame 接口

所有具体帧类型(如DataFrameHeadersFrame 等)都内嵌 http2FrameHeader,自动实现此接口。

type http2Frame interface {
    Header() http2FrameHeader
    invalidate()
}
// 数据帧
type http2DataFrame struct {
	http2FrameHeader
	data []byte
}
// header 帧
type http2HeadersFrame struct {
    http2FrameHeader
    // 经过 HPACK 解码后得到,每个 HeaderField 就是 key‑value
    Headers []http2HeaderField
}

5.2 帧解析器类型与分发表

// 定义 HTTP/2 帧解析器,实际就是一个函数
type http2frameParser func( 
    fc *http2frameCache , fh http2FrameHeader,
    countError func(string), 
    payload []byte
) (http2Frame, error)

// 帧解析器表O(1)查找:下标 = HTTP/2 帧类型的数字,值 = 对应的解析函数
var http2frameParsers = [...]http2frameParser{
    http2FrameData:         http2parseDataFrame, 	// 解析数据帧
    http2FrameHeaders:      http2parseHeadersFrame, // 解析头部帧
    http2FramePriority:     http2parsePriorityFrame,
    http2FrameRSTStream:    http2parseRSTStreamFrame,
    http2FrameSettings:     http2parseSettingsFrame,
    http2FramePushPromise:  http2parsePushPromise,
    http2FramePing:         http2parsePingFrame,
    http2FrameGoAway:       http2parseGoAwayFrame,
    http2FrameWindowUpdate: http2parseWindowUpdateFrame,
    http2FrameContinuation: http2parseContinuationFrame,
}

// 查找解析函数
func http2typeFrameParser(t http2FrameType) http2frameParser {
    // 查表返回对应解析函数
    if int(t) < len(http2frameParsers) {
        return http2frameParsers[t]
    }
    // 未知帧类型,用默认函数处理
    return http2parseUnknownFrame
}

六、Framer

  • 帧结构是 http2FrameHeader、http2DataFrame、http2HeadersFrame 这些具体类型
  • 而 Framer 是帧的读写引擎,负责把请求里的字节流解析成这些帧对象,也负责把这些帧对象编码成字节写出去
type http2Framer struct {
    r         io.Reader                     // 一个接口,里面就一个read()方法,负责读取请求原始字节
    headerBuf   [http2frameHeaderLen]byte   // 9 字节帧头缓冲
    readBuf    []byte                       // 读请求体时的缓冲区
    maxReadSize uint32                      // 帧请求体的最大读取长度,超过这个长度直接报错
    
    lastFrame http2Frame   // 上一此读的帧,下一次读取前,把旧的置为失效( invalidate)
    errDetail error        // 上次读帧的详细错误原因

    MaxHeaderListSize uint32        // 头部列表大小限制,用来防止对端发来的 header(请求头) 过大
    lastHeaderStream uint32        // 用来 HEADERS/CONTINUATION 的连续性校验,如果不为 0,说明前面有个 header(请求头) 还没收完,下一帧必须是同一个 stream 上的 CONTINUATION
    lastFrameType    http2FrameType// 上一帧类型,配合连续帧合法性检查

    ReadMetaHeaders *hpack.Decoder  // 负责把 HEADERS + CONTINUATION 数据合并,然后做 HPACK 解码
    
    // ......
    
	w            io.Writer   // 接口内部同样只有一个Write()方法,Framer 最终会把组装好的整帧用它写出
    maxWriteSize uint32      // 写帧的长度上限,防止一个单帧太大
    wbuf         []byte      // 写缓冲,写的时候先把 9 字节帧头占位,再不断往后 append payload,最后 endWrite 回填长度并一次性写出去。
    
    // ......
} 

七、帧的反序列化(读取)

反序列化入口

读取的唯一公开入口是 ReadFrame(),它只做两件事,职责非常清晰:

  • 先调 ReadFrameHeader() 读 9 字节头部
  • 再调 ReadFrameForHeader(fh) 读 payload 并解析
func (fr *http2Framer) ReadFrame() (http2Frame, error) {
	fh, err := fr.ReadFrameHeader()
	return fr.ReadFrameForHeader(fh)
}

解析帧头

首先来到 ReadFrameHeader( ) — 读并校验 9 字节帧头,做了三件事:

  1. 调 http2readFrameHeader() ,从 TCP 流中读 9 字节,并解析为 http2.FrameHeader 帧头结构
  2. 校验帧大小 —— fh.Length > fr.maxReadSize 就拒绝,防止对端恶意发超大帧
  3. checkFrameOrder() —— 检查 HEADERS/CONTINUATION 的连续,HTTP/2 规范规定,请求头 HEADERS 帧如果有 CONTINUATION 分片,它们必须紧挨着来,中间不能插任何其他帧
func (fr *http2Framer) ReadFrameHeader() (http2FrameHeader, error) {
    // 获取帧头
	fh, err := http2readFrameHeader(fr.headerBuf[:], fr.r)
    // 帧太大就报错
	if fh.Length > fr.maxReadSize {
		// ......
	}
	if err := fr.checkFrameOrder(fh); err != nil {
		return fh, err
	}
	return fh, nil
}

来到帧头解析的具体过程,其实就是一次读 9 字节,然后按照规范提取每一组字节,填充到对应属性中
![header](https://img2024.cnblogs.com/blog/3780431/202604/3780431-20260403110505282-1586201478.png)

```go
func http2readFrameHeader(buf []byte, r io.Reader) (http2FrameHeader, error) {
    // ReadFull 保证恰好读 9 字节(读不够就报错)
    _, 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])), // 提取帧长度:b0<<16 | b1<<8 | b2  
        Type:     http2FrameType(buf[3]), // 帧类型  
        Flags:    http2Flags(buf[4]), // 帧标志 
        StreamID: binary.BigEndian.Uint32(buf[5:]) & (1<<31 - 1), // 5-8字节即 32 位的流ID,同时去除最高位,是保留位
        valid:    true, // 解析后帧有效
    }, nil
}

解析负载

  • 来到 ReadFrameForHeader() ,就是按照帧头的长度读出 payload ,然后帧的类型分发给其对应的处理函数
  • 如果是 header 帧,那就用 HPACK 解码器解析出真正的请求头 key-value
  • 解析完成,返回解析出来的具体帧对象,比如 http2DataFrame
func (fr *http2Framer) ReadFrameForHeader(fh http2FrameHeader) (http2Frame, error) {
    // 该连接读的一帧失效,因为已经读到新的帧了
    if fr.lastFrame != nil {
        fr.lastFrame.invalidate()  // 帧的 payload 是零拷贝复用的缓冲区,调用此方法后,上一次 ReadFrame 返回的帧对象就变脏了,外部代码不能再访问它的 payload
    }
    // 获取读帧负载时的缓冲区
    payload := fr.getReadBuf(fh.Length) 
    // 一次读取完整负载          
    if _, err := io.ReadFull(fr.r, payload); err != nil { 
        return nil, err
    }
    // http2typeFrameParser(fh.Type)按下标返回该帧类型对应的解析函数,(fr.frameCache, fh, fr.countError, payload)直接调用将 fh 和 payload,帧头和负载交给其开始解析
    // 返回具体的帧对象 f
    f, err := http2typeFrameParser(fh.Type)(fr.frameCache, fh, fr.countError, payload)
    if err != nil {
        return nil, err
    }
    // 这一帧读完
    fr.lastFrame = f

    // 如果是 HEADERS 帧,且上层设置了 ReadMetaHeaders(即 HPACK 解码器),就进入 readMetaFrame 做进一步处理。
    if fh.Type == http2FrameHeaders && fr.ReadMetaHeaders != nil {
        return fr.readMetaFrame(f.(*http2HeadersFrame))
    }
    // 返回帧,反序列化完成
    return f, nil
}

八、帧的序列化完(写入)

  1. 反序列化是从字节流剥出结构体,序列化是把结构体压成字节流
  2. 整体思路是占位、中间填内容、回填长度三步走。
  3. 核心在于:wbuf — 一块可复用的写缓冲(字节数组),所有写操作都往 f.wbuf 追加,最后一次性 Write 到底层 TCP 连接,避免多次系统调用。

序列化入口

序列化没有统一入口,每种帧类型都有独立的 WriteXXX() 方法,在各自方法内部完整地走完三步。
以 WriteData() 数据帧为例,完整调用链

  • streamID 标识标识这个数据属于哪个流
  • endStream:这个帧发完后,这个方向的数据是否结束
  • data:真正要发送的数据
func (f *http2Framer) WriteData(streamID uint32, endStream bool, data []byte) error {
    // 传 pad=nil,表示没有 padding 填充。
	return f.WriteDataPadded(streamID, endStream, data, nil)
}

func (f *http2Framer) WriteDataPadded(streamID uint32, endStream bool, data, pad []byte) error {
    // 把帧内容先写进 framer 的内部缓冲区 f.wbuf
	if err := f.startWriteDataPadded(streamID, endStream, data, pad); err != nil {
		return err
	}
    // 把缓冲区里完整的一帧一次性写到底层
	return f.endWrite()
}

在 startWriteDataPadded 里真正开始构造 DATA 帧,往缓冲区填充

  • 流ID 和 填充检查
  • 构造帧标志 Flags,startWrite()往缓冲区写帧头
  • 帧负载继续追加到字节切片中([Pad Length] + [data] + [padding])
func (f *http2Framer) startWriteDataPadded(streamID uint32, endStream bool, data, pad []byte) error {
    // 检查流ID是否合法 
	if !http2validStreamID(streamID) && !f.AllowIllegalWrites {
		return http2errStreamID
	}
    // 如果有 padding,就检查是否符合协议。
	if len(pad) > 0 {
        // 1.长度不能超过 255,因为 Pad Length 只有 1 字节,最大只能表示 255。
		if len(pad) > 255 {
			return http2errPadLength
		}
        // 2.填充字节必须全是 0
		if !f.AllowIllegalWrites {
			for _, b := range pad {
				if b != 0 {
					// "Padding octets MUST be set to zero when sending."
					return http2errPadBytes
				}
			}
		}
	}
    // 构造 DATA 帧的标志位。
	var flags http2Flags
    // END_STREAM 标志,如果 endStream == true,说明这次发送完以后,这个方向的数据结束了。
	if endStream {
		flags |= http2FlagDataEndStream
	}
    // PADDED 标志,如果 pad != nil,说明这个 DATA 帧带 padding
	if pad != nil {
		flags |= http2FlagDataPadded
	}
    // 帧头写入缓冲区
	f.startWrite(http2FrameData, flags, streamID)
    // DATA 帧如果带 padding,payload 的第一个字节必须是 Pad Length。
	if pad != nil {
		f.wbuf = append(f.wbuf, byte(len(pad)))
	}
    // 把业务数据写到缓冲区
	f.wbuf = append(f.wbuf, data...)
    // 填充字节
	f.wbuf = append(f.wbuf, pad...)
	return nil
}

写 9 字节帧头占位

  • 首先 wbuf[:0] 把切片长度归 0,等于清空复用
  • 然后把帧类型 Type,帧标志 Flags,流ID 直接填入字节数组中
  • 前 3 字节的 Length 此刻是 0 0 0 用于占位,因为帧负载还没写,此时不知道多长,等 endWrite 再回填。
Lines 1912-1924
func (f *http2Framer) startWrite(ftype http2FrameType, flags http2Flags, streamID uint32) {
	// Write the FrameHeader.
	f.wbuf = append(f.wbuf[:0],
		0 , 0 , 0 ,
		byte(ftype),
		byte(flags),
		byte(streamID>>24),
		byte(streamID>>16),
		byte(streamID>>8),
		byte(streamID))
}

写入底层

  1. 计算帧负载的长度并填入帧头
  2. 把整个缓冲区一次性写到底层 io.Writer,最后是系统调用写入 socket 缓冲区了
func (f *http2Framer) endWrite() error {
	// 计算帧长度,减去 9 字节帧头
	length := len(f.wbuf) - http2frameHeaderLen
	if length >= (1 << 24) {
		return http2ErrFrameTooLarge
	}
    // 填入 24 位的帧长度
	_ = append(f.wbuf[:0],
		byte(length>>16),
		byte(length>>8),
		byte(length))
	if f.logWrites {
		f.logWrite()
	}
    // write() 调用写入
	n, err := f.w.Write(f.wbuf)
	if err == nil && n != len(f.wbuf) {
		err = io.ErrShortWrite
	}
	return err
}
posted @ 2026-04-08 20:02  huihui~  阅读(1)  评论(0)    收藏  举报