okhttp源码分析

2023-11-19

Okhttp介绍

由square公司贡献的一个处理网络请求的开源项目,是目前Android使用最广泛的网络框架,从Android4.4开始HttpURLConnection的底层实现采用的是okhttp。
项目地址:https://github.com/square/okhttp
须知,本文是基于okhttp3.14.9版本来进行分析,如果版本不一样,可以部分流程会与本文不一致,但是大体思想都是一样的。

优点

  • 支持http/2并允许对同一主机的所有请求共享一个套接字,因为http/2支持并行传输,即一个请求的响应还没回来就可以发起另一个请求。
  • 非http/2的请求,则通过连接池,减少请求延迟。非http/2的请求是串行传输的,必须等前面一个请求的响应回来之后才能发起第二个请求。但是非http/2的传输也可以共享同一个套接字,只不过必须保证传输是串行传输的。
  • 默认允许服务端使用Gzip压缩响应数据
  • 响应缓存,避免了重复请求网络。该缓存默认是关闭的,如果需要开启直接在初始化的时候使用即可,且该缓存只能缓存get请求的数据,无法缓存post请求的数据。缓存是通过okhttp的一个缓存拦截器来实现的。

使用

val client = OkHttpClient.Builder().cache(Cache(File("/xxx"),111))//配置缓存
            //...可以在初始化的时候配置一些相关的信息
            .eventListener(object :EventListener(){
                override fun callStart(call: Call) {
                    super.callStart(call)
                }
            })//一些回调的listener
            .cookieJar(object :CookieJar{
                override fun saveFromResponse(p0: HttpUrl, p1: MutableList<Cookie>) {
                    TODO("Not yet implemented")
                }

                override fun loadForRequest(p0: HttpUrl): MutableList<Cookie> {
                    TODO("Not yet implemented")
                }

            })
            .proxy(Proxy.NO_PROXY)//不适应代理,优先级高于proxySelector
            .proxySelector(object :ProxySelector(){
                override fun select(uri: URI?): MutableList<Proxy> {
                    TODO("Not yet implemented")
                }

                override fun connectFailed(uri: URI?, sa: SocketAddress?, ioe: IOException?) {
                    TODO("Not yet implemented")
                }

            })
            .build()
        val request = Request.Builder().url("https://www.baidu.com").get()/*post()*/.build()//不传的话,默认是get请求
        val call = client.newCall(request)
        val result = call.execute()

调用流程

okhttp请求过程中最少需要解除okhttpclient、request、call、response,但是框架内部进行大量的逻辑处理。
所有的逻辑大部分集中在拦截器中,但是进入拦截器之前还需要依靠分发器来调配请求任务

    分发器:内部维护队列与线程池、完成请求调配
    拦截器:完成整个请求过程

在这里插入图片描述
okhttp创建时相关的源码,笔者就不说了,挺简单的,主要就是一个建造者模式,我们从okhttp的同步请求和异步请求来入手分析:
在这里插入图片描述
call的execute方法代表了同步请求,而enqueue方法代表了异步请求。两者唯一的区别在于一个会直接发起请求,而一个是使用okhttp内置的线程池来进行,这就会涉及到okhttp的任务分发器。
我们通过client.newCall(request)创建的call对象时,实际上时创建了realcall对象,call只是一个接口,而realcall是其实现类。我们先来看下enqueue方法(异步请求),里面的代码很简单,this.transmitter.callStart();这句其实会回调到我们在创建okhttpclient时配置的listener的回调方法,我们最应该重视的是最后一行代码,enqueue方法最后是调用了dispatcher分发器的enqueue方法。我们来看下dispatcher类,该类有3个执行队列以及线程池参数需要注意。
在这里插入图片描述

//异步请求同时存在的最大请求
private int maxRequests = 64;
//异步请求同一域名同时存在的最大请求
private int maxRequestsPerHost = 5;
@Nullable
//闲置任务(没有请求时可执行一些任务,有开发者自己定义)
private Runnable idleCallback;
@Nullable
//异步请求使用的线程池
private ExecutorService executorService;
//异步请求等待执行队列
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque();
//异步请求正在执行队列
    private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque();
//同步请求正在执行队列
    private final Deque<RealCall> runningSyncCalls = new ArrayDeque();

在这里插入图片描述
dispatcher类的enqueue方法有两处比较重要的代码,分别是findExistingCallWithHost方法和promoteAndExecute方法

  • findExistingCallWithHost
    该方法会迭代runningAsyncCalls和readyAsyncCalls,如果该队列中有同一host的call就赋值给Realcall类里面的callsPerHost变量。
  • promoteAndExecute
    该方法比较重要,我们重点来看下这里面做了什么事。
private boolean promoteAndExecute() {
        assert !Thread.holdsLock(this);

        List<AsyncCall> executableCalls = new ArrayList();
        boolean isRunning;
        AsyncCall asyncCall;
        synchronized(this) {
            Iterator i = this.readyAsyncCalls.iterator();
//当正在执行的任务未超过最大限制64,同时 runningcal1sForriost(call)<maxRequestsPerHost 同-Host的请求不超过5个,则会添加到正在执行队列,同时提交给线程池,否则先加入等待队列。
//加入线程池直接执行没啥好说的,但是如果加入等待队列后,就需要等待有空闲名额才开始执行。因此每次执行完-个请求后,会根据是否成功都会调用分发器的 finished方法,详见executeOn方法
            while(true) {
                if (i.hasNext()) {
                    asyncCall = (AsyncCall)i.next();
                    if (this.runningAsyncCalls.size() < this.maxRequests) {
                        if (asyncCall.callsPerHost().get() < this.maxRequestsPerHost) {
                            i.remove();
                            asyncCall.callsPerHost().incrementAndGet();
                            executableCalls.add(asyncCall);
                            this.runningAsyncCalls.add(asyncCall);
                        }
                        continue;
                    }
                }

                isRunning = this.runningCallsCount() > 0;
                break;
            }
        }

        int i = 0;

        for(int size = executableCalls.size(); i < size; ++i) {
            asyncCall = (AsyncCall)executableCalls.get(i);
            //重点
            asyncCall.executeOn(this.executorService());
        }

        return isRunning;
    }
void executeOn(ExecutorService executorService) {
            assert !Thread.holdsLock(RealCall.this.client.dispatcher());

            boolean success = false;

            try {
                executorService.execute(this);//最终会走到下面的execute方法
                success = true;
            } catch (RejectedExecutionException var8) {
                InterruptedIOException ioException = new InterruptedIOException("executor rejected");
                ioException.initCause(var8);
                RealCall.this.transmitter.noMoreExchanges(ioException);
                this.responseCallback.onFailure(RealCall.this, ioException);
            } finally {
                if (!success) {
                    RealCall.this.client.dispatcher().finished(this);
                }
            }
        }
        protected void execute() {
            boolean signalledCallback = false;
            RealCall.this.transmitter.timeoutEnter();

            try {
               //getResponseWithInterceptorChain这个方法十分重要,无论是同步请求还是异步请求,response都是使用这个方法获取的,下面我们在分析这里面究竟做了什么事
                Response response = 
               RealCall.this.getResponseWithInterceptorChain();
                signalledCallback = true;
                this.responseCallback.onResponse(RealCall.this, response);
            } catch (IOException var8) {
                if (signalledCallback) {
                    Platform.get().log(4, "Callback failure for " + RealCall.this.toLoggableString(), var8);
                } else {
                    this.responseCallback.onFailure(RealCall.this, var8);
                }
            } catch (Throwable var9) {
                RealCall.this.cancel();
                if (!signalledCallback) {
                    IOException canceledException = new IOException("canceled due to " + var9);
                    canceledException.addSuppressed(var9);
                    this.responseCallback.onFailure(RealCall.this, canceledException);
                }

                throw var9;
            } finally {
                RealCall.this.client.dispatcher().finished(this);
            }

        }
//异步请求结束时调用
void finished(AsyncCall call) {
        call.callsPerHost().decrementAndGet();
        this.finished(this.runningAsyncCalls, call);
    }
//同步请求结束时调用
    void finished(RealCall call) {
        this.finished(this.runningSyncCalls, call);
    }

//将请求结束的call移除出正在执行的队列
    private <T> void finished(Deque<T> calls, T call) {
        Runnable idleCallback;
        synchronized(this) {
            if (!calls.remove(call)) {
                throw new AssertionError("Call wasn't in-flight!");
            }
            idleCallback = this.idleCallback;
        }

        boolean isRunning = this.promoteAndExecute();
        if (!isRunning && idleCallback != null) {
            idleCallback.run();
        }
    }

异步请求的分析到此结束,下面我们来看下同步请求。

public Response execute() throws IOException {
        synchronized(this) {
            if (this.executed) {
                throw new IllegalStateException("Already Executed");
            }

            this.executed = true;
        }

        this.transmitter.timeoutEnter();
        this.transmitter.callStart();

        Response var1;
        try {
            this.client.dispatcher().executed(this);
            var1 = this.getResponseWithInterceptorChain();
        } finally {
            this.client.dispatcher().finished(this);
        }

        return var1;
    }
synchronized void executed(RealCall call) {
        this.runningSyncCalls.add(call);
    }

同步请求最后也是进入到了dispatcher的executed方法,该方法只是将该请求放入runningSyncCalls队列中,之后调用了getResponseWithInterceptorChain方法获取response,十分简单,没什么好说的。

分发器

前面我们提过,分发器就是来调配请求任务的,内部会包含一个线程池。当异步请求时,会将请求任务交给线程池来执行。那分发器中默认的线程池是如何定义的呢?为什么要这么定义?

public synchronized ExecutorService executorService() {
        if (this.executorService == null) {
            this.executorService = new ThreadPoolExecutor(
            0, //核心线程数
            2147483647, //最大线程数
            60L, //空闲线程闲置时间
            TimeUnit.SECONDS, //闲置时间单位
            new SynchronousQueue(), //线程等待队列
            Util.threadFactory("OkHttp Dispatcher", false)//线程创建工厂
            );
        }
        return this.executorService;
    }

在OkHttp的分发器中的线程池定义如上,其实就和 Executors.newCachedThreadPool()创建的线程一样。首先核心线程为0,表示线程池不会一直为我们缓存线程,线程池中所有线程都是在60s内没有工作就会被回收。而最大线程 Integer.MAX_VALUE 与等待队列 SynchronousQueue 的组合能够得到最大的吞吐量。即当需要线程池执行任务时,如果不存在空闲线程不需要等待,马上新建线程执行任务!等待队列的不同指定了线程池的不同排队机制。一般来说,等待队列 B1ockingQueue 有:ArrayBlockingQueue、LinkedBlockingQueue和SynchronousQueue。
那么上面列举的三个等待队列有什么不同呢?
假设向线程池提交任务时,核心线程都被占用的情况下:

  • ArrayBlockingQueue:基于数组的阻塞队列,初始化需要指定固定大小。
    当使用此队列时,向线程池提交任务,会首先加入到等待队列中,当等待队列满了之后,再次提交任务,尝试加入队列就会失败,这时就会检查如果当前线程池中的线程数末达到最大线程,则会新建线程执行新提交的任务。所以最终可能出现后提交的任务先执行,而先提交的任务一直在等待。
  • LinkedBlockingQueue:基于链表实现的阻塞队列,初始化可以指定大小,也可以不指定。当指定大小后,行为就和 ArrayBlockingQueue一致。而如果末指定大小,则会使用默认的 Integer. MAX_VALUE 作为队列大小。这时候就会出现线程池的最大线程数参数无用,因为无论如何,向线程池提交任务加入等待队列都会成功。最终意味着所有任务都是在核心线程执行。如果核心线程一直被占,那就一直等待。
  • SynchronousQueue:无容量的队列.使用此队列意味着希望获得最大并发量。因为无论如何,向线程池提交任务,往队列提交任务都会失败。而失败后如果没有空闲的非核心线程,就会检查如果当前线程池中的线程数末达到最大线程数,则会新建线程执行新提交的任务。完全没有任何等待,唯一制约它的就是最大线程数的个数。因此一般配合 Integer.MAX VALUE 就实现了真正的无等待。
    但是需要注意的时,我们都知道,进程的内存是存在限制的,而每一个线程都需要分配一定的内存。所以线程并不能无限个数。那么当设置最大线程数为 Integer.MAX VALUE 时,OKHttp同时还有最大请求任务执行个数:64的限制。这样即解决了这个问题同时也能获得最大吞吐。

五大拦截器

okhttp中请求会被交给责任链中的一个个拦截器。默认情况下有五大拦截器:

  1. RetryAndFollowUpInterceptor:第一个接触到请求,最后接触到响应;负责判断是否需要重新发起整个请求。
  2. BridgeInterceptor:补全请求,并对响应进行额外处理。
  3. CacheInterceptor:请求前查询缓存,获得响应并判断是否需要缓存。
  4. ConnectInterceptor:与服务器完成TCP连接。
  5. CallServerInterceptor:与服务器通信;封装请求数据与解析响应数据(如:HTTP报文)

重试与重定向拦截器(RetryAndFollowUpInterceptor)

顾名思义,该拦截器的功能就是重试与重定向。第一个接触到请求,最后接触到响应;负责判断是否需要重新发起整个请求。

重试

请求阶段发生了 RouteException 或者 IOException会进行判断是否重新发起请求。
在这里插入图片描述

RouteException

catch (RouteException e) {
	//todo 路由异常,连接未成功,请求还没发出去
    if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
    	throw e.getLastConnectException();
    }
    releaseConnection = false;
    continue;
} 

IOException

catch (IOException e) {
	//todo 请求发出去了,但是和服务器通信失败了。(socket流正在读写数据的时候断开连接)
    // ConnectionShutdownException只对HTTP2存在。假定它就是false
	boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
	if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
		releaseConnection = false;
		continue;
} 

两个异常都是根据recover 方法判断是否能够进行重试,如果返回true,则表示允许重试。

private boolean recover(IOException e, StreamAllocation streamAllocation,
                            boolean requestSendStarted, Request userRequest) {
	streamAllocation.streamFailed(e);
	//todo 1、在配置OkhttpClient是设置了不允许重试(默认允许),则一旦发生请求失败就不再重试
	if (!client.retryOnConnectionFailure()) return false;
	//todo 2、由于requestSendStarted只在http2的io异常中为true,先不管http2
	if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)
		return false;

	//todo 3、判断是不是属于重试的异常
	if (!isRecoverable(e, requestSendStarted)) return false;

	//todo 4、有没有可以用来连接的路由路线
	if (!streamAllocation.hasMoreRoutes()) return false;

	// For failure recovery, use the same route selector with a new connection.
	return true;
}

所以首先使用者在不禁止重试的前提下,如果出现了某些异常,并且存在更多的路由线路,则会尝试换条线路进行请求的重试。其中某些异常是在isRecoverable中进行判断:

private boolean isRecoverable(IOException e, boolean requestSendStarted) {
	// 出现协议异常,不能重试
    if (e instanceof ProtocolException) {
      return false;
    }

	// requestSendStarted认为它一直为false(不管http2),异常属于socket超时异常,直接判定可以重试
    if (e instanceof InterruptedIOException) {
      return e instanceof SocketTimeoutException && !requestSendStarted;
    }

    // SSL握手异常中,证书出现问题,不能重试
    if (e instanceof SSLHandshakeException) {
      if (e.getCause() instanceof CertificateException) {
        return false;
      }
    }
    // SSL握手未授权异常 不能重试
    if (e instanceof SSLPeerUnverifiedException) {
      return false;
    }
    return true;
}

1、协议异常,如果是那么直接判定不能重试;(你的请求或者服务器的响应本身就存在问题,没有按照http协议来定义数据,再重试也没用)

2、超时异常,可能由于网络波动造成了Socket管道的超时,那有什么理由不重试?(后续还会涉及到路由)

3、SSL证书异常/SSL验证失败异常,前者是证书验证失败,后者可能就是压根就没证书,或者证书数据不正确,那还怎么重试

经过了异常的判定之后,如果仍然允许进行重试,就会再检查当前有没有可用路由路线来进行连接。简单来说,比如 DNS 对域名解析后可能会返回多个 IP,在一个IP失败后,尝试另一个IP进行重试。

重定向

如果请求结束后没有发生异常并不代表当前获得的响应就是最终需要交给用户的,还需要进一步来判断是否需要重定向的判断。重定向的判断位于followUpRequest方法,该方法主要是根据不同的响应码,赋予不同的操作。
在这里插入图片描述
如果此方法返回空,那就表示不需要再重定向了,直接返回响应;但是如果返回非空,那就要重新请求返回的 Request,但是需要注意的是,
我们的 followup 在拦截器中定义的最大次数为20次。

总结

  1. 本拦截器是整个责任链中的第一个,这意味着它会是首次接触到 Request 与最后接收到 Response的角色,在这个拦截器中主要功能就是判断是否需要重试与重定向。
  2. 重试的前提是出现了 RouteException 或者 IOException。一但在后续的拦截器执行过程中出现这两个异常,就会通过 recover 方法讲行判断是否讲行连接重试。
  3. 重定向发生在重试的判定之后,如果不满足重试的条件,还需要进一步调用 followUpRequest 根据 Response的响应码(当然,如果直接请求失败,Response 都不存在就会抛出异常)。followup 最大发生20次。

桥接拦截器

BridgeInterceptor,连接应用程序和服务器的桥梁,我们发出的请求将会经过它的处理才能发给服务器,比如设置请求内容长度,编码,gzip压缩,cookie等,获取响应后保存Cookie等操作。这个拦截器相对比较简单,详细代码可查看BridgeInterceptor类的intercept方法。

补全请求头:

请求头 说明
Content-Type 请求体类型,如:application/x-www-form-urlencoded
Content-Length/Transfer-Encoding 请求体解析方式
Host 请求的主机站点
Connection: Keep-Alive 保持长连接
Accept-Encoding: gzip 接受响应支持gzip压缩
Cookie cookie身份辨别
User-Agent 请求的用户信息,如:操作系统、浏览器等

在补全了请求头后交给下一个拦截器处理,得到响应后,主要干两件事情:

1、保存cookie,在下次请求则会读取对应的数据设置进入请求头,默认的CookieJar不提供实现,需要在构建okhttpclient时,手动配置。

2、如果使用gzip返回的数据,则使用GzipSource包装便于解析。

总结

桥接拦截器的执行逻辑主要就是以下几点

对用户构建的Request进行添加或者删除相关头部信息,以转化成能够真正进行网络请求的Request
将符合网络请求规范的Request交给下一个拦截器处理,并获取Response
如果响应体经过了GZIP压缩,那就需要解压,再构建成用户可用的Response并返回。

缓存拦截器

CacheInterceptor,在发出请求前,判断是否命中缓存。如果命中则可以不请求,直接使用缓存的响应。(只会存在Get请求的缓存)
步骤为:

  1. 从缓存中获得对应请求的响应缓存
  2. 创建 Cachestrategy,创建时会判断是否能够使用缓存,在Cachestrategy 中存在两个成员:networkRequest与 cacheResponse。他们的组合如下:
networkRequest cacheResponse 说明
Null Not Null 直接使用缓存
Not Null Null 向服务器发起请求
Null Null 直接gg, okhttp直接返回504
Not Null Not Null 发起请求,若得到响应为304(无修改),则更新缓存响应并返回
  1. 交给下一个责任链继续处理
  2. 后续工作,返回304则用缓存的响应;否则使用网络响应并缓存本次响应 (只缓存Get请求的响应)

在缓存拦截器中判断是否可以使用缓存还是请求服务器都是通过CacheStrategy判断。
Http的缓存我们可以按照行为将他们分为:强缓存和协商缓存 l

  • 命中强缓存时,浏览器并不会将请求发送给服务器。强缓存是利用http的返回头中的Expires或者Cache- Control两个字段来控制的,用来表示资源的缓存时间;
  • 若未命中强缓存,则浏览器会将请求发送至服务器。服务器根据http头信息中的Last-Modify/If-Modify- Since或Etag/If-None-Match来判断是否命中协商缓存。如果命中,则http返回码为304,客户端从缓存中加载资源。

缓存策略
首先需要认识几个请求头和响应头

响应头 说明 例子
Date 消息发送的时间 Date: Sat, 18 Nov 2028 06:17:41 GMT
Expires 资源过期的时间 Expires: Sat, 18 Nov 2028 06:17:41 GMT
Last-Modified 资源最后修改时间 Last-Modified: Fri, 22 Jul 2016 02:57:17 GMT
ETag 资源在服务器的唯一标识 ETag:"16df0-5383097a03d40’’
Age 服务器用缓存响应请求,该缓存从产生到现在经过多长时间(秒) Age: 3825683
Cache-Control - -
请求头 说明 例子
If-Modified-Since 服务器没有在指定的时间后修改请求对应资源,返回304(无修改) If-Modified Since: Fri, 22 Jul 2016 02:57:17 GMT
If-None-Match 服务器将其与请求对应资源的 Etag 值进行比较,匹配返回304 If-None-Match: “16df0-5383097a03d40”
Cache-Control - -

其中 Cache-control可以在请求头存在,也能在响应头存在,对应的value可以设置多种组合:

  1. maxage=[秒]:资源最大有效时间;
  2. public:表明该资源可以被任何用户缓存,比如客户端,代理服务器等都可以缓存资源;
  3. private:表明该资源只能被单个用户缓存,默认是private。
  4. no-store:资源不允许被缓存
  5. no-cache:(请求)不使用缓存
  6. immutable:(响应)资源不会改变
  7. min-fresh=[秒]:(请求)缓存最小新鲜度(用户认为这个缓存有效的时长)
  8. must revalidate:(响应)不允许使用过期缓存
  9. max-stale=[秒]:(请求)缓存过期后多久内仍然有效

假设存在max-age=100,min-fresh=20。这代表了用户认为这个缓存的响应,从服务器创建响应 到 能够缓存使用的时间为100-20=80s。但是如果max-stale=100。这代表了缓存有效时间80s过后,仍然允许使用
100s,可以看成缓存有效时长为180s。

    public Response intercept(Chain chain) throws IOException {
        Response cacheCandidate = this.cache != null ? this.cache.get(chain.request()) : null;
        long now = System.currentTimeMillis();
        CacheStrategy strategy = (new Factory(now, chain.request(), cacheCandidate)).get();
        Request networkRequest = strategy.networkRequest;
        Response cacheResponse = strategy.cacheResponse;
        if (this.cache != null) {
            this.cache.trackResponse(strategy);
        }

        if (cacheCandidate != null && cacheResponse == null) {
            Util.closeQuietly(cacheCandidate.body());
        }

        if (networkRequest == null && cacheResponse == null) {
            return (new Builder()).request(chain.request()).protocol(Protocol.HTTP_1_1).code(504).message("Unsatisfiable Request (only-if-cached)").body(Util.EMPTY_RESPONSE).sentRequestAtMillis(-1L).receivedResponseAtMillis(System.currentTimeMillis()).build();
        } else if (networkRequest == null) {
            return cacheResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).build();
        } else {
            Response networkResponse = null;

            try {
                networkResponse = chain.proceed(networkRequest);
            } finally {
                if (networkResponse == null && cacheCandidate != null) {
                    Util.closeQuietly(cacheCandidate.body());
                }

            }

            Response response;
            if (cacheResponse != null) {
                if (networkResponse.code() == 304) {
                    response = cacheResponse.newBuilder().headers(combine(cacheResponse.headers(), networkResponse.headers())).sentRequestAtMillis(networkResponse.sentRequestAtMillis()).receivedResponseAtMillis(networkResponse.receivedResponseAtMillis()).cacheResponse(stripBody(cacheResponse)).networkResponse(stripBody(networkResponse)).build();
                    networkResponse.body().close();
                    this.cache.trackConditionalCacheHit();
                    this.cache.update(cacheResponse, response);
                    return response;
                }

                Util.closeQuietly(cacheResponse.body());
            }

            response = networkResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).networkResponse(stripBody(networkResponse)).build();
            if (this.cache != null) {
                if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
                    CacheRequest cacheRequest = this.cache.put(response);
                    return this.cacheWritingResponse(cacheRequest, response);
                }

                if (HttpMethod.invalidatesCache(networkRequest.method())) {
                    try {
                        this.cache.remove(networkRequest);
                    } catch (IOException var13) {
                    }
                }
            }
            return response;
        }
    }

缓存拦截器大体上是根据一些响应码和以及上面的一些请求头、响应头做缓存策略。具体逻辑大家可以看下CacheStrategy类,这里就不一一说明。

总结

  1. 如果从缓存获取的Response 是null,那就需要使用网络请求获取响应。
  2. 如果是Https请求,但是又丢失了握手信息,那也不能使用缓存,需要进行网络请求。
  3. 如果判断响应码不能缓存且响应头有 no-store 标识,那就需要讲行网络请求。
  4. 如果请求头有 no-cache 标识或者有 Ifmodified-Since/If-None-Match,那么需要进行网络请求。
  5. 如果响应头没有no-cache 标识,旦缓存时间没有超过极限时间,那么可以使用缓存,不需要进行网络请求。
  6. 如果缓存过期了,判断响应头是否设置 Etag/Last-modified/Date ,没有那就直接使用网络请求否则需要考虑服务器返回304。并且,只要需要进行网络请求,请求头中就不能包含 only-if-cached,否则框架直接返回504!

连接拦截器

该拦截器的作用是:打开与目标服务器的连接,并执行下一个拦截器,它的源码比较简单,虽然代码量比较少,但是大部分都是封装到别的类里面去,这里只是调用而已。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package okhttp3.internal.connection;

import java.io.IOException;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Interceptor.Chain;
import okhttp3.internal.http.RealInterceptorChain;

public final class ConnectInterceptor implements Interceptor {
    public final OkHttpClient client;

    public ConnectInterceptor(OkHttpClient client) {
        this.client = client;
    }

    public Response intercept(Chain chain) throws IOException {
        RealInterceptorChain realChain = (RealInterceptorChain)chain;
        Request request = realChain.request();
        Transmitter transmitter = realChain.transmitter();
        boolean doExtensiveHealthChecks = !request.method().equals("GET");
        Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);
        return realChain.proceed(request, transmitter, exchange);
    }
}

这个拦截器中比较重要的是realconnection类——封装了一个socket连接池。如何复用socket连接呢?关键在于一个isEligible方法。

boolean isEligible(Address address, @Nullable List<Route> routes) {
        if (this.transmitters.size() < this.allocationLimit && !this.noNewExchanges) {
            if (!Internal.instance.equalsNonHost(this.route.address(), address)) {
                return false;
            } else if (address.url().host().equals(this.route().address().url().host())) {
                return true;
            } else if (this.http2Connection == null) {
                return false;
            } else if (routes != null && this.routeMatchesAny(routes)) {
                if (address.hostnameVerifier() != OkHostnameVerifier.INSTANCE) {
                    return false;
                } else if (!this.supportsUrl(address.url())) {
                    return false;
                } else {
                    try {
                        address.certificatePinner().check(address.url().host(), this.handshake().peerCertificates());
                        return true;
                    } catch (SSLPeerUnverifiedException var4) {
                        return false;
                    }
                }
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

这段代码大体的意思是:

  1. 连接到达最大并发流或者连接不允许建立新的流;如http1.x正在使用的连接不能给其他人用(最大并发流为:1)或者连接被关闭;那就不允许复用;
  2. DNS、代理、SSL证书、服务器域名、端口完全相同则可复用;
  3. 所以综上,如果在连接池中找到个连接参数一致并且末被关闭没被占用的连接,则可以复用。

总结
这个拦截器中所有的实现都是为了获得一份与目标拦截器的连接,获取对应的socket流,在获得结果之后不进行额外的处理。

请求服务器拦截器

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package okhttp3.internal.http;

import java.io.IOException;
import java.net.ProtocolException;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Interceptor.Chain;
import okhttp3.Response.Builder;
import okhttp3.internal.Util;
import okhttp3.internal.connection.Exchange;
import okio.BufferedSink;
import okio.Okio;

public final class CallServerInterceptor implements Interceptor {
    private final boolean forWebSocket;

    public CallServerInterceptor(boolean forWebSocket) {
        this.forWebSocket = forWebSocket;
    }

    public Response intercept(Chain chain) throws IOException {
        RealInterceptorChain realChain = (RealInterceptorChain)chain;
        Exchange exchange = realChain.exchange();
        Request request = realChain.request();
        long sentRequestMillis = System.currentTimeMillis();
        exchange.writeRequestHeaders(request);
        boolean responseHeadersStarted = false;
        Builder responseBuilder = null;
        if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
            if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
            //flushRequest,真正发生数据给服务器
                exchange.flushRequest();
                responseHeadersStarted = true;
                exchange.responseHeadersStart();
                responseBuilder = exchange.readResponseHeaders(true);
            }

            if (responseBuilder == null) {
                BufferedSink bufferedRequestBody;
                if (request.body().isDuplex()) {
                    exchange.flushRequest();
                    bufferedRequestBody = Okio.buffer(exchange.createRequestBody(request, true));
                    request.body().writeTo(bufferedRequestBody);
                } else {
                    bufferedRequestBody = Okio.buffer(exchange.createRequestBody(request, false));
                    request.body().writeTo(bufferedRequestBody);
                    bufferedRequestBody.close();
                }
            } else {
                exchange.noRequestBody();
                if (!exchange.connection().isMultiplexed()) {
                    exchange.noNewExchangesOnConnection();
                }
            }
        } else {
            exchange.noRequestBody();
        }

        if (request.body() == null || !request.body().isDuplex()) {
            exchange.finishRequest();
        }

        if (!responseHeadersStarted) {
            exchange.responseHeadersStart();
        }

        if (responseBuilder == null) {
            responseBuilder = exchange.readResponseHeaders(false);
        }

        Response response = responseBuilder.request(request).handshake(exchange.connection().handshake()).sentRequestAtMillis(sentRequestMillis).receivedResponseAtMillis(System.currentTimeMillis()).build();
        int code = response.code();
        if (code == 100) {
            response = exchange.readResponseHeaders(false).request(request).handshake(exchange.connection().handshake()).sentRequestAtMillis(sentRequestMillis).receivedResponseAtMillis(System.currentTimeMillis()).build();
            code = response.code();
        }

        exchange.responseHeadersEnd(response);
        if (this.forWebSocket && code == 101) {
            response = response.newBuilder().body(Util.EMPTY_RESPONSE).build();
        } else {
            response = response.newBuilder().body(exchange.openResponseBody(response)).build();
        }
        
        if ("close".equalsIgnoreCase(response.request().header("Connection")) || "close".equalsIgnoreCase(response.header("Connection"))) {
            exchange.noNewExchangesOnConnection();
        }

        if ((code == 204 || code == 205) && response.body().contentLength() > 0L) {
            throw new ProtocolException("HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
        } else {
            return response;
        }
    }
}

整个拦截器就是完成http协议报文的封装以及解析,以及判断是否关闭http连接。
总结
真正与服务器进行通信,向服务器发送数据,解析读取的响应数据。

代理

在使用OkHttp时,如果用户在创建 OkHttpClient 时,配置了 proxy 或者 proxyselector,则会使用配置的代理,并且 proxy 优先级高于 proxyselector。而如果末配置,则会获取机器配置的代理并使用。
因此,如果我们不需要自己的App中的请求走代理,则可以配貴一个 proxy (Proxy .NO_ PROxY),这样也可以避免被抓包。
okhttp中一共有三种类型的代理:

public enum Type {
        /**
         * Represents a direct connection, or the absence of a proxy.
         */
        DIRECT,//无代理
        /**
         * Represents proxy for high level protocols such as HTTP or FTP.
         */
        HTTP,//http代理
        /**
         * Represents a SOCKS (V4 or V5) proxy.
         */
        SOCKS//socks代理
    };

对于Socks代理,在HTTP的场景下,代理服务器完成TCP数据包的转发工作;而Http代理服务器,在转发数据之外,还会解析HTTP的请求及响应,并根据请求及响应的内容做一些处理。
设置了SOCKS代理的情况下,创建Socket时,为其传入proxy,写代码时连接时还是以HTTP服务器为目标地址(实际上Socket肯定是与SOCKS代理服务器连);但是如果设置的是Http代理,创建的Socket是与Http代理服务器建立连接。
设置代理时,Http服务器的域名解析会被交给代理服务器执行。但是如果是设買了Http代理,会对Http代理服务器的域名使用 OkhttpClient 配置的dns解析代理服务器,Http服务器的域名解析被交给代理服务器解析。
Http代理也分成两种类型:普通代理与隧道代理,

  • 其中普通代理不需要额外的操作,扮演「中间人」的角色,在两端之间来回传递报文。这个"中间人’在收到客户端发送的请求报文时,需要正确的处理请求和连接状态,同时向服务器发送新的请求,在收到响应后,将响应结果包装成一个响应体返回给客户端。在普通代理的流程中,代理两端都是有可能察觉不到"中间人"的存在。
  • 但是隧道代理不再作为中间人,无法改写客户端的请求,而仅仅是在建立连接后,将客户端的请求,通过建立好的隧道,无脑的转发给终端服务器。隧道代理需要发起Http CONNECT请求,这种请求方式没有请求体,仅供代理服务器使用,并不会传递给终端服务器。请求头 部分一旦结束,后面的所有数据,都被视为应该转发给终端服务器的数据,代理需要把他们无脑的直接转发,直到从客户端的TCP 读通道关闭。CONNECT 的响应报文,在代理服务器和终端服务器建立连接后,可以向客户端返回一个 200 connect established 的状态码,以此表示和终端服务器的连接,建立成功。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

okhttp源码分析 的相关文章

随机推荐

  • 使用pipeline加速Redis

    面试官 怎么快速删除10万个key 某厂面试题 prod环境 如何快速删除10万个key 带着思考 我们一来研究Redis pipeline why pipeline Redis客户端与server的请求 响应模型 前面的文章 Redis底
  • C#开发WinForm之DataGridView开发

    C 开发WinForm之DataGridView开发 原文 https blog csdn net achenyuan article details 84632751 文章目录 C 开发WinForm之DataGridView开发 基本的
  • 在Linux中使用selenium(环境部署)

    在Linux中使用selenium 环境部署 1 安装chrome 用下面的命令安装Google Chrome yum install https dl google com linux direct google chrome stabl
  • 【单片机笔记】K型热电偶单运放放大,单片机ADC采集电路

    以下内容来自百科 K型热电偶作为一种温度传感器 K型热电偶通常和显示仪表 记录仪表和电子调节器配套使用 K型热电偶可以直接测量各种生产中从0 到1300 范围的液体蒸汽和气体介质以及固体的表面温度 高清K型热电偶图片 K型热电偶是目前用量最
  • 【RuoYi-Vue-Plus】学习笔记 09 - 数据权限调用流程分析(参照 Mybatis Plus 数据权限插件)

    文章目录 前言 参考目录 代码分析 1 数据权限配置 MybatisPlusConfig 2 数据权限拦截器 PlusDataPermissionInterceptor 3 数据权限处理器 PlusDataPermissionHandler
  • 20-Docker-常用命令详解-docker attach

    常用命令详解 docker attach 前言 docker attach 语法格式 options 说明 使用示例 进入容器 和docker exec 的区别 前言 本篇来学习docker attach命令 docker attach 作
  • 解决 -bash: ifconfig: command not found 实测有效

    1 查看是否已经联网 输入ip addr 或 ip a 发现ens33 中不包含IP内容 2 修改配置步骤 1 输入 cd etc sysconfig network scripts 回车 找到ifcfg ens33 注意 cd后面有空格
  • 坐标系和投影 知识的内容介绍

    回想一下 接触遥感专业也有几个年头了 而现在越来越偏离遥感了 突然想着把自己脑中的遥感知识整理出来 首先想到的便是坐标系和投影 我想这个东西困扰着80 以上的测绘 遥感和GIS领域的从业人员吧 群里经常有人问 我自己曾经也很迷糊 什么大地坐
  • Java测试题_1

    单选题 1 class Base Base System out print Base public class Alpha extends Base public static void main String args new Alph
  • JavaScript 获取 input 输入框内容的方法

    在 JavaScript 中获取 input 输入框内容的方法有以下几种 使用 document getElementById 方法获取输入框元素 再通过 value 属性获取输入框内容 示例代码如下 var input document
  • 吴恩达老师深度学习视频课笔记:逻辑回归公式推导及C++实现

    逻辑回归 Logistic Regression 是一个二分分类算法 逻辑回归的目标是最小化其预测与训练数据之间的误差 为了训练逻辑回归模型中的参数w和b 需要定义一个成本函数 cost function 成本函数 cost functio
  • Golang匿名结构体的使用

    一 结构体基础 结构体 struct 将多个不同类型的字段集中组成一种复合类型 按声明时的字段顺序初始化 type user struct name string age byte user user Tom 2 定义匿名结构体时没有 ty
  • 编程TRICK

    一 20200729 1 image和annots的数据类型要统一 如image annots设为np float32 在具体函数中 输入和输出的数据类型要保持一致 中间具体应用再改变数据类型 2 仿射变换可以用PIL的transform
  • Flowable入门系列文章113 - 进程实例 02

    1 激活或暂停流程实例 PUT运行时 process instances processInstanceId 表1 激活或暂停流程实例 URL参数 参数 需要 值 描述 processInstanceId 是 串 激活 挂起的流程实例的ID
  • 4.bio、request和request_queue

    通常一个bio对应上层传递给块层的I O请求 每个bio结构体实例及其包含的bvec iter bio vec结构体实例描述了该I O请求的开始扇区 数据方向 读还是写 数据放入的页 其定义如代码清单13 3所示 struct bvec i
  • Redis 分布式锁实现

    原文地址 http blog csdn net zhu tianwei article details 44927331 Redis是一个key value存储系统 和Memcached类似 但是解决了断电后数据完全丢失的情况 而且她支持更
  • 英文版线性代数笔记1

    是一个给自己期中复习做的笔记 第二课 关于矩阵 mxn的矩阵 是m行n列 先行后列 如A2 1 就是3 单位矩阵 关于向量 向量 有序有限的一列数字 定义 了解列向量 横向量 零向量 向量可以是一组解 关于单位向量 只有一个1 其他都是0
  • VC文件扩展名一览表

    VC文件扩展名一览表 2003 12 7 23 05 34 阅读589次 APS 存放二进制资源的中间文件 VC把当前资源文件转换成二进制格式 并存放在APS文件中 以加快资源装载速度 BMP 位图资源文件 BSC 浏览信息文件 由浏览信息
  • 西门子S7通信

    1 总体结构 1 1 西门子通信场景 在讨论更多的技术细节之前 首先我想简单介绍一下西门子通信场景的基本情况 当我谈到 S7协议 时 我指的是以太网S7通信 主要用于将PLC连接到 I PC站 PG PC PLC通信 不要将此与西门子设备使
  • okhttp源码分析

    Okhttp介绍 由square公司贡献的一个处理网络请求的开源项目 是目前Android使用最广泛的网络框架 从Android4 4开始HttpURLConnection的底层实现采用的是okhttp 项目地址 https github