读okhttp源码(一)

okhttp对于android开发来说是很常用的库,最近有时间,就准备仔细的读一下其源码

准备

okhttp源码不是android项目,我也是搜了一下,最终采用intelliJ IEDA导入项目,毕竟android studio也是基于它的开源版本二次开发的,用起来没有什么学习成本,导入过程网上都有说明,就不赘述了。

开始

一开始看到okhttp源码,其中包含了很多module,我们还是从okhttp这个最根本的module看起。根据官网的示例代码说明,最简单的使用如下:

OkHttpClient client = new OkHttpClient();
String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();

  try (Response response = client.newCall(request).execute()) {
    return response.body().string();
  }
}

这段代码涉及到OkHttpClient,Request,Call,Response,那我们一个个看,首先是OkHttpClient。

OkHttpClient实现Call.Factory和WebSocket.Factory接口,以实现其基本功能,构造用于发送请求和解析响应的Call对象。在OkHttpClient类中,基本就是定义各种字段,以支持网络通信,比如ConnectionPool,DNS等,以及一些配置字段,比如readTimeOut,writeTimeOut等,然后就是我们熟知的Builder模式的实现,用以构造OkHttpClient。

Request类则是抽象了http request,其也是用builder进行构造,builder模式在源码的很多地方都有用到。Request包含method,url,headers,body,tag,cacheControl字段,其中url是HttpUrl对象,其也是使用builder模式构造的,并且根据类注释,为了解决java类库提供的URL和URI两个类的使用限制,而重新构造了这个模型。headers是对http header的抽象的模型,也需要使用builder模式构造,此类底层使用了一个数组来存储name-value对,采用name,value,name,value这种方式。body类型是RequestBody,这是一个抽象类,包含抽象方法用于定义ContentType,ContentLength,以及关键的writeTo方法,writeTo方法接收BufferedSink类(这个类定义在okio中,这也是okhttp少数几个依赖库之一),实现将字节数组写到流中。tag字段是一个map映射表,主要是用于拦截器或者监听器上,可以使用这些tag做日志生成等。最后是cacheControl,这其实最后也是放到header中,用于缓存控制,也是使用builder模式构造,主要是设置各种缓存控制指令配置。
  
Call是okhttp中定义的一个接口,代表一个已经准备好执行的request请求,并且只能执行一次,如果需要再次执行,可以使用定义的clone方法创建一个相同的Call,另外Call中还定义了同步执行方法execute和异步执行方法enqueue。异步执行的本质,就是讲这个Call加入队列中,由调度器选择合适的时间执行。
  
Reponse则是代表http reponse,并且此类实现Closeable接口,以使用java7引入的try with resource语法糖,在close方法中,就是检查body是否为null,不是的话就调用body.close(为null的情况包括从cacheResponse,networkResponse,priorResponse返回的response),那我们就接着看一下resonseBody。
  
ResponseBody是一次使用的数据流的抽象表示,提供方法获取响应体的字节流,字符流,字节数组(全部读取到内存中),字符串(全部读取到内存中),此类也实现了Closeable,有多种关闭的方法。
  
了解了这些之后,我们看看newCall方法的实现,如何通过一个request构造一个Call。OkHttpClient中newCall的实现,是调用RealCall.newRealCall方法,并将okhttpclient实例和request作为参数传递过去,看来RealCall才是真正包含执行逻辑的地方,让我们顺着看下去。在newRealCall方法中,先是创建一个RealCall实例,它是Call接口的实现类,然后通过okhttpclient的eventlistenerFactory通过这个RealCall实例构造一个EventListener,实现监控网络请求生命周期内各个节点事件。
  
创建完Call之后,示例代码调用execute执行调用,获取response,根据上一步的分析,execute方法实际是RealCall上调用的,让我们来看一下。

  @Override public Response execute() throws IOException {
    if (originalRequest.body instanceof DuplexRequestBody) {
      DuplexRequestBody duplexRequestBody = (DuplexRequestBody) originalRequest.body;
      return duplexRequestBody.awaitExecute();
    }
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    timeout.enter();
    eventListener.callStart(this);
    try {
      client.dispatcher().executed(this);
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } catch (IOException e) {
      e = timeoutExit(e);
      eventListener.callFailed(this, e);
      throw e;
    } finally {
      client.dispatcher().finished(this);
    }
  }

首先判断requestBody是不是DuplexRequestBody,根据这个类的注释,这个类实现一个请求体和响应体双工模式,只支持http/2,我们这里不做分析。然后检查executed变量防止多次执行。captureCallStackTrace方法,获取调用response.body().close时的堆栈信息,再设置在retryAndFollowUpInterceptor中,这里这个retryAndFollowUpInterceptor是一个提供从错误中恢复及必要时重定向功能的类实例,这里就涉及到interceptor拦截器了,我们后面再说。后面timeout是okio里的类,应该是提供timeout功能,后面对照着okio源码可以再看看,然后eventListener执行callStart方法,提供监控,然后使用okhttpclient中的dispatcher调度器执行executed方法,跟踪到其实现,就是将这个call加入到dispatcher内部维护的一个runningSyncCalls,那我们再来看
getResponseWithInterceptorChain方法:

    Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    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,retryAndFollowUpInterceptor之前已经说明,BridgeInterceptor类注释里提到,它的作用,是把用户的request转化为网络request,然后继续网络请求,最后将网络response转化为用户response,CacheInterceptor类的注释则说明此类对来自缓存的请求提供支持,并将响应写到缓存中,ConnectInterceptor则是提供连接到目标服务器的功能,最后CallServerInterceptor则是真正发送一个网络请求给服务器的地方。 这些Interceptor类都实现了Interceptor接口,在这个接口内部定义了Interceptor.Chain这个拦截器链的抽象接口。在拦截器链的具体实现类RealInterceptorChain的proceed方法实现中,关键执行代码的逻辑是先重新构造一个RealInterceptorChain,跟当前这个的区别只是其中一个index参数,这个参数控制RealInterceptorChain内部的interceptors列表,获取第几个interceptor,执行其intercept方法,并将新构造的RealInterceptorChain作为参数传递给intercept方法,然后就可以在intercept方法内部再次调用RealInterceptorChain的proceed方法,以执行下一个interceptor的intercept方法。
  
我们先来看一下RetryAndFollowUpInterceptor的intercept方法实现,先声明一些变量,其中StreamAllocation是我们新遇到的类,根据类注释,它是负责协调Connection,Stream,Call。声明完之后是一个while死循环,在内部,会执行chain.proceed,然后在异常处理中,都会调用recover方法,判断是否可以恢复,能的话就往下走,否则就抛出异常,当然在finally块中,会调用streamAllocation的streamFailed方法和release方法以释放资源。streamFail方法内部,先是以线程池对象同步代码块,在其中,根据异常类型和http连接属性(是否http/2),判断connection上是否需要创建新连接,还有根据条件判断是否调用routeSelector的connectFail方法,标记失败的route(内部会维护一个LinkedHashSet,失败的route会加入其中,使得使用route时可以避免使用这些已标记的route),同步块之后会调用方法关闭socket,这个socket是通过RealConnection类获取的。
  
这块我们先看到这,回到RetryAndFollowUpInterceptor的intercept实现,在处理完chain.proceed方法之后,后面还有一个关键方法followUpRequest,应该对应着类名的FollowUp处理重定向及其相关逻辑。这个方法根据response code执行不同逻辑:如果是401或407这类认证不通过,则调用authenticator或proxyAuthenticator的authenticate方法,返回一个可以通过认证的request;如果是307,308等表示需要重定向的code,则,则根据header里面的Location重新构造httpUrl,并根据条件判断是否要添加requestBody和是否删除认证头信息,最后构造一个新的request并返回;如果是408(很少见),表明直接使用原request重试,当然代码中还会判断okhttpclient中配置是否重试标志位,否的话不重试,还有请求体是不能重试的(StreamedRequestBody),还有如果已经重试过一次并且依然返回408,则不再重试,最后我们会检查response header里的Retry-After,如果这个延迟重试的值大于0,自然现在不需要重试,这样这段逻辑结束;然后如果是503,其逻辑是如果已经重试过并且依然返回503,则不再重试,然后如果Retry-After头的值是0,则返回原request重试,否则不重试;最后就是如果code都不是这些,则也不重试。这样followUpRequest方法就结束了,在这个方法之后,后面就是释放资源,以及如果不是相同的connection,则构造一个新的streamAllcation,使得下一次循环会使用这个新对象。
  
RetryAndFollowUpInterceptor的intercept的方法大致看到这里,后面我们看到CallServerInterceptor,会再次和这里产生关联。下面我们看一下BridgeInterceptor实现,其接收一个cookieJar作为构造方法参数(cookieJar是一个提供cookie策略和持久化方法的接口,默认配置是no cookie,需要自己实现),然后我们看看它的intercept方法。方法实现可以分为三个部分,调用chain.proceed之前的构造request部分,调用chain.proceed之后的构造response部分,以及chain.proceed方法。构造request部分,主要就是在初始的request上再补充header配置,比如Content-Type,Content-Length,Host,Collection,Cookie(由参数cookie构造出value),User-Agent以及Accept-Encoding(配置gzip,并且在之后的构造response时,还负责解压缩数据),这样完成之后,就可以用这个新的request调用chain.proceed,后面真正处理请求的时候,header已经是完整的了。然后我们看看reponse构造的过程,首先是调用保存响应里的cookie方法,然后通过response构造一个builder,用之前构造的request赋值给builder力的request,然后就是有没有response body以及是否启用了gzip,如果条件满足,那么就要使用GzipSource这个responsebody构造一个RealResponseBody并赋值给builder的body,这样就完成了response的处理,可以用builder构造一个新的response并返回。
  
然后我们看一下CacheInterceptor,接收一个InternalCache的参数,这是一个接口,作为内部缓存的定义,我们要扩展的话,需要使用Okhttp.Cache,是个类,其中有内部类实现InternalCache。在intercept方法实现中,首先会遇到CacheStrategy类,构造其实例并获取其内部域networkRequest和cacheResponse,如果这两者都为null,说明缓存未命中,通过网络请求被禁止,则返回新的reponse,code是504以说明这种情况,如果networkRequest为null,cacheReponse不为null,说明缓存命中,并且不需要网络请求,那么只要用这个cacheReponse构造一个新的response返回即可,如果都不是,则执行chain.proceed,这样,就会走到链上后面真正执行网络请求的interceptor,在通过这个方法获取到network reponse之后,如果cacheResponse不为null以及http code为304,则通过networkReponse和cacheReponse生成一个新的response,用于更新缓存并返回这个reponse,否则就判断是否能创建缓存,可以的话,创建一个新的response,用其创建缓存,并返回这个response。这样CacheInterceptor的intercept方法大致讲到这里。

顺带着我们看一下Cache类,之前已经提到有实现了InternalCache的内部类,其方法实现就是调用Cache中同名方法。在大致浏览了一下实现之后,我们可以看到,Cache内有一个DiskLruCache实例,用其将request url进行处理之后作为key存储缓存,缓存的数据分为body和除了body以外的其他信息(meta_data)。

然后我们看一下ConnectInterceptor,它的intercept方法实现就比较简单,得到chain的streamAllocation域后(在RetryAndFollowUpInterceptor的intercept方法里内部构造然后进行了赋值),调用streamAllocation.newStream得到HttpCodec实例,调用streamAllocation.connection得到RealConnection实例之后,就调用chain.proceed(request, streamAllocation, httpCodec, connection)方法(转换chain为具体实现类型然后调用此方法,非接口方法),方法到此结束。

这里又遇到了streamAllocation,我们看一下newStream方法实现,其调用findHealthyConnection去寻找可用的流,此方法调用findConnection,根据这个方法的注释,优先选择已经存在的collection(streamAllcation类的域Connection),如果这个collection不能用来创建新的流,那么就在collection pool中寻找,这里有两次,一次不使用Route,如果没找到,则使用Route再找一次,如果还是找不到,那么就创建一个新的RealConnection,使用这个RealConnection进行tcp和tls握手之后,将这个connection加入到connection pool中,最后返回这个connection。findCollection返回之后,还要再进行一些检查以最终确定这个connection是healthy,如果不是,则循环再次执行上述步骤。然后就到了newCodec方法,比较简单,就是根据RealConnection的内部域http2Connection是否为null,是的话,那么就构造Http1Codec实例,否则构造Http2Codec实例并返回,这两个实例都是HttpCodec这个接口的实现,这个接口根据注释,是为了编码http request以及解码http response。newStream返回值是HttpCodec,到此返回构造的实例,方法就结束了。

然后看一下streamAllocation.connection实现,由于上一步已经生成了RealConnection,直接返回这个实例,方法就结束了。另外注意一下,connection是底层socket的封装,然后stream是基于connection,最后call则是基于stream,这些关系由StreamAllocation协调。

最后我们看一下CallServerInterceptor的intercept方法实现。主要分为下面几个步骤:
1)httpCodec.writeRequestHeaders(request),写入请求头
2)如果需要写入请求体并且其不为null,则通过下面的代码:
···
long contentLength = request.body().contentLength();
CountingSink requestBodyOut =
new CountingSink(httpCodec.createRequestBody(request, contentLength));
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);

      request.body().writeTo(bufferedRequestBody);
      bufferedRequestBody.close();

···
这样就完成了写入请求体的工作
3)httpCodec.finishRequest(),将数据推入到底层socket中,然后就可以开始读取response了。
4)httpCodec.readResponseHeaders(false),读取response的header部分
5)通过上一步得到responseBuilder,然后设置handshake,sentRequestMillis等域,构造出response
6)调用httpCodec.openResponseBody(response)读取responseBody并设置

基本流程就像这样,这里httpCodec用到很多次,之前已经提到过,httpCodec是个借口,有两个实现:Http1Codec和Http2Codec,分别对应http1.1和http/2,上面在httpCodec上调用的方法,在这两个类中实现都是不同的。我这里主要看了一下Http1Codec的实现,内部用到了BufferedSource和BufferedSink,这两个是okio里的类,BufferedSource包装Source,附加buffer功能,同样,BufferedSink包装Sink,附加buffer功能,而Source是okio实现的替代InputStream的提供读取数据的接口类,Sink是okio实现的替代OutputStream的提供写入数据的接口类,由此可知,底层的读写数据,都是借助okio来实现,

看到这里,我们总算了解了RealCall的getResponseWithInterceptorChain方法实现,得到了其返回值response,execute方法就可以直接返回它,然后我们看到在finally块中调用了client.dispatcher().finished(this)方法,那么我们来看一下它的实现。

首先在用Dispatcher自身synchronized的块,执行runningSyncCalls.remove(call),移除之前执行dispatcher().executed方法时加入到队列的call,然后会执行promoteAndExecute方法。按照promoteAndExecute方法的注释,它执行把readyAsyncCalls队列中的call提升到runningAsyncCalls,换句话说就是,遍历readyAsyncCalls,只要runningAsyncCalls的size没有达到maxRequests以及共享这个call的host的所有call的数量不超过maxRequestsPerHost,那么我们就会把这个call加入到runningAsyncCalls,并且在内部线程池中run,同时这个方法返回runningAsyncCalls和runningSyncCalls两者总大小是否为0,如果为0,为0就说明dispatcher闲置,此时就会调用idleCallback.run()(如果这个idleCallback不为null)。

然后我们可以看看RealCall的enqueue(Callback responseCallback)异步执行方法。代码很短,核心代码是client.dispatcher().enqueue(new AsyncCall(responseCallback)),那我们接着看看Dispatcher的enqueue方法,首先是synchronized的调用readyAsyncCalls.add(call),然后调用promoteAndExecute(),这个方法刚才看过了,我们这里看一下AsyncCall,它继承NamedRunnable,主要看一下其execute方法,核心还是调用getResponseWithInterceptorChain方法,得到结果后根据条件调用responseCallback对应成功或失败的回调方法,以及在finally中调用client.dispatcher().finished(this)。

这样我们也就知道异步执行是怎么实现的。接着我们回过头来,再来看看RealConnection。connect方法,之前已经提到负责tcp和tls握手连接,其实现中有几个关键方法,connectSocket方法根据条件创建底层的socket实例,满足条件则调用Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout)方法建立连接,然后使用这个底层socket实例,我们称之为rawSocket,创建BufferedSink和BufferedSource实例,注意这里用到了Platform类,其有很多子类,分别对应不同平台,比如android,jdk9,等待。然后还有establishProtocol方法,根据条件把不同的值赋给socket和protocol域,这个socket不一定是之前的rawSocket,根据注释说明,如果不需要tls,那么直接把通过connectSocket创建的rawSocket赋值给socket,否则会调用connnectTls方法创建一个SSLSocket(其中包装了rawSocket),然后赋值给socket,这里还涉及到tls握手,建立session,验证证书等操作。然后如果是http2,我们会创建一个Http2Connection实例,将刚得到的socket实例,BufferedSink,BufferedSource等赋值给Http2Connection内部域,这个Http2Connection是给Http2Codec提供支持的,同样的还有Http2Stream类。

这样就大致了解了RealConnection,其实connect方法里还有涉及到http proxy处理的逻辑,http2相关类的逻辑我们后面有时间再看。

我们再回过头来看看Http1Codec类,其中有几个Sink接口的内部实现类,分别是FixedLengthSink(固定大小),ChunkedSink(分块处理),以及几个Source的内部实现类,分别是抽象基类AbstractSource(作为后面几个类的基类,提供read和endOfInput方法的实现),FixedLengthSource(固定大小),ChunkedSource(分块处理),UnknownLengthSource(未知大小,通过底层流结束来判断结束)。

这篇就先写到这里,下一篇我就从okhttp post请求谈起。

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