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

时间:2014-01-28 18:36:39

标签: multithreading delphi tcp network-programming indy

我有一段代码,当许多数据包几乎同时从同一来源到达时,大部分时间都会跳过数据包。这些数据包的构建方式是,数据包大小字段在开头附加,后面有数据包大小,以字节为单位。

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;

如何确保零丢包?

1 个答案:

答案 0 :(得分:2)

TCP读取代码存在两个主要问题:

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

  2. 更重要的是,在每次调用Buffer之前,您没有将ExtractToBytes()变量的大小调整为0。在第一次循环迭代中第一次调用之后,Buffer的长度为4个字节。如果该消息的大小小于4个字节,则会在Buffer末尾留下随机字节,这些字节将被传递给您的解析器,并可能破坏其逻辑。但更糟糕的是,如果缓冲区中有另一个消息大小,则下一次循环迭代会对ExtractToBytes()进行第3次调用,将这4个字节追加到现有Buffer的末尾内容,替换您所假设的内容(默认情况下AAppend ExtractToBytes()参数为True)。因此,您最终将前一个消息数据中的4个字节复制到MsgSize变量中,而不是刚刚提取的新4个字节,因此您在下一个{{1}上使用了损坏的MsgSize值} call。

  3. 由于您的数据包是以长度为前缀的,因此您无需使用ExtractToBytes()或直接访问CheckForDataOnSource()。使用以下代码让Indy为您完成工作:

    InputBuffer

    默认情况下,这将阻止调用者,直到数据可供读取。如果在没有准备好读取数据的情况下需要退出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

    这种方法唯一的问题是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