乱码的终极奥秘

我相信做javaEE开发的程序猿们,不管是经验丰富,还是初生牛犊,都肯定在开发的过程中,遇到过乱码的问题。我本人也深受其害。而且乱码问题防不胜防
,让人十分头疼。
下面我就准备就乱码问题。说一说我自己的心得和体会。
在说之前首先。我要明确几个概念。
1,首先明确一点,记事本的默认编码是ASNI,这个ASNI是什么意思呢,它的意思是说如果是在中国,默认的编码就是GBK,如果是在中欧的话,那可能就是ISO-8859-1
2,  很多数据库的编码类型中并看不到ISO-8859-1这个编码,其实是有的,只是叫了另外一个名字latin1
3,编码意指按照一定规律将字符转化为二进制字节码
4,解码意指将二进制字节按照一定规律解读成字符
5,ISO-8859-1按照单字节去编码字符,也是就8位。所以就算它8位都用上可以编码的字符也就255个。但正因为它是单字节编码,所以其他字符集编码形成的二进制字节码,
如果用ISO-8859-1去解码的话,并不会造成字节丢失。

明确了这些之后,不妨跟着我一起做个小实验。

首先声明,实验的地点是在中国,软件环境是jdk1.7 32、tomcat6.0 32、firebox、 ie8

实验的数据:
序号    字符        GBK编码生成的二进制字节(以下用十六进制表示)  UTF8生成的二进制字节(以下用十六进制表示)
 1       我             CED2                                                                               E68891
 2       我的         CED2B5C4                                                                     E68891E79A84
 3       我1           CED231                                                                          E6889131

向服务端发送请求的方式:
1,超链接   这种请求都为get请求
2,form表单的get请求
3,form表单的post请求
4,ajax的get请求(即xmlhttprequest发出的get请求)
5,ajax的post请求(即xmlhttprequest发出的post请求)

咱们就这几种情况编写jsp与servlet如下。

1,Encoding.jsp

<%@ page language="java" contentType="text/html;charset=GB2312"
    pageEncoding="GB2312"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=GB2312">
<title>Insert title here</title>
<base  href="<%=(request.getContextPath()+"/")%>"/>
<script>
	function createXMLHTTPRequest() {   
                 //1.创建XMLHttpRequest对象   
                 //这是XMLHttpReuquest对象无部使用中最复杂的一步   
                 //需要针对IE和其他类型的浏览器建立这个对象的不同方式写不同的代码   
                 var xmlHttpRequest;
                 if (window.XMLHttpRequest) {   
                     //针对FireFox,Mozillar,Opera,Safari,IE7,IE8   
                    xmlHttpRequest = new XMLHttpRequest();   
                     //针对某些特定版本的mozillar浏览器的BUG进行修正   
                     if (xmlHttpRequest.overrideMimeType) {   
                         xmlHttpRequest.overrideMimeType("text/xml");   
                     }   
                 } else if (window.ActiveXObject) {   
                     //针对IE6,IE5.5,IE5   
                     //两个可以用于创建XMLHTTPRequest对象的控件名称,保存在一个js的数组中   
                     //排在前面的版本较新   
                     var activexName = [ "MSXML2.XMLHTTP", "Microsoft.XMLHTTP" ];   
                     for ( var i = 0; i < activexName.length; i++) {   
                         try {   
                             //取出一个控件名进行创建,如果创建成功就终止循环   
                             //如果创建失败,回抛出异常,然后可以继续循环,继续尝试创建   
                            xmlHttpRequest = new ActiveXObject(activexName[i]); 
                            if(xmlHttpRequest){
                                break;
                            }
                         } catch (e) {   
                         }   
                     }   
                 }   
                 return xmlHttpRequest;
             } 
             
             function get(str){
                var req = createXMLHTTPRequest();
                if(req){
                    req.open("GET", "EncodingServlet.do?str="+str, true);
                    req.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=gbk;");      
                    req.onreadystatechange = function(){
                        if(req.readyState == 4){
                            if(req.status == 200){
                                alert("success");
                            }else{
                                alert("error");
                            }
                        }
                    }
                    req.send(null);
                }
            }  
            
             function post(str){
                var req = createXMLHTTPRequest();
                if(req){
                    req.open("POST", "EncodingServlet.do", true);
                    req.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=gbk;");   
                    req.send("str="+str);
                    req.onreadystatechange = function(){
                        if(req.readyState == 4){
                            if(req.status == 200){
                                alert("success");
                            }else{
                                alert("error");
                            }
                        }
                    }
                }
            }
</script>
</head>
<body>
	<% 
		String str = "我";
	%>
	<a href="EncodingServlet.do?str=<%=str %>">超链接----get传递<%=str %></a><br>
	form---get传递<%=str %>:
	<form action="EncodingServlet.do" method="get">
		<input name="str" value="<%=str %>" type="text"/>
		<input value="提交" type="submit"/>
	</form>
	<br>
	form---post传递<%=str %>:
	<form action="EncodingServlet.do" method="post">
		<input name="str" value="<%=str %>" type="text"/>
		<input value="提交" type="submit"/>
	</form>
	<br>
	<a href="javascript:void(0)" οnclick="get('<%=str%>')">ajax---get传递<%=str %></a><br>
	<a href="javascript:void(0)" οnclick="post('<%=str%>')">ajax---post传递<%=str %></a>
</body>
</html>

2, EncodingServlet

package com.web.test;

import java.io.IOException;
import java.io.PrintWriter;


import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class TestServlet
 */
public class TestServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;

	protected PrintWriter out;
	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		try {
			//获取参数
			String str = request.getParameter("str");
			//获取客户端指定的解码方式
			String encoding = request.getCharacterEncoding();
			System.out.println("解码字符集:"+encoding);
			System.out.println(str+"-------"+to16Byte(str,encoding));
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		doGet(request, response);
	}
       //根据浏览器指定的服务端解码的类型,将以 request.getParameter("str");这种形式接收到的字符还原为二进制字节码
       public static String to16Byte(String str,String decode) throws Exception{
		if(decode == null)decode="ISO-8859-1";
		byte[] b = str.getBytes(decode);
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < b.length; i++) {
			int high = (b[i] >> 4) & 0xf;
			int low = b[i] & 0xf;
			sb.append(Character.forDigit(high, 16)).append(Character.forDigit(low, 16));
		}
		return sb.toString();
	}

}

3, 在web.xml中配置如下

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
  <servlet>
  	<servlet-name>EncodingServlet</servlet-name>
  	<servlet-class>com.web.test.EncodingServlet</servlet-class>
  </servlet>
  <servlet-mapping>
  	<servlet-name>EncodingServlet</servlet-name>
  	<url-pattern>/EncodingServlet.do</url-pattern>
  </servlet-mapping> 
</web-app>

在firebox中Encoding页面如下:

页面的文字编码:



咱们点击超链接发送一个get请求给服务器,这个时候抓的包如下:



从这个包的内容上可以看出,参数被浏览器按照GBK进行了编码,并直接拼在了url后面。至于说%是怎么会事,这个是URLEcode干的活,对我们的编码解码没什么影响,
这里就不深究了。

我在做这个实验的时候,我就很好奇,为什么浏览器就默认地将参数按照GBK进行了转码呢,后来我找了下,发现如果我要是将Encoding.jsp中的contentType改为contentType="text/html; charset=UTF-8"这个时候页面上的文字编码就变成了UTF-8。而且点击超链接时,传给服务器的参数也按照UTF-8进行了编码,并且这个时候地址栏也将按明文进行显示。效果如下图:

这个时候页面上的文字编码也将由gbk变为UTF-8:

这个时候点击超链接之后的效果:




好,咱们将jsp上的contentType="text/html; charset=UTF-8"改回contentType="text/html; charset=GB2312"继续实验。

这个时候浏览器发送给服务端的参数将按照GBK进行编码,而且请求头中并没有指定contentType,意思也就是说浏览器没有指定服务器端该用什么编码来解码。学过web开
发的同学应该知道,当浏览器没有指定服务器用什么编码来解码的时候,tomcat6和tomcat7的做法是用ISO-8859-1来默认解码,很显然ISO-8859-1是不能识别出GBK编码出的二进制字节码的,所以这个时候服务器端将会出现乱码现象。我为了测试,在服务端写了个to16Byte(String name)的方法,目的就是根据浏览器指定的服务端解码的类型,将以 request.getParameter("str");这种形式接收到的字符还原为二进制字节码。

这个时候服务端输出如下:


根据结果显示,很显然虽然服务端用ISO-8859-1去解码浏览器传输过来的二进制字节码的时候,出现无法识别而乱码的现象,但还原出的二进制字节码确是完好无损的。
所以这个时候是可以通过自己还原出的二进制字节码,重新去解码得到不乱码的字符的。

form表单的get请求和超链接发出的get请求几乎一模一样,这里就不另作解释了。(在utf-8编码下在地址栏仍然是明文显示。)仅仅展示下form表单get请求抓到的数据包。


根据抓到的包而言,很显然数据也是用GBK进行编码的,也没有在请求头中指定服务端的解码类型,所以数据到了服务端,依然会用ISO-8859-1来解码,所以这个时候依然会乱码。

再看看form表单post请求的数据包:


不过与get请求不同的是,post请求不会把参数拼在url上,也不会在地址栏显示。而是整个放在了消息体里,跟在请求头之后,并按照键值对的形式用&进行拼接。

这里只有一个参数,所以看起来不是很明显。

在服务器端输出:


很显然post请求也和get请求一样使用GBK来编码传输的参数,然后在服务器端也是用ISO-8859-1来编码。

综上这里先得出结论,在firebox和tomcat6.0的基础上,原生的form表单发送的get请求和post请求,以及超链接发送的get请求,都会根据jsp上的contentType指定的编码
对参数进行编码,而且默认情况下,浏览器不会指定服务端用什么编码进行解码,服务端这个时候会用ISO-8859-1进行解码,解码肯定是不能成功的,所以会出现乱码情况。
不过情况还好,ISO-8859-1解码浏览器传过来的二进制编码的时候,并不会修改二进制字节码,传输的信息并不会丢失。

再看ajax,我在写ajax的时候,不管是get请求,还是post请求,我都指定了服务器端的解码类型为gbk。

服务器端输出:


大家这个时候可以看到,通过ajax发送get请求的时候,浏览器就不再管你的jsp中contentType指定的是什么编码了,一律用UTF-8来进行参数的编码。这个时候请求头中出现了
我们定义的contentType,服务器也正常解析出了浏览器给指定的解码类型gbk,然后后台就去用这个gbk解码浏览器传输过来的UTF-8编码后得到的二进制字节码,结果呢,
出现了乱码。而且这种乱码是不可逆的。如上图UTF将"我"这个字符编码为E68891,而服务器端用GBK解码之后得到的字符的二进制字节码为E6883f。最终显示的字符为"鎴?"
这里面可以看出来,UTF-8以三个字节去编码一个"我"字,而GBK用两个字节来编码一个汉字。所以GBK去解码E68891的时候,是两个字节两个字节的解码的,而E68891只有
三个字节,前两个字节被gbk解码成了一个汉字,而后面的一个字节由于不能识别,就被gbk用3f替换掉了。所以这种时候,发生了字节丢失,所以这种情况下的乱码,不可
恢复。那有没有什么好的解决办法呢,其实我写到这里的时候,就已经想出来了,三个解决办法,第一个就是,直接在发送请求的时候,指定服务端的以UTF-8来解码,第二
个就是,在发送请求的时候不指定服务端以什么来解码了,直接让服务端以ISO-8859-1来解码,这样字节不会丢失,自己可以在服务端恢复。第三种办法,就是在浏览器端,
不指定服务器端以什么来解码,而是在服务端里,在你获取这个参数之前request.setCharacterEncoding("UTF-8");设置一下解码方式为utf-8就可以了。这三种方法经测试都有效。

服务器端输出:


当使用ajax发送post请求的时候,我们也在请求头里指定了服务端的解码字符集为gbk。但是你可以看到浏览器,并不理会我们做的设置,照样是发出一个UTF编码的二进制
字节码,并且浏览器自己指定了服务器端必须得用UTF-8这个编码来解码。所以ajax的post请求在默认情况下,一定不会乱码。

经测试在ie8中超链接依然会根据jsp中定义contentType来指定编码,但是不管是gbk来编码,还是UTF-8来编码,浏览器地址栏都会显示为明文。
经测试在ie8中form发送的get请求会根据jsp中定义contentType来指定编码,但是不管是gbk来编码,还是utf-8编码,浏览器地址栏都不会显示明文。
经测试在ie8中form发送的post请求会根据jsp中定义contentType来指定编码。
经测试在ie8中ajax发送的post请求一样是用utf-8编码,但是在ie8中可以指定用什么来解码,而不是像firebox那样浏览器自己指定用utf-8来解码。

还有一点就是如果我用ajax的get请求以UTF-8编码的二进制字节码,传给浏览器一个字符串"我的",并指定服务器用gbk去解码,这个时候由于"我的"被UTF-8编码成E68891E79A84
一共是6个字节,这和GBK用两个字节编码汉字不谋而合,所以这个时候如果用GBK去解码这个二进制字节码的时候,由于位数和GBK相符合,不会造成字节丢失的现象。所以这个时
候是可以手动还原出二进制的字节码,然后解码成字符的。









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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值