上一篇介绍了ConnectInterceptor,首要是担任衔接的作业,本篇首要介绍CallServerInterceptor,首要担任在流上传输恳求数据,并处理承受数据,而在OKHttp中,这个流工具类便是HttpCodec

CallServerInterceptor完成恳求发送中,首要的逻辑分几个过程,分别是

  1. 恳求数据Header写入
  2. 恳求数据Body写入
  3. 写入全部网络恳求
  4. 回来数据Header读取
  5. 回来数据Body读取

咱们分四个过程分别剖析下,HttpCodec还有Http1.1下的HttpCodec1和Http2.0下的HttpCodec2两个实现类,下一篇会剖析Http2.0的内容。
所以本篇提到的HttpCodec实现都是HttpCodec1。

HttpCodec

HttpCodec作为流的封装,封装了底层Socket的输入/输出流。OKHttp对底层的输入/输出流怎样处理的呢?在上一篇衔接的时分,调用connectSocket衔接Socket的时分,运用了OKio对底层Socket的流进行了封装。

source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
#Okio#source
public static Source source(Socket socket) throws IOException {
  if (socket == null) throw new IllegalArgumentException("socket == null");
  if (socket.getInputStream() == null) throw new IOException("socket's input stream == null");
  AsyncTimeout timeout = timeout(socket);
  Source source = source(socket.getInputStream(), timeout);
  return timeout.source(source);
}

调用的Okio.source,对socket.getInputStream()输入流进行了处理,输出流也同理。

final BufferedSource source;
final BufferedSink sink;

HttpCodec便是封装运用了上面的流来进行网络恳求数据的传输和接收的。最底层还是系统提供的Socket输入输出流。

CallServerInterceptor拦截器全体框架

@Override public Response intercept(Chain chain) throws IOException {
  //恳求数据Header写入
  。。。
  Response.Builder responseBuilder = null;
  if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
    if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
      //处理100-continue情况
      。。。
    }
    if (responseBuilder == null) {
      //恳求数据Body写入
      。。。
    }
  }
  //写入全部网络恳求
  httpCodec.finishRequest();
  if (responseBuilder == null) 
    //回数据Header读取
    。。。
  }
   //回来数据Body读取
   。。。
   //处理异常情况
   。。。
   return response;
}

上面intercept的办法的过程处理便是上面注释的部分,下面每个部分独立的讲下。

恳求数据Header写入

realChain.eventListener().requestHeadersStart(realChain.call());
httpCodec.writeRequestHeaders(request);
realChain.eventListener().requestHeadersEnd(realChain.call(), request);

恳求Header的写入便是调用httpCodec.writeRequestHeaders,传入咱们恳求时创立的request。HttpCodec内部又会调用writeRequest进行传输。

@Override public void writeRequestHeaders(Request request) throws IOException {
  String requestLine = RequestLine.get(
      request, streamAllocation.connection().route().proxy().type());
  writeRequest(request.headers(), requestLine);
}
public void writeRequest(Headers headers, String requestLine) throws IOException {
  if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
  sink.writeUtf8(requestLine).writeUtf8("\r\n");
  for (int i = 0, size = headers.size(); i < size; i++) {
    sink.writeUtf8(headers.name(i))
        .writeUtf8(": ")
        .writeUtf8(headers.value(i))
        .writeUtf8("\r\n");
  }
  sink.writeUtf8("\r\n");
  state = STATE_OPEN_REQUEST_BODY;
}

首要是一个state变量,标记了这次网络恳求的状况。咱们每进行一步,这个state就会迁移到一个新的状况,表明下一个应该进行的操作,相似一个状况机。假如咱们跨状况履行操作,就会出异常。

状况值 含义
STATE_IDLE 初始状况
STATE_OPEN_REQUEST_BODY 应该履行写入恳求Header
STATE_WRITING_REQUEST_BODY 应该履行写入恳求的body
STATE_READ_RESPONSE_HEADERS 应该履行读取呼应Header
STATE_OPEN_RESPONSE_BODY 应该履行读取呼应body
STATE_READING_RESPONSE_BODY 正在读取呼应body
STATE_CLOSED 读取body完成后

实践的写入操作便是往sink流里写入数据。这儿的写入逻辑和Http的数据报的格局一样。

  1. 写入状况行,sink.writeUtf8(requestLine).writeUtf8(“\r\n”); requestLine便是一个状况行,通过RequestLine.get()进行获取。状况行包括恳求办法、恳求url、Http版本。都是用空格分离。
  2. 再写入一个空行(起距离作用)
  3. 写入Header,通过headers.name(i)和headers.value(i)进行读取。每个header字段之间还会运用一个空行进行阻隔。
  4. 写入空行
    通过上面的过程就完成了写入Header

恳求数据Body写入

恳求body写入前,有一个预先的处理,便是Expect"100-continue"的情况,这种情况下需求提早恳求网络,问询是否能够持续写入恳求的呼应部分。假如服务器回来100。那么就能够持续传输body。否则跳过body的传输。

if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
  httpCodec.flushRequest();
  realChain.eventListener().responseHeadersStart(realChain.call());
  responseBuilder = httpCodec.readResponseHeaders(true);
}
if (responseBuilder == null) {
  // 写入body处理
  realChain.eventListener().requestBodyStart(realChain.call());
  long contentLength = request.body().contentLength();
  CountingSink requestBodyOut =
      new CountingSink(httpCodec.createRequestBody(request, contentLength));
  BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
  request.body().writeTo(bufferedRequestBody);
  bufferedRequestBody.close();
  realChain.eventListener()
      .requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
} else if (!connection.isMultiplexed()) {
   streamAllocation.noNewStreams();
}

首要判别了假如Expect字段是100-continue,那么会直接调用httpCodec.flushRequest()恳求网络,把方才写入的Header发送到服务器,恳求的履行便是这么简单,先写入再恳求。
这时分再通过httpCodec.readResponseHeaders(true)读取网络恳求回来的Headers数据。详细怎样读取的下面会讲到。

http 100–continue用于客户端在发送POST数据给服务器前,征询服务器情况,看服务器是否处理POST的数据,假如不处理,客户端则不上传POST数据,假如处理,则POST上传数据。在实际运用中,通过在POST大数据时会运用100-continue协议。

因为这儿的read穿入了true,所以回来的responseBuilder,假如为空,表明此次呼应回来了100,能够持续传送body。假如不为空,那么服务器没有回来100,禁止持续传递body。
下面运用了responseBuilder进行了判别,假如为null,那么会持续写入body部分。不为null就跳出不写入。并且没有回来100的情况下,这个衔接也不能持续运用了。
写入body也比较简单,直接创立了一个CountingSink,调用body的writeTo办法直接进行写入。CountingSink是通过createRequestBody办法创立的。

@Override public Sink createRequestBody(Request request, long contentLength) {
  if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
    return newChunkedSink();
  }
  if (contentLength != -1) {
    return newFixedLengthSink(contentLength);
  }
  throw new IllegalStateException(
      "Cannot stream a request body without chunked encoding or a known content length!");
}

上面对写入body的sink,分为两种情况,假如Transfer-Encoding是chunked,那么就运用ChunkedSink
进行写入,假如有contentLength属性,那么运用FixedLengthSink进行写入。
这儿就有问题,这儿只是写入了上面两种Sink中,并且都是新建的,那是怎样写入HttpCodec的sink中呢。
原来上面两种流是HttpCodec的内部类,内部封装了底层的HttpCodec的sink输入流,所以也是间接的输入到了HttpCodec的sink中。
至此body写入也完成了。

写入全部网络恳求

这块的内容上面现已谈到了,调用httpCodec.flushRequest(),直接把写入流的数据进行传递。就相当一把枪。装入子弹(写入流),进行射击(flush)。

回来数据Header读取

StatusLine statusLine = StatusLine.parse(readHeaderLine());
Response.Builder responseBuilder = new Response.Builder()
     .protocol(statusLine.protocol)
     .code(statusLine.code)
     .message(statusLine.message)
     .headers(readHeaders());

读取Header通过readHeaderLine()和readResponseHeaders()构建出一个Response。readHeaderLine()担任读取状况行,而readResponseHeaders()担任读取呼应的Header。这样就完成了除body外的所有的数据。分别看下两段解析的逻辑。

读取状况行

StatusLine statusLine = StatusLine.parse(readHeaderLine());

运用上面代码就能够读取到StatusLine这个数据,是状况行的抽象。StatusLine#parse这种用法,常常运用java应该很熟悉,这时静态的工厂办法,相似Integer.parse等。readHeaderLine()获取到格局化的数据,再运用StatusLine#parse进行解析成成品。
先看readHeaderLine()

private String readHeaderLine() throws IOException {
  String line = source.readUtf8LineStrict(headerLimit);
  headerLimit -= line.length();
  return line;
}

这儿运用source.readUtf8LineStrict读取独自的行,这个行通过\r\n来分割的。headerLimit标记了回来的最长的长度,避免没有\r\n
因为Http数据报运用\r\n进行分割,所以第一个进行分割的便是状况行的内容,这个内容是一个字符串。得到了这个字符串。就能够运用StatusLine.parse进行解析了。内部解析的逻辑便是解析字符串。有爱好的能够自己阅览。

读取Header

读取Header通过readHeaders

public Headers readHeaders() throws IOException {
  Headers.Builder headers = new Headers.Builder();
  for (String line; (line = readHeaderLine()).length() != 0; ) {
    Internal.instance.addLenient(headers, line);
  }
  return headers.build();
}

通过上面写入Header,咱们知道。Header的各个字段也是通过\r\n进行分割的。所以这儿也是通过readHeaderLine()进行解析的,每次取出一个行,再通过Internal.instance.addLenient传入Header的Builder。
这样就完成了Header的读取。

response的组装

获取了状况行和Header的数据,就要组装进Response里了。

Response.Builder responseBuilder = new Response.Builder()
    .protocol(statusLine.protocol)
    .code(statusLine.code)
    .message(statusLine.message)
    .headers(readHeaders());

通过上面的代码,分别对每个变量进行赋值。在组装完状况行和Header后,拦截器内还会进行组装。

Response response = responseBuilder
    .request(request)
    .handshake(streamAllocation.connection().handshake())
    .sentRequestAtMillis(sentRequestMillis)
    .receivedResponseAtMillis(System.currentTimeMillis())
    .build();

这儿有几个重要的字段

  1. handshake表明TSL握手的记录
  2. sentRequestAtMillis 和 receivedResponseAtMillis表明恳求宣布的时刻和接收到呼应的时刻

回来数据Body读取

response = response.newBuilder()
    .body(httpCodec.openResponseBody(response))
    .build();

相同也是通过Buider进行处理,调用body传入ReesponseBody,这个对象咱们应该很熟悉,每次获取相应body都是通过这个对象。那他是怎样创立的呢。

@Override public ResponseBody openResponseBody(Response response) throws IOException {
  streamAllocation.eventListener.responseBodyStart(streamAllocation.call);
  String contentType = response.header("Content-Type");
  if (!HttpHeaders.hasBody(response)) {
    Source source = newFixedLengthSource(0);
    return new RealResponseBody(contentType, 0, Okio.buffer(source));
  }
  if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
    Source source = newChunkedSource(response.request().url());
    return new RealResponseBody(contentType, -1L, Okio.buffer(source));
  }
  long contentLength = HttpHeaders.contentLength(response);
  if (contentLength != -1) {
    Source source = newFixedLengthSource(contentLength);
    return new RealResponseBody(contentType, contentLength, Okio.buffer(source));
  }
  return new RealResponseBody(contentType, -1L, Okio.buffer(newUnknownLengthSource()));
}

上面的代码咱们如同很熟悉,在哪儿见过。
在写入body时,也有相似的逻辑。

  1. 先判别是否有body,假如没有body,那么回来一个长度0的fixedLengthSource
  2. 判别时分块传输,那就运用一个ChunkedSource
  3. 假如有长度,运用FixedLengthSource,参数是长度
  4. 没有长度也不分块,就回来一个newUnknownLengthSource
    看了上面的逻辑,咱们发现这儿并没有读取出来数据,而是让ResponseBody封装了一个输出流。这个输出流在哪儿读取的呢。这个是咱们自己担任的,咱们读取数据时,都是在读取这个输出流。并且只能读取一次。

处理异常情况

close封闭衔接

有两种情况需求处理

if ("close".equalsIgnoreCase(response.request().header("Connection"))
    || "close".equalsIgnoreCase(response.header("Connection"))) {
  streamAllocation.noNewStreams();
}
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
  throw new ProtocolException(
      "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}

假如呼应Connection字段为close,那么这时需求封闭这个衔接,这个衔接不能持续运用了。调用 streamAllocation.noNewStreams();这个办法上一篇讲过,假如这个衔接不能从ConnectionPoll复用,那么就会创立一个新的Connection。

204/205

假如回来204/205,这两个相当于运用缓存,这时分表明射中缓存,不会回来body,假如回来了,阐明有问题。

总结

通过前面的几章,咱们应该对OKHttp的恳求的全体有了很不多的掌握。OKHttp的规划运用拦截器形式,真的对于网络恳求的复杂性进行很好的解耦,网络的处理很适合用拦截器这种形式。欢迎进行评论。
到这儿就完毕了吗?不是的。还有许多重要的细枝末节还没有涉及,比如Http2.0、Https、webSocket的支撑等。
后边的文章还会剖析下这几个方面的源码处理。