Indy (TIdTCPClient/TIdTCPServer) 傳輸串流資料時,關鍵在於雙端協議一致:發送端如何寫入長度與資料,接收端如何讀取長度與資料。本文詳細比較兩種主流模式:單一串流+長度前綴與連線關閉觸發讀取,並解析 OpenWriteBuffer/CloseWriteBuffer 緩衝區機制。
發送端先寫入 4-byte 整數長度,再寫入串流內容;接收端用 ReadStream 自動讀取長度再讀資料。
procedure TClientForm.BtnSendStreamClick(Sender: TObject);
var
Strm: TMemoryStream;
S: string;
begin
S := 'Hello Indy Stream!';
Strm := TStringStream.Create(S, TEncoding.UTF8);
try
// 1. 開啟寫入緩衝 (合併多次 Write 為單次發送,減少封包碎片)
IdTCPClient1.IOHandler.OpenWriteBuffer;
try
// 2. 寫入長度 (ReadStream 預期前 4 bytes 為長度)
IdTCPClient1.IOHandler.WriteInteger(Strm.Size);
// 3. 寫入串流內容,True = 寫入後自動 Free 串流 (這裡手動管理故 False)
IdTCPClient1.IOHandler.Write(Strm, 0, False);
finally
// 4. 關閉緩衝,實際發送
IdTCPClient1.IOHandler.CloseWriteBuffer;
end;
finally
Strm.Free;
end;
end;
procedure TServerForm.IdTCPServer1Execute(AContext: TIdContext);
var
Strm: TMemoryStream;
begin
// 檢查連線狀態
if AContext.Connection.Connected then
begin
Strm := TMemoryStream.Create;
try
// 關鍵:ReadStream 會自動讀取前 4 bytes 作為長度,再讀取指定位元組數
AContext.Connection.IOHandler.ReadStream(Strm);
Strm.Position := 0;
// 處理接收資料
Memo1.Lines.LoadFromStream(Strm, TEncoding.UTF8);
finally
Strm.Free;
end;
end;
end;
發送端持續寫入串流,不寫入長度;接收端在連線關閉時一次讀取所有資料 (ASize=-1, AReadUntilDisconnect=True)。
procedure TClientForm.BtnSendMultiStreamClick(Sender: TObject);
var
Strm: TMemoryStream;
I: Integer;
begin
IdTCPClient1.Connect;
try
for I := 1 to 3 do
begin
Strm := TStringStream.Create('Data Block ' + IntToStr(I), TEncoding.UTF8);
try
IdTCPClient1.IOHandler.OpenWriteBuffer;
try
// 不寫入長度,直接寫入串流
IdTCPClient1.IOHandler.Write(Strm, 0, True); // True = 寫入後 Free 串流
finally
IdTCPClient1.IOHandler.CloseWriteBuffer;
end;
except
Strm.Free;
raise;
end;
end;
finally
IdTCPClient1.Disconnect; // 關閉連線觸發伺服端讀取結束
end;
end;
procedure TServerForm.IdTCPServer1Execute(AContext: TIdContext);
var
Strm: TMemoryStream;
begin
if AContext.Connection.Connected then
begin
Strm := TMemoryStream.Create;
try
// ASize=-1 表示讀到連線關閉;AReadUntilDisconnect=True 等待斷開
AContext.Connection.IOHandler.ReadStream(Strm, -1, True);
Strm.Position := 0;
Memo1.Lines.LoadFromStream(Strm, TEncoding.UTF8);
finally
Strm.Free;
end;
end;
end;
| 方法 | 簽名 | 行為 |
|---|---|---|
WriteInteger | WriteInteger(Value: Integer) | 寫入 4-byte Little Endian 整數 (網路序需自行轉換) |
Write(Stream, ASize, AFree) | Write(const AStream: TStream; ASize: Integer = 0; AFree: Boolean = False) | 寫入串流內容。ASize=0 表示寫入全部。AFree=True 寫入後 Free 串流 |
ReadStream | ReadStream(ADest: TStream; ASize: Integer = -1; AReadUntilDisconnect: Boolean = False) | 讀取串流。ASize=-1 讀到斷開。若前 4 bytes 為長度,自動解析並讀取指定量 |
OpenWriteBuffer | OpenWriteBuffer | 啟用寫入緩衝,後續 Write 累積在記憶體 |
CloseWriteBuffer | CloseWriteBuffer | 刷新緩衝區一次性發送,減少 TCP 封包碎片 |
| 問題 | 原因 | 解決 |
|---|---|---|
| 接收端卡住/逾時 | 發送端未寫入長度,接收端用預設 ReadStream 等待長度前綴 | 統一協議:雙端都用長度前綴,或雙端都用連線關閉模式 |
| 大檔案記憶體暴漲 | 用 TMemoryStream 接收 GB 級檔案 | 改用 TFileStream 直接寫入磁碟,或分塊處理 |
| 中文字串亂碼 | 編碼不一致 | 雙端明確指定 TEncoding.UTF8 (或約定編碼) |
| WriteStream 後資料未送達 | 未呼叫 CloseWriteBuffer | 務必成對呼叫 Open/CloseWriteBuffer |
| 伺服端 Execute 重複執行 | 非阻塞模式下 Execute 會輪詢 | 使用 AContext.Connection.IOHandler.CheckForDataOnSource 或改用 OnExecute 事件配合阻塞讀取 |
type
TStreamHeader = packed record
Magic: DWORD; // $DE1DSTREAM
CmdType: Word; // 指令類型
Flags: Word; // 壓縮/加密旗標
DataLen: Int64; // 資料長度 (8 bytes 支援 >4GB)
CRC32: DWORD; // 校驗和
end;
// 發送端
procedure SendStreamWithHeader(IO: TIdIOHandler; Strm: TStream; Cmd: Word);
var
Hdr: TStreamHeader;
begin
Hdr.Magic := $DE1DSTREAM;
Hdr.CmdType := Cmd;
Hdr.Flags := 0;
Hdr.DataLen := Strm.Size;
Hdr.CRC32 := CalculateCRC32(Strm); // 需先計算或邊算邊寫
IO.OpenWriteBuffer;
try
IO.WriteBuffer(@Hdr, SizeOf(Hdr));
IO.Write(Strm, 0, False);
finally
IO.CloseWriteBuffer;
end;
end;
// 接收端
function RecvStreamWithHeader(IO: TIdIOHandler; out Cmd: Word): TMemoryStream;
var
Hdr: TStreamHeader;
begin
Result := TMemoryStream.Create;
try
IO.ReadBuffer(@Hdr, SizeOf(Hdr));
if Hdr.Magic <> $DE1DSTREAM then
raise Exception.Create('Invalid stream header');
Cmd := Hdr.CmdType;
IO.ReadStream(Result, Hdr.DataLen, False);
// 驗證 CRC32...
except
Result.Free;
raise;
end;
end;
Indy 串流傳輸的核心在於協議對稱性。模式一 (長度前綴) 適合指令/回應、小型資料、需即時處理的場景;模式二 (連線關閉) 適合大檔案單向傳輸、串流媒體。生產環境強烈建議採用自訂幀頭結構,包含魔數、版本、指令類型、長度、校驗和,構建健壯、可擴展的網路協議。