Межпроцессная коммуникация посредством .NET

ОГЛАВЛЕНИЕ

Данная статья предоставляет простой способ использования .Net-сообщений среди приложений, не требущий особых усилий в конфигурации. В качестве альтернативы сообщениям на основе .NET Remoting предоставляется простая библиотека, основаная на сообщениях Windows низкого уровня.

•    Загрузить демо-проект - 85.41 KB
•    Загрузить исходный код - 11.99 KB

Введение

Данная статья показывает быструю и простую в применении реализацию для связи между доменами приложений в .NET путем использования родной передачи сообщений Windows. Библиотека XDMessaging основана на коде, разработанном для ускорения быстрой разработки для недавнего проекта Vista, требовавшего много связи между доменами приложений в защищенной среде. Библиотека оказалась крайне полезной в ряде ситуаций, где дистанционная связь .NET была бы непрактична или даже невозможна, и решила много проблем при своей простоте. Библиотека предназначена для отправки сообщений между несколькими приложениями в условиях одного и того же окна. Например, приложению панели задач может требоваться связаться  или следить за отдельным настольным приложением.

Библиотека не реализует междоменную связь через сеть, для которой достаточно дистанционной связи .NET.

Обновление: XDMessaging 2.0 сейчас доступна здесь и вводит поддержку для служб и консольных приложений Windows.

Предыстория

Почему бы не использовать дистанционную связь .NET? Она слишком долго устанавливается и настраивается. Другая проблема – отсутствие отправки полезных отчетов об ошибках, когда что-то не удается, всегда с разрешениями. Это не критика дистанционной связи .NET. Она имеет намного больше функций, чем данная реализации, и, разумеется, не ограничена связью в одном окне. Но для связи в одном и том же окне она не должна быть столь сложной. Почему бы не использовать передачу сообщений Windows? Этот механизм неуправляемые приложения используют именно для данной цели. Есть идея…
Возможно, вы не знали, но сообщения Windows – низкоуровневые средства связи, используемые операционной системой Windows для передачи информации о вводе данных пользователем, изменениях системы и других событиях, на которые могут реагировать приложения, работающие в системе. Например, перерисовки приложения запускаются сообщением WM_PAINT. Кроме системных сообщений, неуправляемые приложения могут определять пользовательские сообщения Windows и использовать их для связи с другими окнами. Они обычно принимают вид сообщений WM_USER. Если у вас установлен Spy++ (инструменты Visual Studio), можно отслеживать в реальном времени все сообщения, принимаемые окном.

Библиотека XDMessaging

Библиотека XDMessaging предоставляет простое в использовании, не требующее настройки решение для связи между доменами приложений в одном и том же окне. Она предоставляет простой интерфейс прикладного программирования (API) для отправки и приема адресных строковых сообщений через границы приложений. Библиотека позволяет использовать определенные пользователем 'псевдоканалы', через которые сообщения могут отправляться и приниматься. Любое приложение может отправить сообщение любому каналу, но оно должно зарегистрироваться как слушатель в канале, чтобы принимать. Таким образом, разработчики могут быстро и программно придумать, как лучше их приложениям связываться друг с другом и работать согласованно.

Пример: Отправка сообщения

// Отправить сообщение отключения каналу по имени commands(команды)
XDBroadcast.SendToChannel("commands", "shutdown");

Пример: Слушание канала

// Создать экземпляр слушателя
XDListener listener = new XDListener();

// Зарегистрироваться в каналах для слушания
listener.RegisterChannel("events");
listener.RegisterChannel("status");
listener.RegisterChannel("commands");

// Перестать слушать конкретный канал
listener.UnRegisterChannel("status");

Пример: Обработка сообщений

// Прикрепить обработчик события к экземпляру
listener.MessageReceived+=XDMessageHandler(this.listener_MessageReceived);

// Обработать сообщение
private void listener_MessageReceived(object sender, XDMessageEventArgs e)
{
    // e.DataGram.Message - сообщение
    // e.DataGram.Channel – имя канала
    switch(e.DataGram.Message)
    {
        case "shutdown":
            this.Close();
            break;
    }
}

Демонстрационный пример мессенджера

Чтобы увидеть демонстрационный пример, вам придется запустить несколько экземпляров приложения Messenger.exe. Демонстрационная программа не служит никакой практической цели, кроме демонстрации использования библиотеки XDMessaging. Она показывает передачу сообщений нескольким экземплярам настольного приложения через границы приложений. Приложение использует два произвольных канала по имени Status(статус) и UserMessage(сообщение пользователя). События окна, такие как onClosing и onLoad, передаются как сообщения в канал Status (отображаются зелеными), а сообщения пользователя передаются в канал UserMessage (отображаются синими). Отмечая или снимая выделение опций, можно переключать канал, сообщения которого будет слушать окно.


Как это работает

Библиотека использует системное сообщение Windows типа WM_COPYDATA. Это системное сообщение позволяет передавать данные между несколькими приложениями путем переноса указателя на данные, которые надо скопировать, в данном случае строку. Она отправляется другим окнам с помощью SendMessage из Win32 API с PInvoke.

[StructLayout(LayoutKind.Sequential)]
public struct COPYDATASTRUCT
{
    public IntPtr dwData;
    public int cbData;
    public IntPtr lpData;
}

[DllImport("user32", CharSet = CharSet.Auto)]
public extern static int SendMessage(IntPtr hwnd, int wMsg,
    int wParam, ref COPYDATASTRUCT lParam);

Структура COPYDATASTRUCT содержит информацию о данных сообщения, которые надо передать другому приложению. На данные сообщения ссылаются члены lpData и dwData. lpData является указателем на строковые данные, хранящиеся в памяти. dwData – размер передаваемых данных. Член cdData не используется в данном случае. Чтобы передать данные, сначала надо закрепить за строкой сообщения адрес в памяти и получить указатель на эти данные. Чтобы сделать это, используется API Marshal следующим образом:

// Сериализовать неформатированные строковые данные в двоичный поток
BinaryFormatter b = new BinaryFormatter();
MemoryStream stream = new MemoryStream();
b.Serialize(stream, raw);
stream.Flush();
int dataSize = (int)stream.Length;

// Создать массив байтов и передать данные потока
byte[] bytes = new byte[dataSize];
stream.Seek(0, SeekOrigin.Begin);
stream.Read(bytes, 0, dataSize);
stream.Close();

// Выделить адрес памяти для массива байтов
IntPtr ptrData = Marshal.AllocCoTaskMem(dataSize);

// Скопировать данные в байтах в этот адрес памяти
Marshal.Copy(bytes, 0, ptrData, dataSize);

При наличии в памяти строковых данных, на которые ссылается указатель ptrData в коде выше, можно создать экземпляр COPYDATASTRUCT и заполнить члены lpData и dwData соответственно. Итак, теперь, когда сообщение обернуто как объект COPYDATASTRUCT, можно отправить его другому окну с помощью API SendMessage. Однако чтобы сделать это, сначала  надо узнать, какие приложения должны принимать данные, т.е. какие слушают и на правильном канале. Так же, что если приложение не имеет описателя окна, которому можно отправить сообщение?

Для преодоления этого используются некоторые родные свойства окна и класс XDListener. При вызове экземпляра класса он создает невидимое окно на рабочем столе, действующее как слушатель для всех сообщений Windows. Это делается путем расширения класса NativeWindow из System.Windows.Forms. Переопределение метода WndProc позволяет фильтровать сообщения Windows и искать сообщение WM_COPYDATA, содержащее данные сообщения.

Класс XDListener также использует свойства окна для создания флагов свойств, указывающих, какие каналы слушает экземпляр, и, следовательно, какие сообщения он должен принимать. Когда сообщение передается, оно перечисляет все окна рабочего стола с помощью EnumChildWindows из Win32 API. Он ищет в окне флаг (имя свойства), обозначающий имя канала. Если находит –  отправляет этому окну сообщение Windows WM_COPYDATA. После этого сообщение ловится и обрабатывается экземпляром XDListener, которому принадлежит невидимое окно. Для чтения данных сообщения используется lParam родного сообщения Windows, чтобы расширить экземпляр COPYDATASTRUCT. Из него можно найти и восстановить исходное строковое сообщение, сохраненное в памяти ранее.

// Переопределяется NativeWindow, чтобы фильтровать пакет WM_COPYDATA
protected override void WndProc(ref Message msg)
{
    // Обрабатываются и передаются все системные сообщения
    base.WndProc(ref msg);
   
    // Если нужное сообщение
    if (msg.Msg == Win32.WM_COPYDATA)
    {
        // msg.LParam содержит указатель на структуру COPYDATASTRUCT
        Win32.COPYDATASTRUCT dataStruct =
            (Win32.COPYDATASTRUCT)Marshal.PtrToStructure(
            msg.LParam , typeof(Win32.COPYDATASTRUCT));
       
        // Создается массив байтов для хранения данных
        byte[] bytes = new byte[this.dataStruct.cbData];
       
        // Копируются исходные данные, на которые ссылается
        // структура COPYDATASTRUCT
        Marshal.Copy(this.dataStruct.lpData, bytes, 0,
            this.dataStruct.cbData);
        // Данные десериализуются обратно в строку
        MemoryStream stream = new MemoryStream(bytes);
        BinaryFormatter b = new BinaryFormatter();
       
        // Это сообщение, отправленное из другого приложения
        string rawmessage = (string)b.Deserialize(stream);
       
        // что-то делается с сообщением
    }
}

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

// Освобождается память, на которую ссылается данный указатель
Marshal.FreeCoTaskMem(lpData);

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

// Делегат, используемый при перечислении окон
public delegate int EnumWindowsProc(IntPtr hwnd, int lParam);

// Win32 API, используемый для перечисления потомков окна рабочего стола
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool EnumChildWindows(IntPtr hwndParent,
    EnumWindowsProc lpEnumFunc, IntPtr lParam);

// API, используемый для поиска именованного свойства в окне
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static int GetProp(IntPtr hwnd, string lpString);

// API, используемый для установки именованного свойства в окне, и,
//следовательно, регистрации канала обмена сообщениями
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static int SetProp(IntPtr hwnd, string lpString, int hData);

// API, используемое для удаления свойства из окна, и, следовательно, для отмены
// регистрации канала обмена сообщениями
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static int RemoveProp(IntPtr hwnd, string lpString);