鑑於小哥哥、小姐姐們每天的工作壓力都很大。決定以後每一篇文章講解的知識點最多不超過三個。這樣有三個好處
- 小哥哥、小姐姐們可以多花一點時間休息,或者陪陪家人
- 知識點少,可以保證我們可以理解得更深刻,也更容易記住
- 知識點少,這樣我們就可以在不增加篇幅的情況下,儘可能地講得深入一些。況且篇幅太長,小哥哥、小姐姐們看久了可能會比較累
程式間傳遞資料,常見的有以下幾種方式:
- 管道:包括命名管道和匿名管道,這篇文章將講解這種方式
- 記憶體對映檔案:藉助檔案和記憶體空間之間的對映關係,應用(包括多個程式)可以直接對記憶體執行讀取和寫入操作,從而實現程式間通訊
Socket
:使用套接字在不同的程式間通訊,這種通訊方式下,需要佔用系統至少一個埠SendMessage
:通過視窗控制程式碼的方式來通訊,此通訊方式基於Windows
訊息WM_COPYDATA
來實現- 訊息佇列:在對效能要求不高的情況下,我們可以使用
Msmq
。但在實際專案中,一般使用ActiveMQ
、Kafka
、RocketMQ
、RabbitMQ
等這些針對特定場景優化的訊息中介軟體,以獲得最大的效能或可伸縮性優勢
其中,管道、記憶體對映檔案、SendMessage
的方式,一般用於單機上程式間的通訊,在單機上使用這三種方式,比使用 Socket
要相對高效,且更容易控制
而 Socket
、訊息佇列或其他基於Socket
的通訊方式,則適用範圍更廣。它不僅適用於本機程式間的通訊,還適用於跨機器(包括跨網段)之間的通訊,比如同一個叢集裡面不同伺服器之間的通訊、微服務群下各個微服務之間的通訊。這也是目前用得最多得方式
雖然在網際網路化的今天,本機程式間通訊可能用得不多。但在這篇文章中,我們還是有必要了解基於管道的程式間通訊方式,後面我們會目前用得比較廣泛的一些框架
命名管道
命名管道,它可以在管道伺服器和一個或多個管道客戶端之間提供程式間通訊。其特點如下
- 命名管道可以是單向的,也可以是雙向的
- 它們支援基於訊息的通訊(即建立服務端管道時,指定
PipeTransmissionMode.Message
選項),並允許多個客戶端使用相同的管道名稱同時連線到伺服器端程式 - 支援模擬,這樣連線程式就可以在遠端伺服器上使用自己的許可權
它既可用於本機程式間的通訊,也能用於跨機器之間的通訊,但實際中很少這樣用
跨機器通訊,特別是在跨網路的情況下,目前普遍的做法是使用一些通訊框架(比如分散式或微服務中,可使用Netty
,RPC
、REST
或Thrift
等等),畢竟這些通訊框架大都成熟穩定,還經歷過商用的考驗
用於傳送資料的示例程式碼如下
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
}
複製程式碼
如果對這種通訊方式感興趣,可以參考 NamedPipeServerStream
與 NamedPipeClientStream
其他的建構函式,來找到更加符合自身業務的模式
匿名管道
匿名管道只能在本機上提供程式間通訊。與命名管道相比,其有如下特點
- 匿名管道需要的開銷更少,但提供的服務有限
- 匿名管道是單向的,且不能通過網路使用,即不能跨網進行通訊
- 僅支援一個伺服器例項
- 匿名管道可用於執行緒間通訊,也可用於父程式和子程式之間的通訊,因為管道控制程式碼可以輕鬆傳遞給所建立的子程式。
服務端 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~