zl程序教程

您现在的位置是:首页 >  其它

当前栏目

OkHttp解析(三)关于Okio

解析 关于 okHttp
2023-09-11 14:15:13 时间

OkHttp解析系列

OkHttp解析(一)从用法看清原理
OkHttp解析(二)网络连接
OkHttp解析(三)关于Okio

从前两篇文章我们知道,在OkHttp底层网络连接是使用Socket,连接成功后则通过Okio库与远程socket建立了I/O连接,接着调用createTunnel创建代理隧道,在这里HttpStream与Okio建立了I/O连接。本篇文章就来看看Okio的使用

Okio

最新的Okio上看它的说明
这里介绍到

Okio 补充了 java.iojava.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 有自己的流类型,那就是 SourceSink,它们和 InputStreamOutputStream 类似,前者为输入流,后者为输出流。

它们还有一些新特性:

  • 超时机制,所有的流都有超时机制;

  • API 非常简洁,易于实现;

  • SourceSink 的 API 非常简洁,为了应对更复杂的需求,Okio 还提供了 BufferedSourceBufferedSink 接口,便于使用(按照任意类型进行读写,BufferedSource 还能进行查找和判等);

  • 不再区分字节流和字符流,它们都是数据,可以按照任意类型去读写;

  • 便于测试,Buffer 同时实现了 BufferedSource(读) 和 BufferedSink(写) 接口,便于测试;

介绍完上面几个类后,看个UML图,理解他们之间的关系

在这里插入图片描述

Okio类图

可以看到Buffer这里实现了两个接口,它集 BufferedSourceBufferedSink 的功能于一身,为我们提供了访问数据缓冲区所需要的一切 API。
而这里ReadBufferSourceReadBufferSink虽然各自实现了单独的接口,但他们内部都保存了个成员变量Buffer,而Buffer却涵盖了两者。在ReadBufferSourceReadBufferSink中调用读写实际上是调用到了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.sourceOkio.sink打开对应的输入输出流保存到BufferedSource sourceBufferedSink中。

之后再把创建出来的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,这里就不扩展开了。

参考资料

拆轮子系列:拆 Okio
官方Okio

转载:https://www.jianshu.com/p/ade7bccfff7c