Base64简介

前言

这是我第一次写博客,想理理自己这些年来积累的代码,了解的一些规范,希望也能帮到一些人更好的理解。本博客不会以太专业的角度来写,主要是以我自己的理解为主。如果写的不好,或有什么错误的地方欢迎指正;如果觉得对你有所帮助,请留言鼓励一下;如果有什么更好的想法,欢迎留言交流。文中未尽之处,欢迎留言询问,我会尽快回复,谢谢!~

我目前主要做Android开发,另外也熟悉其他多种语言,今后会在博客中慢慢涉及。这次主要讲讲Base64。

背景

Base64是网络上最常见的用于传输8Bit字节代码的编码方式之一。最大的好处就是能将字节数组转换成一个全部由可见字符组成的字符串。现在很多api接口都是REST风格,其实REST风格的接口并不规定一定要以对象请求,以对象返回。关于REST的介绍请见下一篇博客,这里稍稍带过。但大多数都以json或xml来传输数据。

众所周知,json和xml都是文本标记语言,本身不能保存字节数组,因此将字节数组Base64后放入json或xml是一种不错的选择。但是值得注意的是,使用Base64传输字节数组,会使流量增加约三分之一。但也也比使用HEX方式传输增加二分之一要好得多。

原理

Base64的原理非常简单,就是将3个字节变成4个字节,然后根据字母表(alphabet)转换成相应的字符串。举例说明如下:
如有3个字节(0xA576D9)的二进制表示为:
10100101 01110110 11011001
一个字节是8位,3个字节共识24位,现在重新划分,以6位为一组,可以分成4组:
101001 010111 011011 011001
再给每一组补全成8位,前面补0的方式:
00101001 00010111 00011011 00011001
这样,我们就从3个字节变成了4个字节。这样的好处是我们能保证4个字节中的每个字节最大不会超过2的6次方(严格说来还要减1),因为只有最低的6位可能会出现1。而2的6次方就是64,Base64的名字就是因此而来。
现在我们只需要再定义一个标准,将4个字节重新表示成字符。而这个标准就是Base64的字母表,字母表常用的用两个,默认的就是如下这种
默认Base64字母表

另一种是为了能在URL上传递的变形,其实只是将最后两个从“+/”换成了“-_”。
上面提到的四个字节用十进制表示分别是41,23,27,25。查上表,分别对应pXbZ。因此0xA576D9经过默认Base64转换后是pXbZ。
有人就在想了,那要是字节数不是3的倍数怎么办,很简单,分两步:
一、如果剩余的字节数是1,就在末尾补4个0(8位+4位组成的12位能转换成2个Base64字符);如果剩余的字节数是2,就在末尾补2个0(共组成18位,变换成3个Base64字符);如果剩余的字节数是3,你猜怎么办。。。
二、Base64有一个准则就是转换后的字符串长度必定是4的倍数,因此,需要将最后转出来的用“=”补足。“=”是规定好的补位数用的,并不在字母表里。因此,最多需要补2个“=”。

Base64的java实现

Android端有google实现好的Base64类,直接使用即可,但是它的源码稍嫌复杂,这里用最简单的办法来实现,同时又兼顾算法效率。最关键的是,我的实现还能自己定义字母表。这样除非别人知道你的字母表是什么,否则没法获取你的内容究竟是什么了,也算是一种加密吧。
首先定义三个常量:

public static final Charset UTF8_CHARSET = Charset.forName("utf-8");
private static final byte[] DEFAULT_CODEC_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".getBytes(UTF8_CHARSET);
private static final byte[] URL_CODEC_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=".getBytes(UTF8_CHARSET);

这里将两个最常用的字母表都以字节数组的方式存放起来了,后面使用将非常方便。值得一提的是,最后将补位用的等号也放上了。因此这个字母表的长度是65。以后定义自己的字母表时,长度也要是65。
接下来我们使用3个静态方法来获取实例。用静态方法获取实例的其中一个好处就是相当于可以“自定义构造函数的名字”。

    public static Base64 getDefaultCodec() {

        return new Base64(DEFAULT_CODEC_ALPHABET);
    }

    public static Base64 getUrlCodec() {

        return new Base64(URL_CODEC_ALPHABET);
    }

    public static Base64 getCustomCodec(byte[] alphabet) {

        if (alphabet.length != 65)
            return null;
        return new Base64(alphabet);
    }

于是,我们的构造函数就是

    private final byte[] alphabet;
    private final byte[] codes;

    private Base64(byte[] alphabet) {

        this.alphabet = alphabet;
        this.codes = new byte[256];

        for (int i = 0; i < 256; ++i)
            this.codes[i] = -1;
        for (int i = 0; i < 64; ++i)
            this.codes[this.alphabet[i]] = (byte) i;
    }

这段代码很重要,第一个for循环其实没什么用,标记成-1只是为了我们观察数组方便,可以去掉。第二个for循环是关键,很多实现都没有这段代码,而是直接将结果数组给出,导致很多人都不了解为什么这样的一个“魔鬼数组”就能将Base64字符串还原成字节数组。包括google的实现亦是如此:

private static final int DECODE[] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, };

这是google的Base64的Decoder实现时所用的数组,光看这个实在是有点摸不着头脑。我们的codes数组最终就是上面这个值。
其实说起来也很简单,进行Base64编码的时候,我们得到的每一个转换后的字节都是0~63的数字,由于前面有定义byte[]类型的字母表,因此能直接通过索引获取到对应的字符。但是解码的时候,我们如何获取这个字母对应的索引是几呢?最傻的办法,从第一个开始遍历字母表,数到几,匹配了就是几,但是这样的效率显然不高。因此这里我们牺牲了一点空间,换取了大量的时间。上面数组中-1,就是我们牺牲的空间。牺牲的不多,我们完全可以接受。那这个数组究竟是怎么生成的呢?我们以字母表的字母的ascii码作为索引,它在字母表中的索引为值,生成codes数组。这样我们想要获取字母a在字母表中的索引是几,只需要codes[‘a’]即可。同样是用查表的方式,只牺牲了不到200字节的代价,就换取了很高的效率。由于codecs是构建实例的时候算出来的,不是写死的,因此,用户传入任何字母表都能被成功求出对应的codecs,从而实现自定义字母表的Base64编码和解码。
下面看看encode方法是怎么写的

    /**
     * Encodes all bytes from the specified byte array into a newly-allocated byte array using the Base64 encoding scheme.
     * 
     * @param src
     * @return
     */
    public byte[] encode(byte[] src) {

        byte[] out = new byte[((src.length + 2) / 3) * 4];
        for (int i = 0, index = 0; i < src.length; i += 3, index += 4) {
            boolean quad = false;
            boolean trip = false;
            int val = (0xFF & src[i]);
            val <<= 8;
            if ((i + 1) < src.length) {
                val |= (0xFF & src[i + 1]);
                trip = true;
            }
            val <<= 8;
            if ((i + 2) < src.length) {
                val |= (0xFF & src[i + 2]);
                quad = true;
            }
            out[index + 3] = alphabet[(quad ? (val & 0x3F) : 64)];
            val >>= 6;
            out[index + 2] = alphabet[(trip ? (val & 0x3F) : 64)];
            val >>= 6;
            out[index + 1] = alphabet[val & 0x3F];
            val >>= 6;
            out[index + 0] = alphabet[val & 0x3F];
        }
        return out;
    }

这个函数在需要将编码结果写入OutputStream(比如网络或文件)时很好用,但是有时我们希望获取到的就是一个字符串,很简单:

    /**
     * Encodes the specified byte array into a String using the Base64 encoding scheme.
     * 
     * @param src
     * @return
     */
    public String encodeToString(byte[] src) {

        return new String(encode(src), UTF8_CHARSET);
    }

由于篇幅所限,解码的函数以及整个类,请移步到Base64编码和解码java实现下载。Base64实现中有位运算,可能也会在以后的博客中介绍。由于现在众多框架以及各种工具类的实现,很少有人自己接触位运算了,其实位运算也是很有学问的,它的运算速度非常快。可以用来解决八皇后问题,十六皇后问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值