HTTP 2.0
是对1.x的扩展而非替代,之因此是“2.0”,是由于它改变了客户端与服务器之间交换数据的方式。HTTP 2.0
增长了新的二进制分帧数据层,而这一层并不兼容以前的HTTP 1.x
服务器及客户端——是谓2.0。 在正式介绍HTTP 2.0
以前,咱们须要先了解几个概念。html
Request
、Response
)对应的完整的一系列数据帧。HTTP 2.0
通讯的最小单位,如Header
帧(存储的是Header
)、DATA
帧(存储的是发送的内容或者内容的一部分)。 总所周知,HTTP 1.x
拥有队首阻塞、不支持多路复用、Header
没法压缩等诸多缺点。尽管针对这些缺点也提出了不少解决方案,如长链接、链接与合并请求、HTTP管道等,但都治标不治本,直到HTTP 2.0
的出现,它新增的如下设计从根本上解决了HTTP 1.x
所面临的诸多问题。java
HTTP 2.0
性能加强的核心,改变了客户端与服务器之间交互数据的方式,将传输的信息(Header
、Body
等)分割为更小的消息和帧,并采用二进制格式的编码。HTTP
消息分解为互不依赖的帧,而后乱序发送,最后再在另外一端把这些消息组合起来。HTTP 2.0
可让全部数据流共用一个链接,从而更有效的使用TCP
链接TCP
的流量控制实现是如出一辙的。HTTP 2.0
能够对一个客户端请求发送多个响应,即除了最初请求响应外,服务器还能够额外的向客户端推送资源,而无需客户端明确地请求。HTTP 2.0
会在客户端及服务器使用“首部表”来跟踪和存储以前发送的键-值对,对于相同的数据,不会再经过每次请求和响应发送。首部表在链接存续期间始终存在,由客户端及服务器共同渐进的更新。每一个新的首部键-值对要么追加到当前表的末尾,要么替换表中的值。 虽然HTTP 2.0
解决了1.x中的诸多问题,但它也存在如下问题。linux
HTTP
队首阻塞现象,但TCP
层次上仍然存在队首阻塞现象。要想完全解决这个问题,就须要完全抛弃TCP
,本身来定义协议。能够参考谷歌的QUIC。TCP
窗口缩放被禁用,那宽带延迟积效应可能会限制链接的吞吐量。TCP
拥塞窗口会缩小。 HTTP 2.0
的根本改进仍是新增的二进制分帧层。与HTTP 1.x
使用换行符分割纯文本不一样,二进制分帧层更加简介,经过代码处理起来更简单也更有效。 web
创建了HTTP 2.0
链接后,客户端与服务器会经过交换帧来通讯,帧也是基于这个新协议通讯的最小单位。全部帧都共享一个8字节的首部,其中包括帧的长度、类型、标志,还有一个保留位和一个31位的流标识符。 算法
HTTP 2.0
的流 HTTP 2.0
规定了如下的帧类型。promise
HTTP
消息体Header
) 在发送应用数据以前,必须建立一个新流并随之发送相应的元数据,好比流的优先级、HTTP首部等。HTTP 2.0
协议规定客户端和服务器均可以发起新流,所以有如下两种可能。服务器
HEADERS
帧来发起新流,这个帧里包含带有新流ID的公用首部、可选的31位优先值,以及一组HTTP
键值对首部PUSH_PROMISE
帧来发起推送流,这个帧与HEADER
帧等效,但它包含“要约流ID”,没有优先值 应用数据能够分为多个DATA帧,最后一帧要翻转帧首部的END_STREAM
字段。 网络
数据净荷不会被另行编码或压缩。DATA帧的编码方式取决于应用或者服务器,纯文本、gzip压缩、图片或者视频压缩格式均可以。整个帧由公用的8字节首部及HTTP净荷组成。 从技术上说,DATA帧的长度字段决定了每帧的数据净荷最多可达-1(65535)字节。但是,为了减小队首阻塞,
HTTP 2.0
标准要求DATA帧不能超过 (16383)字节。长度超过这个阀值的数据,就得分帧发送。并发
HTTP 2.0
是经过RealConnection
的startHttp2
方法开启的,在该方法中会建立一个Http2Connection
对象,而后调用Http2Connection
的start
方法。app
private void startHttp2(int pingIntervalMillis) throws IOException {
socket.setSoTimeout(0); // HTTP/2 connection timeouts are set per-stream.
//建立Http2Connection对象
http2Connection = new Http2Connection.Builder(true)
.socket(socket, route.address().url().host(), source, sink)
.listener(this)
.pingIntervalMillis(pingIntervalMillis)
.build();
//开启HTTP 2.0
http2Connection.start();
}
复制代码
在start
方法中会首先给服务器发送一个字符串PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n来进行协议的最终肯定,并用于创建 HTTP/2 链接的初始设置。而后给服务器发送一个SETTINGS
类型的Header
帧,该帧主要是将客户端每一帧的最大容量、Header
表的大小、是否开启推送等信息告诉给服务器。若是Window
的大小发生改变,就还须要更新Window
的大小(HTTP 2.0
的默认窗口大小为64KB,而客户端则须要将该大小改成16M,从而避免频繁的更新)。最后开启一个子线程来读取从服务器返回的数据。
public void start() throws IOException {
start(true);
}
void start(boolean sendConnectionPreface) throws IOException {
if (sendConnectionPreface) {
//发送一个字符串PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n来进行协议的最终肯定,即序言帧
writer.connectionPreface();
//告诉服务器本地的配置信息
writer.settings(okHttpSettings);
//okHttpSetting中Window的大小是设置为16M
int windowSize = okHttpSettings.getInitialWindowSize();
//默认是64kb,但若是在客户端则须要从新设置为16M
if (windowSize != Settings.DEFAULT_INITIAL_WINDOW_SIZE) {
//更新窗口大小
writer.windowUpdate(0, windowSize - Settings.DEFAULT_INITIAL_WINDOW_SIZE);
}
}
//子线程监听服务器返回的消息
new Thread(readerRunnable).start(); // Not a daemon thread.
}
复制代码
从ReaderRunnable
的名称就能够看出它是用来读取从服务器返回的各类类型数据。
class ReaderRunnable extends NamedRunnable implements Http2Reader.Handler {
...
@Override protected void execute() {
ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR;
ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR;
try {
//读取服务器返回的序言帧
reader.readConnectionPreface(this);
//不断的读取下一帧,全部消息从这里开始分发
while (reader.nextFrame(false, this)) {
}
connectionErrorCode = ErrorCode.NO_ERROR;
streamErrorCode = ErrorCode.CANCEL;
} catch (IOException e) {
...
} finally {
...
}
}
//读取返回的DATA类型数据
@Override public void data(boolean inFinished, int streamId, BufferedSource source, int length) throws IOException {...}
//读取返回的HEADERS类型数据
@Override public void headers(boolean inFinished, int streamId, int associatedStreamId, List<Header> headerBlock) {...}
//读取返回的RST_TREAM类型数据
@Override public void rstStream(int streamId, ErrorCode errorCode) {...}
//读取返回的SETTINGS类型数据
@Override public void settings(boolean clearPrevious, Settings newSettings) {...}
//回复服务器返回的ackSettings
private void applyAndAckSettings(final Settings peerSettings) ...} //恢复客户端发送的SETTING数据,客户端默认不实现 @Override public void ackSettings() {...}
//读取返回的PING类型数据
@Override public void ping(boolean reply, int payload1, int payload2) {...}
//读取服务器返回的GOAWAY类型数据
@Override public void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) {...}
//读取服务器返回的WINDOW_UPDATE类型数据
@Override public void windowUpdate(int streamId, long windowSizeIncrement) {...}
//读取服务器返回的PRIORITY类型数据
@Override public void priority(int streamId, int streamDependency, int weight, boolean exclusive) {...}
//读取返回的PUSH_PROMISE类型数据
@Override
public void pushPromise(int streamId, int promisedStreamId, List<Header> requestHeaders) {... }
//备用Service
@Override public void alternateService(int streamId, String origin, ByteString protocol, String host, int port, long maxAge) {...}
}
复制代码
上面简述了在OkHttp
中如何开启HTTP 2.0
协议。下面就来介绍客户端与服务器经过HTTP 2.0
协议来进行数据读写操做。
向服务器写入Header
是经过httpCodec.writeRequestHeaders(request)
来实现的,httpCodec
在HTTP 2.0
协议下的实现类是Http2Codec
。writeRequestHeaders
方法主要是建立一个新流Http2Stream
,在这个流建立成功后就会向服务器发送Headers
类型数据。
boolean hasRequestBody = request.body() != null;
List<Header> requestHeaders = http2HeadersList(request);
//建立新流
stream = connection.newStream(requestHeaders, hasRequestBody);
//咱们可能在建立新流并发送Headers时被要求取消,但仍然没有要关闭的流。
if (canceled) {
stream.closeLater(ErrorCode.CANCEL);
throw new IOException("Canceled");
}
...
}
//如下方法在Http2Connection类中
public Http2Stream newStream(List<Header> requestHeaders, boolean out) throws IOException {
return newStream(0, requestHeaders, out);
}
private Http2Stream newStream( int associatedStreamId, List<Header> requestHeaders, boolean out) throws IOException {
...
synchronized (writer) {
synchronized (this) {
//每一个TCP链接的流数量不能超过Integer.MAX_VALUE
if (nextStreamId > Integer.MAX_VALUE / 2) {
shutdown(REFUSED_STREAM);
}
if (shutdown) {
throw new ConnectionShutdownException();
}
//每一个流的ID
streamId = nextStreamId;
//下一个流的ID是在当前流ID基础上加2
nextStreamId += 2;
//建立新流
stream = new Http2Stream(streamId, this, outFinished, inFinished, null);
flushHeaders = !out || bytesLeftInWriteWindow == 0L || stream.bytesLeftInWriteWindow == 0L;
if (stream.isOpen()) {
streams.put(streamId, stream);
}
}
if (associatedStreamId == 0) {
//向服务器写入Headers
writer.headers(outFinished, streamId, requestHeaders);
} else if (client) {
throw new IllegalArgumentException("client streams shouldn't have associated stream IDs");
} else {//用于服务器
writer.pushPromise(associatedStreamId, streamId, requestHeaders);
}
}
//刷新
if (flushHeaders) {
writer.flush();
}
return stream;
}
复制代码
在客户端,流的ID是从3开始的全部奇数,在服务器,流的ID则是全部偶数。在Http2Connection
的构造函数中定义了定义了流ID的初始值。
Http2Connection(Builder builder) {
....
//若是是客户端,流的ID则从1开始
nextStreamId = builder.client ? 1 : 2;
if (builder.client) {
//在HTTP2中,1保留,用于升级
nextStreamId += 2;
}
...
}
复制代码
readResponseHeaders
是从服务器读取Headers
数据,该方法在Http2Codec
中。
@Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
//从流中拿到Headers信息,
Headers headers = stream.takeHeaders();
Response.Builder responseBuilder = readHttp2HeadersList(headers, protocol);
if (expectContinue && Internal.instance.code(responseBuilder) == HTTP_CONTINUE) {
return null;
}
return responseBuilder;
}
//该方法在Http2Stream中
public synchronized Headers takeHeaders() throws IOException {
readTimeout.enter();
try {
//若是队列中没有数据就等待
while (headersQueue.isEmpty() && errorCode == null) {
waitForIo();
}
} finally {
readTimeout.exitAndThrowIfTimedOut();
}
//从队列中拿到Headers数据
if (!headersQueue.isEmpty()) {
return headersQueue.removeFirst();
}
throw new StreamResetException(errorCode);
}
复制代码
headersQueue
是一个双端队列,它主要是存储服务器返回的Headers
。当服务器返回Headers
时,就会更新该链表。
在建立流的时候,都会建立一个FramingSink
及FramingSource
对象。FramingSink
用来向服务器写入数据,FramingSource
则读取服务器返回的数据。所以关于读/写Body
其实就是对Okio
的运用,不熟悉Okio
的能够先去了解一下Okio的知识。
//向服务器写数据
final class FramingSink implements Sink {
private static final long EMIT_BUFFER_SIZE = 16384;
...
@Override public void write(Buffer source, long byteCount) throws IOException {
assert (!Thread.holdsLock(Http2Stream.this));
sendBuffer.write(source, byteCount);
while (sendBuffer.size() >= EMIT_BUFFER_SIZE) {
emitFrame(false);
}
}
//
private void emitFrame(boolean outFinished) throws IOException {
...
try {
//向服务器写入DATA类型数据
connection.writeData(id, outFinished && toWrite == sendBuffer.size(), sendBuffer, toWrite);
} finally {
writeTimeout.exitAndThrowIfTimedOut();
}
}
...
}
//从服务器读取数据
private final class FramingSource implements Source {
//将从网络读取的数据写入该Buffer,仅供读线程访问
private final Buffer receiveBuffer = new Buffer();
//可读buffer
private final Buffer readBuffer = new Buffer();
//缓冲的最大字节数
private final long maxByteCount;
...
//从receiveBuffer中读取数据
@Override public long read(Buffer sink, long byteCount) throws IOException {...}
...
//接收服务器传递的数据,仅在ReaderRunnable中调用
void receive(BufferedSource in, long byteCount) throws IOException {...}
...
}
复制代码
前面介绍了从服务器读写数据,但不管如何都离不开Http2Reader
与Http2Writer
这两个类,毕竟这两个类才是真正向服务器执行读写操做的。先来看向服务器写数据。
final class Http2Writer implements Closeable {
...
//写入序言帧,来进行协议的最终肯定
public synchronized void connectionPreface() throws IOException {...}
//发送PUSH_PROMISE类型数据
public synchronized void pushPromise(int streamId, int promisedStreamId, List<Header> requestHeaders) throws IOException {...}
...
//发送RST_TREAM类型数据
public synchronized void rstStream(int streamId, ErrorCode errorCode) throws IOException {...}
//发送DATA类型数据
public synchronized void data(boolean outFinished, int streamId, Buffer source, int byteCount) throws IOException {...}
//发送SETTINGS类型数据
public synchronized void settings(Settings settings) throws IOException {...}
//发送PING类型数据
public synchronized void ping(boolean ack, int payload1, int payload2) throws IOException {...}
//发送GOAWAY类型数据
public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData) throws IOException {...}
//发送WINDOW_UPDATE类型数据,进行Window更新
public synchronized void windowUpdate(int streamId, long windowSizeIncrement) throws IOException {...}
//发送HEADERS类型数据
public void frameHeader(int streamId, int length, byte type, byte flags) throws IOException {...}
@Override public synchronized void close() throws IOException {
closed = true;
sink.close();
}
...
//写入CONTINUATION类型数据
private void writeContinuationFrames(int streamId, long byteCount) throws IOException {...}
//写入headers
void headers(boolean outFinished, int streamId, List<Header> headerBlock) throws IOException {...}
}
复制代码
下面再来看看从服务器读数据,基本上就是根据数据的类型来进行分发。
final class Http2Reader implements Closeable {
...
//读取数据
public boolean nextFrame(boolean requireSettings, Handler handler) throws IOException {
try {
source.require(9); // Frame header size
} catch (IOException e) {
return false; // This might be a normal socket close.
}
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Length (24) |
// +---------------+---------------+---------------+
// | Type (8) | Flags (8) |
// +-+-+-----------+---------------+-------------------------------+
// |R| Stream Identifier (31) |
// +=+=============================================================+
// | Frame Payload (0...) ...
// +---------------------------------------------------------------+
int length = readMedium(source);
if (length < 0 || length > INITIAL_MAX_FRAME_SIZE) {
throw ioException("FRAME_SIZE_ERROR: %s", length);
}
byte type = (byte) (source.readByte() & 0xff);
if (requireSettings && type != TYPE_SETTINGS) {
throw ioException("Expected a SETTINGS frame but was %s", type);
}
byte flags = (byte) (source.readByte() & 0xff);
int streamId = (source.readInt() & 0x7fffffff); // Ignore reserved bit.
if (logger.isLoggable(FINE)) logger.fine(frameLog(true, streamId, length, type, flags));
//这里的handler是ReaderRunnable对象
switch (type) {
case TYPE_DATA:
readData(handler, length, flags, streamId);
break;
case TYPE_HEADERS:
readHeaders(handler, length, flags, streamId);
break;
case TYPE_PRIORITY:
readPriority(handler, length, flags, streamId);
break;
case TYPE_RST_STREAM:
readRstStream(handler, length, flags, streamId);
break;
case TYPE_SETTINGS:
readSettings(handler, length, flags, streamId);
break;
case TYPE_PUSH_PROMISE:
readPushPromise(handler, length, flags, streamId);
break;
case TYPE_PING:
readPing(handler, length, flags, streamId);
break;
case TYPE_GOAWAY:
readGoAway(handler, length, flags, streamId);
break;
case TYPE_WINDOW_UPDATE:
readWindowUpdate(handler, length, flags, streamId);
break;
default:
// Implementations MUST discard frames that have unknown or unsupported types.
source.skip(length);
}
return true;
}
...
}
复制代码
在Http2Reader
与Http2Writer
中都是以帧的形式(二进制)来读取或者写入数据的,这样相对字符串效率会更高,固然,咱们还能够用哈夫曼算法(OkHttp
支持哈夫曼算法)来对帧进行压缩,从而得到更好的性能。 记得在HTTP 1.x
协议下的网络优化就有用Protocol Buffer
(二进制)来替代字符串传递这一个选择,而若是用HTTP 2.0
则无需使用Protocol Buffer
。
到这里,相必对HTTP 2.0
有了一个大概的了解,更多的就须要去实践了。固然若是要使用HTTP 2.0
协议,就须要客户端及服务器一块儿才能搞定。注意:目前OKHttp
仅支持在https
请求下使用HTTP 2.0
。
【参考资料】 《Web性能权威指南》 科普:QUIC协议原理分析 初识HTTP2.0协议 HTTP 2.0 协议详解 HTTP协议探究(六):H2帧详解和HTTP优化 HTTP/2笔记之链接创建