OKHTTP异步和同步请求简单分析
OKHTTP拦截器缓存策略CacheInterceptor的简单分析
OKHTTP拦截器ConnectInterceptor的简单分析
OKHTTP拦截器CallServerInterceptor的简单分析
OKHTTP拦截器BridgeInterceptor的简单分析
OKHTTP拦截器RetryAndFollowUpInterceptor的简单分析
OKHTTP结合官网示例分析两种自定义拦截器的区别
先前分析了 OKHTTP拦截器ConnectInterceptor的简单分析
在 ConnectInterceptor 拦截器的功能就是负责与服务器建立 Socket 连接,并且创建了一个 HttpStream 它包括通向服务器的输入流和输出流。而接下来的 CallServerInterceptor 拦截器的功能使用 HttpStream 与服务器进行数据的读写操作的。
下面就来关注这个拦截器的具体实现。
CallServerInterceptor
该拦截器的作用在上面已经说明了,跟 ConnectInterceptor 一样,我们只需要关注 intercept(Chain chain) 方法的具体实现即可,下面分点了解这个方法做了什么事。
@Override public Response intercept(Chain chain) throws IOException {
//HttpStream 就是先前在 ConnectInterceptor 创建出来的
HttpStream httpStream = ((RealInterceptorChain) chain).httpStream();
StreamAllocation streamAllocation = ((RealInterceptorChain) chain).streamAllocation();
Request request = chain.request();
//发送请求的时间戳
long sentRequestMillis = System.currentTimeMillis();
//写入请求头信息
httpStream.writeRequestHeaders(request);
//写入请求体信息(有请求体的情况)
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
Sink requestBodyOut = httpStream.createRequestBody(request, request.body().contentLength());
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
}
//结束请求
httpStream.finishRequest();
//读取响应头信息
Response response = httpStream.readResponseHeaders()
.request(request)
//这个我不知道是干嘛的?
.handshake(streamAllocation.connection().handshake())
//发送请求的时间
.sentRequestAtMillis(sentRequestMillis)
//接收到响应的时间
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
//openResponseBody 获取响应体信息
if (!forWebSocket || response.code() != 101) {
response = response.newBuilder()
.body(httpStream.openResponseBody(response))
.build();
}
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
streamAllocation.noNewStreams();
}
int code = response.code();
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
//返回一个响应
return response;
}
1.、分析 intercept 方法
根据上面的 intercept 方法大体上可以知道主要做了这样的几件事:
写入请求头信息。
有请求体的情况下,写入请求体信息。
结束请求。
读取响应头信息。
往上一级 ConnectInterceptor 返回一个网络请求回来的 Response。
获取响应体信息输入流
1.1、写入请求头信息
httpStream.writeRequestHeaders(request);
- HttpStream 的实现者 Http1xStream 去做写入请求头信息的操作
@Override public void writeRequestHeaders(Request request) throws IOException {
//获取一段字符串只要包括 请求方式(GET/POST...),http 版本号,请求路径等信息。
String requestLine = RequestLine.get(
request, streamAllocation.connection().route().proxy().type());
//真正地将 headers 的头和 requestLine 写入到输出流中
writeRequest(request.headers(), requestLine);
}
- 下面是真正进行写入头信息的代码了,因为在 ConnectInterceptor 这个拦截器中通过 Socket 与服务器进行了连接并且也返回一个 HttpStream 对象,这个对象就封装了 Sink 输出流和 Source 输入流,它们专门负责与服务器进行读写操作的。
/** Returns bytes of a request header for sending on an HTTP transport. */
public void writeRequest(Headers headers, String requestLine) throws IOException {
if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
//第一行信息写的就是刚才拼接的那段字符串
sink.writeUtf8(requestLine).writeUtf8("\r\n");
//遍历 headers 以 key:value 的方式写入到 sink 输出流中
for (int i = 0, size = headers.size(); i < size; i++) {
sink.writeUtf8(headers.name(i))
.writeUtf8(": ")
.writeUtf8(headers.value(i))
.writeUtf8("\r\n");
}
sink.writeUtf8("\r\n");
state = STATE_OPEN_REQUEST_BODY;
}
1.2、有请求体的情况下,写入请求体信息
我没有用过除了 POST 和 GET 之外的请求方式,这里我假定只有这两种方式吧。下面代码就是用于检测是否需要往服务器中写入请求体信息的代码。
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
Sink requestBodyOut = httpStream.createRequestBody(request, request.body().contentLength());
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
}
1.2.1、检测该请求方式是否支持请求体
-
检测该请求方式是否支持请求体。下面的判断方式都是 || 或的判断,只要请求方式是 POST 那么就可以支持写入请求体信息的功能。
同时还要判断 request.body() != null 因为即使 POST 支持请求体的功能但是还有有 body() 内容才行啊。这个 body() 就是通过构建者模式构建 Request 请求时设置给 Request 的 ReqeustBody 对象。
public static boolean permitsRequestBody(String method) {
return requiresRequestBody(method)
|| method.equals("OPTIONS")
|| method.equals("DELETE") // Permitted as spec is ambiguous.
|| method.equals("PROPFIND") // (WebDAV) without body: request <allprop/>
|| method.equals("MKCOL") // (WebDAV) may contain a body, but behaviour is unspecified
|| method.equals("LOCK"); // (WebDAV) body: create lock, without body: refresh lock
}
public static boolean requiresRequestBody(String method) {
//在这里可以看出,只要是 POST 请求那么就表示它具备请求体。
return method.equals("POST")
|| method.equals("PUT")
|| method.equals("PATCH")
|| method.equals("PROPPATCH") // WebDAV
|| method.equals("REPORT"); // CalDAV/CardDAV (defined in WebDAV Versioning)
}
1.2.2、创建一个可以写入请求体的 Sink 对象
-
如果请求是 GET 请求,那么不需要考虑请求体问题了,因为 GET 请求的内容都在 URL 上。如果是 POST 请求就麻烦一些了,在这里需要考虑将请求体内容通过 Sink 输出流写入到 server 中。
通过 createRequestBody 创建一个 Sink 对象,本质还是使用在 ConnectIntercept 创建的 HttpStream 内部封装 Sink 对象进行写操作的。
根据请求头 Transfer-Encoding 是否为 chunked 的方式,来创建不同 Sink 实现类,如果是 chunked 方式那么就创建 newChunkedSink();如果不是 chunked 就表示内容的大小是固定的,那么就根据 content-length 创建指定大小的 newFixedLengthSink(contentLength) 对象。
@Override public Sink createRequestBody(Request request, long contentLength) {
if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
// Stream a request body of unknown length.
return newChunkedSink();
}
if (contentLength != -1) {
// Stream a request body of a known length.
return newFixedLengthSink(contentLength);
}
throw new IllegalStateException(
"Cannot stream a request body without chunked encoding or a known content length!");
}
将刚才创建的 Sink 对象,也就是 reqyestBodyOut 通过 Okio.buffer() 包装为 BufferedSink 对象,之后进行将 request.body()
的内容写入到该 BufferedSink 之中。
```java
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
1.2.3、具体的写入操作
具体是如何写入数据的,要根据传入到 Request 中的 RequestBody 的实现类来定,如果是表单类型的则是有 FormBody 负责具体的写操作,如果是文件类型的则是由 MutilPartBody 负责具体的写操作。下面是它们两者具体的实现源码,怎么写就不分析了。
- FormBody 具体的写操作
private long writeOrCountBytes(BufferedSink sink, boolean countBytes) {
long byteCount = 0L;
Buffer buffer;
if (countBytes) {
buffer = new Buffer();
} else {
buffer = sink.buffer();
}
for (int i = 0, size = encodedNames.size(); i < size; i++) {
if (i > 0) buffer.writeByte('&');
buffer.writeUtf8(encodedNames.get(i));
buffer.writeByte('=');
buffer.writeUtf8(encodedValues.get(i));
}
if (countBytes) {
byteCount = buffer.size();
buffer.clear();
}
return byteCount;
}
- MutilPartBody 具体的写操作
private long writeOrCountBytes(BufferedSink sink, boolean countBytes) throws IOException {
long byteCount = 0L;
Buffer byteCountBuffer = null;
if (countBytes) {
sink = byteCountBuffer = new Buffer();
}
for (int p = 0, partCount = parts.size(); p < partCount; p++) {
Part part = parts.get(p);
Headers headers = part.headers;
RequestBody body = part.body;
sink.write(DASHDASH);
sink.write(boundary);
sink.write(CRLF);
if (headers != null) {
for (int h = 0, headerCount = headers.size(); h < headerCount; h++) {
sink.writeUtf8(headers.name(h))
.write(COLONSPACE)
.writeUtf8(headers.value(h))
.write(CRLF);
}
}
MediaType contentType = body.contentType();
if (contentType != null) {
sink.writeUtf8("Content-Type: ")
.writeUtf8(contentType.toString())
.write(CRLF);
}
long contentLength = body.contentLength();
if (contentLength != -1) {
sink.writeUtf8("Content-Length: ")
.writeDecimalLong(contentLength)
.write(CRLF);
} else if (countBytes) {
// We can't measure the body's size without the sizes of its components.
byteCountBuffer.clear();
return -1L;
}
sink.write(CRLF);
if (countBytes) {
byteCount += contentLength;
} else {
body.writeTo(sink);
}
sink.write(CRLF);
}
sink.write(DASHDASH);
sink.write(boundary);
sink.write(DASHDASH);
sink.write(CRLF);
if (countBytes) {
byteCount += byteCountBuffer.size();
byteCountBuffer.clear();
}
return byteCount;
1.3、结束请求
对 sink.flush() 方法的注释可以看出它是将缓存区中数据写入到底层的 sink 中,其实就是写入到 server 中去了,相当于一个刷新缓冲区的功能。
@Override public void finishRequest() throws IOException {
/**
* Writes all buffered data to the underlying sink, if one exists. Then that sink is recursively
* flushed which pushes data as far as possible towards its ultimate destination. Typically that
* destination is a network socket or file. <pre>{@code
*/
sink.flush();
}
1.4、读取响应头信息
当客户端将请求数据发送给服务端之后,服务端做了处理之后会将结果返回给客户端,这是客户端需要根据这些返回的数据构造出一个 Response 对象出来然后返回给调用者。下面是就是构造响应头部的过程。
/** Parses bytes of a response header from an HTTP transport. */
public Response.Builder readResponse() throws IOException {
if (state != STATE_OPEN_REQUEST_BODY && state != STATE_READ_RESPONSE_HEADERS) {
throw new IllegalStateException("state: " + state);
}
try {
while (true) {
StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());
Response.Builder responseBuilder = new Response.Builder()
//协议,也就是 http 的版本例如 http1/2 /spdy
.protocol(statusLine.protocol)
//响应码
.code(statusLine.code)
//响应消息
.message(statusLine.message)
//响应头
.headers(readHeaders());
if (statusLine.code != HTTP_CONTINUE) {
state = STATE_OPEN_RESPONSE_BODY;
return responseBuilder;
}
}
} catch (EOFException e) {
// Provide more context if the server ends the stream before sending a response.
IOException exception = new IOException("unexpected end of stream on " + streamAllocation);
exception.initCause(e);
throw exception;
}
}
1.5、获取响应体信息输入流
1.5.1、得到一个 ResponseBody 对象
得到一个 ResponseBody 对象,该对象封装了连接服务端的输入流对 Source 对象(响应体 body)和 Headers 信息。
@Override public ResponseBody openResponseBody(Response response) throws IOException {
Source source = getTransferStream(response);
return new RealResponseBody(response.headers(), Okio.buffer(source));
}
1.5.2、响应的不同请求创建不同的 Source 对象
根据响应的不同请求创建不同的 Source 对象。
private Source getTransferStream(Response response) throws IOException {
//没有响应内容,创建长度为 0 的 FixedLengthSource
if (!HttpHeaders.hasBody(response)) {
return newFixedLengthSource(0);
}
//Transfer-Encoding 是 chunked 的方式表示响应体的大小是无法知道的,创建一个 ChunkedSource 返回
if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
return newChunkedSource(response.request().url());
}
long contentLength = HttpHeaders.contentLength(response);
if (contentLength != -1) {
//contentLength 是已知的就创建一个 FixedLengthSource 返回。
return newFixedLengthSource(contentLength);
}
// Wrap the input stream from the connection (rather than just returning
// "socketIn" directly here), so that we can control its use after the
// reference escapes.
return newUnknownLengthSource();
}
1.6、往上一级 ConnectInterceptor 返回一个网络请求回来的 Response。
因为拦截器是一级级递归调用下来的,而 CallServerInterceptor 是整个网络请求中最后一个拦截器,它最终会根据服务器返回的数据通过构造者的模式创建一个 Response ,然后返回到上一级 Interceptor 对象。至于是如何处理就去看上一节吧 OKHTTP拦截器ConnectInterceptor的简单分析。
2、总结
CallServerInterceptor 做的事情很多都是 ConnectInterceptor 都准备好了,例如 HttpStream 的创建等。它主要是利用 HttpStream 向服务器发送请求数据和接受服务器返回的数据,这里设计到 Okio 的知识点,可以参考这篇blog Android 善用Okio简化处理I/O操作