Windows 的行程間通訊該怎麼選 ── 具名管道 / TCP / gRPC / 共享記憶體 / COM 判斷表

· · 行程間通訊, 具名管道, Windows, .NET, C#, gRPC, 共享記憶體, COM, 設計, 判斷表, 技術諮詢

「把 UI 和服務分開之後,中間的通訊該怎麼辦」「想從 32bit 的應用程式使用 64bit DLL 的功能」「想讓另一個行程的量測引擎把資料傳到畫面上」。把應用程式拆成多個行程的設計,經常是應付堅固性、權限分離、位元數問題的現實解法,但拆開的那一刻,「中間的通訊要怎麼做」這個選擇就一定會出現。

這個部落格目前為止已經寫過個別的通訊手段,像是共享記憶體的陷阱TCP 的訊框化子行程的安全處理檔案協作與鎖定。不過「到底該選哪一種手段」這個總論篇還沒有寫過。這篇文章會把 Windows 行程間通訊(IPC)的主要選項──檔案協作・具名管道・本機 TCP・gRPC・共享記憶體・COM──的強項與陷阱,用這個部落格慣用的判斷表格式整理出來。

1. 先講結論

  • 同一台電腦內的請求-回應(送出指令並取得結果)的話,具名管道是首選。 是 OS 標準功能、不需要連接埠,且與 Windows 的存取控制整合,在 .NET 中可以用 System.IO.Pipes 直接寫出來。1
  • 未來如果有可能跨越網路,一開始就選本機 TCP 或 gRPC。 從管道換成 socket 往往不是「之後小改一下」就能解決的差異。不過純 TCP 是位元組串流,因此訊框化的設計是必須的。
  • 當服務或呼叫的種類增加,自訂協定的維護開始變成負擔時,就用 gRPC。 可以取得 proto 定義的結構與程式碼產生、雙向串流,.NET 8 以後 ASP.NET Core(Kestrel)可以把具名管道當作傳輸層。2
  • 只有大量・高頻率的資料(影像畫面、波形)才用共享記憶體。 速度最快,但同步機制要全部自己設計,因此原則是不要連控制訊息都放進共享記憶體。3
  • 鬆散耦合・非同步就夠、且想留下稽核記錄的協作,現在依然以檔案協作為優。 只是成敗取決於排他控制的設計。
  • COM(跨行程)不會是新設計的首選,但在 VBA・其他語言的使用情境,或是 32bit/64bit 橋接的場合,現在依然是現役工具。4
  • 不管選哪一種手段,訊息的分界・版本號・逾時・重新連線這類共同的設計課題都逃不掉(第 6 章)。花在這裡的時間,應該和選手段一樣多。

2. 選項的底細

2.1 檔案協作 ── 鬆散耦合・非同步・可稽核

「A 把檔案放到輸出資料夾,B 拿去處理」是經典的協作方式。雙方不需要同時啟動,處理過程會留下檔案作為證據,發生問題時人可以直接看檔案、修改後重新送入。在不要求即時性的批次式協作中,這種方式至今依然強大。

陷阱幾乎集中在一點:排他控制。「讀到寫入中途的檔案」「兩個處理搶同一個檔案」是常見事故,需要遵循先用暫存檔名寫入再重新命名之類的定石。詳細內容整理在《檔案協作與鎖定的最佳實踐》中,選擇這種方式時務必參考。不適合需要即時回應的互動式通訊,或是每秒發生幾十次的交換。

2.2 具名管道 ── 同一台電腦 IPC 的本命

具名管道是 Windows 在核心層提供的雙向通訊路徑,是同一台電腦內用戶端・伺服器型 IPC 的本命。在 .NET 中以 NamedPipeServerStream / NamedPipeClientStream 處理,一個管道名稱可以讓多個用戶端連線。5 和 TCP 不同,不需要管理連接埠,也不會被防火牆擋下。

以下列出實務上該掌握的重點。

  • 可以使用訊息模式。 指定 PipeTransmissionMode.Message 後,一次寫入就會以一則訊息為單位切分並送達。這讓 OS 代勞了 TCP 中必須自己實作的 length 前置長度等訊框化工作,是實務上很大的優點(接收端需要用 IsMessageComplete 讀完一則訊息。參見第 5 章)。
  • 預設存取權限出乎意料地寬鬆。 管道的預設安全性描述元會給 LocalSystem・管理員・建立者完整控制權,但同時也允許 Everyone 與匿名帳戶讀取6 如果是同一使用者的行程之間,只要加上 PipeOptions.CurrentUserOnly 就能強制「只和同一使用者建立的對象連線」,建議把這個當作預設作法。7 不同帳戶之間(例如和服務通訊)則要用 PipeSecurity 明確指定 ACL。
  • 管道名稱位於所有人都看得到的命名空間中。 管道名稱放在 \\.\pipe\ 底下單一的命名空間裡,同一台電腦上其他使用者的行程也能看到。這裡要注意的是名稱佔用(squatting)。如果惡意行程先用相同名稱建立伺服器並等待連線,用戶端就會連到那邊去。伺服器端可以指定 PipeOptions.FirstPipeInstance,讓已有同名管道時直接失敗;用戶端則可以在連線後驗證管道的擁有者帳戶。在有不同權限使用者共用同一台電腦時,應該加入這類對策。8
  • 可以進行跨越權限邊界的通訊。 作為 UAC 分離出的「標準權限 UI + 管理員權限中介(broker)」之間的通訊路徑,也是常見做法。不過這種架構下無法使用 CurrentUserOnly(因為即使是同一使用者,也要求提升層級必須一致7)。需要明確設計 ACL,具體做法已在《管理員權限的分離(broker)》中詳述。

弱點是實務上不太適合跨機器通訊(規格上可行,但運維上限制很多),也不容易用於和非 Windows 系統的互連。如果看得到這類需求,就選 TCP / gRPC。

2.3 本機 TCP ── 跨語言・跨 OS 的通用性

localhost 的 TCP 連線,幾乎能和任何語言・執行環境・OS 溝通,是最通用的 IPC。像「Linux 上的分析引擎與 Windows 的 UI」這種混合架構,首先會考慮 TCP(或架在其上的 HTTP/gRPC)。

要注意的地方有 3 點。

  • 它是位元組串流。 TCP 沒有「按送出的單位到達」這種性質。Send 送出的 3 則訊息在一次 Receive 中被串接送達,或是一則訊息被拆成好幾次送達,都是正常現象。必須在應用層自行設計 length 前置長度之類的訊框化,省略這一步的程式碼只是「碰巧能動」而已。詳細內容請參考《以為 TCP 的 Send 單位可以對應 Receive 是一種誤解》。
  • 收斂待接聽的公開範圍。 即使原本只打算做同一台電腦內的通訊,只要用 0.0.0.0 待接聽,網路上其他電腦就能連進來。原則上本機 IPC 應該綁定在 127.0.0.1(loopback)上。即便如此,同一台電腦上的其他使用者依然能連線,所以如果需要確認對方身分,就要在應用層加上驗證。這一點和管道那種與 OS 整合的存取控制有明顯差異。
  • 連接埠的維運會一直跟著你。 固定連接埠可能和其他軟體衝突,防火牆產品也可能把它偵測為「可疑的待接聽」。應該從一開始就把變更連接埠號碼的手段納入設計。

2.4 gRPC ── 買的是結構定義和程式碼產生

和純 socket + 自訂協定相比,gRPC 提供的與其說是通訊本身,不如說是開發的框架。只要用 proto 檔案定義服務與訊息,序列化・訊框化・用戶端/伺服器程式碼就會全部產生出來,雙向串流(伺服器主動推播通知)也能像語言內建功能一樣撰寫。當你進入「每次訊息增加就要修改自訂協定的 switch 陳述式和文件」的階段,這個框架就能發揮作用。

.NET 8 以後,ASP.NET Core(Kestrel)不只支援 TCP,還直接支援 Unix 網域 socket 與具名管道作為傳輸層8 伺服器端只需要呼叫 ListenNamedPipe,也能設定 PipeSecurity 的存取控制。2 「通訊路徑維持管道、免連接埠・整合 ACL,協定層則用 gRPC」這種組合,在 UI + 服務分離(第 4 章)中是很有力的選項。

另一方面,對於小規模的工具間通訊,這往往會過度設計。建立 gRPC 伺服器意味著要扛起 ASP.NET Core 的主機基礎設施,會增加部署物、依賴關係與啟動成本。如果指令只有 2〜3 種的工具之間,具名管道 + JSON(第 5 章)的總成本會更低,請保有這條分界線。等到「proto 裡要寫的訊息超過 10 個」「本質上需要串流通知」「對方不是 .NET」三者之一成立後再導入也不遲。

2.5 共享記憶體(記憶體對應檔案) ── 最快,但同步全部要自己來

同一台電腦內最快的 IPC 是共享記憶體。在 .NET 中可以用 MemoryMappedFile.CreateNew 建立具名的共享記憶體,讓多個行程直接讀寫同一段位元組序列。3 因為不涉及複製或序列化,對影像畫面或波形資料這類「又大又快」的資料,幾乎可以說是唯一的選擇,效能表現也確實出色。

不過,共享記憶體並不是「快速的管道」。看到的只是相同的位元組序列,完全不提供任何同步機制。 不讀取寫入中資料的機制、偵測對方存活狀態、一方異常結束後的復原──全部都要自己設計。連環形緩衝區架構與版面配置版本管理都包含在內的設計論,整理在《共享記憶體的陷阱與實務最佳實踐》這篇文章中。

實務上的使用場合很明確,就是只把資料面(data plane)放上共享記憶體,控制面(control plane,如開始・停止・變更設定)則交給具名管道等其他通道的架構(第 4 章的架構 3)。一旦連開始・停止・變更設定之類的控制訊息都想硬塞進共享記憶體,就是該重新檢視設計的訊號。

2.6 COM(跨行程) ── 不是新設計的首選,但仍有現役的場合

COM 的跨行程伺服器(EXE 伺服器),是 Windows 很早就有的「像呼叫本機函式一樣呼叫另一個行程裡的物件」機制。4 在新的應用程式間通訊中,已經不會把它當作首選,但在下列情境中依然實用。

  • 32bit/64bit 橋接:32bit 應用程式無法把 64bit 的 DLL(或反過來)載入同一個行程,但跨行程 COM 可以跨越這個邊界。COM 基礎架構會代勞封送處理,呼叫端幾乎不用改程式碼。實際案例寫在《32bit→64bit COM 橋接實例》中。
  • VBA・其他語言的使用:想讓 Excel VBA 之類的舊執行環境呼叫 .NET 的功能時,把它以 COM 公開,現在依然是最直接的路徑。
  • 與既有 COM 資產的協作:如果對方只能用 COM 溝通,這邊也用 COM 溝通是最快的做法(COM 本身的說明請見《COM・ActiveX・OCX 是什麼》)。

讓人猶豫是否新採用的原因,是伴隨登錄檔登錄的部署麻煩、介面設計與參考計數的學習成本,以及發生問題時調查的困難度。請先和「如果目的是 32/64 橋接,就把 64bit 端做成單純的輔助行程,用具名管道溝通」這個替代方案(第 4 章的架構 2)比較之後再決定。

2.7 古典的手段 ── 新設計不會選

Windows 還有 WM_COPYDATA(用視窗訊息傳資料)、剪貼簿、DDE、mailslot 這類 IPC 手段,官方的 IPC 概觀頁面現在依然列著它們。4 只是這些手段大多以視窗存在為前提,或是正在走向淘汰(遠端 mailslot 正處於淘汰程序中),新設計幾乎沒有理由選擇它們。維護既有應用程式時遇到能認出來就夠了。

3. 判斷表

觀點 檔案 具名管道 本機 TCP gRPC 共享記憶體 COM
通訊範圍 用共享資料夾可跨機器 同一台電腦是實用範圍 可跨機器 可跨機器 限同一台電腦 同一台電腦是實用範圍
通訊模型 檔案交接(非同步) 串流+訊息模式 位元組串流 RPC+串流 共享狀態 方法呼叫
伺服器→用戶端通知 ✕(輪詢) ◎(雙向串流) △(需搭配事件) △(能做但複雜)
跨越權限邊界(UAC・服務) ○(資料夾 ACL) ◎(PipeSecurity) △(驗證要自己做) △〜○(用管道傳輸則為◎) ○(可設 ACL,但設計難)
32/64・語言混用 ◎(從 proto 產生各語言) △(必須設計 ABI) ○(擅長橋接)
實作成本 低〜中 中(訊框化要自己做) 中(要導入基礎設施) 高(對新設計而言)
除錯難易度 ◎(內容留存在檔案中) ○(可擷取封包) △(HTTP/2+二進位) △(症狀較劇烈)
吞吐量/延遲 中〜高

補充一點。正確的用法不是選「◎ 最多的手段」,而是只看影響到需求的那幾行,用排除法收斂範圍。例如一旦決定「每秒 30 個畫面的影像」,資料面就只能選共享記憶體。

4. 典型架構範例

把判斷表套用到個別需求上,實務中常常會收斂到下面 3 種架構。

架構 1:UI 應用程式 + Windows 服務的分離。 把常駐處理或需要特權的處理放在服務中,UI 則是普通的使用者行程(服務端的做法請參考同日公開的《Windows 服務文章》)。通訊以具名管道為本命,協定從 JSON 訊息+訊息模式(或位元組模式+length 前置長度)開始是穩健的做法,等呼叫種類增多後,也有移轉到 gRPC 的具名管道傳輸層的路可以走。2 因為服務是用不同帳戶(如 LocalService)執行,無法使用 CurrentUserOnly,重點是要用 PipeSecurity 明確指定「允許 Users 讀寫、拒絕遠端」之類的 ACL。

架構 2:32bit 應用程式 → 64bit 功能的橋接。 想從 32bit 的既有應用程式使用只能在 64bit 上執行的 DLL・驅動程式 SDK 時,要把 64bit 端切到別的行程。實作方式有兩種:(a) 建立 64bit 輔助行程並用具名管道溝通,(b) 做成 64bit 的跨行程 COM 伺服器。如果呼叫端是 VBA 或舊語言,(b) 比較自然;如果雙方都是 C#,(a) 在部署與除錯上都更輕鬆。採用 (a) 時,輔助行程的啟動・監控・親子連動結束,請直接套用《安全地處理子行程》中提到的 Job Object 模式。

架構 3:量測引擎 → 顯示用的高頻率資料。 把量測・影像處理引擎拆成別的行程,再把資料流向 UI 的架構中,控制(開始・停止・設定)用具名管道,資料(畫面・波形)用共享記憶體+具名事件的雙通道架構是常見做法。控制每秒只有幾次,管道就夠用;資料則用共享記憶體逼近零複製。把通道分開後,就能把資料端的設計(環形緩衝區等)和控制端的考量分開,各自最佳化。引擎或外部裝置的狀態顯示,也可以參考《外部裝置的狀態顯示》。

5. 實作範例 ── 具名管道的非同步伺服器與用戶端

以下展示作為具名管道實作基礎的、.NET 8 下的最小架構,包含訊息模式・支援多用戶端・斷線處理。協定設計成「UTF-8 的 JSON、一則訊息一封」,並且一定要帶 version 欄位。下面的範例以同一使用者行程間通訊為前提,使用了 CurrentUserOnly。如果像架構 1 那樣要和服務(不同帳戶)通訊,請如稍後所述,移除 CurrentUserOnly 並用 PipeSecurity 明確指定 ACL。

先看伺服器端。每次接受連線都會重新建立新的伺服器串流,並把每個用戶端的處理分離到各自的工作。

using System.IO.Pipes;
using System.Text.Json;

public sealed class PipeServer(string pipeName)
{
    // camelCase・不區分大小寫的 Web 預設值。System.Text.Json 的預設值
    // 會區分大小寫,如果不共用這個設定,"version" 就不會
    // 綁定到 Request.Version,導致正常的請求也落入 unknown_type
    internal static readonly JsonSerializerOptions JsonOptions =
        new(JsonSerializerDefaults.Web);

    public async Task RunAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            var pipe = new NamedPipeServerStream(
                pipeName,
                PipeDirection.InOut,
                NamedPipeServerStream.MaxAllowedServerInstances,
                PipeTransmissionMode.Message,   // 寫入一次 = 一則訊息
                PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);

            await pipe.WaitForConnectionAsync(ct);
            _ = Task.Run(() => HandleClientAsync(pipe, ct), ct);  // 每個連線分開處理
        }
    }

    // 單則訊息的上限。即使對方送來巨大的訊息,
    // 也要保護服務端的記憶體(IPC 的對象也算外部輸入。第6章)
    private const int MaxMessageBytes = 1024 * 1024;

    private static async Task HandleClientAsync(
        NamedPipeServerStream pipe, CancellationToken ct)
    {
        await using (pipe)
        {
            var buffer = new byte[64 * 1024];
            try
            {
                while (!ct.IsCancellationRequested)
                {
                    // 即使是訊息模式,也不保證一次 Read 就能讀完
                    // 整則訊息。要讀到 IsMessageComplete 為止
                    using var ms = new MemoryStream();
                    do
                    {
                        int n = await pipe.ReadAsync(buffer, ct);
                        if (n == 0) return;  // 用戶端已斷線
                        ms.Write(buffer, 0, n);
                        if (ms.Length > MaxMessageBytes) return;  // 超過上限,斷線
                    } while (!pipe.IsMessageComplete);

                    byte[] response = Dispatch(ms.ToArray());
                    await pipe.WriteAsync(response, ct);
                }
            }
            catch (IOException)
            {
                // 只是這個用戶端的通訊路徑壞了。
                // 不停掉整個伺服器,繼續處理其他連線
            }
        }
    }

    private static byte[] Dispatch(byte[] payload)
    {
        Request? req;
        try { req = JsonSerializer.Deserialize<Request>(payload, JsonOptions); }
        catch (JsonException) { req = null; }

        // 格式錯誤・未知版本・未知 type 都在這裡檢查並拒絕
        object result = req switch
        {
            null => new { version = 1, error = "bad_request" },
            // version 未指定(會綁定成 0)或未知版本,都在這裡拒絕
            { Version: not 1 } => new { version = 1, error = "version_unsupported" },
            { Type: "getStatus" } => new { version = 1, running = true },
            { Type: "startJob" } => new { version = 1, jobId = StartJob(req) },
            _ => new { version = 1, error = "unknown_type" },
        };
        return JsonSerializer.SerializeToUtf8Bytes(result, JsonOptions);
    }
}

public sealed record Request(int Version, string Type, JsonElement? Body);

用戶端則把「連線 → 請求 → 回應」封裝在一個方法裡,並從一開始就加入逾時與重試。

using System.IO.Pipes;
using System.Text.Json;

public sealed class PipeClient(string pipeName)
{
    // idempotent: 只有呼叫端明確宣告「這個請求即使送達兩次也安全」
    // 時才會重試。預設是「不重試」
    public async Task<TResponse?> RequestAsync<TResponse>(
        object request, CancellationToken ct, bool idempotent = false)
    {
        for (int attempt = 1; ; attempt++)
        {
            // 不只是連線,整個請求→回應的交換過程都設上限時間。
            // 避免伺服器只接受連線卻沒寫回應就卡死時,
            // 造成無限等待
            using var deadline =
                CancellationTokenSource.CreateLinkedTokenSource(ct);
            deadline.CancelAfter(TimeSpan.FromSeconds(10));
            try
            {
                using var pipe = new NamedPipeClientStream(
                    ".", pipeName, PipeDirection.InOut,
                    PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);

                // 不無限等待。如果伺服器沒有啟動,就會丟出例外
                await pipe.ConnectAsync(timeout: 3000, deadline.Token);
                pipe.ReadMode = PipeTransmissionMode.Message;

                await pipe.WriteAsync(
                    JsonSerializer.SerializeToUtf8Bytes(
                        request, PipeServer.JsonOptions),
                    deadline.Token);

                using var ms = new MemoryStream();
                var buffer = new byte[64 * 1024];
                do
                {
                    int n = await pipe.ReadAsync(buffer, deadline.Token);
                    if (n == 0) throw new IOException("伺服器已斷線。");
                    ms.Write(buffer, 0, n);
                } while (!pipe.IsMessageComplete);

                return JsonSerializer.Deserialize<TResponse>(
                    ms.ToArray(), PipeServer.JsonOptions);
            }
            catch (OperationCanceledException) when (ct.IsCancellationRequested)
            {
                throw;  // 呼叫端主動取消,不重試
            }
            catch (Exception ex) when (
                ex is IOException or TimeoutException or OperationCanceledException
                && idempotent && attempt < 3)
            {
                // 針對伺服器重啟中之類的暫時性失敗進行重試。
                // 「送出後但還沒讀到回應就斷線」的情況下,請求有可能
                // 已經執行過了,因此像 startJob 這種有副作用的請求,
                // 依預設不重試;只有在設計成可用請求 ID 擋掉重複之後,
                // 才傳入 idempotent: true(第6章)
                await Task.Delay(500 * attempt, ct);
            }
        }
    }
}

針對這段程式碼的設計判斷,補充 3 點。

  • 從第一個版本就加入 version UI 和服務一定會遇到分別更新(只有一邊變新)的時刻。只要回應端做到「明確拒絕不認識的版本」,原本會悄悄跑出怪異行為的事故,就會變成「看得懂的錯誤」。
  • 決定連線要用一次性,還是常駐連線。 上面的範例是每次請求都重新連線的一次性做法,不需要管理斷線・重新連線的狀態,但不適合高頻率的呼叫。如果需要常駐連線+伺服器主動推播通知,那種複雜度就是換成 gRPC 雙向串流的判斷依據。
  • 和服務通訊時,要移除 CurrentUserOnly,並設計 PipeSecurity 上面的範例假設是同一使用者的行程之間。如果對方是不同帳戶的服務,請用 NamedPipeServerStreamAcl.Create 建立帶 ACL 的伺服器串流,並明確指定允許連線的群組。6

6. 協定設計的共通注意事項 ── 選定手段之後的工作

不管選哪一種 IPC,通訊內容的設計都有共通的課題。其實比起選手段,這裡才是事故的源頭。

  • 訊息的分界(訊框化):除了像管道的訊息模式那樣由 OS 負責分界的情況之外,「從哪裡到哪裡算一則訊息」是應用程式自己的責任。共享記憶體的記錄邊界也是同樣的問題。請以 length 前置長度方式為基本做法。
  • 版本控制:在訊息中包含格式版本,接收端要「明確拒絕不認識的版本」。即使用同一個安裝程式發布,也不保證兩個行程永遠會同時更新。
  • 逾時:把「對方不回應」當成一定會發生的事來設計。為連線・請求・回應各自設定上限時間,不要留下無限等待的程式碼。在 UI 執行緒上同步等待更是不可行。
  • 重新連線與冪等性:重試意味著會製造出同一個請求送達兩次的可能性。要依請求種類分類「執行兩次也安全的請求」,對於不安全的請求(如投遞工作)則加上請求 ID 來擋掉重複。
  • 把對方也當成外部輸入來驗證:如果是同一台電腦內的通訊,很容易因為「對方是自己的應用程式」就省略驗證,但只要存在權限邊界,IPC 的對象就等同於來自網路的輸入,是同樣「不可信任的輸入」。管理員權限的中介或服務,不應該把標準權限用戶端送來的路徑或指令原封不動拿來執行。連同管道名稱佔用(2.2 節)一起,「懷疑對方是誰」「懷疑要做什麼」兩者並重,才是跨越權限邊界的 IPC 該有的作法。
  • 保留觀測手段:只要能輸出通訊記錄(至少要有訊息類型與對象・結果),「連不上」「沒有回應」這類問題的調查時間就會差好幾個量級。

7. 結語

行程間通訊的選擇,按照下面的順序思考幾乎不會迷路。首先確認是否可以限定在同一台電腦內。如果可以限定,請求-回應用具名管道、大量高頻率資料只用共享記憶體、鬆散耦合的批次處理用檔案協作。如果看得到跨機器或跨語言混用,就用本機 TCP 或 gRPC;當自訂協定的維護規模變成負擔時,就選 gRPC。COM 不當作新設計的首選,而是保留作為 32/64 橋接與 VBA 協作的工具──這篇文章的判斷表,就是把這個流程整理成表格。

而且一旦決定了手段,就要把第 6 章的共通設計(訊框化・版本・逾時・重新連線・驗證對方)放進第一個版本裡。從實務上的體感來說,IPC 的問題比起「選錯手段」,更多是因為「省略了協定設計」而發生,而且差距相當大。行程拆分或通訊方式的重新檢視,建議先從盤點「哪個行程・用誰的權限・做什麼・多常互動」開始。

相關文章

相關諮詢領域

小村軟體有限公司承接行程拆分・通訊方式的設計檢視、包含 32bit/64bit 橋接的既有資產協作設計,以及「連不上・沒有回應」這類通訊問題的原因調查。

參考連結

  1. Microsoft Learn,How to: Use Named Pipes for Network Interprocess Communication。關於用 NamedPipeServerStream / NamedPipeClientStream 實作支援多用戶端的伺服器・用戶端範例。 

  2. Microsoft Learn,Inter-process communication with gRPC and Named pipes。關於 .NET 8 以後透過 Kestrel 的 ListenNamedPipe,讓 gRPC 跑在具名管道上的架構,以及 PipeSecurity 的存取控制。  2 3

  3. Microsoft Learn,MemoryMappedFile Class。關於記憶體對應檔案的 .NET API,以及用 CreateNew 建立、不綁定檔案的共享記憶體適合用於 IPC 的說明。  2

  4. Microsoft Learn,Interprocess communications。關於 Windows 提供的 IPC 機制(剪貼簿、COM、WM_COPYDATA、DDE、檔案對應、mailslot、管道、RPC、Windows socket)清單與選用準則。  2 3

  5. Microsoft Learn,NamedPipeServerStream Class。關於具名管道的伺服器端串流,以及指定 PipeTransmissionMode 或 PipeSecurity 的建構函式。 

  6. Microsoft Learn,Named Pipe Security and Access Rights。關於具名管道的預設安全性描述元允許 Everyone 與匿名帳戶讀取,以及存取權限的組成。  2

  7. Microsoft Learn,PipeOptions Enum。關於 CurrentUserOnly 只允許與「同一使用者建立的對象」連線,在 Windows 上除了使用者帳戶之外,還會驗證到提升層級。  2

  8. Microsoft Learn,Inter-process communication with gRPC。關於把 gRPC 用於 IPC 時的傳輸層(Unix 網域 socket、具名管道),以及伺服器擁有者驗證・偽裝(impersonation)對策等安全性考量。  2

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

Windows 的行程間通訊該以什麼為首選?
同一台電腦內的請求-回應(送出指令並取得結果)的話,具名管道是首選。它是 OS 標準功能,不需要管理連接埠,也不會被防火牆擋下,並與 Windows 的存取控制(PipeSecurity)整合,在 .NET 中可以用 System.IO.Pipes 直接寫出來。如果未來有可能跨越網路,一開始就選本機 TCP 或 gRPC;只有大量・高頻率的資料(影像畫面、波形)才用共享記憶體;鬆散耦合・非同步且想留下稽核記錄的協作則用檔案協作──用這種排除法來收斂範圍才是正確的選擇方式。
gRPC 適合在什麼時候用於行程間通訊?
當服務或呼叫的種類增加,自訂協定的 switch 陳述式與文件維護開始變成負擔時。透過 proto 檔案定義結構並產生程式碼、取得雙向串流,而且 .NET 8 以後 ASP.NET Core(Kestrel)可以直接把具名管道當作傳輸層。另一方面,對於只有 2〜3 種指令的工具之間,扛著 ASP.NET Core 的主機基礎設施會是過度設計,具名管道+JSON 的總成本會更低。等到「proto 裡要寫的訊息超過 10 個」「本質上需要串流通知」「對方不是 .NET」這三者之一成立後再導入也不遲。
從 32bit 應用程式使用 64bit DLL 該怎麼做?
32bit 行程無法把 64bit 的 DLL 載入同一個行程,因此要把 64bit 那一側切到別的行程。實作方式有兩種:(a) 建立 64bit 的輔助行程並用具名管道通訊,(b) 做成 64bit 的跨行程 COM 伺服器。如果呼叫端是 VBA 或舊語言,(b) 比較自然,因為 COM 基礎架構會代勞封送處理,呼叫端幾乎不用改程式碼。如果雙方都是 C#,(a) 在部署與除錯上都更輕鬆。採用(a)時,要用 Job Object 設計輔助行程的親子連動結束。
共享記憶體適合在什麼場合使用?
只限定在影像畫面或波形資料這類大量・高頻率的資料上使用。它是同一台電腦內最快的 IPC,不涉及複製或序列化,但完全不提供任何同步機制,因此「不讀取寫入中的資料」的機制、偵測對方存活狀態、異常結束後的復原,全部都要自己設計。實務上常見的做法是只把資料面(data plane)放在共享記憶體上,開始・停止・變更設定等控制面(control plane)則交給具名管道之類的另一個通道,形成雙通道架構。一旦連控制訊息都想硬塞進共享記憶體,就是該重新檢視設計的訊號。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽