為什麼要進行 pack 操作和 unpack 操作
不同類型的語言支援不同的資料類型,比如 Go 有 int32、int64、uint32、uint64 等不同的資料類型,這些類型占用的位元組大小不同,而同樣的資料類型在其他語言中比如 Python 中,又是完全不同的處理方式,比如 Python 的 int 既可以是有符号的,也可以是無符号的,這樣一來 Python 和 Go 在處理同樣大小的數字時存儲方式就有了差異。
除了語言之間的差别,不同的計算機硬體存儲資料的方式也有很大的差異,有的 32 bit 是一個 word,有的 64 bit 是一個 word,而且他們存儲資料的方式或多或少都有些差異。
當這些不同的語言以及不同的機器之間進行資料交換,比如通過 network 進行資料交換,他們需要對彼此發送和接受的位元組流資料進行 pack 和 unpack 操作,以便資料可以正确的解析和存儲。
也就是說 pack 和 unpack 是用來在計算機之間以及不同語言之間進行網絡交流時的對資料資料格式翻譯和轉換操作的。
計算機如何存儲整型
可以把計算機的記憶體看做是一個很大的位元組數組,一個位元組包含 8 bit 資訊可以表示 0-255 的無符号整型,以及 -128—127 的有符号整型。當存儲一個大于 8 bit 的值到記憶體時,這個值常常會被切分成多個 8 bit 的 segment 存儲在一個連續的記憶體空間,一個 segment 一個位元組。有些處理器會把高位存儲在記憶體這個位元組數組的頭部,把低位存儲在尾部,這種處理方式叫 big-endian,有些處理器則相反,低位存儲在頭部,高位存儲在尾部,稱之為 little-endian。
假設一個寄存器想要存儲 0x12345678 到記憶體中,big-endian 和 little-endian 分别存儲到記憶體 1000 的位址表示如下
address | big-endian | little-endian |
1000 | 0x12 | 0x78 |
1001 | 0x34 | 0x56 |
1002 | 0x56 | 0x34 |
1003 | 0x78 | 0x12 |
Python 中位元組在機器中存儲的位元組順序用字母表示如下:
Character | Byte order | Size | Alignment |
| native | native | native |
| native | standard | none |
| little-endian | standard | none |
| big-endian | standard | none |
| network (= big-endian) | standard | none |
計算機如何存儲 character
和存儲 number 的方式類似,character 通過一定的編碼格式進行編碼比如 unicode,然後以位元組的方式存儲。
Python 中的 struct 子產品
Python 提供了三個與 pack 和 unpack 相關的函數
123 | struct.pack(fmt, v1, v2, ...)struct.unpack(fmt, string)struct.calcsize(fmt) |
第一個函數
pack
負責将不同的變量打包在一起,成為一個位元組字元串。
第二個函數
unpack
将位元組字元串解包成為變量。
第三個函數
calsize
計算按照格式 fmt 打包的結果有多少個位元組。
pack 操作
Pack 操作必須接受一個 template string 以及需要進行 pack 一組資料,這就意味着 pack 處理操作定長的資料
123456 | import structa = struct.pack("2I3sI", 12, 34, "abc", 56)b = struct.unpack("2I3sI", a)print b |
上面的代碼将兩個整數 12 和 34,一個字元串 “abc” 和一個整數 56 一起打包成為一個位元組字元流,然後再解包。其中打包格式中明确指出了打包的長度:
"2I"
表明起始是兩個
unsigned int
,
"3s"
表明長度為 4 的字元串,最後一個
"I"
表示最後緊跟一個
unsigned int
,是以上面的列印 b 輸出結果是:(12, 34, ‘abc’, 56),完整的 Python pack 操作支援的資料類型見下表。
Format | C Type | Python type | Standard size | Notes |
| pad byte | no value | ||
| | string of length 1 | 1 | |
| | integer | 1 | (3) |
| | integer | 1 | (3) |
| | bool | 1 | (1) |
| | integer | 2 | (3) |
| | integer | 2 | (3) |
| | integer | 4 | (3) |
| | integer | 4 | (3) |
| | integer | 4 | (3) |
| | integer | 4 | (3) |
| | integer | 8 | (2), (3) |
| | integer | 8 | (2), (3) |
| | float | 4 | (4) |
| | float | 8 | (4) |
| | string | ||
| | string | ||
| | integer | (5), (3) |
計算位元組大小
可以利用 calcsize 來計算模式 “2I3sI” 占用的位元組數
1 | print struct.calcsize("2I3sI") # 16 |
可以看到上面的三個整型加一個 3 字元的字元串一共占用了 16 個位元組。為什麼會是 16 個位元組呢?不應該是 15 個位元組嗎?1 個 int 4 位元組,3 個字元 3 位元組。但是在
struct
的打包過程中,根據特定類型的要求,必須進行位元組對齊(關于位元組對齊詳見 https://en.wikipedia.org/wiki/Data_structure_alignment) 。由于預設
unsigned int
型占用四個位元組,是以要在字元串的位置進行4位元組對齊,是以即使是 3 個字元的字元串也要占用 4 個位元組。
再看一下不需要位元組對齊的模式
1 | print struct.calcsize("2Is") # 9 |
由于單字元出現在兩個整型之後,不需要進行位元組對齊,是以輸出結果是 9。
unpack 操作
對于
unpack
而言,隻要
fmt
對應的位元組數和位元組字元串
string
的位元組數一緻,就可以成功的進行解析,否則
unpack
函數将抛出異常。例如我們也可以使用如下的
fmt
解析出
a
:
123 | c = struct.unpack("2I2sI", a)print struct.calcsize("2I2sI")print c # 16 (12, 34, 'ab', 56) |
不定長資料 pack
如果打包的資料長度未知該如何打包,這樣的打包在網絡傳輸中非常常見。處理這種不定長的内容的主要思路是把長度和内容一起打包,解包時首先解析内容的長度,然後再讀取正文。
打包變長字元串
對于變長字元在處理的時候可以把字元的長度當成資料的内容一起打包。
12 | s = bytes(s)data = struct.pack("I%ds" % (len(s),), len(s), s) |
上面代碼把字元 s 的長度打包成内容,可以在進行内容讀取的時候直接讀取。
解包變長字元串
參考資料
- http://www.perlmonks.org/?node_id=224666
- https://docs.python.org/2/library/struct.html
- http://kaiyuan.me/2015/12/25/python-struct/