В начале работы junior разработчиком мне пришлось столкнуться с таким малопонятным для меня на то время понятием, как Inter-Process Communication. Это была полная дикость для начинающего программиста, который и в рамках логики одного приложения ориентировался то с большим трудом. Стоит упомянуть, что проект написан на Delphi, а сам механизм IPC реализован с помощью File Mapping и Windows Messages.
Что удивительно, узнал я том, как вся эта система работает изнутри, спустя едва ли не год, когда пришлось маленько с ней повозиться. И только тогда я окончательно осознал, насколько высококлассной была её реализация, а API – удобен. Кому интересна реализация чего-то похожего на вышеупомянутую систему под С# – прошу под кат.
Поскольку в свободное время я изучаю .NET Framework, то меня заинтересовало, насколько легко/удобно реализовано IPC на .NET. К собственному разочарованию понял, что функционал с коробки значительно уступает в удобстве и простоте кустарной делфийской системе. Тут можно развести хороший такой холивар, опираясь на то что .NET ориентирован на абстрагирование от канала передачи данных и позволяет одинаково хорошо шарить объекты как по сети, так и между процессами. Но поскольку тема статьи – именно IPC, то попрошу этого не делать.
Небольшое введение. Я не сумел до конца осознать всех хитростей IPC на .NET, но, покурив мануалы ознакомившись с тематическими статьями, понял, что реально использовать следующие варианты:
1. COM,
2. File Mapping,
3. IPC Channels.
Так как реализация с использованием COM или File Mapping будет мало чем отличатся от аналогов на других языках, выбор однозначно пал на встроенный механизм IPC для .NET.
После попыток написать мало-мальски функциональное приложение осознал, что даже передача простой строки с его использованием требует лишних телодвижений на обоих приложениях – как в отправителе, так и в приемнике. В итоге, для шары объекта между процессами необходимо сделать нечто вроде:
Сервер:
IpcChannel serverChannel = new IpcChannel("MyServerChannel");
ChannelServices.RegisterChannel(serverChannel);
RemotingConfiguration.RegisterWellKnownServiceType(
typeof(IPCChannelRemoting.MyRemoteObject),
"SharedObj",
WellKnownObjectMode.SingleCall);
Клиент:
IpcChannel clientChanel = new IpcChannel("myClient");
ChannelServices.RegisterChannel(clientChanel);
ISharedObj obj = (Remoteable.ISharedAssemblyInterface)Activator.GetObject(
typeof(Remoteable.ISharedAssemblyInterface),
"ipc://MyServerChannel/SharedObj ");
Но что, если обмен данными должен использоваться повсеместно в приложении? В таком случае придется дублировать выше описаний код повсюду и оговорить правила создания URI для shared объектов. А если необходимо не просто запрашивать данные с другого процесса, но и подписываться на получение некоего сигнала/сообщения? Можно сделать Thread/Timer который лупит в цикле проверки или использовать WaitForSingleObject/EventWaitHandle. В общем, тут уже не обойтись без собственноручно созданной архитектуры.
По предыдущему опыту разработки приложения, которое широко использует IPC механизмы, очень удобно использовать следующий подход:
– есть три основных сущности: отправитель, получатель и сообщение;
– у каждого получателя и отправителя есть свои уникальные ID, на которые и идет отправка сообщений. Они могут быть привязаны к PID приложения, handle’y окна или любому общеизвестному идентификатору, о котором известно обеим сторонам;
– сообщение – это абстрактное понятие, которое может содержать в себе поля любого* типа, быть синхронным или асинхронным. Приложения, которые обмениваются этими сообщениями, должны знать о конкретных классах наследниках базовых сообщений. Отличие между синхронными и асинхронными сообщениями в том, что в синхронное сообщение можно положить результат запроса, если это необходимо, и он будет доступен отправителю сразу после обработки сообщения (по возврату стека от вызова метода отправки).
В итоге, используя получившуюся систему, отправление данных между процессами сводится к
– созданию объектов отправителя и сообщения,
– отправления сообщения с помощью отправителя,
– его обработки в процессе получателя.
using (BaseIPCDispatcher dispatcher = new BaseIPCDispatcher(slaveReceaverGUID)) {
TestAsyncComplexMessage testMessage = new TestAsyncComplexMessage (ReceaverID, null);
dispatcher.Dispatch(testMessage);
}
Подписка на получение сообщения в другом, ну или том же процессе:
…
Receiver.OnReceaveIPCMessage += OnReceaveMessage;
…
private void OnReceaveMessage(object sender, ReceaveMessageEventArgs e) {
TestAsyncComplexMessage testAsyncMessage = e.Message as TestAsyncComplexMessage;
if (testAsyncMessage != null) {
// process message
}
}
Имхо, получилось немного более очевидно, гибко и читабельно. Код покрыт функциональными тестами с использованием Master/Slave архитектуры.
Кто желает ознакомиться – результат лежит на GitHub
github.com/perevernihata/SimpleIPCCommSystem
З.Ы. Я понимаю, что, возможно, я по незнанию изобрел свой велосипед с квадратными колесами, – но так ведь это чертовски интересно делать! Конструктивная критика приветствуется.
* (Serializable для асинхронных сообщений).
Автор: ivan_p