这标题有点又把ANSI当成一种编码的感觉了。应该这样说,从ANSI的不同国家和地区的不同编码标准,到Unicode的统一所有编码。
产生背景:
在ANSI的标准里,英文操作系统ASCII,简体中文GBK,日文JIS等等,感觉有点像哪里出事就搞哪里,这其实也是软件开发的低等阶段--只增加不重构,复制粘贴改名字。由于ANSI的实现太多,不同 ANSI 编码之间互不兼容,当信息在国际间交流时,需要不停的进行转换。也无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。
随着国际化进程的加快,作为世界中心的老美,由于科技产品输出最多,发现自己是这种局面最大的受害者,于是开始来填自己当初挖的坑--重构。将所有地球上能出现的字符、符号,放到一个大大的码表中,起了个吊炸天的名字,Unicode--统一码。
Unicode范畴内的概念很多,各种概念的定义、之间的关系等等很乱,什么UCS-2、UCS-4、UTF-8、UTF-16、UTF-32。
其实只要了解了Unicode的发展史,这些就很清晰了。我们细说从头。
Unicode1.0
狭义的Unicode只是一个码表,更完整的Unicode应该是一个码表加上它的实现方式。
产生物:
Unicode1.0,UCS-2,定长2字节的UTF-16
产生年份:1991年
老美在开始制定Unicode的时候,是因为发现ASCII、或者ISO-8859-1的2^8是不够用的。那么多少够用呢,因为要整字节增加,第一个想到的是2^16。2^16够不够,老外当时算了一下,2^16=65 536个字符,我靠好多。我们姑且称为时代局限性吧。。认为这么多字符就可以囊括地球上所有的语言的文字、符号了。可能也是做了一番统计,把所有ANSI编码拿来合计一把,但那时候的比如GB2313大家懂的,才6000多个字符,6000个?我泱泱大国,文化博大精深,小学生都会3000个汉字的好吗?还有55个少数民族兄弟呢。鬼知道当时那帮人怎么数出来的6000字。总之,最后决定就用2^16个值来表示,16位2进制写起来很长,大家看过Java原码里面Integer.MACX_VALUE没,32位2进制是怎么写的,,用16进制来表示长的2进制,可以看起来短一些,,,于是搞了两套东西出来:
① 本体:Unicode字符集(Unicode Character Set 简称UCS):4位16进制来定义Unicode的值-码对应关系。这个版本就是UCS-2,在有了UCS-4之后,为了区分,把前一个版本成为UCS-2。
② 传输体:用2个字节的2进制在计算机中表示。这个传输体学名叫做(Unitcode Transformation Format)UTF,这个时候其实不叫UTF-16,也是在UTF-8有了之后为了区分才叫做UTF-16。
因为Unicode太长,采用了16进制来表示,所以才会有字符集和传输形式两种分类,其它编码都是直接用二进制表示的,码值就是传输形式,所以其它编码不分字符集和传输形式。
初代版本就这么出来了,包括4位16进制表示的Unicode/UCS(UCS-2)和2个字节的UTF-(UTF-16)。
包括了能满足所有欧美国家,和汉字常用字的编码,在当时的科技水平下,已经能够满足绝大部分编码需求。
大端序、小端序
这个时候是定长的2字节表示一个字符,这样带来了两个问题,1个问题就是,大端序(Big-Endian,简写为UTF-16 BE)、小端序(Little-Endian)的问题。
2个字节的字节流在传输的过程中,只能被机器一个一个字节接收,然后重新安放成两两的组合。由于不同的CPU平台,对字节顺序的存储不同,Mac将高位字节存储在内存的低地址端,Windows正好相反。假设字符为十六进制编码4E59,按两个字节拆分为4E和59,在Windows上存储会变成4E59,在Mac上会存储成594E,假设Mac从网上获取了一串字节流4E59,这个到底是Windows的4E59(对应汉字“奎”),还是另外一台Mac机器的4E59呢(实际是594E,对应“乙”),这是完全不同的两个字符。为了解决这个问题,我们传输的时候标明,原来是4E59的,叫做LittleEndian,也就是正常的思路。原来是594E的,叫做BigEndian
1.小端序(Little-Endian)就是低位字节排放在内存的低地址端即该值的起始地址,高位字节排放在内存的高地址端。
2.大端序(Big-Endian)就是高位字节排放在内存的低地址端即该值的起始地址,低位字节排放在内存的高地址端。
对应的UTF-16LE和UTF-16BE,由于小端序是人类正向的理解逻辑,所以如果不标明,默认就是LE。所以到此UTF-16有三种表述方式,UTF-16等于UTF-16LE,和UTF-16BE。
UTF-8--一种兼容ASCII的Unicode的实现方式
初代Unicode在推广的过程中受到ASCII阵营的强烈抵触,因为ASCII是一个字节的,UTF-16是两个字节的,等于说是不兼容的,需要转码。这时候就有人想出一种兼容ASCII的Unicode实现方式--UTF-8 Unicode Tranform Format-8-bit,正因为有了这个名字,之前的2字节编码顺利成章的称为UTF-16。
UTF-8的定义规则:
字节数 | Unicode | UTF-8编码 |
1 | 000000-00007F | 0xxxxxxx |
2 | 000080-0007FF | 110xxxxx 10xxxxxx |
3 | 000800-00FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
4 | 010000-10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
看完这个规则,会发现设计的出发点完全就是为了兼容ASCII!
UTF-8是一种可变长度的编码。大于ASCII的字符,用多位表示。其中,第一位用连续的1表示一共有多少位,表示完位数后,用1个0来分隔计数位和真正的有效码值,然后剩下的位数是有效数字。后面每个字节起始位置为10,后面6bit是有效数字。
为什么这么设计,仔细推导其实是必然:
1、为了保证和ASCII兼容,如果是ASCII范围内的数据,一定要和ASCII一样,也就是如果是ASCII范围的字符,会是0xxxxxxx的形式,这是个大前提。
2、为什么后面位置要以10开头。
先讨论为什么要以1开头。
假设我们一直从头到尾来顺序读取,由第一个字节来决定长度是多少,其实后面字节完全可以把8位都用起来。但是很多时候UTF-8是用来做网络传输的,会丢包,会断点续传。如果丢包,假设丢的是第一个字节,那就没办法确定后面的到底是多字节字符的一部分,还是单字节的ASCII字符。假设丢的是后面的字符,可能会把本来的单字节ASCII字符,当成其它字符的一部分解析。断点续传同样会导致无法识别一个字节是ASCII字符还是多字节字符的组成部分。这样就要求必须明确区分ASCII字符和其它字符。既然ASCII是以0开头,那么我就以1开头,后面7为用来表示真正的有效数据。
为什么10
本来以1开头就可以区分是否ASCII字符,但是会和位数字节混淆,假设恰好第二位是1,到底是真正的位数字节,还是前面一个字符出现了丢包呢。为了区分位数字节和非位数字节,使用10开头。
3、为什么一定要位数字节。
我们完全可以只用通用的110来表示一个字符的起始字节,然后找到下一个为110的字节为止(下一个不是ASCII字符),算一个字符。或者0为止(下一个是ASCII字符),算一个字符。然后来解析其中的数据。但是同样,在丢包的时候,会混淆。只有显式声明某个字符到底有多少个字节,才能保证每个字符是独立的,不会因为丢包而造成混淆,不同字符不会相互干扰,即使某些字节丢失了,仍然能转码出基本的意思。
UTF-8的格式虽然有10,但是这些只是标识和分隔符,真正的计算值的时候,会把这些给去掉,所以UTF-8的码值本身是连续的。
第一位最多1111110x,也就限定了UTF-8最多用6位来表示。
不太明白为什么不能使8个1,可能规定就是这样。那么最大值就是
1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx,所有的x都为1。换算起来一共31位,2的31次方,下面的表格摘自百度百科。
Unicode/UCS-4 | bit数 | UTF-8 | byte数 | 备注 |
0000 ~ 007F | 0~7 | 0XXX XXXX | 1 | |
0080 ~ 07FF | 8~11 | 110X XXXX 10XX XXXX | 2 | |
0800 ~ FFFF | 12~16 | 1110XXXX 10XX XXXX 10XX XXXX | 3 | 基本定义范围:0~FFFF |
1 0000 ~ 1F FFFF | 17~21 | 1111 0XXX 10XX XXXX 10XX XXXX 10XX XXXX | 4 | Unicode6.1定义范围:0~10 FFFF |
20 0000 ~ 3FF FFFF | 22~26 | 1111 10XX 10XX XXXX 10XX XXXX 10XX XXXX 10XX XXXX | 5 | 说明:此非unicode编码范围,属于UCS-4 编码 早期的规范UTF-8可以到达6字节序列,可以覆盖到31位元(通用字符集原来的极限)。尽管如此,2003年11月UTF-8 被 RFC 3629 重新规范,只能使用原来Unicode定义的区域, U+0000到U+10FFFF。根据规范,这些字节值将无法出现在合法 UTF-8序列中 |
400 0000 ~ 7FFF FFFF | 27~31 | 1111 110X 10XX XXXX 10XX XXXX 10XX XXXX 10XX XXXX 10XX XXXX | 6 |
注意红色的地方,最早设计的UTF-8(FSS-UTF (1992) / UTF-8 (1993),是6位byte的,容量是2的31次方,2147483648个。
Unicode2.0
产生物:
UCS-4、变长的UTF-16、4位字节的UTF-8
产生年份:1996年
产生背景:
Unicode1.0推出来之后,世界信息交流集合级增长,他们发现之前定义的2^16次方的数量级是远远不够的。痛定思痛,搞了第二个版本。增加到了2的20次方+2^16次方=1114112个容量,并确定了目前的边界U+10FFFF。分为17个平面(虽然我不知道为什么分要是17个,每个平面2^16次方个位置)。第一个平面就是之前的初代版本,也叫作Basic Multilingual Plane (BMP)中文翻译为基本多文种平面,存储着各种语言中最常用的字符。后面有16个平面,叫做Supplementary Plane,中文翻译为辅助平面。平面是按照字符在语言的特性划分,而不是按照语言种类划分的,比如会有Supplementary Multilingual Plane(SMP辅助多文种平面)、Supplementary Ideographic Plane(辅助表意平面)。具体每个平面的内容,请自行wikipedia。还有个特殊的私有区域,是Unicde自身编码用的,范围是从U+D800到U+DFFF,对这个范围内不编码。私有区域的用处,我查了也没看懂,暂时不说了。
随着字符的增多,带来几个改变。
1、2的16次方无法表述这么多字段。需要4个字节,所以这个版本叫做UCS-4,之前的Unicode版本变成了UCS-2。
2、之前的定长(fixed-length)的UTF-16已经不合适了,向UTF-8学习,改做可变长度(variable-length)
3、出现了UTF-32的表达方式,使用至少4个字节来表示,2的31次方啊,为了将来统一全宇宙的字符,也号称自己是可变长度。。。但其实完全没有机会变长。
因为这个是面向宇宙大一统的,对于地球这个小行星来说太占地方,前面位数全是0,浪费空间。所以几乎没什么地方用。
4、至此,UTF-8、UTF-16、UTF-32全部是变长的。
5、UTF-8跟随Unicode,在RFC 3629评议文件中,被限定在了U+10FFFF,位数限定成了4位。
之后:
之后Unicode继续发展,截止目前最新版本已经是Unicode11,但是后续的版本就是不停的把没收录的字符收编进来,Unicode的整体结构上没再有变化。
BOM
最后的问题,可有可无的BOM(Byte Order Mask 字节-顺序标记),用来标记是什么编码,是LE还是BE。在文件头部使用Unicode中不会出现的字符。具体表示的请百度,好处就是在可以不指定编码方式的情况下,通过检测头部来确定是什么编码方式。不过Unicode已经不建议使用Bom来表示了,因为这样其实一定程度上会造成混乱,因为不确定有没有BOM,需要检查一下,假设有了呢,又要把BOM去掉。假设有点语言没有这种机制,转码就会出现乱码,甚至异常。
至此,Unicode就说完了,总结一下:
为了将地球上所有的字符放入一个统一编码中,产生了Unicode1.0。
Unicode1.0版本直接产生了UCS2,和定长两字节的UTF-16。紧接着创造了兼容ASCII的实现--UTF8。UTF-8是可变长度的,这个时候最长是6位。
Unicode2.0版本直接产生了UCS-4,UTF-16变为可变长度,产生了UTF-32。
将Unicode分成17个平面,其中第一个平面BMP就是以前的UCS2的字符集合,所以UCS2是目前UTF-16的绝对子集。
在2003年的RFC3629中,将UTF-8限定为和Unicode范围相同0x10FFFF,限定为4字节。
产生物:
UCS-4、变长的UTF-16、4位字节的UTF-8