V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
ChristopherWu
V2EX  ›  程序员

一文详细介绍 base64 及其原理(v 站聚聚请指教)

  •  
  •   ChristopherWu · 2019-07-19 13:19:02 +08:00 · 2725 次点击
    这是一个创建于 1954 天前的主题,其中的信息可能已经有所发展或是发生改变。

    https://mp.weixin.qq.com/s?__biz=MzIzOTIyNTA4MA==&mid=2649795444&idx=1&sn=ac6766f64ca56f7ddc373a89e0b0ad73&chksm=f1295ae3c65ed3f5580a3b9ce771af38632476a850b12588dd25c012509a4fabcb440b0ec42f&token=1606982819&lang=zh_CN#rd

    鉴于 V 站聚聚每次都能支持错漏之处,以及补充我不懂的东西如 https://www.v2ex.com/t/531232#reply11 还有 https://www.v2ex.com/t/543304#reply30 所以这篇文章发上来虚心请教~

    不知道 v 站支持 markdown 如何,先附上公众号地址,排版好看很多。

    Base32, Base64

    Base32 是一个 binary-to-text encoding schemes,顾名思义,就是将二进制数据转换为编码只有基础 32 个字符的数据编码方式,Base64 则是 64 个。注意编码不等同于加密,网上有误解 Base 编码方式为加密方式,实际上标准 Base64 编码解码无需额外信息即完全可逆。

    Base 编码常见用途如下:

    如定义所言,binary to text

    一些协议如HTTPFTP (File Transfer Protocol)[当指定发送文本时], SMTP (Simple Mail Transfer Protocol)text-based protocol,也就是只支持文本传输,不支持二进制传输。是的,http 上传文件,图片时使用的multipart/form-data也是需要转成文本的。

    所以附件如图片,文件等( binary )就可以用Base64编码为 text 再传输。

    将资源编码为字符串

    data URI scheme定义了如下语法来识别网页中的资源:

     data:[<media type>][;base64],<data>
    

    HTML中可以在标签中指定识别 Base64 编码 来展示资源,

    <div>
      <p>Taken from wikpedia</p>
      <img src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUA
        AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO
            9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Red dot" />
    </div>
    

    但因为Base64 是每 3 个原始字符编码成 4 个字符,不够时补=(下文会详述),因此编码后的大小是有可能会比原文件大的,所以htmlBase64 来展示图片而不是用具体的图片好处大概就只有少建立一条 http 连接以及少一个 http 请求(在 HTTP 1.1 以下),这种办法只有大量的小图片才有优越性了。

    统一转成『合法』字符

    为了避免出现不符合规则的字符,方便把含有不可见字符串的信息用可见字符串表示出来。比如 http 协议当中的 headers 头部,必须进行 URLEncode 不然出现的等号可能使解析失败 空格也会使 http 请求解析出现问题,比如请求行也就是 request 就是以空格来划分的POST /hi/you HTTP/1,值得注意的是 Base32的字符列表里有不合法字符/

    还有避免原始信息经过百花齐开的路由,网关多次转发,因有部分系统不支持此不可识别字符或将此作为控制符,将其转义、丢弃等,造成信息丢失,所以如电子邮件里的附件也是用 base64 编码的。

    base64url

    有 base64 编码的变种base64url,将 base64 编码中的+换成-以及将/换成_,甚至不需要往后面补=了。这样子在 url 中传递东西时,不再需要 URL encode,好处就是长度短了,以及好看了一点,毕竟%有点视觉污染(实际上,还可以直接将编码后的东西存数据库了,因为base64URLEncode更通用了 )

    Base64 的由来——参考 RFC

    RFC 向来都不会说明设计的历史由来,自然base64编码也是一样,我参考的 rfc4648也只是说明了因为当时开发者们自己发明使用 base 64 并不规范,没有统一的标准,因此定义了一份通用标准。

    然后呢,Base64就是自己选了ASCII子集( 64 个字符)为标准字符集,当然这也是因为 64 是 2 的 x 次方 (如 64 就是 2 的 6 次方),而 1 个 bit 分别有 0 和 1 两种状态,6 个 bit 也就是 2 的 6 次方=64 个状态,刚好可以表示 64 个字符,因此6 个 bit就可以表达出 64 个字符了。就是下面定义的 64 个:

                          Table 1: The Base 64 Alphabet
    
         Value Encoding  Value Encoding  Value Encoding  Value Encoding
             0 A            17 R            34 i            51 z
             1 B            18 S            35 j            52 0
             2 C            19 T            36 k            53 1
             3 D            20 U            37 l            54 2
             4 E            21 V            38 m            55 3
             5 F            22 W            39 n            56 4
             6 G            23 X            40 o            57 5
             7 H            24 Y            41 p            58 6
             8 I            25 Z            42 q            59 7
             9 J            26 a            43 r            60 8
            10 K            27 b            44 s            61 9
            11 L            28 c            45 t            62 +
            12 M            29 d            46 u            63 /
            13 N            30 e            47 v
            14 O            31 f            48 w         (pad) =
            15 P            32 g            49 x
            16 Q            33 h            50 y
    
    编码定义
    The encoding process represents 24-bit groups of input bits as output
    strings of 4 encoded characters.
    
    • 输入:二进制(图片,文件,字符串本质就是二进制)
    • 输出:编码后的字符串
    • 处理过程:处理输入的二进制时,每 24 个 bit ( 3 个字节)作为一组,编码输出为 base64 处理后的4个标准字符集中的字符。

    值得注意的是,网上的示例或说明中,都或多或少有以下偏颇之处:

    • 输入的例子可以是 16 进制数字、二进制、一串数字等,很多文章举的例子都是字符串;让人忽略 binary to textbinary
    • 是每 24 位(同样需要注意不一定是 3 个 8 位的字符,3 个字节 bytes 才准确)为一组来处理,输出 4 个编码后的字符。强调这点是因为,24 位为一组,不够的都需要补=,如按其他人的文章说的 8 位 8 位的转,根本不清楚要补多少=
    • 24 位转成 4 个编码后的字符(也就是 4*8=32 位),所以编码后的长度肯定会变大
    • 综上所述,RFC 原文才是最对的定义,有时细微的区别意味着理解有问题。下面会一一说明。
    特殊处理
    When fewer than 24 input
    bits are available in an input group, bits with value zero are added
    (on the right) to form an integral number of 6-bit groups.
    Padding at the end of the data is performed using the '=' character.
    
    • 每 24 位为一组来编码输入的 binary 时,如果最后的一组不足 24 位,往后补 0 直到 补足到 24
    • 对于最后对于全为 0 的一组,补充=

    举一些例子来说明一下:

          Input data:  0x14fb9c03d97e
          16 进制:     1   4    f   b    9   c     | 0   3    d   9    7   e
          2 进制:    00010100 11111011 10011100  | 00000011 11011001 01111110
          6 位一组:  000101 001111 101110 011100 | 000000 111101 100101 111110
          Decimal: 5      15     46     28       0      61     37     62
          Output:  F      P      u      c        A      9      l      +
    

    16 进制的0x14fb9c03d97e作为输入,先转成二进制,然后 2 进制的每 24 位 选出来编码,上面例子就是:00010100 11111011 10011100,然后 6 位一组的分开,得到000101 001111 101110 011100

    然后分别转 10 进制,也就是000101变成 5,001111变成 15 等,再去 base64 定义的字符列表中找出此 10 进制对应的字符,以此类推,就是 base64 后的结果了。

    上面例子是输入刚好是有 48 位,2 个 24 位,刚刚够,不需要补=

    下面看看需要补=的例子:

          Input data:  0x14fb9c03
          Hex:     1   4    f   b    9   c     | 0   3
          8-bit:   00010100 11111011 10011100  | 00000011 开始补 0 =》 00000000 00000000
                                                 pad
          6-bit:   000101 001111 101110 011100 | 000000 110000 000000 000000
          Decimal: 5      15     46     28       0      48
                                                      pad with =      =
          Output:  F      P      u      c        A      w      =      =
    

    注意上述输入只有 32 位,第一个 24 位处理完后,还剩下 8 位,因此需要补 16 个 0.

    补完后,就是 48 位的输入了,照样每 24 位输出 4 个编码后的字符。

    观察后半部分, 000000 110000 000000 000000,第一个000000因为后面还有内容,所以 10 进制为 0,因此编码字符为 A,这个很正常;而1100000之后的两个 6 位0,都是纯粹的填充(pading)了,因此并不用 A 而都用 =代替掉**,注意不用A**

    Base64 decode

    说完 encode,decode 就容易啦,无非就是逆过程,RFC 都不屑于讲了。。。

    一串 base64 后的字符串,根据每个字符在 base64 字符表里找到对应的 10 进制,然后转成 2 进制,最后多余补足的 000000 去掉,完了😰

    URL_Encode 以及Unicode相关留待下次说。

    7 条回复    2019-07-19 22:55:35 +08:00
    ChristopherWu
        1
    ChristopherWu  
    OP
       2019-07-19 15:08:02 +08:00
    手动 @msg7086
    lihongjie0209
        2
    lihongjie0209  
       2019-07-19 15:34:10 +08:00
    base64 的实现可以作为位运算的练习题, 挺简单的
    msg7086
        3
    msg7086  
       2019-07-19 15:37:22 +08:00
    multipart/form-data 不是一定要用 base64。
    具体根据选择的编码方式来,base64 只是其中一种。
    base64 好像是主要用在邮件附件里的,HTTP 还是 binary 比较普遍?
    ChristopherWu
        4
    ChristopherWu  
    OP
       2019-07-19 15:49:44 +08:00
    @lihongjie0209 是,转换规则是挺简单的,撸起来很快。
    ChristopherWu
        5
    ChristopherWu  
    OP
       2019-07-19 15:51:19 +08:00
    @msg7086 >multipart/form-data 不是一定要用 base64
    对的,大多数情况用 binary 吧,用 boundary 作分界线。

    >base64 好像是主要用在邮件附件里的
    是的,附件有用的比较多~
    Ultraman
        6
    Ultraman  
       2019-07-19 18:43:51 +08:00 via Android
    聚聚这个词我好像只在草榴见过🙄
    ChristopherWu
        7
    ChristopherWu  
    OP
       2019-07-19 22:55:35 +08:00
    @vcinex - = -
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1035 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 23:16 · PVG 07:16 · LAX 15:16 · JFK 18:16
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.