2012-10-27 39 views
16

我一直在努力與此,並找不到我的代碼未能正確讀取我寫的TCP服務器正確的原因。我正在使用TcpClient類和其GetStream()方法,但某些功能無法按預期工作。操作無限期地阻塞(最後一次讀操作沒有按預期超時),或者數據被裁剪(出於某種原因,讀操作返回0並退出循環,可能服務器響應速度不夠快)。這三個企圖實現這種功能:什麼是正確的方式從NET讀取NetworkStream

// this will break from the loop without getting the entire 4804 bytes from the server 
string SendCmd(string cmd, string ip, int port) 
{ 
    var client = new TcpClient(ip, port); 
    var data = Encoding.GetEncoding(1252).GetBytes(cmd); 
    var stm = client.GetStream(); 
    stm.Write(data, 0, data.Length); 
    byte[] resp = new byte[2048]; 
    var memStream = new MemoryStream(); 
    int bytes = stm.Read(resp, 0, resp.Length); 
    while (bytes > 0) 
    { 
     memStream.Write(resp, 0, bytes); 
     bytes = 0; 
     if (stm.DataAvailable) 
      bytes = stm.Read(resp, 0, resp.Length); 
    } 
    return Encoding.GetEncoding(1252).GetString(memStream.ToArray()); 
} 

// this will block forever. It reads everything but freezes when data is exhausted 
string SendCmd(string cmd, string ip, int port) 
{ 
    var client = new TcpClient(ip, port); 
    var data = Encoding.GetEncoding(1252).GetBytes(cmd); 
    var stm = client.GetStream(); 
    stm.Write(data, 0, data.Length); 
    byte[] resp = new byte[2048]; 
    var memStream = new MemoryStream(); 
    int bytes = stm.Read(resp, 0, resp.Length); 
    while (bytes > 0) 
    { 
     memStream.Write(resp, 0, bytes); 
     bytes = stm.Read(resp, 0, resp.Length); 
    } 
    return Encoding.GetEncoding(1252).GetString(memStream.ToArray()); 
} 

// inserting a sleep inside the loop will make everything work perfectly 
string SendCmd(string cmd, string ip, int port) 
{ 
    var client = new TcpClient(ip, port); 
    var data = Encoding.GetEncoding(1252).GetBytes(cmd); 
    var stm = client.GetStream(); 
    stm.Write(data, 0, data.Length); 
    byte[] resp = new byte[2048]; 
    var memStream = new MemoryStream(); 
    int bytes = stm.Read(resp, 0, resp.Length); 
    while (bytes > 0) 
    { 
     memStream.Write(resp, 0, bytes); 
     Thread.Sleep(20); 
     bytes = 0; 
     if (stm.DataAvailable) 
      bytes = stm.Read(resp, 0, resp.Length); 
    } 
    return Encoding.GetEncoding(1252).GetString(memStream.ToArray()); 
} 

最後一個「作品」,但它確實看起來難看把一個硬編碼的睡眠考慮到插座已經支持讀取超時循環內!我是否需要在NetworkStreamTcpClient上設置一些房產?問題是否存在於服務器中?服務器不關閉連接,這取決於客戶端。以上內容也在UI線程環境(測試程序)內部運行,可能與此有關......

有人知道如何正確使用NetworkStream.Read來讀取數據,直到沒有更多的數據可用?我想我希望的是像舊的Win32 winsock的超時屬性... ReadTimeout等,嘗試讀取,直到達到超時,然後返回0 ...但它有時似乎回到0時數據應提供(或在路上..如果能提供?讀返回0)無限期最後一次讀取數據時不可用,它再塊...

是的,我不知所措!

+0

要小心。您嘗試(和答案)中的代碼不會關閉客戶端或流,如果重複調用,會導致資源泄漏,並帶來很大的後果。你應該使用(var stm = client.GetStream())'使用(var client = new System.Net.Sockets.TcpClient(ip,port)) '然後用花括號括住剩餘的方法。這將保證在方法退出時,不管什麼原因,連接都關閉,資源被回收。 –

+0

你見過TcpClient類的代碼嗎?我建議看看它...如果您仍想在端點上回答,則不應關閉流或tcpclient(如果我沒有弄錯,會關閉流)。但是我使用的實際代碼是不同的,我會將其挖掘出來並更新這裏的答案。 – Loudenvier

+0

感謝您的建議。我發現[TCPClient.cs](http://referencesource.microsoft.com/#system/net/System/Net/Sockets/TCPClient.cs)。事實上,關閉或處理'TCPClient'會處理流。實際上,當一個程序做出數千個連接時沒有多少注意(其他原因很糟糕),我確實發現連接失敗。爲什麼要在這裏偏離通常的IDisposable模式?實現IDisposable容易出錯,'使用()'它很容易和安全。 –

回答

8

設置底層套接字ReceiveTimeout屬性做了竅門。你可以像這樣訪問它:yourTcpClient.Client.ReceiveTimeout。你可以閱讀docs瞭解更多信息。

現在只要需要某些數據到達套接字時代碼就會「休眠」,或者如果在讀操作開始時沒有數據到達超過20ms,它將引發異常。如果需要,我可以調整此超時。現在我沒有在每次迭代中支付20ms的價格,我只是在最後一次讀取操作時付款。由於我從服務器讀取的第一個字節的消息內容長度,我可以用它來調整它,甚至更多,而不是嘗試讀取是否所有預期的數據已被接收。

我發現使用ReceiveTimeout比實現異步讀取更加容易......這裏是工作代碼:

string SendCmd(string cmd, string ip, int port) 
{ 
    var client = new TcpClient(ip, port); 
    var data = Encoding.GetEncoding(1252).GetBytes(cmd); 
    var stm = client.GetStream(); 
    stm.Write(data, 0, data.Length); 
    byte[] resp = new byte[2048]; 
    var memStream = new MemoryStream(); 
    var bytes = 0; 
    client.Client.ReceiveTimeout = 20; 
    do 
    { 
     try 
     { 
      bytes = stm.Read(resp, 0, resp.Length); 
      memStream.Write(resp, 0, bytes); 
     } 
     catch (IOException ex) 
     { 
      // if the ReceiveTimeout is reached an IOException will be raised... 
      // with an InnerException of type SocketException and ErrorCode 10060 
      var socketExept = ex.InnerException as SocketException; 
      if (socketExept == null || socketExept.ErrorCode != 10060) 
       // if it's not the "expected" exception, let's not hide the error 
       throw ex; 
      // if it is the receive timeout, then reading ended 
      bytes = 0; 
     } 
    } while (bytes > 0); 
    return Encoding.GetEncoding(1252).GetString(memStream.ToArray()); 
} 
+2

小心使用這個黑客。我們有一個Java應用程序依靠套接字超時來檢測消息結束,並且時不時丟失了流中的一個字節。無可否認,我不知道這是否也適用於.NET,但我懷疑它是底層TCP/IP協議棧的原因。在超時後你根本不應該重用套接字。 –

+0

@SørenBoisen我沒有在超時後重新使用套接字...我不會丟失數據,但是我可以有一個錯誤的「正」超時(如果網絡速度較慢,數據可能比字符間超時到達時間要長) ,但在這種情況下,另一方只會處理斷開連接,並希望再次嘗試。這實際上不是黑客,我不能改變的協議發送未知大小的消息,所以我必須依靠超時。實際上,隨着時間的推移,代碼更加健壯,但沒有更新文章。現在是個好時機。 – Loudenvier

0

根據您的要求,Thread.Sleep完全可以使用,因爲您不確定數據何時可用,因此您可能需要等待數據可用。我略微改變了你的功能的邏輯,這可能會幫助你進一步。

string SendCmd(string cmd, string ip, int port) 
{ 
    var client = new TcpClient(ip, port); 
    var data = Encoding.GetEncoding(1252).GetBytes(cmd); 
    var stm = client.GetStream(); 
    stm.Write(data, 0, data.Length); 
    byte[] resp = new byte[2048]; 
    var memStream = new MemoryStream(); 

    int bytes = 0; 

    do 
    { 
     bytes = 0; 
     while (!stm.DataAvailable) 
      Thread.Sleep(20); // some delay 
     bytes = stm.Read(resp, 0, resp.Length); 
     memStream.Write(resp, 0, bytes); 
    } 
    while (bytes > 0); 

    return Encoding.GetEncoding(1252).GetString(memStream.ToArray()); 
} 

希望這有助於!

+1

在發佈問題後,我已經完全按照此方法編寫了該方法!我仍然不覺得可以接受。如果我使用的是舊的WIN32重疊IO,我可以在數據開始到達的時候「阻塞」20個或更多的msecs,所以我的代碼可以更安全,如下這樣的僞代碼:'stm.ReadIfDataBecomesAvailableInUpTo(timeout = 2000)'I知道20ms是一個小小的代價,但它會在每一次交易中支付!要準備一個MB大小的響應,它將是一個巨大的開銷!我不想訴諸寫自己的超時邏輯... :-( – Loudenvier

+5

你必須小心Sleeping在線程上,如果時間太短,你在操作系統上強制不必要的上下文切換,從而顛簸CPU。一個更好的機制可能是中斷驅動或'事件'驅動的,爲此你可以使用stm.BeginRead(),它可以讓你在數據準備就緒時調用一個事件,這樣你的進程可以在當操作系統只有在資源準備好時纔會喚醒它,每當進程喚醒時,睡眠將退回到操作系統。 – user1132959

14

網絡代碼是出了名的難寫,測試和調試。

你經常有很多事情要考慮,如:

  • 什麼「端」你用的是交換(英特爾在x86/x64是基於小端)數據 - 系統即使用big-endian仍然可以讀取little-endian(反之亦然)的數據,但他們必須重新排列數據。當記錄您的「協議」只是讓你在使用它清楚哪一個。

  • 在那裏已經設置這會影響如何「流」的行爲(如SO_LINGER)的插座上的「設置」 - 你可能需要打開某幾個或關閉,如果你的代碼是非常敏感

  • 如何擁塞導致的延遲流中的現實世界影響你的讀/寫邏輯

如果客戶端和服務器(在任一方向)之間進行交換的「消息」的尺寸可以變化這時往往需要才能使用的策略爲「消息」以可靠的方式進行交換(又名 協議)。

這裏有幾種不同的方式來處理交換:

  • 在這之前的數據頭編碼的郵件大小 - 在第2/4/8這可能僅僅是一個「數字」字節發送(取決於您的最大消息大小),或者可能是更具有異國情調的「標題」

  • 使用特殊的「消息結束」標記(sentinel),如果有可能,編碼/的實際數據與「結束標記」相混淆

  • 使用超時......即。沒有接收字節的一段時間意味着沒有更多的消息數據 - 但是,這可能容易出錯並且短暫超時,這很容易在擁塞的流上發生。

  • 在單獨的「連接」上有一個「命令」和「數據」通道....這是FTP協議使用的方法(優點是數據與命令清晰分離......犧牲一個第二連接)

每種方法都有其「正確性」的優點和缺點。

下面的代碼使用「超時」方法,因爲這似乎是你想要的。

請參閱http://msdn.microsoft.com/en-us/library/bk6w7hs8.aspx。您可以訪問TCPClient上的NetworkStream,以便您可以更改ReadTimeout

string SendCmd(string cmd, string ip, int port) 
{ 
    var client = new TcpClient(ip, port); 
    var data = Encoding.GetEncoding(1252).GetBytes(cmd); 
    var stm = client.GetStream(); 
    // Set a 250 millisecond timeout for reading (instead of Infinite the default) 
    stm.ReadTimeout = 250; 
    stm.Write(data, 0, data.Length); 
    byte[] resp = new byte[2048]; 
    var memStream = new MemoryStream(); 
    int bytesread = stm.Read(resp, 0, resp.Length); 
    while (bytesread > 0) 
    { 
     memStream.Write(resp, 0, bytesread); 
     bytesread = stm.Read(resp, 0, resp.Length); 
    } 
    return Encoding.GetEncoding(1252).GetString(memStream.ToArray()); 
} 

至於在寫這篇文章的網絡代碼等變化的註腳......這樣做要避免「塊」一個Read的時候,你可以檢查DataAvailable標誌,然後只讀什麼是在緩衝區檢查.Length財產,例如stm.Read(resp, 0, stm.Length);

+0

尺寸未知,可以在MB範圍內...正如我在問題是,第三個選項確實正確地讀取了所有內容,但我不能接受那裏的睡眠需求,我也無法解釋它。我知道它的作用:通過睡眠,我給了數據到達插座的時間,但如果數據在此之前到達,我不想睡20ms,我知道如何用非blocki來做到這一點ng讀取和我自己的重疊IO ...但我不認爲我應該需要訴諸於.NET! – Loudenvier

+0

你用什麼方法讓讀數知道它是否讀取了所有的數據,即正確的數量/大小?典型的方法是使用在響應的第一部分編碼的大小標題。 –

+0

我有一個Content-lenght(該協議幾乎與HTTP相同),但爲了簡單起見,我將使用「傳入數據超時」,並且我認爲Read會阻塞一段時間然後超時,而不會永遠等待到達!通過「內部循環」睡眠,我將在每次迭代中支付20ms的價格,並且「傳入數據超時」,我只在需要時和最後一次讀取時支付。我可以使用ContentLenght對其進行優化,但僅依賴長度也是有風險的,因爲服務器可能會出現錯誤並且不會發送任何內容! – Loudenvier

相關問題