关于java:高效易用的IO库Okio应用篇

11次阅读

共计 12526 个字符,预计需要花费 32 分钟才能阅读完成。

OkHttp 是 Android 中蕴含的功能强大的 HTTP 客户端,此框架平时用的还挺多的,然而明天的配角是 OkHttp 的低层 IO 库——Okio,Okio 是对 java.io 和 java.nio 的补充,使拜访、存储和解决数据变得更加容易。这里是它的官网:https://square.github.io/okio/,最开始它作为 OKHttp 的一个组件,当初能够独立应用它来解决一些 IO 问题,接下来的内容来自对 Okio 官网的文档以及一些代码示例。

<!– more –>

ByteString 与 Buffer

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

ByteString 是一个不可变的字节序列,String 的根底是字符,而 ByteString 就像是 String 的兄弟一样,它能够轻松将二进制数据视为某些值。这个类十分聪慧:它晓得如何对本人进行十六进制,base64 和 UTF- 8 编码和解码。

Buffer 是可变的字节序列。与 ArrayList 一样,无需事后设置缓冲区大小。以队列的形式读取和写入缓冲区:将数据写入开端,而后从队列头部读取。没有必要去治理读取地位,范畴或容量。

在外部,ByteString 和 Buffer 做一些奇妙的事件来节俭 CPU 和内存。如果将 UTF- 8 字符串编码为 ByteString,它会缓存对该字符串的援用,以便当前进行解码时无需做任何工作。

缓冲区被实现为段的链表。当您将数据从一个缓冲区移到另一个缓冲区时,它会重新分配段的所有权,而不是跨缓冲区复制数据。这种办法对多线程程序特地有用:与网络申请相干的线程能够与工作线程替换数据,而无需任何复制或多余的操作。

Source 与 Sink

java.io 中的一个优雅的设计是如何对流进行分层来解决加密和压缩等转换。同样的 Okio 有本人的 stream 类型:Source 和 Sink,别离相似于 java 的 Inputstream 和 Outputstream,然而有一些要害区别:

  • 超时(Timeout):流提供了对底层 I / O 超时机制的拜访。与 java.io 的 socket 字节流不同,read()和 write()办法都给予超时机制。
  • 实现简略:Source 只申明了三个办法:read()、close()和 timeout()。没有像 available()或单字节读取这样会导致性能降落问题。
  • 使用方便:尽管 source 和 sink 中只有三个办法须要实现,然而调用方能够实现 Bufferedsource 和 Bufferedsink 接口,这两个接口提供了丰盛 API 可能满足你所须要的所有。
  • 字节流和字符流的解决没有直观的区别:因为它们都是数据。你能够以字节、UTF- 8 字符串、big-endian 的 32 位整数、little-endian 的短整数等任何你想要的模式进行读写;再也不须要 InputStreamReader!
  • 测试简略:Buffer 类同时实现了 BufferedSource 和 BufferedSink,因而测试代码简单明了。

Sources 和 Sinks 别离与 InputStream 和 OutputStream 交互操作。你能够将任何 Source 看做 InputStream,也能够将任何 InputStream 当做 Source。对于 Sink 和 Outputstream 也是如此。

Okio 的应用示例

这是它的 Maven 形式依赖:

<dependency>
    <groupId>com.squareup.okio</groupId>
    <artifactId>okio</artifactId>
    <version>2.9.0</version>
</dependency>

1、逐行读取文本

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;
            System.out.println(line);
        }
    }
}

其中 readUtf8Line()这个 API 读取所有数据,直到下一行分隔符 \n、\r\n 或文件开端。它以字符串模式返回该数据,并在最初省略定界符。当遇到空行时,该办法将返回一个空字符串。如果没有更多要读取的数据,它将返回 null,所以应用 for 来代替 while(true)也是 OK 的,这样的写法会让程序更加紧凑:

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

2、将字符串写入文本文件

下面咱们应用了 Source 和 BufferedSource 来读取文件。在写入文件时,咱们应用一个 Sink 和一个 BufferedSink。他们有着殊途同归之处:性能更弱小的 API 和更高的性能。

public void writeToFile(File file) throws IOException {try (Sink fileSink = Okio.sink(file);
         BufferedSink bufferedSink = Okio.buffer(fileSink)) {bufferedSink.writeUtf8("Hello");
        bufferedSink.writeUtf8("\n");
        bufferedSink.writeAll(Okio.source(new File("my.txt")));
    }
}

3、UTF- 8 编码

在以上 API 中,能够看到 Okio 十分喜爱 UTF-8。晚期的计算机系统遇到了许多不兼容的字符编码:ISO-8859-1,ASCII,EBCDIC 等。编写反对多种字符集的软件太蹩脚了,咱们甚至没有表情符号!明天,咱们很侥幸,全世界各地都曾经在 UTF- 8 上实现了标准化,而在遗留零碎中很少应用其余字符集。

如果你须要其余字符集,则能够应用 readString()和 writeString()。这些办法要求传入指定字符集的参数。否则,可能会意外地创立只能由本地计算机读取的数据,大多数程序应该仅应用 writeUtf8()这类办法。

只管每当咱们在 I / O 中读写字符串时都应用 UTF-8,但当它们在内存中时,Java 字符串会应用过期的字符编码 UTF-16。这是一种谬误的编码方式,因为它对大多数字符应用 16 位字符,但有些字符不适合。特地是,大多数表情符号应用两个 Java 字符。这是有问题的,因为 String.length()返回一个令人诧异的后果:UTF-16 字符数而不是字体本来的字符数量:

String s1 = "Café \uD83C\uDF69";
String s2 = "Café \uD83C\uDF69";
System.out.println(s.length());
System.out.println(s2.length());

在大多数状况下,Okio 能够让你疏忽这些问题并专一于数据。然而当你须要它们时,能够使用方便的 API 解决低级 UTF- 8 字符串。应用 Utf8.size()来计算将字符串编码为 UTF- 8 所需的字节数(然而并不会真正去做一次编码操作)。这在诸如协定缓冲区中解决固定长度前缀的时候十分不便。

应用 BufferedSource.readUtf8CodePoint()读取一个 Codepoint,并使 BufferedSink.writeUtf8CodePoint()写入一个 Codepoint。

4、序列化和反序列化

Okio 喜爱测试。该库自身曾经过严格的测试,咱们发现一种十分有用的模式是 ” 黄金价值 ” 测试,此类测试的目标是确认以后程序能够平安地解码应用程序的晚期版本编码的数据。

咱们将通过应用 Java 序列化对值进行编码来阐明这一点。只管咱们必须否定 Java 序列化是一个蹩脚的编码零碎,并且大多数程序应该更喜爱 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();}

这里应用 Buffer 对象代替 Java 的 ByteArrayOutputstream,而后从 buffer 中取得输入流对象,并通过 ObjectOutputStream 写入对象到 buffer 缓冲区当中,当你向 Buffer 中写数据时,总是会写到缓冲区的开端。最初,通过 buffer 对象的 readByteString()从缓冲区读取一个 ByteString 对象,这会从缓冲区的头部开始读取,readByteString()办法能够指定要读取的字节数,如果不指定,则读取全部内容。

咱们利用下面的办法将一个对象进行序列化,并失去的 ByteString 对象依照 base64 格局进行输入:

Point point = new Point(8, 15);
ByteString pointBytes = serialize(point);
System.out.println(pointBytes.base64());
rO0ABXNyAA5qYXZhLmF3dC5Qb2ludLbEinI0fsgmAgACSQABeEkAAXl4cAAAAAgAAAAP

Okio 将这个字符串称之为 Golden Value,接下来,咱们尝试将这个字符串(Golden Value)反序列化为一个 Point 对象,首先转回 ByteString 对象:

public static void main(String[] args) throws Exception {Point point = new Point(8, 15);
    ByteString pointBytes = new App().serialize(point);
    String base64 = pointBytes.base64();
    System.out.println(base64);

    ByteString byteString = ByteString.decodeBase64(base64);
    Point other = (Point) new App().deserialize(byteString);
    System.out.println(other.equals(point)); // true
}

private Object deserialize(ByteString byteString) throws IOException, ClassNotFoundException {Buffer buffer = new Buffer();
    buffer.write(byteString);
    try (ObjectInputStream objectIn = new ObjectInputStream(buffer.inputStream())) {Object result = objectIn.readObject();
        if (objectIn.read() != -1) throw new IOException("Unconsumed bytes in stream");
        return result;
    }
}

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

这样咱们能够在不毁坏兼容性的状况下更改对象的序列化形式。

这个序列化与 Java 原生的序列化有一个显著的区别就是 GodenValue 能够在不同客户端之间兼容(只有序列化和反序列化的 Class 是雷同的)。什么意思呢,比方我在 PC 端应用 Okio 序列化一个 User 对象生成的 GodenValue 字符串,这个字符串你拿到手机端照样能够反序列化进去 User 对象。

5、将字节流写入文件

编码二进制文件与编码文本文件没有什么不同。Okio 应用雷同的 BufferedSink 和 BufferedSource 字节。这对于同时蕴含字节和字符数据的二进制格局十分不便。写入二进制数据比写入文本更危险,因为如果你犯了谬误,通常很难诊断,防止这样的谬误须要留神以下几点:

  • 每个字段的宽度:即字节的数量。Okio 没有开释局部字节的机制。如果你需要的话,须要本人在写操作之前对字节进行 shift 和 mask 运算。
  • 每个字段的字节序:所有多字节的字段都具备结束符:字节的程序是从最高位到最低位(大字节 big endian),还是从最低位到最高位(小字节 little endian)。Okio 中针对小字节排序的办法都带有 Le 的后缀;而没有后缀的办法默认是大字节排序的。
  • 有符号和无符号:Java 没有无符号的根底类型(除了 char!)因而,在应用程序层常常会遇到这种状况。为方便使用,Okio 的 writeByte() 和 writeShort()办法能够承受 int 类型。你能够间接传递一个无符号字节像 255,Okio 会做正确的解决。
办法 宽度 字节排序 编码后的值
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

上面的示例代码是依照 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);
    }
  }
}

代码中对文件依照 BMP 的格局写入二进制数据,这会生成一个 bmp 格局的图片文件,BMP 格局要求每行以 4 字节开始,所以代码中加了很多 0 来做字节对齐。

编码其余二进制的格局十分类似。一些值得注意的点:

  • 应用 Golden values 编写测试,对于确认程序的预期后果能够使调试更容易。
  • 应用 Utf8.size() 办法计算编码字符串的字节长度。这对于 length-prefixed 格局必不可少。
  • 应用 Float.floatToIntBits()Double.doubleToLongBits()来编码浮点型的数值。

6、应用 Socket 进行通信

通过网络发送和接收数据有点像文件的读写。Okio 应用 BufferedSink 对输入进行编码,应用 BufferedSource 对输出进行解码。与文件一样,网络协议能够是文本、二进制或两者的混合。然而网络和文件系统之间也有一些实质性的区别。

当你有一个文件对象,你只能够抉择读或者写,然而网络与之不同的是能够同时进行读和写!在有一些协定中,解决这个问题的形式是轮流的进行:写入申请、读取响应、反复以上操作。你能够用一个单线程来实现这种协定。而在其余协定中,你能够同时进行读写。通常你须要一个专门的线程来读取数据。对于写入数据,你能够应用专门线程或者应用 synchronized,以便多个线程能够共享一个 Sink。Okio 的流在并发状况下应用是不平安的。

对于 Okio 的 Sinks 缓冲区,必须手动调用 flush()来传输数据,以最小化 I / O 操作。通常,面向音讯的协定会在每条音讯之后刷新。留神,当缓冲数据超过某个阈值时,Okio 将主动刷新。但这只是为了节俭内存,不能依赖它进行协定交互。

Okio 是基于 java.io.socket 建设连贯的,当你通过 socket 创立服务器或客户端后,能够应用 Okio.source(Socket)进行读取,应用 Okio.sink(Socket)进行写入,这些 API 也同样实用于 SSLSocket。

在任意线程中想要勾销 socket 连贯能够调用 Socket.close()办法,这将导致 sources 和 sinks 对象立刻抛出 IOException 而失败。Okio 中能够为所有的 socket 操作配置超时限度,但并不需要你去调用 Socket 的办法来设置超时:Source 和 Sink 会提供超时的接口,即便对流进行了装璜,此 API 依然无效。

Okio 官网 Demo 中编写了一个 简略的 Socket 代理服务 来示例残缺的网络交互操作,上面是其中的局部代码截取:

private void handleSocket(final Socket fromSocket) {
    try {final BufferedSource fromSource = Okio.buffer(Okio.source(fromSocket));
        final BufferedSink fromSink = Okio.buffer(Okio.sink(fromSocket));
        //..............
        //..................
    }  catch (IOException e) {.....}
}

能够看到通过 Socket 创立 sources 和 sinks 的形式与通过文件创建的形式一样,都是先通过 Okio.source()拿到 Socket 对应的 Source 或 Sink 对象,而后通过 Okio.buffer()获取对应的装璜者缓冲对象。在 Okio 中,一旦你为 Socket 对象创立了 Source 或者 Sink,那么你就不能再应用 InputStream 或 OutputStream 了。

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

以上代码中,循环从 source 中读取数据写入到 sink 当中,并调用 flush()进行刷新,如果你不须要每次写数据都进行 flush(),那么 for 循环里的两句能够应用 BufferedSink.writeAll(Source)一行代码来代替。

你会发现,在 read()办法中传递了一个 8192 作为读取的字节数,其实这里能够传任何数字,然而 Okio 更喜爱用 8 kib,因为这是 Okio 在单个零碎调用中所能解决的最大值。大多数时候利用程序代码不须要解决这样的限度!

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

Okio 应用的是有符号类型,如 byteshort,但通常协定须要的是无符号的值,而在 Java 中将有符号的值转换为无符号值的首选形式,就是通过是按位与 & 运算符。以下是字节、短整型和整型的转换清单:

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 型的根本类型。

7、哈希

哈希函数利用宽泛,如 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 字节)加密哈希。逆向操作老本很高。

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))) {source.readAll(hashingSink);
  System.out.println("sha256:" + hashingSink.hash().hex());
}

从 Sink 输入流失去哈希值:

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

Okio 还反对 HMAC(哈希音讯认证代码),它联合了一个秘钥值和一个 hash 值。应用程序能够应用 HMAC 进行数据完整性和身份验证:

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

同样样,你能够从 ByteString, Buffer, HashingSource, 和HashingSink 生成 HMAC。留神,Okio 没有为MD5 实现 HMAC。Okio 应用 Java 的java.security.MessageDigest 用于加密散列和javax.crypto.Mac 生成 HMAC。

8、加密和解密

应用 Okio.cipherSink(Sink,Cipher)或 Okio.cipherSource(Source,Cipher)应用区块加密算法对 Stream 进行加密或解密。调用者负责应用算法,密钥和特定于算法的附加参数(如初始化向量)初始化加密或解密明码。以下示例显示了 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();
  }
}

以上就是对 OKio 官网文档的局部翻译,英文比拟好的话能够参考官网文档:《Okio Reference》,对于 Okio 的具体实现细节等到前面的源码剖析文章再详谈。

正文完
 0