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

IPC(Inter-Process Communication,行程間通訊)是指不同Process之間進行資料交換與通訊的機制。IPC提供不同方法如Socket、Message Queues(訊息佇列)、Shared Memory(共享記憶體)等,能夠有效地交換訊息、共享資源,進行系統的協作及高效通訊。我們就來看看IPC有哪些及看它們的簡單範例吧。

IPC應用在哪邊

  1. 當一個應用程式被拆分為多個獨立的程序時,這些程序需要進行通訊以協調操作,IPC 提供了這些程序之間傳遞資料或訊息的機制。
  2. IPC運用在用戶端-伺服器(Client-Server)架構中進行通訊 。
  3. 有些應用程式有訊息通知功能,例如電子郵件、訊息佇列系統,這些系統通常使用 IPC 機制來傳遞訊息。
  4. 作業系統本身也使用 IPC 機制,例如在核心模組之間的通訊。

管道(Pipes)

管道(Pipes)可讓兩個Process進行單向或雙向的通信。

管道(Pipes)分為命名管道(Named Pipes)匿名管道(Anonymous Pipes),以下是不同之處。

命名管道(Named Pipes)匿名管道(Anonymous Pipes)
命名可指定名稱不需指定名稱
通訊方向雙向通訊單向通訊
用處跨網路通訊用在父處理序子處理序之間通訊
持久性Process結束後仍存在當Process結束後會關閉
命名管道(Named Pipes)匿名管道(Anonymous Pipes)差異

如果需要跨網路通訊,考慮訊息的持久性,命名管道會比較適合;如果只要在同一個程式之間的Process進行通訊,匿名管道是很好的選擇。

匿名管道(Anonymous Pipes)

以下示範匿名管道(Anonymous Pipes),父Process利用AnonymousPipeServerStream類別建立匿名管道,並啟動子Process,讓子Process讀取訊息。執行程式時,PipeServer與PipeClient的執行檔需在同一個目錄下。

  • Server端
C#
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端
C#
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:找不到訊息");
    }
}
  • 執行結果
Console
父Process傳送訊息
子Process: 訊息內容 : Hello from Parent Process

命名管道(Named Pipes)

以下示範命名管道(Named Pipes)的用法。Server建立一個命名管道,等待Client連線。一旦連線建立,Server端會向Client傳送訊息。

  • Server端
C#
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端
C#
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()}");
                }
            }
        }
    }
}
  • 執行結果
Console
正在連線至Server端
Server端已連線
test123
收到Server 訊息內容: test123
Console
等待client端連線
client端已連線
收到Client訊息內容: test123

訊息佇列(Message Queues)

訊息佇列能讓應用程式以非同步方式傳送訊息,傳送端會將訊息放入佇列(queue),而接收端會從佇列取得訊息。發送端接收端不需要即時就能互相通訊,因此接受端可以負責傳送並處理其他事情,而接收端可以在適合的時機點進行訊息讀取。

  • 可靠性:當電腦故障,訊息仍可保留。
  • 順序性:具有先進先出特性。
  • 非同步通訊:傳送端可將訊息放入佇列即可進行其他工作,而接收者可在合適的時機佇列處理訊息。

在示範以前,需要將Windows的MSMQ服務開啟,按下Win+R,輸入OptionalFeatures,將Microsoft Message 佇列(MSMQ)服務器開啟。

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

以下示範訊息佇列(Message Queues)的用法。傳送端利用Send的方法將訊息傳送至佇列,而Reveive方法是負責將佇列的資料取出。

C#
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機制,共享記憶體具有低延遲的特性,可以減少讀取的速度。
C#
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();
                }
            }
        }
    }
}
  • 接收端
C#
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();
                }
            }
        }
    }
}
  • 執行結果
Console
Process 1 Send Hello
Console
Process 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端
C#
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端
C#
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();
        }
    }
}
  • 執行結果
Console
正在等待Client連線
Client已連線
已接受Server訊息:Hello Server
Console
Server已連線
向Server傳送訊息:Hello Server
分享這篇文章

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *