网页中的字符集编码与乱码(3)--响应头 Content-Type 中的 charset 编码

深入介绍了响应头 Response Headers 中的 Content-Type 中的 charset 信息的应用, 包括许多在静态文档和动态文档中的实验与测试的细节, 以及一些具体配置和与文档内编码声明的优先级问题.

目录

在上一篇说完了如何 通过文档内的编码声明来确定网页的编码 通过文档内的编码声明来确定网页的编码, 这一篇则开始具体讲述如何通过响应头下的 Content-Type 条目中的 charset 信息来确定文档的编码, 包括如何去配置这个响应头, 以及一些具体的实验, 还有它与文档内编码声明的优先级选择问题.

content-type 中的 charset

通过前面的介绍, 你已经知道了所谓的"响应头下的 Content-Type 条目中的 charset 信息", 就是这样:

Response Headers Content-Type charset utf-8

或这样的东西:

Response Headers Content-Type charset gbk

它指出了这段响应流的编码.

响应头的优先级为什么更高?

前面已经说到, 它的优先级是要高过文档内编码声明的. 这其实也很好理解, 因为它属于外部信息, 而文档内编码声明则属于内部信息.

上一篇说到, 浏览器还要通过嗅探算法才能获取到文档内的编码声明.

所以, 如果服务端的响应已经直接告诉你这段响应流的编码, 你都不用去"嗅探"了, 浏览器自然很乐意直接采用它.

也可以这样理解, 这个编码的头信息服务端是可以不发送的, 现在反而专门主动地告诉你, 怕你不知道, 自然它的可信度也是很高的. 浏览器优先采用它也不足为奇了, 还省略了嗅探的步骤, 提高了效率.

配置静态页面的响应头编码

前面说到, 当在 tomcat8 缺省配置下运行时, 它不会为 Content-Type 中增加编码信息:

响应头 content-type 不带 charset

我们需要主动去配置这个信息. 具体而言, 可以去到工程的 web.xml 下, 在 web-app 根节点下增加一个 mime-mapping 配置:

<mime-mapping>   
    <extension>html</extension>   
    <mime-type>text/html;charset=utf-8</mime-type>   
</mime-mapping>

这里, extension 表示文件的扩展名(后缀名), 而 mime-type 中的内容则成为 Content-Type 的值, 如此, 就增加了编码的信息.

web.xml 最终结果如下:

tomcat mime-type charset

当然, 不同的 web server 间具体配置的语法会有所区别, 如果用的不是 tomcat, 你可以查询相应文档了解.

这样一来, 所有对 html 的静态请求, 响应头就会变成这样:

Response Headers Content-Type charset utf-8

显然, 这样的一个配置是一个工程全局级别的,

注意: 如果你直接修改 tomcat server 的相关配置文件, 就会成为服务器级别的, 将影响它下面部署的所有工程!

这时你要保证工程下所有的 html 文件都是使用 utf-8 来编码的, 否则由于它具有更高的优先级, 那些比如 gbk 编码的文档将会发生乱码, 在上一篇中的实验中也已经证实了这一点:

响应头编码优先页面编码声明

在开发实践中应该遵循"审慎"原则, 当你做某项调整时, 要清楚知道它的后果. 你不能光顾着解决自己当下碰到的问题, 还要留心是否会引发更多的问题, 也即是人们常说的 "按下葫芦起了瓢".

很不幸的, 碰到乱码问题时, 我们经常是"病急乱投医", 而这点又往往是因为我们对编码问题没有一个整体认识所导致的.

配置动态响应的响应头编码信息

如果说静态的响应头配置在 tomcat8 中显得还不是很灵活, 那么在动态的响应中, 你则可以自由地根据需要调整. 比如这样:

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/encoding/page/rightHeader/utf8")
public class EncodingRightHeaderUTF8 extends HttpServlet {
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		response.setHeader("Content-type", "text/html;charset=utf-8");
		ServletOutputStream os = response.getOutputStream();
		os.write("编码与 header 一致,UTF-8.".getBytes("utf-8"));
	}
}

或者这样:

@WebServlet("/encoding/page/rightHeader/gbk")
public class EncodingRightHeaderGBK extends HttpServlet {
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		response.setHeader("Content-type", "text/html;charset=gbk");
		ServletOutputStream os = response.getOutputStream();
		os.write("编码与 header 一致,GBK.".getBytes("gbk"));
	}
}

文档中也可以省略 meta charset 声明了. 当然, 现在你要注意的依旧是要保证宣称编码与实际所用的一致:

正如上面两段代码呈现的那样, 宣称是 utf-8, 实际也得是 utf-8;宣称是 gbk, 实际也得是 gbk.

不管是文档内的还是文档外的, 保持一致性才能避免乱码的发生. 以上两种情况下, 文档都能浏览器中正确显示:

编码与 header 一致, 显示正常

假如不一致:

@WebServlet("/encoding/page/wrongHeader")
public class EncodingWrongHeader extends HttpServlet {
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		response.setHeader("Content-type", "text/html;charset=gbk"); //宣称 gbk
		ServletOutputStream os = response.getOutputStream();
		os.write("编码与 header 不一致,乱码!".getBytes("utf-8")); //实际 utf-8
	}
}

结局依然是乱码:

编码与 header 不一致, 乱码!

以上, 为每个请求都单独设置了响应头信息, 能够做到很灵活的设置. 但有时我们可能也不想处处都要这样弄一下, 对于动态响应, 同样可以对全局(或者更小一点的局部)做一个统一配置, 这点可以通过 filter 来做到. 不过这些不是这里谈论的重点, 这里也是稍微提下.

优先级测试

通过动态响应, 很容易构建一个冲突的编码声明的例子:

@WebServlet("/encoding/page/rightHeaderWrongMeta")
public class EncodingRightHeaderWrongMeta extends HttpServlet {
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		response.setHeader("Content-type", "text/html; charset=utf-8"); // utf-8 与 getbytes 一致
		ServletOutputStream os = response.getOutputStream();
		String meta = "<head><meta charset=\"gbk\"></head>";// gbk 与 getBytes 冲突
		os.write((meta + "编码与 header 一致,与 meta 冲突,header 优先级高,结果正常。").getBytes("utf-8")); // 实际 utf-8
	}
}

如上, Content-Type 中是 utf-8, 文档内声明 meta charset 是 gbk, 实际是 utf-8.

最终, 显示是正常的:

编码与 header 一致, 与 meta 冲突, header 优先级高, 结果正常

虽然 meta charset 给出了一个错误的建议, 但 Content-Type 的优先级更高, 浏览器先采纳了它, 忽略了文档内的声明.

反之, 如果优先级更高的 Content-Type 给出了错误的建议, 页面就会乱码了, 具体代码及演示略, 读者可以自行构建类似的测试.

另: 我这里一直用的是 Java 平台来演示, 但这些 meta 呀, headers 呀, Content-Type 呀, response 呀之类的都是跟平台无关的, 属于 html 规范或 http 协议范畴, 无论你是 php, asp 还是 nodejs 等等, 与这些相关的东西都是类似的, 原理也是相通的. 如果你不是 Java 平台, 应该也不难构建类似的测试.

关于通过响应头下的 Content-Type 条目中的 charset 信息来确定文档的编码就介绍到这里, 下一篇将讨论 BOM 存在时的页面编码的确定问题.