2016-09-25 66 views
1

我有一個簡單的服務器客戶端應用程序,使用命名管道。我在服務器中使用StreamWriter,在客戶端使用StreamReader。只要客戶端進程不從管道讀取(即,不從StreamReader讀取,封裝管道),StreamWriter就不會處理。我想明白爲什麼。StreamWriter無法關閉,而其包裹的管道沒有排空?

下面是詳細信息:

這是服務器:

using System; 
using System.IO; 
using System.IO.Pipes; 

class PipeServer 
{ 
    static void Main() 
    { 
     using (NamedPipeServerStream pipeServer = 
      new NamedPipeServerStream("testpipe")) 
     { 
      Console.Write("Waiting for client connection..."); 
      pipeServer.WaitForConnection(); 
      Console.WriteLine("Client connected."); 

      try 
      { 
       StreamWriter sw = new StreamWriter(pipeServer); 
       try 
       { 
        sw.WriteLine("hello client!"); 
       } 
       finally 
       { 
        sw.Dispose(); 
       } 
       // Would print only after the client finished sleeping 
       // and reading from its StreamReader 
       Console.WriteLine("StreamWriter is now closed"); 
      } 
      catch (IOException e) 
      { 
       Console.WriteLine("ERROR: {0}", e.Message); 
      } 
     } 
    } 
} 

和這裏的客戶:

using System; 
using System.IO; 
using System.IO.Pipes; 
using System.Threading; 

class PipeClient 
{ 
    static void Main(string[] args) 
    { 
     using (NamedPipeClientStream pipeClient = 
      new NamedPipeClientStream(".", "testpipe")) 
     { 
      Console.Write("Attempting to connect to pipe..."); 
      pipeClient.Connect(); 
      Console.WriteLine("Connected to pipe."); 

      using (StreamReader sr = new StreamReader(pipeClient)) 
      { 
       Thread.Sleep(100000); 

       string temp = sr.ReadLine(); 
       if (temp != null) 
       { 
        Console.WriteLine("Received from server: {0}", temp); 
       } 
      } 
     } 
    } 
} 

注意的Thread.Sleep(100000);的客戶:我添加它,以確保只要客戶端進程處於睡眠狀態,StreamWriter就不在服務器中,服務器將不會執行Console.WriteLine("StreamWriter is now closed");。爲什麼?

編輯:

我切斷其在第二以爲我猜可能是無關緊要的以前的信息。 我還想補充一點 - 感謝Scott在評論中 - 我觀察到這種行爲發生的另一種方式:如果服務器寫入,然後睡眠,客戶端(試圖)用它的StreamReader讀取 - 讀取isn直到服務器醒來纔會發生。

第二個編輯:

另一方向圍繞頭的行爲我在第一編輯談到是無關緊要的,它是一個flush問題。 我試着給它多一些試驗,並得出了斯科特的權利 - 如果不排水管道不能被處置。那爲什麼?這似乎與StreamWriter認爲它擁有該流的事實相矛盾,除非另有說明(請參閱here)。

下面是添加細節到上面的代碼:

在服務器程序,該try-finally現在看起來像這樣:

try 
{ 
    sw.AutoFlush = true; 
    sw.WriteLine("hello client!"); 
    Thread.Sleep(10000); 
    sw.WriteLine("hello again, client!"); 
} 
finally 
{ 
    sw.Dispose(); // awaits while client is sleeping 
} 
Console.WriteLine("StreamWriter is now closed"); 

在客戶端程序,所述using塊現在看起來像這樣:

using (StreamReader sr = new StreamReader(pipeClient)) 
{ 
    string temp = sr.ReadLine(); 
    Console.WriteLine("blah"); // prints while server sleeps 

    Console.WriteLine("Received from server: {0}", temp); // prints while server is sleeping 
    Thread.Sleep(10000); 

    temp = sr.ReadLine(); 
    Console.WriteLine("Received from server: {0}", temp); 
} 
+0

請問行爲的改變在所有如果你改變了服務器'新NamedPipeServerStream(「testpipe」,PipeDirection.Out)'和客戶端到'新的NamedPipeClientStream(「。」,「testpipe」,PipeDirection.In)'?另外,如果在讀取之後放置一個'Thread.Sleep(100000);',但仍然在客戶端的'using'中,會發生什麼情況?流在服務器上仍然保持打開狀態,直到它退出使用狀態? –

+0

@ScottChamberlain,根本沒有任何改變,並且改變了'Sleep'的位置是我期望的:服務器進程繼續,打印''StreamWriter現在關閉了''並返回,而客戶端休眠。 – HeyJude

+0

也許它不會讓你關閉,直到緩衝區被耗盡。如果您執行'服務器寫入 - >客戶端讀取 - >服務器寫入 - >服務器處理 - >客戶端讀取',會發生什麼? –

回答

3

所以問題歸結爲Windows命名管道的工作方式。當調用CreateNamedPipe時,您可以指定輸出緩衝區大小。現在文檔中提到了這樣的內容:

輸入和輸出緩衝區大小是建議性的。爲命名管道的每端保留的實際緩衝區大小可以是系統默認值,系統最小值或最大值,也可以是指定大小四捨五入到下一個分配邊界。

這裏的問題在於,NamedPipeServerStream構造函數傳遞0作爲默認的輸出尺寸(我們可以使用源,或者對於我來說只是發射了ILSpy驗證)。您可能會認爲這會根據註釋創建「默認」緩衝區大小,但它不會,它實際上會創建一個0字節的輸出緩衝區。這意味着,除非有人正在讀取緩衝區,然後寫入管道塊。您還可以使用OutBufferSize屬性驗證大小。

您看到的行爲是因爲當您處置StreamWriter時,它會調用管道流上的Write(因爲它緩衝了內容)。寫入調用塊,因此Dispose不會返回。

要驗證這一點,我們將使用WinDBG(因爲VS只是在sw.Dispose調用中顯示阻塞線程)。

在WinDBG下運行服務器應用程序。當它的阻止暫停調試器(按CTRL + BREAK例如),併發出以下命令:

裝入SOS調試器模塊:.loadby sos clr

自卸所有正在運行的堆棧與交錯的管理和未管理堆棧幀(可能需要因爲在sos.dll一個愚蠢的錯誤)運行兩次這樣的:!eestack

您應該看到在這個過程中第一個堆棧看起來是這樣(大量編輯爲簡潔起見):

Thread 0 
Current frame: ntdll!NtWriteFile+0x14 
KERNELBASE!WriteFile+0x76, calling ntdll!NtWriteFile 
System.IO.Pipes.PipeStream.WriteCore 
System.IO.StreamWriter.Flush(Boolean, Boolean)) 
System.IO.StreamWriter.Dispose(Boolean)) 

因此,我們可以看到我們卡在NtWriteFile中,這是因爲它無法將字符串寫出到大小爲0的緩衝區中。

要解決此問題很簡單,請使用一個顯式輸出緩衝區大小NamedPipeServerStream的構造函數,如this之一。例如,這將盡一切默認的構造函數會做,但有一個合理的輸出緩衝區大小:

new NamedPipeServerStream("testpipe", PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.None, 0, 4096) 
+0

我可能在你的解釋中遺漏了一些東西,但是:爲什麼寫入調用僅在Dispose上阻塞?你說它有一個連接到零大小的緩衝區。那麼,爲什麼其他的寫電話不會被阻止呢?另一件我不明白的事情是,爲什麼服務器在Dispose中脫離其阻塞會受到客戶端休眠狀態的影響?順便說一句,我必須得到WinDBG的竅門,因爲我還不太熟悉它。與此同時,我很樂意進一步澄清。 – HeyJude

+0

因此,阻塞只發生在Dispose中,因爲StreamWriter在將內容寫入底層文件句柄(最終所有命名管道都是OS)之前緩衝內容,直到它達到一定量,並且除非它遇到該緩衝區限制它不叫Write。它叫Stream ::直接寫_should_ block,但當然你不這樣做,你把它包裝在一個StreamWriter中。 – tyranid

+0

至於爲什麼它會在客戶端喚醒並調用ReadLine之後退出,這是因爲它從管道讀取數據,這會打開來自Dispose調用的隱式Write,並且每個人都很高興。 – tyranid