繁体   English   中英

如何从indy TCP的输入缓冲区中可靠地提取多个数据包?

[英]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.

TCP线程代码

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读取代码存在两个主要问题:

  1. 你是不是保证InputBuffer实际上有MsgSize之前调用可用的字节数ExtractToBytes()的第二次。 如果您尝试提取的字节数多于缓冲区中的实际字节数,则ExtractToBytes()会引发异常。

  2. 更重要的是,在每次调用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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM