上一篇介绍了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的支撑等。
后边的文章还会剖析下这几个方面的源码处理。