首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

深入分析 web 请求响应中的编码问题(1)乱码原因

深入分析 web 请求响应中的编码问题(1)乱码原因

乱码问题一直是困扰开发人员的比较头疼的问题,而发生在 web 请求响应中的乱码问题由于牵扯到比较多的协议框架和技术实现,又显得更加棘手。web                请求中的乱码一般容易出现在两个地方:一是所请求的资源名称,二是查询参数;更复杂的是,不同的浏览器对 URL                和查询参数采用的默认编码可能还不一样,这就更加加深了问题的难度。本文将深入浅出地分析 web                请求响应中乱码产生的原因与解决该问题的关键因素,并举例说明给出该问题的最佳解决方案。
web                请求响应中乱码产生的原因相关概念URL,URI                及查询字符串URL 是统一资源定位器,是用来引导指向对应的网络资源的,狭义来说查询字符串并不是 URL 的一部分,URL 是由协议、域名、端口和 URI                组成的。URI 是统一资源标识符,是用来引导指向某站点的服务资源的,如图一所示:
图 1. URL 与 URI                    和查询字符串的关系RFC 1738(Uniform Resource Locators (URL))规定 URL 只能包含英文字母、阿拉伯数字和某些标点符号。这要求在                URL 中使用非英文字符的话必须编码后使用,不过 RFC 1738                并没有规定如何进行编码,而是交给应用程序(浏览器)自己决定。糟糕的是各浏览器在编码 URL 时采用了不同的机制,有的默认按                UTF-8,有的则默认跟随系统代码页。更有甚者,同一浏览器在编码 URL 和查询参数时使用的编码也不同。这就是 web                请求中出现乱码的根源所在。
Request 与                Response 对象服务端收到客户端的 HTTP 请求,中间件/应用程序服务器会针对每一次请求分别创建一个 request 对象和 response 对象。
  • request 对象是操作客户端发送过来的数据的容器。
request 中的 setCharacterEncoding(String enc) 方法可以设定针对请求体(post                提交的数据即在请求体中)的解码方式,通过它设置与请求体一致的编码是 post body                中的数据不乱码的关键。不过需要注意的是该方法必须在读取请求参数或者获得输入流之前调用,一般写在比较靠前的过滤器中。
  • response 对象是操作服务端发出的数据的容器。
response 中的 setContentType(String ct) 方法可以设置即将发送给客户端的响应的 content                type,content type 中可以包含对响应编码的设定,比如"text/html;charset=UTF-8"。同调用 request 的                setCharacterEncoding 一样,它也必须在获得输出流之前调用。另外该方法等价于下面两句:
1
2
response.setCharacterEncoding("UTF-8");//设置响应编码  
response.setHeader("Content-Type","text/html;charset=UTF-8");//通知浏览器如何解码




参照清单 1 中 testResponse1 和 testResponse2 可以看出 setContentType                既设置了响应的编码,也通知了浏览器解码方式;结合 testRespond2 与图二和 testRespond3 与图三可以看出 response                是如何影响浏览器解码的。
清单 1. 通过                    response content type                影响浏览器的默认解码方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RequestMapping("/testResponse1")
public void response1(HttpServletRequest request, HttpServletResponse response) throws IOException{
    response.setCharacterEncoding("utf-8");  
    response.setHeader("content-type", "text/html;charset=utf-8");
    response.getWriter().write("测试");
}

@RequestMapping("/testResponse2")
public void response2(HttpServletRequest request, HttpServletResponse response) throws IOException{
    System.out.println(response.getCharacterEncoding());//ISO-8859-1
    response.setContentType("text/html;charset=utf-8");
    System.out.println(response.getCharacterEncoding());//utf-8
    response.getWriter().write("测试");
}

@RequestMapping("/testResponse3")
public void response3(HttpServletRequest request, HttpServletResponse response) throws IOException{
    System.out.println(response.getCharacterEncoding());//ISO-8859-1
    response.setContentType("text/html;charset=gb18030");
    System.out.println(response.getCharacterEncoding());//gb18030
    response.getWriter().write("测试");
}




图 2.                    Response 中的 content type 影响浏览器的默认解码方式-1
图 3.                    Response 中的 content type 影响浏览器的默认解码方式-2
乱码示例换句话讲,编码和解码时用的字符编码方式不一致导致的,下面模拟几种常见的请求参数变乱码的情况,参考清单 2 、图 2 、清单 3:
清单 2. Web                请求中的乱码模拟
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Test
public void testParameter() throws UnsupportedEncodingException {
    WebRelated wr = new WebRelated();

    //模拟浏览器编码跟随英文系统取 ISO8859-1,中间件取默认 URIEncoding 即 ISO8859-1
    String coding1 = "ISO8859-1";
    String coding2 = "ISO8859-1";
    wr.mockParse(coding1, coding2);

    //模拟浏览器编码跟随中文系统取 GB18030,中间件取默认 URIEncoding 即 ISO8859-1
    coding1 = "GB18030";
    coding2 = "ISO8859-1";
    wr.mockParse(coding1, coding2);

    //模拟浏览器编码跟随中文系统取 GB18030,中间件设置 URIEncoding 为 UTF-8
    coding1 = "GB18030";
    coding2 = "UTF-8";
    wr.mockParse(coding1, coding2);

    //模拟浏览器按 UTF-8 编码,中间件设置 URIEncoding 为 UTF-8
    coding1 = "UTF-8";
    coding2 = "UTF-8";
    wr.mockParse(coding1, coding2);
}

public void mockParse(String coding1, String coding2) throws UnsupportedEncodingException {
    String sendStr = "测试";
    // 模拟浏览器将参数按 coding1 编码
    String encodedByBrowser = URLEncoder.encode(sendStr, coding1);
    System.out.println("通过浏览器按 " + coding1 + " 编码为:");
    System.out.printf("%40s\n", encodedByBrowser);

    //模拟 Tomcat 使用 coding2 解码
    String receivedStr = URLDecoder.decode(encodedByBrowser, coding2);
    System.out.println("通过中间件按 " + coding2 + " 解码:");
    System.out.printf("%40s\n", receivedStr);
    TestEncoding.printHex(receivedStr, coding2);
}




结果:
清单 3. Web                请求中的乱码模拟结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
通过浏览器按 ISO8859-1 编码为:
                                  %3F%3F
通过中间件按 ISO8859-1 解码:
                                      ??
Hex of <??> by [ISO8859-1]:
                                  3f 3f

通过浏览器按 GB18030 编码为:
                            %B2%E2%CA%D4
通过中间件按 ISO8859-1 解码:
                                    ²âÊÔ
Hex of <²âÊÔ> by [ISO8859-1]:
                            b2 e2 ca d4

通过浏览器按 GB18030 编码为:
                            %B2%E2%CA%D4
通过中间件按 UTF-8 解码:
                                    ����
Hex of <����> by [UTF-8]:
    ef bf bd ef bf bd ef bf bd ef bf bd

通过浏览器按 UTF-8 编码为:
                      %E6%B5%8B%E8%AF%95
通过中间件按 UTF-8 解码:
                                      测试
Hex of <测试> by [UTF-8]:
                      e6 b5 8b e8 af 95




对应乱码成因解析
  • "??"乱码分析:
ISO-8859-1 仅能编码非英文字符,所以非英文字符被其编码时会被转换为 0x3F(即?的 ASCII 编码,也是 UTF-8                编码),这时编码已经真被转成不可逆的乱码了。之后无论用兼容 ASCII 的哪种编码方案解码还原出的字符串都是"?"。
结果:所以出现?时基本可以猜测是客户端错误按 ISO-8859-1 进行了编码。
  • "²âÊÔ"乱码分析:
ISO-8859-1 仅能表示非英文字符,所以使用其解码时会严格按一个字节一个字节地进行解析(这种操作其实对编码没构成破坏,还可以重新用                ISO-8859-1 获取字节流后再用正确的编码方式解码得到正确的字符串)。
结果:所以乱码字符均是来自 ISO-8859-1 中字符集中的字符时基本可以猜测是服务端错误按 ISO-8859-1 进行了解码。
  • "����"乱码分析:
用 UTF-8 解码经 GB18030 编码的字节流时发现四个字节均为 UTF-8 非法字节流,所以直接转化为了�。
返回列表