前幾天的五倍券開搶,九點一到不意外地系統當機了。話說,幾百萬人同時殺進來,系統撐不住很正常,完全沒事才叫意外,能很快復原就算設計者功力不差了。依據事前事後的網路評論,感覺會設計高流量系統的鄉民路人真不少,不管有沒寫過程式都能說出一番道理,分流啦、動態擴充伺服器、預先做好壓測、不能用 ASP.NET 要用 GO 語言寫... 以我對系統設計的淺薄了解,每項說法都有道理(除了 ASP.NET 不行得用 GO 之外,你知道工程師賴以維生的 StackOverflow 是用 ASP.NET 寫的嗎? 就不是南拳北拳的問題呀),但絕對不是要做好某一項就能安全下莊,而是每個細節都得考慮周到,否則系統通常會在最脆弱的一環爆炸給你看,提醒你哪裡還沒做好。

設計高流量系統不是我的專業,但五倍券當機時噴出的亂碼卻引起我的好奇。當時出現的錯誤訊息因中文編碼解析錯誤出現「甇斗?滚?嗵?⊥?蓥蝙?鍂??」,因其中夾雜「滚」字還意外引發討論成為話題 (冏!五倍券無法綁定 亂碼藏字叫你「滾」),依經驗是多是套錯 UTF8、BIG5 編碼造成,之前研究過,例如:中文亂碼"蕞蕞蕞蕞"是怎麼來的?中文亂碼「嚙踝蕭嚙踝蕭」是怎麼來的?,而依據報導,有網友切換編碼將 BIG5 改成 UTF8 即可看到原本的錯誤訊息是 IIS 過度忙碌時噴出的 HTTP 503 訊息「此服務無法使用。」,故推測狀況為系統回傳的是 UTF8 編碼,但瀏覽器誤判為 BIG5 變成亂碼,寫了程式驗證將「此服務無法使用。」用 UTF8 轉成 byte[] 再用 BIG5 轉回 String,原本預期會看到「甇斗?滚?嗵?⊥?蓥蝙?鍂??」秒結案,結果卻不然:

是會變成亂碼,但我得到的是「甇斗?? 瘜 蝙?具?」,根本不會出現「滚」,而跟「甇斗?滚?嗵?⊥?蓥蝙?鍂??」只有「甇斗蝙」三個字相同,開啟線上中文編碼解析進一步調查,「滚嗵蓥鍂」四個字根本不是 BIG5 編碼支援的字元,算是 Unicode 難字,在簡體中文 GB2312 編碼才有對映,這到底是怎麼一回事?


註:擷圖之 UTF16 編碼有誤,已於 2.0.1 版修正,感謝讀者 anthonywang 指正。

為了驗證我寫了簡單網頁重現問題:

<html>
	<head>
	<meta http-equiv="Content-Type" content="text/html; charset=Big5">
	</head>
<body>此服務無法使用。</body>
</html>

檔案存成 UTF8,但 meta 宣告故意寫成 charset=Big5,分別用 IE、Edge、Chrome、Firefox 開啟,發現有趣的事:

一樣是 UTF8 誤判成 BIG5,IE、Edge/Chrome(都是 Chromium 核心)、Firefox 出現的亂碼不盡相同,也跟 .NET 轉換結果不同。IE 顯示為「甇斗? ? 瘜 蝙?」(比 .NET 結果少了「具」),Edge/Chrome 就如新聞所提是「甇斗?滚?嗵?⊥?蓥蝙?鍂??」,Firefox 很有趣,沒有「滚」但集兩家之大成「甇斗??嗵?瘜蓥蝙?具??」比 Edge/Chrome 多了「瘜」跟「具」。由此可知,各家瀏覽器將 byte[] 解讀成 BIG5 字元的實作存在一些差異。但,Chrome/Edge 中的「滚」是怎麼來的呢?

為了查出真相,用線上中文編碼解析將「此服務無法使用。」轉成 UTF8 Bytes E6 AD A4 E6 9C 8D E5 8B 99 E7 84 A1 E6 B3 95 E4 BD BF E7 94 A8 E3 80 82:

再用以下 ASP.NET 程式測試不同 byte[] 在瀏覽器硬轉成 BIG5 的結果:

<%@Page Language="C#"%>
<%@Import Namespace="System.Text.RegularExpressions"%>
<script runat="server">
void Page_Load(object sender, EventArgs e)
{
	var hex = Request["h"] ?? "";
	var bytes = new List<byte>();
	foreach (Match m in 
		Regex.Matches(hex, "[0-9a-fA-F]{2}")) 
	{
		bytes.Add(Convert.ToByte(m.Value, 16));
	}
	var big5 = System.Text.Encoding.GetEncoding("big5");
	Response.BinaryWrite(big5.GetBytes("<html><head><meta charset='big5'></head><body>"));
	Response.BinaryWrite(bytes.ToArray());
	Response.BinaryWrite(big5.GetBytes("</body></html>"));
	Response.End();
}
</script>

我很快找出「滚」是 8D E5 變成的:

依據 BIG5 編碼規則,8D E5 落在 0x8140-0xA0FE 使用者自訂字元(造字區),理論上並不是一個通用的有效字元,那 Chrome 為什麼會判別它是「滚」字呢?我想到了 Unicode 補完計劃,並在香港的 Unicode 補完計劃字元表找到答案!

8D E5 是「滚」的 HKSCS 編碼,喚起我的記憶(延伸閱讀:Big5-HKSCS編碼初探(上)Big5-HKSCS編碼初探(下)Big5-HKSCS編碼補遺),

結論:這次五倍券的錯訊息亂碼是 UTF8 編碼硬解成 BIG5 導致,IE 跟 .NET 會依據標準 BIG5 (CodePage 950)解析,而 Chrome/Edge/Firefox 則會自動切換 Big5-HKSCS 企圖解析出更多字元,這就是「滚」字冒出來的原因。

解開一個謎團,開心!

Find the Chinese message is currupted due to using BIG5 to decode UTF8 content, but I found some char in Edge/Chrome which cannot reproduce with .NET Encoding API. It turned out non-IE browsers will use Big5-HKSCS to decode data.


Comments

# by anthonywang

線上版的UTF-16編碼錯了!?怎麼跟UTF-8一樣?應該是2 bytes是吧~

# by AC

最後一段 BIG5 打成 BUG5 😂

# by student

看來這篇有 Bug5 問題

# by Jeffrey

to anthonywang, 真的耶,居然一直都沒被發現,查到是 Vue 的 v-bind 寫錯了,稍後會更新。感謝指正。 to AC, student, 身為程式人員,不自覺就會寫出 BUG 來 :P,謝謝指正。

# by abb

想到之前特斯拉導航map 遇到罕見字 直接亂碼 螢幕直接重開機 被捅一刀 中文字太奇妙了 哈

# by Rita

誠如你所言,若有興趣,我國中央政府機關官網或是信件,也有亂碼問題,可惜,還沒人搞懂怎麼處理唷!

Post a comment


40 + 39 =