天天看点

Okio学习之官方文档翻译Okio学习之官方文档翻译ByteStrings and Buffers【字节字符串及缓冲区】Sources and Sinks【来源与接收器】Presentations【演示文稿】Recipes【菜谱】ReleasesR8 / ProGuardLicense【许可证】

Okio学习之官方文档翻译

链接: Okio官方文档连接

Okio是一个对

java.io

java.nio

的补充库。它使我们对数据的访问、存储和加工更加容易。

它最初是作为 OkHttp的一个组件库,OkHttp是Android中包含的功能强大的HTTP客户端。 它已被很好地锻炼,并准备解决新问题。

ByteStrings and Buffers【字节字符串及缓冲区】

Okio是围绕两种类型构建的,它们将大量功能集成到了简单的API中:

  • ByteString 是不可变的字节序列。对于字符数据,

    String

    是基础。

    ByteString

    是String失散已久的兄弟,它很容易将二进制数据视为数值。 此类是符合人体工程学的类:它知道如何以十六进制、base64和UTF-8编码和解码自身。
  • Buffer是一个可变的字节序列。与

    ArrayList

    一样,您不需要提前调整缓冲区的大小。作为队列读写缓冲区:将数据写到缓冲区的末尾,然后从缓冲区的前端读取数据。没有义务管理职位、限制或能力。

在内部,

ByteString

Buffer

做了一些聪明的事情来节省CPU和内存。如果您将一个UTF-8字符串编码为

ByteString

,它会缓存一个对该字符串的引用,这样如果您稍后解码它,就不用做任何工作了。

Buffer

被实现为段的链表。当您将数据从一个缓冲区移动到另一个缓冲区时,它将重新分配段的所有权,而不是跨缓冲区复制数据。这种方法对多线程程序特别有用:与网络对话的线程可以与工作线程交换数据,而无需任何复制或仪式。

Sources and Sinks【来源与接收器】

java.io

一个优雅的设计部分是如何对流进行分层以进行诸如加密和压缩之类的转换。 Okio包括自己的称为Source 和Sink 的流类型,它们的工作方式类似于

InputStream

OutputStream

,但有一些主要区别:

  • 超时. 流提供对基础I / O机制超时的访问。 与

    java.io

    套接字流不同,

    read()

    write()

    调用都允许超时。
  • 容易实现.

    Source

    定义了三个方法:

    read()

    ,

    close()

    , and

    timeout()

    . 不会出现像

    available()

    或单字节读取这样的会导致正确性和性能意外的危险。
  • 易于使用. 实现

    Source

    Sink

    类的时候虽然只有三个方法需要重写,调用者通过BufferedSource 和BufferedSink 接口被赋予丰富的API 。这些接口可在一个类中为您提供所需的一切。
  • 字节流和字符流之间没有人为的区别. 都是数据。以字节、UTF-8字符串、大端32位整数、小端短字符的形式读写;无论您想要什么。不再有

    InputStreamReader

  • 容易测试.

    Buffer

    同时实现了

    BufferedSource

    and

    BufferedSink

    接口,让你的测试代码是简单明了。

Sources and sinks 与

InputStream

OutputStream

可进行互相操作。 您可以将任何

Source

视为

InputStream

,也可以将任何

InputStream

视为

Source

。 对于

Sink

OutputStream

同样。

Presentations【演示文稿】

一些“Ok”库 (幻灯片): :介绍Okio和用它编写的三个库。

解码二进制数据的秘密 (幻灯片): 数据编码是如何工作的,Okio是如何做到的。

Ok的多平台! (幻灯片): 我们如何将Okio的实现语言从Java更改为Kotlin。

Recipes【菜谱】

我们编写了一些食谱,展示了如何解决Okio的常见问题。 通读它们以了解一切如何协同工作。 随意剪切并粘贴这些示例; 这就是他们的目的。

Read a text file line-by-line (Java/Kotlin)【逐行读取文本文件(Java / Kotlin)】

使用

Okio.source(File)

打开源流以读取文件。返回的

Source

接口非常小,用途有限。因此我们用buffer包装source 。这有两个好处:

  • 它使API更强大。与

    Source

    提供的基本方法不同,

    BufferedSource

    有几十种方法可以简洁地解决最常见的问题。
  • 它使你的程序运行得更快。缓冲允许Okio用更少的I/O操作完成更多的工作。

每个打开的

Source

都需要关闭。打开流的代码负责确保流已关闭。

Java

在这里,我们使用Java的

try

块自动关闭源。

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);
      }
    }

  }
}
           

Kotlin

请注意,静态

Okio

方法成为扩展函数(

Okio.source(file)=> file.source()

),并

use

用于自动关闭流:

@Throws(IOException::class)
fun readLines(file: File) {
  file.source().use { fileSource ->
    fileSource.buffer().use { bufferedFileSource ->
      while (true) {
        val line = bufferedFileSource.readUtf8Line() ?: break
        if ("square" in line) {
          println(line)
        }
      }
    }
  }
}
           

readUtf8Line()

API读取所有的数据,直到下一个行分隔符-要么

\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);
      }
    }
  }
}
           

在Kotlin中,我们可以包装

source.readUtf8Line()

转换为

generateSequence

builder,以创建一个在返回null后将结束的行序列。另外,由于扩展功能,转换流很容易:

@Throws(IOException::class)
fun readLines(file: File) {
  file.source().buffer().use { source ->
    generateSequence { source.readUtf8Line() }
      .filter { line -> "square" in line }
      .forEach(::println)
  }
}
           

readUtf8Line()

方法适用于解析大多数文件。 对于某些用例,您还可以考虑

readUtf8LineStrict()

。 后者与前者相似,但是要求每行以

\ n

\ r \ n

结尾。 如果在此之前遇到文件末尾,它将抛出

EOFException

。 严格的变体还允许字节限制以防止格式错误的输入。

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);
      }
    }
  }
}
           

这是用Kotlin编写的类似示例:

@Throws(IOException::class)
fun readLines(file: File) {
  file.source().buffer().use { source ->
    while (!source.exhausted()) {
      val line = source.readUtf8LineStrict(1024)
      if ("square" in line) {
        println(line)
      }
    }
  }
}
           

Write a text file(Java/Kotlin) 【编写文本文件】

在上面,我们使用

Source

BufferedSource

来读取文件。在编写时,我们使用

Sink

BufferedSink

。缓冲的优点是相同的:功能更强大的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()) {
      bufferedSink.writeUtf8(entry.getKey());
      bufferedSink.writeUtf8("=");
      bufferedSink.writeUtf8(entry.getValue());
      bufferedSink.writeUtf8("\n");
    }

  }
}
           

没有API可以编写一行输入;相反,我们手动插入自己的换行符。大多数程序应将硬编码

"\n"

作为换行符。在极少数情况下,您可以使用

System.lineSeparator()

代替

"\n"

:他在Windows上返回

"\r\n"

,在其他平台返回

"\n"

我们可以通过内联

fileSink

变量并利用方法链来更紧凑地编写上述程序:

Java

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

Kotlin

@Throws(IOException::class)
fun writeEnv(file: File) {
  file.sink().buffer().use { sink ->
    for ((key, value) in System.getenv()) {
      sink.writeUtf8(key)
      sink.writeUtf8("=")
      sink.writeUtf8(value)
      sink.writeUtf8("\n")
    }
  }
}
           

在上面的代码中,我们对

writeUtf8()

方法进行了四次调用。进行四次调用比下面的代码更有效,因为VM不必创建和垃圾收集临时字符串。

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

UTF-8(Java / Kotlin)

在以上API中,您可以看到Okio非常喜欢UTF-8。早期的计算机系统遇到了许多不兼容的字符编码:ISO-8859-1,ShiftJIS,ASCII,EBCDIC等。编写支持多种字符集的软件太糟糕了,我们甚至没有表情符号!今天,我们很幸运,世界各地都已在UTF-8上实现了标准化,而在遗留系统中很少使用其他字符集。

如果您需要另一个字符集,

readString()

并且

writeString()

在那里就可以找到。这些方法要求您指定字符集。否则,您可能会意外地创建只能由本地计算机读取的数据。大多数程序应仅使用UTF-8方法。

编码字符串时,您需要注意字符串的表示和编码的方式不同。当一个字形有重音或其他装饰时,它可以表示为一个单一的复杂代码点(

é

),也可以表示为一个简单的代码点(

e

),后跟它的修饰符(

´

)。当整个字形是一个称为NFC的代码点时;当它是多个时,它是NFD。

尽管每当我们在I / O中读写字符串时都使用UTF-8,但是当它们在内存中时,Java字符串会使用过时的字符编码UTF-16。这是一种错误的编码,因为它对

char

大多数字符使用16位,但是有些字符不合适。特别是,大多数表情符号使用两个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()

读取单个可变长度代码点,并且

BufferedSink.writeUtf8CodePoint()

代码点写一个。

Golden Values(Java / Kotlin)【黄金值】

Okio喜欢测试。该库本身已经过严格的测试,并且具有测试应用程序代码时通常有用的功能。我们发现非常有用的一种模式是“黄金价值”测试。此类测试的目的是确认当前程序可以安全地解码使用程序的早期版本编码的数据。

我们将通过使用Java序列化对值进行编码来说明这一点。尽管我们必须否认Java序列化是一个糟糕的编码系统,并且大多数程序应该更喜欢JSON或protobuf之类的其他格式!无论如何,这是一个获取对象,对其进行序列化并将结果返回为的方法

ByteString

Java

private ByteString serialize(Object o) throws IOException {
  Buffer buffer = new Buffer();
  try (ObjectOutputStream objectOut = new ObjectOutputStream(buffer.outputStream())) {
    objectOut.writeObject(o);
  }
  return buffer.readByteString();
}
           

Kotlin

@Throws(IOException::class)
private fun serialize(o: Any?): ByteString {
  val buffer = Buffer()
  ObjectOutputStream(buffer.outputStream()).use { objectOut ->
    objectOut.writeObject(o)
  }
  return buffer.readByteString()
}
           

这里有很多事情。

  1. 我们创建一个缓冲区作为序列化数据的保存空间。这是的便捷替代品

    ByteArrayOutputStream

  2. 我们向缓冲区请求其输出流。写入缓冲区或其输出流总是将数据追加到缓冲区的末尾。
  3. 我们创建一个

    ObjectOutputStream

    (用于Java序列化的编码API)并编写我们的对象。try块负责为我们关闭流。请注意,关闭缓冲区无效。
  4. 最后,我们从缓冲区读取一个字节字符串。该

    readByteString()

    方法允许我们指定要读取的字节数。在这里,我们没有指定计数来读取整个内容。从缓冲区读取数据总是消耗缓冲区前端的数据。

有了我们

serialize()

方便的方法,我们就可以计算和打印黄金值了。

Java

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

Kotlin

val point = Point(8.0, 15.0)
val pointBytes = serialize(point)
println(pointBytes.base64())
           

我们以base64打印

ByteString

,因为它是一种紧凑的格式,适合嵌入在测试用例中。程序将输出以下内容:

rO0ABXNyAB5va2lvLnNhbXBsZXMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA
           

那就是我们的黄金价值!我们可以再次使用base64将其嵌入测试用例中,以将其转换成一个

ByteString

Java

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

Kotlin

val goldenBytes = ("rO0ABXNyACRva2lvLnNhbXBsZXMuS290bGluR29sZGVuVmFsdWUkUG9pbnRF9yaY7cJ9EwIAA" +
  "kQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA").decodeBase64()
           

下一步是将

ByteString

反序列化到类中。此方法与上面的

serialize()

方法相反:我们将字节字符串附加到缓冲区,然后使用

ObjectInputStream

使用它:

Java

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

Kotlin

@Throws(IOException::class, ClassNotFoundException::class)
private fun deserialize(byteString: ByteString): Any? {
  val buffer = Buffer()
  buffer.write(byteString)
  ObjectInputStream(buffer.inputStream()).use { objectIn ->
    return objectIn.readObject()
  }
}
           

现在我们可以针对黄金值测试解码器:

Java

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

Kotlin

val goldenBytes = ("rO0ABXNyACRva2lvLnNhbXBsZXMuS290bGluR29sZGVuVmFsdWUkUG9pbnRF9yaY7cJ9EwIAA" +
  "kQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA").decodeBase64()!!
val decoded = deserialize(goldenBytes) as Point
assertEquals(point, decoded)
           

通过这个测试,我们可以在不破坏兼容性的情况下更改

Point

类的序列化。

Write a binary file(Java / Kotlin)【编写一个二进制文件】

编码二进制文件与编码文本文件没有什么不同。

Okio使用相同的

BufferedSink

BufferedSource

字节。对于包含字节和字符数据的二进制格式来说很方便。

编写二进制数据比编写文本危险得多,因为如果您犯了一个错误,则通常很难诊断。谨慎处理以下陷阱,避免发生此类错误:

  • **每个字段的宽度。**这是使用的字节数。Okio不包含发出部分字节的机制。如果需要,则需要在写入之前进行自己的移位和屏蔽。
  • **每个字段的字节序。**具有一个以上字节的所有字段都具有字节顺序:字节的顺序是从最高有效到最低(大字节序)或者从最低有效到最高(小字节序)。Okio将

    Le

    后缀用于小尾数法。没有后缀的方法是big-endian。
  • **有符号与无符号。**Java没有无符号基元类型(

    char

    !除外),因此,处理这一问题通常发生在应用程序层。为了使操作更简单,Okio接受

    writeByte()

    writeShort()

    int

    类型 。你可以传递一个像255这样的“无符号”字节,Okio会做正确的事。
方法 宽度 字节序 编码值
writeByte 1个 3

03

writeShort 2 3

00 03

writeInt 4 3

00 00 00 03

写长 8 3

00 00 00 00 00 00 00 03

writeShortLe 2 3

03 00

writeIntLe 4 3

03 00 00 00

写长乐 8 3

03 00 00 00 00 00 00 00

writeByte 1个 Byte.MAX_VALUE

7f

writeShort 2 空头。MAX_VALUE

7f ff

writeInt 4 整数MAX_VALUE

7f ff ff ff

写长 8 Long.MAX_VALUE

7f ff ff ff ff ff ff ff

writeShortLe 2 空头。MAX_VALUE

ff 7f

writeIntLe 4 整数MAX_VALUE

ff ff ff 7f

写长乐 8 Long.MAX_VALUE

ff ff ff ff ff ff ff 7f

该代码按照BMP文件格式对位图进行编码。

Java

void encode(Bitmap bitmap, BufferedSink sink) throws IOException {
  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--) {
    for (int x = 0; x < width; x++) {
      sink.writeByte(bitmap.blue(x, y));
      sink.writeByte(bitmap.green(x, y));
      sink.writeByte(bitmap.red(x, y));
    }

    // Padding for 4-byte alignment.
    for (int p = rowByteCountWithoutPadding; p < rowByteCount; p++) {
      sink.writeByte(0);
    }
  }
}
           

Kotin

@Throws(IOException::class)
fun encode(bitmap: Bitmap, sink: BufferedSink) {
  val height = bitmap.height
  val width = bitmap.width
  val bytesPerPixel = 3
  val rowByteCountWithoutPadding = bytesPerPixel * width
  val rowByteCount = (rowByteCountWithoutPadding + 3) / 4 * 4
  val pixelDataSize = rowByteCount * height
  val bmpHeaderSize = 14
  val 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_BITS)
  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 (y in height - 1 downTo 0) {
    for (x in 0 until width) {
      sink.writeByte(bitmap.blue(x, y))
      sink.writeByte(bitmap.green(x, y))
      sink.writeByte(bitmap.red(x, y))
    }

    // Padding for 4-byte alignment.
    for (p in rowByteCountWithoutPadding until rowByteCount) {
      sink.writeByte(0)
    }
  }
}
           

这个程序最棘手的部分是格式所需的填充。BMP格式要求每一行从4字节的边界开始,因此需要添加零来保持对齐。

编码其他二进制格式通常非常相似。一些技巧:

  • 用黄金值编写测试!确认您的程序发出了预期的结果可以使调试更加容易。
  • 使用

    Utf8.size()

    计算编码序列的字节数。这对于长度前缀的格式至关重要。
  • 使用

    Float.floatToIntBits()

    Double.doubleToLongBits()

    编码浮点值。

Communicate on a Socket (Java/Kotlin)【在套接字上通信】

通过网络发送和接收数据有点像读写文件。我们使用

BufferedSink

对输出进行编码,使用

BufferedSource

对输入进行解码。与文件一样,网络协议可以是文本、二进制或两者的混合。但是网络和文件系统之间也有一些实质性的区别。

有了一个文件,你可以读或写,但有了网络,你可以两者兼得!有些协议轮流处理这个问题:写请求,读响应,重复。你可以用一个线程来实现这种协议。在其他协议中,你可以同时读写。通常你需要一个专门的线程来阅读。对于编写,您可以使用专用线程或使用

synchronized

以便多个线程可以共享一个接收器。Okio的流对于并发使用是不安全的。

接收缓冲区出站数据以最小化I/O操作。这是有效的,但这意味着您必须手动调用

flush()

来传输数据。通常面向消息的协议在每条消息之后刷新。请注意,当缓冲数据超过某个阈值时,Okio将自动刷新。这是为了节省内存,你不应该依赖它来进行交互协议。

Okio建立在

java.io.Socket

接口为了连通性。将套接字创建为服务器或客户端,然后使用

Okio.source(Socket)

读取和

Okio.sink(Socket)

写入。这些api也可以与

SSLSocket

一起工作。你应该使用SSL,除非你有很好的理由不使用!

取消一个Socket可通过任何线程调用

Socket.close()

;这将导致其源和接收器立即失败,并出现

IOException

。还可以为所有套接字操作配置超时。您不需要对套接字的引用来调整超时:

Source

and

Sink

直接暴露超时。即使流被修饰,这个API也能工作。

作为与Okio联网的完整示例,我们编写了一个基本的SOCKS代理服务器。一些重点:

Java

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

Kotlin

val fromSocket: Socket = ...
val fromSource = fromSocket.source().buffer()
val fromSink = fromSocket.sink().buffer()
           

为套接字创建源和接收器与为文件创建源和接收器相同。一旦为套接字创建了

Source

and

Sink

,就不能分别使用其

InputStream

OutputStream

Java

Buffer buffer = new Buffer();
for (long byteCount; (byteCount = source.read(buffer, 8192L)) != -1; ) {
  sink.write(buffer, byteCount);
  sink.flush();
}
           

Kotlin

val buffer = Buffer()
var byteCount: Long
while (source.read(buffer, 8192L).also { byteCount = it } != -1L) {
  sink.write(buffer, byteCount)
  sink.flush()
}
           

上面的循环将数据从源复制到接收器,每次读取后都会刷新。如果我们不需要刷新,我们可以用一个调用来替换这个循环

BufferedSink.writeAll(Source)

read()

8192

参数是返回之前要读取的最大字节数。我们可以在这里传递任何值,但是我们喜欢8kib,因为这是Okio在单个系统调用中可以做的最大值。大多数时候应用程序代码不需要处理这些限制!

Java

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

Kotlin

val addressType = fromSource.readByte().toInt() and 0xff
val port = fromSource.readShort().toInt() and 0xffff
           

Okio使用带符号的类型,例如

byte

short

,但是协议通常需要无符号的值。按位运算

&

符是Java首选的习惯用法,用于将有符号值转换为无符号值。这是有关bytes, shorts, and ints的备忘单:

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没有可以表示无符号长型的原始类型。

Hashing (Java/Kotlin)【散列】

在Java程序员的生活中,我们受到哈希算法的轰炸。早先介绍了该

hashCode()

方法,我们知道需要重载一些东西,否则将发生无法预料的不良情况。后来我们显示了

LinkedHashMap

它的朋友。这些基于该

hashCode()

方法来组织数据以便快速检索。

在其他地方,我们具有加密哈希函数。这些到处都有使用。HTTPS证书,Git提交,BitTorrent完整性检查和区块链块均使用加密哈希。良好地使用哈希可以提高应用程序的性能,隐私性,安全性和简便性。

每个加密哈希函数都接受输入字节的可变长度流,并生成一个称为“哈希”的固定长度字节字符串值。哈希函数具有以下重要特征:

  • 确定性的:每个输入总是产生相同的输出。
  • 统一:每个输出字节字符串的可能性相等。很难找到或创建产生相同输出的不同输入对。这就是所谓的“碰撞”。
  • 不可逆的:知道输出并不能帮助您找到输入。请注意,如果您知道一些可能的输入,则可以对它们进行散列以查看其散列是否匹配。
  • 众所周知:哈希在任何地方都得到了实现,并且得到了严格的理解。

好的散列函数的计算成本非常低(几十微秒),而反转的成本也很高(千分之五)。计算和数学的稳步发展使得曾经伟大的散列函数变得更便宜。选择哈希函数时,请注意并非所有函数都是相等的!Okio支持以下众所周知的加密哈希函数:

  • MD5:128位(16字节)加密哈希。它既不安全又过时,因为它的逆转并不难!提供这个散列是因为它在不安全敏感的遗留系统中很流行,而且使用起来很方便。
  • SHA-1:160位(20字节)加密哈希。最近的研究表明,制造SHA-1碰撞是可行的。考虑从SHA-1升级到SHA-256。
  • SHA-256:256位(32字节)加密哈希。SHA-256被广泛理解,而且逆转成本高昂。这是大多数系统应该使用的哈希。
  • SHA-512:512位(64字节)加密哈希。逆转成本很高。

每个哈希都创建一个指定长度的

ByteString

。使用

hex()

获得常规的人类可读的形式。或将其保留为

ByteString

因为这是一种方便的模型类型!

Okio可以从字节字符串生成加密哈希:

Java

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());
           

Kotlin

val byteString = readByteString(File("README.md"))
println("       md5: " + byteString.md5().hex())
println("      sha1: " + byteString.sha1().hex())
println("    sha256: " + byteString.sha256().hex())
println("    sha512: " + byteString.sha512().hex())
           

从缓冲区:

Java

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());
           

Kotlin

val buffer = readBuffer(File("README.md"))
println("       md5: " + buffer.md5().hex())
println("      sha1: " + buffer.sha1().hex())
println("    sha256: " + buffer.sha256().hex())
println("    sha512: " + buffer.sha512().hex())
           

从源进行流式处理时:

Java

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

Kotlin

sha256(blackholeSink()).use { hashingSink ->
  file.source().buffer().use { source ->
    source.readAll(hashingSink)
    println("    sha256: " + hashingSink.hash.hex())
  }
}
           

流到接收器【sink】时:

Java

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

Kotlin

sha256(blackholeSink()).use { hashingSink ->
  hashingSink.buffer().use { sink ->
    file.source().use { source ->
      sink.writeAll(source)
      sink.close() // Emit anything buffered.
      println("    sha256: " + hashingSink.hash.hex())
    }
  }
}
           

Okio还支持HMAC(哈希消息验证码),它结合了秘密和哈希。应用程序使用HMAC来实现数据完整性和身份验证。

Java

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

Kotlin

val secret = "7065616e7574627574746572".decodeHex()
println("hmacSha256: " + byteString.hmacSha256(secret).hex())
           

与哈希一样,可以从

ByteString

Buffer

HashingSource

HashingSink

生成HMAC。注意,Okio没有为MD5实现HMAC。Okio使用Java的

java.security.MessageDigest

对于加密哈希和

javax.crypto.Mac

对于HMAC。

Encryption and Decryption 【加密与解密】

使用

Okio.cipherSink(Sink, Cipher)

或者

Okio.cipherSource(Source, Cipher)

使用分组密码对流进行加密或解密。

调用者负责使用所选算法、密钥和特定于算法的附加参数(如初始化向量)初始化加密或解密密码。 以下示例显示了AES加密的典型用法,其中

key

iv

参数都应为16个字节长。

void encryptAes(ByteString bytes, File file, byte[] key, byte[] iv)
    throws GeneralSecurityException, IOException {
  Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
  cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
  try (BufferedSink sink = Okio.buffer(Okio.cipherSink(Okio.sink(file), cipher))) {
    sink.write(bytes);
  }
}

ByteString decryptAesToByteString(File file, byte[] key, byte[] iv)
    throws GeneralSecurityException, IOException {
  Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
  cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
  try (BufferedSource source = Okio.buffer(Okio.cipherSource(Okio.source(file), cipher))) {
    return source.readByteString();
  }
}
           

在Kotlin中,这些加密和解密方法是

Cipher

的扩展:

fun encryptAes(bytes: ByteString, file: File, key: ByteArray, iv: ByteArray) {
  val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
  cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
  val cipherSink = file.sink().cipherSink(cipher)
  cipherSink.buffer().use { 
    it.write(bytes) 
  }
}

fun decryptAesToByteString(file: File, key: ByteArray, iv: ByteArray): ByteString {
  val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
  cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
  val cipherSource = file.source().cipherSource(cipher)
  return cipherSource.buffer().use { 
    it.readByteString()
  }
}
           

Releases

release版本的修改日志

implementation("com.squareup.okio:okio:2.9.0")
           

快照版本也是可用的

repositories {
    maven {
        url = uri("https://oss.sonatype.org/content/repositories/snapshots/")
    }
}

dependencies {
   implementation("com.squareup.okio:okio:2.9.0")
}
           

R8 / ProGuard

If you are using R8 or ProGuard add the options from this file.

# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.

-dontwarn org.codehaus.mojo.animal_sniffer.*

License【许可证】

Copyright 2013 Square, Inc. Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.