ASP.NET乱码深度剖析

写在前面

在Web开发中,乱码应该算一个常客了。今天还好好的一个页面,第二天过来打开一看,中文字符全变“外星文”了。有时为了解决这样的问题,需要花上很长的时间去调试,直至抓狂,笔者也曾经历过这样的时期。有时虽然是“侥幸”解决了,但对其中的原理却一知半解。

为了弄清楚这个问题,今天查了大半天的资料、测试。现把这些点滴记录下来,以激励自己重视基础,同时和大家分享一下,望大家不吝批评指正。

预备知识

先介绍一些字符编码方面的基本知识,如果你对这些已经比较了解了,请直接跳过此节。

1.       字符集与字符编码概述

简单来说,字符集就是与特定区域相关的一系列有效字符的有序集合,比如字母、数字、标点符号等。注意关键字“有序”,表明集合中的每一个字符都是具有唯一数字编号(码值)的。不同国家使用的语言文字、符号不一样,相应的字符集必定也不一样。比如中国使用汉字,美国使用英语,韩国使用韩文,等等。

字符集是为了信息交互而设计的,最终还是要转化成计算机的表示法。我们知道,计算机只认识0和1,它对字符集符号不感冒。所以,我们必须想办法把字符转化为0和1的序列。我们知道,计算机最小的存储单位是位(bit),程序中一般使用的最小单位是字节(byte)。为了把字符存储到计算机中,我们就要考虑用几个byte几个bit,考虑每一个bit上是0还是1,考虑存储和读取效率,并且必须兼顾整个字符集,这就是字符编码

一句话,字符集只关心字符的定义,而字符编码负责字符的存储和读取细节。用三层模式来打比喻的话,字符集是模型层,而字符编码是业务层。注意:一般常说的GB2312、GBK等其实同时包含了这两方面的定义

2.       常用中文字符编码简介

GB2312

GB2312的全称是《信息交换用汉字编码字符集-基本集》,由国家标准总局于1980发布,1981年5月1日施行,中国大陆、新加坡使用此编码。基本集收录了6763个汉字,只能显示简体汉字。

GBK

1995颁布,全称是《汉字编码扩展规范》。在GB2312的其他上,增加了繁体汉字,支持ISO/IEC 10646-1 和GB-13000-1的全部中、日、韩(CJK)字符,共20902个。向下兼容GB2312。

GB18030

全称是《信息交换用汉字编码字符集基本集的扩充》,目前两个版本,分别于2000年和20005年颁布。该字符集收录了70000多个汉字,包括了藏、蒙古、维吾文等少数民族字符,是我国计算机系统必须遵循的基础性标准之一。向下兼容GBK和GB2312。

BIG5

台湾和港台地区使用的汉字编码,俗称“大五码”,共收录了13060个汉字。

UTF-8

这是目前使用最多的一种Unicode编码,是Visual Studio内置的编码,相信大家一定都不陌生。根据字符码值的不同,可能用1、2、3个字节表示。

注意,编码之间一般都不是兼容的。其它编码在此不作介绍,若想进一步了解字符编码,请看我收藏的一篇文章:http://blog.csdn.net/tomysea/article/details/6712344

3.       字符串、字符数组和字节数组

C#中的字符串(string)和字符(char)其实都是对象,他们有相应的类String和Char,string和char只是这两个类的一别名而已,内部都是采用Unicode码值表示。请注意我说的是码值,不是编码。

我们已经知道,Unicode的字符大多是多字节表示的,那么一个char就得用几个byte来表示。这里我要说的重点是,使用不同的编码表示字符串,其对应的byte可能是不一样的。请看下面的代码,注意输出字节数部分。UTF-8编码的字节数是22,而GB2312编码的字节数是16。

 

 

string title = "2012真的来了吗?";      //字符串 

char[] chars = title.ToCharArray();     //字符数组 

byte[] bytes = System.Text.Encoding.UTF8.GetBytes(title); 

Response.Write(chars.Length + " ");     //10  (字符数) 

Response.Write(bytes.Length + " ");     //22  (UTF-8编码的字节数) 

 

bytes = System.Text.Encoding.GetEncoding("GB2312").GetBytes(title); 

Response.Write(bytes.Length + " ");     //16  (GB2312编码的字节数)   

 

从http请求响应模型说起

http是一个请求/响应的模型,这个我们大家都知道。http请求可以分为请求头和请求实体两部分,相应地http响应也可以分为响应头和响应实体。请求头或响应头是浏览器与Web服务器通信用的(假定用浏览器访问Web服务器),而实体则是实际发送的数据,比如Form表单的数据、Ajax提交的数据、传回来的html代码等。不管是浏览器还是Web服务器,在发送实体前都会把它转换为字节流。明白这一点很重要,因为涉及字节流就一定会与字符编码有关。

从上面的请求响应模型中我们可以得出一个结论:请求和响应编码必须严格保持一致!为什么呢?这很好理解,浏览器和Web服务器是要通信的,如果编码不一样的话,势必会造成许多“误解”。假设浏览器是中国人(不懂E文),而Web服务器是美国人,他们两个的“编码”(语言)不一致,悲催的结局不言而喻。

ASP.NET中请求响应编码的设置

你可以在machine.config或web.config文件指定全局配置,也可以在页面级特别指定。如果你未手动指定且machine.config中也为空,则默认会读取计算机上“区域选项”中的设置。

1.       全局配置

在machine.config或web.config文件(根目录或者子目录都有效)中的system.web节点中配置globalization节点。如果在根目录下的web.config配置,则会响应整个网站,若只是在子目录下配置,则只会响应该目录及其子目录。 详细配置如下:

 

 

 

<system.web> 

    <globalization fileEncoding="utf-8" requestEncoding="utf-8" responseEncoding="utf-8"/> 

<!--按顺序是:文件编码 请求编码 响应编码--> 

<!—-fileEncoding会在后面说到--> 

 <!--后面还有其它配置-->

 

 

2.       页面级的配置

在aspx页面的Page指令中设置响应编码

 

 

 

<%@ Page Language="C#" AutoEventWireup="true" ResponseEncoding="utf-8"  

CodeBehind="byte.aspx.cs" Inherits="DevKit.Web.test.charset._byte" %>

 

在aspx页面中手动指定meta标签

 

 

<meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> 

在后台cs文件中配置

 

 

Request.ContentEncoding = System.Text.Encoding.UTF8;    //请求编码 

Response.ContentEncoding = System.Text.Encoding.UTF8;   //响应编码   接下来,我们从几个示例中去体验乱码,从而总结出解决乱码的一般方法。

测试环境

操作系统:Windows XP Professional SP3 雨林木风版

开发环境:Visual Studio 2008 专业版 + SP1(.NET 3.5)

Web容器:VS集成的Development Server

浏览器:IE8 、FireFox 5

实例分析与研究

实例1  aspx页面提示意外的字符“XXX”,引号里面是乱码

背景

网站配置了在根目录配置了文件、请求、响应编码都为utf-8,页面成功编译,没有任务错误。详细错误见下图:

html代码

 

 

<div> 

    aspx页面中的中文 

    <br />后台的中文变量:<em><%=汽车%></em> 

</div> 

后台代码

 

view plain

public partial class _byte : System.Web.UI.Page 

    protected string 汽车= "我是凯迪拉克"; //别怀疑,中文变量是可以的:) 

    protected void Page_Load(object sender, EventArgs e) 

    { 

        //... 

    } 

}   

 

 

分析与解决

既然web.config已经配置了一样的请求响应编码,而且页面级别也没设置,可以排除这方面的问题了。注意到文件编码是UTF-8,会不是会文件编码引起的呢?(提示:这里的文件编码指的是保存文件时指定的编码,点击“另存为…”,在弹出的窗口中选择“编码保存”可以看到)。果然,此aspx页面的保存编码为GB2312,与web.config文件不一样,把它修改为UTF-8。

小提示:UTF-8有两种编码:UTF-8(带签名)和UTF-8(无签名)。带签名的UTF-8会在文件的开头写入“EF BB BF”(16进制),以标示自己采用的编码格式,这个标志称为BOM(Byte Order Mark),即字节序。打个比方,UTF-8(带签名)戴了校徽的学生,就算不认识他的人,一看校徽就明白了;而UTF-8(无签名)则是没戴校徽的。这里的校徽就是我们说的BOM,一个能够表明自己身份的标志。

小结

这是因为文件的保存编码与当前网站指定的文件编码不一致引起的,所以最佳实践是:手动在web.config中指定文件编码,并确保所有页面的保存编码与web.config一致。

其实最容易出这种问题的是js和css文件,如果你用其它工具(比如DreamWeaver)来编写这些文件却采用不同的编码保存,一旦文件包含中文就可能出这样的错误,导致js脚本错误,css无效!

 

实例2    跨页post提交时接收的Form数据变成了乱码

背景

有两个页面,注册页面(register.html)和处理注册的页面(handle.aspx),注册页面的表单信息以post方式提交到handle.aspx。根目录的配置的文件编码、请求编码和响应编码都是UTF-8。

register.html页面的关键html

 

 

<head> 

    <title></title> 

    <meta http-equiv="Content-Type" content="text/html;charset=gb2312" /> 

</head> 

<body> 

<form id="form1" name="form1" action="handle.aspx" method="post"> 

    <input type="text" id="txtName" name="txtName" /> 

    <input type="submit" id="btnSubmit" value="Post" /> 

</form> 

handle.aspx页面关键后台代码

 

view plain

protected void Page_Load(object sender, EventArgs e) 

    string name = Request.Form["txtName"]; 

    Response.Write(name); 

}   

 

 

错误信息如下

分析与解决

这是在提交表单信息过程产生的乱码,这里就涉及http请求和http响应的编码问题。我们上面说过,在请求时浏览器会把表单信息按指定编码转化成字节流发向Web服务器,在服务器ASP.NET会把这些字节流按指定的编码解码,以取得表单信息。那么我们就要检查这两个页面的编码了。仔细检查之后,发现register.html有这么一行“<meta http-equiv="Content-Type" content="text/html;charset=gb2312" />”,这里手动指定了页面编码为GB2312。问题很有可能就出在这里了,把本行删除之后,handle.aspx页面成功接收到表单信息。

没错,这就是由于两个页面的编码不一样引起。让我们再深入一点,仔细看看问题是怎么一步一步产生的吧。register.html的编码为GB2312,当我们点击了“Post”按钮时,浏览器会把“我是中文”这几个字按GB2312的方式编码成字节流,然后提到到handle.aspx页面。handle.aspx没有手动指定编码,那么他将会采用web.config里面的配置,为UTF-8。它收到请求后,用UTF-8编码解码字符流。由于请求用的是GB2312,而接收用的却是UTF-8,这样就导致乱码的产生。通过下面这幅图可以看到这个过程。

小结

所有的页面(不管是aspx,还是html,或其它)都必须使用相同的编码。如果涉及跨页提交,不管是get还是post,更应该严格保持相关页面编码的一致性。特别是跨站点提交时,更应该注意!

实例3  cookie存取发生乱码

背景

这是一个旧项目,现在决定增加一个自动登录的功能。详细过程是这样的:

在登录页面,用户登录成功后把用户名写到cookie中。这样,当用户再次访问时,就可以根据cookie判定用户是否已登录,从而实现自动登录。

登录成功后cookie是这样保存的

 

 

 

view plain

string userName = "cookie大侠";     //待保存的用户名 

userName = HttpUtility.UrlEncode(userName);     //编码特殊字符,如中文 

HttpCookie cookie = new HttpCookie("userName", userName); 

Response.Cookies.Add(cookie); 

判断用户是否已登录时,代码是这样的

 

view plain

string userName = Request.Cookies["userName"].Value; 

userName = Server.UrlDecode(userName); 

Response.Write(userName);    //总是获取不到cookie,所以决定打印出来看看  

结果在测试读取cookie的时候,页面输出了乱码,如下图:

分析与解决

全球化信息是这样配置的

<globalization fileEncoding="utf-8" requestEncoding="gb2312" responseEncoding="gb2312"/>

所有页面的保存编码都为UTF-8,请求响应编码是GB2312。再次声明,这是一个旧项目,任何改动都必须向后兼容。

首先从全球化配置里看到三种编码不一致,初步怀疑是这里引发的问题。尝试把请求、响应编码都修改为UTF-8,再次运行页面,乱码消失了。窃喜,小样,原来问题就在这里。但是,这样一来,在其它很多页面中却莫名其妙出现了乱码。这…,心里好不容易生起的一股小火,却被这样无情的浇灭了。冷静地回忆了下,自己只改了请求响应编码,其它地方没动过啊。于是改回来原来的GB2312,其它页面运行也正常了。如果把编码改为UTF-8的话,就不能兼容以前的页面,且会导致一连串的问题,全部修改将是一个非常艰巨的任务。

仔细检查了几遍所有页面的编码,都没有手动设置过,那应该都是读取配置文件的GB2312。新功能急于上线,交期一秒一秒狠狠地砸着绷紧的神经。怎么办呢?难道是GB2312不支持cookie存取吗?搜索了大量资料后,也没有发现什么端倪,感觉这也不太可能,毕竟中国这么多GB2312的网站…。

现在可以确定的是编码没有任何问题!那问题会出现在哪里呢?是自己写的代码有问题吗?仔细检查了之后,就发现了一点:

userName = HttpUtility.UrlEncode(userName);     //编码特殊字符,如中文
userName = Server.UrlDecode(userName); 

红色部分不一样,从智能提示中可以看到这样的说明。

原来,两个调用的是不同类的方法。一个是HttpUtility的方法,另一个是HttpServerUtility的方法,不小心还真看不出来。于是把Server.UrlEncode()换成了HttpUtility.UrlEncode(),重新运行测试页面,页面正常显示。既然都已经到这里了,我们不防看看这两个方法的实现细节有哪些差异吧。打开Reflector,找到System.Web.HttpUtility中的UrlEecode方法。嘿嘿,终于被我发现了这样一个片段(我把反射后的代码加上了注释):

 

//这是HttpUtility的UrlEncode方法 

public static string UrlEncode(string str) 

    if (str == null) 

    { 

        return null; 

    } 

    return UrlEncode(str, Encoding.UTF8);   //默认采用UTF-8编码 

接着看看HttpServerUtility.UrlDecode()方法,Page.Server其实是HttpServerUtility的一个实例,但它并不是在Page类中实例化的,而是在HttpContext中。

 

view plain

//HttpServerUtility中的UrlDecode方法 

public string UrlDecode(string s) 

    //注意这里的差异,会优先使用context中的编码, 

    //也就是我们配置了的GB2312 

    Encoding e = (this._context != null) ?  

        this._context.Request.ContentEncoding : Encoding.UTF8; 

    return HttpUtility.UrlDecode(s, e); 

这下总算明白为什么了,存cookie时调用的是HttpUtility.Encode()方法,将以UTF-8编码。而读取时调用的是HttpServerUtility的Decode()方法,它会根据当前上下文采用GB2312方法,自然无法正确解析UTF-8编码的字符串了。

 

小结

 

在调用方法时,要成对调用。比如编码时调用的是HttpUtility.UrlEncode(),那么在解码时你就必须调用HttpUtility.UrlDecode(),保持这种一致性,有利于减少错误的发生。

 

必须充分考虑代码的向后兼容性。

 

如果你有兴趣,去看看微软是怎么实现这些方法的吧,这样对你的帮助会很大。

 

实例4   jQuery Ajax请求传中文参数导致乱码

 

背景

 

老项目(实例3提到的)的需求又来了,大致要求是这样的:在前台页面中,要根据当前商品的名称去异步获取它的详细说明(当然了,一般是按id等主键获取的,这里我只是做一个假设),当用户点击时就显示。于是决定用jQuery 的Ajax去做,简单方便且功能强大。由于jQuery的易用性,代码一下子就写好了,后台采用ashx处理ajax请求,先看看是怎么实现的吧。

 

前台页面的代码

 

view plain

<head runat="server"> 

    <title>产品列表页</title> 

    <script src="../../js/jquery-1.4.2.js" type="text/javascript"></script> 

    <script type="text/javascript"> 

        $(function() { 

            $(‘#product‘).click(function() { 

                var productName = this.innerHTML;   //产品名称 

                $.get(‘getInfo.ashx‘, { name: productName }, function(description) { 

                    alert(description); //显示详细说明 

                }); 

            }); 

        }); 

    </script> 

</head> 

<body> 

    <form id="form1" runat="server"> 

    <div> 

        <a id="product" href="javascript:void(0);">奋斗牌牙膏</a> 

    </div> 

    </form> 

</body> 

ashx的关键处理代码

 

view plain

string name = context.Request.QueryString["name"]; 

context.Response.Write(name + ": ");   //调试用,看参数传递是否正确 

if (name != null && name.Trim() == "奋斗牌牙膏") 

    context.Response.Write("奋斗牌,你懂的!\n每天一点点,强身健体,天气再冷,牙也不颤!"); 

}   

 

但不幸的是,运行页面时又出乱码了,无法正确获取产品名称。

分析与解决

项目的所有配置还是和例3一样,请求响应编码都是GB2312。首先也是尝试把web.config文件的请求编码改为UTF-8,运行页面,可以正常显示。但这样改肯定是不行的,必须考虑其它页面的兼容性。有了例3的经验,现在已经知道问题出现在哪个部分了。必定是请求的编码和解析请求的编码不一致产生的!现在的重点是找出产生这个不一致的原因。

仔细检查了产品页面的编码,没任何任何与编码相关的设置,所以这个页面肯定也是用web.config中的请求编码GB2312。在ashx中也没有设置,它肯定也是用GB2312来解析请求。理论上应该不会出现乱码的啊。

为了能看清楚细节,打开抓包工具Fiddler,监测Ajax请求,看到的请求头是这样的。

可以看到,$.get()方法自动把请求参数附加到url里,并且实施了url编码。所以我们就得$.get()这个方法入手,看看请求参数是如何被附加到url中的。打开jquery.1.4.2.js,一步一步查找,发现了这样一个方法:

 

 

 

function add( key, value ) { 

    // If value is a function, invoke it and return its value 

    value = jQuery.isFunction(value) ? value() : value; 

    s[ s.length ] = encodeURIComponent(key) + "=" + encodeURIComponent(value); 

 

原来是对参数调用了encodeURIComponent()方法来进行url编码的,但这个方法的实现细节是看不到的,不像.NET里可以反编译。在网上搜索了很多资料,但很少有资料提到它的工作细节。

好吧,我们就自己实践一下吧。还是用“奋斗牌牙膏”这几个字来测试,先看看用不同的字符编码来对它实行url编码后产生的字符串是什么吧。下面是我的测试结果:

把这些结果和Fiddler的抓包结果对比一下(看url中的参数),发现当采用UTF-8进行UrlEncode时,两者的结果是一致的。

可以确定encodeURIComponent()是采用UTF-8编码来进行url编码的。不仅仅是get方法,jQuery实现的所有ajax方法都是一样的,采用UTF-8字符编码对参数进行url编码。到这里,问题产生的原因已经很明朗了,请求的字节流采用了UTF-8编码,而服务器端的ASP.NET却采用GB2312来解析,肯定解析不到了。

因此,我们可以在ASP.NET中手动指明解析请求的编码。修改后的ashx代码如下:

 

 

 

context.Request.ContentEncoding = System.Text.Encoding.UTF8; //指明Request使用的编码是UTF-8 

string name = context.Request.QueryString["name"]; 

context.Response.Write(name + ": ");   //调试用,看参数传递是否正确 

if (name != null && name.Trim() == "奋斗牌牙膏") 

    context.Response.Write("奋斗牌,你懂的!\n每天一点点,强身健体,天气再冷,牙也不颤!"); 

再次点击页面,成功返回所需的内容。

总结

使用jQuery的ajax方法时,一定要记得它是采用UTF-8编码数据的。

把http的请求响应过程弄清楚。

 

通过上面4个例子,你应该对乱码产生的原因有所了解了吧。记住一点,最根本的原因是字符编码不一致产生的。然后顺着这条线索,顺藤摸瓜,一步一步把确切的原因找出来。方法很重要,你不应该为了得到了答案而高兴,而应该真正弄懂问题产生的原因,这样你才能真正成长。这时体会一下“授人以鱼不如授人以渔”这句话的魅力吧。

后记

    其实问题真的不可怕,适当地来些问题,来些压力,会让你更好地成长。不要只是想得到答案,更重要的是积累获得答案的方法。

写这篇文章的过程中,遇到了不少问题,感谢那么多的前辈们分享了他们的经验,让我得以站在他们的肩膀上。

 

参考资料:

字符集与字符编码简介:http://www.2cto.com/kf/201110/109312.html

HTTP 请求报头详解:http://www.2cto.com/kf/201110/109311.html

encodeURIComponent()导致乱码解决:http://www.2cto.com/kf/201110/109310.html

unicode编码 http://baike.baidu.com/view/40801.htm

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。