2017-05-26 182 views
3

我是Delphi的新手,我正嘗試做一些網絡操作。在這種情況下,我想連接到(讓我們稱之爲)通知服務器,該服務器在發生某些事件時將發送字符串。使用TIdTCPClient異步讀取

我的第一個方法是這樣的: 我在自己的線程上運行TIdTCPClient並設置ReadTimeout,所以我並不總是被阻塞。這樣我可以檢查線程的終止狀態。

ConnectionToServer.ReadTimeout := MyTimeOut; 
while(Continue) do 
begin 
    // 
    try 
     Command := ConnectionToServer.ReadLn(); 
    except 
    on E: EIdReadTimeout do 
     begin 
       //AnotarMensaje(odDepurar, 'Timeout ' + E.Message); 
     end; 
    on E: EIdConnClosedGracefully do 
     begin 
       AnotarMensaje(odDepurar, 'Conexión cerrada ' + E.Message); 
       Continue := false; 
     end; 
    on E: Exception do 
     begin 
       AnotarMensaje(odDepurar, 'Error en lectura ' + E.Message); 
       Continue := false; 
     end; 
    end; 
    // treat the command 
    ExecuteRemoteCommand(Command);  
    if(self.Terminated) then 
    begin 
     Continue := false; 
    end; 
end; // while continue 

讀ReadLn代碼從來就看出it's在做重複一些積極等到循環,檢查某種緩衝區大小,所有的時間。

有沒有辦法以TIdTCPServer與OnExecute等方法一起工作的方式異步執行此操作?或者,至少,有些方法可以避免主動等待。

回答

3

Indy使用阻塞套接字,客戶端和服務器端。關於它沒有任何異步。在TIdTCPServer的情況下,它將在單獨的工作線程中運行每個客戶端套接字,就像您在客戶端嘗試執行的一樣。 TIdTCPClient 不是多線程的,所以你必須運行你自己的線程。

:如果您升級到印第安納波利斯10,它有一個TIdCmdTCPClient客戶端是多線程的,運行其自己的線程你,觸發TIdCommandHandler.OnCommand事件從服務器接收的數據包。

ReadLn()運行一個循環,直到在InputBuffer中找到指定的ATerminator,或者直到發生超時。在找到ATerminator之前,ReadLn()會將更多數據從套接字讀取到InputBuffer並再次掃描。緩衝區大小檢查只是爲了確保它不會重新掃描已經掃描的數據。

「喚醒」阻塞ReadLn()調用(或任何阻塞的套接字調用,就此而言)的唯一方法是從另一個線程關閉套接字。否則,你只需等待通話超時。請注意0​​在超時時不會引發異常。它設置ReadLnTimedout屬性爲True,然後返回一個空字符串,如:

ConnectionToServer.ReadTimeout := MyTimeOut; 

while not Terminated do 
begin 
    try 
    Command := ConnectionToServer.ReadLn; 
    except 
    on E: Exception do 
    begin 
     if E is EIdConnClosedGracefully then 
     AnotarMensaje(odDepurar, 'Conexión cerrada') 
     else 
     AnotarMensaje(odDepurar, 'Error en lectura: ' + E.Message); 
     Exit; 
    end; 
    end; 

    if ConnectionToServer.ReadLnTimedout then begin 
    //AnotarMensaje(odDepurar, 'Timeout'); 
    Continue; 
    end; 

    // treat the command 
    ExecuteRemoteCommand(Command);  
end; 

如果你不喜歡這種模式,你不必用印。一個更高效和響應的模型應該是直接使用WinSock。您可以使用WSARecv()的Overlapped I/O,並通過CreateEvent()TEvent創建一個等待事件以表示線程終止,然後您的線程可以使用WaitForMultipleObjects()同時等待套接字和終端,同時休眠時無需任何操作例如:

hSocket = socket(...); 
connect(hSocket, ...); 
hTermEvent := CreateEvent(nil, True, False, nil); 

... 

var 
    buffer: array[0..1023] of AnsiChar; 
    wb: WSABUF; 
    nRecv, nFlags: DWORD; 
    ov: WSAOVERLAPPED; 
    h: array[0..1] of THandle; 
    Command: string; 
    Data, Chunk: AnsiString; 
    I, J: Integer; 
begin 
    ZeroMemory(@ov, sizeof(ov)); 
    ov.hEvent := CreateEvent(nil, True, False, nil); 
    try 
    h[0] := ov.hEvent; 
    h[1] := hTermEvent; 

    try 
     while not Terminated do 
     begin 
     wb.len := sizeof(buffer); 
     wb.buf := buffer; 

     nFlags := 0; 

     if WSARecv(hSocket, @wb, 1, @nRecv, @nFlags, @ov, nil) = SOCKET_ERROR then 
     begin 
      if WSAGetLastError() <> WSA_IO_PENDING then 
      RaiseLastOSError; 
     end; 

     case WaitForMultipleObjects(2, PWOHandleArray(@h), False, INFINITE) of 
      WAIT_OBJECT_0: begin 
      if not WSAGetOverlappedResult(hSocket, @ov, @nRecv, True, @nFlags) then 
       RaiseLastOSError; 

      if nRecv = 0 then 
      begin 
       AnotarMensaje(odDepurar, 'Conexión cerrada'); 
       Exit; 
      end; 

      I := Length(Data); 
      SetLength(Data, I + nRecv); 
      Move(buffer, Data[I], nRecv); 

      I := Pos(Data, #10); 
      while I <> 0 do 
      begin 
       J := I; 
       if (J > 1) and (Data[J-1] = #13) then 
       Dec(J); 

       Command := Copy(Data, 1, J-1); 
       Delete(Data, 1, I); 

       ExecuteRemoteCommand(Command); 
      end; 
      end; 

      WAIT_OBJECT_0+1: begin 
      Exit; 
      end; 

      WAIT_FAILED: begin 
      RaiseLastOSError; 
      end; 
     end; 
     end; 
    except 
     on E: Exception do 
     begin 
     AnotarMensaje(odDepurar, 'Error en lectura ' + E.Message); 
     end; 
    end; 
    finally 
    CloseHandle(ov.hEvent); 
    end; 
end; 

如果使用的Delphi XE2或更高版本,TThread具有虛擬TerminatedSet()方法可以重寫到信號hTermEventTThread.Terminate()被調用。否則,致電Terminate()後致電SetEvent()

+0

感謝您的分解。我不擔心ReadLn的阻塞性質,我擔心檢查緩衝區大小的繁忙等待循環。 我堅持使用老版本的Indy和Delphi 7(遺留代碼),所以沒有太大的改變空間。 –

+1

@HéctorC。然後考慮徹底擺脫讀取超時,讓'ReadLn'在沒有數據要讀取時阻塞線程,然後在準備好終止線程時斷開客戶端連接。 –

3

您可以在單獨的線程中執行此操作。

TIdTCPServer在後臺使用線程來支持偵聽並與多個客戶端通信。

由於TIdTCPClient連接到一臺服務器,我認爲它沒有內置此功能,但您可以在獨立的線程中自行創建和使用TIdTCPClient,因此對您而言,您的解決方案很好。我會以同樣的方式解決它。

如果您使超時時間很短,那麼這個問題應該不會成爲問題在那段時間內,套接字仍處於打開狀態,因此您不會錯過任何數據。您可以將超時設置爲10ms等小數值。這樣,你的線程很長時間不會徘徊,但是超時足夠長,不會導致退出和重新進入readln的顯着開銷。

+0

這就是我所做的。但是我必須定期從ReadLn操作中退出以檢查Terminated線程屬性。 另外,當我在別處終止這個線程時,我必須等到ReadLn超時退出線程。 –

+1

是的。如果你讓超時時間很短,這是一個問題嗎?在此期間套接字仍處於打開狀態,因此您可以將超時設置爲10ms等較小值。這樣,你的線程很長時間不會徘徊,但是超時足夠長,不會導致退出和重新進入readln的顯着開銷。 – GolezTrol

+1

順便說一句,我很好奇,並搜索了一下。我發現了類似的問題[Indy 10 IdTCPClient使用單獨的線程讀取數據](https://stackoverflow.com/questions/554142/indy-10-idtcpclient-reading-data-using-a-separate-thread)和[Indy TIdTCPClient閱讀數據](https://stackoverflow.com/questions/17372366/delphi-indy-tidtcpclient-reading-data),這兩者都顯示了更詳細的例子,說明如何實現這個線程,並克服與這個事件同步的問題主線程。 – GolezTrol