[英]How to reliably extract multiple packets from indy TCP's input buffer?
我有一段代码,当许多数据包几乎同时从同一个源到达时,大部分时间都会跳过数据包。 构建分组使得在开始时附加分组大小字段,其具有在其之后的分组的大小(以字节为单位)。
TCP客户端线程以10毫秒的间隔运行。
unit AgThread11;
interface
uses
SysUtils, Classes, Windows, Rtti;
type
TAgThreadMethod1 = procedure of object;
TAgThreadMethod2 = procedure;
TAgThread = class ( TThread )
private
fInterval : Cardinal;
fTerminateEvent : THandle;
fRun : Boolean;
fOnRun1 : TAgThreadMethod1;
fOnRun2 : TAgThreadMethod2;
protected
procedure Execute; override;
public
constructor Create
( const AOnRun: TAgThreadMethod1; const AInterval: Cardinal;
const ARun: Boolean = True ); overload;
constructor Create
( const AOnRun: TAgThreadMethod2; const AInterval: Cardinal;
const ARun: Boolean = True ); overload;
destructor Destroy; override;
procedure Signal;
property Run : Boolean read fRun write fRun;
property Interval : Cardinal read fInterval write fInterval;
property OnRun1 : TAgThreadMethod1 read fOnRun1 write fOnRun1;
property OnRun2 : TAgThreadMethod2 read fOnRun2 write fOnRun2;
end;
implementation
constructor TAgThread.Create
( const AOnRun: TAgThreadMethod1; const AInterval: Cardinal;
const ARun: Boolean = True );
begin
fTerminateEvent := CreateEvent ( nil, TRUE, FALSE, nil );
fInterval := AInterval;
fRun := ARun;
fOnRun1 := AOnRun;
inherited Create ( False );
end;
constructor TAgThread.Create
( const AOnRun: TAgThreadMethod2; const AInterval: Cardinal;
const ARun: Boolean = True );
begin
fTerminateEvent := CreateEvent ( nil, TRUE, FALSE, nil );
fInterval := AInterval;
fRun := ARun;
fOnRun2 := AOnRun;
inherited Create ( False );
end;
destructor TAgThread.Destroy;
begin
Terminate;
Signal;
WaitFor;
inherited;
end;
procedure TAgThread.Signal;
begin
SetEvent ( FTerminateEvent );
end;
procedure TAgThread.Execute;
begin
while not Terminated do
begin
if fRun then
if Assigned ( fOnRun1 ) then fOnRun1
else if Assigned ( fOnRun2 ) then fOnRun2;
WaitForSingleObject ( FTerminateEvent, fInterval );
end;
end;
end.
procedure TForm1.THEX_TCP;
var
Buffer : TBytes;
MsgSize : Integer;
begin
if TCPClient.IOHandler.CheckForDataOnSource then
begin
while TCPClient.IOHandler.InputBuffer.Size >= 4 do
begin
fRXCount := fRXCount + 1;
TCPClient.IOHandler.InputBuffer.ExtractToBytes ( Buffer, 4 );
Move ( Buffer [0], MsgSize, 4 );
TCPClient.IOHandler.InputBuffer.ExtractToBytes ( Buffer, MsgSize, False );
NAT.RecievedNATData ( Buffer ); // Packet Processor
end;
end;
end;
我该怎么做才能确保零丢包?
TCP读取代码存在两个主要问题:
你是不是保证InputBuffer
实际上有MsgSize
之前调用可用的字节数ExtractToBytes()
的第二次。 如果您尝试提取的字节数多于缓冲区中的实际字节数,则ExtractToBytes()
会引发异常。
更重要的是,在每次调用ExtractToBytes()
之前,不会将Buffer
变量的大小调整为0。 在第一次循环迭代中的第一次调用之后, Buffer
的长度是4个字节。 如果该消息的大小小于4个字节,则会在Buffer
末尾留下随机字节,这些字节将被传递给您的解析器,并可能破坏其逻辑。 但更糟糕的是,如果缓冲区中有另一个消息大小,则下一个循环迭代会对ExtractToBytes()
进行第三次调用, 并将这4个字节追加到现有Buffer
内容的末尾, 而不是像您假设的那样替换内容( AAppend
默认情况下, ExtractToBytes()
参数为True。 因此,您最终将前一个消息数据中的4个字节复制到MsgSize
变量中,而不是刚刚提取的新4个字节,因此您在下一个ExtractToBytes()
调用中使用了损坏的MsgSize
值。
因为您的数据包是长度前缀的,所以您根本不需要使用CheckForDataOnSource()
或直接访问InputBuffer
。 使用以下代码,让Indy为您完成工作:
procedure TForm1.THEX_TCP;
var
Buffer : TBytes;
MsgSize : Integer;
begin
MsgSize := TCPClient.IOHandler.ReadLongInt;
TCPClient.IOHandler.ReadBytes(Buffer, MsgSize);
Inc(fRXCount);
NAT.RecievedNATData(Buffer);
end;
默认情况下,这将阻止调用者,直到数据可供读取。 如果在没有准备好读取数据的THEX_TCP
需要退出THEX_TCP
,请使用以下内容:
procedure TForm1.THEX_TCP;
var
Buffer : TBytes;
MsgSize : Integer;
begin
if TCPClient.IOHandler.InputBufferIsEmpty then
begin
TCPClient.IOHandler.CheckForDataOnSource;
TCPClient.IOHandler.CheckForDisconnect;
if TCPClient.IOHandler.InputBufferIsEmpty then Exit;
end;
repeat
MsgSize := TCPClient.IOHandler.ReadLongInt;
TCPClient.IOHandler.ReadBytes(Buffer, MsgSize);
Inc(fRXCount);
NAT.RecievedNATData(Buffer);
SetLength(Buffer, 0);
until TCPClient.IOHandler.InputBufferIsEmpty;
end;
这种方法唯一的问题是ReadLongInt()
和ReadBytes()
可能会在InputBuffer
读取更多字节,因此如果在很短的时间内发送大量数据,您的循环可能会运行很长时间。 如果您绝对必须一次只读取一个缓冲区并且只处理完整的消息,那么使用以下内容:
procedure TForm1.THEX_TCP;
var
MsgSizeBuffer: array[0..3] of Byte;
MsgSize, I : Integer;
Buffer : TBytes;
begin
TCPClient.IOHandler.CheckForDataOnSource;
TCPClient.IOHandler.CheckForDisconnect;
while TCPClient.IOHandler.InputBuffer.Size >= 4 do
begin
// unfortunately, TIdBuffer does not have a way to peek
// multiple bytes at a time without removing them
for I := 0 to 3 do
MsgSizeBuffer[I] := TCPClient.IOHandler.InputBuffer.Peek(I);
Move(MsgSizeBuffer[0], MsgSize, 4);
MsgSize := LongInt(GStack.NetworkToHost(LongWord(MsgSize)));
if TCPClient.IOHandler.InputBuffer.Size < (4+MsgSize) then
Break;
TCPClient.IOHandler.InputBuffer.Remove(4);
TCPClient.IOHandler.InputBuffer.ExtractToBytes(Buffer, MsgSize);
Inc(fRXCount);
NAT.RecievedNATData(Buffer);
SetLength(Buffer, 0);
end;
end;
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.