2012年11月22日 星期四

[aspx]HttpUtility.UrlDecode 與 Server.UrlDecode 真的不一樣

我以為我經過上次的錯誤,中文亂碼可以不會再出現在我的生活中。

直到我的膝蓋中了一箭。 (這裡)

找了一陣子,原來有人,以前的程式碼用了Server.UrlDecode

HttpUtility.UrlDecode 與 Server.UrlDecode 真的不一樣。

我寫了以下的程式來證明:

Function WriteOutput(ByVal str As String) As String
    Response.AppendHeader("Content-Type", "text/html;charset=UTF-8")
    Dim bs As Byte() = System.Text.Encoding.UTF8.GetBytes(str)
    Response.ContentEncoding = System.Text.Encoding.UTF8
    Response.OutputStream.Write(bs, 0, bs.Length)
    Return "OK"
End Function
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
    WriteOutput("System.Text.Encoding.Default.EncodingName:" + System.Text.Encoding.Default.EncodingName + "<br/>")
    WriteOutput("System.Text.Encoding.Default.WebName:" + System.Text.Encoding.Default.WebName + "<br/>")
    WriteOutput("HttpUtility.UrlDecode:" + HttpUtility.UrlDecode("%e4%b8%ad") + "<br/>")
    WriteOutput("Server.UrlDecode:" + Server.UrlDecode("%e4%b8%ad") + "<br/>")
End Sub

結果這就是不一樣:

image

附帶說明一下,這個現象不一定會在你的伺服器出現。在我的開發環境,如下。

image

我現在還找不到確切指出設定不同的地方。

我只看到兩個 EncodingName 所出現的字串是不同的。

據網路消息,說 Server.UrlDecode 是用系統編碼。可是我還沒辦法找到如何顯示現在系統編碼。

2012年11月20日 星期二

[aspx]不會用設定,不要怪微軟!(原題:好心的微軟幫了倒忙。中文亂碼解決實錄)

更新:要看最後解法,請直接跳最下面

有史以來解決中文亂碼最長的一次。從我2000年開始,踩進可怕的中文混沌中,
從沒有這麼令人生氣的一次了。

原因就是微軟幫倒忙!微軟好心幫忙但是我還是得告訴它我的 charset。

「絕聖棄智,民利百倍。絕仁棄義,民復孝慈。絕巧棄利,盜賊無有。」--老子

故事是這樣開始的。要求是:
「我需要用 post 方法,傳送一筆記錄的資料,而網頁伺服器接到之後,幫忙下 SQL 插入資料。」

你看多簡單的一句話!結果發生亂碼,沒關係,那我就寫個測試的程式,看是傳過去的字就亂了,還是 SQL 執行完才錯。

好,簡化過的發送端程式如下:

Function SendPutRequest(ByVal keyname As String, ByVal keyvalue As String)
    Dim uristr As String = "http://localhost:1490/WebSite1/poid_fail.aspx"
    Dim req As System.Net.HttpWebRequest = System.Net.HttpWebRequest.Create(uristr)
    req.Method = "POST"

    Dim param As String = "a=put"
    param += "&" & "key" & "=" & keyname
    param += "&" & "value" & "=" & keyvalue
    Dim bs As Byte() = Encoding.UTF8.GetBytes(param)
    req.ContentType = "application/x-www-form-urlencoded"
    req.ContentLength = bs.Length

    Using reqStream As System.IO.Stream = req.GetRequestStream()
        reqStream.Write(bs, 0, bs.Length)
    End Using
    Using wr As System.Net.WebResponse = req.GetResponse()
        Dim rs(wr.ContentLength) As Byte
        Dim expectcount As Integer = wr.ContentLength
        Dim readcount As Integer = 0
        Response.ContentEncoding = System.Text.Encoding.UTF8
        readcount += wr.GetResponseStream().Read(rs, 0, wr.ContentLength)
        Response.Write(Encoding.UTF8.GetChars(rs))
    End Using
    Return "OK"
End Function

Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
    SendPutRequest("DataName", "中文測試")
End Sub

發送端,就只是一個假裝用 form 打出資料的 request。其中也很簡單的一個 keyname 跟 keyvalue,傳送至伺服端

而,接收端程式(簡化版)很快就寫好了:

<%@ Page Language="VB" %>

<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.OleDb" %>

<script runat="server">
    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs)
        Dim act As String
        Dim insertSql As String = ""
        Try
            act = Request.Form("a")
            If act = "put" Then
                Dim keyname As String
                keyname = Request.Form("key")
                Dim keyvalue As String
                keyvalue = Request.Form("value")
                insertSql = "insert into [poidata] ($columns$) values ($values$)"
                insertSql = insertSql.Replace("$columns$", keyname).Replace("$values$", keyvalue)
                ExecuteSql(insertSql)
            End If
        Catch ex As Exception
            insertSql = ex.Message
        End Try
        Dim contentbody As String = insertSql
        Response.AppendHeader("Content-Type", "text/json")
        Response.ContentEncoding = System.Text.Encoding.UTF8
        Response.OutputStream.Write(Encoding.UTF8.GetBytes(contentbody), 0, System.Text.Encoding.UTF8.GetByteCount(contentbody))
    End Sub
</script>

在開發環境很正常。發送端與接收端都是在同一個電腦上,沒有問題。

image 

把接收端程式丟到真的伺服器去,就變亂碼!:

image

好。很正常的,因為值有中文,沒有 encode 所以變亂碼,我可以接受。
而我還特別把接收端的程式的 response,以 Encoding.UTF8.GetBytes 轉成 byte,然後用 write byte 方式,這是最保險,照字串byte回傳回來的保險方式。

那我 encode 吧。

把傳送端 encode。

param += "&" & "key" & "=" & HttpUtility.UrlEncode(keyname)

param += "&" & "value" & "=" & HttpUtility.UrlEncode(keyvalue)

結果長一模一樣。奇怪了。

image

那我接收端 decode 看看,結果一樣。

那來檢查兩邊的 default encoding 好了。
把接收端的 default encoding 回傳回來看看。

Dim contentbody As String = insertSql + "<br />" + System.Text.Encoding.Default.EncodingName

在開發環境,看起來是這樣

image

把程式丟到真的伺服器上,嗯,都是 big5 啊!

image

啊,等等,不對勁耶,為什麼真的伺服器不是寫「繁體中文 (Big5)」傳回來,而是用「Chinese Traditional (Big5)」這是搞什麼鬼?

後來,同事看我搞很久,決定傳授我妙方。
原本,在 IIS 的設定,我是使用虛擬目錄的方式,指到網站程式所放的目錄。
而他的經驗是,在 IIS 上,指定用「網路應用程式」
他也幫我弄好了。只是,網路位址會移動,而我,只是改別人的程式,不太想去改位址這種事。
如果,只想研究到這裡的人,就去改成「網路應用程式」試試吧。

因為沒辦法、因為不死心,我決定來試試強迫接到 encode 過的字串為目標,
想確定傳過去的 byte 沒有變動。
所以,接收端先不 decode,就接到什麼回寫什麼看看。
再來,因為傳送端我 encode 一次不夠,我再多 encode 一次看看。

param += "&" & "value" & "=" & HttpUtility.UrlEncode(HttpUtility.UrlEncode(keyvalue))

很賭氣的作法。UrlEncode 這種 encode 法,作兩次 encode,要得回原來的字,就要 decode 兩次。只要我拿到不是亂碼的字串,算清楚encode次數,就知道是不是 byte 被動到了。

image

很好,這是好的開始,這看來像是 encode 一次的字串。
那我接收端,就 decode 一次試試。

keyvalue = HttpUtility.UrlDecode(Request.Form("value"))

image

耶,看來好了。

而實際把組好的 SQL 字串執行下去,資料庫裡,也就正常的中文了。

回頭討論,我的傳送端 encode 兩次,我的接收端 decode 一次。
這代表,有某個地方幫我 decode 一次。而且在我的程式執行之前。
按照我的推斷,那一次的 decode 的時候,參考不知道哪裡的編碼,在那裡字串亂掉了。
明明就看不懂中文,還硬要轉,當然就搞亂了。
這從 default encoding name 可以看得出來,雖然都是 Big5,但是一個用中文表示,一個用英文表示,明白的指出,兩個的執行環境是不同的,而這個執行環境的不同,就會導致亂碼。
而我,用兩次 encode,讓我的字串被自動 decode (也就是第一次,不是我的程式做的) 之後,成為 encode 一次的狀態,這時是 ascii 的字串表示式,用這個字串,讓我的程式來 decode 回 .net 裡面的字串。由於資訊保持完整,所以可以完美地解回 .net 字串。問題才得以解決。
確切幫忙 decode 一次的程式我沒有找到,不過應該不脫離 iis,asp .net 這個範圍。
所以這次幫倒忙就算在微軟的頭上!

我想也許會有高手更了解 IIS 或 asp .net 的可以用更漂亮的方式解決這問題吧?
我的確是不了解所以硬幹啊。

更新:

因為無可避免地會被 decode 一次,所以指定正確 charset 一定是正解。指定的方式即在 request 的 Content-Type 裡。如下:

req.ContentType = "application/x-www-form-urlencoded;charset=UTF-8"

接收端程式,不用需 decode,微軟會按指定編碼幫你搞定。

它就是要幫你解,你只好一定要告訴他怎麼解。