# 服务端字符编解码&乱码处理 charset_enc_dec
30 秒速记
- 乱码的本质:编码用的规则和解码用的规则不一样,字节被「翻译错了」
- 排查顺序:拿到原始字节(
hex)→ 看声明的编码 → 看实际用什么解码,别反复toString碰运气 - 两种经典乱码:
UTF-8字节按GBK读 → 「浣犲ソ」;GBK字节按UTF-8读 →�� - 流式数据里中文可能被切在两个
chunk之间 → 先Buffer.concat再解码,或用StringDecoder Content-Type的charset、HTML的meta、数据库连接编码要统一,新系统一律UTF-8
乱码说白了就是编码和解码用的不是同一套规则,同样的字节被翻译成了别的字。 服务端排查时我第一件事是把原始字节用 hex 打出来,比如「你」在 UTF-8 里是 e4 bd a0,在 GBK 里是 c4 e3,一看字节就知道发送方用的是哪种编码。然后对照接口声明的 charset 和代码里实际怎么解码的,哪一环对不上问题就在哪。另外流式读取时中文一个字占好几个字节,可能被切在两个 chunk 里,逐块 toString 会把半个字弄坏。根治的办法是全链路统一 UTF-8。
const buf = Buffer.from('你好') // Node 默认按 UTF-8 编码
console.log(buf.toString('hex')) // e4bda0e5a5bd
console.log(new TextDecoder('gbk').decode(buf)) // 浣犲ソ ← 按 GBK 读就乱了
理解「服务端字符编解码&乱码处理 charset_enc_dec」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 network 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
服务端处理文本时,经过的每一个边界都可能发生一次编码或解码:请求体从网络进来是字节,要解码成字符串;字符串写数据库,要按连接编码转成字节;读文件、写日志、返回响应也是一样。乱码只会出现在某一次「编码规则和解码规则不一致」的边界上,排查就是沿着链路找到第一个出错的边界。
实操上有三个习惯很管用。
第一,在边界处打出原始字节。buf.toString('hex') 或者 xxd file.txt,对照已知字符的编码就能判断:「你」在 UTF-8 是 e4 bd a0,在 GBK 是 c4 e3。
第二,认识两种最常见的乱码长相。UTF-8 字节被当成 GBK 读,会出现「浣犲ソ」「鍙戦€」这种生僻汉字;GBK 字节被当成 UTF-8 读,会出现大量 �。前者往往还能救回来,后者原始信息已经丢了。
第三,流式数据别逐块解码。一个中文字符在 UTF-8 里占 3 个字节,网络分块可能正好把它切开:
const { StringDecoder } = require('string_decoder')
const buf = Buffer.from('你') // e4 bd a0
const a = buf.subarray(0, 2), b = buf.subarray(2)
console.log(a.toString() + b.toString()) // ��� 逐块解码,字被切坏
const decoder = new StringDecoder('utf8')
console.log(decoder.write(a) + decoder.write(b)) // 你 残缺的字节会留到下一块
Node 的 Buffer.toString() 只认 utf8、utf16le、latin1 等少数编码,处理 GBK、Big5 这类编码要用 iconv-lite,或者在带完整 ICU 的 Node 里用 new TextDecoder('gbk')。新系统的协议最好直接约定 UTF-8,并在 Content-Type 里写明 charset=utf-8。
面试官追问
追问 1页面上中文显示正常,写进日志就乱码,说明是日志的问题吗?
不一定,只能说明页面那条路径的编解码是对的。可能是日志写入时编码不对,也可能是你看日志的终端或编辑器用了 GBK,先 hexdump 看文件里的字节是不是 UTF-8 再下结论。
追问 2看到「锟斤拷」这种乱码,大概能判断出什么?
说明数据被错误解码过两次。先按 UTF-8 解码失败变成了替换字符 U+FFFD,它的 UTF-8 字节是 ef bf bd,两个连起来再按 GBK 读就是「锟斤拷」。到这一步原始数据已经丢了,只能回源头查。
追问 3乱码字符串能不能转回来?
看损坏程度。UTF-8 字节被当 GBK 解出来的「浣犲ソ」,再按 GBK 编码回去、按 UTF-8 解码通常能恢复;已经变成 � 的就不行了,信息在解码那一步就丢了。
追问 4用户名里的 emoji 存进 MySQL 后变成了问号,是编解码问题吗?
多半是表或连接用的 utf8,它在 MySQL 里最多只存 3 个字节,emoji 要 4 个字节。改成 utf8mb4,表、字段和连接参数都要改。
# 写在前面
30 秒速记
- 服务端每天都在做编解码:读请求体、读写文件、写数据库、返回响应
- 网络和磁盘上只有字节,「字符串」是按某种编码解释出来的结果
- 英文正常不代表没问题:
ASCII范围内UTF-8、GBK的字节一样,中文才会暴露差异 - 二进制(图片、压缩包)不要转字符串,全程保持
Buffer - 解决乱码的思路永远是:找到原始字节,确认它真实的编码,用对应的规则解码
服务端开发绕不开字符编解码,因为网络和磁盘上流动的只有字节,字符串是按某种编码规则解释出来的。 读请求体、读文件、查数据库、写响应,每一步都在字节和字符串之间转换,只要有一步用错规则就是乱码。很多人遇到乱码就挨个试 toString('utf8')、toString('latin1'),其实效率很低。正确的思路是先确认手上这段字节到底是按什么编码生成的,再用同一套规则解回来。还有个常见误判:英文没问题不代表编码对了,因为英文字母在 UTF-8 和 GBK 里字节是一样的。
在web服务端开发中,字符的编解码几乎每天都要打交道。编解码一旦处理不当,就会出现令人头疼的乱码问题。
不少从事node服务端开发的同学,由于对字符编码码相关知识了解不足,遇到问题时,经常会一筹莫展,花大量的时间在排查、解决问题。
文本先对字符编解码的基础知识进行简单介绍,然后举例说明如何在node中进行编解码,最后是服务端的代码案例。
面试官追问
追问 1接口收英文一直正常,收中文就乱码,后端说网络没问题,你怎么看?
这恰好说明是编码不一致。ASCII 范围的字符在 UTF-8、GBK 里字节完全相同,怎么解都对;中文的字节不同,规则一错就乱。
追问 2上传接口文本和图片走同一条链路,代码里先统一 toString() 再处理,有什么问题?
图片会被损坏。图片字节里有大量不合法的 UTF-8 序列,toString() 会把它们替换成 U+FFFD,再转回 Buffer 已经不是原图了。二进制全程保持 Buffer,只有确定是文本才解码。
追问 3要导入一批历史 GBK 文件,产品要求统一按 UTF-8 读,能直接这么干吗?
不行,GBK 字节按 UTF-8 解出来就是一堆 �,而且无法恢复。导入时用 iconv.decode(buf, 'gbk') 先转成字符串,系统内部再统一存 UTF-8。
追问 4线上用户名偶发乱码,前端、后端、数据库三方都说自己没问题,怎么定位?
沿链路在每个边界打出字节:请求进来的原始 Buffer、写库前的字符串、库里存的字节、响应出去的字节。第一个对不上的位置就是问题所在,只看最终页面是分不出来的。
# 关于字符编解码
30 秒速记
- 网络里传的永远是二进制,不管是中文、英文、文本还是图片
- 编码:字符串 → 字节(客户端发送前);解码:字节 → 字符串(服务端收到后)
- 字节本身不带「我是什么编码」的信息,双方必须事先约定,或者在
Content-Type的charset里声明 Node里对应Buffer.from(str, 'utf8')编码、buf.toString('utf8')解码
编解码就是字符和二进制之间的互相翻译:发送方把字符串编码成字节传出去,接收方再把字节解码回字符串。 比如客户端发「你好」,网络上实际跑的是 e4 bd a0 e5 a5 bd 这 6 个字节,服务端要按 UTF-8 解码才能还原成「你好」。关键在于字节本身不会告诉你它是用什么编码的,所以双方必须提前约定好,或者在 HTTP 头里写明 charset。约定不一致,同样的字节就会被翻译成完全不同的字。
const bytes = Buffer.from('你好', 'utf8') // 编码
console.log(bytes) // <Buffer e4 bd a0 e5 a5 bd>
console.log(bytes.toString('utf8')) // 你好 解码
