
【倉頡語言入門 · 第二篇 第 6 課】二進(jìn)制數(shù)據(jù)處理跟字節(jié)序死磕std.binary上一課哈希把字節(jié)攪碎成指紋用了異或、移位、與或非——這些位運算只是操作單個數(shù)值內(nèi)部的比特。這一課的std.binary管的是另一件事一個數(shù)值在內(nèi)存/文件里到底占幾個字節(jié)、這些字節(jié)怎么排。為什么同一張 BMP 圖片Windows 和 Mac 讀出來一樣為什么網(wǎng)絡(luò)協(xié)議里寫先傳高字節(jié)答案是同一個詞——字節(jié)序Endianness。本課把std.binary的三個接口大端、小端、字節(jié)反轉(zhuǎn)講透最后動手解析一張 BMP 圖片的文件頭看看真實世界的二進(jìn)制格式長什么樣。本文所有代碼、輸出與報錯文案均在倉頡 SDK 1.2.0 下逐行實測編譯運行。目錄第二篇導(dǎo)航第一篇《倉頡語言入門》共 30 課已完結(jié)環(huán)境搭建 → 語法 → 函數(shù) → 類型系統(tǒng) → 工程化 → 并發(fā) → 兩場實戰(zhàn)。第二篇在更高的起點上繼續(xù)共17 課、4 個模塊模塊課次內(nèi)容一、補遺篇0106正則、時間日期、命令行參數(shù)、隨機數(shù)、加密哈希、二進(jìn)制處理二、數(shù)據(jù)庫篇0710std.database.sql 入門與 CRUD、預(yù)編譯與事務(wù)、手寫 DAO、綜合小實戰(zhàn)三、進(jìn)階硬核篇1114反射、宏與 deriving、C 互操作 FFI、并發(fā)容器與對象池四、實戰(zhàn)項目篇1517HTTP 服務(wù)端、多線程日志分析器、簡易爬蟲正則表達(dá)式std.regex時間日期處理std.time命令行參數(shù)解析std.argopt隨機數(shù)與概率std.random加密與哈希std.crypto二進(jìn)制數(shù)據(jù)處理本文數(shù)據(jù)庫入門std.database.sql 與 CRUD預(yù)編譯、事務(wù)與防注入手寫 DAO 數(shù)據(jù)訪問層數(shù)據(jù)庫綜合小實戰(zhàn)反射在運行時打量類型宏與 deriving讓編譯器幫你寫代碼與 C 互操作FFI并發(fā)容器與對象池實戰(zhàn)三HTTP 服務(wù)端實戰(zhàn)四多線程日志分析器實戰(zhàn)五簡易爬蟲一、為什么需要二進(jìn)制處理1.1 一個數(shù)占幾個字節(jié)倉頡里Int32是 32 位整數(shù)——也就是 4 個字節(jié)。把0x12345678這個數(shù)存進(jìn)文件或發(fā)過網(wǎng)絡(luò)物理上就是 4 個字節(jié)依次排開。但哪 4 個字節(jié)、什么順序0x12345678拆成 4 個字節(jié)0x12、0x34、0x56、0x78。其中0x12是最高位字節(jié)MSBMost Significant Byte0x78是最低位字節(jié)LSB。寫進(jìn)內(nèi)存時誰放低地址、誰放高地址兩種主流約定字節(jié)序低地址 → 高地址誰先用大端序Big-Endian高位在前12 34 56 78網(wǎng)絡(luò)協(xié)議TCP/IP、Java、PowerPC小端序Little-Endian低位在前78 56 34 12x86/x64 CPU、Windows、ARM多數(shù)模式名字來自《格列佛游記》里吃雞蛋先敲大頭還是小頭的荒唐爭論——沒有誰對誰錯只是約定。麻煩在于不同約定的系統(tǒng)之間傳數(shù)據(jù)必須先講清楚用哪種字節(jié)序否則同一段字節(jié)讀出來是完全不同的數(shù)。1.2 不處理字節(jié)序會怎樣假設(shè)你用 x86 電腦小端把0x12345678寫進(jìn)文件字節(jié)是78 56 34 12。另一個程序或另一臺機器按大端序讀回來得到的是0x78563412——同一個文件兩個數(shù)。這就是所有跨平臺二進(jìn)制格式圖片、視頻、網(wǎng)絡(luò)包必須在規(guī)范里寫死字節(jié)序的原因。std.binary就是管這件事的給你一套統(tǒng)一接口顯式指定按大端還是小端讀寫把約定寫進(jìn)代碼而不是靠默契。1.3 先認(rèn)人std.binary 的家底std.binary只提供三個接口各司其職接口作用關(guān)鍵方法BigEndianOrderT大端序轉(zhuǎn)換readBigEndian/writeBigEndianLittleEndianOrderT小端序轉(zhuǎn)換readLittleEndian/writeLittleEndianSwapEndianOrderT翻轉(zhuǎn)字節(jié)順序swapBytes注意它的設(shè)計思路不是給你一個ByteBuffer類那在std.io里第 5 課用過而是用接口擴展的方式直接給所有數(shù)值類型裝上讀/寫字節(jié)序列的能力。Int32、Float64、Bool……統(tǒng)統(tǒng)自帶writeBigEndian。這種用接口擴展內(nèi)建類型的寫法本身就值得學(xué)。二、大端序BigEndianOrder2.1 接口形狀public interface BigEndianOrderT { func writeBigEndian(buffer: ArrayByte): Int64 static func readBigEndian(buffer: ArrayByte): T }writeBigEndian把當(dāng)前值按大端序?qū)戇M(jìn)buffer返回實際寫入的字節(jié)數(shù)Int64 寫 8 字節(jié)Int16 寫 2 字節(jié)。readBigEndian從buffer開頭按大端序讀出一個 T 值。已實現(xiàn)的類型Bool、Int8/16/32/64、UInt8/16/32/64、Float16/32/64。import std.binary.*后直接可用。2.2 寫值 → 字節(jié)數(shù)組import std.binary.* main(): Int64 { let buf ArrayByte(8, repeat: 0) // 分配 8 字節(jié)緩沖區(qū)桌面 SDK 用 repeat: let n 0x1234i16.writeBigEndian(buf) println(寫入字節(jié)數(shù): ${n}) println(字節(jié)內(nèi)容: ${buf[..n]}) 0 }寫入字節(jié)數(shù): 2 字節(jié)內(nèi)容: [18, 52]0x1234 十進(jìn)制 4660拆成大端序兩個字節(jié)就是0x12(18)、0x34(52)——高位在前和你腦子里讀十六進(jìn)制的順序一致??庸俜皆诰€文檔鴻蒙 V5 版里數(shù)組分配寫的是ArrayByte(8, item: 0)桌面版 SDK 1.2.0 不支持item:命名參數(shù)實測報錯unknown named argument prefix item:。桌面版統(tǒng)一用repeat:ArrayByte(8, repeat: 0)。本文所有代碼均用桌面版寫法。2.3 讀字節(jié)數(shù)組 → 值let buf2: ArrayByte [0x12, 0x34] let v Int16.readBigEndian(buf2) println(讀回的值: ${v})讀回的值: 4660readBigEndian是靜態(tài)方法掛在類型上調(diào)用Int16.readBigEndian(...)不是在實例上。寫出來的[18, 52]原樣讀回4660往返一致。2.4 多字節(jié)類型Int64 與 Float64Int64 占 8 字節(jié)Float64 也占 8 字節(jié)看大端序怎么排let val 0x0102030405060708i64 let beBuf ArrayByte(8, repeat: 0) val.writeBigEndian(beBuf) println(Int64 BE: ${beBuf}) let f 12.5f64 let fBuf ArrayByte(8, repeat: 0) f.writeBigEndian(fBuf) println(Float64 BE: ${fBuf}) println(Float64 讀回: ${Float64.readBigEndian(fBuf)})Int64 BE: [1, 2, 3, 4, 5, 6, 7, 8] Float64 BE: [64, 41, 0, 0, 0, 0, 0, 0] Float64 讀回: 12.500000Int64 的字節(jié)01 02 03 04 05 06 07 08一眼就能看出高位在前。Float64 的64 41 00 00 00 00 00 00是 IEEE 754 雙精度浮點的大端編碼——人腦算不出來沒關(guān)系只要讀寫用同一種字節(jié)序就能無損還原這就是接口的價值。2.5 緩沖區(qū)不夠會怎樣Int32需要 4 字節(jié)只給 2 字節(jié)的緩沖區(qū)讀寫都會拋異常實測原文let small: ArrayByte [0x12, 0x34] Int32.readBigEndian(small)Exception in thread main java.lang.IllegalArgumentException: Buffer size(2) is too small.寫也是一樣的報錯Buffer size(2) is too small.。所以分配緩沖區(qū)時大小要 ≥ 目標(biāo)類型的字節(jié)數(shù)Int32→4Int64→8Float32→4……。嫌算麻煩就直接開 8 字節(jié)用buf[..n]切出實際寫入的部分。三、小端序LittleEndianOrder3.1 接口形狀和大端序一模一樣只是方法名換了public interface LittleEndianOrderT { func writeLittleEndian(buffer: ArrayByte): Int64 static func readLittleEndian(buffer: ArrayByte): T }實現(xiàn)的類型也完全相同Bool、Int8/16/32/64、UInt8/16/32/64、Float16/32/64。3.2 小端序?qū)懭氲臀辉谇發(fā)et buf ArrayByte(8, repeat: 0) let n 0x12345678i32.writeLittleEndian(buf) println(Int32 LE 寫入字節(jié): ${buf[..n]})Int32 LE 寫入字節(jié): [120, 86, 52, 18]0x12345678的 4 個字節(jié)按小端序排列0x78(120)、0x56(86)、0x34(52)、0x12(18)——低位在前正好和大端序反過來。讀回來驗證let buf2: ArrayByte [0x78, 0x56, 0x34, 0x12] let v Int32.readLittleEndian(buf2) println(讀回: ${v})讀回: 305419896305419896就是0x12345678。讀寫配對順序一致值就對。3.3 大端 vs 小端同一張圖把同一個Int64值分別用大端、小端寫出來對比一目了然let val 0x0102030405060708i64 let beBuf ArrayByte(8, repeat: 0) val.writeBigEndian(beBuf) let leBuf ArrayByte(8, repeat: 0) val.writeLittleEndian(leBuf) println(大端: ${beBuf}) println(小端: ${leBuf})大端: [1, 2, 3, 4, 5, 6, 7, 8] 小端: [8, 7, 6, 5, 4, 3, 2, 1]同樣 8 個字節(jié)順序完全相反。這就是字節(jié)序這三個字的全部含義。四、翻轉(zhuǎn)字節(jié)序SwapEndianOrder4.1 接口與用途public interface SwapEndianOrderT { func swapBytes(): T }swapBytes()返回一個字節(jié)順序完全翻轉(zhuǎn)的新值。實現(xiàn)的類型Int8/16/32/64、UInt8/16/32/64浮點和 Bool 沒有——翻轉(zhuǎn)浮點字節(jié)沒有意義。它的典型用途從文件里讀出來后才發(fā)現(xiàn)字節(jié)序不對就地翻轉(zhuǎn)一下。比如某格式文檔寫的是大端序你誤用了小端序的readLittleEndian得到一個亂碼數(shù)值swapBytes()一下就救回來了——等價于換一種讀法重算。4.2 實測let val 0x0102030405060708i64 let swapped val.swapBytes() let swapBuf ArrayByte(8, repeat: 0) swapped.writeBigEndian(swapBuf) println(翻轉(zhuǎn)后大端字節(jié): ${swapBuf})翻轉(zhuǎn)后大端字節(jié): [8, 7, 6, 5, 4, 3, 2, 1]原值大端是[1,2,3,4,5,6,7,8]swapBytes()后再按大端寫出就是[8,7,6,5,4,3,2,1]——正好等于原值的小端寫法。換句話說val.swapBytes()的大端表示 val的小端表示。UInt 類型也一樣let u 0x1234u16 println(0x1234 翻轉(zhuǎn)后: ${u.swapBytes()})0x1234 翻轉(zhuǎn)后: 133300x1234翻轉(zhuǎn)字節(jié)得到0x3412 13330。五、實戰(zhàn)解析 BMP 圖片文件頭光看接口太干來個真實格式。BMP 是 Windows 原生圖片格式文件頭全部用小端序因為 Windows 跑在 x86 上。我們用LittleEndianOrder把一個 BMP 文件頭拆開看看。5.1 BMP 文件頭結(jié)構(gòu)BMP 文件頭14 字節(jié) DIB 頭40 字節(jié)BITMAPINFOHEADER的關(guān)鍵字段偏移長度字段類型說明02signature字符“BM” 魔數(shù)24fileSizeUInt32 LE文件總字節(jié)數(shù)64reserved—保留通常 0104dataOffsetUInt32 LE像素數(shù)據(jù)起始偏移144dibHeaderSizeUInt32 LEDIB 頭大小40184widthInt32 LE圖像寬度像素224heightInt32 LE圖像高度像素262planesUInt16 LE顏色平面數(shù)1282bitsPerPixelUInt16 LE每像素位數(shù)24真彩色5.2 構(gòu)造 解析為了不依賴外部圖片文件我們在代碼里手動構(gòu)造一個 54 字節(jié)的 BMP 頭再用LittleEndianOrder逐字段讀出來——寫和讀都練到import std.binary.* func hexOf(bytes: ArrayByte): String { let hex [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f] var s for (b in bytes) { s s hex[Int64(b 4)] hex[Int64(b 0xF)] } return s } main(): Int64 { // 構(gòu)造 BMP 文件頭54 字節(jié) let bmp ArrayByte(54, repeat: 0) bmp[0] 0x42 // B bmp[1] 0x4D // M 54u32.writeLittleEndian(bmp[2..6]) // fileSize 54u32.writeLittleEndian(bmp[10..14]) // dataOffset 40u32.writeLittleEndian(bmp[14..18]) // dibHeaderSize 800i32.writeLittleEndian(bmp[18..22]) // width 600i32.writeLittleEndian(bmp[22..26]) // height 1u16.writeLittleEndian(bmp[26..28]) // planes 24u16.writeLittleEndian(bmp[28..30]) // bitsPerPixel println(原始頭 (前 30 字節(jié)): ${hexOf(bmp[..30])}) // 解析 let sig String.fromUtf8(bmp[0..2]) let fileSize UInt32.readLittleEndian(bmp[2..6]) let dataOffset UInt32.readLittleEndian(bmp[10..14]) let dibSize UInt32.readLittleEndian(bmp[14..18]) let width Int32.readLittleEndian(bmp[18..22]) let height Int32.readLittleEndian(bmp[22..26]) let bpp UInt16.readLittleEndian(bmp[28..30]) println(簽名: ${sig}) println(文件大小: ${fileSize} 字節(jié)) println(像素數(shù)據(jù)偏移: ${dataOffset}) println(DIB 頭大小: ${dibSize}) println(寬度: ${width} px) println(高度: ${height} px) println(色深: ${bpp} bpp) 0 }原始頭 (前 30 字節(jié)): 424d36000000000000003600000028000000200300005802000001001800 簽名: BM 文件大小: 54 字節(jié) 像素數(shù)據(jù)偏移: 54 DIB 頭大小: 40 寬度: 800 px 高度: 600 px 色深: 24 bpp幾個要點切片直接當(dāng)緩沖區(qū)bmp[2..6]是從第 2 字節(jié)開始、長度 4 的切片writeLittleEndian/readLittleEndian直接吃切片不用額外拷數(shù)組。這是處理固定偏移格式最順手的寫法。魔數(shù)用字符串讀前兩字節(jié)0x42 0x4D是 ASCII 的BM用String.fromUtf8還原成人話。寬度 800 的小端字節(jié)是20 03 00 000x00000320 800小端序低位在前 →20 03 00 00??摧敵龅谌?..2003000058020000...20030000是寬 80058020000是高 6000x00000258 600。真實讀.bmp文件時把構(gòu)造那段換成File.readFrom(path)讀到bmp數(shù)組即可解析代碼完全不變。六、常用 API 速查API說明val.writeBigEndian(buf)把val按大端序?qū)懭隻uf返回寫入字節(jié)數(shù)Type.readBigEndian(buf)從buf按大端序讀出一個Type值靜態(tài)方法val.writeLittleEndian(buf)按小端序?qū)懭敕祷貙懭胱止?jié)數(shù)Type.readLittleEndian(buf)按小端序讀出靜態(tài)方法val.swapBytes()返回字節(jié)順序翻轉(zhuǎn)后的新值僅整數(shù)類型ArrayByte(N, repeat: 0)分配 N 字節(jié)零填充緩沖區(qū)桌面 SDK 用repeat:一張表分清三個接口接口方向?qū)崿F(xiàn)的類型BigEndianOrderT高位在前Bool, Int8/16/32/64, UInt8/16/32/64, Float16/32/64LittleEndianOrderT低位在前同上SwapEndianOrderT翻轉(zhuǎn)Int8/16/32/64, UInt8/16/32/64無浮點、無 Bool七、常見問題 FAQQ1writeBigEndian返回的Int64是干嘛的為什么不直接返回字節(jié)數(shù)組返回的是實際寫入的字節(jié)數(shù)。接口設(shè)計成調(diào)用方管緩沖區(qū)——你自己開數(shù)組、自己決定大小方法只負(fù)責(zé)往里寫并告訴你寫了幾個字節(jié)。好處是可以復(fù)用同一個大緩沖區(qū)連續(xù)寫多個字段像 BMP 頭那樣不用反復(fù)分配。要取實際寫入的部分用buf[..n]切片。Q2readBigEndian為什么是靜態(tài)方法Int32.readBigEndian(...)而不是實例方法因為讀之前你手里還沒有Int32實例——讀的目的就是造出一個實例。所以它必須掛在類型上作為靜態(tài)方法。寫則相反你已經(jīng)有一個值了在值上調(diào)writeBigEndian。一讀一寫一個靜態(tài)一個實例對稱得很。Q3為什么swapBytes()沒有 Float64/Float32 版本翻轉(zhuǎn)浮點數(shù)的字節(jié)在數(shù)學(xué)上沒有意義——IEEE 754 浮點的符號位、指數(shù)、尾數(shù)是按位劃分的翻轉(zhuǎn)字節(jié)會把它們?nèi)騺y得到的是一個幾乎隨機的浮點數(shù)沒有應(yīng)用場景。所以SwapEndianOrder只給整數(shù)類型實現(xiàn)。Q4緩沖區(qū)太小的報錯文案是什么怎么避免實測報錯Buffer size(N) is too small.N 是你給的實際大小。避免方法分配緩沖區(qū)時大小 ≥ 類型字節(jié)數(shù)Int8/UInt8→1Int16/UInt16→2Int32/UInt32/Float32→4Int64/UInt64/Float64→8。圖省事就統(tǒng)一開 8 字節(jié)。Q5官方文檔里ArrayByte(8, item: 0)為什么我這里編譯不過官方在線文檔是鴻蒙OpenHarmony版 SDK 的寫法桌面版 SDK 1.2.0 的數(shù)組構(gòu)造命名參數(shù)是repeat:不是item:。寫ArrayByte(8, item: 0)實測報unknown named argument prefix item:。桌面版統(tǒng)一用ArrayByte(8, repeat: 0)。Q6std.binary有沒有 Base64、十六進(jìn)制編碼之類的功能1.2.0 的std.binary只做字節(jié)序轉(zhuǎn)換大端/小端/翻轉(zhuǎn)沒有 Base64、Hex 編碼。第 5 課的toHex是我們手寫的十六進(jìn)制編碼目前需要自己實現(xiàn)或找第三方庫。Base64 同理。八、課后練習(xí)規(guī)矩不變前 4 題必做第 5 題選做。每題只考本課 12 個知識點題面給全函數(shù)簽名和 API 提示先看懂題再動筆。第 1 題必做大端讀整數(shù)用Int32.readBigEndian從字節(jié)數(shù)組[0x00, 0x00, 0x01, 0xF4]讀出一個 Int32 值并打印。提示直接Int32.readBigEndian(buf)0x000001F4 500。import std.binary.* main(): Int64 { let buf: ArrayByte [0x00, 0x00, 0x01, 0xF4] // 補全讀出并打印 0 }期望輸出500第 2 題必做小端寫 Int16把0x0100i16按小端序?qū)懭?2 字節(jié)緩沖區(qū)打印寫入的字節(jié)內(nèi)容。提示writeLittleEndian返回寫入字節(jié)數(shù)0x0100小端序是00 01低位0x00在前。import std.binary.* main(): Int64 { let buf ArrayByte(2, repeat: 0) // 補全寫入并打印 buf 內(nèi)容 0 }期望輸出[0, 1]第 3 題必做翻轉(zhuǎn)字節(jié)序?qū)?x12345678u32調(diào)用swapBytes()打印結(jié)果的十進(jìn)制值。提示0x12345678翻轉(zhuǎn)后是0x78563412直接println(val.swapBytes())。import std.binary.* main(): Int64 { let v 0x12345678u32 // 補全打印翻轉(zhuǎn)后的值 0 }期望輸出2018915346第 4 題必做大端讀 Float32字節(jié)數(shù)組[0x41, 0x48, 0x00, 0x00]是12.5f32的大端編碼。用Float32.readBigEndian讀出來并打印。提示Float32 也是BigEndianOrder的實現(xiàn)類型調(diào)用方式和整數(shù)一樣。import std.binary.* main(): Int64 { let buf: ArrayByte [0x41, 0x48, 0x00, 0x00] // 補全讀出 Float32 并打印 0 }期望輸出12.500000第 5 題選做小端讀 BMP 寬度給定一個 22 字節(jié)的 BMP 頭片段用小端序讀出其中的寬度字段偏移 18長度 4Int32。提示寬度在偏移 18 處用切片buf[18..22]讀Int32.readLittleEndian。import std.binary.* main(): Int64 { // 一個 BMP 頭片段寬 10240x00000400高 7680x00000300 let buf: ArrayByte [ 0x42, 0x4D, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00 ] // 補全讀出寬度并打印 0 }期望輸出1024下節(jié)預(yù)告字節(jié)序解決了數(shù)值怎么變成字節(jié)的問題但真實世界的數(shù)據(jù)格式遠(yuǎn)不止整數(shù)——數(shù)據(jù)庫里一行行的結(jié)構(gòu)化記錄怎么存、怎么查、怎么改下一課進(jìn)入數(shù)據(jù)庫篇從std.database.sql開始把數(shù)據(jù)持久化從文件升級到真正的數(shù)據(jù)庫。系列說明本系列基于 Windows 平臺 CIDE 倉頡 SDK1.2.0編寫所有代碼均已實際編譯運行通過。如遇 SDK 版本差異導(dǎo)致的細(xì)節(jié)出入以你本地版本為準(zhǔn)歡迎評論區(qū)交流。 遇到問題掃碼聯(lián)系作者跟著課程練習(xí)時如果在 SDK 安裝、環(huán)境變量配置、編譯報錯或調(diào)試上卡住歡迎掃碼加作者企業(yè)微信直接咨詢請備注倉頡課程離線環(huán)境下圖片可能加載不出來也可以在 CIDE 菜單Help ? 聯(lián)系作者 / Contact中查看同一張二維碼應(yīng)用內(nèi)置兜底圖無需聯(lián)網(wǎng)。 工具下載本系列全程使用的倉頡 IDE ——CIDE免費開源、社區(qū)版GitCode 倉庫 / 安裝包下載https://gitcode.com/wp_upala/cide打開頁面后進(jìn)入發(fā)行版Releases兩種包任選其一安裝版下載CIDE-版本-x64-Setup.exe雙擊安裝適合日常長期使用免安裝版Portable下載CIDE-版本-x64-Portable.zip解壓到任意目錄即用不寫注冊表、不留安裝痕跡拷到 U 盤也能在別的電腦直接運行包內(nèi)附《使用說明.txt》。適合先試用、或在受限電腦上學(xué)習(xí)本系列課程。倉頡 SDK 請前往倉頡編程語言官網(wǎng)下載https://cangjie-lang.cn