混淆可能源于这样一个事实,即当您说“base 32”时,您可能指的是两种不同的事物(尽管密切相关)。
事情1:数字基数
数字表示的结构方式,定义单个“数字”有多少个不同的符号选项。我们人类通常使用以 10 为底的数字用 10 个不同的符号 (0-9) 来表示我们的数字,但您也可以使用以 2 为底的二进制(仅使用符号 0 和 1),以 8 为底的八进制(使用符号 0-7 ) 等等。请参阅Wikipedia 上的 base/radix。对于高于 10 的基数,您通常使用字母,从 A 开始表示第 11 个符号。例如,十六进制(以 16 为底)使用符号 0-9 和 AF。
由于除了 0-9 的 10 个符号之外,我们只有 26 个不同的字母可以使用,因此在大多数情况下,只定义了以 36 为底的表示。如果你尝试运行12345..toString(40)
,你会RangeError: toString() radix argument must be between 2 and 36
因为这个原因得到一个。
现在,以这种方式表示以 32 为底的数字 12345(使用符号 0-9 和 AV)会给你,C1P
因为C
它的值为 13,1
值为 1,P
值为 25,以及 13 * 32^2 + 1 * 32^ 1 + 25 * 32^0 等于 12345。这只是一种使用 32 个不同符号而不是 10 个来编写数字的方法,我不会将其称为“编码”。
如果基数大于 10,这将导致比常规基数 10更短(或等长)的表示。
事情 2:base N编码
术语“ base64 ”(最著名的这种编码)中的“base N ”编码是指使用“字母表”(一组允许的符号),其中N指定字母表有多少个符号3。这用于在不允许使用字节中所有可能值的介质上存储或传输任何八位字节流(无论其内容如何)(例如,诸如电子邮件之类的介质,它只能包含文本或不允许某些特殊字符的 URL,例如或因为它们具有语义等。/
?
0
O
以及I
and 和l
and1
不能可靠地使用而不会在被人类读回时造成混淆)。
现在是标记与第一个“事物”关系的部分:可以通过将输入字节转换为一个巨大的数字并更改其基数来想象转换的工作方式,但使用编码定义的字母表,不一定是数字后跟字母。可以在这里找到一个很好的视觉解释。
“将输入字节转换为一个巨大的数字”部分是charCodeAt
您提到的发挥作用的地方:例如,我可以将字符串ABC
转换为数字 4276803,这在以十六进制表示形式查看字节时变得更加明显,因为一个字节可以有 256 个值,这个范围正好适合两个十六进制“数字”(0x00-0xFF 4)的确切范围。三个字节5 inABC
的十六进制值分别为 0x65、0x66 和 0x67,如果我将它们放在一起,我可以将它们视为一个大数字 0x656667 = 4276803。
与第一个“事情”的另一个重叠是,在密码学中,非常大的数字开始发挥作用,而且通常这些数字也使用 base32 或 base58 或 base64 等机制进行编码,但除非编程语言和/或处理器具有数据类型/register 适合大数字,此时该数字已经再次表示为某种八位字节流(与我刚刚描述的相反,但有时具有相反的字节顺序)。
当然,这只是概念上的实现方式,否则一旦我们谈论的不是 3 个字节而是 3,000,000 个字节的编码,算法将不得不处理巨大的数字。实际上,涉及位移等的巧妙方法用于顺序地对任何长度的数据实现相同的结果。
由于您习惯于看到的“字符串”(忽略 Unicode 一秒钟)可以与以基数 256 表示的字节数字数(一个字节中可能的 256 个值中的每一个值的一个符号)进行粗略的比较,所以这个意味着任何这样的 base N编码都会使输出更长,因为新的“基数”低于 256。请注意,放入12345
base32 算法将意味着字符串 12345
在我上面的解释中可以被视为数字 211295614005(或 0x3132333435) . 这样看,64t36d1n
(你从base32得到的)肯定比base 10中的211295614005短,所以这一切都说得通了。
重要说明:如果您的输入数据由于其长度而无法完全映射到其新表示形式而无需填充,则此解释并不完全正确。例如,一个 3 字节长的数据块占用 3*8=24 位,并且每个符号使用 6 位的 base64 表示很容易实现,因为这些符号中恰好有四个也将占用 4*6=24 位。但是一个 4 字节长的数据块占用 4*8=32 位,因此需要 5.333...base64 中的符号(5.333...*6=32)。为了“填充”剩余的数据空间,使用了某种填充6,因此我们可以将其四舍五入为 6 个符号。通常,填充添加到末尾输入数据,这就是现实与我上面的“改变大数的基数”概念不同的地方,因为在数学中你会期望前导零作为填充。
道格拉斯·克罗克福德的 base32
要解决您最初的问题:
Douglas Crockford 的 base32 算法实际上是为数字设计的,但使用经过修改的字母表,它不像程序员习惯的那样将八位字节流作为输入。所以它更像是上述两件事的中间立场。你是对的,toString(32)
这只是你需要的一半,但你必须在基数 32 的常规“字母”(0-9,AV,不区分大小写)和 Crockford 的(有 0-9 和 AZ 但没有I、O 和 U,不区分大小写,解码时将 I 映射为 1,将 O 映射为 0)。
来回替换这些东西已经足够复杂了,我想从头开始自己编写算法而不是依赖toString
.
(此外,Crockford 在最后提出了一个额外的“检查符号”,无论如何都超出了这里的解释。)
脚注:
1:这是假设整数。如果你有分数,那么情况就大不相同了,因为对于旧基数中没有循环小数的数字,你可以在新的基数中得到循环小数,或者反过来。例如,基数 32 中的 0.1 是 0.36CPJ6CPJ6CPJ6CPJ... 这是一个无限长的数字(在那个特定的表示中)。
2:这里的术语“二进制”不是指基数 2 中的表示,而是指“可以使用每个字节 0-255 的全范围值的任何类型的数据,不限于以 ASCII 表示人类可读文本的值范围 32-126"。
3:请注意,仅从N无法推断出字母表到底是什么,只能推断出它有多长。众所周知的编码具有普遍接受的关于使用哪种字母的约定,例如 base64 和 base58(后者通常用于加密货币地址,顺便说一下,它的字母甚至不是按字母顺序排列的)。请注意,即使对于 base64,也有一些变体,例如 base64url,它们会稍微改变字母表。其他诸如 base32 还没有普遍接受的字母表,这就是为什么您链接的网站提到“这是base32编码而不是base32编码” - 特别是它与Crockford 的字母表不同。
4:前缀0x
通常用于表示以下符号将被解释为以 16 为基数(十六进制)而不是以 10 为基数的数字。
5:我在这里说的是字节,因为这是base N算法使用的,但实际上字符串是基于字符而不是字节的,它们也可能包含数值超过255的Unicode字符,因此不适合一个字节了。通常,首先使用UTF-8等字符编码将字符串编码为字节,然后对这些字节执行基本N编码。
6: base64=
用作填充,为了保留使用了多少填充字符的信息,=
输出也附加了相同数量的字符(=
不在 base64 的字母表中)。