文中除了标明出处的内容,均为个人理解后的归纳。水平有限,难免有错误的地方,欢迎指正。
基本概念
字符与字符集
- 字符 (Character):包括类字形的符号,例如汉字、中英标点符号、英文字母、拉丁字母等,以及各种控制字符,如换行、响铃等。
- 字符集 (Character Set):由有限个字符所组成的集合。
编码与码点
- 字符编码 (Character Encoding):将字符集中的元素(单个字符)逐一映射为不同的整数。这些整数实际上是字节流,用于计算机内部处理或网络传输。编码的逆向操作为解码,即将二进制流或整数还原成字符元素。
- 码点 (code point):码点(也可称为码位),是由编码规范定义的、一个字符在字符集中的索引值。
- 编码空间 (code space):由所有码点组成的一个范围。
关于码点的进一步说明
码点是由编码规范所定义,而在具体的编码实现方式中可以自行定义。
为了演示这个情况,我自定义一个字符规范:LCODE (L for Lawrence)。该规范可编码 abcd
四个字符,且码位依次为 00, 01, 10, 11
。
假设有如下几种实现方案:
- LTP-1: 使用 3-bit 宽度来保存单个字符,此时将有 1-bit 处于闲置。我将第二位 (bit) 定义为保留位用于将来的扩展,该 bit 固定为0。 因此
abcd
的编码值依次为000, 001, 100, 101
. - LTP-2: 使用 4-bit 宽度来保存单个字符,此时有 2-bit 处于闲置。我将最后两位保留,即用前两位进行编码,因此
abcd
的编码值依次为0000, 0100, 1000, 1100
.
由此可见,码点的定义是由编码规范来负责,而编码值则是由编码实现来处理。一般来说,码点在逻辑上是连续的,而相邻码点的编码值却不一定相邻。所有码点共同组成编码空间。码点的值是一个整数,也可以视作该码点在码点空间中的索引值。
我们通常说的 ASCII 编码,实际上指的是 US-ASCII, 由于只有单字节、没有其他字符的扩展,且没有一些莫名其妙的设定 (如 LCODE 的定义),所以码点与编码值的关系不明显。但是在多字节编码的情况下,则需要注意这个问题。
UTF-8 的定义
本节内容引用自 RFC-3629: UTF-8 definition
(注:octet 表示一组八位的二进制数,即 1-octet 等于 1-Byte. 之所以使用 octet 而不是 Byte, 是因为后者主要用于表示存储空间的大小,而前者主要用于电子通讯领域,使用不同的称呼以避免混淆。)
- 一个 UTF-8 字符由长度可变的 1-4 octet 序列组成。
- 对于 1-octet 的字符(即长度为1的序列),最高位为0,剩下的 7-bit 用于编码;(由于 UTF-8 兼容单字节编码的 ASCII,所以这个 octet 肯定与 ASCII 的内容一致,包括最高位也必然为0)
- 对于 n-octet (n>1) 的序列
- 首个 octet: 最高 n-bit 皆为1,然后紧接着的 bit 为0, 余下的 bits 用于字符编码。
- 余下的 octet: 最高两位 bit 均为 10, 余下 6-bit 用于字符编码。
以下表格展示了单个 UTF-8 字符在不同 octet 下的规范:
Char. number range (hexadecimal) |
UTF-8 octet sequence (binary) |
---|---|
0000 0000 - 0000 007F | 0xxxxxxx |
0000 0080 - 0000 07FF | 110xxxxx 10xxxxxx |
0000 0800 - 0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000 - 0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
- 根据表格的左列决定编码需要多少个 octet, 需要注意的是,每行的编码范围是互斥的。即对于每一个给定的字符,只有一个有效的方法对其进行编码。
- 根据与左列同一行的 octet sequence 的模式,决定每个 octet 的高位部分的值。
- 将 character number 转为二进制,并在相应的x标记处进行填充。从左往右(从最低位到最高位)依次将 charater number 填入。
使用 UTF-8 进行编码
本节内容引用自 wikipedia: UTF-8 Examples
以欧元符号 € 的编码过程为例子:
- Unicode 中 € 的码点为 U+20AC
- 由于该码点位于 U+0800 - U+FFFF 之间,参考表格左列的范围,确定该码点使用 3-octet 进行编码。
- 十六进制 20AC 整数对应的二进制表示为
0010 0000 1010 1100
(由于 3-octet 的可编码位为 16-bit,因此在最高位补了2个0,使其也为 16-bit). - 该字符使用 3-octet 进行编码,因此最高位 octet 的最高位分别为3个1和1个0,即
1110
. - 根据 3-octet 的格式(模板)
1110xxxx 10xxxxxx 10xxxxxx
,可编码位分别为 4-6-6 个,因此将第三步得到的码点0010 0000 1010 1100
也依次分割为 4-6-6 三部分,所以得到0010 000010 101100
,并填入模板。 - 最终得到3字节的编码结果:
11100010 10000010 10101100
,写成十六进制为E2 82 AC
UTF-8 的安全问题
不难发现,Character number range 表格所表示的范围都只占该模式下的一部分。
1-octet 模式,实际上就是 ASCII 编码,这没什么好说的。
2-octet 的最大值为 0x07FF,这是因为出去模板所占的 5-bit 外,还有 11-bit 可用作编码,而 11-bit 的最大值即为 0x7FF。问题在于最小值,不是从 0x00 开始,而是从 0x80 开始。也就是说,2-octet 中有 2^7 个空间是无效的,因此只有 2^11 - 2^7 = 1920 得到利用。
同样的情况也出现在 3-octet, 4-octet 中。所以为什么要忽略掉开头的部分、以及 4-octet 最高只到 0x10FFFF (17 planes)?
答案是出于安全考虑,例如错误的解码 (导致不被允许的作为输入的字符以其他形式绕过了限制) 或存在 buffer overflow 的风险。
RFC-3629: Security Considerations 一节对这个问题进行了描述。
编码平面
Unicode 将 0xFFFF 作为单位,称为一个平面 (plane), 在 UTF-8 最新规范中定义了 17-planes, 即可以表示 17 * 65536 = 1114,112 个字符。
首个(编号为0) plane 被称为 Basic Multilingual Plane (BMP, 基础多语言平面), 余下的16个 plane 则被称为 Supplementary Planes (次级平面).
从编码平面的角度来看,1-3 octets 字符的编码空间正好全部处于 0xFFFF (BMP) 之内,且没有重叠的部分。该平面包含了最常用的字符。
BMP 包含了大量符号,以及几乎所有现代语言的字符(大量 code point 被用于中日韩的编码)。