字符集轉換概述
我們有必要說明一下, 字符 其實是面向人類的一個概念,計算機可并不關心字符是什麼,它隻關心這個字符對應的字節編碼是什麼。對于一個字節序列,計算機怎麼知道它是使用什麼字符集編碼的呢?計算機不知道,所以其實在計算機中表示一個字符串時,都需要附帶上它對應的字符集是什麼,就像這樣(以C 語言為例):
class String {
byte* content;
CHARSET_INFO* charset;
}
比方說我們現在有一個以 utf8 字符集編碼的漢字 '我' ,那麼意味着計算機中不僅僅要存儲 '我' 的utf8編碼 0xE68891 ,還需要存儲它是使用什麼字符集編碼的信息,就像這樣:
{
content: 0xE68891;
charset: utf8;
}
計算機内部包含将一種字符集轉換成另一種字符集的函數庫,也就是某個字符在某種字符集下的編碼可以很順利的轉換為另一種字符集的編碼,我們将這個過程稱之為 字符集轉換 。比方說我們可以将上述采用utf8字符集編碼的字符'我',轉換成gbk字符集編碼的形式,就變成了這樣:
{
content: 0xCED2;
charset: gbk;
}
小貼士:我們上邊所說的'編碼'可以當作動詞,也可以當作名詞來理解。當作動詞的話意味着将一個字符映射到一個字節序列的過程,當作名詞的話意味着一個字符對應的字節序列。大家根據上下文理解'編碼'的含義。
MySQL客戶端發送給服務器的請求以及服務器發送給客戶端的響應其實都是遵從一定格式的,我們把它們通信過程中事先規定好的數據格式稱之為MySQL通信協議,這個協議是公開的,我們可以簡單的使用wireshark等截包軟件十分方便的分析這個通信協議。在了解了這個通信協議之後,我們甚至可以動手制作自己的客戶端軟件。市面上的MySQL客戶端軟件多種多樣,我們并不想各個都分析一下,現在隻選取在MySQL安裝目錄的 bin 目錄下自帶的 mysql 程序(此處的 mysql 程序指的是名字叫做 mysql 的一個可執行文件),如圖所示:
我們在計算機的黑框框中執行該可執行文件,就相當于啟動了一個客戶端,就像這樣:
小貼士:我們這裡的'黑框框'指的是Windows操作系統中的cmd.exe或者UNIX系統中的Shell。
我們通常是按照下述步驟使用MySQL的:
下邊我們就詳細分析一下每個步驟中都影響到了哪些字符集。
每個MySQL客戶端都維護者一個客戶端默認字符集,這個默認字符集按照下邊的套路進行取值:
在确認了MySQL客戶端默認字符集之後,客戶端就會向服務器發起登陸請求,傳輸一些諸如用戶名、密碼等信息,在這個請求裡就會包含客戶端使用的默認字符集是什麼的信息,服務器收到後就明白了稍後客戶端即将發送過來的請求是采用什麼字符集編碼的,自己生成的響應應該以什麼字符集編碼了(劇透一下:其實服務器在明白了客戶端使用的默認字符集之後,就會将 character_set_client 、 character_set_connection 以及 character_set_result 這幾個系統變量均設置為該值)。
登陸成功之後,我們就可以使用鍵盤在黑框框中鍵入我們想要輸入的MySQL語句,輸入完了之後就可以點擊回車鍵将該語句當作請求發送到服務器,可是客戶端發送的語句(本質是個字符串)到底是采用什麼字符集編碼的呢?這其實涉及到應用程序和操作系統之間的交互,我們的MySQL客戶端程序其實是一個應用程序,它從黑框框中讀取數據其實是要調用操作系統提供的讀取接口。在不同的操作系統中,調用的讀取接口其實是不同的,我們還得分情況讨論一下:
服務器接收到到的請求本質上就是一個字節序列,服務器将其看作是采用系統變量 character_set_client 代表的字符集進行編碼的字節序列。 character_set_client 是一個SESSION級别的系統變量,也就是說每個客戶端和服務器建立連接後,服務器都會為該客戶端維護一個單獨的 character_set_client 變量,每個客戶端在登錄服務器的時候都會将客戶端的默認字符集通知給服務器,然後服務器設置該客戶端專屬的 character_set_client 。
我們可以使用SET命令單獨修改 character_set_client 對應的值,就像這樣:
SET character_set_client=gbk;
需要注意的是, character_set_client 對應的字符集一定要包含請求中的字符,比方說我們把 character_set_client 設置成 ascii ,而請求中發送了一個漢字 '我' ,将會發生這樣的事情:
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 WARNINGS\G
*************************** 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設置為latin1,看看還會不會報告WARNINGS,以及為什麼~
服務器在處理請求時會将請求中的字符再次轉換為一種特定的字符集,該字符集由系統變量 character_set_connection 表示,該系統變量也是SESSION級别的。每個客戶端在登錄服務器的時候都會将客戶端的默認字符集通知給服務器,然後服務器設置該客戶端專屬的 character_set_connection 。
不過我們之後可以通過SET命令單獨修改這個 character_set_connection 系統變量。比方說客戶端發送給服務器的請求中包含字節序列 0xE68891 ,然後服務器針對該客戶端的系統變量 character_set_client 為 utf8 ,那麼此時服務器就知道該字節序列其實是代表漢字 '我' ,如果此時服務器針對該客戶端的系統變量 character_set_connection 為gbk,那麼在計算機内部還需要将該字符轉換為采用gbk字符集編碼的形式,也就是 0xCED2 。
有同學可能會想這一步有點兒像脫了褲子放屁的意思,但是大家請考慮下邊這個查詢語句:
mysql> SELECT 'a' = 'A';
請問大家這個查詢語句的返回結果應該是TRUE還是FALSE?其實結果是不确定。這是因為我們并不知道比較兩個字符串的大小到底比的是什麼!我們應該從兩個方面考慮:
MySQL 中支持若幹種字符集,我們可以使用 SHOW CHARSET 命令查看,如下圖所示(太多了,隻展示幾種,具體自己運行一下該命令):
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)
其中每一種字符集又對應着若幹種比較規則,我們以utf8字符集為例(太多了,也隻展示幾個):
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字符集默認的比較規則,在這種比較規則下是不區分大小寫的,不過 utf8_bin 這種比較規則就是區分大小寫的。
在我們将請求中的字節序列轉換為 character_set_connection 對應的字符集編碼的字節序列後,也要配套一個對應的比較規則,這個比較規則就由 collation_connection 系統變量來指定。我們現在通過SET命令來修改一下和 collation_connection 的值分别設置為 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)
可以看到在這種情況下這兩個字符串就是相等的。
我們現在通過SET命令來修改一下和 collation_connection 的值分别設置為 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)
可以看到在這種情況下這兩個字符串就是不相等的。
當然,如果我們并不需要單獨指定将請求中的字符串采用何種字符集以及比較規則的話,并不用太關心 character_set_connection 和 collation_connection 設置成啥,不過需要注意一點,就是 character_set_connection 對應的字符集必須包含請求中的字符。
為了故事的順利發展,我們先創建一個表:
CREATE TABLE t (
c VARCHAR(100)
) ENGINE=INNODB CHARSET=utf8;
然後向這個表插入一條記錄:
INSERT INTO t VALUE('我');
現在這個表中的數據就如下所示:
mysql> SELECT * FROM t;
------
| c |
------
| 我 |
------
1 row in set (0.00 sec)
我們可以看到該表中的字段其實是使用 utf8 字符集編碼的,所以底層存放格式是: 0xE68891 ,将它讀出後需要發送到客戶端,是不是直接将 0xE68891 發送到客戶端呢?這可不一定,這個取決于 character_set_result 系統變量的值,該系統變量也是一個SESSION級别的變量。服務器會将該響應轉換為 character_set_result 系統變量對應的字符集編碼後的字節序列發送給客戶端。每個客戶端在登錄服務器的時候都會将客戶端的默認字符集通知給服務器,然後服務器設置該客戶端專屬的 character_set_result 。
我們也可以使用SET命令來設置 character_set_result 的值。不過也需要注意, character_set_result 對應的字符集應該包含響應中的字符。
這裡再強調一遍, character_set_client 、 character_set_connection 和 character_set_result 這三個系統變量是服務器的系統變量,每個客戶端在與服務器建立連接後,服務器都會為這個連接維護這三個變量,如圖所示(我們假設連接1的這三個變量均為 utf8 ,連接1的這三個變量均為 gbk ,連接1的這三個變量均為 ascii ,):
一般情況下 character_set_client 、 character_set_connection 和 character_set_result 這三個系統變量應該和客戶端的默認字符集相同, SET names 命令可以一次性修改這三個系統變量:
SET NAMES 'charset_name'
該語句和下邊三個語句等效:
SET character_set_client = charset_name;
SET character_set_results = charset_name;
SET character_set_connection = charset_name;
不過這裡需要大家特别注意, SET names 語句并不會改變客戶端的默認字符集!
客戶端收到的響應其實仍然是一個字節序列。客戶端是如何将這個字節序列寫到黑框框中的呢,這又涉及到應用程序和操作系統之間的一次交互。
好了,介紹了各個步驟中涉及到的各種字符集,大家估計也看的眼花缭亂了,下邊總結一下我們遇到亂碼的時候應該如何分析,而不是胡子眉毛一把抓,随便百度一篇文章,然後修改某個參數,運氣好修改了之後改對了,運氣不好改了一天也改不好。知其然也要知其所以然,在學習了本篇文章後,大家一定要有節奏的去分析亂碼問題:
更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!