在Okhttp源码分析专栏的几篇博客分析了Okhttp几个拦截器的主要功能,还剩下最后一个拦截器CallServerInterceptor没有分析,本篇博客就简单分析下该拦截器的功能。
在Okhttp拦截器链上CallServerInterceptor拦截器是最后一个拦截器,该拦截器前面的拦截器ConnectInterceptor主要负责打开TCP链接(详见《 OkHttp之ConnectInterceptor简单分析 》)。而CallServerInterceptor的主要功能就是—向服务器发送请求,并最终返回Response对象供客户端使用。
下面就从源码的角度来简单分析CallServerInterceptor的是怎么像服务器发送Request,以及生成Response对象的。
public Response intercept(Chain chain) throws IOException {
// 省略部分代码
// 获取HttpCodec
HttpCodec httpCodec = realChain.httpStream();
// 省略部分代码
Request request = realChain.request();
//向服务器发送请求
httpCodec.writeRequestHeaders(request);
Response.Builder responseBuilder = null;
// 检测是否有请求body
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();
//构建responseBuilder对象
responseBuilder = httpCodec.readResponseHeaders(true);
}
//如果服务器允许发送请求body发送
if (responseBuilder == null) {
Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
} else if (!connection.isMultiplexed()) {
//省略部分代码
}
}
//结束请求
httpCodec.finishRequest();
//构建请求buidder对象
if (responseBuilder == null) {
responseBuilder = httpCodec.readResponseHeaders(false);
}
Response response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
int code = response.code();
if (forWebSocket && code == 101) {
//省略部分代码
} else {
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
}
//省略部分代码
return response;
}
该方法首先是获取了httpCodec对象,该对象的主要功能就是对不同http协议(http1.1和http/2)的请求和响应做处理,该对象的初始化是在ConnectIntercepor的intercept里面:
HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
最终httpCodec的初始化又是在StreamAllocation的newStream方法(详见《 OkHttp之ConnectInterceptor简单分析 》):
public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
//:省略部分代码;
HttpCodec resultCodec = resultConnection.newCodec(client, this);
}
public HttpCodec newCodec(
OkHttpClient client, StreamAllocation streamAllocation) throws SocketException {
if (http2Connection != null) {
return new Http2Codec(client, streamAllocation, http2Connection);
} else {
//设置socket的读超时时间
socket.setSoTimeout(client.readTimeoutMillis());
//InputStream的超时时间
source.timeout().timeout(client.readTimeoutMillis(), MILLISECONDS);
//OutputStream的超时时间
sink.timeout().timeout(client.writeTimeoutMillis(), MILLISECONDS);
return new Http1Codec(client, streamAllocation, source, sink);
}
}
可以发现Okhttp的提供了两种HttpCodec的实现类,如果使用了http2协议则返回Http2Codec,否则返回Http1Codec!并且设置了超时时间,本篇就以Http1Codec对象来进行分析。
我们知道Http发送网络请求前两个步骤是:
1、建立TCP链接
2、客户端向web服务器发送请求命令:形如GET /login/login.jsp?username=android&password=123 HTTP/1.1的信息
在Okhttp中ConnectInterceptor负责第一个步骤,那么第二个步骤是如何实现的呢?答案就是httpCodec对象的writeRequestHeaders方法。(该方法在CallserverInterceptor的intercept里面调用,见上面代码)
public void writeRequestHeaders(Request request) throws IOException {
//RequestLine.get用来构建形如GET xx HTTP/1.1的字符串
String requestLine = RequestLine.get(
request, streamAllocation.connection().route().proxy().type());
//像服务器发送请求,形如GET xxx HTTP/1.1
writeRequest(request.headers(), requestLine);
}
可以发现Okhttp通过OkIO的Sink对象(该对象可以看做Socket的OutputStream对象)的writeRequest来向服务器发送请求的。
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;
}
我们知道HTTP支持post,delete,get,put等方法,而post,put等方法是需要请求体的(在Okhttp中用RequestBody来表示)。所以接着writeRequestHeaders之后Okhttp对请求体也做了响应的处理:
//如果当前request请求需要请求体
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
//询问Server使用愿意接受数据
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();
//构建responseBuilder对象
responseBuilder = httpCodec.readResponseHeaders(true);
}
//向服务器发送请求体
if (responseBuilder == null) {
//发送请求体,详见下文描述
} else if (!connection.isMultiplexed()) {
//省略部分代码
}
}
通过上面的代码可以发现Okhttp对Expect头部也做了支持,上面代码对客户端是否使用该头部做了判断,“100 continue”的作用就是:客户端有一个RequestBody(比如post或者PUT方法)要发给服务器,但是客户端希望在发送RequestBody之前查看服务器是否接受这个body,服务端在接受到这个请求后必须进行响应。客户端通过Expect首部来发送这个消息,当然如果客户端没有实体发送,就不应该发送100 continue 首部,因为这样会使服务器误以为客户端有body要发送。所以okhttp在发送这个之前要permitsRequestBody来判断。当然常规的get请求是不会走这个方法的。
如果服务器允许发送ReqeustBody,那么就通过下面这三行代码来发送请求体:
//构建请求体对象组成的输入流
Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
//发送请求体
request.body().writeTo(bufferedRequestBody);
最终调用ReqeustBody的writeTo方法来发送请求体,实际上是调用bufferedRequestBody对象的write方法,简单:
ublic static RequestBody create(
final @Nullable MediaType contentType, final ByteString content) {
return new RequestBody() {
//省略部分代码
@Override public void writeTo(BufferedSink sink) throws IOException {
sink.write(content);
}
};
}
到现在为止,客户端向服务端发送请求的部分已经讲解完毕,下面就剩下读取服务器响应然后构建Response对象了:
//构建请求buider对象
if (responseBuilder == null) {
responseBuilder = httpCodec.readResponseHeaders(false);
}
//构建response对象
Response response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
int code = response.code();
if (forWebSocket && code == 101) {
//返回空的即无效的响应
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
}
上面的代码做了三个工作:
1、调用HttpCodec的readResponseHeaders方法读取服务器响应的数据,构建Response.Builder对象(以Hppt1Codec分析):
public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
//省略部分代码
//读取服务器
StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());
Response.Builder responseBuilder = new Response.Builder()
.protocol(statusLine.protocol)//http协议版本
.code(statusLine.code)//http响应状态码
//http的message :like "OK" or "Not Modified"
.message(statusLine.message)
.headers(readHeaders());//http响应header
//省略部分代码
return responseBuilder;
}
2、通过ResopnseBuilder对象来最终创建Response对象,并返回。
最关键的是服务器的响应体或者响应内容是如果传给Response的,代码如下:
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
Response的body通过httpCodec对象的openResponseBody传进来,进入Http1Codec对象的openResponseBody方法看看都做了些神马:
public ResponseBody openResponseBody(Response response) throws IOException {
Source source = getTransferStream(response);
return new RealResponseBody(response.headers(), Okio.buffer(source));
}
很简单,openResponseBody将Socket的输入流InputStream对象交给OkIo的Source对象(在本篇博文中只需简单的将Sink作为Socket的输入流,Source作为Socket的输入流看待即可,详细的分析可参考OKIO),然后封装成RealResponseBody(该类是ResponseBody的子类)作为Response的body.
那么我们怎么通过这个body来获取服务器发送过来的字符串呢?ResponseBody提供了string()方法:
public final String string() throws IOException {
BufferedSource source = source();
try {
Charset charset = Util.bomAwareCharset(source, charset());
//InputStream 读取数据
return source.readString(charset);
} finally {
Util.closeQuietly(source);
}
}
string()方法也很简单,就是通过一些处理然后让调用source.readString来读取服务器的数据。需要注意的是该方法最后调用closeQuietly来关闭了当前请求的InputStream输入流,所以string()方法只能调用一次,再次调用的话会报错,毕竟输入流已经关闭了,你还怎么读取数据呢?
到此为止CallServerInterceptor简单分析完毕,总结下主要做了如下工作:
1、获取HttpCodec对象,对<=Http1.1之前的或者http/2不同协议的http请求处理。
2、发送http请求数据,构建Resposne.Builder对象,然后构建Response并返回。
到此为止Okhttp从发起请求到响应请求生成Response对象的流程已经分析完毕。如果想详细了解Okhttp工作原理的话,可参考博主的Okhttp源码分析专栏,如有不当之处,欢迎批评指正。