- 消息 message:消息由一个或多个帧组成。例如请求的消息和响应的消息。一个消息包含消息头和消息体
- 帧 frame:HTTP2 中最小通信数据单元,每个帧至少包含了一个标识(stream identifier,简称stream id)该帧所属的流。
- 流是一个逻辑上的概念,它包含了几个特点,第一:是可以双向传输。第二:单个流是有序的,并且传递的帧都包含了一个流ID。第三:流就是一系列的帧组成的
- 连接:一个http2.0建立的连接,可以跑多个流
帧的定义
HTTP Frame {
Length (24),
Type (8),
Flags (8),
Reserved (1),
Stream Identifier (31),
Frame Payload (..),
}
(来自:httpwg.org/specs/rfc91… 帧的类型
- DATA 帧
DATA Frame {
Length (24),
Type (8) = 0x00,
Unused Flags (4),
PADDED Flag (1),
Unused Flags (2),
END_STREAM Flag (1),
Reserved (1),
Stream Identifier (31),
[Pad Length (8)],
Data (..),
Padding (..2040),
}
- header 帧
HEADERS Frame {
Length (24),
Type (8) = 0x01,
Unused Flags (2),
PRIORITY Flag (1),
Unused Flag (1),
PADDED Flag (1),
END_HEADERS Flag (1),
Unused Flag (1),
END_STREAM Flag (1),
Reserved (1),
Stream Identifier (31),
......
}
多路复用
http1.1和http2.0的区别
从流的角度看多路复用
流的定义
- 头部的特性
- 头部压缩
使用静态字典表比如数字8代表状态是200
如果头部在静态表里面没有,则更新静态表,并且发送一份给客户端 \ 静态表压缩之后
- 头部压缩
- 消息body
- 二进制
- 传输过程
- 加密
- 多路复用
- 一个tcp连接上可以处理多个请求,多个请求的数据流跑在上面。http1.0有对头阻塞的概念,就算是有pipeline的技术,对于对头的返回阻塞也无能为力
- 流的方向
- 流是双向的,可以是客户端发起的,也可以是服务端发起的。客户端的流id是奇数,服务端的流id是偶数
拥有相同的流标识符的帧属于同一个流,它是有序的。
帧的头部和发送帧的golang代码
在 HTTP2 的golang实现中可以看到Framer这个结构体,这个结构体规定了帧的读取和发送。在创建新的 ClientConn 时,会对一个ClientConn 通过调用 cc.fr = NewFramer(cc.bw, cc.br) 创建一个 Framer ,并且将整个 net.Conn 当做了 io.Reader 和 io.Writer 参数传给了 Framer 。 在Framer内部,会维护一个 wbuf 的字节队列,新写入的 Frame 会源源不断的写入到队列中。每写入的Frame的所携带的 Stream ID 并不一样。但是相同 Stream ID 会严格保持顺序。
// A Framer reads and writes Frames.
type Framer struct {
r io.Reader
w io.Writer
wbuf []byte
...
}
func (t *Transport) newClientConn(c net.Conn, singleUse bool, hooks *testSyncHooks) (*ClientConn, error) {
cc := &ClientConn{
t: t,
tconn: c,
readerDone: make(chan struct{}),
nextStreamID: 1,
maxFrameSize: 16 << 10, // spec default
initialWindowSize: 65535, // spec default
maxConcurrentStreams: initialMaxConcurrentStreams, // "infinite", per spec. Use a smaller value until we have received server settings.
peerMaxHeaderListSize: 0xffffffffffffffff, // "infinite", per spec. Use 2^64-1 instead.
streams: make(map[uint32]*clientStream),
singleUse: singleUse,
wantSettingsAck: true,
pings: make(map[[8]byte]chan struct{}),
reqHeaderMu: make(chan struct{}, 1),
syncHooks: hooks,
}
if hooks != nil {
hooks.newclientconn(cc)
c = cc.tconn
}
if d := t.idleConnTimeout(); d != 0 {
cc.idleTimeout = d
cc.idleTimer = cc.afterFunc(d, cc.onIdleTimeout)
}
if VerboseLogs {
t.vlogf("http2: Transport creating client conn %p to %v", cc, c.RemoteAddr())
}
cc.cond = sync.NewCond(&cc.mu)
cc.flow.add(int32(initialWindowSize))
// TODO: adjust this writer size to account for frame size +
// MTU + crypto/tls record padding.
cc.bw = bufio.NewWriter(stickyErrWriter{
conn: c,
timeout: t.WriteByteTimeout,
err: &cc.werr,
})
cc.br = bufio.NewReader(c)
cc.fr = NewFramer(cc.bw, cc.br)
------忽略中间代码--------
cc.bw.Write(clientPreface)
cc.fr.WriteSettings(initialSettings...)
cc.fr.WriteWindowUpdate(0, transportDefaultConnFlow)
cc.inflow.init(transportDefaultConnFlow + initialWindowSize)
cc.bw.Flush()
if cc.werr != nil {
cc.Close()
return nil, cc.werr
}
cc.goRun(cc.readLoop)
return cc, nil
}
cc.fr = NewFramer(cc.bw, cc.br)上有两个连接,一个是读,一个是写 cc.fr.WriteSettings(initialSettings...)是写头部的帧
// WriteSettings writes a SETTINGS frame with zero or more settings
// specified and the ACK bit not set.
//
// It will perform exactly one Write to the underlying Writer.
// It is the caller's responsibility to not call other Write methods concurrently.
func (f *Framer) WriteSettings(settings ...Setting) error {
f.startWrite(FrameSettings, 0, 0)
for _, s := range settings {
f.writeUint16(uint16(s.ID))
f.writeUint32(s.Val)
}
return f.endWrite()
}
func (f *Framer) startWrite(ftype FrameType, flags Flags, streamID uint32) {
// Write the FrameHeader.
f.wbuf = append(f.wbuf[:0],
0, // 3 bytes of length, filled in in endWrite
0,
0,
byte(ftype),
byte(flags),
byte(streamID>>24),
byte(streamID>>16),
byte(streamID>>8),
byte(streamID))
}
func (f *Framer) endWrite() error {
// Now that we know the final size, fill in the FrameHeader in
// the space previously reserved for it. Abuse append.
length := len(f.wbuf) - frameHeaderLen
if length >= (1 << 24) {
return ErrFrameTooLarge
}
_ = append(f.wbuf[:0],
byte(length>>16),
byte(length>>8),
byte(length))
if f.logWrites {
f.logWrite()
}
n, err := f.w.Write(f.wbuf)
if err == nil && n != len(f.wbuf) {
err = io.ErrShortWrite
}
return err
}
- Data Frame的发送:
// startWriteDataPadded is WriteDataPadded, but only writes the frame to the Framer's internal buffer.
// The caller should call endWrite to flush the frame to the underlying writer.
func (f *Framer) startWriteDataPadded(streamID uint32, endStream bool, data, pad []byte) error {
if !validStreamID(streamID) && !f.AllowIllegalWrites {
return errStreamID
}
if len(pad) > 0 {
if len(pad) > 255 {
return errPadLength
}
if !f.AllowIllegalWrites {
for _, b := range pad {
if b != 0 {
// "Padding octets MUST be set to zero when sending."
return errPadBytes
}
}
}
}
var flags Flags
if endStream {
flags |= FlagDataEndStream
}
if pad != nil {
flags |= FlagDataPadded
}
f.startWrite(FrameData, flags, streamID)
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
}
- 流控
- 流量控制机制的一个重要目的是防止接收端缓冲区溢出。如果发送端发送的数据超过了接收端设置的流量控制窗口大小,那么接收端可能会丢弃这些数据或采取其他措施来防止溢出。
- tcp的流控和http2.0的流控的区别是,tcp的流控指的是单个连接上的,而http2.0的流控指的具体是连接上的流的控制
- HTTP2 一般 Connection 和 Stream 的初始 flow-control window 大小都是 65535 bytes