📘 第六章 HTTP首部
📄 一句话概括

Accept-Charset 协商的是字符集。 它今天基本已经废了——浏览器只发 utf-8,规范也把它的请求语义移除了。

📝 本节和前面几节的关系

上一节协商格式,这一节协商字符集。两个字段长得很像,但这一个的结局是被时代淘汰。

一、字符集与编码的区别

字符集是一组符号的集合,编码是把这些符号变成字节的规则。 说「utf-8」时严格讲说的是编码,但 HTTP 里这个词一直混着用。 Demo 里那句「中文测试 abc」,用 UTF-8 编是 16 字节,用 GBK 编是 12 字节。 同一句话,两套字节,长度都不一样。 这就是协商的意义:如果服务端按 GBK 发,客户端按 UTF-8 解,看到的就是一堆乱码。 字符集错了不是「显示难看」,是「读不出来」。

二、为什么它死了

因为 UTF-8 赢了。2010 年代之后,全球绝大多数网站都用 UTF-8,浏览器也就懒得再发这个字段。 2014 年的 RFC 7231 干脆把 Accept-Charset 从请求语义里移除,同时规定:服务端可以忽略它, 客户端也不该再发。这个字段现在只剩下「历史兼容」的意义。 顺带一个相关的变化:响应里的 Content-Type 至今仍然带 charset 参数,这一半还活着。 所以字符集这件事没有消失,只是不再协商了——直接约定用 UTF-8。

三、一个常被忽略的事实

Demo 里客户端点名要 us-ascii,服务端最后给了 utf-8。 这不是服务端耍赖——是因为正文里有中文,ASCII 根本编不出来。 编码能力是有边界的:七个比特装不下一个汉字。 同理,如果客户端点名要 GBK 而服务端的内容里有生僻字,也会编不出来。 所以「协商」在这件事上天生不可靠:能不能编出来,取决于内容,而不取决于双方意愿。 这也是 UTF-8 胜出的根本原因:它是唯一能装下所有字符的编码,协商就没有必要了。

四、动手 Demo:四组请求,看服务端怎么选字符集

准备同一句话的三个编码版本(UTF-8、GBK、以及一个纯 ASCII 的替代文本),按客户端点名优先, 挑第一个服务端能编出来的。

ENC = {u'utf-8': SAMPLE.encode(u'utf-8'),
       u'gbk': SAMPLE.encode(u'gbk'),
       u'iso-8859-1': u'latin only abc'.encode(u'latin-1')}

want = [x.strip().lower() for x in ac.split(u',') if x.strip()]
chosen = u'utf-8'
for w in want:
    w = w.split(u';')[0]
    if w in ENC:
        chosen = w
        break
body = ENC[chosen]
return reply(u'HTTP/1.1 200 OK',
             [(u'Content-Type', u'text/plain; charset=' + chosen)],
             body)

实跑输出:

--------------------------------------------------------------------
Accept-Charset:字符集协商,一个已经过时的字段
--------------------------------------------------------------------
  【点名要 utf-8】
    Accept-Charset           utf-8
    选中的字符集             utf-8
    响应 Content-Type        text/plain; charset=utf-8
    正文字节数               16
    按该字符集解回           中文测试 abc

  【点名要 gbk】
    Accept-Charset           gbk
    选中的字符集             gbk
    响应 Content-Type        text/plain; charset=gbk
    正文字节数               12
    按该字符集解回           中文测试 abc

  【点名要 ascii】
    Accept-Charset           us-ascii
    选中的字符集             utf-8
    响应 Content-Type        text/plain; charset=utf-8
    正文字节数               16
    按该字符集解回           中文测试 abc

  【什么都不发】
    Accept-Charset           (没发)
    选中的字符集             utf-8
    响应 Content-Type        text/plain; charset=utf-8
    正文字节数               16
    按该字符集解回           中文测试 abc

  「中文测试 abc」这句话,用 utf-8 编是 16 字节,用 gbk 编是 12 字节,用 iso-8859-1 根本编不出来。
  所以字符集协商的本质是:**同一个字符串,不同编码的字节完全不同**。
  发错编码的后果不是显示难看,而是直接乱码或解不出来。

  这个字段今天的处境很尴尬:几乎所有浏览器都只发 `utf-8`,
  于是 2014 年的 RFC 7231 干脆把它从请求语义里移除,
  现在只剩「响应里的 Content-Type 带 charset」这一半还在用。

要 UTF-8:16 字节,解回来是「中文测试 abc」。 要 GBK:12 字节。同样一句话,GBK 更省——因为汉字在 GBK 里占两字节,在 UTF-8 里占三字节。 这也是早年中文网站偏爱 GBK 的原因。 要 us-ascii:退到 UTF-8,16 字节。因为那句话有中文,ASCII 编不出来。 什么都不发:默认 UTF-8,16 字节。这就是今天所有浏览器的实际行为。

30 秒实验

  1. 把 SAMPLE 换成纯英文,再要 us-ascii,看能不能命中。
  2. 加一个 gb2312 的候选(GBK 的子集),看该不该接受。
  3. 想一想:如果服务端的内容是多语言的,还能用 GBK 吗?

常见误区

  1. 「字符集和编码是一回事」——严格说是两件,虽然 HTTP 里混着用。
  2. 「Accept-Charset 还广泛使用」——2014 年就被从请求语义里移除了。
  3. 「客户端点名要什么就一定给什么」——编不出来的时候给不了。
自测三问(点开对答案) 1. 同一句中文,UTF-8 和 GBK 哪个更长? —— UTF-8。汉字在 UTF-8 里通常三字节,在 GBK 里两字节。 2. 为什么 `Accept-Charset` 基本没用了? —— UTF-8 成了事实标准,浏览器不再发这个字段,RFC 7231 也移除了它的请求语义。 3. 客户端要 `us-ascii` 而内容里有中文,会怎样? —— 给不了。ASCII 只有七位,编不出汉字,服务端只能改用别的编码。

字符集说完,下一个是压缩——这个字段今天很活跃。

相关笔记