尚学堂.张志宇.乱码分析_08_pageEncoding.doc

 

1.   结论

pageEncoding 会影响读取 jsp 时的解码过程。

pageEncoding 会影响转换后的 servletresponse.setContentType 这句话。

对于不合法的 utf-8 编码进行解码,每个字节都会得到 ff fd 。这个代码点代表在 unicode 中不存在或者不可表示的字符

 

场景 1

场景 2

场景 3

 

1.jsp 存储格式

gbk

gbk

存为 gbk

 

<%@page pageEncoding

gbk

utf-8

pageEncoding="utf-8"

 

浏览器字符编码

gbk

utf-8( 默认 )

gbk (手工设定)

 

转换后的 servlet

我们

����!

����!

 

浏览器

我们

����!

锟斤拷锟斤拷 !

 

 

2.   场景 1

没有乱码,分析略过

3.   场景 2

1.jsp

<%@page pageEncoding=" utf-8"%>

<%=(" 我们 !")%>

 

详细分析

首先, 1.jsp 文件默认存储为 ansi ,即 jbk 我们 !” 编码成 jbk 得到如下字节 ce d2 c3 c7 21

tomcatjsp 转换为 servlet 的过程中,需要先读入 jsp 中的字符信息,因为 jsp 是按照 gbk 来存储的,如果 tomcat 按照 gbk 来解码,能够得到正确的中文 ;但是由于在 jsp 文件中指明 pageEncodingutf-8 ,所以 tomcat 会把硬盘上的 jbk 编码错误的当作 utf-8 来解码。 将解码之后得到的错误的字符串再编码成 utf-8 存储在转换后的 servlet 源文件 _1_jsp.java.

ultraedit 打开 tomcat 工作目录 work\Catalina\localhost\my\org\apache\jsp 目录下的 _1_jsp.java ,切换到 16 进制可以看到这些字节, ef bf bd ef bf bd ef bf bd ef bf bd 21 , 这些字节就是错误的 utf-8 编码。

为什么会编码成这些字节?

先来回忆一下 uft-8 的编码规则。

UTF-814 个字节来表示代码点。表示方式如下:

UCS-2 (UCS-4)

位序列

第一字节

第二字节

第三字节

第四字节

U+0000 .. U+007F

00000000-0xxxxxxx

0xxxxxxx

 

 

 

U+0080 .. U+07FF

00000xxx-xxyyyyyy

110xxxxx

10yyyyyy

 

 

U+0800 .. U+FFFF

xxxxyyyy-yyzzzzzz

1110xxxx

10yyyyyy

10zzzzzz

 

U+10000..U+1FFFFF

00000000-000wwwxx-
xxxxyyyy-yyzzzzzzz

11110www

10xxxxxx

10yyyyyy

10zzzzzz

 

ce d2 c3 c7 解码的意思是说,这些字节是合法的 utf-8 编码,现在要还原成正确的字符。

问题是: ce d2 c3 c7 是合法的 utf-8 编码吗?

 

ce d2 c3 c7 对应的 2 进制是:

11001110

11010010

11000011

11000111

 

先看 ce 对应的二进制是 11001110110 开头说明是符合 uft-8 的编码规则第二行,范围是 U+0080 .. U+07FFunicode 会被编码成 110xxxxx 10yyyyyy , 紧跟着下来的第二个字节应该是 10 开头,但是,接下来的 d2 却是 110 开头的。说明这不是个合法的 utf-8 编码。

 

ce d2 c3 c7 不是合法的 utf-8 编码! 或者可以这样说,这些所谓的 utf8 编码对应的字符在 unicode 中就根本不存在。!

 

在接着往下看!

虽然错误解码了。但是字符串对象还是得到了。只不过不是我们想要的而已。

在错误解码后得到的字符串应该在内存中表示为 utf-16 编码吧。

既然这些不合法的 utf-8 编码 对应的字符在 unicode 中就根本不存在。那内存里怎么办?

 

先来看 ef bf bd 的二进制是 11101111 10111111 10111101

根据 utf-8 编码规则,它代表的是 unicode 中的 ff fd 代码点。

 

打开 UFFF0.pdf 来验证下。(这个文档可以去 unicode 官网去下载)

看看 unicode 关于 fffd 这个代码点的解释: used to replace an incoming character whose value is unknown  or un representable in Unicode.

哦,用来代表在 unicode 中未知的或者无法表示的字符   

 

来稍微总结一下:

我们要对 ce d2 c3 c7 按照 utf-8 进行解码,当然这些字节不是合法的 utf-8 编码。

解码的目的是得到一些个字符。我们得到了所谓的字符。一共 4 个,这 4 个字符在 unicode 里面的代码点都是 ff fd 。在 unicodeff fd 这个代码点代表未知的或者无法表示的字符。

对于这个代码点,当然有合法的 utf-8 编码,即 ef bf bd

 

 

 

 

打开 _1_jsp.class 这个文件,同样可以发现 ef bf bd ef bf bd ef bf bd ef bf bd 21 这些字节,说明 class 文件也错误的 utf-8 编码!

servlet 开始运行,会将字节码里面的错误的 utf-8 编码按照 utf-8 进行解码。得到的当然是错误的内容。错误解码得到的错误的字符串在内存里标识为 utf-16 编码。这时候内存里面应该是 ff fd ff fd ff fd ff fd 0 21

由于 jsp 中咱们是这么写的 <%@page pageEncoding=" utf-8"%> ,这样会造成转换 servlet 时自动产生这句 response.setContentType("text/html;charset=utf-8"); 。这句代码的意思是输出到浏览器之前,按照 utf-8 进行编码。这将得到 ef bf bd ef bf bd ef bf bd ef bf bd 21 ,这些直接被传到浏览器。

可以靠下面的代码来验证,客户端浏览器是不是得到这些字节呢?

 

 

import java.io.BufferedReader;

import java.io.BufferedWriter ;

import java.io.InputStreamReader;

import java.io.OutputStreamWriter;

import java.net.Socket;

 

public class TestHTTP2 {

    public static void main(String[] args) throws Exception {

       Socket s = new Socket( "127.0.0.1" , 80);

       BufferedWriter bw = new BufferedWriter ( new OutputStreamWriter(s

              .getOutputStream()));

       bw.write( "GET /my/1.jsp HTTP/1.1" );

       bw.newLine();

       bw.write( "Host: 127.0.0.1" );

       bw.newLine();

       bw.write( "Content-Type: text/html" );

       bw.newLine();

       bw.newLine();

       bw.flush();

 

       BufferedReader br = new BufferedReader( new InputStreamReader(s

              .getInputStream()));

       String str = null ;

       while ((str = br.readLine()) != null ) {

           System. out .println(str);

       }

       bw.close();

       br.close();

       s.close();

    }

}

 

这段程序得到下面的结果。“锟斤拷锟斤拷 ! ” 这个怪怪的字符串是按照 gbk 解码得到的结果。

这个结果没法直观的了解客户接收的字节信息。

 

HTTP/1.1 200 OK

Server: Apache-Coyote/1.1

Set-Cookie: JSESSIONID=7D6AE9D95A8D2562AD6B19A4DE0EE08C; Path=/my

Content-Type: text/html;charset=utf-8

Content-Length: 15

Date: Tue, 25 Aug 2009 03:29:09 GMT

 

 

锟斤拷锟斤拷 !

 

程序稍微改动一下,原来用的是转换流。现在只用基于字节的输入流,不做任何转换。

import java.io.BufferedWriter;

import java.io.InputStream;

import java.io.OutputStreamWriter;

import java.net.Socket;

 

public class TestHTTP1 {

    public static void main(String[] args) throws Exception {

       Socket s = new Socket( "127.0.0.1" , 80);

       BufferedWriter bw = new BufferedWriter( new OutputStreamWriter(s

              .getOutputStream()));

       bw.write( "GET /my/1.jsp HTTP/1.1" );

       bw.newLine();

       bw.write( "Host: 127.0.0.1" );

       bw.newLine();

       bw.write( "Content-Type: text/html" );

       bw.newLine();

       bw.newLine();

       bw.flush();

 

       InputStream ins = s.getInputStream();

       int i = 0;

       while ((i = ins.read ()) != -1) {

           System. out .format( "%x " , i);

       }

       ins.close();

       bw.close();

       ins.close();

       s.close();

    }

}

 

 

 

48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b d a 53 65 72 76 65 72 3a 20 41 70 61 63 68 65 2d 43 6f 79 6f 74 65 2f 31 2e 31 d a 53 65 74 2d 43 6f 6f 6b 69 65 3a 20 4a 53 45 53 53 49 4f 4e 49 44 3d 33 43 36 32 36 42 33 35 43 43 43 38 42 32 31 37 38 37 36 34 38 30 37 36 42 38 42 42 38 46 41 44 3b 20 50 61 74 68 3d 2f 6d 79 d a 43 6f 6e 74 65 6e 74 2d 54 79 70 65 3a 20 74 65 78 74 2f 68 74 6d 6c 3b 63 68 61 72 73 65 74 3d 75 74 66 2d 38 d a 43 6f 6e 74 65 6e 74 2d 4c 65 6e 67 74 68 3a 20 31 35 d a 44 61 74 65 3a 20 54 75 65 2c 20 32 35 20 41 75 67 20 32 30 30 39 20 30 33 3a 32 37 3a 34 30 20 47 4d 54 d a d a d a ef bf bd ef bf bd ef bf bd ef bf bd 21

 

ef bf bd ef bf bd ef bf bd ef bf bd 21 ,这些内容就是浏览器收到的内容。

 

由于浏览器收到的头信息是: Content-Type: text/html;charset=utf-8 ,浏览器就会默认 按照 utf-8 进行解码。看看浏览器的字符编码是不是默认选在了 utf-8

解码之后得到的是不存在的字符,所以

eclipse 中显示为;得到的是 ????!

在浏览器看到的是 ����!

4.   场景 3

如果客户端浏览器把这些字节 ef bf bd ef bf bd ef bf bd ef bf bd 21 按照 gbk 来解码,得到的是

锟斤拷锟斤拷 !

 

5.   验证代码

import java.io.UnsupportedEncodingException;

 

public class TestC {

    public static void main(String[] args) throws UnsupportedEncodingException {

       //jsp 文件默认存储为 ansi ,即 jbk 编码: ce d2 c3 c7 21

       byte [] bytes = new byte []{( byte )0xCE,( byte )0xD2,( byte )0xc3 ,( byte )0xc7 ,( byte )0x21 };

 

       // 如果按照 gbk 来解码,能够得到正确的中文

       System. out .println( "-------- 如果按照 gbk 来解码,能够得到正确的中文 " );

       String s = new String(bytes, "gbk" );

       System. out .println(s);

      

       // 但是由于在 jsp 文件中指明 pageEncodingutf-8

       // 所以 tomcat 会把硬盘上 1.jspjbk 编码错误的当作 utf-8 来解码。

       String s1 = new String(bytes, "utf-8" );

       System. out .println( "--------tomcat 会把硬盘上 1.jspjbk 编码错误的当作 utf-8 来解码 " );

       System. out .println(s1);

      

       System. out .println( "--------servlet 源文件 h 和字节码文件都是这些 utf-8 编码 " );

       // 将解码之后得到的错误的字符串再编码成 utf-8 存在转换的 servlet 源文件中。

       //servletclass 文件也是这些 utf-8 编码

       byte [] bytes1 = s1.getBytes( "utf-8" );

       for ( byte b:bytes1){

           System. out .format( "%x " ,b);

       }

       System. out .println();

      

 

       //servlet 载入执行,将字节码中的 utf-8 编码解码成字符串对象。

       // 如果再按照 utf-16 编码,我们可以看到内存里面的 utf-16 编码。

       System. out .println( "-------- 内存里面的 utf-16 编码 " );

       String s2 = new String(bytes1, "utf-8" );

       byte [] bytes_uft_16 = s2.getBytes( "utf-16" );

       for ( byte b:bytes_uft_16){

           System. out .format( "%x " ,b);

       }

       System. out .println();

      

      

       // 被编码成 utf-8 ,传到浏览器客户端

       System. out .println( "-------- 被编码成 utf-8 ,传到浏览器客户端 " );

       byte [] bytes2 = s1.getBytes( "utf-8" );

       for ( byte b:bytes2){

           System. out .format( "%x " ,b);

       }

 

       // 客户端按照 utf-16 解码

       String s3 = new String(bytes1, "utf-8" );

       System. out .println();

       System. out .println( "-------- 客户端按照 utf-16 解码 " );

       System. out .println(s3);

    }

}

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值