Okio官方文档翻译

Okio官方文档翻译

Okio是一个辅助java.iojava.nio变得更易于访问、存储、操作数据的库。它一开始是作为Okhttp的组件存在。Okhttp是在Android中非常好用的HTTP客户端。Okio经过充分测试,并且准备好处理新的问题了。

ByteStrings and Buffers

Okio围绕两种类型构建。将大量的功能集成到一个简单的API中:

  • ByteString是不可更改的字节序列。对于字符数据,String是基础。ByteString则是String失散多年的兄弟,更易于将二进制数据作为值来处理。这个类非常好用:它知道如何编码和解码为16进制、base64UTF-8
  • Buffer是可更改的字节序列。和ArrayList一样,我们不需要提前设置它的大小。应该像一个队列一样读取和写入Buffer:在末尾写入数据,从头部读取数据。不必考虑管理位置、长度限制和容量。

在内部,ByteStringBuffer可以节省CPU和内存。如果将UTF-8字符串转换为ByteString,它将保存这个字符串的引用,需要时再进行解码,无需做执行任何操作。

Buffer使用一个Segment链表实现。当我们将数据从一个Buffer移到另一个Buffer时,它重新分配Segment的所有权,而不是复制数据。这在多线程编程中特别有用:一个线程用于网络交换数据,与另一个工作线程交换数据时不必对数据进行复制。

Sources and Sinks

java.io包设计非常好的一点是能够将如加密、压缩等转换进行分层。Okio包含了它自己的流类型,被称为SourceSink。它用起来和InputStreamOutputStream差不多,但是有几点关键的不同:

  • Timeout,超时。流提供底层IO机制的超时访问。和java.io套接字流不同,read()write()调用都会有超时信息。
  • 易于实现。Source定义了三个方法:read()close()timeout()。没有如available()或是单字节读取造成正确性和性能的危险。
  • 易于使用。即是SourceSink都只有三个方法用于读写,调用者可以通过使用BufferedSourceBufferedSink接口获得更多的API
  • 字节流和字符流本质上并没有任何区别,都是数据。UTF-8字符串、big-endian32位整数,little-endian短整数或是任何想要的数据,都可以被认为是字节读取和写入。不必再使用InputStreamReader了。
  • 易于测试。Buffer类实现了BufferedSourceBufferedSink接口,所以测试代码可以非常简单和清晰。

SourceSink可以和InputStreamOutputStream相互操作,你可以将任何Source视为InputStream,也可以将InputStream视为SourceSinkOutputStream同理。

例子

官方写了一些例子来演示如何使用Okio来处理一些常见的问题。阅读学习如何使用他们。需要的话可以进行复制粘贴。

按行读取一个文本文件

使用Okio.source(File)打开一个文件。返回的Source接口非常轻量级,并且只有有限的方法。通常会使用一个Buffer来包装这个Source。有两个好处:

  1. API更有用。不像Source只提供了基础的方法,BufferedSource有非常多的方法易于使用。
  2. 编程更快。Buffer可以使用更少的IO操作来完成工作。

Source每次打开都需要关闭。打开流的代码负责保证它的关闭。这里我们使用Javatry块来自动关闭Source

public void readLines(File file) throws IOException {
  try (Source fileSource = Okio.source(file);
       BufferedSource bufferedSource = Okio.buffer(fileSource)) {

    while (true) {
      String line = bufferedSource.readUtf8Line();
      if (line == null) break;

      if (line.contains("square")) {
        System.out.println(line);
      }
    }

  }
}

readUtf8Line()方法读取一行数据(以换行符\n\r\n分隔),或者直到文件末尾(无换行符)。返回一个字符串,省略末尾的换行符。如果遇到空行,将返回空字符串。如果到文件末尾,则返回null

上面的代码可以内联fileSource变量使得代码更加紧凑,使用漂亮的for循环代替while循环:

public void readLines(File file) throws IOException {
  try (BufferedSource source = Okio.buffer(Okio.source(file))) {
    for (String line; (line = source.readUtf8Line()) != null; ) {
      if (line.contains("square")) {
        System.out.println(line);
      }
    }
  }
}

readUtf8Line()方法适用于解析大多数文件。对于某些特定例子,可以考虑使用readUtf8LineStrict()。两者非常相似,但是readUtf8LineStrice()方法要求每行以换行符(\n\r\n)结尾。在此之前遇到文件末尾,将抛出EOFException异常。readUtf8LineStrict()方法还允许字节数限制,用于防止输入格式错误。

public void readLines(File file) throws IOException {
  try (BufferedSource source = Okio.buffer(Okio.source(file))) {
    while (!source.exhausted()) {
      String line = source.readUtf8LineStrict(1024L);
      if (line.contains("square")) {
        System.out.println(line);
      }
    }
  }
}

写入文件

上面我们使用了SourceBufferedSource读取一个文件。对于写入,我们则使用SinkBufferedSink。使用Buffer的好处一样:更好的API和性能。

public void writeEnv(File file) throws IOException {
  try (Sink fileSink = Okio.sink(file);
       BufferedSink bufferedSink = Okio.buffer(fileSink)) {

    for (Map.Entry<String, String> entry : System.getenv().entrySet()) &#123;
      bufferedSink.writeUtf8(entry.getKey());
      bufferedSink.writeUtf8("=");
      bufferedSink.writeUtf8(entry.getValue());
      bufferedSink.writeUtf8("\n");
    &#125;

  &#125;
&#125;

因为没有提供写入换行的API,所以我们手动插入换行字符。大多数时候使用\n作为换行字符。在很少情况下,可以使用System.lineSeparator()代替\nSystem.lineSeparator()Windows系统返回\r\n,而在其他系统则返回\n

我们内联fileSink变量,并且利用链式调用来重写上面的方法:

public void writeEnv(File file) throws IOException &#123;
  try (BufferedSink sink = Okio.buffer(Okio.sink(file))) &#123;
    for (Map.Entry<String, String> entry : System.getenv().entrySet()) &#123;
      sink.writeUtf8(entry.getKey())
          .writeUtf8("=")
          .writeUtf8(entry.getValue())
          .writeUtf8("\n");
    &#125;
  &#125;
&#125;

上面的代码,我们使用了writeUtf8()方法。调用四次writeUtf8()比下面的代码更高效,因为VM不用去对产生的临时字符串进行垃圾回收。

sink.writeUtf8(entry.getKey() + "=" + entry.getValue() + "\n"); // Slower!

UTF-8

从上面用到的API可以看出,Okio非常喜欢UTF-8。早期的电脑系统经历了各种字符编码:ISO-8859-1ShiftJISASCIIEBCDIC等等。为了支持多种字符集的编程非常糟糕,甚至,我们还要使用emoji。目前非常幸运的是,世界各地统一都支持UTF-8。只有很少一部分老系统还在使用其他字符集。

如果你需要另外的字符集,可以使用readString()writeString()。这两个方法需要你指定字符集。除非数据只需要本机读取,否则大多数情况下,都应该使用UTF-8方法来编程。

当在解析字符串时,你需要记住字符串时如何表示和编码的。当一个字形有声调或是其他变形时,它代表一个单独的混合字符,如é,或是一个字符e接一个修饰符´。当整个字形是一个单独的字符时,它被称为NFC,当它是多个字符组成时,被称为NFD

即使我们使用IO操作字符串时都使用UTF-8,他们在内存的存在形式是Java String,使用的是另一个变种UTF-16进行编码。这是非常糟糕的编码,因为它大部分字符都使用16字节字符,但是却用不到16个字节。特别是,emoji字符占两个java字符。这存在的问题是,String.length()返回不一样的结果:UTF-16的字符长度,并不是原生字形的长度。

Café 🍩 Café 🍩
Form NFC NFD
Code Points c a f é ␣ 🍩 c a f e ´ ␣ 🍩
UTF-8 bytes 43 61 66 c3a9 20 f09f8da9 43 61 66 65 cc81 20 f09f8da9
String.codePointCount 6 7
String.length 7 8
Utf8.size 10 11

大部分情况,Okio可以让你忽略这些问题更关注数据。但是当你需要这些时,有一些方便的API处理低级的UTF-8字符串。

使用Utf8.size()来统计使用UTF-8编码时的字节长度。这在如协议缓冲等长度前缀编码时非常方便。

使用BufferedSource.readUtf8CodePoint()读取单个可变长度代码点,使用BufferedSinkel.writeUtf8CodePoint()写入。

Golden Values

Okio喜欢测试。库已经经过了严格的测试,并且它有易于测试的特性。我们找到一种十分好用的测试模式,称为golden value测试。这种测试的目的是确认被早期版本的程序进行编码的数据能够安全的使用当前版本的程序进行解码。

我们将通过使用Java SerializationJava序列化)编码来进行演示。题外话,我们一定要放弃糟糕的Java序列化,大多数编程我们应该使用其他编码,如JSONprotobuf。下面是对一个对象进行序列化的方法,返回一个ByteString

private ByteString serialize(Object o) throws IOException &#123;
  Buffer buffer = new Buffer();
  try (ObjectOutputStream objectOut = new ObjectOutputStream(buffer.outputStream())) &#123;
    objectOut.writeObject(o);
  &#125;
  return buffer.readByteString();
&#125;

这里对上面的代码进行解释:

  1. 我们构造一个Buffer存储序列化后的数据。BufferByteArrayOutputStream更好的替代者。
  2. 我们获取BufferOutputStream,通过它,我们将数据写入Buffer中,并且总在Buffer结尾添加数据。
  3. 构造一个ObjectOutputStreamJava序列化API)并且写入对象。try块为我们自动关闭流。注意关闭Buffer没有用。
  4. 最后,调用Buffer.readByteString()方法,获取ByteString。这个方法允许我们指定读取的字节数,在这里我们不指定数量,获取整个字符串。从Buffer中读取数据,总是能保证数据从Buffer头部读取的。

通过上面的serialize()方法,我们准备好计算和打印golden value了。

Point point = new Point(8.0, 15.0);
ByteString pointBytes = serialize(point);
System.out.println(pointBytes.base64());

打印出ByteStringbase64值,是因为base64紧凑和便于嵌入测试用例的特性。程序打印如下:

rO0ABXNyAB5va2lvLnNhbXBsZXMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA

这就是我们的golden value了。我们再次使用base64键入到我们的测试用例中,将其转换回一个ByteString

ByteString goldenBytes = ByteString.decodeBase64("rO0ABXNyAB5va2lvLnNhbXBsZ"
    + "XMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuA"
    + "AAAAAAA");

下一步,将ByteString反序列成我们需要的对象。这个方法和上面的serialize()方法相反:添加一个ByteStringBuffer中,然后使用一个ObjectInputStream进行读取。

private Object deserialize(ByteString byteString) throws IOException, ClassNotFoundException &#123;
  Buffer buffer = new Buffer();
  buffer.write(byteString);
  try (ObjectInputStream objectIn = new ObjectInputStream(buffer.inputStream())) &#123;
    return objectIn.readObject();
  &#125;
&#125;

下面我们对golden value进行解码测试:

ByteString goldenBytes = ByteString.decodeBase64("rO0ABXNyAB5va2lvLnNhbXBsZ"
    + "XMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuA"
    + "AAAAAAA");
Point decoded = (Point) deserialize(goldenBytes);
assertEquals(new Point(8.0, 15.0), decoded);

通过这里的测试,我们可以在不破坏兼容性的情况下改变Point类的序列化。

写入二进制文件

对二进制文件编码和对文本文件编码没有什么不同。Okio同样使用BufferedSinkBufferedSource进行操作。这对那些既包含字节,有包含字符的二进制编码非常友好。

写入二进制数据比写入文字更加的危险,这是因为非常难诊断出出错的地方。围绕这些问题,可以通过注意下面几点来尽量避免:

  1. 每个域的宽度。这是使用的字节数量。Okio不包含发射部分字节的机制。如果需要这个功能,那么就需要在写入前自己进行位移和屏蔽。
  2. 每个域的字节序。所有超过一个字节的域都有字节序:字节是否经过了排序(从大到小的big endian和从小到达的little endian)。Okio使用在方法名加后缀Le的方式表示使用little-endian的方法;没有后缀则是使用big-endian
  3. 是否带符号。Java没有不带符号的基础类型(除了char),所以对于这种,通常是在应用层进行处理。为了保证更简单,OkiowriteByte()writeShort()接收int类型。这样你就可以传入一个无符号的byte如255(byte值最大是127),Okio可以保证写入正确。

解释:对于上面的第二点,如果写入一个int值3,如果是big endian,那么写入的则是00 00 00 03,如果是little endian则是03 00 00 00。对于上面的第三点,如果写入的一个值是255,如果是Java中的byte类型,它最大值是127,并不能写入255,所以Okio可以使用writeShort()方法写入int值,保证一个字节可以表示255

Method Width Endianness Value Encoded Value
writeByte 1 3 03
writeShort 2 big 3 00 03
writeInt 4 big 3 00 00 00 03
writeLong 8 big 3 00 00 00 00 00 00 00 03
writeShortLe 2 little 3 03 00
writeIntLe 4 little 3 03 00 00 00
writeLongLe 8 little 3 03 00 00 00 00 00 00 00
writeByte 1 Byte.MAX_VALUE 7f
writeShort 2 big Short.MAX_VALUE 7f ff
writeInt 4 big Int.MAX_VALUE 7f ff ff ff
writeLong 8 big Long.MAX_VALUE 7f ff ff ff ff ff ff ff
writeShortLe 2 little Short.MAX_VALUE ff 7f
writeIntLe 4 little Int.MAX_VALUE ff ff ff 7f
writeLongLe 8 little Long.MAX_VALUE ff ff ff ff ff ff ff 7f

下面的代码将一个bitmap写入成一个BMP的文件格式。

void encode(Bitmap bitmap, BufferedSink sink) throws IOException &#123;
  int height = bitmap.height();
  int width = bitmap.width();

  int bytesPerPixel = 3;
  int rowByteCountWithoutPadding = (bytesPerPixel * width);
  int rowByteCount = ((rowByteCountWithoutPadding + 3) / 4) * 4;
  int pixelDataSize = rowByteCount * height;
  int bmpHeaderSize = 14;
  int dibHeaderSize = 40;

  // BMP Header
  sink.writeUtf8("BM"); // ID.
  sink.writeIntLe(bmpHeaderSize + dibHeaderSize + pixelDataSize); // File size.
  sink.writeShortLe(0); // Unused.
  sink.writeShortLe(0); // Unused.
  sink.writeIntLe(bmpHeaderSize + dibHeaderSize); // Offset of pixel data.

  // DIB Header
  sink.writeIntLe(dibHeaderSize);
  sink.writeIntLe(width);
  sink.writeIntLe(height);
  sink.writeShortLe(1);  // Color plane count.
  sink.writeShortLe(bytesPerPixel * Byte.SIZE);
  sink.writeIntLe(0);    // No compression.
  sink.writeIntLe(16);   // Size of bitmap data including padding.
  sink.writeIntLe(2835); // Horizontal print resolution in pixels/meter. (72 dpi).
  sink.writeIntLe(2835); // Vertical print resolution in pixels/meter. (72 dpi).
  sink.writeIntLe(0);    // Palette color count.
  sink.writeIntLe(0);    // 0 important colors.

  // Pixel data.
  for (int y = height - 1; y >= 0; y--) &#123;
    for (int x = 0; x < width; x++) &#123;
      sink.writeByte(bitmap.blue(x, y));
      sink.writeByte(bitmap.green(x, y));
      sink.writeByte(bitmap.red(x, y));
    &#125;

    // Padding for 4-byte alignment.
    for (int p = rowByteCountWithoutPadding; p < rowByteCount; p++) &#123;
      sink.writeByte(0);
    &#125;
  &#125;
&#125;

这段代码最麻烦的部分是格式需要填充。BMP格式期望一行以4个字节的边界开始,所以添加0值进行对齐非常重要。

对其他二进制格式进行编码通常非常相似。下面是一些建议:

  1. 使用golden values编写测试代码。确保程序发送期望的结果,可以让测试更加的简单。
  2. 使用Utf8.size()计算一个编码的字符串的字节长度。这对长度前缀的格式非常有效。
  3. 使用Float.floatToIntBits()Double.doubleToLongBits()对浮点值进行编码。

通过Socket进行通信

通过网络发送和接受数据,和写入和读取文件非常相似,使用BufferedSink对输出值编码,使用BufferedSource对输入值编码。和文件一样,网络协议可以使用文本、二进制,或是两者的组合。但是网络和文件系统中也有很多的不同。

你只能同时读、或者写入一个文件,但是通过网络,你两者可以同时进行。一些协议通过循环的方式完成:发送一个请求,读取结果,重复。你可以通过单线程来实现这种协议。另外有的协议可能就允许你同时进行读写。典型的,你想使用一个专门的线程进行读。对于写,你可以使用一个专门的线程进行写,或是在多线程中使用同一个Sink使用。Okio的流对于多线程使用是不安全的。

Sink对输出的数据进行缓冲以最大化地减少IO操作。这是非常有效的,但是这意味着你必须手动调用flush()方法发送数据。典型地面向消息协议在每个消息之后都会进行flush。当缓冲数据超过一定阈值时,Okio将自动进行flush。这是为了节省内存,但你也不应该依赖这个机制。

Okio基于java.io.Socket进行连接。一个服务器或是客户端构造一个Socket,然后使用Okio.source(Socket)读取,Okio.sink(Socket)进行写。这两个API也可以和SSLSocket使用。你也应该尽量使用SSL

在任何线程中调用Socket.close()取消Socket,这样会导致SourceSink立即失败并抛出IOException。你也可以为所有的Socket操作配置超时。不需要为了配合超时去持有一个Socket的引用:SourceSink直接暴露超时。即使流被装饰过,这个API也可以工作。

作为使用Okio进行网络操作的一个完整的例子,我们写了一个基本的SOCKS代理服务器。下面是一些重点:

Socket fromSocket = ...
BufferedSource fromSource = Okio.buffer(Okio.source(fromSocket));
BufferedSink fromSink = Okio.buffer(Okio.sink(fromSocket));

Socket构造SourceSink和为文件构造它们是一样的。一旦构造得到SourceSink,那么你将禁止在使用InputStreamOutputStream

Buffer buffer = new Buffer();
for (long byteCount; (byteCount = source.read(buffer, 8192L)) != -1; ) &#123;
  sink.write(buffer, byteCount);
  sink.flush();
&#125;

上面的代码循环复制Source中的数据到Sink中,在每次读取之后进行flush。如果我们不需要flush,我们可以使用一句代码替换这个循环,BufferedSink.writeAll(Source)

参数8192指的是在每次返回之前读取的最大的字节数。我们可以在其中传入任何的值。但是我们喜欢8kb,因为这是Okio在一次系统调用中的最大值。大多数时候,应用代码不需要处理这个限制。

int addressType = fromSource.readByte() & 0xff;
int port = fromSource.readShort() & 0xffff;

Okio使用有符号的类型如byteshort,但是部分协议协议使用的是无符号的值。使用位操作符&将一个有无符号的值转换成一个无符号的值是Java的惯用方法。下面是byteshort、和int的一个转换备忘单(注意将byteshort转换成了intint转换成了long):

Type Signed Range Unsigned Range Signed to Unsigned
byte -128..127 0..255 int u = s & 0xff;
short -32,768..32,767 0..65,535 int u = s & 0xffff;
int -2,147,483,648..2,147,483,647 0..4,294,967,295 long u = s & 0xffffffffL;

Java没有可替代无符号long类型的基础类型。

哈希

作为Java程序员,我们都受到过哈希的轰炸。早期,我们被介绍hashCode()方法,我们知道需要重写这个方法,否则可能会发生意想不到的事情。后来我们我们接触LinkedHashMap和与其相关的类。它们组织数据以快速取出的机制都建立在hashCode()方法上。

在其他地方我们有用到加密哈希函数。这些都被广泛使用。如HTTPS证书、git提交,BitTorrent完整性检查和区块链分块,都使用了加密哈希。哈希使用得当可以提升性能、隐私、安全和应用的简洁性。

每个加密哈希函数接受一个变长的字节输入流,产生一个固定长度字节字符串(称为hash)。哈希函数有以下几个重要特征:

  • 确定性:同一个输入总是产生同样的输出。
  • 统一性:每个输出字节字符串都容易进行对比。很难去找到不同的输入有同样输出的哈希。这被称为“意外”。
  • 不可逆:知道输出并不能帮助你找到输入。注意,如果你知道一些可能的输入,你可以对他们进行hash,看他们的结果是否匹配。
  • 知名:哈希在各种地方都有实现,并且严格。

好的哈希函数计算消耗很低(大约10微秒),并且逆向消耗很高(以千年为单位)。计算稳定和数学性质使得一个好的哈希函数很难被逆向。当选择一个哈希函数时,一定注意不是所有的都是相等的。Okio支持以下几种知名的哈希加密算法:

  • MD5:一个128位(16字节)的哈希值。不安全并且已经过时了,因为逆向消耗并不是非常巨大。提供这种哈希算法是因为它非常流行,并且对于早期对安全不敏感的系统来说非常便捷。
  • SHA-1:一个160位(20字节)的哈希值。最近被证明是可能发生“意外”的。所以考虑从SHA-1升级到SHA-256
  • SHA-256:256位(32字节)的哈希值。SHA-256被广泛的理解,并且逆向消耗巨大。大多数系统应该使用这个哈希函数。
  • SHA-512:512位(64字节)的哈希值。非常难以逆向。

每一个哈希构造一个固定长度的ByteString。使用hex()方法获取方便的16进制字符串。或者保留为ByteString,因为ByteString是一个方便的数据类型。

Okio使用ByteString生成加密哈希值:

ByteString byteString = readByteString(new File("README.md"));
System.out.println("   md5: " + byteString.md5().hex());
System.out.println("  sha1: " + byteString.sha1().hex());
System.out.println("sha256: " + byteString.sha256().hex());
System.out.println("sha512: " + byteString.sha512().hex());

Buffer中读取:

Buffer buffer = readBuffer(new File("README.md"));
System.out.println("   md5: " + buffer.md5().hex());
System.out.println("  sha1: " + buffer.sha1().hex());
System.out.println("sha256: " + buffer.sha256().hex());
System.out.println("sha512: " + buffer.sha512().hex());

Source对应的输入流:

try (HashingSink hashingSink = HashingSink.sha256(Okio.blackhole());
     BufferedSource source = Okio.buffer(Okio.source(file))) &#123;
  source.readAll(hashingSink);
  System.out.println("sha256: " + hashingSink.hash().hex());
&#125;

Sink对应的输出流:

try (HashingSink hashingSink = HashingSink.sha256(Okio.blackhole());
     BufferedSink sink = Okio.buffer(hashingSink);
     Source source = Okio.source(file)) &#123;
  sink.writeAll(source);
  sink.close(); // Emit anything buffered.
  System.out.println("sha256: " + hashingSink.hash().hex());
&#125;

Okio也支持HMAC(),它组合密钥和一个哈希值。应用程序通常使用HMAC保证数据正确性和验证。

ByteString secret = ByteString.decodeHex("7065616e7574627574746572");
System.out.println("hmacSha256: " + byteString.hmacSha256(secret).hex());

通过哈希,你可以使用ByteStringBufferHashingSourceHashingSink生成HMAC。注意Okio没有实现MD5HMACOkio利用Javajava.security.MessageDigest生成哈希,使用javax.crypto.Mac生成HMAC


   转载规则


《Okio官方文档翻译》 Mycroft Wong 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
okhttp实现token验证 okhttp实现token验证
okhttp实现token验证前言公司目前的项目使用了token来验证用户。登陆之后会返回最新的access token,后续在每次请求API时,服务端会返回最新的access token,客户端进行保存。若一段时间内(假定是7天)没有进行
下一篇 
FileProvider FileProvider
FileProvider前言Android开发始终脱离不了图片处理,特别是Android 7.0开始,无法通过file:///的URI来进行在应用之间共享文件,取而代之的是content uri。这样必然增加了开发难度,如必须生成conte
2019-09-05
  目录