OkHttp 源码分析

时间:2021-11-03 20:55:06

在工作中用到封装HTTP传输的OkHTTP,OkHttp是相对成熟的解决方案,同时也是开源项目。本文将从源码角度看下OkHttp是如何实现一些网络操作的。

HTTP GET:

OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
Request request = new Request.Builder().url(url).build();
Response response = client.newCall(request).execute();
if (response.isSuccessful()) {
return response.body().string();
} else {
throw new IOException("Unexpected code " + response);
}   //enqueue方式
  Call call2 = client.newCall(request);
  call2.enqueue(new Callback() {
@Override
public void onResponse(Response response) throws IOException {
System.out.println("调用enqueue返回的结果:"+response.body().string());
} @Override
public void onFailure(Request request, IOException e) { }
  });
}

  

Request是OkHttp中访问的请求,Builder是辅助类。Response即OkHttp中的响应。

Builder作用见: http://www.cnblogs.com/mywy/p/5103683.html

execute和enqueue方法的区别

1.调用execute方法会立即执行我们的请求,execute()的代码逻辑如下

public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
try {
//立即把本次Call放入 已执双端行队列中
client.getDispatcher().executed(this);
//调用
Response result = getResponseWithInterceptorChain(false);
if (result == null) throw new IOException("Canceled");
return result;
} finally {
//从执行完成队列中移除
client.getDispatcher().finished(this);
}
}

execute的逻辑是首先判断 是否已经执行过,如果已经执行了则直接抛出异常,也就是说一个Call 的实例只能调用一次execute方法。 Call类的注释:

A call is a request that has been prepared for execution. A call can be canceled. As this object represents a single request/response pair (stream), it cannot be executed twice.

如果没有执行过则首先client调用Dispatcher的executed(Call call)方法把本次调用加入已经执行的双端队列中,调用getResponseWithInterceptorChain方法返回Response对象,从已经执行的双端队列中移除本次Call,返回response对象。

2.调用enqueue方法执行代码逻辑如下:

public void enqueue(Callback responseCallback) {
enqueue(responseCallback, false);
} void enqueue(Callback responseCallback, boolean forWebSocket) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
client.getDispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket));
}

  

其实是调用了Call内部的私有enqueue方法,同样的也进行了判断本次Call是否是第一次调用,如果是第一次调用则调用client获取Dispatcher对象,然后调用enqueue(AsyncCall call)进行入队操作。

AsyncCall 是Call的内部类,final修饰,继承了NamedRunnable,而NamedRunnable  实现了Runnable接口,可以指定线程的名称。

NamedRunnbale提供了一个抽象方法execute()来供AsyncCall 实现

AsyncCall 的实现逻辑:

@Override protected void execute() {
boolean signalledCallback = false;
try {
//同样调用getResponseWithInterceptorChain方法来获取Response对象
Response response = getResponseWithInterceptorChain(forWebSocket);
if (canceled) {
signalledCallback = true;
//取消执行的时候调用
responseCallback.onFailure(originalRequest, new IOException("Canceled"));
} else {
signalledCallback = true;
//执行成功的回调
responseCallback.onResponse(response);
}
} catch (IOException e) {
if (signalledCallback) {
// Do not signal the callback twice!
logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e);
} else {
//网络请求失败的时候
responseCallback.onFailure(engine.getRequest(), e);
}
} finally {
//从dispatcher 删除本次Call
client.getDispatcher().finished(this);
}
}
}

通过该段代码可以很清楚的看到我们在通过异步请求的时候传入的回调函数Callback 就是在这里调用的。client.getDispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket));发生了

调用execute的网络请求时序图:

OkHttp 源码分析

在哪里调用等待队列中的请求?

OkHttp 源码分析

当AsyncCall中的execute执行的时候,代码会走到finally块,OkHttpClient 调用getDispatcher()方法获取Dispathcer对象,然后调用finished方法。下面我们看一下finished方法里面的代码逻辑:

synchronized void finished(AsyncCall call) {
//从正在执行队列中移除本次Call
if (!runningCalls.remove(call)) throw new AssertionError("AsyncCall wasn't running!");
///把准备队列中的请求加入到执行队列中,并且执行其他请求
promoteCalls();
}

  

 

promoteCalls的执行逻辑:

//把准备队列中的请求加入到执行队列中,并且执行其他请求
private void promoteCalls() {
//判断是否超过了最大的请求数量(默认为64个)
if (runningCalls.size() >= maxRequests) return; // Already running max capacity.
if (readyCalls.isEmpty()) return; // No ready calls to promote.
//遍历准备队列
for (Iterator<AsyncCall> i = readyCalls.iterator(); i.hasNext(); ) {
AsyncCall call = i.next();
if (runningCallsForHost(call) < maxRequestsPerHost) {
i.remove();
//把准备队列中的请求加入到执行队列中
runningCalls.add(call);
//调用线程池,执行本次线线程
getExecutorService().execute(call);
}
//执行队列已经满了,不再添加执行任务。
if (runningCalls.size() >= maxRequests) return; // Reached max capacity.
}
}

  

 

getResponseWithInterceptorChain方法内部实现逻辑:

private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException {
//实例化一个拦截器
Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket);
//调用proceed方法获取Response对象
return chain.proceed(originalRequest);
}

  

proceed处理逻辑:

核心方法Response getResponse(Request request, boolean forWebSocket)实现逻辑:

Response getResponse(Request request, boolean forWebSocket) throws IOException {
// Copy body metadata to the appropriate request headers.
//post有http请求body
RequestBody body = request.body();
if (body != null) {
//完善http请求头信息添加Content-Type,Content-Length
Request.Builder requestBuilder = request.newBuilder();
MediaType contentType = body.contentType();
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString());
}
//获取请求body的长度
long contentLength = body.contentLength();
if (contentLength != -1) {
//body长度确定 一次性发送给服务器
requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
//body长度不确定设置标记为Transfer-Encoding:chunked
requestBuilder.header("Transfer-Encoding", "chunked");
requestBuilder.removeHeader("Content-Length");
}
request = requestBuilder.build();
}
// Create the initial HTTP engine. Retries and redirects need new engine for each attempt.
//初始化Http 引擎,重新连接,重定向都需要一个新的引擎。
engine = new HttpEngine(client, request, false, false, forWebSocket, null, null, null, null);
//记录重新请求的次数
int followUpCount = 0;
while (true) {
//是否已经取消执行
if (canceled) {
//释放连接
engine.releaseConnection();
throw new IOException("Canceled");
}
try {
//发起请求
engine.sendRequest();
//获取响应
engine.readResponse();
} 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进行连接
HttpEngine retryEngine = engine.recover(e);
if (retryEngine != null) {
//如果恢复过来重新发送请求,重新获取response对象
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) {
//如果恢复过来重新发送请求,重新获取response对象
engine = retryEngine;
continue;
}
// Give up; recovery is not possible.
//抛出异常 无法重新连接
throw e;
}
//获取响应
Response response = engine.getResponse();
//获取重新请求对象
Request followUp = engine.followUpRequest();
//重新发情请求对象为空,说明无需重新构建请求,直接返回响应对象response
if (followUp == null) {
//没有采用websocket,无需进维护连接,释放连接
if (!forWebSocket) {
//释放连接
engine.releaseConnection();
}
//返回response退出循环
return response;
}
//如果重连的次数超过最大连接数这里默认是20,则抛出异常
if (++followUpCount > MAX_FOLLOW_UPS) {
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
if (!engine.sameConnection(followUp.httpUrl())) {
engine.releaseConnection();
}
Connection connection = engine.close();
request = followUp;
//从新构建http引擎
engine = new HttpEngine(client, request, false, false, forWebSocket, connection, null, null,
response);
}
}

  

底层如何建立连接和服务器进行交互?

  • 如何发送请求头?
  • 如何发送请求体?
  • 如何接受服务器响应?

这里要使用到一个关键的接口:Transport

OkHttp 源码分析

可以看出该接口有两个实现类,一个是HttpTransport,支持用来http协议。另一个是FramedTransport用来支持spdy协议。该接口定义了一系列的方法来支持我们

向服务器发送请求头、请求体、创建请求体等。

HttpTransport:

public HttpTransport(HttpEngine httpEngine, HttpConnection httpConnection) {
/传入HttpEngine 对象
this.httpEngine = httpEngine;
//传入HttpConnection 对象
this.httpConnection = httpConnection;
}

  

 

FramedTransport:

  

public FramedTransport(HttpEngine httpEngine, FramedConnection framedConnection) {
//传入HttpEngine 对象
this.httpEngine = httpEngine;
////传入HttpConnection 对象
this.framedConnection = framedConnection;
}

我们可以看到实际上Transport实现类的相关方法是调用HttpConnection(支持Http协议)或FramedConnection(支持spdy协议)来发送请求头、请求体、获取响应对象的。


总结

OKHttpClient是一个基于java优秀的网络请求框架,

  • 支持SPDY, 可以合并多个到同一个主机的请求
  • 使用连接池技术减少请求的延迟(如果SPDY是可用的话)
  • 使用GZIP压缩减少传输的数据量
  • 缓存响应避免重复的网络请求

当你的网络出现拥挤的时候,就是OKHttp 大显身手的时候, 它可以避免常见的网络问题,如果你的服务是部署在不同的IP上面的,如果第一个连接失败, OkHTtp会尝试其他的连接. 这个对现在IPv4+IPv6 中常见的把服务冗余部署在不同的数据中心上.  OkHttp 将使用现在TLS特性(SNI ALPN) 来初始化新的连接. 如果握手失败, 将切换到SLLv3使用OkHttp很容易,   同时支持 异步阻塞请求和回调。我们可以结合自己的项目合理应用起来帮助我们构建健壮的应用程序。

下面整理出一般的OkHttp工具类:

import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicNameValuePair;
import cn.wiz.sdk.constant.WizConstant;
import com.squareup.okhttp.Callback;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response; public class OkHttpUtil {
private static final OkHttpClient mOkHttpClient = new OkHttpClient();
static{
mOkHttpClient.setConnectTimeout(30, TimeUnit.SECONDS);
}
/**
* 该步会开启异步线程。
* @param request
* @return
* @throws IOException
*/
public static Response execute(Request request) throws IOException{
return mOkHttpClient.newCall(request).execute();
}
/**
* 开启异步线程访问网络
* @param request
* @param responseCallback
*/
public static void enqueue(Request request, Callback responseCallback){
mOkHttpClient.newCall(request).enqueue(responseCallback);
}
/**
* 开启异步线程访问网络, 且不在意返回结果(实现空callback)
* @param request
*/
public static void enqueue(Request request){
mOkHttpClient.newCall(request).enqueue(new Callback() { @Override
public void onResponse(Response arg0) throws IOException { } @Override
public void onFailure(Request arg0, IOException arg1) { }
});
}
public static String getStringFromServer(String url) throws IOException{
Request request = new Request.Builder().url(url).build();
Response response = execute(request);
if (response.isSuccessful()) {
String responseUrl = response.body().string();
return responseUrl;
} else {
throw new IOException("Unexpected code " + response);
}
}
private static final String CHARSET_NAME = "UTF-8";
/**
* 这里使用了HttpClinet的API。只是为了方便
* @param params
* @return
*/
public static String formatParams(List<BasicNameValuePair> params){
return URLEncodedUtils.format(params, CHARSET_NAME);
}
/**
* 为HttpGet 的 url 方便的添加多个name value 参数。
* @param url
* @param params
* @return
*/
public static String attachHttpGetParams(String url, List<BasicNameValuePair> params){
return url + "?" + formatParams(params);
}
/**
* 为HttpGet 的 url 方便的添加1个name value 参数。
* @param url
* @param name
* @param value
* @return
*/
public static String attachHttpGetParam(String url, String name, String value){
return url + "?" + name + "=" + value;
}
}