前言
本文从源码角度分析netty-codec-http2对http2协议的实现,netty 对 http1.1的实现参见netty对http协议解析原理(一)
http2协议
HTTP2引入了一下的三个新概念:
- Stream: 已经建立连接的双向字节流,用唯一ID标示,可以传输一个或多个消息
- Message:逻辑/语义上的HTTP消息,请求或者响应,可以包含多个 frame
- Frame:HTTP2通信的最小单位,二进制头封装,封装HTTP头部或body
所以,直观来说,http2通信,就是收发一个个Http2Frame
Http2Frame 格式
bit数 | 作用 | |
---|---|---|
length | 24 | payload length |
type | 8 | 帧的类型 |
flags | 8 | 比如一个END_STREAM 标志位,表示一个流的结束 |
R | 1 | 保留字段 |
stream identifier | 31 | 标明帧所属的stream |
payload |
Http2Frame 类型
type值 | |||
---|---|---|---|
data | 0x0 | ||
header | 0x1 | ||
PRIORITY | 0x2 | ||
RST_STREAM | 0x3 | 流结束帧,用于终止异常流 | |
SETTINGS | 0x4 | 连接配置参数帧 | 设置帧由两个终端在连接开始时发送,连接生存期的任意时间发送;设置帧的参数将替换参数中现有值;client和server都可以发送;设置帧总是应用于连接,而不是一个单独的流; |
PUSH_PROMISE | 0x5 | 推送承诺帧 | |
PRIORITY | 0x6 | 检测连接是否可用 | |
GOAWAY | 0x7 | 通知对端不要在连接上建新流 | |
WINDOW_UPDATE | 0x8 | 实现流量控制 | |
CONTINUATION | 0x9 |
我们可以将frame笼统的分为data frame和控制frame,每一种类型的payload都是有自己的结构
请求过程
http2 的版本标识:
- h2:基于TLS之上构建的HTTP/2,作为ALPN的标识符,两个字节表示,0x68, 0x32,即https
- h2c:直接在TCP之上构建的HTTP/2,缺乏安全保证,即http
在不知道服务器是否支持http2的情况下,可以利用http的升级机制发送试探包
http2连接过程(不同于http1直接发送请求)
流量控制
简单说,就是发送方启动是有个窗口大小(默认64K-1),发送了10K的DATA帧,就要在窗口里扣血(减掉10K),如果扣到0或者负数,就不能再发送;接收方收到后,回复WINDOW_UPDATE帧,里面包含一个窗口大小,数据发送方收到这个窗口大小,就回血,如果回血到正数,就又能发不超过窗口大小的DATA帧。
这种流控方式就带来一些问题:
- 如果接收方发的WINDOW_UPDATE frame丢了,当然tcp会保证重传,但在WINDOW_UPDATE重传之前,就限制了发送方发送数据
- 一旦发送方初始windows size确定,那么发送方的发送速度是由接收方 + 网络传输决定的,如果发送方的速度大于接收方的应答,那么就会有大量的数据pending。
流控只限定data类型的frame,其它限定参见http2-frame-WINDOW_UPDATE
netty实现
对外的使用接口
http2本身一个复杂的协议,有着自己的一套“复杂的类图”,那么netty 与 netty-http2如何结合?
- 配置Http2ConnectionHandler 负责decode数据,decode时会触发Http2FrameListener的执行
- 自定义Http2FrameListener,并与Http2ConnectionHandler关联
- 使用Http2ConnectionEncoder.writeXX发送frame,write方法的执行要传入ctx对象。
整体结构
前文提过,http2通信,就是收发一个个Http2Frame。在代码层面上,接口也是围绕各个类型的frame的onXXRead和writeXXX来进行的。
上层接口关系如下
主要的点如下
- Http2ConnectionHandler extends ByteToMessageDecoder,整个读的流程由ByteToMessageDecoder.onDecode方法驱动。
- Http2FrameReader和Http2FrameWriter只是单纯的负责读写frame,
Http2FrameReader.readFrame(ChannelHandlerContext ctx, ByteBuf input, Http2FrameListener listener) throws Http2Exception;
中有一个listener成员,读取的frame会根据类型的不同,触发listener onXXRead方法的执行。Http2FrameWriter负责各类frame的写入。 -
frame包括数据frame和控制frame
- 读取到控制frame时,要更新本地控制数据,比如收到window update frame
- 读取到控制frame时,要对远端控制指定做出一定的反应,比如收到end frame of stream 或者 rst frame,即需要关闭stream,清除本地的stream数据(这里工作由Http2LifecycleManager负责)
- write数据时,要考虑本地控制model的实际情况,比如流控。
- write数据时,要对调用方控制指令做出一定的反应,比如调用方发送了end frame of stream
这也是Http2ConnectionHandler没有直接聚合Http2FrameReader和Http2FrameWriter,而是另提一个Http2ConnectionDecoder、Http2ConnectionEncoder的原因。同时,因为读写逻辑的任务不同,其代码组织也就稍有不同。
从中学到的:
- 接口只能指定方法,这个方法可以描述一个功能,也可以描述一个实现类应该具备哪些成员。
- 接口只是描述一个角色,而类可以根据自己实现这个角色的便捷性(比如具备所有相关的能力对象),实现多个角色。
- 面向过程,是从驱动代码的开始处(一般是main方法,此处则是eventloop及其驱动的onDecode方法),线性的实现逻辑。面向对象,则是抛开驱动逻辑,分析业务应该具备哪些对象,对象之间的关系是怎样的。对象和线程通常是撕裂的。
明确整体结构,还有两个主线
- netty 和 netty-http2如何交互
- netty-http2 和频控组件如何交互
数据收发
数据接收
Http2ConnectionHandler extends ByteToMessageDecoder.onDecode ==> Http2ConnectionDecoder.decodeFrame ==> Http2FrameReader. readFrame(ChannelHandlerContext ctx, ByteBuf input, Http2FrameListener listener) ==> Http2FrameListener 回调函数
netty对接收数据的抽象,基本上就是各种帧的监听事件。
interface Http2FrameListener{
int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding,
boolean endOfStream) throws Http2Exception;
void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
boolean endOfStream) throws Http2Exception;
void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency,
short weight, boolean exclusive) throws Http2Exception;
void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception;
void onSettingsAckRead(ChannelHandlerContext ctx) throws Http2Exception;
void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) throws Http2Exception;
void onPingRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception;
void onPingAckRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception;
void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId,
Http2Headers headers, int padding) throws Http2Exception;
void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData)
throws Http2Exception;
void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement)
throws Http2Exception;
void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags, ByteBuf payload)
throws Http2Exception;
}
一般情况下
- 用户处理onHeadersRead和onDataRead等业务数据
- netty http2处理onWindowUpdateRead和onGoAwayRead等控制数据
同时,提供Http2Connection、Http2Stream来存储对应帧、Stream的上下文数据。
DefaultHttp2Connection Http2Connection{
IntObjectMap<Http2Stream> streamMap = new IntObjectHashMap<Http2Stream>();
DefaultEndpoint<Http2LocalFlowController> localEndpoint;
DefaultEndpoint<Http2RemoteFlowController> remoteEndpoint;
List<Listener> listeners = new ArrayList<Listener>(4);
Promise<Void> closePromise;
}
- 存储连接上的Http2Stream
- 流控
- 监听者
- 关闭状态
数据发送
Http2FrameWriter{
ChannelFuture writeHeaders(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
int padding, boolean endStream, ChannelPromise promise);
ChannelFuture writePriority(ChannelHandlerContext ctx, int streamId, int streamDependency,
short weight, boolean exclusive, ChannelPromise promise);
...
}
写有两种
- 非data数据直接发送,this.encoder().writeHeaders ==> Http2FrameWriter.writeHeaders(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding, boolean endStream, ChannelPromise promise)
- data数据由流控组件负责发送,this.encoder().writeData ==> encoder.flowController().addFlowControlled
所以流控是一个大头
流控
流控是双向的,根据远程的window update更新流控数据,同时,根据消费数据以及本地空间发送window update。本文主要侧重于前者。
即便window size富余,流控组件也只是write数据,不会write flush数据。
流控组件的接口
流控组件Http2FlowController与外部的接口
-
提交数据
encoder.writeData() ==> flowController().addFlowControlled 将数据加入内部队列。可以看到,这里并没有真正写数据。
-
何时真正写数据,写操作时,会根据window size是否富余,来判断是否实际进行读写。:
- Http2ConnectionHandler extends ByteToMessageDecoder implements ChannelOutBoundHandler
- ByteToMessageDecoder extends ChannelInboundHandlerAdapter
- Http2ConnectionHandler 覆盖了ChannelOutBoundHandler的flush方法,执行
encoder.flowController().writePendingBytes();
。 - Http2ConnectionHandler 覆盖了ChannelInboundHandlerAdapter 的channelWritabilityChanged、channelReadComplete方法,触发flush方法的执行,执行
encoder.flowController().writePendingBytes();
。
-
更新window size
DefaultHttp2ConnectionDecoder 本身内置一个 Http2FrameListener,decoder回调onWindowUpdateRead,执行
encoder.flowController().incrementWindowSize(stream, windowSizeIncrement);
流控组件的发送逻辑
首先,连接有一个整体的window size,每个stream也有自己的window size。
encoder.flowController().writePendingBytes();
==> streamByteDistributor.distribute(bytesToWrite, writer)
触发的是整个连接的发送,在整个连接window size 富余的前提下,从连接里拿出还有富余window size的stream,streamByteDistributor确定stream此次的numBytes,由writer负责实际的ctx.write。
对于每一个stream,有两个state,state对stream做进一步封装
- 一个StremByteDistributor.StreamState负责记录它的window size,pending bytes。尽管caller调用组件发送帧,但流控组件按byte发送数据,数量由streamByteDistributor确定。
- 一个UniformStreamByteDistributor.State/WeightedFairQueueByteDistributor.State,该state作为载体 ,存储stream及优先级数据。该state存储在个UniformStreamByteDistributor/WeightedFairQueueByteDistributor的队列中。
比如采用公平策略的UniformStreamByteDistributor,假设connection window size = 1000,stream window size =120,该connection 一共10个stream,则本次encoder.flowController().writePendingBytes()
,该stream可以发送min(120,1000/10)个字节的数据。在实际情况下,还要考虑channel 写缓冲区的大小。
收到一个window update frame,接收方处理该frame100的数据。则一方面会更新对应stream的window size,加100,也会更新conneciton window size,加100。