Unicode和UTF-8、UTF-16到底有什么不同?

由于字符编码不同,计算机在不同国家之间的交流变得很困难,经常会出现乱码的问题,比如:对于同一个二进制数据,不同的编码会解析出不同的字符。
当互联网迅猛发展,地域限制打破之后,人们迫切的希望有一种统一的规则, 对所有国家和地区的字符进行编码,于是 unicode 就出现了。
unicode 简介
unicode 是国际标准字符集,它将世界各种语言的每个字符定义一个唯一的编码,以满足跨语言、跨平台的文本信息转换。
unicode 字符集的编码范围是 0x0000 - 0x10ffff , 可以容纳一百多万个字符, 每个字符都有一个独一无二的编码,也即每个字符都有一个二进制数值和它对应,这里的二进制数值也叫 码点 , 比如:汉字 中 的 码点是 0x4e2d, 大写字母 a 的码点是 0x41, 具体字符对应的 unicode 编码可以查询 unicode字符编码表。
字符集和字符编码
字符集是很多个字符的集合,例如 gb2312 是简体中文的字符集,它收录了六千多个常用的简体汉字及一些符号,数字,拼音等字符
字符编码是 字符集的一种实现方式,把字符集中的字符映射为特定的字节或字节序列,它是一种规则
比如:unicode 只是字符集,utf-8、utf-16、utf-32 才是真正的字符编码规则
unicode 字符存储
unicode 是一个符号集, 它只规定了每个符号的二进制值,但是符号具体如何存储它并没有规定
前面提到, unicode 字符集的编码范围是 0x0000 - 0x10ffff,因此需要 1 到 3 个字节来表示
那么,对于三个字节的 unicode字符,计算机怎么知道它表示的是一个字符而不是三个字符呢 ?
如果所有字符都用三个字节表示,那么对于那些一个字节就能表示的字符来说,有两个字节是无意义的,对于存储来说,这是极大的浪费,假如 , 一个普通的文本, 大部分字符都只需一个字节就能表示,现在如果需要三个字节才能表示,文本的大小会大出三倍左右
因此,unicode 出现了多种存储方式,常见的有 utf-8、utf-16、utf-32,它们分别用不同的二进制格式来表示 unicode 字符
utf-8、utf-16、utf-32 中的 utf 是 unicode transformation format 的缩写,意思是unicode 转换格式,后面的数 字表明至少使用多少个比特位来存储字符, 比如:utf-8 最少需要8个比特位也就是一个字节来存储,对应的, utf-16 和 utf-32 分别需要最少 2 个字节 和 4 个字节来存储
utf-8 编码
utf-8: 是一种变长字符编码,被定义为将码点编码为 1 至 4 个字节,具体取决于码点数值中有效二进制位的数量
utf-8 的编码规则:
对于单字节的符号,字节的第一位设为 0,后面 7 位为这个符号的 unicode 码。因此对于英语字母,utf-8 编码和 ascii 码是相同的, 所以 utf-8 能兼容 ascii 编码,这也是互联网普遍采用 utf-8 的原因之一
对于 n 字节的符号( n > 1),第一个字节的前 n 位都设为 1,第 n + 1 位设为 0,后面字节的前两位一律设为 10 。剩下的没有提及的二进制位,全部为这个符号的 unicode 码
下表是unicode编码对应utf-8需要的字节数量以及编码格式
unicode编码范围(16进制) utf-8编码方式(二进制)
000000 - 00007f 0xxxxxxx ascii码
000080 - 0007ff 110xxxxx 10xxxxxx
000800 - 00ffff 1110xxxx 10xxxxxx 10xxxxxx
01 0000 - 10 ffff 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
表格中第一列是unicode编码的范围,第二列是对应utf-8编码方式,其中红色的二进制 1 和 0 是固定的前缀, 字母 x 表示可用编码的二进制位
根据上面表格,要解析 utf-8 编码就很简单了,如果一个字节第一位是 0 ,则这个字节就是一个单独的字符,如果第一位是 1 ,则连续有多少个 1 ,就表示当前字符占用多少个字节
下面以 中 字 为例来说明 utf-8 的编码,具体的步骤如下图, 为了便于说明,图中左边加了 1,2,3,4 的步骤编号
首先查询 中 字的 unicode 码 0x4e2d, 转成二进制, 总共有 16 个二进制位, 具体如上图 步骤1 所示
通过前面的 unicode 编码和 utf-8 编码的表格知道,unicode 码 0x4e2d 对应 000800 - 00ffff 的范围,所以, 中 字的 utf-8 编码 需要 3 个字节,即格式是 1110xxxx 10xxxxxx 10xxxxxx
然后从 中 字的最后一个二进制位开始,按照从后向前的顺序依次填入格式中的 x 字符,多出的二进制补为 0, 具体如上图 步骤2、步骤3 所示
于是,就得到了 中 的 utf-8 编码是 11100100 10111000 10101101, 转换成十六进制就是 0xe4b8ad, 具体如上图 步骤4 所示
utf-16 编码
utf-16 也是一种变长字符编码, 这种编码方式比较特殊, 它将字符编码成 2 字节 或者 4 字节
具体的编码规则如下:
对于 unicode 码小于 0x10000 的字符, 使用 2 个字节存储,并且是直接存储 unicode 码,不用进行编码转换
对于 unicode 码在 0x10000 和 0x10ffff 之间的字符,使用 4 个字节存储,这 4 个字节分成前后两部分,每个部分各两个字节,其中,前面两个字节的前 6 位二进制固定为 110110,后面两个字节的前 6 位二进制固定为 110111, 前后部分各剩余 10 位二进制表示符号的 unicode 码 减去 0x10000 的结果
大于 0x10ffff 的 unicode 码无法用 utf-16 编码
下表是unicode编码对应utf-16编码格式
unicode编码范围(16进制) 具体unicode码(二进制) utf-16编码方式(二进制) 字节
0000 0000 - 0000 ffff xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx 2
0001 0000 - 0010 ffff yy yyyyyyyy xx xxxxxxxx 110110yy yyyyyyyy 110111xx xxxxxxxx 4
表格中第一列是unicode编码的范围,第二列是 具体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-16 4 字节的编码,具体步骤如下,为了便于说明,图中左边加了 1,2,3,4 、5的步骤编号
首先把 unicode 码 0x10a6f 转成二进制, 对应上图的 步骤 1
然后把 unicode 码 0x10a6f 减去 0x10000, 结果为 0xa6f 并把这个值转成二进制 00 00000010 10 01101111,对应上图的 步骤 2
然后 从二进制 00 00000010 10 01101111 的最后一个二进制为开始,按照从后向前的顺序依次填入格式中的 x 和 y 字符,多出的二进制补为 0, 对应上图的 步骤 3、 步骤 4
于是,就计算出了 unicode 码 0x10a6f 的 utf-16 编码是 11011000 00000010 11011110 01101111 , 转换成十六进制就是 0xd802de6f, 对应上图的 步骤 5
utf-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 时,计算机如何识别它表示的是字符 中 还是 字符 ⵎ 呢 ?
所以,对于多字节的编码单元,需要有一个标记显式的告诉计算机,按照什么样的顺序解析字符,也就是字节序,字节序分为 大端字节序 和 小端字节序
小端字节序简写为 le( little-endian ), 表示 低位字节在前,高位字节在后, 高位字节保存在内存的高地址端,而低位字节保存在内存的低地址端
大端字节序简写为 be( big-endian ), 表示 高位字节在前,低位字节在后,高位字节保存在内存的低地址端,低位字节保存在在内存的高地址端
下面以 0x4e2d 为例来说明大端和小端,具体参见下图:
数据是从高位字节到低位字节显示的,这也更符合人们阅读数据的习惯,而内存地址是从低地址向高地址增加
所以,字符0x4e2d 数据的高位字节是 4e,低位字节是 2d
按照大端字节序的高位字节保存内存低地址端的规则,4e 保存到低内存地址 0x10001 上,2d 则保存到高内存地址 0x10002 上
对于小端字节序,则正好相反,数据的高位字节保存到内存的高地址端,低位字节保存到内存低地址端的,所以 4e 保存到高内存地址 0x10002 上,2d 则保存到低内存地址 0x10001 上
bom
bom 是 byte-order mark 的缩写,是 字节序标记 的意思, 它常被用来当做标识文件是以 utf-8、utf-16 或 utf-32 编码的标记
在 unicode 编码中有一个叫做 零宽度非换行空格 的字符 ( zero width no-break space ), 用字符 feff 来表示
对于 utf-16 ,如果接收到以 feff 开头的字节流, 就表明是大端字节序,如果接收到 fffe, 就表明字节流 是小端字节序
utf-8 没有字节序问题,上述字符只是用来标识它是 utf-8 文件,而不是用来说明字节顺序的。零宽度非换行空格 字符 的 utf-8 编码是 ef bb bf, 所以如果接收到以 ef bb bf 开头的字节流,就知道这是utf-8 文件
下面的表格列出了不同 utf 格式的固定文件头
utf编码 固定文件头
utf-8 ef bb bf
utf-16le ff fe
utf-16be fe ff
utf-32le ff fe 00 00
utf-32be 00 00 fe ff
根据上面的 固定文件头,下面列出了 中 字在文件中的存储 ( 包含文件头 )
编码 固定文件头
unicode 编码 0x004e2d
utf-8 ef bb bf 4e 2d
utf-16be fe ff 4e 2d
utf-16le ff fe 2d 4e
utf-32be 00 00 fe ff 00 00 4e 2d
utf-32le ff fe 00 00 2d 4e 00 00
常见的字符编码的问题
redis 中文key的显示
有时候我们需要向redis中写入含有中文的数据,然后在查看数据,但是会看到一些其他的字符,而不是我们写入的中文
上图中,我们向redis 写入了一个 中 字,通过 get 命令查看的时候无法显示我们写入的 中 字
这时候加一个 --raw 参数,重新启动 redis-cli 即可,也即 执行 redis-cli --raw 命令启动redis客户端,具体的如下图所示
mysql 中的 utf8 和 utf8mb4
mysql 中的 utf8 实际上不是真正的 utf-8, utf8 只支持每个字符最多 3 个字节, 对于超过 3 个字节的字符就会出错, 而真正的 utf-8 至少要支持 4 个字节
mysql 中的 utf8mb4 才是真正的 utf-8
下面以 test 表为例来说明, 表结构如下:
mysql> show create table testg *************************** 1. row *************************** table: test create table: create table `test` ( `name` char(32) not null ) engine=innodb default charset=utf8 1 row in set (0.00 sec)
向 test 表分别插入 中 字 和 unicode 码为 0x10a6f 的字符,这个字符需要从 https://unicode-table.com/cn/10a6f/ 直接复制到 mysql 控制台上,手工输入会无效,具体的执行结果如下图:
从上图可以看出,插入 中 字 成功,插入 0x10a6f 字符失败,错误提示无效的字符串,xf0x90xa9xaf 正是 0x10a6f 字符的 utf-8 编码,占用 4 个字节, 因为 mysql 的 utf8 编码最多只支持 3 个字节,所以插入会失败
把 test 表的字符集改成 utf8mb4 , 排序规则 改成 utf8bm4_unicode_ci, 具体如下图所示:
字符集和排序方式修改之后,再次插入 0x10a6f 字符, 结果是成功的,具体执行结果如下图所示
上图中,set names utf8mb4 是为了测试方便,临时修改当前会话的字符集,以便保持和 服务器一致,实际解决这个问题需要修改 my.cnf 配置中 服务器和客户端的字符集
小结
本文从字符编码的历史介绍了 unicode 出现的原因,接着介绍了 unicode 字符集中 三种不同的编码方式:utf-8、utf-16、utf-32 以及它们的的编码方法,紧接着介绍了 字节序、bom ,最后讲到了字符集在 mysql 和 redis 应用中常见的问题以及解决方案 。


未来电动汽车充电会是什么样子?插电、换电和无线哪个最有潜力
物联网的哪五种营销策略将可能零售市场的发展
dfrobotMaKey MaKey教程简介
PPG测量原理详解
应对数智化时代下SAP用户的系统转型挑战——SNP TDO
Unicode和UTF-8、UTF-16到底有什么不同?
E资讯:小米新品发布,一张长图放不下
飞利浦猛腾326M6FJSB电竞显示器评测 为游戏玩家打造的显示器
WT2003HP8-32N编解码芯片的功能特点
适用于传感器融合应用的STM32L0
大联大品佳集团推出基于Infineon产品的11kW DC-DC转换器方案
自力式调节阀工作原理,自力式调节阀常见的10大问题
电池涂层氧化铝获得开发,促进未来电池工业多样化
【开源项目】智慧宿舍,高校防火防盗就靠它啦
6G网络的理论峰值速度将有望达到1TB/s是当前5G的8000倍
用晶体管编程的机器人设计方案
联发科年底有望将10纳米HelioX30推上市场
无人机测绘具备怎样的优点
CAN总线与MSP430红外检测系统电路
又一智慧灯杆项目将竣工?广东佛山智慧灯杆建设迈上新台阶