2017-08-29 53 views
1

如何接收一個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'。 –

回答

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. 

在一個側面說明,你的聲明說:「在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協議。這些數據來自哪裏? –