NOTES
本文來源:Designing File Formats
翻譯由 本人(赤石俊哉) 整理,若您是原做者並認爲此文涉及版權侵犯,我會配合刪除。html
如今有不少不少種文件,它們又有着不少不少的文件格式。從簡單的 ASCII 文本文檔到複雜的數據庫,下面是幾個文件結構中必要的幾個元素,設計者們每每會忽視掉其中一部分。數據庫
一個好的文件格式應該至少擁有下面的幾個元素:小程序
這也一樣適用於一些歷來不會實際存儲在一個文件中的數據,好比經過網絡傳送到移動設備的數據。緩存
這個叫作 Magic 字符的歷史已經好久遠了,它一般是一個 2 ~ 4 字符,可能更多或者更少,用來惟一地標識一個二進制文件格式。應該儘可能避免和天然語言相近的值,若是文件可能與文本文檔混合使用,那使用一個純 ASCII 字符就是一個很差的選擇。隨着存儲容量的變大,短 Magic 正在慢慢地被長一些的字符串所代替。網絡
身份識別字符在一些非強類型系統中(好比 UNIX )使用,是頗有用的。在 Macintosh HFS 文件系統中,是很難將文件和它的建立者類型分開的, 可是在 Windows 下,你只須要重命名它就好了。函數
在全部的系統中,他們都是有用的:能夠確保所讀取的文件是你所指望的文件內容。假如一個文件的類型在文件名中缺失,在網絡傳輸中,可能你就要話大量的時間去猜想這個文件的內容是什麼。可能你又要說了,我這做爲系統「內部使用」就沒有必要了吧。可是在開發中,若是做爲一個資源被使用的話,當你讀取錯誤的類型的文件時,這將會很快地讓你意識到而不是發生了一系列問題以後才意識到。性能
最帥氣(沒有之一)的識別字符串當屬 PNG 圖片文件格式中定義的,它看起來是這樣的:測試
(decimal) 137 80 78 71 13 10 26 10 (hexadecimal) 89 50 4e 47 0d 0a 1a 0a (ASCII C notation) \211 P N G \r \n \032 \n
第一個字符是一個非ASCII字符以防止來自文本文件的干擾。接下來的三個字符則是讓人類很顯眼的就認出這是一個 PNG 文件。\r\n
序列則是一個能夠進行一個快速測試,系統會將CRLF
轉換成CR
仍是LF
。而最後的\n
則測試系統會將LF
轉換成CR
仍是CRLF
。倒數第二個字符是一個CTRL + Z
,在有些系統裏面,這個做爲一個文本文件的結束標記。他不光會檢測不正確的文本處理,假如你在 MS-DOS 中打印這個文件,他也能把你從一堆垃圾亂碼中解救出來。線程
ASCII文件格式能夠從身份識別字符中獲益,由於讀取它們的程序能夠馬上知道它們是否在讀取正確的文件類型。當經過網絡傳入一個數據流的時候,能夠從身份識別字符中識別出傳入數據的性質。
你能夠用任何字符校驗來作,好比 32 位的 CRC,又或者是 128 位的 MD5 哈希。頭部驗證碼緊跟在 Magic 字符以後,用來計算它以後,數據內容(用 數據偏移 標識的位置)以前的內容的校驗值。它具備較高的可信度,讓你確保你如今所讀取的內容與當時寫入時的內容是一致的。
不少開發者將內部校驗碼視爲是沒必要要的,並且他們有信息地認爲 TCP/IP 網絡是至關可靠的,並且若是你都不能相信你硬盤裏面的數據了,你將會遇到不少問題。可是,仍然是有必要進行文件頭的校驗的。
好比說,存儲其實並無你想的那麼穩定。在早些天我收到了不少問題,在 CD 中記錄的音樂文件,文本文件和 JPEG 圖像都沒問題,可是存入的 ZIP 文件卻出問題了。而他們沒有意識到,只有 ZIP 文件檔是存在 CRC 校驗的。全部的數據都被損壞了,多是受損的 SCSI 或者 IDE 數據線所引發的。可是問題那麼少,只是由於在不少類型的文件中沒有體現出來。你可能不會意識到你的「文本」變成了「又本」。也許你不會意識到你的圖片上有些許奇怪的斑點。可是一個 32 位的 CRC 卻極少可能會被錯誤給欺騙過去。
還有一個常見的可能損壞文件的途徑是用一個 ASCII 模式的 FTP 傳輸,他會作行末字符轉換(好比,將 LF 轉換成 CRLF)。將字節混合或者修改以後可能會引發一些有趣的問題。若是有頭部的校驗碼,你能夠馬上知道頭部中是否有損壞。若是你能夠信任建立這個文件的程序代碼,那你能夠認爲這個頭部是可用的,或者說你能夠減小你代碼執行的檢查量。
有一種思想認爲,校驗碼應該放在頭部的最後位置,他老是能夠在 (OffsetToData - 4) 的位置上找到,並且可讓 CRC 覆蓋整個頭部,包含 Magic 字符。雖然測試一遍它是冗餘的。可是更重要的是這樣可讓他做爲網絡傳輸的頭部。你能夠計算 CRC 在你輸出了頭部字節以後,而不用將插入位置回移到前面去填寫它。一般來講,文件頭很小,不須要折中類型的處理,可是必定要記住。
這應該很明顯是一個有必要的字段。應用和文件格式隨時間不斷迭代,並且也很須要肯定一個文件的內容是否能夠被讀取。有兩種基本的方法,序列和主/次。
序列方法用一個簡單的值,一般用一個字節存儲。數字從0
或者1
開始,每次遞增。程序能夠認清和處理它的當前版本或者更早的版本,可是拒絕任何更新的版本。
主/次方法有兩個值,主版本和序列方式同樣,任何舊版本均可以被處理,可是更新版本不能夠。而次要版本對於每個主要版原本說,都是從0
開始。當有新字段被添加的時候,增加。舊版本中不用的字段始終保留,就算過期了不用了也要填充。這個方法比較適合保證向後兼容:較舊的應用程序能夠讀取較新的文件,由於就算被棄用了的字段在次版本中也是確定存在的。若是一個文件的次版本更低,程序是知道如何轉義它的。若是一個文件的次版本更高,則程序知道全部的字段都被明確地標識出來了,而新加的字段能夠直接跳過不讀。若是一個文件被從新設計整改告終構,更新主版本以防止舊版本的應用程序會讀取新版本的文件。
文件版本號不該該跟程序版本號進行綁定,也不用多此一舉地加額外信息,好比1.3.5d1
。一個或者兩個穩定增加的值就足夠了。
若是你不想顯式地顯示出版本號,好比在 PNG 文件中,就用了一種叫作塊(chunk)
的東西。若是數據格式須要被修改,則塊類型的名稱會被修改。總體的文件結構不會改變,版本數字被有效地內嵌在塊名中(或者在塊自己內)。這種方法只在你確信總體文件結構不會被改變時纔有用。
有些文件格式會包括一個最小程序版本的數字。這個聽起來有點像把馬車放在馬前面:應用程序最有能力決定是否處理給定的文件格式。文件格式版本應該存儲在程序中,而不是其餘地方。這個調整是爲了保證版本的向後兼容,由於它容許文件格式設計者告訴程序它們是否能夠讀取這個文件。最好仍是交給上面描述的主/次方法來處理。
這個字段的優點並不會馬上體現出來,直到哪天你在考慮向後兼容的時候。一箇舊的程序能夠讀一個新的數據文件,由於他知道如何去尋找他須要的字段,跳過不須要的字段。這個位移值告訴程序如何跳過不須要關心的頭部字段。
這個偏移值應當是基於文件最開始進行測量的。這對於真實文件(SEEK_SET)和內存緩衝區(將 char* 與位移相加)進行計算都是更簡便的。
你可能會試圖用這個數字做爲版本號。好比,Windows 中使用 sizeof() 來肯定多種類型的結構,好比位圖。請不要這樣作。這會讓你進入一種只能不斷地把你的文件變大的狀況。這對於 Windows API 結構來講很合理,由於他要保證多個版本的二進制兼容性。除非你須要始終向後兼容,不然這在設計上是個錯誤的決定。
這個屬性對於一個 ASCII 文件格式是一個不須要的,在一個邊長的頭部後面有點顯式地在說「數據從這裏開始」。
有些格式有一些複雜的結構。它們可能有不少個數據區域,每一個數據區域有一個偏移值,或者是有一個鏈表。這些字段是從文件頭部仍是放入那些區塊的頭部就取決於設計者你了。
有一個字段你很是值得你去考慮,就是長度字段。對於一個磁盤中的文件,數據的長度被隱式地被文件長度所表示。可是,若是內嵌長度將會讓你檢測到文件是否不經意之間被修改了(好比從網絡上下載的文件),對於經過網絡流傳輸的數據,這點也是很重要的。
使用 C 的結構直接進行讀寫是很是有吸引力的。經過名字訪問比較便利,並且可使代碼最小化。
Stop,從歷史上來講,這是一個很是糟糕的主意。由於結構的填充和組織隨着平臺、編譯器、甚至不一樣版本的相同編譯器不一樣會有不一樣,統一 C 的編譯器和明確使用progmas
可能能夠大部分地解決這個問題,可是要保持最好的兼容性,你仍是單獨寫入它們會保險一點。
使用標準的 libc 緩衝的 I/O 方法(fopen
、fread
、fwrite
、getc
、putc
)或者使用緩存的 C++ iostream
。可能會感受每一個字節調用getc
或者putc
會比較慢。可是請記住,這些宏在處理一個緩衝區的數據時是很小的。讀取數據到一個緩衝區而後你本身再轉義不會讓你收穫多少便利,反而會嚴重影響代碼的清晰度。
有一個常見的錯誤,必定要避免,當你寫 C/C++ 時,寫成:
unsigned short val = getc(infp) | getc(infp) << 8;
這裏遇到的麻煩是,不是全部的編譯器都用相同的順序處理參數,因此你不知道第一個getc()
會被先運行仍是放到第二位。(ANSI C 中對於這個有定義,可是你不能確保它的實現是遵守 ANSI C 的),把他們分開十分簡單,並且編譯以後也確定都是正確的順序。
在進行一系列的getc()
和putc()
以後,不要忘了檢查feof()
和ferror()
(以及其餘相似的函數)。
也就是 Little-Endian 和 Big-Endian,這裏最好的建議是使用和數據使用者大致相同的格式。若是你寫的文件將主要在80x86
機器上使用,使用低字節序。當讀取數據的時候,你須要選擇假設你運行在低字節序機器或者是寫可移植的代碼。在前者的狀況下,你能夠一次抓取 2 ~ 4 字節數據,並將其填充入一個整型。在後者的狀況,你須要一次讀取一個字節,而後進行適當的排序。若是你讀取任何東西都經過小函數(Read16LE
來讀取 16 位的低字節序值),你能夠封裝你的(非)能夠執行問題。
將一個 CRC 放入文件數據比放在文件頭部更值得去作。CRC 最好是放在一個數據塊的結尾。這讓你能夠在一個流中寫入數據,而不須要回滾位置填入 CRC,對於網絡傳輸數據來講尤爲重要。
參數跟文件頭部的校驗值差很少,可是文件頭部要比數據區小得多。
文件更改可能會發生在經過網絡傳輸數據或者磁盤產生壞道。你的程序能夠經過如下途徑檢測出這些修改:
(length - 1)
,而後嘗試讀取一個字節。fseek
到SEEK_END
)。若是你在將一個大文件直接在內存中映射到多個線程地址空間,並且不想花大量的時間去進行整個 CRC 的校驗,用一個文件位標識是一個很是值得考慮的方法。
若是你穿件了一個二進制文件格式,文檔記錄每個字節的意義。好比:
All values little-endian +00 2B Magic number (0xd5aa) +02 1B Version number (currently 1) +03 1B (pad byte) +04 4B CRC-32 +08 4B Length of data +0c 2B Offset to start of data +xx [data]
ASCII 文件格式能夠自行記錄,若是你在文檔中定義了註釋。建立一個默認的使用大量註釋的配置文件,而後在你的代碼樹中保存下來。