接收部分数据使用TIdTcpClient

问题描述:

如何接收一个100字节的字符串使用TIdTcpClient下列条件超时:接收部分数据使用TIdTcpClient

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

我现在不是最佳的代码如下:

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. 
+1

为什么不直接使用['IOHandler.ReadBytes()'](http://www.indyproject.org/docsite/html/[email protected]@[email protected])而不是自己努力去做这项艰苦工作? –

+2

@Paul我很确定Indy处理缓冲区比我们大多数人希望能够管理的更好。如果你只需要50个字节,为什么要求100个?在某些时候,你有一个最小的数据包大小,这可能是有用的。等待那个数据包 - 如果它来了,然后处理它,如果它没有,那么你无事可做。如果你不关心丢失的数据,或许UDP比TCP更好。 –

+0

@Paul而不是试图盲目地阅读,然后处理异常,可能更好的方法是在尝试读取之前检查缓冲区('如果不是IOHandler.InputBufferIsEmpty,则...')。试图从空的缓冲区读取没有意义。更好的是,如果你正在读取字符串并控制你的命令协议,那么选择一个合理的命令终止符,并使用'ReadLn'。 –

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具有用于控制异常行为的属性和方法参数,并且如果您不喜欢调试程序处理异常的方式,则只需将其配置为忽略它们即可。您可以将调试器配置为忽略特定的异常类型,也可以使用断点告诉调试器跳过处理特定代码块中的异常。

+2

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

+1

@Paul:我不使用Visual Studio,但不管IDE如何,调试器总是首先得到异常,然后将它传递给调试过程。这是在与调试器交互的操作系统层处理的。请参阅[使用Visual Studio进行调试时了解异常](https://blogs.msdn.microsoft.com/devops/2015/01/07/understanding-exceptions-while-debugging-with-visual-studio/)。 VS和Delphi都可以配置为不中断“一次机会”异常,只让被调试的进程正常处理它们。 –

+1

@Paul:调用'WaitFor('$')''后可以使用IOHandler的'InputBuffer.IndexOf()'方法来帮助查找CRLF,同时处理82字节的限制。但是你正在处理一个非常尴尬和可怕的TCP协议。这些数据来自哪里? –