zl程序教程

您现在的位置是:首页 >  其它

当前栏目

Okhttp之CallServerInterceptor简单分析

分析 简单 okHttp
2023-09-14 09:12:23 时间

Okhttp源码分析专栏的几篇博客分析了Okhttp几个拦截器的主要功能,还剩下最后一个拦截器CallServerInterceptor没有分析,本篇博客就简单分析下该拦截器的功能。
在Okhttp拦截器链上CallServerInterceptor拦截器是最后一个拦截器,该拦截器前面的拦截器ConnectInterceptor主要负责打开TCP链接(详见《 OkHttp之ConnectInterceptor简单分析 》)。而CallServerInterceptor的主要功能就是—向服务器发送请求,并最终返回Response对象供客户端使用。

下面就从源码的角度来简单分析CallServerInterceptor的是怎么像服务器发送Request,以及生成Response对象的。

 public Response intercept(Chain chain) throws IOException {
    // 省略部分代码
    // 获取HttpCodec 
    HttpCodec httpCodec = realChain.httpStream();
    // 省略部分代码
    Request request = realChain.request();
    //向服务器发送请求
    httpCodec.writeRequestHeaders(request);

    Response.Builder responseBuilder = null;
    // 检测是否有请求body
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {

      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        httpCodec.flushRequest();
        //构建responseBuilder对象
        responseBuilder = httpCodec.readResponseHeaders(true);
      }

       //如果服务器允许发送请求body发送
      if (responseBuilder == null) {
        Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();
      } else if (!connection.isMultiplexed()) {
         //省略部分代码
      }
    }

    //结束请求
    httpCodec.finishRequest();

    //构建请求buidder对象
    if (responseBuilder == null) {
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

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

    int code = response.code();
    if (forWebSocket && code == 101) {
      //省略部分代码
    } else {
      response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
    }

    //省略部分代码

    return response;
  }

该方法首先是获取了httpCodec对象,该对象的主要功能就是对不同http协议(http1.1和http/2)的请求和响应做处理,该对象的初始化是在ConnectIntercepor的intercept里面:

 HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);

最终httpCodec的初始化又是在StreamAllocation的newStream方法(详见《 OkHttp之ConnectInterceptor简单分析 》):

  public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
     //:省略部分代码;

      HttpCodec resultCodec = resultConnection.newCodec(client, this);

  }
    public HttpCodec newCodec(
      OkHttpClient client, StreamAllocation streamAllocation) throws SocketException {
    if (http2Connection != null) {
      return new Http2Codec(client, streamAllocation, http2Connection);
    } else {
      //设置socket的读超时时间
      socket.setSoTimeout(client.readTimeoutMillis());
      //InputStream的超时时间
      source.timeout().timeout(client.readTimeoutMillis(), MILLISECONDS);
      //OutputStream的超时时间
      sink.timeout().timeout(client.writeTimeoutMillis(), MILLISECONDS);
      return new Http1Codec(client, streamAllocation, source, sink);
    }
  }

可以发现Okhttp的提供了两种HttpCodec的实现类,如果使用了http2协议则返回Http2Codec,否则返回Http1Codec!并且设置了超时时间,本篇就以Http1Codec对象来进行分析。

我们知道Http发送网络请求前两个步骤是:
1、建立TCP链接
2、客户端向web服务器发送请求命令:形如GET /login/login.jsp?username=android&password=123 HTTP/1.1的信息

在Okhttp中ConnectInterceptor负责第一个步骤,那么第二个步骤是如何实现的呢?答案就是httpCodec对象的writeRequestHeaders方法。(该方法在CallserverInterceptor的intercept里面调用,见上面代码)

 public void writeRequestHeaders(Request request) throws IOException {
  //RequestLine.get用来构建形如GET xx HTTP/1.1的字符串
    String requestLine = RequestLine.get(
        request, streamAllocation.connection().route().proxy().type());
  //像服务器发送请求,形如GET xxx HTTP/1.1
    writeRequest(request.headers(), requestLine);
  }  

可以发现Okhttp通过OkIO的Sink对象(该对象可以看做Socket的OutputStream对象)的writeRequest来向服务器发送请求的。

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

我们知道HTTP支持post,delete,get,put等方法,而post,put等方法是需要请求体的(在Okhttp中用RequestBody来表示)。所以接着writeRequestHeaders之后Okhttp对请求体也做了响应的处理:

    //如果当前request请求需要请求体
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {

      //询问Server使用愿意接受数据 
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        httpCodec.flushRequest();
        //构建responseBuilder对象
        responseBuilder = httpCodec.readResponseHeaders(true);
      }

       //向服务器发送请求体
      if (responseBuilder == null) {

         //发送请求体,详见下文描述
      } else if (!connection.isMultiplexed()) {
        //省略部分代码
      }
    }

通过上面的代码可以发现Okhttp对Expect头部也做了支持,上面代码对客户端是否使用该头部做了判断,“100 continue”的作用就是:客户端有一个RequestBody(比如post或者PUT方法)要发给服务器,但是客户端希望在发送RequestBody之前查看服务器是否接受这个body,服务端在接受到这个请求后必须进行响应。客户端通过Expect首部来发送这个消息,当然如果客户端没有实体发送,就不应该发送100 continue 首部,因为这样会使服务器误以为客户端有body要发送。所以okhttp在发送这个之前要permitsRequestBody来判断。当然常规的get请求是不会走这个方法的。

如果服务器允许发送ReqeustBody,那么就通过下面这三行代码来发送请求体:

  //构建请求体对象组成的输入流
  Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
        //发送请求体
        request.body().writeTo(bufferedRequestBody);

最终调用ReqeustBody的writeTo方法来发送请求体,实际上是调用bufferedRequestBody对象的write方法,简单实例如下(当然实际可能是FormBody或者是自定义的ReqeustBody):

ublic static RequestBody create(
      final @Nullable MediaType contentType, final ByteString content) {
    return new RequestBody() {
      //省略部分代码
      @Override public void writeTo(BufferedSink sink) throws IOException {
        sink.write(content);
      }
    };
  }

到现在为止,客户端向服务端发送请求的部分已经讲解完毕,下面就剩下读取服务器响应然后构建Response对象了:

//构建请求buider对象
    if (responseBuilder == null) {
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

    //构建response对象
    Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    int code = response.code();
    if (forWebSocket && code == 101) {
      //返回空的即无效的响应
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
    }

上面的代码做了三个工作:
1、调用HttpCodec的readResponseHeaders方法读取服务器响应的数据,构建Response.Builder对象(以Hppt1Codec分析):

 public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
      //省略部分代码
      //读取服务器
      StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());

      Response.Builder responseBuilder = new Response.Builder()
          .protocol(statusLine.protocol)//http协议版本
          .code(statusLine.code)//http响应状态码
          //http的message :like "OK" or "Not Modified"
          .message(statusLine.message)
          .headers(readHeaders());//http响应header
      //省略部分代码

      return responseBuilder;

  }

2、通过ResopnseBuilder对象来最终创建Response对象,并返回。
最关键的是服务器的响应体或者响应内容是如果传给Response的,代码如下:

  response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();

Response的body通过httpCodec对象的openResponseBody传进来,进入Http1Codec对象的openResponseBody方法看看都做了些神马:

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

很简单,openResponseBody将Socket的输入流InputStream对象交给OkIo的Source对象(在本篇博文中只需简单的将Sink作为Socket的输入流,Source作为Socket的输入流看待即可,详细的分析可参考OKIO),然后封装成RealResponseBody(该类是ResponseBody的子类)作为Response的body.

那么我们怎么通过这个body来获取服务器发送过来的字符串呢?ResponseBody提供了string()方法:

  public final String string() throws IOException {
    BufferedSource source = source();
    try {
      Charset charset = Util.bomAwareCharset(source, charset());
      //InputStream 读取数据
      return source.readString(charset);
    } finally {
      Util.closeQuietly(source);
    }
  }

string()方法也很简单,就是通过一些处理然后让调用source.readString来读取服务器的数据。需要注意的是该方法最后调用closeQuietly来关闭了当前请求的InputStream输入流,所以string()方法只能调用一次,再次调用的话会报错,毕竟输入流已经关闭了,你还怎么读取数据呢?

到此为止CallServerInterceptor简单分析完毕,总结下主要做了如下工作:
1、获取HttpCodec对象,对<=Http1.1之前的或者http/2不同协议的http请求处理。
2、发送http请求数据,构建Resposne.Builder对象,然后构建Response并返回。

到此为止Okhttp从发起请求到响应请求生成Response对象的流程已经分析完毕。如果想详细了解Okhttp工作原理的话,可参考博主的Okhttp源码分析专栏,如有不当之处,欢迎批评指正。