OkHttp也对Http2.0进行了支撑,2.0版别对1.1功用有了很大的提升,前几章剖析的根本流程没有变化,仅仅传输的过程有了变化。咱们先介绍下Http2.0的特征,再在OKHttp源码的根底上看看是怎么完结的。
Http各版别差异
- Http从0.9版别开端,只支撑Get恳求,没有头部的设定
- 1.0版别确认了根本的结构,包含报文的格局,从状况行/Header/body等。缓存和长衔接等规范,可是都是初级的设定
- 1.1版别默许了长衔接的装备,供给了管道化,进一步提高了传输的功用。在缓存部分,增加了更多的灵活性,包含Cache-Control和ETag等属性,前面讲
CacheInterceptor
时,都剖析了相应的缓存字段。 - 2.0版别在功用上更近了一步,首要升级在功用上,而且增加了服务器推送等功用。下面具体看下
Http 2.0特色
已然出台了更新的计划,肯定是之前的计划有问题。根本都是功用的问题
Http1.1存在的问题
- Header数据冗余,每次发送都要带着儿好多共同的数据,比如Cookie和agent等完全相同的字段,是非常浪费带宽的。
- 头部堵塞,关于多次的恳求,只能供给一问一答的办法,也便是第一次恳求完结,才能发送第二次恳求。尽管供给了pipeline机制,可是恳求和相应有必要依照顺序进行传输的,浏览器很难完结,大都浏览器封闭了这个功用。
- 半双工,尽管TCP是全双工的,浪费了TCP的能力,pipeline也是单向串行。也是线头堵塞的根本原因。
- 客户端需求主动恳求 为了解决上面的问题,Http2.0出台了一些办法优化
二进制分帧层
这个是一个根底的功用,在上一篇剖析传输时,咱们看到Http 1.1往流中写入的多是文本编码的Utf-8格局的,可是Http2.0默许把数据运用二进制进行编码。并抽象成帧这种数据结构。又分了许多不同类型的帧。帧便是在传输中的根本单位了。由于Http2不改变之前的语意,所以分帧层是作用在应用层的下一层的,对用户是感知不到的。
多路复用
上一篇剖析传输时,在Http1.1中,一个衔接只能树立一个流,可是在Http2.0中,一个衔接上能够创立多个流,这多个流还能够一起的传递帧,而且由于是二进制分帧层,所以就解决了方才提到的线头堵塞的问题,多个流和二进制是完结复用的两个根底。可是由于TCP的缓存有限,线头堵塞仍是会产生的。Http1.1假如要完结一起发送,只能另外树立TCP衔接,可是由于慢启动等特性,功用是非常差的。关于乱序发送的帧,能够运用ID和序号来从头进行排列,组成终究的数据。
紧缩Header
Header没有进行紧缩,而且每次都要发送许多一样很长的数据。Http2.0运用了HPACK紧缩格局来紧缩首部。分为是哪个过程,先经过静态词典紧缩、再经过动态词典紧缩、终究Huffman编码对传输的首部字段进行编码。三个过程大大的减少了需求传输的数据量了。这些鄙人面的OkHttp代码中都有涉及,能够说非常高效。
服务端推送
服务端能够主意向客户端推送数据。完结原理便是客户端宣布页面恳求时,服务器端能够剖析这个页面所依赖的其他资源,主动推送到客户端的缓存,当客户端收到原始网页的恳求时,它需求的资源现已坐落缓存。并发送一个帧到客户端。相关于WebSocket,功用仍是差了点。
OKHttp怎么完结Http 2.0特色的
OKHttp为了完结Http 2.0。顶层的类首要运用了Http2Codec
和Http2Connection
。剖析也是经过他们作为切入点。Http2Codec
咱们比较应该表明了解,上一篇讲了Http1Codec
两个类都是完结了HttpCodec
接口,都有相同的功用,包含写入Header、body、进行恳求等。Http2Codec
运用了Http2.0的办法进行了作业。
怎么判别运用Http2.0
在OKHttp中有两种Http2.0协议,分别是Http2.0明文,简称H2c,运用了明文协议,另一种是一般的Http2.0.简称H2,能够运用SSL协议进行加密。
这些在咱们装备OKHttpClient时,能够经过protocols()
进行设置,默许的协议是1.1和2.0。
在树立完结衔接后,首要检查是否支撑Https,假如支撑那么就会进行TSL握手,假如协商了Http的版别,就会用装备的版别,假如没有装备,默许1.1。假如没有支撑Https,那么假如在OkHttpClient装备了H2
c的协议,就运用2.0协议。没有就运用1.1。startHttp2(pingIntervalMillis);
开端对2.0的运用。
private void startHttp2(int pingIntervalMillis) throws IOException {
socket.setSoTimeout(0); // HTTP/2 connection timeouts are set per-stream.
http2Connection = new Http2Connection.Builder(true)
.socket(socket, route.address().url().host(), source, sink)
.listener(this)
.pingIntervalMillis(pingIntervalMillis)
.build();
http2Connection.start();
}
这儿取消了socket的超时时刻,由于超时时刻需求对每个流进行设置,生效的单位变了。而且创立了一个Http2Connection。奇怪的是调用了start办法,敞开了一个线程。这儿线程的作用是敞开读取网络呼应。从readerRunnable这个变量也能够看出。
void start(boolean sendConnectionPreface) throws IOException {
。。。
new Thread(readerRunnable).start();
}
Http2Codec
的创立也是经过http2Connection的状况进行判别,假如是http2Connection不为空,那么就创立一个Http2.0下的流Http2Codec。
public HttpCodec newCodec(OkHttpClient client, Interceptor.Chain chain,
StreamAllocation streamAllocation) throws SocketException {
if (http2Connection != null) {
return new Http2Codec(client, chain, streamAllocation, http2Connection);
} else {
//http1.1
}
}
经过上面的两部,就完结了顶层类的创立。
怎么完结多路复用
关于多路复用,有两个前提条件,分别是多个流和二进制分帧。在一个衔接上创立更多的流。这个最大值是在RealConnection#allocationLimit
进行设置的,默许为1,也便是在Http1.1的默许装备,在敞开Http2后,又对去进行了赋值。
allocationLimit = http2Connection.maxConcurrentStreams();
多个流现已完结了,关于二进制分帧,咱们在剖析传输的时分就会看到了。
下面的剖析流程,仍是依照上一篇的结构来,也便是依照HttpCodec
接口,分为写入Header、写入Body、恳求、读取Header、读取Body
写入Header
写入Header,首要经过writeRequestHeaders
办法,在Http1Codec
中的完结,首要是经过写入UTF-8格局的状况行和Header部分。在Http2Codec
的完结,会比较复杂。
@Override public void writeRequestHeaders(Request request) throws IOException {
if (stream != null) return;
boolean hasRequestBody = request.body() != null;
List<Header> requestHeaders = http2HeadersList(request);
stream = connection.newStream(requestHeaders, hasRequestBody);
stream.readTimeout().timeout(chain.readTimeoutMillis(), TimeUnit.MILLISECONDS);
stream.writeTimeout().timeout(chain.writeTimeoutMillis(), TimeUnit.MILLISECONDS);
}
首要经过http2HeadersList对Header各个字段进行了处理。然后调用了newStream新建里一个stream。Header的紧缩和写入逻辑也在里面,之后对单独的Stream设置了读和写的超时时刻。整体逻辑是这样,咱们细看下每一块。
Stream是什么
Http2Stream
是逻辑双向流。Http2.0的传输功用和1.x的类结构还有很大差异。Http2Codec
仍是传送的进口,可是不像Http1Codec
直接封装了输入输出流,传输Header交给了Http2Connection
,今后的传输和接收作业交给了Http2Stream
。
在上面写入Header数据,看到了创立了一个stream,也便是Http2Stream
。今后的写入Body,读取恳求都是调用她来完结的。
Http2.0的流便是Http2Stream
,存储的一个衔接上多个流的数据结构是Http2Connection#streams
。是一个Map,在数据回来时,会经过Http2Stream
的id进行匹配,拿到对应的Http2Stream
进行写入。这样外部在经过Http2Codec
读取数据时,就从Http2Stream
读取即可。Http1Codec
的责任由Http2Connection
来完结,作为实践封装底层输入输出流的控件。在输出数据时,输出到Http2Stream
中,经过StreamId进行不同的写入。大体结构如此。
stateDiagram-v2
[*] --> 写入Header数据
写入Header数据 --> Http2Connection
Http2Connection --> Http2Stream
Http2Connection --> 读取写入呼应Header
读取写入呼应Header --> Http2Stream
Http2Codec --> 读取呼应
读取呼应 --> Http2Stream
header的转化
List<Header> requestHeaders = http2HeadersList(request);
经过http2HeadersList对原始的Header数据进行了转化。
public static List<Header> http2HeadersList(Request request) {
Headers headers = request.headers();
List<Header> result = new ArrayList<>(headers.size() + 4);
result.add(new Header(TARGET_METHOD, request.method()));
result.add(new Header(TARGET_PATH, RequestLine.requestPath(request.url())));
String host = request.header("Host");
if (host != null) {
result.add(new Header(TARGET_AUTHORITY, host)); // Optional.
}
result.add(new Header(TARGET_SCHEME, request.url().scheme()));
for (int i = 0, size = headers.size(); i < size; i++) {
ByteString name = ByteString.encodeUtf8(headers.name(i).toLowerCase(Locale.US));
if (!HTTP_2_SKIPPED_REQUEST_HEADERS.contains(name.utf8())) {
result.add(new Header(name, headers.value(i)));
}
}
return result;
}
这儿没有看到对状况行的处理,Http2.0把状况行也放到了header帧里。比如经过TARGET_METHOD,放置了恳求的办法,TARGET_PATH放置了途径,这些都是在恳求的状况行里的。并放置了TARGET_SCHEME,当前时http仍是https。header的name和value同样经过了utf-8编码。
header的紧缩写入
header的写入是经过writer.synStream进行的。
writer.synStream(outFinished, streamId, associatedStreamId, requestHeaders);
writer是Http2Writer
类型,它是担任写入的,封住了底层Socket的输入流。在这儿做分帧的操作。呼应的数据首要经过Http2Reader
类。封装了底层Socket的输出流。Http2Connection
代替了Http2Codec
的功用。
graph TD
Http2Connection --> 输入Http2Writer
Http2Connection --> 输出Http2Reader
synStream又调用了Http2Writer#headers()
进行终究的写入。
void headers(boolean outFinished, int streamId, List<Header> headerBlock) throws IOException {
if (closed) throw new IOException("closed");
hpackWriter.writeHeaders(headerBlock);
long byteCount = hpackBuffer.size();
int length = (int) Math.min(maxFrameSize, byteCount);
byte type = TYPE_HEADERS;
byte flags = byteCount == length ? FLAG_END_HEADERS : 0;
if (outFinished) flags |= FLAG_END_STREAM;
frameHeader(streamId, length, type, flags);
sink.write(hpackBuffer, length);
if (byteCount > length) writeContinuationFrames(streamId, byteCount - length);
}
2.0对Header的紧缩是经过hpack,那么便是经过hpackWriter.writeHeaders进行紧缩,并写入hpackBuffer
。并经过下面的sink.write(hpackBuffer, length)进行写入的。两部就完结了终究的写入。
header紧缩的逻辑首要在hpackWriter.writeHeaders中。上面也说了,分为静态词典,动态词典, huffman
编码等。
- 静态表
静态表便是匹配name和value,假如相同,就运用index进行传输,服务器和客户端都有一份相同的表,这样传递相应的id就能够完结数据的传输。
2. 动态表
动态表传输现已传输过的字段,比如cookie,这样在传输今后,就不需求从头传输了,只要传输动态表里的idnex即可。经过dynamicTable
进行存储。
3. huffman编码
写入的时分经过Huffman.get().encode(data, huffmanBuffer);
进行紧缩
写入Body
@Override public Sink createRequestBody(Request request, long contentLength) {
return stream.getSink();
}
写入body经过createRequestBody进行,逻辑和Http1Codec
共同。经过回来的sink,调用 RequestBody#writeTo
进行写入。
恳求
@Override public void flushRequest() throws IOException {
connection.flush();
}
直接调用了Http2Connection
的flush。进而调用Http2Writer#flush
。终究调用 sink.flush();
完结写入缓存区提交。完结恳求。
读取Header
不管是Header的读取仍是body的读取。都是在Http2Connection
中完结的。前面提到Http2Connection
的ReaderRunnable
是一个另一个线程运转的。他不断在读取输出流,拿到传输过来的帧。并交给Http2Stream
。
首要的获取逻辑都在ReaderRunnable
中。直接看最首要的run办法。
@Override protected void execute() {
ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR;
ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR;
try {
reader.readConnectionPreface(this);
while (reader.nextFrame(false, this)) {
}
}
。。。
}
能够看出循环里面的reader.nextFrame,共同在取输出流的帧。
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.
}
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));
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;
}
首要调用了require
办法,这是一个堵塞的办法,知道输出流中又了9个字节的数据,才会进行回来。拿到了9个字节的数据。就开端处理这些数据。
为什么是9个字节呢?Headers Frame: 帧头固定的9个字节。装备了这个帧的属性。
比如type和streamId。依据不同的帧的类型,调用不同的办法进行读取。比如Header,type便是TYPE_HEADERS。并调用readHeaders(handler, length, flags, streamId)进行读取。其他类型的逻辑也相似。
读取Body
读取body的逻辑和上面相似,仅仅类型不同。这儿不细讲了。
代码里许多流和位运算仍是很难懂。