文中除了标明出处的内容,均为个人理解后的归纳。水平有限,难免有错误的地方,欢迎指正。


基本概念

字符与字符集

  • 字符 (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
  1. 根据表格的左列决定编码需要多少个 octet, 需要注意的是,每行的编码范围是互斥的。即对于每一个给定的字符,只有一个有效的方法对其进行编码。
  2. 根据与左列同一行的 octet sequence 的模式,决定每个 octet 的高位部分的值。
  3. 将 character number 转为二进制,并在相应的x标记处进行填充。从左往右(从最低位到最高位)依次将 charater number 填入。

使用 UTF-8 进行编码

本节内容引用自 wikipedia: UTF-8 Examples

以欧元符号 € 的编码过程为例子:

  1. Unicode 中 € 的码点为 U+20AC
  2. 由于该码点位于 U+0800 - U+FFFF 之间,参考表格左列的范围,确定该码点使用 3-octet 进行编码。
  3. 十六进制 20AC 整数对应的二进制表示为 0010 0000 1010 1100 (由于 3-octet 的可编码位为 16-bit,因此在最高位补了2个0,使其也为 16-bit).
  4. 该字符使用 3-octet 进行编码,因此最高位 octet 的最高位分别为3个1和1个0,即 1110.
  5. 根据 3-octet 的格式(模板) 1110xxxx 10xxxxxx 10xxxxxx,可编码位分别为 4-6-6 个,因此将第三步得到的码点 0010 0000 1010 1100 也依次分割为 4-6-6 三部分,所以得到 0010 000010 101100,并填入模板。
  6. 最终得到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 被用于中日韩的编码)。

参考资料

  1. RFC-3629
  2. 维基百科: UTF-8
  3. 维基百科: Plane (Unicode)