OkHttp是Android开发中使用非常普遍的一个网络请求库,它封装和实现了Http协议的相关功能。官方地址: https://github.com/square/okhttp
既然是对Http协议的实现,那就绕不开Http协议的原理与报文结构,Http报文结构可参考笔记: Http协议报文格式_丞恤猿的博客-CSDN博客_http协议请求报文的完整格式。很多网络库都是围绕某种网络协议,实现了网络协议的相关功能,并且提供对应接口,封装了内部各种细节,供使用者简化开发过程。Android从最初用HttpUrlConnection到现在普遍适用OkHttp、Retrofit,网络库会不断推陈出新,但作为其内核的Http网络协议则鲜有重大变化,所以要理解这些网络库的原理,首先要先基本了解对应的网络协议。
1.OkHttp内部在TCP层的连接是用Socket来实现的,内置socket连接池,能够高效复用TCP连接,减少了握手次数和请求延迟。
而且允许上层连接到同一个主机地址的Http请求共享Socket,减少了在TCP层对服务器的请求次数。
2.内置线程池,高效管理异步请求时的线程使用;
3.支持缓存,可减少不必要的网络请求;
4.支持gzip对传输数据进行压缩/解压缩。
5.提供了强大的API支持,能够进行各种自定义配置。
Request : 封装Http请求报文Response : 封装Http响应报文Call : 在OkHttp架构中,对应一次网络请求任务OkHttpClient : 在OkHttp架构中,承担客户端角色,通过它可进行网络请求的各种通用设置,例如超时时长、缓存策略、是否设置代理、各种拦截器等。(OkHttp内部在TCP层的连接是用Socket来实现的,内部会自动维护socket连接池,减少握手次数,减少了请求延迟。允许上层连接到同一个主机地址的所有Http请求共享Socket,减少了在TCP层对服务器的请求次数。)
1.创建OkHttpClient,设置各种网络请求参数,所有未手动设置的,在OkHttpClient构造时内部都会指定一个默认参数。2.创建Request对象并设置对应参数,实际上是在设置Http请求报文。3.用Request来创建对应的Call对象,一个Call对应一次网络请求任务4.调用Call的同步请求/异步请求方法,获取网络请求结果Response,该对象封装了Http响应报文。按照自己的需求对Response中的数据做解析。
1.OkHttp内部通过Dispatcher类(调度器)来支持Call的同步请求/异步请求1.1Call的同步请求:会在当前线程中执行网络请求并等待请求结果;1.2.Call的异步请求:会在额外的工作线程中执行网络请求并在回调接口中返回结果。这些额外的线程在OkHttp内部通过线程池管理和复用,默认支持的最大同时网络请求数是64.
2.OkHttp提供了一堆拦截器(Interceptor),针对Http协议中的各个步骤,调整Request和Response中的参数。这些拦截器是链式执行的,如下图所示。在发起网络请求时,由上往下执行,最先执行的是自定义的拦截器,这个过程会不断地在各个拦截器根据需要调整Request中的请求参数;在网络请求数据返回后,由下往上执行,最后执行的是自定义的拦截器,这个过程这个过程会不断地在各个拦截器根据需要调整Response中的数据。这样,开发者可调整某个步骤的拦截器逻辑来满足自己的需求。比方说,可以新增一个自定义拦截器,在网络请求最初填入一些通用的请求参数,在网路请求结果返回时把数据解析成自己需要的数据对象。
// //方式1.直接构建客户端对象OkHttpClient,所有的参数都使用默认值
// mHttpClient = new OkHttpClient();
//方式2.用构造器模式创建OkHttpClient实例
//首先设置好构造器的相关参数
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(5, TimeUnit.SECONDS)//连接超时,针对过程:建立TCP连接的过程(三次握手)。默认时长是10s
//读操作超时,这个值用于两个过程:1.TCP层连接后,Socket若超过该时长仍无数据可读,则超时;2.IO操作,从Source对象读数据的过程
.readTimeout(10, TimeUnit.SECONDS)
//写操作超时,针对过程:IO操作,用Sink对象写数据的过程
.writeTimeout(10, TimeUnit.SECONDS);
// //call超时,针对过程:从Call请求开始执行/入异步队列开始,到获取到返回数据生成ResponseBody的整个过程。默认没有超时时间。
// .callTimeout(60, TimeUnit.SECONDS)
// 非测试包, 设置为禁止代理抓包
if (!BuildConfig.DEBUG) {
builder.proxy(Proxy.NO_PROXY);
}
//构建客户端对象OkHttpClient
mHttpClient = builder.build();
OkHttpClient的一些参数:final @Nullable Proxy proxy; //代理final Listprotocols; //支持的协议,默认支持的协议为HTTP_1.1 和 HTTP_2 final Listinterceptors; //拦截器集合 final EventListener.Factory eventListenerFactory; //监听器,整个网络请求过程的监听器final @Nullable Cache cache; //缓存final boolean followRedirects; //是否允许重定向final boolean retryOnConnectionFailure; //连接失败是否重试final int callTimeout; //整个请求过程的超时,默认没有超时final int connectTimeout; //连接超时,默认10秒final int readTimeout; //读超时,默认10秒final int writeTimeout; //写超时,默认10秒
GET请求对应的请求报文不含数据体,其参数附在Url后面,格式Url?参数1=值1&参数2=值2……,存储在Http请求报文请求行的URL字段。
//构造Http 请求报文对象
Request request = new Request.Builder()
.url(url)
//指明是GET请求
.get()
// //设置请求头,只会有一个值,后面设置的值会覆盖前面的值
// .header("User-Agent", "OkHttp Headers.java")
// //添加请求头,可有多个值,后面设置的值不会覆盖前面的值
// .addHeader("Accept", "application/json; charset=utf-8")
// .addHeader("Accept", "application/xxxxxxxxx")
.build();
GET请求对应的请求报文包含数据体,数据体的对应类是RequestBody。发送不同数据时,数据体在创建时,要设置不同的数据类型。
// //方式1:自己构造json字符串并传入
// String json = "{\"tagExample\":\"valueExample\"}";
// //构造Http POST请求报文数据体对象
// RequestBody requestBody1 = RequestBody.create(MediaType.get("application/json; charset=utf-8"), json);
// //方式2:通过new FormBody()调用build方法,创建一个RequestBody,可以用add添加键值对
// RequestBody requestBody2 = new FormBody.Builder()
// .add("name","xxxx")
// .add("age","25")
// .build();
// //方式3:上传文件
// File file = new File(Environment.getExternalStorageDirectory(), "zhuangqilu.png");
// //通过RequestBody.create 创建requestBody对象,application/octet-stream 表示文件是任意二进制数据流
// RequestBody requestBody3 =RequestBody.create(MediaType.parse("application/octet-stream"), file);
//方式4:以多媒体表单形式上传多种类型的数据,此处是:一些参数+图片文件
File file = new File(AppUtils.getFileDirPicture(), "xxxx.png");
//通过new MultipartBody build() 创建requestBody对象,
RequestBody requestBody4 = new MultipartBody.Builder()
//一定要设置类型是表单。这里设置的值,最终会影响Http请求报文的Content-Type头部字段。
.setType(MultipartBody.FORM)
//添加数据
.addFormDataPart("username","xxx")
.addFormDataPart("age","25")
//addFormDataPart()方法的第一个参数就是类似于键值对的键,是供服务端使用的,第二个参数是文件的本地的名字,
// 第三个参数是RequestBody,里面包含了我们要上传的文件的MidiaType以及路径。
.addFormDataPart("image","xxxx.png",
RequestBody.create(MediaType.parse("image/png"),file))
// //也可以添加其它RequestBody做为表单的一部分
// .addPart(requestBody1)
.build();
//构造Http 请求报文对象
Request request = new Request.Builder()
.url(url)
//指明是POST请求,并设置请求数据体
.post(requestBody4)
.build();
try {
//执行网络请求,并获取Http响应报文对象
Response response = mHttpClient.newCall(request).execute();
if(response == null){
return;
}
//如果请求成功
if(response.code() == HttpURLConnection.HTTP_OK){
//获取Http响应报文数据体
ResponseBody responseBody = response.body();
//.....做自己的数据解析.....
}
} catch (IOException e) {
e.printStackTrace();
}
异步请求会将本次网络请求(Call)加入调度队列,在稍后可执行时去执行该网络请求,并在请求结果返回后执行设置的回调方法。如果当前满足执行条件的话,会立即开始执行本次网络请求(一般情况)。具体的流程和条件后面会分析。
//创建Call对象,一个Call可以理解为一次网络请求任务
Call call = mHttpClient.newCall(request);
//异步请求方式
//将本次网络请求加入调度队列,在稍后可执行时去执行该网络请求,并在请求结果返回后执行设置的回调方法
//如果当前满足执行条件的话,会立即开始执行本次网络请求(一般情况)
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if(response == null){
return;
}
//获取响应报文的数据体。响应报文的数据体只能使用该方法消费一次。
ResponseBody responseBody = response.body();
//根据需要的数据类型来选择数据输出方式
//如果下载的数据量很大,应该选用数据流的方式
//因为一次性获取,需要将这些数据全部同时加载入内存,会占用大量内存,容易OOM
//使用数据流,可以每从网络获取到一部分数据,就处理一部分数据
// 数据是分批加载入内存的,不会同时占用内存空间,大大降低了内存消耗
// responseBody.bytes();//以byte数组形式输出
// responseBody.string();//以String形式输出
// responseBody.charStream();//返回对应的char字符流
// responseBody.byteStream();//返回对应的String字节流
//......进行自己的数据解析.......
//注意不要再这里直接操作View,因为这里执行在工作线程而非主线程
}
});
注意:如果下载的数据量很大,应该选用数据流的方式来读取。因为一次性获取,需要将这些数据全部同时加载入内存,会占用大量内存,容易OOM。使用数据流,可以每从网络获取到一部分数据,就处理一部分数据,数据是分批加载入内存的,不会同时占用内存空间,大大降低了内存消耗。
Call是一个接口,其实际功能完成者是RealCall。每个RealCall内部有字段标记是否已经发起过请求,发起过就不能再次发起请求。如果希望同样的Call再发起一次请求,可以使用Call.clone()获取一个同样的Call对象。调度器Dispatcher中有三个请求缓存队列:
public final class Dispatcher {
//允许同时执行的最大异步网络请求数量
private int maxRequests = 64;
//针对单个主机允许同时执行的的最大异步网络请求数量
//Host示例:www.xxxxxxxxx.com
private int maxRequestsPerHost = 5;
//线程池
private @Nullable ExecutorService executorService;
//异步请求队列,异步请求开始时会加入该队列,但并未真正开始执行网络请求
private final Deque readyAsyncCalls = new ArrayDeque<>();
//正在执行的异步请求队列。加入该队列后,会去真正执行网络请求。
private final Deque runningAsyncCalls = new ArrayDeque<>();
//同步请求队列
//Call发起访问时加入该队列,访问完毕后移出该队列
private final Deque runningSyncCalls = new ArrayDeque<>();
同步请求比较简单,就是调用同步请求时,加入Dispatcher的同步请求队列,然后网络请求完毕后,移出同步请求队列。
2.1调用异步请求时,Call会被封装成AsyncCall,并加入异步请求队列,但并未真正开始去做网络请求。(AsyncCall是实现了Runnable的类,可被线程池中线程去执行。)
2.2接下去会执行Dispatcher的 promoteAndExecute (),该方法内部会过滤出异步请求队列中满足以下条件的AsyncCall:当前正在执行的异步请求数小于64,且针对目标Host上正执行的异步请求数小于5。满足条件的AsyncCall会被移动到正在执行异步队列中,从线程池中调配线程来执行其run()方法。
2.3执行完毕后,会执行Dispatcher的 finished()方法,再次触发 promoteAndExecute ()方法,进行下一轮筛选,不断重复这个过程,最终将异步请求队列中还未执行的AsyncCall执行完。
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
transmitter.timeoutEnter();
transmitter.callStart();
try {
client.dispatcher().executed(this);
return getResponseWithInterceptorChain();
} finally {
client.dispatcher().finished(this);
}
}
异步请求最终会执行:
下面的responseCallback就是做异步请求时传入的Callback。无论同步请求还是异步请求,最终实际完成网络请求的都是getResponseWithInterceptorChain(),后面讲拦截器是会分析该方法。
@Override protected void execute() {
boolean signalledCallback = false;
transmitter.timeoutEnter();
try {
Response response = getResponseWithInterceptorChain();
signalledCallback = true;
responseCallback.onResponse(okhttp3.RealCall.this, response);
} catch (IOException e) {
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
} else {
responseCallback.onFailure(okhttp3.RealCall.this, e);
}
} catch (Throwable t) {
cancel();
if (!signalledCallback) {
IOException canceledException = new IOException("canceled due to " + t);
canceledException.addSuppressed(t);
responseCallback.onFailure(okhttp3.RealCall.this, canceledException);
}
throw t;
} finally {
client.dispatcher().finished(this);
}
}
public class HttpInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
//1.获取当前传递给当前拦截器的Request对象(封装了请求报文的参数和数据)
Request request = chain.request();
//.......根据需要对Request对象做调整........
//2.将修改后Request传递给链条中的下一个拦截器,并最终触发下一个拦截器的intercept(Chain chain)方法
// 获取从下一个拦截器处返回的数据
Response response = chain.proceed(request);
//3......根据需要,对从下一个拦截器处返回的数据Response做调整......
return response;
}
}
OkHttp拦截器的设计采用了责任链模式,每个拦截器在链条中只处理自己那一环的输入和输出。在代码实现中,就是先创建一个列表,把自定义和预设的拦截器按顺序加入到列表中。用一个索引值来标明当前该那个拦截器来处理了,每次取出对应的拦截器触发其intercept()方法,然后将索引值+1。再具体点儿,就是每次都会创建一个RealInterceptorChain对象,这个对象中存储着当前拦截器索引、拦截器列表、当前拦截器的传入Request对象等,然后调用chain.proceed(Request)。第一次的chain.proceed(Request)是由getResponseWithInterceptorChain()触发的,后面都是由拦截器中的intercept(Chain chain)触发的。
Response getResponseWithInterceptorChain() throws IOException {
//0.创建一个拦截器链列表
List interceptors = new ArrayList<>();
//1.添加自定义拦截器
interceptors.addAll(client.interceptors());
//2.添加重试和重定向的拦截器
interceptors.add(new RetryAndFollowUpInterceptor(client));
//3.添加处理请求头和响应体的拦截器
interceptors.add(new BridgeInterceptor(client.cookieJar()));
//4.添加处理缓存逻辑的拦截器
interceptors.add(new CacheInterceptor(client.internalCache()));
//5.添加处理连接的拦截器
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
//6.添加发送请求报文、解析响应报文的拦截器
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
originalRequest, this, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
boolean calledNoMoreExchanges = false;
try {
Response response = chain.proceed(originalRequest);
chain.proceed(originalRequest)里的关键代码,如果index索引值未到达列表尾部,会构建一个新的RealInterceptorChain。该RealInterceptorChain的索引值是下一个拦截器的索引值,但其中的Request是当前要执行的拦截器的输入Request,然后触发当前拦截器的intercept(Chain chain),当前拦截器的intercept(Chain chain)又会触发下一个chain.proceed(originalRequest),然后又会触发下一个拦截器的intercept(Chain chain),……周而复始……,直到所有拦截器都被触发完。
//构建新的RealInterceptorChain,其中的索引值是下一个拦截器的索引值,其中的Request是当前要执行的拦截器的输入Request
RealInterceptorChain next = new RealInterceptorChain(interceptors, transmitter, exchange,
index + 1, request, call, connectTimeout, readTimeout, writeTimeout);
Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
//添加自定义的拦截器
.addInterceptor(new HttpInterceptor());
//构建客户端对象OkHttpClient
mHttpClient = builder.build();
根据Response的返回码,来判断是否进行重新请求或者重定向。当返回码是200时,说明请求成功,向上层返回结果。当返回码是307、407时,说明是重定向,此时服务端向客户端返回了新的请求新的url地址,放在头部的Location字段,OkHttp会取出该url,封装新的request并发起请求。其它情况时,一般是访问失败,会重新发起请求。最多只能重试20次。
添加一些默认的请求头,如Cookie、Connection、Content-Type、Content-Length等;对返回数据做处理,例如当返回数据被压缩时,会解压数据。
OkHttp在 CacheInterceptor中按照请求、响应报文中设置的缓存策略,来进行数据缓存并使用这些缓存数据。缓存策略由Http报文中一些头部字段来决定,最常见的就是Cache-Control字段,可以在请求报文中指定、也可以在响应报文中指定。OkHttp默认构造的Request是不设置这些字段的,如果要用,需要自己设置该头部字段。如果服务端返回的相应报文中配置了缓存策略,OkHttp也会根据缓存策略进行相应的处理。
3.1.1请求报文中:no-store: 不缓存任何内容,永远去服务端获取最新内容;no-cache: 并非完全不缓存,而是每次都要去服务端确认客户端的缓存数据是否有效未过期,有效则可使用本地缓存。max-age: 表示可使用过期一定时间的缓存数据,同指定了参数的max-stale;max-stale: 表示可使用过期的缓存,如后面未指定参数,则表示永远接收缓存数据。如max-stale: 3600, 表示可接受过期1小时内的数据;min-fresh: 表示可使用指定时间内的缓存数据,不考虑其是否过期,只要是缓存后未超过这个时间就可用。only-if-cache: 表示直接获取缓存数据,若没有数据返回,则返回504(Gateway Timeout)no-transform:不得对响应进行转换或转变
3.1.2响应报文中public: 可向任一方提供缓存数据;private: 只向指定用户提供缓存数据;no-cache: 缓存前需确认其有效性;no-store: 不缓存请求或响应的任何内容;max-age: 表示缓存的最大时间,在此时间范围内,访问该资源时,直接返回缓存数据。不需要对资源的有效性进行确认;must-revalidate: 访问缓存数据时,需要先向源服务器确认缓存数据是否有效,如无法验证其有效性,则需返回504。需要注意的是:如果使用此值,则max-stale将无效。
//创建缓存对象,指定缓存路径和大小
Cache cache = new Cache(new File(AppUtils.getFileDirDocuments(),"cacheFile"),10 * 1024 * 1024);
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(5, TimeUnit.SECONDS)/
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
//指定缓存路径和大小
.cache(cache)
//添加自定义的拦截器
.addInterceptor(new HttpInterceptor());
//构建客户端对象OkHttpClient
mHttpClient = builder.build();
自定义的拦截器,来为Request添加缓存设置。有网络,使用网络请求;无网络,使用未过期的缓存数据;
public class HttpInterceptor implements Interceptor {
//自定义的拦截器,来为Request添加缓存设置
//有网络,使用网络请求;
//无网络,使用未过期的缓存数据;
@Override
public Response intercept(Chain chain) throws IOException {
//1.获取当前传递给当前拦截器的Request对象(封装了请求报文的参数和数据)
Request request = chain.request();
//.......根据需要对Request对象做调整........
//2.将修改后Request传递给链条中的下一个拦截器,并最终触发下一个拦截器的intercept(Chain chain)方法
// 获取从下一个拦截器处返回的数据
Response response;
if (DeviceUtils.isNetworkConnected()) {
//有网络,使用网络
response = chain.proceed(request);
} else {
//没网络,使用未过期的缓存数据
//使用CacheControl类来设置缓存策略
CacheControl cacheControl = new CacheControl.Builder()
.onlyIfCached()
.maxStale(3600, TimeUnit.SECONDS)
.build();
Request newRequest = request.newBuilder()
//此处的缓存策略
//方式1:官方推荐的方式,使用提供的CacheControl类
.cacheControl(cacheControl)
//方式2:直接设置头部
// .header("Cache-Control", "only-if-cached, max-stale=" + 3600)
.build();
response = chain.proceed(newRequest);
}
//3......根据需要,对从下一个拦截器处返回的数据Response做调整......
return response;
}
}