浅析okHttp3的网络请求流程

写在开头

okHttp目前可以称的上是Android主流网络框架,甚至连谷歌官方也将网络请求的实现替换成okHttp.

网上也有很多人对okHttp的源码进行了分析,不过基于每个人的分析思路都不尽相同,读者看起来的收获也各不相同,所以我还是整理了下思路,写了点自己的分析感悟。

本文基于okhttp3.11.0版本分析

基本用法

String url = "http://www.baidu.com";
//'1. 生成OkHttpClient实例对象'
OkHttpClient okHttpClient = new OkHttpClient();
//'2. 生成Request对象'
Request request = new Request.Builder()
    .url(url)
    .post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"),"test content"))
    .build();
Call call = okHttpClient.newCall(request);

call.enqueue(new Callback() {
    @Override
    public void onFailure(@NonNull Call call, @NonNull IOException e) {
    }

    @Override
    public void onResponse(@NonNull Call call, @NonNull Response response)  {

    }
});

整体流程

借用别人的一张流程图来概括一下okHttp的请求走向 原图出处

image
okHttp的整体流程大致分为以下几个阶段
  1. 创建请求对象 (url, method,body)-->request-->Call

  2. 请求事件队列,线程池分发 enqueue-->Runnable-->ThreadPoolExecutor

  3. 递归Interceptor拦截器,发送请求。 InterceptorChain

  4. 请求回调,数据解析。 Respose-->(code,message,requestBody)

创建请求对象

其中 Request维护请求对象的属性

public final class Request {
    final HttpUrl url;  
    final String method;
    final Headers headers;
    final @Nullable RequestBody body;
    //请求的标记,在okHttp2.x的时候,okHttpClint提供Cancel(tag)的方法来批量取消请求
    //不过在3.x上批量请求的api被删除了,要取消请求只能在Callback中调用 call.cancel()
    //因此这个tags参数只能由开发者自己编写函数来实现批量取消请求的操作
    final Map<Class<?>, Object> tags;  
}

请求响应的包装接口Call

public interface Call extends Cloneable {
    Request request();
    Response execute() throws IOException;
    void enqueue(Callback responseCallback);
    void cancel();
}

请求事件队列,线程池分发

Call的实现类RealCallAsyncCall

  @Override 
  public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }

//其中AsyncCall是RealCall的一个内部类,继承自Runnable,这样就能通过线程池来回调AsyncCall的execute函数

final class AsyncCall extends NamedRunnable {
    @Override 
    protected void execute() {
        boolean signalledCallback = false;
        try {
            //getResponseWithInterceptorChain 拦截链的逻辑,也是发起请求的真正入口
            Response response = getResponseWithInterceptorChain();
            if (retryAndFollowUpInterceptor.isCanceled()) {
                signalledCallback = true;
                responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
            } else {
                signalledCallback = true;
                responseCallback.onResponse(RealCall.this, response);
            }
        } catch (IOException e) {
            ...
        } 
        ...
    }
}

递归Interceptor拦截器,发送请求

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    //用户自定义的拦截器(注意addAll 所以可以添加多个自定义的拦截器)
    interceptors.addAll(client.interceptors());
    //重试与重定向拦截器
    interceptors.add(retryAndFollowUpInterceptor);
    //内容拦截器
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    //缓存拦截器
    interceptors.add(new CacheInterceptor(client.internalCache()));
    //网络连接拦截器
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      //用户自定义的网络拦截器
      interceptors.addAll(client.networkInterceptors());
    }
    //服务请求的拦截器
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }

okHttp的核心部分就是这个Interceptor拦截链,每个Interceptor各自负责一部分功能,内部通过递归的方式遍历每一个Interceptor拦截器。递归逻辑在RealInterceptorChain类下

public final class RealInterceptorChain implements Interceptor.Chain {
    
    //拦截器递归的入口
    public Response proceed(Request request, StreamAllocation streamAllocation,
               HttpCodec httpCodec, RealConnection connection) throws IOException {
     ...
    //拦截器递归的核心代码,根据interceptors列表执行每一个拦截器的intercept函数
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
        connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
        writeTimeout);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);
    
    ....
    return response;
  }
}

递归结束后会获得请求响应,那么说明我们的request行为就在这个拦截链中,接下来我们先看看负责网络请求的那部分拦截器,从类名上就能比较容易的看出 ConnectInterceptorCallServerInterceptor这两个拦截器的主要工作。

网络连接拦截器ConnectInterceptor
public final class ConnectInterceptor implements Interceptor {
  
    @Override 
    public Response intercept(Chain chain) throws IOException {
        RealInterceptorChain realChain = (RealInterceptorChain) chain;
        Request request = realChain.request();
        StreamAllocation streamAllocation = realChain.streamAllocation();

        // We need the network to satisfy this request. Possibly for validating a conditional GET.
        boolean doExtensiveHealthChecks = !request.method().equals("GET");
        HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
        RealConnection connection = streamAllocation.connection();

        return realChain.proceed(request, streamAllocation, httpCodec, connection);
    }
}

其中有几个对象说明一下

  • StreamAllocation:内存流的存储空间,这个对象可以直接从realChain中直接获取,说明在之前的拦截链中就已经赋值过

  • HttpCodec(Encodes HTTP requests and decodes HTTP responses): 对请求的编码以及对响应数据的解码

  • realChain.proceed():通知下一个拦截器执行

接下来看创建HttpCodec对象的newStream函数中做了些什么

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

public HttpCodec newStream(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    
    ...
    try {
      //findHealthyConnection内部通过一个死循环查找一个可用的连接,优先使用存在的可用连接,否则就通过      //线程池来生成,其中多处使用 synchronized关键字,防止因为多并发导致问题
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }

沿着代码往下走,你会发现实际上负责网络连接功能的类是一个叫RealConnection的类,该类中有一个connect的函数

RealConnection#connect

 public void connect(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
      EventListener eventListener) {
    ...
        
    while (true) {
      try {
        if (route.requiresTunnel()) {
          //这个函数最终还是会走到connectSocket()函数中
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
          if (rawSocket == null) {
            // We were unable to connect the tunnel but properly closed down our resources.
            break;
          }
        } else {
          connectSocket(connectTimeout, readTimeout, call, eventListener);
        }
      }
      ...
  }
  
  //最终调用的还是Socket对象来创建网络连接,包括connectTimeout,readTimeout等参数也是这个时候真正设置的。
网络请求拦截器 CallServerInterceptor

This is the last interceptor in the chain. It makes a network call to the server.

直接看CallServerInterceptor的intercept函数

@Override
public Response intercept(Chain chain) throws IOException{
    //下面的各参数都是之前几个拦截器所生成的
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    HttpCodec httpCodec = realChain.httpStream();
    StreamAllocation streamAllocation = realChain.streamAllocation();
    RealConnection connection = (RealConnection) realChain.connection();
    Request request = realChain.request();
    
    //发送请求头,也是网络请求的开始
    httpCodec.writeRequestHeaders(request);
    
    Response.Builder responseBuilder = null;
    //请求不是get,并且有添加了请求体,写入请求体信息
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      //如果请求头中有Expect:100-continue这么一个属性
      //会先发送一个header部分给服务器,并询问服务器是否支持Expect:100-continue 这么一个扩展域
      //okhttp3提供这么个判断是为了兼容http2的连接复用行为的
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        //刷新缓存区,可以理解为向服务端写入数据
        httpCodec.flushRequest();
        realChain.eventListener().responseHeadersStart(realChain.call());
        responseBuilder = httpCodec.readResponseHeaders(true);
      }
        
      //写入请求body
      if (responseBuilder == null) {
        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);
      } 
      ...
    httpCodec.finishRequest();
    
    //响应相关的代码
    ...
}

写入请求body的核心代码

//将请求体写入到BufferedSink中,而BufferedSink是另外一个类库Okio中的类
CountingSink requestBodyOut =
            new CountingSink(httpCodec.createRequestBody(request, contentLength));
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);


//httpCodec.finishRequest 最终会调用 sink.flush(),sink是BufferedSink的对象,BufferedSink在底层
//会将其内的数据推给服务端,相当于是一个刷新缓冲区的功能
httpCodec.finishRequest();

响应相关的代码

if (responseBuilder == null) {
    realChain.eventListener().responseHeadersStart(realChain.call());
    //读取响应头,实际的返回流存放位置在okio库下的buffer对象中,读取过程中做了判断,只有当code==100时才会    //有返回,不然抛出异常并拦截,所以下面这段代码肯定有响应头返回,不然直到超时也不会回调
    responseBuilder = httpCodec.readResponseHeaders(false);
}

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

int code = response.code();
if (code == 100) {
    //如果服务端响应码为100,需要我们再次请求,注意这里的100是响应码和之前的100不同
    //之前的100是headerLine的标识码
    responseBuilder = httpCodec.readResponseHeaders(false);

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

    code = response.code();
}

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 {
    //读取响应body
    response = response.newBuilder()
        .body(httpCodec.openResponseBody(response))
        .build();
}
return response;

读取响应body HttpCodec#openResponseBody

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

//openResponseBody将Socket的输入流InputStream对象交给OkIo的Source对象,然后封装成RealResponseBody(该类是ResponseBody的子类)作为Response的body.

//具体读取是在RealResponseBody父类ResponseBody中,其中有个string()函数

//响应主体存放在内存中,然后调用source.readString来读取服务器的数据。需要注意的是该方法最后调用closeQuietly来关闭了当前请求的InputStream输入流,所以string()方法只能调用一次,再次调用的话会报错
public final String string() throws IOException {
    BufferedSource source = source();
    try {
        Charset charset = Util.bomAwareCharset(source, charset());
        return source.readString(charset);
    } finally {
        Util.closeQuietly(source);
    }
}

请求回调,数据解析

拿到请求回调的Response之后,再回到我们最开始调用的代码,

String url = "http://www.baidu.com";
//'1. 生成OkHttpClient实例对象'
OkHttpClient okHttpClient = new OkHttpClient();
//'2. 生成Request对象'
Request request = new Request.Builder()
    .url(url)
    .post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"),"test content"))
    .build();
Call call = okHttpClient.newCall(request);

call.enqueue(new Callback() {
    @Override
    public void onFailure(@NonNull Call call, @NonNull IOException e) {
    }

    @Override
    public void onResponse(@NonNull Call call, @NonNull Response response)  {
        Headers responseHeaders = response.headers();
        for (int i = 0, size = responseHeaders.size(); i < size; i++) {
            System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
        }
        System.out.println(response.body().string());
    }
});

我们可以从Response对象中获取所有我们所需要的数据,包括header,body.至此,okHttp的网络请求的大致流程已经分析完成,至于还有部分没有讲到的拦截器就不再本文缀述了.有兴趣的可以看下文末的参考连接或者自行谷歌。

参考文章

Okhttp之CallServerInterceptor简单分析

okHttp各拦截器解析

Android技能树 — 网络小结之 OkHttp超超超超超超超详细解析

OkHttp3.0解析 —— 从源码的角度谈谈发起网络请求时做的操作

扩展阅读

关于Http的请求头 Expect:100-Continue

Expect请求头部域,用于指出客户端要求的特殊服务器行为。若服务器不能理解或者满足
Expect域中的任何期望值,则必须返回417(Expectation Failed)状态,或者如果请求
有其他问题,返回4xx状态。

Expect:100-Continue握手的目的,是为了允许客户端在发送请求内容之前,判断源服务器是否愿意接受
请求(基于请求头部)。
Expect:100-Continue握手需谨慎使用,因为遇到不支持HTTP/1.1协议的服务器或者代理时会引起问题。

http2比起http1.x的有点主要体现在以下几点

  • 新的数据格式, http基于文件协议解析,http2基于二进制协议解析,
  • 连接共享,多路复用(MultiPlexing)
  • header压缩,减小header的体积,使得请求更快
  • 压缩算法从gzip改成HPACK的算法,防破解
  • 重置连接表现更好,http1.x取消请求的是直接断开连接,http2则是断开某个连接的stream流
  • 更安全的SSL

参考资料

http1.x与http2的区别

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

推荐阅读更多精彩内容