編碼 解碼


https://mp.weixin.qq.com/s/ykYXDQBNqhCYF0MDFx19gQ

程序員必備:10分鍾搞懂各種編碼丨另附實戰案例

背景

HTTP 協議基於文本傳輸,字符編碼將文本變為二進制,二進制編碼將二進制變為文本。TCP 協議基於二進制傳輸,數據讀取時需要處理字節序。本文將介紹常見的字符編碼、二進制編碼及字節序,並一探 Golang 中的實現。

 

字符編碼

引言:如何把“Hello world”變成字節?

  • Step1:得到要表示的全量字符(字符表)

  • Step2:為每個字符指定一個整數編號(編碼字符集)

  • Step3:將編號映射成有限長度比特值(字符編碼表)

字符是各種文字和符號的總稱,包括各國家文字、標點符號、圖形符號、數字等。全世界共使用 5651 種語言,其中使用人數超過 5000 萬的語言有 13 種,每種語言有自己的字符。漢語中,一個漢字就是一個字符。英語中,一個字母就是一個字符。甚至看不見的也可以是字符(如控制字符)。字符的集合即為字符表,如英文字母表,阿拉伯數字表。ASCII 碼表中一共有 128 個字符。

編碼字符集(CCS:Coded Character Set)

為字符表中的每個字符指定一個編號(碼點,Code Point),即得到編碼字符集。常見有 ASCII 字符集、Unicode 字符集、GB2312 字符集、BIG5 字符集、 GB18030 字符集等。ASCII 字符集中一共有 128 個字符,包括了 94 個可打印字符(英文大小寫字母 52 個、阿拉伯數字 10 個、西文符號 32 個)和 34 個控制符或通信專用字符,碼點值范圍為[0, 128),如下圖所示。Unicode 字符集是一個很大的集合,現有容量將近 2^21 個字符,碼點值范圍為[0, 2^20+2^16)

圖片ASCII字符編碼表

字符編碼表(CEF:Character Encoding Form)

編碼字符集只定義了字符與碼點的映射,並沒有規定碼點的字節表示方式。由於 1 個字節可以表示 256 個編號,足以容納 ASCII 字符集,因此ASCII 編碼的規則很簡單:直接將碼點值用 uint8 表示即可。對於 Unicode 字符集,容納 2^21 至少需要 3 字節。可以采用類似 ASCII 的編碼規則:直接將編碼點值用 uint32 表示即可,這正是 UTF-32 編碼

這種一刀切的定長編碼方式雖然簡單粗暴,弊端也很明顯:對於純英文文本,UTF-32 編碼空間占用將是 ACSII 編碼的 4 倍,造成極大的空間浪費,幾乎沒什么人用。有沒有更優雅的解決方案?當然,這就是 UTF-8 和 UTF-16,兩種當前比較流行的 Unicode 編碼方式。

UTF-8

歷史告訴我們,成功的設計往往具有包容性。UTF-8 是一個典型,漂亮的實現了對 ASCII 碼的向后兼容,以保證可以被大眾接受。UTF-8 是目前互聯網上使用最廣泛的一種 Unicode 編碼方式,它的最大特點就是可變長,隨碼點變換長度(從 1 字節到 4 字節)。text

圖片

大道至簡,優雅的設計一定是簡單的,UTF-8 的編碼規則也詮釋了這一點。編碼規則如下:

  1. <=127(U+7F)的碼點采用單字節編碼,與 ASCII 保持一致;

  2. >127(U+7F)的碼點采用 N 字節(N 屬於 2,3,4)編碼,首字節的前 N 位為 1,第 N+1 位為 0,剩余 N-1 個字節的前兩位都為 10,剩下的二進制位使用字符的碼點來填充。

其中(U+7F)表示 Unicode 的十六進制碼點值,即 127。如果覺得編碼規則抽象,結合下表更加清晰:

Unicode 碼點范圍 碼點數量 UTF-8 編碼格式
0000 0000 ~ 0000 007F 2^7 0xxxxxxx
0000 0080 ~ 0000 07FF 2^11 - 2^7 110xxxxx 10xxxxxx
0000 0800 ~ 0000 FFFF 2^16 - 2^11 1110xxxx 10xxxxxx 10xxxxxx
0001 0000 ~ 0010 FFFF 2^20 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

舉個例子,如“漢”的 Unicode 碼點是 U+6C49(110 1100 0100 1001),根據上表可得需要 3 字節編碼,填充碼點值后得到 0xE6 0xB7 0x89(11100110 10110001 10001001)。

根據編碼規則,解碼也很簡單,關鍵是如何判斷連續的字節數:首字節連續 1 的個數即為字節數

需要一提的是,在 MySQL 中,utf8 是“虛假的 utf8”,最大只支持 3 個字節,如果建表時選擇 CHARSET=utf8,會導致很多特殊字符和 emoji 表情都無法插入。utf8mb4 才是“真正的 utf8”,mb4 即most bytes 4。為什么 MySQL 中 utf8 最大只支持 3 字節?歷史原因,在 MySQL 剛開發那會兒,Unicode 空間只有 2^16,Unicode 委員會還在做 “65535 個字符足夠全世界用了”的美夢呢。

UTF-16

在 C/C++ 中遇到的wchar_t類型或 Java 中的char類型,這些類型占內存兩個字節,因為 Unicode 中常用的字符都處於[U+0, U+FFFF](基本平面)的范圍之內,因此兩個字節幾乎可以覆蓋大部分的常用字符,這正是 UTF-16 編碼的一個前提。

相比 UTF-32 與 UTF-8,UTF-16 編碼是一個折中:小於(U+FFFF)2^16 的碼點(基本平面)使用 2 字節編碼,大於(U+FFFF)2^16 的碼點(輔助碼點)使用 4 字節編碼。由於基礎平面空間會占用 2 字節的所有比特位,無法像 UTF-8 那樣留有“10”前綴。那么問題來了:當我們遇到兩個節時,如何判斷是 2 字節編碼還是 4 字節編碼?

UTF-16 的編碼的另一個前提:在基本平面內,[U+D800, U+DFFF]是一個空段(空間大小為 2^11),這些碼點不對應任何字符。因此,這個空段可以用來映射輔助平面的字符。

輔助平面容量為 2^20,至少需要 20 個二進制位,UTF-16 將這 20 個二進制位分成兩半,前 10 位映射在 U+D800 到 U+DBFF(空間大小 2^10),稱為高位(H),后 10 位映射在 U+DC00 到 U+DFFF(空間大小 2^10),稱為低位(L)。

映射方式采用線性映射。Unicode3.0 中給出了輔助平面字符的轉換公式:

H = Math.floor((c-0x10000) / 0x400) + 0xD800

L = (c - 0x10000) % 0x400 + 0xDC00

也就是說,一個輔助平面的碼點,被拆成兩個基本平面的空段碼點表示。如果雙字節的值在[U+D800, U+DBFF]中,則要和后續相鄰的雙字節一同解碼。具體編碼規則為:

  1. <= (U+FFFF)的碼點采用雙字節編碼,直接將碼點使用 uint16 表示;

  2. > (U+FFFF)的碼點采用 4 字節編碼,作差計算碼點溢出值,將溢出值用 uint20 表示后,前 10 位映射到[U+D800, U+DBFF],后 10 位映射到[U+DC00, U+DFFF];

小結: 定長編碼的優點是轉換規則簡單直觀,查找效率高,缺點是空間浪費,以及不可擴展。如果 Unicode 字符集進一步擴充,UTF-16 和 UTF-32 都將不可用,而 UTF-8 具有更強的可擴展性。

Golang 中字符編碼

不像 C++、Java 等語言支持五花八門的字符編碼,Golang 遵從“大道至簡”的原則:全給老子用 UTF-8。所以 go 程序員再也不用擔心亂碼問題,甚至可以用漢字和表情包寫代碼,string 與字節數組轉換也是直接轉換,十分酸爽。

func TestTemp(t *testing.T) {
    來自打工人的問候()
}

func 來自打工人的問候() {
    問候語 := "早安,打工人😁"
    fmt.Println(問候語)
    bytes := []byte(問候語)
    fmt.Println(hex.EncodeToString(bytes))
}

// 執行結果-->
早安,打工人😁
e697a9e5ae89efbc8ce68993e5b7a5e4babaf09f9881

值得一提的是,Golang 中 string 的底層模型就是字節數組,所以類型轉換過程中無需編解碼。也因此,Golang 中 string 的底層模型是字節數組,其長度並非字符數,而是對應字節數。如果要取字符數,需要先將字符串轉換為字符數組。字符類型(rune)實際上是 int32 的別名,即用 UTF-32 編碼表示字符

func TestTemp(t *testing.T) {
    fmt.Println(len("早")) // 3
    fmt.Println(len([]byte("早"))) // 3
    fmt.Println(len([]rune("早")) // 1
}

// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

再看一下 go 中 utf-8 編碼的具體實現。首先獲取字符的碼點值,然后根據范圍判斷字節數,根據對應格式生成編碼值。如果是無效的碼點值,或碼點值位於空段,則返回U+FFFD(即 �)。解碼過程不再贅述。

// EncodeRune writes into p (which must be large enough) the UTF-8 encoding of the rune.
// It returns the number of bytes written.
func EncodeRune(p []byte, r rune) int {
    // Negative values are erroneous. Making it unsigned addresses the problem.
    switch i := uint32(r); {
    case i <= rune1Max:
        p[0] = byte(r)
        return 1
    case i <= rune2Max:
        _ = p[1] // eliminate bounds checks
        p[0] = t2 | byte(r>>6)
        p[1] = tx | byte(r)&maskx
        return 2
    case i > MaxRune, surrogateMin <= i && i <= surrogateMax:
        r = RuneError
        fallthrough
    case i <= rune3Max:
        _ = p[2] // eliminate bounds checks
        p[0] = t3 | byte(r>>12)
        p[1] = tx | byte(r>>6)&maskx
        p[2] = tx | byte(r)&maskx
        return 3
    default:
        _ = p[3] // eliminate bounds checks
        p[0] = t4 | byte(r>>18)
        p[1] = tx | byte(r>>12)&maskx
        p[2] = tx | byte(r>>6)&maskx
        p[3] = tx | byte(r)&maskx
        return 4
    }
}

const(
    t1 = 0b00000000
    tx = 0b10000000
    t2 = 0b11000000
    t3 = 0b11100000
    t4 = 0b11110000
    t5 = 0b11111000
    maskx = 0b00111111
    mask2 = 0b00011111
    mask3 = 0b00001111
    mask4 = 0b00000111
    rune1Max = 1<<7 - 1
    rune2Max = 1<<11 - 1
    rune3Max = 1<<16 - 1
    RuneError = '\uFFFD' // the "error" Rune or "Unicode replacement character"
)

// Code points in the surrogate range are not valid for UTF-8.
const (
    surrogateMin = 0xD800
    surrogateMax = 0xDFFF
)

 

二進制編碼

引言:HTTP 是怎么傳輸二進制數據的?

  • Step1:定義字符集;

  • Step2:將二進制數據分組;

  • Step3:將每組映射為字符;

字符編碼是「文本」變為「二進制」的過程,那如何將任意「二進制」變為「文本」?答案是進行二進制編碼,常見有 Hex 編碼與 Base64 編碼。

顯然不能按字符編碼直接解碼,因為字符編碼的結果二進制是滿足編碼規律的,而非「任意」的,非法格式進行字符解碼會出現亂碼(比如對0b11xxxxxx進行 UTF-8 解碼)。

Hex 編碼

Hex 編碼是最直觀的二進制編碼方式,所見即所得。上文中的十六進制表示就是用的 Hex 編碼。規則如下:

  1. Hex 字符集為0123456789abcdef
  2. 每 4bit 為 1 組(2^4=16);
  3. 每組映射為一個 Hex 字符;

計算機中二進制數據都是以字節為單位存儲的,1 個字節 8bit,不會出現無法被 4 整除的情況。

每個字節編碼為 2 個 Hex 字符,即編碼后的字符數是原始數據字節數的 2 倍。在 ASCII 或 UTF-8 編碼下,存儲 Hex 結果字符串需要的空間是原始數據的 2 倍,存儲效率為 50%。

Base64 編碼

Base64 編碼,顧名思義,是基於 64 個字符進行編碼。規則如下:

  1. Base64 字符集(以標准 Base64 為例, 26 大寫, 26 小寫, 10 數字, 以及+/)為ABC...YZabc...yz012...89+/
  2. 每 6bit 為一組(2^6=64),即每 3 個字節為 4 組
  3. 每組映射為一個 Base64 字符;

如果要編碼的二進制數據不是 3 的倍數,最后會剩下 1 個或 2 個字節怎么辦?標准編碼(StdEncoding) 會先在末尾用 0x00 補齊再分組,並將最后 2 個或 1 個 6bit 分組(全為 0 填充)映射為'=',表示補齊的 0 字節數量。

圖片

舉個例子,以 0x12 34 ab cd編碼為標准 base64 為例:

 

  1. 不足 3 的倍數,先用兩個 0 字節補齊 -->0x12 34 ab cd 00 00
  2. 0x12 34 ab編碼為EjSr
  3. 0xcd 00 00二進制為0b1100 1101 0000 0000 0000 0000,分為 4 組后為110011 010000 000000 000000,編碼結果為zQ==
  4. 最終編碼結果為EjSrzQ==

解碼過程注意末尾字節的處理即可,此處不再贅述。

  1. EjSrzQ==-->0x12 34 ab cd 00 00-->0x12 34 ab cd

標准編碼中編碼結果字符長度一定是 4 的倍數,且是原始數據字節數的 4/3 倍,因為會將字節數據補齊至 3 的倍數,每 3 個字節編碼為 4 個字符。在 ASCII 或 UTF-8 編碼下,存儲結果字符串需要的空間是原始數據的 4/3 倍,存儲效率為 75%

根據字符集的不同,Base64 編碼有幾個變種,除了標准編碼(StdEncoding),常見的還有 URL 編碼(URLEncoding)、原始標准編碼(RawStdEncoding)以及原始 URL 編碼(RawUrlEncoded)。

簡單來說,Raw 指的是無 Padding,URL 指的是用-_取代編碼結果中包含的 url 關鍵字+/。不妨參考 Golang 中encoding/base64包中的描述:

// StdEncoding is the standard base64 encoding, as defined in
// RFC 4648.
var StdEncoding = NewEncoding(*encodeStd*)

// URLEncoding is the alternate base64 encoding defined in RFC 4648.
// It is typically used in URLs and file names.
var URLEncoding = NewEncoding(*encodeURL*)

// RawStdEncoding is the standard raw, unpadded base64 encoding,
// as defined in RFC 4648 section 3.2.
// This is the same as StdEncoding but omits padding characters.
var RawStdEncoding = StdEncoding.WithPadding(*NoPadding*)

// RawURLEncoding is the unpadded alternate base64 encoding defined in RFC 4648.
// It is typically used in URLs and file names.
// This is the same as URLEncoding but omits padding characters.
var RawURLEncoding = URLEncoding.WithPadding(*NoPadding*)

與標准編碼不同的是,原始編碼中,字節數不足 3 的倍數時不會補齊字節數,采用如下方案:

  1. 如果剩余 1 字節,則左移 4bit 后轉換為 2 字符;
  2. 如果剩余 2 字節,則左移 2bit 后轉化為 3 字符;

原始編碼方案中,結果字符串長度可以不是 4 的倍數

最后,聰明的你一定已經發現了,Hex 編碼可以看成“Base16 編碼”。隨着字符數量的增加,存儲效率也隨之增加。如果有“Base256”編碼,存儲效率豈不就 100%了?很遺憾,主流字符編碼中,單字節能表示的可打印字符只有 92 個。通過擴充多字節字符,或用組合字符實現 base256 意義不大。

Golang 中的二進制編碼

看一下 Golang 中 Base64 編碼的實現。首先通過EncodedLen方法確定結果長度,生成輸出buf,然后通過Encode方法將編碼結果填充到buf並返回結果字符串。

// EncodeToString returns the base64 encoding of src.
func (enc *Encoding) EncodeToString(src []byte) string {
    buf := make([]byte, enc.EncodedLen(len(src)))
    enc.Encode(buf, src)
    return string(buf)
}

如前述,標准編碼和原始編碼(無 Padding)的結果長度不同:如果需要 Padding,直接根據字節數計算即可,反之則需要根據 bit 數計算。

// EncodedLen returns the length in bytes of the base64 encoding
// of an input buffer of length n.
func (enc *Encoding) EncodedLen(n int) int {
    if enc.padChar == *NoPadding* {
        return (n*8 + 5) / 6 // minimum # chars at 6 bits per char
    }
    return (n + 2) / 3 * 4 // minimum # 4-char quanta, 3 bytes each
}

Encode方法實現了編碼細節。首先遍歷字節數組,將每 3 個字節編碼為 4 個字符。最后處理剩余的 1 或 2 個字節(如有):首先使用移位運算進行 0bit 填充,然后進行字符轉換。如前述,無 Padding 時,剩下 1 字節對應 2 字符,剩下 2 字節對應 3 字符,即至少會有 2 字符。最后在switch代碼段中,根據剩余字節數填充第 3 個字符和 Padding 字符(如有)即可。

func (enc *Encoding) Encode(dst, src []byte) {
    if len(src) == 0 {
        return
    }
    // enc is a pointer receiver, so the use of enc.encode within the hot
    // loop below means a nil check at every operation. Lift that nil check
    // outside of the loop to speed up the encoder.
    _ = enc.encode
    di, si := 0, 0
    n := (len(src) / 3) * 3
    for si < n {
        // Convert 3x 8bit source bytes into 4 bytes
        val := uint(src[si+0])<<16 | uint(src[si+1])<<8 | uint(src[si+2])
        dst[di+0] = enc.encode[val>>18&0x3F]
        dst[di+1] = enc.encode[val>>12&0x3F]
        dst[di+2] = enc.encode[val>>6&0x3F]
        dst[di+3] = enc.encode[val&0x3F]
        si += 3
        di += 4
    }
    remain := len(src) - si
    if remain == 0 {
        return
    }
    // Add the remaining small block
    val := uint(src[si+0]) << 16
    if remain == 2 {
        val |= uint(src[si+1]) << 8
    }
    dst[di+0] = enc.encode[val>>18&0x3F]
    dst[di+1] = enc.encode[val>>12&0x3F]
    switch remain {
    case 2:
        dst[di+2] = enc.encode[val>>6&0x3F]
        if enc.padChar != *NoPadding* {
            dst[di+3] = byte(enc.padChar)
        }
    case 1:
        if enc.padChar != *NoPadding* {
            dst[di+2] = byte(enc.padChar)
            dst[di+3] = byte(enc.padChar)
        }
    }
}

 

字節序

引言:拿到兩個字節,如何解析為整形?

  • Step1:明確字節高低位順序

  • Step2:按高低位權重計算結果

上述二進制編碼主要用於文本傳輸,能不能不進行編碼,直接傳輸二進制?當然可以,基於二進制傳輸協議,如 TCP 協議。那么什么是文本傳輸,什么是二進制傳輸?簡單來說,文本傳輸,內容為文本,自帶描述信息(參數名),如 HTTP 中的字段都以 KV 形式存在。二進制傳輸,內容為二進制,以預先定義好的格式拼在一起,如 TCP 協議報文格式。

圖片

 

大端與小端

聊到二進制傳輸,一個避不開的話題是字節序。什么是字節序?假設讀取到一個兩字節的 uint16 0x04 0x00,如果從左往右(從高位往低位)解碼,得到的是 1024,反過來(從低位往高位)解碼則是 4,這就是字節序。符合人類閱讀習慣的(從高位往低位)是大端(BigEndian),反之為小端(LittleEndian)。

另一種大小端的定義:LittleEndian 將低序字節存儲在低地址,BigEndian 將高序字節存儲在低地址。理解起來有些抽象,本質上是一致的。

圖片

 

為什么會有小端字節序,統一都用大端不好么?

計算機不這么想,因為計算機中計算都是從低位開始的,電路先處理低位字節效率比較高。但是,人類還是習慣讀寫大端字節序。所以, 除了計算機的內部處理,其他的場合幾乎都是大端字節序,比如網絡傳輸和文件儲存。

那什么時候程序員需要進行字節序處理呢?當多字節整形(uint16,uint32,uint64)需要和字節數組互相轉換時。字節數組是無字節序的,客戶端寫入啥,服務端就讀取啥,不會出現逆序,寫入和讀取無需考慮字節序,這點大可放心只有當多字節整形和字節數組互轉時必須指明字節序。

Golang 中的字節序

以 uint16 與字節數組互轉為例,看一下 Golang 中 encoding/binary 包中的字節序處理與實現。可見實現並不復雜,注意字節順序即可。

func TestEndian(t *testing.T) {
    bytes := make([]byte, 2)
    binary.LittleEndian.PutUint16(bytes, 1024) // 小端寫 --> 0x0004
    binary.BigEndian.PutUint16(bytes, 1024) // 大端寫 --> 0x0400
    binary.LittleEndian.Uint16(bytes) // 小端讀 --> 4
    binary.BigEndian.Uint16(bytes) // 大端讀 --> 1024
}

func (littleEndian) PutUint16(b []byte, v uint16) {
    _ = b[1] // early bounds check to guarantee safety of writes below
    b[0] = byte(v)
    b[1] = byte(v >> 8)
}

func (bigEndian) PutUint16(b []byte, v uint16) {
    _ = b[1] // early bounds check to guarantee safety of writes below
    b[0] = byte(v >> 8)
    b[1] = byte(v)
}

func (littleEndian) Uint16(b []byte) uint16 {
    _ = b[1] // bounds check hint to compiler; see golang.org/issue/14808
    return uint16(b[0]) | uint16(b[1])<<8
}

func (bigEndian) Uint16(b []byte) uint16 {
    _ = b[1] // bounds check hint to compiler; see golang.org/issue/14808
    return uint16(b[1]) | uint16(b[0])<<8
}

 

 

實戰:加解密中的編碼與字節序

在加解密場景中,通常我們會對明文加密得到密文,對密文解密得到明文。比如對密碼"123456"(明文)進行對稱加密(如 SM4)得到"G7EeTPnuvSU41T68qsuc_g"(密文)。明文和密文都是由可打印字符構成的文本,通常明文人類可直接閱讀其含義(不考慮二次加密),密文需要解密后才能理解含義。

那么上述明文變成密文,期間經歷了哪些編碼過程呢?以加密為例:

  1. 將明文"123456"進行字符解碼(如 UTF-8),得到明文字節序列0x31 32 33 34 35 36;
  2. 將明文字節序列輸入 SM4 加密算法,輸出密文字節序列0x1b b1 1e 4c f9 ee bd 25 38 d5 3e bc aa cb 9c fe
  3. 將密文字節序列進行二進制編碼(如 RawURLBase64),得到密文"G7EeTPnuvSU41T68qsuc_g"

同理,將"G7EeTPnuvSU41T68qsuc_g"解密成"123456"過程中,應與加密過程的編碼方式對應:先進行 RawRULBase64 解碼,再解密,最后再進行 UTF-8 編碼。

加解密算法的輸入輸出都是字節序列,所以要將明文、密文與字節序列進行轉換。有兩點需要注意:

  1. 明文解碼為明文字節序列,解碼方式因場景而定。對於多次加密場景(如對“G7EeTPnuvSU41T68qsuc_g”再次加密),明文是 Base64 編碼得到的,建議采用一致的方式解碼。雖然也可以直接進行 UTF-8 解碼,但會使加解密流程設計變得復雜。
  2. 密文字節序列編碼為密文,必須用二進制編碼,不能用字符編碼。使用字符編碼會產生亂碼(意味着數據丟失,無法逆向解碼出原始數據)。上述密文序列密文序列進行 UTF-8 編碼的結果是 �L���%8�>��˜�。

合規要求,加解密場景中應使用硬件加密機。通常硬件加密機提供基於 TCP 的字節流通信方式,比如約定每次通信數據中的前 2 字節為數據長度,后面的為真實數據。發送時,需要將真實數據長度轉為 2 字節拼在前面,接收時,需要先讀取前兩字節得到真實數據長度 N,再讀取 N 字節得到真實數據。其中長度與字節序列的轉換需要關注字節序:發送方和接收方的字節序處理保持一致即可,比如全用大端。下面給出了數據發送的示例代碼:

func (m *EncryptMachine) sendData(conn net.Conn, data []byte) error {
    // add length
    newData := m.addLength(data)
    // send new data
    return util.SocketWriteData(conn, newData)
}

func (m *EncryptMachine) addLength(data []byte) []byte {
    lengthBytes := make([]byte, 2)
    binary.BigEndian.PutUint16(lengthBytes, uint16(len(data)))
    return append(lengthBytes, data...)
}

 

 

總結

編碼雖然基礎,但卻容易出錯,切莫眼高手低。希望本文能幫助大家進一步了解字符編碼、二進制編碼與字節序,避免踩坑。真誠點贊,手留余香。

參考資料


[1] 十分鍾搞清字符集和字符編碼: http://cenalulu.github.io/linux/character-encoding/

[2] 字符編碼筆記:ASCII,Unicode 和 UTF-8: http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html

[3] 徹底弄懂 Unicode: https://liyucang-git.github.io/2019/06/17/%E5%BD%BB%E5%BA%95%E5%BC%84%E6%87%82Unicode%E7%BC%96%E7%A0%81/

[4] 理解字節序: https://www.ruanyifeng.com/blog/2016/11/byte-order.html

 

 
 
 
 
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM