【问题标题】:Receive partial data with timeout using TIdTcpClient使用 TIdTcpClient 接收超时的部分数据
【发布时间】:2017-08-30 01:50:35
【问题描述】:

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

  • 如果什么都没有进来,Read 调用将被阻塞,线程将永远等待
  • 如果收到 100 个字节,则 Read 调用应返回字节字符串
  • 如果收到的字节数多于 0,但少于 100,则 Read 调用应在超时(例如 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.

【问题讨论】:

  • 为什么不直接使用IOHandler.ReadBytes()而不是自己尝试做这项艰苦的工作?
  • @Paul 我很确定 Indy 对缓冲区的处理比我们大多数人希望管理的要好。如果你只想要 50 个字节,为什么要 100 个?在某些时候,您有一个有意义的最小数据包大小。等待那个数据包——如果它来了,就处理它,如果它没有,那么你无事可做。如果您不在乎丢失数据,也许 UDP 是比 TCP 更好的协议。
  • @Paul 这是您设计的协议,还是您遵循某种协议?这种方法似乎没有多大意义。
  • TCP 是面向流的,而不是面向消息的。在没有任何结构的情况下读取任意字节是不好的设计,并且当您过早停止读取时很容易破坏您的通信,然后您想要读取的字节在您停止读取后到达。在读取它们之前,它们不会从套接字中删除。如果您期望 100 个字节,则读取 100 个字节。如果发送方只发送 50 个字节,它需要告诉接收方,以便在收到 50 个字节后停止读取。如果您的发件人没有这样做,那么这是一个设计得很糟糕的 TCP 协议。
  • 而您关于“Delphi IDE 的调试模式中的异常处理不方便”的说法是荒谬的。 Indy 的IOHandler 具有用于控制异常行为的属性和方法参数,如果您不喜欢调试器处理异常的方式,则将其配置为根本不处理它们。您可以将调试器配置为忽略特定异常,也可以使用断点禁止调试器处理特定代码块中的异常。

标签: delphi indy


【解决方案1】:

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.

顺便说一句,您所说的“Delphi IDE 的调试模式中的异常处理不方便”简直是荒谬的。 Indy 的IOHandler 具有用于控制异常行为的属性和方法参数,如果您不喜欢调试器处理异常的方式,那么只需将其配置为忽略它们。您可以将调试器配置为忽略特定的异常类型,也可以使用断点告诉调试器跳过处理特定代码块中的异常。

【讨论】:

  • @Paul Indy 专为处理基于分隔符的文本协议而设计。 IOHandler 具有WaitFor()ReadLn() 方法,以及ReadLnTimedOut 属性。您可以WaitFor 开始字符,丢弃它返回的任何内容,然后ReadLn 以下字符并检查校验和,然后重复。
  • @Paul:我不使用 Visual Studio,但无论 IDE 是什么,调试器总是先获取异常,然后将其传递给被调试的进程。这是在与调试器交互的操作系统层处理的。见Understanding Exceptions while debugging with Visual Studio。 VS 和 Delphi 都可以配置为不中断“第一次机会”异常,让被调试的进程正常处理它们。
  • @Paul:您可以在调用WaitFor('$') 后利用IOHandler 的InputBuffer.IndexOf() 方法在处理82 字节限制时帮助查找CRLF。但是您正在处理一个非常笨拙且设计糟糕的 TCP 协议。这些数据来自哪里?
  • @Paul:你有关于这个协议的书面规范吗?请编辑您的问题以提供所有相关详细信息。然后我会考虑更新我的答案,向您展示如何在 Indy 中最好地处理它。您不断更改细节,所以现在我的答案中的代码与您的实际情况无关。
  • 目标是将解析与传输分离。协议可能会改变。我想要: 1. 一系列具有通用接口的组件,提供无穷无尽的分块字符流。它可以是 TCP 客户端、TCP 服务器、UDP 阅读器、UDP 多播阅读器、串行端口。他们都有一个类似的事件来传递一个字符串; 2. 一系列不同的解析器,应该独立于所使用的协议。各种有效载荷的多个解析器可以连接到单个数据检索组件。作为接收器和解析器之间的标准接口应该是一个方法AddMoreCharacters(s: AnsiString)
猜你喜欢
  • 2011-09-24
  • 1970-01-01
  • 1970-01-01
  • 2017-07-20
  • 1970-01-01
  • 1970-01-01
  • 2015-12-28
  • 1970-01-01
  • 2012-03-15
相关资源
最近更新 更多