MySQL的字符编码体系可以分成两部分:一部分是关于数据库服务器本身存储数据表时如何管理字符数据的编码;另一部分是关于客户端与数据库服务器传输数据如何编码。上一篇MySQL的字符编码体系(一)——数据存储编码讨论了数据存储编码,本篇讨论数据传输编码。
MySQL的客户端可以分为两种:一种就是用C语言写的官方客户端——MySQL命令程序;一种就是平常程序员使用JDBC等connector API写成的客户端。这里只讨论第一种。
Windows客户端
MySQL命令程序在Windows和Linux系统中关于字符编码处理的部分并不等效,下图是Windows系统的客户端字符编码转换逻辑:
其中的三个character变量存在于服务器上,而charset_info存在于客户端。
当客户端启动连接到服务器时,客户端将根据配置参数设置charset_info为指定编码,同时通知服务器让服务器把三个character变量设置为相同编码。
数据传输流程
- 客户端从控制台标准输入读取一行命令文本,其编码为操作系统编码;
- 客户端将命令从系统编码转码为客户端charset_info变量设定的编码;
- 客户端将命令文本发送给服务器;
- 服务器把收到的文本解码为character_set_client编码,这个编码通常与客户端charset_info一致;
- 服务器把命令文本转码为character_set_connection;
- 服务器执行命令,产生结果;
- 将结果转码为character_set_results发送给客户端;
- 客户端把收到的结果解码为charset_info编码,这个编码通常与character_set_results一致;
- 客户端将结果转码为操作系统编码,输出到控制台标准输出。
由于在Windows平台上MySQL程序在读取控制台时使用了Unicode Console Read API,所以程序从控制台获取的原始字符串实际上是UTF16编码,所以这里的“操作系统编码”并不是Windows通常的GBK,而应该看做UTF16。
Linux客户端
下图是Linux系统中的MySQL客户端程序字符编码转换逻辑:
它与Windows版的不同之处就在于,它并不把来自终端标准输入的操作系统编码字符串强制转换为charset_info编码,也不会把输出到终端的charset_info编码结果字符串强制转换为操作系统编码。也就是说,Linux平台的MySQL程序这时候会会忽略charset_info变量。当然,这样一来Linux客户端的数据传输流程就比Windows客户端对应地少几步。
乱码陷阱模拟
根据Linux平台MySQL程序的这一特点,很容易产生这样一个可能的陷阱:在Linux系统中通过MySQL客户端向数据库插入中文数据后,查询结果没有乱码,但从配置正确的Windows平台MySQL客户端查询同一个表得到的却是乱码。
可以这样模拟上述的情况:
创建一个表,其中只包含一个GBK字符串字段和UTF8字符串字段。Linux中启动MySQL连接到数据库服务器,将服务器的三个character变量从默认的UTF8修改为GBK。向数据库插入中文数据,立即select,结果无异常:
但是使用Windows的MySQL客户端查询时,结果却是乱码:
乱码分析
结合前面的数据传输流程,就能知道问题出在什么地方:
- 客户端从终端读取了一行utf8编码(Linux默认)的命令文本,忽略charset_info变量,直接把文本发送给服务器;
- 服务器因为事先的命令charset gbk把三个character变量都设置为了GBK,所以服务器认为收到的文本是GBK编码;
- 接下来服务器会不经过任何转码将文本字符串直接存入数据表中,因为数据表第一个字段也是GBK。
到这里为止,数据表中存了一个UTF8字符串,而服务器却当它是GBK,在同一个Linux客户端查询时:
- 表中的字符串不经过任何转码直接发给客户端,因为character_set_results也是GBK;
- 客户端收到查询结果后因为忽略charset_info而直接不经过转码输出到终端标准输出;
- 终端得到的数据实际上是UTF8编码的,所以正常输出。
在Windows客户端查询时:
- 表中的字符串(UTF8)不经过任何转码直接发给客户端,因为character_set_results也是GBK;
- 客户端收到查询结果后认为是charset_info编码(此时为GBK);
- 客户端把查询结果从charset_info转码为UTF16,然后调用Unicode Console Write API输出,看到乱码。
乱码“修复”
如果Windows客户端也想看到正确的结果,那就要故意错误地配置:
- 执行命令charset utf8,这会将charset_info和三个服务器character都设置为UTF8;
- 执行命令set names gbk,这只会将三个服务器character设置为GBK;
- 现在select,结果看上去不再乱码了。
MySQL的字符编码体系(二)——数据传输编码