前言
http协议在Web应用中得到了普遍的使用。甚至基于http rest规范开发内部接口调用。受限于http本身效率问题的限制内部API使用率不是很高。而Http2.0 支持的 Streaming 和 Duplexing 可以将请求和响应消息进行分片交叉传送,可以大幅提升传输效率。
Http2.0
http2.0 把http1.1的内容拆成了二个部份,Headers + DATA 桢。
即 桢是其最小单体,遵守二进制协议。
帧格式
所有的帧都以一个9字节的报头开始, 后接变长的报体:
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
报头部分的字段定义如下:
-
Length
: 报体的长度, 无符号24位整型. 对于发送值大于2^14 (长度大于16384字节)的报体, 只有在接收方设置SETTINGS_MAX_FRAME_SIZE
为更大的值时才被允许注: 帧的报头9字节不算在length里.
-
Type
: 8位的值表示帧类型, 决定了帧的格式和语义.- HEADERS 帧 ,头信息对应于HTTP HEADER
- DATA 帧 ,对应于HTTP Response Body
- PRIORITY 帧,用于调整流的优先级
- RST_STREAM 帧, 流终止帧,用于中断资源的传输
- SETTINGS 帧, 用户客户服务器交流连接配置信息
- PUSH_PROMISE 帧, 服务器向客户端主动推送资源
- GOAWAY 帧, 礼貌地通知对方断开连接
- PING 帧, 心跳帧,检测往返时间和连接可用性
- WINDOW_UPDATE 帧, 调整帧窗口大小
- CONTINUATION 帧, HEADERS 帧太大时分块的续帧
-
Flags
: 为Type保留的bool标识, 大小是8位. 对确定的帧类型赋予特定的语义, 否则发送时必须忽略(设置为0x0).- END_STREAM (0x1) : 位1用来标识这是发送端对确定的流发送的最后报头区块。
- END_HEADERS (0x4) : 位3表示帧包含了整个的报头块,且后面没有延续帧。 不带有END_HEADERS标记的报头帧在同个流上后面必须跟着延续帧。接收端接收到任何其他类型的帧或者在其他流上的帧必须作为类型为协议错误的连接错误处理。
- PADDED (0x8): 表示是否有Padding
- PRIORITY (0x20) : 位6设置指示专用标记(E),流依赖及权重字段将会呈现;
-
R
: 1位的保留字段, 尚未定义语义. 发送和接收必须忽略(0x0). -
Stream Identifier
: 31位无符号整型的流标示符. 其中0x0作为保留值, 表示与连接相关的frames作为一个整体而不是一个单独的流.
HEADERS帧
HEADERS帧(type=0x1)用于打开一个流,此外还携带一个首部块片段。
+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|E| Stream Dependency? (31) |
+-+-------------+-----------------------------------------------+
| Weight? (8) |
+-+-------------+-----------------------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
报头帧主体有以下字段:
- Pad Length : 8位的包含单位为字节帧填充长度字段。这个字段是可选的并只有在设置了PADDED 标记的情况下才呈现。
- E : 1位的标记用于标识流依赖是否是专用的。这个字段是可选的,并且只在优先级标记设置的情况下才呈现。
- Stream Dependency : 31位流所依赖的流的标识符的字段。这个字段是可选的,并且只在优先级标记设置的情况下才呈现。
- Weight : 流的8位权重标记。添加一个1-256的值来存储流的权重。这个字段是可选的,并且只在优先级标记设置的情况下才呈现。
- Header Block Fragment : 报头块。
- Padding : 填充字节
支持flags:
- END_STREAM
- END_HEADERS
- PADDED
- PRIORITY
DATA帧
DATA帧(type=0x0)传送与一个流关联的任意的,可变长度的字节序列。一个或多个DATA帧被用于,比如,携带HTTP请求或响应报体。
+---------------+
|Pad Length? (8)|
+---------------+-----------------------------------------------+
| Data (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
DATA帧包含如下的字段:
- Pad Length : 包含字节为单位的帧填充长度的8位字段。这个字段是可选的,并且只在设置了PADDED标记的情况下呈现。
- Data : 应用数据。数据量的大小是帧的有效载荷减去其他呈现字段的长度。
- Padding : 填充字节不包含任何应用语义值。填充字节必须在发送的时候设置为0,在接收的时候忽略。
支持flags:
- END_STREAM
- PADDED
其它桢
具体参考《RFC 7540 Translation 》中各桢介绍
流
通常我们最常用的 HTTP 请求 / 响应的帧形式如下
我们把这一系列桢表示一个逻辑请求的称之为流。
在一个HTTP/2的连接中, 流是服务器与客户端之间用于帧交换的一个独立双向序列. 流有几个重要的特点:
- 一个HTTP/2连接可以包含多个并发的流, 各个端点从多个流中交换frame
- 流可以被客户端或服务器单方面建立, 使用或共享
- 流也可以被任意一方关闭
- frames在一个流上的发送顺序很重要. 接收方将按照他们的接收顺序处理这些frame. 特别是
HEADERS
和DATA
frame的顺序, 在协议的语义上显得尤为重要. - 流用一个整数(流标识符)标记. 端点初始化流的时候就为其分配了标识符.
流的状态
下图展示了流的生存周期:
+--------+
send PP | | recv PP
,--------| idle |--------.
/ | | \
v +--------+ v
+----------+ | +----------+
| | | send H / | |
,------| reserved | | recv H | reserved |------.
| | (local) | | | (remote) | |
| +----------+ v +----------+ |
| | +--------+ | |
| | recv ES | | send ES | |
| send H | ,-------| open |-------. | recv H |
| | / | | \ | |
| v v +--------+ v v |
| +----------+ | +----------+ |
| | half | | | half | |
| | closed | | send R / | closed | |
| | (remote) | | recv R | (local) | |
| +----------+ | +----------+ |
| | | | |
| | send ES / | recv ES / | |
| | send R / v send R / | |
| | recv R +--------+ recv R | |
| send R / `----------->| |<-----------' send R / |
| recv R | closed | recv R |
`----------------------->| |<----------------------'
+--------+
send: 发送这个frame的终端
recv: 接受这个frame的终端
H: HEADERS帧 (隐含CONTINUATION帧)
PP: PUSH_PROMISE帧 (隐含CONTINUATION帧)
ES: END_STREAM标记
R: RST_STREAM帧
从上图一线路可知:
- 流是初始是idle
- 发送了HEADERS帧的到了 open
- 发送END_STREAM标记到了 half close
- 接收到RST_STREAM帧 就 close
Http2.0实战
根据《Netty概述》中http2组件.
Sever部份
public class Http2HelloWorldServerHandler extends Http2FrameAdapter {
private Http2ConnectionHandler http2ConnectionHandler;
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception {
if (endStream) {
res(ctx, streamId);
}
System.out.println(streamId + "headers");
}
@Override
public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) throws Http2Exception {
int len = data.readableBytes();
byte[] bytes = new byte[len];
data.readBytes(bytes);
System.out.println(streamId + "]accept---msg:" + new String(bytes));
if (endOfStream) {
res(ctx, streamId);
}
return super.onDataRead(ctx, streamId, data, padding, endOfStream);
}
private void res(ChannelHandlerContext ctx, int streamId) {
Http2Headers headers = new DefaultHttp2Headers().status(OK.codeAsText());
http2ConnectionHandler.encoder().writeHeaders(ctx, streamId, headers, 0, false, ctx.newPromise());
http2ConnectionHandler.encoder().writeData(ctx, streamId, ByteBufUtil.writeAscii(ctx.alloc(), "hello yoyo - via HTTP/2"), 0, true, ctx.newPromise());
}
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.option(ChannelOption.SO_BACKLOG, 1024);
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer(){
@Override
protected void initChannel(Channel ch) throws Exception {
// 创建frame(桢)监听器
Http2HelloWorldServerHandler handler = new Http2HelloWorldServerHandler();
// 创建http2.0解析器,并注入监听器
final Http2ConnectionHandler http2ConnectionHandler =
new Http2ConnectionHandlerBuilder()
.server(true)
.frameListener(handler)
.build();
handler.setHttp2ConnectionHandler(http2ConnectionHandler);
ChannelPipeline p = ch.pipeline();
p.addLast(http2ConnectionHandler);
}
});
Channel ch = b.bind(8080).sync().channel();
System.err.println("Open your web browser and navigate to http://127.0.0.1:8080/");
ch.closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public void setHttp2ConnectionHandler(Http2ConnectionHandler http2ConnectionHandler) {
this.http2ConnectionHandler = http2ConnectionHandler;
}
}
client部分
public final class Http2ClientStreamFrameResponseHandler extends SimpleChannelInboundHandler<Http2StreamFrame> {
private final CountDownLatch latch = new CountDownLatch(1);
@Override
protected void channelRead0(ChannelHandlerContext ctx, Http2StreamFrame msg) throws Exception {
System.out.println("Received HTTP/2 'stream' frame: " + msg);
// isEndStream() is not from a common interface, so we currently must check both
if (msg instanceof Http2DataFrame && ((Http2DataFrame) msg).isEndStream()) {
ByteBuf byteBuf = ((Http2DataFrame)msg).content();
int len = byteBuf.readableBytes();
byte[] bytes = new byte[len];
byteBuf.readBytes(bytes);
System.out.println("------------" + new String(bytes));
latch.countDown();
} else if (msg instanceof Http2HeadersFrame && ((Http2HeadersFrame) msg).isEndStream()) {
latch.countDown();
}
}
public boolean responseSuccessfullyCompleted() {
try {
return latch.await(5, TimeUnit.SECONDS);
} catch (InterruptedException ie) {
System.err.println("Latch exception: " + ie.getMessage());
return false;
}
}
public static void main(String[] args) throws Exception {
final EventLoopGroup clientWorkerGroup = new NioEventLoopGroup();
try {
final Bootstrap b = new Bootstrap();
b.group(clientWorkerGroup);
b.channel(NioSocketChannel.class);
b.option(ChannelOption.SO_KEEPALIVE, true);
b.remoteAddress("127.0.0.1", 8080);
b.handler(new ChannelInitializer(){
@Override
protected void initChannel(Channel ch) throws Exception {
final Http2FrameCodec http2FrameCodec = Http2FrameCodecBuilder.forClient()
.initialSettings(Http2Settings.defaultSettings()) // this is the default, but shows it can be changed.
.build();
ch.pipeline().addLast(http2FrameCodec);
ch.pipeline().addLast(new Http2MultiplexHandler(new SimpleChannelInboundHandler() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
}
}));
}
});
// Start the client.
final Channel channel = b.connect().syncUninterruptibly().channel();
System.out.println("Connected to [127.0.0.1:8080]");
final Http2ClientStreamFrameResponseHandler streamFrameResponseHandler = new Http2ClientStreamFrameResponseHandler();
final Http2StreamChannelBootstrap streamChannelBootstrap = new Http2StreamChannelBootstrap(channel);
final Http2StreamChannel streamChannel = streamChannelBootstrap.open().syncUninterruptibly().getNow();
streamChannel.pipeline().addLast(streamFrameResponseHandler);
// Send request (a HTTP/2 HEADERS frame - with ':method = GET' in this case)
final DefaultHttp2Headers headers = new DefaultHttp2Headers();
headers.method("GET");
headers.path("/");
headers.scheme( "http");
final Http2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(headers, false);
final Http2DataFrame http2DataFrame = new DefaultHttp2DataFrame(ByteBufUtil.writeAscii(PooledByteBufAllocator.DEFAULT, "hello - via HTTP/2") , true);
streamChannel.writeAndFlush(headersFrame);
streamChannel.writeAndFlush(http2DataFrame);
System.out.println("Sent HTTP/2 GET request to /");
// Wait for the responses (or for the latch to expire), then clean up the connections
if (!streamFrameResponseHandler.responseSuccessfullyCompleted()) {
System.err.println("Did not get HTTP/2 response in expected time.");
}
System.out.println("Finished HTTP/2 request, will close the connection.");
channel.close().syncUninterruptibly();
} finally {
clientWorkerGroup.shutdownGracefully();
}
}
}
http2.0对于http1.1改进
-
多路复用
-
在HTTP/1.1中已经默认使用来持久连接,可以做到多个请求复用在同一个tpc连接上,同时利用pipeline机制,可以让请求同时在一个tcp上发送,但是http本质上还是一个请求/响应模型,服务端仍然需要按照请求的顺序依次恢复,不能乱序回复。这样要是前面的回应特别慢,后面会有许多请求排队等着。这种情况称之为队头阻塞(head-of-line blocking)。
-
http2.0数据流以消息的形式发送,而消息由一个或多个帧组成,帧可以在数据流上乱序发送,然后再根据每个帧首部的流标识符重新组装.
-
性能会有极大的提升:
- 同个域名只需要占用一个TCP连接,消除了因多个TCP连接而带来的延时和内存消耗。突破浏览器同一域名只允许5个tcp的并发限制
- 单个连接上可以并行交错的请求和响应,之间互不干扰。
- 流优先级( Stream priority)
通过PRIORITY桢(标识)设置请求的优先值。 - 服务器推送(Server push)
通过PUSH_PROMISE帧主动推送数据 - 头部压缩(Header Compression)
HTTP 协议的请求头有大量的 key/value 文本组成,多个请求直接 key/value 重复程度很高。为了优化这部分,HTTP2.0采用了 HPACK.
- HPACK 提供了一个静态和动态的 table,静态 table 定义了通用的 HTTP header fields,对于常用的 key/value 文本无需重复传送,而是通过引用内部字典的整数索引来达到显著节省请求头传输流量的目的。对于动态 table,初始化为空,如果两边交互之后,发现有新的 field,就添加到动态 table 上面,这样后面的请求就可以跟静态 table 一样,只需要带上相关的 index 就可以了。
- 传输过程是: 先用压缩把报头列表序列化成一个报头区块. 然后将这个区块分割成一个或多个字节序列, 称之为区块分片. 分片可以作为HEADERS帧, PUSH_PROMISE帧或者CONTINUATION帧的报体
- @see io.netty.handler.codec.http2.HpackStaticTable
主要参考
《HTTP/2 RFC7540 中文版》
《RFC 7540 Translation 》
《rfc7541 》
《Http2.0 协议》
《HTTP、HTTP2.0 详解》