阅读 625
从设计角度看OkHttp请求源码

从设计角度看OkHttp请求源码

OKHttp作为Square公司开发的网络请求框架,热度从发布至今稳占第一。网上有很多大佬对OkHttp进行源码分析,很多文章都特别有见解,但是一大篇源码属实对新人不太友好,这篇文章主要会从我们平时的设计角度来看OkHttp请求的源码。

整体思路

  1. 定义一个网络服务肯定需要客户端,服务端,服务请求,返回值,也就是Client,Server,Request,Response,暂不考虑细节,元素就确定下来了。
  2. Client -> Server 通过Call行为来实现Client沟通Server。
  3. Call:发送Request,接收Response,取消发送等功能。
interface Call{ 
    Response excute(); //传出去的是Request,返回的是Response。
    cancel(); //取消发送。
    ......    
  }
复制代码

初步想法大致就这样,接下来一个个去看细节,首先是Request。

Request

HTTP请求报文

探究Request之前,我们先看一下HTTP的请求报文格式:

可以看到,HTTP请求报文分为请求行,请求头,请求体三部分。我们继续看这三部分装载了什么信息:

  • 请求行: 包含了请求方法、URL地址、协议名称和版本。请求方法也就是我们熟知的GET/POST请求,一般来说GET请求发出资源"显示"请求,它是幂等的,因此一般只用在读取数据上;而POST请求通常需要提交指定数据,请求服务器进行处理,这个请求可能会创建新的数据或修改现有数据;当然还有HEAD、PUT、TRACE、CONNECT、OPTIONS、DELETE请求,在这里就不做多的叙述。请求行报文格式如下:

  • 请求头: 包含着若干个属性,格式为键: 值,例如Accept: text/plain,表示客户端只能接受纯文本格式的数据。其他更多属性参照en.wikipedia.org/wiki/List_o… ,请求头报文格式如下:


  • 请求体: 根据不同请求场景,请求体的形式有所不同。常见的有application/x-www-from-urlencoded(键值对),multipart/form-data(文件/键值对,多用于文件上传),raw(任意格式文本)等。请求体报文格式如下:

OkHttp3的Request

看完HTTP的请求报文后,我们接着回归主题,那么OkHttp的Request请求应该怎么处理呢?显而易见,既然要充当Request,里面肯定是含有url,method,head,body等参数的,如果让你设计一个Request类,你会怎么设计?

class Request{ 
    String url;
    string method;
    string head;
    string body;
    ......  
  }
复制代码

没有任何问题,但是我们发现平时使用Request的姿势是:
Request request = new Request.Buidler().url("").addHeader("").build();
你应该知道我想说什么,没错,Request是使用建造者模式创建的,通过建造者模式我们不需要了解太多细节就能够创建一个Request对象,解耦了创建过程和对象本身,所以我们可以得到一个伪代码:

class Request{ 
    String url;
    string method;
    string head;
    string body;

    Request(Builder builder){
    	this.url = builder.url;
        this.method = builder.method;
        this.head = builder.head;
        this.body = builder.body;
    }
    
    static class Builder{
    	String url;
        String method;
        String head;
        String body;
    	Builder url(String url){
            this.url = url;
            return this;
        }
        
        Builder method(String method){
            this.method = method;
            return this;
        }
        ......
        Request builder(){
            return new Request(this);
        }
    }
    ......  
  }
复制代码

OkHttp的Request源码实现跟上面伪代码差不多,只不过更加细节,通过建造者模式设计Request类,我们就可以实现链式创建Request对象。

Call

Request对象创建之后,我们接下来就是考虑怎么将Request请求发送出去,之前讲到通过Call行为来发送Request请求,这里我们结合平时使用OkHttp来看:

Request request = new Request.Builder()
                .url("")
                .build();
                
OkHttpClient okHttpClient = new OkHttpClient();
Call call = okHttpClient.newCall(request);
Response response = call.execute();
复制代码

这里创建了一个okHttpClient对象,将之前创建的Request对象当作参数,调用了newCall()方法,定位到源码:


返回了一个由RealCall.newRealCall生成的对象,继续看RealCall的newRealCall()方法:


可以看到newRealCall()方法主要是创建了一个RealCall对象,并且用工厂方法创建了一个eventListener,最后返回了一个RealCall对象,接下来具体看一下RealCall对象是怎么实例化的:


我们可以看到RealCall的实例化需要三个参数,client,originalRequest,forWebSocket,这三个参数分别是OkHttpClient的一个实例,Request的一个实例,也就是之前传递进来的Request对象,最后一个参数判断是否是一个WebSocket请求,并且设置了一个失败重试或者重定向拦截器。拦截器这里我们先略过,它的重要性值得新开一篇文章来讲解,我们可以把拦截器理解成一个可以对action进行拦截并且能够修改的东西。于是到这里前提条件已经准备好了,接下来我们看Call行为的execute()方法。

从上面的代码不难看出execute()方法实际上是由RealCall的一个实例对象执行的,由于RealCall类实现了Call接口,我们先大致看一下Call接口的内容:

public interface Call extends Cloneable {
    Request request();
    Response execute() throws IOException;
    void enqueue(Callback responseCallback);
    void cancel();
    boolean isExecuted();
    boolean isCanceled();
    Call clone();
    
    interface Factory {
        Call newCall(Request request);
    }
  ......
}
复制代码

可以看到跟我们一开始预测的一样(不要脸,其实提前知道是这个结果),有execute()方法负责发送请求并且获得返回值,cancel()方法取消发送,另外还有isExecuted()、isCanceled()方法来标记状态等等,其实除了之前提到的execute()方法外,还有一个需要额外关注的方法,就是enqueue()方法,接下来我们看这两个方法的具体实现:


可以看到execute()方法里面通过getResponseWithInterceptorChain()方法获取到了Response数据,如果只能同步请求网络,这篇分析差不多快到终点了,但是我们更常用的场景是异步请求网络,所以这里先略过getResponseWithInterceptorChain()方法,因为就算是异步请求网络,最后也要走到获取Response返回值。

于是enqueue()方法闪亮登场,没错,enqueue()方法就是OkHttp异步请求网络的方式,下面会以异步请求分析走完整个流程,可以看到异步请求方法里面调用了client.dispatcher().enqueue()方法,Callback对象被封装成了AsyncCall对象作为参数传递下去,我们接着往下看:


到这里我们可以看到,dispatcher()方法其实就是返回了一个Dispatcher对象,然后执行Dispathcher对象中的enqueue()方法。大致意思就是如果正在运行的异步队列中的数量少于最大并发数并且少于主机最大请求数,将传过来的AsyncCall对象添加到队列中,并且调用executorService().execute()方法,不满足条件则将对象加入等待执行的异步队列中。继续看executorService().execute()方法是怎么被调用的:


看到这里接触java开发的同学应该熟悉了起来,哦,原来executorService()方法只是创建了一个默认的线程池。由于ThreadPoolExecutor类最终是实现ExecutorService接口的,而ExecutorService接口是继承Executor接口的,Executor接口中只含有execute()方法,因此executorService().execute()本质上就是调用了ThreadPoolExecutor类的execute方法。

看到这里差不多确定方向了,既然本质是调用了线程池,由于文章重心原因,我们不用关心线程池是怎么启动线程的,而是把细节放在线程中发生了什么,于是把重心放在之前当作参数的AsyncCall类中:

final class AsyncCall extends NamedRunnable{
   ......
}

public abstract class NamedRunnable implements Runnable {
   void run() {
   	try{execute();}
   }
   ......
   protected abstract void execute();
}

public interface Runnable{
   public abstract void run();
}
复制代码

可以看到Runnable接口中只有一个run()方法,而NameRunanble类实现了Runnable接口,NamedRunnable中的run()方法又去调用了execute()抽象方法,AsyncCall继承了NamedRunnable类,因此实际发生了什么可以定位到AsyncCall的execute()方法:


看到response = getResponseWithInterceptorChain()这一行!大声告诉我,之前同步请求是不是见过,没错,异步请求跟同步请求的区别只是异步请求利用了线程池去请求返回值。

补充:可以看到finally语句中调用了client.dispatcher().finished(this),细心的朋友可能会发现同步请求finally语句中也执行了finished()方法。之前提到Dispatcher的enqueue()方法中会根据是否满足最大并发数和最大请求数将请求添加到正在执行的异步队列或待执行的异步队列,而execute()方法则是直接添加到了同步队列中。而它们finished()方法的区别在于同步请求每次结束都会移除该队列元素,而异步请求移除该队列元素后会从待执行异步队列取一个元素添加上去,通过线程池开始新一轮的请求。

最后我们看看getResponseWithInterceptorChain()方法内部到底干了什么:


可以看到里面定义了一个拦截器的列表,往列表里添加了各种拦截器,将装载着各种拦截器的列表作为参数传入RealInterceptorChain构造方法进行处理,最后通过proceed()方法返回了Response数据。

到这里Response的返回值就被我们拿到了,拦截器底层实现主要利用到了责任链模式,通过责任链,将拦截任务分发到指定的拦截器。本着一篇文章一个重点的原则,这里关于线程池和拦截器的知识并没有详细深入,并不是代表它们不重要,我觉得线程池和拦截器的重要性值得新开一篇去记录它们,如果有小伙伴想要了解的,可以去网上阅读相关文档。

结语

这篇文章算是告一段落了,我觉得不管是写文章还是说接触到一个新框架,我们可以从自己的角度去考虑、设计整个流程,考虑完整个流程后,辅以源码,给人的感受一定会更加深刻、易懂。

文章分类
Android
文章标签