你不知道的頁面編碼,瀏覽器選擇編碼,get,post各種亂碼由來

笨豬跳的饅頭發表於2014-11-08

asp.net頁面編碼和瀏覽器的選擇編碼

每個asp.net的朋友都知道,在新版本的visual studio,在沒有任何設定的情況下,新建頁面時的預設編碼為utf-8

我們可以從兩個地方可以看出:

第一:開啟aspx頁面,“檔案”->“高階儲存選項”,如下圖,可以看出編碼為:Unicode(UTF-8帶簽名)

第二:找到aspx存放路徑,用系統自帶的文字編輯器開啟,然後“檔案”->”另存為”,如下圖,可以看出編碼為UTF-8

很多時候我們有些疑問,我們經常在aspx頁面的<meta http-equiv=”Content-Type” content=”text/html; charset=gb2312″ />強制把carset改為gb2312

然後我們在“檔案”->“高階儲存選項”,可以看出編碼為:GB2312(如果你前面把carset改為gb2312,vs會自動在高階儲存選項中進行繫結改變),然後編譯執行後,右擊html“檢視源”發現<meta http-equiv=”Content-Type” content=”text/html; charset=gb2312″ />沒有變化,這時候一切正常

下面以IE為例:我們以為此時 “右擊瀏覽器”->“編碼” 看到的是 瀏覽器會選中簡體中文(GB2312),但是事實上,你看到的還是選中的Unicode(UTF-8)  (再勾選了‘自動選擇’前提下)

現象已經很明顯,但是需要驗證瀏覽器為何會這樣,F12除錯瀏覽器(如下圖),我們發現content-type竟然是“text/html;charset=utf-8”!

這個現象至少說了兩個問題點:

1、asp.net機制至少在某個地方改變了response的ContentEncoding,導致雖然html頁面程式碼上看到的設定<meta http-equiv=”Content-Type” content=”text/html; charset=gb2312″ />並沒有生效

2、瀏覽器再自動選擇編碼方式的時候不會優先根據html原始碼中的所展示的<meta http-equiv=”Content-Type” content=”text/html; charset=gb2312″ />程式碼來決定選擇什麼編碼方式,很明顯,以上的現象證明瀏覽器是優先根據“響應標頭-response header”中的鍵為“Content-Type”的值來自動選擇判斷,導致html中的所看到的<meta http-equiv=”Content-Type” content=”text/html; charset=gb2312″ />形同虛設。

以上兩個問題點很快得到論證:

問題1、在任意新建一個測試頁面,在第一個“}”處設定斷點,然後命中斷點後再“即時視窗”中寫入“Response.ContentEncoding.EncodingName”,按enter執行,輸出什麼?沒錯:”Unicode (UTF-8)”

protected void Page_Load(object sender, EventArgs e)
{

}

如果瞭解asp.net生命機制的朋友知道,在執行到Page_Load之前已經執行了很多潛在的初始化事件,類似:Page_Init,LoadViewState, LoadPostData等等,可以想象一定是在某個地方系統為響應頁面指定修改了ContentEncoding的值,也就是“響應標頭-response header”中的鍵為“Content-Type”的值為“UTF-8”

我們不妨做一個測試,上面說過,我把<meta http-equiv=”Content-Type” content=”text/html; charset=gb2312″ />的carset改為gb2312,是沒有效果的,那麼我如果在Page_Load事件中如下寫上程式碼:

protected void Page_Load(object sender, EventArgs e)
{
        Encoding gb2312 = Encoding.GetEncoding("gb2312");
        Response.ContentEncoding = gb2312;
 }

即我在load事件中再次強制性把響應標頭中的“Content-Type”改為gb2312,那麼瀏覽器表現如何呢?

這正是我們想看到的,我相信很多朋友有過中文亂碼的情況,我先不說具體亂碼的解決方案,但是至少搜尋發現很多解決方案是在web.config下新增如下節點,即把網站內所有網頁的請求編碼和響應編碼都改為utf-8

<system.web>
<globalization requestEncoding="utf-8" responseEncoding="utf-8" />
</system.web>

其實,上面案例其實只是單個頁面的修改response Encoding為gb2312,我們也可以在web.config中新增<globalization requestEncoding=”gb2312″ responseEncoding=”gb2312″ />,即整個網站有效

問題2、瀏覽器編碼方式是根據“響應標頭-response header”中的鍵為“Content-Type”的值來自動選擇判斷,而不會簡單的根據你在html中看到的標籤值<meta http-equiv=”Content-Type” content=”text/html; charset=gb2312″ />來判定,雖然這個標籤一般情況下會寫入header,但是有時候會被暗中修改掉,導致html中看到的和除錯捕捉到的Content-Type不一致的情況

當然在老版本的ie中,有時候出現的頁面全部為空白,右擊ie瀏覽器編碼發現沒有勾選“自動選擇”的情況下會出現這種白屏現象,那不是本文討論的範圍,但是簡單的說下原因(拷貝):老版本的ie瀏覽器解析網頁編碼時以HTML內的標籤優先,而後才是HTTP Header內的訊息,而mozilla系列的瀏覽器則剛剛相反,由於UTF-8為3個位元組表示一個漢字,而普通的GB2312或BIG5是兩個。頁面輸出時,由於上述原因,使瀏覽器解析、輸 出<title$amp;>amp;$lt;/title>的內容時,如果在</title>前有奇數個全形字元時,IE把UTF-8當作兩 個位元組解析時出現半個漢字的情況,這時該半個漢字會和</title>的<結合成一個亂碼字,導致IE無法讀 完<title>部分,使整個頁面為空百輸出。而這個時候如果察看原始檔的話,會發現實際上整個葉面全部已經輸出了

解決方法:將<meta http-equiv=”Content-Type” content=”text/html; charset=utf-8″ />放在<title>測試標題</title>之前(好像現在新建網頁預設都在title之前)

asp.net URL引數編碼

幾乎所有的朋友都會遇到中文情況下亂碼的問題,其原因到底是為何?

我先不說亂碼問題,先說get和post的區別,幾乎沒有人不知道

我們在新建asp.net頁面時,是很少去對form進行修改的,即保持預設的<form id=”form1″ runat=”server”>,可是編譯執行後檢視程式碼發現變成<form method=”post” action=”Default2.aspx” id=”form1″>

很顯然,asp.net預設會為form的method寫上post,但是需要注意的是如果僅僅是單純的html頁面,form預設的method是get

我這邊可以舉一個例子詮釋一些無關緊要但是又比較重要的東西:

情況1(post):

瀏覽器選擇編碼 :  utf-8

編譯前的aspx  :  <form id=”form1″  runat=”server”>

執行後的html  :  <form id=”form1″ action=”Default2.aspx” method=”post”>

點選伺服器按鈕(按鈕文字:阿道夫):F12在請求正文中有如下圖內容

情況2(get):

瀏覽器選擇編碼 :  utf-8

編譯前的aspx  :  <form id=”form1″  runat=”server” method=”get”>

執行後的html  :  <form id=”form1″ action=”Default2.aspx” method=”get”>

點選伺服器按鈕(按鈕文字:阿道夫):F12在請求正文中有如下圖內容

其實,情況1和情況2的對比,並不是我今天想說的意圖,但是我還是要稍微順帶說下:

1、我們可以看到get方式的提交,引數僅僅是拼接在url後面,然後直接向web伺服器請求,所以我們圖中“請求標頭-request header”中就可以看到引數的值,而post可以從圖中看到,在“請求標頭”中並看不到值,而在“請求正文”中看到值,說明post提交時值是包裝在請求的body中,傳送給伺服器,然後向伺服器請求資料

2、在asp.net中,圖中可以看到不管是get還是post,提交形式不一樣,內容確是一樣的,本文僅為測試,所以內容相對較少,但是看起來也非常的長了,如果用get提交方式,這就帶來隱患,瀏覽器到底支援多長的uri,web伺服器到底支援多長的uri,反正是有限制的(具體長度見:http://www.cnblogs.com/henryhappier/archive/2010/10/09/1846554.html),不僅僅是長度,資料量也是有限制,get資料量較小,不能大於2KB。post傳送的資料量較大,一般被預設為不受限制。但理論上,web伺服器載體例如iis應該會有限制,比如IIS5的post最大傳輸量為100kb等

3、安全性,get更加容易暴露引數,而且會被儲存在瀏覽器的歷史記錄中,但是對於稍微專業點的人來說,post請求傳送的資料也是可以被捕捉到的

4、快取和seo優化等就不提了

5、編碼問題!!!(這邊上面說這麼多,就是為了最後一個“編碼問題”)

我將著重講解編碼問題:

在Form元素的語法中,EncType表明提交資料的格式
用 Enctype 屬性指定將資料回發到伺服器時瀏覽器使用的編碼型別。
一般是下面幾種型別:
application/x-www-form-urlencoded: 窗體資料被編碼為名稱/值對。這是標準的編碼格式。
multipart/form-data: 窗體資料被編碼為一條訊息,頁上的每個控制元件對應訊息中的一個部分。
text/plain: 窗體資料以純文字形式進行編碼,其中不含任何控制元件或格式字元。

假設此時使用get提交form方式:

瀏覽器則會用x-www-form-urlencoded的資料格式,雖然在F12瀏覽器除錯或者cs程式碼中的Request.ContentType都看不出來。注意如下是我的get提交的url:

GET /Default2.aspx?__VIEWSTATE=MGASeC9kBMq4iDCI2YLRzkZYqkKYhDhWH2jlP5mpv7idP8gAoNcy0T0y6g6wRvccP%2BFz%2FVx4HdMGwLLW%2BYPbJsMEOTMi5PjS7Ea66DmHQJc%3D&__VIEWSTATEGENERATOR=9BD98A7D&__EVENTVALIDATION=BFMAr0Q6mSwngMhaCLeScGaXywIIRlFClDYAnVhHprxOeifBIGWKNbsunWO9yVOAV6jWHW%2FJ4g2laHQpTvJe%2Fc7X8vralK3hyO5Y0nuiJkT%2FdfxEj9NnCb8S5BfNvZKXVJA%2FOy8yH4Bf9K5DN%2FRI9aDR3EFR86Zm6fN4iEkvJfc%3D&Button2=%E9%98%BF%E9%81%93%E5%A4%AB

我只看最後部分“Button2=%E9%98%BF%E9%81%93%E5%A4%AB”,這是我的伺服器按鈕“阿道夫”,這一串“%E9%98%BF%E9%81%93%E5%A4%AB”是“阿道夫”三個漢字編碼後的,究竟這個編碼方式到底是什麼?又是如何經常引起亂碼問題的呢?

首先:get只能向伺服器傳送ASCII字元,這是W3C組織規定的,所以任何引數最後都要以ASCII碼的形式傳遞,例如“Button2=%E9%98%BF%E9%81%93%E5%A4%AB”都是ASCII碼中的英文字元和符號等字元,沒有任何中文字元,其次編碼方式是根據當前網頁採用選擇的編碼來編碼,例如asp.net網頁使用的是utf-8碼,那麼“阿道夫”用utf-8的編碼後的十六進位制字串就是“E9-98-BF-E9-81-93-E5-A4-AB”,和上面提到的“%E9%98%BF%E9%81%93%E5%A4%AB”是不是非常的類似,只是多了百分號

如何檢視中文字元的十六進位制字串?方法:BitConverter.ToString(System.Text.Encoding.UTF8.GetBytes(“阿道夫”));

如果用本文一開始介紹的方法,在Page_Load中加上

Encoding gb2312 = Encoding.GetEncoding(“gb2312″);
Response.ContentEncoding = gb2312;

強制把當前頁面編碼改為gb2312,然後點選按鈕,那麼我們猜測在F12瀏覽器除錯時,get提交的url的最後部分一定不再是“%E9%98%BF%E9%81%93%E5%A4%AB”,

除錯後發現是:“%B0%A2%B5%C0%B7%F2”

那麼用BitConverter.ToString(System.Text.Encoding.Default.GetBytes(“阿道夫”))生成的值呢?答案是:B0-A2-B5-C0-B7-F2

這一切證明了url引數編碼方式是根據當前網頁採用選擇的編碼來編碼

然後我將在服務端接受引數,這邊有兩種情況

情況1、ok,我再次以get方式提交form,並是以utf-8編碼(預設),此時,我在服務端通過Request.QueryString["Button2"],我將得到“阿道夫”,一切正常

情況2、ok,我繼續以get方式提交form,並是以gb2312編碼(如何設定上文講過),此時,我在服務端通過Request.QueryString["Button2"],我將得到“������”,正如我願

為何一開始正常,後面會出現亂碼,我這邊做幾個假設:

假設1、對於情況1 的Request.QueryString["Button2"]沒有出現亂碼,我是否可以猜測微軟Request.QueryString內部自帶有解碼的操作?並且在這種情況下該操作是utf-8的解碼方式

假設2、對於情況2 的Request.QueryString["Button2"]出現亂碼,我是否可以猜測是因為微軟Request.QueryString內部自帶有解碼的操作,並按照utf-8解碼方式 解碼我用gb2312編碼的字元,這種不對稱的解碼肯定是錯誤的

如何驗證我的結論?路過秋天  的這篇文章寫的很詳細,我總結下大概就是:

反編譯Request.QueryString屬性,你會發現有這麼如下程式碼:最後深入到程式碼關鍵點:this._queryString.FillFromEncodedBytes(queryStringBytes, this.QueryStringEncoding);從FillFromEncodedBytes方法中可以看出呼叫HttpUtility.UrlDecode(bytes, num2, num3 - num2, encoding)的解碼方法,我們現在知道Request.QueryString內部實現是呼叫了HttpUtility.UrlDecode解碼的方法,那麼關鍵點就在HttpUtility.UrlDecode方法的第三個引數encoding到底是哪種解碼方式

public NameValueCollection QueryString
{
    get
    {
        this.EnsureQueryString();
        if (this._flags[1])
        {
            this._flags.Clear(1);
            this.ValidateHttpValueCollection(this._queryString, RequestValidationSource.QueryString);
        }
        return this._queryString;
    }
}
internal HttpValueCollection EnsureQueryString()
{
    if (this._queryString == null)
    {
        this._queryString = new HttpValueCollection();
        if (this._wr != null)
        {
            this.FillInQueryStringCollection();
        }
        this._queryString.MakeReadOnly();
    }
    return this._queryString;
}
private void FillInQueryStringCollection()
{
    byte[] queryStringBytes = this.QueryStringBytes;
    if (queryStringBytes != null)
    {
        if (queryStringBytes.Length != 0)
        {
            this._queryString.FillFromEncodedBytes(queryStringBytes, this.QueryStringEncoding);
            return;
        }
    }
    else
    {
        if (!string.IsNullOrEmpty(this.QueryStringText))
        {
            this._queryString.FillFromString(this.QueryStringText, true, this.QueryStringEncoding);
        }
    }
}

下面是FillFromEncodedBytes方法實現:

internal void FillFromEncodedBytes(byte[] bytes, Encoding encoding)
{
    int num = (bytes != null) ? bytes.Length : 0;
    for (int i = 0; i < num; i++)
    {
        this.ThrowIfMaxHttpCollectionKeysExceeded();
        int num2 = i;
        int num3 = -1;
        while (i < num)
        {
            byte b = bytes[i];
            if (b == 61)
            {
                if (num3 < 0)
                {
                    num3 = i;
                }
            }
            else
            {
                if (b == 38)
                {
                    break;
                }
            }
            i++;
        }
        string name;
        string value;
        if (num3 >= 0)
        {
            name = HttpUtility.UrlDecode(bytes, num2, num3 - num2, encoding);
            value = HttpUtility.UrlDecode(bytes, num3 + 1, i - num3 - 1, encoding);
        }
        else
        {
            name = null;
            value = HttpUtility.UrlDecode(bytes, num2, i - num2, encoding);
        }
        base.Add(name, value);
        if (i == num - 1 && bytes[i] == 38)
        {
            base.Add(null, string.Empty);
        }
    }
}

this.QueryStringEncoding是HttpUtility.UrlDecode解碼的關鍵,我們發現系統預設會先取globalization配置節點的編碼方式,如果取不到,則預設為UTF-8編碼方式

internal Encoding QueryStringEncoding
{
    get
    {
        Encoding contentEncoding = this.ContentEncoding;
        if (!contentEncoding.Equals(Encoding.Unicode))
        {
            return contentEncoding;
        }
        return Encoding.UTF8;
    }
}
public Encoding ContentEncoding
{
    get
    {
        if (this._flags[32] && this._encoding != null)
        {
            return this._encoding;
        }
        this._encoding = this.GetEncodingFromHeaders();
        if (this._encoding is UTF7Encoding && !AppSettings.AllowUtf7RequestContentEncoding)
        {
            this._encoding = null;
        }
        if (this._encoding == null)
        {
            GlobalizationSection globalization = RuntimeConfig.GetLKGConfig(this._context).Globalization;
            this._encoding = globalization.RequestEncoding;
        }
        this._flags.Set(32);
        return this._encoding;
    }
    set
    {
        this._encoding = value;
        this._flags.Set(32);
    }
}

得出結論:

在method為get的提交方式中,如果在web.config中不配置任何globalization相關節點,那麼Request.QueryString屬性獲取uri引數時會自動用utf-8解碼,如果此時你的頁面是採用gb2312編碼,那麼cs端獲取必定會是亂碼

解決方法(form提交method為get):

方法1、配置globalization節點,例如<globalization requestEncoding=”gb2312″ responseEncoding=”gb2312″ fileEncoding=”gb2312″ culture=”zh-CN”/>
那麼get提交的uri附加的引數會採用gb2312編碼,cs服務端Request.QueryString就會根據globalization配置的requestEncoding值gb2312進行內部的HttpUtility.UrlDecode
方法2、不配置globalization任何節點,在html端對將要拼接到uri後面的中文引數進行encodeURIComponent或者encodeURI編碼處理,因為encodeURIComponent或者encodeURI就是utf-8的編碼方法(永遠不會變),然後再cs服務端Request.QueryString接收後,再用 HttpUtility.UrlDecode(“”, Encoding.Default)進行解碼(或者用Server.UrlDecode()解碼,效果一樣,Server.UrlDecode為使用當前作業系統的編碼解碼方式),如下:

假設form method=get,當前環境ContentEncoding為gb2312

未做任何處理操作時要請求伺服器的uri的一部分:
Default2.aspx?Button2=%B0%A2%B5%C0%B7%F2

在指令碼端用encodeURIComponent對”阿道夫“進行編碼後的將要請求伺服器的uri的一部分:
Default2.aspx?Button2=%E9%98%BF%E9%81%93%E5%A4%AB

cs服務端:
string param1 = Request.QueryString["Button2"];//param1的值為:%E9%98%BF%E9%81%93%E5%A4%AB,雖然Request.QueryString內部有utf-8解碼操作,但是對於全是英文和符號等屬於assic碼的字元不會做任何解碼操作(utf-8包含assic)
string param2 = HttpUtility.UrlDecode(param1, Encoding.UTF8);//再針對性的用Encoding.UTF8對在指令碼端用encodeURIComponent(編碼方式為:utf-8)編碼的param1進行對應解碼,一切都安靜了。值為“阿道夫”

如果理解了編碼解碼的機制,那麼如果僅僅是在cs服務端編碼傳遞帶有中文引數的url到另一個頁面,也需要注意對應的編碼解碼問題,比如A頁面的按鈕點選後跳轉到B頁面,我舉四種情況,大家判斷哪種情況下在B頁面接收時會有亂碼出現

備註:  HttpUtility.UrlDecode(param1)在沒有第二個引數的情況下預設和HttpUtility.UrlDecode(param1, Encoding.UTF8)等效,除非你強制指定第二個引數比如:HttpUtility.UrlDecode(param1, Encoding.Default)
第一種:沒有配置任何globalization節點(正確
A頁面的按鈕程式碼:

protected void Button1_Click(object sender, EventArgs e)
         {
             string param = "阿道夫";
             Response.Redirect("~/Default.aspx?param=" + param);
         }

B頁面的接收程式碼:

string param1 = Request.QueryString["param"];

第二種:配置了globalization節點<globalization requestEncoding=”gb2312″ responseEncoding=”gb2312″ fileEncoding=”gb2312″ culture=”zh-CN”/>(正確

A頁面的按鈕程式碼:

protected void Button1_Click(object sender, EventArgs e)
         {
             string param = "阿道夫";
             Response.Redirect("~/Default.aspx?param=" + param);
         }

B頁面的接收程式碼:

string param1 = Request.QueryString["param"];

第三種:沒有配置任何globalization節點(正確
A頁面的按鈕程式碼:

protected void Button1_Click(object sender, EventArgs e)
         {
             string param = "阿道夫";
             Response.Redirect("~/Default.aspx?param=" + HttpUtility.UrlEncode(param));
         }

B頁面的接收程式碼:

string param1 = Request.QueryString["param"];

第四種:配置了globalization節點<globalization requestEncoding=”gb2312″ responseEncoding=”gb2312″ fileEncoding=”gb2312″ culture=”zh-CN”/>(亂碼

A頁面的按鈕程式碼:

protected void Button1_Click(object sender, EventArgs e)
         {
             string param = "阿道夫";
             Response.Redirect("~/Default.aspx?param=" + HttpUtility.UrlEncode(param));
         }

B頁面的接收程式碼:

string param1 = Request.QueryString["param"];

參考上面的解釋,應該能理解為何第四種是亂碼,這裡不再做太多解釋

另外在jquery被大行其道的現在,$.ajax{}、$.post()、$.get()等函式使用時更是應該注意編碼解碼的問題,大致注意如下:
如果你的頁面使用的是gb2312編碼,不要用jquery的$.get()或$.post()做ajax提交,因為這兩個方法預設為utf-8,而且是無法指定修改contentType的,預設為:contentType:”pplication/x-www-form-urlencoded; charset=UTF-8″,為什麼無法修改?因為$.post 和$.get()本身就只是 $.ajax 的 post 或者get方式的一種簡寫形式,既然是簡寫為了方便使用當然會固定死很多屬性
可以用$.ajax()並在其中加入:contentType:”application/x-www-form-urlencoded; charset=GB2312″;
寫成以下形式,可以在大多數情況避免亂碼:

$.ajax({ 
  type: "POST",
 contentType:"pplication/x-www-form-urlencoded; charset=GB2312", 
  url: "XXX“, 
  data: {},
  success: function(msg){ alert( msg ); }
});

*****以上使用get提交form方式的介紹真的可以告一段落,我想我寫的很臃腫,表達上會有很多冗餘的地方******

下面介紹post提交form的方式

在asp.net頁面的form不做任何處理的時候,預設編譯生成html時會自動加上method=”post” ,F12除錯,發現Content-Type的值是:application/x-www-form-urlencoded,這也是我上面提到過的Form元素的EncType屬性:表明提交資料的格式
一般Enctype 屬性指定將資料回發到伺服器時瀏覽器使用的編碼型別。
application/x-www-form-urlencoded: 窗體資料被編碼為名稱/值對。這是標準的編碼格式。
multipart/form-data: 窗體資料被編碼為一條訊息,頁上的每個控制元件對應訊息中的一個部分。
text/plain: 窗體資料以純文字形式進行編碼,其中不含任何控制元件或格式字元

那也就是說,假如我使用post方式提交,Enctype為application/x-www-form-urlencoded,那麼那些需要提交伺服器的值依然會被編碼,只不過這次不是拼接在uri後面,而是存放在body裡面,那麼這樣依然在不小心的情況下會帶來上面描述的亂碼問題,當然如果你是post提交,(或者你在asp.net頁面不操作form任何屬性,保持預設)那麼在cs服務端請不要再用Request.QueryString獲取值了,這是獲取不到的,應該用Request.Form[""],請儘量少用Request[""]或者Request.Params[""],這兩個將加大你的遍歷搜尋你需要引數值的範圍,Request可以訪問的有QueryString、Form、Cookies 或 ServerVariables這4個集合的資料,get請求用Request.QueryString,post用Request.Form,而Request[""]是依次訪問這4個集合,找到就返回結果,而Request.Params[""]是在訪問時,先將4個集合的資料合併到一個新集合(集合不存在時建立)再去查詢,效率可想而知

 

我現在手動將form改為:<form id=”form1″ enctype=”multipart/form-data” method=”post” runat=”server”> 注意multipart/form-data只能用於post

瀏覽器會把整個表單以控制元件為單位分割,併為每個部分加上Content-Disposition(form-data或者file),Content-Type(預設為text/plain),name(控制元件name)等資訊,並加上分割符
 

“boundary”是分隔符的意思,一般multipart/form-data用於檔案上傳,普通的傳參或者提交form元素列表值還是使用預設的application/x-www-form-urlencoded吧

 

相關文章