二进制分帧
一、帧(Frame)
-
HTTP/2 把所有通信数据都切分成帧(Frame),是最小的通信单位。
-
每一帧由 9 字节固定帧头 + 可变长度负载 组成:

二、帧头数据结构
// 常量固定 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 接口
所有具体帧类型(如DataFrame、HeadersFrame 等)都内嵌 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 字节帧头,做了三件事:
- 调 http2readFrameHeader() ,从 TCP 流中读 9 字节,并解析为 http2.FrameHeader 帧头结构
- 校验帧大小 —— fh.Length > fr.maxReadSize 就拒绝,防止对端恶意发超大帧
- 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 字节,然后按照规范提取每一组字节,填充到对应属性中

```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
}
八、帧的序列化完(写入)
- 反序列化是从字节流剥出结构体,序列化是把结构体压成字节流。
- 整体思路是占位、中间填内容、回填长度三步走。
- 核心在于: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))
}
写入底层
- 计算帧负载的长度并填入帧头
- 把整个缓冲区一次性写到底层 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
}

浙公网安备 33010602011771号