首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> ASP.NET 基礎 >> 關於ASP.NET

.NET 實現多文件合並為單一包文件:自定義二進制格式設計與高效讀寫

     編輯:關於ASP.NET

將多個分散文件打包為單一「包文件」,便於傳輸、存儲、版本管理。本文設計一個高效的自定義二進制格式,支持隨機讀取任意內嵌文件,無需全量載入內存,附帶完整管理器類實現。

包文件格式規範

[Header: "PackFile" (String)]
[FileCount: Int32]
[FileLengths: Int64 × FileCount]       // 每個文件長度,按添加順序
[FileNames: String × FileCount]        // 文件名(不含路徑),BinaryWriter.Write(string) 自帶長度前綴
[FileContents: Byte[] × FileCount]     // 文件實體數據,順序寫入

設計亮點:

  • 固定長度頭部,便於快速定位
  • 長度數組分離,讀取時可先載入索引,按需 Seek 讀取內容
  • 文件名存儲短名,路徑信息由調用方維護
  • 無壓縮、無加密,專注結構清晰,易於擴展

核心類:PackFileManager(構建端)

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;
}

核心類:PackFile(隨機讀取端)

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"));
}

擴展方向

  • 壓縮:寫入內容前對每文件或整體內容區 GZip/Deflate,頭部增加壓縮標誌位。
  • 目錄結構:文件名存相對路徑,支持遞歸目錄,類似 ZIP 非固態壓縮。
  • 校驗和:每文件追加 CRC32/MD5,讀取時驗證完整性。
  • 加密:AES-GCM 加密內容區,密鑰派生結合文件名/長度。
  • 真隨機讀取 Stream:實現 PackFileStream : Stream 內部持有 FileStream + 偏移/長度,支持 Seek/Read/Position,避免 GetBytes 全量載入大文件。

性能提示

  • 大量小文件:File.ReadAllBytes 可接受;超大文件(>100MB)必須分塊流式寫入/讀取。
  • 頻繁隨機讀取:將索引(長度、名稱、起始偏移)緩存到內存字典,OpenRead 直接 _stream.Seek(offset, SeekOrigin.Begin)。
  • 併發讀取:FileShare.Read 允許進程內多線程同時讀取同一包文件不同位置。

這套輕量級包文件格式無第三方依賴、代碼可控、擴展性強,適合遊戲資源包、離線數據分發、插件打包等場景。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved