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操作

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容