OkHttp3可以说很目前很流行的网路访问框架了,它可以帮我实现重定向访问,而且其还有缓存机制能够帮我们减少与服务器的交互,它还支持gzip格式的response,这能够有效的帮我们节省流量。从以上可以看出OkHttp好处是大大的多,而OkHttp从使用上来说也是很简单的,即可以通过execute方法完成同步网络访问,通过enqueue方法来完成异步网络访问。
这边文章的目的并不是想对OkHttp3的各个相关文件做一个概括,而是作者因为好奇OkHttp3底层到底是如何实现网络访问的,从而对其底层实现网络访问机制这个问题做的一次探索。我们知道OkHttp3是依赖Okio包的,那么这个Okio包在OkHttp3实现网络访问的过程中起到了什么样的作用呢,这里先给出结论吧。
1、Okio包在OkHttp3中起到通过Buffer缓存发送以及接收到的数据;
2、OkHttp3底层通过Okio包以Socket的方式来进行网络访问;
下面我们就一步步的证明以上的两个结论吧
1、Http3的基本使用
为了研究问题,我们先看看一个简单的OkHttp3的使用,这里列举一个OkHttp3的同步post请求方法
String url = "http://192.168.8.136:8081/test/post";
OkHttpClient httpClient = new OkHttpClient();
RequestBody body = new FormBody.Builder().add("name", "yoryky").add("age", "12").build();
Request request = new Request.Builder().url(url).post(body).header("Cookie","{'name':'yoryky','age':'12'").build();
Call call = httpClient.newCall(request);
try {
Response response = call.execute();
System.out.println("status_code:" + response.code() + response.body().string());
} catch (Exception e) {
e.printStackTrace();
}
从这里可以看出,OkHttp3通过call.execute来触发网络访问,并最终返回Response数据。
2、OkHttp的优点说明
第一小节的call是RealCall的实例,call.execute方法会调用到RealCall.getResponse方法中去,而该方法中有两行代码
engine.sendRequest();
engine.readResponse();
这两行代码就是OkHttp3的核心代码了,这里的engine变量是HttpEngine的实例。sendRequest()方法是client向server发送request的主要方法。它先对request的header添加了一些默认字段,如keep-alive。然后对cache进行处理,判断是否可以直接使用cache,如果不行,才真正发送网络request请求。这这一点可以从HttpEngine.sendRequest方法中的部分代码加以说明
if (networkRequest == null) {
userResponse = cacheResponse.newBuilder()
.request(userRequest)
.priorResponse(stripBody(priorResponse))
.cacheResponse(stripBody(cacheResponse))
.build();
userResponse = unzip(userResponse);
return;
}
从代码中可以看出当有缓存时,直接从缓存中取数据,然后就返回了,而且这里可以unzip方法中的以下代码看出OkHttp3支持gzip格式。
GzipSource responseBody = new GzipSource(response.body().source());
这里的GzipSource是okio.Source接口的子类,当设置通过gzip方式来进行网络访问时,在获取服务器返回数据时会通过GzipSrource.read中以对应编码格式来返回response数据。
而在HttpEngine.networkRequest方法中可以看到OkHttp3默认是长连接的
if (request.header("Connection") == null) {
result.header("Connection", "Keep-Alive");
}
3、HttpEngine.sendRequest方法
上面提到sendRequest方法是OkHttp3的核心代码之一,那么这一小节就来说说它大概干了些什么事情。这里还是先说出观点吧。
1、sendRequest方法主要功能是初始化请求头并与服务器建立连接。
在阅读这里要讲解的内容以及后面的内容之前,建议大家先看看Socket在Java中的使用这篇文章吧,后面讲的内容都会结合着这篇文章中的内容来说。
这里来看HttpEngine.sendRequest代码,这里就不贴具体的代码了,童鞋们对比着看自己的AS上的代码即可(这里用的okhttp3版本号为3.3.0,okio版本号我1.8.0,如果版本号不一致那么代码有可能不一样)。
sendRequest先是如上一小节所说的先是判断缓存,如果无缓存或者缓存过期时,便重新通过创建Socket来连接服务器。sendRequest同服务器建立连接的调用堆栈为
从调用堆栈可以最终看出会调用到RealConnection.connectSocket方法,该方法中先创建了一个Socket变量
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
? address.socketFactory().createSocket()
: new Socket(proxy);
rawSocket.setSoTimeout(readTimeout);
由“Socket在Java中的使用”文章我们知道创建并初始化Socket的过程就是与服务器建立连接的过程,于是这里客户端便与服务器端建立好了连接。然后在connectSocket方法中另外两行特别重要的代码
source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
这里的source是RealBufferedSource实例,sink是RealBufferedSink。我们可以这样理解这里的sink和source : sink相当于“Socket在Java中的使用”文章中的OutputStream,而source相当于InputStream(不懂为什么的先这里理解着,看完下面的内容,就能明白为什么能这么理解)。于是我们便可以同故宫sink的write方法来写服务器数据,根据source来读取服务器数据。
这里还是要特别说明一点,HttpEngine.sendRequest方法作用只是创建Socket并实例化sink和source,其并没有进行与服务器的读写操作,那么读写操作实在什么地方进行的呢,这个就是下一小节要讲的内容了。
4、HttpEngine.readResponse方法
HttpEngine的readResponse方法其实实现了两个功能,一个是向服务器写数据,一个是获取服务器返回的数据。
向服务器写数据
这里我们通过一张堆栈图来说明向服务器写数据的具体过程。
我们看到HttpEngine.readResponse方法会调用到RealBufferedSink的flush方法
@Override public void flush() throws IOException {
if (closed) throw new IllegalStateException("closed");
if (buffer.size > 0) {
sink.write(buffer, buffer.size);
}
sink.flush();
}
这里的buffer变量中是Okio包中的Buffer类的实例,其中存放着包括要发送的请求头信息以及body数据,这一点可以从以下图片中看出
那么buffer中的数据又是在什么时候,添加进去的呢,实际上是通过HttpEngine.proceed方法中的下面代码来往buffer中添加请求头部和body数据的的
httpStream.writeRequestHeaders(request);
//Update the networkRequest with the possibly updated interceptor request.
networkRequest = request;
if (permitsRequestBody(request) && request.body() != null) {
Sink requestBodyOut = httpStream.createRequestBody(request, request.body().contentLength());
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
}
其中httpStream.writeRequestHeaders(request)是添加头部数据,而request.body().writeTo(bufferedRequestBody);方法是添加body数据,当然具体深入的代码逻辑需要小伙伴们自己去梳理下,实际上也不是很难的,跟着代码调试下就知道了,这里不深入(要不然不知道要写到什么时候去了)。
现在buffer数据有了,sink也有了,接着看sink是怎么向服务器发送数据的sink.write(buffer, buffer.size)这个代码实际上会先调用到AysncTimeout的Sink类的write方法中去,然后才调用到Okio.wirte方法,至于为什么是这个调用逻辑,请去看看RealConnnection.connectSocket中的
sink = Okio.buffer(Okio.sink(rawSocket));
这段代码,为什么要看这段,因为RealBufferedSink中的sink变量就是这里的sink,至于为什么是这种关系,请去梳理本小节的堆栈图。
到这里我们知道了,OkHttp3向服务器写数据,最终会调用到Okio.write方法中来,接着看Okio.write方法
@Override public void write(Buffer source, long byteCount) throws IOException {
checkOffsetAndCount(source.size, 0, byteCount);
while (byteCount > 0) {
timeout.throwIfReached();
Segment head = source.head;
int toCopy = (int) Math.min(byteCount, head.limit - head.pos);
out.write(head.data, head.pos, toCopy);
head.pos += toCopy;
byteCount -= toCopy;
source.size -= toCopy;
if (head.pos == head.limit) {
source.head = head.pop();
SegmentPool.recycle(head);
}
}
}
注意到这里的out变量,还是要看下面代码
sink = Okio.buffer(Okio.sink(rawSocket));
这个代码在实例化sink的过程中会调用Okio.sink
public static Sink sink(Socket socket) throws IOException {
if (socket == null) throw new IllegalArgumentException("socket == null");
AsyncTimeout timeout = timeout(socket);
Sink sink = sink(socket.getOutputStream(), timeout);
return timeout.sink(sink);
}
这里的Sink sink = sink(socket.getOutputStream(), timeout)代码中的socket.getOutputSteam就是上面的out变量。所以对比“Socket在Java中的使用”这篇文章,我们就能理解为什么out.write(head.data, head.pos, toCopy)这条语句就能够向服务器发送数据了。
其实到这里已经知道刚看时抛出的两条结论了
1、Okio包在OkHttp3中起到通过Buffer缓存发送以及接收到的数据;
2、OkHttp3底层通过Okio包以Socket的方式来进行网络访问;
但是写文章要写全,在看看从服务器获取数据吧
从服务器获取数据
获取数据还是看Okio读数据的调动堆栈
我们看到读数据的调用堆栈还是从HttpEngine.readResponse进去的,后面会去到RealBufferedSource类的indexof方法,然后调用AsyncTimeout的read方法,最后还是调用到了Okio.read方法,至于为什么会调用到Okio.read,也要去看RealConnection.connectSocket中的以下代码
source = Okio.buffer(Okio.source(rawSocket));
RealBufferedSource中的source变量,就是这个source。再看Okio.read方法
@Override public long read(Buffer sink, long byteCount) throws IOException {
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
if (byteCount == 0) return 0;
try {
timeout.throwIfReached();
Segment tail = sink.writableSegment(1);
int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);
int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
if (bytesRead == -1) return -1;
tail.limit += bytesRead;
sink.size += bytesRead;
return bytesRead;
} catch (AssertionError e) {
if (isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
}
}
这里有个in变量,这个in变量是通过socket.getInputStream()实例化的,对应“Socket在Java中的使用”文章中的is变量,我们也能够理解为什么OkHttp3能够能服务器端获取数据了。
5、总结
文章写了一大篇,最后还是来总结下吧。
1、OkHttp3使用起来简单,但是实际上其原理代码十分复杂,这也使得OkHttp3功能很强大,包括缓存、gzip、长连接、重定向等。
2、OkHttp3原理复杂,但是它的主要功能就是网络访问,网络访问就是要完成读写功能,于是我们只需要好好把握HttpEngine中的sendRequest作用和readResponse作用即可,即sendRequest完成初始化,并通过Socket方式建立同服务器的网络连接,而readResponse通过Socket完成读写功能。
3、OkHttp3依赖于Okio,主要体现在网络访问数据放在okio.Buffer中,通过okio.Okio write方法写服务器数据,通过okio.Okio read方法读取服务器返回数据。
6、参考文献
以下几篇参考文献,有些是参考了的,有些是我觉得写的比较好的,都贴出来吧
1、[Socket在Java中的使用](http://blog.csdn.net/yoryky/article/details/78557801)
2、[OKHttp源码分析3 - HttpEngine底层实现](http://blog.csdn.net/u013510838/article/details/52431516)
3、[OkHttp3源码分析\[缓存策略\]](http://www.jianshu.com/p/9cebbbd0eeab)
4、[UrlConnection连接和Socket连接的区别](http://blog.csdn.net/u013378580/article/details/51770124)
7、源码Demo
提供一个自己平时写着玩的Demo地址吧,Android Demo