Mysql客户端如何处理中文
n 两个问题
我们在mysql客户端输入中文的时候,经常会碰到以下两个现象:
1) 不能输入中文。例如输入"你好netease",并敲回车,mysql表示只接受到了"netease",或者一些乱码。
2) 可以输入中文,但是不能回显。例如输入"你好netease",并敲回车,mysql表示接受到了"你好netease",但是屏幕上只回显了"netease"。
n 字符集和编码
要了解上述两个现象的前缘后世,我们首先需要了解字符集和编码:
1) Unicode: Unicode定义了所收录的字符和字符位置,覆盖了世界上所有的文字和符号,我们可以把它理解成是一个字符集。Unicode编码占四个字节。
2) ASCII: 不解释。
3) GB2312: GB2312定义了所收录的字符的GB2312编码及其对应的Unicode码,GB2312编码占两个字节。
4) GBK: GBK定义了所收录的字符的GBK编码及其对应的Unicode码,GBK编码是GB2312编码的超集,向下完全兼容GB2312编码。GBK编码占两个字节。
5) GB18030: GB18030定义了所收录的字符的GB18030编码及其对应的Unicode码,GB18030编码是GBK编码的超集,向下完全兼容GBK编码。
GB18030编码是变长编码,有单字节,双字节和四字节三种方式。
6) UTF-8: UTF-8定义了所收录的字符的UTF-8编码及其对应的Unicode码,UTF-8定义了全世界所有国家需要用到的符号,与GB18030编码不兼容。
UTF-8编码也是变长编码,最高可以达到6字节,英文占一个字节,中文占三个字节。
综上所述:
1) 我们可以把Unicode理解为字符集,而把GB2312,GBK,GB18030,UTF-8理解为编码方式,每个编码对应Unicode字符集中的一个字符。
2) GB2312,GBK,GB18030三者相互兼容,三者关系为GB2312 < GBK < GB18030,与UTF-8不兼容,UTF-8与GB18030需要通过unicode做中转。
3) GB2312,GBK,GB18030,UTF-8都覆盖了ASCII码,GB2312,GBK,GB18030,UTF-8的字节高位为0时表示ascii码,为1时表示非ascii码。
n 终端和mysql客户端
终端:指我们打开shell,连接远程机器的软件,例如SecureCRT和putty,我们在终端输入中文的时候,发到远程机器的字符取决于终端的编码设置。
SecureCRT的编码设置可以通过Options->Session Options->Terminal->Apperance->Character来设置,“default”表示gbk编码,“utf-8”表示utf8编码。
例如,设置终端编码为default,在终端输入“你”,远程机器接受到的编码为"/xc4/xe3"
设置终端编码为utf-8,在终端输入“你”,远程机器接受到的编码为"/xe4/xbd/xa0"
Mysql客户端:指mysql客户端程序,我们一般通过终端连接到远程机器,然后运行远程机器的mysql客户端程序,连接到mysql服务器,mysql服务器可能与mysql客户端位于同一台机器上,也可能位于不同机器上。
我们通过终端在mysql客户端输入中文的时候,mysql客户端接收到终端发来的编码,经过客户端的解析,发送到mysql服务器。
Mysql客户端所在机器的编码可以通过LC_ALL来设置,mysql客户端对终端发来的编码的解析依赖于该参数。
例如,LC_ALL="zh_CN.GBK",mysql客户端就用gbk编码格式对编码进行解析。
LC_ALL="zh_CN.UTF-8",mysql客户端就用utf-8编码格式对编码进行解析。
所以,终端和mysql客户端所在机器的编码要保持一致,如果不一致,就会出现用一种编码格式去解析另一种编码的情况,详见下文的“第一个问题”。
n Mysql处理中文流程
*nix处理多字节语言编码有两种方式:
1) 数据都以多字节编码存放在各处,统一处理。
2) 数据输入输出使用多字节编码(mb),但是在内部使用c语言的宽字节字符(wc),wc占三到四个字节,这样就涉及到多字节字符与宽字节字符的互相转换,
glibc对宽字节字符提供了很好的支持。
libedit使用了第二种方式。
我们在mysql客户端输入中文的时候,libedit会调用mbrtowc函数把终端发来的多字节字符转换为宽字节字符,然后回显到myql客户端并发往mysql服务器。
mysql5.5客户端处理输入输出的库有两种,分别是libedit和readline,可以通过"mysql -V"查看,源代码安装mysql默认使用的库为libedit。
我们基于libedit库对mysql5.5客户端进行了调试,以下的分析都是基于libedit库,调试环境如下:
$ uname
Linux
$ mysql -V
mysql Ver 14.14 Distrib 5.5.20, for Linux (x86_64) using EditLine wrapper
n 第一个问题
我们在mysql客户端输入中文的时候,libedit首先调用mbrtowc函数把终端发来的多字节字符转换为宽字节字符。
mbrtowc函数的行为依赖于LC_CTYPE的值。例如当前LC_CTYPE=zh_CN.GB18030,mbrtowc会调用GB18030.c中的_GB18030_mbrtowc()函数来完成实质性工作。
如果终端的编码为gbk,而mysql客户端所在机器的LC_CTYPE=zh_CN.UTF8,即用_UTF8_mbrtowc()来处理gbk编码,由于gbk编码与UTF8编码不兼容,libedit会认为这是无效字符,从而丢弃,或者恰巧可以识别,但是识别成其他字符了。这就会发生本文开头提到的第一个问题啦。
所以如果终端编码是gbk,那么mysql客户端所在机器的LC_ALL值必须设置为与gbk编码相兼容的编码方式,可以设置为zh_CN.GB2312,zh_CN.GBK,zh_CN.GB18030,甚至可以设置为latin1编码。如果终端编码为utf8,那么mysql客户端所在机器的LC_ALL值必须设置为utf8编码,在这台测试机器上,可以设置为zh_CN.UTF-8或者en_US.UTF-8。
n 第二个问题
mysql客户端接收到了中文,会把中文回显到客户端,此时会把宽字节字符转换为多字节字符。
阅读libedit的源码,发现以下代码(line200@chartype.c):
if (MB_CUR_MAX < ct_enc_width(c))
return -1;
l = wcrtomb(dst, c, state);
对以上代码解释如下:
wcrtomb函数把宽字节字符转换为多字节字符。
c是即将回显的宽字节字符,ct_enc_width(c)函数返回该字符所占用的字节数。
MB_CUR_MAX是当前系统的LC_CTYPE值的编码最大长度。
上文提及宽字节字符占四个字节,而zh_CN.GB2312,zh_CN.GBK,zh_CN.GB18030的MB_CUR_MAX值分别是2,2,4。
如果终端编码为gbk,mysql客户端所在机器的LC_ALL值为zh_CN.GB2312,zh_CN.GBK,即MB_CUR_MAX < ct_enc_width(c)为true,此时系统执行不到wcrtomb函数就退出了,回显的字符串为空,从而发生本文开头提到的第二个现象啦。
如果终端编码和mysql客户端所在机器的编码都是utf8,由于utf-8的MB_CUR_MAX值为6,就不会出现这个问题了。
所以如果终端编码为gbk,mysql客户端所在机器的LC_ALL必须设置为zh_CN.GB18030。
题外话,这点貌似是libedit库的bug,其实不是这样的,本来unicode覆盖了世界上所有的字符,已经解决问题了,解决中国要自己搞一套,这就是gb2312,微软也推出了一个字符集,这就是gbk。Libedit对于unicode是完美的,不会出现这些问题。
n 总结
综上所述,无论mysql客户端使用libedit还是readline处理输入输出,终端和mysql客户端所在机器的编码都必须对应,例如:
1) 终端和mysql客户端所在的机器都采用utf8编码。
2) 终端使用gbk编码,mysql客户端所在的机器采用GB18030编码。