OkHttp详解
目录
-
拦截器机制
-
Http状态码
-
重试机制
-
BridgeInterceptor
-
CacheInterceptor
-
ConnectInterceptor
-
CallServerInterceptor
-
Call.cancel
-
各种Post请求的处理
拦截器机制
wiki:Interceptors一张图说明了OkHttp的拦截器机制:
RealCall.java
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 (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
拦截器按顺序如下:
- OkHttpClient.Builder.addInterceptor()方法定义的拦截器;
- 重试拦截器RetryAndFollowUpInterceptor
- BridgeInterceptor
- CacheInterceptor
- ConnectInterceptor
- OkHttpClient.Builder.addNetworkInterceptor()方法定义的拦截器;
- CallServerInterceptor
Http状态码
成功:2xx
重定向:3xx
Client错误:4xx
Server错误:5xx
重试机制
408重试
情景:服务器等待客户端请求超时,状态码408(Request Timeout): 如果服务端返回的Header:"Retry-After", "N",N>0,则表示服务器希望客户端在N秒之后再重试,此时客户端放弃;如果没有Retry-After头,或者值为0,则客户端发起重试;如果重试仍然出现408,放弃。
503重试
重试机制和408类似,规律是:重试的状态码和初始请求的错误码一致,放弃重试,例如服务器第一次和第二次均返回503,那么客户端不会进行第三次请求。但是如果服务器返回的状态码依次是408,503,408,503依次交替,那么客户端最多会重试20次,之后便抛出异常。
RetryAndFollowUpInterceptor.java
public final class RetryAndFollowUpInterceptor implements Interceptor {
/**
* How many redirects and auth challenges should we attempt? Chrome follows 21 redirects; Firefox,
* curl, and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
*/
private static final int MAX_FOLLOW_UPS = 20;
@Override public Response intercept(Chain chain) throws IOException {
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release(true);
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
}
}
3xx重试
OkHttp的300-303都是同样的重试逻辑:当服务端返回的状态码为其中一个,那么解析Response的Header,找到Location字段。若无此字段,放弃,如果有,读取其值;使用这个值构建请求发起重试。
BridgeInterceptor
gzip相关逻辑在这里处理:
- 为App的请求头加上"Accept-Encoding": "gzip";
- 根据服务端的返回头"Content-Encoding",如果其值也等于gzip,则需要进行gzip解压缩再交给App。
post参数也在这里处理,例如构建如下请求
public static final MediaType JSON
= MediaType.get("application/json; charset=utf-8");
RequestBody body = RequestBody.create(JSON, "{\"a\":\"a\"}");
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
BridgeInterceptor会加上如下Header:
Content-Type: application/json; charset=utf-8
Content-Length: 9
另外,如果用户没有设置Host、Connection、User-Agent这两个请求头,BridgeInterceptor也会补充默认的:
Host: localhost:51857
Connection: Keep-Alive
User-Agent: okhttp/3.13.1
CacheInterceptor
缓存机制由CacheInterceptor来实现。
- App调用OkHttpClient.Builder().cache(new Cache(getExternalCacheDir(), 1024 * 1024 * 20))来创建缓存目录;
- 初次请求,当response和request的Cache-Control头的值均不为no-store时,网络请求缓存至本地。
- 再次请求,客户端取缓存,如果有ETag,把ETag值取出来,放到请求头"If-None-Match"供服务端校验,如果服务器核对此值没有修改,将返回304 Not Modify,客户端可以直接使用已有缓存;
- 如果服务端上次指定了Cache-Control: max-age=N,则客户端计算出当前时间是否处在缓存有效的区间,如果是,则不进行网络请求。
- 如果接口返回了Date或者Last-Modified: Sat, 09 Mar 2019 13:06:48 GMT,下一次请求头加上If-Modified-Since: Sat, 09 Mar 2019 13:06:48 GMT,服务端判断超过了这个时间则返回新数据,否则可以返回304 Not Modify,app继续使用缓存。
ConnectInterceptor
ConnectInterceptor的主要目的是找到可用的socket连接。连接池的复用逻辑在这里处理,用到的是http/1.1 keep-alive=true的属性,tcp连接保活。连接(RealConnection)可复用的依据:
- 连接池不为空;
- 连接池空闲,即此连接的上一次客户端-服务器请求应答完毕;
- 请求地址的主机名host相等;
连接池的保活时间,默认5分钟:
public ConnectionPool() {
this(5, 5, TimeUnit.MINUTES);
}
可自定义:
OkHttpClient.Builder().connectionPool(new ConnectionPool(5, 15, TimeUnit.SECONDS));
CallServerInterceptor
CallServerInterceptor负责向输出流写入request,并从输入流读取response。
写入request:
- 写入"GET / HTTP/1.1”;
- 写入空行("\n\r")
- 遍历请求头,按格式"header.name: header.value"依次写入,每对Header换行;
- 换行;
- 写入requestBody,长度等于请求头"Content-Length"的长度。
读response:
- 读取状态行,如"HTTP/1.1 200 OK";
- 逐行读取Header,以":"为分隔符解析成key: value的形式;如"Content-Length: 19";
- 从返回头Content-Length字段读取返回body的长度:例如上面的19;
- 如果返回头"Connection: close”,则关闭连接,否则保持连接(keep-alive);
- 构造Response对象并返回。
- 通过Response.body().string()真正的把流读完。
Call.cancel
一个进行中的请求(RealCall),调用cancel()会关闭socket:
public static void closeQuietly(Socket socket) {
if (socket != null) {
try {
socket.close();
} catch (AssertionError e) {
if (!isAndroidGetsocknameError(e)) throw e;
} catch (RuntimeException rethrown) {
throw rethrown;
} catch (Exception ignored) {
}
}
}
各种Post请求的处理
Post String
如果格式服务器不支持,则服务器将返回415 Unsupported Media Type。
public static final MediaType TEXT
= MediaType.get("text/plain; charset=utf-8");
RequestBody body = RequestBody.create(TEXT, "hello I am a string");
Request request = new Request.Builder()
.url(getUrl())
.post(body)
.build();
Response response = client.newCall(request).execute();
请求头:
Content-Type: text/plain; charset=utf-8
Content-Length: 19
请求body为"hello I am a string"
Post Form
private void doPostForm() {
RequestBody formBody = new FormBody.Builder()
.add("search", "Android")
.add("from", "mwp")
.build();
Request request = new Request.Builder()
.url("https://zh.wikipedia.org/w/index.php")
.post(formBody)
.build();
try {
Response response = client.newCall(request).execute();
Log.d(TAG, "doPostForm: " + response.body().string());
} catch (IOException e) {
Log.e(TAG, "doPostForm: ", e);
}
}
请求头:
Content-Type: application/x-www-form-urlencoded
Content-Length: 23
请求body为"search=Android&from=mwp"。
Post File
public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.get("text/x-markdown; charset=utf-8");
File file = new File(Environment.getExternalStorageDirectory() + "/Download/test.md");
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
.build();
Response response = client.newCall(request).execute();
请求头
Content-Type: text/x-markdown; charset=utf-8
Content-Length: 9
Post Multipart
File f = new File(Environment.getExternalStorageDirectory() + "/Download/test.md");
Log.d(TAG, "doPostMultipart: File:" + f.length());
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("title", "test title")
.addFormDataPart("content", "file_name",
RequestBody.create(TEXT, f))
.build();
Request request = new Request.Builder()
.url(getUrl())
.post(requestBody)
.build();
Response response = client.newCall(request).execute();
请求头:
Content-Type: multipart/form-data; boundary=daa37881-9c17-4e83-99c5-08cdd5627d24
Content-Length: 345