netty 系列之:java 中的 base64 编码器

Posted 倾听铃的声

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了netty 系列之:java 中的 base64 编码器相关的知识,希望对你有一定的参考价值。

 

简介

什么是 Base64 编码呢?在回答这个问题之前,我们需要了解一下计算机中文件的分类,对于计算机来说文件可以分为两类,一类是文本文件,一类是二进制文件。

对于二进制文件来说,其内容是用二进制来表示的,对于人类是不可立马理解的。如果你尝试用文本编辑器打开二进制文件,可能会看到乱码。这是因为二进制文件的编码方式和文本文件的编码方式是不一样的,所以当文本编辑器尝试将二进制文件翻译成为文本内容的时候,就会出现乱码。

对于文本文件来说,也有很多种编码方式,比如最早的 ASCII 编码和目前常用的 UTF-8 和 UTF-16 等编码方式。即使是文本文件,如果你使用不同的编码方式打开,也可能会看到乱码。

所以不管是文本文件还是二进制文件也好,都需要进行编码格式的统一。也就是说写入的编码是什么样子的,那么数据读取的编码也应该和其匹配。

Base64 编码实际上就是将二进制数据编码成为可视化 ASCII 字符的一种编码方式。

为什么会有这样的要求呢?

我们知道计算机世界的发展不是一蹴而就的,它是一个慢慢成长的过程,对于字符编码来说,最早只支持 ASCII 编码,后面才扩展到 Unicode 等。所以对于很多应用来说除了 ASCII 编码之外的其他编码格式是不支持的,那么如何在这些系统中展示非 ASCII code 呢?

解决的方式就是进行编码映射,将非 ASCII 的字符映射成为 ASCII 的字符。而 base64 就是这样的一种编码方式。

常见的使用 Base64 的地方就是在 web 网页中,有时候我们需要在网页中展示图片,那么可以将图片进行 base64 编码,然后填充到 html 中。

还有一种应用就是将文件进行 base64 编码,然后作为邮件的附件进行发送。

JAVA 对 base64 的支持

既然 base64 编码这么好用,接下来我们来看一下 JAVA 中的 base64 实现。

java 中有一个对应的 base64 实现,叫做 java.util.Base64。这个类是 Base64 的工具类,是 JDK 在 1.8 版本引入的。

Base64 中提供了三个 getEncoder 和 getDecoder 方法,通过获取对应的 Encoder 和 Decoder,然后就可以调用 Encoder 的 encode 和 decode 方法对数据进行编码和解码,非常的方便。

我们先来看一下 Base64 的基本使用例子:

 // 使用encoder进行编码 String encodedString = Base64.getEncoder().encodeToString("what is your name baby?".getBytes("utf-8")); System.out.println("Base64编码过后的字符串 :" + encodedString);
 // 使用encoder进行解码 byte[] decodedBytes = Base64.getDecoder().decode(encodedString);
 System.out.println("解码过后的字符串: " + new String(decodedBytes, "utf-8"));

作为一个工具类,JDK 中提供的 Base64 工具类还是很好用的。

这里就不详细讲解它的使用,本篇文章主要分析 JDK 中 Base64 是怎么实现的。

JDK 中 Base64 的分类和实现

JDK 中 Base64 类有提供了三个 encoder 方法,分别是 getEncoder,getUrlEncoder 和 getMimeEncoder:

    public static Encoder getEncoder()          return Encoder.RFC4648;    
    public static Encoder getUrlEncoder()          return Encoder.RFC4648_URLSAFE;    
    public static Encoder getMimeEncoder()         return Encoder.RFC2045;    

同样的,它也提供了三个对应的 decoder,分别是 getDecoder,getUrlDecoder,getMimeDecoder:

    public static Decoder getDecoder()          return Decoder.RFC4648;    
    public static Decoder getUrlDecoder()          return Decoder.RFC4648_URLSAFE;    
    public static Decoder getMimeDecoder()          return Decoder.RFC2045;    

从代码中可以看出,这三种编码分别对应的是 RFC4648,RFC4648_URLSAFE 和 RFC2045。

这三种都属于 base64 编码的变体,我们看下他们有什么区别:

可以看到 base64 和 Base64url 的区别是第 62 位和第 63 位的编码字符不一样,而 base64 for MIME 跟 base64 的区别是补全符是否是强制的。

另外,对于 Basic 和 base64url 来说,不会添加 line separator 字符,而 base64 for MIME 在一行超出 76 字符之后,会添加'\\r' 和 '\\n'作为 line separator。

最后,如果在解码的过程中,发现有不存于 Base64 映射表中的字符的处理方式也不一样,base64 和 Base64url 会直接拒绝,而 base64 for MIME 则会忽略。

base64 和 Base64url 的区别可以通过下面两个方法来看出:

        private static final char[] toBase64 =             'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',            'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',            'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'        ;
        private static final char[] toBase64URL =             'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',            'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',            'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_'        ;

而对 MIME 来说,定义了一个一行的最大字符个数,和换行符:

        private static final int MIMELINEMAX = 76;        private static final byte[] CRLF = new byte[] '\\r', '\\n';

Base64 的高级用法

一般情况下我们用 Base64 进行编码的对象长度是固定的,我们只需要将输入对象转换成为 byte 数组即可调用 encode 或者 decode 的方法。

但是在某些情况下我们需要对流数据进行转换,这时候就可以用到 Base64 中提供的两个对 Stream 进行 wrap 的方法:

        public OutputStream wrap(OutputStream os)             Objects.requireNonNull(os);            return new EncOutputStream(os, isURL ? toBase64URL : toBase64,                                       newline, linemax, doPadding);        
        public InputStream wrap(InputStream is)             Objects.requireNonNull(is);            return new DecInputStream(is, isURL ? fromBase64URL : fromBase64, isMIME);        

这两个方法分别对应于 encoder 和 decoder。

总结

以上就是 JDK 中对 Base64 的实现和使用,虽然 base64 的变种有很多种,但是 JDK 中的 Base64 只实现了其中用处最为广泛的 3 种。大家在使用的时候一定要区分具体是那种 Base64 的实现方式,以免出现问题。

需要点化的小伙伴请点赞收藏+评论转发+关注我之后私信我,注意回复【000】即可获取更多免费资料!

 

netty系列之:不用怀疑,netty中的ByteBuf就是比JAVA中的好用

简介

netty作为一个优秀的的NIO框架,被广泛应用于各种服务器和框架中。同样是NIO,netty所依赖的JDK在1.4版本中早就提供nio的包,既然JDK已经有了nio的包,为什么netty还要再写一个呢?

不是因为JDK不优秀,而是因为netty的要求有点高。

ByteBuf和ByteBuffer的可扩展性

在讲解netty中的ByteBuf如何优秀之前,我们先来看一下netty中的ByteBuf和jdk中的ByteBuffer有什么关系。

事实上,没啥关系,只是名字长的有点像而已。

jdk中的ByteBuffer,全称是java.nio.ByteBuffer,属于JAVA nio包中的一个基础类。它的定义如下:

public abstract class ByteBuffer
    extends Buffer
    implements Comparable<ByteBuffer>

而netty中的ByteBuf,全称是io.netty.buffer,属于netty nio包中的一个基础类。它的定义如下:

public abstract class ByteBuf 
implements ReferenceCounted, Comparable<ByteBuf>

两者的定义都很类似,两者都是抽象类,都需要具体的类来实现他们。

但是,当你尝试去创建一个类来继承JDK的ByteBuffer,则会发现继承不了,为什么命名一个abstract的类会继承不了呢?

仔细研究会发现,在ByteBuffer中,定义了下面两个没有显示标记其作用域访问的方法:

    abstract byte _get(int i);                          // package-private
    abstract void _put(int i, byte b);                  // package-private

根据JDK的定义,没有显示标记作用域的方法,默认其访问访问是package,当这两个方法又都是abstract的,所以只有同一个package的类才能继承JDK的ByteBuffer。

当然,JDK本身有5个ByteBuffer的实现,他们分别是DirectByteBuffer,DirectByteBufferR,HeapByteBuffer,HeapByteBufferR和MappedByteBuffer。

但是JDK限制了用户自定义类对ByteBuffer的扩展。虽然这样可以保证ByteBuffer类在使用上的安全性,但是同时也现在了用户需求的多样性。

既然JDK的ByteBuffer不能扩展,那么很自然的netty中的ByteBuf跟它就没有任何关系了。

netty中的ByteBuff是参考了JDK的ByteBuffer,并且做了很多有意义的提升,让ByteBuff更加好用。

和JDK的ByteBuffer相比,netty中的ByteBuf并没有扩展的限制,你可以自由的对其进行扩展和修改。

不同的使用方法

JDK中的ByteBuffer和netty中的ByteBuff都提供了对各种类型数据的读写功能。

但是相对于netty中的ByteBuff, JDK中的ByteBuffer使用其来比较复杂,因为它定义了4个值来描述ByteBuffer中的数据和使用情况,这四个值分别是:mark,position,limit和capacity。

  • capacity是它包含的元素数。 capacity永远不会为负且永远不会改变。
  • limit是不应读取或写入的第一个元素的索引。 limit永远不会为负,也永远不会大于其容量。
  • position是要读取或写入的下一个元素的索引。 position永远不会为负,也永远不会大于其限制。
  • mark是调用 reset 方法时其位置将重置到的索引。 mark并不一定有值,但当它有值的时候,它永远不会是负的,也永远不会大于position。

上面4个值的关系是:

0 <= mark <= position <= limit <= capacity

然后JDK还提供了3个处理上面4个标记的方法:

  • clear : 将 limit设置为capacity,并将position设置为0,表示可以写入。
  • flip : 将 limit设置为当前位置,并将position设置为0.表示可以读取。
  • rewind : limit不变,将position设置为0,表示重新读取。

是不是头很大?

太多的变量,太多的方法,虽然现在你可能记得,但是过一段时间就会忘记到底该怎么正确使用JDK的ByteBuffer了。

和JDK不同的是,netty中的ByteBuff,只有两个index,分别是readerIndex 和 writerIndex 。

除了index之外,ByteBuff还提供了更加丰富的读写API,方便我们使用。

性能上的不同

对于JDK的java.nio.ByteBuffer来说,当我们为其分配空间的时候,buffer中会被使用0来填充。虽然这些0可能会马上被真正有意义的值来进行替换。但是不可否认,填充的过程消耗了CPU和内存。

另外JDK的java.nio.ByteBuffer是依赖于垃圾回收器来进行回收的,但是我们之前讲过了,ByteBuffer有两种内型,一种是HeapBuffer,这种类型是由JVM进行管理的,用垃圾回收器来进行回收是没有问题的。

但是问题在于还有一类ByteBuffer是DirectByteBuffer,这种Buffer是直接分配在外部内存上的,并不是由JVM来进行管理.通常来说DirectBuffer可能会存在较长的时间,如果短时间分配大量的短生命周期的DirectBuffer,会导致这些Buffer来不及回收,从而导致OutOfMemoryError.

另外使用API来回收DirectBuffer的速度也不是那么快。

相对而言,netty中的ByteBuf使用的是自己管理的引用计数。当ByteBuf的引用计数归零的时候,底层的内存空间就会被释放,或者返回到内存池中。

我们看一下netty中direct ByteBuff的使用:

ByteBufAllocator alloc = PooledByteBufAllocator.DEFAULT;
ByteBuf buf = alloc.directBuffer(1024);
...
buf.release(); // 回收directBuffer

当然,netty这种自己管理引用计数也有一些缺点,可能会在pooled buffer被垃圾回收之后,pool中的buffer才返回,从而导致内存泄露。

还好,netty提供了4种检测引用计数内存泄露的方法,分别是:

  • DISABLED---禁用泄露检测
  • SIMPLE --默认的检测方式,占用1% 的buff。
  • ADVANCED - 也是1%的buff进行检测,不过这个选项会展示更多的泄露信息。
  • PARANOID - 检测所有的buff。

具体的检测选项如下:

java -Dio.netty.leakDetection.level=advanced ...

总结

以上就是netty中优秀的ByteBuff和JDK中的对比。还不赶紧用起来。

以上是关于netty 系列之:java 中的 base64 编码器的主要内容,如果未能解决你的问题,请参考以下文章

Netty系列之Netty线程模型

netty系列之:不用怀疑,netty中的ByteBuf就是比JAVA中的好用

netty系列之:JVM中的Reference count原来netty中也有

netty系列之:NIO和netty详解

netty系列之:netty对marshalling的支持

netty系列之:我有一个可扩展的Enum你要不要看一下?