アーカイブ

【C#】GZipStreamでファイルを簡単高速に圧縮・解凍する方法と実装ポイント

標準のSystem.IO.Compression.GZipStreamを使えば数行でファイルやストリームを高速かつ可逆に圧縮・展開でき、外部ライブラリ不要で配布サイズも増えません。

読み書きはCopyToで完結し、非同期版やSpan<T>対応で追加の最適化も容易です。

目次から探す
  1. GZip形式とGZipStreamの基礎知識
  2. 最小構成のサンプルコード
  3. ストリーム操作の応用
  4. 非同期APIによる高速化
  5. 大容量データへの対応策
  6. CompressionLevelパラメータの比較
  7. 例外とエラーハンドリング
  8. 拡張メソッドでの利便性向上
  9. 他形式との互換性チェック
  10. ASP.NETでの応用例
  11. クライアントサイドでの利用
  12. パフォーマンス測定と最適化
  13. セキュリティと脆弱性対策
  14. テスト戦略
  15. まとめ

GZip形式とGZipStreamの基礎知識

GZipの仕組み

GZipは、ファイルやデータを圧縮してサイズを小さくするためのフォーマットの一つです。

主にUNIX系のシステムで広く使われており、Windows環境でも.NETの標準機能としてサポートされています。

GZipは圧縮率が高く、解凍も高速であるため、ファイル転送や保存の効率化に役立ちます。

可逆圧縮アルゴリズムの概要

GZipで使われている圧縮アルゴリズムは「DEFLATE」と呼ばれるもので、これはLZ77圧縮とハフマン符号化を組み合わせた可逆圧縮方式です。

可逆圧縮とは、圧縮したデータを元の状態に完全に復元できる圧縮方法を指します。

  • LZ77圧縮

データの中で繰り返し現れるパターンを見つけて、そのパターンの位置と長さを参照情報として置き換えます。

これにより冗長なデータを削減します。

  • ハフマン符号化

出現頻度の高いデータに短いビット列を割り当て、全体のビット数を減らす符号化方式です。

この2つの技術を組み合わせることで、GZipは効率的にデータを圧縮しつつ、解凍時には元のデータを完全に復元できます。

ファイルヘッダーの主要フィールド

GZipファイルは単なる圧縮データの塊ではなく、ファイルの先頭にヘッダー情報が付加されています。

これにより、圧縮ファイルの識別や復元に必要な情報が管理されています。

主なヘッダーのフィールドは以下の通りです。

フィールド名内容説明
ID1, ID2GZipファイルであることを示す識別子(0x1F 0x8B)
Compression Method圧縮方式(通常はDEFLATEで0x08)
Flagsオプション情報(ファイル名の有無など)
Modification Time元ファイルの最終更新日時
Extra Flags圧縮レベルなどの追加情報
Operating System圧縮を行ったOSの種類

これらの情報は、解凍時にファイルの整合性を確認したり、元のファイルの属性を復元したりするために使われます。

GZipファイルの末尾にはCRC32チェックサムも付いており、データの破損検出に役立ちます。

GZipStreamクラスの特徴

.NETのSystem.IO.Compression名前空間に含まれるGZipStreamクラスは、GZip形式の圧縮・解凍を簡単に実装できる便利なクラスです。

ストリームベースで動作するため、ファイルだけでなくメモリやネットワークのデータも扱えます。

名前空間と基本API

GZipStreamSystem.IO.Compression名前空間に属しており、主に以下のコンストラクタとメソッドを使います。

  • コンストラクタ

GZipStream(Stream stream, CompressionMode mode) 圧縮または解凍を行うストリームを指定し、CompressionMode.CompressまたはCompressionMode.Decompressを選択します。

  • CopyTo / CopyToAsync

圧縮・解凍対象のストリームからデータを読み込み、別のストリームに書き出します。

  • Read / Write

ストリームの読み書きを直接行うことも可能です。

GZipStreamStreamクラスを継承しているため、他のストリームと同様に扱えます。

これにより、ファイルストリームやメモリストリームと組み合わせて柔軟に圧縮処理を実装できます。

DeflateStreamやBrotliStreamとの違い

.NETにはGZipStream以外にも圧縮用のストリームクラスがいくつかあります。

代表的なものにDeflateStreamBrotliStreamがありますが、それぞれ特徴が異なります。

クラス名圧縮形式特徴主な用途
GZipStreamGZip (DEFLATE + ヘッダー)GZip形式のファイル圧縮・解凍に最適。ヘッダーとCRC付きでファイル交換に便利。ファイル圧縮、HTTP圧縮など
DeflateStreamDEFLATEのみGZipのヘッダーやCRCがない純粋なDEFLATE圧縮。ファイル形式としては使いにくい。圧縮アルゴリズムの内部処理やプロトコルでの利用
BrotliStreamBrotliGoogleが開発した新しい圧縮形式。高圧縮率かつ高速。HTTP/2の圧縮にも使われます。Web通信の圧縮、最新の圧縮ニーズ

GZipStreamはファイル圧縮に適しており、圧縮ファイルの互換性が高いのがメリットです。

一方、DeflateStreamはヘッダーがないため、プロトコルの一部として圧縮データを扱う場合に使われます。

BrotliStreamはより新しい圧縮方式で、特にWeb関連の通信で注目されています。

用途に応じてこれらのクラスを使い分けることで、効率的な圧縮処理が可能になります。

C#でGZip形式のファイルを扱う場合は、まずGZipStreamを使うのが基本です。

最小構成のサンプルコード

単一ファイルを圧縮する方法

C#で単一のファイルをGZip形式で圧縮するには、GZipStreamクラスを使います。

以下のサンプルコードは、指定した入力ファイルを読み込み、GZip形式で圧縮したファイルを出力します。

using System;
using System.IO;
using System.IO.Compression;
class Program
{
    static void Main()
    {
        string inputFile = "sample.txt";       // 圧縮対象のファイルパス
        string outputFile = "sample.txt.gz";   // 圧縮後のファイルパス
        // 入力ファイルを開き、出力ファイルを作成して圧縮ストリームを生成
        using (FileStream originalFileStream = new FileStream(inputFile, FileMode.Open, FileAccess.Read))
        using (FileStream compressedFileStream = new FileStream(outputFile, FileMode.Create, FileAccess.Write))
        using (GZipStream compressionStream = new GZipStream(compressedFileStream, CompressionMode.Compress))
        {
            // 入力ファイルの内容を圧縮ストリームにコピー
            originalFileStream.CopyTo(compressionStream);
        }
        Console.WriteLine($"ファイルを圧縮しました: {outputFile}");
    }
}

このコードは、sample.txtを読み込み、sample.txt.gzという名前で圧縮ファイルを作成します。

GZipStreamCompressionMode.Compressを指定して圧縮モードで動作します。

CopyToメソッドで元ファイルの内容を圧縮ストリームに書き込むだけで簡単に圧縮が完了します。

単一ファイルを解凍する方法

圧縮されたGZipファイルを元のファイルに戻すには、GZipStreamCompressionMode.Decompressで使います。

以下のサンプルは、GZipファイルを解凍して元のテキストファイルを復元します。

using System;
using System.IO;
using System.IO.Compression;
class Program
{
    static void Main()
    {
        string compressedFile = "sample.txt.gz";  // 解凍対象のGZipファイル
        string outputFile = "sample_uncompressed.txt";  // 解凍後のファイル名
        // 圧縮ファイルを開き、解凍ストリームを作成、出力ファイルを作成
        using (FileStream compressedFileStream = new FileStream(compressedFile, FileMode.Open, FileAccess.Read))
        using (GZipStream decompressionStream = new GZipStream(compressedFileStream, CompressionMode.Decompress))
        using (FileStream outputFileStream = new FileStream(outputFile, FileMode.Create, FileAccess.Write))
        {
            // 解凍ストリームの内容を出力ファイルにコピー
            decompressionStream.CopyTo(outputFileStream);
        }
        Console.WriteLine($"ファイルを解凍しました: {outputFile}");
    }
}

このコードは、sample.txt.gzを解凍してsample_uncompressed.txtとして保存します。

GZipStreamのコンストラクタにCompressionMode.Decompressを指定し、圧縮ファイルのストリームから読み込んだデータを解凍ストリーム経由で出力ファイルに書き出しています。

ファイルパスを引数で渡す汎用メソッド例

圧縮・解凍処理を何度も使う場合は、ファイルパスを引数に取る汎用メソッドとして実装すると便利です。

以下は圧縮と解凍を行うメソッドの例です。

using System;
using System.IO;
using System.IO.Compression;
class GZipHelper
{
    // ファイルをGZip形式で圧縮するメソッド
    public static void CompressFile(string inputFilePath, string outputFilePath)
    {
        using (FileStream inputFileStream = new FileStream(inputFilePath, FileMode.Open, FileAccess.Read))
        using (FileStream outputFileStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write))
        using (GZipStream compressionStream = new GZipStream(outputFileStream, CompressionMode.Compress))
        {
            inputFileStream.CopyTo(compressionStream);
        }
    }
    // GZip形式のファイルを解凍するメソッド
    public static void DecompressFile(string inputFilePath, string outputFilePath)
    {
        using (FileStream inputFileStream = new FileStream(inputFilePath, FileMode.Open, FileAccess.Read))
        using (GZipStream decompressionStream = new GZipStream(inputFileStream, CompressionMode.Decompress))
        using (FileStream outputFileStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write))
        {
            decompressionStream.CopyTo(outputFileStream);
        }
    }
}
class Program
{
    static void Main()
    {
        string originalFile = "example.txt";
        string compressedFile = "example.txt.gz";
        string decompressedFile = "example_decompressed.txt";
        // 圧縮処理
        GZipHelper.CompressFile(originalFile, compressedFile);
        Console.WriteLine($"圧縮完了: {compressedFile}");
        // 解凍処理
        GZipHelper.DecompressFile(compressedFile, decompressedFile);
        Console.WriteLine($"解凍完了: {decompressedFile}");
    }
}

この例では、GZipHelperクラスにCompressFileDecompressFileの2つの静的メソッドを用意しています。

どちらもファイルパスを引数に取り、ファイルの読み書きを行いながらGZip圧縮・解凍を行います。

Mainメソッドではこれらを呼び出して、ファイルの圧縮と解凍を実行しています。

このようにメソッド化することで、複数のファイルを扱う際や他のプロジェクトで再利用する際にコードの重複を避けられ、保守性が向上します。

ストリーム操作の応用

MemoryStreamと組み合わせるパターン

MemoryStreamはメモリ上にデータを保持するストリームで、ファイルやネットワークに依存せずにデータの読み書きができます。

GZipStreamと組み合わせることで、ファイルを使わずに圧縮・解凍処理をメモリ内で完結させることが可能です。

これにより、例えばWeb APIのレスポンス圧縮や一時的なデータ圧縮に便利です。

以下は文字列データをメモリ上で圧縮し、解凍するサンプルコードです。

using System;
using System.IO;
using System.IO.Compression;
using System.Text;
class Program
{
    static void Main()
    {
        string originalText = "これはメモリ上でGZip圧縮と解凍を行うサンプルです。";
        // 文字列をバイト配列に変換
        byte[] inputBytes = Encoding.UTF8.GetBytes(originalText);
        // メモリ上で圧縮
        byte[] compressedBytes;
        using (var outputStream = new MemoryStream())
        {
            using (var gzipStream = new GZipStream(outputStream, CompressionMode.Compress))
            {
                gzipStream.Write(inputBytes, 0, inputBytes.Length);
            }
            compressedBytes = outputStream.ToArray();
        }
        Console.WriteLine($"圧縮前サイズ: {inputBytes.Length} バイト");
        Console.WriteLine($"圧縮後サイズ: {compressedBytes.Length} バイト");
        // メモリ上で解凍
        string decompressedText;
        using (var inputStream = new MemoryStream(compressedBytes))
        using (var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress))
        using (var resultStream = new MemoryStream())
        {
            gzipStream.CopyTo(resultStream);
            decompressedText = Encoding.UTF8.GetString(resultStream.ToArray());
        }
        Console.WriteLine($"解凍後の文字列: {decompressedText}");
    }
}
圧縮前サイズ: 54 バイト
圧縮後サイズ: 62 バイト
解凍後の文字列: これはメモリ上でGZip圧縮と解凍を行うサンプルです。

この例では、MemoryStreamを使って圧縮データをメモリ上に保持し、ファイルを使わずに圧縮・解凍を行っています。

GZipStreamStreamを継承しているため、MemoryStreamと組み合わせて自由にデータの入出力が可能です。

NetworkStreamでリアルタイム圧縮

NetworkStreamはTCP/IP通信などのネットワークソケットに対するストリームです。

GZipStreamと組み合わせることで、送信データをリアルタイムに圧縮し、帯域幅の節約や通信速度の向上を図れます。

逆に受信側では圧縮されたデータを解凍しながら受け取ることが可能です。

以下はTCPクライアントが送信データをGZip圧縮してサーバーに送る例です。

using System;
using System.IO;
using System.IO.Compression;
using System.Net.Sockets;
using System.Text;
class TcpGZipClient
{
    static void Main()
    {
        string serverIp = "127.0.0.1";
        int port = 9000;
        string message = "ネットワーク上でリアルタイムにGZip圧縮を行うテストメッセージです。";
        using (TcpClient client = new TcpClient(serverIp, port))
        using (NetworkStream networkStream = client.GetStream())
        using (GZipStream gzipStream = new GZipStream(networkStream, CompressionMode.Compress, leaveOpen: true))
        {
            byte[] data = Encoding.UTF8.GetBytes(message);
            gzipStream.Write(data, 0, data.Length);
            gzipStream.Flush();
        }
        Console.WriteLine("圧縮データを送信しました。");
    }
}

サーバー側は受信したデータをGZipStreamで解凍しながら読み取ります。

using System;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Net.Sockets;
using System.Text;
class TcpGZipServer
{
    static void Main()
    {
        int port = 9000;
        TcpListener listener = new TcpListener(IPAddress.Any, port);
        listener.Start();
        Console.WriteLine("サーバー起動。接続待ち...");
        using (TcpClient client = listener.AcceptTcpClient())
        using (NetworkStream networkStream = client.GetStream())
        using (GZipStream gzipStream = new GZipStream(networkStream, CompressionMode.Decompress))
        using (MemoryStream ms = new MemoryStream())
        {
            gzipStream.CopyTo(ms);
            string receivedMessage = Encoding.UTF8.GetString(ms.ToArray());
            Console.WriteLine($"受信したメッセージ: {receivedMessage}");
        }
        listener.Stop();
    }
}

このように、NetworkStreamGZipStreamを組み合わせることで、通信データをリアルタイムに圧縮・解凍できます。

通信の帯域を節約しつつ、遅延を最小限に抑えられるため、ネットワーク負荷の軽減に役立ちます。

PipeStreamでのパイプライン構築

.NETには標準でPipeStreamというクラスはありませんが、名前の通りパイプライン処理を行うストリームとして、AnonymousPipeServerStreamAnonymousPipeClientStreamNamedPipeServerStreamNamedPipeClientStreamが存在します。

これらはプロセス間通信(IPC)に使われるストリームで、GZipStreamと組み合わせてデータの圧縮・解凍を行うことが可能です。

例えば、親プロセスが圧縮データをパイプで子プロセスに送信し、子プロセスが解凍して処理するシナリオが考えられます。

以下は匿名パイプを使って圧縮データを送る例です。

using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.IO.Pipes;
using System.Text;
class PipeCompressionExample
{
    static void Main()
    {
        using (var pipeServer = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable))
        {
            ProcessStartInfo psi = new ProcessStartInfo();
            psi.FileName = "dotnet";
            psi.Arguments = $"child {pipeServer.GetClientHandleAsString()}";
            psi.UseShellExecute = false;
            Process child = Process.Start(psi);
            pipeServer.DisposeLocalCopyOfClientHandle();
            using (GZipStream gzipStream = new GZipStream(pipeServer, CompressionMode.Compress))
            using (StreamWriter writer = new StreamWriter(gzipStream, Encoding.UTF8))
            {
                writer.WriteLine("パイプを使ったGZip圧縮データの送信テスト");
            }
            child.WaitForExit();
        }
    }
}

子プロセス側は以下のように受信して解凍します。

using System;
using System.IO;
using System.IO.Compression;
using System.IO.Pipes;
using System.Text;
class ChildProcess
{
    static void Main(string[] args)
    {
        if (args.Length < 1)
        {
            Console.WriteLine("パイプハンドルが指定されていません。");
            return;
        }
        string pipeHandle = args[0];
        using (var pipeClient = new AnonymousPipeClientStream(PipeDirection.In, pipeHandle))
        using (GZipStream gzipStream = new GZipStream(pipeClient, CompressionMode.Decompress))
        using (StreamReader reader = new StreamReader(gzipStream, Encoding.UTF8))
        {
            string line = reader.ReadLine();
            Console.WriteLine($"受信したメッセージ: {line}");
        }
    }
}

この例では、親プロセスが匿名パイプの書き込み側を持ち、GZipStreamで圧縮しながらデータを書き込みます。

子プロセスはパイプの読み込み側を受け取り、GZipStreamで解凍しながらデータを読み取ります。

これにより、プロセス間で圧縮データを効率的にやり取りできます。

パイプライン処理は大量データの逐次処理やプロセス間通信での帯域節約に役立ちます。

GZipStreamとパイプストリームを組み合わせることで、リアルタイムに圧縮・解凍を行う柔軟なシステムを構築できます。

非同期APIによる高速化

CopyToAsyncとawait using

GZipStreamを使った圧縮・解凍処理は、ファイルやネットワークなどのI/O操作が絡むため、同期的に処理するとUIのフリーズやパフォーマンス低下を招くことがあります。

そこで、非同期APIを活用して効率的に処理を行う方法が重要です。

.NETのStreamクラスにはCopyToAsyncメソッドが用意されており、ストリーム間のデータ転送を非同期で行えます。

GZipStreamStreamを継承しているため、このメソッドを使って圧縮・解凍処理を非同期化できます。

また、C# 8.0以降ではawait using構文が導入され、非同期でのリソース解放が簡単に書けるようになりました。

これにより、ストリームの開閉も非同期で安全に行えます。

以下は非同期でファイルを圧縮するサンプルコードです。

using System;
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
class Program
{
    static async Task Main()
    {
        string inputFile = "input.txt";
        string outputFile = "input.txt.gz";
        await using (FileStream originalFileStream = new FileStream(inputFile, FileMode.Open, FileAccess.Read))
        await using (FileStream compressedFileStream = new FileStream(outputFile, FileMode.Create, FileAccess.Write))
        await using (GZipStream compressionStream = new GZipStream(compressedFileStream, CompressionMode.Compress))
        {
            // 非同期でコピー(圧縮)処理を実行
            await originalFileStream.CopyToAsync(compressionStream);
        }
        Console.WriteLine("非同期圧縮が完了しました。");
    }
}
非同期圧縮が完了しました。

このコードでは、await usingでストリームを非同期に開閉し、CopyToAsyncで圧縮処理を非同期に実行しています。

UIスレッドをブロックせずに大容量ファイルの圧縮が可能です。

ReadAsync・WriteAsyncの粒度調整

非同期処理をさらに細かく制御したい場合は、ReadAsyncWriteAsyncを使ってバッファ単位で読み書きを行う方法があります。

これにより、処理の粒度を調整し、メモリ使用量やレスポンスのタイミングを最適化できます。

以下は非同期でGZip圧縮を行う際に、バッファサイズを指定して読み書きする例です。

using System;
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
class Program
{
    static async Task CompressFileAsync(string inputFile, string outputFile, int bufferSize = 81920)
    {
        byte[] buffer = new byte[bufferSize];
        await using (FileStream inputStream = new FileStream(inputFile, FileMode.Open, FileAccess.Read))
        await using (FileStream outputStream = new FileStream(outputFile, FileMode.Create, FileAccess.Write))
        await using (GZipStream compressionStream = new GZipStream(outputStream, CompressionMode.Compress))
        {
            int bytesRead;
            while ((bytesRead = await inputStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
            {
                await compressionStream.WriteAsync(buffer, 0, bytesRead);
            }
        }
    }
    static async Task Main()
    {
        string inputFile = "input.txt";
        string outputFile = "input.txt.gz";
        await CompressFileAsync(inputFile, outputFile, bufferSize: 16384);
        Console.WriteLine("バッファ単位で非同期圧縮が完了しました。");
    }
}
バッファ単位で非同期圧縮が完了しました。

この例では、16KBのバッファを使って非同期に読み込み・書き込みを繰り返しています。

バッファサイズを調整することで、メモリ消費やI/O効率をコントロールできます。

小さすぎると処理回数が増え、オーバーヘッドが大きくなります。

大きすぎるとメモリ使用量が増加するため、適切なサイズを選ぶことが重要です。

IProgressで進捗を通知する実装

大きなファイルを圧縮・解凍する際は、処理の進捗をユーザーに通知することが望ましいです。

IProgress<T>インターフェースを使うと、非同期処理の進捗を簡単に報告できます。

以下は、非同期圧縮処理に進捗通知を組み込んだ例です。

using System;
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
class Program
{
    static async Task CompressFileWithProgressAsync(string inputFile, string outputFile, IProgress<double> progress, int bufferSize = 81920)
    {
        byte[] buffer = new byte[bufferSize];
        long totalBytesRead = 0;
        FileInfo fileInfo = new FileInfo(inputFile);
        long totalLength = fileInfo.Length;
        await using (FileStream inputStream = new FileStream(inputFile, FileMode.Open, FileAccess.Read))
        await using (FileStream outputStream = new FileStream(outputFile, FileMode.Create, FileAccess.Write))
        await using (GZipStream compressionStream = new GZipStream(outputStream, CompressionMode.Compress))
        {
            int bytesRead;
            while ((bytesRead = await inputStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
            {
                await compressionStream.WriteAsync(buffer, 0, bytesRead);
                totalBytesRead += bytesRead;
                // 進捗をパーセンテージで報告
                double percentage = (double)totalBytesRead / totalLength * 100;
                progress.Report(percentage);
            }
        }
    }
    static async Task Main()
    {
        string inputFile = "input.txt";
        string outputFile = "input.txt.gz";
        var progress = new Progress<double>(percent =>
        {
            Console.WriteLine($"圧縮進捗: {percent:F2}%");
        });
        await CompressFileWithProgressAsync(inputFile, outputFile, progress);
        Console.WriteLine("進捗通知付きの非同期圧縮が完了しました。");
    }
}
圧縮進捗: 12.34%
圧縮進捗: 25.67%
圧縮進捗: 38.90%
圧縮進捗: 52.13%
圧縮進捗: 65.45%
圧縮進捗: 78.78%
圧縮進捗: 92.01%
圧縮進捗: 100.00%
進捗通知付きの非同期圧縮が完了しました。

このコードでは、IProgress<double>を使って圧縮処理の進捗をパーセンテージで報告しています。

Progress<T>クラスはスレッドセーフで、UIスレッドに安全に進捗を通知できるため、WPFやWinFormsのアプリケーションでも活用しやすいです。

進捗通知を実装することで、ユーザーに処理状況をわかりやすく伝えられ、操作性が向上します。

大容量データへの対応策

バッファサイズ最適化の指針

大容量データを圧縮・解凍する際、バッファサイズの設定は処理速度やメモリ使用量に大きく影響します。

バッファサイズが小さすぎるとI/O操作の回数が増え、オーバーヘッドが大きくなります。

一方で大きすぎるとメモリ消費が増え、システム全体のパフォーマンスに悪影響を及ぼす可能性があります。

一般的な指針としては、以下のポイントを考慮します。

  • デフォルトバッファサイズの活用

.NETのStream.CopyToCopyToAsyncのデフォルトバッファサイズは81920バイト(約80KB)です。

多くのケースでバランスが良いため、特別な理由がなければこのサイズを使うのが無難です。

  • I/Oデバイスの特性に合わせる

SSDや高速ネットワークでは大きめのバッファ(128KB〜1MB)を使うと効率が上がることがあります。

逆に低速なHDDやネットワークでは小さめのバッファが安定する場合もあります。

  • メモリ制約を考慮する

サーバーや組み込み環境などメモリが限られる場合は、バッファサイズを小さくしてメモリ使用量を抑えます。

  • 並列処理との兼ね合い

複数スレッドで圧縮処理を行う場合は、各スレッドのバッファサイズの合計がメモリに与える影響を考慮し、適切に調整します。

バッファサイズの最適化は実際に処理を計測しながら調整するのが効果的です。

Stopwatchで処理時間を計測し、メモリプロファイラで使用量を監視しながら最適なサイズを見つけましょう。

Span<T>とMemory<T>活用術

C# 7.2以降で導入されたSpan<T>Memory<T>は、メモリ効率の良いデータ操作を可能にする構造体とクラスです。

これらを活用すると、大容量データの圧縮・解凍処理で不要なコピーを減らし、パフォーマンスを向上させられます。

  • Span<T>

スタック上に割り当てられ、配列やメモリの一部を参照する軽量な構造体です。

非同期処理には使えませんが、同期処理で高速にバッファを操作できます。

  • Memory<T>

ヒープ上に割り当てられ、非同期処理にも対応可能なメモリ領域のラッパーです。

Span<T>と似ていますが、非同期メソッドの引数として使えます。

GZipStreamの標準APIはbyte[]配列を使いますが、ReadWriteのオーバーロードでSpan<byte>Memory<byte>を使うことで、余計な配列コピーを避けられます。

以下はSpan<byte>を使った同期的な読み書きの例です。

using System;
using System.IO;
using System.IO.Compression;
class Program
{
    static void CompressWithSpan(string inputFile, string outputFile)
    {
        byte[] buffer = new byte[81920];
        using (FileStream inputStream = new FileStream(inputFile, FileMode.Open, FileAccess.Read))
        using (FileStream outputStream = new FileStream(outputFile, FileMode.Create, FileAccess.Write))
        using (GZipStream compressionStream = new GZipStream(outputStream, CompressionMode.Compress))
        {
            int bytesRead;
            while ((bytesRead = inputStream.Read(buffer.AsSpan())) > 0)
            {
                compressionStream.Write(buffer.AsSpan(0, bytesRead));
            }
        }
    }
    static void Main()
    {
        CompressWithSpan("largefile.dat", "largefile.dat.gz");
        Console.WriteLine("Span<T>を使った圧縮が完了しました。");
    }
}

ReadWriteSpan<byte>を渡すことで、配列の一部を効率的に扱い、GC負荷を軽減できます。

非同期処理であればMemory<byte>を使い、ReadAsyncWriteAsyncのオーバーロードを活用しましょう。

分割アーカイブの実装アイデア

大容量ファイルを扱う際、1つの巨大な圧縮ファイルにまとめると扱いにくくなることがあります。

そこで、分割アーカイブ(スプリットアーカイブ)として複数の小さな圧縮ファイルに分割する方法があります。

これにより、転送や保存の柔軟性が向上し、部分的な復元も可能になります。

分割アーカイブの実装例のポイントは以下の通りです。

  • 分割サイズの指定

例えば100MBごとに分割するなど、適切なサイズを決めます。

  • 分割単位で圧縮処理を行う

入力ファイルを指定サイズのチャンクに分割し、それぞれを独立したGZipファイルとして圧縮します。

  • 分割ファイルの命名規則

連番や拡張子で分割ファイルを識別しやすくします。

例: file.part1.gz, file.part2.gzなど。

  • 復元時の結合処理

分割ファイルを順番に解凍し、元のファイルに連結します。

以下は分割圧縮の簡単なイメージコードです。

using System;
using System.IO;
using System.IO.Compression;
class SplitCompressor
{
    public static void CompressInParts(string inputFile, string outputDir, long partSize)
    {
        Directory.CreateDirectory(outputDir);
        using (FileStream inputStream = new FileStream(inputFile, FileMode.Open, FileAccess.Read))
        {
            int partNumber = 1;
            byte[] buffer = new byte[81920];
            while (inputStream.Position < inputStream.Length)
            {
                string partFileName = Path.Combine(outputDir, $"part{partNumber}.gz");
                using (FileStream partStream = new FileStream(partFileName, FileMode.Create, FileAccess.Write))
                using (GZipStream compressionStream = new GZipStream(partStream, CompressionMode.Compress))
                {
                    long bytesWritten = 0;
                    while (bytesWritten < partSize && inputStream.Position < inputStream.Length)
                    {
                        int bytesToRead = (int)Math.Min(buffer.Length, partSize - bytesWritten);
                        int bytesRead = inputStream.Read(buffer, 0, bytesToRead);
                        if (bytesRead == 0) break;
                        compressionStream.Write(buffer, 0, bytesRead);
                        bytesWritten += bytesRead;
                    }
                }
                Console.WriteLine($"パート{partNumber}を作成しました。");
                partNumber++;
            }
        }
    }
}
class Program
{
    static void Main()
    {
        string inputFile = "largefile.dat";
        string outputDir = "split_archive";
        long partSize = 100 * 1024 * 1024; // 100MB
        SplitCompressor.CompressInParts(inputFile, outputDir, partSize);
        Console.WriteLine("分割圧縮が完了しました。");
    }
}

このコードは、指定したサイズごとにファイルを分割して圧縮ファイルを複数作成します。

復元時は分割ファイルを順に解凍し、元のファイルに連結する処理を別途実装してください。

分割アーカイブは大容量ファイルの管理やネットワーク転送での再送効率向上に役立ちます。

用途に応じて分割サイズや命名規則を工夫し、使いやすい仕組みを作りましょう。

CompressionLevelパラメータの比較

OptimalとFastestの性能差

GZipStreamの圧縮処理では、CompressionLevelというパラメータを指定して圧縮の品質と速度のバランスを調整できます。

代表的な値にOptimalFastestがあります。

  • Optimal

圧縮率を最大化する設定です。

圧縮処理に時間がかかりますが、ファイルサイズをできるだけ小さくしたい場合に適しています。

CPU負荷は高めですが、転送や保存のコスト削減に効果的です。

  • Fastest

圧縮速度を優先する設定です。

圧縮率はOptimalより低くなりますが、処理時間が短縮されます。

リアルタイム処理やCPUリソースが限られる環境で有効です。

以下のサンプルコードは、CompressionLevelを指定して圧縮する例です。

using System;
using System.IO;
using System.IO.Compression;
class Program
{
    static void CompressWithLevel(string inputFile, string outputFile, CompressionLevel level)
    {
        using (FileStream inputStream = new FileStream(inputFile, FileMode.Open, FileAccess.Read))
        using (FileStream outputStream = new FileStream(outputFile, FileMode.Create, FileAccess.Write))
        using (GZipStream compressionStream = new GZipStream(outputStream, level))
        {
            inputStream.CopyTo(compressionStream);
        }
    }
    static void Main()
    {
        string inputFile = "example.txt";
        CompressWithLevel(inputFile, "output_optimal.gz", CompressionLevel.Optimal);
        Console.WriteLine("Optimal圧縮が完了しました。");
        CompressWithLevel(inputFile, "output_fastest.gz", CompressionLevel.Fastest);
        Console.WriteLine("Fastest圧縮が完了しました。");
    }
}

Optimalは圧縮率が高い分、処理時間が長くなる傾向があります。

Fastestは処理が速いですが、圧縮後のファイルサイズは大きくなることが多いです。

用途に応じて使い分けるのがポイントです。

SmallestSizeでリソースを最小化

.NET 6以降では、CompressionLevel.SmallestSizeという設定も利用可能です。

これは圧縮率を最大化しつつ、メモリ使用量やCPU負荷を抑えることを目指したモードです。

SmallestSizeOptimalよりもさらに圧縮率を追求し、特にメモリ制約のある環境で効果を発揮します。

ただし、圧縮にかかる時間はOptimalより長くなる場合があります。

以下はSmallestSizeを使った圧縮例です。

using System;
using System.IO;
using System.IO.Compression;
class Program
{
    static void Main()
    {
        string inputFile = "example.txt";
        string outputFile = "output_smallest.gz";
        using (FileStream inputStream = new FileStream(inputFile, FileMode.Open, FileAccess.Read))
        using (FileStream outputStream = new FileStream(outputFile, FileMode.Create, FileAccess.Write))
        using (GZipStream compressionStream = new GZipStream(outputStream, CompressionLevel.SmallestSize))
        {
            inputStream.CopyTo(compressionStream);
        }
        Console.WriteLine("SmallestSize圧縮が完了しました。");
    }
}

SmallestSizeは特にストレージ容量が限られる環境や、圧縮率を最優先したい場合に適しています。

CPUリソースに余裕がある場合に使うと良いでしょう。

カスタム設定とCPU負荷のバランス

CompressionLevelはあらかじめ用意された3つのモードが基本ですが、より細かい制御が必要な場合は、圧縮ライブラリの内部設定や別の圧縮方式を検討することもあります。

.NET標準のGZipStreamでは細かい圧縮パラメータの調整はできませんが、SharpZipLibDotNetZipなどのサードパーティ製ライブラリを使うと、圧縮レベルやメモリ使用量、CPU負荷のバランスを細かく設定可能です。

CPU負荷と圧縮率のバランスを取る際のポイントは以下の通りです。

  • 圧縮率を上げるほどCPU負荷は増加する

高圧縮率を目指すと、より複雑なアルゴリズムや多くのメモリを使うため、CPU負荷が高まります。

  • リアルタイム性が求められる場合は圧縮速度優先

例えばログのリアルタイム圧縮やストリーミング配信では、圧縮速度を優先し、多少圧縮率が下がっても許容することが多いです。

  • バッチ処理やアーカイブ作成は圧縮率優先

処理時間に余裕がある場合は、圧縮率を最大化してストレージ容量を節約します。

  • マルチスレッド圧縮の活用

一部のライブラリはマルチスレッド圧縮をサポートしており、CPUコアを有効活用して高速かつ高圧縮率を実現できます。

.NET標準のGZipStreamはシンプルで使いやすい反面、細かいパラメータ調整はできません。

用途に応じて標準APIとサードパーティ製ライブラリを使い分けると良いでしょう。

例外とエラーハンドリング

InvalidDataExceptionの原因と対策

InvalidDataExceptionは、GZipStreamを使った圧縮・解凍処理でよく発生する例外の一つです。

この例外は、読み込んだデータがGZip形式として正しくない場合や、破損している場合にスローされます。

主な原因は以下の通りです。

  • 圧縮ファイルの破損

ファイルの一部が欠損していたり、転送中にデータが壊れている場合。

  • 誤ったファイル形式の読み込み

GZip形式ではないファイルを誤ってGZipStreamで解凍しようとした場合。

  • ストリームの途中終了

圧縮データの途中でストリームが閉じられたり、読み込みが不完全な場合。

  • 複数ファイルの連結

複数のGZipファイルを連結した場合、GZipStreamは最初のファイルしか処理できず、後続のデータで例外が発生することがあります。

対策としては以下を検討してください。

  • ファイル形式の検証

解凍前にファイルの拡張子やマジックナンバー(GZipは先頭2バイトが0x1F 0x8B)をチェックし、誤ったファイルを処理しないようにします。

  • ファイルの整合性チェック

CRCやファイルサイズを検証し、破損ファイルを検出します。

可能なら再取得や再生成を促します。

  • 例外処理の実装

try-catchInvalidDataExceptionを捕捉し、ユーザーにエラーメッセージを表示したり、ログに記録します。

  • 連結ファイルの対応

複数のGZipファイルを連結している場合は、分割して個別に解凍するか、専用のライブラリを使うことを検討します。

try
{
    using (FileStream compressedFileStream = new FileStream("file.gz", FileMode.Open))
    using (GZipStream decompressionStream = new GZipStream(compressedFileStream, CompressionMode.Decompress))
    using (FileStream outputFileStream = new FileStream("output.txt", FileMode.Create))
    {
        decompressionStream.CopyTo(outputFileStream);
    }
}
catch (InvalidDataException ex)
{
    Console.WriteLine("圧縮ファイルが破損しているか、形式が正しくありません。");
    Console.WriteLine($"詳細: {ex.Message}");
}

ストリーム未クローズによる不具合

GZipStreamFileStreamなどのストリームは、使用後に必ず閉じる(Disposeする)必要があります。

ストリームを閉じないと、以下のような不具合が発生します。

  • ファイルロックの継続

ファイルが開いたままになるため、他のプロセスや操作でファイルを開けなくなります。

  • データの不完全な書き込み

バッファリングされたデータがフラッシュされず、圧縮ファイルが不完全になることがあります。

  • メモリリークやリソース枯渇

ストリームが解放されず、メモリやハンドルが無駄に消費され続けます。

対策としては、using文やawait usingを使ってストリームのライフサイクルを管理します。

これにより、例外が発生しても確実にリソースが解放されます。

using (FileStream inputStream = new FileStream("input.txt", FileMode.Open))
using (FileStream outputStream = new FileStream("output.gz", FileMode.Create))
using (GZipStream compressionStream = new GZipStream(outputStream, CompressionMode.Compress))
{
    inputStream.CopyTo(compressionStream);
} // ここで全てのストリームが自動的に閉じられる

また、非同期処理ではawait usingを使うことで、非同期にDisposeを呼び出せます。

タイムアウト・キャンセル処理の設計

圧縮・解凍処理は大容量データを扱うことが多く、処理時間が長くなる場合があります。

ユーザー操作やシステム要件に応じて、処理のタイムアウトやキャンセルを設計することが重要です。

.NETのGZipStream自体にはタイムアウトやキャンセルの機能はありませんが、以下の方法で対応可能です。

  • CancellationTokenの活用

非同期の読み書きメソッドReadAsyncWriteAsyncCopyToAsyncCancellationTokenを受け取れます。

これを使って処理を途中でキャンセルできます。

  • タイムアウトの実装

タイムアウトはTaskWaitTask.WhenAnyを使って実装します。

一定時間内に処理が終わらなければキャンセルを発行します。

  • 例外処理でキャンセルを検知

キャンセル時はOperationCanceledExceptionがスローされるため、適切に捕捉して処理を中断します。

以下はキャンセルトークンを使った非同期圧縮の例です。

using System;
using System.IO;
using System.IO.Compression;
using System.Threading;
using System.Threading.Tasks;
class Program
{
    static async Task CompressFileAsync(string inputFile, string outputFile, CancellationToken cancellationToken)
    {
        await using (FileStream inputStream = new FileStream(inputFile, FileMode.Open, FileAccess.Read))
        await using (FileStream outputStream = new FileStream(outputFile, FileMode.Create, FileAccess.Write))
        await using (GZipStream compressionStream = new GZipStream(outputStream, CompressionMode.Compress))
        {
            await inputStream.CopyToAsync(compressionStream, 81920, cancellationToken);
        }
    }
    static async Task Main()
    {
        var cts = new CancellationTokenSource();
        // 5秒後にキャンセルする例
        cts.CancelAfter(TimeSpan.FromSeconds(5));
        try
        {
            await CompressFileAsync("largefile.dat", "largefile.dat.gz", cts.Token);
            Console.WriteLine("圧縮が完了しました。");
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("圧縮処理がキャンセルされました。");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"エラーが発生しました: {ex.Message}");
        }
    }
}

このようにキャンセルトークンを使うことで、ユーザーの操作やシステムの状態に応じて圧縮・解凍処理を安全に中断できます。

タイムアウトやキャンセルは長時間処理の安定性向上に欠かせない設計要素です。

拡張メソッドでの利便性向上

Zip()とUnzip()の自作ユーティリティ

GZipStreamを使った圧縮・解凍処理は基本的にストリーム操作が中心ですが、毎回同じようなコードを書くのは手間です。

そこで、拡張メソッドとしてZip()Unzip()を自作すると、コードの可読性と再利用性が向上します。

以下はbyte[]string(ファイルパス)に対して使えるシンプルな拡張メソッドの例です。

using System;
using System.IO;
using System.IO.Compression;
using System.Text;
public static class GZipExtensions
{
    // バイト配列をGZip圧縮して返す
    public static byte[] Zip(this byte[] data)
    {
        using (var outputStream = new MemoryStream())
        {
            using (var gzipStream = new GZipStream(outputStream, CompressionMode.Compress))
            {
                gzipStream.Write(data, 0, data.Length);
            }
            return outputStream.ToArray();
        }
    }
    // GZip圧縮されたバイト配列を解凍して返す
    public static byte[] Unzip(this byte[] compressedData)
    {
        using (var inputStream = new MemoryStream(compressedData))
        using (var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress))
        using (var outputStream = new MemoryStream())
        {
            gzipStream.CopyTo(outputStream);
            return outputStream.ToArray();
        }
    }
    // ファイルを圧縮して別ファイルに保存する拡張メソッド
    public static void Zip(this string inputFilePath, string outputFilePath)
    {
        using (var inputStream = new FileStream(inputFilePath, FileMode.Open, FileAccess.Read))
        using (var outputStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write))
        using (var gzipStream = new GZipStream(outputStream, CompressionMode.Compress))
        {
            inputStream.CopyTo(gzipStream);
        }
    }
    // GZipファイルを解凍して別ファイルに保存する拡張メソッド
    public static void Unzip(this string compressedFilePath, string outputFilePath)
    {
        using (var inputStream = new FileStream(compressedFilePath, FileMode.Open, FileAccess.Read))
        using (var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress))
        using (var outputStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write))
        {
            gzipStream.CopyTo(outputStream);
        }
    }
}
class Program
{
    static void Main()
    {
        string originalFile = "example.txt";
        string compressedFile = "example.txt.gz";
        string decompressedFile = "example_unzip.txt";
        // ファイル圧縮(拡張メソッドを利用)
        originalFile.Zip(compressedFile);
        Console.WriteLine("ファイルを圧縮しました。");
        // ファイル解凍(拡張メソッドを利用)
        compressedFile.Unzip(decompressedFile);
        Console.WriteLine("ファイルを解凍しました。");
        // バイト配列圧縮・解凍の例
        byte[] data = Encoding.UTF8.GetBytes("拡張メソッドで簡単に圧縮・解凍できます。");
        byte[] compressedData = data.Zip();
        byte[] decompressedData = compressedData.Unzip();
        Console.WriteLine(Encoding.UTF8.GetString(decompressedData));
    }
}

このように拡張メソッドを用意すると、string型のファイルパスやbyte[]に対して直感的にZip()Unzip()を呼び出せるため、コードがすっきりします。

非同期拡張メソッドのテンプレート

非同期処理が必要な場合は、async/awaitを使った非同期拡張メソッドを作成すると便利です。

CopyToAsyncReadAsyncWriteAsyncを活用し、UIのフリーズやスレッドブロックを防ぎます。

以下は非同期版のファイル圧縮・解凍拡張メソッドのテンプレートです。

using System;
using System.IO;
using System.IO.Compression;
using System.Threading;
using System.Threading.Tasks;
public static class GZipAsyncExtensions
{
    public static async Task ZipAsync(this string inputFilePath, string outputFilePath, CancellationToken cancellationToken = default)
    {
        await using (var inputStream = new FileStream(inputFilePath, FileMode.Open, FileAccess.Read))
        await using (var outputStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write))
        await using (var gzipStream = new GZipStream(outputStream, CompressionMode.Compress))
        {
            await inputStream.CopyToAsync(gzipStream, 81920, cancellationToken);
        }
    }
    public static async Task UnzipAsync(this string compressedFilePath, string outputFilePath, CancellationToken cancellationToken = default)
    {
        await using (var inputStream = new FileStream(compressedFilePath, FileMode.Open, FileAccess.Read))
        await using (var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress))
        await using (var outputStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write))
        {
            await gzipStream.CopyToAsync(outputStream, 81920, cancellationToken);
        }
    }
}
class Program
{
    static async Task Main()
    {
        string originalFile = "example.txt";
        string compressedFile = "example_async.txt.gz";
        string decompressedFile = "example_async_unzip.txt";
        await originalFile.ZipAsync(compressedFile);
        Console.WriteLine("非同期でファイルを圧縮しました。");
        await compressedFile.UnzipAsync(decompressedFile);
        Console.WriteLine("非同期でファイルを解凍しました。");
    }
}

キャンセルトークンを受け取る設計にしておくと、処理の途中キャンセルにも対応しやすくなります。

DIコンテナと組み合わせたサービス化

拡張メソッドは便利ですが、依存性注入(DI)コンテナと組み合わせてサービスとして提供すると、より柔軟でテストしやすい設計になります。

特に大規模なアプリケーションやASP.NET Coreなどの環境では、圧縮処理をサービス化して管理するのが一般的です。

以下は圧縮・解凍サービスのインターフェースと実装例です。

public interface ICompressionService
{
    void Compress(string inputFilePath, string outputFilePath);
    void Decompress(string compressedFilePath, string outputFilePath);
}
public class GZipCompressionService : ICompressionService
{
    public void Compress(string inputFilePath, string outputFilePath)
    {
        using var inputStream = new FileStream(inputFilePath, FileMode.Open, FileAccess.Read);
        using var outputStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write);
        using var gzipStream = new GZipStream(outputStream, CompressionMode.Compress);
        inputStream.CopyTo(gzipStream);
    }
    public void Decompress(string compressedFilePath, string outputFilePath)
    {
        using var inputStream = new FileStream(compressedFilePath, FileMode.Open, FileAccess.Read);
        using var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress);
        using var outputStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write);
        gzipStream.CopyTo(outputStream);
    }
}

ASP.NET CoreのStartup.csProgram.csでDIコンテナに登録します。

services.AddScoped<ICompressionService, GZipCompressionService>();

利用側はコンストラクタインジェクションでサービスを受け取り、圧縮・解凍処理を呼び出せます。

public class FileController : ControllerBase
{
    private readonly ICompressionService _compressionService;
    public FileController(ICompressionService compressionService)
    {
        _compressionService = compressionService;
    }
    public IActionResult CompressFile(string inputPath, string outputPath)
    {
        _compressionService.Compress(inputPath, outputPath);
        return Ok("圧縮完了");
    }
}

このようにサービス化すると、テスト時にモックを差し替えたり、将来的に圧縮方式を切り替えたりするのが容易になります。

拡張メソッドと組み合わせて、用途に応じた柔軟な設計を目指しましょう。

他形式との互換性チェック

Unix環境でのgzip検証

GZip形式はUnix系システムで広く使われている圧縮フォーマットです。

C#のGZipStreamで作成した圧縮ファイルは、Unix環境のgzipコマンドで問題なく解凍できるか検証することが重要です。

逆に、Unixで作成された.gzファイルがWindowsのGZipStreamで正しく解凍できるかも確認しましょう。

検証手順の例:

STEP
Windowsで圧縮しUnixで解凍

C#のGZipStreamで圧縮したファイルをUnix環境に転送し、gzip -d filename.gzで解凍します。

正常に解凍できれば互換性があります。

STEP
Unixで圧縮しWindowsで解凍

Unixのgzipコマンドで圧縮したファイルをWindowsに転送し、GZipStreamで解凍します。

エラーなく解凍できれば互換性が保たれています。

STEP
ファイルの整合性チェック

解凍後のファイルのハッシュ値(SHA256など)を比較し、元ファイルと同一であることを確認します。

注意点として、GZipStreamは標準的なGZipフォーマットに準拠していますが、Unixのgzipコマンドで付加されるファイル名やタイムスタンプのメタデータは復元されない場合があります。

ファイル内容の圧縮・解凍には問題ありませんが、メタ情報の扱いに差異があることを理解しておきましょう。

ZlibやZIPとの混同を避けるポイント

GZip形式はDEFLATE圧縮アルゴリズムを使っていますが、zlibZIPとは異なるファイルフォーマットです。

これらの違いを理解し、混同しないことが重要です。

フォーマット圧縮アルゴリズムファイル構造の特徴主な用途
GZipDEFLATEヘッダーとCRC付きの単一圧縮データ単一ファイルの圧縮
zlibDEFLATE簡易ヘッダー付きの圧縮データネットワーク通信やライブラリ内部
ZIPDEFLATEなど複数複数ファイルをまとめるアーカイブ形式複数ファイルの圧縮・アーカイブ
  • GZipとzlibの違い

GZipはファイル圧縮用にヘッダーやCRCを持ちますが、zlibはより軽量なヘッダーでストリーム圧縮に適しています。

GZipStreamはGZip形式を扱い、DeflateStreamはzlib形式に近い圧縮を行います。

  • ZIPとの違い

ZIPは複数ファイルをまとめて圧縮・アーカイブできる形式で、ファイルごとに圧縮方式やメタデータを持ちます。

GZipは単一ファイルの圧縮に特化しています。

混同すると、例えばZIPファイルをGZipStreamで解凍しようとしてエラーになることがあります。

ファイルの拡張子やマジックナンバーを確認し、適切な処理を行うことが大切です。

クロスプラットフォーム運用の注意点

C#のGZipStreamはWindowsだけでなくLinuxやmacOSなどの.NET対応環境でも動作しますが、クロスプラットフォームで運用する際にはいくつか注意点があります。

  • 改行コードの違い

圧縮対象のテキストファイルにWindowsのCRLF\r\nとUnixのLF\nの違いがあると、解凍後のファイル内容が異なる場合があります。

圧縮前後で改行コードの統一を検討してください。

  • ファイル属性の扱い

GZip形式はファイルのタイムスタンプなど一部のメタデータを保持しますが、パーミッションや所有者情報は保持しません。

Unixのパーミッション情報を保持したい場合は、tarアーカイブと組み合わせる方法が一般的です。

  • 文字コードの違い

ファイル名やメタデータに非ASCII文字が含まれる場合、プラットフォーム間で文字化けが起きることがあります。

UTF-8で統一するか、ファイル名の扱いに注意してください。

  • ファイルシステムの差異

WindowsとUnix系でファイル名の大文字小文字の扱いや予約文字が異なるため、圧縮・解凍時にファイル名の衝突やエラーが起きることがあります。

  • テスト環境の整備

クロスプラットフォーム対応を行う場合は、実際に各環境で圧縮・解凍テストを行い、互換性や動作の違いを確認することが重要です。

これらのポイントを押さえ、GZipファイルの生成・解凍を行うことで、異なるOS間でも安定したファイル圧縮運用が可能になります。

ASP.NETでの応用例

静的ファイルを事前圧縮して配信

ASP.NETアプリケーションでは、静的ファイル(CSS、JavaScript、画像など)を事前にGZip圧縮しておくことで、クライアントへの転送データ量を削減し、ページの読み込み速度を向上させられます。

事前圧縮ファイルを用意しておくと、サーバー側の負荷も軽減されます。

事前圧縮ファイルの作成例は以下の通りです。

例えば、wwwroot/css/site.cssを圧縮してwwwroot/css/site.css.gzとして保存します。

using System;
using System.IO;
using System.IO.Compression;
class Program
{
    static void Main()
    {
        string inputFile = "wwwroot/css/site.css";
        string outputFile = "wwwroot/css/site.css.gz";
        using (FileStream inputStream = new FileStream(inputFile, FileMode.Open, FileAccess.Read))
        using (FileStream outputStream = new FileStream(outputFile, FileMode.Create, FileAccess.Write))
        using (GZipStream gzipStream = new GZipStream(outputStream, CompressionMode.Compress))
        {
            inputStream.CopyTo(gzipStream);
        }
        Console.WriteLine("静的ファイルの事前圧縮が完了しました。");
    }
}

このように事前に圧縮ファイルを用意したら、ASP.NETの静的ファイルミドルウェアで.gzファイルを優先的に配信する設定を行います。

例えば、UseStaticFilesのオプションでServeUnknownFileTypesを有効にし、Content-Encodingヘッダーを付与してGZip圧縮済みファイルを返すようにカスタマイズします。

app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        if (ctx.File.Name.EndsWith(".gz"))
        {
            ctx.Context.Response.Headers["Content-Encoding"] = "gzip";
            ctx.Context.Response.Headers["Content-Type"] = "text/css"; // ファイルタイプに応じて変更
        }
    }
});

これにより、ブラウザがGZip圧縮をサポートしている場合、圧縮済みファイルを直接配信でき、サーバーのリアルタイム圧縮負荷を削減できます。

ResponseCompressionミドルウェアの仕組み

ASP.NET CoreにはResponseCompressionミドルウェアが用意されており、動的に生成されるレスポンスを自動で圧縮してクライアントに送信できます。

これにより、APIレスポンスやHTML、JSONなどのデータ転送量を削減し、通信効率を向上させます。

ResponseCompressionミドルウェアは、クライアントのAccept-Encodingヘッダーを解析し、対応する圧縮方式(GZip、Brotliなど)でレスポンスを圧縮します。

圧縮対象のコンテンツタイプやサイズの閾値も設定可能です。

設定例は以下の通りです。

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.ResponseCompression;
using System.Linq;
var builder = WebApplication.CreateBuilder(args);
// ResponseCompressionサービスを追加
builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true; // HTTPSでも圧縮を有効化
    options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "application/json" });
});
var app = builder.Build();
// ミドルウェアを登録
app.UseResponseCompression();
app.MapGet("/", () => "Hello, compressed world!");
app.Run();

この設定により、JSONやテキストなどのレスポンスが自動的にGZipやBrotliで圧縮され、クライアントに送信されます。

ミドルウェアは圧縮の可否を自動判定し、圧縮済みのレスポンスにはContent-Encodingヘッダーを付与します。

Web APIで圧縮リクエストを受け取る方法

Web APIでクライアントから送信されるリクエストボディがGZip圧縮されている場合、サーバー側で解凍処理を行う必要があります。

ASP.NET Coreでは標準でリクエストの圧縮解除を自動で行わないため、カスタムミドルウェアやフィルターを実装して対応します。

以下は、リクエストのContent-Encodingヘッダーがgzipの場合に、リクエストボディを解凍する簡単なミドルウェアの例です。

using Microsoft.AspNetCore.Http;
using System.IO.Compression;
using System.Threading.Tasks;
public class GzipRequestDecompressionMiddleware
{
    private readonly RequestDelegate _next;
    public GzipRequestDecompressionMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    public async Task InvokeAsync(HttpContext context)
    {
        if (context.Request.Headers.TryGetValue("Content-Encoding", out var encoding) &&
            encoding.ToString().Contains("gzip"))
        {
            var originalBody = context.Request.Body;
            using var decompressedStream = new GZipStream(originalBody, CompressionMode.Decompress);
            var memoryStream = new MemoryStream();
            await decompressedStream.CopyToAsync(memoryStream);
            memoryStream.Seek(0, SeekOrigin.Begin);
            context.Request.Body = memoryStream;
            // Content-Encodingヘッダーを削除して後続の処理に影響を与えないようにする
            context.Request.Headers.Remove("Content-Encoding");
        }
        await _next(context);
    }
}

このミドルウェアをStartupProgramで登録します。

app.UseMiddleware<GzipRequestDecompressionMiddleware>();

これにより、GZip圧縮されたリクエストボディを自動で解凍し、通常のAPIコントローラーやモデルバインディングで扱えるようになります。

クライアント側はリクエストヘッダーにContent-Encoding: gzipを付けて圧縮データを送信します。

サーバー側で解凍後、通常のJSONやフォームデータとして処理可能です。

この仕組みを導入することで、帯域幅の節約や高速な通信が可能になり、特にモバイル環境や低速回線でのAPI利用に効果的です。

クライアントサイドでの利用

HttpClientのリクエストボディ圧縮

HttpClientを使ってサーバーにデータを送信する際、リクエストボディをGZip圧縮することで通信量を削減し、帯域幅の節約や送信速度の向上が期待できます。

特に大きなJSONやXMLデータを送る場合に有効です。

以下は、HttpClientのリクエストボディをGZip圧縮して送信するサンプルコードです。

using System;
using System.IO;
using System.IO.Compression;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class Program
{
    static async Task Main()
    {
        var httpClient = new HttpClient();
        string url = "https://example.com/api/data";
        string jsonData = "{\"name\":\"太郎\",\"age\":30,\"city\":\"東京\"}";
        byte[] compressedData;
        // JSON文字列をGZip圧縮
        using (var outputStream = new MemoryStream())
        {
            using (var gzipStream = new GZipStream(outputStream, CompressionMode.Compress))
            using (var writer = new StreamWriter(gzipStream, Encoding.UTF8))
            {
                writer.Write(jsonData);
            }
            compressedData = outputStream.ToArray();
        }
        // 圧縮データをHttpContentに設定
        var content = new ByteArrayContent(compressedData);
        content.Headers.Add("Content-Encoding", "gzip");
        content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
        // POSTリクエスト送信
        HttpResponseMessage response = await httpClient.PostAsync(url, content);
        Console.WriteLine($"レスポンスステータスコード: {response.StatusCode}");
    }
}
レスポンスステータスコード: OK

このコードでは、JSON文字列をGZipStreamで圧縮し、ByteArrayContentにセットしています。

Content-Encodingヘッダーにgzipを指定することで、サーバー側に圧縮データであることを通知します。

サーバーはこのヘッダーを見て解凍処理を行う必要があります。

ブラウザから送信されるgzipを解凍

ブラウザが送信するHTTPリクエストで、Content-Encoding: gzipが付いている場合、サーバー側でリクエストボディを解凍する必要があります。

ASP.NET Coreでは標準でリクエストの圧縮解除は行われないため、カスタムミドルウェアやフィルターで対応します。

例えば、前述のGzipRequestDecompressionMiddlewareを使うと、ブラウザから送信されたgzip圧縮リクエストを自動で解凍できます。

ブラウザ側でJavaScriptを使ってリクエストボディをgzip圧縮するには、pakoなどのライブラリを利用します。

圧縮したバイナリデータをfetchbodyにセットし、Content-Encoding: gzipヘッダーを付けて送信します。

// pakoライブラリを使った例
const data = JSON.stringify({ name: "太郎", age: 30 });
const compressed = pako.gzip(data);
fetch('/api/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Content-Encoding': 'gzip'
  },
  body: compressed
});

サーバー側で解凍ミドルウェアを用意しておけば、圧縮されたリクエストを問題なく処理できます。

Unityでのアセット圧縮配信

Unityでゲームやアプリを開発する際、アセット(画像、音声、テキストなど)のサイズが大きくなることがあります。

これらをGZip圧縮して配信し、ダウンロード時間やストレージ使用量を削減する方法があります。

UnityのC#スクリプトでGZip圧縮・解凍を行うには、System.IO.Compression名前空間のGZipStreamを利用します。

以下は、Unityでファイルを圧縮・解凍する基本的な例です。

using System.IO;
using System.IO.Compression;
using UnityEngine;
public class GZipUtility : MonoBehaviour
{
    public static void CompressFile(string inputPath, string outputPath)
    {
        using (FileStream inputFileStream = new FileStream(inputPath, FileMode.Open))
        using (FileStream outputFileStream = new FileStream(outputPath, FileMode.Create))
        using (GZipStream gzipStream = new GZipStream(outputFileStream, CompressionMode.Compress))
        {
            inputFileStream.CopyTo(gzipStream);
        }
        Debug.Log("ファイルを圧縮しました: " + outputPath);
    }
    public static void DecompressFile(string inputPath, string outputPath)
    {
        using (FileStream inputFileStream = new FileStream(inputPath, FileMode.Open))
        using (GZipStream gzipStream = new GZipStream(inputFileStream, CompressionMode.Decompress))
        using (FileStream outputFileStream = new FileStream(outputPath, FileMode.Create))
        {
            gzipStream.CopyTo(outputFileStream);
        }
        Debug.Log("ファイルを解凍しました: " + outputPath);
    }
}

Unityのビルド環境によっては、System.IO.Compressionが利用できない場合があります。

その場合は、SharpZipLibなどの外部ライブラリを導入して対応してください。

また、アセットバンドルやAddressable Assetsと組み合わせて、圧縮済みアセットをサーバーからダウンロードし、解凍して利用する仕組みを作ると効率的です。

これにより、ユーザーのダウンロード時間短縮やストレージ節約が可能になります。

パフォーマンス測定と最適化

Stopwatchで処理時間を計測

C#で圧縮・解凍処理のパフォーマンスを測定する際、最も手軽で基本的な方法がSystem.Diagnostics.Stopwatchクラスを使った処理時間の計測です。

Stopwatchは高精度なタイマーで、処理の開始から終了までの経過時間をミリ秒単位で取得できます。

以下は、GZipStreamを使ったファイル圧縮処理の実行時間を計測するサンプルコードです。

using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
class Program
{
    static void Main()
    {
        string inputFile = "largefile.dat";
        string outputFile = "largefile.dat.gz";
        var stopwatch = new Stopwatch();
        stopwatch.Start();
        using (FileStream inputStream = new FileStream(inputFile, FileMode.Open, FileAccess.Read))
        using (FileStream outputStream = new FileStream(outputFile, FileMode.Create, FileAccess.Write))
        using (GZipStream compressionStream = new GZipStream(outputStream, CompressionMode.Compress))
        {
            inputStream.CopyTo(compressionStream);
        }
        stopwatch.Stop();
        Console.WriteLine($"圧縮処理時間: {stopwatch.ElapsedMilliseconds} ms");
    }
}
圧縮処理時間: 1234 ms

このように、Stopwatchを使うことで簡単に処理時間を計測でき、圧縮アルゴリズムのパフォーマンス比較や最適化の効果検証に役立ちます。

非同期処理の場合も同様に、awaitの前後でStopwatchを開始・停止すれば正確な時間を測れます。

MemoryProfilerでメモリ消費を確認

圧縮・解凍処理は大量のバッファを扱うため、メモリ消費がパフォーマンスや安定性に大きく影響します。

Visual StudioのMemory Profilerdotnet-tracedotMemoryなどのツールを使ってメモリ使用状況を詳細に分析しましょう。

Visual StudioのDiagnostic Toolsでメモリスナップショットを取得し、圧縮処理前後のメモリ使用量やGCの発生状況を確認できます。

これにより、不要なメモリ割り当てやリークの有無を特定し、バッファサイズの調整やSpan<T>の活用などの最適化に繋げられます。

例えば、圧縮処理中に大量の一時配列が生成されている場合は、MemoryPool<byte>ArrayPool<byte>を使ってバッファの再利用を検討するとメモリ効率が向上します。

using System.Buffers;
byte[] buffer = ArrayPool<byte>.Shared.Rent(81920);
try
{
    // バッファを使った読み書き処理
}
finally
{
    ArrayPool<byte>.Shared.Return(buffer);
}

このようにバッファプールを活用することで、GC負荷を軽減し、メモリ消費を抑えられます。

メモリプロファイラでの分析は、こうした改善点を見つけるために欠かせません。

ベンチマークフレームワーク活用

より詳細かつ再現性のあるパフォーマンス測定には、BenchmarkDotNetのようなベンチマークフレームワークを使うのがおすすめです。

BenchmarkDotNetはマイクロベンチマークを簡単に作成でき、JIT最適化やGCの影響を考慮した正確な測定結果を提供します。

以下はBenchmarkDotNetを使ってGZipStreamの圧縮処理をベンチマークする例です。

using System;
using System.IO;
using System.IO.Compression;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
public class GZipBenchmark
{
    private byte[] inputData;
    [GlobalSetup]
    public void Setup()
    {
        // 10MBのランダムデータを生成
        inputData = new byte[10 * 1024 * 1024];
        new Random(42).NextBytes(inputData);
    }
    [Benchmark]
    public void CompressData()
    {
        using var outputStream = new MemoryStream();
        using var gzipStream = new GZipStream(outputStream, CompressionMode.Compress);
        gzipStream.Write(inputData, 0, inputData.Length);
    }
}
class Program
{
    static void Main()
    {
        var summary = BenchmarkRunner.Run<GZipBenchmark>();
    }
}

実行すると、処理時間やメモリ割り当て量、GC発生回数などが詳細にレポートされます。

|      Method |      Mean |     Error |    StdDev |  Gen 0 | Allocated |
|------------ |----------:|----------:|----------:|-------:|----------:|
| CompressData | 1234.56 ms | 12.34 ms | 10.23 ms |  500.0 |  10.5 MB  |

BenchmarkDotNetを使うことで、異なる圧縮レベルやバッファサイズ、非同期処理との比較など、多角的にパフォーマンスを評価できます。

これにより、最適な実装やパラメータ設定を科学的に導き出せるため、品質の高い圧縮機能を提供できます。

セキュリティと脆弱性対策

zip bombを防ぐサイズ制限

「zip bomb(ジップボム)」とは、非常に小さな圧縮ファイルが解凍すると膨大なサイズのデータに展開され、システムのリソースを枯渇させる攻撃手法です。

GZip形式でも同様の問題が起こり得るため、圧縮ファイルの解凍時にはサイズ制限を設けることが重要です。

具体的には、解凍後のデータサイズが異常に大きくならないように、以下の対策を行います。

  • 最大展開サイズの設定

解凍処理中に展開済みのデータサイズを監視し、あらかじめ定めた上限を超えた場合は処理を中断して例外をスローします。

  • 入力ファイルサイズのチェック

圧縮ファイルのサイズに対して、妥当な展開サイズの範囲を設定し、極端に小さいファイルから巨大な展開を行うものを警戒します。

  • タイムアウトやリソース制限の併用

解凍処理に時間制限やメモリ制限を設け、異常な負荷を防ぎます。

以下は展開サイズを監視しながら解凍する簡単な例です。

using System;
using System.IO;
using System.IO.Compression;
public class SafeGZipDecompressor
{
    private const long MaxDecompressedSize = 100 * 1024 * 1024; // 100MB
    public static void DecompressWithLimit(string inputFile, string outputFile)
    {
        long totalBytesWritten = 0;
        using (FileStream inputStream = new FileStream(inputFile, FileMode.Open))
        using (GZipStream gzipStream = new GZipStream(inputStream, CompressionMode.Decompress))
        using (FileStream outputStream = new FileStream(outputFile, FileMode.Create))
        {
            byte[] buffer = new byte[81920];
            int bytesRead;
            while ((bytesRead = gzipStream.Read(buffer, 0, buffer.Length)) > 0)
            {
                totalBytesWritten += bytesRead;
                if (totalBytesWritten > MaxDecompressedSize)
                {
                    throw new InvalidOperationException("展開サイズが制限を超えました。zip bombの可能性があります。");
                }
                outputStream.Write(buffer, 0, bytesRead);
            }
        }
    }
}

このように展開サイズを監視することで、zip bomb攻撃によるリソース枯渇を防止できます。

信頼できない入力データの扱い

外部から受け取る圧縮ファイルやストリームは、信頼できないデータとして扱う必要があります。

悪意のあるデータは、zip bombのほかにも以下のリスクを含みます。

  • 破損ファイルによる例外発生

不正なフォーマットや破損した圧縮データはInvalidDataExceptionなどの例外を引き起こします。

例外処理を適切に行い、サービスの停止を防ぎます。

  • リソース消費攻撃

CPUやメモリを過剰に消費させる圧縮データを送信される可能性があります。

タイムアウトやメモリ制限を設けて防御します。

  • ファイル名の悪用

ZIP形式ではファイル名にパスを含めてディレクトリトラバーサル攻撃を仕掛けることがあります。

GZipは単一ファイル圧縮なので影響は少ないですが、ファイル名の扱いには注意が必要です。

対策としては以下を実施します。

  • 入力検証と例外処理

圧縮ファイルのヘッダーやマジックナンバーを検証し、不正なファイルは拒否します。

例外はキャッチしてログに記録し、サービス継続を優先します。

  • リソース制限の適用

解凍処理にタイムアウトやメモリ制限を設け、異常な負荷を検知したら処理を中断します。

  • サンドボックス環境での処理

可能であれば、解凍処理を隔離された環境で実行し、万が一の攻撃による影響を最小限に抑えます。

  • ログと監視の強化

不正な圧縮ファイルの受信や例外発生を監視し、攻撃の兆候を早期に検知します。

HTTPSと組み合わせた安全設計

圧縮ファイルの送受信を安全に行うためには、通信経路の暗号化が不可欠です。

HTTP通信をそのまま使うと、圧縮データが盗聴や改ざんされるリスクがあります。

HTTPS(TLS)を利用して通信を暗号化し、データの機密性と完全性を確保しましょう。

HTTPSを利用する際のポイントは以下の通りです。

  • サーバー証明書の適切な管理

信頼できる認証局から発行された証明書を使い、中間者攻撃を防ぎます。

  • 強力なTLS設定

TLS 1.2以上を使用し、弱い暗号スイートは無効化します。

  • 圧縮と暗号化の順序

圧縮は送信前に行い、圧縮済みデータをTLSで暗号化して送信します。

これにより、圧縮データの内容が外部に漏れません。

  • HTTPヘッダーのセキュリティ強化

Strict-Transport-SecurityContent-Security-Policyなどのヘッダーを設定し、通信の安全性を高めます。

  • 認証・認可の適用

圧縮ファイルのアップロードやダウンロードには適切な認証・認可を設け、不正アクセスを防ぎます。

これらの対策を組み合わせることで、圧縮ファイルの送受信におけるセキュリティリスクを大幅に低減できます。

特にインターネット経由でのファイル転送やAPI通信では、HTTPSの利用は必須と考えてください。

テスト戦略

単体テストで圧縮結果を確認

GZip圧縮・解凍機能の単体テストでは、圧縮処理が正しく動作しているか、解凍後に元のデータと一致するかを検証します。

主なポイントは以下の通りです。

  • 圧縮後のファイルサイズが元ファイルより小さいことを確認

圧縮処理が実際に効果を発揮しているかをチェックします。

ただし、圧縮率はデータによって異なるため、必ずしも小さくならないケースも考慮します。

  • 解凍後のデータが元データと完全に一致することを検証

バイト単位で比較し、圧縮・解凍の可逆性を保証します。

  • 例外が発生しないことを確認

不正な入力や境界値での動作もテストし、例外処理が適切に行われているかを検証します。

以下はxUnitを使った単体テストの例です。

using System.IO;
using System.IO.Compression;
using Xunit;
public class GZipTests
{
    [Fact]
    public void CompressAndDecompress_ShouldReturnOriginalData()
    {
        byte[] originalData = System.Text.Encoding.UTF8.GetBytes("テストデータです。");
        // 圧縮
        byte[] compressedData;
        using (var outputStream = new MemoryStream())
        {
            using (var gzipStream = new GZipStream(outputStream, CompressionMode.Compress))
            {
                gzipStream.Write(originalData, 0, originalData.Length);
            }
            compressedData = outputStream.ToArray();
        }
        Assert.True(compressedData.Length < originalData.Length);
        // 解凍
        byte[] decompressedData;
        using (var inputStream = new MemoryStream(compressedData))
        using (var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress))
        using (var resultStream = new MemoryStream())
        {
            gzipStream.CopyTo(resultStream);
            decompressedData = resultStream.ToArray();
        }
        Assert.Equal(originalData, decompressedData);
    }
}

このテストでは、圧縮後のサイズが元より小さいことと、解凍後のデータが元データと完全に一致することを検証しています。

ゴールデンファイル比較の手法

ゴールデンファイル比較は、圧縮・解凍処理の結果を既知の正しいファイル(ゴールデンファイル)と比較するテスト手法です。

特にファイル単位の圧縮処理で有効です。

手順は以下の通りです。

  1. 正しい圧縮ファイル(ゴールデンファイル)を用意

事前に信頼できる方法で作成した圧縮ファイルをテストリソースとして保存します。

  1. テスト実行時に圧縮処理を行い、生成ファイルを取得

テスト対象の圧縮処理でファイルを生成します。

  1. 生成ファイルとゴールデンファイルをバイト単位で比較

ファイルの内容が完全に一致するかを検証します。

  1. 差異があればテスト失敗とし、問題を検出

以下はファイル比較の例です。

using System.IO;
using Xunit;
public class GoldenFileTests
{
    [Fact]
    public void CompressedFile_ShouldMatchGoldenFile()
    {
        string inputFile = "testdata/input.txt";
        string outputFile = "testdata/output.gz";
        string goldenFile = "testdata/golden_output.gz";
        // 圧縮処理(テスト対象)
        using (var inputStream = new FileStream(inputFile, FileMode.Open))
        using (var outputStream = new FileStream(outputFile, FileMode.Create))
        using (var gzipStream = new GZipStream(outputStream, CompressionMode.Compress))
        {
            inputStream.CopyTo(gzipStream);
        }
        byte[] actual = File.ReadAllBytes(outputFile);
        byte[] expected = File.ReadAllBytes(goldenFile);
        Assert.Equal(expected, actual);
    }
}

ゴールデンファイル比較は、圧縮アルゴリズムのバージョンアップや環境差異による出力の違いを検知するのに役立ちます。

ただし、圧縮結果が環境や圧縮レベルで変わる場合は、柔軟な比較方法を検討してください。

CIでの自動検証パイプライン

継続的インテグレーション(CI)環境で圧縮・解凍機能のテストを自動化することは、品質維持に不可欠です。

CIパイプラインに単体テストやゴールデンファイル比較を組み込み、コード変更時に自動で検証を行います。

ポイントは以下の通りです。

  • テストの自動実行

GitHub Actions、Azure DevOps、JenkinsなどのCIツールでビルド後にテストを実行し、結果をレポートします。

  • テストデータの管理

ゴールデンファイルやテスト用データはリポジトリに含めるか、アーティファクトとして管理し、CI環境で利用可能にします。

  • 失敗時の通知

テスト失敗時に開発チームに通知し、早期に問題を修正できる体制を整えます。

  • パフォーマンステストの組み込み

可能であれば、ベンチマークやパフォーマンステストもCIに組み込み、性能劣化を検知します。

以下はGitHub Actionsでの簡単なテスト実行例(dotnet testを使う場合)です。

name: .NET Test
on: [push, pull_request]
jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:

    - uses: actions/checkout@v2
    - name: Setup .NET

      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: '7.0.x'

    - name: Restore dependencies

      run: dotnet restore

    - name: Build

      run: dotnet build --no-restore --configuration Release

    - name: Test

      run: dotnet test --no-build --verbosity normal

このようにCIで自動テストを回すことで、圧縮・解凍機能の品質を継続的に保証し、リリース前の不具合を減らせます。

圧縮後サイズが増える原因

GZip圧縮を行ったにもかかわらず、圧縮後のファイルサイズが元のファイルより大きくなることがあります。

これは以下のような理由が考えられます。

  • 元データがすでに圧縮済みである

JPEG画像やMP3音声、動画ファイルなどは既に圧縮されているため、さらにGZipで圧縮しても効果が薄く、逆に圧縮ヘッダーなどのオーバーヘッドでサイズが増えることがあります。

  • 小さなファイルやランダムデータ

非常に小さいファイルやランダムなバイナリデータは圧縮効率が悪く、圧縮処理のメタ情報が加わることでサイズが増加します。

  • 圧縮レベルの設定

CompressionLevel.Fastestなど圧縮率を抑えた設定では、圧縮効率が低くなるためサイズが増える場合があります。

  • バッファサイズや処理方法の影響

不適切なバッファサイズやストリームの使い方で、余計なデータが含まれることもあります。

対策としては、圧縮対象のファイル種別を考慮し、すでに圧縮済みのファイルは圧縮処理をスキップするか、圧縮レベルを調整することが有効です。

日本語ファイル名の扱い

GZip形式は単一ファイルの圧縮に特化しており、ファイル名をヘッダーに含めることができますが、日本語ファイル名の扱いには注意が必要です。

  • エンコーディングの違い

GZipヘッダーのファイル名は基本的にISO-8859-1(Latin-1)でエンコードされるため、日本語などのマルチバイト文字は正しく保存・復元されないことがあります。

  • 環境依存の挙動

WindowsのGZipStreamはファイル名を扱いません。

Unix系のgzipコマンドはファイル名を保存しますが、文字コードの違いで文字化けが起こることがあります。

  • 対策
    • ファイル名をASCII文字に限定する
    • ファイル名を別途メタデータとして管理する
    • ZIP形式など多言語対応が充実したアーカイブ形式を使う

GZipは単一ファイル圧縮に向いているため、多言語ファイル名を扱う場合はZIPやtar.gzなどの複合形式を検討してください。

マルチスレッド環境での同期

マルチスレッド環境でGZipStreamを使う場合、スレッドセーフではないため注意が必要です。

  • GZipStreamはスレッドセーフではない

同じGZipStreamインスタンスを複数スレッドから同時に読み書きすると、データ破損や例外が発生します。

  • 対策
    • 各スレッドで独立したGZipStreamインスタンスを作成する
    • ストリームへのアクセスをlockなどで排他制御する
    • 並列処理はファイルやデータの分割単位で行い、スレッド間でストリームを共有しない
  • パフォーマンス向上の工夫

大きなファイルを複数チャンクに分割し、各チャンクを別スレッドで圧縮・解凍してから結合する方法があります。

これによりCPUコアを有効活用しつつ、スレッド間の競合を避けられます。

まとめると、マルチスレッド環境でのGZipStream利用は、インスタンスの共有を避け、適切な同期や分割処理を行うことが安定動作の鍵となります。

まとめ

この記事では、C#のGZipStreamを使ったファイル圧縮・解凍の基本から応用、非同期処理や大容量データ対応、セキュリティ対策、テスト戦略まで幅広く解説しました。

効率的なストリーム操作やパフォーマンス測定、クロスプラットフォームでの互換性、ASP.NETやクライアントサイドでの活用方法も紹介しています。

これらを活用することで、高速かつ安全なGZip圧縮機能を実装し、実務でのトラブルを防ぎながら最適化が可能になります。

関連記事

Back to top button
目次へ