OKHTTP拦截器CallServerInterceptor的简单分析

OKHTTP异步和同步请求简单分析
OKHTTP拦截器缓存策略CacheInterceptor的简单分析
OKHTTP拦截器ConnectInterceptor的简单分析
OKHTTP拦截器CallServerInterceptor的简单分析
OKHTTP拦截器BridgeInterceptor的简单分析
OKHTTP拦截器RetryAndFollowUpInterceptor的简单分析
OKHTTP结合官网示例分析两种自定义拦截器的区别

先前分析了 OKHTTP拦截器ConnectInterceptor的简单分析

在 ConnectInterceptor 拦截器的功能就是负责与服务器建立 Socket 连接,并且创建了一个 HttpStream 它包括通向服务器的输入流和输出流。而接下来的 CallServerInterceptor 拦截器的功能使用 HttpStream 与服务器进行数据的读写操作的。

下面就来关注这个拦截器的具体实现。

CallServerInterceptor的功能.png

CallServerInterceptor

该拦截器的作用在上面已经说明了,跟 ConnectInterceptor 一样,我们只需要关注 intercept(Chain chain) 方法的具体实现即可,下面分点了解这个方法做了什么事。

@Override public Response intercept(Chain chain) throws IOException {
  //HttpStream 就是先前在 ConnectInterceptor 创建出来的
  HttpStream httpStream = ((RealInterceptorChain) chain).httpStream();
  StreamAllocation streamAllocation = ((RealInterceptorChain) chain).streamAllocation();
  Request request = chain.request();
  //发送请求的时间戳
  long sentRequestMillis = System.currentTimeMillis();
  //写入请求头信息
  httpStream.writeRequestHeaders(request);
  //写入请求体信息(有请求体的情况)
  if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
    Sink requestBodyOut = httpStream.createRequestBody(request, request.body().contentLength());
    BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
    request.body().writeTo(bufferedRequestBody);
    bufferedRequestBody.close();
  }
  //结束请求
  httpStream.finishRequest();
  //读取响应头信息
  Response response = httpStream.readResponseHeaders()
      .request(request)
      //这个我不知道是干嘛的?
      .handshake(streamAllocation.connection().handshake())
      //发送请求的时间
      .sentRequestAtMillis(sentRequestMillis)
      //接收到响应的时间
      .receivedResponseAtMillis(System.currentTimeMillis())
      .build();
   //openResponseBody 获取响应体信息
  if (!forWebSocket || response.code() != 101) {
    response = response.newBuilder()
        .body(httpStream.openResponseBody(response))
        .build();
  }
  if ("close".equalsIgnoreCase(response.request().header("Connection"))
      || "close".equalsIgnoreCase(response.header("Connection"))) {
    streamAllocation.noNewStreams();
  }
  int code = response.code();
  if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
    throw new ProtocolException(
        "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
  }
  //返回一个响应
  return response;
}

1.、分析 intercept 方法

根据上面的 intercept 方法大体上可以知道主要做了这样的几件事:

  • 写入请求头信息。

  • 有请求体的情况下,写入请求体信息。

  • 结束请求。

  • 读取响应头信息。

  • 往上一级 ConnectInterceptor 返回一个网络请求回来的 Response。

  • 获取响应体信息输入流

1.1、写入请求头信息

httpStream.writeRequestHeaders(request);

  • HttpStream 的实现者 Http1xStream 去做写入请求头信息的操作
@Override public void writeRequestHeaders(Request request) throws IOException {
  //获取一段字符串只要包括 请求方式(GET/POST...),http 版本号,请求路径等信息。
  String requestLine = RequestLine.get(
      request, streamAllocation.connection().route().proxy().type());
  //真正地将 headers 的头和 requestLine  写入到输出流中
  writeRequest(request.headers(), requestLine);
}
  • 下面是真正进行写入头信息的代码了,因为在 ConnectInterceptor 这个拦截器中通过 Socket 与服务器进行了连接并且也返回一个 HttpStream 对象,这个对象就封装了 Sink 输出流和 Source 输入流,它们专门负责与服务器进行读写操作的。
/** Returns bytes of a request header for sending on an HTTP transport. */
public void writeRequest(Headers headers, String requestLine) throws IOException {
  if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
  //第一行信息写的就是刚才拼接的那段字符串
  sink.writeUtf8(requestLine).writeUtf8("\r\n");
  //遍历 headers 以 key:value 的方式写入到 sink 输出流中
  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;
}

1.2、有请求体的情况下,写入请求体信息

我没有用过除了 POST 和 GET 之外的请求方式,这里我假定只有这两种方式吧。下面代码就是用于检测是否需要往服务器中写入请求体信息的代码。

if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
  Sink requestBodyOut = httpStream.createRequestBody(request, request.body().contentLength());
  BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
  request.body().writeTo(bufferedRequestBody);
  bufferedRequestBody.close();
}

1.2.1、检测该请求方式是否支持请求体

  • 检测该请求方式是否支持请求体。下面的判断方式都是 || 或的判断,只要请求方式是 POST 那么就可以支持写入请求体信息的功能。
    同时还要判断 request.body() != null 因为即使 POST 支持请求体的功能但是还有有 body() 内容才行啊。

    这个 body() 就是通过构建者模式构建 Request 请求时设置给 Request 的 ReqeustBody 对象。

FormBody.png
public static boolean permitsRequestBody(String method) {
  return requiresRequestBody(method)
      || method.equals("OPTIONS")
      || method.equals("DELETE")    // Permitted as spec is ambiguous.
      || method.equals("PROPFIND")  // (WebDAV) without body: request <allprop/>
      || method.equals("MKCOL")     // (WebDAV) may contain a body, but behaviour is unspecified
      || method.equals("LOCK");     // (WebDAV) body: create lock, without body: refresh lock
}

public static boolean requiresRequestBody(String method) {
  //在这里可以看出,只要是 POST 请求那么就表示它具备请求体。
  return method.equals("POST")
      || method.equals("PUT")
      || method.equals("PATCH")
      || method.equals("PROPPATCH") // WebDAV
      || method.equals("REPORT");   // CalDAV/CardDAV (defined in WebDAV Versioning)
}

1.2.2、创建一个可以写入请求体的 Sink 对象

  • 如果请求是 GET 请求,那么不需要考虑请求体问题了,因为 GET 请求的内容都在 URL 上。如果是 POST 请求就麻烦一些了,在这里需要考虑将请求体内容通过 Sink 输出流写入到 server 中。

    通过 createRequestBody 创建一个 Sink 对象,本质还是使用在 ConnectIntercept 创建的 HttpStream 内部封装 Sink 对象进行写操作的。

    根据请求头 Transfer-Encoding 是否为 chunked 的方式,来创建不同 Sink 实现类,如果是 chunked 方式那么就创建 newChunkedSink();如果不是 chunked 就表示内容的大小是固定的,那么就根据 content-length 创建指定大小的 newFixedLengthSink(contentLength) 对象。

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


将刚才创建的 Sink 对象,也就是 reqyestBodyOut 通过     Okio.buffer() 包装为 BufferedSink 对象,之后进行将 request.body()
的内容写入到该 BufferedSink 之中。

```java
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close(); 

1.2.3、具体的写入操作

具体是如何写入数据的,要根据传入到 Request 中的 RequestBody 的实现类来定,如果是表单类型的则是有 FormBody 负责具体的写操作,如果是文件类型的则是由 MutilPartBody 负责具体的写操作。下面是它们两者具体的实现源码,怎么写就不分析了。

  • FormBody 具体的写操作
private long writeOrCountBytes(BufferedSink sink, boolean countBytes) {
long byteCount = 0L;
Buffer buffer;
if (countBytes) {
  buffer = new Buffer();
} else {
  buffer = sink.buffer();
}
for (int i = 0, size = encodedNames.size(); i < size; i++) {
  if (i > 0) buffer.writeByte('&');
  buffer.writeUtf8(encodedNames.get(i));
  buffer.writeByte('=');
  buffer.writeUtf8(encodedValues.get(i));
}
if (countBytes) {
  byteCount = buffer.size();
  buffer.clear();
}
return byteCount;
}
  • MutilPartBody 具体的写操作
private long writeOrCountBytes(BufferedSink sink, boolean countBytes) throws IOException {
long byteCount = 0L;
Buffer byteCountBuffer = null;
if (countBytes) {
  sink = byteCountBuffer = new Buffer();
}
for (int p = 0, partCount = parts.size(); p < partCount; p++) {
  Part part = parts.get(p);
  Headers headers = part.headers;
  RequestBody body = part.body;
  sink.write(DASHDASH);
  sink.write(boundary);
  sink.write(CRLF);
  if (headers != null) {
    for (int h = 0, headerCount = headers.size(); h < headerCount; h++) {
      sink.writeUtf8(headers.name(h))
          .write(COLONSPACE)
          .writeUtf8(headers.value(h))
          .write(CRLF);
    }
  }
  MediaType contentType = body.contentType();
  if (contentType != null) {
    sink.writeUtf8("Content-Type: ")
        .writeUtf8(contentType.toString())
        .write(CRLF);
  }
  long contentLength = body.contentLength();
  if (contentLength != -1) {
    sink.writeUtf8("Content-Length: ")
        .writeDecimalLong(contentLength)
        .write(CRLF);
  } else if (countBytes) {
    // We can't measure the body's size without the sizes of its components.
    byteCountBuffer.clear();
    return -1L;
  }
  sink.write(CRLF);
  if (countBytes) {
    byteCount += contentLength;
  } else {
    body.writeTo(sink);
  }
  sink.write(CRLF);
}
sink.write(DASHDASH);
sink.write(boundary);
sink.write(DASHDASH);
sink.write(CRLF);
if (countBytes) {
  byteCount += byteCountBuffer.size();
  byteCountBuffer.clear();
}
return byteCount;

1.3、结束请求

对 sink.flush() 方法的注释可以看出它是将缓存区中数据写入到底层的 sink 中,其实就是写入到 server 中去了,相当于一个刷新缓冲区的功能。

  
 @Override public void finishRequest() throws IOException {
  /**
   * Writes all buffered data to the underlying sink, if one exists. Then that sink is recursively
   * flushed which pushes data as far as possible towards its ultimate destination. Typically that
   * destination is a network socket or file. <pre>{@code
  */
  sink.flush();
}

1.4、读取响应头信息

当客户端将请求数据发送给服务端之后,服务端做了处理之后会将结果返回给客户端,这是客户端需要根据这些返回的数据构造出一个 Response 对象出来然后返回给调用者。下面是就是构造响应头部的过程。

/** Parses bytes of a response header from an HTTP transport. */
public Response.Builder readResponse() throws IOException {
  if (state != STATE_OPEN_REQUEST_BODY && state != STATE_READ_RESPONSE_HEADERS) {
    throw new IllegalStateException("state: " + state);
  }
  try {
    while (true) {
      StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());
      Response.Builder responseBuilder = new Response.Builder()
          //协议,也就是 http 的版本例如 http1/2 /spdy
          .protocol(statusLine.protocol)
          //响应码
          .code(statusLine.code)
          //响应消息
          .message(statusLine.message)
          //响应头
          .headers(readHeaders());
      if (statusLine.code != HTTP_CONTINUE) {
        state = STATE_OPEN_RESPONSE_BODY;
        return responseBuilder;
      }
    }
  } catch (EOFException e) {
    // Provide more context if the server ends the stream before sending a response.
    IOException exception = new IOException("unexpected end of stream on " + streamAllocation);
    exception.initCause(e);
    throw exception;
  }
}

1.5、获取响应体信息输入流

1.5.1、得到一个 ResponseBody 对象

得到一个 ResponseBody 对象,该对象封装了连接服务端的输入流对 Source 对象(响应体 body)和 Headers 信息。

@Override public ResponseBody openResponseBody(Response response) throws IOException {
  Source source = getTransferStream(response);
  return new RealResponseBody(response.headers(), Okio.buffer(source));
}

1.5.2、响应的不同请求创建不同的 Source 对象

根据响应的不同请求创建不同的 Source 对象。

private Source getTransferStream(Response response) throws IOException {
  //没有响应内容,创建长度为 0 的 FixedLengthSource
  if (!HttpHeaders.hasBody(response)) {
    return newFixedLengthSource(0);
  }
  //Transfer-Encoding 是 chunked 的方式表示响应体的大小是无法知道的,创建一个 ChunkedSource 返回
  if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
    return newChunkedSource(response.request().url());
  }
  long contentLength = HttpHeaders.contentLength(response);
  if (contentLength != -1) {
    //contentLength 是已知的就创建一个 FixedLengthSource 返回。
    return newFixedLengthSource(contentLength);
  }
  // Wrap the input stream from the connection (rather than just returning
  // "socketIn" directly here), so that we can control its use after the
  // reference escapes.
  return newUnknownLengthSource();
}

1.6、往上一级 ConnectInterceptor 返回一个网络请求回来的 Response。

因为拦截器是一级级递归调用下来的,而 CallServerInterceptor 是整个网络请求中最后一个拦截器,它最终会根据服务器返回的数据通过构造者的模式创建一个 Response ,然后返回到上一级 Interceptor 对象。至于是如何处理就去看上一节吧 OKHTTP拦截器ConnectInterceptor的简单分析

2、总结

CallServerInterceptor 做的事情很多都是 ConnectInterceptor 都准备好了,例如 HttpStream 的创建等。它主要是利用 HttpStream 向服务器发送请求数据和接受服务器返回的数据,这里设计到 Okio 的知识点,可以参考这篇blog Android 善用Okio简化处理I/O操作

推荐阅读更多精彩内容