跳到内容

示例

我们编写了一些示例,演示如何使用 Okio 解决常见问题。阅读这些示例,了解它们如何协同工作。你可以随意复制粘贴这些示例;它们就是为此而设的。

这些示例适用于所有平台:Java、Android、Kotlin/Native 和 Kotlin/JS。请参阅java.io 示例,获取集成 Java API 的示例。

逐行读取文本文件 (Java/Kotlin)

使用 FileSystem.source(Path) 打开源流以读取文件。返回的 Source 接口非常小巧,用途有限。因此,我们用缓冲区包装源。这有两个好处

  • 它使 API 更强大。 BufferedSource 不仅提供 Source 的基本方法,还有几十种方法可以简洁地解决大多数常见问题。

  • 它使你的程序运行得更快。 缓冲使得 Okio 可以用更少的 I/O 操作完成更多工作。

每个打开的 Source 都需要关闭。打开流的代码负责确保它被关闭。

这里我们使用 Java 的 try 块来自动关闭源。

public void readLines(Path path) throws IOException {
  try (Source fileSource = FileSystem.SYSTEM.source(path);
       BufferedSource bufferedFileSource = Okio.buffer(fileSource)) {

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

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

  }
}

这使用 use 来自动关闭流。这可以防止资源泄露,即使抛出异常也是如此。

fun readLines(path: Path) {
  FileSystem.SYSTEM.source(path).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。

上面的 Java 程序可以通过内联 fileSource 变量并使用更简洁的 for 循环代替 while 来写得更紧凑

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

在 Kotlin 中,我们可以使用 FileSystem.read() 在代码块之前缓冲源,并在之后关闭源。在代码块主体中,this 是一个 BufferedSource

@Throws(IOException::class)
fun readLines(path: Path) {
  FileSystem.SYSTEM.read(path) {
    while (true) {
      val line = readUtf8Line() ?: break
      if ("square" in line) {
        println(line)
      }
    }
  }
}

readUtf8Line() 方法适用于解析大多数文件。对于某些用例,你也可以考虑使用 readUtf8LineStrict()。它类似,但要求每行都以 \n\r\n 终止。如果在遇到行终止符之前到达文件末尾,它将抛出 EOFException。严格版本还允许设置字节限制以防御格式错误输入。

public void readLines(Path path) throws IOException {
  try (BufferedSource source = Okio.buffer(FileSystem.SYSTEM.source(path))) {
    while (!source.exhausted()) {
      String line = source.readUtf8LineStrict(1024L);
      if (line.contains("square")) {
        System.out.println(line);
      }
    }
  }
}
@Throws(IOException::class)
fun readLines(path: Path) {
  FileSystem.SYSTEM.read(path) {
    while (!source.exhausted()) {
      val line = source.readUtf8LineStrict(1024)
      if ("square" in line) {
        println(line)
      }
    }
  }
}

写入文本文件 (Java/Kotlin)

上面我们使用 SourceBufferedSource 来读取文件。要写入文件,我们使用 SinkBufferedSink。缓冲的好处是相同的:更强大的 API 和更好的性能。

public void writeEnv(Path path) throws IOException {
  try (Sink fileSink = FileSystem.SYSTEM.sink(path);
       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 变量并利用方法链,我们可以更紧凑地编写上述程序

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

在 Kotlin 中,我们可以使用 FileSystem.write() 在代码块之前缓冲 Sink,并在之后关闭 Sink。在代码块主体中,this 是一个 BufferedSink

@Throws(IOException::class)
fun writeEnv(path: Path) {
  FileSystem.SYSTEM.write(path) {
    for ((key, value) in System.getenv()) {
      writeUtf8(key)
      writeUtf8("=")
      writeUtf8(value)
      writeUtf8("\n")
    }
  }
}

在上面的代码中,我们调用了四次 writeUtf8()。调用四次比下面的代码更有效,因为虚拟机不必创建和垃圾回收临时字符串。

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。这是一种糟糕的编码,因为它对大多数字符使用 16 位 char,但有些字符不适用。特别是,大多数表情符号使用两个 Java char。这会带来问题,因为 String.length() 返回一个令人惊讶的结果:UTF-16 char 的数量,而不是自然字形(glyph)的数量。

Café 🍩 Café 🍩
形式 NFC NFD
码点 c  a  f  é    ␣   🍩 c  a  f  e  ´    ␣   🍩
UTF-8 字节 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 在很大程度上让你忽略这些问题,专注于你的数据。但是当你需要时,它提供了处理低级 UTF-8 字符串的便捷 API。

使用 Utf8.size() 计算将字符串编码为 UTF-8 所需的字节数,而无需实际编码。这在诸如 protocol buffers 等长度前缀编码中非常方便。

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

public void dumpStringData(String s) throws IOException {
  System.out.println("                       " + s);
  System.out.println("        String.length: " + s.length());
  System.out.println("String.codePointCount: " + s.codePointCount(0, s.length()));
  System.out.println("            Utf8.size: " + Utf8.size(s));
  System.out.println("          UTF-8 bytes: " + ByteString.encodeUtf8(s).hex());
  System.out.println();
}
fun dumpStringData(s: String) {
  println("                       " + s)
  println("        String.length: " + s.length)
  println("String.codePointCount: " + s.codePointCount(0, s.length))
  println("            Utf8.size: " + s.utf8Size())
  println("          UTF-8 bytes: " + s.encodeUtf8().hex())
  println()
}

Golden Values (Java/Kotlin)

Okio 喜欢测试。库本身经过了大量测试,并且其功能在测试应用程序代码时经常很有帮助。我们发现一种非常有用的模式是“黄金值”测试(golden value testing)。这类测试的目标是确认使用程序早期版本编码的数据可以安全地由当前程序解码。

我们将通过使用 Java Serialization 编码一个值来对此进行说明。不过我们必须声明 Java Serialization 是一种糟糕的编码系统,大多数程序应该优先选择 JSON 或 protobuf 等其他格式!无论如何,这里有一个方法,它接收一个对象,对其进行序列化,并将结果作为 ByteString 返回

private ByteString serialize(Object o) throws IOException {
  Buffer buffer = new Buffer();
  try (ObjectOutputStream objectOut = new ObjectOutputStream(buffer.outputStream())) {
    objectOut.writeObject(o);
  }
  return buffer.readByteString();
}
@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() 方法,我们就可以计算并打印黄金值了。

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

我们将 ByteString 打印为 base64,因为它是一种紧凑的格式,适合嵌入到测试用例中。程序打印如下内容

rO0ABXNyAB5va2lvLnNhbXBsZXMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA

这就是我们的黄金值!我们可以再次使用 base64 将其转换回 ByteString 并嵌入到我们的测试用例中

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

下一步是将 ByteString 反序列化回我们的值类。这个方法是上面 serialize() 方法的反向操作:我们将一个字节字符串追加到缓冲区,然后使用 ObjectInputStream 消耗它

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

现在我们可以使用黄金值测试解码器了。

ByteString goldenBytes = ByteString.decodeBase64("rO0ABXNyAB5va2lvLnNhbXBsZ"
    + "XMuR29sZGVuVmFsdWUkUG9pbnTdUW8rMji1IwIAAkQAAXhEAAF5eHBAIAAAAAAAAEAuA"
    + "AAAAAAA");
Point decoded = (Point) deserialize(goldenBytes);
assertEquals(new Point(8.0, 15.0), decoded);
val goldenBytes = ("rO0ABXNyACRva2lvLnNhbXBsZXMuS290bGluR29sZGVuVmFsdWUkUG9pbnRF9yaY7cJ9EwIAA" +
  "kQAAXhEAAF5eHBAIAAAAAAAAEAuAAAAAAAA").decodeBase64()!!
val decoded = deserialize(goldenBytes) as Point
assertEquals(point, decoded)

通过这个测试,我们可以更改 Point 类的序列化方式,而不会破坏兼容性。

写入二进制文件 (Java/Kotlin)

编码二进制文件与编码文本文件并无太大区别。Okio 对两者都使用相同的 BufferedSinkBufferedSource 字节。这对于包含字节和字符数据的二进制格式来说非常方便。

写入二进制数据比文本更危险,因为一旦出错通常很难诊断。通过仔细注意这些陷阱来避免此类错误

  • 每个字段的宽度。 这是使用的字节数。Okio 不包含发送部分字节的机制。如果你需要这样做,则需要在写入之前自行进行位移和掩码操作。

  • 每个字段的字节序。 所有长度超过一个字节的字段都具有字节序(endianness):字节的排序方式是最高有效字节在前(大端序,big endian)还是最低有效字节在前(小端序,little endian)。Okio 为小端序方法使用 Le 后缀;没有后缀的方法是大端序。

  • 有符号 vs. 无符号。 Java 没有无符号基本类型(除了 char!),因此处理这个问题通常发生在应用程序层。为了使其更容易一些,Okio 接受 writeByte()writeShort()int 类型参数。你可以传递一个像 255 这样的“无符号”字节,Okio 将会正确处理。

方法 宽度 字节序 编码后的值
writeByte 1 3 03
writeShort 2 大端序 3 00 03
writeInt 4 大端序 3 00 00 00 03
writeLong 8 大端序 3 00 00 00 00 00 00 00 03
writeShortLe 2 小端序 3 03 00
writeIntLe 4 小端序 3 03 00 00 00
writeLongLe 8 小端序 3 03 00 00 00 00 00 00 00
writeByte 1 Byte.MAX_VALUE 7f
writeShort 2 大端序 Short.MAX_VALUE 7f ff
writeInt 4 大端序 Int.MAX_VALUE 7f ff ff ff
writeLong 8 大端序 Long.MAX_VALUE 7f ff ff ff ff ff ff ff
writeShortLe 2 小端序 Short.MAX_VALUE ff 7f
writeIntLe 4 小端序 Int.MAX_VALUE ff ff ff 7f
writeLongLe 8 小端序 Long.MAX_VALUE ff ff ff ff ff ff ff 7f

此代码按照BMP 文件格式编码位图。

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);
    }
  }
}
@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() 编码浮点值。

通过 Socket 通信 (Java/Kotlin)

请注意,Okio 尚未在 Kotlin/Native 或 Kotlin/JS 上支持 Socket。

通过网络发送和接收数据有点像读写文件。我们使用 BufferedSink 编码输出,使用 BufferedSource 解码输入。与文件一样,网络协议可以是文本、二进制或两者的混合。但是,网络和文件系统之间也存在一些显著差异。

对于文件,你要么读要么写,但对于网络,你可以同时进行!有些协议通过轮流处理:写入请求,读取响应,重复。你可以用单个线程实现这种协议。在其他协议中,你可能同时读写。通常,你会需要一个专门的线程进行读取。对于写入,你可以使用专门的线程,也可以使用 synchronized,以便多个线程可以共享一个 Sink。Okio 的流不适用于并发使用。

Sinks 缓冲传出数据以最小化 I/O 操作。这很高效,但这意味你必须手动调用 flush() 来传输数据。通常面向消息的协议在每条消息后都会刷新。请注意,当缓冲数据超过某个阈值时,Okio 会自动刷新。这旨在节省内存,你不应依赖它来实现交互式协议。

Okio 基于 java.io.Socket 提供连接功能。你可以将 Socket 创建为服务器或客户端,然后使用 Okio.source(Socket) 读取,使用 Okio.sink(Socket) 写入。这些 API 也适用于 SSLSocket。除非你有充分的理由不使用 SSL,否则应该使用它!

从任何线程调用 Socket.close() 可以取消一个 Socket;这将导致其 Source 和 Sink 立即失败并抛出 IOException。你还可以配置所有 Socket 操作的超时时间。你不需要 Socket 的引用来调整超时:SourceSink 直接暴露了超时设置。即使流被装饰,此 API 也能工作。

作为使用 Okio 进行网络通信的完整示例,我们编写了一个基本的 SOCKS 代理服务器。一些重点如下

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

为 Socket 创建 Source 和 Sink 与为文件创建 Source 和 Sink 相同。一旦为 Socket 创建了 SourceSink,就不能再分别使用它的 InputStreamOutputStream

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

上面的循环将数据从源复制到 Sink,并在每次读取后刷新。如果不需要刷新,我们可以用对 BufferedSink.writeAll(Source) 的单次调用来替换这个循环。

read() 方法的 8192 参数是在返回之前读取的最大字节数。我们可以在这里传递任何值,但我们喜欢 8 KiB,因为这是 Okio 可以在单个系统调用中处理的最大值。大多数时候,应用程序代码无需处理此类限制!

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

Okio 使用像 byteshort 这样的有符号类型,但协议通常需要无符号值。位运算符 & 是 Java 中将有符号值转换为无符号值的常用惯用法。这里是针对 byte、short 和 int 的备忘单

类型 有符号范围 无符号范围 有符号转无符号
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/Kotlin)

作为 Java 程序员,我们的日常生活中充斥着哈希。很早就接触到了 hashCode() 方法,我们知道需要重写它,否则会发生不可预见的糟糕情况。后来我们接触了 LinkedHashMap 及其同类。它们都基于 hashCode() 方法来组织数据以实现快速检索。

在其他地方,我们还有加密哈希函数。它们应用广泛。HTTPS 证书、Git 提交、BitTorrent 完整性检查以及区块链区块都使用了加密哈希。良好地使用哈希可以提高应用程序的性能、隐私性、安全性和简洁性。

每个加密哈希函数接受一个变长输入字节流,并产生一个固定长度的字节字符串值,称为“哈希”。哈希函数具有这些重要特性

  • 确定性:每个输入总是产生相同的输出。
  • 均匀性:每个输出字节字符串的可能性相同。很难找到或创建产生相同输出的不同输入对。这称为“冲突”(collision)。
  • 不可逆性:知道输出并不能帮助你找到输入。请注意,如果你知道一些可能的输入,你可以对它们进行哈希,看看它们的哈希是否匹配。
  • 众所周知:该哈希在各地都有实现,并得到了严格的理解。

好的哈希函数计算成本非常低廉(几十微秒),而逆向工程成本很高昂(万亿年)。计算和数学的稳步进步使得曾经优秀的哈希函数变得廉价可逆。选择哈希函数时,请注意并非所有函数都同等优越!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 可以从字节字符串生成加密哈希

ByteString byteString = readByteString(Path.get("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());
val byteString = readByteString("README.md".toPath())
println("       md5: " + byteString.md5().hex())
println("      sha1: " + byteString.sha1().hex())
println("    sha256: " + byteString.sha256().hex())
println("    sha512: " + byteString.sha512().hex())

从缓冲区

Buffer buffer = readBuffer(Path.get("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());
val buffer = readBuffer("README.md".toPath())
println("       md5: " + buffer.md5().hex())
println("      sha1: " + buffer.sha1().hex())
println("    sha256: " + buffer.sha256().hex())
println("    sha512: " + buffer.sha512().hex())

从源流式传输时

try (HashingSink hashingSink = HashingSink.sha256(Okio.blackhole());
     BufferedSource source = Okio.buffer(FileSystem.SYSTEM.source(path))) {
  source.readAll(hashingSink);
  System.out.println("sha256: " + hashingSink.hash().hex());
}
sha256(blackholeSink()).use { hashingSink ->
  FileSystem.SYSTEM.source(path).buffer().use { source ->
    source.readAll(hashingSink)
    println("    sha256: " + hashingSink.hash.hex())
  }
}

向 Sink 流式传输时

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

Okio 还支持 HMAC(哈希消息认证码),它结合了密钥和哈希。应用程序使用 HMAC 进行数据完整性和认证。

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

与哈希一样,你可以从 ByteStringBufferHashingSourceHashingSink 生成 HMAC。请注意,Okio 不为 MD5 实现 HMAC。

在 Android 和 Java 上,Okio 使用 Java 的 java.security.MessageDigest 进行加密哈希,使用 javax.crypto.Mac 进行 HMAC。在其他平台上,Okio 使用自己的优化实现这些算法。

加密和解密

在 Android 和 Java 上,加密流很容易。

调用者负责使用选定的算法、密钥以及初始化向量等算法特定的附加参数来初始化加密或解密密码器。以下示例展示了 AES 加密的典型用法,其中 keyiv 参数都应为 16 字节长。

使用 Okio.cipherSink(Sink, Cipher)Okio.cipherSource(Source, Cipher) 使用分组密码加密或解密流。

void encryptAes(ByteString bytes, Path path, 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(FileSystem.SYSTEM.sink(path), cipher))) {
    sink.write(bytes);
  }
}

ByteString decryptAesToByteString(Path path, 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(FileSystem.SYSTEM.source(path), cipher))) {
    return source.readByteString();
  }
}

加密和解密函数是 Cipher 的扩展

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

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