简体   繁体   English

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

[英]Receive partial data with timeout using TIdTcpClient

How to receive a 100-byte-string with following conditions using TIdTcpClient ?: 如何使用TIdTcpClient在以下条件下接收100字节的字符串?

  • If nothing comes in, the Read call shall be blocking and thread will wait eternally 如果什么也没有发生,则Read调用将被阻塞,线程将永远等待
  • If 100 bytes were received the Read call should return the byte string 如果接收到100个字节,则Read调用应返回字节字符串
  • If more than 0 bytes, but less than 100 were received, Read call should return after some timeout (say 1 second) in order to return at least something in a reasonable time, without producing timeout exception, because exception handling in Delphi IDE's debug mode hasn't been made convenient. 如果接收到的字节数多于0,但少于100,则应在一定的超时时间(例如1秒)后返回Read调用,以便在合理的时间内至少返回某些内容,而不会产生超时异常,因为在Delphi IDE的调试模式下会进行异常处理还没有变得方便。

My not optimal code for now is as follows: 我目前不是最佳的代码如下:

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.

TCP is stream oriented, not message oriented like UDP is. TCP是面向流的,而不是像UDP那样面向消息的。 Reading arbitrary bytes without any structure to them is bad design, and will easily corrupt your communications if you stop reading prematurely and then the bytes you wanted to read arrive after you have stopped reading. 读取任意字节而没有任何结构的结构是不好的设计,如果过早停止读取,然后在停止读取后想要读取的字节到达,则很容易破坏您的通信。 The bytes are not removed from the socket until they are read, so the next read may have more/different bytes than expected. 在读取字节之前,不会从套接字中删除这些字节,因此下一次读取的字节可能比预期的更多/不同。

If you are expecting 100 bytes, then just read 100 bytes and be done with it. 如果您期望100个字节,那么只需读取100个字节并完成操作即可。 If the sender only sends 50 bytes, it needs to tell you that ahead of time so you can stop reading after 50 bytes are received. 如果发送方仅发送50个字节,则需要提前告知您,这样您就可以在收到50个字节后停止读取。 If the sender is not doing that, then this is a very poorly designed protocol. 如果发件人没有这样做,那么这是一个设计不良的协议。 Using a timeout to detect end-of-transmission in general is bad design. 通常,使用超时来检测传输结束是不好的设计。 Network lag could easily cause false detections. 网络滞后很容易导致错误的检测。

TCP messages should be adequately framed so that the receiver knows exactly where one message ends and the next message begins. 应该对TCP消息进行适当的构架,以便接收方准确知道一条消息的结束位置和下一条消息的开始位置。 There are three ways to do that in TCP: 在TCP中有三种方法可以做到这一点:

  1. use fixed-length messages. 使用固定长度的消息。 The receiver can keep reading until the expected number of bytes has arrived. 接收器可以继续读取,直到达到预期的字节数为止。

  2. send a message's length before sending the message itself. 发送消息本身之前发送消息的长度。 The receiver can read the length first and then keep reading until the specified number of bytes has arrived. 接收器可以先读取长度,然后继续读取,直到到达指定的字节数为止。

  3. terminate a message with a unique delimiter that does not appear in the message data. 使用不出现在消息数据中的唯一定界符终止消息。 The receiver can keep reading bytes until that delimiter has arrived. 接收器可以继续读取字节,直到该分隔符到达为止。


That being said, what you are asking for can be done in TCP (but shouldn't be done in TCP!). 话虽如此,您要的内容可以在TCP中完成(但不应在TCP中完成!)。 And it can be done without using a manual buffer at all, use Indy's built-in buffer instead. 可以完全不使用手动缓冲区来完成,而应使用Indy的内置缓冲区。 For example: 例如:

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.

On a side note, your statement that " exception handling in Delphi IDE's debug mode hasn't been made convenient " is simply ridiculous. 附带一提,您的说法“ 没有使Delphi IDE的调试模式下的异常处理变得方便 ”,这简直是荒谬的。 Indy's IOHandler has properties and method parameters for controlling exception behavior, and also if you don't like the way the debugger handles exceptions then simply configure it to ignore them. Indy的IOHandler具有用于控制异常行为的属性和方法参数,并且如果您不喜欢调试器处理异常的方式,则只需将其配置为忽略它们即可。 You can configure the debugger to ignore specific exception types, or you can use breakpoints to tell the debugger to skip handling exceptions in specific blocks of code. 您可以配置调试器以忽略特定的异常类型,也可以使用断点来告诉调试器跳过处理特定代码块中的异常。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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