一、字符集和字符编码的区别和联系 两者的概念与区别: 字符集: 多个字符的集合。例如 GB2312 是中国国家标准的简体中文字符集,GB2312 收录简化汉字(6763 个)及一般符号、序号、数字、拉丁字母、日文假名、希腊字母、俄文字母、汉语拼音符号、汉语注音字母,共 7445 个图形字符 字符编码: 把字符集中的字符编码为(映射)指定集合中的某一对象(例如:比特模式、自然数序列、电脉冲),以便文本在计算机中存储和通过通信网络的传递 两者的关系:
  • 字符集是书写系统字母与符号的集合
  • 而字符编码则是将字符映射为一特定的字节或字节序列,是一种规则
  • 通常特定的字符集采用特定的编码方式: 即一种字符集对应一种字符编码 ,例如ASCII、IOS-8859-1、GB2312、GBK等,它们都采用一种字符编码按时限方式,因此它们既可以叫做是字符集也可以叫做是字符编码 但 Unicode 不是 ,它采用现代的模型,Unicode字符集有多种字符编码方式,例如Unicode字符集有UTF-8字符编码方式、UTF-16字符编码方式
二、字符集编码的发展 发展历史为: 单字节 ===> 双字节 ===> 多字节
ASCII(American Standard Code for Information Interchange) ,128 个字符,用 7 位二进制表示(00000000-01111111即 0x00-0x7F) EASCII(Extended ASCII) ,256 个字符,用8位二进制表示(00000000-11111111 即 0x00-0xFF)
  • 当计算机传到了欧洲,国际标准化组织在 ASCII 的基础上进行了扩展,形成了 ISO-8859标准,跟 EASCII 类似,兼容 ASCII,在高 128 个码位上有所区别。但是由于欧洲的语言环境十分复杂,所以根据各地区的语言又形成了很多子标准,ISO-8859-1、ISO-8859-2、ISO-8859-3、……、ISO-8859-16
    • 当计算机传到了亚洲,256 个码位就不够用了。于是乎继续扩大二维表,单字节改双字节,16 位二进制数,65536 个码位。在不同国家和地区 又出现了很多编码,大陆的 GB2312、港台的 BIG5、日本的 Shift JIS 等等
    • 注意 65536 个码位这种说法只是理想情况,由于双字节编码可以是变长的,也就是说 同一个编码里面有些字符是单字节表示,有些字符是双字节表示 。这样做的好处是,一方面可以兼容 ASCII,另一方面可以节省存储容量,代价就是会损失一部分码位
    • GBK(Chinese Internal Code Specification 汉字内码扩展规范):
      • 是GB2312 的扩展(gbk 编码能够用来同时表示繁体字和简体字),按理说都属于双字节编码,码位是一样的,根本谈不上扩展,但实际上是预留空间在起作用
      • 比如下图为 GBK 的编码空间,GBK/1、GBK/2 是 GB2312 的区域,GBK/3、GBK/4、GBK/5 是 GBK 的区域,红色是用户自定义区域,白色可能就是由于变长编码损失的区域了
      • 支持国际标准 ISO/IEC10646-1 和国家标准 GB13000-1 中的全部中日韩汉字。 GBK 字符集中所有中文字符和全角符号占 2 个字节,字母和半角符号占一个字节 。 没有特殊的编码方式,习惯称呼 GBK 编码。一般在国内,汉字较多时使用
      • UNICODE字符集国际标准字符集:
        • 当互联网席卷了全球,地域限制被打破了,不同国家和地区的计算机在交换数据的过程中,就会出现乱码的问题,即对同一组二进制数据,不同的编码会解析出不同的字符
        • 它将世界各种语言的每个字符定义一个唯一的编码, 以满足跨语言、跨平台的文本信息转换
        • 有多个编码方式,分别是UTF-8,UTF-16,UTF-32编码
        • 范例:“汉字”对应的 UNICODE 数字是 0x6c49 和 0x5b57,而编码的程序数据是:
          • UTF8 编码:       E6B189       E5AD97
          • UTF16BE 编码:6C49            5B57
          • UTF32BE 编码:00006C490  0005B57
          • Unicode字符集可以使用的编码有三种:
            • UFT-8:一种变长的编码方案,使用 1~6 个字节来存储
            • UFT-32:一种固定长度的编码方案,不管字符编号大小,始终使用4个字节来存储
            • UTF-16:介于 UTF-8 和 UTF-32 之间,使用2个或者4个字节来存储,长度既固定又可变
            • UTF是Unicode Transformation Format的缩写,意思是“Unicode 转换格式”,后面的数 表明至少使用多少个比特位(Bit)来存储字符
            • 三、Unicode字符集
              • UTF是Unicode Transformation Format的缩写,意思是“Unicode 转换格式”,后面的数表明至少使用多少个比特位(Bit)来存储字符
              • Unicode字符集可以使用的编码有三种:
                • UFT-8:一种变长的编码方案,使用 1~6 个字节来存储
                • UFT-32:一种固定长度的编码方案,不管字符编号大小,始终使用4个字节来存储
                • UTF-16:介于 UTF-8 和 UTF-32 之间,使用2个或者4个字节来存储,长度既固定又可变
                • Unicode是字符集, utf-8、utf-16、utf-32才是真正的 编码方式

                  UTF-8

                  UTF-8: 是一种变长字符编码,被定义为将代码点编码为 1 至 4 个字节,具体取决于代码点数值中有效位的数量
                • 注意:UTF-8 不是编码规范,而是编码方式
                • 下表为 Unicode 值对应的 utf8 需要的字节数量:
                  • 前面红色的数字属于编码前缀,Unicode就是用这些前缀来判断你的字符类型属于什么编码方式(utf-8、utf-16、utf-32),例如对一堆二进制解析的时候就需要用前缀来判断

                  演示案例①

                  • 例如,汉字“董”的Unicode编码(16进制)为8463,对应上图的unicode编码(16进制)属于第3种类型(位于000800-00FFFF之间)

                  Linux(程序设计):26---字符集与字符编码概述(附Unicode字符集实现原理)_字符编码_03

                  • 8463转换为二进制就是“ 1000 0100 0110 0011 ”,通过上面的编码表可以看出“董”对应的UTF-8编码前缀为“ 1110 xxxx 10 xxxxxx 10 xxxxxx”,因此将编码前缀加到二进制前面,“董”的utf-8编码为“ 1110 1000 10 010001 10 100011” ,因此“董”字在utf8编码下占用3字节

                  Linux(程序设计):26---字符集与字符编码概述(附Unicode字符集实现原理)_Unicode字符集_04

                  • 然后再通过下图我们知道UTF8编码的十六进制为“E891A3”,转换为二进制就是“ 1110 1000 10 010001 10 100011”

                    演示案例②

                    • 例如,字符“a”的Unicode编码(16进制)为61,对应上图的unicode编码(16进制)属于第1种类型(位于000000-00007F之间)

                    Linux(程序设计):26---字符集与字符编码概述(附Unicode字符集实现原理)_字符集与字符编码_07

                    • 61转换为二进制就是“ 0110 0001 ”,通过上面的编码表可以看出“a”对应的UTF-8编码前缀为“ 0 xxxxxxx”,因此将编码前缀加到二进制前面,“a”的utf-8编码为“ 0 0 1100001 ,前面的0可以省略,因此就是“ 0 1100001 ”。 因此“a”字在utf8编码下占用1字节

                    Linux(程序设计):26---字符集与字符编码概述(附Unicode字符集实现原理)_字符编码_08

                    • 然后再通过下图我们知道UTF8编码的十六进制为“64”,转换为二进制就是“ 0 1100001

                      UTF-16

                      • UFT-16 比较奇葩,它 使用 2 个或者 4 个字节来存储 对于 Unicode 编号范围在 0~FFFF 之间的字符,UTF-16 使用两个字节存储 ,并且 直接存储 Unicode 编号,不用进行编码转换,这跟 UTF-32 非常类似。
                      • 对于 Unicode 编号范围在 10000~10FFFF 之间的字符,UTF-16 使用四个字节存储 , 具体来说就是:将字符编号的所有比特位分成两部分,较高的一些比特位用一个值 介于 D800~DBFF 之间的双字节存储,较低的一些比特位(剩下的比特位)用一个值介于 DC00~DFFF 之间的双字节存储

                        UTF-32

                        UTF-32 是固定长度的编码,始终占用 4 个字节 ,足以容纳所有的 Unicode 字符,所以直接存储 Unicode 编号即可,不需要任何编码转换
                      • 浪费了空间,提高了效率
                      • 四、UTF BOM问题
                        • BOM(Byte Order Mark)字节序(字节顺序的标识),其实就是用 大端(BE)还是小端(LE) 比如 UTF-16BE 和 UTF-16LE: UTF-16BE, 其后缀是 BE 即 big-endian,大端的意思。大端就是将高位的字节放在低地址表示
                        • UTF-16LE, 其后缀是 LE 即 little-endian,小端的意思。小端就是将高位的字节放 在高地址表示 UTF-16, 没有指定后缀,即不知道其是大小端,所以其开始的两个字节表示该字节数组是大端还是小端。即FE FF表示大端,FF FE表示小端
                        • UTF 在文件中的存储。UTF 格式在文件中总有固定文件头:
                        • 当打开一个文件时,判断其属于什么编码类型,只要判断其开头处的标志就可以了:
                          • EF BB BF 表示 UTF-8
                          • FE FF 表示 UTF-16BE
                          • FF FE 表示 UTF-16LE
                          • 00 00 FE FF 表示 UTF32-BE
                          • FF FE 00 00 表示 UTF32-LE
                          • 如果一个Unicode编码类型的文件 缺少了这些标志位,那么文件打开之后就会出现乱码 (见下面演示案例②)
                          • 注意: 只有 UTF-8 兼容 ASCII,UTF-32 和 UTF-16 都不兼容 ASCII,因为它们没有单字节编码 UTF-8缺省不带BOM:
                            • UTF-8 中有一字节的情况,这种情况,就没有两端的说法了。至于另外的二,三,四字节情况,以三字节为例,如果你一定要弄出端法,也不是说不可以,比如,小端法就是“小-中-大”,大端法就是“大-中-小”
                            • 但现实情况是 UTF-8 仅仅采用了一种端法, 就是大端法

                              演示案例①(验证UTF-16)

                              例如, “汉”字在文件中的存储就如下所示

                            Linux(程序设计):26---字符集与字符编码概述(附Unicode字符集实现原理)_字符集与字符编码_13

                            • 例如分别 采用UTF-16、UTF-16BE、UTF-16LE 存储“汉”这个中文字符

                            Linux(程序设计):26---字符集与字符编码概述(附Unicode字符集实现原理)_字符集与字符编码_14

                            • 然后我们使用二进制编辑器查看这三个文件对应的二进制内容,可以看出:
                              • UTF-16:因为没有指定后缀,此处通过查看前两个字节,可以看出其是小端(LE)存储的,并且“汉”被存储为“49 6C”
                              • UTF-16LE:小端存储,“汉”被存储为“49 6C”
                              • UTF-16BE:大端存储,“汉”被存储为“6C 49”
                              • 演示案例②(乱码演示)

                                • 我们在上面说过,如果一个Unicode编码类型的文件 缺少了这些标志位,那么文件打开之后就会出现乱码
                                • 接着上面的演示案例①,我们以UTF-16BE为例,其正常情况如下所示,文件以FE FF标志位开头
                                  • 现在我们使用二进制编辑器,将前面的FE FF标志位删去(随意删除),例如我们将FE删除

                                  Linux(程序设计):26---字符集与字符编码概述(附Unicode字符集实现原理)_ico_18

                                  • 现在再打开查看,发现已经出现了乱码

                                  Linux(程序设计):26---字符集与字符编码概述(附Unicode字符集实现原理)_ico_19

                                  演示案例③(UTF-8缺省不带BOM)

                                  • 上面说过了 UTF-8缺省不带BOM
                                  • 现在我们有一个UTF-8格式的文件,其存储的内容为“汉”
                                    • 通过二进制查看,可以看到其以“EF BB BF”开头

                                    Linux(程序设计):26---字符集与字符编码概述(附Unicode字符集实现原理)_字符集与字符编码_21

                                    五、相关命令

                                    file命令

                                    • file文件可以查看文件的类型
                                    • 演示案例和详细内容可以参阅: javascript:void(0)

                                      iconv命令

                                      • iconv可以用来转换文件的编码方式
                                      • 演示案例和详细内容可以参阅: javascript:void(0) iconv开发库: javascript:void(0) 六、字符集应用案例

                                        MySQL

                                        部分汉字在mysql使用utf8字符时写入出现异常,或者读取出现异常 。比如????,其和熙存在区别。????在utf8模式下需要4个字节表示

                                      Linux(程序设计):26---字符集与字符编码概述(附Unicode字符集实现原理)_ico_22

                                      MySQL中的UTF-8:
                                      • MySQL的 “utf8 ”实际上不是真正的 UTF-8 , “utf8”只支持每个字符最多3个字节。真正的utf8至少要支持4个字节
                                      • MySQL一直没有修复这个bug,他们在2010年发布了一个叫作"utf8mb4"的字符集,MySQL的 "utf8mb4"是真正的"UTF-8"
                                        • 下面我们调用MySQL的C API操作MySQL,在建表的时候其表的编码格式指定为utf8而不是utf8mb4,因此其只支持每个字符最多3个字节,然后向表中插入数据。并且调用mysql_set_character_set()函数将当前用户的默认字符集设置为utf8
                                        • 在插入数据的时候(见下面的INSERT_SAMPLE_TABLE宏),其插入的第二个熙字占用4字节,因此下面程序的结果应该会出错
                                        #include <stdio.h>
                                        #include <stdlib.h>
                                        #include <mysql.h>
                                        #define DROP_SAMPLE_TABLE "DROP TABLE IF EXISTS utf8_tbl"
                                        #define CREATE_SAMPLE_TABLE "CREATE TABLE utf8_tbl ( \
                                                                        utf8_id INT UNSIGNED AUTO_INCREMENT, \
                                                                        utf8_title VARCHAR(100) NOT NULL,    \
                                                                        PRIMARY KEY (utf8_id)            \
                                                                    )ENGINE=InnoDB DEFAULT CHARSET=utf8"
                                        #define INSERT_SAMPLE_TABLE "INSERT INTO utf8_tbl(utf8_title) VALUES('????熙')"
                                        #define SELECT_SAMPLE_TABLE "SELECT * FROM utf8_tbl"
                                        #define DBNAME "unicode"
                                        #define CHARSET "utf8"
                                        int main()
                                            MYSQL *mysql; //MySQL连接句柄
                                            //初始化连接句柄
                                            mysql = mysql_init(NULL);
                                            if(mysql == NULL)
                                                fprintf(stderr, "mysql_init failed, code: %d, reason: %s\n", mysql_errno(mysql), mysql_error(mysql));
                                                return -1;
                                            //连接MySQL服务端
                                            if(mysql_real_connect(mysql, "localhost", "root", "123456", DBNAME, 0, NULL, 0) == NULL)
                                                fprintf(stderr, "mysql_connect failed, code: %d, reason: %s\n", mysql_errno(mysql), mysql_error(mysql));
                                                mysql_close(mysql);
                                                return -1;
                                            printf("Connection success\n\n");
                                            //设置当前连接的默认字符集(此处设置为utf8)
                                            if(mysql_set_character_set(mysql, CHARSET) != 0)
                                                fprintf(stderr, "mysql_set_character_set failed, code: %d, reason: %s\n", mysql_errno(mysql), mysql_error(mysql));
                                                mysql_close(mysql);
                                                return -1;
                                            //删除表
                                            if(mysql_query(mysql, DROP_SAMPLE_TABLE) != 0)
                                                fprintf(stderr, "drop table failed, code: %d, reason: %s\n", mysql_errno(mysql), mysql_error(mysql));
                                                mysql_close(mysql);
                                                return -1;
                                            printf("drop table success\n\n");
                                            //创建表
                                            if(mysql_query(mysql, CREATE_SAMPLE_TABLE) != 0)
                                                fprintf(stderr, "create table failed, code: %d, reason: %s\n", mysql_errno(mysql), mysql_error(mysql));
                                                mysql_close(mysql);
                                                return -1;
                                            printf("create table success\n\n");
                                            //向表中插入数据
                                            if(mysql_query(mysql, INSERT_SAMPLE_TABLE) != 0)
                                                fprintf(stderr, "insert failed, code: %d, reason: %s\n", mysql_errno(mysql), mysql_error(mysql));
                                                mysql_close(mysql);
                                                return -1;
                                            printf("insert success, insert %lu rows\n\n", (unsigned long)mysql_affected_rows(mysql));
                                            //查询数据(此处调用mysql_store_result一次性读取所有数据,然后再调用mysql_fetch_row逐行获取数据)
                                            if(mysql_query(mysql, SELECT_SAMPLE_TABLE) != 0)
                                                fprintf(stderr, "select failed, code: %d, reason: %s\n", mysql_errno(mysql), mysql_error(mysql));
                                                mysql_close(mysql);
                                                return -1;
                                                //查询数据,将所有的结果保存到结果集中
                                                MYSQL_RES *res;
                                                res = mysql_store_result(mysql);
                                                if(res == NULL)
                                                    fprintf(stderr, "mysql_store_result failed, code: %d, reason: %s\n", mysql_errno(mysql), mysql_error(mysql));
                                                    mysql_close(mysql);
                                                    return -1;
                                                printf("select %lu rows\n", (unsigned long)mysql_num_rows(res));
                                                //逐行获取数据
                                                MYSQL_ROW sqlrow;
                                                int columnNum = mysql_num_fields(res);
                                                while((sqlrow = mysql_fetch_row(res)) != NULL)
                                                    //逐列获取数据
                                                    for(int i = 0; i < columnNum; ++i)
                                                        printf("%s\t", sqlrow[i]);
                                                    printf("\n");
                                                //查询完成之后释放结果集
                                                mysql_free_result(res);
                                            mysql_close(mysql);
                                            return 0;
                                         
                                        • 下面是效果,在插入数据的时候出错了,因为在插入数据的时候第二个熙字其占用4个字节,超过了MySQL中utf8编码的最大值,因此会出错

                                        Linux(程序设计):26---字符集与字符编码概述(附Unicode字符集实现原理)_mysql_23

                                        • 现在我们修改程序,将表的格式设置为utf8mb4,当前用户的编码设置也设置为utf8mb4,然后再重新编译运行程序,那么就执行成功了

                                        Linux(程序设计):26---字符集与字符编码概述(附Unicode字符集实现原理)_mysql_24

                                        • 修改之后再次操作,可以看到成功了

                                        Linux(程序设计):26---字符集与字符编码概述(附Unicode字符集实现原理)_mysql_25

                                        Nginx

                                        • 如果在Nginx中使用中文可能会出现错误(例如页面中有中文)
                                        • 可以在Nginx配置文件中加入下面的选项来设置UTF-8配置
                                        • 例如下面是一个Nginx配置文件
                                        server {
                                            listen  8888;
                                            # 设置为utf-8格式
                                            charset utf-8;
                                            server_name 192.168.221.141;
                                            location / {
                                                root /mnt/hgfs/ubuntu/vip/20191017-unicode/src/nginx;
                                                index index.php index.html index.htm;
                                         

                                        Redis

                                        • 默认情况下,在Redis中使用中文会出现乱码

                                        Linux(程序设计):26---字符集与字符编码概述(附Unicode字符集实现原理)_ico_26

                                        可以配置--raw选项来解决乱码问题,例如下面在连接时指定--raw
                                      redis-cli --raw