《亿级Android架构》小专栏文章列表:java
《Android 架构之网络链接与加速》github
《Android 架构之秒级移动配置中心》markdown
私觉得,阅读开源项目是与世界级技术大牛直接对话的最好方式。 这次来分享下 OkHttp 源码的分析。cookie
在Android、Java开发领域中,相信你们都听过或者在使用Square家大名鼎鼎的网络请求库:OkHttp ,当前多数著名的开源项目如 Fresco、Glide、 Picasso、 Retrofit都在使用OkHttp,这足以说明其质量,并且该项目仍处在不断维护中。网络
在分析okhttp源码以前,我想先提出一个问题,若是咱们本身来设计一个网络请求库,这个库应该长什么样子?大体是什么结构呢?
下面我和你们一块儿来构建一个网络请求库,并在其中融入okhttp中核心的设计思想,但愿借此让读者感觉并学习到okhttp中的精华之处,而非仅限于了解其实现。
笔者相信,若是你能耐心阅读完本篇,不只能对http协议有进一步理解,更可以学习到世界级项目的思惟精华,提升自身思惟方式。
首先,咱们假设要构建的的网络请求库叫作WingjayHttpClient
,那么,做为一个网络请求库,它最基本功能是什么呢?
在我看来应该是:接收用户的请求 -> 发出请求 -> 接收响应结果并返回给用户。
那么从使用者角度而言,须要作的事是:
Request
:在里面设置好目标URL;请求method如GET/POST等;一些header如Host、User-Agent等;若是你在POST上传一个表单,那么还须要body。Request
传递给WingjayHttpClient
。WingjayHttpClient
去执行Request
,并把返回结果封装成一个Response
给用户。而一个Response
里应该包括statusCode如200,一些header如content-type等,可能还有body到此即为一次完整请求的雏形。那么下面咱们来具体实现这三步。
下面咱们先来实现一个httpClient的雏形,只具有最基本的功能。
Request
类首先,咱们要创建一个Request
类,利用Request
类用户能够把本身须要的参数传入进去,基本形式以下:
class Request {
String url;
String method;
Headers headers;
Body requestBody;
public Request(String url, String method, @Nullable Headers headers, @Nullable Body body) {
this.url = url;
...
}
}
复制代码
Request
对象传递给WingjayHttpClient
咱们能够设计WingjayHttpClient
以下:
class WingjayHttpClient { public Response sendRequest(Request request) { return executeRequest(request); } } 复制代码
Request
,并把返回结果封装成一个Response
返回class WingjayHttpClient { ... private Response executeRequest(Request request) { //使用socket来进行访问 Socket socket = new Socket(request.getUrl(), 80); ResponseData data = socket.connect().getResponseData(); return new Response(data); } ... } class Response { int statusCode; Headers headers; Body responseBody ... } 复制代码
利用上面的雏形,能够获得其使用方法以下:
Request request = new Request("http://wingjay.com"); WingjayHttpClient client = new WingjayHttpClient(); Response response = client.sendRequest(request); handle(response); 复制代码
然而,上面的雏形是远远不能胜任常规的应用需求的,所以,下面再来对它添加一些经常使用的功能模块。
通常的request中,每每用户只会指定一个URL和method,这个简单的user request是不足以成为一个http request,咱们还须要为它添加一些header,如Content-Length, Transfer-Encoding, User-Agent, Host, Connection, 和 Content-Type,若是这个request使用了cookie,那咱们还要将cookie添加到这个request中。
咱们能够扩展上面的sendRequest(request)
方法:
[class WingjayHttpClient] public Response sendRequest(Request userRequest) { Request httpRequest = expandHeaders(userRequest); return executeRequest(httpRequest); } private Request expandHeaders(Request userRequest) { if (userRequest.header("Connection") == null) { requestBuilder.header("Connection", "Keep-Alive"); } if (userRequest.header("User-Agent") == null) { requestBuilder.header("User-Agent", Version.userAgent()); } ... } 复制代码
有时咱们请求的URL已经被移走了,此时server会返回301状态码和一个重定向的新URL,此时咱们要可以支持自动访问新URL而不是向用户报错。
对于重定向这里有一个测试性URL:www.publicobject.com/helloworld.… ,经过访问并抓包,能够看到以下信息:
所以,咱们在接收到Response后要根据status_code是否为重定向,若是是,则要从Response Header里解析出新的URL-Location
并自动请求新URL。那么,咱们能够继续改写sendRequest(request)
方法:
[class WingjayHttpClient]
private boolean allowRedirect = true;
// user can set redirect status when building WingjayHttpClient
public void setAllowRedirect(boolean allowRedirect) {
this.allowRedirect = allowRedirect;
}
public Response sendRequest(Request userRequest) {
Request httpRequest = expandHeaders(userRequest);
Response response = executeRequest(httpRequest);
switch (response.statusCode()) {
// 300: multi choice; 301: moven permanently;
// 302: moved temporarily; 303: see other;
// 307: redirect temporarily; 308: redirect permanently
case 300:
case 301:
case 302:
case 303:
case 307:
case 308:
return handleRedirect(response);
default:
return response;
}
}
// the max times of followup request
private static final int MAX_FOLLOW_UPS = 20;
private int followupCount = 0;
private Response handleRedirect(Response response) {
// Does the WingjayHttpClient allow redirect?
if (!client.allowRedirect()) {
return null;
}
// Get the redirecting url
String nextUrl = response.header("Location");
// Construct a redirecting request
Request followup = new Request(nextUrl);
// check the max followupCount
if (++followupCount > MAX_FOLLOW_UPS) {
throw new Exception("Too many follow-up requests: " + followUpCount);
}
// not reach the max followup times, send followup request then.
return sendRequest(followup);
}
复制代码
利用上面的代码,咱们经过获取原始userRequest
的返回结果,判断结果是否为重定向,并作出自动followup处理。
一些经常使用的状态码 100~199:指示信息,表示请求已接收,继续处理 200~299:请求成功,表示请求已被成功接收、理解、接受 300~399:重定向,要完成请求必须进行更进一步的操做 400~499:客户端错误,请求有语法错误或请求没法实现 500~599:服务器端错误,服务器未能实现合法的请求
所谓重试,和重定向很是相似,即经过判断Response
状态,若是链接服务器失败等,那么能够尝试获取一个新的路径进行从新链接,大体的实现和重定向很是相似,此不赘述。
这是很是核心的部分。
经过上面的从新组装request
和重定向机制,咱们能够感觉的,一个request
从user建立出来后,会通过层层处理后,才真正发出去,而一个response
,也会通过各类处理,最终返回给用户。
笔者认为这和网络协议栈很是类似,用户在应用层发出简单的数据,而后通过传输层、网络层等,层层封装后真正把请求从物理层发出去,当请求结果回来后又层层解析,最终把最直接的结果返回给用户使用。
最重要的是,每一层都是抽象的,互不相关的!
所以在咱们设计时,也能够借鉴这个思想,经过设置拦截器Interceptor
,每一个拦截器会作两件事情:
那么,咱们能够为拦截器定义一个抽象接口,而后去实现具体的拦截器。
interface Interceptor { Response intercept(Request request); } 复制代码
你们能够看下上面这个拦截器设计是否有问题?
咱们想象这个拦截器可以接收一个request,进行拦截处理,并返回结果。
但实际上,它没法返回结果,并且它在处理request后,并不能继续向下传递,由于它并不知道下一个Interceptor
在哪里,也就没法继续向下传递。
那么,如何解决才能把全部Interceptor
串在一块儿,并可以依次传递下去。
public interface Interceptor { Response intercept(Chain chain); interface Chain { Request request(); Response proceed(Request request); } } 复制代码
使用方法以下:假如咱们如今有三个Interceptor
须要依次拦截:
// Build a full stack of interceptors. List<Interceptor> interceptors = new ArrayList<>(); interceptors.add(new MyInterceptor1()); interceptors.add(new MyInterceptor2()); interceptors.add(new MyInterceptor3()); Interceptor.Chain chain = new RealInterceptorChain( interceptors, 0, originalRequest); chain.proceed(originalRequest); 复制代码
里面的RealInterceptorChain
的基本思想是:咱们把全部interceptors
传进去,而后chain
去依次把request
传入到每个interceptors
进行拦截便可。
经过下面的示意图能够明确看出拦截流程:
其中,RetryAndFollowupInterceptor
是用来作自动重试和自动重定向的拦截器;BridgeInterceptor
是用来扩展request
的header
的拦截器。这两个拦截器存在于okhttp
里,实际上在okhttp
里还有好几个拦截器,这里暂时不作深刻分析。
CacheInterceptor
这是用来拦截请求并提供缓存的,当request进入这一层,它会自动去检查缓存,若是有,就直接返回缓存结果;不然的话才将request继续向下传递。并且,当下层把response返回到这一层,它会根据需求进行缓存处理;
ConnectInterceptor
这一层是用来与目标服务器创建链接
CallServerInterceptor
这一层位于最底层,直接向服务器发出请求,并接收服务器返回的response,并向上层层传递。
上面几个都是okhttp自带的,也就是说须要在WingjayHttpClient
本身实现的。除了这几个功能性的拦截器,咱们还要支持用户自定义拦截器
,主要有如下两种(见图中非虚线框蓝色字部分):
interceptors
这里的拦截器是拦截用户最原始的request。
NetworkInterceptor
这是最底层的request拦截器。
如何区分这两个呢?举个例子,我建立两个LoggingInterceptor
,分别放在interceptors
层和NetworkInterceptor
层,而后访问一个会重定向的URL_1
,当访问完URL_1
后会再去访问重定向后的新地址URL_2
。对于这个过程,interceptors
层的拦截器只会拦截到URL_1
的request,而在NetworkInterceptor
层的拦截器则会同时拦截到URL_1
和URL_2
两个request。具体缘由能够看上面的图。
这是很是核心的部分。
经过上面的工做,咱们修改WingjayHttpClient
后获得了下面的样子:
class WingjayHttpClient { public Response sendRequest(Request userRequest) { Request httpRequest = expandHeaders(userRequest); Response response = executeRequest(httpRequest); switch (response.statusCode()) { // 300: multi choice; 301: moven permanently; // 302: moved temporarily; 303: see other; // 307: redirect temporarily; 308: redirect permanently case 300: case 301: case 302: case 303: case 307: case 308: return handleRedirect(response); default: return response; } } private Request expandHeaders(Request userRequest) {...} private Response executeRequest(Request httpRequest) {...} private Response handleRedirect(Response response) {...} } 复制代码
也就是说,WingjayHttpClient
如今可以同步
地处理单个Request
了。
然而,在实际应用中,一个WingjayHttpClient
可能会被用于同时处理几十个用户request,并且这些request里还分红了同步
和异步
两种不一样的请求方式,因此咱们显然不能简单把一个request直接塞给WingjayHttpClient
。
咱们知道,一个request除了上面定义的http协议相关的内容,还应该要设置其处理方式同步
和异步
。那这些信息应该存在哪里呢?两种选择:
直接放入Request
从理论上来说是能够的,可是却违背了初衷。咱们最开始是但愿用Request
来构造符合http协议的一个请求,里面应该包含的是请求目标网址URL,请求端口,请求方法等等信息,而http协议是不关心这个request是同步仍是异步之类的信息
建立一个类,专门来管理Request
的状态 这是更为合适的,咱们能够更好的拆分职责。
所以,这里选择建立两个类SyncCall
和AsyncCall
,用来区分同步
和异步
。
class SyncCall { private Request userRequest; public SyncCall(Request userRequest) { this.userRequest = userRequest; } } class AsyncCall { private Request userRequest; private Callback callback; public AsyncCall(Request userRequest, Callback callback) { this.userRequest = userRequest; this.callback = callback; } interface Callback { void onFailure(Call call, IOException e); void onResponse(Call call, Response response) throws IOException; } } 复制代码
基于上面两个类,咱们的使用场景以下:
WingjayHttpClient client = new WingjayHttpClient(); // Sync Request syncRequest = new Request("http://wingjay.com"); SyncCall syncCall = new SyncCall(request); Response response = client.sendSyncCall(syncCall); handle(response); // Async AsyncCall asyncCall = new AsyncCall(request, new CallBack() { @Override public void onFailure(Call call, IOException e) {} @Override public void onResponse(Call call, Response response) throws IOException { handle(response); } }); client.equeueAsyncCall(asyncCall); 复制代码
从上面的代码能够看到,WingjayHttpClient
的职责发生了变化:之前是response = client.sendRequest(request);,而如今变成了
response = client.sendSyncCall(syncCall);
client.equeueAsyncCall(asyncCall);
复制代码
那么,咱们也须要对WingjayHttpClient
进行改造,基本思路是在内部添加请求池
来对全部request进行管理。那么这个请求池
咱们怎么来设计呢?有两个方法:
直接在WingjayHttpClient
内部建立几个容器 一样,从理论上而言是可行的。当用户把(a)syncCall传给client后,client自动把call存入对应的容器进行管理。
建立一个独立的类进行管理 显然这样能够更好的分配职责。咱们把WingjayHttpClient
的职责定义为,接收一个call,内部进行处理后返回结果。这就是WingjayHttpClient
的任务,那么具体如何去管理这些request的执行顺序和生命周期,天然不须要由它来管。
所以,咱们建立一个新的类:Dispatcher
,这个类的做用是:
SyncCall
和AsyncCall
,若是用户想取消则能够遍历全部的call进行cancel操做;SyncCall
,因为它是即时运行的,所以Dispatcher
只须要在SyncCall
运行前存储进来,在运行结束后移除便可;AsyncCall
,Dispatcher
首先启动一个ExecutorService,不断取出AsyncCall
去进行执行,而后,咱们设置最多执行的request数量为64,若是已经有64个request在执行中,那么就将这个asyncCall存入等待区。根据设计能够获得Dispatcher
构造:
class Dispatcher { // sync call private final Deque<SyncCall> runningSyncCalls = new ArrayDeque<>(); // async call private int maxRequests = 64; private final Deque<AsyncCall> waitingAsyncCalls = new ArrayDeque<>(); private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>(); private ExecutorService executorService; // begin execute Sync call public void startSyncCall(SyncCall syncCall) { runningSyncCalls.add(syncCall); } // finish Sync call public void finishSyncCall(SyncCall syncCall) { runningSyncCalls.remove(syncCall); } // enqueue a new AsyncCall public void enqueue(AsyncCall asyncCall) { if (runningAsyncCalls.size() < 64) { // run directly runningAsyncCalls.add(asyncCall); executorService.execute(asyncCall); } else { readyAsyncCalls.add(asyncCall); } } // finish a AsyncCall public void finishAsyncCall(AsyncCall asyncCall) { runningAsyncCalls.remove(asyncCall); } } 复制代码
有了这个Dispatcher
,那咱们就能够去修改WingjayHttpClient
以实现
response = client.sendSyncCall(syncCall);
client.equeueAsyncCall(asyncCall);
复制代码
这两个方法了。具体实现以下
[class WingjayHttpClient]
private Dispatcher dispatcher;
public Response sendSyncCall(SyncCall syncCall) {
try {
// store syncCall into dispatcher;
dispatcher.startSyncCall(syncCall);
// execute
return sendRequest(syncCall.getRequest());
} finally {
// remove syncCall from dispatcher
dispatcher.finishSyncCall(syncCall);
}
}
public void equeueAsyncCall(AsyncCall asyncCall) {
// store asyncCall into dispatcher;
dispatcher.enqueue(asyncCall);
// it will be removed when this asyncCall be executed
}
复制代码
基于以上,咱们可以很好的处理同步
和异步
两种请求,使用场景以下:
WingjayHttpClient client = new WingjayHttpClient(); // Sync Request syncRequest = new Request("http://wingjay.com"); SyncCall syncCall = new SyncCall(request); Response response = client.sendSyncCall(syncCall); handle(response); // Async AsyncCall asyncCall = new AsyncCall(request, new CallBack() { @Override public void onFailure(Call call, IOException e) {} @Override public void onResponse(Call call, Response response) throws IOException { handle(response); } }); client.equeueAsyncCall(asyncCall); 复制代码
到此,咱们基本把okhttp
里核心的机制都讲解了一遍,相信读者对于okhttp的总体结构和核心机制都有了较为详细的了解。
业务的快速增加离不开稳定可靠的架构。《亿级Android架构》小专栏会基于做者实际工做经验,结合国内大厂如阿里、腾讯、美团等基础架构现状,尝试谈谈如何设计一套好的架构来支持业务从0到1,甚至到亿,但愿与你们多多探讨。
本专栏主要内容:
《亿级Android架构》小专栏文章列表:
若是有问题欢迎联系我,或者关注个人公众号:wingjay。
谢谢。