前言
前面我们分析了第一个拦截器RetryAndFollowUpInterceptor
,他在intercept
方法中调用了chain的proceed,从而触发了下一个拦截器的调用,也就是BridgeInterceptor
,本文将一起分析这个拦截器的作用与实现逻辑。
BridgeInterceptor
由于命名都是英文单词,如果要给一个中文翻译来表示BridgeInterceptor
的话,首先要搞明白他的作用:
/**
* Bridges from application code to network code. First it builds a network request from a user
* request. Then it proceeds to call the network. Finally it builds a user response from the network
* response.
*/
public final class BridgeInterceptor implements Interceptor
总结一下,起作用就是桥接业务层逻辑到网络层代码。将根据开发者的请求内容,构造一个网络请求,然后调用proceed去请求网络,最后从网络响应构造一个向上传递的用户响应体。
如此,我们姑且称BridgeInterceptor
为桥接拦截器
。
拦截器开始处理后,立刻构造了一份新的Request对象,初始化的赋值和原有Request一致。
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
这个模式在OkHttp
中大量出现,后续也会频繁遇到。既然构造的是相同的Reuqest实例,为什么不直接使用还需要克隆
一份呢?
这个问题笔者是这样理解的:
> 桥接器所对应的Request是用于网络请求的,克隆一份后,可以对其做新增,删除等操作,而不会影响到开发者上层构造的实例,者可以保证内部逻辑相对独立,表现为一个黑盒状态,上层不需要关注这些header的变更,只需关注业务层的一些参数。
Header
整个桥接拦截器
的核心逻辑在于对网络请求Request
的构造,涉及一些header的添加与删除操作。关于完整的Http协议的头部定义,可以查阅RFC7231#section-5 。协议的定义内容比较多,而且是因为,简单起见也可以参考维基百科
在这里,我们会涉及的头包括:
- Content-Type
- Content-Length
- Transfer-Encoding
- Host
- Connection
- Accept-Encoding
- Cookie
- User-Agent
- Content-Encoding
请求体相关Header
一个请求可能带有请求体也可能没有请求体,如果是有请求体的,需要处理Content-Type
,Content-Length
,Transfer-Encoding
来告诉服务端我们的请求体是什么类型,长度多少,编码是什么。
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");
}
}
Content-Type
根据 RFC7321#section-3.1.1.5 的定义
Content-Type
属于Representation Metadata
,标识性的元数据。当请求包含请求体时,用Content-Type
来表示它的类型和编码。
例如:
Content-Type: text/html; charset=ISO-8859-4
严格来说客户端只要携带了请求体,都应该正确设置Content-Type
,一遍服务端解析,但是如果客户端确实不知道数据类型,或者么有设置,后端可能默认将理解为application/octet-stream,或者根据内容解析后再判断。
Content-Length
根据RFC7230#section-3.3.2的定义,Content-Length
属于Payload Semantics
以八位字节数组(8位的字节)表示的请求体的长度,代表确切长度,和Transfer-Encoding
是互斥的。
>
Content-Length: 348
Transfer-Encoding
用来将实体安全地传输给用户的编码形式,包括:分块(chunked)、compress、deflate、gzip和identity。
> Transfer-Encoding: chunked
如果一个HTTP消息(请求消息或应答消息)的Transfer-Encoding消息头的值为chunked,那么,消息体由数量未定的块组成,并以最后一个大小为0的块为结束。
每一个非空的块都以该块包含数据的字节数(字节数以十六进制表示)开始,跟随一个CRLF (回车及換行),然后是数据本身,最后块CRLF结束。在一些实现中,块大小和CRLF之间填充有白空格(0x20)。
最后一块是单行,由块大小(0),一些可选的填充白空格,以及CRLF。最后一块不再包含任何数据,但是可以发送可选的尾部,包括消息头字段。
消息最后以CRLF结尾
示例
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked
25
This is the data in the first chunk
1C
and this is the second one
3
con
8
sequence
0
Host
请求的域名,可以省略标准端口号,比如默认http是80,https是443
> Host: en.wikipedia.org:80 Host: en.wikipedia.org
public static String hostHeader(HttpUrl url, boolean includeDefaultPort) {
String host = url.host().contains(":")
? "[" + url.host() + "]"
: url.host();
return includeDefaultPort || url.port() != HttpUrl.defaultPort(url.scheme())
? host + ":" + url.port()
: host;
}
public static int defaultPort(String scheme) {
if (scheme.equals("http")) {
return 80;
} else if (scheme.equals("https")) {
return 443;
} else {
return -1;
}
}
Connection
连接类型,这里如果开发者不指定,那么默认设置为Keep-Alive
,复用连接
RFC7230#section-6.1
>
Connection: Keep-Alive
Connection: Upgrade
Accept-Encoding
接受的编码类型,常见的是gzip和deflate
- deflate – 基于deflate算法(定义于RFC 1951)的压缩,使用zlib数据格式(RFC 1950)封装
- gzip – GNU zip格式(定义于RFC 1952)。此方法截至2011年3月,是应用程序支持最广泛的方法。[5]
OkHttp
默认支持gzip在,这里有个限制,如果开发者设置了断点续传的Range
,那么将不会设置gzip。
// If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
// the transfer stream.
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}
Cookie
浏览器中常说的cookie设置。
> Cookie: $Version=1; Skin=new;
List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
if (!cookies.isEmpty()) {
requestBuilder.header("Cookie", cookieHeader(cookies));
}
User-Agent
客户端标识,比如客户端名称,版本号等。
> User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/21.0
在OkHttp
中这个字段的取值如下:
public static String userAgent() {
return "okhttp/${project.version}";
}
响应gzip处理
前面Content-Encoding
中提到了请求体的编码,响应体也可以有这个字段,用于客户端解析。
> 注意header是大小写不敏感的,习惯上我们一般首字母大写。
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)));
}