史上最通俗,完全搞懂字符亂碼問題的本質

一、引言

IM等社交應用的開發工做中,亂碼問題也很常見,好比:php

1)IM聊天消息中的Emoji表情爲何發給後端後MySQL數據庫裏會亂碼;
2)文件名中帶有中文的大文件聊天消息發送後,對方看到的文名是亂碼;
3)Http rest接口調用時,後端讀取到APP端傳過來的參數有中文亂碼問題;
... ...

那麼,對於亂碼這個看似不起眼,但並非一兩話能講清楚的問題,是頗有必要從根源瞭解字符集和編碼原理,知其然知其因此然顯然是一個優秀碼農的基本素養,因此,便有了本文,但願能幫助到你。html

* 推薦閱讀:關於字符編碼知識的詳細講解請見《字符編碼那點事:快速理解ASCII、Unicode、GBK和UTF-8》。前端

學習交流:python

- 即時通信/推送技術開發交流5羣: 215477170 [推薦]
- 移動端IM開發入門文章:《 新手入門一篇就夠:從零開發移動端IM

(本文同步發佈於:http://www.52im.net/thread-2868-1-1.htmlmysql

二、關於做者 

盧鈞軼:愛搗騰Linux的DBA。曾任職於大衆點評網DBA團隊,主要關注MySQL、Memcache、MMM等產品的高性能和高可用架構。git

我的微博:米雪兒儂好的cenalulu
Github地址: https://github.com/cenalulu

三、系列文章

本文是IM開發乾貨系列文章中的第21篇,總目錄以下:github

IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞
IM消息送達保證機制實現(二):保證離線消息的可靠投遞
如何保證IM實時消息的「時序性」與「一致性」?
IM單聊和羣聊中的在線狀態同步應該用「推」仍是「拉」?
IM羣聊消息如此複雜,如何保證不丟不重?
一種Android端IM智能心跳算法的設計與實現探討(含樣例代碼)
移動端IM登陸時拉取數據如何做到省流量?
通俗易懂:基於集羣的移動端IM接入層負載均衡方案分享
淺談移動端IM的多點登錄和消息漫遊原理
IM開發基礎知識補課(一):正確理解前置HTTP SSO單點登錄接口的原理
IM開發基礎知識補課(二):如何設計大量圖片文件的服務端存儲架構?
IM開發基礎知識補課(三):快速理解服務端數據庫讀寫分離原理及實踐建議
IM開發基礎知識補課(四):正確理解HTTP短鏈接中的Cookie、Session和Token
IM羣聊消息的已讀回執功能該怎麼實現?
IM羣聊消息到底是存1份(即擴散讀)仍是存多份(即擴散寫)?
IM開發基礎知識補課(五):通俗易懂,正確理解並用好MQ消息隊列
一個低成本確保IM消息時序的方法探討
IM開發基礎知識補課(六):數據庫用NoSQL仍是SQL?讀這篇就夠了!
IM裏「附近的人」功能實現原理是什麼?如何高效率地實現它?
IM開發基礎知識補課(七):主流移動端帳號登陸方式的原理及設計思路
IM開發基礎知識補課(八):史上最通俗,完全搞懂字符亂碼問題的本質》(本文)

四、正文概述

字符集和編碼無疑是IT菜鳥甚至是各類大神的頭痛問題。當遇到紛繁複雜的字符集,各類火星文和亂碼時,問題的定位每每變得很是困難。算法

本文內容就將會從原理方面對字符集和編碼作個簡單的科普介紹,同時也會介紹一些通用的亂碼故障定位的方法以方便讀者之後可以更從容的定位相關問題。sql

在正式介紹以前,先作個小申明:若是你但願很是精確的理解各個名詞的解釋,那麼能夠詳細閱讀這篇《字符編碼那點事:快速理解ASCII、Unicode、GBK和UTF-8》。數據庫

本文是博主經過本身理解消化後並轉化成易懂淺顯的表述後的介紹,會盡可能以簡單明瞭的文字來從要源講解字符集、字符編碼的概念,以及在遭遇亂碼時的一些經常使用診斷技巧,但願能助你對於「亂碼」問題有更深地理解。

五、什麼是字符集

在介紹字符集以前,咱們先了解下爲何要有字符集。

咱們在計算機屏幕上看到的是實體化的文字,而在計算機存儲介質中存放的實際是二進制的比特流。那麼在這二者之間的轉換規則就須要一個統一的標準,不然把咱們的U盤插到老闆的電腦上,文檔就亂碼了;小夥伴QQ上傳過來的文件,在咱們本地打開又亂碼了。

因而爲了實現轉換標準,各類字符集標準就出現了。

簡單的說:字符集就規定了某個文字對應的二進制數字存放方式(編碼)和某串二進制數值表明了哪一個文字(解碼)的轉換關係。 

那麼爲何會有那麼多字符集標準呢?

這個問題實際很是容易回答。問問本身爲何咱們的插頭拿到英國就不能用了呢?爲何顯示器同時有DVI、VGA、HDMI、DP這麼多接口呢?不少規範和標準在最初制定時並不會意識到這將會是之後全球普適的準則,或者處於組織自己利益就想從本質上區別於現有標準。因而,就產生了那麼多具備相同效果但又不相互兼容的標準了。 

說了那麼多咱們來看一個實際例子,下面就是「屌」這個字在各類編碼下的十六進制和二進制編碼結果,怎麼樣有沒有一種很屌的感受?

六、什麼是字符編碼

字符集只是一個規則集合的名字,對應到真實生活中,字符集就是對某種語言的稱呼。例如:英語,漢語,日語。

對於一個字符集來講要正確編碼轉碼一個字符須要三個關鍵元素:

1)字庫表(character repertoire):是一個至關於全部可讀或者可顯示字符的數據庫,字庫表決定了整個字符集可以展示表示的全部字符的範圍;
2)編碼字符集(coded character set):即用一個編碼值code point來表示一個字符在字庫中的位置;
3)字符編碼(character encoding form):將編碼字符集和實際存儲數值之間的轉換關係。

通常來講都會直接將code point的值做爲編碼後的值直接存儲。例如在ASCII中「A」在表中排第65位,而編碼後A的數值是_ 0100 0001 _也即十進制的65的二進制轉換結果。

看到這裏,可能不少讀者都會有和我當初同樣的疑問:字庫表和編碼字符集看來是必不可少的,那既然字庫表中的每個字符都有一個本身的序號,直接把序號做爲存儲內容就行了。爲何還要畫蛇添足經過字符編碼把序號轉換成另一種存儲格式呢?

其實緣由也比較容易理解:統一字庫表的目的是爲了可以涵蓋世界上全部的字符,但實際使用過程當中會發現真正用的上的字符相對整個字庫表來講比例很是低。例如中文地區的程序幾乎不會須要日語字符,而一些英語國家甚至簡單的ASCII字庫表就能知足基本需求。而若是把每一個字符都用字庫表中的序號來存儲的話,每一個字符就須要3個字節(這裏以Unicode字庫爲例),這樣對於本來用僅佔一個字符的ASCII編碼的英語地區國家顯然是一個額外成本(存儲體積是原來的三倍)。算的直接一些,一樣一塊硬盤,用ASCII能夠存1500篇文章,而用3字節Unicode序號存儲只能存500篇。因而就出現了UTF-8這樣的變長編碼。在UTF-8編碼中本來只須要一個字節的ASCII字符,仍然只佔一個字節。而像中文及日語這樣的複雜字符就須要2個到3個字節來存儲。

關於字符編碼知識的詳細講解請見:字符編碼那點事:快速理解ASCII、Unicode、GBK和UTF-8》。

七、UTF-8和Unicode的關係

看完上面兩個概念解釋,那麼解釋UTF-8和Unicode的關係就比較簡單了。

Unicode就是上文中提到的編碼字符集,而UTF-8就是字符編碼,即Unicode規則字庫的一種實現形式。

隨着互聯網的發展,對同一字庫集的要求愈來愈迫切,Unicode標準也就天然而然的出現。它幾乎涵蓋了各個國家語言可能出現的符號和文字,並將爲他們編號。詳見:Unicode百科介紹

Unicode的編號從 _0000_ 開始一直到_10FFFF_ 共分爲17個Plane,每一個Plane中有65536個字符。而UTF-8則只實現了第一個Plane,可見UTF-8雖然是一個當今接受度最廣的字符集編碼,可是它並無涵蓋整個Unicode的字庫,這也形成了它在某些場景下對於特殊字符的處理困難(下文會有提到)。

八、UTF-8編碼簡介

爲了更好的理解後面的實際應用,咱們這裏簡單的介紹下UTF-8的編碼實現方法。即UTF-8的物理存儲和Unicode序號的轉換關係。 

UTF-8編碼爲變長編碼,最小編碼單位(code unit)爲一個字節。一個字節的前1-3個bit爲描述性部分,後面爲實際序號部分:

  • 1)若是一個字節的第一位爲0,那麼表明當前字符爲單字節字符,佔用一個字節的空間。0以後的全部部分(7個bit)表明在Unicode中的序號;
  • 2)若是一個字節以110開頭,那麼表明當前字符爲雙字節字符,佔用2個字節的空間。110以後的全部部分(5個bit)加上後一個字節的除10外的部分(6個bit)表明在Unicode中的序號。且第二個字節以10開頭;
  • 3)若是一個字節以1110開頭,那麼表明當前字符爲三字節字符,佔用3個字節的空間。110以後的全部部分(5個bit)加上後兩個字節的除10外的部分(12個bit)表明在Unicode中的序號。且第2、第三個字節以10開頭;
  • 4)若是一個字節以10開頭,那麼表明當前字節爲多字節字符的第二個字節。10以後的全部部分(6個bit)和以前的部分一同組成在Unicode中的序號。

具體每一個字節的特徵可見下表,其中「x」表明序號部分,把各個字節中的全部x部分拼接在一塊兒就組成了在Unicode字庫中的序號。以下圖所示。

咱們分別看三個從一個字節到三個字節的UTF-8編碼例子: 

細心的讀者不難從以上的簡單介紹中得出如下規律:

1)3個字節的UTF-8十六進制編碼必定是以E開頭的;

2)2個字節的UTF-8十六進制編碼必定是以C或D開頭的;

3)1個字節的UTF-8十六進制編碼必定是以比8小的數字開頭的。

九、爲何會出現亂碼

亂碼也就是英文常說的mojibake(由日語的文字化け音譯)。

簡單的說亂碼的出現是由於:編碼和解碼時用了不一樣或者不兼容的字符集。

對應到真實生活中:就比如是一個英國人爲了表示祝福在紙上寫了bless(編碼過程)。而一個法國人拿到了這張紙,因爲在法語中bless表示受傷的意思,因此認爲他想表達的是受傷(解碼過程)。這個就是一個現實生活中的亂碼狀況。

在計算機科學中同樣:一個用UTF-8編碼後的字符,用GBK去解碼。因爲兩個字符集的字庫表不同,同一個漢字在兩個字符表的位置也不一樣,最終就會出現亂碼。 

咱們來看一個例子,假設咱們用UTF-8編碼存儲「很屌」兩個字,會有以下轉換:

因而咱們獲得了E5BE88E5B18C這麼一串數值,而顯示時咱們用GBK解碼進行展現,經過查表咱們得到如下信息: 

解碼後咱們就獲得了「寰堝睂」這麼一個錯誤的結果,更要命的是連字符個數都變了。

十、如何識別亂碼的原本想要表達的文字

要從亂碼字符中反解出原來的正確文字須要對各個字符集編碼規則有較爲深入的掌握。可是原理很簡單,這裏用以MySQL數據庫中的數據操縱中最多見的UTF-8被錯誤用GBK展現時的亂碼爲例,來講明具體反解和識別過程。

10.1 第1步:編碼

假設咱們在頁面上看到「寰堝睂」這樣的亂碼,而又得知咱們的瀏覽器當前使用GBK編碼。那麼第一步咱們就能先經過GBK把亂碼編碼成二進制表達式。

固然查表編碼效率很低,咱們也能夠用如下SQL語句直接經過MySQL客戶端來作編碼工做:

mysql [localhost] {msandbox} > selecthex(convert('寰堝睂'using gbk));
hex(convert('寰堝睂'using gbk))   
E5BE88E5B18C                       

1 row inset(0.01 sec)

10.2 第2步:識別

如今咱們獲得瞭解碼後的二進制字符串E5BE88E5B18C。而後咱們將它按字節拆開。

而後套用以前UTF-8編碼介紹章節中總結出的規律,就不難發現這6個字節的數據符合UTF-8編碼規則。若是整個數據流都符合這個規則的話,咱們就能大膽假設亂碼以前的編碼字符集是UTF-8。

10.3 第3步:解碼

而後咱們就能拿着 _E5BE88E5B18C _用UTF-8解碼,查看亂碼前的文字了。

固然咱們能夠不查表直接經過SQL得到結果:

mysql [localhost] {msandbox} ((none)) > selectconvert(0xE5BE88E5B18C using utf8);
convert(0xE5BE88E5B18C using utf8)
很屌                              

1 row inset(0.00 sec)

十一、常見的IM亂碼問題處理之MySQL中的Emoji字符

所謂Emoji就是一種在Unicode位於 _\u1F601-\u1F64F_ 區段的字符。這個顯然超過了目前經常使用的UTF-8字符集的編碼範圍 _\u0000-\uFFFF_。Emoji表情隨着IOS的普及和微信的支持愈來愈常見。

下面就是幾個常見的Emoji(IM聊天軟件中常常會被用到):

那麼Emoji字符表情會對咱們平時的開發運維帶來什麼影響呢?

最多見的問題就在於將他存入MySQL數據庫的時候。通常來講MySQL數據庫的默認字符集都會配置成UTF-8(三字節),而utf8mb4在5.5之後才被支持,也不多會有DBA主動將系統默認字符集改爲utf8mb4。

那麼問題就來了,當咱們把一個須要4字節UTF-8編碼才能表示的字符存入數據庫的時候就會報錯:_ERROR 1366: Incorrect string value: '\xF0\x9D\x8C\x86' for column_ 。 

若是認真閱讀了上面的解釋,那麼這個報錯也就不難看懂了:咱們試圖將一串Bytes插入到一列中,而這串Bytes的第一個字節是 _\xF0_ 意味着這是一個四字節的UTF-8編碼。可是當MySQL表和列字符集配置爲UTF-8的時候是沒法存儲這樣的字符的,因此報了錯。 

那麼遇到這種狀況咱們如何解決呢?

有兩種方式:

  • 1)升級MySQL到5.6或更高版本,而且將表字符集切換至utf8mb4;
  • 2)在把內容存入到數據庫以前作一次過濾,將Emoji字符替換成一段特殊的文字編碼,而後再存入數據庫中。以後從數據庫獲取或者前端展現時再將這段特殊文字編碼轉換成Emoji顯示。

第二種方法咱們假設用 _-*-1F601-*- _來替代4字節的Emoji,那麼具體實現python代碼能夠參見Stackoverflow上的回答

十二、參考文獻

[1]  如何配置Python默認字符集
[2]  字符編碼那點事:快速理解ASCII、Unicode、GBK和UTF-8
[3]  Unicode中文編碼表
[4]  Emoji Unicode Table
[5]  Every Developer Should Know About The Encoding

附錄:更多IM開發方面的文章

[1] IM開發綜合文章:
新手入門一篇就夠:從零開發移動端IM
移動端IM開發者必讀(一):通俗易懂,理解移動網絡的「弱」和「慢」
移動端IM開發者必讀(二):史上最全移動弱網絡優化方法總結
從客戶端的角度來談談移動端IM的消息可靠性和送達機制
現代移動端網絡短鏈接的優化手段總結:請求速度、弱網適應、安全保障
騰訊技術分享:社交網絡圖片的帶寬壓縮技術演進之路
小白必讀:閒話HTTP短鏈接中的Session和Token
IM開發基礎知識補課:正確理解前置HTTP SSO單點登錄接口的原理
移動端IM開發須要面對的技術問題
開發IM是本身設計協議用字節流好仍是字符流好?
請問有人知道語音留言聊天的主流實現方式嗎?
一個低成本確保IM消息時序的方法探討
徹底自已開發的IM該如何設計「失敗重試」機制?
通俗易懂:基於集羣的移動端IM接入層負載均衡方案分享
微信對網絡影響的技術試驗及分析(論文全文)
即時通信系統的原理、技術和應用(技術論文)
開源IM工程「蘑菇街TeamTalk」的現狀:一場虎頭蛇尾的開源秀
QQ音樂團隊分享:Android中的圖片壓縮技術詳解(上篇)
QQ音樂團隊分享:Android中的圖片壓縮技術詳解(下篇)
騰訊原創分享(一):如何大幅提高移動網絡下手機QQ的圖片傳輸速度和成功率
騰訊原創分享(二):如何大幅壓縮移動網絡下APP的流量消耗(上篇)
騰訊原創分享(三):如何大幅壓縮移動網絡下APP的流量消耗(下篇)
如約而至:微信自用的移動端IM網絡層跨平臺組件庫Mars已正式開源
基於社交網絡的Yelp是如何實現海量用戶圖片的無損壓縮的?
騰訊技術分享:騰訊是如何大幅下降帶寬和網絡流量的(圖片壓縮篇)
騰訊技術分享:騰訊是如何大幅下降帶寬和網絡流量的(音視頻技術篇)
字符編碼那點事:快速理解ASCII、Unicode、GBK和UTF-8
全面掌握移動端主流圖片格式的特色、性能、調優等
子彈短信光鮮的背後:網易雲信首席架構師分享億級IM平臺的技術實踐
微信技術分享:微信的海量IM聊天消息序列號生成實踐(算法原理篇)
自已開發IM有那麼難嗎?手把手教你自擼一個Andriod版簡易IM (有源碼)
融雲技術分享:解密融雲IM產品的聊天消息ID生成策略
適合新手:從零開發一個IM服務端(基於Netty,有完整源碼)
拿起鍵盤就是幹:跟我一塊兒徒手開發一套分佈式IM系統
>>  更多同類文章 …… 
[2] 有關IM架構設計的文章:
淺談IM系統的架構設計
簡述移動端IM開發的那些坑:架構設計、通訊協議和客戶端
一套海量在線用戶的移動端IM架構設計實踐分享(含詳細圖文)
一套原創分佈式即時通信(IM)系統理論架構方案
從零到卓越:京東客服即時通信系統的技術架構演進歷程
蘑菇街即時通信/IM服務器開發之架構選擇
騰訊QQ1.4億在線用戶的技術挑戰和架構演進之路PPT
微信後臺基於時間序的海量數據冷熱分級架構設計實踐
微信技術總監談架構:微信之道——大道至簡(演講全文)
如何解讀《微信技術總監談架構:微信之道——大道至簡》
快速裂變:見證微信強大後臺架構從0到1的演進歷程(一)
17年的實踐:騰訊海量產品的技術方法論
移動端IM中大規模羣消息的推送如何保證效率、實時性?
現代IM系統中聊天消息的同步和存儲方案探討
IM開發基礎知識補課(二):如何設計大量圖片文件的服務端存儲架構?
IM開發基礎知識補課(三):快速理解服務端數據庫讀寫分離原理及實踐建議
IM開發基礎知識補課(四):正確理解HTTP短鏈接中的Cookie、Session和Token
WhatsApp技術實踐分享:32人工程團隊創造的技術神話
微信朋友圈千億訪問量背後的技術挑戰和實踐總結
王者榮耀2億用戶量的背後:產品定位、技術架構、網絡方案等
IM系統的MQ消息中間件選型:Kafka仍是RabbitMQ?
騰訊資深架構師乾貨總結:一文讀懂大型分佈式系統設計的方方面面
以微博類應用場景爲例,總結海量社交系統的架構設計步驟
快速理解高性能HTTP服務端的負載均衡技術原理
子彈短信光鮮的背後:網易雲信首席架構師分享億級IM平臺的技術實踐
知乎技術分享:從單機到2000萬QPS併發的Redis高性能緩存實踐之路
IM開發基礎知識補課(五):通俗易懂,正確理解並用好MQ消息隊列
微信技術分享:微信的海量IM聊天消息序列號生成實踐(算法原理篇)
微信技術分享:微信的海量IM聊天消息序列號生成實踐(容災方案篇)
新手入門:零基礎理解大型分佈式架構的演進歷史、技術原理、最佳實踐
一套高可用、易伸縮、高併發的IM羣聊、單聊架構方案設計實踐
阿里技術分享:深度揭祕阿里數據庫技術方案的10年變遷史
阿里技術分享:阿里自研金融級數據庫OceanBase的艱辛成長之路
社交軟件紅包技術解密(一):全面解密QQ紅包技術方案——架構、技術實現等
社交軟件紅包技術解密(二):解密微信搖一搖紅包從0到1的技術演進
社交軟件紅包技術解密(三):微信搖一搖紅包雨背後的技術細節
社交軟件紅包技術解密(四):微信紅包系統是如何應對高併發的
社交軟件紅包技術解密(五):微信紅包系統是如何實現高可用性的
社交軟件紅包技術解密(六):微信紅包系統的存儲層架構演進實踐
社交軟件紅包技術解密(七):支付寶紅包的海量高併發技術實踐
社交軟件紅包技術解密(八):全面解密微博紅包技術方案
社交軟件紅包技術解密(九):談談手Q紅包的功能邏輯、容災、運維、架構等
即時通信新手入門:一文讀懂什麼是Nginx?它可否實現IM的負載均衡?
即時通信新手入門:快速理解RPC技術——基本概念、原理和用途
多維度對比5款主流分佈式MQ消息隊列,媽媽不再擔憂個人技術選型了
從游擊隊到正規軍(一):馬蜂窩旅遊網的IM系統架構演進之路
從游擊隊到正規軍(二):馬蜂窩旅遊網的IM客戶端架構演進和實踐總結
IM開發基礎知識補課(六):數據庫用NoSQL仍是SQL?讀這篇就夠了!
瓜子IM智能客服系統的數據架構設計(整理自現場演講,有配套PPT)
阿里釘釘技術分享:企業級IM王者——釘釘在後端架構上的過人之處
>>  更多同類文章 ……

(本文同步發佈於:http://www.52im.net/thread-2868-1-1.html

相關文章
相關標籤/搜索