OKHttp源码分析(七)网络请求 CallServerInterceptor 和 HttpCodec
上一篇介绍了ConnectInterceptor
,主要是负责连接的工作,本篇主要介绍CallServerInterceptor
,主要负责在流上传输请求数据,并处理接受数据,而在OKHttp中,这个流工具类就是HttpCodec
。
在CallServerInterceptor
完成请求发送中,主要的逻辑分几个步骤,分别是
- 请求数据Header写入
- 请求数据Body写入
- 写入全部网络请求
- 返回数据Header读取
- 返回数据Body读取
我们分四个步骤分别分析下,HttpCodec还有Http1.1下的HttpCodec1和Http2.0下的HttpCodec2两个实现类,下一篇会分析Http2.0的内容。 所以本篇提到的HttpCodec实现都是HttpCodec1。
HttpCodec
HttpCodec
作为流的封装,封装了底层Socket的输入/输出流。OKHttp对底层的输入/输出流怎么处理的呢?在上一篇连接的时候,调用connectSocket
连接Socket的时候,使用了OKio对底层Socket的流进行了封装。
source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
#Okio#source
public static Source source(Socket socket) throws IOException {
if (socket == null) throw new IllegalArgumentException("socket == null");
if (socket.getInputStream() == null) throw new IOException("socket's input stream == null");
AsyncTimeout timeout = timeout(socket);
Source source = source(socket.getInputStream(), timeout);
return timeout.source(source);
}
调用的Okio.source,对socket.getInputStream()输入流进行了处理,输出流也同理。
final BufferedSource source;
final BufferedSink sink;
HttpCodec
就是封装使用了上面的流来进行网络请求数据的传输和接收的。最底层还是系统提供的Socket输入输出流。
CallServerInterceptor拦截器整体框架
@Override public Response intercept(Chain chain) throws IOException {
//请求数据Header写入
。。。
Response.Builder responseBuilder = null;
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
//处理100-continue情况
。。。
}
if (responseBuilder == null) {
//请求数据Body写入
。。。
}
}
//写入全部网络请求
httpCodec.finishRequest();
if (responseBuilder == null)
//回数据Header读取
。。。
}
//返回数据Body读取
。。。
//处理异常情况
。。。
return response;
}
上面intercept的方法的步骤处理就是上面注释的部分,下面每个部分独立的讲下。
请求数据Header写入
realChain.eventListener().requestHeadersStart(realChain.call());
httpCodec.writeRequestHeaders(request);
realChain.eventListener().requestHeadersEnd(realChain.call(), request);
请求Header的写入就是调用httpCodec.writeRequestHeaders,传入我们请求时创建的request。HttpCodec内部又会调用writeRequest进行传输。
@Override public void writeRequestHeaders(Request request) throws IOException {
String requestLine = RequestLine.get(
request, streamAllocation.connection().route().proxy().type());
writeRequest(request.headers(), requestLine);
}
public void writeRequest(Headers headers, String requestLine) throws IOException {
if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
sink.writeUtf8(requestLine).writeUtf8("\r\n");
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;
}
首先是一个state变量,标记了这次网络请求的状态。我们每进行一步,这个state就会迁移到一个新的状态,表示下一个应该进行的操作,类似一个状态机。如果我们跨状态执行操作,就会出异常。
状态值 | 含义 |
---|---|
STATE_IDLE | 初始状态 |
STATE_OPEN_REQUEST_BODY | 应该执行写入请求Header |
STATE_WRITING_REQUEST_BODY | 应该执行写入请求的body |
STATE_READ_RESPONSE_HEADERS | 应该执行读取响应Header |
STATE_OPEN_RESPONSE_BODY | 应该执行读取响应body |
STATE_READING_RESPONSE_BODY | 正在读取响应body |
STATE_CLOSED | 读取body完成后 |
实际的写入操作就是往sink流里写入数据。这里的写入逻辑和Http的数据报的格式一样。
- 写入状态行,sink.writeUtf8(requestLine).writeUtf8("\r\n"); requestLine就是一个状态行,通过RequestLine.get()进行获取。状态行包括请求方法、请求url、Http版本。都是用空格分离。
- 再写入一个空行(起间隔作用)
- 写入Header,通过headers.name(i)和headers.value(i)进行读取。每个header字段之间还会使用一个空行进行隔离。
- 写入空行 经过上面的步骤就完成了写入Header
请求数据Body写入
请求body写入前,有一个预先的处理,就是Expect
为"100-continue"
的情况,这种情况下需要提前请求网络,询问是否可以继续写入请求的响应部分。如果服务器返回100。那么就可以继续传输body。否则跳过body的传输。
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();
realChain.eventListener().responseHeadersStart(realChain.call());
responseBuilder = httpCodec.readResponseHeaders(true);
}
if (responseBuilder == null) {
// 写入body处理
realChain.eventListener().requestBodyStart(realChain.call());
long contentLength = request.body().contentLength();
CountingSink requestBodyOut =
new CountingSink(httpCodec.createRequestBody(request, contentLength));
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
realChain.eventListener()
.requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
} else if (!connection.isMultiplexed()) {
streamAllocation.noNewStreams();
}
首先判断了如果Expect字段是100-continue,那么会直接调用httpCodec.flushRequest()请求网络,把刚才写入的Header发送到服务器,请求的执行就是这么简单,先写入再请求。 这时候再通过httpCodec.readResponseHeaders(true)读取网络请求返回的Headers数据。具体怎么读取的下面会讲到。
http 100--continue用于客户端在发送POST数据给服务器前,征询服务器情况,看服务器是否处理POST的数据,如果不处理,客户端则不上传POST数据,如果处理,则POST上传数据。在现实应用中,通过在POST大数据时会使用100-continue协议。
因为这里的read穿入了true,所以返回的responseBuilder,如果为空,表示此次响应返回了100,可以继续传送body。如果不为空,那么服务器没有返回100,禁止继续传递body。 下面使用了responseBuilder进行了判断,如果为null,那么会继续写入body部分。不为null就跳出不写入。并且没有返回100的情况下,这个连接也不能继续使用了。 写入body也比较简单,直接创建了一个CountingSink,调用body的writeTo方法直接进行写入。CountingSink是通过createRequestBody方法创建的。
@Override public Sink createRequestBody(Request request, long contentLength) {
if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
return newChunkedSink();
}
if (contentLength != -1) {
return newFixedLengthSink(contentLength);
}
throw new IllegalStateException(
"Cannot stream a request body without chunked encoding or a known content length!");
}
上面对写入body的sink,分为两种情况,如果Transfer-Encoding是chunked,那么就使用ChunkedSink
进行写入,如果有contentLength属性,那么使用FixedLengthSink
进行写入。
这里就有问题,这里只是写入了上面两种Sink中,而且都是新建的,那是怎么写入HttpCodec的sink中呢。
原来上面两种流是HttpCodec的内部类,内部封装了底层的HttpCodec的sink输入流,所以也是间接的输入到了HttpCodec的sink中。
至此body写入也完成了。
写入全部网络请求
这块的内容上面已经谈到了,调用httpCodec.flushRequest(),直接把写入流的数据进行传递。就相当一把枪。装入子弹(写入流),进行射击(flush)。
返回数据Header读取
StatusLine statusLine = StatusLine.parse(readHeaderLine());
Response.Builder responseBuilder = new Response.Builder()
.protocol(statusLine.protocol)
.code(statusLine.code)
.message(statusLine.message)
.headers(readHeaders());
读取Header通过readHeaderLine()和readResponseHeaders()构建出一个Response。readHeaderLine()负责读取状态行,而readResponseHeaders()负责读取响应的Header。这样就完成了除body外的所有的数据。分别看下两段解析的逻辑。
读取状态行
StatusLine statusLine = StatusLine.parse(readHeaderLine());
使用上面代码就可以读取到StatusLine这个数据,是状态行的抽象。StatusLine#parse这种用法,经常使用java应该很熟悉,这时静态的工厂方法,类似Integer.parse等。readHeaderLine()获取到格式化的数据,再使用StatusLine#parse进行解析成成品。 先看readHeaderLine()
private String readHeaderLine() throws IOException {
String line = source.readUtf8LineStrict(headerLimit);
headerLimit -= line.length();
return line;
}
这里使用source.readUtf8LineStrict读取单独的行,这个行通过\r\n
来分割的。headerLimit标记了返回的最长的长度,防止没有\r\n
。
因为Http数据报使用\r\n
进行分割,所以第一个进行分割的就是状态行的内容,这个内容是一个字符串。得到了这个字符串。就可以使用StatusLine.parse进行解析了。内部解析的逻辑就是解析字符串。有兴趣的可以自己阅读。
读取Header
读取Header通过readHeaders
public Headers readHeaders() throws IOException {
Headers.Builder headers = new Headers.Builder();
for (String line; (line = readHeaderLine()).length() != 0; ) {
Internal.instance.addLenient(headers, line);
}
return headers.build();
}
通过上面写入Header,我们知道。Header的各个字段也是通过\r\n
进行分割的。所以这里也是通过readHeaderLine()进行解析的,每次取出一个行,再通过Internal.instance.addLenient传入Header的Builder。
这样就完成了Header的读取。
response的组装
获取了状态行和Header的数据,就要组装进Response里了。
Response.Builder responseBuilder = new Response.Builder()
.protocol(statusLine.protocol)
.code(statusLine.code)
.message(statusLine.message)
.headers(readHeaders());
通过上面的代码,分别对每个变量进行赋值。在组装完状态行和Header后,拦截器内还会进行组装。
Response response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
这里有几个重要的字段
- handshake表示TSL握手的记录
- sentRequestAtMillis 和 receivedResponseAtMillis表示请求发出的时间和接收到响应的时间
返回数据Body读取
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
同样也是通过Buider进行处理,调用body传入ReesponseBody,这个对象我们应该很熟悉,每次获取相应body都是通过这个对象。那他是怎么创建的呢。
@Override public ResponseBody openResponseBody(Response response) throws IOException {
streamAllocation.eventListener.responseBodyStart(streamAllocation.call);
String contentType = response.header("Content-Type");
if (!HttpHeaders.hasBody(response)) {
Source source = newFixedLengthSource(0);
return new RealResponseBody(contentType, 0, Okio.buffer(source));
}
if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
Source source = newChunkedSource(response.request().url());
return new RealResponseBody(contentType, -1L, Okio.buffer(source));
}
long contentLength = HttpHeaders.contentLength(response);
if (contentLength != -1) {
Source source = newFixedLengthSource(contentLength);
return new RealResponseBody(contentType, contentLength, Okio.buffer(source));
}
return new RealResponseBody(contentType, -1L, Okio.buffer(newUnknownLengthSource()));
}
上面的代码我们好像很熟悉,在哪儿见过。 在写入body时,也有类似的逻辑。
- 先判断是否有body,如果没有body,那么返回一个长度0的fixedLengthSource
- 判断时分块传输,那就使用一个ChunkedSource
- 如果有长度,使用FixedLengthSource,参数是长度
- 没有长度也不分块,就返回一个newUnknownLengthSource 看了上面的逻辑,我们发现这里并没有读取出来数据,而是让ResponseBody封装了一个输出流。这个输出流在哪儿读取的呢。这个是我们自己负责的,我们读取数据时,都是在读取这个输出流。并且只能读取一次。
处理异常情况
close关闭连接
有两种情况需要处理
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
streamAllocation.noNewStreams();
}
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
如果响应Connection字段为close,那么这时需要关闭这个连接,这个连接不能继续使用了。调用 streamAllocation.noNewStreams();这个方法上一篇讲过,如果这个连接不能从ConnectionPoll复用,那么就会创建一个新的Connection。
204/205
如果返回204/205,这两个相当于应用缓存,这时候表示命中缓存,不会返回body,如果返回了,说明有问题。
总结
经过前面的几章,我们应该对OKHttp的请求的整体有了很不多的掌握。OKHttp的设计使用拦截器模式,真的对于网络请求的复杂性进行很好的解耦,网络的处理很适合用拦截器这种模式。欢迎进行讨论。 到这里就结束了吗?不是的。还有很多重要的细枝末节还没有涉及,比如Http2.0、Https、webSocket的支持等。 后面的文章还会分析下这几个方面的源码处理。
转载自:https://juejin.cn/post/7140974233802244104