OKHttp源码分析(八)Http2.0版本处理

OKHttp源码分析(八)Http2.0版本处理

OkHttp也对Http2.0进行了支撑,2.0版别对1.1功用有了很大的提升,前几章剖析的根本流程没有变化,仅仅传输的过程有了变化。咱们先介绍下Http2.0的特征,再在OKHttp源码的根底上看看是怎么完结的。

Http各版别差异

OKHttp源码分析(八)Http2.0版本处理

  1. Http从0.9版别开端,只支撑Get恳求,没有头部的设定
  2. 1.0版别确认了根本的结构,包含报文的格局,从状况行/Header/body等。缓存和长衔接等规范,可是都是初级的设定
  3. 1.1版别默许了长衔接的装备,供给了管道化,进一步提高了传输的功用。在缓存部分,增加了更多的灵活性,包含Cache-Control和ETag等属性,前面讲CacheInterceptor时,都剖析了相应的缓存字段。
  4. 2.0版别在功用上更近了一步,首要升级在功用上,而且增加了服务器推送等功用。下面具体看下

Http 2.0特色

已然出台了更新的计划,肯定是之前的计划有问题。根本都是功用的问题

Http1.1存在的问题

  1. Header数据冗余,每次发送都要带着儿好多共同的数据,比如Cookie和agent等完全相同的字段,是非常浪费带宽的。
  2. 头部堵塞,关于多次的恳求,只能供给一问一答的办法,也便是第一次恳求完结,才能发送第二次恳求。尽管供给了pipeline机制,可是恳求和相应有必要依照顺序进行传输的,浏览器很难完结,大都浏览器封闭了这个功用。
  3. 半双工,尽管TCP是全双工的,浪费了TCP的能力,pipeline也是单向串行。也是线头堵塞的根本原因。
  4. 客户端需求主动恳求 为了解决上面的问题,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。顶层的类首要运用了Http2CodecHttp2Connection。剖析也是经过他们作为切入点。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编码等。

  1. 静态表

OKHttp源码分析(八)Http2.0版本处理
静态表便是匹配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中完结的。前面提到Http2ConnectionReaderRunnable是一个另一个线程运转的。他不断在读取输出流,拿到传输过来的帧。并交给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的逻辑和上面相似,仅仅类型不同。这儿不细讲了。

代码里许多流和位运算仍是很难懂。