Okio—— 更加高效易用的IO库

在OkHttp的源码中经常能看到Okio的身影,所以单独拿出来学习一下,作为OkHttp的低层IO库,Okio确实比传统的java输入输出流读写更加方便高效。Okio补充了java.io和java.nio的不足,使访问、存储和处理数据更加容易,它起初只是作为OKHttp的一个组件,现在你可以独立的使用它来解决一些IO问题。

ByteStrings and Buffers

Okio是围绕这两种类型构建的,它们将大量功能打包到一个简单的API中:

  • ByteString是不可变的字节序列。对于字符数据,最基本的就是String。而ByteString就像是String的兄弟一般,它使得将二进制数据作为一个变量值变得容易。这个类很聪明:它知道如何将自己编码和解码为十六进制、base64和utf-8。
  • Buffer是一个可变的字节序列。像Arraylist一样,你不需要预先设置缓冲区的大小。你可以将缓冲区读写为一个队列:将数据写到队尾,然后从队头读取。

在内部,ByteStringBuffer做了一些聪明的事情来节省CPU和内存。如果您将UTF-8字符串编码为ByteString,它会缓存对该字符串的引用,这样,如果您稍后对其进行解码,就不需要做任何工作。


Buffer是作为片段的链表实现的。当您将数据从一个缓冲区移动到另一个缓冲区时,它会重新分配片段的持有关系,而不是跨片段复制数据。这对多线程特别有用:与网络交互的子线程可以与工作线程交换数据,而无需任何复制或多余的操作。

Sources and Sinks

java.io设计的一个优雅部分是如何对流进行分层来处理加密和压缩等转换。Okio有自己的stream类型: SourceSink,分别类似于java的InputstreamOutputstream,但是有一些关键区别:

  • 超时(Timeouts)。流提供了对底层I/O超时机制的访问。与java.io的socket字流不同,read()write()方法都给予超时机制。
  • 易于实施source只声明了三个方法:read()close()timeout()。没有像available()或单字节读取这样会导致正确性和性能意外的危险操作。
  • 使用方便。虽然sourcesink的实现只有三种方法可写,但是调用方可以实现BufferedsourceBufferedsink接口, 这两个接口提供了丰富API能够满足你所需的一切。
  • 字节流和字符流之间没有人为的区别。都是数据。你可以以字节、UTF-8字符串、big-endian的32位整数、little-endian的短整数等任何你想要的形式进行读写;不再有InputStreamReader
  • 易于测试Buffer类同时实现了BufferedSourceBufferedSink接口,因此测试代码简单明了。

Sources 和 Sinks分别与InputStreamOutputStream交互操作。你可以将任何Source看做InputStream ,也可以将任何InputStream 当做Source。对于SinkOutputstream也是如此。

读文本文件

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

这个示例代码是用来读取文本文件的,Okio通过Okio.source(File) 的方式来读取文件流,它返回的是一个Source对象,但是Source对象的方法是比较少的(只有3个),因此Okio提供了一个装饰者对象接口BufferedSource,通过Okio.buffer(fileSource)来生成,这个方法内部实际会生成一个RealBufferedSource类对象,RealBufferedSource内部持有Buffer缓冲对象可使IO速度更快,该类实现了BufferedSource接口,而BufferedSource接口提供了大量丰富的接口方法:

可以看到,几乎你想从输入流中读取任何的数据类型都可以,而不需要你自己去转换,可以说是非常强大而且人性化了,除了read方法以外,还有一些别的方法:

在上面的示例代码中,打开输入流对象的方法需要负责关闭对象资源,调用close方法,okio官方推荐使用java的try-with-source语法,上面示例代码可以写成下面这样:

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

try-with-source是jdk1.7开始提供的语法糖,在try语句()里面的资源对象,jdk最终会自动调用它的close方法去关闭它, 即便try里有多个资源对象也是可以的,这样就不用你手动去关闭资源了。但是在android里面使用的话,会提示你要求API level最低为19才可以。

readUtf8Line()方法适用于大多数文件。对于某些用例,还可以考虑使用readUtf8LineStrict()。类似readUtf8Line(),但它要求每一行都以\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);
        System.out.println(line);
    }
  }
}

写文本文件

    public void writeEnv(File file) throws IOException {
        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");
        }
        bufferedSink.close();
    }

类似于读文件使用SourceBufferedSource, 写文件的话,则是使用的SinkBufferedSink,同样的在BufferedSink接口中也提供了丰富的接口方法:

其中Okio.buffer(fileSink)内部返回的实现对象是一个RealBufferedSink类的对象, 跟RealBufferedSource一样它也是一个装饰者对象,具备Buffer缓冲功能。同样,以上代码可以使用jdk的try-with-source语法获得更加简便的写法:

public void writeEnv(File file) throws IOException {
  try (BufferedSink sink = Okio.buffer(Okio.sink(file))) {
       sink.writeUtf8("啊啊啊")
       	  .writeUtf8("=")
          .writeUtf8("aaa")
          .writeUtf8("\n");
  }
}

其中的换行符\n,Okio没有提供单独的api方法,而是要你手动写,因为这个跟操作系统有关,不过你可以使用System.lineSeparator()来代替\n,这个方法在Windows上返回的是"\r\n"在UNIX上返回的是"\n"。

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

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

Gzip压缩和读取

//zip压缩
GzipSink gzipSink = new GzipSink(Okio.sink(file));
BufferedSink bufferedSink = Okio.buffer(gzipSink);
bufferedSink.writeUtf8("this is zip file");
bufferedSink.flush();
bufferedSink.close();
 
//读取zip
GzipSource gzipSource = new GzipSource(Okio.source(file));
BufferedSource bufferedSource = Okio.buffer(gzipSource);
String s = bufferedSource.readUtf8();
bufferedSource.close();

UTF-8

在上面的代码中,可以看到使用的基本都是带UTF-8的读写方法。Okio推荐优先使用UTF-8的方法,why UTF-8? 这是因为UTF-8在世界各地都已标准化,而在早期的计算机系统中有许多不兼容的字符编码如:ISO-8859-1、ShiftJIS、 ASCII、EBCDIC等,使用UTF-8可以避免这些问题。

如果你需要使用其他编码的字符集,可以使用readString()writeString(),这两个方法可以指定字符编码参数,但在大多数情况下应该只使用带UTF-8的方法。

在编码字符串时,需要特别注意字符串的表达形式和编码方式。当字形有重音或其他装饰时,情况可能会有点复杂。尽管在I/O中读写字符串时使用的都是UTF-8,但是当在内存中,Java字符串使用的是已过时的UTF-16进行编码的。这是一种糟糕的编码格式,因为它对大多数字符使用 16-bit char,但有些字符不适合。特别是大多数的表情符号使用的是两个Java字符, 这时就会出现一个问题: String.length()返回的结果是utf-16字符的数量,而不是字体原本的字符数量。

Café ?Café ?
FormNFCNFD
Code Pointsc a f é ␣ ?c a f e ´ ␣ ?
UTF-8 bytes43 61 66 c3a9 20 f09f8da943 61 66 65 cc81 20 f09f8da9
String.codePointCount67
String.length78
Utf8.size1011

在大多数情况下,Okio无需你关心这些问题,从而可以将关注点放在数据本身的使用上。但是当你确实需要处理这些低级的UTF-8字符串问题时,也有一些方便的API来处理,如使用utf8.size()可以计算字符串按钮UTF-8形式编码的字节数(但是并不会实际编码),使用bufferedsource.readutf8codepoint()读取单个可变长度的Code Point,使用bufferedsink.writeutf8codepoint()写入一个Code Point

public final class ExploreCharsets {
    public void run() throws Exception {
        dumpStringData("Café \uD83C\uDF69"); // NFC: é is one code point.
        dumpStringData("Café \uD83C\uDF69"); // NFD: e is one code point, its accent is another.
    }

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

    private void readUtf8CodePoint(String s) throws IOException {
        Buffer buffer = new Buffer();
        buffer.writeString(s, Charset.forName("utf-8"));
        Source source = Okio.source(buffer.inputStream());
        BufferedSource bufferedSource = Okio.buffer(source);
        int i = -1;
        StringBuilder sb = new StringBuilder();
        while (!bufferedSource.exhausted() && (i = bufferedSource.readUtf8CodePoint()) != -1) {
            sb.append((char) i).append("---");
        }
        System.out.println("    readUtf8CodePoint: " + sb.toString());
        bufferedSource.close();
    }

    public static void main(String... args) throws Exception {
        new ExploreCharsets().run();
    }
}

序列化和反序列化

将一个对象进行序列化并以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的ByterrayOutputstream,然后从buffer中获得输出流对象,并通过ObjectOutputStream(来自java的Api)写入对象到buffer缓冲区当中,当你向Buffer中写数据时,总是会写到缓冲区的末尾。
最后,通过buffer对象的readByteString()从缓冲区读取一个ByteString对象,这会从缓冲区的头部开始读取,readByteString()方法可以指定要读取的字节数,如果不指定,则读取全部内容。

我们利用上面的方法将一个对象进行序列化,并得到的ByteString对象按照base64格式进行输出:

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

这样我们会得到输出的一串字符串:

rO0ABXNyADVjb20uZXhhbXBsZS5hc3VzLm15YXBwbGljYXRpb24ub2tpby5Hb2xkZW5WYWx1ZSRQb2ludIRwF2M5CCK2AgACRAABeEQAAXl4cEAgAAAAAAAAQC4AAAAAAAA=

Okio将这个字符串称之为 Golden Value

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

    ByteString goldenBytes = ByteString.decodeBase64("rO0ABXNyADVjb20uZXhhbXBsZS5hc3VzLm1"
 +"5YXBwbGljYXRpb24ub2tpby5Hb2xkZW5WYWx1ZSRQb2ludIRwF2M5CCK2AgACRAABeEQAAXl4cEAgAAAAAAAAQC4AAAAAAAA=");

然后将ByteString对象反序列化:

  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;
    }
  }
public void run() throws Exception {
    Point point = new Point(8.0, 15.0);
    ByteString pointBytes = serialize(point);
    System.out.println(pointBytes.base64());

    ByteString goldenBytes = ByteString.decodeBase64("rO0ABXNyADVjb20uZXhhbXBsZS5hc3VzLm15YXBwbGljYXRpb24ub2tpby5Hb2xkZW5WYWx1ZSRQb2ludIRwF2M5CCK2AgACRAABeEQAAXl4cEAgAAAAAAAAQC4AAAAAAAA=");
    Point decoded = (Point) deserialize(goldenBytes);
    if (point.x == decoded.x || point.y == decoded.y) {
        System.out.println("Equals");
    }
  }

输出:
在这里插入图片描述
这样我们可以在不破坏兼容性的情况下更改对象的序列化方式。

这个序列化与Java原生的序列化有一个明显的区别就是GodenValue可以在不同客户端之间兼容(只要序列化和反序列化的Class是相同的)。什么意思呢,比如我在PC端使用Okio序列化一个User对象生成的GodenValue字符串,这个字符串你拿到手机端照样可以反序列化出来User对象。

写二进制文件

编码二进制文件与编码文本文件没有什么不同,Okio使用相同的BufferedSinkBufferedSource字节。这对于同时包含字节和字符数据的二进制格式很方便。写二进制数据比写文本更容易出错,需要注意以下几点:

  • 字段的宽度 ,即字节的数量。Okio没有释放部分字节的机制。如果你需要的话,需要自己在写操作之前对字节进行shift和mask运算。
  • 字段的字节顺序 , 所有多字节的字段都具有结束符:字节的顺序是从最高位到最低位(大字节 big endian),还是从最低位到最高位(小字节 little endian)。Okio中针对小字节排序的方法都带有Le的后缀;而没有后缀的方法默认是大字节排序的。
  • 有符号和无符号,Java没有无符号的基础类型(除了char!)因此,在应用程序层经常会遇到这种情况。为方便使用,Okio的writeByte()writeShort()方法可以接受int类型。你可以直接传递一个“无符号”字节像255,Okio会做正确的处理。
方法宽度字节排序编码后的值
writeByte1303
writeShort2big300 03
writeInt4big300 00 00 03
writeLong8big300 00 00 00 00 00 00 03
writeShortLe2little303 00
writeIntLe4little303 00 00 00
writeLongLe8little303 00 00 00 00 00 00 00
writeByte1Byte.MAX_VALUE7f
writeShort2bigShort.MAX_VALUE7f ff
writeInt4bigInt.MAX_VALUE7f ff ff ff
writeLong8bigLong.MAX_VALUE7f ff ff ff ff ff ff ff
writeShortLe2littleShort.MAX_VALUEff 7f
writeIntLe4littleInt.MAX_VALUEff ff ff 7f
writeLongLe8littleLong.MAX_VALUEff ff ff ff ff ff ff 7f

下面的示例代码是按照 BMP文件格式 对文件进行编码:

public final class BitmapEncoder {
    static final class Bitmap {
        private final int[][] pixels;

        Bitmap(int[][] pixels) {
            this.pixels = pixels;
        }

        int width() {
            return pixels[0].length;
        }

        int height() {
            return pixels.length;
        }

        int red(int x, int y) {
            return (pixels[y][x] & 0xff0000) >> 16;
        }

        int green(int x, int y) {
            return (pixels[y][x] & 0xff00) >> 8;
        }

        int blue(int x, int y) {
            return (pixels[y][x] & 0xff);
        }
    }

    /**
     * Returns a bitmap that lights up red subpixels at the bottom, green subpixels on the right, and
     * blue subpixels in bottom-right.
     */
    Bitmap generateGradient() {
        int[][] pixels = new int[1080][1920];
        for (int y = 0; y < 1080; y++) {
            for (int x = 0; x < 1920; x++) {
                int r = (int) (y / 1080f * 255);
                int g = (int) (x / 1920f * 255);
                int b = (int) ((Math.hypot(x, y) / Math.hypot(1080, 1920)) * 255);
                pixels[y][x] = r << 16 | g << 8 | b;
            }
        }
        return new Bitmap(pixels);
    }

    void encode(Bitmap bitmap, File file) throws IOException {
        try (BufferedSink sink = Okio.buffer(Okio.sink(file))) {
            encode(bitmap, sink);
        }
    }

    /**
     * https://en.wikipedia.org/wiki/BMP_file_format
     */
    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);
            }
        }
    }

    public static void main(String[] args) throws Exception {
        BitmapEncoder encoder = new BitmapEncoder();
        Bitmap bitmap = encoder.generateGradient();
        encoder.encode(bitmap, new File("gradient.bmp"));
    }
}

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

编码其他二进制的格式非常相似。一些值得注意的点:

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

使用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的方法来设置超时:SourceSink会提供超时的接口。即使流对象被装饰,这个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对应的SourceSink对象,然后通过Okio.buffer()获取对应的装饰者缓冲对象。在Okio中,一旦你为Socket对象创建了Source 或者 Sink,那么你就不能再使用InputStreamOutputStream 了。

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中将有符号的值转换为无符号值的首选方式,就是通过是按位与&运算符。以下是字节、短整型和整型的转换清单:

TypeSigned RangeUnsigned RangeSigned to Unsigned
byte-128…1270…255int u = s & 0xff;
short-32,768…32,7670…65,535int u = s & 0xffff;
int-2,147,483,648…2,147,483,6470…4,294,967,295long u = s & 0xffffffffL;

Java中没有能够表示无符号的long型的基本类型。

Hashing

哈希散列函数应用广泛,如HTTPS证书、Git提交、BitTorrent完整性检查和区块链块等都使用到加密散列, 良好地使用哈希可以提高应用程序的性能、隐私性、安全性和简单性。每个加密哈希函数接受一个可变长度的字节输入流,并生成一个长度固定的字符串值,称之为“hash”值。哈希函数具有以下重要特性:

  • 确定性:每个输入总是产生相同的输出。
  • 统一:每个输出的字节字符串的可能性相同。很难找到或创建产生相同输出的不同输入对。即“碰撞”。
  • 不可逆:知道输出并不能帮助你找到输入。
  • 易于理解:哈希在很多环境中都已被实现并且被严格理解。

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

public ByteString readByteString(File file) throws IOException {
    try (BufferedSource source = Okio.buffer(Okio.source(file))) {
        return source.readByteString();
    }
}

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

public Buffer readBuffer(File file) throws IOException {
    try (Source source = Okio.source(file)) {
        Buffer buffer = new Buffer();
        buffer.writeAll(source);
        return buffer;
    }
}

从source输入流中获取hash值:

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

从sink输出流中获取hash值:

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。

以上,主要介绍了Okio的简单使用的一些API,其中的代码和说明主要是从Okio的GitHub的文档翻译过来的。总的来说,Okio这个库已经算是很强大很方便的了,几乎涵盖了所有你需要的东西。官方demo中还有一些代码,没有文档说明用途,暂时没有介绍。后面再单独来一篇简要分析学习一下OKio的源码组成结构和功能实现。目前的Okio版本源码已经是用Kotlin写的了。

参考:https://github.com/square/okio
Okio源码分析请参考下一篇博文:Okio源码学习分析

©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页