okhttp之CallServerInterceptor 分析

CallServerInterceptor是okhttp中的最后一个拦截器,用来向服务器发送客户端的请求数据,并且封装服务器返回来的Response。开始分析代码:

@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
//获取到HttpCodec
HttpCodec httpCodec = realChain.httpStream();
StreamAllocation streamAllocation = realChain.streamAllocation();
RealConnection connection = (RealConnection) realChain.connection();
Request request = realChain.request();

long sentRequestMillis = System.currentTimeMillis();

realChain.eventListener().requestHeadersStart(realChain.call());
//向服务器中写入请求头
httpCodec.writeRequestHeaders(request);
realChain.eventListener().requestHeadersEnd(realChain.call(), request);

Response.Builder responseBuilder = null;
//如果请求中有请求体
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
  // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
  // Continue" response before transmitting the request body. If we don't get that, return
  // what we did get (such as a 4xx response) without ever transmitting the request body.
  //如果请求头中包含Expect:100-continue的请求头
  if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
    httpCodec.flushRequest();
    realChain.eventListener().responseHeadersStart(realChain.call());
    //读取服务端返回的响应
    //readResponseHeaders(true)这个方法里面做的操作如下:
    //1、读取服务器返回的响应头
    //2、根据响应码和传入的参数判断返回的respone.builer是否为空
    // (当传入的参数为true(这个可以理解为客户端希望继续发送请求体)并且
    // 服务端返回的状态码也是希望我们继续传入请求体的时候,就返回null)
    responseBuilder = httpCodec.readResponseHeaders(true);
  }
//如果responseBuilder为空,说明可以继续传入请求体
  if (responseBuilder == null) {
    // Write the request body if the "Expect: 100-continue" expectation was met.
    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表示不需要传入请求体,那么这次请求就算是结束了,
//    如果不是http2协议的话,这次连接就可以关闭掉了
  } else if (!connection.isMultiplexed()) {

    // If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
    // from being reused. Otherwise we're still obligated to transmit the request body to
    // leave the connection in a consistent state.
    streamAllocation.noNewStreams();
  }
}

//执行到这里,不管有没有发送请求体,本次请求就算是结束了
httpCodec.finishRequest();

由于在之前的拦截器中已经和服务器的连接打通了,在这个拦截器中只负责数据的传输。上面的代码就是向服务端发送请求的全部了,可以看到发送请求一共做了如下几步:

1、获取到httpCodec,这是一个接口,主要是封装了request的写入和response的读取,可以理解为通过这个接口向外发送请求和接收数据。这个实体类是在之前的拦截器中生成的,我们直接拿过来用便好。其实现类为Http1Codec和Http2Codec,分别对应了HTTP/1.1和HTTP/2

2、向服务端发送请求头

 //向服务器中写入请求头
httpCodec.writeRequestHeaders(request);

这里使用Http1Codec中的代码,其中用到的sinkOkIO的Sink对象(该对象可以看做Socket的OutputStream对象)

@Override public void writeRequestHeaders(Request request) throws IOException {
    String requestLine = RequestLine.get(
        request, streamAllocation.connection().route().proxy().type());
    writeRequest(request.headers(), requestLine);
}
 /** 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");
    //把头部的键值对写进输出流中
    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;
  }

3、判断是否需要向服务器发送请求体,主要通过以下几方面判断:

1)HttpMethod.permitsRequestBody(request.method()) && request.body() != null

通过判断请求方式(比如get请求是不需要请求体的,那么就不需要发送请求体)和请求体是否为空来判断。

2)在符合上面条件的情况下,再次判断请求头中是否有Expect:100-continue,如果有,那么就获取获取到请求头的响应状态码,判断服务端是否想要接收请求体。如果服务端返回的状态码是100,表示接收请求体,那么就通过readResponseHeaders()方法返回的响应体responseBuilder为空。为什么返回空呢?在后面的代码中是通过responseBuilder是否为空来判断是否需要发送请求体的。

    //1、读取服务器返回的响应头
    //2、根据响应码和传入的参数判断返回的respone.builer是否为空
    // (当传入的参数为true(这个可以理解为客户端希望继续发送请求体)并且
    // 服务端返回的状态码也是希望我们继续传入请求体的时候,就返回null)
    responseBuilder = httpCodec.readResponseHeaders(true);

Http1Codec中的代码

  @Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
  。。。。。
    try {
      StatusLine statusLine = StatusLine.parse(readHeaderLine());
        //这里生成了responseBuilder
      Response.Builder responseBuilder = new Response.Builder()
          .protocol(statusLine.protocol)
          .code(statusLine.code)
          .message(statusLine.message)
          .headers(readHeaders());
    //但是如果code为100并且expectContinue为true(从上面传进来的true)时,返回null
    //需要请求体,暂时不返回响应体
      if (expectContinue && statusLine.code == HTTP_CONTINUE) {
        return null;
      } else if (statusLine.code == HTTP_CONTINUE) {
        state = STATE_READ_RESPONSE_HEADERS;
        return responseBuilder;
      }

      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;
    }
}

4、发送请求体

从3中判断如果返回的responseBuilder为空,那么就发送请求体

//如果responseBuilder为空,说明可以继续传入请求体
  if (responseBuilder == null) {
    // Write the request body if the "Expect: 100-continue" expectation was met.
    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表示不需要传入请求体,那么这次请求就算是结束了,
    //  如果不是http2协议的话,这次连接就可以关闭掉了
  } else if (!connection.isMultiplexed()) {

    // If the "Expect: 100-continue" expectation wasn't met, prevent the    HTTP/1 connection
    // from being reused. Otherwise we're still obligated to transmit the request body to
    // leave the connection in a consistent state.
    streamAllocation.noNewStreams();
  }
}

//    执行到这里,不管有没有发送请求体,本次请求就算是结束了
httpCodec.finishRequest();

代码中的注释应该已经比较清楚了,主要是通过BufferedSink把body写入输出流传送给服务器。

写到这里,发送请求就算完成了,下面来看获取响应体。

if (responseBuilder == null) {
  realChain.eventListener().responseHeadersStart(realChain.call());
  //有请求体的情况下获取responseBuilder
  responseBuilder = httpCodec.readResponseHeaders(false);
}
//封装respone响应 写入原请求,握手情况,请求时间,得到的结果时间
Response response = responseBuilder
    .request(request)
    .handshake(streamAllocation.connection().handshake())
    .sentRequestAtMillis(sentRequestMillis)
    .receivedResponseAtMillis(System.currentTimeMillis())
    .build();

上面的代码主要是通过httpCodec.readResponseHeaders(false)获取到response,这时的response并不包含请求体。然后再根据响应码来做不同的处理

int code = response.code();
if (code == 100) {
  //如果响应码是100,重新读取一遍响应头
  // server sent a 100-continue even though we did not request one.
  // try again to read the actual response
  responseBuilder = httpCodec.readResponseHeaders(false);

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

  code = response.code();
}
    //code=101  Switching Protocols 服务器将遵从客户的请求转换到另外一种协议
 if (forWebSocket && code == 101) {
  // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
  response = response.newBuilder()
      .body(Util.EMPTY_RESPONSE)
      .build();
} else {
    //这里才为response附上了服务器返回的body
  response = response.newBuilder()
      .body(httpCodec.openResponseBody(response))
      .build();
}

//根据connection值关闭连接
if ("close".equalsIgnoreCase(response.request().header("Connection"))
    || "close".equalsIgnoreCase(response.header("Connection"))) {
  streamAllocation.noNewStreams();
}
    
//• 204 - No Content 没有新文档,浏览器应该继续显示原来的文档。如果用户定期地刷新页/面,而Servlet可以确定用户文档足够新,这个状态代码是很有用的。 
//• 205 - Reset Content 没有新的内容,但浏览器应该重置它所显示的内容。用来强制浏览器清除表单输入内容(HTTP 1.1新)。
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
  throw new ProtocolException(
      "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}

关于获取响应体到这里就结束了,一共分两步:

1)组装到响应体的头部

2)根据头部的响应码,来判断有没有响应体,并组装响应体

到这里整个CallServerInterceptor拦截器就介绍完了,主要做了这么几件事:

1、发送请求头

2、根据请求方式和响应头来判断是否发送请求体

3、如果需要发送请求体,通过BufferedSink写入请求体

4、封装响应头

5、根据响应码组装响应体

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

推荐阅读更多精彩内容