寫在前面
在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
![]() |
不含病毒。www.avast.com |