一、前言:
在項目開發中經常會遇到,多個進程間進行通信調用的需求。
常用的方式有:
- 管道:包括命名管道和匿名管道
- 內存映射文件:借助文件和內存空間之間的映射關系,應用(包括多個進程)能夠直接對內存執行讀取和寫入操做,從而實現進程間通訊
Socket:使用套接字在不一樣的進程間通訊,這種通訊方式下,須要占用系統至少一個端口SendMessage:經過窗口句柄的方式來通訊,此通訊方式基於Windows消息WM_COPYDATA來實現- 消息隊列:在對性能要求不高的狀況下,咱們可使用
Msmq。但在實際項目中,通常使用ActiveMQ、Kafka、RocketMQ、RabbitMQ等這些針對特定場景優化的消息中間件,以得到最大的性能或可伸縮性優點
其中,管道、內存映射文件、SendMessage的方式,通常用於單機上進程間的通訊,在單機上使用這三種方式,比使用 Socket 要相對高效,且更容易控制網絡。
本篇將介紹采用.Net 管道方式實現多進程間通信。
二、管道
管道為進程間通信提供了平台。 管道分為兩種類型:
- 匿名管道:
匿名管道在本地計算機上提供進程間通信。 與命名管道相比,雖然匿名管道需要的開銷更少,但提供的服務有限。
匿名管道是單向的,不能通過網絡使用。 僅支持一個服務器實例。
匿名管道可用於線程間通信,也可用於父進程和子進程之間的通信,因為管道句柄可以輕松傳遞給所創建的子進程。
- 命名管道:
命名管道在管道服務器和一個或多個管道客戶端之間提供進程間通信。
命名管道可以是單向的,也可以是雙向的。 它們支持基於消息的通信,並允許多個客戶端使用相同的管道名稱同時連接到服務器進程。
命名管道還支持模擬,這樣連接進程就可以在遠程服務器上使用自己的權限。
三、應用
1、匿名管道:實現效果:服務端將輸入內容傳遞給客戶端輸出。
- 服務端:創建輸出匿名管道,將控制台輸入內容通過管道傳遞到子進程中
using System; using System.Diagnostics; using System.IO; using System.IO.Pipes; namespace IpcServer { class AonymousPipeServer { public static void Run() { //客戶端進程 Process pipeClient = new Process(); pipeClient.StartInfo.FileName = "IpcClient.exe"; //創建輸出匿名管道、指定句柄由子進程繼承 using (AnonymousPipeServerStream pipeServer = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable)) { Console.WriteLine("[SERVER] 管道傳輸模式: {0}.", pipeServer.TransmissionMode); //將客戶端進程的句柄傳遞給服務器。 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] 輸入文本: "); sw.WriteLine(Console.ReadLine()); } } catch (IOException e) { Console.WriteLine("[SERVER] 異常: {0}", e.Message); } } pipeClient.WaitForExit(); pipeClient.Close(); Console.WriteLine("[SERVER] 客戶端退出,服務端終止."); } } }
- 客戶端:創建輸入管道,從管道中獲取服務端傳入的數據並輸出
using System; using System.IO; using System.IO.Pipes; namespace IpcClient { class AonymousPipeClient { public static void Run(string[] args) { if (args.Length > 0) { //創建輸入類型匿名管道 using (PipeStream pipeClient = new AnonymousPipeClientStream(PipeDirection.In, args[0])) { Console.WriteLine("[CLIENT] 當前管道傳輸模式: {0}.", pipeClient.TransmissionMode); //創建讀取流,從管道中讀取 using (StreamReader sr = new StreamReader(pipeClient)) { string temp; // 等待來着服務器的消息 do { Console.WriteLine("[CLIENT] 同步等待..."); temp = sr.ReadLine(); } while (!temp.StartsWith("SYNC")); // Read the server data and echo to the console. while ((temp = sr.ReadLine()) != null) { Console.WriteLine("[CLIENT] 響應: " + temp); } } } } Console.Write("[CLIENT] 任意鍵退出..."); Console.ReadLine(); } } }
示例效果:

2、命名管道:
- 服務端:通過管道讀取客戶端發送的文件地址,在服務端中讀取后返回給客戶端
using System; using System.IO; using System.IO.Pipes; using System.Text; using System.Threading; namespace IpcServer { public class NamedPipeServer { private static int numThreads = 4; public static void Run() { int i; Thread[] servers = new Thread[numThreads]; Console.WriteLine("\n*** 命名管道服務端示例 ***\n"); Console.WriteLine("等待客戶端連接...\n"); for (i = 0; i < numThreads; i++) { servers[i] = new Thread(ServerThread); servers[i].Start(); } Thread.Sleep(250); while (i > 0) { for (int j = 0; j < numThreads; j++) { if (servers[j] != null) { if (servers[j].Join(250)) { Console.WriteLine("Server thread[{0}] 結束.", servers[j].ManagedThreadId); servers[j] = null; i--; // 減少線程數量 } } } } Console.WriteLine("\n服務器線程已完成,正在退出."); Console.ReadLine(); } private static void ServerThread(object data) { //管道名稱、管道方向 NamedPipeServerStream pipeServer = new NamedPipeServerStream("testpipe", PipeDirection.InOut, numThreads); int threadId = Thread.CurrentThread.ManagedThreadId; // 等待客戶端連接 pipeServer.WaitForConnection(); Console.WriteLine("客戶端連接成功 thread[{0}].", threadId); try { //讀取客戶的請求。客戶端寫入管道后,其安全令牌將可用 StreamString ss = new StreamString(pipeServer); //使用客戶端預期的字符串向連接的客戶端驗證我們的身份。 ss.WriteString("I am the one true server!"); string filename = ss.ReadString(); //在模擬客戶端時讀入文件的內容。 ReadFileToStream fileReader = new ReadFileToStream(ss, filename); Console.WriteLine("讀取文件: {0} 線程[{1}] 管道用戶: {2}.", filename, threadId, pipeServer.GetImpersonationUserName()); pipeServer.RunAsClient(fileReader.Start); } catch (IOException e) { Console.WriteLine("異常: {0}", e.Message); } pipeServer.Close(); } } //定義用於在流上讀取和寫入字符串的數據協議 public class StreamString { private Stream ioStream; private UnicodeEncoding streamEncoding; public StreamString(Stream ioStream) { this.ioStream = ioStream; streamEncoding = new UnicodeEncoding(); } public string ReadString() { int len = 0; len = ioStream.ReadByte() * 256; len += ioStream.ReadByte(); byte[] inBuffer = new byte[len]; ioStream.Read(inBuffer, 0, len); return streamEncoding.GetString(inBuffer); } public int WriteString(string outString) { byte[] outBuffer = streamEncoding.GetBytes(outString); int len = outBuffer.Length; if (len > UInt16.MaxValue) { len = (int)UInt16.MaxValue; } ioStream.WriteByte((byte)(len / 256)); ioStream.WriteByte((byte)(len & 255)); ioStream.Write(outBuffer, 0, len); ioStream.Flush(); return outBuffer.Length + 2; } } // 包含在模擬用戶的上下文中執行的方法 public class ReadFileToStream { private string fn; private StreamString ss; public ReadFileToStream(StreamString str, string filename) { fn = filename; ss = str; } public void Start() { string contents = File.ReadAllText(fn); ss.WriteString(contents); } } }
- 客戶端:連接服務端命名管道后,發送要讀取的文件路徑;讀取服務端返回文件內容並輸出
using System; using System.Diagnostics; using System.IO; using System.IO.Pipes; using System.Security.Principal; using System.Text; using System.Threading; namespace IpcClient { class NamedPipeClient { private static int numClients = 4; public static void Run(string[] args) { if (args.Length > 0) { if (args[0] == "spawnclient") { //.代表本機(可替換為具體IP) var pipeClient = new NamedPipeClientStream(".", "testpipe", PipeDirection.InOut, PipeOptions.None, TokenImpersonationLevel.Impersonation); Console.WriteLine("連接服務端...\n"); pipeClient.Connect(); var ss = new StreamString(pipeClient); // Validate the server's signature string. if (ss.ReadString() == "I am the one true server!") { //發送文件路徑,服務端讀取后返回 ss.WriteString(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "namedpipeTxt.txt")); //輸出文件內容 Console.Write($"文件內容:{ss.ReadString()}\r\n"); } else { Console.WriteLine("服務端未驗證通過"); } pipeClient.Close(); Console.ReadLine(); } } else { Console.WriteLine("\n*** 命名管道客戶端示例 ***\n"); StartClients(); } } // 啟動客戶端 private static void StartClients() { string currentProcessName = Environment.CommandLine; currentProcessName = currentProcessName.Trim('"', ' '); currentProcessName = Path.ChangeExtension(currentProcessName, ".exe"); Process[] plist = new Process[numClients]; Console.WriteLine("生成客戶端進程...\n"); if (currentProcessName.Contains(Environment.CurrentDirectory)) { currentProcessName = currentProcessName.Replace(Environment.CurrentDirectory, String.Empty); } //兼容處理 currentProcessName = currentProcessName.Replace("\\", String.Empty); currentProcessName = currentProcessName.Replace("\"", String.Empty); int i; for (i = 0; i < numClients; i++) { //啟動客戶端進程,使用同一個命名管道 plist[i] = Process.Start(currentProcessName, "spawnclient"); } while (i > 0) { for (int j = 0; j < numClients; j++) { if (plist[j] != null) { if (plist[j].HasExited) { Console.WriteLine($"客戶端進程[{plist[j].Id}]已經退出."); plist[j] = null; i--; } else { Thread.Sleep(250); } } } } Console.WriteLine("\n客戶端進程完成,退出中"); } } //定義用於在流上讀取和寫入字符串的數據協議 public class StreamString { private Stream ioStream; private UnicodeEncoding streamEncoding; public StreamString(Stream ioStream) { this.ioStream = ioStream; streamEncoding = new UnicodeEncoding(); } public string ReadString() { int len; len = ioStream.ReadByte(); len = len * 256; len += ioStream.ReadByte(); var inBuffer = new byte[len]; ioStream.Read(inBuffer, 0, len); return streamEncoding.GetString(inBuffer); } public int WriteString(string outString) { byte[] outBuffer = streamEncoding.GetBytes(outString); int len = outBuffer.Length; if (len > UInt16.MaxValue) { len = (int)UInt16.MaxValue; } ioStream.WriteByte((byte)(len / 256)); ioStream.WriteByte((byte)(len & 255)); ioStream.Write(outBuffer, 0, len); ioStream.Flush(); return outBuffer.Length + 2; } } }
四、總結:
若使用管道來實現進程間的通訊,可按如下方式選擇:
- 若是只須要單向通訊,且兩個進行間的關系為父子進程,則可使用匿名管道
- 若是須要雙向通訊,則使用命名管道
- 若是咱們沒法決定到底該選擇什么,那就選擇命名管道的雙向通訊方式。在如今的電腦上,命名管道於匿名管道性能的差異咱們能夠忽略不記。而雙向通訊的命名管道,既可單向,又可雙向,更加靈活
源碼:IpcService
