分散式系統中,WCF 傳輸大量 XML/序列化資料時,網路頻寬常成瓶頸。理想方案是在通道層自動壓縮/解壓,對上層業務代碼完全透明,無需修改契約、序列化邏輯。本文詳細講解如何實現自訂 MessageEncoder 掛載到 WCF 通道堆疊,實現 GZip 壓縮傳輸。
發送端:業務對象 → 序列化 → XML/二進制 → 【壓縮編碼器】 → 位元組陣列 → 傳輸層 (TCP/HTTP) 接收端:傳輸層 → 位元組陣列 → 【解壓編碼器】 → XML/二進制 → 反序列化 → 業務對象
關鍵點:壓縮編碼器包裝內建編碼器 (如 BinaryMessageEncoder、TextMessageEncoder),在 WriteMessage 時壓縮輸出緩衝區,ReadMessage 時解壓輸入緩衝區。
| 類別 | 繼承/介面 | 職責 |
|---|---|---|
CompactMessageEncoder | MessageEncoder | 核心邏輯:WriteMessage 壓縮、ReadMessage 解壓、包裝內建編碼器 |
CompactMessageEncoderFactory | MessageEncoderFactory | 創建 CompactMessageEncoder 實例 |
CompactMessageEncodingBindingElement | BindingElement | 定義綁定元素,加入通道堆疊 |
CompactMessageEncodingElement | BindingElementExtensionElement | 支援 app.config/web.config 宣式配置 |
WriteMessage 內部調用)
private static ArraySegment<byte> CompressBuffer(ArraySegment<byte> buffer, BufferManager bufferManager, int messageOffset)
{
// 1. 建立記憶體流,先寫入不壓縮的前綴 (如訊息版本標頭)
var memoryStream = new MemoryStream();
memoryStream.Write(buffer.Array, 0, messageOffset);
// 2. GZip 壓縮訊息本體
using (var gzStream = new GZipStream(memoryStream, CompressionMode.Compress, true))
gzStream.Write(buffer.Array, messageOffset, buffer.Count - messageOffset);
// 3. 轉換為位元組陣列,分配新緩衝區
byte[] compressedBytes = memoryStream.ToArray();
byte[] bufferedBytes = bufferManager.TakeBuffer(compressedBytes.Length);
Array.Copy(compressedBytes, 0, bufferedBytes, 0, compressedBytes.Length);
// 4. 釋放原緩衝區,返回新 ArraySegment
bufferManager.ReturnBuffer(buffer.Array);
return new ArraySegment<byte>(bufferedBytes, messageOffset, compressedBytes.Length - messageOffset);
}
ReadMessage 內部調用)
private static ArraySegment<byte> DecompressBuffer(ArraySegment<byte> buffer, BufferManager bufferManager)
{
// 1. 輸入緩衝區包含完整壓縮資料 (含前綴)
var compressedStream = new MemoryStream(buffer.Array, buffer.Offset, buffer.Count);
var decompressedStream = new MemoryStream();
byte[] tempBuffer = bufferManager.TakeBuffer(1024);
// 2. 串流式解壓
using (var gzStream = new GZipStream(compressedStream, CompressionMode.Decompress)) {
int bytesRead;
while ((bytesRead = gzStream.Read(tempBuffer, 0, tempBuffer.Length)) > 0)
decompressedStream.Write(tempBuffer, 0, bytesRead);
}
bufferManager.ReturnBuffer(tempBuffer);
// 3. 組裝解壓後緩衝區 (保留前綴位移)
byte[] decompressedBytes = decompressedStream.ToArray();
byte[] resultBuffer = bufferManager.TakeBuffer(decompressedBytes.Length + buffer.Offset);
Array.Copy(buffer.Array, 0, resultBuffer, 0, buffer.Offset); // 複製前綴
Array.Copy(decompressedBytes, 0, resultBuffer, buffer.Offset, decompressedBytes.Length); // 複製解壓資料
bufferManager.ReturnBuffer(buffer.Array);
return new ArraySegment<byte>(resultBuffer, buffer.Offset, decompressedBytes.Length);
}
<system.serviceModel>
<services>
<service name="Server.MyService">
<!-- 關鍵:binding 改為 customBinding -->
<endpoint address="net.tcp://localhost:1234/MyService"
binding="customBinding"
contract="Server.IMyService" />
</service>
</services>
<bindings>
<customBinding>
<binding name="compactBinding">
<!-- 壓縮編碼器包裝二進制編碼器 -->
<compactMessageEncoding>
<binaryMessageEncoding />
</compactMessageEncoding>
<tcpTransport />
</binding>
</customBinding>
</bindings>
<extensions>
<bindingElementExtensions>
<add name="compactMessageEncoding"
type="Amib.WCF.CompactMessageEncodingElement, CompactMessageEncoder, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</bindingElementExtensions>
</extensions>
</system.serviceModel>
<system.serviceModel>
<client>
<endpoint address="net.tcp://localhost:1234/MyService"
binding="customBinding"
bindingConfiguration="compactBinding"
contract="Client.IMyService" />
</client>
<bindings>
<customBinding>
<binding name="compactBinding">
<compactMessageEncoding>
<binaryMessageEncoding />
</compactMessageEncoding>
<tcpTransport />
</binding>
</customBinding>
</bindings>
<extensions> ... (同伺服器端) ... </extensions>
</system.serviceModel>
| 指標 | 無壓縮 | GZip 壓縮 | 說明 |
|---|---|---|---|
| 頻寬佔用 | 基準 | 降低 60-85% | XML/JSON 文本壓縮比極高 |
| CPU 開銷 | 無 | 壓縮/解壓消耗 CPU | 現代 CPU 單核可達 200-500 MB/s 壓縮吞吐 |
| 延遲增加 | 0 | < 1ms (小訊息) | 大訊息網路傳輸時間節省遠大於壓縮時間 |
| 適用訊息大小 | - | > 1 KB | 極小訊息壓縮開銷可能大於收益 |
TransferMode.Streamed 時,訊息編碼器不參與,需改用 Stream 包裝器 (另文介紹)。
// 在 CompactMessageEncodingBindingElement 擴展屬性
public int CompressionThreshold { get; set; } = 1024; // 預設 1KB
// CompactMessageEncoder.WriteMessage 判斷
if (buffer.Count - messageOffset > CompressionThreshold)
return CompressBuffer(...);
else
return base.WriteMessage(...); // 直接用內建編碼器
配置檔中即可調整:<compactMessageEncoding compressionThreshold="2048">...
MessageEncoder 是 WCF 透明壓縮的正規做法,核心在 WriteMessage/ReadMessage 緩衝區轉換。binaryMessageEncoding) 保持序列化格式不變,僅壓縮位元組載荷。BufferManager 正確管理緩衝區生命週期,避免記憶體洩漏與過度分配。