字符集轉(zhuǎn)換概述
我們有必要說(shuō)明一下, 字符 其實(shí)是面向人類的一個(gè)概念,計(jì)算機(jī)可并不關(guān)心字符是什么,它只關(guān)心這個(gè)字符對(duì)應(yīng)的字節(jié)編碼是什么。對(duì)于一個(gè)字節(jié)序列,計(jì)算機(jī)怎么知道它是使用什么字符集編碼的呢?計(jì)算機(jī)不知道,所以其實(shí)在計(jì)算機(jī)中表示一個(gè)字符串時(shí),都需要附帶上它對(duì)應(yīng)的字符集是什么,就像這樣(以C++語(yǔ)言為例):
class String {
byte* content;
CHARSET_INFO* charset;
}
比方說(shuō)我們現(xiàn)在有一個(gè)以 utf8 字符集編碼的漢字 '我' ,那么意味著計(jì)算機(jī)中不僅僅要存儲(chǔ) '我' 的utf8編碼 0xE68891 ,還需要存儲(chǔ)它是使用什么字符集編碼的信息,就像這樣:
{
content: 0xE68891;
charset: utf8;
}
計(jì)算機(jī)內(nèi)部包含將一種字符集轉(zhuǎn)換成另一種字符集的函數(shù)庫(kù),也就是某個(gè)字符在某種字符集下的編碼可以很順利的轉(zhuǎn)換為另一種字符集的編碼,我們將這個(gè)過(guò)程稱之為 字符集轉(zhuǎn)換 。比方說(shuō)我們可以將上述采用utf8字符集編碼的字符'我',轉(zhuǎn)換成gbk字符集編碼的形式,就變成了這樣:
{
content: 0xCED2;
charset: gbk;
}
小貼士:我們上邊所說(shuō)的'編碼'可以當(dāng)作動(dòng)詞,也可以當(dāng)作名詞來(lái)理解。當(dāng)作動(dòng)詞的話意味著將一個(gè)字符映射到一個(gè)字節(jié)序列的過(guò)程,當(dāng)作名詞的話意味著一個(gè)字符對(duì)應(yīng)的字節(jié)序列。大家根據(jù)上下文理解'編碼'的含義。
MySQL客戶端和服務(wù)器是怎么通信的
MySQL客戶端發(fā)送給服務(wù)器的請(qǐng)求以及服務(wù)器發(fā)送給客戶端的響應(yīng)其實(shí)都是遵從一定格式的,我們把它們通信過(guò)程中事先規(guī)定好的數(shù)據(jù)格式稱之為MySQL通信協(xié)議,這個(gè)協(xié)議是公開(kāi)的,我們可以簡(jiǎn)單的使用wireshark等截包軟件十分方便的分析這個(gè)通信協(xié)議。在了解了這個(gè)通信協(xié)議之后,我們甚至可以動(dòng)手制作自己的客戶端軟件。市面上的MySQL客戶端軟件多種多樣,我們并不想各個(gè)都分析一下,現(xiàn)在只選取在MySQL安裝目錄的 bin 目錄下自帶的 mysql 程序(此處的 mysql 程序指的是名字叫做 mysql 的一個(gè)可執(zhí)行文件),如圖所示:
我們?cè)谟?jì)算機(jī)的黑框框中執(zhí)行該可執(zhí)行文件,就相當(dāng)于啟動(dòng)了一個(gè)客戶端,就像這樣:
小貼士:我們這里的'黑框框'指的是windows操作系統(tǒng)中的cmd.exe或者UNIX系統(tǒng)中的Shell。
我們通常是按照下述步驟使用MySQL的:
- 啟動(dòng)客戶端并連接到服務(wù)器
- 客戶端發(fā)送請(qǐng)求。
- 服務(wù)器接收到請(qǐng)求
- 服務(wù)器處理請(qǐng)求
- 服務(wù)器處理請(qǐng)求完畢生成對(duì)該客戶端的響應(yīng)
- 客戶端接收到響應(yīng)
下邊我們就詳細(xì)分析一下每個(gè)步驟中都影響到了哪些字符集。
啟動(dòng)客戶端并連接到服務(wù)器過(guò)程
每個(gè)MySQL客戶端都維護(hù)者一個(gè)客戶端默認(rèn)字符集,這個(gè)默認(rèn)字符集按照下邊的套路進(jìn)行取值:
- 自動(dòng)檢測(cè)操作系統(tǒng)使用的字符集MySQL客戶端會(huì)在啟動(dòng)時(shí)檢測(cè)操作系統(tǒng)當(dāng)前使用的字符集,并按照一定規(guī)則映射成為MySQL支持的一些字符集(通常是操作系統(tǒng)當(dāng)前使用什么字符集,就映射為什么字符集,有一些特殊情況,比方說(shuō)如果操作系統(tǒng)當(dāng)前使用的是ascii字符集,會(huì)被映射為latin1字符集)。
- 當(dāng)我們使用UNIX操作系統(tǒng)時(shí)此時(shí)會(huì)調(diào)用操作系統(tǒng)提供的 nl_langinfo(CODESET) 函數(shù)來(lái)獲取操作系統(tǒng)當(dāng)前正在使用的字符集,而這個(gè)函數(shù)的結(jié)果是依賴 LC_ALL 、 LC_CTYPE 、 LANG 這三個(gè)環(huán)境變量的。其中 LC_ALL 的優(yōu)先級(jí)比 LC_CTYPE 高, LC_CTYPE 的優(yōu)先級(jí)比 LANG 高。也就是說(shuō)如果設(shè)置了 LC_ALL ,不論有沒(méi)有設(shè)置 LC_CTYPE 或者 LANG ,最終都以 LC_ALL 為準(zhǔn);如果沒(méi)有設(shè)置 LC_ALL ,那么就以 LC_CTYPE 為準(zhǔn);如果既沒(méi)有設(shè)置 LC_ALL 也沒(méi)有設(shè)置 LC_CTYPE ,就以 LANG 為準(zhǔn)。比方說(shuō)我們將環(huán)境變量 LC_ALL 設(shè)置為 zh_CN.UTF-8 ,就像這樣:export LC_ALL=zh_CN.UTF-8 那么我們?cè)诤诳蚩蚶飭?dòng)MySQL客戶端時(shí),MySQL客戶端就會(huì)檢測(cè)到這個(gè)操作系統(tǒng)使用的是 utf8 字符集,并將客戶端默認(rèn)字符集設(shè)置為 utf8 。當(dāng)然,如果這三個(gè)環(huán)境變量都沒(méi)有設(shè)置,那么 nl_langinfo(CODESET) 函數(shù)將返回操作系統(tǒng)默認(rèn)的字符集,比方說(shuō)在我的 macOS 10.15.3 操作系統(tǒng)中,該默認(rèn)字符集為:US-ASCII 此時(shí)MySQL客戶端的默認(rèn)字符集將會(huì)被設(shè)置為 latin1 。另外,我們這里還需要強(qiáng)調(diào)一下,我們使用的黑框框展示字符的時(shí)候有一個(gè)自己特有的字符集,比如在我的mac上使用 iTerm2 作為黑框框,我們可以打開(kāi):Preferences->Profiles->Terminal選項(xiàng)卡,可以看到 iTerm2 使用 utf8 來(lái)展示字符:我們一般要把黑框框展示字符時(shí)采用的編碼和操作系統(tǒng)當(dāng)前使用的編碼保持一致,如果不一致的話,我們敲擊的字符可能都無(wú)法顯示到屏幕上。比方說(shuō)如果我此時(shí)把 LC_ALL 屬性設(shè)置成 GBK ,那么我們?cè)傧蚝诳蚩蛏陷斎霛h字的話,屏幕都不會(huì)顯示了,就像這樣(如下圖所示,我敲擊了漢字 '我' 的效果):
- 當(dāng)我們使用Windows操作系統(tǒng)時(shí)此時(shí)會(huì)調(diào)用操作系統(tǒng)提供的 GetConsoleCP 函數(shù)來(lái)獲取操作系統(tǒng)當(dāng)前正在使用的字符集。在Windows里,會(huì)把當(dāng)前cmd.exe使用的字符集映射到一個(gè)數(shù)字,稱之為代碼頁(yè)(英文名: code page ),我們可以通過(guò)右鍵點(diǎn)擊 cmd.exe 標(biāo)題欄,然后點(diǎn)擊屬性->選項(xiàng),如下圖所示, 當(dāng)前代碼頁(yè) 的值是936,代表當(dāng)前cmd.exe使用gbk字符集:更簡(jiǎn)便一點(diǎn),我們可以運(yùn)行 chcp 命令直接看到當(dāng)前code page是什么:這樣我們?cè)诤诳蚩蚶飭?dòng)MySQL客戶端時(shí),MySQL客戶端就會(huì)檢測(cè)到這個(gè)操作系統(tǒng)使用的是 gbk 字符集,并將客戶端默認(rèn)字符集設(shè)置為 gbk 。我們前邊提到的utf8字符集對(duì)應(yīng)的代碼頁(yè)為 65001 ,如果當(dāng)前代碼頁(yè)的值為65001,之后再啟動(dòng)MySQL客戶端,那么客戶端的默認(rèn)字符集就會(huì)變成 utf8 。
- 如果MySQL不支持自動(dòng)檢測(cè)到的操作系統(tǒng)當(dāng)前正在使用的字符集,或者在某些情況下不允許自動(dòng)檢測(cè)的話,MySQL會(huì)使用它自己的內(nèi)建的默認(rèn)字符集作為客戶端默認(rèn)字符集。這個(gè)內(nèi)建的默認(rèn)字符集在 MySQL 5.7 以及之前的版本中是 latin1 ,在 MySQL 8.0 中修改為了 utf8mb4 。
- 使用了 default-character-set 啟動(dòng)參數(shù)如果我們?cè)趩?dòng)MySQL客戶端是使用了 default-character-set 啟動(dòng)參數(shù),那么客戶端的默認(rèn)字符集將不再檢測(cè)操作系統(tǒng)當(dāng)前正在使用的字符集,而是直接使用啟動(dòng)參數(shù) default-character-set 所指定的值。比方說(shuō)我們使用如下命令來(lái)啟動(dòng)客戶端:mysql --default-character-set=utf8 那么不論我們使用什么操作系統(tǒng),操作系統(tǒng)目前使用的字符集是什么,我們都將會(huì)以u(píng)tf8作為MySQL客戶端的默認(rèn)字符集。
在確認(rèn)了MySQL客戶端默認(rèn)字符集之后,客戶端就會(huì)向服務(wù)器發(fā)起登陸請(qǐng)求,傳輸一些諸如用戶名、密碼等信息,在這個(gè)請(qǐng)求里就會(huì)包含客戶端使用的默認(rèn)字符集是什么的信息,服務(wù)器收到后就明白了稍后客戶端即將發(fā)送過(guò)來(lái)的請(qǐng)求是采用什么字符集編碼的,自己生成的響應(yīng)應(yīng)該以什么字符集編碼了(劇透一下:其實(shí)服務(wù)器在明白了客戶端使用的默認(rèn)字符集之后,就會(huì)將 character_set_client 、 character_set_connection 以及 character_set_result 這幾個(gè)系統(tǒng)變量均設(shè)置為該值)。
客戶端發(fā)送請(qǐng)求
登陸成功之后,我們就可以使用鍵盤在黑框框中鍵入我們想要輸入的MySQL語(yǔ)句,輸入完了之后就可以點(diǎn)擊回車鍵將該語(yǔ)句當(dāng)作請(qǐng)求發(fā)送到服務(wù)器,可是客戶端發(fā)送的語(yǔ)句(本質(zhì)是個(gè)字符串)到底是采用什么字符集編碼的呢?這其實(shí)涉及到應(yīng)用程序和操作系統(tǒng)之間的交互,我們的MySQL客戶端程序其實(shí)是一個(gè)應(yīng)用程序,它從黑框框中讀取數(shù)據(jù)其實(shí)是要調(diào)用操作系統(tǒng)提供的讀取接口。在不同的操作系統(tǒng)中,調(diào)用的讀取接口其實(shí)是不同的,我們還得分情況討論一下:
- 對(duì)于UNIX操作系統(tǒng)來(lái)說(shuō)在我們使用某個(gè)輸入法軟件向黑框框中輸入字符時(shí),該字符采用的編碼字符集其實(shí)是操作系統(tǒng)當(dāng)前使用的字符集。比方說(shuō)當(dāng)前 LC_ALL 環(huán)境變量的值為 zh_CN.UTF-8 ,那么意味著黑框框中的字符其實(shí)是使用utf8字符集進(jìn)行編碼。稍后MySQL客戶端程序?qū)⒄{(diào)用操作系統(tǒng)提供的read函數(shù)從黑框框中讀取數(shù)據(jù)(其實(shí)就是所謂的從標(biāo)準(zhǔn)輸入流中讀取數(shù)據(jù)),所讀取的數(shù)據(jù)其實(shí)就是采用utf8字符集進(jìn)行編碼的字節(jié)序列,稍后將該字節(jié)序列作為請(qǐng)求內(nèi)容發(fā)送到服務(wù)器。這樣其實(shí)會(huì)產(chǎn)生一個(gè)問(wèn)題,如果客戶端的默認(rèn)字符集和操作系統(tǒng)當(dāng)前正在使用的字符集不同,那么將產(chǎn)生比較尷尬的結(jié)果。比方說(shuō)我們?cè)趩?dòng)客戶端是攜帶了 --default-character-set=gbk 的啟動(dòng)參數(shù),那么客戶端的默認(rèn)字符集將會(huì)被設(shè)置成gbk,而如果操作系統(tǒng)此時(shí)采用的字符集是utf8。比方說(shuō)我們的語(yǔ)句中包含漢字 '我' ,那么客戶端調(diào)用 read 函數(shù)讀到的字節(jié)序列其實(shí)是 0xE68891 ,從而將 0xE68891 發(fā)送到服務(wù)器,而服務(wù)器認(rèn)為客戶端發(fā)送過(guò)來(lái)的請(qǐng)求都是采用gbk進(jìn)行編碼的,這樣就會(huì)產(chǎn)生問(wèn)題(當(dāng)然,這僅僅是發(fā)生亂碼問(wèn)題的前奏,并不意味著產(chǎn)生亂碼,亂碼只有在最后一步,也就是客戶端應(yīng)用程序?qū)⒎?wù)器返回的數(shù)據(jù)寫到黑框框里時(shí)才會(huì)發(fā)生)。
- 對(duì)于Windows操作系統(tǒng)來(lái)說(shuō)在Windows操作系統(tǒng)中,從黑框框中讀取數(shù)據(jù)調(diào)用的是Windows提供的 ReadConsoleW 函數(shù)。在該函數(shù)執(zhí)行后,MySQL客戶端會(huì)得到一個(gè)寬字符數(shù)組(其實(shí)就是一組16位的UNICODE),然后客戶端需要把該寬字符數(shù)組再次轉(zhuǎn)換成客戶端使用的默認(rèn)字符集編碼的字節(jié)序列,然后才將該字節(jié)序列作為請(qǐng)求的內(nèi)容發(fā)送到服務(wù)器。這樣在UNIX操作系統(tǒng)中可能產(chǎn)生的問(wèn)題,在Windows系統(tǒng)中卻可以避免。比方說(shuō)我們?cè)趩?dòng)客戶端是攜帶了 --default-character-set=gbk 的啟動(dòng)參數(shù),那么客戶端的默認(rèn)字符集將會(huì)被設(shè)置成gbk,假如此時(shí)操作系統(tǒng)采用的字符集是utf8。比方說(shuō)我們的語(yǔ)句中包含漢字 '我' ,那么客戶端調(diào)用 ReadConsoleW 函數(shù)先讀到一個(gè)代表著 我 字的寬字符數(shù)組,之后又將其轉(zhuǎn)換為客戶端的默認(rèn)字符集,也就是gbk字符集編碼的數(shù)據(jù) 0xCED2 ,然后將 0xCED2 發(fā)送到服務(wù)器。此時(shí)服務(wù)器也認(rèn)為客戶端發(fā)送過(guò)來(lái)的請(qǐng)求就是采用gbk進(jìn)行編碼的,這樣就完全正確了~
服務(wù)器接收請(qǐng)求
服務(wù)器接收到到的請(qǐng)求本質(zhì)上就是一個(gè)字節(jié)序列,服務(wù)器將其看作是采用系統(tǒng)變量 character_set_client 代表的字符集進(jìn)行編碼的字節(jié)序列。 character_set_client 是一個(gè)SESSION級(jí)別的系統(tǒng)變量,也就是說(shuō)每個(gè)客戶端和服務(wù)器建立連接后,服務(wù)器都會(huì)為該客戶端維護(hù)一個(gè)單獨(dú)的 character_set_client 變量,每個(gè)客戶端在登錄服務(wù)器的時(shí)候都會(huì)將客戶端的默認(rèn)字符集通知給服務(wù)器,然后服務(wù)器設(shè)置該客戶端專屬的 character_set_client 。
我們可以使用SET命令單獨(dú)修改 character_set_client 對(duì)應(yīng)的值,就像這樣:
SET character_set_client=gbk;
需要注意的是, character_set_client 對(duì)應(yīng)的字符集一定要包含請(qǐng)求中的字符,比方說(shuō)我們把 character_set_client 設(shè)置成 ascii ,而請(qǐng)求中發(fā)送了一個(gè)漢字 '我' ,將會(huì)發(fā)生這樣的事情:
mysql> SET character_set_client=ascii;
Query OK, 0 rows affected (0.00 sec)
mysql> SHOW VARIABLES LIKE 'character%';
+--------------------------+------------------------------------------------------+
| Variable_name | Value |
+--------------------------+------------------------------------------------------+
| character_set_client | ascii |
| character_set_connection | utf8 |
| character_set_database | utf8 |
| character_set_filesystem | binary |
| character_set_results | utf8 |
| character_set_server | utf8 |
| character_set_system | utf8 |
| character_sets_dir | /usr/local/Cellar/mysql/5.7.21/share/mysql/charsets/ |
+--------------------------+------------------------------------------------------+
8 rows in set (0.00 sec)
mysql> SELECT '我';
+-----+
| ??? |
+-----+
| ??? |
+-----+
1 row in set, 1 warning (0.00 sec)
mysql> SHOW WARNINGSG
*************************** 1. row ***************************
Level: Warning
Code: 1300
Message: Invalid ascii character string: '\xE6\x88\x91'
1 row in set (0.00 sec)
如圖所示,最后提示了 'E6、88、91' 并不是正確的ascii字符。
小貼士:可以將character_set_client設(shè)置為latin1,看看還會(huì)不會(huì)報(bào)告WARNINGS,以及為什么~
服務(wù)器處理請(qǐng)求
服務(wù)器在處理請(qǐng)求時(shí)會(huì)將請(qǐng)求中的字符再次轉(zhuǎn)換為一種特定的字符集,該字符集由系統(tǒng)變量 character_set_connection 表示,該系統(tǒng)變量也是SESSION級(jí)別的。每個(gè)客戶端在登錄服務(wù)器的時(shí)候都會(huì)將客戶端的默認(rèn)字符集通知給服務(wù)器,然后服務(wù)器設(shè)置該客戶端專屬的 character_set_connection 。
不過(guò)我們之后可以通過(guò)SET命令單獨(dú)修改這個(gè) character_set_connection 系統(tǒng)變量。比方說(shuō)客戶端發(fā)送給服務(wù)器的請(qǐng)求中包含字節(jié)序列 0xE68891 ,然后服務(wù)器針對(duì)該客戶端的系統(tǒng)變量 character_set_client 為 utf8 ,那么此時(shí)服務(wù)器就知道該字節(jié)序列其實(shí)是代表漢字 '我' ,如果此時(shí)服務(wù)器針對(duì)該客戶端的系統(tǒng)變量 character_set_connection 為gbk,那么在計(jì)算機(jī)內(nèi)部還需要將該字符轉(zhuǎn)換為采用gbk字符集編碼的形式,也就是 0xCED2 。
有同學(xué)可能會(huì)想這一步有點(diǎn)兒像脫了褲子放屁的意思,但是大家請(qǐng)考慮下邊這個(gè)查詢語(yǔ)句:
mysql> SELECT 'a' = 'A';
請(qǐng)問(wèn)大家這個(gè)查詢語(yǔ)句的返回結(jié)果應(yīng)該是TRUE還是FALSE?其實(shí)結(jié)果是不確定。這是因?yàn)槲覀儾⒉恢辣容^兩個(gè)字符串的大小到底比的是什么!我們應(yīng)該從兩個(gè)方面考慮:
- 考慮一:這些字符串是采用什么字符集進(jìn)行編碼的呢?
- 考慮二:在我們確定了編碼這些字符串的字符集之后,也就意味著每個(gè)字符串都會(huì)映射到一個(gè)字節(jié)序列,那么我們?cè)趺幢容^這些字節(jié)序列呢,是直接比較它們二進(jìn)制的大小,還是有別的什么比較方式?比方說(shuō) 'a' 和 'A' 在utf8字符集下的編碼分別為 0x61 和 0x41 ,那么 'a' = 'A' 是應(yīng)該直接比較 0x61 和 0x41 的大小呢,還是將 0x61 減去32之后再比較大小呢?其實(shí)這兩種比較方式都可以,每一種比較方式我們都稱作一種 比較規(guī)則 (英文名: collation )。
MySQL 中支持若干種字符集,我們可以使用 SHOW CHARSET 命令查看,如下圖所示(太多了,只展示幾種,具體自己運(yùn)行一下該命令):
mysql> SHOW CHARSET;
+----------+---------------------------------+---------------------+--------+
| Charset | Description | Default collation | Maxlen |
+----------+---------------------------------+---------------------+--------+
| big5 | Big5 Traditional Chinese | big5_chinese_ci | 2 |
| latin1 | cp1252 West European | latin1_swedish_ci | 1 |
| latin2 | ISO 8859-2 Central European | latin2_general_ci | 1 |
| ascii | US ASCII | ascii_general_ci | 1 |
| gb2312 | GB2312 Simplified Chinese | gb2312_chinese_ci | 2 |
| gbk | GBK Simplified Chinese | gbk_chinese_ci | 2 |
| utf8 | UTF-8 Unicode | utf8_general_ci | 3 |
| utf8mb4 | UTF-8 Unicode | utf8mb4_general_ci | 4 |
| utf16 | UTF-16 Unicode | utf16_general_ci | 4 |
| utf16le | UTF-16LE Unicode | utf16le_general_ci | 4 |
| utf32 | UTF-32 Unicode | utf32_general_ci | 4 |
| binary | Binary pseudo charset | binary | 1 |
| gb18030 | China National Standard GB18030 | gb18030_chinese_ci | 4 |
+----------+---------------------------------+---------------------+--------+
41 rows in set (0.04 sec)
其中每一種字符集又對(duì)應(yīng)著若干種比較規(guī)則,我們以u(píng)tf8字符集為例(太多了,也只展示幾個(gè)):
mysql> SHOW COLLATION WHERE Charset='utf8';
+--------------------------+---------+-----+---------+----------+---------+
| Collation | Charset | Id | Default | Compiled | Sortlen |
+--------------------------+---------+-----+---------+----------+---------+
| utf8_general_ci | utf8 | 33 | Yes | Yes | 1 |
| utf8_bin | utf8 | 83 | | Yes | 1 |
| utf8_unicode_ci | utf8 | 192 | | Yes | 8 |
| utf8_icelandic_ci | utf8 | 193 | | Yes | 8 |
| utf8_latvian_ci | utf8 | 194 | | Yes | 8 |
| utf8_romanian_ci | utf8 | 195 | | Yes | 8 |
+--------------------------+---------+-----+---------+----------+---------+
27 rows in set (0.00 sec)
其中 utf8_general_ci 是utf8字符集默認(rèn)的比較規(guī)則,在這種比較規(guī)則下是不區(qū)分大小寫的,不過(guò) utf8_bin 這種比較規(guī)則就是區(qū)分大小寫的。
在我們將請(qǐng)求中的字節(jié)序列轉(zhuǎn)換為 character_set_connection 對(duì)應(yīng)的字符集編碼的字節(jié)序列后,也要配套一個(gè)對(duì)應(yīng)的比較規(guī)則,這個(gè)比較規(guī)則就由 collation_connection 系統(tǒng)變量來(lái)指定。我們現(xiàn)在通過(guò)SET命令來(lái)修改一下和 collation_connection 的值分別設(shè)置為 utf8 和 utf8_general_ci ,然后比較一下 'a' 和 'A' :
mysql> SET character_set_connection=utf8;
Query OK, 0 rows affected (0.00 sec)
mysql> SET collation_connection=utf8_general_ci;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT 'a' = 'A';
+-----------+
| 'a' = 'A' |
+-----------+
| 1 |
+-----------+
1 row in set (0.00 sec)
可以看到在這種情況下這兩個(gè)字符串就是相等的。
我們現(xiàn)在通過(guò)SET命令來(lái)修改一下和 collation_connection 的值分別設(shè)置為 utf8 和 utf8_bin ,然后比較一下 'a' 和 'A' :
mysql> SET character_set_connection=utf8;
Query OK, 0 rows affected (0.00 sec)
mysql> SET collation_connection=utf8_bin;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT 'a' = 'A';
+-----------+
| 'a' = 'A' |
+-----------+
| 0 |
+-----------+
1 row in set (0.00 sec)
可以看到在這種情況下這兩個(gè)字符串就是不相等的。
當(dāng)然,如果我們并不需要單獨(dú)指定將請(qǐng)求中的字符串采用何種字符集以及比較規(guī)則的話,并不用太關(guān)心 character_set_connection 和 collation_connection 設(shè)置成啥,不過(guò)需要注意一點(diǎn),就是 character_set_connection 對(duì)應(yīng)的字符集必須包含請(qǐng)求中的字符。
服務(wù)器處理請(qǐng)求完畢生成對(duì)該客戶端的響應(yīng)
為了故事的順利發(fā)展,我們先創(chuàng)建一個(gè)表:
CREATE TABLE t (
c VARCHAR(100)
) ENGINE=INNODB CHARSET=utf8;
然后向這個(gè)表插入一條記錄:
INSERT INTO t VALUE('我');
現(xiàn)在這個(gè)表中的數(shù)據(jù)就如下所示:
mysql> SELECT * FROM t;
+------+
| c |
+------+
| 我 |
+------+
1 row in set (0.00 sec)
我們可以看到該表中的字段其實(shí)是使用 utf8 字符集編碼的,所以底層存放格式是: 0xE68891 ,將它讀出后需要發(fā)送到客戶端,是不是直接將 0xE68891 發(fā)送到客戶端呢?這可不一定,這個(gè)取決于 character_set_result 系統(tǒng)變量的值,該系統(tǒng)變量也是一個(gè)SESSION級(jí)別的變量。服務(wù)器會(huì)將該響應(yīng)轉(zhuǎn)換為 character_set_result 系統(tǒng)變量對(duì)應(yīng)的字符集編碼后的字節(jié)序列發(fā)送給客戶端。每個(gè)客戶端在登錄服務(wù)器的時(shí)候都會(huì)將客戶端的默認(rèn)字符集通知給服務(wù)器,然后服務(wù)器設(shè)置該客戶端專屬的 character_set_result 。
我們也可以使用SET命令來(lái)設(shè)置 character_set_result 的值。不過(guò)也需要注意, character_set_result 對(duì)應(yīng)的字符集應(yīng)該包含響應(yīng)中的字符。
這里再?gòu)?qiáng)調(diào)一遍, character_set_client 、 character_set_connection 和 character_set_result 這三個(gè)系統(tǒng)變量是服務(wù)器的系統(tǒng)變量,每個(gè)客戶端在與服務(wù)器建立連接后,服務(wù)器都會(huì)為這個(gè)連接維護(hù)這三個(gè)變量,如圖所示(我們假設(shè)連接1的這三個(gè)變量均為 utf8 ,連接1的這三個(gè)變量均為 gbk ,連接1的這三個(gè)變量均為 ascii ,):
一般情況下 character_set_client 、 character_set_connection 和 character_set_result 這三個(gè)系統(tǒng)變量應(yīng)該和客戶端的默認(rèn)字符集相同, SET names 命令可以一次性修改這三個(gè)系統(tǒng)變量:
SET NAMES 'charset_name'
該語(yǔ)句和下邊三個(gè)語(yǔ)句等效:
SET character_set_client = charset_name;
SET character_set_results = charset_name;
SET character_set_connection = charset_name;
不過(guò)這里需要大家特別注意, SET names 語(yǔ)句并不會(huì)改變客戶端的默認(rèn)字符集!
客戶端接收到響應(yīng)
客戶端收到的響應(yīng)其實(shí)仍然是一個(gè)字節(jié)序列。客戶端是如何將這個(gè)字節(jié)序列寫到黑框框中的呢,這又涉及到應(yīng)用程序和操作系統(tǒng)之間的一次交互。
- 對(duì)于UNIX操作系統(tǒng)來(lái)說(shuō),MySQL客戶端向黑框框中寫入數(shù)據(jù)使用的是操作系統(tǒng)提供的 fputs 、 putc 或者 fwrite 函數(shù),這些函數(shù)基本上相當(dāng)于直接就把接收到的字節(jié)序列寫到了黑框框中(請(qǐng)注意我們用詞: '基本上相當(dāng)于' ,其實(shí)內(nèi)部還會(huì)做一些工作,但是我們這里就不想再關(guān)注這些細(xì)節(jié)了)。此時(shí)如果該字節(jié)序列實(shí)際的字符集和黑框框展示字符所使用的字符集不一致的話,就會(huì)發(fā)生所謂的亂碼(大家注意,這個(gè)時(shí)候和操作系統(tǒng)當(dāng)前使用的字符集沒(méi)啥關(guān)系)。比方說(shuō)我們?cè)趩?dòng)MySQL客戶端的時(shí)候使用了 --default-character-set=gbk 的啟動(dòng)參數(shù),那么服務(wù)器的 character_set_result 變量就是gbk。然后再執(zhí)行 SELECT * FROM t 語(yǔ)句,那么服務(wù)器就會(huì)將字符 '我' 的gbk編碼,也就是 0xCDE2 發(fā)送到客戶端,客戶端直接把這個(gè)字節(jié)序列寫到黑框框中,如果黑框框此時(shí)采用utf8字符集展示字符,那自然就會(huì)發(fā)生亂碼。
- 對(duì)于Windows操作系統(tǒng)來(lái)說(shuō),MySQL客戶端向黑框框中寫入數(shù)據(jù)使用的是操作系統(tǒng)提供的 WriteConsoleW 函數(shù),該函數(shù)接收一個(gè)寬字符數(shù)組,所以MySQL客戶端調(diào)用它的時(shí)候需要顯式地將它從服務(wù)器收到的字節(jié)序列按照客戶端默認(rèn)的字符集轉(zhuǎn)換成一個(gè)寬字符數(shù)組。正因?yàn)檫@一步驟的存在,所以可以避免上邊提到的一個(gè)問(wèn)題。比方說(shuō)我們?cè)趩?dòng)MySQL客戶端的時(shí)候使用了 --default-character-set=gbk 的啟動(dòng)參數(shù),那么服務(wù)器的 character_set_result 變量就是gbk。然后再執(zhí)行 SELECT * FROM t 語(yǔ)句,那么服務(wù)器就會(huì)將字符 '我' 的gbk編碼,也就是 0xCDE2 發(fā)送到客戶端,客戶端將這個(gè)字節(jié)序列先從客戶端默認(rèn)字符集,也就是gbk的編碼轉(zhuǎn)換成一個(gè)寬字符數(shù)組,然后再調(diào)用 WriteConsoleW 函數(shù)寫到黑框框,黑框框自然可以把它顯示出來(lái)。
亂碼問(wèn)題應(yīng)該如何分析
好了,介紹了各個(gè)步驟中涉及到的各種字符集,大家估計(jì)也看的眼花繚亂了,下邊總結(jié)一下我們遇到亂碼的時(shí)候應(yīng)該如何分析,而不是胡子眉毛一把抓,隨便百度一篇文章,然后修改某個(gè)參數(shù),運(yùn)氣好修改了之后改對(duì)了,運(yùn)氣不好改了一天也改不好。知其然也要知其所以然,在學(xué)習(xí)了本篇文章后,大家一定要有節(jié)奏的去分析亂碼問(wèn)題:
- 我使用的是什么操作系統(tǒng)
- 對(duì)于UNIX系統(tǒng)用戶來(lái)說(shuō),要搞清楚我使用的黑框框到底是使用什么字符集展示字符,就像是 iTerm2 中的 character encoding 屬性:同樣還要搞清楚操作系統(tǒng)當(dāng)前使用什么字符集,運(yùn)行 locale 命令查看:王大爺喊你輸入呢,跟這兒>locale LANG="" LC_COLLATE="zh_CN.UTF-8" LC_CTYPE="zh_CN.UTF-8" LC_MESSAGES="zh_CN.UTF-8" LC_MONETARY="zh_CN.UTF-8" LC_NUMERIC="zh_CN.UTF-8" LC_TIME="zh_CN.UTF-8" LC_ALL="zh_CN.UTF-8" 王大爺喊你輸入呢,跟這兒>沒(méi)有什么特別極端的特殊需求的話,一定要保證上述兩個(gè)字符集是相同的,否則可能連漢字都輸入不進(jìn)去!
- 對(duì)于Windows用戶來(lái)說(shuō)搞清楚自己使用的黑框框的代碼頁(yè)是什么,也就是操作系統(tǒng)當(dāng)前使用的字符集是什么。
- 搞清楚客戶端的默認(rèn)字符集是什么啟動(dòng)MySQL客戶端的時(shí)候有沒(méi)有攜帶 --default-character-set 參數(shù),如果攜帶了,那么客戶端默認(rèn)字符集就以該參數(shù)指定的值為準(zhǔn)。否則分析自己操作系統(tǒng)當(dāng)前使用的字符集是什么。
- 搞清楚客戶端發(fā)送請(qǐng)求時(shí)是以什么字符集編碼請(qǐng)求的
- 對(duì)于UNIX系統(tǒng)來(lái)說(shuō),我們可以認(rèn)為請(qǐng)求就是采用操作系統(tǒng)當(dāng)前使用的字符集進(jìn)行編碼的。
- 對(duì)于Windows系統(tǒng)來(lái)說(shuō),我們可以認(rèn)為請(qǐng)求就是采用客戶端默認(rèn)字符集進(jìn)行編碼的。
- 通過(guò)執(zhí)行 SHOW VARIABLES LIKE 'character%' 命令搞清楚:
- character_set_client :服務(wù)器是怎樣認(rèn)為客戶端發(fā)送過(guò)來(lái)的請(qǐng)求是采用何種字符集編碼的
- character_set_connection :服務(wù)器在運(yùn)行過(guò)程中會(huì)采用何種字符集編碼請(qǐng)求中的字符
- character_set_result :服務(wù)器會(huì)將響應(yīng)使用何種字符集編碼后再發(fā)送給客戶端的
- 客戶端收到響應(yīng)之后:對(duì)于服務(wù)器發(fā)送過(guò)來(lái)的字節(jié)序列來(lái)說(shuō):
- 在UNIX操作系統(tǒng)上,可以認(rèn)為會(huì)把該字節(jié)序列直接寫到黑框框里。此時(shí)應(yīng)該搞清楚我們的黑框框到底是采用何種字符集展示數(shù)據(jù)。
- 在Windows操作系統(tǒng)上,該字節(jié)序列會(huì)被認(rèn)為是由客戶端字符集編碼的數(shù)據(jù),然后再轉(zhuǎn)換成寬字符數(shù)組寫入到黑框框中。