本文为过往笔记整理, 在此只作记录,不做严谨的技术分享。
OkHttp使用详解
OkHttp使用完全教程
深入浅出安卓热门网络框架 OkHttp3 和 Retrofit 原理
OkHttp3 源码解析 连接池的复用
OkHttp框架源码解析 - 多路复用
OKHTTP3源码和设计模式(上篇)
OKHTTP3源码和设计模式(下篇)
使用
- 默认GET、request.method()配置其它类型
//GET
OkHttpClient client = new OkHttpClient.Builder()
.build();
Request request = new Request.Builder()
.url(Api.COMMON_LIST)
.build();
Call call = client.newCall(request);
call.enqueue(new Callback() {});
//POST
OkHttpClient client = new OkHttpClient.Builder()
.build();
FormBody formBody = new FormBody.Builder()
.add("username", "10022000000")
.add("password", "111111")
.build();
Request request = new Request.Builder()
.post(formBody)
.url(Api.SIGN_IN)
.build();
Call call = client.newCall(request);
call.enqueue(new Callback() {});
源码
不用特意记它的流程,这些看源码都能找到的
- OKHttpClient:配置:拦截器、代理、cookie...
- Request:配置:url、method、headers...
- RealCall:
- call.enqueue/execute:进行分发、处理listener等
- execute()
- 直接调用getResponseWithInterceptorChain
- enqueue(callback)
- 包装成AsyncCall(--> Runnable)
- 线程池中执行
- asyncCall.run --> asyncCall.execute中调用getResponseWithInterceptorChain
execute
//call.execute
<!--#RealCall-->
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); //--> Dispatcher#runningSyncCalls.add(call);
//call并不会在拦截器中执行,是用来计数、判断等操作的
return getResponseWithInterceptorChain();
} finally {
client.dispatcher().finished(this);// 从runningSyncCalls中移除
}
}
enqueue
- 异步时会有
Dispatcher# maxRequests = 64; maxRequestsPerHost = 5限制
//call.enqueue
<!--#RealCall-->
public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
transmitter.callStart();
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
<!--#Dispatcher-->
void enqueue(AsyncCall call) {
synchronized (this) {
readyAsyncCalls.add(call);
...
}
promoteAndExecute(); //--> runningAsyncCalls.add(asyncCall);
}
private boolean promoteAndExecute() {
assert (!Thread.holdsLock(this));
List<AsyncCall> executableCalls = new ArrayList<>();
boolean isRunning;
synchronized (this) {
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall asyncCall = i.next();
if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.
i.remove();
asyncCall.callsPerHost().incrementAndGet();
executableCalls.add(asyncCall);
runningAsyncCalls.add(asyncCall);
}
isRunning = runningCallsCount() > 0;
}
for (int i = 0, size = executableCalls.size(); i < size; i++) {
AsyncCall asyncCall = executableCalls.get(i);
asyncCall.executeOn(executorService()/**线程池*/);
}
return isRunning;
}
<!--#RealCall.AsyncCall-->
final class AsyncCall extends NamedRunnable {
void executeOn(ExecutorService executorService) {
try {
//放入线程池,子线程执行run方法调用下面的execute
executorService.execute(this);
} catch (RejectedExecutionException e) {
responseCallback.onFailure(RealCall.this, ioException);
} finally {
client.dispatcher().finished(this); //从runningAsyncCalls中移除
}
}
protected void execute() {
try {
Response response = getResponseWithInterceptorChain();
responseCallback.onResponse(RealCall.this, response);
} catch (IOException e) {
responseCallback.onFailure(RealCall.this, e);
} finally {
client.dispatcher().finished(this);
}
}
}
拦截器
七种拦截器:
- addInterceptor(Interceptor),
应用拦截器,开发者设置,在所有的拦截器处理之前进行最早的拦截处理,比如一些公共参数,Header都可以在这里添加。 - RetryAndFollowUpInterceptor,对连接做一些初始化工作,以及请求失败的重试工作,重定向的后续请求工作。
- BridgeInterceptor,为用户构建一个能够进行网络访问的请求(比如添加content-type、content-length、User-Agent、Host、Cookie、gzip等),同时后续工作将网络请求回来的响应转化为用户可用的Response,以及使用Cookie存取。
- CacheInterceptor,处理cache相关处理,会根据OkHttpClient对象的配置以及缓存策略对请求值进行缓存,而且如果本地有了可⽤的Cache,就可以在没有网络交互的情况下就返回缓存结果。
- ConnectInterceptor,负责建立连接了,会建立TCP连接或者TLS连接,以及负责编码解码的HttpCode。
- networkInterceptors,
网络拦截器,开发者设置,所以本质上和第一个拦截器差不多,但是由于位置不同,用处也不同。这个位置添加的拦截器可以看到请求和响应的数据了,所以可以做一些网络调试。 - CallServerInterceptor,进行网络数据的请求和响应了,也就是实际的网络I/O操作,通过socket读写数据。
应重桥缓连网请
Response getResponseWithInterceptorChain() throws IOException {
List<Interceptor> interceptors = new ArrayList<>();
//自定义:应用程序拦截器
interceptors.addAll(client.interceptors());
//重连重定向拦截器
interceptors.add(new RetryAndFollowUpInterceptor(client));
//桥接拦截器
interceptors.add(new BridgeInterceptor(client.cookieJar()));
//缓存拦截器
interceptors.add(new CacheInterceptor(client.internalCache()));
//连接拦截器
interceptors.add(new ConnectInterceptor(client));
//自定义:网络拦截器
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
//请求拦截器
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
originalRequest, this, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
try {
Response response = chain.proceed(originalRequest);
return response;
} catch (IOException e) {
} finally {
}
}
RetryAndFollowUpInterceptor
重连重定向拦截器:对连接做一些初始化工作,以及请求失败的重试工作,重定向的后续请求工作。
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Transmitter transmitter = realChain.transmitter();
int followUpCount = 0;
Response priorResponse = null;
while (true) {
transmitter.prepareToConnect(request);
// 处理取消事件
if (transmitter.isCanceled()) {
throw new IOException("Canceled");
}
Response response;
boolean success = false;
try {
response = realChain.proceed(request, transmitter, null);
success = true;
} catch (RouteException e) {
// 判断是否满足重定向条件,满足则重试,不满足就抛异常
// The attempt to connect via a route failed. The request will not have been sent.
if (!recover(e.getLastConnectException(), transmitter, false, request)) {
throw e.getFirstConnectException();
}
continue;
} catch (IOException e) {
// 与服务器连接失败,检查是否满足重定向条件,满足则重试,不满足就抛异常
// An attempt to communicate with a server failed. The request may have been sent.
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, transmitter, requestSendStarted, request)) throw e;
continue;
} finally {
// 检测到其他未知异常,则释放连接和资源
// The network call threw an exception. Release any resources.
if (!success) {
transmitter.exchangeDoneDueToException();
}
}
// 绑定上一个Response,指定body为空
// Attach the prior response if it exists. Such responses never have a body.
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build();
}
Exchange exchange = Internal.instance.exchange(response);
Route route = exchange != null ? exchange.connection().route() : null;
// 根据响应码处理请求,返回Request不为空时则进行重定向处理
Request followUp = followUpRequest(response, route);
if (followUp == null) {
if (exchange != null && exchange.isDuplex()) {
transmitter.timeoutEarlyExit();
}
return response;
}
RequestBody followUpBody = followUp.body();
if (followUpBody != null && followUpBody.isOneShot()) {
return response;
}
closeQuietly(response.body());
if (transmitter.hasExchange()) {
exchange.detachWithViolence();
}
// 限制重定向次数最多为20,超过则抛出异常
if (++followUpCount > MAX_FOLLOW_UPS) {
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
request = followUp;
priorResponse = response;
}
}
1.1 重试
private boolean recover(IOException e, Transmitter transmitter,
boolean requestSendStarted, Request userRequest) {
// The application layer has forbidden retries.
if (!client.retryOnConnectionFailure()) return false;
// We can't send the request body again.
if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;
// This exception is fatal.
if (!isRecoverable(e, requestSendStarted)) return false;
// No more routes to attempt.
if (!transmitter.canRetry()) return false;
// For failure recovery, use the same route selector with a new connection.
return true;
}
1.2 资源重定向
客户端向服务器发送一个请求,获取对应的资源,服务器收到请求后,发现请求的这个资源实际放在另一个位置,于是服务器在返回的响应头的Location字段中写入那个请求资源的正确的URL,并设置reponse的状态码为30x 。
private Request followUpRequest(Response userResponse, @Nullable Route route) throws IOException {
if (userResponse == null) throw new IllegalStateException();
int responseCode = userResponse.code();
final String method = userResponse.request().method();
switch (responseCode) {
// 407
case HTTP_PROXY_AUTH:
// 代理认证
Proxy selectedProxy = route != null
? route.proxy()
: client.proxy();
if (selectedProxy.type() != Proxy.Type.HTTP) {
throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
}
return client.proxyAuthenticator().authenticate(route, userResponse);
// 401
case HTTP_UNAUTHORIZED:
// 身份认证
return client.authenticator().authenticate(route, userResponse);
// 308
case HTTP_PERM_REDIRECT:
// 307
case HTTP_TEMP_REDIRECT:
// 如果是307、308 这两种状态码
// 不对GET、HEAD 以外的请求重定向
if (!method.equals("GET") && !method.equals("HEAD")) {
return null;
}
// fall-through
// 300
case HTTP_MULT_CHOICE:
// 301
case HTTP_MOVED_PERM:
// 302
case HTTP_MOVED_TEMP:
// 303
case HTTP_SEE_OTHER:
// 客户端关闭重定向
if (!client.followRedirects()) return null;
// 从响应头中获取Location字段
String location = userResponse.header("Location");
if (location == null) return null;
HttpUrl url = userResponse.request().url().resolve(location);
// Don't follow redirects to unsupported protocols.
if (url == null) return null;
// If configured, don't follow redirects between SSL and non-SSL.
boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
if (!sameScheme && !client.followSslRedirects()) return null;
// 重建一个新的Request
Request.Builder requestBuilder = userResponse.request().newBuilder();
if (HttpMethod.permitsRequestBody(method)) {
final boolean maintainBody = HttpMethod.redirectsWithBody(method);
if (HttpMethod.redirectsToGet(method)) {
requestBuilder.method("GET", null);
} else {
RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
requestBuilder.method(method, requestBody);
}
if (!maintainBody) {
requestBuilder.removeHeader("Transfer-Encoding");
requestBuilder.removeHeader("Content-Length");
requestBuilder.removeHeader("Content-Type");
}
}
// When redirecting across hosts, drop all authentication headers. This
// is potentially annoying to the application layer since they have no
// way to retain them.
if (!sameConnection(userResponse.request().url(), url)) {
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();
// 408
case HTTP_CLIENT_TIMEOUT:
// 需要发送一次相同的请求
// ......
// 503
case HTTP_UNAVAILABLE:
if (userResponse.priorResponse() != null
&& userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
// We attempted to retry and got another timeout. Give up.
return null;
}
if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
// specifically received an instruction to retry without delay
return userResponse.request();
}
return null;
default:
return null;
}
}
当重定向时,如果想要根据某个状态码或重定向地址,自定义操作,可以自定义拦截器:
- 路径重定向
- http --> https重定向
Okhttp配置:禁止重定向或自定义重定向拦截器时使用
new OkHttpClient().newBuilder()
.followRedirects(false) //禁制OkHttp的重定向操作,我们自己处理重定向
.followSslRedirects(false)//https的重定向也可处理
自定义拦截器:
public class RedirectInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
okhttp3.Request request = chain.request();
Response response = chain.proceed(request);
int code = response.code();
if (code == 307) {
//获取重定向的地址
String location = response.headers().get("Location");
LogUtils.e("重定向地址:", "location = " + location);
//重新构建请求
Request newRequest = request.newBuilder().url(location).build();
response = chain.proceed(newRequest);
}
return response;
}
}
BridgeInterceptor
桥拦截器:为用户构建一个能够进行网络访问的请求(比如添加content-type、content-length、User-Agent、Host、Cookie、gzip等),同时后续工作将网络请求回来的响应转化为用户可用的Response,以及使用Cookie存取。
- cookieJar默认使用CookieJar.NO_COOKIES,所以OKHttp默认不支持cookie
public final class BridgeInterceptor implements Interceptor {
public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
RequestBody body = userRequest.body();
if (body != null) {
MediaType contentType = body.contentType();
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString());
}
long contentLength = body.contentLength();
if (contentLength != -1) {
requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
requestBuilder.header("Transfer-Encoding", "chunked");
requestBuilder.removeHeader("Content-Length");
}
}
if (userRequest.header("Host") == null) {
requestBuilder.header("Host", hostHeader(userRequest.url(), false));
}
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive");
}
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}
// cookieJar默认使用CookieJar.NO_COOKIES,所以OKHttp默认不支持cookie
List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
if (!cookies.isEmpty()) {
//设置到请求头中
requestBuilder.header("Cookie", cookieHeader(cookies));
}
if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", Version.userAgent());
}
Response networkResponse = chain.proceed(requestBuilder.build());
//保存cookie
HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
Response.Builder responseBuilder = networkResponse.newBuilder()
.request(userRequest);
if (transparentGzip
&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
&& HttpHeaders.hasBody(networkResponse)) {
GzipSource responseBody = new GzipSource(networkResponse.body().source());
Headers strippedHeaders = networkResponse.headers().newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build();
responseBuilder.headers(strippedHeaders);
String contentType = networkResponse.header("Content-Type");
responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
}
return responseBuilder.build();
}
}
使用CookieJar
链接关闭后,服务端不会记录用户的信息,Cookie 的作用就是用于解决 "如何记录客户端的用户信息";
- 键值对:username=John Doe
- 请求时:自动携带,供服务器识别
- 返回时:更新cookie信息;
public interface CookieJar {
/** 内部默认实现,不做任何操作. */
CookieJar NO_COOKIES = new CookieJar() {
//返回结果时,缓存或者持久化cookie
@Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
}
//请求时,获取对应的cookie
@Override public List<Cookie> loadForRequest(HttpUrl url) {
return Collections.emptyList();
}
};
}
Cookie本地化读写
实现CookieJar接口,设置到OkHttpClientBuilder。 Cookie存取方式:
- 本地存储:SP、文件
- 内存:map
CacheInterceptor
缓存拦截器:处理cache相关处理,会根据OkHttpClient对象的配置以及缓存策略对请求值进行缓存,而且如果本地有了可⽤的Cache,就可以在没有网络交互的情况下就返回缓存结果。
缓存机制
客户端和服务端配合,请求和返回中携带各种参数来判断缓存策略;
- 客户端、服务器配合,设置参数Expires、Cache-Control、max-Age等参数;
- 不属于okhttp上的功能,只是通过http协议和DiskLruCache做了处理而已;
强制缓存
- 有缓存、有效-->用缓存
- 没有缓存、失效--> 请求新数据--> 数据放入本地
对比缓存
缓存标识-->服务器返回是否有效
-
有效--> 本地缓存
-
无效--> 返回新数据--> 数据放入本地
-
上传、下载
-
异步:onResponse、onFailed是在子线程工作的
-
如何判断是否联网、WIFI、4G、网速?
-
现在的Android源码网络实现,用的是OKhttp
-
maxRequest:64(最多64个请求)、maxRequestPerHost:5(每个主机最多5个),多的进队列等待。
ConnectInterceptor
负责建立连接了,会建立TCP连接或者TLS连接,以及负责编码解码的HttpCode。
CallServerInterceptor
进行网络数据的请求和响应了,也就是实际的网络I/O操作,通过socket读写数据。
两种自定义拦截器区别
- applications:
- 最开始调用:关注
最原始的请求数据 - 不会因为重定向、缓存等原因不执行
- 只会被调用一次,即使这个响应是从缓存中获取的
- 最开始调用:关注
- network:
- 已经建立链接,可以观察到
真实的请求和响应数据 - 当返回缓存时不被调用
- 已经建立链接,可以观察到
自定义拦截器
- 请求
- 固定参数封装
- 请求数据加密
- 测试/正式url切换
- header动态添加
- 返回数据
- 返回数据读取、封装
- log日志打印
public class TestInterceptor implements Interceptor {
private final Charset UTF8 = Charset.forName("UTF-8");
@Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
/**
* pre:对参数进行封装:校验参数、添加公有参数、打印参数url等信息...
*/
HttpUrl httpUrl = originalRequest.url()
.newBuilder()
.addQueryParameter("key","varmin")
.build();
Request request = originalRequest.newBuilder().url(httpUrl).build();
for (String queryParameterName : request.url().queryParameterNames()) {
Log.d(TAG, "intercept: paramName = " + queryParameterName);
}
Response response = chain.proceed(request);
/**
* after:对返回内容:长度、格式、打印等控制
*/
ResponseBody responseBody = response.body();
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE);
Buffer buffer = source.getBuffer();
Charset charset = UTF8;
MediaType contentType = responseBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
//clone:不会引起callback中responseBody的close
Log.d(TAG, "intercept: response" + buffer.clone().readString(charset));
return response;
}
}
OKio
Tips
response.body().string() 只能调用一次?
- 多次调用:java.lang.IllegalStateException: closed
- 使用buffer.clone方法获取内容
<!--再次读取是报错-->
public long read(Buffer sink, long byteCount) throws IOException {
//...
if (closed) throw new IllegalStateException("closed");
return buffer.read(sink, toRead);
}
<!--读取以后close释放资源-->
@Override
public void close() throws IOException {
if (closed) return;
closed = true;
source.close();
buffer.clear();
}
<!--使用buffer.clone方法获取内容-->
BufferedSource source = response.body().source();
Buffer buffer = source.getBuffer();
Charset charset = UTF8;
MediaType contentType = responseBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
//clone:不会引起callback中responseBody的close
Log.d(TAG, "intercept: response" +buffer.clone().readString(charset));