Android OKHttp系列6-CallServerInterceptor

文章将会被同步至微信公众号:Android部落格

1、概述

开始写入request body数据,并读取服务端返回的数据

上一篇文章说到有两个Http协议兼容处理请求,因此有两个流程图,基本是相似的处理流程:

  • Http1Codec

在这里插入图片描述

  • Http2Codec

在这里插入图片描述

1.1 Http1Codec

1.1.1 写请求头
@Override public void writeRequestHeaders(Request request) throws IOException {
    String requestLine = RequestLine.get(
        request, streamAllocation.connection().route().proxy().type());
    writeRequest(request.headers(), requestLine);
}
  • 请求行(Request Line)

分为三个部分:请求方法、请求地址和协议及版本,以CRLF(rn)结束。

HTTP/1.1 定义的请求方法有8种:GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、TRACE,最常的两种GET和POST。

看看这个get方法就知道了:

public static String get(Request request, Proxy.Type proxyType) {
    StringBuilder result = new StringBuilder();
    result.append(request.method());
    result.append(' ');
    
    if (includeAuthorityInRequestLine(request, proxyType)) {
      result.append(request.url());
    } else {
      result.append(requestPath(request.url()));
    }
    
    result.append(" HTTP/1.1");
    return result.toString();
}

先写请求方法,在写请求地址信息,对于http请求,直接写地址,对于https请求要写入地址相关参数,然后写入协议版本。

例如:https://www.idataapi.cn/?rec=baidu_2&renqun_youhua=752394

写入的就是:Post www.idataapi.cn/?rec=baidu_2&renqun_youhua=752394 HTTP/1.1

接下来开始写头数据:

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

看图说话:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zzATGtOV-1589365608551)(https://ftp.bmp.ovh/imgs/2020/05/b814c57fb5af3ace.png)]

1.1.2 写请求体

如果是非GET和HEAD请求,并且body数据不为空的情况下,就可以写request body数据了:

  • 100-continue

通知接收方客户端要发送一个体积可能很大的消息体,期望收到状态码为100 (Continue) 的临时回复。

服务器开始检查请求消息头,可能会返回一个状态码为 100 (Continue) 的回复来告知客户端继续发送消息体,也可能会返回一个状态码为417 (Expectation Failed) 的回复来告知对方要求不能得到满足。

if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
    httpCodec.flushRequest();
    realChain.eventListener().responseHeadersStart(realChain.call());
    responseBuilder = httpCodec.readResponseHeaders(true);
}
  • 第一步就是刷新请求,将数据写入并发送到服务端;

  • 第二步开始读取返回的头数据,看看服务端是否允许继续发送数据。

if (expectContinue && statusLine.code == HTTP_CONTINUE) {
        return null;
} 

如果服务端返回100,意味着客户端可以继续发送body数据了:

long contentLength = request.body().contentLength();
CountingSink requestBodyOut =
    new CountingSink(httpCodec.createRequestBody(request, contentLength));
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);

request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
@Override public void finishRequest() throws IOException {
    sink.flush();//数据写入
}
1.1.3 读返回数据
  • 读返回Headers
    正常逻辑下,如果客户端发送100-continue,服务端返回100,此时返回构造Buidler为空,此时就是公共流程(意思是不管有没有请求body):
if (responseBuilder == null) {
  responseBuilder = httpCodec.readResponseHeaders(false);
}
Response.Builder responseBuilder = new Response.Builder()
  .protocol(statusLine.protocol)
  .code(statusLine.code)
  .message(statusLine.message)
  .headers(readHeaders());

数据从上一个拦截器(ConnectInterceptor)初始化的source里面读取。

source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
  • 封装Response

读取Header返回之后封装一个Response对象出来:

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

这里处理了一个例外的情况,就是客户端Header没有100-continue字段,但是服务端返回100,此时sdk仍然把字段里面的值读取出来。

int code = response.code();
if (code == 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();
}

接下来处理Response body:

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 = response.newBuilder()
      .body(httpCodec.openResponseBody(response))
      .build();
}

第一种情况是websocket协议请求并且表示服务器应客户端升级协议的请求(Upgrade请求头)正在进行协议切换。
服务器会发送一个Upgrade响应头来表示其正在切换过去的协议。
此时直接封装一个空的body并构建。

WebSockets 是一种先进的技术。它可以在用户的浏览器和服务器之间打开交互式通信会话。使用此API,您可以向服务器发送消息并接收事件驱动的响应,而无需通过轮询服务器的方式以获得响应。

第二种就是正常情况了,读取body数据:

  • 没有body数据

创建一个newFixedLengthSource对象,长度为0,此时释放Http1CodecRealConnection对象。

将source对象封装到RealResponseBody

  • body数据被分块传输
if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding")))

将请求url封装到newChunkedSource对象;
将source对象封装到RealResponseBody

  • body数据以固定长度传输

将数据长度封装到newFixedLengthSource对象;
将source对象封装到RealResponseBody

  • 返回的body数据长度未知

创建UnknownLengthSource对象;
将source对象封装到RealResponseBody

body数据在哪里读取呢?在BridgeInterceptor中:

if (transparentGzip
    && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
    && HttpHeaders.hasBody(networkResponse)) {
  GzipSource responseBody = new GzipSource(networkResponse.body().source());
  Headers strippedHeaders = networkResponse.headers().newBuilder()
      .removeAll("Content-Encoding")
      .removeAll("Content-Length")
      .build();
  responseBuilder.headers(strippedHeaders);
  String contentType = networkResponse.header("Content-Type");
  responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
}

1.2 处理返回异常

  • 服务端连接关闭
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());
}

HTTP协议中 204 No Content 成功状态响应码表示目前请求成功,但客户端不需要更新其现有页面。204 响应默认是可以被缓存的。在响应中需要包含头信息 ETag。

在 HTTP 协议中,响应状态码 205 Reset Content 用来通知客户端重置文档视图,比如清空表单内容、重置 canvas 状态或者刷新用户界面。

这两种情况下就抛异常返回了。

1.3 Http2Codec

流程上基本与Http1Codec一致,只是部分方法实现不一样。

1.3.1 写请求头

将key,value封装到Header列表中,然后将header数据封装到Http2Stream并写入:

List<Header> requestHeaders = http2HeadersList(request);
stream = connection.newStream(requestHeaders, hasRequestBody);

newStream方法中比较关键的两步是:

stream = new Http2Stream(streamId, this, outFinished, inFinished, requestHeaders);
writer.synStream(outFinished, streamId, associatedStreamId, requestHeaders);

创建数据流完毕之后开始写入数据并等待服务端返回了。

1.3.2 读取返回数据
  • 读取返回Header数据
@Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
    List<Header> headers = stream.takeResponseHeaders();
    Response.Builder responseBuilder = readHttp2HeadersList(headers, protocol);
    if (expectContinue && Internal.instance.code(responseBuilder) == HTTP_CONTINUE) {
      return null;
    }
    return responseBuilder;
}

takeResponseHeaders()方法中,有个无限循环等着Header数据被返回:

readTimeout.enter();
try {
  while (responseHeaders == null && errorCode == null) {
    waitForIo();
  }
} finally {
  readTimeout.exitAndThrowIfTimedOut();
}

那么这个responseHeaders在哪里初始化呢?
记得在ConnectInterceptor中说过,startHttp2方法中,连接开始之后就有一个While循环等着读取数据,数据返回之后将数据回调到Http2Connection的内部类ReaderRunnable中处理,ReaderRunnable封装了一个读取类Http2Reader,在这个类中分别复写了data,headers等方法。

@Override public void headers(boolean inFinished, int streamId, int associatedStreamId,
    List<Header> headerBlock) {
      Http2Stream stream;
      synchronized (Http2Connection.this) {
        stream = getStream(streamId);
        if (stream == null) {
            final Http2Stream newStream = new Http2Stream(streamId, Http2Connection.this,
                  false, inFinished, headerBlock);
        }
    }
    stream.receiveHeaders(headerBlock);
}

这个stream对象就是上一步中初始化的。

到这里就可以读取到Headers数据了。

接下来把Header数据封装到Response.Builder对象,如果是100-continue返回就直接返回null。

对于客户端请求头字段包含100-continue,而服务端恰好返回100,此时要填充请求Header body数据再次发送请求。

接着再次读取服务端返回的Header的数据。

responseBuilder = httpCodec.readResponseHeaders(false);
1.3.3 读取返回数据

封装返回的数据比较简单:

@Override public ResponseBody openResponseBody(Response response) throws IOException {
    streamAllocation.eventListener.responseBodyStart(streamAllocation.call);
    String contentType = response.header("Content-Type");
    long contentLength = HttpHeaders.contentLength(response);
    Source source = new StreamFinishingSource(stream.getSource());
    return new RealResponseBody(contentType, contentLength, Okio.buffer(source));
}

stream.getSource()数据来源是:

Http2ReadernextFrame方法中,会有数据返回

readData(handler, length, flags, streamId);
handler.data(inFinished, streamId, source, length);

回调到Http2Connection

dataStream.receiveData(source, length);

Http2Stream

void receiveData(BufferedSource in, int length) throws IOException {
    assert (!Thread.holdsLock(Http2Stream.this));
    this.source.receive(in, length);
}

上面的整个数据回调流程在ConnectInterceptor中有详细说明。

异常处理与Http2Codec一致。数据在BridgeInterceptor中处理。

微信公众号二维码:
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值