PHP连接MySQL时如果字符集没设置对,轻则中文乱码,重则留下SQL注入漏洞。设置字符集最常见的两种方式是执行SQL语句SET NAMES utf8mb4,或者调用mysqli_set_charset()函数。很多教程里两者混用,仿佛随便选一个就行,但它们的行为并不完全等价。这篇文章详细对比这两种方式的底层差异,说明为什么官方文档明确推荐使用mysqli_set_charset()。

两种方式分别做了什么
先看第一种方式,直接执行SQL语句:
<?php
// 方式一:通过SQL语句设置字符集
$conn = mysqli_connect('127.0.0.1', 'root', 'pass', 'test');
mysqli_query($conn, "SET NAMES utf8mb4");
?>
SET NAMES utf8mb4告诉MySQL服务器三件事:客户端发送的数据用的是utf8mb4编码,服务器返回的结果也用utf8mb4编码,两者之间的连接层字符集是utf8mb4。注意这只是通知了服务器端,PHP这一侧的mysqli客户端库对这件事一无所知。
再看第二种方式:
<?php
// 方式二:调用专用函数设置字符集
$conn = mysqli_connect('127.0.0.1', 'root', 'pass', 'test');
mysqli_set_charset($conn, 'utf8mb4');
?>
mysqli_set_charset()在内部同样会发送SET NAMES,但它多做了一件关键的事:同步更新了mysqli客户端库内部记录的字符集状态。这个差异看似不起眼,实际影响很大,主要体现在转义函数和预处理语句的行为上。
关键差异:转义函数依赖客户端字符集状态
问题出在mysqli_real_escape_string()这类函数上。这个函数在PHP侧对字符串做转义,它必须知道当前连接使用什么字符集,才能正确判断哪些字节需要转义。当字符集状态不同步时,转义就会出错。
举个例子,如果连接建立时没有设置字符集,默认可能是latin1。此时你执行了SET NAMES gbk,服务器端认为连接是GBK编码,但mysqli客户端库仍然以为自己是latin1。当调用mysqli_real_escape_string()时,它按latin1的规则处理字符串,而服务器按GBK解释,规则不匹配就会出现漏转义。
最典型的案例就是宽字节注入。GBK等双字节编码中,某些字符的第二个字节范围覆盖了0x5C(也就是反斜杠)。攻击者提交特殊构造的字节序列,转义函数插入的转义符会被吞掉,恶意引号就能逃逸出来。来看一段有漏洞的代码:
<?php
// 有隐患的写法
$conn = mysqli_connect('127.0.0.1', 'root', 'pass', 'test');
mysqli_query($conn, "SET NAMES gbk");
// 客户端库不知道当前是gbk,转义可能不正确
$id = mysqli_real_escape_string($conn, $_GET['id']);
$sql = "SELECT * FROM users WHERE id = '$id'";
mysqli_query($conn, $sql);
?>
如果把mysqli_query($conn, "SET NAMES gbk")换成mysqli_set_charset($conn, 'gbk'),转义函数就能感知到真实的字符集,按正确规则处理输入,宽字节注入的风险随之消除。这就是官方文档反复强调不要用SET NAMES语句的根本原因。
预处理语句为何能绕开这个坑
有人会问:既然转义函数有风险,预处理语句是不是也受影响?答案是不受影响,但原因值得说清楚。
预处理语句的参数是单独传输的,MySQL协议层面会明确标记参数的边界,值永远不会被解析成SQL语法的一部分,所以不存在引号逃逸的问题。这也是为什么现代PHP开发中普遍推荐PDO或mysqli的预处理方式来处理用户输入。
<?php
$conn = mysqli_connect('127.0.0.1', 'root', 'pass', 'test');
mysqli_set_charset($conn, 'utf8mb4');
$stmt = $conn->prepare("SELECT * FROM users WHERE username = ?");
$stmt->bind_param('s', $_GET['user']);
$stmt->execute();
$result = $stmt->get_result();
?>
不过要注意,预处理语句只解决了注入问题,不解决乱码问题。字符集如果不统一,中文数据照样可能变成问号或乱码。所以无论用不用预处理,第一步都应该用mysqli_set_charset()把连接字符设置好。
utf8与utf8mb4的选择及完整配置建议
聊字符集绕不开utf8和utf8mb4的区别。MySQL中的utf8实际是最多三个字节的阉割版,无法存储Emoji等四字节字符。utf8mb4才是完整的UTF-8实现。现在的项目应该统一使用utf8mb4,避免用户昵称带个表情符号就插入失败。
除了连接层,还要保证库、表、字段的字符集一致,混合字符集会导致隐式转换,既影响性能又可能产生乱码。可以在建库建表时明确指定:
-- 建库时指定字符集和排序规则
CREATE DATABASE app DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
-- 建表时同样明确指定
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;
一个完整的PHP连接配置示例如下,供参考:
<?php mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); $conn = mysqli_init(); // 使用正确的字符集建立连接 mysqli_options($conn, MYSQLI_SET_CHARSET_NAME, 'utf8mb4'); mysqli_real_connect($conn, '127.0.0.1', 'root', 'pass', 'app'); ?>
总结一下:两者在纯查询场景下效果一样,但只要涉及mysqli_real_escape_string(),就必须用mysqli_set_charset()。多花一个函数调用,换的是转义逻辑的正确性和安全性,这笔账怎么算都划算。再配合预处理语句和utf8mb4统一编码,中文乱码和字符集注入问题就能从根源上避免。
mysqli_set_charsetSET NAMES字符集修改时间:2026-09-09 19:00:49