终于看懂了Unicode、UTF-8、UTF-16转载这篇文章,请联系Linux开发那些东西公众号。计算机起源于美国。上个世纪,他们对英文字符与二进制位的关系进行了统一规定,制定了一套字符编码规则。这套编码规则称为ASCII编码。ASCII编码一共定义了128个字符的编码规则,用七位二进制(0x00-0x7F)表示。这些字符的集合称为ASCII字符集。随着计算机的普及,不同地区和国家出现了很多字符编码,如:中国大陆的GB2312、港台的BIG5、日本的ShiftJIS等。由于字符编码的不同,计算机很难识别不同国家之间进行通信,经常会出现乱码。例如,对于同样的二进制数据,不同的编码会解析当互联网飞速发展,地域限制被打破,人们迫切希望有一个统一的规则来对所有国家和地区的字符进行编码,于是Unicode出现了。Unicode简介Unicode是一个国际标准字符集,它将世界上各种语言的每一个字符都定义了一个唯一的编码,以满足跨语言、跨平台的文本信息转换的需要。Unicode字符集的编码范围是0x0000-0x10FFFF,可以容纳超过一百万个字符。每个字符都有一个唯一的编码,即每个字符都有一个与之对应的二进制值。这里的二进制值也称为代码点。例如:汉字“中”的码位为0x4E2D,大写字母A的码位为0x41。具体可以查询字符对应的Unicode编码Unicode字符编码表字符集和字符编码字符集是很多字符的集合,比如GB2312是简体中文字符集,里面收录了6000多个常用的简体中文字符和一些符号、数字、拼音等字符字符编码是字符集的一种实现,它将字符集中的字符映射为特定的字节或字节序列。这是一个规则。例如:Unicode只是一个字符集,UTF-8、UTF-16、UTF-32才是真正的字符编码规则Unicode字符存储Unicode是一个符号集,它只规定了每个符号的二进制值,但是符号如何存储时没有指定上面提到的Unicode字符集的编码范围是0x0000-0x10FFFF,所以需要1到3个字节来表示那么,对于一个三字节的Unicode字符,计算机怎么知道它代表的是一个字符呢?三个字符?如果所有字符都使用三个字符Section表示,那么对于那些可以用一个字节表示的词对于字符来说,两个字节是没有意义的,对存储是很大的浪费。如果,对于一个普通的文本,大部分的字符只能用一个字节表示,现在如果三个只能用一个字节表示,那么文本的大小就会大三倍左右。因此,Unicode的存储方式有很多种。常见的有UTF-8、UTF-16、UTF-32。它们使用不同的二进制格式来表示Unicode。UTF-8、UTF-16、UTF-32字符中的“UTF”是“UnicodeTransformationFormat”的缩写,意思是“UnicodeTransformationFormat”,后面的数字表示至少用多少位来表示存储字符,如:UTF-8至少需要8位,即一个字节来存储。相应地,UTF-16和UTF-32至少需要2个字节和4个字节来存储UTF-8编码UTF-8:是一种可变长度的字符编码,定义为将一个代码点编码成1到4个字节,具体取决于数量代码点值中有效二进制位的个数UTF-8编码规则:对于单字节符号,bytes的第1位设置为0,后7位为该符号的Unicode编码。所以,对于英文字母,UTF-8编码和ASCII码是一样的,所以UTF-8可以兼容ASCII编码,这也是为什么网上普遍使用UTF-8n字节符号的原因之一(n>1)、第一个字节的前n位全部设为1,第n+1位设为0,后面字节的前两位全部设为10。其余未提及的二进制位均为该符号的Unicode编码。下表是UTF-8对应的Unicode编码需要的字节数和编码格式。000000-00007F0xxxxxxxASCII码000080-0007FF110xxxxx10xxxxxx000800-00FFFF1110xxxx10xxxxxx10xxxxxx010000-10FFFF11110xxx10xxxxxx10xxxxxx10xxxxxx表格中第一列是Unicode编码的范围,第二列是对应UTF-8编码方式,其中红色的二进制"1"和"0”是固定前缀,字母x代表可用编码的二进制数。根据上表,解析UTF-8编码就很简单了。如果一个字节的第一位是0,那么这个字节就是一个单独的字符。如果第一位是1,那么一行中有多少个1,就是当前字符占多少个字节Stepnumber1,2,3,4被添加到中间的左边。先查询“中”字的Unicode编码0x4E2D,转换成二进制。二进制数共有16位。具体如上步骤1所示,通过前面的Unicode编码和UTF-8编码表知道,Unicode编码0x4E2D对应的范围是000800-00FFFF,所以“中”字的UTF-8编码需要3字节,即格式为1110xxxx10xxxxxx10xxxxxx然后从“中”的最后一个字符开始从二进制的前两位开始,从后往前依次填入格式中的x个字符,多余的二进制补码为0,如上图步骤2和步骤3所示。然后,得到“中”的UTF-8编码为111001001011100010101101,转成十六进制就是0xE4B8AD,具体如上面步骤4所示UTF-16编码UTF-16也是一种变长字符编码,这种编码方式比较特殊,它把字符编码成2字节或4字节的具体编码规则如下:对于Unicode码小于0x10000的字符,使用2字节存储,不进行编码转换直接存储Unicode码对于Unicode码小于0x10000的字符code介于0x10000和0x10FFFF之间,用4个字节存储,这4个字节分为两部分,每部分有两个字节,其中,前两个字节的前6个二进制位固定为110110,前两个后两个字节的bytes6位二进制数固定为110111,前后剩余的10位二进制码将符号的Unicode编码减去0x10000,结果大于0x10FFFF。Unicode代码不能以UTF-16编码。下表为UTF-16编码格式对应的Unicode编码的Unicode编码范围(十六进制)具体Unicode编码(二进制)UTF-16编码方式(二进制)Byte00000000-0000FFFFxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx200010000-0010FFFFyyyyyyyyyyxxxxxxxxxx110110yyyUniyyyyyxx11xx1xx1码表中编码的范围,第二列是具体的Unicode编码的二进制(第二行第二列表示Unicode编码的二进制减去0x10000),第三列是对应的UTF-16编码方式,其中红色二进制“1”和“0”是固定的前缀,字母x和y表示可用编码的二进制数,第四列表示编码占用的字节数。前面提到,字符“中”的Unicode编码是4E2D,小于0x10000。根据表,其UTF-16编码占用两个字节,与Unicode编码相同,所以“中”字的UTF-16编码为4E2D。我从Unicode字符表网站上找到了一个古老的南阿拉伯字母,它的Unicode编码是:0x10A6F,可以访问https://unicode-table.com/cn/10A6F/查看字符的描述。Unicode编码对应的字符如下图所示。下面以旧南阿拉伯字母的Unicode编码0x10A6F为例,说明UTF-164字符段编码,具体步骤如下,为便于说明,步骤编号1、2、3、4、5分别为在图的左侧添加。转到0x10000,结果为0xA6F,将这个值转换成二进制00000000101001101111,对应上图中的步骤2,然后从二进制00000000101001101111的最后一个二进制开始,按照从后到到的顺序填写格式前面的x和y字符,多出的二进制补码为0,对应上图中的第3步和第4步。因此,Unicode编码0x10A6F的UTF-16编码计算为11011000000000101101111001101111,转换成十六进制0xD802DE6F,对应上图步骤5UTF-32编码UTF-32是定长编码,即总是占用4个字节,足够容纳所有的Unicode字符,所以直接存放Unicode码就可以了,不需要任何编码转换,虽然浪费空间,但是效率提高了。UTF-8、UTF-16、UTF-32之间如何转换前面提到,UTF-8、UTF-16、UTF-32是Unicode编码以不同的二进制格式表示的编码规则。同样,通过这三种编码也可以得到对应的Unicode编码的二进制表示。有了字符的Unicode编码,就可以按照前面介绍的UTF-8、UTF-16、UTF-32的编码方式,转换成任意编码的UTF字节序列。最小代码单元是多字节的,所以会有字节顺序的问题。UTF-8的最小编码单元是一个字节,所以没有字节顺序的问题。UTF-16的最小编码单元是2个字节。在解析一个UTF-16字符之前,您需要知道每个代码单元的字节顺序。例如:前面提到,“中”字的Unicode编码是4E2D,而“?”的Unicode编码是角色是2D4E。当我们收到一个UTF-16字节流4E2D时,计算机如何识别它代表的是字符“中”还是字符“?”?因此,对于一个多字节的代码单元,需要有一个标记,明确地告诉计算机,按照什么顺序来解析字符,即字节顺序。字节序分为big-endian字节序和little-endian字节序。字节之后,高位字节存放在内存的高地址端,而低位字节存放在内存的低地址端。section之后,高位字节存放在内存的低地址端,低位字节存放在内存的高地址端。以0x4E2D为例说明big-endian和little-endian,具体见下图:数据从高位字节开始存储更符合人们读取数据的习惯,内存地址增加从低地址到高地址。因此,字符0x4E2D数据的高字节为4E,低字节为2D,根据bigendian字节4E保存到内存低地址0x10001,2D保存到内存高地址0x10002。对于littleendian字节序,正好相反,数据的高字节保存到内存高地址端,低字节保存到内存低地址端,所以4E保存到高地址内存地址0x10002,2D保存到内存低地址0x10001。BOMBOM是字节或dermark的缩写是“字节顺序标记”的意思。它通常用作标识以UTF-8、UTF-16或UTF-32编码的文件。在Unicode编码中,有一个“零宽非换行符”字符(ZEROWIDTHNO-BREAKSPACE),用字符FEFF表示。对于UTF-16,如果接收到以FEFF开头的字节流,则表明它是big-endian字节序,如果收到FFFE,则表示字节流是little-endianUTF-8。不存在字节顺序问题。以上字符仅用于标识为UTF-8文件,不用于描述字节顺序。“零宽非换行符”字符的UTF-8编码是EFBBBF,所以如果你收到一个以EFBBBF开头的字节流,你就知道它是UTF-8。文件下方的表格列出了不同的UTF格式。固定文件头UTF编码固定文件头UTF-8EFBBBFUTF-16LEFFFEUTF-16BEFEFFUTF-32LEFFFE0000UTF-32BE0000FEFF根据上述固定文件头,下面列出”“中”字符在文件中的存储(包括文件头)encoding固定文件头Unicode编码0X004E2DUTF-8EFBBBF4E2DUTF-16BEFEFF4E2DUTF-16LEFFFE2D4EUTF-32BE0000FEFF00004E2DUTF-32LEFFFE00002D4E0000常见字符编码问题Redis中文key的显示有时候我们需要向redis写入包含中文的数据,然后查看数据,但是会看到一些其他的字符,而不是我们写入的中文。在上图中,我们写了一个“”给redis的“中”字符,我们写的这个“中”字符通过get命令查看是无法显示的。此时添加一个--raw参数,重启redis-cli,即执行redis-cli--raw命令启动redis客户端具体如下图,mysql中为“utf8”,在mysql中为utf8mb4MySQL实际上不是UTF-8。“utf8”仅支持每个字符最多3个字节。对于超过3个字节的字符会出错,真正的UTF-8至少要支持4个字节。MySQL中的“utf8mb4”是真正的UTF-8。我们以测试表为例。表结构如下:mysql>showcreatetabletest\G***************************1.row***************************Table:testCreateTable:CREATETABLE`test`(`name`char(32)NOTNULL)ENGINE=InnoDBDEFAULTCHARSET=utf81rowinset(0.00sec)分别插入字符“中”和Unicode编码为0x10A6F的字符写入测试表,该字符需要直接从https://unicode-table.com/cn/10A6F/复制到MySQL控制台,手动输入无效.具体执行结果如下:从上图可以看出,字符“中”插入成功,0x10A6F字符插入失败,错误提示无效字符串,\xF0\X90\XA9\xAF是0x10A6F字符的UTF-8编码,占用4个字节,因为MySQL的utf8编码最多只支持3个字节,所以会插入失败。将test表的字符集改为utf8mb4,collat??ion改为utf8bm4_unicode_ci,如下图显示:修改字符集和排序方式后,再次插入0x10A6F字符,结果成功,具体执行结果如下图所示。服务器是一致的。要真正解决这个问题,需要修改my.cnf配置中服务端和客户端的字符集总结本文从字符编码的历史上介绍了Unicode出现的原因,然后介绍了三种不同的编码方式Unicode字符集:UTF-8、UTF-16、UTF-32及其编码方式,接着是字节序、BOM的介绍,最后是字符集在MySQL和Redis应用中的常见问题及解决方法。更多关于Unicode的信息,请参考UnicodeRFC文档
