{"id":17335139,"url":"https://github.com/nice3point/interprocesscommunication","last_synced_at":"2025-08-01T11:31:47.686Z","repository":{"id":200788994,"uuid":"704623361","full_name":"Nice3point/InterprocessCommunication","owner":"Nice3point","description":null,"archived":false,"fork":false,"pushed_at":"2023-10-17T18:48:14.000Z","size":141,"stargazers_count":4,"open_issues_count":0,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-07T20:42:59.076Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Nice3point.png","metadata":{"files":{"readme":"Readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-10-13T17:02:42.000Z","updated_at":"2025-03-25T13:10:01.000Z","dependencies_parsed_at":null,"dependency_job_id":"5b0b5e18-e8fd-40e9-a6f7-8f93f73315c2","html_url":"https://github.com/Nice3point/InterprocessCommunication","commit_stats":null,"previous_names":["nice3point/interprocesscommunication"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Nice3point/InterprocessCommunication","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nice3point%2FInterprocessCommunication","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nice3point%2FInterprocessCommunication/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nice3point%2FInterprocessCommunication/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nice3point%2FInterprocessCommunication/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Nice3point","download_url":"https://codeload.github.com/Nice3point/InterprocessCommunication/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nice3point%2FInterprocessCommunication/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":268215576,"owners_count":24214363,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-01T02:00:08.611Z","response_time":67,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-15T15:08:21.048Z","updated_at":"2025-08-01T11:31:46.564Z","avatar_url":"https://github.com/Nice3point.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"﻿# Interprocess Communication: Strategies and Best Practices\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://github.com/Nice3point/InterprocessCommunication/assets/20504884/21d38cc0-9dfe-46af-959d-8deffaf91b3c\" /\u003e\n\u003c/p\u003e\n\nВсе мы знаем как сложно поддерживать крупные программы, и успевать за прогрессом. Разработчики плагинов для Revit это понимают как никто лучше.\nНам приходится писать свои программы на .NET Framework 4.8. Нам приходится отказываться от современных и быстрых библиотек.\nЭто в конечном сказывается и на пользователях, которые вынуждены пользоваться устаревшим программным обеспечением.\n\nВ таких сценариях разделение приложения на несколько процессов с использованием Named Pipes представляется превосходным решением благодаря своей производительности и надежности.\nВ этой статье мы рассмотрим, как создать и использовать Named Pipes для взаимодействия между приложением Revit, работающим на .NET 4.8 и его плагина, работающим на .NET 7.\n\n\n# Содержание\n\n* [Введение в использование Named Pipes для общения между приложениями на разных версиях .NET](#введение-в-использование-named-pipes-для-общения-между-приложениями-на-разных-версиях-net)\n* [Что такое Named Pipes?](#что-такое-named-pipes)\n* [Взаимодействие между приложениями на .NET 4.8 и .NET 7](#взаимодействие-между-приложениями-на-net-48-и-net-7)\n  * [Создание сервера](#создание-сервера)\n  * [Создание клиента](#создание-клиента)\n  * [Протокол передачи](#протокол-передачи)\n  * [Управление соединениями](#управление-соединениями)\n  * [Двусторонняя передача](#двусторонняя-передача)\n  * [Реализация плагина для Revit](#реализация-плагина-для-revit)\n* [Установка .NET Runtime во время установки плагина](#установка-net-runtime-во-время-установки-плагина)\n* [Заключение](#заключение)\n\n# Введение в использование Named Pipes для общения между приложениями на разных версиях .NET\n\nВ мире разработки приложений часто требуется обеспечить обмен данными между разными приложениями, особенно в случаях, когда они работают на разных версиях .NET или разных языках.\nРазделение одного приложения на несколько процессов должно быть обоснованным. Что проще, вызвать функцию напрямую, или обменяться сообщениями? Очевидно первое.\n\nТогда какие преимущества в том чтобы это делать?\n\n- Решение конфликта зависимостей\n\n  С каждым годом размер плагинов для Revit все больше и больше растет, а зависимости растут в геометрической прогрессии.\n  Плагины могут использовать несовместимые версии одной библиотеки, что вызовет краш программы. Изоляция процессов решает эту проблему.\n\n- Производительность\n\n    Ниже приведены замеры производительности сортировки и математических вычислений на разных версиях .NET\n\n    ```\n    BenchmarkDotNet v0.13.9, Windows 11 (10.0.22621.1702/22H2/2022Update/SunValley2)\n    AMD Ryzen 5 2600X, 1 CPU, 12 logical and 6 physical cores\n    .NET 7.0           : .NET 7.0.9 (7.0.923.32018), X64 RyuJIT AVX2\n    .NET Framework 4.8 : .NET Framework 4.8.1 (4.8.9139.0), X64 RyuJIT VectorSize=256\n    ```\n  | Method      | Runtime            | Mean           | Error        | StdDev       | Allocated |\n  |------------ |------------------- |---------------:|-------------:|-------------:|----------:|\n  | ListSort    | .NET 7.0           | 1,113,161.8 ns | 20,385.15 ns | 21,811.88 ns |  804753 B |\n  | ListOrderBy | .NET 7.0           | 1,064,851.1 ns | 12,401.25 ns | 11,600.13 ns |  807054 B |\n  | MinValue    | .NET 7.0           |       979.4 ns |      7.40 ns |      6.56 ns |         - |\n  | MaxValue    | .NET 7.0           |       970.6 ns |      4.32 ns |      3.60 ns |         - |\n  | ListSort    | .NET Framework 4.8 | 2,144,723.5 ns | 40,359.72 ns | 37,752.51 ns | 1101646 B |\n  | ListOrderBy | .NET Framework 4.8 | 2,192,414.7 ns | 25,938.78 ns | 24,263.15 ns | 1105311 B |\n  | MinValue    | .NET Framework 4.8 |    58,019.0 ns |    460.30 ns |    430.57 ns |      40 B |\n  | MaxValue    | .NET Framework 4.8 |    66,053.4 ns |    610.28 ns |    541.00 ns |      41 B |\n\n  Разница в 68 раз в скорости при нахождении минимального значение, и полное отсутствие выделения памяти, впечатляет.\n\nТогда как написать программу на последней версии .NET, которая будет взаимодействовать с несовместимым .NET framework?\nСоздать два приложения, Server и Client, не добавляя зависимостей между друг другом и настроить взаимодействие между ними по настроенному протоколу.\n\nНиже приведены некоторые из возможных вариантов взаимодействия двух приложений:\n\n1. Использование WCF (Windows Communication Foundation)\n2. Использование сокетов (TCP или UDP)\n3. Использование Named Pipes\n4. Использование сигналов операционной системы (например, сигналов Windows):\n\n   Пример кода компании Autodesk, взаимодействие плагина Project Browser с бекендом Revit посредством сообщений\n\n    ```c#\n    public class DataTransmitter : IEventObserver\n    {\n        private void PostMessageToMainWindow(int iCmd) =\u003e \n            this.HandleOnMainThread((Action) (() =\u003e \n                Win32Api.PostMessage(Application.UIApp.getUIApplication().MainWindowHandle, 273U, new IntPtr(iCmd), IntPtr.Zero)));\n    \n        public void HandleShortCut(string key, bool ctrlPressed)\n        {\n            string lower = key.ToLower();\n            switch (PrivateImplementationDetails.ComputeStringHash(lower))\n            {\n            case 388133425:\n              if (!(lower == \"f2\")) break;\n              this.PostMessageToMainWindow(DataTransmitter.ID_RENAME);\n              break;\n            case 1740784714:\n              if (!(lower == \"delete\")) break;\n              this.PostMessageToMainWindow(DataTransmitter.ID_DELETE);\n              break;\n            case 3447633555:\n              if (!(lower == \"contextmenu\")) break;\n              this.PostMessageToMainWindow(DataTransmitter.ID_PROJECTBROWSER_CONTEXT_MENU_POP);\n              break;\n            case 3859557458:\n              if (!(lower == \"c\") || !ctrlPressed) break;\n              this.PostMessageToMainWindow(DataTransmitter.ID_COPY);\n              break;\n            case 4077666505:\n              if (!(lower == \"v\") || !ctrlPressed) break;\n              this.PostMessageToMainWindow(DataTransmitter.ID_PASTE);\n              break;\n            case 4228665076:\n              if (!(lower == \"y\") || !ctrlPressed) break;\n              this.PostMessageToMainWindow(DataTransmitter.ID_REDO);\n              break;\n            case 4278997933:\n              if (!(lower == \"z\") || !ctrlPressed) break;\n              this.PostMessageToMainWindow(DataTransmitter.ID_UNDO);\n              break;\n            }\n        }\n    }\n    ```\n\nУ каждого варианта есть свои достоинства и недостатки, самым удобным на мой взгляд, для взаимодействия на одной локальной машине является Named Pipes. Его мы и рассмотрим.\n\n# Что такое Named Pipes?\n\nNamed Pipes представляют собой механизм межпроцессного взаимодействия (Inter-Process Communication, IPC), который позволяет процессам обмениваться данными через именованные каналы.\nОни обеспечивают однонаправленное или двунаправленное соединение между процессами.\nПомимо высокой производительности, Named Pipes также предлагают различные уровни безопасности, что делает их привлекательным решением для многих сценариев взаимодействия между процессами.\n\n# Взаимодействие между приложениями на .NET 4.8 и .NET 7\n\nРассмотрим два приложения, одно из которых содержит бизнес-логику (сервер), а другое - пользовательский интерфейс (клиент).\nДля обеспечения связи между этими двумя процессами используется NamedPipe.\n\nПринцип работы NamedPipe включает в себя следующие шаги:\n\n1. **Создание и конфигурирование NamedPipe**: Сервер создает и конфигурирует\n   NamedPipe с определенным именем, которое будет доступно клиенту. Клиенту необходимо\n   знать это имя, чтобы подключиться к трубе.\n2. **Ожидание подключения**: Сервер начинает ожидать подключения клиента к трубе.\n   Это блокирующая операция, и сервер остается в подвешенном состоянии до тех пор, пока клиент не подключится.\n3. **Подключение к NamedPipe**: Клиент инициирует подключение к NamedPipe, указывая имя трубы, к которой он хочет подключиться.\n4. **Обмен данными**: После успешного соединения клиент и сервер могут обмениваться\n   данными в виде байтовых потоков. Клиент отправляет запросы на выполнение бизнес-логики, а сервер обрабатывает эти запросы и отсылает результаты.\n5. **Завершение сеанса**: После завершения обмена данными клиент и сервер могут закрыть соединение с NamedPipe.\n\n## Создание сервера\n\nНа платформе .NET серверная часть представлена классом `NamedPipeServerStream`. Реализация класса предоставляет асинхронные и синхронные методы для работы с NamedPipe.\nВо избежание блокировки основного потока, мы будем использовать асинхронные методы.\n\nПример кода для создания NamedPipeServer:\n\n```C#\npublic static class NamedPipeUtil\n{\n    /// \u003csummary\u003e\n    /// Create a server for the current user only\n    /// \u003c/summary\u003e\n    public static NamedPipeServerStream CreateServer(PipeDirection? pipeDirection = null)\n    {\n        const PipeOptions pipeOptions = PipeOptions.Asynchronous | PipeOptions.WriteThrough;\n        return new NamedPipeServerStream(\n            GetPipeName(),\n            pipeDirection ?? PipeDirection.InOut,\n            NamedPipeServerStream.MaxAllowedServerInstances,\n            PipeTransmissionMode.Byte,\n            pipeOptions);\n    }\n    \n    private static string GetPipeName()\n    {\n        var serverDirectory = AppDomain.CurrentDomain.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar);\n        var pipeNameInput = $\"{Environment.UserName}.{serverDirectory}\";\n        var hash = new SHA256Managed().ComputeHash(Encoding.UTF8.GetBytes(pipeNameInput));\n    \n        return Convert.ToBase64String(hash)\n            .Replace(\"/\", \"_\")\n            .Replace(\"=\", string.Empty);\n    }\n}\n```\n\nИмя сервера не должно содержать специальные символы во избежание исключения.\nДля создания имени трубы мы будем использовать хеш, созданный из имени пользователя и текущей папки, достаточно уникально чтобы клиент при подключении использовал именно этот сервер.\nВы можете изменить это поведение или использовать любое имя в рамках своего проекта, особенно если клиент и сервер находятся в разных директориях.\n\nДанный подход используется в [Roslyn .NET compiler](https://github.com/dotnet/roslyn). Для тех кто сильнее хочет углубиться в эту тему, рекомендую изучить исходный код проекта\n\n`PipeDirection` указывает направления канала, `PipeDirection.In` говорит о том что сервер будет только принимать сообщения, а `PipeDirection.InOut` сможет как принимать, так и отправлять их.\n\n## Создание клиента\n\nДля создания клиента воспользуемся классом NamedPipeClientStream. Код практически аналогичен с сервером, и может немного отличаться в зависимости от версий .NET.\nНапример, в .NET framework 4.8 значения `PipeOptions.CurrentUserOnly` нет, но появилось в .NET 7.\n\n```C#\n/// \u003csummary\u003e\n/// Create a client for the current user only\n/// \u003c/summary\u003e\npublic static NamedPipeClientStream CreateClient(PipeDirection? pipeDirection = null)\n{\n    const PipeOptions pipeOptions = PipeOptions.Asynchronous | PipeOptions.WriteThrough | PipeOptions.CurrentUserOnly;\n    return new NamedPipeClientStream(\".\",\n        GetPipeName(),\n        pipeDirection ?? PipeDirection.Out,\n        pipeOptions);\n}\n\nprivate static string GetPipeName()\n{\n    var clientDirectory = AppDomain.CurrentDomain.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar);\n    var pipeNameInput = $\"{System.Environment.UserName}.{clientDirectory}\";\n    var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(pipeNameInput));\n\n    return Convert.ToBase64String(bytes)\n        .Replace(\"/\", \"_\")\n        .Replace(\"=\", string.Empty);\n}\n```\n\n## Протокол передачи\n\nNamedPipe представляет собой Stream, что позволяет нам записывать любую последовательность байтов в поток.\nОднако, работать с байтами напрямую может быть не очень удобно, особенно когда мы имеем дело со сложными данными или структурами.\nДля упрощения взаимодействия с потоками данных и структурирования информации в удобном формате используются протоколы передачи.\n\nПротоколы передачи определяют формат и порядок передачи данных между приложениями.\nОни обеспечивают структурирование информации, чтобы обеспечить понимание и правильную интерпретацию данных между отправителем и получателем.\n\nВ случая когда нам нужно отправить \"Запрос на выполнение определенной команды на сервере\" или \"Запрос на обновление настроек приложения\",\nсервер должен понимать как его обрабатывать от клиента.\nПоэтому для облегчения обработки запросов и управлением обмена данными, создадим Enum `RequestType`.\n\n```C#\npublic enum RequestType\n{\n    PrintMessage,\n    UpdateModel\n}\n```\n\nСам заброс будет представлять класс, который будет содержать всю информацию о передаваемых данных.\n\n```c#\npublic abstract class Request\n{\n    public abstract RequestType Type { get; }\n\n    protected abstract void AddRequestBody(BinaryWriter writer);\n\n    /// \u003csummary\u003e\n    ///     Write a Request to the given stream.\n    /// \u003c/summary\u003e\n    public async Task WriteAsync(Stream outStream)\n    {\n        using var memoryStream = new MemoryStream();\n        using var writer = new BinaryWriter(memoryStream, Encoding.Unicode);\n\n        writer.Write((int) Type);\n        AddRequestBody(writer);\n        writer.Flush();\n\n        // Write the length of the request\n        var length = checked((int) memoryStream.Length);\n        \n        // There is no way to know the number of bytes written to\n        // the pipe stream. We just have to assume all of them are written\n        await outStream.WriteAsync(BitConverter.GetBytes(length), 0, 4);\n        memoryStream.Position = 0;\n        await memoryStream.CopyToAsync(outStream, length);\n    }\n\n    /// \u003csummary\u003e\n    /// Write a string to the Writer where the string is encoded\n    /// as a length prefix (signed 32-bit integer) follows by\n    /// a sequence of characters.\n    /// \u003c/summary\u003e\n    protected static void WriteLengthPrefixedString(BinaryWriter writer, string value)\n    {\n        writer.Write(value.Length);\n        writer.Write(value.ToCharArray());\n    }\n}\n```\n\nКласс содержит базовый код для записи данных в поток. `AddRequestBody()` используется производными классами, для записи для записи собственных структурированных данных.\n\nПримеры производных классов:\n\n```C#\n/// \u003csummary\u003e\n/// Represents a Request from the client. A Request is as follows.\n/// \n///  Field Name         Type            Size (bytes)\n/// --------------------------------------------------\n///  RequestType        Integer         4\n///  Message            String          Variable\n/// \n/// Strings are encoded via a character count prefix as a \n/// 32-bit integer, followed by an array of characters.\n/// \n/// \u003c/summary\u003e\npublic class PrintMessageRequest : Request\n{\n    public string Message { get; }\n\n    public override RequestType Type =\u003e RequestType.PrintMessage;\n\n    public PrintMessageRequest(string message)\n    {\n        Message = message;\n    }\n\n    protected override void AddRequestBody(BinaryWriter writer)\n    {\n        WriteLengthPrefixedString(writer, Message);\n    }\n}\n\n/// \u003csummary\u003e\n/// Represents a Request from the client. A Request is as follows.\n/// \n///  Field Name         Type            Size (bytes)\n/// --------------------------------------------------\n///  ResponseType       Integer         4\n///  Iterations         Integer         4\n///  ForceUpdate        Boolean         1\n///  ModelName          String          Variable\n/// \n/// Strings are encoded via a character count prefix as a \n/// 32-bit integer, followed by an array of characters.\n/// \n/// \u003c/summary\u003e\npublic class UpdateModelRequest : Request\n{\n    public int Iterations { get; }\n    public bool ForceUpdate { get; }\n    public string ModelName { get; }\n\n    public override RequestType Type =\u003e RequestType.UpdateModel;\n\n    public UpdateModelRequest(string modelName, int iterations, bool forceUpdate)\n    {\n        Iterations = iterations;\n        ForceUpdate = forceUpdate;\n        ModelName = modelName;\n    }\n\n    protected override void AddRequestBody(BinaryWriter writer)\n    {\n        writer.Write(Iterations);\n        writer.Write(ForceUpdate);\n        WriteLengthPrefixedString(writer, ModelName);\n    }\n}\n```\n\nИспользуя данную структуру, клиенты могут создавать запросы различных типов, каждый из которых определяет собственную логику обработки данных и параметров.\nКлассы `PrintMessageRequest` и `UpdateModelRequest` предоставляют примеры запросов, которые можно отправить серверу для выполнения конкретных задач.\n\nНа стороне сервера, необходимо разработать соответствующую логику обработки входящих запросов.\nДля этого сервер должен читать данные из потока и использовать полученные параметры для выполнения нужных операций.\n\nПример полученного запроса на стороне сервера:\n\n```c#\n/// \u003csummary\u003e\n/// Represents a request from the client. A request is as follows.\n/// \n///  Field Name         Type                Size (bytes)\n/// ----------------------------------------------------\n///  RequestType       enum RequestType   4\n///  RequestBody       Request subclass   variable\n/// \n/// \u003c/summary\u003e\npublic abstract class Request\n{\n    public enum RequestType\n    {\n        PrintMessage,\n        UpdateModel\n    }\n    \n    public abstract RequestType Type { get; }\n\n    /// \u003csummary\u003e\n    ///     Read a Request from the given stream.\n    /// \u003c/summary\u003e\n    public static async Task\u003cRequest\u003e ReadAsync(Stream stream)\n    {\n        var lengthBuffer = new byte[4];\n        await ReadAllAsync(stream, lengthBuffer, 4).ConfigureAwait(false);\n        var length = BitConverter.ToUInt32(lengthBuffer, 0);\n\n        var requestBuffer = new byte[length];\n        await ReadAllAsync(stream, requestBuffer, requestBuffer.Length);\n\n        using var reader = new BinaryReader(new MemoryStream(requestBuffer), Encoding.Unicode);\n\n        var requestType = (RequestType) reader.ReadInt32();\n        return requestType switch\n        {\n            RequestType.PrintMessage =\u003e PrintMessageRequest.Create(reader),\n            RequestType.UpdateModel =\u003e UpdateModelRequest.Create(reader),\n            _ =\u003e throw new ArgumentOutOfRangeException()\n        };\n    }\n    \n    /// \u003csummary\u003e\n    /// This task does not complete until we are completely done reading.\n    /// \u003c/summary\u003e\n    private static async Task ReadAllAsync(Stream stream, byte[] buffer, int count)\n    {\n        var totalBytesRead = 0;\n        do\n        {\n            var bytesRead = await stream.ReadAsync(buffer, totalBytesRead, count - totalBytesRead);\n            if (bytesRead == 0) throw new EndOfStreamException(\"Reached end of stream before end of read.\");\n            totalBytesRead += bytesRead;\n        } while (totalBytesRead \u003c count);\n    }\n\n    /// \u003csummary\u003e\n    /// Read a string from the Reader where the string is encoded\n    /// as a length prefix (signed 32-bit integer) followed by\n    /// a sequence of characters.\n    /// \u003c/summary\u003e\n    protected static string ReadLengthPrefixedString(BinaryReader reader)\n    {\n        var length = reader.ReadInt32();\n        return length \u003c 0 ? null : new string(reader.ReadChars(length));\n    }\n}\n\n/// \u003csummary\u003e\n/// Represents a Request from the client. A Request is as follows.\n/// \n///  Field Name         Type            Size (bytes)\n/// --------------------------------------------------\n///  RequestType        Integer         4\n///  Message            String          Variable\n/// \n/// Strings are encoded via a character count prefix as a \n/// 32-bit integer, followed by an array of characters.\n/// \n/// \u003c/summary\u003e\npublic class PrintMessageRequest : Request\n{\n    public string Message { get; }\n\n    public override RequestType Type =\u003e RequestType.PrintMessage;\n\n    public PrintMessageRequest(string message)\n    {\n        Message = message;\n    }\n\n    protected override void AddRequestBody(BinaryWriter writer)\n    {\n        WriteLengthPrefixedString(writer, Message);\n    }\n}\n\n/// \u003csummary\u003e\n/// Represents a Request from the client. A Request is as follows.\n/// \n///  Field Name         Type            Size (bytes)\n/// --------------------------------------------------\n///  RequestType        Integer         4\n///  Iterations         Integer         4\n///  ForceUpdate        Boolean         1\n///  ModelName          String          Variable\n/// \n/// Strings are encoded via a character count prefix as a \n/// 32-bit integer, followed by an array of characters.\n/// \n/// \u003c/summary\u003e\npublic class UpdateModelRequest : Request\n{\n    public int Iterations { get; }\n    public bool ForceUpdate { get; }\n    public string ModelName { get; }\n\n    public override RequestType Type =\u003e RequestType.UpdateModel;\n\n    public UpdateModelRequest(string modelName, int iterations, bool forceUpdate)\n    {\n        Iterations = iterations;\n        ForceUpdate = forceUpdate;\n        ModelName = modelName;\n    }\n\n    protected override void AddRequestBody(BinaryWriter writer)\n    {\n        writer.Write(Iterations);\n        writer.Write(ForceUpdate);\n        WriteLengthPrefixedString(writer, ModelName);\n    }\n}\n```\n\nМетод `ReadAsync()` считывает тип запроса из потока, а затем, в зависимости от типа, считывает соответствующие данные и создает объект соответствующего запроса.\n\nРеализация протокола передачи данных и структурирование запросов в виде классов позволяют эффективно управлять обменом информацией между клиентом и сервером, обеспечивая при этом\nструктурированное и понятное взаимодействие между двумя сторонами.\nОднако, при проектировании подобных протоколов необходимо учитывать возможные риски безопасности, а также убедиться, что оба конца взаимодействия правильно обрабатывают все возможные случаи.\n\n## Управление соединениями\n\nДля отправки сообщений с UI клиента на сервер, создадим класс ClientDispatcher который будет обеспечивать обработку соединений,\nтайм-аутов и планирование запросов, предоставляя интерфейс для взаимодействия клиента с сервером через именованные трубы.\n\n```C#\n/// \u003csummary\u003e\n///     This class manages the connections, timeout and general scheduling of requests to the server.\n/// \u003c/summary\u003e\npublic class ClientDispatcher\n{\n    private const int TimeOutNewProcess = 10000;\n\n    private Task _connectionTask;\n    private readonly NamedPipeClientStream _client = NamedPipeUtil.CreateClient(PipeDirection.Out);\n\n    /// \u003csummary\u003e\n    ///     Connects to server without awaiting\n    /// \u003c/summary\u003e\n    public void ConnectToServer()\n    {\n        _connectionTask = _client.ConnectAsync(TimeOutNewProcess);\n    }\n\n    /// \u003csummary\u003e\n    ///     Write a Request to the server.\n    /// \u003c/summary\u003e\n    public async Task WriteRequestAsync(Request request)\n    {\n        await _connectionTask;\n        await request.WriteAsync(_client);\n    }\n}\n```\n\nПринцип работы:\n\n1. **Инициализация:** В конструкторе класса инициализируется `NamedPipeClientStream`, используемый для создания клиентского потока с именованным каналом.\n2. **Установка подключения:** Метод `ConnectToServer` инициирует асинхронное подключение к серверу. Результат операции сохраняется в `Task`.\n   `TimeOutNewProcess` используется для отключения клиента в случае возникновения непредвиденных исключений.\n3. **Отправка запросов:** Метод `WriteRequestAsync` предназначен для асинхронной отправки объекта Request через установленное соединение. Запрос отправится только после установки соединения.\n\nДля приема сообщений сервером, создадим класс ServerDispatcher который управлять соединением и читать запросы.\n\n```C#\n/// \u003csummary\u003e\n///     This class manages the connections, timeout and general scheduling of the client requests.\n/// \u003c/summary\u003e\npublic class ServerDispatcher\n{\n    private readonly NamedPipeServerStream _server = NamedPipeUtil.CreateServer(PipeDirection.In);\n\n    /// \u003csummary\u003e\n    ///     This function will accept and process new requests until the client disconnects from the server\n    /// \u003c/summary\u003e\n    public async Task ListenAndDispatchConnections()\n    {\n        try\n        {\n            await _server.WaitForConnectionAsync();\n            await ListenAndDispatchConnectionsCoreAsync();\n        }\n        finally\n        {\n            _server.Close();\n        }\n    }\n\n    private async Task ListenAndDispatchConnectionsCoreAsync()\n    {\n        while (_server.IsConnected)\n        {\n            try\n            {\n                var request = await Request.ReadAsync(_server);\n                if (request.Type == Request.RequestType.PrintMessage)\n                {\n                    var printRequest = (PrintMessageRequest) request;\n                    Console.WriteLine($\"Message from client: {printRequest.Message}\");\n                }\n                else if (request.Type == Request.RequestType.UpdateModel)\n                {\n                    var printRequest = (UpdateModelRequest) request;\n                    Console.WriteLine($\"The {printRequest.ModelName} model has been {(printRequest.ForceUpdate ? \"forcibly\" : string.Empty)} updated {printRequest.Iterations} times\");\n                }\n            }\n            catch (EndOfStreamException)\n            {\n                return; //Pipe disconnected\n            }\n        }\n    }\n}\n```\n\nПринцип работы:\n\n1. **Инициализация:** В конструкторе класса инициализируется `NamedPipeServerStream`, используемый для создания серверного потока с именованным каналом.\n2. **Прослушивание подключений:** Метод `ListenAndDispatchConnections` асинхронного ожидает подключения клиента, после завершения обработки запросов закрывает именованный канал и освобождает\n   ресурсы.\n3. **Обработка запросов:** Метод `ListenAndDispatchConnectionsCoreAsync` обрабатывает запросы, до момента отключения клиента.\n   В зависимости от типа запроса происходит соответствующая обработка данных, например, вывод в консоль содержания сообщения или обновление модели.\n\nПример отправки запроса из UI на сервер:\n\n```C#\n\n/// \u003csummary\u003e\n///     Programme entry point\n/// \u003c/summary\u003e\npublic sealed partial class App\n{\n    public static ClientDispatcher ClientDispatcher { get; }\n\n    static App()\n    {\n        ClientDispatcher = new ClientDispatcher();\n        ClientDispatcher.ConnectToServer();\n    }\n}\n\n/// \u003csummary\u003e\n///     WPF view business logic \n/// \u003c/summary\u003e\npublic partial class MainViewModel : ObservableObject\n{\n    [ObservableProperty] private string _message = string.Empty;\n\n    [RelayCommand]\n    private async Task SendMessageAsync()\n    {\n        var request = new PrintMessageRequest(Message);\n        await App.ClientDispatcher.WriteRequestAsync(request);\n    }\n\n    [RelayCommand]\n    private async Task UpdateModelAsync()\n    {\n        var request = new UpdateModelRequest(AppDomain.CurrentDomain.FriendlyName, 666, true);\n        await App.ClientDispatcher.WriteRequestAsync(request);\n    }\n}\n```\n\nПример кода полностью доступен в репозитории, вы можете запустить его на своей машине выполнив несколько шагов:\n\n- Запустите \"Build Solution\"\n- Запустите \"Run OneWay\\Backend\"\n\nПриложение автоматически запустит Server и Client, а полный вывод сообщений передающихся по NamedPipe вы увидите в консоли IDE.\n\n## Двусторонняя передача\n\nЧасто возникают ситуации, когда обычная однонаправленная передача данных от клиента к серверу недостаточна.\nВ таких случаях необходимо обрабатывать ошибки или отправлять результаты в ответ. Чтобы обеспечить более сложное взаимодействие между клиентом и сервером, разработчикам приходится прибегать\nк применению двухсторонней передачи данных, которая позволяет обмениваться информацией в обоих направлениях.\n\nКак и в случае с запросами, для эффективной обработки ответов также необходимо определить перечисление для типов ответов.\nЭто позволит клиенту правильно интерпретировать полученные данные.\n\n```C#\npublic enum ResponseType\n{\n    // The update request completed on the server and the results are contained in the message. \n    UpdateCompleted,\n    \n    // The request was rejected by the server.\n    Rejected\n}\n```\n\nДля эффективной обработки ответов потребуется создать новый класс, названный Response.\nПо функционалу он ничем не отличается от класса Request, однако в отличие от Request, который может читаться на сервере, Response будет записываться в поток.\n\n```C#\n/// \u003csummary\u003e\n/// Base class for all possible responses to a request.\n/// The ResponseType enum should list all possible response types\n/// and ReadResponse creates the appropriate response subclass based\n/// on the response type sent by the client.\n/// The format of a response is:\n///\n/// Field Name       Field Type          Size (bytes)\n/// -------------------------------------------------\n/// ResponseType     enum ResponseType   4\n/// ResponseBody     Response subclass   variable\n/// \u003c/summary\u003e\npublic abstract class Response\n{\n    public enum ResponseType\n    {\n        // The update request completed on the server and the results are contained in the message. \n        UpdateCompleted,\n    \n        // The request was rejected by the server.\n        Rejected\n    }\n\n    public abstract ResponseType Type { get; }\n\n    protected abstract void AddResponseBody(BinaryWriter writer);\n\n    /// \u003csummary\u003e\n    ///     Write a Response to the stream.\n    /// \u003c/summary\u003e\n    public async Task WriteAsync(Stream outStream)\n    {\n        // Same as request class from client\n    }\n\n    /// \u003csummary\u003e\n    /// Write a string to the Writer where the string is encoded\n    /// as a length prefix (signed 32-bit integer) follows by\n    /// a sequence of characters.\n    /// \u003c/summary\u003e\n    protected static void WriteLengthPrefixedString(BinaryWriter writer, string value)\n    {\n        // Same as request class from client\n    }\n}\n```\n\nПроизводные классы вы можете найти в репозитории проекта: [PipeProtocol](https://github.com/Nice3point/InterprocessCommunication/blob/main/TwoWay/Backend/Server/PipeProtocol.cs)\n\nДля того чтобы сервер мог отправлять ответы клиенту, мы должны модифицировать класс `ServerDispatcher`.\nЭто позволит записывать ответы в Stream после выполнения задачи.\n\nТак же изменим направление трубы на двунаправленное:\n\n```C#\n_server = NamedPipeUtil.CreateServer(PipeDirection.InOut);\n\n/// \u003csummary\u003e\n///     Write a Response to the client.\n/// \u003c/summary\u003e\npublic async Task WriteResponseAsync(Response response) =\u003e await response.WriteAsync(_server);\n```\n\nДля демонстрации работы добавим задержку на 2 секунды, эмулируя тяжелую задачу, в методе ListenAndDispatchConnectionsCoreAsync:\n\n```C#\nprivate async Task ListenAndDispatchConnectionsCoreAsync()\n{\n    while (_server.IsConnected)\n    {\n        try\n        {\n            var request = await Request.ReadAsync(_server);\n            \n            // ...\n            if (request.Type == Request.RequestType.UpdateModel)\n            {\n                var printRequest = (UpdateModelRequest) request;\n\n                await Task.Delay(TimeSpan.FromSeconds(2));\n                await WriteResponseAsync(new UpdateCompletedResponse(changes: 69, version: \"2.1.7\"));\n            }\n        }\n        catch (EndOfStreamException)\n        {\n            return; //Pipe disconnected\n        }\n    }\n}\n```\n\nВ настоящий момент клиент не обрабатывает ответы от сервера. \nДавайте сделаем это. \nСоздадим в клиенте класс Response, который будет обрабатывать полученные ответы.\n\n```C#\n/// \u003csummary\u003e\n/// Base class for all possible responses to a request.\n/// The ResponseType enum should list all possible response types\n/// and ReadResponse creates the appropriate response subclass based\n/// on the response type sent by the client.\n/// The format of a response is:\n///\n/// Field Name       Field Type          Size (bytes)\n/// -------------------------------------------------\n/// ResponseType     enum ResponseType   4\n/// ResponseBody     Response subclass   variable\n/// \n/// \u003c/summary\u003e\npublic abstract class Response\n{\n    public enum ResponseType\n    {\n        // The update request completed on the server and the results are contained in the message. \n        UpdateCompleted,\n\n        // The request was rejected by the server.\n        Rejected\n    }\n\n    public abstract ResponseType Type { get; }\n\n    /// \u003csummary\u003e\n    ///     Read a Request from the given stream.\n    /// \u003c/summary\u003e\n    public static async Task\u003cResponse\u003e ReadAsync(Stream stream)\n    {\n        // Same as request class from server\n    }\n\n    /// \u003csummary\u003e\n    /// This task does not complete until we are completely done reading.\n    /// \u003c/summary\u003e\n    private static async Task ReadAllAsync(Stream stream, byte[] buffer, int count)\n    {\n        // Same as request class from server\n    }\n\n    /// \u003csummary\u003e\n    /// Read a string from the Reader where the string is encoded\n    /// as a length prefix (signed 32-bit integer) followed by\n    /// a sequence of characters.\n    /// \u003c/summary\u003e\n    protected static string ReadLengthPrefixedString(BinaryReader reader)\n    {\n        // Same as request class from server\n    }\n}\n```\n\nДалее обновим класс ClientDispatcher, чтобы он мог обрабатывать ответы от сервера. Для этого добавим новый метод и изменим направление на двунаправленное.\n\n```C#\n_client = NamedPipeUtil.CreateClient(PipeDirection.InOut);\n\n/// \u003csummary\u003e\n///     Read a Response from the server.\n/// \u003c/summary\u003e\npublic async Task\u003cResponse\u003e ReadResponseAsync() =\u003e await Response.ReadAsync(_client);\n```\n\nТакже добавим обработку ответа во ViewModel, где будем просто выводить его как сообщение.\n\n```C#\n[RelayCommand]\nprivate async Task UpdateModelAsync()\n{\n    var request = new UpdateModelRequest(AppDomain.CurrentDomain.FriendlyName, 666, true);\n    await App.ClientDispatcher.WriteRequestAsync(request);\n\n    var response = await App.ClientDispatcher.ReadResponseAsync();\n    if (response.Type == Response.ResponseType.UpdateCompleted)\n    {\n        var completedResponse = (UpdateCompletedResponse) response;\n\n        MessageBox.Show($\"{completedResponse.Changes} elements successfully updated to version {completedResponse.Version}\");\n    }\n    else if (response.Type == Response.ResponseType.Rejected)\n    {\n        MessageBox.Show(\"Update failed\");\n    }\n}\n```\n\nЭти изменения позволят более эффективно организовать взаимодействие между клиентом и сервером, обеспечивая более полную и надежную обработку запросов и ответов.\n\n## Реализация плагина для Revit\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://github.com/Nice3point/InterprocessCommunication/assets/20504884/09e0dee3-d4bd-4858-87eb-6bf6766b8dde\" /\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003eТехнологии развиваются, а Revit не меняется © Конфуций\u003c/p\u003e\n\nВ настоящее время Revit использует .NET Framework 4.8. \nОднако для улучшения пользовательского интерфейса плагинов, рассмотрим переход на .NET 7. \nВажно отметить, что бэкэнд плагина будет взаимодействовать только с Revit на устаревшем Framework, и будет выступать в качестве сервера.\n\nДавайте создадим механизм взаимодействия, который позволит клиенту отправлять запросы на удаление элементов модели, а затем получать ответы о результате удаления. \nДля реализации этой функциональности мы будем использовать двустороннюю передачу данных между сервером и клиентом.\n\nПервым шагом в нашем процессе разработки будет научить плагин автоматически закрываться при закрытии Revit. \nДля этого мы написали метод, который отправляет ID текущего процесса клиенту. \nЭто поможет клиенту осуществить автоматическое закрытие своего процесса при закрытии родительского процесса Revit.\n\nКод для отправки ID текущего процесса клиенту:\n\n```C#\nprivate static void RunClient(string clientName)\n{\n    var startInfo = new ProcessStartInfo\n    {\n        FileName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!.AppendPath(clientName),\n        Arguments = Process.GetCurrentProcess().Id.ToString()\n    };\n\n    Process.Start(startInfo);\n}\n```\n\nА вот код для клиента, который осуществляет закрытие своего процесса при закрытии родительского процесса Revit:\n\n```C#\nprotected override void OnStartup(StartupEventArgs args)\n{\n    ParseCommandArguments(args.Args);\n}\n\nprivate void ParseCommandArguments(string[] args)\n{\n    var ownerPid = args[0];\n    var ownerProcess = Process.GetProcessById(int.Parse(ownerPid));\n    ownerProcess.EnableRaisingEvents = true;\n    ownerProcess.Exited += (_, _) =\u003e Shutdown();\n}\n```\n\nКроме того, нам необходим метод, который будет отвечать за удаление выбранных элементов модели:\n\n```C#\npublic static ICollection\u003cElementId\u003e DeleteSelectedElements()\n{\n    var transaction = new Transaction(Document);\n    transaction.Start(\"Delete elements\");\n    \n    var selectedIds = UiDocument.Selection.GetElementIds();\n    var deletedIds = Document.Delete(selectedIds);\n\n    transaction.Commit();\n    return deletedIds;\n}\n```\n\nТакже обновим метод ListenAndDispatchConnectionsCoreAsync для обработки входящих соединений:\n\n```C#\nprivate async Task ListenAndDispatchConnectionsCoreAsync()\n{\n    while (_server.IsConnected)\n    {\n        try\n        {\n            var request = await Request.ReadAsync(_server);\n            if (request.Type == Request.RequestType.DeleteElements)\n            {\n                await ProcessDeleteElementsAsync();\n            }\n        }\n        catch (EndOfStreamException)\n        {\n            return; //Pipe disconnected\n        }\n    }\n}\n\nprivate async Task ProcessDeleteElementsAsync()\n{\n    try\n    {\n        var deletedIds = await Application.AsyncEventHandler.RaiseAsync(_ =\u003e RevitApi.DeleteSelectedElements());\n        await WriteResponseAsync(new DeletionCompletedResponse(deletedIds.Count));\n    }\n    catch (Exception exception)\n    {\n        await WriteResponseAsync(new RejectedResponse(exception.Message));\n    }\n}\n```\n\nИ, наконец, обновленный код ViewModel:\n\n```C#\n[RelayCommand]\nprivate async Task DeleteElementsAsync()\n{\n    var request = new DeleteElementsRequest();\n    await App.ClientDispatcher.WriteRequestAsync(request);\n\n    var response = await App.ClientDispatcher.ReadResponseAsync();\n    if (response.Type == Response.ResponseType.Success)\n    {\n        var completedResponse = (DeletionCompletedResponse) response;\n        MessageBox.Show($\"{completedResponse.Changes} elements successfully deleted\");\n    }\n    else if (response.Type == Response.ResponseType.Rejected)\n    {\n        var rejectedResponse = (RejectedResponse) response;\n        MessageBox.Show($\"Deletion failed\\n{rejectedResponse.Reason}\");\n    }\n}\n```\n\n# Установка .NET Runtime во время установки плагина\n\nНе у каждого пользователя может быть установлена последняя версия .NET Runtime на локальной машине, нам необходимо внести изменения в установщик плагина.\n\nЕсли вы используете шаблоны [Nice3point.RevitTemplates](https://github.com/Nice3point/RevitTemplates), то внести изменения не составит труда.\nВ шаблонах используется библиотека WixSharp, которая позволяет создавать .msi файлы прямо на C#.\n\nДля добавления пользовательских действий, и установки .NET Runtime создадим `CustomAction`\n\n```C#\npublic static class RuntimeActions\n{\n    /// \u003csummary\u003e\n    ///     Add-in client .NET version\n    /// \u003c/summary\u003e\n    private const string DotnetRuntimeVersion = \"7\";\n\n    /// \u003csummary\u003e\n    ///     Direct download link\n    /// \u003c/summary\u003e\n    private const string DotnetRuntimeUrl = $\"https://aka.ms/dotnet/{DotnetRuntimeVersion}.0/windowsdesktop-runtime-win-x64.exe\";\n\n    /// \u003csummary\u003e\n    ///     Installing the .NET runtime after installing software\n    /// \u003c/summary\u003e\n    [CustomAction]\n    public static ActionResult InstallDotnet(Session session)\n    {\n        try\n        {\n            var isRuntimeInstalled = CheckDotnetInstallation();\n            if (isRuntimeInstalled) return ActionResult.Success;\n\n            var destinationPath = Path.Combine(Path.GetTempPath(), \"windowsdesktop-runtime-win-x64.exe\");\n\n            UpdateStatus(session, \"Downloading .NET runtime\");\n            DownloadRuntime(destinationPath);\n\n            UpdateStatus(session, \"Installing .NET runtime\");\n            var status = InstallRuntime(destinationPath);\n\n            var result = status switch\n            {\n                0 =\u003e ActionResult.Success,\n                1602 =\u003e ActionResult.UserExit,\n                1618 =\u003e ActionResult.Success,\n                _ =\u003e ActionResult.Failure\n            };\n\n            File.Delete(destinationPath);\n            return result;\n        }\n        catch (Exception exception)\n        {\n            session.Log(\"Error downloading and installing DotNet: \" + exception.Message);\n            return ActionResult.Failure;\n        }\n    }\n\n    private static int InstallRuntime(string destinationPath)\n    {\n        var startInfo = new ProcessStartInfo(destinationPath)\n        {\n            Arguments = \"/q\",\n            UseShellExecute = false\n        };\n\n        var installProcess = Process.Start(startInfo)!;\n        installProcess.WaitForExit();\n        return installProcess.ExitCode;\n    }\n\n    private static void DownloadRuntime(string destinationPath)\n    {\n        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;\n\n        using var httpClient = new HttpClient();\n        var responseBytes = httpClient.GetByteArrayAsync(DotnetRuntimeUrl).Result;\n\n        File.WriteAllBytes(destinationPath, responseBytes);\n    }\n\n    private static bool CheckDotnetInstallation()\n    {\n        var startInfo = new ProcessStartInfo\n        {\n            FileName = \"dotnet\",\n            Arguments = \"--list-runtimes\",\n            RedirectStandardOutput = true,\n            UseShellExecute = false,\n            CreateNoWindow = true\n        };\n\n        try\n        {\n            var process = Process.Start(startInfo)!;\n            var output = process.StandardOutput.ReadToEnd();\n            process.WaitForExit();\n\n            return output.Split('\\n')\n                .Where(line =\u003e line.Contains(\"Microsoft.WindowsDesktop.App\"))\n                .Any(line =\u003e line.Contains($\"{DotnetRuntimeVersion}.\"));\n        }\n        catch\n        {\n            return false;\n        }\n    }\n\n    private static void UpdateStatus(Session session, string message)\n    {\n        var record = new Record(3);\n        record[2] = message;\n\n        session.Message(InstallMessage.ActionStart, record);\n    }\n}\n```\n\nЭтот код проверяет, установлена ли требуемая версия .NET на локальной машине, и если нет, то скачивает и устанавливает ее.\nВо время установки обновляется Status текущего хода выполнения скачивания и распаковки Runtime.\n\nОсталось подключить CustomAction в проект WixSharp, для этого инициализируем свойство `Actions`:\n\n```C#\nvar project = new Project\n{\n    Name = \"Wix Installer\",\n    UI = WUI.WixUI_FeatureTree,\n    GUID = new Guid(\"8F2926C8-3C6C-4D12-9E3C-7DF611CD6DDF\"),\n    Actions = new Action[]\n    {\n        new ManagedAction(RuntimeActions.InstallDotnet, \n            Return.check,\n            When.Before,\n            Step.InstallFinalize,\n            Condition.NOT_Installed)\n    }\n};\n```\n\n# Заключение\n\nВ данной статье мы рассмотрели как Named Pipes, преимущественно используемые для взаимодействия между разными процессами, могут быть использованы в сценариях, где требуется обмен данными между приложениями на разных версиях .NET. \nИмея дело с кодом, который необходимо поддерживать в нескольких версиях, выверенная стратегия межпроцессного взаимодействия (Inter-Process Communication, IPC) может быть полезной и обеспечивать ключевые преимущества, такие как:\n\n- Решение конфликтов зависимостей\n- Улучшение производительности\n- Функциональная гибкость\n\nМы обсудили процесс создания сервера и клиента, которые взаимодействуют друг с другом через заранее определенный протокол, а также различные способы управления соединениями.\n\nРассмотрели пример ответов сервера и демонстрацию работы обеих сторон взаимодействия.\n\nНаконец, мы подчеркнули как Named Pipes используются в разработке плагина для Revit для обеспечения взаимодействия между бекендом, работающим на устаревшей платформе .NET 4.8, и пользовательским интерфейсом, работающим на более новой версии .NET 7.\n\nДемонстрационный код для каждой части этой статьи доступен на GitHub.\n\nВ определенных случаях разделение приложений на отдельные процессы может не только уменьшить зависимости в программе, но и ускорить его выполнение.\nНо давайте не забывать, что выбор подхода требует анализа и должен основываться на реальных требованиях и ограничениях вашего проекта.\n\nМы надеемся, что эта статья поможет вам найти оптимальное решение для ваших сценариев межпроцессного взаимодействия и даст понимание, как применять подходы IPC на практике.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnice3point%2Finterprocesscommunication","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnice3point%2Finterprocesscommunication","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnice3point%2Finterprocesscommunication/lists"}