[C#] 不同程序之間的溝通 – IPC進程間通訊介紹及範例

IPC(Inter-Process Communication,行程間通訊)是指不同Process之間進行資料交換與通訊的機制。IPC提供不同方法如Socket、Message Queues(訊息佇列)、Shared Memory(共享記憶體)等,能夠有效地交換訊息、共享資源,進行系統的協作及高效通訊。我們就來看看IPC有哪些及看它們的簡單範例吧。
IPC應用在哪邊
- 當一個應用程式被拆分為多個獨立的程序時,這些程序需要進行通訊以協調操作,IPC 提供了這些程序之間傳遞資料或訊息的機制。
- IPC運用在用戶端-伺服器(Client-Server)架構中進行通訊 。
- 有些應用程式有訊息通知功能,例如電子郵件、訊息佇列系統,這些系統通常使用 IPC 機制來傳遞訊息。
- 作業系統本身也使用 IPC 機制,例如在核心模組之間的通訊。
管道(Pipes)
管道(Pipes)可讓兩個Process進行單向或雙向的通信。
管道(Pipes)分為命名管道(Named Pipes)與匿名管道(Anonymous Pipes),以下是不同之處。
| 命名管道(Named Pipes) | 匿名管道(Anonymous Pipes) | |
| 命名 | 可指定名稱 | 不需指定名稱 |
| 通訊方向 | 雙向通訊 | 單向通訊 |
| 用處 | 跨網路通訊 | 用在父處理序子處理序之間通訊 |
| 持久性 | Process結束後仍存在 | 當Process結束後會關閉 |
如果需要跨網路通訊,考慮訊息的持久性,命名管道會比較適合;如果只要在同一個程式之間的Process進行通訊,匿名管道是很好的選擇。
匿名管道(Anonymous Pipes)
以下示範匿名管道(Anonymous Pipes),父Process利用AnonymousPipeServerStream類別建立匿名管道,並啟動子Process,讓子Process讀取訊息。執行程式時,PipeServer與PipeClient的執行檔需在同一個目錄下。
- Server端
using System.IO.Pipes;
using System.Diagnostics;
class PipeServer
{
static void Main()
{
///建立了匿名管道Server端(Out),對子process傳送訊息。
using (var serverStream = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable))
{
// 啟動子Process
var psi = new ProcessStartInfo();
psi.FileName = "PipeClient.exe";
//將args參數傳到子Process
psi.Arguments = serverStream.GetClientHandleAsString();
///向子Process發送訊息
using (Process? process = System.Diagnostics.Process.Start(psi))
{
//利用StreamWriter將訊息寫入匿名管道,傳遞訊息
using (var writer = new StreamWriter(serverStream))
{
Console.WriteLine($"父Process傳送訊息");
writer.WriteLine("Hello from Parent Process");
}
}
Console.ReadLine();
}
}
}- Client端
using System.IO.Pipes;
class PipeClient
{
static void Main(string[] args)
{
/////讀取父Process args
if (args.Length > 0)
{
///建立了匿名管道client端(In),從父process接收訊息。
using (var clientStream = new AnonymousPipeClientStream(PipeDirection.In, args[0]))
using (var reader = new StreamReader(clientStream))//使用StreamReader从匿名管道中讀取訊息
{
/////讀取一行訊息,並進行輸出
string? data = reader.ReadLine();
Console.WriteLine($"子Process: 訊息內容 : {data}");
}
}
else Console.WriteLine("子Process:找不到訊息");
}
}- 執行結果
父Process傳送訊息
子Process: 訊息內容 : Hello from Parent Process命名管道(Named Pipes)
以下示範命名管道(Named Pipes)的用法。Server建立一個命名管道,等待Client連線。一旦連線建立,Server端會向Client傳送訊息。
- Server端
using System.IO.Pipes;
class NamedPipeServer
{
static void Main()
{
///建立命名管道Server端,並指定了管道的名稱為"MyNamedPipe"
using (NamedPipeServerStream pipeServer = new NamedPipeServerStream("MyNamedPipe", PipeDirection.InOut))
{
Console.WriteLine("等待client端連線");
///等待client端連線
pipeServer.WaitForConnection();
Console.WriteLine("client端已連線");
///利用StreamReader與StreamWriter讀取與傳送訊息
StreamReader reader = new StreamReader(pipeServer);
StreamWriter writer = new StreamWriter(pipeServer);
while (true)
{
try
{
///從client端讀取訊息
var line = reader.ReadLine();
Thread.SpinWait(1000);
Console.WriteLine($"收到Client訊息內容: {line}");
///對client送出訊息
writer.WriteLine(line);
writer.Flush();
}
catch (IOException ex)
{
// 客戶端中斷連線,中斷迴圈
Console.WriteLine($"Client端中斷連線 : {ex.Message}");
return;
}
}
}
}
}- Client端
using System.IO.Pipes;
class NamedPipeClient
{
static void Main()
{
///建立命名管道Client端,並指定了管道的名稱為"MyNamedPipe"
using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", "MyNamedPipe", PipeDirection.InOut))
{
Console.WriteLine("正在連線至Server端");
pipeClient.Connect();
Console.WriteLine("Server端已連線");
///利用StreamReader與StreamWriter讀取與傳送訊息
StreamReader reader = new StreamReader(pipeClient);
StreamWriter writer = new StreamWriter(pipeClient);
while (true)
{
string input = Console.ReadLine();
if (!String.IsNullOrEmpty(input))
{
writer.WriteLine(input);
writer.Flush();
Console.WriteLine($"收到Server 訊息內容: {reader.ReadLine()}");
}
}
}
}
}- 執行結果
正在連線至Server端
Server端已連線
test123
收到Server 訊息內容: test123等待client端連線
client端已連線
收到Client訊息內容: test123訊息佇列(Message Queues)
訊息佇列能讓應用程式以非同步方式傳送訊息,傳送端會將訊息放入佇列(queue),而接收端會從佇列取得訊息。發送端接收端不需要即時就能互相通訊,因此接受端可以負責傳送並處理其他事情,而接收端可以在適合的時機點進行訊息讀取。
- 可靠性:當電腦故障,訊息仍可保留。
- 順序性:具有先進先出特性。
- 非同步通訊:傳送端可將訊息放入佇列即可進行其他工作,而接收者可在合適的時機佇列處理訊息。
在示範以前,需要將Windows的MSMQ服務開啟,按下Win+R,輸入OptionalFeatures,將Microsoft Message 佇列(MSMQ)服務器開啟。

可在電腦管理的私用佇列查詢狀態。

以下示範訊息佇列(Message Queues)的用法。傳送端利用Send的方法將訊息傳送至佇列,而Reveive方法是負責將佇列的資料取出。
using System;
using System.Messaging;
class QueueMessage
{
static string queuePath = ".\\Private$\\MyQueue"; //路徑
static void Main(string[] args)
{
///傳送訊息
Send("Hello");
Console.ReadLine();
///接收訊息
Console.WriteLine($"Reveive Message:{Reveive()}");
Console.ReadLine();
}
/// <summary>
/// 傳送訊息
/// </summary>
private static void Send(string message)
{
MessageQueue messageQueue;
if (MessageQueue.Exists(queuePath))
{
//佇列存在,實體化該物件
messageQueue = new MessageQueue(queuePath);
}
else
{
// 如果佇列不存在,則建立一個新的
messageQueue = MessageQueue.Create(queuePath);
}
// 發送消息
System.Messaging.Message msg = new System.Messaging.Message();
msg.Body = message;
messageQueue.Send(msg);
Console.WriteLine($"Send Message:{message}");
}
/// <summary>
/// 接收訊息
/// </summary>
private static string Reveive()
{
//對佇列實體化物件
MessageQueue messageQueue = new MessageQueue(queuePath);
// 指定佇列格式
messageQueue.Formatter = new XmlMessageFormatter(new Type[] { typeof(string) });
//接收消息
System.Messaging.Message message = null;
message = messageQueue.Receive();
return message.Body.ToString();
}
}共享記憶體(Shared Memory)
共享記憶體可以讓不同的程式存取相同的記憶體區塊資料。因此可讓不同程式間的讀取速度很快。以下示範共享記憶體的範例,ProcessA將一個訊息寫入記憶體區塊,ProcessB將該記憶體的資料讀出。以下為Shared Memory的特性:
- 高效能、低延遲:相較於其他的IPC機制,共享記憶體具有低延遲的特性,可以減少讀取的速度。
using System.IO.MemoryMappedFiles;
using System.Text;
namespace SharedMemoryProcessA
{
internal class Program
{
static void Main()
{
string message = "Hello";
///創建一個新的共享記憶體區塊,名稱為 "SharedMemoryTest",大小為 1000 個位元組。
using (MemoryMappedFile mmf = MemoryMappedFile.CreateNew("SharedMemoryTest", 1000))
{
///建立讀寫作業實體
using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor())
{
///將訊息轉換成 ASCII 編碼的字元陣列
byte[] messageBytes = Encoding.ASCII.GetBytes(message);
//在共享記憶體位置0的地方寫入字元陣列。
accessor.WriteArray(0, messageBytes, 0, messageBytes.Length);
Console.WriteLine($"Process 1 Send {message}");
Console.ReadLine();
}
}
}
}
}- 接收端
using System.IO.MemoryMappedFiles;
using System.Text;
namespace SharedMemoryProcessB
{
internal class Program
{
static void Main()
{
string receivedMessage = string.Empty;
///讀取目前的共享記憶體區塊,名稱為 "SharedMemoryTest"。
using (MemoryMappedFile mmf = MemoryMappedFile.OpenExisting("SharedMemoryTest"))
{
///建立讀寫作業實體
using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor())
{
/////讀取訊息長度。
byte[] messageBytes = new byte[accessor.Capacity];
/////在共享記憶體位置0的地方讀取字元陣列。
accessor.ReadArray(0, messageBytes, 0, messageBytes.Length);
///將字元從ASCII轉換成string
receivedMessage = Encoding.ASCII.GetString(messageBytes).Trim('\0');
Console.WriteLine($"Process 2 Reveice {receivedMessage}");
Console.ReadLine();
}
}
}
}
}- 執行結果
Process 1 Send HelloProcess 2 Reveice Hello網路插座(Socket)
Socket是透過網路協定通訊的機制,因此可以讓不同電腦利用Socket進行通訊,Server端會建立Socket實體並進行監聽(Listening)等待Client端連線,而客戶端需要指定IP及Port來指定要連線到某一台Server來進行通訊。以下為Socket的特性:
- 通用性:不同電腦、作業系統、網路環境可以透過網路協定來進行通訊。
- 雙向通訊:Socket有全雙工的特性,Client端與Server端都能互相傳送訊息。
- 支持多種協議:Socket提供TCP(Transmission Control Protocol)和UDP(User Datagram Protocol)的協議,前者能保證資料的送達;後者追求訊息傳輸的速度例如媒體串流、網路遊戲等。
以下介紹Socket的範例,Server端建立Socket連線實體進行監聽,等待Client端連練,當Client連線傳送訊息。
- Server端
using System.Net.Sockets;
using System.Net;
using System.Text;
namespace SocketServer
{
internal class Program
{
static void Main(string[] args)
{
///設定IP位置及port
IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
int port = 5000;
///建立socket物件實體
Socket listener = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint localEndPoint = new IPEndPoint(ipAddress, port);
///binding 伺服器 可監聽5個連線
listener.Bind(localEndPoint);
listener.Listen(5);
Console.WriteLine("正在等待Client連線");
Socket clientsocket = listener.Accept();
Console.WriteLine("Client已連線");
//對客戶端傳送訊息
byte[] buffer = new byte[1024];
int byterec = clientsocket.Receive(buffer);
string receivemessage = Encoding.ASCII.GetString(buffer, 0, byterec);
Console.WriteLine($"已接受Server訊息:{receivemessage}");
// 關閉連線
clientsocket.Shutdown(SocketShutdown.Both);
clientsocket.Close();
listener.Close();
Console.ReadLine();
}
}
}- Client端
using System;
using System.Net.Sockets;
using System.Net;
using System.Text;
using System.IO;
namespace SocketClient // Note: actual namespace depends on the project name.
{
internal class Program
{
static void Main(string[] args)
{
//////設定IP位置及port
IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
IPEndPoint serverendpoint = new IPEndPoint(ipAddress, 5000);
///建立socket物件實體
Socket clientsocket = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
///連線server
clientsocket.Connect(serverendpoint);
Console.WriteLine("Server已連線");
///對server傳送訊息
string sendMessage = "Hello Server";
byte[] message = Encoding.ASCII.GetBytes(sendMessage);
Console.WriteLine($"向Server傳送訊息:{sendMessage}");
clientsocket.Send(message);
// 關閉連線
clientsocket.Close();
Console.ReadLine();
}
}
}- 執行結果
正在等待Client連線
Client已連線
已接受Server訊息:Hello ServerServer已連線
向Server傳送訊息:Hello Server


