一、简介
OkHttp是一个高效的HTTP客户端,它有以下默认特性:
- 支持HTTP/2,允许所有同一个主机地址的请求共享同一个socket连接
- 连接池减少请求延时
- 透明的GZIP压缩减少响应数据的大小
- 缓存响应内容,避免一些完全重复的请求
当网络出现问题的时候OkHttp依然坚守自己的职责,它会自动恢复一般的连接问题,如果你的服务有多个IP地址,当第一个IP请求失败时,OkHttp会交替尝试你配置的其他IP。
它的请求/响应 API 使用构造器模式builders来设计,它支持阻塞式的同步请求和带回调的异步请求。
二、基本使用方式
在OKHttp,每次网络请求就是一个Request,我们在Request里填写我们需要的url,header等其他参数,再通过Request构造出Call,Call内部去请求服务器,得到回复,并将结果告诉调用者。同时okhttp提供了同步和异步两种方式进行网络操作。
1、同步Get
private void executeRequest() {
String url = "http://wwww.baidu.com";
OkHttpClient okHttpClient = new OkHttpClient();
final Request request
= new Request.Builder()
.url(url)
.build();
final Call call = okHttpClient.newCall(request);
new Thread(new Runnable() {
@Override
public void run() {
try {
Response response = call.execute();
Log.d("HttpExecuteRequest", "run: " + response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
2、异步Get
private void getAsynHttp() {
okHttpClient = new OkHttpClient();
Request.Builder requestBuilder = new Request.Builder().url("http://www.baidu.com");
requestBuilder.method("GET", null);
Request request = requestBuilder.build();
Call call = okHttpClient.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 (null != response.cacheResponse()) {
String str = response.cacheResponse().toString();
Log.i("OkHttp getAsyn", "cache---" + str);
} else {
response.body().string();
String str = response.networkResponse().toString();
Log.i("OkHttp getAsyn", "network---" + str);
}
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(), "请求成功", Toast.LENGTH_SHORT).show();
}
});
}
});
}
3、异步Post
private void postAsynHttp() {
okHttpClient = new OkHttpClient();
RequestBody formBody = new FormBody.Builder()
.add("size", "10")
.build();
Request request = new Request.Builder()
.url("http://api.1-blog.com/biz/bizserver/article/list.do")
.post(formBody)
.build();
Call call = okHttpClient.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();
resopnseString = str;
Log.i("OkHttp postAsyn", str);
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(), "请求成功", Toast.LENGTH_SHORT).show();
}
});
}
});
}
4、异步上传文件
private void postAsynFile() {
okHttpClient = new OkHttpClient();
File file = new File("/sdcard/kwm.txt");
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
.build();
okHttpClient.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("OkHttp postAsynFile", response.body().string());
resopnseString = response.body().string();
}
});
}
5、异步下载文件
private void downAsynFile() {
okHttpClient = new OkHttpClient();
String url = "https://img-my.csdn.net/uploads/201603/26/1458988468_5804.jpg";
Request request = new Request.Builder().url(url).build();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) {
InputStream inputStream = response.body().byteStream();
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(new File("/sdcard/wangshu.jpg"));
byte[] buffer = new byte[2048];
int len = 0;
while ((len = inputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, len);
}
fileOutputStream.flush();
} catch (IOException e) {
Log.i("OkHttp downAsynFile", "IOException");
e.printStackTrace();
}
Log.d("OkHttp downAsynFile", "文件下载成功");
}
});
}
6、异步上传Multipart文件
private void sendMultipart() {
okHttpClient = new OkHttpClient();
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("title", "this is a title")
.addFormDataPart("image", "image.jpg",
RequestBody.create(MEDIA_TYPE_PNG, new File("/sdcard/image.jpg")))
.build();
Request request = new Request.Builder()
.header("Authorization", "Client-ID " + "...")
.url("https://api.imgur.com/3/image")
.post(requestBody)
.build();
okHttpClient.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("OkHttp downAsynFile", response.body().string());
}
});
}
三、核心源码分析
Okhttp四个核心点:任务,拦截器,缓存,复用连接池
在OkHttp框架下一个网络请求需要经历的过程:
1、任务
查看源码,进行execute或者enqueue操作,当我们调用newCall实际返回的是一个RealCall类。
@Override public Call newCall(Request request) {
return new RealCall(this, request);
}
我们调用enqueue异步请求网络实际上是调用了RealCall的enqueue方法
void enqueue(Callback responseCallback, boolean forWebSocket) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
client.dispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket));
}
可以看到,请求是通过Dispatcher实现任务调度的
Dispatcher主要用于控制并发的请求,它主要维护了以下变量:
/** 最大并发请求数*/
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<>();
Dispatcher有两个构造函数,可以使用自己设定线程池,如果没有设定线程池则会在请求网络前自己创建线程池,这个线程池类似于CachedThreadPool比较适合执行大量的耗时比较少的任务。
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;
}
Dispatcher执行enqueue方法:
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
当正在运行的异步请求队列中的数量小于64并且正在运行的请求主机数小于5时则把请求加载到runningAsyncCalls中并在线程池中执行,否则就再入到readyAsyncCalls中进行缓存等待。
线程池中传进来的参数就是AsyncCall它是RealCall的内部类,内部也实现了execute方法:
@Override protected void execute() {
boolean signalledCallback = false;
try {
Response response = getResponseWithInterceptorChain(forWebSocket);
if (canceled) {
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!
logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e);
} else {
responseCallback.onFailure(RealCall.this, e);
}
} finally {
client.dispatcher().finished(this);
}
}
最后一行, 无论这个请求的结果如何都会执行client.dispatcher().finished(this)
/** Used by {@code AsyncCall#run} to signal completion. */
synchronized void finished(AsyncCall call) {
if (!runningAsyncCalls.remove(call)) throw new AssertionError("AsyncCall wasn't running!");
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.
}
}
从promoteCalls方法,我们可以看到从readyAsyncCalls取出下一个请求,并加入runningAsyncCalls中并交由线程池处理。
2、拦截器
从上面我们可以看到,同步和异步都会殊途同归,执行execute中的getResponseWithInterceptorChain方法。
private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException {
Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket);
return chain.proceed(originalRequest);
}
ApplicationInterceptorChain,它是一个拦截器链,这个类也是RealCall的内部类,接下来执行了它的proceed方法:
@Override public Response proceed(Request request) throws IOException {
// If there's another interceptor in the chain, call that.
if (index < client.interceptors().size()) {
Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
Interceptor interceptor = client.interceptors().get(index);
Response interceptedResponse = interceptor.intercept(chain);
if (interceptedResponse == null) {
throw new NullPointerException("application interceptor " + interceptor
+ " returned null");
}
return interceptedResponse;
}
// No more interceptors. Do HTTP.
return getResponse(request, forWebSocket);
}
proceed方法每次从拦截器列表中取出拦截器,当存在多个拦截器时都会在第七行阻塞,并等待下一个拦截器的调用返回。拦截器主要用来观察,修改以及可能短路的请求输出和响应的回来。通常情况下拦截器用来添加,移除或者转换请求或者响应的头部信息。
3、缓存
getResponse方法比较长,其中创建了HttpEngine并调用了sendRequest和readResponse方法。
...
try {
engine.sendRequest();
engine.readResponse();
releaseConnection = false;
} catch (RequestException e) {
// The attempt to interpret the request failed. Give up.
throw e.getCause();
} catch (RouteException e) {
// The attempt to connect via a route failed. The request will not have been sent.
HttpEngine retryEngine = engine.recover(e.getLastConnectException(), null);
if (retryEngine != null) {
releaseConnection = false;
engine = retryEngine;
continue;
}
// Give up; recovery is not possible.
throw e.getLastConnectException();
} catch (IOException e) {
// An attempt to communicate with a server failed. The request may have been sent.
HttpEngine retryEngine = engine.recover(e, null);
if (retryEngine != null) {
releaseConnection = false;
engine = retryEngine;
continue;
}
// Give up; recovery is not possible.
throw e;
} finally {
// We're throwing an unchecked exception. Release any resources.
if (releaseConnection) {
StreamAllocation streamAllocation = engine.close();
streamAllocation.release();
}
}
...
sendRequest显然是在发送请求,查看代码会发现,里面做了缓存的策略。根据cacheStrategy的处理来判断缓存是否可用或者需要最新数据。
当请求失败时,会执行HttpEngine的recover方法,完成重连.
4、复用连接池
连接池的类位于okhttp3.ConnectionPool:
private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));
/** The maximum number of idle connections for each address. */
//空闲的socket最大连接数
private final int maxIdleConnections;
//socket的keepAlive时间
private final long keepAliveDurationNs;
// 双向队列
private final Deque<RealConnection> connections = new ArrayDeque<>();
final RouteDatabase routeDatabase = new RouteDatabase();
boolean cleanupRunning;
executor线程池,类似于CachedThreadPool,采用没有容量的SynchronousQueue
Deque,双向队列,双端队列同时具有队列和栈性质,经常在缓存中被使用,里面维护了RealConnection也就是socket物理连接的包装。
RouteDatabase,它用来记录连接失败的Route的黑名单,当连接失败的时候就会把失败的线路加进去。
构造函数:
public ConnectionPool() {
this(5, 5, TimeUnit.MINUTES);
}
通过构造函数可以看出ConnectionPool默认的空闲的socket最大连接数为5个,socket的keepAlive时间为5分钟。
在okhttp中,使用了类似于引用计数的方式跟踪Socket流的调用,计数对象是StreamAllocation,它被反复执行aquire与release操作。增加和减少引用的计数。如果计数为0的话,说明此连接没有被使用就是空闲的,通过算法实现回收,如果计数不为0,则表示上层代码仍然引用,就不需要关闭连接。
在ConnectionPool中:put、get、connectionBecameIdle和evictAll分别对应放入连接、获取连接、移除连接和移除所有连接.
/** Returns a recycled connection to {@code address}, or null if no such connection exists. */
RealConnection get(Address address, StreamAllocation streamAllocation) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (connection.allocations.size() < connection.allocationLimit
&& address.equals(connection.route().address)
&& !connection.noNewStreams) {
streamAllocation.acquire(connection);
return connection;
}
}
return null;
}
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
移除连接,则是从队列对realconnettion 进行remove。put之前清理空闲的线程(回收连接),get方法会遍历connections缓存列表,当某个连接计数的次数小于限制的大小并且request的地址和缓存列表中此连接的地址完全匹配。则直接复用缓存列表中的connection作为request的连接。
回收主要是要cleanup线程去实现的,核心代码如下:
// 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;
}
}
根据连接中的引用计数来计算空闲连接数和活跃连接数,然后标记出空闲的连接,如果空闲连接keepAlive时间超过5分钟,或者空闲连接数超过5个,则从Deque中移除此连接。接下来根据空闲连接或者活跃连接来返回下次需要清理的时间数:如果空闲连接大于0则返回此连接即将到期的时间,如果都是活跃连接并且大于0则返回默认的keepAlive时间5分钟,如果没有任何连接则跳出循环并返回-1。
在上述代码中的第13行,通过pruneAndGetAllocationCount方法来判断连接是否闲置的,如果pruneAndGetAllocationCount方法返回值大于0则是空闲连接,否则就是活跃连接。