在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源码分析专栏,如有不当之处,欢迎批评指正。

Okhttp之CallServerInterceptor简单分析的更多相关文章

  1. OkHttp之ConnectInterceptor简单分析

    在< Okhttp之CacheInterceptor简单分析 >这篇博客中简单的分析了下缓存拦截器的工作原理,通过此博客我们知道在执行完CacheInterceptor之后会执行下一个浏览 ...

  2. Okhttp之CacheInterceptor简单分析

    <OkHttp之BridgeInterceptor简单分析 >简单分析了BridgeInterceptor的工作原理,在Okhttp的拦截器链上BridgeInterceptor的下一个拦 ...

  3. OkHttp之BridgeInterceptor简单分析

    在< Okhttp源码简单解析(一) >这篇博客简单分析了Okhttp请求的执行流程,通过该篇博客我们知道OkHttp的核心网络请求中内置"拦截器"发挥了重大作用:本篇 ...

  4. Okhttp之连接池ConnectionPool简单分析(一)

    开篇声明:由于本篇博文用到的一些观点或者结论在之前的博文中都已经分析过,所以本篇博文直接拿来用,建议读此博文的Monkey们按照下面的顺序读一下博主以下博文,以便于对此篇博文的理解: <Okht ...

  5. Okhttp对http2的支持简单分析

    在< Okhttp之RealConnection建立链接简单分析>一文中简单的分析了RealConnection的connect方法的作用:打开一个TCP链接或者打开一个隧道链接,在打开t ...

  6. Okhttp之RealConnection建立链接简单分析

    在之前的博客中我们知道Okhttp在发起链接请求先从链接池中获取连接,如果链接池中没有链接则创建新的链接RealConnection对象,然后执行其connet方法打开SOCKET链接(详见< ...

  7. Okhttp之RouteSelector简单解析

    继前面的几篇OKhttp的拦截器简单分析之后,对于后续Okhttp之间的分析自己也着实琢磨了一段时间,是分析RealConnection?还是ConnectionPool,随着对Okhttp源码的深入 ...

  8. 简单分析JavaScript中的面向对象

    初学JavaScript的时候有人会认为JavaScript不是一门面向对象的语言,因为JS是没有类的概念的,但是这并不代表JavaScript没有对象的存在,而且JavaScript也提供了其它的方 ...

  9. CSipSimple 简单分析

    简介 CSipSimple是一款可以在android手机上使用的支持sip的网络电话软件,可以在上面设置使用callda网络电话.连接使用方式最好是使用wifi,或者3g这样上网速度快,打起电话来效果 ...

随机推荐

  1. 环境变量.JAVA_HOME

    1.资料:(我的文章) http://www.cnblogs.com/cppskill/p/8341263.html 2. 2.1.界面 2.2.代码 unit formMain; interface ...

  2. [原]设置vs的release版本调试

    通过三步设置就可以实现realse下的调试啦: 1.设置“C/C++”------>“常规”------->“调试信息格式”设置成:“程序数据库(/Zi)” 2.设置“链接器”------ ...

  3. 一个纯净的webpack4+angular5脚手架

    该篇主要是结合刚发布不久的webpack4,搭建一个非cli的angular5的脚手架demo,主要分为以下几个方面阐述下脚手架结构: # 脚手架基础架构(根据angular5的新规范) /** * ...

  4. CSS sprites(css 精灵):将小图标整合到一张图片上

    一.什么是css sprites css sprites直译过来就是CSS精灵.通常被解释为“CSS图像拼合”或“CSS贴图定位”.其实就是通过将多个图片融合到一张图里面,然后通过CSS backgr ...

  5. Web端主流框架,jquery、angular、react、vue

    不得不说,前端技术发展非常迅速,时不多久就有一个新的东西冒出来,并且迅速膨胀发展,让旁观者眼花缭乱,让开发者目眩神迷,但总体上来说,这波互联网大浪潮带动了前端技术的大发展,给曾经那些苦苦挣扎于DOM操 ...

  6. LeetCode--225--用队列实现栈

    问题描述: 使用队列实现栈的下列操作: push(x) -- 元素 x 入栈 pop() -- 移除栈顶元素 top() -- 获取栈顶元素 empty() -- 返回栈是否为空 注意: 你只能使用队 ...

  7. android--------自定义弹幕控件(视频弹幕操作)

    弹幕功能在很多平台上都有,直播平台,视频播放器,音乐等上面都有, 直播平台发送弹幕和主播互动,那么弹幕效果是怎么实现的呢. 案例主要是实现视频出现弹幕,可以文字和表情弹幕,启动和关闭弹幕,发布弹幕. ...

  8. Confluence 6 使用 LDAP 授权连接一个内部目录 - 用户组 Schema 设置

    请注意:这部分仅在拷贝用户登录(Copy User on Login)和 同步组成员(Synchronize Group Memberships)被启用后可见. 其他用户组 DN(Additional ...

  9. php安装redis扩展全

    一.安装redis mac 下安装也可以使用 homebrew,homebrew 是 mac 的包管理器. 1.执行 brew install redis 2.启动 redis,可以使用后台服务启动  ...

  10. bzoj4025: 二分图 lct

    题意:带增删边的查询二分图 题解:因为二分图肯定带奇环,lct维护,每次要加入一条边之前判断会不会构成环,如果会就把最先会删除的边删掉,然后如果是奇环就打个标记,然后把奇环数++,删除的时候,把标记删 ...