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

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

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

進程間傳遞數據,常見的有如下幾種方式:服務器

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

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

Socket 、消息隊列或其餘基於Socket的通訊方式,則適用範圍更廣。它不只適用於本機進程間的通訊,還適用於跨機器(包括跨網段)之間的通訊,好比同一個集羣裏面不一樣服務器之間的通訊、微服務羣下各個微服務之間的通訊。這也是目前用得最多得方式框架

雖然在互聯網化的今天,本機進程間通訊可能用得很少。但在這篇文章中,咱們仍是有必要了解基於管道的進程間通訊方式,後面咱們會目前用得比較普遍的一些框架分佈式

命名管道

命名管道,它能夠在管道服務器和一個或多個管道客戶端之間提供進程間通訊。其特色以下ide

  • 命名管道能夠是單向的,也能夠是雙向的
  • 它們支持基於消息的通訊(即建立服務端管道時,指定 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:\Users\Jame\source\repos\ConsoleApp4\ConsoleApp4\bin\Debug\ConsoleApp4.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~

公衆號二維碼
相關文章
相關標籤/搜索