Interprocess Communications

Давно было интересно, как делается так, чтобы был запущен только один экземпляр программы. Т.е. я знал в теории примерно с помощью чего это делается, но попробовать это в деле руки так и не доходили. Еще очень интересно было как так делается, что я открываю скажем таблицу Excel и она открывается не в еще одном приложении, а в том что уже открыто. Это конечно выбешивает порой, но все равно технология интересна.

Шуршание в интернете подвело к тому, что это называется Interprocess Communication (IPC) и существует множество способов как это осуществить, в зависимости от конечной цели, то ли вам надо только один экземпляр программы, то ли как в Экселе, то ли еще чего. Дальнейшее изучение вопроса и родило эту статью.

Теория

ОС Windows предоставляет широкие возможности по общению программ и пользованию общих данных между приложениями.  В наиболее общем случае это выливается в клиент-серверные схемы, причем одно приложение выступает как сервер так и как клиент. Для решения вопроса какой тип общения выбрать, можно задать себе следующие вопросы, которые помогут в выборе:

  • Будет ли приложение взаимодействовать с другими приложениями в сети или только на локальном компьютере.
  • Будет ли приложение взаимодействовать с приложениями написанными для других ОС.
  • Должно ли приложение само определять с кем взаимодействовать или же выбор возложить на пользователя.
  • Будет ли взаимодействие по принципу copy-paste с други приложениями или же взаимодействие будет жестко ограничено по действиям.
  • Насколько критична производительность.
  • Будет ли проложение консольное или же с интерфейсом пользователя. Некоторые способы взаимодействия требуют графического интерфейса.

Windows поддерживает следующие механизмы обмена данными:

  • Буфер обмена
  • COM
  • Data Copy
  • DDE
  • File mapping
  • Mailslots
  • Pipes
  • RPC
  • Windows Sockets
Буфер обмена

Действует как общее хранилище данных. Когда что-то копируется в буфер, приложение помещает данные в одном или нескольких форматах. Любое другое приложение может их оттуда забирать. К примеру блокнот и последний Ворд.  Clipboard.

COM

Это для приложений поддерживающих модель OLE – составных документов. Основа OLE это компонентная объектная модель (СОМ). Приложение использующее данную технологию может взаимодействовать с огромным количеством приложений, даже с теми которые еще только будут написаны. При использовании этой модели вы можете запускать другие приложения, для данных которые есть у вас.

Data Copy

Приложение может послать сообщение с помощью маркера WM_COPYDATA. Это требует кооперации во всем между отправляющей и принимающей стороной, базируется на Windows Messaging.  Data Copy.

DDE

Dynamic Data Exchange – может представляться как расширение механизма буфера обмена. Может применятся для сетевого и локального обмена данными.  Dynamic Data Exchange и Dynamic Data Exchange Management Library.

File Mapping

С помощью этого процесса можно представлять содержимое файла как область памяти в адресном сегменте процесса. Процесс может использовать простые указатели на области файла. Если же два приложения смотрят на один и тот же файл, то можно одним приложением модифицировать данные, чтобы дургое тут же получало обновленные данные. Это достаточно эффективный механизм в котором поддерживает безопасность на уровне ОС. Работает локально и необходимо  обеспечивать синхронизацию между процессами. File Mapping и Synchronization.

Mailslot

Предоставляет односторонний способ общения, есть сервер и клиент, сообщения идут только от клиента. Но ничто не мешает сделать сервер на двух сторонах. Поддерживается общение по сети, причем можно делать широковещательные сообщения, но не более 400 байт. Обычные сообщения не ограничены по размеру.  Mailslots.

Pipes

Существует два вида – анонимные каналы и именованные каналы. Анонимные каналы позволяют общаться связанным процессам. Для дуплексного общения требуется создать 2 канала. Именованные каналы могут использоваться для сетевого общения и между несвязанными процессами.  Pipes.

RPC

Remote Process Call – позволяет удаленно управлять приложением как локально, так и по сети. Этот способ совместим с Open Software Foundation (OSF) Distributed Computing Environment (DCE) что делает возможным обмен сообщениями с другими ОС. Поддерживается автоматическая конвертация данных для разных платформ. Клиент и сервер очень слабо связанны, но все равно показывают высокую эффективность при работе. Microsoft RPC Components

Windows Sockets

Это независимый от протокола интерфейс передачи данных. Можно обмениваться сообщениями с любой системой, которая поддерживает реализацию данного протокола.  Windows Sockets 2.

Да, на первом курсе я бы не заморачивался со всем этим зоопарком и влепил бы код на создание и удаление файла в корне программы. И заработал бы незапускающееся приложения после первого же экстренного выключения программы! =D

Цель

На локальном компьютере получить возможность посылать аргументы запуска второго экземпляра программы первому. Т.е. реализовать функционал любой программы, которая открывает документы в одном своем окне. Для примера это могут быть Excell, Notepad++, WinAmp.

Достигать цель мы будем с помощью именованных каналов и RPC. В конкретной реализации это выступит WCF для именованных каналов и Mutex для RPC. Два метода для сравнения реализации. Забегая вперед скажу, что WCF мне понравился гораздо больше, так как проще код и его меньше.

В процессе достижения цели у нас будет 2 основных задачи:

  1. Определить, есть ли уже экземпляр программы в памяти;
  2. Если запускается второй экземпляр, то его аргументы передать в первый экземпляр.

Подготовка

В обоих реализациях будет по два проекта, для лучшего разделения функциональности. Один WinForms, где будет проверка и обработка данных. И второй проект собственно с необходимым функционалом.

Способ номер 1

Первый способ будет основан на использовании RPC. Для создания решения нам понадобится какой-нибудь проект с визуальными компонентами (WinForm, WPF) и библиотека классов.

Для решения задачи номер один будет использован мьютекс.

Когда двум или более потокам одновременно требуется доступ к общему ресурсу, системе необходим механизм синхронизации, чтобы обеспечить использование ресурса только одним потоком одновременно.Mutex — примитив, который предоставляет эксклюзивный доступ к общему ресурсу только одному поток синхронизации.Если поток получает семафор, второй поток, желающий получить этот семафор, приостанавливается до тех пор, пока первый поток не освободит семафор.

Именованый мьютекс в системе может быть только один, и по этому имени можно его искать. Соответственно сразу рождается идея о том, что проверка на существование его в системе скажет нам, есть ли в системе уже запущенный экземпляр нашего приложения.

Начнем с основного класса, который будет создавать мьютексы.

Создание самого мьютекса ограничивается одной строкой.

Эта же строка кода и скажет нам, это единственный экземпляр или нет. Что ж, первый пукт программы выполнен сверхбыстро. Но это никак не продвинуло нас в проблеме передачи данных от одного экземпляра к другому.

Для передачи данных будем использовать специальное пространство имен System.Runtime.Remoting.Channels.Ipc. В нем определен класс IpcServerChannel с помощью которого и будет идти передача данных.

Данные передаваемые по этому каналу должны быть унаследованны от класса MarshalByRefObject, так как они могут создаются удаленно, и для того чтобы они могли путешествовать за пределы домена приложения. Данные также должны быть сериализуемые. Еще было бы неплохо определить делегат, который будет выполнять методы в 1ом приложении, но запускаться из 2ого. Итак, прокси-класс

Создание и регистрация канала делается следующим образом:

Где name – имя канала.

Алгоритм работы с каналами, после создания мьютекса будет таким:

Если это первый экземпляр, то надо

  • создать канал
  • зарегистрировать канал
  • зарегистрировать тип прокси как известный тип
  • создать прокси и сделать на него ссылку через адрес icp

Если у нас второй экземпляр, то

  • создать канал
  • зарегистрировать канал
    • получить прокси из первого экземпляра
    • послать через прокси данные

Передача и обработка сообщений

Определять единственность и неповторимость программы в памяти будем при запуске, а значит в Program.cs.

Сначала пытаемся создать трэкер и определить его единственность. В случае если уже процесс запущен был ранее, передаем параметры из этого процесса с помощью метода SendMessageToFirstInstance.

Обработка этого сообщения будет происходить на форме приложения (для простоты). Форма должна реализовывать интерфейс ISingleInstanceEnforcer.

В делегате определены действия, которые надо будет совершить при получении сообщения. В жизни тут надо будет воспользоваться шаблоном «менеджер команд», чтобы можно было красиво обработать много различных сообщений.

Способ  номер 2

Вторым способом у нас выступает использование WCF сервиса. Он будет и индикатором работы первичного экземпляра и способ для передачи сообщений. Т.е. нам опять понадобится визуальный проект и WCF библиотка.

WCF сервис может работать оберткой очень многих вещей и может быть применен в почти в любой жизненной ситуации, когда надо организовать взаимодействие между частями программы, либо между разными программами. На то он и Windows Communication Service, для того и задумывался.

Для работы в пределах одной машины будет использоваться биндинг NetNamedPipeBinding. Идеально подходит для нашей задачи, так как именованный канал может быть один и легко очень находится.

Начнем издалека, из некоторого подобия Command Manager. Для все той же простоты это будет статический класс. И да, начнем в этом случае со второй задачи, т.е. передачи данных.

Сделаем теперь регистрацию это действия на главной форме. Или в более сложном случае во ViewModel.

Теперь приступим к реализации собственно WCF сервиса. В проекте по умолчанию есть уже шаблон Service1, можно его свободно переименовать или оставить как есть без всякого ущерба. Я переименовал его в  SingleInstanceService.

В сервисе пока что будет только один метод, который будет принимать массив строк.

В этом весь сервис. Больше тут не будет ни строчки кода! Фактически у нас все построено для принятия и обработки данных. Теперь надо их научиться передавать. Данный функционал будет размещен в Program.cs.

Сначала надо будет задать уникальное имя для канала. Потом создаем хост для WCF сервиса. Можно сказать, что настройка WCF сервиса идет в полностью ручном режиме. При создании сервиса указываем какой класс будет основным функционалом.

Далее указываем привязки. Это будет именованные каналы. Здесь можно задавать поведение привязки, максимальное время ожидания, размер буфера и все в таком духе.

Следующим шагом будет создание конечных точек. При начале разработки создадим 2 точки: одна служебная MEX, для того чтобы можно было сконфигурировать ссылку на сервис. Другая будет конечной точкой самого сервиса. После настройки и в финальном релизе MEX точку можно будет убрать.

В конце запускаем сервис и не забываем его закрыть после окончания работы приложения. Теперь надо создать ссылку на сервис. Запускаем приложение без дебага Ctrl-F5 и добавляем ссылку на сервис.

Вбиваем адрес сервиса, который мы указали в коде и жмем “GO”. Должно получится примерно как на картинке.

Выбираем пространство имен для сервиса и заканчиваем работу с этим диалоговым окном. Готово!

Теперь можно решить первую задачу, о единственности сервиса. Просто попытаемся присоеденится к ниму создав клиентскую часть.

если это удается, то просто завершаем работу приложения, если же нет, то создаем новый хост. При неудачном выполнии операции SomeArgs будет исключение типа EndpointNotFoundException, которое однозначно говорит о том, что хост не запущен.

Все!

Как проверить что все работает

В первом способе нормально все проверяется по нажатию кнопки на самой форме, после чего создается процесс и идет обмен информацией.

Во втором способе такой фокус не прокатывает при запуске в дебаге. Я в целом догадываюсь почему так происходит, но исправлять это слишком лениво в этом примере. Главное что оно работает, и проверить это можно следующим образом. Идете в папку bin, где собрано приложение уже. Делаете ссылку на исполняемыйфайл и в ярлыке в свойсте Target после пути до файла пишете все что хотите.

Запускаете основной файл, потом запускаете то же самое через ярлык и видите, что в основном приложении обновились данные.

Заключение

На мой взгляд второй способ значительно проще при создании и эксплуатации. В улучшенной версии второго способа можно передавать не набор аргументов из командной строки, а уже сами сформированные команды, которые будут обрабатыватся на первичном экземпляре. Так же сам сервис более богат, все типизированно и прозрачно, что где приходит и отсылается.

Для обоих способов хорошо будет написать менеджер команд, чтобы не было жесткой связки со слоем интерфейса скажем.

Исходный код первого примера, второго примера.

Hard’n’heavy!

Оставить комментарий