<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    Jack Jiang

    我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
    posts - 494, comments - 13, trackbacks - 0, articles - 1

    本文由阿里技術(shù)團(tuán)隊(duì)詹向陽(驍飏)分享,原題“一文讀懂字符編碼”,有修訂和改動(dòng)。

    一、引言

    說起計(jì)算機(jī)字符編碼,讓我想起了科幻巨作《三體-黑暗深林》人類遇到外星文明魔戒的畫面(以下內(nèi)容摘自大劉的原文)。

    人類第一次近距離看到四維物體魔戒,卓文用中頻電波發(fā)送了一個(gè)問候語。這是一幅簡單的點(diǎn)陣圖,圖中由六行不同數(shù)量的點(diǎn)組成了一個(gè)質(zhì)數(shù)數(shù)列:1,3,5,7,11,13。

    他們沒有指望得到應(yīng)答,但應(yīng)答立刻出現(xiàn)了

    .....

    太空艇收到了來自“魔戒”的一系列點(diǎn)陣圖,第一幅是很整齊的一個(gè)8×8點(diǎn)陣,共六十四個(gè)點(diǎn);第二幅圖中點(diǎn)陣的一角少了一個(gè)點(diǎn),剩下六十三個(gè);第三幅圖中又少一點(diǎn),剩六十二個(gè)……“這是倒計(jì)數(shù),也相當(dāng)于一個(gè)進(jìn)度條,可能表示‘它’已經(jīng)收到了羅塞塔,正在譯解,讓我們等侍。”韋斯特說。

    “可為什么是六十四點(diǎn)呢?”

    “使用二進(jìn)制時(shí)一個(gè)不大不小的數(shù)唄,與十進(jìn)制的一百差不多。”

    卓文和關(guān)一帆都很慶幸能帶韋斯特來,在與未知的智慧體建立交流方面、心理學(xué)家確實(shí)很有才能。

    在倒計(jì)數(shù)達(dá)到五十七時(shí),令人激動(dòng)的事情出現(xiàn)了:下一個(gè)計(jì)數(shù)沒有用點(diǎn)陣表示,“魔戒”發(fā)來的圖片上赫然顯示出人類的阿拉伯?dāng)?shù)字56!

    .....

    在人類探索外星文明、邁向星辰大海的宇宙征程里,也離不開這種最最基礎(chǔ)的編碼問題。

    前一陣跟同事碰到了字符亂碼的問題,了解后發(fā)現(xiàn)這個(gè)問題存在兩年了,我們程序員每天都在跟編碼打交道,但大家對(duì)字符編碼都是一知半解:“天天吃豬肉卻很少見過豬跑”,今天我就把它徹底講透!

    * 推薦閱讀:如果本文太“硬”,就看看這兩篇吧:《史上最通俗,徹底搞懂字符亂碼問題的本質(zhì)》、《字符編碼那點(diǎn)事:快速理解ASCII、Unicode、GBK和UTF-8》。

    技術(shù)交流:

    (本文已同步發(fā)布于:http://www.52im.net/thread-4210-1-1.html

    二、什么是字符編碼?

    我們知道計(jì)算機(jī)的世界只有0和1,如果沒有字符編碼,我們看到的就是一串"110010100101100111001....",我們的溝通就好像是在對(duì)牛彈琴,我看不懂它,它看不懂我。

    字符編碼就好比人類和機(jī)器之間的翻譯程序,把我們熟知的字符文字翻譯成機(jī)器能讀懂的二進(jìn)制,同時(shí)把二進(jìn)制翻譯成我們能看懂的字符。

    以下是百科對(duì)字符編碼的解釋:

    字符編碼(Character encoding)也稱字集碼,是把字符集中的字符,編碼為指定集合中的某一對(duì)象(例如:比特模式、自然數(shù)序列、8位組或者電脈沖),以便文本在計(jì)算機(jī)中存儲(chǔ)或者通信網(wǎng)絡(luò)的傳遞。常見的例子是將拉丁字母表編碼成摩斯電碼和ASCII,比如ASCII編碼是將字母、數(shù)字和其它符號(hào)進(jìn)行編號(hào),并用7比特的二進(jìn)制來表示這個(gè)整數(shù)。

    字符集(Character set)是多個(gè)字符的集合,字符集種類較多,每個(gè)字符集包含的字符個(gè)數(shù)不同,常見字符集名稱:ASCII字符集、GB2312字符集、BIG5字符集、 GB18030字符集、Unicode字符集等。計(jì)算機(jī)要準(zhǔn)確的處理各種字符集文字,就需要進(jìn)行字符編碼,以便計(jì)算機(jī)能夠識(shí)別和存儲(chǔ)各種文字。

    三、為什么計(jì)算機(jī)需要編碼?

    3.1、概述

    編碼(Encode)是信息從一種形式轉(zhuǎn)換為另一種形式的過程,比如用預(yù)先規(guī)定的方法將字符(文字、數(shù)字、符號(hào)等)、圖像、聲音或其它對(duì)象轉(zhuǎn)換成規(guī)定的電脈沖信號(hào)或二進(jìn)制數(shù)字。

    我們現(xiàn)在看到的一幅幅圖畫,聽到的一首首音樂,甚至我們寫的一行行代碼,敲下的一個(gè)個(gè)字符,所看到的所聽到的都是那么的真實(shí),但其實(shí)在背后都是一串“01”的數(shù)字。你昨天在手機(jī)上看到的那個(gè)心動(dòng)女孩,真實(shí)世界中并不存在,只是計(jì)算機(jī)用“01”數(shù)字幫你生成的“骷髏”而已(宅男夢(mèng)碎。。。)。

    3.2、二進(jìn)制其實(shí)不存在

    你可能認(rèn)為計(jì)算機(jī)中的數(shù)據(jù)就是“01”二進(jìn)制,但是實(shí)際上計(jì)算機(jī)中并沒有二進(jìn)制,即便我們知道所有的內(nèi)容都是存儲(chǔ)在硬盤中,但是你把它拆開可找不到里面有任何“0101”的數(shù)字,里面也只有盤片、磁道。就算我們放大了去看盤片,也只有凹凸不平的盤面,凸起的地方是被磁化過的,凹進(jìn)去的地方是沒有被磁化的;只是我們給凸起的地方取了個(gè)名字叫數(shù)字“1”,凹進(jìn)的地方取名叫數(shù)字“0”。

    同樣內(nèi)存里你也找不到二進(jìn)制數(shù)字:內(nèi)存放大了看就是一堆電容組,內(nèi)存單元存儲(chǔ)的是“0”還是“1”取決于電容是否有電荷,有電荷我們認(rèn)為他是“1”,無電荷認(rèn)為他是“0”。但是電容是會(huì)放電的,時(shí)間一長,代表“1”的電容會(huì)放電,代表“0”的電容會(huì)吸電,這也是我們內(nèi)存不能斷電的原因,需要定期對(duì)電容進(jìn)行充電,保證“1”的電容電量有電。

    再說顯示器:這個(gè)大家感受是最直接的,你透過顯示器看到的美女畫皮、日月山川,其實(shí)就是一個(gè)個(gè)不同顏色的發(fā)光二極管發(fā)出強(qiáng)弱不一的光點(diǎn),顯示器就是一群發(fā)光二極管組成的矩陣,其中每一個(gè)二極管可以被稱為一個(gè)像素,“1”表示亮,“0”表示滅,而我們平時(shí)能看到五彩的顏色,是把三種顏色(紅綠藍(lán)三原色)的發(fā)光二極管做到了一起。那對(duì)于一個(gè)ASCII編碼“65”最后又怎么顯示成“A”的呢?這就是顯卡的功勞,顯卡中存儲(chǔ)了每一個(gè)字符的圖形數(shù)據(jù)(也稱字形碼),將二維矩陣的圖形數(shù)據(jù)傳給顯示器成像(如下圖所示)。

    因此:所謂的0和1都是電流脈沖信號(hào),二進(jìn)制其實(shí)是我們抽象出來的數(shù)學(xué)邏輯概念。那我們?yōu)槭裁匆枚M(jìn)制表示?

    因?yàn)椋?/strong>二進(jìn)制只有兩種狀態(tài),使用有兩個(gè)穩(wěn)定狀態(tài)的物理器件就可以表示二進(jìn)制中的每一位,例如用高低電平或電荷的正負(fù)性、燈的亮和滅都可以很方便地用“0”和“1”來表示,這為計(jì)算機(jī)實(shí)現(xiàn)邏輯運(yùn)算和邏輯判斷提供了便利條件。

    四、計(jì)算機(jī)編碼轉(zhuǎn)換過程

    4.1、概述

    正因?yàn)橛?jì)算機(jī)只能表示“01”的邏輯概念,無法直接表示圖片以及文字,所以我們需要一定的轉(zhuǎn)換過程。

    這其實(shí)就是我們按照一定的規(guī)則維護(hù)了字符-數(shù)字的映射關(guān)系,比如我們把“A”抽象成計(jì)算機(jī)中的“1”,當(dāng)我們看到1的時(shí)候就認(rèn)為這是“A”,本質(zhì)上就是一張映射表,理論上你可以隨意給每個(gè)字符分配一個(gè)獨(dú)一無二的編號(hào)(character code,字符編碼)。

    比如下表這樣:

    接下來我們來看下一個(gè)文字從“輸入-轉(zhuǎn)碼存儲(chǔ)-輸出(顯示/打印)”的簡單流程。

    首先:我們知道計(jì)算機(jī)是美國人發(fā)明的,規(guī)則是美國人定的,鍵盤上的按鍵也都是英文字母,所以編號(hào)不是你想怎么分配就怎么分配。對(duì)于英文字母的輸入,鍵盤和ASCII碼之間是直接對(duì)應(yīng)的,鍵盤按鍵“A”對(duì)應(yīng)的編號(hào)“65”,存儲(chǔ)到磁盤上也是“65”的二進(jìn)制直譯“01000001”,這很好理解。

    但是:對(duì)于漢字輸入就不是這么回事了,鍵盤上可沒有漢字對(duì)應(yīng)的輸入按鍵,我們不可能直接敲出漢字字符。于是就有了輸入碼、機(jī)內(nèi)碼、字形碼的轉(zhuǎn)換關(guān)系,輸入碼幫助我們把英文鍵盤按鍵轉(zhuǎn)換成漢字字符,機(jī)內(nèi)碼幫助我們把漢字字符轉(zhuǎn)換成二進(jìn)制序列,字形碼幫助我們把二進(jìn)制序列輸出到顯示器成像。

    4.2、輸入碼

    我們模擬下漢字的輸入過程。

    首先:打開txt文本敲下“nihao”的拼音字母,然后輸入欄會(huì)彈出多個(gè)符合條件的漢字詞組,最后我們會(huì)選擇相應(yīng)的編號(hào),就能實(shí)現(xiàn)漢字的輸入。

    那這過程又是如何實(shí)現(xiàn)的呢?

    計(jì)算機(jī)領(lǐng)域有一句如同摩西十誡般的神圣哲言:“計(jì)算機(jī)科學(xué)領(lǐng)域的任何問題都可以通過增加一個(gè)間接的中間層來解決”。

    這里我們?cè)偌右粚影存I字母組合和漢字的映射表,好比英漢字典,這層我們稱為輸入碼,輸入碼到內(nèi)碼的過程就是一次查表轉(zhuǎn)換操作,比如“nihao”這幾個(gè)ASCII字符,大家可以隨便修改映射表以及候選編號(hào),我可以把他映射成“你好驍飏”(如下圖所示)。

    4.3、機(jī)內(nèi)碼

    機(jī)內(nèi)碼也稱內(nèi)碼,是字符編碼最核心的部分。

    機(jī)內(nèi)碼是字符集在計(jì)算機(jī)中實(shí)際存儲(chǔ)、交換、通信使用的二進(jìn)制編碼,通過內(nèi)碼我們可以達(dá)到高效率的存儲(chǔ)、傳輸文本的目的。我們的外碼(輸入碼)實(shí)現(xiàn)了鍵盤按鍵和字符的映射轉(zhuǎn)換,但是機(jī)內(nèi)碼是讓字符真正變成了機(jī)器能讀懂的二進(jìn)制語言。

    4.4、字形碼

    計(jì)算機(jī)中的字符都是以內(nèi)碼的二進(jìn)制形式表示,我們?cè)趺窗褦?shù)字對(duì)應(yīng)的字符在顯示器上顯示出來呢,比如數(shù)字“1”代表漢字“你”,怎么把“1”顯示成“你”?

    這就需要依賴字形碼,字形碼本質(zhì)上是一個(gè)n*n 的像素點(diǎn)陣,把某些位置的像素設(shè)置為白色(用 1 表示),其它位置像素設(shè)置為黑色(用 0 表示),每一個(gè)字符的字形都是預(yù)先存放在計(jì)算機(jī)內(nèi),而這樣的字形信息庫我們稱為字庫

    比如中文“你”的點(diǎn)陣圖,這樣一個(gè) 16*16 的像素矩陣,需要 16 * 16 / 8 = 32 字節(jié)的空間來表示,右邊的字模信息稱為字形碼。不同的字庫(如宋體、黑體)對(duì)同一個(gè)字符的字形編碼是不同的。

    所以字符編碼到顯示的字形碼,其實(shí)又是另一張查找表,也就是字符編碼-字形碼的映射關(guān)系表。

    其實(shí)我們也可以認(rèn)為字符編碼是字形碼的一種壓縮方式,一個(gè)占32字節(jié)的像素點(diǎn)陣壓縮成了2字節(jié)的機(jī)內(nèi)碼。

    五、字符編碼的歷史

    5.1、電報(bào)編碼

    從廣義上來說,編碼的歷史很悠久,一直可以追溯到結(jié)繩記事的遠(yuǎn)古時(shí)期,但跟現(xiàn)代字符編碼比較接近的還是摩爾斯電碼的發(fā)明,自此開啟了信息通信時(shí)代的大門。

    摩爾斯電碼是由美國人摩爾斯在1837年發(fā)明的,比起ASCII還要早100多年,在早期的無線電上作用非常大,它是每個(gè)無線電通訊者需必知的,它的是由點(diǎn)dot “.” 和劃dash “-” 這兩種符號(hào)所組成的,電報(bào)中表達(dá)為短滴和長嗒,跟二進(jìn)制一樣也是二元碼。

    一個(gè)二元肯定不夠表示我們的字母,那么就用多個(gè)二元來表示,比如嘀嗒“.-”代表字母“A”,嗒嘀嘀嘀“-...”代表字母“B”。

    摩爾斯電碼表:

    5.2、編碼紀(jì)元

    計(jì)算機(jī)一開始發(fā)明出來時(shí)是用來解決數(shù)學(xué)計(jì)算問題的,后來人們發(fā)現(xiàn),計(jì)算機(jī)還可以做更多的事,例如文本處理等。那個(gè)時(shí)候的機(jī)器都很大,機(jī)器之間都是隔離的,沒考慮過機(jī)器的通信問題,各大廠商也各干各個(gè)的,搞自己的硬件搞自己的軟件,想怎么編碼就怎么編碼。

    后來機(jī)器間需要相互通信的時(shí)候,發(fā)現(xiàn)在不同計(jì)算機(jī)上顯示出來的字符不一樣,在IBM上“00010100”數(shù)字代表“A”,跑到微軟系統(tǒng)上顯示成了“B”,大家就傻眼了。于是美國的標(biāo)準(zhǔn)化組織就跑出來制定了ASCII編碼(American Standard Code for Information Interchange),統(tǒng)一了游戲規(guī)則,規(guī)定了常用符號(hào)用哪些二進(jìn)制數(shù)來表示。

    5.3、百花齊放

    統(tǒng)一ASCII 碼標(biāo)準(zhǔn)對(duì)于英語國家很開心,但是ASCII編碼只考慮了英文字母,后來計(jì)算機(jī)傳到歐洲地區(qū),法國人需要加個(gè)字母符號(hào)(如:é),德國人又需要加幾個(gè)字母(Ä ä、Ö ö、ü ü、ß),幸好ASCII只用了前127個(gè)編號(hào),于是歐洲人就將ASCII沒用完的編碼(128-255)為自己特有的符號(hào)編碼,也能很好的一起玩耍。

    但是等傳到我們中國后,做為博大精深的漢語言就徹底蒙圈了,我們有幾萬個(gè)漢字,255個(gè)編號(hào)完全不夠用啊,所以有了后來的多字節(jié)編碼… 因此,各個(gè)國家都推出了本國語言的編碼表,也就有了后來的 ISO 8859 系列、GB系列(GB2312、GBK、GB18030、GB13000)、Big5、EUC-KR、JIS … ,不過為了能在計(jì)算機(jī)系統(tǒng)中通用,這些擴(kuò)展的編碼均直接或間接兼容 ASCII 碼。

    而微軟/IBM這些國際化產(chǎn)商為了把自己的產(chǎn)品賣到全世界,就需要支持各個(gè)國家的語言,要在不同的地方采用當(dāng)?shù)氐木幋a方式,于是他們就把全世界的編碼方式都集中到一起并編上號(hào),并且起了個(gè)名字叫代碼頁(Codepage,又稱內(nèi)碼表),所以我們有時(shí)候也會(huì)看到xx代碼頁來指代某種字符編碼,比如在微軟系統(tǒng)里 中文GBK編碼對(duì)應(yīng)的是936代碼頁,繁體中文 Big5編碼對(duì)應(yīng)的是950代碼頁。

    這些既兼容ASCII又互相之間不兼容的字符編碼,后來又統(tǒng)稱為ANSI編碼。看到下面這張圖估計(jì)大家就很熟悉了,window下面我們基本上都用ANSI編碼保存。

    ANSI的字面意思并非指字符編碼,而是美國的一個(gè)非營利組織,是美國國家標(biāo)準(zhǔn)學(xué)會(huì)(American National Standards Institute)的縮寫,ANSI這個(gè)組織為字符編碼做了很多標(biāo)準(zhǔn)制定工作,后來大家習(xí)慣把這類混亂的多字節(jié)編碼叫ANSI編碼或者標(biāo)準(zhǔn)代碼頁。

    ANSI編碼只是一個(gè)范稱,一般代表系統(tǒng)默認(rèn)的編碼方式,而且并不是確定的某一種編碼方式——比如在Window操作系統(tǒng)里,中國區(qū)ANSI編碼指的是GB編碼,在香港地區(qū)ANSI編碼指的是Big5編碼,在韓國ANSI編碼指的是EUC-KR編碼。

    5.4、天下一統(tǒng)

    由于各個(gè)國家各搞各的字符編碼,如果有些人想裝逼中文里飚兩句韓文怎么辦呢?不好意思,你的逼級(jí)太高,沒法支持,你選擇了GB2312就只能打出中文字符。同時(shí)各大國際廠商在兼容各種字符編碼問題上也深受折磨,于是忍無可忍之下,決定開發(fā)一套能容納全世界所有字符的編碼,就有了后面大名鼎鼎的Unicode。

    Unicode也叫萬國碼,包括字符集、編碼方案等。

    Unicode是為了解決傳統(tǒng)的字符編碼方案的局限而產(chǎn)生的,它為每種語言中的每個(gè)字符設(shè)定了統(tǒng)一并且唯一的二進(jìn)制編碼,在這種語言環(huán)境下,不會(huì)再有語言的編碼沖突,在同屏下也可以顯示任何國家語言的內(nèi)容,這就是Unicode的最大好處。

    在Unicode編碼方案里常見的有四種編碼實(shí)現(xiàn)方案UTF-7、 UTF-8、UTF-16、UTF-32,最為知名的就是 UTF-8。Unicode設(shè)計(jì)之初是采用雙字節(jié)定長編碼的UTF-16,但是發(fā)現(xiàn)歷史包袱太重推不動(dòng),最后出了個(gè)變長的UTF-8才被廣泛接受。

    六、字符編碼模型

    6.1、傳統(tǒng)編碼模型

    在傳統(tǒng)字符編碼模型中,基本上都是將字符集里的字符用十進(jìn)制進(jìn)行逐一的編號(hào),然后把十進(jìn)制編號(hào)直接轉(zhuǎn)成對(duì)應(yīng)的二進(jìn)制碼,可以說該字符編號(hào)就是字符的編碼。

    計(jì)算機(jī)在處理字符與數(shù)字的轉(zhuǎn)換關(guān)系上其實(shí)就是查找映射表的過程。

    像ASCII編碼就是給每個(gè)英文字符編一個(gè)獨(dú)一無二的數(shù)字,整個(gè)編碼處理過程相對(duì)還是比較簡單的,計(jì)算機(jī)內(nèi)部直接就映射成了二進(jìn)制,十進(jìn)制的編號(hào)只是方便我們看的。

    6.2、現(xiàn)代編碼模型

    Unicode編碼模型采用了一個(gè)全新的編碼思路,將編碼模型劃分為4 個(gè)層次(也有說5個(gè)層次的),不過第五層是傳輸層的編碼適配,放在編碼模型里嚴(yán)格來說不是很恰當(dāng)。

    這 4 個(gè)層次分別是:

    • 1)第一層,抽象字符集 ACR(Abstract Character Repertoire):定義抽象字符集合,明確各個(gè)抽象字符;
    • 2)第二層,編號(hào)字符集 CCS(Coded Character Set):將抽象字符集進(jìn)行數(shù)字編號(hào);
    • 3)第三層,字符編碼方式 CEF(Character Encoding Form):將字符編號(hào)編碼為邏輯上的碼元序列;
    • 4)第四層,字符編碼方案 CES(Character Encoding Scheme):將邏輯上的碼元序列編碼為物理字節(jié)序列。

    下面將分別來詳細(xì)講講各層。

    6.3、第一層:抽象字符集 ACR

    所謂抽象字符集,就是抽象字符的合集。

    它是一個(gè)無序集合,這里強(qiáng)調(diào)了字符是抽象的,也就是不僅包括我們視覺上能看到的狹義字符,比如“a”這樣的有形字符,也包括一些我們看不到的無形字符,比如一些控制字符“DELETE”、“NULL”等。

    抽象的另一層含義是有些字形是由多個(gè)字符組合成的,比如西班牙語的 “ñ” 由“n”和“~”兩個(gè)字符組成,這一點(diǎn)上 Unicode 和傳統(tǒng)編碼標(biāo)準(zhǔn)不同,傳統(tǒng)編碼標(biāo)準(zhǔn)多是將 ñ 視作一個(gè)獨(dú)立的字符,而 Unicode 中將其視為兩個(gè)字符的組合。

    同時(shí)一個(gè)字符也可能會(huì)有多種視覺上的字形表示,比如一個(gè)漢字有楷、行、草、隸等多種形體,這些都視為同一個(gè)抽象字符(即字符集編碼是對(duì)字符而非字形編碼),如何顯示是字形庫的事。

    漢字“人”的不同形態(tài):

    抽象字符集有開放與封閉之分:開放的字符集指還會(huì)不斷新增字符的字符集,封閉字符集是指不會(huì)新增字符的字符集。比如ASCII就是封閉式的,只有128個(gè)字符,以后也不會(huì)再加,但是Unicode是開放式的,會(huì)不斷往里加新字符的,已經(jīng)從最初的 7163 個(gè)增加到現(xiàn)在的144,697 個(gè)字符。

    6.4、第二層:編號(hào)字符集 CCS

    編號(hào)字符集就是對(duì)抽象字符集里的每個(gè)字符進(jìn)行編號(hào),映射到一個(gè)非負(fù)整數(shù)的集合

    編號(hào)一般用方便人類閱讀的十進(jìn)制、十六進(jìn)制來表示,比如“A”字符編號(hào)“65”,“B”字符編號(hào)是“66”。

    大家需要清楚對(duì)于有些字符編碼的編號(hào)就是存儲(chǔ)的二進(jìn)制序列,如ASCII編碼;有些字符編碼的編號(hào)跟存儲(chǔ)的二進(jìn)制序列并不一樣,比如GB2312、Unicode等。

    另外:編號(hào)字符集合是有范圍限制的,比如ASCII字符集范圍是0~127,ISO-8859-1范圍是0~256,而GB2312是用一個(gè)94*94的二維矩陣空間來表示,Unicode是用Plane平面空間的概念來表示,這稱為字符集的編號(hào)空間。

    編號(hào)空間中的一個(gè)位置稱為碼點(diǎn)( Code Point 代碼點(diǎn) )。一個(gè)字符占用的碼點(diǎn)所在的坐標(biāo)(非負(fù)整數(shù)值對(duì))或所代表的非負(fù)整數(shù)值,就是該字符的碼值(碼點(diǎn)編號(hào))。

    ASCII碼點(diǎn)編號(hào):

    6.5、第三層:字符編碼方式 CEF

    抽象字符集和編號(hào)字符集是站在方便我們理解的角度來看的,所以最后我們需要翻譯成計(jì)算機(jī)能懂的語言,將十進(jìn)制的編號(hào)轉(zhuǎn)換成二進(jìn)制的形式。

    因此:字符編碼方式就是將字符集的碼點(diǎn)編號(hào),轉(zhuǎn)換成二進(jìn)制碼元序列( Code Unit Sequence )的過程。

    碼元:字符編碼的最小處理單元,比如ASCII一個(gè)字符等于一個(gè)字節(jié),屬于單字節(jié)碼元;UTF-16一個(gè)字符等于兩個(gè)字節(jié),處理過程是按字“word”來處理,所以是雙字節(jié)碼元;UTF-8是多字節(jié)編碼,有單字節(jié)字符,也有多字節(jié)字符,每次處理是按單個(gè)單個(gè)字節(jié)解析處理,所以處理最小單位是字節(jié),也屬于單字節(jié)碼元。

    這里大家可能會(huì)有疑問:十進(jìn)制直接轉(zhuǎn)二進(jìn)制不就好了嗎,為什么要單獨(dú)抽出這么一層?

    早期的字符編碼確實(shí)也是這么處理的,十進(jìn)制和二進(jìn)制之間是直接轉(zhuǎn)換過去的,比如ASCII碼,字符“A”的十進(jìn)制是“65”,那對(duì)應(yīng)的二進(jìn)制就是“1000001”,同時(shí)存儲(chǔ)到硬盤里的也是這個(gè)二進(jìn)制,所以那時(shí)候的編碼比較簡單。

    隨著后來多字節(jié)字符編碼(Muilti-Bytes Character Set,MBCS多字節(jié)字符集)的出現(xiàn),字符編號(hào)和二進(jìn)制之間不是直接轉(zhuǎn)換過去的,比如GB2312編碼,“萬”字的區(qū)位編號(hào)是“45,82”,對(duì)應(yīng)的二進(jìn)制機(jī)內(nèi)碼卻是“1100 1101 1111 0010”(其十進(jìn)制是“205,242”)。

    如果這里不轉(zhuǎn)換直接映射成二進(jìn)制碼會(huì)出什么問題呢?“萬”字的字符編號(hào)“45,82”,45在ASCII里是“-”,82是“U”,那到底是顯示兩個(gè)字符“-U”還是顯示一個(gè)字符“萬”字,為了避免這種沖突 所以增加了前綴處理,詳細(xì)的過程會(huì)在下文具體來講解。

    6.6、第四層:字符編碼方案 CES

    字符編碼方案也稱作“序列化格式“( Serialization Format ),指的是將字符編號(hào)進(jìn)行編碼之后的碼元序列映射為字節(jié)序列(即字節(jié)流)的形式,以便經(jīng)過編碼后的字符能在計(jì)算機(jī)中進(jìn)行處理、存儲(chǔ)和傳輸。

    字符編碼方式CEF有點(diǎn)像我們數(shù)據(jù)庫結(jié)構(gòu)設(shè)計(jì)里的邏輯設(shè)計(jì),而這一層編碼方案CES就像是物理設(shè)計(jì)了,將碼元序列映射為跟特定的計(jì)算機(jī)系統(tǒng)平臺(tái)相關(guān)的物理意義上的二進(jìn)制過程。

    這里大家可能又會(huì)有疑問:為什么二進(jìn)制的碼元序列和實(shí)際存儲(chǔ)的二進(jìn)制又會(huì)不一樣呢?

    這主要是計(jì)算機(jī)的大小端序造成的,具體端序內(nèi)容會(huì)在UTF-16編碼部分詳細(xì)介紹。

    “大小端序名詞”出自Jonathan Swift的《格列夫游記》一書 :

    所有人都認(rèn)為,吃雞蛋前,原始的方法是打破雞蛋較大的一端。可是當(dāng)今皇帝的祖父小時(shí)候吃雞蛋,一次按古法打雞蛋時(shí)碰巧將一個(gè)手指弄破了,因此他的父親,當(dāng)時(shí)的皇帝,就下了一道敕令,命令全體臣民吃雞蛋時(shí)打破雞蛋較小的一端,違令者重罰。

    老百姓們對(duì)這項(xiàng)命令極為反感。歷史告訴我們,由此曾發(fā)生過六次叛亂,其中一個(gè)皇帝送了命,另一個(gè)丟了王位…關(guān)于這一爭端,曾出版過幾百本大部著作,不過大端派的書一直是受禁的,法律也規(guī)定該派的任何人不得做官。

    ▲ 圖片引用自《面試必考,史上最通俗大小端字節(jié)序詳解

    對(duì)大小端字節(jié)序問題感興趣的可以詳讀:《腦殘式網(wǎng)絡(luò)編程入門(九):面試必考,史上最通俗大小端字節(jié)序詳解》一文。

    七、常見字符編碼1:ASCII

    很久以前:計(jì)算機(jī)制造商都是按各自的方式來將字符渲染到屏幕上,當(dāng)時(shí)的計(jì)算機(jī)動(dòng)不動(dòng)可就是一套房子的大小,這家伙可不是誰都能玩的起的,那時(shí)人們并不關(guān)心計(jì)算機(jī)如何交流。

    隨著上世紀(jì)七八十年代微處理器的出現(xiàn),計(jì)算機(jī)變得越來越小,個(gè)人計(jì)算機(jī)開始進(jìn)入大眾的視線,隨后出現(xiàn)了井噴式的發(fā)展,但是之前廠商都是各自為政,沒考慮過自家的產(chǎn)品要兼容別人家的東西,導(dǎo)致在不同計(jì)算機(jī)體系間的數(shù)據(jù)轉(zhuǎn)換變得十分蛋疼,因此美國的標(biāo)準(zhǔn)協(xié)會(huì)在1967年制定出了ASCII編碼,到目前為止共定義了128個(gè)字符。

    ASCII 編碼:

    (注意:該表是列表示字節(jié)高 4 位。上圖引用自《字符編碼那點(diǎn)事:快速理解ASCII、Unicode、GBK和UTF-8》)

    其中:前 32 個(gè)(0~31)是不可見的控制字符,32~126 是可見字符,127 是 DELETE 命令(鍵盤上的 DEL 鍵)。

    其實(shí):早在ASCII之前,IBM在1963年也推出過一套字符編碼系統(tǒng)EBCDIC,跟ASCII碼一樣囊括了控制字符、數(shù)字、常用標(biāo)點(diǎn)、大小寫英文字母。

    EBCDIC 編碼:

    但是:他的字符編號(hào)并不是連續(xù)的,這給后續(xù)程序處理帶來了麻煩,后來ASCII 編碼吸取了 EBCDIC 的經(jīng)驗(yàn)教訓(xùn),給英文單詞分配了連續(xù)的編碼,方便程序處理,因此被后來廣泛接受。

    ASCII 和 EBCDIC 編碼相比:除了字符連續(xù)排列之外,最大的優(yōu)點(diǎn)是ASCII 只用了一個(gè)字節(jié)的低 7 位,最高位永遠(yuǎn)是 0。可別小看了這個(gè)最高位的 0,看似無足輕重,但這是ASCII設(shè)計(jì)最成功的地方,后面介紹各編碼原理的時(shí)候你會(huì)發(fā)現(xiàn),正是因?yàn)檫@個(gè)高位0,其它編碼規(guī)范才能對(duì) ASCII 碼無縫兼容,使得 ASCII 被廣泛接受。

    八、常見字符編碼2:ISO-8859系列

    美國市場(chǎng)雖然統(tǒng)一了字符編碼,但是計(jì)算機(jī)制造商在進(jìn)入歐洲市場(chǎng)的時(shí)候又遇到了麻煩。。。

    歐洲的主流語言雖然也是用拉丁字母,但卻存在很多擴(kuò)展體,比如法語的“é”,挪威語中的“Å”,都無法用 ASCII 表示。但是大家發(fā)現(xiàn)ASCII后面的128個(gè)還沒有被使用可以利用起來,這對(duì)于歐洲主流語言就足夠了。

    于是就有了大家所熟知的這個(gè)ISO-8859-1(Latin-1),它只是擴(kuò)展了ASCII后128個(gè)字符,還是屬于單字節(jié)編碼。同時(shí)為了兼容原先的 ASCII碼,當(dāng)最高位是0的時(shí)候仍然表示原先的 ASCII 字符不變,當(dāng)最高位是1的時(shí)候表示擴(kuò)展的歐洲字符。

    但是到這里還沒有完:剛說了這只是歐洲主流的語言,但主流語言里沒有法語使用的 œ、Œ、Ÿ 三個(gè)字母,也沒有芬蘭語使用的 Š、š、?、? ,而單字節(jié)編碼里的256個(gè)碼點(diǎn)都被用完了,于是就出現(xiàn)了更多的變種 ISO-8859-2/3/.../16 系列,他們都兼容 ASCII,但彼此間又不完全兼容。

    ISO-8859-n系列字符集如下:

    • 1)ISO8859-1 字符集,也就是 Latin-1,是西歐常用字符,包括德法兩國的字母;
    • 2)ISO8859-2 字符集,也稱為 Latin-2,收集了東歐字符;
    • 3)ISO8859-3 字符集,也稱為 Latin-3,收集了南歐字符;
    • 4)ISO8859-4 字符集,也稱為 Latin-4,收集了北歐字符;
    • 5)ISO8859-5 字符集,也稱為 Cyrillic,收集了斯拉夫語系字符;
    • 6)ISO8859-6 字符集,也稱為 Arabic,收集了阿拉伯語系字符;
    • 7)ISO8859-7 字符集,也稱為 Greek,收集了希臘字符;
    • .......

    九、常見字符編碼3:GB系列

    9.1、概述

    當(dāng)計(jì)算機(jī)進(jìn)入東亞國家的時(shí)候,廠商們更傻眼了,美國和歐洲國家語言基本都是表音字符,一個(gè)字節(jié)就足夠用了,但亞洲國家有不少是表意字符,字符個(gè)數(shù)動(dòng)輒幾萬十幾萬的,一個(gè)字節(jié)完全不夠用。

    所以我們國家有關(guān)部門按照ISO規(guī)范設(shè)計(jì)了GB2312雙字節(jié)編碼。但是GB2312是一個(gè)封閉字符集,只收錄了常用字符總共也就7000多個(gè)字符,因此為了擴(kuò)充更多的字符包括一些生僻字,才有了之后的GBK、GB18030、GB13000(“GB” 為 “國標(biāo)” 的漢語拼音首字母縮寫)。

    按照 GB 系列編碼方案,在一段文本中,如果一個(gè)字節(jié)是 0~127,那么這個(gè)字節(jié)的含義與 ASCII 編碼相同,否則,這個(gè)字節(jié)和下一個(gè)字節(jié)共同組成漢字(或是 GB 編碼定義的其他字符),所以GB系列都是兼容ASCII編碼的。

    9.2、GB2312

    GB2312是使用兩個(gè)字節(jié)來表示漢字的編碼標(biāo)準(zhǔn),共收入漢字6763個(gè)和非漢字圖形字符682個(gè)。

    為了避免與 ASCII 字符編碼(0~127)相沖突,規(guī)定表示一個(gè)漢字的編碼字節(jié)其值必須大于127(即字節(jié)的最高位為 1 ),并且必須是兩個(gè)大于 127 的字節(jié)連在一起來共同表示一個(gè)漢字( GB2312 為雙字節(jié)編碼),所以GB2312 屬于變長編碼,當(dāng)是英文字符的時(shí)候占一個(gè)字節(jié),中文字符的時(shí)候占兩個(gè)字節(jié),可以認(rèn)為 GB2312是對(duì) ASCII 的中文擴(kuò)展。

    GB2312字符集編號(hào)空間是一個(gè)94*94的二維表,行表示區(qū)(高位字節(jié)),列表示位(低位字節(jié)),每區(qū)有94個(gè)位,每個(gè)區(qū)位對(duì)應(yīng)一個(gè)字符,稱為區(qū)位碼。區(qū)位碼上加2020H,就得到國標(biāo)碼,國標(biāo)碼上加8080H,就得到常用的計(jì)算機(jī)機(jī)內(nèi)碼。

    這里引入了區(qū)位碼、國標(biāo)碼、機(jī)內(nèi)碼概念,下面我們說下三者的關(guān)系。

    9.2.1國標(biāo)碼

    國標(biāo)碼是我國漢字信息交換的標(biāo)準(zhǔn)編碼,規(guī)定由4位16進(jìn)制數(shù)組成,用兩個(gè)低7位字節(jié)表示,為了避開 ASCII 字符中的前32個(gè)控制指令字符,所以每個(gè)字節(jié)都是從第33個(gè)編號(hào)開始。

    如下圖所示:

    9.2.2區(qū)位碼

    由于上述國標(biāo)碼的16進(jìn)制可編碼區(qū)不夠直觀不方便我們使用,所以我們把他映射成了十進(jìn)制的94*94二維表編號(hào)空間,我們稱之為區(qū)位碼,同時(shí)區(qū)位碼也可以當(dāng)成一種外碼使用,輸入法可以直接切換成區(qū)位碼進(jìn)行漢字輸入。

    不過這種輸入法無規(guī)則可言 人們很難記住區(qū)位編號(hào),用的人也不多了。

    下圖是區(qū)位碼的二維表,比如“萬”字是45 區(qū) 82 位,所以“萬” 字的區(qū)位碼是“45,82”。

    其中:

    • 1)01~09區(qū)(682個(gè)):特殊符號(hào)、數(shù)字、英文字符、制表符等(包括拉丁字母、希臘字母、日文平假名及片假名字母、俄語西里爾字母等在內(nèi)的682個(gè)全角字符);
    • 2)10~15區(qū):空區(qū),留待擴(kuò)展;
    • 3)16~55區(qū)(3755個(gè)):常用漢字(也稱一級(jí)漢字),按拼音排序;
    • 4)56~87區(qū)(3008個(gè)):非常用漢字(也稱二級(jí)漢字),按部首/筆畫排序;
    • 5)88~94區(qū):空區(qū),留待擴(kuò)展。

    9.2.3機(jī)內(nèi)碼

    GB2312國標(biāo)碼規(guī)范是覆蓋掉ASCII中可見部分的符號(hào)和英文字母,使用兩個(gè)7位碼將其中的英文字母和符號(hào)重新編入。

    但是這樣產(chǎn)生一個(gè)弊端:早期用ASCII碼編碼的英文文章無法打開,一打開就是亂碼,也就是說應(yīng)該要兼容早期ASCII碼而不是覆蓋它。

    后來微軟為了解決這個(gè)問題:將字節(jié)的最高位設(shè)為1,因?yàn)锳SCII中使用7位,最高位為0,轉(zhuǎn)換后的編碼稱為機(jī)內(nèi)碼(內(nèi)碼),這種方式本質(zhì)上是修改了GB2312的編碼標(biāo)準(zhǔn),最后被大家接受沿用。

    總結(jié)下三者轉(zhuǎn)換關(guān)系:區(qū)位碼 ---> 區(qū)碼和位碼分別 + 32(即 + 20H )得到國標(biāo)碼 ---> 再分別 + 128(即 + 80H)得到機(jī)內(nèi)碼(與 ACSII 碼不再?zèng)_突)。

    9.3、GBK

    GBK即“國標(biāo)擴(kuò)展”的意思,因?yàn)镚B2312雙字節(jié)的最高位都要求大于1,上限也不會(huì)超過1萬個(gè)字符,所以對(duì)此進(jìn)行了擴(kuò)展,對(duì)GB2312的字符不重新編碼直接沿用,因此完全兼容GB2312。

    GBK雖然也是雙字節(jié)編碼,但是只要求第一個(gè)字節(jié)大于 127 就固定表示這是一個(gè)漢字的開始,正因?yàn)槿绱耍珿BK的編碼空間比GB2312大很多。

    GBK 整體編碼范圍為 8140-FEFE,首字節(jié)在 81-FE 之間,尾字節(jié)在 40-FE 之間,剔除 xx7F 一條線,總計(jì) 23940 個(gè)碼位,共收入 21886 個(gè)漢字和圖形符號(hào)。其中 GBK/1 收錄除 GB 2312 字符外的其他增補(bǔ)字符,GBK/2 收錄 GB2312 字符,GBK/3 收錄 CJK 字符,GBK/4 收錄 CJK 字符和增補(bǔ)字符,GBK/5 為非中文字符,UDC 為用戶自定義字符。

    詳細(xì)如下如所示:

    這里大家可能會(huì)有兩個(gè)疑問:為什么尾字節(jié)要從40開始,而不是00開始?為什么要排除 FF、xx7F這兩條線的編號(hào)?

    GBK的尾字節(jié)編碼高位沒有強(qiáng)制要求是1,當(dāng)高位是0時(shí)跟ASCII碼是沖突的,ASCII碼里00-40之間大部分都是控制字符,所以排除控制字符主要是為了防止丟失高字節(jié)導(dǎo)致出現(xiàn)系統(tǒng)性嚴(yán)重后果。

    排除FF是為了兼容GB2312,GB2312這個(gè)位是保留不使用的;而7F表示DEL字符就是向后刪除一個(gè)字符,如果傳輸過程中丟失首字節(jié)那么就會(huì)出現(xiàn)嚴(yán)重的后果,所以需要將xx7F也排除,這是所有編碼方案都需要注意的地方。

    9.4、GB18030

    隨著計(jì)算機(jī)的發(fā)展,GBK的2萬多個(gè)字符也還是扛不住。

    于是2000年我國又制定了新標(biāo)準(zhǔn) GB18030,用來替代 GBK 標(biāo)準(zhǔn)。GB18030是強(qiáng)制性標(biāo)準(zhǔn),現(xiàn)在在中國大陸銷售的軟件都支持 GB18030。

    GB18030其實(shí)是對(duì)齊Unicode標(biāo)準(zhǔn)的,里面包括了所有Unicode字符集,也算是Unicode的一種實(shí)現(xiàn)(UTF)。

    那既然有了UTF我們?yōu)槭裁催€要搞一套Unicode實(shí)現(xiàn)?

    主要是UTF-8/UCS-2他們是不兼容GB2312的,如果直接升級(jí)那么就全亂碼了,所以GB18030是為了兼容GB系列,是GBK、GB2312的超集,當(dāng)我們?cè)鹊腉B2312(GBK)軟件考慮升級(jí)到國際化Unicode時(shí),可以直接使用GB18030進(jìn)行升級(jí)。

    GB18030雖然也是GB2312的擴(kuò)展,但它和GBK的擴(kuò)展方式不一樣,GBK主要是充分利用了GB2312的一些沒定義的編碼空間,而GB18030采用的是字節(jié)變長編碼,單字節(jié)區(qū)兼容ASCII、雙字節(jié)區(qū)兼容GBK、四字節(jié)區(qū)對(duì)齊所有Unicode 碼位。

    實(shí)現(xiàn)原理上主要是采用第二字節(jié)未使用到的0x30~0x39編碼空間來判斷是否四字節(jié)。

    具體就是:

    • 1)單字節(jié),其值從0到0x7F。
    • 2) 雙字節(jié),第一個(gè)字節(jié)的值從0x81到0xFE,第二個(gè)字節(jié)的值從0x40到0xFE(不包括0x7F)。
    • 3) 四字節(jié),第一個(gè)字節(jié)的值從0x81到0xFE,第二個(gè)字節(jié)的值從0x30到0x39,第三個(gè)字節(jié)的值從0x81到0xFE,第四個(gè)字節(jié)的值從0x30到0x39。

    十、常見字符編碼4:UNICODE

    10.1、背景介紹

    在統(tǒng)一碼之前,各國創(chuàng)造了大量的節(jié)編碼標(biāo)準(zhǔn),有單字節(jié)的、雙字節(jié)的(如 GB 2312、Shift JIS、Big5 、ISO8859等),各自又相互不兼容。在1987 年,蘋果、Sun、微軟等公司開始討論囊括全世界所有字符的統(tǒng)一編碼標(biāo)準(zhǔn),組成了 Unicode 聯(lián)盟,這個(gè)期間做了很多研討工作,討論核心要點(diǎn)如下。

    1)目前世界上有多少個(gè)字符,需要幾個(gè)字節(jié)存儲(chǔ)?

    工作組統(tǒng)計(jì)了當(dāng)時(shí)全世界的報(bào)紙等刊物,結(jié)論是兩個(gè)字節(jié)足以囊括全世界有實(shí)用意義的字符(當(dāng)然這只統(tǒng)計(jì)了當(dāng)前使用的字符,不包括古代語言或者廢棄語言)。

    2)采用固定長度編碼還是變長編碼?

    一種采用變長編碼形式,對(duì)于 ASCII 字符使用一個(gè)字節(jié),其他字符使用兩個(gè)字節(jié),類似 GBK。另一種采用定長編碼形式,不管是不是 ASCII 字符統(tǒng)一使用兩個(gè)字節(jié)。

    方案選擇上主要從計(jì)算機(jī)處理過程中的時(shí)間和空間兩個(gè)維度,也就是編解碼的執(zhí)行效率和存儲(chǔ)大小兩方面。

    最后結(jié)論是采用雙字節(jié)定長編碼,因?yàn)槎ㄩL帶來的空間變大在整體傳輸、存儲(chǔ)成本上其實(shí)影響并不大,而定長編碼處理效率會(huì)明顯高于變長編碼,所以早期 Unicode 采用了定長編碼形式。

    3)中、日、韓中有很多相近的表意文字是否可以統(tǒng)一?

    由于漢字表意文字字符量較大,如果可以統(tǒng)一那么能大幅減少收錄漢字的數(shù)量。

    所以最初收錄漢字遵循兩個(gè)基本原則:表意文字認(rèn)同原則和字源分離原則。

    所謂表意文字認(rèn)同原則:即“只對(duì)字,不對(duì)形”編碼,將同一字的不同字形(即異體字)合并。例如“房”字的第一筆,在中日韓的寫法都不同,但它本身是同一個(gè)字,只給一個(gè)編碼,而寫法的不同交由字體進(jìn)行區(qū)分。

    字源分離原則:是指一個(gè)字源中同時(shí)收錄了同一個(gè)字的不同字形,則給予兩個(gè)字形分別編碼。例如:之前GBK中就收錄了“戶”、“戶”、“戸”三個(gè)字,那么Unicode也需要保留三個(gè)字,如果直接合并會(huì)造成使用上的困擾。

    例如下面這句話如果不做字源分離,會(huì)是什么情況呢?

    原句 :戶有三種寫法,分別是“戶”、“戶”、“戸”,

    改寫后:戶有三種寫法,分別是“戶”、“戶”、“戶”

    10.2、Unicode介紹

    Unicode 稱為統(tǒng)一碼(也叫萬國碼),是按現(xiàn)代編碼模型進(jìn)行設(shè)計(jì)的一套字符編碼體系,涵蓋抽象字符集、編號(hào)、邏輯編碼、編碼實(shí)現(xiàn)。

    Unicode是為了解決傳統(tǒng)的字符編碼方案的局限而產(chǎn)生的,在這種語言環(huán)境下,不會(huì)再有語言的編碼沖突,可以在同屏下顯示任何國家的語言。

    UTF-n編碼(Unicode Transformation Format Unicode字符集轉(zhuǎn)換格式,n表示碼元位數(shù))是Unicode這套編碼體系里的編碼實(shí)現(xiàn)CES部分,像UTF-8、UTF-16、UTF-32都是將數(shù)字轉(zhuǎn)換到實(shí)際的二進(jìn)制編碼實(shí)現(xiàn),Unicode的編碼實(shí)現(xiàn)除了UTF系列之外,還有UCS-2/4,GB18030等。但是現(xiàn)在很多人誤把Unicode當(dāng)成只是一個(gè)字符編號(hào),這其實(shí)是不對(duì)的。

    Unicode可以容納世界上所有國家的文字和符號(hào),其編號(hào)范圍是0-0x10FFFF,有1,114,112個(gè)碼位,為了方便管理劃分成17個(gè)平面,現(xiàn)已定義的碼位有238,605個(gè),分布在平面0、平面1、平面2、平面14、平面15、平面16。其中平面0又稱為基本多語言平面(Basic Multilingual Plane,簡稱BMP),這個(gè)平面基本涵蓋了當(dāng)今世界上正在使用中的常用字符。我們平常用到的字符,一般都是位于 BMP 平面上的,其范圍擁有 65,536 個(gè)碼點(diǎn),其他平面統(tǒng)稱增補(bǔ)平面,關(guān)于平面的概念會(huì)在UTF-16章節(jié)詳細(xì)介紹。

    10.3、與UCS的關(guān)系

    說起Unicode我們不得不提UCS(全稱Universal Multiple-Octet Coded Character Set 通用多八位編碼字符集),國際標(biāo)準(zhǔn)編號(hào)ISO/IEC 10646,是由 ISO 和 IEC 兩家國際標(biāo)準(zhǔn)組織聯(lián)合成立的工作組設(shè)計(jì)的一套新的統(tǒng)一字符集項(xiàng)目,目的與Unicode 聯(lián)盟一樣致力于開發(fā)一款全世界通用的編碼集。

    早在1984 年ISO 和 IEC 兩家組織就成立了一個(gè)聯(lián)合工作組來設(shè)計(jì)一套新的統(tǒng)一字符集標(biāo)準(zhǔn),但是這兩個(gè)組織都不知道對(duì)方的存在,直到Unicode聯(lián)盟1988年發(fā)布了Unicode草案(UCS草案1989年發(fā)布),才發(fā)現(xiàn)大家在做同一件事,沒有必要搞兩套標(biāo)準(zhǔn) 所以后面又考慮合并。

    由于UCS 最初設(shè)計(jì)的是 31 位編碼空間(UCS-4編碼實(shí)現(xiàn)),可以容納 2^31 約 21 億個(gè)字符,而Unicode是16位空間(UTF-16編碼實(shí)現(xiàn)),所以最開始Unicode 打算作為 UCS 的真子集,即 Unicode 中的每個(gè)字符都存在于 UCS 中,而且兩者的碼點(diǎn)相同,但 UCS 中的字符(編號(hào)超過65,536的)則不一定存在于 Unicode 中。

    不過:由于雙方利益關(guān)系并沒有說誰解散誰,最后雙方作出一些妥協(xié)保持一致共同發(fā)展,兩個(gè)標(biāo)準(zhǔn)中相同字符的編碼(碼點(diǎn))必須是一樣的。這是一個(gè)屁股決定腦袋的決策,如果最初Unicode知道UCS的存在,就不會(huì)再出現(xiàn)Unicode了。

    當(dāng)然合并工作不是一蹴而就的而是經(jīng)過多輪迭代, ISO/IEC 和 Unicode在 1993 年發(fā)布了第一版相互兼容版本,到了 1996年Unicode 2.0標(biāo)準(zhǔn)發(fā)布時(shí),Unicode 字符集和 UCS 字符集(即 ISO/IEC 10646-1 )基本保持了一致,同時(shí)Unicode為了跟UCS的四字節(jié)保持一致推出了UTF-32編碼實(shí)現(xiàn),UCS為了跟Unicode的兩字節(jié)保持一致推出了UCS-2編碼實(shí)現(xiàn)。

    所以:現(xiàn)在我們可以認(rèn)為UCS和Unicode是同一個(gè)東西,比如我們常見的java內(nèi)部運(yùn)行就采用的是UTF-16編碼,而window操作系統(tǒng)采用的是UCS-2,他們都是同一個(gè)Unicode標(biāo)準(zhǔn)。

    為什么這里使用的是2字節(jié)編碼,而不是4字節(jié)呢?先留個(gè)懸念,后續(xù)會(huì)詳細(xì)講解。

    10.4、UTF-16(Java內(nèi)部編碼)

    UTF是Unicode Transfer Format的縮寫,即把Unicode轉(zhuǎn)做某種格式的意思,所以UTF-16是Unicode編碼里的其中一種實(shí)現(xiàn)方式,16代表的是字節(jié)位數(shù),占兩個(gè)字節(jié)(UTF-32則表示4個(gè)字節(jié))。

    Unicode 設(shè)計(jì)之初是采用UTF-16這種雙字節(jié)定長編碼的,其字符編號(hào)就是對(duì)應(yīng)的二進(jìn)制編號(hào),也就是說第二層的CCS和第三層的CEF是一致的。比如漢字“萬”的 Unicode 碼點(diǎn)是 “U+4E07”,其二進(jìn)制序列就是直譯的“0100 1110 0000 0111 ”,這種編碼方式的優(yōu)點(diǎn)是高效,不需要檢查標(biāo)志位,但缺點(diǎn)是不兼容ASCII,ASCII編碼的文本都會(huì)顯示亂碼。

    不過:后來Unicode聯(lián)盟發(fā)現(xiàn) 16 位編碼空間根本不夠用,與此同時(shí) ISO/IEC組織也覺得 UCS的 32 位編碼空間太多了,實(shí)際中根本沒有幾十億字符,也挺浪費(fèi)空間的。

    所以最終 Unicode 聯(lián)盟和 ISO/IEC 工作組達(dá)成一致:兩者使用統(tǒng)一的編碼空間“ 0000 ~ 10FFFF”(即 UCS 保證永遠(yuǎn)不分配大于 10FFFF 的字符碼點(diǎn)),而且雙方在字符編碼上保持同步,即一方標(biāo)準(zhǔn)中增加了字符,也要通知另一方同步。

    于是:Unicode在UTF-16基礎(chǔ)上拓展編碼空間到 21 位,UCS則搞了一個(gè)雙字節(jié)的UCS-2編碼實(shí)現(xiàn)。

    UTF-16 編碼是雙字節(jié)的,上限也只有6w多個(gè)碼點(diǎn),怎么讓他支持到10FFFF(100w+)個(gè)碼點(diǎn)呢?

    本質(zhì)就是:多加幾個(gè)字節(jié)來表示更多的字符,只是UTF-16不像UCS那樣采用定長4字節(jié),而是使用變長的形式,但是這個(gè)跟UTF-8變長方式又不太一樣,他是采用代理對(duì)的方式實(shí)現(xiàn),大部分常用字符用一個(gè)碼元表示(定長2個(gè)字節(jié)),其他擴(kuò)展的特殊字符用兩個(gè)碼元表示(定長4字節(jié))。

    10.4.1代理對(duì)

    UTF-16跟UTF-8、GB系列等都算是變長字節(jié),但是設(shè)計(jì)初衷卻不一樣,像GBK是為了兼容ASCII,但是UTF-16一開始就沒考慮要兼容ASCII,所以他的變長是為了節(jié)約存儲(chǔ)空間而采用的自然增長方案,當(dāng)空間不夠的時(shí)候增長到4個(gè)字節(jié)。

    那問題來了,我怎么知道存儲(chǔ)的4個(gè)字節(jié)是表示一個(gè)字符,還是兩個(gè)字符呢?比如當(dāng)程序遇到字節(jié)序列01001110 00101101 01010110 11111101時(shí),到底是判斷成一個(gè)字符還是兩個(gè)字符?

    這就需要一個(gè)前導(dǎo)識(shí)別,比如GB2312識(shí)別第一個(gè)字節(jié)高位是不是1來判斷是單字節(jié)還是雙字節(jié),但是UTF-16的高位1已經(jīng)被用來編碼了,當(dāng)然這也難不倒我們,第一位被用了那么就用前幾位的組合形式。

    UTF-16采用了代理對(duì)來解決,也就是高半?yún)^(qū)編碼(前兩個(gè)字節(jié))范圍D800-DBFF(稱為代理碼點(diǎn)),低半?yún)^(qū)編碼(后兩個(gè)字節(jié))范圍DC00-DFFF,組成一個(gè)四個(gè)字節(jié)表示的字符。

    上述前導(dǎo)6位組合也是有講究的,ISO組織要求編號(hào)范圍是0~10FFFF,也就是說用20位就可以表示10FFFF個(gè)字符,對(duì)于雙碼元就是每個(gè)碼元各自負(fù)責(zé)10位,一個(gè)碼元是16位,數(shù)字位占去10位后,剩下的6位做為前導(dǎo)位。

    當(dāng)UTF-16使用一個(gè)碼元表示的時(shí)候,Unicode字符編號(hào)跟碼元序列是等值映射的,但是當(dāng)采用雙碼元后,字符編號(hào)跟碼元序列就需要轉(zhuǎn)換了。

    下面是碼元和Unicode編號(hào)值之間的計(jì)算公式。

    換算碼元序列(CH高半?yún)^(qū)/CL低半?yún)^(qū)):

    換算字符編號(hào)(CH高半?yún)^(qū)/CL低半?yún)^(qū)):

    10.4.2平面空間

    UTF-16把編碼空間0000 ~ 10FFFF切成了17個(gè)平面,其實(shí)就是劃分成17個(gè)區(qū)塊,每個(gè)平面空間碼點(diǎn)數(shù)都是=65536個(gè),第一個(gè)平面稱為基本多語言平面(Basic Multilingual Plane,簡稱BMP),這個(gè)平面涵蓋了當(dāng)今世界上最常用的字符,固定使用定長兩個(gè)字節(jié),除此之外的字符都放到增補(bǔ)平面里,都是使用兩個(gè)碼元的定長4個(gè)字節(jié)。

    下面是各個(gè)平面的用途:

    增補(bǔ)平面的編號(hào)是采用雙碼元4個(gè)字節(jié)來表示的,去除代理對(duì)之后有效位數(shù)是20位,然后將這20位的編號(hào)再劃成16個(gè)平面區(qū)域,其中高半?yún)^(qū)的數(shù)字位里取出4位表示平面,剩下的16位表示每個(gè)平面可以表示的字符數(shù)也就是2的16次方65536個(gè)(兩個(gè)字節(jié)大小)。

    UTF-16可看成是UCS-2的父集。在沒有輔助平面前,UTF-16與UCS-2所指的是同一的意思。但當(dāng)引入輔助平面字符后,就稱為UTF-16了。

    10.4.3字節(jié)序

    字節(jié)序顧名思義是指字節(jié)的順序,對(duì)于單字節(jié)編碼來說,一個(gè)字符對(duì)應(yīng)一個(gè)字節(jié),也就不存在字節(jié)序問題。但是對(duì)于UTF-16這種定長多字節(jié)編碼,就有字節(jié)順序問題了。

    字節(jié)序其實(shí)跟操作系統(tǒng)和底層硬件有關(guān),不僅只是UTF-16這種多字節(jié)編碼存在字節(jié)序,只要是多字節(jié)類型的數(shù)據(jù)都存在字節(jié)順序問題,比如short、int、long。

    為了方便說明,我們這里舉個(gè)例子:比如存一個(gè)整數(shù)值“305419896”對(duì)應(yīng)16進(jìn)制是0x12345678,有人習(xí)慣從左到右按順序去存,也有人說高位當(dāng)然要放到高位地址而低位放到低位地址,要從右往左存。

    于是就有了下面兩種存取方式:

    其實(shí)這兩種方式?jīng)]有孰優(yōu)孰劣,只是我們認(rèn)知習(xí)慣有所不同 最終的設(shè)計(jì)不同,說來這都是阿拉伯人的鍋啊,為什么數(shù)字高位非要在左邊,這也引起了著名的大小端之爭。

    因此字節(jié)序也就有了大端和小端的概念,也形成了各自的陣營,比如Windows、FreeBSD、Linux 是小端序,Mac是大端序。其實(shí)大小端序并沒有技術(shù)上的好壞之分。

    小端序(Little-Endian):就是低位字節(jié)(即小端字節(jié)、尾端字節(jié))存放在內(nèi)存的低地址,而高位字節(jié)(即大端字節(jié)、頭端字節(jié))存放在內(nèi)存的高地址。

    大端序(Big-Endian ):就是高位字節(jié)(即大端字節(jié)、頭端字節(jié))存放在內(nèi)存的低地址,低位字節(jié)(即小端字節(jié)、尾端字節(jié))存放在內(nèi)存的高地址。

    ▲ 圖片引用自《面試必考,史上最通俗大小端字節(jié)序詳解

    對(duì)大小端字節(jié)序問題感興趣的可以詳讀:腦殘式網(wǎng)絡(luò)編程入門(九):面試必考,史上最通俗大小端字節(jié)序詳解》一文。

    10.5、UTF-8

    10.5.1概述

    Unicode還是UCS最初都是采用多字節(jié)定長編碼,由于沒有兼容現(xiàn)有的 ASCII 標(biāo)準(zhǔn)的文件和軟件,新標(biāo)準(zhǔn)很難被推廣,于是兼容ASCII版本的UTF-8就誕生了。

    UTF-8(8-bit Unicode Transformation Format)是一種針對(duì)Unicode的可變長度字符編碼,是現(xiàn)代字符編碼模型中的第三層 CEF 。它可以用一至四個(gè)字節(jié)對(duì) Unicode 字符集中的所有有效編碼點(diǎn)進(jìn)行編碼,屬于Unicode標(biāo)準(zhǔn)的一部分,UTF-8 就是為了解決向后兼容 ASCII 碼而設(shè)計(jì),Unicode 中前 128 個(gè)字符(與 ASCII 碼一一對(duì)應(yīng)),使用與 ASCII 碼相同的二進(jìn)制值的單個(gè)字節(jié)進(jìn)行編碼,這使得原來處理 ASCII 字符的軟件無須或只須做少部分修改,即可繼續(xù)使用。因此,它逐漸成為電子郵件、網(wǎng)頁及其他存儲(chǔ)或發(fā)送文字優(yōu)先采用的編碼方式。

    —— 維基百科

    UTF-8需要兼容ASCII,所以也需要有前綴碼來控制,前綴規(guī)則如下:

    • 1)如果首字節(jié)以 0 開頭,則是單字節(jié)編碼(即單個(gè)單字節(jié)碼元);
    • 2)如果首字節(jié)以 110 開頭,則是雙字節(jié)編碼(即由兩個(gè)單字節(jié)碼元所組成的雙碼元序列);
    • 3)如果首字節(jié)以 1110 開頭,則是三字節(jié)編碼(即由三個(gè)單字節(jié)碼元所組成的三碼元序列),以此類推。

    理論上UTF-8變長可以超過4個(gè)字節(jié),只是Unicode聯(lián)盟規(guī)范上限是10FFFF,所以UTF-8規(guī)則設(shè)計(jì)上也限制了大小。

    10.5.2程序算法

    用文字不太好描述算法結(jié)構(gòu),我們就直接來欣賞一下UTF-8鼻祖寫的這段解析代碼,這是Ken Thompson(B語言、C語言的作者、Unix之父)和 Rob Pike 用一個(gè)晚上寫出來的編解碼算法,代碼非常簡短精煉,為了方便閱讀我加了注釋解讀。

    typedefstruct

    {

      intcmask; //前綴碼掩碼

      intcval;  //前綴碼

      intshift; //移動(dòng)位數(shù)

      longlmask; //Unicode值掩碼

      longlval;  //Unicode下限值

    } Tab;

     

    staticTab  tab[] =

    {

      0x80, 0x00, 0*6, 0x7F,       0,         /* 1 byte sequence */

      0xE0, 0xC0, 1*6, 0x7FF,      0x80,      /* 2 byte sequence */

      0xF0, 0xE0, 2*6, 0xFFFF,     0x800,     /* 3 byte sequence */

      0xF8, 0xF0, 3*6, 0x1FFFFF,   0x10000,   /* 4 byte sequence */

      0xFC, 0xF8, 4*6, 0x3FFFFFF,  0x200000,  /* 5 byte sequence */

      0xFE, 0xFC, 5*6, 0x7FFFFFFF, 0x4000000, /* 6 byte sequence */

      0, /* end of table */

    };

     

    /**

    * 把一個(gè)多字節(jié)序列轉(zhuǎn)換為一個(gè)寬字符

    *

    * @param p 存放計(jì)算后的unicode值

    * @param s 需要解析的UTF-8字節(jié)序列

    * @param n 字節(jié)長度

    * @return 解析的字節(jié)長度

    */

    intmbtowc(wchar_t*p, char*s, size_tn)

    {

      longl;  intc0, c, nc;  Tab *t;

      if(s == 0) return0;

      nc = 0;

      //異常校驗(yàn)(可不用關(guān)注)

      if(n <= nc) return-1;

      //c0 此處備份一下首字節(jié),后續(xù)需要用到前綴碼

      c0 = *s & 0xff;

      //l 保存 Unicode 結(jié)果

      l = c0;

      /* 遍歷tab,從單字節(jié)結(jié)構(gòu)->2字節(jié)結(jié)構(gòu)->..依次檢查找到對(duì)應(yīng)tab */

      for(t=tab; t->cmask; t++) {

        //字節(jié)數(shù)+1,字節(jié)數(shù)和tab結(jié)構(gòu)是對(duì)應(yīng)的,也就是當(dāng)nc=1時(shí) tab結(jié)構(gòu)是單字節(jié),nc=2是tab是兩字節(jié)

        nc++;

        /* 判斷前綴碼跟當(dāng)前的tab是否一致, 如果一致計(jì)算最終unicode值并返回*/

        if((c0 & t->cmask) == t->cval) {

          //通過 & Unicode有效值掩碼,移除高位前綴碼,得到最終unicode值

          l &= t->lmask;

          //異常校驗(yàn)

          if(l < t->lval) return-1;

          //保存結(jié)果并反回

          *p = l;

          returnnc;

        }

        //異常校驗(yàn)

        if(n <= nc) return-1;

        //讀取下個(gè)字節(jié);如果上面判斷前綴碼不一致,說明需要再讀取下個(gè)字節(jié)

        s++;

        //計(jì)算有效位的值,目的是去除UTF-8 編碼從第二個(gè)字節(jié)開始的高兩位10

        // 例如 s=10101111、0x80=10000000 計(jì)算結(jié)果是00101111,這樣就去除了高位前綴10

        c = (*s ^ 0x80) & 0xFF;

        //異常校驗(yàn)

        if(c & 0xC0) return-1;

        //重新計(jì)算unicode值,根據(jù)UTF-8規(guī)則c只有低 6 位有效,所以通過移位把c填入到l的低6位

        l = (l<<6) | c;

      }

      //返回異常

      return-1;

    }

    10.5.3容錯(cuò)性

    通過上面的程序我們知道:解析過程是一個(gè)字節(jié)一個(gè)字節(jié)往下處理的,我們?cè)趥鬏斶^程中如果發(fā)生局部的字節(jié)錯(cuò)誤、丟失,或者中間有一個(gè)字節(jié)規(guī)則對(duì)不上,會(huì)不會(huì)影響整個(gè)文本的解析?

    我們先來看下其他編碼的容錯(cuò)情況:從對(duì)于單字節(jié)的ASCII碼來說,丟失一個(gè)字節(jié)就丟失一個(gè)字符,并不影響后續(xù)文本的內(nèi)容,比如Hello world,丟失b2字節(jié)后內(nèi)容是Hllo world少個(gè)e而已。

    我們?cè)賮砜碐B2312這種多字節(jié)編碼:如果丟失了b2字節(jié)那么整個(gè)文本都亂套了,這是最糟糕的,大部分多字節(jié)編碼都有類似問題,一旦出現(xiàn)錯(cuò)誤可能導(dǎo)致整個(gè)文件都需要重傳。

    接下來我們看看UTF-8是如何避免這種“一顆老鼠屎壞了一鍋粥”的情況:UTF-8 的碼元序列的第一個(gè)字節(jié)指明了后面所跟字節(jié)的個(gè)數(shù),比如首字節(jié)高位是0就表示單字節(jié),110表示總共兩個(gè)字節(jié),1110表示三個(gè)字節(jié)依次類推,除首字節(jié)之外后續(xù)字節(jié)都是10開頭。所以UTF-8的前綴碼具有很強(qiáng)的魯棒性,即使丟失、增加、改變個(gè)別字節(jié)也不會(huì)導(dǎo)致后續(xù)字符全部錯(cuò)亂這樣的傳遞性、連鎖性的錯(cuò)誤問題。

    十一、本文總結(jié)

    看起來好像誰都懂的字符編碼知識(shí),深入了解之后發(fā)現(xiàn)也有這么濃重的發(fā)展歷程,試想一下,如果計(jì)算機(jī)還是跟之前大型機(jī)一樣,個(gè)人計(jì)算機(jī)沒有井噴式發(fā)展起來就沒有這些字符編碼的事了,如果ASCII當(dāng)初就設(shè)計(jì)成多字節(jié)編碼,也沒有后面UNICODE什么事了。

    計(jì)算機(jī)字符編碼發(fā)展歷程其實(shí)就是一個(gè)很典型的架構(gòu)設(shè)計(jì)問題。

    到底好的架構(gòu)是設(shè)計(jì)出來的,還是演化出來的?

    有人說靠演化出來的:沒有設(shè)計(jì)的產(chǎn)品架構(gòu)是沒有靈魂的,發(fā)展的路上死的很快。

    有人說靠設(shè)計(jì)出來的:這是一種完美主義者,你超前設(shè)計(jì)個(gè)50年、100年等你設(shè)計(jì)出來了,說不定公司都已經(jīng)倒閉了,有很多叫好不叫做的產(chǎn)品、架構(gòu)也比比皆是。

    其實(shí):一個(gè)好的架構(gòu)是既要靠設(shè)計(jì)又要靠演化,老話說的好三分靠設(shè)計(jì)七分靠演化,我們既要學(xué)會(huì)務(wù)實(shí),也要懂得前瞻,至少我們首先需要活下來。

    十二、參考資料

    [1] Unicode中文編碼表

    [2] Every Developer Should Know About The Encoding

    [3] 史上最通俗,徹底搞懂字符亂碼問題的本質(zhì)

    [4] 字符編碼那點(diǎn)事:快速理解ASCII、Unicode、GBK和UTF-8

    [5] 面試必考,史上最通俗大小端字節(jié)序詳解

    附錄:阿里技術(shù)文章匯總

    阿里釘釘技術(shù)分享:企業(yè)級(jí)IM王者——釘釘在后端架構(gòu)上的過人之處

    現(xiàn)代IM系統(tǒng)中聊天消息的同步和存儲(chǔ)方案探討

    阿里技術(shù)分享:深度揭秘阿里數(shù)據(jù)庫技術(shù)方案的10年變遷史

    阿里技術(shù)分享:阿里自研金融級(jí)數(shù)據(jù)庫OceanBase的艱辛成長之路

    來自阿里OpenIM:打造安全可靠即時(shí)通訊服務(wù)的技術(shù)實(shí)踐分享

    釘釘——基于IM技術(shù)的新一代企業(yè)OA平臺(tái)的技術(shù)挑戰(zhàn)(視頻+PPT) [附件下載]

    阿里技術(shù)結(jié)晶:《阿里巴巴Java開發(fā)手冊(cè)(規(guī)約)-華山版》[附件下載]

    重磅發(fā)布:《阿里巴巴Android開發(fā)手冊(cè)(規(guī)約)》[附件下載]

    作者談《阿里巴巴Java開發(fā)手冊(cè)(規(guī)約)》背后的故事

    《阿里巴巴Android開發(fā)手冊(cè)(規(guī)約)》背后的故事

    干了這碗雞湯:從理發(fā)店小弟到阿里P10技術(shù)大牛

    揭秘阿里、騰訊、華為、百度的職級(jí)和薪酬體系

    淘寶技術(shù)分享:手淘億級(jí)移動(dòng)端接入層網(wǎng)關(guān)的技術(shù)演進(jìn)之路

    難得干貨,揭秘支付寶的2維碼掃碼技術(shù)優(yōu)化實(shí)踐之路

    淘寶直播技術(shù)干貨:高清、低延時(shí)的實(shí)時(shí)視頻直播技術(shù)解密

    阿里技術(shù)分享:電商IM消息平臺(tái),在群聊、直播場(chǎng)景下的技術(shù)實(shí)踐

    阿里技術(shù)分享:閑魚IM基于Flutter的移動(dòng)端跨端改造實(shí)踐

    阿里IM技術(shù)分享(三):閑魚億級(jí)IM消息系統(tǒng)的架構(gòu)演進(jìn)之路

    阿里IM技術(shù)分享(四):閑魚億級(jí)IM消息系統(tǒng)的可靠投遞優(yōu)化實(shí)踐

    阿里IM技術(shù)分享(五):閑魚億級(jí)IM消息系統(tǒng)的及時(shí)性優(yōu)化實(shí)踐

    阿里IM技術(shù)分享(六):閑魚億級(jí)IM消息系統(tǒng)的離線推送到達(dá)率優(yōu)化

    阿里IM技術(shù)分享(七):閑魚IM的在線、離線聊天數(shù)據(jù)同步機(jī)制優(yōu)化實(shí)踐

    阿里IM技術(shù)分享(八):深度解密釘釘即時(shí)消息服務(wù)DTIM的技術(shù)設(shè)計(jì)

    阿里IM技術(shù)分享(九):深度揭密RocketMQ在釘釘IM系統(tǒng)中的應(yīng)用實(shí)踐

    (本文已同步發(fā)布于:http://www.52im.net/thread-4210-1-1.html



    作者:Jack Jiang (點(diǎn)擊作者姓名進(jìn)入Github)
    出處:http://www.52im.net/space-uid-1.html
    交流:歡迎加入即時(shí)通訊開發(fā)交流群 215891622
    討論:http://www.52im.net/
    Jack Jiang同時(shí)是【原創(chuàng)Java Swing外觀工程BeautyEye】【輕量級(jí)移動(dòng)端即時(shí)通訊框架MobileIMSDK】的作者,可前往下載交流。
    本博文 歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明出處(也可前往 我的52im.net 找到我)。


    只有注冊(cè)用戶登錄后才能發(fā)表評(píng)論。


    網(wǎng)站導(dǎo)航:
     
    Jack Jiang的 Mail: jb2011@163.com, 聯(lián)系QQ: 413980957, 微信: hellojackjiang
    主站蜘蛛池模板: 成av免费大片黄在线观看| 99久久成人国产精品免费 | 亚洲精品色在线网站| 日韩精品无码免费专区午夜| 大地资源二在线观看免费高清| 国产亚洲精品a在线观看 | 免费看黄的成人APP| 67194成是人免费无码| 亚洲欧洲成人精品香蕉网| 亚洲熟妇丰满xxxxx| 野花香在线视频免费观看大全| 免费高清在线爱做视频| 亚洲AV无码乱码在线观看富二代| 亚洲暴爽av人人爽日日碰| 国内精品久久久久影院免费| 国产自产拍精品视频免费看| 亚洲综合无码一区二区| 十八禁的黄污污免费网站| 国产卡二卡三卡四卡免费网址| 亚洲人成在线播放网站| 国产成人人综合亚洲欧美丁香花| 7m凹凸精品分类大全免费| 国产av无码专区亚洲国产精品| 亚洲综合色丁香婷婷六月图片| 国产情侣久久久久aⅴ免费| 国产jizzjizz视频免费看| 亚洲AV无码一区二区三区在线| 久草免费福利视频| 亚洲男女内射在线播放| 亚洲日韩一区二区三区| 精品国产污污免费网站aⅴ| 亚洲理论电影在线观看| 人妻仑刮八A级毛片免费看| 无码视频免费一区二三区 | 久久久久亚洲AV无码专区首JN| www免费黄色网| 国产一级淫片a视频免费观看| 亚洲人成网站看在线播放| 99久久久国产精品免费牛牛| 色噜噜AV亚洲色一区二区| 黄色免费网址在线观看|