Как выбрать межпроцессное взаимодействие в Windows — таблица: именованные каналы, TCP, gRPC, разделяемая память, COM
· Обновлено: · Го Комура · Межпроцессное взаимодействие, Именованные каналы, Windows, .NET, C#, gRPC, Разделяемая память, COM, Проектирование, Таблица решений, Техническая консультация
История изменений (1 обновлений, последнее 30 Aug 2026)
Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.
- Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
- Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.21619937)
Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.
Го Комура (2026). Как выбрать межпроцессное взаимодействие в Windows — таблица: именованные каналы, TCP, gRPC, разделяемая память, COM. KomuraSoft LLC. https://comcomponent.com/ru/blog/windows-ipc-decision-table/
- DOI (зарегистрированный архив)
- 10.5281/zenodo.21619937
- DOI (последняя зарегистрированная версия)
- 10.5281/zenodo.21619938
«Разнесли UI и службу — как им общаться?» «Из 32-битного приложения нужны функции 64-битной DLL». «Хотим гнать данные с измерительного движка в отдельном процессе на экран». Разнести приложение по нескольким процессам — частый практический ответ на надёжность, разделение прав и разрядность. Но в тот же момент встаёт выбор: как процессы будут общаться.
На этом блоге уже были разборы отдельных механизмов: «Подводные камни разделяемой памяти», «Фрейминг TCP», «Безопасная работа с дочерними процессами», «Интеграция через файлы и блокировки». Не хватало общей картины: «а что выбирать в принципе». Здесь разберём основные варианты межпроцессного взаимодействия (IPC) в Windows — интеграцию через файлы, именованные каналы, локальный TCP, gRPC, разделяемую память и COM — по сильным сторонам и типичным ошибкам, в привычном для блога виде таблицы решений.
Термины, которые встречаются дальше
Ниже собрано то, что в тексте появляется аббревиатурой.
| Термин | Смысл |
|---|---|
| IPC (Inter-Process Communication) | Межпроцессное взаимодействие. Общее название механизмов, которыми процессы обмениваются данными и сигналами |
| ACL (Access Control List) | Список управления доступом. Перечень «кому какую операцию разрешить»; в Windows его вешают на каналы, файлы, разделяемую память и т. д. В .NET его собирают классами вроде PipeSecurity |
| UAC (User Account Control) | Контроль учётных записей. Даже учётная запись администратора по умолчанию работает со стандартными правами и повышает их только когда нужно. Повышенная и не повышенная стороны работают с разными правами, поэтому связь между ними — тема раздела 2.2 |
| RPC (Remote Procedure Call) | Механизм, которым процедуры другого процесса или другой машины записывают как обычный локальный вызов. Внепроцессные вызовы COM стоят поверх этого |
| data plane / control plane | Слой данных и слой управления. Так говорят, когда поток полезных данных отделяют от потока команд вроде старта, останова и смены настроек (раздел 2.5 и глава 4) |
| Фрейминг | Механизм, который задаёт, где в потоке байтов начинается и кончается одно сообщение. Для TCP его нужно проектировать самим (раздел 2.3 и глава 6) |
1. Сначала вывод
- Для запроса-ответа на одной машине (отправить команду и получить результат) первый кандидат — именованный канал. Штатный механизм ОС, порт не нужен, стыкуется с контролем доступа Windows, из .NET пишется прямо через
System.IO.Pipes.1 - Если связь может выйти в сеть — сразу локальный TCP или gRPC. Позже пересадить канал на сокет обычно оказывается не «чуть поправить», а полноценной переделкой. У чистого TCP поток байтов, поэтому фрейминг обязателен.
- Когда растёт число служб и видов вызовов, и свой протокол уже тяжело сопровождать — gRPC. Получаете схему и генерацию кода из proto, двунаправленный стриминг, а начиная с .NET 8 ASP.NET Core (Kestrel) умеет брать именованный канал как транспорт.2
- Разделяемую память — только для больших объёмов с высокой частотой (кадры, сигналы). Это самый быстрый вариант, но всю синхронизацию пишете сами, поэтому железное правило: управляющие сообщения туда не класть.3
- Если нужна слабая связность, асинхронность и след для аудита, интеграция через файлы и сегодня сильный вариант. Успех целиком зависит от взаимного исключения.
- COM (внепроцессный) не делаем первым кандидатом для новой разработки, но для вызова из VBA и других языков и как мост 32/64 бит это по-прежнему рабочий инструмент.4
- Какой механизм ни выберите, от общих задач — границы сообщений, номер версии, тайм-ауты, повторное подключение — никуда не деться (глава 6). Уделите этому не меньше времени, чем выбору самого механизма.
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 21, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
2. Что из себя представляет каждый вариант
2.1 Интеграция через файлы — слабая связность, асинхронность, аудит
Классическая схема: «A кладёт файл в выходную папку, B забирает и обрабатывает». Обеим сторонам не обязательно работать одновременно, след обработки остаётся файлами, а при сбое человек может открыть файл, поправить и снова отдать в обработку. Для пакетной интеграции без требования реального времени способ и сегодня сильный.
Ловушка почти одна — взаимное исключение. «Прочитали файл, который ещё пишут» и «два процесса дерутся за один файл» — типичные аварии; нужна проверенная практика вроде записи под временным именем и последующего переименования. Подробности собраны в «Интеграция через файлы и блокировки: практические рекомендации» — если выбираете этот способ, туда обязательно загляните. Для интерактивной связи, где нужен ответ, и для обмена десятки раз в секунду он не подходит.
2.2 Именованные каналы — основной выбор для IPC на одной машине
Именованный канал — двунаправленный канал связи, который даёт ядро Windows, и основной выбор для клиент-серверного IPC на одной машине. В .NET с ним работают через NamedPipeServerStream / NamedPipeClientStream; к одному имени могут подключиться несколько клиентов.5 В отличие от TCP, номер порта не нужен, и брандмауэр канал не режет.
Что важно на практике.
- Доступен режим сообщений. Если указать
PipeTransmissionMode.Message, одна запись доходит как одно чётко ограниченное сообщение. Существенный плюс: ОС берёт на себя фрейминг вроде префикса длины, без которого TCP не обойтись (принимающая сторона всё равно должна дочитать одно сообщение до конца черезIsMessageComplete; см. главу 5). - Права по умолчанию неожиданно широкие. Дескриптор безопасности канала по умолчанию даёт полный контроль LocalSystem, администраторам и создателю, но при этом разрешает чтение и Everyone, и анонимной учётной записи.6 Между процессами одного пользователя достаточно
PipeOptions.CurrentUserOnly: подключение только к собеседнику, созданному тем же пользователем. Имеет смысл сделать это обычной практикой.7 Между разными учётными записями (связь со службой и т. п.) ACL задают явно черезPipeSecurity. - Имена каналов лежат в пространстве имён, которое видно всем. Имена живут в одном пространстве под
\\.\pipe\и видны процессам других пользователей на той же машине. Здесь опасен захват имени (squatting): вредоносный процесс раньше поднимает сервер с тем же именем и ждёт подключений — клиент придёт именно к нему. На машинах, где рядом работают пользователи с разными правами, стоит заложить защиту: на сервере —PipeOptions.FirstPipeInstance, чтобы завершиться ошибкой, если канал с таким именем уже есть; на клиенте — после подключения проверить учётную запись владельца канала.8 - Можно связать стороны по разные стороны границы прав. Это обычный канал между «UI со стандартными правами + брокер с правами администратора», которых развёл UAC. В такой схеме
CurrentUserOnlyне подходит (он требует совпадения вплоть до уровня повышения прав, даже у одного пользователя7). ACL нужно проектировать явно; конкретная реализация разобрана в «Как вынести только операции с правами администратора (брокер)».
Слабые стороны: для связи между машинами он по сути не годится (по спецификации возможно, но эксплуатационных ограничений много) и с системами не на Windows стыкуется неудобно. Если такие требования уже видны, берите TCP / gRPC.
2.3 Локальный TCP — универсальность независимо от языка и ОС
TCP-соединение на localhost — самый универсальный IPC: на нём могут говорить почти любой язык, среда выполнения и ОС. В смешанных схемах вроде «движок анализа на Linux и UI на Windows» первым кандидатом становится TCP (или HTTP/gRPC поверх него).
Три момента, на которые смотрят.
- Это поток байтов. У TCP нет свойства «дошло теми же порциями, что отправили». Три сообщения из
Send, склеившиеся в одномReceive, — норма; одно сообщение, пришедшее кусками, — тоже норма. Фрейминг (например, префикс длины) проектируют на уровне приложения, а код без него просто «случайно работает». Подробности — в «Заблуждение, будто TCP отдаёт Receive теми же порциями, что Send». - Сузьте, кто может подключиться к прослушиванию. Даже если имелась в виду только одна машина, прослушивание на
0.0.0.0открывает вход с других машин сети. Для локального IPC правило — привязываться к127.0.0.1(loopback). Даже тогда подключится другой пользователь на той же машине, поэтому если собеседника нужно опознать, аутентификацию добавляют на уровне приложения. Явное отличие от каналов: штатного контроля доступа ОС нет. - Номер порта придётся сопровождать. Фиксированный порт может столкнуться с другим ПО, а продукты брандмауэра иногда видят в этом «подозрительное прослушивание». Способ сменить номер порта закладывайте в проект сразу.
2.4 gRPC — покупаем схему и генерацию кода
Рядом с чистым сокетом и своим протоколом gRPC даёт скорее не саму связь, а рамку разработки. Описали службы и сообщения в proto-файле — сериализация, фрейминг, код клиента и сервера генерируются, а двунаправленный стриминг (push с сервера) пишется как обычная возможность языка. Как только вы входите в фазу «каждое новое сообщение — правка switch и документации своего протокола», эта рамка начинает окупаться.
Начиная с .NET 8 ASP.NET Core (Kestrel) напрямую поддерживает как транспорт не только TCP, но и Unix domain sockets и именованные каналы.8 На сервере достаточно ListenNamedPipe; контроль доступа тоже настраивается через PipeSecurity.2 Связка «канал связи остаётся именованным каналом без порта и с ACL, а слой протокола — gRPC» сильна для разделения UI и службы (глава 4).
С другой стороны, для связи между небольшими утилитами это часто избыточно. Поднять сервер gRPC значит взять на себя хостинг ASP.NET Core: растут поставка, зависимости и стоимость запуска. Держите границу: для пары утилит с двумя-тремя командами именованный канал + JSON (глава 5) в сумме дешевле. На gRPC не поздно переходить, когда сработает хотя бы одно: «сообщений, которые хочется описать в proto, больше десяти», «стриминговые уведомления нужны по сути», «собеседник — не .NET».
2.5 Разделяемая память (файл, отображённый в память) — максимальная скорость, но всю синхронизацию пишете сами
Самый быстрый IPC на одной машине — разделяемая память. В .NET именованную область создают через MemoryMappedFile.CreateNew, и несколько процессов читают и пишут одну и ту же последовательность байтов напрямую.3 Копирования и сериализации нет, поэтому для «больших и быстрых» данных вроде кадров и сигналов по производительности это почти безальтернативно.
Но разделяемая память — не «быстрый канал». Стороны видят одну последовательность байтов, а синхронизации нет ни на байт. Механизм, который не читает данные посередине записи, обнаружение, жив ли собеседник, восстановление после аварийного завершения одной стороны — всё это проектируете сами. Устройство кольцевого буфера и версионирование раскладки собраны в «Подводные камни разделяемой памяти и практические рекомендации».
На практике место ясное: только слой данных (data plane) кладут в разделяемую память, слой управления (control plane) выносят в отдельный канал, например именованный (схема 3 в главе 4). Если через разделяемую память начинают тащить и управляющие сообщения вроде старта, останова и смены настроек, это знак пересмотреть схему.
2.6 COM (внепроцессный) — не первый кандидат для новой разработки, но живые сценарии остаются
Внепроцессный сервер COM (EXE-сервер) — давний механизм Windows: «вызвать объект в другом процессе как локальную функцию».4 Для связи между новыми приложениями первым кандидатом его уже не ставят, но в следующих контекстах он по-прежнему практичен.
- Мост 32/64 бит: 64-битную DLL нельзя загрузить в процесс 32-битного приложения (и наоборот), а внепроцессный COM эту границу пересекает. Инфраструктура COM берёт маршалинг на себя, поэтому код вызывающей стороны почти не меняется. Живой пример — «Пример COM-моста: вызов 64-битной DLL из 32-битного приложения».
- Вызов из VBA и других языков: если из старой среды вроде Excel VBA нужно вызвать функциональность .NET, публикация как COM и сегодня самый прямой путь.
- Стыковка с уже существующим COM: если собеседник говорит только на COM, кратчайший путь — говорить на COM и со своей стороны (сам COM — в «Что такое COM, ActiveX и OCX»).
Почему для нового проекта COM обычно не берут: поставка с регистрацией в реестре хлопотна, проектирование интерфейсов и подсчёт ссылок дорого учить, разбирать сбои сложно. Прежде чем решать, сравните с альтернативой «если цель — мост 32/64 бит, 64-битную сторону сделать обычным вспомогательным процессом и говорить по именованному каналу» (схема 2 в главе 4).
2.7 Классические механизмы — в новой разработке не выбираем
В Windows есть и другие IPC: WM_COPYDATA (передача данных оконным сообщением), буфер обмена, DDE, почтовые слоты. Они по-прежнему перечислены на официальной странице обзора IPC.4 Но они либо предполагают окно, либо постепенно выводятся из употребления (удалённые почтовые слоты — в процессе устаревания), поэтому причин брать их в новый проект почти нет. Достаточно узнавать их, когда встречаете в сопровождении существующего приложения.
3. Таблица решений
Сначала зафиксируем смысл обозначений. Четыре уровня показывают, сколько дополнительного проектирования и кода понадобится, чтобы выбранный механизм закрыл этот аспект. Это не абсолютная оценка скорости или возможностей.
| Обозначение | Критерий |
|---|---|
| ◎ | Для этой задачи механизм и сделан. Хватает штатных возможностей, почти без дополнительного проектирования |
| ○ | Работает нормально. Но нужна реализация и настройка по проверенным практикам (описание ACL, фрейминг и т. п.) |
| △ | Возможно, но потребуется заметный объём своего проектирования либо есть ограничения и побочные эффекты, которые нельзя игнорировать |
| ✕ | По сути нельзя. Нужно сочетать с другим механизмом |
Строка «стоимость реализации» записана не символами, а как низкая / средняя / высокая, и лучше меньшее значение. Строка «пропускная способность / задержка» наоборот: выше лучше (◎ — самый быстрый вариант).
| Аспект | Файл | Именованный канал | Локальный TCP | gRPC | Разделяемая память | COM |
|---|---|---|---|---|---|---|
| Область связи | Может пересекать машины через общую папку | На практике одна машина | Может пересекать машины | Может пересекать машины | Только одна машина | На практике одна машина |
| Модель связи | Передача файла (асинхронно) | Поток + режим сообщений | Поток байтов | RPC + стриминг | Разделяемое состояние | Вызов методов |
| Уведомление сервер → клиент | ✕ (поллинг) | ○ | ○ | ◎ (двунаправленный стриминг) | △ (нужен отдельный механизм событий) | △ (можно, но сложно) |
| Пересечение границы прав (UAC, службы) | ○ (ACL папки) | ◎ (PipeSecurity) | △ (аутентификация своя) | △–○ (◎ при транспорте на канале) | ○ (ACL возможен, проектировать сложно) | ○ |
| 32/64 бит и смесь языков | ◎ | ○ | ◎ | ◎ (генерация кода из proto на каждый язык) | △ (обязательно проектировать ABI) | ○ (мосты — сильная сторона) |
| Стоимость реализации | Низкая | Низкая–средняя | Средняя (фрейминг свой) | Средняя (внедрение платформы) | Высокая | Высокая (для нового проекта) |
| Удобство отладки | ◎ (содержимое остаётся в файле) | ○ | ○ (можно снять трафик) | △ (HTTP/2 + двоичный формат) | △ (симптомы бросаются в глаза) | △ |
| Пропускная способность / задержка | Низкая | Средняя–высокая | Средняя | Средняя | ◎ | Средняя |
Одно замечание. Правильный способ — не выбирать «механизм с наибольшим числом ◎», а смотреть только на строки, которые закрывают требования, и сужать методом исключения. Например, как только зафиксировано «30 кадров в секунду», для слоя данных остаётся один вариант — разделяемая память.
И то, к чему приходите, почти всегда совпадает с одной из трёх типовых схем следующей главы. Когда механизм в таблице выбран, по этой перекладине возвращайтесь к главе 4.
| Что осталось в таблице и какие требования | Куда смотреть |
|---|---|
| Именованный канал + пересечение границы прав (UAC, служба) | Схема 1: разделение UI-приложения и службы Windows |
| Именованный канал или COM + смесь 32/64 бит | Схема 2: мост от 32-битного приложения к 64-битной функциональности |
| Разделяемая память + ещё и управление (старт, стоп и т. п.) | Схема 3: два канала — управление и данные |
| Остался gRPC | Схема 1, но слой связи заменён на транспорт gRPC поверх именованного канала2 |
| Осталась интеграция через файлы | Не столько типовая схема, сколько слабо связанный пакет. Ядро проектирования — взаимное исключение (раздел 2.1) |
4. Примеры типовых схем
Если наложить таблицу на конкретные требования, на практике чаще всего получаются следующие три схемы. На рисунке это выглядит так.
flowchart TB
accTitle: Три типовые схемы IPC в Windows
accDescr: Разделение UI и службы через именованный канал, мост 32/64 бит через канал или внепроцессный COM, и двухканальная схема измерительного движка с управлением по каналу и данными через разделяемую память
subgraph K1["Схема 1: разделение UI-приложения и службы Windows"]
U1["UI-приложение<br/>права пользователя, вошедшего в систему"]
P1["Именованный канал<br/>запрос-ответ в JSON<br/>разные учётные записи: ACL через PipeSecurity"]
S1["Служба Windows<br/>другая учётная запись, например LocalSystem<br/>резидентная и привилегированная обработка"]
U1 <--> P1
P1 <--> S1
end
subgraph K2["Схема 2: мост от 32-битного приложения к 64-битной функциональности"]
A2["32-битное приложение (существующий актив)"]
P2["Именованный канал<br/>или внепроцессный COM"]
H2["64-битный вспомогательный процесс<br/>согласованное завершение родителя и потомка через Job Object"]
D2["DLL или SDK только для 64 бит"]
A2 <--> P2
P2 <--> H2
H2 --> D2
end
subgraph K3["Схема 3: высокочастотные данные от измерительного движка к отображению"]
E3["Движок измерений и обработки изображений"]
C3["Канал управления: именованный канал<br/>старт, стоп, смена настроек. Несколько раз в секунду"]
D3["Канал данных: разделяемая память + именованное событие<br/>кадры, сигналы. Десятки раз в секунду и чаще"]
V3["UI-приложение (отображение)"]
V3 <--> C3
C3 <--> E3
E3 --> D3
D3 --> V3
end
Схема 1: разделение UI-приложения и службы Windows. Резидентную обработку и работу, которой нужны привилегии, кладут в службу, UI оставляют обычным пользовательским процессом (как строить сторону службы — в опубликованной в тот же день «статье про службы Windows»). Связь строят на именованном канале; надёжный старт — протокол JSON-сообщений плюс режим сообщений (или байтовый режим плюс префикс длины). Когда видов вызовов становится больше, есть путь перейти на транспорт gRPC поверх именованного канала.2 Служба работает под другой учётной записью (например, LocalService), поэтому CurrentUserOnly не подходит — ключевой момент: явно задать ACL через PipeSecurity, например «Users — чтение и запись, удалённый доступ запретить».
Схема 2: мост от 32-битного приложения к 64-битной функциональности. Когда из существующего 32-битного приложения нужно использовать DLL или SDK драйвера, которые работают только в 64 битах, 64-битную сторону выносят в отдельный процесс. Два пути: (а) поднять 64-битный вспомогательный процесс и говорить по именованному каналу; (б) сделать его 64-битным внепроцессным COM-сервером. Если вызывающая сторона — VBA или другой старый язык, естественнее (б); если обе стороны на C#, (а) проще и в поставке, и в разборе сбоев. Для (а) запуск, наблюдение и согласованное завершение родителя и потомка берите прямо из схемы с Job Object в «Безопасная работа с дочерними процессами».
Схема 3: высокочастотные данные от измерительного движка к отображению. Когда движок измерений или обработки изображений вынесен в отдельный процесс и гонит данные в UI, стандартна схема из двух каналов: управление (старт, стоп, настройки) — именованный канал, данные (кадры, сигналы) — разделяемая память плюс именованное событие. Управление случается несколько раз в секунду, канала хватает; данные через разделяемую память приближаются к нулевому копированию. Разделение каналов позволяет оптимизировать сторону данных (кольцевой буфер и т. п.) отдельно от нужд управления. Про отображение состояния движка и внешнего оборудования см. также «Отображение состояния внешнего оборудования».
5. Пример реализации — асинхронные сервер и клиент на именованных каналах
Покажем минимальную схему на .NET 8, на которой стоит реализация именованного канала. В неё входят режим сообщений, несколько клиентов и обработка отключений. Протокол — «один UTF-8 JSON на одно сообщение», и поле version обязательно. В примере ниже CurrentUserOnly рассчитан на процессы одного пользователя. Если связь идёт со службой (другая учётная запись), как в схеме 1, уберите CurrentUserOnly и явно задайте ACL через PipeSecurity, как описано дальше.
Сначала сервер. На каждое входящее подключение создаётся новый серверный поток, обработка каждого клиента уходит в отдельную задачу.
using System.IO.Pipes;
using System.Text.Json;
public sealed class PipeServer(string pipeName)
{
// Веб-настройки по умолчанию: camelCase и без учёта регистра. Собственные
// настройки 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);
}
}
}
}
Три проектных решения, заложенных в этом коде.
- Кладите
versionуже в первый выпуск. Обязательно наступит момент, когда UI и службу обновляют раздельно (обновляется только одна сторона). Достаточно, чтобы принимающая сторона «явно отклоняла неизвестную версию»: вместо тихого некорректного поведения получите «понятную ошибку». - Решите, будет ли соединение одноразовым или постоянным. В примере выше одноразовый тип — подключение на каждый запрос: не нужно вести состояние отключения и повторного подключения, но для частых вызовов это не подходит. Как только понадобится постоянное соединение плюс push с сервера, эта сложность станет поводом перейти на двунаправленный стриминг gRPC.
- При связи со службой уберите
CurrentUserOnlyи спроектируйтеPipeSecurity. Пример выше рассчитан на процессы одного пользователя. Если собеседник — служба под другой учётной записью, создавайте серверный поток с ACL черезNamedPipeServerStreamAcl.Createи явно укажите группу, которой разрешено подключение.6
5.1 Проверка работы — порядок запуска и критерии «прошло»
Код выше заработает, если вставить его в два консольных приложения. После переноса проверьте в таком порядке.
Сначала сервер. В Program.cs проекта с PipeServer оставьте только запуск и остановку.
// Сервер, Program.cs (.NET 8 / top-level statements)
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
Console.WriteLine(@"Прослушивание: \\.\pipe\demo-ipc (остановка по Ctrl+C)");
try
{
await new PipeServer("demo-ipc").RunAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Остановлено.");
}
Дальше клиент. Один запрос и вывод ответа.
// Клиент, Program.cs (.NET 8 / top-level statements)
using System.Text.Json;
var client = new PipeClient("demo-ipc");
var res = await client.RequestAsync<JsonElement>(
new { version = 1, type = "getStatus" }, CancellationToken.None);
Console.WriteLine(res); // {"version":1,"running":true}
Порядок проверки такой.
- Сначала запустите сервер. Строка «Прослушивание» — готовность.
- Запустите клиент. На экране окажется JSON, который вернул
Dispatchна сервере. Ответ{"version":1,"running":true}получается потому, чтоJsonSerializerDefaults.Webпечатает camelCase. - Остановите сервер и запустите только клиент. Сработает
ConnectAsync(timeout: 3000, ...), и через 3 секунды вернётся ошибка. Если здесь процесс зависает, позже это всплывёт как «упала служба — завис и UI». - Прогоните три аварийных случая. Только после них можно сказать, что «протокол работает».
| Что пробуем | Что шлёт клиент | Ожидаемый результат |
|---|---|---|
| Неизвестная версия | new { version = 2, type = "getStatus" } |
{"version":1,"error":"version_unsupported"} |
| Неизвестная команда | new { version = 1, type = "reboot" } |
{"version":1,"error":"unknown_type"} |
| Сообщение сверх лимита | Тело больше 1 МБ (MaxMessageBytes) |
Сервер молча разрывает соединение, на клиенте — IOException |
Если нужно увидеть с стороны ОС, что канал действительно поднят, pipelist из Sysinternals покажет имена под \\.\pipe\. За минуту отделяется опечатка в имени канала от упавшего сервера — имеет смысл внести это в регламент диагностики.
6. Общие задачи проектирования протокола — работа после выбора механизма
Какой IPC ни выберите, у содержимого связи одни и те же задачи. На деле именно они, а не выбор механизма, чаще становятся источником аварий.
- Границы сообщений (фрейминг): кроме случаев, когда ОС сама режет границы, как в режиме сообщений канала, «где начинается и кончается одно сообщение» — ответственность приложения. Та же задача — границы записей в разделяемой памяти. За основу берите схему с префиксом длины.
- Версионирование: кладите версию формата в сообщение; принимающая сторона должна «явно отклонять неизвестные версии». Гарантии, что оба процесса всегда обновляются одновременно, нет даже если их кладут одним установщиком.
- Тайм-ауты: исходите из того, что «собеседник не ответит» обязательно случится. Задайте верхнюю границу отдельно для подключения, запроса и ответа и не оставляйте бесконечного ожидания. Синхронно ждать в UI-потоке нельзя вообще.
- Повторное подключение и идемпотентность: повторная попытка значит, что один и тот же запрос может прийти дважды. Каждый вид запроса классифицируйте: «безопасно ли выполнить его дважды»; для небезопасных (постановка задания и т. п.) добавляйте ID запроса и отклоняйте дубликаты.
- Собеседника проверяйте как внешний ввод. На одной машине легко пропустить проверку, решив, что «собеседник — наше приложение», но при границе прав собеседник по IPC — такой же «недоверенный ввод», как данные из сети. Брокер или служба с правами администратора не должны напрямую выполнять путь или команду, пришедшую от клиента со стандартными правами. Включая захват имени канала (раздел 2.2), правило IPC через границу прав — подозревать и «кто», и «что».
- Оставляйте средства наблюдения: возможность вести журнал связи (хотя бы тип сообщения, собеседник и результат) на порядок сокращает время разбора сбоев вида «не подключается» или «нет ответа».
7. Итог
Если выбирать межпроцессное взаимодействие в таком порядке, сомнений почти не остаётся. Сначала — достаточно ли ограничиться одной машиной. Если да: для запроса-ответа — именованный канал, только для больших объёмов с высокой частотой — разделяемая память, для слабо связанного пакета — интеграция через файлы. Если уже видно выход за пределы машины или смесь языков — локальный TCP или gRPC, а gRPC — когда сопровождение своего протокола на таком масштабе начинает давить. COM не делаем первым кандидатом для нового проекта, а держим как инструмент для моста 32/64 бит и стыковки с VBA — таблица решений этой статьи как раз раскладывает эту логику по строкам.
Как только механизм выбран, общее проектирование из главы 6 (фрейминг, версии, тайм-ауты, повторное подключение, проверка собеседника) кладите уже в первый выпуск. По опыту практики, сбои IPC в подавляющем большинстве случаев вызваны не «не тем механизмом», а «пропущенным проектированием протокола». Пересмотр разнесения процессов или схемы связи лучше начинать с инвентаризации: какой процесс, под чьими правами, что и с какой частотой передаёт.
Похожие статьи
- Разделяемая память: подводные камни и практические рекомендации
- TCP не отдаёт Receive теми же порциями, что Send — приём как байтовый поток
- Как в Windows-приложении вынести только операции, которым нужны права администратора
- Как создать и эксплуатировать службу Windows — от выбора между Планировщиком заданий и службой до превращения BackgroundService в службу
Смежные области консультаций
Komura Software Co., Ltd. делает ревью проектирования разнесения процессов и схем связи, проектирует стыковку с уже существующими активами, включая мост 32/64 бит, и разбирает причины сбоев связи вида «не подключается / нет ответа».
- Техническая консультация / ревью архитектуры
- Разработка Windows-приложений
- Использование и миграция существующих активов
- Контакты
Источники
-
Microsoft Learn, How to: Use Named Pipes for Network Interprocess Communication. Пример сервера и клиента с несколькими клиентами на NamedPipeServerStream / NamedPipeClientStream. ↩
-
Microsoft Learn, Inter-process communication with gRPC and Named pipes. Схема запуска gRPC поверх именованных каналов через ListenNamedPipe Kestrel начиная с .NET 8 и контроль доступа через PipeSecurity. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, MemoryMappedFile Class. API .NET для файлов, отображённых в память, и то, что разделяемая память, создаваемая через CreateNew без привязки к файлу, хорошо подходит для IPC. ↩ ↩2
-
Microsoft Learn, Interprocess communications. Перечень механизмов IPC, которые даёт Windows (буфер обмена, COM, WM_COPYDATA, DDE, проекция файлов, почтовые слоты, каналы, RPC, Windows Sockets), и ориентиры, как их выбирать. ↩ ↩2 ↩3
-
Microsoft Learn, NamedPipeServerStream Class. Серверный поток именованного канала и конструкторы, в которых задают PipeTransmissionMode и PipeSecurity. ↩
-
Microsoft Learn, Named Pipe Security and Access Rights. Дескриптор безопасности именованного канала по умолчанию разрешает чтение Everyone и анонимной учётной записи; устройство прав доступа. ↩ ↩2
-
Microsoft Learn, PipeOptions Enum. CurrentUserOnly разрешает подключение только к собеседнику, созданному тем же пользователем; в Windows дополнительно проверяется уровень повышения прав, не только учётная запись. ↩ ↩2
-
Microsoft Learn, Inter-process communication with gRPC. Транспорты gRPC как IPC (Unix domain sockets, именованные каналы) и соображения безопасности: проверка владельца сервера и защита от подмены (impersonation). ↩ ↩2
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Где хранить данные Windows-приложения: таблица решений SQLite / JSON / реестр / Access
Куда и в каком виде хранить данные настольного Windows-приложения. Разбираем выбор между AppData и ProgramData, сильные стороны и ловушки...
Защита Windows-приложения от повторного запуска — именованный Mutex и активация окна при втором старте
Разбираем, как в бизнес-приложении Windows запретить повторный запуск через именованный Mutex. Разберём ловушку RDP из-за разницы Global\...
Как создать и эксплуатировать службу Windows — от выбора между Планировщиком заданий и службой до превращения BackgroundService в службу
Стоит ли держать постоянно работающую обработку как службу Windows или хватит Планировщика заданий. Практический разбор: таблица выбора, ...
Практические рекомендации по многопоточности: .NET — что решить до добавления потоков
Проверенные приёмы проектирования на .NET/C#, чтобы код не «иногда падал или зависал»: не создавать потоки вручную и опираться на Task, с...
Обратная совместимость интерфейсов DLL и COM — таблица: какие изменения ломают вызывающий код
Какие изменения DLL или COM-компонента ломают вызывающий код. Разбираем три слоя совместимости — бинарную, исходного кода и поведенческую...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Миграция ActiveX
Решения о сохранении, обёртке или замене компонентов COM / ActiveX / OCX.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Бизнес-приложения, интеграция оборудования и средства связи — от требований до разработки.
Использование и перенос существующих активов
Помогаем использовать и переносить активы COM / ActiveX / OCX и зависимости 32/64 бит.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Что брать первым кандидатом для межпроцессного взаимодействия в Windows?
- Для запроса-ответа на одной машине (отправить команду и получить результат) первый кандидат — именованный канал. Это штатный механизм ОС: номер порта не нужен, брандмауэр его не режет, он стыкуется с контролем доступа Windows (PipeSecurity), а из .NET пишется прямо через System.IO.Pipes. Дальше сужают методом исключения: если связь может выйти в сеть — сразу локальный TCP или gRPC; только для больших объёмов с высокой частотой (кадры, сигналы) — разделяемая память; если нужна слабая связность, асинхронность и след для аудита — интеграция через файлы.
- Когда для межпроцессного взаимодействия стоит брать gRPC?
- Когда растёт число служб и видов вызовов, и сопровождение switch и документации своего протокола уже давит. gRPC даёт схему и генерацию кода из proto-файлов и двунаправленный стриминг, а начиная с .NET 8 ASP.NET Core (Kestrel) умеет брать именованный канал как транспорт напрямую. Для пары утилит с двумя-тремя командами тащить хостинг ASP.NET Core обычно избыточно: именованный канал плюс JSON выходит дешевле в сумме. На gRPC не поздно переходить, когда сработает хотя бы одно: сообщений, которые хочется описать в proto, больше десяти; стриминговые уведомления нужны по сути, а не «на всякий случай»; собеседник — не .NET.
- Как вызвать 64-битную DLL из 32-битного приложения?
- 64-битную DLL нельзя загрузить в тот же процесс, что и 32-битное приложение, поэтому 64-битную сторону выносят в отдельный процесс. Два пути: (а) поднять 64-битный вспомогательный процесс и говорить с ним по именованному каналу; (б) сделать его 64-битным внепроцессным COM-сервером. Если вызывающая сторона — VBA или другой старый язык, естественнее (б): инфраструктура COM берёт маршалинг на себя, и код вызывающей стороны почти не меняется. Если обе стороны на C#, (а) проще и в поставке, и в разборе сбоев. Для (а) согласованное завершение родителя и потомка проектируют через Job Object.
- В каких сценариях нужна разделяемая память?
- Только для больших объёмов с высокой частотой — кадров изображения, сигналов и т. п. Это самый быстрый IPC на одной машине: без копирования и сериализации. Синхронизации нет ни на байт, поэтому механизм, который не читает данные посередине записи, обнаружение, жив ли собеседник, и восстановление после аварийного завершения придётся проектировать самим. На практике стандартна схема из двух каналов: слой данных (data plane) — в разделяемой памяти, слой управления (control plane) — старт, стоп, смена настроек — в отдельном канале, например именованном. Если через разделяемую память начинают тащить и управляющие сообщения, это знак пересмотреть схему.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.