字符編碼技術(shù)專題(五):前端必讀的計(jì)算機(jī)字符編碼知識(shí)入門(mén)

本文由字節(jié)教育-成人與創(chuàng)新前端團(tuán)隊(duì)分享,本文有修訂和改動(dòng)。
1、引言
作為開(kāi)發(fā)人員,工作中我們可能會(huì)遇到以下問(wèn)題:
1)可能你知道 JavaScript 中 '??'.length = 2,但 '????????'.length 呢?
2)困惑于 Unicode 和 UTF-8 的關(guān)系?
3)學(xué)計(jì)算機(jī)時(shí)會(huì)遇到這樣的提問(wèn):一個(gè)漢字是幾個(gè)字節(jié)?
4)讀取二進(jìn)制數(shù)據(jù)時(shí),為何有大端序小端序的分別?
5)為何 UTF-8 文件最好存儲(chǔ)為無(wú) BOM 頭格式?
6)數(shù)據(jù)亂碼時(shí)總會(huì)看到“錕斤拷”、“燙燙燙”,這是什么鬼?
以上這些問(wèn)題都涉及到計(jì)算機(jī)中*礎(chǔ)的知識(shí)點(diǎn)——字符集及字符編碼的概念,本篇將從前端開(kāi)發(fā)人員的視解,讓你徹底搞清并理解這些問(wèn)題的本質(zhì)。

?
技術(shù)交流:
- 移動(dòng)端IM開(kāi)發(fā)入門(mén)文章:《新手入門(mén)一篇就夠:從零開(kāi)發(fā)移動(dòng)端IM》
- 開(kāi)源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK(備用地址點(diǎn)此)
(本文已同步發(fā)布于:http://www.52im.net/thread-4453-1-1.html)
2、系列文章
本文是系列文章中的第?5?篇,本系列總目錄如下:
《字符編碼技術(shù)專題(一):快速理解ASCII、Unicode、GBK和UTF-8》
《字符編碼技術(shù)專題(二):史詩(shī)級(jí)計(jì)算機(jī)字符編碼知識(shí)入門(mén),一文即懂!》
《字符編碼技術(shù)專題(三):徹底搞懂字符亂碼的本質(zhì),一篇就夠!》
《字符編碼技術(shù)專題(四):史上最通俗大小端字節(jié)序詳解,一文即懂!》
《字符編碼技術(shù)專題(五):前端必讀的計(jì)算機(jī)字符編碼知識(shí)入門(mén)》(* 本文)
3、什么是字符集與字符編碼
首先通過(guò) wiki 中關(guān)于字符編碼(Character_encoding)的定義來(lái)引入幾個(gè)概念:
Character encoding is the process of assigning numbers to graphical characters, especially the written characters of human language, allowing them to be stored, transmitted, and transformed using digital computers.
The numerical values that make up a character encoding are known as "code points" and collectively comprise a "code space", a "code page", or a "character map".
字符編碼是將數(shù)字分配給圖形字符的過(guò)程,特別是人類(lèi)語(yǔ)言的書(shū)寫(xiě)字符,使它們能夠使用計(jì)算機(jī)進(jìn)行存儲(chǔ)、傳輸和轉(zhuǎn)換。組成字符編碼的數(shù)值稱為“碼位”,它們共同組成“代碼空間”、“代碼頁(yè)”或“字符映射”。
這里所說(shuō)的代碼頁(yè)(Code Page)其實(shí)就可以理解為編碼字符集(coded character set),如 Unicode、GBK 字符集等。
簡(jiǎn)單來(lái)說(shuō):字符編碼就是將字符映射為固定的碼位值,存儲(chǔ)在對(duì)應(yīng)的編碼字符集中。在不同的字符集中,同一個(gè)字符的碼位不同。其中碼位也有翻譯成碼點(diǎn)或者內(nèi)碼。
4、ASCII 字符集
我們知道在計(jì)算機(jī)存儲(chǔ)數(shù)據(jù)時(shí)要使用二進(jìn)制進(jìn)行表示。而最初計(jì)算機(jī)只在美國(guó)使用,因此人們要考慮如何使用二進(jìn)制來(lái)表達(dá) 52 個(gè)英文字母(包括大小寫(xiě))、阿拉伯?dāng)?shù)字(0-9)以及常用的符號(hào)(如! @ # $ 等)。
于是便有從電報(bào)碼發(fā)展而來(lái)的 ASCII(American Standard Code for Information Interchange,美國(guó)信息交換標(biāo)準(zhǔn)代碼)(發(fā)音 /??ski/)編碼。它定義了英文字符和二進(jìn)制的對(duì)應(yīng)關(guān)系,一直沿用至今。
它采用了單字節(jié)編碼方案(SBCS) ,一個(gè)字節(jié)的首位 bit 為 0,用其余 7 個(gè) bit 來(lái)表示 128 個(gè)字符(范圍 0x00-0x7F)。
具體是:
1)其中 0-31 和 127 (0x00-0x1F 和 0x7F) 為控制字符,共 33 個(gè)。這些字符是不可見(jiàn)的,用于進(jìn)行終端的換行、響鈴、刪除等動(dòng)作;
2)32-126 (0x20-0x7E) 位可見(jiàn)字符,共 95 個(gè),存儲(chǔ)了空格、0-9 十個(gè)阿拉伯?dāng)?shù)字、52 個(gè)大小寫(xiě)英文字母,以及標(biāo)點(diǎn)、運(yùn)算符號(hào)等。
雖然現(xiàn)代英語(yǔ)使用 128 個(gè)字符就足夠了,但表示其他語(yǔ)言就遠(yuǎn)遠(yuǎn)不夠了。因此當(dāng) ASCII 進(jìn)入歐洲后,又被擴(kuò)展為了 EASCII(Extended ASCII),將 7 bit 擴(kuò)展為 8 bit,并且前 127 個(gè)編碼含義和 ASCII 保持一致。
但 256 個(gè)字符依舊無(wú)法解決眾多使用拉丁字母的語(yǔ)言(主要是歐洲語(yǔ)言)問(wèn)題。于是又?jǐn)U展出了 15 個(gè) ISO 8859 字符集。
舉幾個(gè)字符集作為了解:
ISO/IEC 8859-1 (Latin-1) - 西歐語(yǔ)言
ISO/IEC 8859-2 (Latin-2) - 中歐語(yǔ)言
ISO/IEC 8859-3 (Latin-3) - 南歐語(yǔ)言
ISO/IEC 8859-4 (Latin-4) - 北歐語(yǔ)言
...
5、中文字符集
5.1概述
前面講到拉丁文所使用是 ASCII 和 EASCII,但在亞洲——主要是中日韓(CJK)——光常用漢字就 6000 多個(gè),而漢字總共有 5、6 萬(wàn)之多,單靠一個(gè)字節(jié)是遠(yuǎn)遠(yuǎn)沒(méi)辦法做到的。
因此聰明的中國(guó)人(也可能是日本人)就想到使用雙字節(jié)編碼(DBCS) 來(lái)表示一個(gè)漢字,這樣理論上 2 個(gè)字節(jié)可以表達(dá) 65535 個(gè)字符(當(dāng)然很理論,實(shí)際上要少很多)。
這樣就可以表示大部分常用字符了,接下來(lái)就要講到和中文有關(guān)的 GB 系列(如 GB2312、GBK、GB18030)的字符集了。
其實(shí) GB 就是“國(guó)標(biāo)”漢語(yǔ)拼音的首字母,而 GBK 就是“國(guó)標(biāo)擴(kuò)展”的意思,而 GB18030 是在保留 GBK 編碼*礎(chǔ)上再度擴(kuò)展為可變的 4 字節(jié)編碼空間的編碼規(guī)范。
這里我們著重介紹下 GB2312 的編碼結(jié)構(gòu)。
5.2GB/T 2312
GB2312 全稱《信息交換用漢字編碼字符集·*本集》,收錄了 6763 個(gè)漢字,682 個(gè)拉丁、希臘、日文假名等字符。就將其中漢字覆蓋了大陸 99.75% 的使用頻率,足夠大部分的場(chǎng)景使用的。
同時(shí)還把 ASCII 中標(biāo)點(diǎn)符號(hào)、阿拉伯?dāng)?shù)字、英文字符用雙字節(jié)收錄在內(nèi),這里為了區(qū)別于 ASCII 中的字符,就將其做成了與漢字等寬的正方型效果(即拉丁字母的兩倍寬),以表示和編碼存儲(chǔ)方式一一對(duì)應(yīng)。因?yàn)樽址幋a和字寬的對(duì)應(yīng)關(guān)系,我們這就稱這類(lèi)字符為「全角」字符,而 ASCII 中的字符則為「半角」字符。(這種叫法源于日本,因?yàn)樵谌毡局小敖恰庇小胺綁K”的意思)
接下來(lái)看看如何編碼的。規(guī)范將收錄的漢字分成 94 個(gè)區(qū),每個(gè)區(qū)又包含 94 個(gè)漢字,用所在的區(qū)位來(lái)表示一個(gè)字符(這種方法也稱為區(qū)位碼) 。每個(gè)字符用 2 個(gè)字節(jié)表示,其中第一個(gè)字節(jié)稱為「高位字節(jié)」表示分區(qū)號(hào),第二個(gè)字節(jié)稱為「低位字節(jié)」表示區(qū)段內(nèi)的碼位。另外實(shí)際 GB2312 僅用了 87 個(gè)區(qū),88-94 區(qū)保留待擴(kuò)展。
為了避開(kāi) ASCII 中前面的 31 個(gè)不可見(jiàn)控制符和空格,在區(qū)位碼*礎(chǔ)上 +32(0x20),這樣獲得的編碼就稱為 ISO-2022 國(guó)標(biāo)碼。
但是實(shí)際使用時(shí),英文字符和漢字混用的情況很常見(jiàn),國(guó)標(biāo)碼僅避讓開(kāi)了控制字符,依然和 ASCII 的英文字符有重合,因此決定在國(guó)標(biāo)碼的*礎(chǔ)上將單字節(jié)的最高位 bit 存為 1,即在國(guó)標(biāo)碼上 +128(0x80),也就是區(qū)位碼上 +160 (0xA0),這樣就完美的避讓開(kāi)了 ASCII 字符區(qū)間。
這樣種編碼就叫做 EUC-CN 機(jī)內(nèi)碼:
EUC-CN 機(jī)內(nèi)碼 = 區(qū)位碼 + 160
而目前 GB2312 所采用的就是 EUC 這種主流編碼方式,以便于兼容 ASCII 碼。同時(shí)也可以根據(jù)最高位 bit 來(lái)判斷是讀取 1 個(gè)字符(ASCII)還是 2 個(gè)字符來(lái)進(jìn)行解析。在 GB2312 內(nèi),高位字節(jié)范圍 0xA1-0xF7(01-87 區(qū)+160 或 0xA0),低位字節(jié)范圍 0xA1-0xFE(01-94 + 160 或 0xA0)。
以“節(jié)”為例:
1)區(qū)位碼是 29-58;
2)EUC 編碼就是 <29+160, 58+160> = <189, 218>;
3)十六進(jìn)制就是 <0xBD, 0xDA>。
5.3GBK/GB18030
但是新的問(wèn)題來(lái)了,GB2312 僅收入了 6763 個(gè)常用的漢字,一些在標(biāo)準(zhǔn)推出以后的簡(jiǎn)化字(如 啰)、港澳臺(tái)使用的的繁體字、某些領(lǐng)導(dǎo)人的名字(***的*),以及各地區(qū)戶籍中用到奇奇怪怪的名字、古籍中的漢字都沒(méi)法正確表示。
因此在 GB2312 的*礎(chǔ)上又?jǐn)U展出了 GBK 和 GB18030,并且向下兼容(嚴(yán)格來(lái)講 GB18030 完全兼容 GB2312,*本兼容 GBK)。
GBK 的*本原理就是繼續(xù)擴(kuò)展了 GB2312 中未使用的雙字節(jié)空間,字符多達(dá) 23940 個(gè)。而 GB18030 則更為激進(jìn),干脆將存儲(chǔ)空間變成可變的 1、2、4 字節(jié),理論上可以存儲(chǔ) 161 萬(wàn)個(gè)字符,完全涵蓋 Unicode 范圍。同時(shí) GB18030 還收錄了中日韓、繁體字、少數(shù)民族文字等多種語(yǔ)言。
5.4Big5
于此同時(shí),海峽對(duì)岸的程序員也發(fā)明出了一套自己的繁體中文標(biāo)準(zhǔn),由于起初項(xiàng)目名稱是「五大中文套裝軟件」,因此稱為 Big5,也叫大五碼。記得當(dāng)年想要玩臺(tái)灣“流入”的游戲,MagicWin 這類(lèi)的轉(zhuǎn)碼軟件是裝機(jī)必備。
6、Unicode 字符集
隨著世界各個(gè)地區(qū)編碼方式越來(lái)越多,給不同地區(qū)間數(shù)據(jù)傳輸造成非常大的困難,比如一個(gè)從中國(guó)發(fā)送到日本郵件的,在不知道其編碼的情況下,就會(huì)出現(xiàn)亂碼。
因此終于有人開(kāi)始想用一種更加通用的方案,來(lái)涵蓋世界上所有的字符和符號(hào)——這就是 Unicode,如其名字本身的含義一樣。
首先 Unicode 也采用了 16 位的編碼空間,即 2 個(gè)字節(jié)表示一個(gè)字符,用 "U+"接 4 個(gè)十六進(jìn)制數(shù)字表示(例如U+4AE0),每個(gè)字符分配一個(gè)唯一的「碼點(diǎn)」(CodePoint,也稱碼位)。同時(shí)又定義了 17 個(gè)編組,每組稱為一個(gè)「平面」(Plane)。這樣字符范圍從?0x00000 - 0x10FFFF,這樣就可以最多表示?17 * 65536?也就是 100多萬(wàn)個(gè)字符,完全可以存儲(chǔ)全世界所有的語(yǔ)言和符號(hào)了。
這其中將常用的字符存放在第一個(gè)編組,即 0 號(hào)平面(Plane 0),也稱之為「*本多文種平面 」(BMP - Basic Multilingual Plane),其范圍是 0x0000 - 0xFFFF 。而 1-16 號(hào)平面被稱為「輔助平面」(Supplementary Planes),用于存儲(chǔ)很少使用的文字或者圖形符號(hào)(emoji 表情符號(hào)就存于這些平面),其范圍是 0x10000-0x10FFFF。

Unicode 字符表的范圍涵蓋的字符和符號(hào)非常廣,不僅收錄了表情符號(hào)、麻將、音樂(lè)符號(hào)等表意符號(hào)外,還收錄了一些早已不再使用的文字,比如甲骨文、古埃及的圣書(shū)體,甚至收錄了為影視劇而創(chuàng)造的語(yǔ)言——克林貢語(yǔ)。并且還在持續(xù)更新中,截止此文完成,最近的一次更新是在2022年9月。
但是這里有個(gè)比較嚴(yán)重的問(wèn)題:
1)不同碼位的字符使用不同的字符長(zhǎng)度,但計(jì)算機(jī)無(wú)法知道到底是按照 2 個(gè)還是 4 個(gè)字符解析;
2)即便只使用英文字符前一個(gè)字節(jié)也必須是 0,嚴(yán)重浪費(fèi)了空間。
由于這個(gè)問(wèn)題導(dǎo)致 Unicode 的在初期完全沒(méi)法推廣,直到互聯(lián)網(wǎng)的普及,急需一種對(duì) Unicode 字符的編碼方式出現(xiàn),這就是 ****UTF(Unicode Transformation Format)。在 UTF 中又出現(xiàn)了 UTF-8、UTF-16、UTF-32 這些不同的編碼方式。
7、UTF-32
首先,人們想到的就是干脆所有字符直接用 4 個(gè)字節(jié)存儲(chǔ),不足 4 位的就補(bǔ) 0 代替。但是這樣簡(jiǎn)單粗暴,尤其僅使用英文的數(shù)據(jù),會(huì)造成了空間的極大浪費(fèi)。
比如拉丁字母 A 的 Unicode 碼點(diǎn)為?U+0065,十六進(jìn)制為 0x41,如果使用 UTF-32 編碼就是?0x00000041。
于是人們又開(kāi)始設(shè)計(jì)一種可以節(jié)省空間的編碼方式——UTF-8。
8、UTF-8
UTF-8 最大的特點(diǎn)是可變長(zhǎng)編碼,它使用 1-4 個(gè)字節(jié)表示一個(gè)字符。
它的編碼方式如下:
1)由于 Unicode 在 0+0000-0+007F 范圍和 ASCII 完全相同,因此使用單字節(jié)表示(這樣 ASCII 和 UTF-8 的編碼一樣,是完全兼容的)。
2)在大于 0+007F 的字符時(shí),是由 1 個(gè)前導(dǎo)字節(jié)(leading bytes)和 n 個(gè)(n >=1 )尾字節(jié)(trailing bytes)的多字節(jié)結(jié)構(gòu)組成。前導(dǎo)字節(jié)從最左側(cè)起用 1來(lái)表示這個(gè)字符有多少位,直到遇到 0 為止。尾字節(jié)前兩位都是 10,其余的 bit 位就是可用編碼空間。
比如:
1)第一個(gè)字節(jié)是 0xxxxxxx,就表示該字符只有 1 個(gè)字節(jié);
2)第一個(gè)字節(jié)是 110xxxxx,表示該字符有 2 個(gè)字節(jié),第二個(gè)字節(jié)是 10xxxxx;
3)第一個(gè)字符是 1110xxxx,表示該字符有 3 個(gè)字節(jié),后面字節(jié)分別是 10xxxxx 10xxxxx;
4)第一個(gè)字節(jié)是 11110xxx,表示該字節(jié)有 4 個(gè)字節(jié),后面字節(jié)分別是 10xxxxx 10xxxxx 10xxxxx。
這里 x 就表示可用的編碼空間,這也就是 UTF-8 中 8 表示至少 8 位表示一個(gè)字符,同時(shí)也是以 8 位為一組實(shí)現(xiàn)可變字節(jié)的編碼方式。(這里之所以跳過(guò)了 10xxxxxx,是因?yàn)?10 表示尾字節(jié))
以下就是完整編碼方式,當(dāng)然 5、6 字節(jié)的編碼方式不會(huì)出現(xiàn),因?yàn)橐呀?jīng)遠(yuǎn)超 Unicode 最大碼點(diǎn)范圍 U+10FFFF 了。

接下來(lái)我們用“兔” 和 emoji “🐰” 來(lái)舉例,來(lái)說(shuō)明下 UTF-8 是如何編碼的。
兔:
1)碼點(diǎn)為 U+5154;
2)二進(jìn)制表示為 101000101010100;
3)從末尾按 6 個(gè) bit 分組 101 000101 010100;
4)需要 3 個(gè)字節(jié),則開(kāi)頭插入 1110,中間不足 8 位用 0 補(bǔ)足,剩余前面插入 10,則得到 11100101 10000101 10010100;
5)轉(zhuǎn)為十六進(jìn)制,E5 85 94。
🐰 的 emoji:
1)碼點(diǎn)為 U+1F430;
2)二進(jìn)制為 11111010000110000;
3)從末尾按 6 個(gè) bit 分組 11111 010000 110000;
4)第一部分為 5 位,插入 1110 位后占 9 位,超出一個(gè)字節(jié),所以前面補(bǔ)一個(gè)字節(jié),用 4 個(gè)字節(jié)表示。不足 8 位的用 0 補(bǔ)足。則得到 11110000 10011111 10010000 10110000;
5)轉(zhuǎn)為十六進(jìn)制,F(xiàn)0 9F 90 B0;
由于 JS 中 encodeURI 是按照 UTF-8 進(jìn)行百分號(hào)編碼,因此感興趣的同學(xué)可以進(jìn)行驗(yàn)證下(代碼如下所示)。
encodeURI('兔');
// %E5%85%94
?
encodeURI('🐰');
// %F0%9F%90%B0
除了相對(duì)于 4 字節(jié)編碼更省空間外,UTF-8 編碼還有以下比較優(yōu)秀的特性。
1)ASCII 是 UTF-8 的一個(gè)子集,一個(gè)純 ASCII 字符串也是一個(gè)合法的 UTF-8 字符串。因此現(xiàn)存的 ASCII 數(shù)據(jù)可以不經(jīng)修改即可使用。
2)單字節(jié)范圍 0x00-0x7F,而多字節(jié)的前導(dǎo)字節(jié)范圍總是在?0xC0-0xFD,尾字節(jié)都在?0x80-0xBF?中,三者完全沒(méi)有重疊。通過(guò)范圍就可以確定字節(jié)的類(lèi)型,如果字節(jié)流在傳輸時(shí)中斷,不完整的字節(jié)也不會(huì)被解碼。這樣數(shù)據(jù)有損壞、丟失,影響的范圍也會(huì)很小。試想下,如果有一個(gè)按照每 4 個(gè)字節(jié)為一組的編碼方法,如果丟失了第一個(gè)字節(jié)的數(shù)據(jù),那么繼續(xù)按照 4 字節(jié)一組的方式讀取數(shù)據(jù),后面的數(shù)據(jù)都將無(wú)法讀取。
3)另外由于前導(dǎo)字節(jié)和尾字節(jié)范圍互不重疊,即便從一個(gè)字符的某個(gè)中間字節(jié)開(kāi)始讀取,也不會(huì)錯(cuò)把它和下一個(gè)字符拼接錯(cuò)。這種特性也叫自同步碼。比如,“兔子”的編碼為?E5 85 94 / E5 AD 90,你不會(huì)錯(cuò)把?85 94 E5?或者?AD 90?當(dāng)做一個(gè)字符,因?yàn)楦静淮嬖谶@樣規(guī)則的 UTF-8 編碼。
4)僅通過(guò)首字節(jié)就能確定整體字節(jié)長(zhǎng)度,而無(wú)需等待下一個(gè)字節(jié)的讀取。
5)理論 6 個(gè)字節(jié)可以存放 2^31 個(gè)字符,即 21 億個(gè)字符!即便是 4 字節(jié)也可以存放 200 多萬(wàn)個(gè)字符。其范圍已超 Unicode 的容量了。
6)UTF-8 中未使用 0xFE 和 0xFF。
7)UTF-8 字節(jié)串的排列順序是固定的,和系統(tǒng)無(wú)關(guān)。沒(méi)有字節(jié)序(即是按大端序還是小端序的先后順序)的問(wèn)題。因此也無(wú)需使用 BOM 頭(雖然其 BOM 頭是 0xEF 0xBB 0xBF,但僅表示 UTF-8 編碼格式,不表示字節(jié)順序)。
雖然 UTF-8 有諸多優(yōu)勢(shì),但是也有缺點(diǎn):
1)中日韓文字至少需要 3 個(gè)字節(jié)存儲(chǔ),比 GBK 這類(lèi)雙字節(jié)存儲(chǔ)要多一個(gè)字節(jié);
2)由于其是變長(zhǎng)字節(jié),一個(gè)字符可能是 1、2、3 個(gè)字節(jié),因此當(dāng)計(jì)算一組字節(jié)中包含多少個(gè)字符,就要從頭遍歷,才能確定到底有多少個(gè)字符。當(dāng)我們需要獲取指定位置的字符時(shí),依然要從頭遍歷。這也為程序的實(shí)現(xiàn)帶來(lái)了很大的麻煩。
因此:一些程序在內(nèi)部處理字符串時(shí),就使用了另外一種編碼方式 UTF-16,其中 JavaScript、Python 就使用了這種編碼方式來(lái)存儲(chǔ)字符串。
9、UTF-16
UTF-16 也采用了變長(zhǎng)的編碼方式。使用 2 個(gè)或者 4 個(gè)字節(jié)表示一個(gè)字符。
其規(guī)則是:
1)在*本平面 BMP 內(nèi)(U+0000 至 U+FFFF)的字符用 2 個(gè)字節(jié)表示;
2)在輔助平面內(nèi)(U+10000 至 U+10FFFF)的字符用 4 個(gè)字節(jié)表示。
其中 BMP 的字符直接使用 Unicode 碼點(diǎn)對(duì)應(yīng)即可,比如“兔”的碼點(diǎn)?U+5154,UTF-16 編碼就是?0x5154。
可是 BMP 外,要如何使用 4 個(gè)字節(jié)表示呢?假如我們使用 UTF-32 的方式,直接使用碼點(diǎn)映射會(huì)遇到什么問(wèn)題?
還以“🐰”為例,其碼點(diǎn)是 U+1F430。如果直接映射為 0x0001F430,頭 2 個(gè)字節(jié) 0x0001 對(duì)應(yīng)的字符在 BMP 中已經(jīng)存在,這樣在讀取數(shù)據(jù)時(shí)就無(wú)法區(qū)分到底是按照 2 字節(jié)解析,還是按照 4 個(gè)字節(jié)解析了。
幸而在 BMP 中,從 U+D800 到 U+DFFF 是一個(gè)永遠(yuǎn)保留不做映射的空段,于是 UTF-16 將輔助平面內(nèi)的字符 —— 共需要 20 個(gè) bit 表示(由 0x10FFFF - 0x100000 = 0xFFFFF 計(jì)算得來(lái)) —— 拆分成 2 段,前 10 個(gè) bit 位映射到 U+D800 - U+DBFF(正好 1024 個(gè)),后 10 個(gè) bit 映射到 0xDC00 - 0xDFFF(也正好 1024 個(gè))。這樣剛好就可以在范圍互不重復(fù)的情況下,用 4 個(gè)字節(jié)表示一個(gè)字符了。
這種用 4 個(gè)字節(jié)表示的方式,就被稱作「代理對(duì)」(Surrogate Pair),高位的 10 bit 被稱為「前導(dǎo)代理」(lead surrogates),低位 10 bit 被稱為「后尾代理」(trail surrogates)。
其具體的算法如下:
1)BMP 內(nèi)字符,直接使用碼點(diǎn)作為對(duì)應(yīng)的字節(jié),長(zhǎng)度為 2 個(gè)字節(jié);
2)輔助面內(nèi)的字符:
? ? ? ? ? ? ?a. 碼位減去 0x10000;
? ? ? ? ? ? ?b. 高位的 10 bit 的值加上 0xD800 ,得到前導(dǎo)代理;
? ? ? ? ? ? ?c. 低位的 10 bit 的值加上 0xDC00 ,得到后尾代理。
用公式計(jì)算如下:
// 高位
H = Math.floor((c-0x10000) / 0x400)+0xD800
?
// 低位
L = (c - 0x10000) % 0x400 + 0xDC00
我們依舊使用“🐰”來(lái)舉例,看下 UTF-16 是如何進(jìn)行編碼的。
🐰:
1)碼點(diǎn)為 U+1F430;
2)減去 0x10000,為 0xF430;
3)二進(jìn)制為 1111010000110000;
4)前 10 bit 111101,十六進(jìn)制 0x3D。后 10 bit 0000110000,十六進(jìn)制 0x30;
5)分別加上 0xD800 和 0xDC00,得到 0xD83D 和 0xDC30;
6)最終得到 D8 3D DC 30。
JS 中 escape 方法(已不推薦,這里僅驗(yàn)證用)返回的就是 UTF-16 編碼,可以進(jìn)行驗(yàn)證:
escape('🐰')
// %uD83D%uDC30
同時(shí) ES6 中支持用 Unicode 碼點(diǎn)和 UTF-16 編碼表示一個(gè)字符,也可驗(yàn)證:
'\u{1F430}'== '\uD83D\uDC30'
// true
由于前導(dǎo)代理、后尾代理、BMP 中的有效字符的碼位,三者互不重疊,因此在檢查字符時(shí),可以很容易的確定字符的邊界。這也意味著 UTF-16 也是一種自同步的編碼方式,這點(diǎn)和 UTF-8 是一樣的。
UTF-16 相對(duì)于 UTF-8 更容易進(jìn)行隨機(jī)訪問(wèn)和索引,是因?yàn)?UTF-16 中每個(gè)字符都使用固定的2個(gè)或4個(gè)字節(jié)表示,因此可以通過(guò)簡(jiǎn)單的數(shù)學(xué)運(yùn)算來(lái)快速計(jì)算出每個(gè)字符的位置。而 UTF-8 需要解析整個(gè)數(shù)據(jù)流才能確定每個(gè)字符的位置,這使得 UTF-8 在進(jìn)行隨機(jī)訪問(wèn)和索引時(shí)比 UTF-16 更加復(fù)雜和耗時(shí)。
10、UCS-2
在談及 UTF-16 時(shí),通常會(huì)涉及到 UCS-2 的概念,這里著重講下兩者的區(qū)別。
UCS-2 是 Universal Character Set coded in 2 octets 的簡(jiǎn)稱,由于 1989 年 UCS 標(biāo)準(zhǔn)發(fā)布時(shí),只有 Unicode *本面字符,因此使用 2 個(gè)字節(jié)就可以表示一個(gè)字符了。
而 1996 年 UTF-16 發(fā)布時(shí),已經(jīng)擴(kuò)展出了輔助平面,可使用代理對(duì)方式表示輔助平面的字符了,因此 UTF-16 已明確表示是 UCS-2 的超集。
所以目前 UCS-2 已經(jīng)是過(guò)時(shí)的叫法了,UTF-16 已經(jīng)取代了 UCS-2 的概念。
如果某個(gè)程序說(shuō)自己支持 UCS-2 可能就意味著僅支持 BMP 內(nèi)的字符集。
11、大端序和小端序
將字符轉(zhuǎn)換為 UTF-16(或 UTF-32) 后,在使用時(shí)需要讀取并存儲(chǔ)在內(nèi)存中。以“乙”字為例,轉(zhuǎn)成 UTF-16 后,編碼為?0x4E59,需要用 2 個(gè)字節(jié)來(lái)存儲(chǔ)——0x4E 0x59。
如果 4E 在前,59 在后,我們就稱作為大端序(Big-Endian):
內(nèi)存地址???? 內(nèi)存值
0x00000000? 4E
0x00000001? 59
反之 59 在前,4E 在后,就稱之為小端序(Little-Endian):
內(nèi)存地址???? 內(nèi)存值
0x00000000? 59
0x00000001? 4E
之所以會(huì)有這樣不同的存儲(chǔ)方式,是因?yàn)橛械南到y(tǒng)或者處理器,在處理多字節(jié)數(shù)據(jù)時(shí),會(huì)從低位內(nèi)存地址到高位內(nèi)存地址的順序讀取數(shù)據(jù),因此為了保證讀取后數(shù)據(jù)的正確,就采用了不同字節(jié)序(Endianness)的方式。
為了標(biāo)識(shí)一段數(shù)據(jù)的字節(jié)序,通常使用字節(jié)順序標(biāo)記 (Byte-Order Mark,BOM)來(lái)進(jìn)行標(biāo)識(shí)。通常 BOM 會(huì)出現(xiàn)在文件、字節(jié)流的頭部,用來(lái)標(biāo)識(shí)接下來(lái)數(shù)據(jù)的字節(jié)順序。
在 UTF-16,0xFEFF 表示大端序,0xFFFE 表示小端序。
我們來(lái)驗(yàn)證下,使用 VSCode 安裝 HexEditor 插件,然后新建一個(gè)空白文件,輸入“乙”和“🐰”,然后分別使用兩種編序,通過(guò) HexEditor 查看。
UTF-16 BE:


?
UTF-16 LE:


?
另外 UTF-8 也存在 BOM 頭,值為?0xEF 0xBB 0xBF。但它只用來(lái)標(biāo)識(shí)文件的編碼方式,而不用來(lái)說(shuō)明字節(jié)順序。因?yàn)?UTF-8 本身有著固定的編碼順序,字節(jié)序?qū)τ?UTF-8 來(lái)說(shuō)毫無(wú)意義。
在實(shí)際使用中,也應(yīng)該盡量避免帶 BOM 頭的 UTF-8,因?yàn)?BOM 頭可能會(huì)影響一些程序的解析器(如 Unix下的Shebang)的處理。
另外由于 UTF-8 是一種稀疏的編碼,很大一部分可能的字符組合不會(huì)產(chǎn)生有效的 UTF-8 文本,因此許多程序的在解析 UTF-8 文件時(shí),使用啟發(fā)式的分析方法可以很有把握地檢測(cè)出文件是否使用 UTF-8,而無(wú)需加入 BOM。
PS:關(guān)于大小端字節(jié)序問(wèn)題,可以詳讀另一篇《史上最通俗大小端字節(jié)序詳解,一文即懂!》。
12、錕斤拷燙燙燙
12.1概述
“錕斤拷”和“燙燙燙”恐怕是程序界中最經(jīng)典的故事(事故)之一了,了解了前面的編碼知識(shí),就會(huì)很容易理解它的由來(lái)了。

12.2錕斤拷
由于 Unicode 字符集在不斷更新中,因此會(huì)出現(xiàn) A 系統(tǒng)發(fā)送的字符,在 B 系統(tǒng)中無(wú)法識(shí)別的情況。
于是 Unicode 規(guī)定對(duì)于無(wú)法識(shí)別的字符,一律使用 ?(0xFFFD) (Replacement character) 字符來(lái)代替。
而?0xFFFD?在 UTF-8 編碼下為?0xEF 0xBF 0xBD,當(dāng)多 ? 出現(xiàn)時(shí),就會(huì)產(chǎn)生連續(xù)的?0xEF 0xBF 0xBD 0xEF 0xBF 0xBD。
如果這些字符又被使用了 GB 編碼的程序中打開(kāi),就會(huì)按照 GB 雙字節(jié)編碼將其解析。這樣剛好就對(duì)應(yīng)了 「0xEFBF 錕」 、「0xBDEF 斤」、「0xBFBD 拷」 這幾個(gè)字。
12.3燙燙燙
C\C++ 編譯器在 debug 模式下,引入的一種內(nèi)存保護(hù)機(jī)制,會(huì)給特定的內(nèi)存賦一個(gè)特定的初值。其中未被初始化的棧內(nèi)存,會(huì)被寫(xiě)入 0XCC。當(dāng)連續(xù)的 0xCCCC 在 GB 編碼下就是「0xCCCC 燙」了。
當(dāng)然一千人眼中有一千個(gè)哈姆雷特,程序員也是如此。比如臺(tái)灣是“嚙踝蕭”,而日本是“フフフフフフ”了。
13、JavaScript 與 Unicode
13.1薛定諤的 length
前面我們提到 JavaScript 中 '😁'.length = 2,要說(shuō)明這個(gè)問(wèn)題之前,先簡(jiǎn)單介紹下 JavaScript 與編碼的歷史。
1990 年 UCS-2 編碼發(fā)布,1995 年 JavaScript 誕生,第一個(gè) JS 解釋器被使用,1996 年 UTF-16 發(fā)布。
從時(shí)間線上來(lái)看 JavaScript 解釋器只能采用了 UCS-2 的編碼方式。而 length 統(tǒng)計(jì)的是代碼單元而非真正的字符數(shù)量,因此按照 UCS-2 的編碼方式,每個(gè)字符由 2 個(gè)字節(jié)構(gòu)成,即兩個(gè) 2 字節(jié)組成字符的 length = 1。
而 ?? 是輔助平面的字符,其 UTF-16 的編碼為?D8 3D DE 01,因此 JavaScript 會(huì)把它當(dāng)做?0xD83D?和?0xDE01?兩個(gè)字符。這也就是 length = 2 的原因。
除 length 以外,JavaScript 中和字符串處理相關(guān)的函數(shù),大都存在這類(lèi)問(wèn)題。
如:
vars = "🐰";
?
s.length // 2
s.charAt(0) // '\uD83D'
s.charAt(1) // '\uDC30'
s.charCodeAt(0) // 55357
s.charCodeAt(1) // 56368
?
vars = "🐰子";
s.substring(1); // '\uDC30子'
s.slice(1); // '\uDC30子'
?
/^.$/.test('🐰') // false
13.2ES6 的字符處理
但在 ES6 中,大大增強(qiáng)了對(duì) Unicode 的支持,提供了新的方法解決了以上問(wèn)題。
為了保持兼容,length 屬性還是原來(lái)的行為方式。為了得到字符串的正確長(zhǎng)度,可以采用如下的方式:
[...str].length;
?
// or
Array.from(str).length
原 Unicode 表示字符法,僅支持 \uxxxx 的表示,僅支持 \u0000 - \uffff 范圍。如:“兔”也可以用 \u5154 表示:
vara = '\u5154';
a == '兔'// true
而超出 \uffff 的部分需要使用雙字節(jié)方式表示,如:“🐰”需要用 \uD83D\uDC30 表示,而不能用碼位值 \u1F430 表示:
vara = ' \uD83D\uDC30';
a == '🐰'// true
?
vara = '\u1F430';
// '?0'
而在 ES6 中只要將碼點(diǎn)放入 {} 中,即可正確表示其含義:
vara = '\u{1F430}';
// 🐰
ES6 新增了幾個(gè)專門(mén)處理 4 字節(jié)碼點(diǎn)的函數(shù),如:
1)String.fromCodePoint():從 Unicode 碼點(diǎn)返回對(duì)應(yīng)字符;
2)String.prototype.codePointAt():從字符返回對(duì)應(yīng)的碼點(diǎn);
3)String.prototype.at():返回字符串給定位置的字符。
ES6 正則提供了 u 修飾符,對(duì)正則表達(dá)式添加 4 字節(jié)碼點(diǎn)的支持:
// '\u{1F430}'
/^.$/.test('🐰'); // false
?
/^.$/u.test('🐰'); // true
?
vars = '\u0065\u0323\u0301'; // '??'
/^.$/.test(s); // false
/^.$/u.test(s); // false
13.3length 依舊不準(zhǔn)的合成字符
上面解釋了為什么 length=2,是否意味著 length 非 1 即 2 呢?別急,我們運(yùn)行下 '👨👩👧👦'.length。

居然 length=11,為什么整整齊齊的一家人要這么長(zhǎng)?這到底發(fā)生了什么?
要解釋這個(gè)問(wèn)題,就要講到 Unicode 的合成字符(combining character)。
在一些語(yǔ)言中,會(huì)在字符的上面添加一些符號(hào),以表示不同的發(fā)音或表示不同的含義。例如:漢語(yǔ)拼音有 ā 的音標(biāo)、ü 上面的兩點(diǎn),法語(yǔ)和西班牙語(yǔ)中的 é 表示重音,越南國(guó)語(yǔ)字中的 ? 表示一個(gè)字母。
而在 Unicode 中有兩種表示方法:
1)使用一個(gè)獨(dú)立的碼點(diǎn),例如 ? 的碼點(diǎn)為 U+00D4;
2)使用*本字符+**附加符號(hào)**組合的方式(如 O 的碼點(diǎn)是 U+004F,揚(yáng)抑符 ? 的碼點(diǎn)是 U+0302,組合后同樣展示 ?,其表意與第一種相同)。
'\u00d4'
// ?
?
'\u004f\u0302'
// ?
這兩種方式表示雖然在視覺(jué)和語(yǔ)義上含義相同,但是在 js 中卻無(wú)法理解:
'\u004f\u0302'== '\u00d4'
// false
因此,在 ES6 中提供了?String.prototype.normalize(),用來(lái)將字符的不同表示方法統(tǒng)一為同樣的形式,稱為 Unicode 正規(guī)化(該方法有局限性,后文會(huì)講)。
'\u004f\u0302'.normalize() == '\u00d4'
// true
?
'\u004f\u0302'.normalize().codePointAt(0).toString(16)
// 'd4'
那么回到 '👨👩👧👦'.length 問(wèn)題上來(lái),這個(gè) emoji 就使用了合成符號(hào)表示(這個(gè)表情沒(méi)有單獨(dú)的碼點(diǎn)),其組合是由 7 個(gè) Unicode 組合而成,分別是 ['👨', '', '👩', '', '👧', '', '👦'], 使用了 U+ 200D 零寬連字符(Zero-Width Joiner,ZWJ)將四個(gè) emoji 鏈接。
這類(lèi)表情也被稱為 Emoji ZWJ Sequences :

而 👨 👩 👧 👦 的 length 分別為 2,而零寬連字符的 length 為 1,因此最終 length 就是 24 + 31 = 11。
通過(guò)下面的方式也可以驗(yàn)證其組合:
[...'👨👩👧👦']
// ['👨', '', '👩', '', '👧', '', '👦']
?
'\u{1F468}\u{200D}\u{1F469}\u200D\u{1F467}\u200D\u{1F466}'
// 👨👩👧👦
當(dāng)然這里想通過(guò) normalize() 判斷是否等價(jià)是不行的,首先 👨👩👧👦 沒(méi)有獨(dú)立的碼點(diǎn), 其次該方法目前不能識(shí)別三個(gè)或三個(gè)以上字符的合成。
[...'👨👩👧👦'.normalize()];
// ['👨', '', '👩', '', '👧', '', '👦']
?
// '\u0065\u0323\u0301' => '??'
[...'??'.normalize()]
// ['?', '?']
13.4合成字符的大麻煩
上面提到關(guān)于 Unicode 的問(wèn)題不僅限于 JavaScript,在 Python、Java 等語(yǔ)言中也會(huì)遇到。通常這類(lèi)合成符號(hào)在實(shí)際開(kāi)發(fā)中會(huì)造成很多棘手的問(wèn)題。
13.4.1)input 的 maxlength:
在設(shè)置 input 的 maxLength 屬性時(shí),就會(huì)遇到不同瀏覽器的差異。
<input maxlength="10">
Chrome 將 👨👩👧👦 粘貼后,會(huì)按照 String.prototype.length 進(jìn)行裁剪,多出的字符會(huì)被裁剪掉:

例子在這里:Safari(PC 16.3+Moblie)則會(huì)按照實(shí)際字符個(gè)數(shù)計(jì)算:

在華為默認(rèn)的瀏覽器測(cè)試,行為和 Chrome 的相同:

該問(wèn)題在開(kāi)發(fā)多語(yǔ)言業(yè)務(wù)中,可能會(huì)更加明顯(如后臺(tái)的字符數(shù)限制、前端展示等)。如果希望正確計(jì)算需要使用三方庫(kù),例如:https://github.com/orling/grapheme-splitter?(拋磚引玉,未驗(yàn)證覆蓋情況)。
13.4.2)帶音標(biāo)的文字:
在一些場(chǎng)景下,會(huì)有多個(gè)附加音標(biāo)修飾一個(gè)字符的情況。例如 '??' 是由字母“e” \u0065、尖音符 “??” ****\u0301 、 下句點(diǎn) ****“??” \u0323 ****組成。其中后兩個(gè)音標(biāo)的順序可互換, 甚至也可以是 '?' + '?' 或者 'é' + '?' 的組合。
'\u0065\u0301\u0323'; // ??
'\u0065\u0323\u0301'; // ??
'\u1eb9\u0301'; // '?' + '?'
'\u00e9\u0323'; // 'é' + '?'

?

這時(shí)要使用正則匹配字符變得異常復(fù)雜。
雖然 Unicode 屬性轉(zhuǎn)義表達(dá)式(Unicode property escapes),但可惜的是ES2018 以前的版本并不支持,因此可以考慮使用 XRegExp 來(lái)實(shí)現(xiàn)。
// 例子在這里
varreg = XRegExp('\pL\pM*', 'g');
XRegExp.match('??', reg);
// ["??"]
其中 \pL 和 \pM 的含義如下:
1)\p{L} or \p{Letter}: any kind of letter from any language,匹配一個(gè)字母;
2)\p{M} or \p{Mark}: a character intended to be combined with another character (e.g. accents, umlauts, enclosing boxes, etc.). 匹配附加符號(hào)。
雖然在 ES2018 中引入了 Unicode 屬性轉(zhuǎn)義符,但在瀏覽端上依然要考慮使用 XRegExp 來(lái)實(shí)現(xiàn),當(dāng)然可以考慮在服務(wù)端處理,因?yàn)?Python 3.6、Perl 5.24 、Ruby 2.4 、.NET 4.6、Go 10 等已經(jīng)支持這些表達(dá)式了,當(dāng)然不同語(yǔ)言的支持程度可能略有不同。
13.5數(shù)據(jù)庫(kù)中存儲(chǔ)表情
另外在數(shù)據(jù)庫(kù)存儲(chǔ)時(shí)使用了不同的表示方式存儲(chǔ)同一含義的字符,會(huì)導(dǎo)致檢索失敗。MySQL 存儲(chǔ)時(shí)也要考慮使用 utf8mb4_unicode 編碼格式,才能正確存儲(chǔ)表情符。
14、本文小結(jié)
最后關(guān)于 Unicode 字符編碼模型,可以概況為四個(gè)層級(jí)。
1)抽象字符庫(kù)(ACR, Abstract Character Repertoire):即要編碼的字符,比如某些字母表和符號(hào)集。這一層級(jí)會(huì)將語(yǔ)言中的字符進(jìn)行抽象。
2)編碼字符集(CCS, Coded Character Set):從抽象字符庫(kù)到一組非負(fù)整數(shù)(即代碼點(diǎn))的映射。這一層級(jí)可以理解為字符到 Unicode 碼點(diǎn)映射的過(guò)程。
3)字符編碼形式(CEF, Character Encoding Form):從代碼點(diǎn)到特定寬度(比如32-bit整數(shù))的代碼單元序列的映射。這一層級(jí)理解為 UTF-8、UTF-16、UTF-32 的編碼過(guò)程。
4)字符編碼方案(CES, Character Encoding Scheme):從代碼單元序列到字節(jié)序列的映射。這一層級(jí)就涉及到了如大端序、小端序的設(shè)計(jì)和存取過(guò)程。
附錄:更多IM技術(shù)精華文章
[1]?新手入門(mén)一篇就夠:從零開(kāi)發(fā)移動(dòng)端IM》
[2]?零*礎(chǔ)IM開(kāi)發(fā)入門(mén)(一):什么是IM系統(tǒng)?》
[3]?零*礎(chǔ)IM開(kāi)發(fā)入門(mén)(二):什么是IM系統(tǒng)的實(shí)時(shí)性?》
[4]?零*礎(chǔ)IM開(kāi)發(fā)入門(mén)(三):什么是IM系統(tǒng)的可靠性?》
[5]?零*礎(chǔ)IM開(kāi)發(fā)入門(mén)(四):什么是IM系統(tǒng)的消息時(shí)序一致性?》
[6]?移動(dòng)端IM開(kāi)發(fā)者必讀(一):通俗易懂,理解移動(dòng)網(wǎng)絡(luò)的“弱”和“慢”》
[7]?移動(dòng)端IM開(kāi)發(fā)者必讀(二):史上最全移動(dòng)弱網(wǎng)絡(luò)優(yōu)化方法總結(jié)》
[8]?從客戶端的角度來(lái)談?wù)勔苿?dòng)端IM的消息可靠性和送達(dá)機(jī)制》
[9]?現(xiàn)代移動(dòng)端網(wǎng)絡(luò)短連接的優(yōu)化手段總結(jié):請(qǐng)求速度、弱網(wǎng)適應(yīng)、安全保障》
[10]?史上最通俗Netty框架入門(mén)長(zhǎng)文:*本介紹、環(huán)境搭建、動(dòng)手實(shí)戰(zhàn)
[11]?強(qiáng)列建議將Protobuf作為你的即時(shí)通訊應(yīng)用數(shù)據(jù)傳輸格式
[12]?IM通訊協(xié)議專題學(xué)習(xí)(一):Protobuf從入門(mén)到精通,一篇就夠!
[13]?微信新一代通信安全解決方案:*于TLS1.3的MMTLS詳解
[14]?探討組合加密算法在IM中的應(yīng)用
[15]?從客戶端的角度來(lái)談?wù)勔苿?dòng)端IM的消息可靠性和送達(dá)機(jī)制
[16]?IM消息送達(dá)保證機(jī)制實(shí)現(xiàn)(一):保證在線實(shí)時(shí)消息的可靠投遞
[17]?理解IM消息“可靠性”和“一致性”問(wèn)題,以及解決方案探討
[18]?融云技術(shù)分享:全面揭秘億級(jí)IM消息的可靠投遞機(jī)制
[19]?IM群聊消息如此復(fù)雜,如何保證不丟不重?
[20]?零*礎(chǔ)IM開(kāi)發(fā)入門(mén)(四):什么是IM系統(tǒng)的消息時(shí)序一致性?
[21]?一套億級(jí)用戶的IM架構(gòu)技術(shù)干貨(下篇):可靠性、有序性、弱網(wǎng)優(yōu)化等
[22]?如何保證IM實(shí)時(shí)消息的“時(shí)序性”與“一致性”?
[23]?阿里IM技術(shù)分享(六):閑魚(yú)億級(jí)IM消息系統(tǒng)的離線推送到達(dá)率優(yōu)化
[24]?微信的海量IM聊天消息序列號(hào)生成實(shí)踐(算法原理篇)
[25]?社交軟件紅包技術(shù)解密(一):全面解密QQ紅包技術(shù)方案——架構(gòu)、技術(shù)實(shí)現(xiàn)等
[26]?網(wǎng)易云信技術(shù)分享:IM中的萬(wàn)人群聊技術(shù)方案實(shí)踐總結(jié)
[27]?企業(yè)微信的IM架構(gòu)設(shè)計(jì)揭秘:消息模型、萬(wàn)人群、已讀回執(zhí)、消息撤回等
[28]?融云IM技術(shù)分享:萬(wàn)人群聊消息投遞方案的思考和實(shí)踐
[29]?為何*于TCP協(xié)議的移動(dòng)端IM仍然需要心跳保活機(jī)制?
[30]?一文讀懂即時(shí)通訊應(yīng)用中的網(wǎng)絡(luò)心跳包機(jī)制:作用、原理、實(shí)現(xiàn)思路等
[31]?微信團(tuán)隊(duì)原創(chuàng)分享:Android版微信后臺(tái)?;顚?shí)戰(zhàn)分享(網(wǎng)絡(luò)保活篇)
[32]?融云技術(shù)分享:融云安卓端IM產(chǎn)品的網(wǎng)絡(luò)鏈路?;罴夹g(shù)實(shí)踐
[33]?阿里IM技術(shù)分享(九):深度揭密RocketMQ在釘釘IM系統(tǒng)中的應(yīng)用實(shí)踐
[34]?徹底搞懂TCP協(xié)議層的KeepAlive保活機(jī)制
[35]?深度解密釘釘即時(shí)消息服務(wù)DTIM的技術(shù)設(shè)計(jì)
[36]?*于實(shí)踐:一套百萬(wàn)消息量小規(guī)模IM系統(tǒng)技術(shù)要點(diǎn)總結(jié)
[37]?跟著源碼學(xué)IM(十):*于Netty,搭建高性能IM集群(含技術(shù)思路+源碼)
[38]?一套十萬(wàn)級(jí)TPS的IM綜合消息系統(tǒng)的架構(gòu)實(shí)踐與思考
(本文已同步發(fā)布于:http://www.52im.net/thread-4453-1-1.html)