Android框架源码分析-浅析OkHttp3

news2024/9/23 23:22:15

浅析OkHttp3

这篇文章主要用来回顾Okhttp3源码中,同步异步请求的区别、拦截器的责任链模式、连接池管理以及探讨socket通信到底在哪里实现。

列出的代码可能删掉了非核心部分的展示,如果有异议请查看源码

连接池涉及知识:可能根据 IP 地址集合,尝试进行子网合并/构造超网,这部分请大家回顾计算机网络的构造超网进行复习。

1.基本使用

使用OkHttp来发送网络请求,一般我们会通过同步请求和异步请求两种方式:

OkHttpClient okhttpClient = new OkHttpClient();

//拼接请求
Request request = new Request.Builder()
    .url("http://xxx.xxx.xxx")
    .get()//发送get请求
    //.post(requestBody)//发送post请求
    .build();

//创建请求实体Call
Call call = okHttpClient.newCall(request);

//发起同步请求,阻塞等待返回的response
Response response = call.execute();

//发送异步请求,通过接口回调来获取数据
call.enqueue(new Callback(){
    @Override
    public void onFailure(Call call,IOException e){
        //e.getMessage()
    }
    @Override
    public void onResponse(Call call,Response response) throws IOException{
        //处理返回的响应体response
        //response.body()
    }
});

同步请求,将会阻塞等待返回的response。异步请求,将会通过接口回调来获得返回的数据。这也是最经典的获取返回数据的两个方式:同步return返回,以及异步回调返回。

首先我们关注到请求实体,请求实体实现了 Call 接口,Call 接口中定义了一些请求实体会用到的方法:

public interface Call extends Cloneable {
    //获取请求实体中的Request对象
    Request request();
	//发起同步请求
    Response execute() throws IOException;
	//发起异步请求
    void enqueue(Callback var1);
	//取消请求
    void cancel();
	//是否被执行了(子类实现用到了synchronized,我们认为标志是临界区,不希望在多线程场景中产生歧义)
    boolean isExecuted();
	//是否被取消了(子类的调用链中,也用到了synchronized)
    boolean isCanceled();
	//返回一个AsyncTimeout对象实例,它的timedOut()方法可以调用到transmitter的cancel()方法
    //简言:可以用于超时取消
    Timeout timeout();
	//这里是浅拷贝
    Call clone();
	//让外界能够以此规范来产生出一个Call请求实体
    public interface Factory {
        Call newCall(Request var1);
    }
}

在它的实现类RealCall和AsyncCall中,isExecuted()和 isCanceled()都被加以了 synchronized 修饰,因为 executed 和 canceled 在多线程场景下,可能会被多个线程使用,认为是临界区数据,使用 synchronized 修饰是希望这两个变量对所有线程保持可见性以及互斥性。

我们接下来来看一下 Call 接口的具体实现类,并对比他们的区别

2. RealCall 和 AsyncCall

我们发现,虽然异步任务也命名为Call结尾,但它并没有实现 Call 接口,而是实现了 Runnable 接口。这里的设计思想涉及到【非静态内部类】的使用,它可以调用外部类的方法。我们来看一下哪些方法是RealCall和AsyncCall所共用的。

先来看一下两个类的结构:

final class RealCall implements Call {
    //这部分代码很多,而且不论是同步还是异步,处理逻辑是一模一样的!
    Response getResponseWithInterceptorChain(){
        //...
    }
    public Response execute(){
        //外部类RealCall可以调用它
        this.getResponseWithInterceptorChain();
    }
    
    final class AsyncCall extends NamedRunnable{
        protected void execute(){
            //内部类AsyncCall也可以调用它
            RealCall.this.getResponseWithInterceptorChain();
        }
    }
}

我们发现有一个方法 getResponseWithInterceptorChain() 被他们共用了。

我们需要认识到,AsyncCall 实现了 Runnable 接口,所以它执行的逻辑需要放在 run() 方法中执行,我们可以把代码解耦,将对线程的命名和请求执行逻辑分离开来:

  • 具体执行逻辑写在 execute() 方法中
  • run() 除了调用 execute() 还要对线程进行命名

AsyncCall 继承的 NamedRunnable 的 run() 方法就做了上述的逻辑解耦,让代码更清晰:

public abstract class NamedRunnable implements Runnable {
    //...
    public final void run() {
       	
        String oldName = Thread.currentThread().getName();
        Thread.currentThread().setName(this.name);

        try {
            this.execute();
        } finally {
            Thread.currentThread().setName(oldName);
        }

    }
	//可以理解为这是一个模板方法设计模式,将具体的execute()逻辑让子类实现,自己只负责对线程命名
    protected abstract void execute();
}

我们注意到这里的命名很有意思,先把线程旧的名字保留,在执行 execute() 期间,线程的名字是 this.name,执行结束后,线程的名字又变成了原先的名字。

不难理解,在线程复用的场景里,我们不希望对借用的线程产生影响,“原物归还”是一个好习惯,所以在试用期间,我们改了名字,但是当前线程执行完这个任务后,名字需要改回去。避免别人无法通过原先的名字找到它

他们还有什么区别呢?从设计他们的初衷来看,希望 RealCall 用来执行同步请求,AsyncCall 要用来执行异步请求,所以 AsyncCall 中自然就有提交任务到线程池的步骤,而 RealCall 只有同步执行的步骤。

我们先来看一下 RealCall 的 execute() 方法,执行同步任务。

3. Response response = call.execute() 执行同步任务

由 okhttpclient.newCall() 创建出来的,是RealCall对象,如果你想要的执行同步任务,调用它的 execute() 方法即可。

我们来看一下 RealCall 提交同步任务的步骤:

final class RealCall implements Call{
    public Response execute() throws IOException{
        //临界区标志修改,需要加锁
        synchronized(this){
            this.executed = true;
        }
        
        this.transmitter.timeoutEnter();
        this.transmitter.callStart();
        Response var1;
        try {
            //1. 将同步请求记录在runningSyncCalls集合中,外界可以通过 runningSyncCalls 获取到当前有多少同步任务被提交
            this.client.dispatcher().executed(this);
            //2. 直接在当前线程环境下,执行网络请求
            var1 = this.getResponseWithInterceptorChain();
        } finally {
            //3. 执行完毕后,将同步请求从记录中移除
            this.client.dispatcher().finished(this);
        }
        return var1;
    }
}

在通过 getResponseWithInterceptorChain() 进行拦截器处理和网络请求之前,记录在 runningSyncCalls 队列中,执行完成之后,不论是否有异常,都将它从该记录队列中移除。我们只需要注意到,getReponseWithInterceptorChian() 在 RealCall.execute() 中,是在当前调用者所在的线程环境下发起的,所以是同步请求,具体 getResponseWithInterceptorChain() 到底做了什么我们后面分析。

4. call.enqueue( callback ) 提交异步任务

由 okhttpclient.newCall() 创建出来的,是RealCall对象,如果你想要的执行同步任务,调用它的 execute() 方法即可,如果你想要的执行异步任务,调用它的 enqueue() 方法,RealCall 内部会帮你把任务转变为 AsyncCall,提交为异步任务:

Call realCall = okhttpClient.newCall(request);//将request封装在RealCall对象中
realCall.enqueue(new Callback(){...});//发起异步请求

为了解耦,我们将异步任务的执行逻辑写在了 AsyncCall 这个内部类中,而 request 仍然封装在 RealCall 这个外部类中。AsyncCall 需要 request 的时候直接在外部类拿就好,代码很简洁,结构很清晰。我们只需要让 AsyncCall 来执行异步任务请求的逻辑就好了:

//RealCall.java
public void enqueue(Callback callback) {
    //修改临界区资源需要加锁
    synchronized(this) {
        this.executed = true;
    }
    this.transmitter.callStart();
    //避免破坏RealCall的结构,新加的Callback回调对象,写在了 AsyncCall 里面,让AsyncCall去管理
    AsyncCall asyncCall = new RealCall.AsyncCall(callback);
    this.client.dispatcher().enqueue(asyncCall);
}

我们知道框架总是不断更新的,在更新的过程中,需要增加功能,要符合对拓展开放,对修改关闭的设计原则。我猜测 OkHttp 是后来添加的异步任务请求的功能,但是原先的 RealCall 已经实现好了,而异步请求 AsyncCall 的逻辑可以复用很多 RealCall 的逻辑,所以将 AsyncCall 设计为了内部类,和外部类共享成员变量!异步需要的回调接口,也不直接设计为 RealCall 的成员变量,因为要避免对原有代码的破坏,所以写在了 AsyncCall 中!

RealCall 创建好一个 AsyncCall 实例后,就交给调度器去发起异步请求,我们先要注意到 AsyncCall 实现了 Runnable,然后我们看一下 dispatcher.enqueue() 做了什么:

//Dispatcher.java
void enqueue(AsyncCall call) {
    synchronized(this) {
        //1. 将AsyncCall 放入异步请求准备队列中
        this.readyAsyncCalls.add(call);
    }
    //2. 尝试将准备队列中的AsyncCall拿去执行
    this.promoteAndExecute();
}

Dispatcher 首先将 AsyncCall 放入异步请求队列 readyAsyncCalls 中,然后看看是否满足异步请求条件,如果满足就可以尝试发起异步请求!我们来看一下 promoteAndExecute() 中做了什么:

//Dispatcher.java
private boolean promoteAndExecute() {
    //1. 这个集合存了从 readyAsyncCalls 准备队列中取出来可以执行的 AsyncCall 异步任务
    List<AsyncCall> executableCalls = new ArrayList();
    boolean isRunning;
    AsyncCall asyncCall;
    //需要注意,runningAsyncCalls类似的这些队列也是临界区资源,也需要上锁使用
    synchronized(this) {
        Iterator i = this.readyAsyncCalls.iterator();
        while(true) {
            if (i.hasNext()) {
                asyncCall = (AsyncCall)i.next();
                //2. OkHttp3默认并发数为64,所以 runningAsyncCalls 当前可以执行异步任务的队列长度设置为 64
                if (this.runningAsyncCalls.size() < this.maxRequests) {
                    //3. 只要还在并发数要求内,就把它取出来,准备执行!
                    //放到 executableCalls 中,用于发起执行
                    executableCalls.add(asyncCall);
                    //4.记录到 runningAsyncCalls 中,用于记录!
                    this.runningAsyncCalls.add(asyncCall);
                }
            }
            //当前是否有异步请求被取出,如果有,认为okhttp为运行状态
            isRunning = this.runningCallsCount() > 0;
            break;
        }
    }

    int i = 0;
	//不涉及临界区资源操作的代码,放到临界区之外来执行,不占用锁资源!
    for(int size = executableCalls.size(); i < size; ++i) {
        asyncCall = (AsyncCall)executableCalls.get(i);
        //由此发起AsyncCall的异步任务!!!
        asyncCall.executeOn(this.executorService());
    }

    return isRunning;
}

想要执行异步任务,需要满足并发数小于64。并发数由 runningAsyncCalls 队列提供,它记录了从 readyAsyncCalls 队列中取出来被执行的任务,所以这条“记录”是个临界区资源,使用需要上锁。

其次,临界区代码不应该太多,执行时间尽量要短,所以synchronized修饰的代码块中,只有让 AsyncCall 在集合中转移,并没有进行实际的执行工作。执行工作放到了同步代码块之后执行!我们注意发起异步任务的代码是:

asyncCall.executeOn(this.executorService());

看变量名字,我们可以猜到,Dispatcher将线程池的引用交给了AsyncCall,希望 AsyncCall 的异步任务在提供的线程池中执行。具体如何执行,让 asyncCall.executeOn() 方法来执行。我们回到 AsyncCall 对象中,来看看是如何发起异步任务的:

//RealCall.AsyncCall
void executeOn(ExecutorService executorService) {
    assert !Thread.holdsLock(RealCall.this.client.dispatcher());
	//传入了一个线程池对象
    boolean success = false;

    try {
        //AsyncCall实现了Runnable接口,其run()方法调用了AsyncCall.execute()方法,即具体进行网络请求的逻辑
        executorService.execute(this);
        success = true;
    } catch (RejectedExecutionException var8) {
        //发生错误的回调
        this.responseCallback.onFailure(RealCall.this, ioException);
    } finally {
        if (!success) {
            //失败的后续处理
            RealCall.this.client.dispatcher().finished(this);
        }
    }
}

我们记得,AsyncCall实现了Runnable接口,其run()方法调用了AsyncCall.execute()方法,即具体进行网络请求的逻辑。设计者通过 executorService.execute(this) 将 runnable 实例提交给线程池去执行!

当线程池调度到当前AsyncCall的时候,执行其execute()方法。和 RealCall 一样,都叫做 execute() 方法,区别是 AsyncCall 的 execute() 方法在内部的线程池中执行!而 RealCall 的 execute() 方法则是在调用者的线程中同步执行。

我们来看 AsyncCall 的 execute() 方法

protected void execute() {
    try {
        //发起网络请求,阻塞等待response处理并返回
        Response response = RealCall.this.getResponseWithInterceptorChain();
        //将response回调出去
        this.responseCallback.onResponse(RealCall.this, response);
    } catch (IOException var8) {
		//发生异常的回调
        this.responseCallback.onFailure(RealCall.this, var8);

    } catch (Throwable var9) {
        //如果发生了异常,将这个异步任务取消
        RealCall.this.cancel();
    } finally {
        //执行结束后,将asyncCall从runningAsyncCalls记录队列中剔除,对外表示并发数/任务数 -1
        RealCall.this.client.dispatcher().finished(this);
    }
}

AsyncCall 的 execute() 方法执行在内部的线程池中!首先通过 getResponseWithInterceptorChain() 发起网络请求,并获得Response响应,通过callback将response回调出去。任务如果出错,调用 call.cancel() 来告知上层任务失败并取消,最后将 asyncCall从runningAsyncCalls记录队列中剔除,对外表示当前并发数/任务数减少了。

和 RealCall 中的 runningSyncCalls 的设计一样,是临界区资源,其用途之一,就是记录当前任务数。至于为什么不仅用一个变量来记录我还没深入探究,我猜测应该这个队列还有其他用途,大家可以参考其他博客分享。

探讨完 RealCall 和 AsyncCall 发起同步或者异步任务的逻辑之后,我们重点关注一下 getResponseWithInterceptorChain() 到底是如何发起网络请求的:

5. getResponseWithInterceptorChain() 真正发起socket通信

代码和逻辑比较多,这里我先说一下大概逻辑:

  1. 首先把用户设计的拦截器,以及OkHttp内部实现的拦截器进行一个汇总
  2. 通过责任链的设计模式,逐个对 request 进行处理
  3. 其中,有两个拦截器比较重要:
    1. ConnectInterceptor 拦截器用于维护socket连接池
    2. CallServerInterceptor 拦截器用于发送处理后的request请求,并接收返回的response。

责任链的逻辑我画了一张图,大概是这样子的:

请添加图片描述

我来解释一下:过程中,所有 RealInterceptorChain实例都引用着相同的数据,包括 interceptors 集合,call实例,OkHttpClient中设置的超时时间等。唯一变化的是 index。chain.proceed() 方法通过 index 来判断当前轮到拦截器集合中的哪一个来执行 intercept(next) 方法。这里的next是下一个chain实例,注意它的 index+1 。也就是下个 chain实例将会调用下标为 index+1 的拦截器去执行相关操作。

我们回到代码:

//RealCall.java
Response getResponseWithInterceptorChain() throws IOException {
    //所有拦截器的集合
    List<Intetceptor> interceptors = new ArrayList<>();
    //RealInterceptorChain责任链的主导者,只需要注意到传入了拦截器集合,以及当前目标拦截器的下标,以及一些基本设置。
    Interceptor.Chain chain = new RealInterceptorChain(interceptors,transmitter,null,0,request,call.this,client.connectTimeoutMillis(),client.readTimeoutMillis(),client.writeTimeoutMillis());
    //发起调用,chain将会逐个地调用拦截器,进行request的处理
    Response response = chain.proceed(request);
}

getResponseWithInterceptorChain() 是整个责任链调度的起点,它整合了所有拦截器,放在 interceptors 集合中。并初始化了一个 index=0 的 RealInterceptorChain。chain将会通过 proceed() 来调用当前拦截器的intercept()方法:

//RealInterceptorChain.java
public Response proceed(Request,Transmitter transmitter,Exchange exchange){
    //...
    //新的一个chain,注意index为 index+1,说明下一个调用的拦截器的下标相对当前的,是+1的
    RealInterceptorChain next = new RealInterceptorChain(interceptors, transmitter, exchange,index + 1, request, call, connectTimeout, readTimeout, writeTimeout);
    //获得当前拦截器
    Interceptor interceptor = interceptors.get(index);
    //调用当前拦截器的intercept() 方法
    Response response = interceptor.intercept(next);
}

看到拦截器中责任链调用部分的代码:

//ConnectInterceptor.java
@Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    //拦截器对request做一些处理,然后再交给下一层的chain
    return realChain.proceed(request, transmitter, exchange);
}

intercept()传入的chain的index是当前拦截器index+1的值。拦截器先对request做一些自己的处理,然后让index=index+1的chain去继续调用下一个拦截器。

我们发现,chain不仅仅用来寻找当前是哪个拦截器要执行,还保存了被之前拦截器处理过的 request对象,以及一些新的变量,例如ConnectionInterceptor拦截器还会创建一个 exchange 交给 chain来保管。

所以 chain 的主要任务就是数据托管,按顺序让interceptor来处理数据。

等到最后一个 interceptor 处理完request之后,就会请求网络,得到 response,再通过逐层的 return 返回,从 getResponseWithInterceptorChain() 中返回!

我们理解了 RealInterceptorChain 的作用,以及 Interceptors 被责任链式地调用过程。我们重点关注一下 Okhttp 对连接池的优化,以及对 socket 请求发起的执行逻辑:

6. RealConnectionPool 连接池

首先我们来关注一下 Socket 连接池。我们知道,socket 是基于 TCP 协议的网络请求工具,建立连接和释放连接,分别需要三次握手和四次挥手,这是比较耗时的。通过维持 socekt 的连接,我们可以增大通信时间的占比,从而让建立连接的时间忽略不计,达到性能的优化。

RealConnectionPool 维护了 socket 连接池。Exchange中的ExchangeFinder维护了RealConnectionPool连接池,RealConnection对象中就有socket实例。

我们来看一下 ConnectionInterceptor是如何获取socekt连接的:

作为拦截器,ConnectionInterceptor 在对 reqeuest 进行处理的同时,获取了一个Exchange对象:

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

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

    @Override 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,可以通过这个对象操作socekt
        Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);
        return realChain.proceed(request, transmitter, exchange);
    }
}

通过 transmitter.newExchange() 来获取一个exchange:

  1. 通过ExchangeFinder获取一个 ExchangeCodec
  2. 将这个ExchangeCodec连同其他信息一同包装在 Exchange 实例中
//Transmitter.java
Exchange newExchange(Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    //通过ExchangeFinder获取到一个 ExchangeCodec
    ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
    Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);
    return result;
}

我们注意到 ExchangeFinder 的成员变量中有 RealConnectionPool,明白了,连接池的管理是 ExchangeFinder 来做的,它维护着连接池,如果外界需要使用连接,它将可用的连接通过 ExchangeCodec 对象发挥出去,提供外界使用。而具体的连接逻辑等,都写在内部。对外人来说,具体实现就是黑盒,外界只要知道,拿到了 exchange,就说明可以通过这个叫做“exchange”的工具去进行网络请求。

final class ExchangeFinder {
    private final RealConnectionPool connectionPool;

    //1. 从 chain 中获取用户设置的数据
    public ExchangeCodec find(
        OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
        //从 chain 中获取到用户的设置
        int connectTimeout = chain.connectTimeoutMillis();
        int readTimeout = chain.readTimeoutMillis();
        int writeTimeout = chain.writeTimeoutMillis();
        int pingIntervalMillis = client.pingIntervalMillis();
        boolean connectionRetryEnabled = client.retryOnConnectionFailure();
        //2. 通过这些设置去寻找合适的连接
        try {
            RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
            //如果找到了,包装在ExchangeCodec中,返回出去
            return resultConnection.newCodec(client, chain);
        } catch (RouteException e) {
            trackFailure();
            throw e;
        } catch (IOException e) {
            trackFailure();
            throw new RouteException(e);
        }
    }
    
	//2.通过用户的设置,去寻找合适的连接
    private RealConnection findHealthyConnection(...) throws IOException {
        while (true) {
            RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,pingIntervalMillis, connectionRetryEnabled);
			//将合适的连接交出去
            return candidate;
        }
    }
    
}

可以看到,ExchangeFinder通过逐层的检验,最后需要通过 findConnection,将RealConnection 以 ExchangeCodec的方式返回出去,再由transmitter包装进 Exchange 对象返回给用户使用。findConnection() 是最核心的获取连接的逻辑,代码很长,我们来看看核心。

先来看前半段:

  1. 首先尝试获取 transmitter 中当前使用的已连接的 connection
  2. 然后尝试从realConnectionPool 连接池中,取一个已连接的 connection
  3. 如果前两步成功取到了一个已连接的connection,就可以返回出去复用了!
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    boolean foundPooledConnection = false;
    RealConnection result = null;
    Route selectedRoute = null;
    RealConnection releasedConnection;
    Socket toClose;
    synchronized (connectionPool) {
        //1. 先尝试获取 transmitter 中已连接的 connection
        if (transmitter.connection != null) {
            // We had an already-allocated connection and it's good.
            result = transmitter.connection;
            releasedConnection = null;
        }
		//如果没有已连接的connection
        if (result == null) {
            //2. 尝试从连接池中,获取一个已连接的 connection,由于传入的router路由集合是空,所以只有可能获取到那些http2条件下可以多路复用的链接!
            if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
                result = transmitter.connection;
            }
        }
    }
    //如果我们已经获取到了可复用的 connection,就直接返回,交给上层使用
    if (result != null) {
        return result;
    }
}

再来看后半段:

private RealConnection findConnection(){
    //如果已经取到了可复用的已连接的 connection,上面就返回出去了
    //到这里说明没有可用的链接,我们就需要开始建立连接了:
    // 如果我们需要路由选择,就获取一个(阻塞从routeSelector中获取)
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
        newRouteSelection = true;
        routeSelection = routeSelector.next();
    }
	
    List<Route> routes = null;
    synchronized (connectionPool) {
		//如果需要路由选择,
        if (newRouteSelection) {
            //1. 现在我们已经有了一组IP地址,请再次尝试从池中获取连接。由于连接合并,这可能匹配。如果连接池中的 connection符合这个路由匹配,就可以使用!
            routes = routeSelection.getAll();
            if (connectionPool.transmitterAcquirePooledConnection(
                address, transmitter, routes, false)) {
                foundPooledConnection = true;
                result = transmitter.connection;
            }
        }
		//如果连接池中没有获取到可以路由合并(超网聚合)的连接connection
        if (!foundPooledConnection) {
            if (selectedRoute == null) {
                selectedRoute = routeSelection.next();
            }
			//2. 那么就主动创建一个新的链接,引用交给result
            result = new RealConnection(connectionPool, selectedRoute);
            connectingConnection = result;
        }
    }
	
    //如果从连接池中获取到了可以路由合并的连接connection
    if (foundPooledConnection) {
        eventListener.connectionAcquired(call, result);
        //将这个connection返回出去
        return result;
    }

    //到这里,说明连接池中无论如何也获取不到可以用的 connection 了。
    //那么我们就需要让刚创建的 result,这个 RealConnection 去进行一个建立连接:TCP三次握手,会阻塞在这里,直到链接完成
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
                   connectionRetryEnabled, call, eventListener);
    //连接完成后,将路由添加到连接池中,以备查询和复用
    connectionPool.routeDatabase.connected(result.route());
	//到这里,连接池中一定存在一个可用的已连接的connection 了!
    Socket socket = null;
    synchronized (connectionPool) {
        connectingConnection = null;
       	//最后一次尝试连接合并,只有当我们尝试多个并发连接到同一主机时才会发生连接合并。
        if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
            //我们尝试链接合并失败了,我们要关闭我们创建的链接,并返回连接池
            result.noNewExchanges = true;
            socket = result.socket();
            result = transmitter.connection;
            //在这个情况下,我们有可能获得一个立即不健康的合并连接。在这种情况下,我们将 重试 刚才成功连接的路由。
            nextRouteToTry = selectedRoute;
        } else {
            //如果我们成功获取到了刚刚建立好的链接,将它放到复用池中!
            connectionPool.put(result);
            transmitter.acquireConnectionNoEvents(result);
        }
    }
    //上面的判断说明,如果这里socket不为空,说明刚创建的socket连接是一个不健康的合并连接,我们要把它给关了!
    closeQuietly(socket);
    eventListener.connectionAcquired(call, result);
    //返回连接池(如果成功了,连接池中就有刚建立好的链接,如果失败了,池中就没有,需要再次去尝试)
    return result;
}

简单来说 ExchangeFinder 管理着连接池,如果有可复用的连接 RealConnection,或者可以合并连接的RealConnection,都是可以的。如果没有就要通过 TCP+TLS 握手,进行连接的建立。这里的TCP握手是阻塞的。中间过程代码太繁杂,大家可以一路点进去,发现中间配置好了 InetSocketAdress,以及 connectTimeout,然后调用到 socket.connect(address,connectTimeout); 进行socekt连接!

连接建立完成后,提交到连接池的路由数据库中。然后再次尝试从连接池中获取可用的连接。

7. CallServerInterceptor

最后我们看到 CallServerInterceptor 这个拦截器对 request的处理,是真正地进行网络请求!利用的是之前 ConnectionInterceptor 中获取到的可用的socket 连接。

//CallServerInterceptor.java
public Response intercept(Chain chain){
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    //拿到了从ConnectionInterceptor中拿到的可用的连接,它被装在了Exchange实例中
    Exchange exchange = realChain.exchange();
    //被之前的拦截器处理好的,即将用来发送的request
    Request request = realChain.request();
    //将request的头写入outputstream
    exchange.writeRequestHeaders(request);
    //如果有请求体,写入bufferedRequestBody,然后再写入connection,然后发送
    request.body.writeTo(bufferedRequestBody);
    
    //获得返回内容,生成一个ResponseBuilder
    responseBuilder= exchange.readReponseHeaders();
    //获得请求体
    responseBody = exchange.openResponseBody(response);
    //将response拼装好后,返回
    return response;
}

发送请求头后,如果收到了100 Continue

  • 客户端应当继续发送请求。这个临时响应是用来通知客户端它的部分请求已经被服务器接收,且仍未被拒绝。客户端应当继续发送请求的剩余部分,或者如果请求已经完成,忽略这个响应.服务器必须在请求完成后向客户端发送一个最终响应。HTTP/1.1 可用

接收到响应后,如果收到了 100 Continue,同上,说明服务端可能还有数据要发过来,比如除了已经发送的header以外,还要发送body。

如果收到了 code = 101 Switching Protocal 协议切换

  • 服务器已经理解了客户端的请求,并将通过Upgrade消息头通知客户端采用不同的协议来完成这个请求。在发送完这个响应最后的空行后,服务器将会切换到 在Upgrade消息头中定义的那些协议。只有在切换新的协议更有好处的时候才应该采取类似措施。例如:切换到新的HTTP版本比旧版本更有优势,或者切换到一个实时且同步的协议以传送利用此类特性的资源。HTTP/1.1 可用。

如果收到了code=204或者code=205:

  • 204 No Content: 状态码204表示请求已经执行成功,但没有内容
  • 205 Reset Content 表示服务器成功地处理了客户端的请求,要求客户端重置它发送请求时的文档视图。这个响应码跟 204 No Content 类似,也不返回任何内容

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/351139.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

iis7.5应用程序池的启动模式设置

最近发现公司的网站第一次登录时比较慢&#xff0c;甚至有超时的时候&#xff0c;当我检查应用程序池(IIS 7.5)时&#xff0c;应用程序池正常启动&#xff0c;但有时候处于停止状态&#xff0c;停止原因未知。所以必须第一时间重新启动它&#xff0c;以保证网站能被正常访问。于…

kubeadm Dashboard harbor

主机名IP地址安装组件master01192.168.186.10docker、kubeadm、kubelet、kubectl、flannelnode01192.168.186.20docker、kubeadm、kubelet、kubectl、flannelnode02192.168.186.30docker、kubeadm、kubelet、kubectl、flannelharbor192.168.186.40docker、docker-compose、harb…

python语言基础(最详细版)

文章目录一、程序的格式框架缩进1、定义2、这里就简单的举几个例子注释二、语法元素的名称三、数据类型四、数值运算符五、关系运算六、逻辑运算七、运算符的结合性八、字符串一、程序的格式框架 缩进 1、定义 &#xff08;1&#xff09;python中通常用缩进来表示代码包含和…

Python迭代器、生成器和装饰器

一、迭代器 1、迭代器简介 迭代操作是访问集合元素的一种方式&#xff0c;是 Python最强大的功能之一。 迭代器是用来迭代取值的工具&#xff0c;是一个可以记住遍历的位置的对象。 迭代器对象从集合的第一个元素开始访问&#xff0c;直到所有的元素被访问完结束。迭代器只能…

QT项目_RPC(进程间通讯)

QT项目_RPC(进程间通讯) 前言&#xff1a; 两个进程间通信、或是说两个应用程序之间通讯。实际情况是在QT开发的一个项目中&#xff0c;里面包含两个子程序&#xff0c;子程序有单独的界面和应用逻辑&#xff0c;这两个子程序跑起来之后需要一些数据的交互&#xff0c;例如&…

GEE学习笔记 八十四:【GEE之Python版教程十四】矢量数据(ee.feature)

上一节讲了几何图形Geometry&#xff0c;这一节讲矢量数据&#xff08;ee.feature&#xff09;&#xff0c;它的构成也就是几何图形以及属性字典。 1、API定义 首先看一下GEE的python版API&#xff08;Welcome to GEE-Python-API’s documentation! — GEE-Python-API 1.0 do…

【论文】智能隧道检测车的现状及改进策略

本文转载自《智慧城轨》2022年第11期 作者&#xff1a;黄丹樱1,韦强1,朱椰毅2,范骁1,林浩立1 单位&#xff1a;1 浙江师范大学工学院&#xff1b;2 浙江金温铁道开发有限公司 声明&#xff1a;本文仅用于学术分享&#xff0c;不做商业用途&#xff0c;如有侵权&#xff0c;联…

从实现到原理,聊聊Java中的SPI动态扩展

原创&#xff1a;微信公众号 码农参上&#xff0c;欢迎分享&#xff0c;转载请保留出处。 八股文背多了&#xff0c;相信大家都听说过一个词&#xff0c;SPI扩展。 有的面试官就很喜欢问这个问题&#xff0c;SpringBoot的自动装配是如何实现的&#xff1f; 基本上&#xff0c…

Redis第二讲

二、Redis02 2.1 发布和订阅 Redis 发布订阅 (pub/sub) 是一种消息通信模式&#xff1a;发送者 (pub) 发送消息&#xff0c;订阅者 (sub) 接收消息。 Redis 客户端可以订阅任意数量的频道。 发布订阅的实现 1、打开一个客户端订阅channel1 127.0.0.1:6379> subscribe ch…

红黑树的原理+实现

文章目录红黑树定义性质红黑树的插入动态效果演示代码测试红黑树红黑树 定义 红黑树是一个近似平衡的搜索树&#xff0c;关于近似平衡主要体现在最长路径小于最短路径的两倍&#xff08;我认为这是红黑树核心原则&#xff09;&#xff0c;为了达到这个原则&#xff0c;红黑树所…

LeetCode刷题--- 面试题 01.07. 旋转矩阵(原地旋转+翻转替旋转)

&#x1f48c; 所属专栏&#xff1a;【LeetCode题解&#xff08;持续更新中&#xff09;】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;…

【C++之容器篇】二叉搜索树的理论与使用

目录前言一、二叉搜索树的概念二、二叉搜素树的模拟实现&#xff08;增删查非递归实现&#xff09;1. 二叉搜素树的结点2. 二叉搜索树的实现&#xff08;1&#xff09;. 二叉搜索树的基本结构&#xff08;2&#xff09;构造函数&#xff08;3&#xff09;查找函数&#xff08;4…

1225057-68-0,Alkyne PEG4 TAMRA-5,四甲基罗丹明-四聚乙二醇-炔基TAMRA红色荧光染料连接剂

中英文别名&#xff1a;CAS号&#xff1a;1225057-68-0 | 英文名&#xff1a;5-TAMRA-PEG4-Alkyne |中文名&#xff1a;5-四甲基罗丹明-四聚乙二醇-炔基物理参数&#xff1a;CASNumber&#xff1a;1225057-68-0Molecular formula&#xff1a;C36H41N3O8Molecular weight&#x…

P16 激活函数与Loss 的梯度

参考&#xff1a;https://www.ngui.cc/el/507608.html?actiononClick这里面简单回顾一下PyTorch 里面的两个常用的梯度自动计算的APIautoGrad 和 Backward, 最后结合 softmax 简单介绍一下一下应用场景。目录&#xff1a;1 autoGrad2 Backward3 softmax一 autoGrad输入 x输出损…

buu [UTCTF2020]basic-crypto 1

题目描述&#xff1a; 01010101 01101000 00101101 01101111 01101000 00101100 00100000 01101100 01101111 01101111 01101011 01110011 00100000 01101100 01101001 01101011 01100101 00100000 01110111 01100101 00100000 01101000 01100001 01110110 01100101 00100000 0…

【Kubernetes】【七】命令式对象配置和声明式对象配置

命令式对象配置 命令式对象配置就是使用命令配合配置文件一起来操作kubernetes资源。 1&#xff09; 创建一个nginxpod.yaml&#xff0c;内容如下&#xff1a; apiVersion: v1 kind: Namespace metadata:name: dev---apiVersion: v1 kind: Pod metadata:name: nginxpodnames…

调用Windows安全中心实现登录验证

文章目录运行效果用到的运行库代码实现使用日志Win10 Flat风格XP风格总结运行效果 输入用户名和密码点击确定后获取到的信息&#xff1a; 用到的运行库 NuGet搜索安装即可 Kang.ExtendMethodKang.ExtendMethod.Log https://gitee.com/haozekang/kang Vanara.PInvoke https:…

安全算法 - 国密算法

国密即国家密码局认定的国产密码算法。主要有SM1&#xff0c;SM2&#xff0c;SM3&#xff0c;SM4&#xff0c;SM7, SM9。国密算法分类国家标准官方网站如下&#xff1a;http://openstd.samr.gov.cn/bzgk/gb/SM1 为对称加密。其加密强度与AES相当。该算法不公开&#xff0c;调用…

Nacos——配置管理基础应用

目录 一、快速入门 1.1 发布配置 1.2 nacos client远程获取配置 1.2.1 导入坐标 1.2.2 程序代码 二、Nacos配置管理基础应用 2.1 Nacos配置管理模型 2.1.2 配置集(Data Id) 2.1.3 配置项 2.1.4 配置分组 (Group) 2.1.5 命名空间(Namespace) 2.1.6 最佳实践&#xff0…

Worok:专门针对亚洲实体的网络间谍组织

ESET 的研究人员发现了一个全新的攻击组织 Worok&#xff0c;该组织自动 2020 年就一直处于活跃状态。Worok 使用的工具集包括一个 C 编写的加载程序 CLRLoad、一个 PowerShell 编写的后门 PowHeartBeat 和一个 C# 编写的加载程序 PNGLoad&#xff0c;攻击者使用隐写术来提取隐…