likes
comments
collection
share

OKHttp源码分析(七)网络请求 CallServerInterceptor 和 HttpCodec

作者站长头像
站长
· 阅读数 59

上一篇介绍了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的支持等。 后面的文章还会分析下这几个方面的源码处理。

转载自:https://juejin.cn/post/7140974233802244104
评论
请登录