Unicode,UCS,UTF渊源太深,一篇文章恐怕讲不完,这里只说UTF-8.
0. Character Set & Encoding Form
先做一个小说明,通常我们说到Unicode, UCS, UCS-2, UCS-4, UTF-8, UTF-16, UTF-32时,很容易混淆其概念,有必要做一个区分.
Unicode和UCS是一种字符集,它定义了每一个抽象字符与数字的对应关系.它就像一张表格,上面数字与字符一一对应,告诉你1代表什么,2代表什么,3代表什么,如此...这是一种映射关系,无关乎具体字符是怎么样存储在电脑中的; 而关于如何存储,这就是UCS-2,UCS-4,UTF-8,UTF-16,UTF-32的事儿了,它告诉你每一个字符以多少个字节代表,字符所对应的数字是直接转换为其对应的2进制还是经过一定的编码转换存储起来. 用官方的术语来说Unicode,UCS是Character Set; UCS-2, UCS-4, UTF-8, UTF-16, UTF-32是Encoding Form.
有关讲述字符集及其编码形式的文章,网上一搜一大堆,这里就不再赘述,本文只把重点放在UTF-8的由来.
1. 由来
1.1 UTF-8之前
UTF-8出现之前,处理多语言字符集时多是用的双字节序来编码存储字符.什么是双字节序?UCS-2,UTF-16就是双字节序,它们使用两个字节表示一个字符(UTF-16会使用到4个字节编码一个字符,即两个双字节为一个编码),那个时候UCS与Unicode还是各自独立的字符集标准,双方委员会所制定标准时还有不少差异,不过两个标准所定义的字符范围都未超过[0x0000, 0xFFFF]这个区间,于是16位(双字节)足以表示所有字符.
UTF-8的创造者Rob Pike与Ken Thompson大犇那个时候也在用ISO/IEC 10646(UCS)所定义的双字节编码来处理文本.按照他们的话来说
We hate it!
这种编码有很多问题,其中最要紧的两个问题就是,在文本中双字节会产生错误的NULL(\0)边界,以及在文件系统中,一个使用双字节表示的斜线(/)很难判断其路径是否结束,它很难兼容单字节编码系统.
1.2 The Phone Call
直到1992年9月的某一天晚上,Rob Pike接到一个电话,是18M的一个工作小组打来的,他们想让Rob Pike和Ken Thompson审阅一份由他们起草的变长字符编码提案,那会Ken Thompson就在Rob旁边,收到提案后他们立马研究起来,他们发现这份提案中解决了许多(包括上面提到的两个)双字节编码面临的问题,还能够完美的兼容US-ASCII,也就是说它能够兼容许多单字节编码系统,提案将这种编码命名为FSS-UTF.这看上去这份提案十分令人满意,可是Rob和Ken并没有满足,因为在他们心中,这个提案至少还有一个问题没有解决.
这种编码在存储,处理和转换上表现得都很不错,可是在传输过程中,没法做到自描述边界.换句话说,计算机在接收来自网络的字符数据时,不通过上下文就无法知道当前接收的这个字节是上一个字符的一部分还是新的字符的开始,这会给字符拼接带来难度和效率上的开销,要做到这一点,就必须进一步改进.
Rob和Ken立刻意识到他们有机会在此基础上设计一种更完美的字符编码系统.那天晚餐过后,他们开始着手修改这个设计中缺陷的部分.很快,他们就完成了设计,并将这份修改提案回发给了18M小组,收到提案的小组很快作出了回复,他们一致认为这份修改比原始的那一份更优秀.经过确认后,Rob和Ken开始着手编写编码装换程序与C语言库,到了第二天,代码已经能在机器上跑起来了.这个修改提案便是UTF-8的由来.后来18M小组把这份修改后的FSS-UTF提案提交了,这就有了后来被误解为18M设计了UTF-8,当然再后来Rob和Ken发表了一篇paper正式介绍了UTF-8(见文章结尾参考),paper里将其称作UTF-2,然后经过漫长的标准化,最后UTF-8的名字得意确定,当然那是后来的事了.
要理解Rob和Ken在这份提案中做了什么样的修改使得这个编码系统设计的更加完美,我们先来看看原版FSS-UTF的设计,在引入变长编码方案时,FSS-UTF与UTF-8在处理首字节的做法很相近,都是在高位1的数目来描述一个字符编码所占字节长度(需要对UTF-8编码方式有了解):
字节长度 | FSS-UTF | UTF-8 |
---|---|---|
1 | 0xxxxxxx | 0xxxxxxx |
2 | 10xxxxxx 1xxxxxxx | 110xxxxx 10xxxxxx |
3 | 110xxxxx 1xxxxxxx 1xxxxxxx | 1110xxxx 10xxxxxx 10xxxxxx |
4 | 1110xxxx 1xxxxxxx 1xxxxxxx 1xxxxxxx | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
5 | 11110xxx 1xxxxxxx 1xxxxxxx 1xxxxxxx 1xxxxxxx | 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
6 | 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
以上表格中,x为任意二级制值,我们可以看出,FSS-UTF设计问题就在于当字符是多字节时,非首字节部分无法表达自身是非首字节.例如,在传输过程中,传递一个双字节的FSS-UTF编码: 10111111 10111111,这显然是一个有效FSS-UTF编码,但它的第二个字节本身是无法自描述,这就造成了字符在网络传输过程中很难容错.
而UTF-8就完美的解决了这个问题,因为对于任何长度的编码,首字节高2位是绝不可能出现10这个特征值的,所以在传输过程中,接收方只要发现当前接收字节为10xxxxxx,便可知道它是上一个未传完字符的一部分,如果以其他非10xxxxxx形式,便可知道它是新字符的首字节.这样就完美的解决了问题.
2. 有关UTF-8的趣事
关于UTF,其实有两个解释,一个是UCS Transformation Format,另一个是Unicode Transformation Format.为什么会有两个?
这是因为当初UTF-8有两种实现,一个是作为UCS的实现,当时UCS的字符编码范围是[0x00000000, 0x7FFFFFFF]整整31位,这就是上面表格所看到的UTF-8的编码规则,如果你仔细数一下x的个数,你就会发现正好是31个,这种最高6个字节的UTF-8编码就是当时针对UCS字符集的实现,熟悉UTF-8的人刚看到上面表格中的定义可能会诧异甚至反对,因为现在的UTF-8编码规定了只占4个字节,这就跟第二种实现有关了.
这第二种实现是针对Unicode的,自从Unicode 3.1开始将其编码范围调整到[0x000000, 0x10FFFF]整整21位,因此依照上面的表格,数一数4个字节的UTF-8编码x的个数,也是21个.
后来ISO/IEC 10646与Unicode的标准委员会决定统一并同步两种字符集的更新,ISO/IEC 10646承诺不再分配21位以上的字符空间,于是两种UTF-8变成了一回事.因此有了这么一个有趣的事实,标准中仍然将UTF采用原先的命名即UCS Transformation Format,但在这份标准文件中又将Unicode的UTF-8实现做为权威参考,所以今天我们可以说的UTF-8指的就是Unicode Transformation Format-8.另外两种规范的统一又使得UCS-4与UTF-32变成了一回事.
3. 参考
UTF-8, a transformation format of ISO 10646
UTF-8 history
UTF-8 and Unicode FAQ for Unix/Linux
UTF-8 Wikipedia