溫故之.NET進程間通信——管道

溫故之.NET進程間通信——管道

來自專欄嘿嘿的學習日記7 人贊了文章

鑒於小哥哥、小姐姐們每天的工作壓力都很大。決定以後每一篇文章講解的知識點最多不超過三個。這樣有三個好處

  • 小哥哥、小姐姐們可以多花一點時間休息,或者陪陪家人
  • 知識點少,可以保證我們可以理解得更深刻,也更容易記住
  • 知識點少,這樣我們就可以在不增加篇幅的情況下,儘可能地講得深入一些。況且篇幅太長,小哥哥、小姐姐們看久了可能會比較累

進程間傳遞數據,常見的有以下幾種方式:

  • 管道:包括命名管道和匿名管道,這篇文章將講解這種方式
  • 內存映射文件:藉助文件和內存空間之間的映射關係,應用(包括多個進程)可以直接對內存執行讀取和寫入操作,從而實現進程間通信
  • Socket:使用套接字在不同的進程間通信,這種通信方式下,需要佔用系統至少一個埠
  • SendMessage:通過窗口句柄的方式來通信,此通信方式基於 Windows 消息 WM_COPYDATA 來實現
  • 消息隊列:在對性能要求不高的情況下,我們可以使用 Msmq。但在實際項目中,一般使用 ActiveMQKafkaRocketMQRabbitMQ等這些針對特定場景優化的消息中間件,以獲得最大的性能或可伸縮性優勢

其中,管道、內存映射文件、SendMessage的方式,一般用於單機上進程間的通信,在單機上使用這三種方式,比使用 Socket 要相對高效,且更容易控制

Socket 、消息隊列或其他基於Socket的通信方式,則適用範圍更廣。它不僅適用於本機進程間的通信,還適用於跨機器(包括跨網段)之間的通信,比如同一個集群裡面不同伺服器之間的通信、微服務群下各個微服務之間的通信。這也是目前用得最多得方式

雖然在互聯網化的今天,本機進程間通信可能用得不多。但在這篇文章中,我們還是有必要了解基於管道的進程間通信方式,後面我們會目前用得比較廣泛的一些框架

命名管道

命名管道,它可以在管道伺服器和一個或多個管道客戶端之間提供進程間通信。其特點如下

  • 命名管道可以是單向的,也可以是雙向的
  • 它們支持基於消息的通信(即創建服務端管道時,指定 PipeTransmissionMode.Message 選項),並允許多個客戶端使用相同的管道名稱同時連接到伺服器端進程
  • 支持模擬,這樣連接進程就可以在遠程伺服器上使用自己的許可權

它既可用於本機進程間的通信,也能用於跨機器之間的通信,但實際中很少這樣用

跨機器通信,特別是在跨網路的情況下,目前普遍的做法是使用一些通信框架(比如分散式或微服務中,可使用NettyRPCRESTThrift等等),畢竟這些通信框架大都成熟穩定,還經歷過商用的考驗

用於發送數據的示例代碼如下

using System;using System.IO;using System.IO.Pipes;namespace App { class Program { static void Main(string[] args) { /// 第一個參數為管道的名稱,第二個參數表示此處的管道用於發送數據 using (NamedPipeServerStream pipeServer = new NamedPipeServerStream("pipe_demo", PipeDirection.Out)) { // 等待連接,程序會阻塞在此處,直到有一個連接到達 pipeServer.WaitForConnection(); try { using (StreamWriter sw = new StreamWriter(pipeServer)) { sw.AutoFlush = true; // 向連接的客戶端發送消息 sw.WriteLine("hello world "); } } catch (IOException e) { Console.WriteLine("ERROR: {0}", e.Message); } } Console.ReadLine(); } }}

用於接收數據的示例代碼如下

using System;using System.IO;using System.IO.Pipes;namespace App { class Program { static void Main(string[] args) { /// 第一個參數:"." 表示此管道用於本機。此處用 "localhost"、"127.0.0.1" 也是可以的 /// 第二個參數:管道的名稱 /// 第三個參數:表示此處的管道用於接收數據 using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", "pipe_demo", PipeDirection.In)) { pipeClient.Connect(); using (StreamReader sr = new StreamReader(pipeClient)) { string tmp; while ((tmp = sr.ReadLine()) != null) { Console.WriteLine($"收到數據: {tmp}"); } } } Console.ReadLine(); } }}

關於命名管道的命名,我們這兒使用的是 "pipe_demo" , 推薦採用 "公司名.項目名稱.模塊名稱.管道用途" 的方式命名。這不但可以減小與其他命名管道名稱衝突的可能性,還可以讓這個管道更具有識別性(通過名稱就能知道這個管道是幹嘛的)

其中,通過在創建管道時,指定 PipeDirection 選項,可以讓管道工作於雙工、半雙工的通信模式下

public enum PipeDirection { // 表示此管道用於接收數據 In = 1, // 表示此管道用於發送數據 Out = 2, // 表示此管道既可發送數據,也可以接收數據 InOut = 3}

如果對這種通信方式感興趣,可以參考 NamedPipeServerStreamNamedPipeClientStream 其他的構造函數,來找到更加符合自身業務的模式

匿名管道

匿名管道只能在本機上提供進程間通信。與命名管道相比,其有如下特點

  • 匿名管道需要的開銷更少,但提供的服務有限
  • 匿名管道是單向的,且不能通過網路使用,即不能跨網進行通信
  • 僅支持一個伺服器實例
  • 匿名管道可用於線程間通信,也可用於父進程和子進程之間的通信,因為管道句柄可以輕鬆傳遞給所創建的子進程。

服務端 AnonymousPipeServerStream 定義如下

public sealed class AnonymousPipeServerStream : PipeStream { public AnonymousPipeServerStream(); public AnonymousPipeServerStream(PipeDirection direction); public AnonymousPipeServerStream(PipeDirection direction, HandleInheritability inheritability); public AnonymousPipeServerStream(PipeDirection direction, HandleInheritability inheritability, int bufferSize); public AnonymousPipeServerStream(PipeDirection direction, SafePipeHandle serverSafePipeHandle, SafePipeHandle clientSafePipeHandle); public AnonymousPipeServerStream(PipeDirection direction, HandleInheritability inheritability, int bufferSize, PipeSecurity pipeSecurity); public SafePipeHandle ClientSafePipeHandle { get; } // 此管道的傳輸模式:在匿名管道中,只支持 PipeTransmissionMode.Byte 這種方式 public override PipeTransmissionMode TransmissionMode { get; } public override PipeTransmissionMode ReadMode { set; } public void DisposeLocalCopyOfClientHandle(); public string GetClientHandleAsString(); protected override void Dispose(bool disposing);}

可以看到,其定義了多個構造函數,提供了本機進程中的多種管道通信模式。其中

  • HandleInheritability 用於指明子進程是否可以繼承伺服器端的底層句柄
  • SafePipeHandle 用於指定客戶端和服務端的安全句柄
  • PipeSecurity 用於指定客戶端的訪問許可權

一般情況下,我們只需要使用前三個構造函數即可,後面幾個用的很少

客戶端 AnonymousPipeClientStream 定義如下

public sealed class AnonymousPipeClientStream : PipeStream { public AnonymousPipeClientStream(string pipeHandleAsString); public AnonymousPipeClientStream(PipeDirection direction, string pipeHandleAsString); public AnonymousPipeClientStream(PipeDirection direction, SafePipeHandle safePipeHandle); // 此管道的傳輸模式:在匿名管道中,只支持 PipeTransmissionMode.Byte 這種方式 public override PipeTransmissionMode TransmissionMode { get; } public override PipeTransmissionMode ReadMode { set; }}

其中,pipeHandleAsString 參數是父進程在創建此子進程的時候傳遞的安全句柄

其服務端示例如下

using System;using System.Diagnostics;using System.IO;using System.IO.Pipes;namespace App { class Program { static void Main(string[] args) { Process pipeClient = new Process(); // 客戶端可執行文件的路徑 pipeClient.StartInfo.FileName = @"C:UsersJamesource
eposConsoleApp4ConsoleApp4inDebugConsoleApp4.exe"; using (AnonymousPipeServerStream pipeServer = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable)) { // 將句柄傳入 pipeClient.StartInfo.Arguments =pipeServer.GetClientHandleAsString(); pipeClient.StartInfo.UseShellExecute = false; pipeClient.Start(); pipeServer.DisposeLocalCopyOfClientHandle(); try { using (StreamWriter sw = new StreamWriter(pipeServer)) { sw.AutoFlush = true; sw.WriteLine("SYNC"); pipeServer.WaitForPipeDrain(); Console.Write("[SERVER] Enter text: "); sw.WriteLine(Console.ReadLine()); } } catch (IOException e) { Console.WriteLine("[SERVER] Error: {0}", e.Message); } } pipeClient.WaitForExit(); pipeClient.Close(); Console.WriteLine("[SERVER] Client quit. Server terminating."); Console.ReadLine(); } }}

客戶端代碼如下

using System;using System.IO;using System.IO.Pipes;namespace App { class Program { static void Main(string[] args) { if (args.Length > 0) { // 其中,args[0] 表示傳入的句柄 using (PipeStream pipeClient = new AnonymousPipeClientStream(PipeDirection.In, args[0])) { using (StreamReader sr = new StreamReader(pipeClient)) { string temp; do { Console.WriteLine("[CLIENT] Wait for sync..."); temp = sr.ReadLine(); } while (!temp.StartsWith("SYNC")); while ((temp = sr.ReadLine()) != null) { Console.WriteLine("[CLIENT] Echo: " + temp); } } } } Console.ReadLine(); } }}

在匿名管道這個例子中,需要我們先編譯客戶端的代碼,否則可能會有錯誤

  • 如果客戶端還未編譯,則父進程會找不到文件
  • 如果客戶端已經編譯,父進程可啟動。但如果我們需要再次編譯子進程項目時,會報文件被佔用的錯誤

通過以上的講解,如果需要使用管道來實現進程間的通信,我們可以按以下方式選擇

  • 如果只需要單向通信,且兩個進行間的關係為父子進程,則可以使用匿名管道
  • 如果需要雙向通信,則使用命名管道
  • 如果我們無法決定到底該選擇什麼,那就選擇命名管道的雙向通信方式。在現在的電腦上,命名管道於匿名管道性能的差別我們可以忽略不記。而雙向通信的命名管道,既可單向,又可雙向,更加靈活

至此,這篇文章的內容講解完畢。歡迎關注公眾號【嘿嘿的學習日記】,所有的文章,都會在公眾號首發,Thank you~


推薦閱讀:

關於地暖盤管
技術|圖文講解管道支承件
知識布局-jediscluster-pipeline
五臟六腑——人體的蓄水池和輸送管道 文 / 蕭言生
人無「管道」不富

TAG:Net開發 | 進程間通信 | 管道 |