使用TIdTcpClient接收超时的部分数据

Pau*_*aul 1 delphi indy

如何使用TIdTcpClient接收具有以下条件的100字节字符串?:

  • 如果没有任何内容,Read调用将被阻塞,线程将永远等待
  • 如果接收到100个字节,则Read调用应返回字节字符串
  • 如果接收到超过0个字节但少于100个字节,则读取调用应该在一些超时(比如1秒)后返回,以便在合理的时间内返回至少某些内容,而不会产生超时异常,因为Delphi IDE的调试模式中的异常处理没有方便.

我现在的最佳代码如下:

unit Unit2;

interface

uses
  System.Classes, IdTCPClient;

type
  TTcpReceiver = class(TThread)
  private
    _tcpc: TIdTCPClient;
    _onReceive: TGetStrProc;
    _buffer: AnsiString;
    procedure _receiveLoop();
    procedure _postBuffer;
  protected
    procedure Execute(); override;
  public
    constructor Create(); reintroduce;
    destructor Destroy(); override;
    property OnReceive: TGetStrProc read _onReceive write _onReceive;
  end;

implementation

uses
  System.SysUtils, Vcl.Dialogs, IdGlobal, IdExceptionCore;

constructor TTcpReceiver.Create();
begin
  inherited Create(True);
  _buffer := '';
  _tcpc := TIdTCPClient.Create(nil);
  //_tcpc.Host := '192.168.52.175';
  _tcpc.Host := '127.0.0.1';
  _tcpc.Port := 1;
  _tcpc.ReadTimeout := 1000;
  _tcpc.Connect();
  Suspended := False;
end;

destructor TTcpReceiver.Destroy();
begin
  _tcpc.Disconnect();
  FreeAndNil(_tcpc);
  inherited;
end;

procedure TTcpReceiver.Execute;
begin
  _receiveLoop();
end;

procedure TTcpReceiver._postBuffer();
var buf: string;
begin
  if _buffer = '' then Exit;
  buf := _buffer;
  _buffer := '';
  if Assigned(_onReceive) then begin
    Synchronize(
      procedure()
      begin
        _onReceive(buf);
      end
    );
  end;
end;

procedure TTcpReceiver._receiveLoop();
var
  c: AnsiChar;
begin
  while not Terminated do begin
    try
      c := AnsiChar(_tcpc.IOHandler.ReadByte());
      _buffer := _buffer + c;
      if Length(_buffer) > 100 then
        _postBuffer();
    except
      //Here I have to ignore EIdReadTimeout in Delphi IDE everywhere, but I want just to ignore them here
      on ex: EIdReadTimeout do _postBuffer();
    end;
  end;
end;

end.
Run Code Online (Sandbox Code Playgroud)

Rem*_*eau 5

TCP是面向流的,而不像UDP那样面向消息.读取没有任何结构的任意字节是不好的设计,如果你过早地停止读取,然后在你停止阅读后想要读取的字节到达,很容易破坏你的通信.在读取字节之前,不会从套接字中删除字节,因此下一次读取可能具有比预期更多/不同的字节.

如果您期望100个字节,那么只需读取100个字节即可完成.如果发送方只发送50个字节,则需要提前告诉您,以便在收到50个字节后停止读取.如果发送者没有这样做,那么这是一个设计很差的协议.通常使用超时来检测传输结束是糟糕的设计.网络滞后很容易导致错误检测.

TCP消息应该有足够的框架,以便接收者确切地知道一条消息的结束位置和下一条消息的开始.在TCP中有三种方法可以做到这一点:

  1. 使用固定长度的消息.接收器可以继续读取,直到达到预期的字节数.

  2. 在发送消息之前发送消息的长度.接收器可以先读取长度,然后继续读取,直到达到指定的字节数.

  3. 终止具有未出现在消息数据中的唯一分隔符的消息.接收器可以继续读取字节,直到分隔符到达.


话虽这么说,你要求的可以在TCP中完成(但应该在TCP中完成!).它可以在不使用手动缓冲区的情况下完成,而是使用Indy的内置缓冲区.例如:

unit Unit2;

interface

uses
  System.Classes, IdTCPClient;

type
  TTcpReceiver = class(TThread)
  private
    _tcpc: TIdTCPClient;
    _onReceive: TGetStrProc;
    procedure _receiveLoop;
    procedure _postBuffer;
  protected
    procedure Execute; override;
  public
    constructor Create; reintroduce;
    destructor Destroy; override;
    property OnReceive: TGetStrProc read _onReceive write _onReceive;
  end;

implementation

uses
  System.SysUtils, Vcl.Dialogs, IdGlobal;

constructor TTcpReceiver.Create;
begin
  inherited Create(False);
  _tcpc := TIdTCPClient.Create(nil);
  //_tcpc.Host := '192.168.52.175';
  _tcpc.Host := '127.0.0.1';
  _tcpc.Port := 1;
end;

destructor TTcpReceiver.Destroy;
begin
  _tcpc.Free;
  inherited;
end;

procedure TTcpReceiver.Execute;
begin
  _tcpc.Connect;
  try
    _receiveLoop;
  finally
    _tcpc.Disconnect;
  end;
end;

procedure TTcpReceiver._postBuffer;
var
  buf: string;
begin
  with _tcpc.IOHandler do
    buf := ReadString(IndyMin(InputBuffer.Size, 100));
  { alternatively:
  with _tcpc.IOHandler.InputBuffer do
    buf := ExtractToString(IndyMin(Size, 100));
  }
  if buf = '' then Exit;
  if Assigned(_onReceive) then
  begin
    Synchronize(
      procedure
      begin
        if Assigned(_onReceive) then
          _onReceive(buf);
      end
    );
  end;
end;

procedure TTcpReceiver._receiveLoop;
var
  LBytesRecvd: Boolean;
begin
  while not Terminated do
  begin
    while _tcpc.IOHandler.InputBufferIsEmpty do
    begin
      _tcpc.IOHandler.CheckForDataOnSource(IdTimeoutInfinite);
      _tcpc.IOHandler.CheckForDisconnect;
    end;

    while _tcpc.IOHandler.InputBuffer.Size < 100 do
    begin
      // 1 sec is a very short timeout to use for TCP.
      // Consider using a larger timeout...
      LBytesRecvd := _tcpc.IOHandler.CheckForDataOnSource(1000);
      _tcpc.IOHandler.CheckForDisconnect;
      if not LBytesRecvd then Break;
    end;

    _postBuffer;
  end;
end;

end.
Run Code Online (Sandbox Code Playgroud)

在旁注中,您的声明" Delphi IDE的调试模式中的异常处理不方便 "简直荒谬.Indy的IOHandler有控制的异常行为的属性和方法的参数,并且如果你不喜欢调试器处理异常的方式则只需配置它忽略他们.您可以将调试器配置为忽略特定的异常类型,或者可以使用断点来告诉调试器跳过处理特定代码块中的异常.

  • @Paul Indy专为处理基于分隔符的文本协议而设计.IOHandler有`WaitFor()`和`ReadLn()`方法,以及`ReadLnTimedOut`属性.你可以`WaitFor`开始字符,丢弃它返回的任何内容,然后`ReadLn`以下字符并检查校验和,然后重复. (2认同)