PHP 手冊明確建議:優先使用 mysqli_set_charset() 而非執行 SET NAMES SQL 語句。許多開發者知其然不知其所以然。本文從 C 源碼層面深度剖析兩者本質差異,揭示 SET NAMES 隱藏的字符集同步缺陷與注入風險。
SET NAMES utf8mb4
-- 等價於以下三條語句:
SET character_set_client = utf8mb4;
SET character_set_connection = utf8mb4;
SET character_set_results = utf8mb4;
這三個會話變量分別控制:客戶端發送編碼、連接層轉換編碼、結果集返回編碼。看似完美,實則只修改了 MySQL 服務端狀態。
追踪 PHP 源碼 ext/mysqli/mysqli_nonapi.c → MySQL C API libmysql/client.c:
int mysql_set_character_set(MYSQL *mysql, const char *cs_name) {
// 1. 獲取字符集資訊結構
CHARSET_INFO *cs = get_charset_by_csname(cs_name, ...);
// 2. 執行 SET NAMES
char buff[64];
sprintf(buff, "SET NAMES %s", cs_name);
mysql_real_query(mysql, buff, strlen(buff));
// 3. 【關鍵】更新客戶端 MYSQL 結構體的 charset 指針
mysql->charset = cs;
return 0;
}
它直接影響 mysql_real_escape_string() 的轉義行為。
<?php
$link = mysqli_connect('localhost', 'root', 'pass', 'test', 3306);
// 字符 "縗" GBK 編碼:0x91 0x5c(第二字節恰是反斜槓)
$payload = "\x91\x5c";
// 1. 預設 latin1 連接
var_dump(mysqli_real_escape_string($link, $payload));
// 輸出: string(3) "縗\" --> 0x5c 被轉義為 \0x5c,破壞字符邊界!
// 2. 執行 SET NAMES gbk
mysqli_query($link, "SET NAMES gbk");
var_dump(mysqli_real_escape_string($link, $payload));
// 輸出: string(3) "縗\" --> 依然錯誤!因為 mysql->charset 仍指向 latin1
// 3. 使用 mysqli_set_charset
mysqli_set_charset($link, 'gbk');
var_dump(mysqli_real_escape_string($link, $payload));
// 輸出: string(2) "縗" --> 正確!mysql->charset 已更新為 gbk,識別雙字節不轉義第二字節
| 維度 | SET NAMES | mysqli_set_charset() |
|---|---|---|
| 服務端字符集變量 | ✅ 更新 | ✅ 更新 |
| 客戶端 MYSQL 結構體 charset | ❌ 不變 | ✅ 同步更新 |
| mysql_real_escape_string 行為 | ❌ 仍按舊字符集轉義 | ✅ 按新字符集正確轉義 |
| 防注入可靠性 | 低(特定多字節字符集可繞過) | 高 |
| 適用場景 | 舊版兼容、非預處理查詢 | 所有新代碼、安全敏感場景 |
$pdo = new PDO("mysql:host=localhost;dbname=test;charset=utf8mb4", 'user', 'pass');
// DSN 中 charset 參數會自動調用 mysql_set_character_set
// 等價於:
$pdo->exec("SET NAMES utf8mb4"); // 但 PDO 內部也同步了 charset 指針
mysqli_set_charset() 或 PDO DSN charset 設置連接字符集SET NAMES,特別是 GBK、BIG5、SJIS 等多字節字符集環境prepare()/bind_param()),從根源消除轉義依賴SET NAMES、mysql_real_escape_string 殘留用法"mysql_set_character_set() is the preferred way to change the character set. Using SET NAMES does not update the charset member of the MYSQL structure, and thus does not affect the character set used by mysql_real_escape_string()."
mysqli_set_charset() 不僅執行 SET NAMES,更關鍵地同步了客戶端 MYSQL 結構體的 charset 指針,確保 mysql_real_escape_string() 能根據正確字符集進行轉義,防範多字節字符集注入繞過。這是 C API 設計層面的安全保障,請務必在所有專案中強制使用。