將多個分散文件打包為單一「包文件」,便於傳輸、存儲、版本管理。本文設計一個高效的自定義二進制格式,支持隨機讀取任意內嵌文件,無需全量載入內存,附帶完整管理器類實現。
[Header: "PackFile" (String)]
[FileCount: Int32]
[FileLengths: Int64 × FileCount] // 每個文件長度,按添加順序
[FileNames: String × FileCount] // 文件名(不含路徑),BinaryWriter.Write(string) 自帶長度前綴
[FileContents: Byte[] × FileCount] // 文件實體數據,順序寫入
設計亮點:
public class PackFileManager {
private List _sourcePaths = new List();
private PackFile _currentPack;
public void AddSourceFile(string path) {
if (File.Exists(path)) _sourcePaths.Add(path);
}
public void Build(string outputPath) {
using (var fs = new FileStream(outputPath, FileMode.Create, FileAccess.Write))
using (var bw = new BinaryWriter(fs, Encoding.UTF8, leaveOpen: false)) {
// 1. Magic Number
bw.Write("PackFile");
// 2. File Count
bw.Write(_sourcePaths.Count);
// 3. File Lengths
foreach (var f in _sourcePaths) {
bw.Write(new FileInfo(f).Length);
}
// 4. File Names
foreach (var f in _sourcePaths) {
bw.Write(Path.GetFileName(f));
}
// 5. File Contents
foreach (var f in _sourcePaths) {
byte[] bytes = File.ReadAllBytes(f); // 小文件可一次讀取;大文件建議分塊流式寫入
bw.Write(bytes);
bw.Flush();
}
}
_currentPack = new PackFile(outputPath); // 立即加載驗證
}
public PackFile CurrentPackFile => _currentPack;
}
public class PackFile : IDisposable {
private FileStream _stream;
private BinaryReader _reader;
private List _fileLengths = new List();
private List _fileNames = new List();
private long _contentStartPos;
private int _fileCount;
internal PackFile(string path) {
_stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
_reader = new BinaryReader(_stream, Encoding.UTF8, leaveOpen: false);
LoadIndex();
}
internal PackFile(FileStream fs, BinaryReader br) { // 供 Manager 內部調用
_stream = fs; _reader = br; LoadIndex();
}
private void LoadIndex() {
if (_reader.ReadString() != "PackFile")
throw new InvalidDataException("非法包文件格式");
_fileCount = _reader.ReadInt32();
for (int i = 0; i < _fileCount; i++) _fileLengths.Add(_reader.ReadInt64());
for (int i = 0; i < _fileCount; i++) _fileNames.Add(_reader.ReadString());
_contentStartPos = _stream.Position; // 內容區起始偏移
}
public int FileCount => _fileCount;
public IReadOnlyList FileNames => _fileNames;
/// 按索引讀取整個文件到內存(適合小文件)
public byte[] GetBytes(int index) {
if (index < 0 || index >= _fileCount) throw new ArgumentOutOfRangeException(nameof(index));
long pos = _contentStartPos;
for (int i = 0; i < index; i++) pos += _fileLengths[i];
_stream.Position = pos;
return _reader.ReadBytes((int)_fileLengths[index]);
}
/// 按文件名讀取
public byte[] GetBytes(string fileName) {
int idx = _fileNames.FindIndex(n => n.Equals(fileName, StringComparison.OrdinalIgnoreCase));
return idx >= 0 ? GetBytes(idx) : null;
}
/// 流式讀取大文件(不載入全量內存)
public Stream OpenRead(int index) {
var bytes = GetBytes(index); // 簡化版;生產環境應返回自定義 Stream 實現真正隨機讀取
return new MemoryStream(bytes, writable: false);
}
public void Dispose() {
_reader?.Dispose();
_stream?.Dispose();
}
}
// 構建包
var mgr = new PackFileManager();
mgr.AddSourceFile(@"C:\Docs\report.pdf");
mgr.AddSourceFile(@"C:\Images\logo.png");
mgr.AddSourceFile(@"C:\Data\config.json");
mgr.Build(@"C:\Packages\release_v1.pack");
// 讀取包
using (var pack = new PackFile(@"C:\Packages\release_v1.pack")) {
Console.WriteLine($"共 {pack.FileCount} 個文件");
foreach (var name in pack.FileNames) {
Console.WriteLine($"- {name} ({pack.GetBytes(name).Length} bytes)");
}
// 提取單個文件
File.WriteAllBytes(@"C:\Temp\logo.png", pack.GetBytes("logo.png"));
}
PackFileStream : Stream 內部持有 FileStream + 偏移/長度,支持 Seek/Read/Position,避免 GetBytes 全量載入大文件。File.ReadAllBytes 可接受;超大文件(>100MB)必須分塊流式寫入/讀取。OpenRead 直接 _stream.Seek(offset, SeekOrigin.Begin)。FileShare.Read 允許進程內多線程同時讀取同一包文件不同位置。這套輕量級包文件格式無第三方依賴、代碼可控、擴展性強,適合遊戲資源包、離線數據分發、插件打包等場景。