深入解析OkHttp3
<p>OkHttp是一个精巧的网络请求库,有如下特性:</p> <p>1)支持http2,对一台机器的所有请求共享同一个socket</p> <p>2)内置连接池,支持连接复用,减少延迟</p> <p>3)支持透明的gzip压缩响应体</p> <p>4)通过缓存避免重复的请求</p> <p>5)请求失败时自动重试主机的其他ip,自动重定向</p> <p>6)好用的API</p> <p>其本身就是一个很强大的库,再加上Retrofit2、Picasso的这一套组合拳,使其愈发的受到开发者的关注。本篇博客,我将对Okhttp3进行分析(源码基于Okhttp3.4)。</p> <h2>如何引入Okhttp3?</h2> <p>配置Okhttp3非常简单,只需要在Android Studio 的gradle进行如下的配置:</p> <pre> <code class="language-java">compile 'com.squareup.okhttp3:okhttp:3.4.1'</code></pre> <p>添加网络权限:</p> <pre> <code class="language-java"><uses-permission android:name="android.permission.INTERNET"/></code></pre> <h2>Okhttp3的基本使用</h2> <p>okHttp的get请求okHttp的一般使用如下,okHttp默认使用的就是get请求</p> <pre> <code class="language-java">String url = "http://write.blog.csdn.net/postlist/0/0/enabled/1"; mHttpClient = new OkHttpClient(); Request request = new Request.Builder().url(url).build(); okhttp3.Response response = null; try { response = mHttpClient.newCall(request).execute(); String json = response.body().string(); Log.d("okHttp",json); } catch (IOException e) { e.printStackTrace(); } }</code></pre> <p>我们试着将数据在logcat进行打印,发现会报错,原因就是不能在主线程中进行耗时的操作</p> <p><img src="https://simg.open-open.com/show/afb86b0094e065fc392f295964e922e4.png"></p> <p>说明mHttpClient.newCall(request).execute()是同步的,那有没有异步的方法呢,答案是肯定的,就是mHttpClient.newCall(request).enqueue()方法,里面需要new一个callback我们对代码进行修改,如下</p> <pre> <code class="language-java">public void requestBlog() { String url = "http://write.blog.csdn.net/postlist/0/0/enabled/1"; mHttpClient = new OkHttpClient(); Request request = new Request.Builder().url(url).build(); /* okhttp3.Response response = null;*/ /*response = mHttpClient.newCall(request).execute();*/ mHttpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { String json = response.body().string(); Log.d("okHttp", json); } }); }</code></pre> <p><img src="https://simg.open-open.com/show/1474bfaef515e95d6d46c69b6138523a.png"></p> <p>Okhttp的POST请求</p> <p>POST提交Json数据</p> <pre> <code class="language-java">private void postJson() throws IOException { String url = "http://write.blog.csdn.net/postlist/0/0/enabled/1"; String json = "haha"; OkHttpClient client = new OkHttpClient(); RequestBody body = RequestBody.create(JSON, json); Request request = new Request.Builder() .url(url) .post(body) .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { Log.d(TAG, response.body().string()); } }); }</code></pre> <p>POST提交键值对很多时候我们会需要通过POST方式把键值对数据传送到服务器。 OkHttp提供了很方便的方式来做这件事情。</p> <pre> <code class="language-java">private void post(String url, String json) throws IOException { OkHttpClient client = new OkHttpClient(); RequestBody formBody = new FormBody.Builder() .add("name", "liming") .add("school", "beida") .build(); Request request = new Request.Builder() .url(url) .post(formBody) .build(); Call call = client.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { String str = response.body().string(); Log.i(TAG, str); } }); }</code></pre> <p><strong>异步上传文件</strong></p> <p>上传文件本身也是一个POST请求</p> <p>定义上传文件类型</p> <pre> <code class="language-java">public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8");</code></pre> <p>将文件上传到服务器上:</p> <pre> <code class="language-java">private void postFile() { OkHttpClient mOkHttpClient = new OkHttpClient(); File file = new File("/sdcard/demo.txt"); Request request = new Request.Builder() .url("https://api.github.com/markdown/raw") .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file)) .build(); mOkHttpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { Log.i(TAG, response.body().string()); } }); }</code></pre> <p>添加如下权限:</p> <pre> <code class="language-java"><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/></code></pre> <p>提取响应头典型的HTTP头 像是一个 Map</p> <pre> <code class="language-java">private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url("https://api.github.com/repos/square/okhttp/issues") .header("User-Agent", "OkHttp Headers.java") .addHeader("Accept", "application/json; q=0.5") .addHeader("Accept", "application/vnd.github.v3+json") .build(); Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println("Server: " + response.header("Server")); System.out.println("Date: " + response.header("Date")); System.out.println("Vary: " + response.headers("Vary")); }</code></pre> <p>Post方式提交String使用HTTP POST提交请求到服务。这个例子提交了一个markdown文档到web服务,以HTML方式渲染markdown。因为整个请求体都在内存中,因此避免使用此api提交大文档(大于1MB)。</p> <pre> <code class="language-java">private void postString() throws IOException { OkHttpClient client = new OkHttpClient(); String postBody = "" + "Releases\n" + "--------\n" + "\n" + " * zhangfei\n" + " * guanyu\n" + " * liubei\n"; Request request = new Request.Builder() .url("https://api.github.com/markdown/raw") .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody)) .build(); Call call = client.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { System.out.println(response.body().string()); } }); }</code></pre> <p>Post方式提交流</p> <p>以流的方式POST提交请求体。请求体的内容由流写入产生。这个例子是流直接写入Okio的BufferedSink。你的程序可能会使用OutputStream,你可以使用BufferedSink.outputStream()来获取。</p> <pre> <code class="language-java">public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8"); private void postStream() throws IOException { RequestBody requestBody = new RequestBody() { @Override public MediaType contentType() { return MEDIA_TYPE_MARKDOWN; } @Override public void writeTo(BufferedSink sink) throws IOException { sink.writeUtf8("Numbers\n"); sink.writeUtf8("-------\n"); for (int i = 2; i <= 997; i++) { sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i))); } } private String factor(int n) { for (int i = 2; i < n; i++) { int x = n / i; if (x * i == n) return factor(x) + " × " + i; } return Integer.toString(n); } }; Request request = new Request.Builder() .url("https://api.github.com/markdown/raw") .post(requestBody) .build(); Call call = client.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { System.out.println(response.body().string()); } }); }</code></pre> <p>Post方式提交表单</p> <pre> <code class="language-java">private void postForm() { OkHttpClient client = new OkHttpClient(); RequestBody formBody = new FormBody.Builder() .add("search", "Jurassic Park") .build(); Request request = new Request.Builder() .url("https://en.wikipedia.org/w/index.php") .post(formBody) .build(); Call call = client.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { System.out.println(response.body().string()); } }); }</code></pre> <p>Post方式提交分块请求MultipartBody 可以构建复杂的请求体,与HTML文件上传形式兼容。多块请求体中每块请求都是一个请求体,可以定义自己的请求头。这些请求头可以用来描述这块请求,例如他的Content-Disposition。如果Content-Length和Content-Type可用的话,他们会被自动添加到请求头中。</p> <pre> <code class="language-java">private static final String IMGUR_CLIENT_ID = "..."; private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png"); private void postMultipartBody() { OkHttpClient client = new OkHttpClient(); // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image MultipartBody body = new MultipartBody.Builder("AaB03x") .setType(MultipartBody.FORM) .addPart( Headers.of("Content-Disposition", "form-data; name=\"title\""), RequestBody.create(null, "Square Logo")) .addPart( Headers.of("Content-Disposition", "form-data; name=\"image\""), RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png"))) .build(); Request request = new Request.Builder() .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID) .url("https://api.imgur.com/3/image") .post(body) .build(); Call call = client.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { System.out.println(response.body().string()); } }); }</code></pre> <p><strong>响应缓存</strong></p> <p>为了缓存响应,你需要一个你可以读写的缓存目录,和缓存大小的限制。这个缓存目录应该是私有的,不信任的程序应不能读取缓存内容。</p> <p>一个缓存目录同时拥有多个缓存访问是错误的。大多数程序只需要调用一次new OkHttpClient(),在第一次调用时配置好缓存,然后其他地方只需要调用这个实例就可以了。否则两个缓存示例互相干扰,破坏响应缓存,而且有可能会导致程序崩溃。</p> <p>响应缓存使用HTTP头作为配置。你可以在请求头中添加Cache-Control: max-stale=3600 ,OkHttp缓存会支持。你的服务通过响应头确定响应缓存多长时间,例如使用Cache-Control: max-age=9600。</p> <pre> <code class="language-java">int cacheSize = 10 * 1024 * 1024; // 10 MiB Cache cache = new Cache(cacheDirectory, cacheSize); OkHttpClient.Builder builder = new OkHttpClient.Builder(); builder.cache(cache); OkHttpClient client = builder.build(); Request request = new Request.Builder() .url("http://publicobject.com/helloworld.txt") .build(); Call call = client.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { String response1Body = response.body().string(); System.out.println("Response 1 response: " + response); System.out.println("Response 1 cache response: " + response.cacheResponse()); System.out.println("Response 1 network response: " + response.networkResponse()); } });</code></pre> <p>超时没有响应时使用超时结束call。没有响应的原因可能是客户点链接问题、服务器可用性问题或者这之间的其他东西。OkHttp支持连接,读取和写入超时。</p> <pre> <code class="language-java">private void ConfigureTimeouts() { OkHttpClient.Builder builder = new OkHttpClient.Builder(); OkHttpClient client = builder.build(); client.newBuilder().connectTimeout(10, TimeUnit.SECONDS); client.newBuilder().readTimeout(10,TimeUnit.SECONDS); client.newBuilder().writeTimeout(10,TimeUnit.SECONDS); Request request = new Request.Builder() .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay. .build(); Call call = client.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { System.out.println("Response completed: " + response); } }); }</code></pre> <h2>简单封装okHttp框架</h2> <p>新建一个工具类OkHttpUtils</p> <p>OkHttpClient必须是单例的,所以这里我们需要使用到单例设计模式,私有化构造函数,提供一个方法给外界获取OkHttpUtils实例对象</p> <pre> <code class="language-java">public class OkHttpUtils { private static OkHttpUtils mInstance; private OkHttpClient mHttpClient; private OkHttpUtils() { }; public static OkHttpUtils getInstance(){ return mInstance; } }</code></pre> <p>一般网络请求分为get和post请求两种,但无论哪种请求都是需要用到request的,所以我们首先封装一个request,创建一个doRequest方法,在其内先编写mHttpClient.newCall(request).enqueue(new Callback())相关逻辑</p> <pre> <code class="language-java">public void doRequest(final Request request){ mHttpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { } }); }</code></pre> <p>我们需要自定义一个callback,BaseCallback,并将其传入request方法中</p> <pre> <code class="language-java">public class BaseCallback { }</code></pre> <p>在OkHttpUtils中编写get和post方法</p> <pre> <code class="language-java">public void get(String url){ } public void post(String url,Map<String,Object> param){ }</code></pre> <p>post方法中构建request对象,这里我们需要创建一个buildRequest方法,用于生成request对象</p> <pre> <code class="language-java">private Request buildRequest(String url,HttpMethodType methodType,Map<String,Object> params){ return null; }</code></pre> <p>这里需要定一个枚举对象HttpMethodType,用于区分是get还是post</p> <pre> <code class="language-java">enum HttpMethodType{ GET, POST, }</code></pre> <p>buildRequest方法根据HttpMethodType不同有相应的逻辑处理</p> <pre> <code class="language-java">private Request buildRequest(String url,HttpMethodType methodType,Map<String,Object> params){ Request.Builder builder = new Request.Builder() .url(url); if (methodType == HttpMethodType.POST){ builder.post(body); } else if(methodType == HttpMethodType.GET){ builder.get(); } return builder.build(); }</code></pre> <p>builder.post()方法中需要一个body,所以我们需要创建一个方法builderFormData()方法用于返回RequestBody,这里内部逻辑后面再进行完善</p> <pre> <code class="language-java">private RequestBody builderFormData(Map<String,Object> params){ return null; }</code></pre> <p>于是buildRequest方法变成了这样</p> <pre> <code class="language-java">private Request buildRequest(String url,HttpMethodType methodType,Map<String,Object> params){ Request.Builder builder = new Request.Builder() .url(url); if (methodType == HttpMethodType.POST){ RequestBody body = builderFormData(params); builder.post(body); } else if(methodType == HttpMethodType.GET){ builder.get(); } return builder.build(); }</code></pre> <p>get方法进行修改:</p> <pre> <code class="language-java">public void get(String url,BaseCallback callback){ Request request = buildRequest(url,HttpMethodType.GET,null); doRequest(request,callback); }</code></pre> <p>post方法进行修改:</p> <pre> <code class="language-java">public void post(String url,Map<String,Object> params,BaseCallback callback){ Request request = buildRequest(url,HttpMethodType.POST,params); doRequest(request,callback); }</code></pre> <p>完善builderFormData()方法</p> <pre> <code class="language-java">private RequestBody builderFormData(Map<String,String> params){ FormBody.Builder builder = new FormBody.Builder(); if(params!=null){ for(Map.Entry<String,String> entry:params.entrySet()){ builder.add(entry.getKey(),entry.getValue()); } } return builder.build(); }</code></pre> <p>BaseCallback中定义一个抽象方法onBeforeRequest,这样做的理由是我们在加载网络数据成功前,一般都有进度条等显示,这个方法就是用来做这些处理的</p> <pre> <code class="language-java">public abstract class BaseCallback { public abstract void onBeforeRequest(Request request); }</code></pre> <p>OkHttpUtils的doRequest方法增加如下语句:</p> <pre> <code class="language-java">baseCallback.onBeforeRequest(request);</code></pre> <p>BaseCallback中多定义2个抽象方法</p> <pre> <code class="language-java">public abstract void onFailure(Request request, Exception e) ; /** *请求成功时调用此方法 * @param response */ public abstract void onResponse(Response response);</code></pre> <p>由于Response的状态有多种,比如成功和失败,所以需要onResponse分解为3个抽象方法</p> <pre> <code class="language-java">/** * * 状态码大于200,小于300 时调用此方法 * @param response * @param t * @throws */ public abstract void onSuccess(Response response,T t) ; /** * 状态码400,404,403,500等时调用此方法 * @param response * @param code * @param e */ public abstract void onError(Response response, int code,Exception e) ; /** * Token 验证失败。状态码401,402,403 等时调用此方法 * @param response * @param code */ public abstract void onTokenError(Response response, int code);</code></pre> <p>response.body.string()方法返回的都是String类型,而我们需要显示的数据其实是对象,所以我们就想抽取出方法,直接返回对象,由于我们不知道对象的类型是什么,所以我们在BaseCallback中使用范型</p> <pre> <code class="language-java">public abstract class BaseCallback<T></code></pre> <p>BaseCallback中需要将泛型转换为Type,所以要声明Type类型</p> <pre> <code class="language-java">public Type mType;</code></pre> <p>BaseCallback中需要如下一段代码,将泛型T转换为Type类型</p> <pre> <code class="language-java">static Type getSuperclassTypeParameter(Class<?> subclass) { Type superclass = subclass.getGenericSuperclass(); if (superclass instanceof Class) { throw new RuntimeException("Missing type parameter."); } ParameterizedType parameterized = (ParameterizedType) superclass; return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]); }</code></pre> <p>在BaseCallback的构造函数中进行mType进行赋值</p> <pre> <code class="language-java">public BaseCallback() { mType = getSuperclassTypeParameter(getClass()); }</code></pre> <p>OkHttpUtils中doRequest方法的onFailure与onResponse方法会相应的去调用baseCallback的方法</p> <pre> <code class="language-java">mHttpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { baseCallback.onFailure(request,e); } @Override public void onResponse(Call call, Response response) throws IOException { if(response.isSuccessful()) { baseCallback.onSuccess(response,null); }else { baseCallback.onError(response,response.code(),null); } /*mGson.fromJson(response.body().string(),baseCallback.mType);*/ } });</code></pre> <p>onResponse方法中成功的情况又有区分,根据mType的类型不同有相应的处理逻辑,同时还要考虑Gson解析错误的情况</p> <pre> <code class="language-java">@Override public void onResponse(Call call, Response response) throws IOException { if(response.isSuccessful()) { String resultStr = response.body().string(); if (baseCallback.mType == String.class){ baseCallback.onSuccess(response,resultStr); } else { try { Object obj = mGson.fromJson(resultStr, baseCallback.mType); baseCallback.onSuccess(response,obj); } catch (com.google.gson.JsonParseException e){ // Json解析的错误 baseCallback.onError(response,response.code(),e); } } }else { baseCallback.onError(response,response.code(),null); } }</code></pre> <p>构造函数中进行一些全局变量的初始化的操作,还有一些超时的设计</p> <pre> <code class="language-java">private OkHttpUtils() { mHttpClient = new OkHttpClient(); OkHttpClient.Builder builder = mHttpClient.newBuilder(); builder.connectTimeout(10, TimeUnit.SECONDS); builder.readTimeout(10,TimeUnit.SECONDS); builder.writeTimeout(30,TimeUnit.SECONDS); mGson = new Gson(); };</code></pre> <p>静态代码块初始化OkHttpUtils对象</p> <pre> <code class="language-java">static { mInstance = new OkHttpUtils(); }</code></pre> <p>在okHttpUtils内,需要创建handler进行UI界面的更新操作,创建callbackSuccess方法</p> <pre> <code class="language-java">private void callbackSuccess(final BaseCallback callback , final Response response, final Object obj ){ mHandler.post(new Runnable() { @Override public void run() { callback.onSuccess(response, obj); } }); }</code></pre> <p>doRequest方法的onResponse方法也进行相应的改写</p> <pre> <code class="language-java">if (baseCallback.mType == String.class){ /*baseCallback.onSuccess(response,resultStr);*/ callbackSuccess(baseCallback,response,resultStr); }</code></pre> <p>创建callbackError方法</p> <pre> <code class="language-java">private void callbackError(final BaseCallback callback, final Response response, final Exception e) { mHandler.post(new Runnable() { @Override public void run() { callback.onError(response, response.code(), e); } }); }</code></pre> <p>将doRequest方法的onResponse方法中的baseCallback.onError(response,response.code(),e);替换为callbackError(baseCallback,response,e);方法</p> <pre> <code class="language-java">@Override public void onResponse(Call call, Response response) throws IOException { if(response.isSuccessful()) { String resultStr = response.body().string(); if (baseCallback.mType == String.class){ /*baseCallback.onSuccess(response,resultStr);*/ callbackSuccess(baseCallback,response,resultStr); } else { try { Object obj = mGson.fromJson(resultStr, baseCallback.mType); /*baseCallback.onSuccess(response,obj);*/ callbackSuccess(baseCallback,response,obj); } catch (com.google.gson.JsonParseException e){ // Json解析的错误 /*baseCallback.onError(response,response.code(),e);*/ callbackError(baseCallback,response,e); } } }else { callbackError(baseCallback,response,null); /*baseCallback.onError(response,response.code(),null);*/ } }</code></pre> <p>至此,我们的封装基本完成。</p> <h2>OkHttp3源码分析</h2> <p>请求处理分析当我们要请求网络的时候我们需要用OkHttpClient.newCall(request)进行execute或者enqueue操作,当我们调用newCall时:</p> <pre> <code class="language-java">/** * Prepares the {@code request} to be executed at some point in the future. */ @Override public Call newCall(Request request) { return new RealCall(this, request); }</code></pre> <p>实际返回的是一个RealCall类,我们调用enqueue异步请求网络实际上是调用了RealCall的enqueue方法:</p> <pre> <code class="language-java">@Override public void enqueue(Callback responseCallback) { synchronized (this) { if (executed) throw new IllegalStateException("Already Executed"); executed = true; } client.dispatcher().enqueue(new AsyncCall(responseCallback)); }</code></pre> <p>最终的请求是dispatcher来完成的。</p> <p>Dispatcher任务调度</p> <p>Dispatcher的本质是异步请求的管理器,控制最大请求并发数和单个主机的最大并发数,并持有一个线程池负责执行异步请求。对同步的请求只是用作统计。他是如何做到控制并发呢,其实原理就在上面的2个execute代码里面,真正网络请求执行前后会调用executed和finished方法,而对于AsyncCall的finished方法后,会根据当前并发数目选择是否执行队列中等待的AsyncCall。并且如果修改Dispatcher的maxRequests或者maxRequestsPerHost也会触发这个过程。</p> <p>Dispatcher主要用于控制并发的请求,它主要维护了以下变量:</p> <pre> <code class="language-java">/** 最大并发请求数*/ private int maxRequests = 64; /** 每个主机最大请求数*/ private int maxRequestsPerHost = 5; /** 消费者线程池 */ private ExecutorService executorService; /** 将要运行的异步请求队列 */ private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>(); /**正在运行的异步请求队列 */ private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>(); /** 正在运行的同步请求队列 */ private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();</code></pre> <p>构造函数</p> <pre> <code class="language-java">public Dispatcher(ExecutorService executorService) { this.executorService = executorService; } public Dispatcher() { } public synchronized ExecutorService executorService() { if (executorService == null) { executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false)); } return executorService; }</code></pre> <p>Dispatcher有两个构造函数,可以使用自己设定线程池,如果没有设定线程池则会在请求网络前自己创建线程池,这个线程池类似于CachedThreadPool比较适合执行大量的耗时比较少的任务。</p> <p>异步请求</p> <pre> <code class="language-java">synchronized void enqueue(AsyncCall call) { if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) { runningAsyncCalls.add(call); executorService().execute(call); } else { readyAsyncCalls.add(call); } }</code></pre> <p>当正在运行的异步请求队列中的数量小于64并且正在运行的请求主机数小于5时则把请求加载到runningAsyncCalls中并在线程池中执行,否则就再入到readyAsyncCalls中进行缓存等待。</p> <p>AsyncCall线程池中传进来的参数就是AsyncCall它是RealCall的内部类,内部也实现了execute方法:</p> <pre> <code class="language-java">@Override protected void execute() { boolean signalledCallback = false; try { Response response = getResponseWithInterceptorChain(); if (retryAndFollowUpInterceptor.isCanceled()) { signalledCallback = true; responseCallback.onFailure(RealCall.this, new IOException("Canceled")); } else { signalledCallback = true; responseCallback.onResponse(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(RealCall.this, e); } } finally { client.dispatcher().finished(this); } } }</code></pre> <p>首先我们来看看最后一行, 无论这个请求的结果如何都会执行client.dispatcher().finished(this);</p> <pre> <code class="language-java">/** Used by {@code AsyncCall#run} to signal completion. */ void finished(AsyncCall call) { finished(runningAsyncCalls, call, true); } /** Used by {@code Call#execute} to signal completion. */ void finished(RealCall call) { finished(runningSyncCalls, call, false); } private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) { int runningCallsCount; Runnable idleCallback; synchronized (this) { if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!"); if (promoteCalls) promoteCalls(); runningCallsCount = runningCallsCount(); idleCallback = this.idleCallback; } if (runningCallsCount == 0 && idleCallback != null) { idleCallback.run(); } } finished方法将此次请求从runningAsyncCalls移除后还执行了promoteCalls方法: private void promoteCalls() { if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity. if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote. for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) { AsyncCall call = i.next(); if (runningCallsForHost(call) < maxRequestsPerHost) { i.remove(); runningAsyncCalls.add(call); executorService().execute(call); } if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity. } }</code></pre> <p>可以看到最关键的点就是会从readyAsyncCalls取出下一个请求,并加入runningAsyncCalls中并交由线程池处理。好了让我们再回到上面的AsyncCall的execute方法,我们会发getResponseWithInterceptorChain方法返回了Response,很明显这是在请求网络。</p> <p><strong>Interceptor拦截器</strong></p> <p>在回到RealCall中,我们看到无论是execute还是enqueue,真正的Response是通过这个函数getResponseWithInterceptorChain获取的,其他的代码都是用作控制与回调。而这里就是真正请求的入口,也是到了OkHttp的一个很精彩的设计:Interceptor与Chain</p> <p>看一下RealCall中的getResponseWithInterceptorChain方法</p> <pre> <code class="language-java">private 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 (!retryAndFollowUpInterceptor.isForWebSocket()) { interceptors.addAll(client.networkInterceptors()); } interceptors.add(new CallServerInterceptor( retryAndFollowUpInterceptor.isForWebSocket())); Interceptor.Chain chain = new RealInterceptorChain( interceptors, null, null, null, 0, originalRequest); return chain.proceed(originalRequest); }</code></pre> <p>这也是与旧版本不一致的地方,在3.4.x以前,没有这些内部的这些拦截器,只有用户的拦截器与网络拦截器。而Request和Response是通过HttpEngine来完成的。在RealCall实现了用户拦截器与RetryAndFollowUp的过程,而在HttpEngine内部处理了请求转换、Cookie、Cache、网络拦截器、连接网络的过程。值得一提的是,在旧版是获取到Response后调用网络拦截器的拦截。</p> <p>而在这里,RealInterceptorChain会递归的创建并以此调用拦截器,去掉诸多异常,简化版代码如下:</p> <pre> <code class="language-java">public Response proceed(Request request, StreamAllocation streamAllocation, HttpStream httpStream, Connection connection) throws IOException { if (index >= interceptors.size()) throw new AssertionError(); calls++; // If we already have a stream, confirm that the incoming request will use it. if (this.httpStream != null && !sameConnection(request.url())) { throw new IllegalStateException("network interceptor " + interceptors.get(index - 1) + " must retain the same host and port"); } // If we already have a stream, confirm that this is the only call to chain.proceed(). if (this.httpStream != null && calls > 1) { throw new IllegalStateException("network interceptor " + interceptors.get(index - 1) + " must call proceed() exactly once"); } // Call the next interceptor in the chain. RealInterceptorChain next = new RealInterceptorChain( interceptors, streamAllocation, httpStream, connection, index + 1, request); Interceptor interceptor = interceptors.get(index); Response response = interceptor.intercept(next); // Confirm that the next interceptor made its required call to chain.proceed(). if (httpStream != null && index + 1 < interceptors.size() && next.calls != 1) { throw new IllegalStateException("network interceptor " + interceptor + " must call proceed() exactly once"); } // Confirm that the intercepted response isn't null. if (response == null) { throw new NullPointerException("interceptor " + interceptor + " returned null"); } return response; }</code></pre> <p>Chain与Interceptor会互相递归调用,直到链的尽头。</p> <p>我们看到,通过职责链模式,清楚地切开了不同的逻辑,每个拦截器完成自己的职责,从而完成用户的网络请求。</p> <p>大概流程是:</p> <p>1)先经过用户拦截器</p> <p>2)RetryAndFollowUpInterceptor负责自动重试和进行必要的重定向</p> <p>3)BridgeIntercetor负责将用户Request转换成一个实际的网络请求的Request,再调用下层的拦截器获取Response,最后再将网络Response转换成用户的Reponse</p> <p>4)CacheInterceptor负责控制缓存</p> <p>5)ConnectInterceptor负责进行连接主机</p> <p>6)网络拦截器进行拦截</p> <p>7)CallServerInterceptor是真正和服务器通信,完成http请求</p> <p>连接与通信在RetryAndFollowUpInterceptor中,会创建StreamAllocation,然后交给下游的ConnectInterceptor</p> <pre> <code class="language-java">@Override public Response intercept(Chain chain) throws IOException { RealInterceptorChain realChain = (RealInterceptorChain) chain; Request request = realChain.request(); StreamAllocation streamAllocation = realChain.streamAllocation(); // We need the network to satisfy this request. Possibly for validating a conditional GET. boolean doExtensiveHealthChecks = !request.method().equals("GET"); HttpStream httpStream = streamAllocation.newStream(client, doExtensiveHealthChecks); RealConnection connection = streamAllocation.connection(); return realChain.proceed(request, streamAllocation, httpStream, connection); }</code></pre> <p>这里会创建一个HttpStream,并且取到一个RealConnection,继续交给下游的CallServerInterceptor。</p> <p>我们跟踪进去看看,StreamAllocation里面做了什么</p> <pre> <code class="language-java">public HttpStream newStream(OkHttpClient client, boolean doExtensiveHealthChecks) { int connectTimeout = client.connectTimeoutMillis(); int readTimeout = client.readTimeoutMillis(); int writeTimeout = client.writeTimeoutMillis(); boolean connectionRetryEnabled = client.retryOnConnectionFailure(); try { RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks); HttpStream resultStream; if (resultConnection.framedConnection != null) { resultStream = new Http2xStream(client, this, resultConnection.framedConnection); } else { resultConnection.socket().setSoTimeout(readTimeout); resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS); resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS); resultStream = new Http1xStream( client, this, resultConnection.source, resultConnection.sink); } synchronized (connectionPool) { stream = resultStream; return resultStream; } } catch (IOException e) { throw new RouteException(e); } }</code></pre> <p>这里的代码逻辑是这样的,找一个健康的连接,设置超时时间,然后根据协议创建一个HttpStream并返回。</p> <p>继续跟进去看findHealthyConnection:</p> <pre> <code class="language-java">private RealConnection findHealthyConnection(int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks) throws IOException { while (true) { RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled); // If this is a brand new connection, we can skip the extensive health checks. synchronized (connectionPool) { if (candidate.successCount == 0) { return candidate; } } // Do a (potentially slow) check to confirm that the pooled connection is still good. If it // isn't, take it out of the pool and start again. if (!candidate.isHealthy(doExtensiveHealthChecks)) { noNewStreams(); continue; } return candidate; } }</code></pre> <p>上面的逻辑也很简单,在findConnection中找一个连接,然后做健康检查,如果不健康就回收,并再次循环,那么真正寻找连接的代码就在findConnection里面了:</p> <pre> <code class="language-java">/** * Returns a connection to host a new stream. This prefers the existing connection if it exists, * then the pool, finally building a new connection. */ private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) throws IOException { Route selectedRoute; synchronized (connectionPool) { if (released) throw new IllegalStateException("released"); if (stream != null) throw new IllegalStateException("stream != null"); if (canceled) throw new IOException("Canceled"); RealConnection allocatedConnection = this.connection; if (allocatedConnection != null && !allocatedConnection.noNewStreams) { return allocatedConnection; } // Attempt to get a connection from the pool. RealConnection pooledConnection = Internal.instance.get(connectionPool, address, this); if (pooledConnection != null) { this.connection = pooledConnection; return pooledConnection; } selectedRoute = route; } if (selectedRoute == null) { selectedRoute = routeSelector.next(); synchronized (connectionPool) { route = selectedRoute; refusedStreamCount = 0; } } RealConnection newConnection = new RealConnection(selectedRoute); acquire(newConnection); synchronized (connectionPool) { Internal.instance.put(connectionPool, newConnection); this.connection = newConnection; if (canceled) throw new IOException("Canceled"); } newConnection.connect(connectTimeout, readTimeout, writeTimeout, address.connectionSpecs(), connectionRetryEnabled); routeDatabase().connected(newConnection.route()); return newConnection; }</code></pre> <p>这里大概分成分成3大步:</p> <p>1)如果当前有连接并且符合要求的话,就直接返回</p> <p>2)如果线程池能取到一个符合要求的连接的话,就直接返回</p> <p>3)如果Route为空,从RouteSelector取一个Route,然后新建一个RealConnection,并放入ConnectionPool,随后调用connect,再返回</p> <p>也就是说不管当前走的是步骤1还是2,一开始一定是从3开始的,也就是在RealConnection的connect中真正完成了socket连接。</p> <p>connect里面代码比较长,真正要做的就是一件事,如果是https请求并且是http代理,则建立隧道连接,隧道连接请参考RFC2817,否则建立普通连接。</p> <p>这两者都调用了2个函数:connectSocket(connectTimeout, readTimeout); establishProtocol(readTimeout, writeTimeout, connectionSpecSelector);</p> <p>但是隧道连接则多了一个代理认证的过程,可能会反复的connectSocket和构造请求。</p> <p>看一下connectSocket:</p> <pre> <code class="language-java">private void connectSocket(int connectTimeout, int readTimeout) throws IOException { Proxy proxy = route.proxy(); Address address = route.address(); rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP ? address.socketFactory().createSocket() : new Socket(proxy); rawSocket.setSoTimeout(readTimeout); try { Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout); } catch (ConnectException e) { throw new ConnectException("Failed to connect to " + route.socketAddress()); } source = Okio.buffer(Okio.source(rawSocket)); sink = Okio.buffer(Okio.sink(rawSocket)); }</code></pre> <p>就是根据Route来创建socket,在connect,随后将rawSocket的InputStream与OutputStream包装成Source与Sink。这里提一下,OkHttp是依赖Okio的,Okio封装了Java的IO API,如这里的Source与Sink,非常简洁实用。</p> <p>而establishProtocol里,如果是https则走TLS协议,生成一个SSLSocket,并进行握手和验证,同时如果是HTTP2或者SPDY3的话,则生成一个FrameConnection。这里不再多提,HTTP2和HTTP1.X大相径庭,我们这里主要是分析HTTP1.X的连接,后面有机会我们会单独开篇讲HTTP2。同时TLS相关的话题这里也一并略过,想了解的朋友可以看一看相应的Java API和HTTPS连接的资料。</p> <p>再回到StreamAllcation.newStream的代码resultStream = new Http1xStream( client, this, resultConnection.source, resultConnection.sink);实质上HttpStream其实就是Request和Response读写Socket的抽象,我们看到Http1xStream取到了Socket输入输出流,随后在CallServerInterceptor可以拿来做读写。</p> <p>我们看CallServerInterceptor做了什么:</p> <pre> <code class="language-java">@Override public Response intercept(Chain chain) throws IOException { HttpStream httpStream = ((RealInterceptorChain) chain).httpStream(); StreamAllocation streamAllocation = ((RealInterceptorChain) chain).streamAllocation(); Request request = chain.request(); long sentRequestMillis = System.currentTimeMillis(); httpStream.writeRequestHeaders(request); if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) { Sink requestBodyOut = httpStream.createRequestBody(request, request.body().contentLength()); BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut); request.body().writeTo(bufferedRequestBody); bufferedRequestBody.close(); } httpStream.finishRequest(); Response response = httpStream.readResponseHeaders() .request(request) .handshake(streamAllocation.connection().handshake()) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); if (!forWebSocket || response.code() != 101) { response = response.newBuilder() .body(httpStream.openResponseBody(response)) .build(); } if ("close".equalsIgnoreCase(response.request().header("Connection")) || "close".equalsIgnoreCase(response.header("Connection"))) { streamAllocation.noNewStreams(); } int code = response.code(); if ((code == 204 || code == 205) && response.body().contentLength() > 0) { throw new ProtocolException( "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength()); } return response; }</code></pre> <p>CallServerInterceptor顾名思义,就是真正和Server进行通信的地方。这里也是按照HTTP协议,依次写入请求头,还有根据情况决定是否写入请求体。随后读响应头闭构造一个Response。</p> <p>里面具体是如何实现呢,我们看Http1xStream:</p> <p>首先是写头:</p> <pre> <code class="language-java">@Override public void writeRequestHeaders(Request request) throws IOException { String requestLine = RequestLine.get( request, streamAllocation.connection().route().proxy().type()); writeRequest(request.headers(), requestLine); }</code></pre> <p>构造好请求行,进入writeRequest:</p> <pre> <code class="language-java">/** Returns bytes of a request header for sending on an HTTP transport. */ public void writeRequest(Headers headers, String requestLine) throws IOException { if (state != STATE_IDLE) throw new IllegalStateException("state: " + state); sink.writeUtf8(requestLine).writeUtf8("\r\n"); for (int i = 0, size = headers.size(); i < size; i++) { sink.writeUtf8(headers.name(i)) .writeUtf8(": ") .writeUtf8(headers.value(i)) .writeUtf8("\r\n"); } sink.writeUtf8("\r\n"); state = STATE_OPEN_REQUEST_BODY; }</code></pre> <p>这里就一目了然了,就是一行行的写请求行和请求头到sink中</p> <p>再看readResponse:</p> <pre> <code class="language-java">/** Parses bytes of a response header from an HTTP transport. */ public Response.Builder readResponse() throws IOException { if (state != STATE_OPEN_REQUEST_BODY && state != STATE_READ_RESPONSE_HEADERS) { throw new IllegalStateException("state: " + state); } try { while (true) { StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict()); Response.Builder responseBuilder = new Response.Builder() .protocol(statusLine.protocol) .code(statusLine.code) .message(statusLine.message) .headers(readHeaders()); if (statusLine.code != HTTP_CONTINUE) { state = STATE_OPEN_RESPONSE_BODY; return responseBuilder; } } } catch (EOFException e) { // Provide more context if the server ends the stream before sending a response. IOException exception = new IOException("unexpected end of stream on " + streamAllocation); exception.initCause(e); throw exception; } }</code></pre> <p>也是一样的,从source中读请求行和请求头</p> <p>最后看openResponseBody:</p> <pre> <code class="language-java">@Override public ResponseBody openResponseBody(Response response) throws IOException { Source source = getTransferStream(response); return new RealResponseBody(response.headers(), Okio.buffer(source)); }</code></pre> <p>这里说一下就是根据请求的响应把包裹InputStream的source再次封装,里面做一些控制逻辑,然后再封装成ResponseBody。</p> <p>例如FiexdLengthSource,就是期望获取到byte的长度是固定的值:</p> <pre> <code class="language-java">/** An HTTP body with a fixed length specified in advance. */ private class FixedLengthSource extends AbstractSource { private long bytesRemaining; public FixedLengthSource(long length) throws IOException { bytesRemaining = length; if (bytesRemaining == 0) { endOfInput(true); } } @Override public long read(Buffer sink, long byteCount) throws IOException { if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); if (closed) throw new IllegalStateException("closed"); if (bytesRemaining == 0) return -1; long read = source.read(sink, Math.min(bytesRemaining, byteCount)); if (read == -1) { endOfInput(false); // The server didn't supply the promised content length. throw new ProtocolException("unexpected end of stream"); } bytesRemaining -= read; if (bytesRemaining == 0) { endOfInput(true); } return read; } @Override public void close() throws IOException { if (closed) return; if (bytesRemaining != 0 && !Util.discard(this, DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) { endOfInput(false); } closed = true; } }</code></pre> <p>当读完期望的长度时就把这个RealConnection回收,如果少于期望的长度则抛异常。</p> <p><strong>ConnectionPool</strong></p> <p>到了OkHttp3时代,ConnectionPool就是每个Client独享的了,我们刚才提到了ConnectionPool,那么他到底是如何运作呢。</p> <p>ConnectionPool持有一个静态的线程池。</p> <p>StreamAllocation不管通过什么方式,在获取到RealConnection后,RealConnection会添加一个对StreamAllocation的引用。</p> <p>在每个RealConnection加入ConnectionPool后,如果当前没有在清理,就会把cleanUpRunnable加入线程池。</p> <p>cleanUpRunnable里面是一个while(true),一个循环包括:</p> <p>调用一次cleanUp方法进行清理并返回一个long, 如果是-1则退出,否则调用wait方法等待这个long值的时间</p> <p>cleanUp代码如下:</p> <pre> <code class="language-java">ong cleanup(long now) { int inUseConnectionCount = 0; int idleConnectionCount = 0; RealConnection longestIdleConnection = null; long longestIdleDurationNs = Long.MIN_VALUE; // Find either a connection to evict, or the time that the next eviction is due. synchronized (this) { for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) { RealConnection connection = i.next(); // If the connection is in use, keep searching. if (pruneAndGetAllocationCount(connection, now) > 0) { inUseConnectionCount++; continue; } idleConnectionCount++; // If the connection is ready to be evicted, we're done. long idleDurationNs = now - connection.idleAtNanos; if (idleDurationNs > longestIdleDurationNs) { longestIdleDurationNs = idleDurationNs; longestIdleConnection = connection; } } if (longestIdleDurationNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) { // We've found a connection to evict. Remove it from the list, then close it below (outside // of the synchronized block). connections.remove(longestIdleConnection); } else if (idleConnectionCount > 0) { // A connection will be ready to evict soon. return keepAliveDurationNs - longestIdleDurationNs; } else if (inUseConnectionCount > 0) { // All connections are in use. It'll be at least the keep alive duration 'til we run again. return keepAliveDurationNs; } else { // No connections, idle or in use. cleanupRunning = false; return -1; } } closeQuietly(longestIdleConnection.socket()); // Cleanup again immediately. return 0; }</code></pre> <p>遍历每一个RealConnection,通过引用数目确定哪些是空闲的,哪些是在使用中,同时找到空闲时间最长的RealConnection。</p> <p>如果空闲数目超过最大空闲数或者空闲时间超过最大空闲时间,则清理掉这个RealConnection,并返回0,表示需要立刻再次清理</p> <p>否则如果空闲的数目大于0个,则等待最大空闲时间-已有的最长空闲时间</p> <p>否则如果使用中的数目大于0,则等待最大空闲时间</p> <p>否则 返回 -1,并标识退出清除状态</p> <p>同时如果某个RealConnection空闲后,会进入ConnectionPool.connectionBecameIdle方法,如果不可被复用,则被移除,否则立刻唤醒上面cleanUp的wait,再次清理,因为可能超过了最大空闲数目</p> <p>这样通过一个静态的线程池,ConnectionPool做到了每个实例定期清理,保证不会超过最大空闲时间和最大空闲数目的策略。</p> <p>OkHttp3分析就到此结束了。</p> <p> </p> <p>来自:http://blog.csdn.net/u012124438/article/details/54236967</p> <p> </p>
本文由用户 djer9096 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
转载本站原创文章,请注明出处,并保留原始链接、图片水印。
本站是一个以用户分享为主的开源技术平台,欢迎各类分享!