OkHttp解析(三)关于Okio

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容