OkHttp解析系列
OkHttp解析(一)从用法看清原理
OkHttp解析(二)网络连接
OkHttp解析(三)关于Okio
从前两篇文章我们知道,在OkHttp底层网络连接是使用Socket,连接成功后则通过Okio库与远程socket建立了I/O连接,接着调用
createTunnel
创建代理隧道,在这里HttpStream与Okio建立了I/O连接。本篇文章就来看看Okio的使用
Okio
从最新的Okio上看它的说明
这里介绍到
Okio 补充了
java.io
和java.nio
的内容,使得数据访问、存储和处理更加便捷。
ByteString and Buffer
Okio则建立在ByteStrings和Buffers上
-
ByteStrings:它是一个不可变的字节序列,对于字符数据来说,
String
是非常基础的,但在二进制数据的处理中,则没有与之对应的存在,ByteString
应运而生。ByteStrings
很多方法与String
用法一样,它更容易把一些二进制数据当作一个值来处理,它更容易处理一些二进制数据。此外它也可以把二进制数据编解码为十六进制(hex),base64和UTF-8格式。
它向我们提供了和String
非常类似的 API:获取字节:指定位置,或者整个数组;
编解码:hex,base64,UTF-8;
判等,查找,子串等操作;
Buffer:
Buffer
是一个可变的字节序列,就像ArrayList
一样。我们使用时只管从它的头部读取数据,往它的尾部写入数据就行了,而无需考虑容量、大小、位置等其他因素。
Source and Sink
Okio 吸收了 java.io
一个非常优雅的设计:流(stream),流可以一层一层套起来,不断扩充能力,最终完成像加密和压缩这样复杂的操作。这正是“修饰模式”的实践。
修饰模式,是面向对象编程领域中,一种动态地往一个类中添加新的行为的设计模式。就功能而言,修饰模式相比生成子类更为灵活,这样可以给某个对象而不是整个类添加一些功能。
Okio 有自己的流类型,那就是 Source
和 Sink
,它们和 InputStream
与 OutputStream
类似,前者为输入流,后者为输出流。
它们还有一些新特性:
超时机制,所有的流都有超时机制;
API 非常简洁,易于实现;
Source
和Sink
的 API 非常简洁,为了应对更复杂的需求,Okio 还提供了BufferedSource
和BufferedSink
接口,便于使用(按照任意类型进行读写,BufferedSource 还能进行查找和判等);不再区分字节流和字符流,它们都是数据,可以按照任意类型去读写;
便于测试,
Buffer
同时实现了BufferedSource
(读) 和BufferedSink
(写) 接口,便于测试;
介绍完上面几个类后,看个UML图,理解他们之间的关系
可以看到Buffer
这里实现了两个接口,它集 BufferedSource
和 BufferedSink
的功能于一身,为我们提供了访问数据缓冲区所需要的一切 API。
而这里ReadBufferSource
和ReadBufferSink
虽然各自实现了单独的接口,但他们内部都保存了个成员变量Buffer
,而Buffer
却涵盖了两者。在ReadBufferSource
和ReadBufferSink
中调用读写实际上是调用到了Buffer
的读写。这种设计有点类似装饰模式
官方例子
我们来看一下官方文档中 PNG 解码的例子:
private static final ByteString PNG_HEADER = ByteString.decodeHex("89504e470d0a1a0a");
public void decodePng(InputStream in) throws IOException {
try (BufferedSource pngSource = Okio.buffer(Okio.source(in))) {
ByteString header = pngSource.readByteString(PNG_HEADER.size());
if (!header.equals(PNG_HEADER)) {
throw new IOException("Not a PNG.");
}
...
}
我们先一点一点看,这里有个静态成员变量PNG_HEADER
,它则是把相应的十六进制字符串转换为相应的字节串。
public static ByteString decodeHex(String hex) {
if (hex == null) throw new IllegalArgumentException("hex == null");
if (hex.length() % 2 != 0) throw new IllegalArgumentException("Unexpected hex string: " + hex);
byte[] result = new byte[hex.length() / 2];
for (int i = 0; i < result.length; i++) {
int d1 = decodeHexDigit(hex.charAt(i * 2)) << 4;
int d2 = decodeHexDigit(hex.charAt(i * 2 + 1));
result[i] = (byte) (d1 + d2);
}
return of(result);
}
public static ByteString of(byte... data) {
if (data == null) throw new IllegalArgumentException("data == null");
return new ByteString(data.clone());
}
可以看到,这里把十六进制中每个字符通过decodeHexDigit
方法转换为对应的字节,再存放到字节数组中,最后调用of
方法来创建出ByteString
继续看官方例子
public void decodePng(InputStream in) throws IOException {
try (BufferedSource pngSource = Okio.buffer(Okio.source(in))) {
ByteString header = pngSource.readByteString(PNG_HEADER.size());
if (!header.equals(PNG_HEADER)) {
throw new IOException("Not a PNG.");
}
while (true) {
Buffer chunk = new Buffer();
// Each chunk is a length, type, data, and CRC offset.
int length = pngSource.readInt();
String type = pngSource.readUtf8(4);
pngSource.readFully(chunk, length);
int crc = pngSource.readInt();
decodeChunk(type, chunk);
if (type.equals("IEND")) break;
}
}
}
我们先来看下Okio.buffer(Okio.source(in))
这里
private static Source source(final InputStream in, final Timeout timeout) {
return new Source() {
@Override public long read(Buffer sink, long byteCount) throws IOException {
...
}
...
};
}
public static BufferedSource buffer(Source source) {
return new RealBufferedSource(source);
}
可以看到,首先调用Okio.source(in)
把InputStream
输入流转换为Source
,接着调用buffer
方法创建了RealBufferedSource
它实现了BufferSource
方法。
此时这个pngSource
则代表了图片的输入流信息
接着调用ByteString header = pngSource.readByteString(PNG_HEADER.size());
来读取图片首部的字节串
@Override public ByteString readByteString(long byteCount) throws IOException {
require(byteCount);
return buffer.readByteString(byteCount);
}
可以看到,最终的读取转换则是通过Buffer
来进行调用。而Buffer
同样也实现了和ReadBufferdSource
的接口BufferedSource
。
为什么要这么折腾呢?明明可以简单的调用ReadBufferedSource
为什么还要通过Buffer
来调用?
让我们从功能需求和设计方案来考虑。
BufferedSource 要提供各种形式的读取操作,还有查找与判等操作。大家可能会想,那我就在实现类中自己实现不就好了吗?干嘛要经过 Buffer 中转呢?这里我们实现的时候,需要考虑效率的问题,而且不仅 BufferedSource 需要高效实现,BufferedSink 也需要高效实现,这两者的高效实现技巧,很大部分都是共通的,所以为了避免同样的逻辑重复两遍,Okio 就直接把读写操作都实现在了 Buffer 这一个类中,这样逻辑更加紧凑,更加内聚。而且还能直接满足我们对于“两用数据缓冲区”的需求:既可以从头部读取数据,也能向尾部写入数据。至于我们单独的读写操作需求,Okio 就为 Buffer 分别提供了委托类:RealBufferedSource 和 RealBufferedSink,实现好 Buffer 之后,它们两者的实现将非常简洁(前者 450 行,后者 250 行)。
OkHttp里面Okio的使用
前面说到
在OkHttp底层网络连接是使用Socket,连接成功后则通过Okio库与远程socket建立了I/O连接,接着调用
createTunnel
创建代理隧道,在这里HttpStream与Okio建立了I/O连接。
我们直接定位到RealConnection.connectSocket
方法这里
private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
Proxy proxy = route.proxy();
Address address = route.address();
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
? address.socketFactory().createSocket()
: new Socket(proxy);
rawSocket.setSoTimeout(readTimeout);
try {
Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
} catch (ConnectException e) {
throw new ConnectException("Failed to connect to " + route.socketAddress());
}
source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
}
可以看到,这里根据挑选出来的线路代理,创建完Socket后,调用了连接,连接成功后,则使用Okio.source
和Okio.sink
打开对应的输入输出流保存到BufferedSource source
和BufferedSink
中。
之后再把创建出来的source和sink绑定到HttpStream,使得HttpStream拥有两者的调用。
前一篇文章说到,当Socket连接完成后,就会根据source和sink来选择创建对应HttpStream
public HttpStream newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
...
HttpStream resultStream;
if (resultConnection.framedConnection != null) {
resultStream = new Http2xStream(client, this, resultConnection.framedConnection);
} else {
resultConnection.socket().setSoTimeout(readTimeout);
resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS);
resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS);
resultStream = new Http1xStream(
client, this, resultConnection.source, resultConnection.sink);
...
}
之后就可以进行读取和写入数据。
写入数据的话,由第一篇文章可知道是在CallServerInterceptor
中,在里面写入我们的请求体
// CallServerInterceptor#intercept
// 发送请求 body
Sink requestBodyOut = httpCodec.createRequestBody(request,
request.body().contentLength());
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
// 读取响应 body
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
可以看到在这里,先调用了createRequestBody
来根据request创建一个Sink
不过此时还未写入数据,里面只是空的,只是根据request来选择创建Sink而已
@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);
}
...
}
接着把创建好的Sink
包装到BufferedSink
中,最终调用request.body().writeTo(bufferedRequestBody);
来把自己的请求体写入BufferedSink
,这里也就是写入到Socket
里面了。
同理,读取数据到Response
则是使用BufferedSource
,这里就不扩展开了。