Обмен данными между процессами в windows

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

Written on 04 Февраля 2007. Posted in Win32

Страница 14 из 15

Взаимодействие между процессами

Потоки одного процесса не имеют доступа к адресному пространству другого процесса. Однако существуют механизмы для передачи данных между процессами.

Разделяемая память

Как уже говорилось, система виртуальной памяти в Win32 использует файл подкачки — swap file (или файл размещения — page file), имея возможность преобразования страниц оперативной памяти в страницы файла на диске и наоборот. Система может проецировать на оперативную память не только файл размещения, но и любой другой файл. Приложения могут использовать эту возможность. Это может использоваться для обеспечения более быстрого доступа к файлам, а также для совместного использования памяти.

Такие объекты называются проекциями файлов (на оперативную память) (file-mapping object). Для создания проекции файла сначала вызывается функция CreateFileMapping(). Ей передается дескриптор (уже открытого) файла или указывается, что нужно использовать page file операционной системы. Кроме этого, в параметрах ей передается флаг защиты, максимальный размер проекции и имя объекта. Затем вызывается функция MapViewOfFile(). Она отображает представление файла (view of a file) в адресное пространство процесса. По окончании работы вызывается функция UnmapViewOfFile(). Она освобождает память и записывает данные в файл (если это не файл подкачки). Чтобы записать данные на диск немедленно, используется функция FlushViewOfFile(). Проекция файла, как и другие объекты ядра, может использоваться другими процессами через наследование, дублирование дескриптора или по имени.

Пример 11. Вот пример программы, которая создает проекцию в page file и записывает в нее данные.


#include <windows.h>

void main()
{
HANDLE hMapping;
char* lpData;
char* lpBuffer;
...
//Создание или открытие существующей проекции файла
hMapping = CreateFileMapping( (HANDLE)(-1), // используем page file
NULL, PAGE_READWRITE, 0, 0x0100, "MyShare");
if (hMapping == NULL) exit(0);
// Размещаем проекцию hMapping в адресном пространстве нашего процесса;
// lpData получает адрес размещения
lpData = (char*) MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS,0,0,0);
if (lpData == NULL) exit(0);
// Копируем в проекцию данные
memcpy ( lpData , lpBuffer );
...
// Заканчиваем работу. Освобождаем память.
UnmapViewOfFile(lpData);
// Закрываем объект ядра
CloseHandle(hMapping);
};

Прочие механизмы (сокеты, pipe)

Кроме разделяемой памяти, в Windows есть и другие способы передачи информации между процессами, например, каналы, поименованные каналы и сокеты. Все они имеют сходный принцип и представляют собой своеобразный канал или соединение, «трубу», соединяющую процессы. Программа, имея один конец такого соединения, может читать и/или писать в него данные, обмениваясь таким образом информацией с программой на другом конце.

Каналы используются для пересылки данных в одном направлении между дочерним и родительским процессами или между двумя дочерними процессами. Операции чтения/записи в канал похожи на подобные операции при работе с файлами.

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

Сокет — это абстрактный объект для обозначения одного из концов сетевого соединения, в том числе и через Internet. Сокеты Windows бывают двух типов: сокеты дейтаграмм и сокеты потоков. Интерфейс Windows Sockets (WinSock) основан на BSD-версии сокетов, но в нем имеются также расширения, специфические для Windows.

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

Передача может осуществляться несколькими способами:

— разделяемая память;

— канал (pipe, конвейер) — псевдофайл, в который один процесс пишет, а другой читает;

— сокеты — поддерживаемый ядром механизм, скрывающий особенности среды и позволяющий единообразно взаимодействовать процессам, как на одном компьютере, так и в сети;

— почтовые ящики (только в Windows), однонаправленные, возможность широковещательной рассылки;

— вызов удаленной процедуры, процесс А Может вызвать процедуру в процессе В, и получить обратно данные. 

Конвейеры (каналы)

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

Безымянные (или анонимные) каналы позволяют связанным процессам передавать информацию друг другу. Обычно, безымянные каналы используются для перенаправления стандартного ввода/вывода дочернего процесса так, чтобы он мог обмениваться данными с родительским процессом. Чтобы производить обмен данными в обоих направлениях, необходимо создать два безымянных канала. Родительский процесс записывает данные в первый канал, используя его дескриптор записи, в то время как дочерний процесс считывает данные из канала, используя дескриптор чтения. Аналогично, дочерний процесс записывает данные во второй канал и родительский процесс считывает из него данные. Безымянные каналы не могут быть использованы для передачи данных по сети и для обмена между несвязанными процессами.

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

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

3-8

Рис. 2.22 Схема реализации канала

Очереди сообщений

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

Разделяемая память

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

а) б)

Рис. 2.23 а) файл, отображенный на память; б) разделяемая память

Одно из преимуществ файлов, отображаемых в память, заключается в том, что их легко использовать совместно. Присвоение имени объекту «отображение файла» делает возможным совместное использование файла несколькими процессами. В этом случае его содержимое отображено на совместно используемую физическую память.

Почтовые ящики

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

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

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

Сокеты

3-9

Рис. 2.24 Схема реализации сокетов

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

Введение

Из курса ОС известно, что для выполнения таких задач, как совместное использование данных, построение интегрированных многофункциональных приложений и т.д., различным процессам (а также различным потокам) необходимо взаимодействовать между собой. Поскольку процессы изначально задумывались как обособленные сущности, для обеспечения корректного взаимодействия процессов требуются специальные средства и действия операционной системы.

Известно также, что в основе межпроцессного (Inter Process Communications, IPC) обмена обычно находится разделяемый ресурс (например, канал или сегмент разделяемой памяти), и, следовательно, ОС должна предоставить средства для генерации, именования, установки режима доступа и атрибутов защиты таких ресурсов. Обычно такой ресурс может быть доступен всем процессам, которые знают его имя и имеют необходимые привилегии.

Кроме того, организация связи между процессами всегда предполагает установления таких ее характеристик, как:

  • направление связи. Связь бывает однонаправленная (симплексная) и двунаправленная (полудуплексная для поочередной передачи информации и дуплексная с возможностью одновременной передачи данных в разных направлениях);
  • тип адресации. В случае прямой адресации информация посылается непосредственно получателю, например, процессу P-Send (P, message). В случае непрямой или косвенной адресации информация помещается в некоторый промежуточный объект, например, в почтовый ящик;
  • используемая модель передачи данных — потоковая или модель сообщений (см. ниже);
  • объем передаваемой информации и сведения о том, обладает ли канал буфером необходимого размера;
  • синхронность обмена данными. Если отправитель сообщения блокируется до получения этого сообщения адресатом, то обмен считается синхронным, в противном случае — асинхронным.

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

Способы межпроцессного обмена.

Традиционно считается, что основными способами межпроцессного обмена являются каналы и разделяемая память (
рис.
7.1), которые базируются на соответствующих объектах ядра.

Основные способы межпроцессного обмена

Рис.
7.1.
Основные способы межпроцессного обмена

В случае разделяемой памяти два или более процессов совместно используют сегмент памяти. Общение происходит с помощью обычных операций копирования или перемещения данных в памяти (средствами обычных языков программирования).

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

Ограниченный объем курса не позволяет рассмотреть другие механизмы межпроцессного обмена, реализованные в ОС Windows, например, сокеты, Clipboard или удаленный вызов процедуры (RPC). Исчерпывающая справочная информация на эту тему имеется в MSDN.

Понятие о разделяемом ресурсе

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

В качестве примера рассмотрим общение через разделяемую память (
рис.
7.2).

Адресные пространства процессов, взаимодействующих через сегмент разделяемой памяти

Рис.
7.2.
Адресные пространства процессов, взаимодействующих через сегмент разделяемой памяти

В ОС Windows сегмент разделяемой памяти создается с помощью Win32-функции CreateFileMapping (см.
рис.
7.3). В случае успешного выполнения данной функции создается ресурс — фрагмент памяти, доступный по имени (параметр lpname ), который базируется на соответствующем объекте ядра — «объекте-файле, отображаемом в память» с присущими любому объекту атрибутами. Процессу-создателю возвращается описатель (handle) ресурса. Другие процессы, желающие иметь доступ к ресурсу, также должны получить его описатель. В данном случае это можно сделать с помощью функции OpenFileMapping, указав имя ресурса в качестве одного из параметров.

Создание сегмента разделяемой памяти базируется на разделяемом ресурсе, которому соответствует объект ядра

Рис.
7.3.
Создание сегмента разделяемой памяти базируется на разделяемом ресурсе, которому соответствует объект ядра

Способы создания и характеристики файлов, отображаемых в память, будут рассмотрены в Части III курса «Система управления памятью», а в рамках данной темы ограничимся сведениями об обмене информации по каналам связи. При этом не надо забывать, что при любом способе общения в рамках одной вычислительной системы всегда будет использоваться элемент общей памяти. Другое дело, что в случае каналов эта память может быть выделена не в адресном пространстве процесса, а в адресном пространстве ядра системы, как это показано на
рис.
7.4.

Обмен через каналы связи осуществляется через буфер в адресном пространстве ядра системы

Рис.
7.4.
Обмен через каналы связи осуществляется через буфер в адресном пространстве ядра системы

Каналы связи

Основной принцип работы канала состоит в буферизации вывода одного процесса и обеспечении возможности чтения содержимого программного канала другим процессом. При этом часто интерфейс программного канала совпадает с интерфейсом обычного файла и реализуется обычными файловыми операциями read и write. Для обмена могут использоваться потоковая модель и модель обмена сообщениями.

Механизм генерации канала предполагает получение процессом-создателем (процессом-сервером) двух описателей (handles) для пользования этим каналом. Один из описателей применяется для чтения из канала, другой — для записи в канал.

Один из вариантов использования канала — это его использование процессом для взаимодействия с самим собой. Рассмотрим следующее изображение системы, состоящей из процесса и ядра, после создания канала (
рис.
7.5):

Общение процесса с самим собой через канал связи

Рис.
7.5.
Общение процесса с самим собой через канал связи

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

Очевидно, что обмен процесса с самим собой через канал большого смысла не имеет, поэтому обычно через канал взаимодействуют два (или более) процессов. Процесс, создающий канал, принято называть сервером, а другой процесс — клиентом. Для общения с каналом клиент и сервер должны иметь описатели (дескрипторы, handles) для чтения и записи. Процесс-сервер получает описатель при создании канала. Процесс-клиент может получить описатели в результате наследования, в том случае, когда клиент является потомком сервера. Это типично для общения через так называемые анонимные каналы. Другой способ получения — открытие по имени уже существующего именованного канала неродственным процессом, который в результате также становится обладателем необходимых описателей. Если организация доступа к каналу прошла успешно, то схема взаимодействия может выглядеть так, как показано на
рис.
7.6.

Общение процессов через канал связи

Рис.
7.6.
Общение процессов через канал связи

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

Передача информации от клиента серверу через канал связи

Рис.
7.7.
Передача информации от клиента серверу через канал связи

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

Для обмена данными между приложениями Windows существует много механизмов: буфер обмена (Clipboard), технология COM, передача данных между окнами процессов с помощью сообщения WM_COPYDATA, удаленный вызов процедур (RPC), сокеты (Windows Sockets), динамический обмен данными (DDE), именованные каналы (Named Pipes), почтовый ящик (Mailslot) и проецируемые в память файлы (Mapped file). У каждого способа есть свои плюсы и минусы. Здесь я не буду их рассматривать.

В данной статье, я хотел бы рассмотреть только один из перечисленных механизмов – именованные каналы (Named Pipes). Вообще существует два вида каналов: именованные и анонимные. Анонимные каналы обычно создаются родительским процессом и передаются дочернему процессу при его создании. У именованных каналов есть имя и любой процесс, знающий это имя, может подключиться к каналу. В любом случае, при работе с каналами у вас есть сервер (приложение создающее канал) и клиенты (приложения, подключающиеся к каналу).

Для работы с каналами в Windows есть системные функции, такие как CreateNamedPipe, TransactNamedPipe и другие. Но реализация Pipe-сервера и Pipe-клиента не лёгкая задача, поэтому я отправился на поиски готовых реализаций в Интернет.

Демонстрационные классы FWIOCompletionPipe

Первое, что я нашел решение FWIOCompletionPipes. Собственно это демонстрационная реализация сервера и клиента. В архиве вы найдёте юнит FWIOCompletionPipes.pas и примеры. В юните FWIOCompletionPipes.pas есть класс для создания сервера TFWPipeServer и класс клиента TFWPipeClient. Сразу скажу, что работу сервера я не проверял, а клиент пригодился, но об этом ниже.

Юнит Pipes

Отличная реализация Pipe-клиента и Pipe-сервера попалась мне на блоге Mick’s Mix. Чтобы код заработал под Delphi XE3 без ошибок и предупреждений, пришлось кое-что поправить. Мой исправленный вариант вы можете скачать здесь:

Файлы:

Есть вариант юнита c поддержкой всех версий Delphi и платформы Win64:

В юните есть класс-сервер TPipeServer, класс-клиент TPipeClient и даже есть приятный бонус — класс для запуска и управления консольным приложением с перехватом потока вывода TPipeConsole. Юнит лучше всего подключить к новому bpl-проекту и инсталлировать в среде Delphi. Тогда компоненты будут доступны в окне инструментов Tool Palette.

Пользоваться компонентами одно удовольствие. Чтобы создать сервер, создайте экземпляр класса TPipeServer (или положите его на форму или модуль данных), задайте имя канала, установите свойство Active в true и обрабатывайте событие OnPipeMessage. Пример обработки полученного сообщения (здесь и ниже будем считать, что в качестве сообщения мы передаём и получаем XML-документ):

procedure TForm1.PipeServer1PipeMessage(Sender: TObject; Pipe: NativeUInt;
   Stream: TStream);
var
   msg: IXMLDocument;
begin
   msg := TXMLDocument.Create(nil);
   //Загружаем XML-документ из потока.
   msg.LoadFromStream(Stream);
   //Отображаем полученный XML-документ.
   ShowMessage(msg.XML.Text);
end;

Теперь, чтобы отправить сообщение серверу нужно воспользоваться функцией SendStream:

procedure TForm2.Button1Click(Sender: TObject);
var
   xml: IXMLDocument;
   memoryStream: TMemoryStream;
begin
   memoryStream := TMemoryStream.Create;
   try
      //Создаём XML-документ для отправки.
      xml := TXMLDocument.Create(nil);
      xml.LoadFromXML('<MyMessage />');
      //Сохраняем XML-документ в поток.
      xml.SaveToStream(memoryStream);
      //Подключаемся к Pipe-серверу.
      PipeClient1.Connect;
      //Отправляем данные серверу.
      PipeClient1.SendStream(memoryStream);
   finally
      memoryStream.Free;
   end;
end;

Теперь разберёмся, как ответить клиенту на сообщение. Это можно сделать так:

procedure TForm1.PipeServer1PipeMessage(Sender: TObject; Pipe: NativeUInt;
   Stream: TStream);
var
   xml: IXMLDocument;
begin
   xml := TXMLDocument.Create(nil);
   //Загружаем XML-документ из потока.
   xml.LoadFromStream(Stream);
   //Отображаем полученный XML-документ.
   ShowMessage(xml.XML.Text);
   //Отправляем клиенту полученное сообщение обратно.
   TPipeServer(Sender).SendStream(Pipe, Stream);
end;

Если нам нужно отправить сообщение всем клиентам (сделать рассылку), это можно сделать так:

procedure TForm1.Button1Click(Sender: TObject);
var
   i: Integer;
   xml: IXMLDocument;
   memoryStream: TMemoryStream;
begin
   memoryStream := TMemoryStream.Create;
   try
      //Создаём XML-документ для отправки.
      xml := TXMLDocument.Create(nil);
      xml.LoadFromXML('<MyMessage />');
      //Сохраняем XML-документ в поток.
      xml.SaveToStream(memoryStream);
      //Рассылаем сообщение всем клиентам.
      for i := 0 to PipeServer1.ClientCount - 1 do
         PipeServer1.SendStream(PipeServer1.Clients[i], memoryStream);
   finally
      memoryStream.Free;
   end;
end;

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

program CmdClient;
 
 {$APPTYPE CONSOLE}
 
 uses
   Windows, Messages, SysUtils, Pipes;
 
 type
   TPipeEventHandler = class(TObject)
   public
      procedure OnPipeSent(Sender: TObject; Pipe: HPIPE; Size: DWORD);
   end;
 
   procedure TPipeEventHandler.OnPipeSent(Sender: TObject; Pipe: HPIPE; Size: DWORD);
   begin
      WriteLn('On Pipe Sent has executed!');
   end;
 
 var
   lpMsg: TMsg;
   WideChars: Array [0..255] of WideChar;
   myString: String;
   iLength: Integer;
   pcHandler: TPipeClient;
   peHandler: TPipeEventHandler;
 
 begin
 
// Create message queue for application
PeekMessage(lpMsg, 0, WM_USER, WM_USER, PM_NOREMOVE);
 
// Create client pipe handler
pcHandler:=TPipeClient.CreateUnowned;
// Resource protection
try
   // Create event handler
   peHandler:=TPipeEventHandler.Create;
   // Resource protection
   try
      // Setup clien pipe
      pcHandler.PipeName:='myNamedPipe';
      pcHandler.ServerName:='.';
      pcHandler.OnPipeSent:=peHandler.OnPipeSent;
      // Resource protection
      try
         // Connect
         if pcHandler.Connect(5000) then
         begin
            // Dispatch messages for pipe client
            while PeekMessage(lpMsg, 0, 0, 0, PM_REMOVE) do DispatchMessage(lpMsg);
            // Setup for send
            myString:='the message I am sending';
            iLength:=Length(myString) + 1;
            StringToWideChar(myString, wideChars, iLength);
            // Send pipe message
            if pcHandler.Write(wideChars, iLength * 2) then
            begin
               // Flush the pipe buffers
               pcHandler.FlushPipeBuffers;
               // Get the message
               if GetMessage(lpMsg, pcHandler.WindowHandle, 0, 0) then DispatchMessage(lpMsg);
            end;
         end
         else
            // Failed to connect
            WriteLn('Failed to connect to ', pcHandler.PipeName);
      finally
         // Show complete
         Write('Complete...');
         // Delay
         ReadLn;
      end;
   finally
      // Disconnect event handler
      pcHandler.OnPipeSent:=nil;
      // Free event handler
      peHandler.Free;
   end;
finally
   // Free pipe client
   pcHandler.Free;
end;
 
end.

Но этот случай не подходит для оконного приложения. Здесь мне пришлось изобретать велосипед, чтобы получить ответ от сервера синхронно. Я использовал компонент TFWPipeClient, что описан выше, чтобы отправить сообщение и получить ответ. Но выяснилось, что компонент TPipeServer отправляет клиенту не просто данные, а так называемый пакет данных. Т.е. сначала он отправляет клиенту маркер начала пакета, затем сами данные (т.к. если объём данных большой, то данные разбиваются на куски) и, в конце, маркер конца пакета. Также мне понадобился хэндл канала открытого компонентом TFWPipeClient поэтому пришлось добавить свойство Pipe. Мой дополненный вариант юнита FWIOCompletionPipes.pas можете скачать здесь:

В итоге, синхронная отправка сообщения серверу и получение ответа выглядит так:

//Функция отправляет сообщение серверу и ждёт ответа, если параметр waitForAnswer выставлен в true.
function TForm2.SendMessageToServer(Message: IXMLDocument; waitForAnswer: boolean): IXMLDocument;
var
   inputStream, outputStream: TMemoryStream;
   fwPipeClient: TFWPipeClient;
   buff: array [0..MAXWORD - 1] of Char;
   lpBytesRead: DWORD;
 
    //Функция проверки, являются ли полученные данные маркерами начала и конца пакета.
   function IsPacketBound(buffer: Pointer; size: Int64; controlCode: Cardinal): boolean;
   var
      lpControlMsg : PPipeMsgBlock;
   begin
      result := false;
      if size = SizeOf(TPipeMsgBlock) then
      begin
         lpControlMsg := PPipeMsgBlock(buffer);
         if (lpControlMsg^.Size = SizeOf(TPipeMsgBlock))
               and (lpControlMsg^.MagicStart = MB_MAGIC)
               and (lpControlMsg^.MagicEnd = MB_MAGIC)
               and (lpControlMsg^.ControlCode = controlCode) then
            result := true;
      end;
   end;
 
 begin
   result := nil;
   inputStream := TMemoryStream.Create;
   try
      Message.SaveToStream(inputStream);
      if not waitForAnswer then
         //Если не нужно ожидать ответа, то отправляем сообщение как обычно.
         PipeClient1.SendStream(inputStream)
      else
      begin
         //Создаём альтернативный Pipe-клиент.
         fwPipeClient := TFWPipeClient.Create('.', PipeClient1.PipeName);
         try
            //Подключаем Pipe-клиент к серверу.
            fwPipeClient.Active := true;
            outputStream := TMemoryStream.Create;
            try
               //Отправляем данные серверу.
               fwPipeClient.SendData(inputStream, outputStream);
               //Если сервер вернул маркер начала пакета, то будем считывать весь пакет.
               if IsPacketBound(outputStream.Memory, outputStream.Size, MB_START) then
               begin
                  outputStream.Size := 0;
                  while True do
                  begin
                     if ReadFile(fwPipeClient.Pipe, buff[0], MAXWORD, lpBytesRead, nil) then
                     begin
                        if IsPacketBound(@buff[0], lpBytesRead, MB_END) then
                           break
                        else
                           outputStream.Write(buff, lpBytesRead);
                     end;
                  end;
               end;
               result := TXMLDocument.Create(nil);
               result.LoadFromStream(outputStream);
            finally
               outputStream.Free;
            end;
         finally
            fwPipeClient.Free;
         end;
      end;
   finally
      inputStream.Free;
   end;
end;

А вот пример использования функции SendMessageToServer:

procedure TForm2.Button2Click(Sender: TObject);
var
   msg, answer: IXMLDocument;
begin
   //Создаём XML-документ для отправки.
   msg := TXMLDocument.Create(nil);
   msg.LoadFromXML('<MyMessage />');
   //Отправляем сообщение и получаем ответ.
   answer := SendMessageToServer(msg, true);
   //Отображаем полученное от сервера сообщение.
   ShowMessage(answer.XML.Text);
end;

Позже, при тестировании, выяснилось, что в классе TFWPipeClient неправильно реализовано подключение к каналу, поэтому при активной работе с каналом часто возникает ошибка System Error. Code: 231. Все копии канала заняты. Поэтому мне пришлось переписать функцию SendMessageToServer отказавшись от использования класса TFWPipeClient:

//Функция отправляет сообщение серверу и ждёт ответа, если параметр waitForAnswer выставлен в true.
function TForm2.SendMessageToServer(Message: IXMLDocument; waitForAnswer: boolean): IXMLDocument;
var
   inputStream, outputStream: TMemoryStream;
   buff: array [0..MAXWORD - 1] of Char;
   lpBytesRead: DWORD;
   lpNumberOfBytesWritten: DWORD;
   lpMode: DWORD;
   pipe: THandle;
 
   //Функция проверки, являются ли полученные данные маркерами начала и конца пакета.
   function IsPacketBound(buffer: Pointer; size: Int64; controlCode: Cardinal): boolean;
   var
      lpControlMsg : PPipeMsgBlock;
   begin
      result := false;
      if size = SizeOf(TPipeMsgBlock) then
      begin
         lpControlMsg := PPipeMsgBlock(buffer);
         if (lpControlMsg^.Size = SizeOf(TPipeMsgBlock))
               and (lpControlMsg^.MagicStart = MB_MAGIC)
               and (lpControlMsg^.MagicEnd = MB_MAGIC)
               and (lpControlMsg^.ControlCode = controlCode) then
            result := true;
      end;
   end;
 
 begin
   result := nil;
   inputStream := TMemoryStream.Create;
   try
      Message.SaveToStream(inputStream);
      if not waitForAnswer then
         //Если не нужно ожидать ответа, то отправляем сообщение как обычно.
         PipeClient1.SendStream(inputStream)
      else
      begin
         pipe := INVALID_HANDLE_VALUE;
         try
            //Подключаемся к каналу.
            while True do
            begin
               pipe := CreateFile(PChar('\.pipe' + PipeClient1.PipeName), GENERIC_READ or GENERIC_WRITE, 0, nil, OPEN_EXISTING, 0, 0);
               if pipe <> INVALID_HANDLE_VALUE then
                  break;
               if GetLastError <> ERROR_PIPE_BUSY then
                  RaiseLastOSError;
               Win32Check(WaitNamedPipe(PChar('\.pipe' + PipeClient1.PipeName), NMPWAIT_WAIT_FOREVER));
            end;
            lpMode := PIPE_READMODE_MESSAGE;
            Win32Check(SetNamedPipeHandleState(pipe, lpMode, nil, nil));
            //Отправляем данные серверу.
            Win32Check(WriteFile(pipe, inputStream.Memory^, inputStream.Size, lpNumberOfBytesWritten, nil));
            outputStream := TMemoryStream.Create;
            try
               Win32Check(ReadFile(pipe, buff[0], MAXWORD, lpBytesRead, nil));
               outputStream.Write(buff, lpBytesRead);
               //Если сервер вернул маркер начала пакета, то будем считывать весь пакет.
               if IsPacketBound(outputStream.Memory, outputStream.Size, MB_START) then
               begin
                  outputStream.Size := 0;
                  while True do
                  begin
                     if ReadFile(pipe, buff[0], MAXWORD, lpBytesRead, nil) then
                     begin
                        if IsPacketBound(@buff[0], lpBytesRead, MB_END) then
                           break
                        else
                           outputStream.Write(buff, lpBytesRead);
                     end;
                  end;
               end;
               result := TXMLDocument.Create(nil);
               result.LoadFromStream(outputStream);
            finally
               outputStream.Free;
            end;
         finally
            if pipe <> INVALID_HANDLE_VALUE then
               CloseHandle(pipe);
         end;
      end;
   finally
      inputStream.Free;
   end;
end;

Добавлено 29.07.2016. Но функция, приведённая выше не совершенна, и если наш сервер упал и не отвечает, то программа в ней зависнет и будет до бесконечности ждать, пока сервер не пришлёт ответ. Это можно исправить, если ввести таймауты для каждой отдельной операции при работе с каналом (подключение к каналу, чтение из него и запись в него). В этом случае функция существенно усложнится. Вот что получится:

//Функция отправляет сообщение серверу и ждёт ответа указанное в параметре timeout время (в мс), если параметр waitForAnswer выставлен в true.
function TForm2.SendMessageToServer(Message: IXMLDocument;
    waitForAnswer: boolean; timeout: DWORD): IXMLDocument;
var
  inputStream, outputStream: TMemoryStream;
  buff: array [0..MAXWORD - 1] of Char;
  lpBytesRead: DWORD;
  lpNumberOfBytesWritten: DWORD;
  lpMode: DWORD;
  pipe: THandle;
  isPacket: boolean;
  pendingRead: boolean;
  lastError: integer;
  olapRead: TOverlapped;
  olapWrite: TOverlapped;
  events: array [0 .. 0] of THandle;
 
  function IsPacketBound(buffer: Pointer; size: Int64; controlCode: Cardinal): boolean;
  var
    lpControlMsg : PPipeMsgBlock;
  begin
    result := false;
    if size = SizeOf(TPipeMsgBlock) then
    begin
      lpControlMsg := PPipeMsgBlock(buffer);
      if (lpControlMsg^.Size = SizeOf(TPipeMsgBlock))
        and (lpControlMsg^.MagicStart = MB_MAGIC)
        and (lpControlMsg^.MagicEnd = MB_MAGIC)
        and (lpControlMsg^.ControlCode = controlCode) then
        result := true;
    end;
  end;
 
  function AddPortion: boolean;
  begin
    result := false;
    if (not isPacket) and IsPacketBound(@buff[0], lpBytesRead, MB_START) then
      isPacket := true
    else if (not isPacket) then
    begin
      outputStream.Write(buff, lpBytesRead);
      result := true;
    end
    else if isPacket then
    begin
      if IsPacketBound(@buff[0], lpBytesRead, MB_END) then
      begin
        isPacket := false;
        result := true;
      end
      else
        outputStream.Write(buff, lpBytesRead);
    end;
  end;
 
  procedure ReadMessage(timeout: integer; generateTimeoutError: boolean);
  begin
    while true do
    begin
      pendingRead := false;
      if ReadFile(pipe, buff[0], MAXWORD, lpBytesRead, @olapRead) then
      begin
        if AddPortion then
          exit;
        ResetEvent(olapRead.hEvent);
      end
      else
      begin
        lastError := GetLastError;
        if lastError = ERROR_IO_PENDING then
          pendingRead := true
        else if (lastError = ERROR_MORE_DATA) then
        begin
          if AddPortion then
            exit;
        end
        else
          RaiseLastOSError(lastError);
      end;
      if pendingRead then
      begin
        events[0] := olapRead.hEvent;
        case WaitForMultipleObjects(1, @events, false, timeout) of
          WAIT_OBJECT_0:
            begin
              if GetOverlappedResult(pipe, olapRead, lpBytesRead, true) then
              begin
                if AddPortion then
                  exit;
              end
              else
              begin
                lastError := GetLastError;
                if lastError = ERROR_MORE_DATA then
                begin
                  if AddPortion then
                    exit;
                end
              end;
            end;
          WAIT_TIMEOUT:
            if generateTimeoutError then
              raise Exception.Create('За отведённое время не получен ответ.')
            else
              break;
        else
          RaiseLastOSError;
        end;
      end;
    end;
  end;
 
begin
  isPacket := false;
  pipe := INVALID_HANDLE_VALUE;
  pendingRead := false;
  ClearOverlapped(olapRead, True);
  ClearOverlapped(olapWrite, True);
  result := nil;
  inputStream := TMemoryStream.Create;
  try
    Message.SaveToStream(inputStream);
    if not waitForAnswer then
      PipeClient1.SendStream(inputStream)
    else
    begin
      try
        olapRead.hEvent := CreateEvent(nil, True, False, nil);
        olapWrite.hEvent := CreateEvent(nil, True, False, nil);
        try
          outputStream := TMemoryStream.Create;
          try
            while True do
            begin
              pipe := CreateFile(PChar('\.pipe' + PipeClient1.PipeName), GENERIC_READ or GENERIC_WRITE,
                0, nil, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, 0);
              if pipe <> INVALID_HANDLE_VALUE then
                break;
              if GetLastError <> ERROR_PIPE_BUSY then
                RaiseLastOSError;
              Win32Check(WaitNamedPipe(PChar('\.pipe' + PipeClient1.PipeName), timeout));
            end;
            lpMode := PIPE_READMODE_MESSAGE;
            Win32Check(SetNamedPipeHandleState(pipe, lpMode, nil, nil));
 
            while PeekNamedPipe(pipe, nil, 0, nil, @lpBytesRead, nil) and (lpBytesRead > 0) do
            begin
              ReadMessage(timeout, false);
              outputStream.Size := 0;
            end;
 
            if WriteFile(pipe, inputStream.Memory^, inputStream.Size,
              lpNumberOfBytesWritten, @olapWrite) then
            begin
              FlushFileBuffers(pipe);
              ResetEvent(olapWrite.hEvent);
            end
            else
            begin
              lastError := GetLastError;
              if lastError = ERROR_IO_PENDING then
              begin
                events[0] := olapWrite.hEvent;
                case WaitForMultipleObjects(1, @events, false, timeout) of
                  WAIT_OBJECT_0:
                  begin
                    if GetOverlappedResult(pipe, olapWrite, lpNumberOfBytesWritten, True) then
                      FlushFileBuffers(pipe)
                    else
                      RaiseLastOSError;
                  end;
                  WAIT_TIMEOUT:
                    raise Exception.Create('За отведённое время не удалось отправить сообщение.');
                else
                  RaiseLastOSError;
                end;
              end
              else
                RaiseLastOSError(lastError);
            end;
 
            ReadMessage(timeout, true);
 
            result := TXMLDocument.Create(nil);
            result.LoadFromStream(outputStream);
 
          finally
            outputStream.Free;
          end;
        finally
          CloseHandle(olapRead.hEvent);
          CloseHandle(olapWrite.hEvent);
        end;
      finally
        if pipe <> INVALID_HANDLE_VALUE then
          CloseHandle(pipe);
      end;
    end;
  finally
    inputStream.Free;
  end;
end;

Подведём итог. Используя вышеописанное решение, вы можете быстро наладить взаимодействие между несколькими процессами на одном компьютере под управлением Windows. Вы сможете отправлять большие объёмы информации и реализовать синхронное и асинхронное взаимодействие.

Межпроцессное взаимодействие

Передача
информации

Взаимодействие
между процессами (IPC)

  • DDE
    (Dynamic Data Exchange),

  • OLE,

  • atom
    (атомы)

  • pipes
    (анонимные каналы),

  • named
    pipes (именованные каналы)

  • почтовые
    ящики (mailslots)

  • RPC

  • сокеты

  • файлы,
    проецируемые в память (memory-mapped files)

  • разделяемая
    память (Shared Memory). Отличается от предыдущего
    способа только тем, что в качестве
    разделяемого файла используется часть
    файла подкачки.

Сообщения WM_COPYDATA

Лучший способ пересылки блока данных
из одной программы в другую.

Анонимные каналы (Anonymous pipes)

Полезны для организации прямой связи
между двумя процессами на одном ПК.

Именованные каналы (Named pipes)

Полезны для организации прямой связи
между двумя процессами на одном ПК
или в сети.

Почтовые ячейки (mailslots)

Полезны для организации связи одного
процесса со многими на одном ПК или в
сети.

Гнезда (sockets)

Полезны для организации пересылки
данных в гетерогенных средах.

Вызов удаленных процедур RPC

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

Разделяемая память

Непросто выделить вне DLL.

Файлы отображаемой памяти

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

Атомы

  • Атомы —
    это очень простой и доступный путь IPC.
    Идея состоит в том, что процесс может
    поместить строку в таблицу атомов и
    эта строка будет видна другим процессам.
    Когда процесс помещает строку в таблицу
    атомов, он получает 32-х битное значение
    (атом), и это значение используется для
    доступа к строке. Система не различает
    регистр строки.

  • Набор
    атомов собирается в таблицу (atom table).
    Система обеспечивает несколько таблиц
    атомов для разных задач. По типу доступа
    их два типа:

    • Локальные
      (доступны только из приложения)

    • Глобальные
      (доступны из всех приложений)

  • GlobalAddAtom

  • GlobalGetAtomName

  • GlobalFindAtom

  • GlobalDeleteAtom

Сообщение
WM_COPYDATA

Отправитель:

COPYDATASTRUCT cds;

cds.cbData = (DWORD)
nSize;

cds.lpData
= (PVOID) pBuffer;

SendMessage
(hWndTarget, WM_COPYDATA, (WPARAM) hWnd, (LPARAM) &cds);

Получатель:

PCOPYDATASTRUCT
pcds = (PCOPYDATASTRUCT) lParam;

PBYTE
pBuffer = (PBYTE) pcds -> lpData;

  • Сообщение
    WM_COPYDATA позволяет приложениям копировать
    данные между их адресными пространствами.
    При этом приложения не обязательно
    должны быть 32-разрядными — для
    16-разрядных приложений поддерживается
    автоматическая трансляция указателей.

  • Перед
    отправкой сообщения WM_COPYDATA необходимо
    инициализировать структуру COPYDATASTRUCT с
    информацией о предстоящей пересылке
    данных, в том числе с указателем на блок
    данных. Затем с помощью функции
    SendMessage сообщение WM_COPYDATA пересылается
    в принимающую программу; при этом
    параметр wParam содержит дескриптор окна
    вашей программы, а lParam — адрес структуры
    COPYDATASTRUCT.

  • Когда
    сообщение поступает обработчику
    WM_COPYDATA принимающей программы, его
    средствами указатель может быть
    скопирован из структуры COPYDATASTRUCT и
    использован, как любой другой указатель.

  • В
    принимающем процессе размер блока
    данных, адрес которого содержит lpData,
    извлекается из элемента cbData. Значение,
    считываемое из элемента lpData принимающей
    программой, возможно, будет отличаться
    от значения, помещенного туда отправляющей
    программой. Этого следовало ожидать,
    поскольку перед передачей сообщения
    WM_COPYDATA операционная система Windows NT
    выделяет в адресном пространстве
    принимающего процесса блок памяти,
    копирует данные из отправляющего
    процесса и обновляет значение lpData. По
    всей вероятности, в адресных пространствах
    этих процессов адреса размещении блока
    не совпадут.

  • В структуре
    COPYDATASTRUCT имеется третье, необязательное
    поле, dwData, в котором можно передать
    32-разрядное значение, определяемое в
    программе. Только не передавайте в этом
    поле указатель, потому что в принимающем
    процессе он не будет воспринят.

  • При
    использовании метода WM_COPYDATA необходимо
    помнить об одной детали: сообщения
    должны именно пересылаться, а не просто
    регистрироваться. Для того чтобы
    освободить память, выделенную в адресном
    пространстве принимающего процесса,
    операционная система должна быть
    информирована о моменте завершении
    пересылки. Кроме того, получатель
    сообщения WM_COPYDATA должен обращаться с
    блоком данных так, словно он предназначен
    только для чтения. Чтобы внести изменении
    в полученные данные, в принимающей
    программе необходимо подготовить их
    локальную копию. И наконец, не следует
    сохранять указатель, переданный в
    элементе lpData, дл применения его в
    дальнейшем.

Анонимные
каналы

  • Анонимные
    каналы не имеют имен.

  • Не пригодны
    для обмена через сеть.

  • Главная
    цель – служить каналом между родительским
    и дочерним процессом или между дочерними
    процессами.

  • Односторонний
    обмен.

  • Не возможен
    асинхронный обмен.

Каналы

Канал
представляет собой псевдофайл с
органзацией типа буфера FIFO (first input and
first output — первый вошел, первый вышел).

Образно
говоря, канал представляет собой трубу
(pipe) с двумя открытыми концами, в который
один процесс пишет, а другой читает

Использование
анонимных каналов

  • Главная
    цель – служить каналом между родительским
    и дочерним процессом или между дочерними
    процессами.

  • Родительский
    пpоцесс может быть консольным или
    GUI-пpиложение, но дочернее приложение
    должно быть консольным. Как вы знаете,
    консольное приложение использует
    стандартные дескрипторы для ввода и
    вывода.

  • Если мы
    хотите пеpенапpавить ввод/вывод
    консольного приложения, мы можем
    заменить один дескриптор другим
    дескриптором одного конца канала.
    Консольное приложение не будет знать,
    что оно использует один конец канала.
    Оно будет считать, что это стандартный
    дескриптор. Это вид полимоpфизма на
    ООП-жаpгоне.

  • Это мощный
    подход, так как нам не нужно модифицировать
    родительский процесс ни каким образом.

Создание
анонимных каналов

BOOL
CreatePipe(

PHANDLE
hReadPipe,

PHANDLE
hWritePipe,
LPSECURITY_ATTRIBUTES lpPipeAttributes,

DWORD
nSize

);

ReadFile

WriteFile

  • pReadHandle —
    это указатель на переменную типа dword,
    которая получит дескриптов конца чтения
    канала.

  • pWriteHandle
    — это указатель на переменную типа
    dword, которая получить дескриптов конца
    записи канала.

  • pPipeAttributes
    указывает на стpуктуpу SECURITY_ATTRIBUTES,
    котоpая опpеделяет, наследуется ли
    каждый из концов дочерним процессом.

  • nBufferSize —
    это предполагаемый pазмеp буфера, который
    канал заpезеpвиpует для использования.
    Это всего лишь предполагаемый pазмеp.
    Вы можете пеpедать NULL, чтобы указать
    функции использовать pазмеp по умолчанию.

  • Не забудьте
    установить паpаметp bInheritable стpуктуpы
    SECURITY_ATTRIBUTES в TRUE, чтобы дескриптры могли
    наследоваться.

Передача
дескрипторов

  • Установить
    паpаметp bInheritable стpуктуpы SECURITY_ATTRIBUTES в
    TRUE, чтобы дескрипторы могли наследоваться.

  • Вызов
    функции CreateProcess с
    параметром blnheritHandles =
    TRUE

  • Передача
    дескрипторов (командная строка,
    сообщения…)

  • Вызов
    функции DuplicateHandle

Дубликаты
дескрипторов

BOOL
DuplicateHandle(

HANDLE
hSourceProcessHandle,

HANDLE
hSourceHandle,

HANDLE
hTargetProcessHandle,

LPHANDLE
lpTargetHandle,

DWORD
dwDesiredAccess,

BOOL
bInheritHandle,

DWORD
dwOptions

);

  • Первый
    и третий параметры функции DuplicateHandle
    представляют собой описатели объектов
    ядра, специфичные для вызывающего
    процесса Кроме того, эти параметры
    должны идентифицировать именно процессы
    — функция завершится с ошибкой, если
    Вы передадите описатели на объекты
    ядра любого другого типа.

  • Второй
    параметр, hSourceHandle, — описатель объекта
    ядра любого типа. Однако его значение
    специфично не для процесса, вызывающего
    DuplicateHandle, а для того, на который указывает
    описатель hSourceProcessHandie. Параметр
    phTargetHandle — это адрес
    переменной типа HANDLE, в которой возвращается
    индекс записи с копией описателя из
    процесса-источника. Значение возвращаемого
    описателя специфично для процесса,
    определяемого параметром
    phTargetProcessHandle.

  • Предпоследние
    два параметра DuplicateHandle позволяют задать
    маску доступа и флаг наследования,
    устанавливаемые для данного описателя
    в процессе-приемнике. И, наконец, параметр
    dwOptions может быть 0 или любой комбинацией
    двух флагов. DUPLICATE_SAME_ACCESS и
    DUPLICATE_CLOSE_SOURCE

  • Первый
    флаг подсказывает DuplicateHandle: у описателя,
    получаемого процессом-приемником,
    должна быть та же маска доступа, что и
    у описателя в процессе-источнике Этот
    флаг заставляет DuplicateHandle игнорировать
    параметр dwDesiredAccess.

  • Второй
    флаг приводит к закрытию описателя в
    процессе-источнике. Он позволяет
    процессам обмениваться объектом ядра
    как эстафетной палочкой При этом счетчик
    объекта не меняется.

Пример
использования анонимного канала

  • Создаем
    анонимный канал с помощью CreatePipe.

  • Теперь
    мы должны подготовить параметры, которые
    передадим CreateProcess (мы используем эту
    функцию для загрузки консольного
    приложения).

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

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

  • Теперь
    вы можете читать данные с помощью
    ReadFile. Вы должны последовательно вызывать
    ReadFile, пока она не возвратит ноль, что
    будет означать, что больше данных нет.

  • Закроем
    дескриптор чтения канала.

NPFS
(Named Pipe File System)

  • Named Pipe
    File System
    является виртуальной файловой
    системой, которая управляет каналами
    named pipes.

  • Каналы
    named pipes относятся к классу файловых
    объектов (API Win32).

  • RPC
    реализован как надстройка над NPFS;

  • Канал
    представляет собой виртуальное
    соединение, по которому передается
    информация от одного процесса к другому.

  • Канал
    может быть однонаправленным или
    двунаправленным (дуплексным).

Работа
с именованными каналами

  • Серверный
    процесс создает канал на локальном
    компьютере
    с помощью функции
    программного интерфейса Win32
    «CreateNamedPipe».

  • Серверный
    процесс активизирует канал при помощи
    функции «ConnectNamedPipe», после чего
    к каналу могут подключаться клиенты.

  • Далее
    производится подключение к каналу
    \computer_namepipepipe_name посредством вызова
    функции «Create File».

Создание
именованного канала

HANDLE
CreateNamedPipe (
LPCTSTR
lpName,
DWORD
dwOpenMode,
DWORD
dwPipeMode,
DWORD
nMaxInstances,
DWORD
nOutBufferSize,
DWORD
nInBufferSize,
DWORD
nDefaultTimeOut,
LPSECURITY_ATTRIBUTES
lpSecurityAttributes

);

Параметры
создания канала

  • lpName –
    имя именованного канала;

  • dwOpenMode –
    определяет направление передачи,
    возможные варианты — PIPE_ACCESS_DUPLEX,
    PIPE_ACCESS_INBOUND, PIPE_ACCESS_OUTBOUND ;

  • dwPipeMode –
    способ передачи информации (PIPE_TYPE_BYTE
    или PIPE_TYPE_MESSAGE

  • nMaxInstances
    – количество каналов с данным именем
    которые может открыть пользователь;

  • nOutBufferSize
    и nInBufferSize – размер буферов приема и
    отправки;

  • nDefaultTimeout
    – максимальное время ожидания при
    асинхронном вводе/выводе через канал;

  • lpSecurityAttributes
    – указатель на структуру SECURITY_ATTRIBUTES,
    которая задает уровень защиты создаваемого
    объекта.

Подключение
к именованному каналу

BOOL
ConnectNamedPipe (
HANDLE
hNamedPipe,
LPOVERLAPPED
lpOverlapped );

BOOL
DisconnectNamedPipe (
HANDLE hNamedPipe

);

  • После
    того как канал создан, сервер подключается
    к нему с помощью функции ConnectNamedPipe
    () и начинает ожидать подключения
    клиента.

  • Необходимо
    отметить, что подключение сервера к
    каналу может осуществляться как
    синхронным, так и асинхронным способом.
    В первом случае ConnectNamePipe возвращает
    управление программе лишь после того,
    как клиент подключился к каналу, во
    втором же случае, возврат управления
    происходит сразу же, а уведомление
    программы о подключении осуществляется
    через структуру OVERLAPPED , указатель
    на которую передается вторым параметром
    в ConnectNamedPipe.

  • После
    завершения обмена необходимо отключиться
    от канала с помощью функции
    DisconnectNamedPipe.

  • Затем
    можно снова открыть канал и ожидать
    подключения следующего клиента, а по
    завершению работы с каналом необходимо
    закрыть его дескриптор функцией
    CloseHandle ().

Обмен
данными по именованному каналу

BOOL
ReadFile/WriteFile
(
HANDLE hFile,

LPVOID lpBuffer,

DWORD nNumberOfBytesToRead,

LPDWORD
lpNumberOfBytesRead,
LPOVERLAPPED
lpOverlapped

);

Работа
с каналом и ее завершение

  • После
    установления виртуального соединение
    серверный процесс и клиентский процесс
    могут обмениваться информацией при
    помощи пар функций «ReadFile» и
    «WriteFile».

  • При помощи
    одного и того же канала сервер может
    одновременно обслуживать нескольких
    клиентов. Для этого серверный процесс
    может создать N-ное количество экземпляров
    канала, вызвав N-ное количество раз
    функцию «CreateNamedPipe» (при этом в
    каждом вызове должно быть указано одно
    и то же имя канала).

  • Клиентский
    процесс может отключиться от канала в
    любой момент с помощью функции
    «CloseHandle». Серверный процесс
    может отключить клиента в любой момент
    с помощью функции «DisconnectNamedPipe».

Пример
клиент-серверного приложения (сервер)

HANDLE
hPipe = CreateNamedPipe(«\\.\pipe\PipeSrv»,PIPE_ACCESS_DUPLEX
| WRITE_DAC, PIPE_TYPE_BYTE,1,100,100,100,NULL);

if
(hPipe==INVALID_HANDLE_VALUE) {
…//Обработка
ошибки создания
канала

}

ConnectNamedPipe(hPipe,NULL);

DWORD
lpBuf; char cName[100];

ZeroMemory(&cName[0],sizeof(cName));

strcpy(&cName[0],»Hello
world!»);

WriteFile(hPipe,&cName,sizeof(cName),&lpBuf,NULL);

DisconnectNamedPipe(hPipe);

CloseHandle(hPipe);

Пример
клиент-серверного
приложения (клиент)

Работа с
именованным каналом на клиентской
стороне еще проще чем на серверной. Для
чтения и записи информации используются
уже знакомые нам функции ReadFile и WriteFile,
описатель канала закрывается аналогичным
образом. Вся разница заключается лишь
в способе открытия канала. Делается это
с помощью функции CreateFile, в которую
в качестве имени файла передается строка
следующего формата: \ServerNamepipePipеName.

…//pName
= “\ServerNamepipePipеSrv”

HANDLE
hFile = CreateFile(pName,GENERIC_READ | GENERIC_WRITE,
0,NULL,OPEN_EXISTING,0,NULL);

if
(hFile==INVALID_HANDLE_VALUE) {
…//Обработка
ошибки открытия
канала

}

char
str[100]; DWORD lpBuff;

ZeroMemory(&str[0],sizeof(str));

ReadFile(hFile,str,sizeof(str),&lpBuff,NULL);

printf(«Server
sent:n%s»,str);

CloseHandle(hFile);

Почтовые
ящики
(MailSlots)

  • Почтовые
    слоты — способ однонаправленной
    коммуникации. Приложение-сервер
    открывает слот, а клиенты могут писать
    в него. Слот сохраняет сообщения до тех
    пор, пока сервер их не прочтет. Разумеется,
    одно приложение может одновременно
    быть сервером и клиентом, обеспечивая
    двунаправленную связь. При этом
    приложения могут находиться даже на
    разных компьютерах в сети.

  • Процесс-сервер
    может завести почтовый ящик и дать ему
    имя, глобальное в сети. Любой клиент
    может с помощью операций работы с
    файлами отправить данные в этот ящик.
    Сервер, по мере необходимости, может
    читать переданные ему данные. Кроме
    этого, возможно широковещательная
    передача информации клиентом всем
    серверам домена.

Mailslot является
одним из механизмов, предназначенных
для осуществления обмена данными между
процессами (IPC). При этом процессы могут
быть запущены как на одной ПЭВМ (локально),
так и разных ПЭВМ, включённых в одну ЛВС
(удалённо).

  • Mailslot
    представляет собой псевдофайл,
    хранящийся в памяти. Для доступа к
    данным, содержащимся в этом псевдофайле,
    используются стандартные файловые
    функции Win32.

  • Объект
    Mailslot является временным объектом.
    После того, как будет закрыт последний
    дескриптор, ссылающийся на объект
    Mailslot, сам объект с данными будет
    уничтожен.

  • Обмен
    данными посредством Mailslot осуществляется
    в большинстве случаев между двумя
    процессами – клиентом и сервером.

Формат
имени сервера

  • При
    создании объекта Mailslot сервером,
    имя объекта должно иметь следующий
    формат: \.mailslot[path]name Имя объекта
    Mailslot должно содержать две наклонные
    черты влево, точку, ещё одну наклонную
    черту влево, слово «mailslot» и последнюю
    наклонную черту. После последней
    наклонной черты указывается собственно
    имя создаваемого объекта. Имя может
    также содержать путь. Примеры имени
    создаваемого объекта Mailslot:

  • \.mailslotTest.msl
    \.mailslotSampleDirSample

Форматы
имени клиента

  • Для того
    чтобы записать сообщение в Mailslot,
    клиент обращается к нему по имени. При
    этом если клиент и сервер запушены на
    одной ПЭВМ, то формат имени, используемый
    клиентом, может совпадать с форматом
    имени сервера. Однако чаще необходимо
    записывать сообщения в удалённый
    Mailslot, для чего необходимо использовать
    следующий формат имени объекта Mailslot:
    \ComputerNamemailslotTest.msl Здесь ComputerName
    сетевое имя ПЭВМ, на которой расположен
    сервер Mailslot.

  • Для того
    чтобы клиент мог поместить сообщение
    в каждый Mailslot с данным именем,
    созданный в пределах домена, формат
    имени объекта Mailslot для клиента
    должен быть следующим:
    \DomainNamemailslotTest.msl Здесь DomainName
    имя домена, включающие в себя те ПЭВМ,
    на которых запущены серверы Mailslot.
    Для того чтобы клиент мог поместить
    сообщение в каждый Mailslot с данным
    именем в первичном системном домене,
    имя объекта Mailslot должно иметь
    следующую форму: \*mailslotTest.msl

Клиенты,
сервера и имена

  • MailSlot
    cервер – является процессом, который
    создает и, обладает MailSlot. Когда
    сервер создает MailSlot, он получает
    указатель. Этот указатель должен
    использоваться, когда процесс читает
    сообщения от MailSlot. Только процесс,
    который создает MailSlot или получил
    указатель некоторым другим механизмом
    может прочитать данные из MailSlot. Все
    MailSlot локальные на процессе, который
    создает их; процесс не может создать
    дистанционный MailSlot.

  • MailSlot
    Клиент – является процессом, который
    пишет сообщение в MailSlot. Любой
    процесс, который имеет имя MailSlot
    может записать в него информацию.

Есть два вида программ использующих
данную возможность: MailSlot сервер MailSlot
клиент.

Создание
почтового ящика на сервере

HANDLE
CreateMailslot (

LPCTSTR
lpName, // имя

DWORD
nMaxMessageSize, // максимальный
//размер

DWORD lReadTimeout,
// интервал-тайм аута //чтения

LPSECURITY_ATTRIBUTES
lpSecurityAttributes

//
информация о
безопасности

);

  • Для
    открытия канала, созданного на другом
    компьютере в сети, необходимо указать
    имя в формате

  • \ИмяКомпьютераmailslot[Путь]ИмяКанала

  • Можно
    открыть канал для передачи информации
    сразу всем компьютерам указанного
    домена. Для этого формируется имя

  • \ИмяДоменаmailslot[Путь]ИмяКанала

  • Для
    передачи сообщения всем компьютерам
    первичного домена имя задается в форме

  • \*mailslot[Путь]ИмяКанала

Пример
создания сервера

HANDLE
hSlot = NULL;

hSlot
= CreateMailslot («\\computername\mailslot\messngr», 0,
MAILSLOT_WAIT_FOREVER, NULL);

if
(hSlot != INVALID_HANDLE_VALUE)

{
char
buffer[255]; DWORD nBytesRead;

ReadFile(hSlot,
&buffer, 255, &nBytesRead, NULL);

}

Создание
клиента
почтового
ящика

HANDLE
hSlot = CreateFile((«\\computername\mailslot\messngr»,
GENERIC_WRITE, FILE_SHARE_READ, NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);

if
(hSlot != INVALID_HANDLE_VALUE)

{
char
buf = «FromToMessage»;
uint cb =
sizeof(buf);
WriteFile(hSlot, buf, cb, &cb, NULL);

}

Использование
mailslot

  • Использование
    мэйлслотов особенно удобно в системах
    такого pода, pаботающих в пpеделах
    локальной сети. Пpогpамма-сеpвеp создает
    на своем компьютере мэйлслот с именем,
    известным всем клиентам (напpимеp
    BWCronServerMailSlot). Пpогpамма-клиент производит
    соединение с сеpвеpом путем создания
    клиентского мэйлслота (BWCronClientMailSlot) и
    пpовеpки существования сеpвеpа. Такую
    пpовеpку можно произвести путем посылки
    сеpвеpу контрольного сообщения. Сеpвеp
    дает добро на подключение либо отклоняет
    запрос. Дальнейший обмен данными будет
    производиться путем посылки сеpвеpу
    сообщений, и получения ответов от
    сеpвеpа через клиентский мэйлслот.

  • Мэйлслоты
    кроме систем «клиент-сеpвеp» можно
    использовать, напpимеp, для определения,
    запущена ли еще одна копия пpогpаммы
    где-либо в локальной сети. Это делается
    посылкой сообщения всем компьютерам
    в заданном домене (второй сеpвеp
    прикидывается клиентом и пытается
    установить связь с сеpвеpом. Если связь
    установлена, то работу не продолжаем,
    а если нет, то можно самому работать
    сеpвеpом).

В MSDN
написано, что если клиент открывает
слот прежде чем слот был создан сервером,
то он получит INVALID_HANDLE_VALUE

Получение
информации о почтовом ящике

BOOL
GetMailslotInfo (

HANDLE
hMailslot, // указатель на
слот

LPDWORD
lpMaxMessageSize, // максимальный
размер

LPDWORD
lpNextSize, // размер следующего

LPDWORD
lpMessageCount, // количество
сообщений

LPDWORD
lpReadTimeout // тайм аут

);

Изменение
настроек почтового ящика

BOOL
SetMailslotInfo(
HANDLE hMailslot,
DWORD lReadTimeout
);

Динамически
компонуемые библиотеки (DLL)

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

Раздел
с общими данными в DLL

#pragma
data_seg(«.shared»)

//Общие
данные

#pragma
data_seg()

UsersDll.def:

LIBRARY
«UsersDll» SECTIONS .shared READ WRITE SHARED

Удаленный
вызов
процедур
(RPC — Remote Procedure Call)

  • RPC (Remote
    Procedure Call) – это API, позволяющий приложению
    удаленно вызывать функции в других
    процессах как на своем, так и на удаленном
    компьютере.

  • Предоставляемая
    Win32 API модель RPC совместима со спецификациями
    Distributed Computing Environment (DCE), разработанными
    Open Software Foundation. Это позволяет приложениям
    Win32 удаленно вызывать процедуры
    приложений, выполняющихся на других
    компьютерах под другими операционными
    системами. RPC обеспечивают автоматическое
    преобразование данных между различными
    аппаратными и программными архитектурами.

Сокеты
(программные гнезда)

  • Взаимодействие
    процессов на основе программных гнезд
    основано на модели «клиент-сервер».
    Процесс-сервер «слушает (listens)»
    свое программное гнездо, одну из конечных
    точек двунаправленного пути коммуникаций,
    а процесс-клиент пытается общаться с
    процессом-сервером через другое
    программное гнездо, являющееся второй
    конечной точкой коммуникационного
    пути и, возможно, располагающееся на
    другом компьютере. Ядро поддерживает
    внутренние соединения и маршрутизацию
    данных от клиента к серверу.

  • Выделяются
    два типа программных гнезд — гнезда с
    виртуальным соединением (stream sockets) и
    датаграммные гнезда (datagram sockets). При
    использовании программных гнезд с
    виртуальным соединением обеспечивается
    передача данных от клиента к серверу
    в виде непрерывного потока байтов с
    гарантией доставки. При этом до начала
    передачи данных должно быть установлено
    соединение, которое поддерживается до
    конца коммуникационной сессии.
    Датаграммные программные гнезда не
    гарантируют абсолютной надежной,
    последовательной доставки сообщений
    и отсутствия дубликатов пакетов данных
    — датаграмм. Но для использования
    датаграммного режима не требуется
    предварительное дорогостоящее
    установление соединений, и поэтому
    этот режим во многих случаях является
    предпочтительным. Система по умолчанию
    сама обеспечивает подходящий протокол.
    Например, протокол TCP используется по
    умолчанию для виртуальных соединений,
    а протокол UDP — для датаграммного способа
    коммуникаций.

19

Соседние файлы в папке Кое-что к #4 тесту

  • #
  • #

Понравилась статья? Поделить с друзьями:
  • Обмен данными между приложениями ms windows dynamic data exchange dde обеспечивается
  • Обмен данными между компьютерами windows 10
  • Обмен данными в windows назначение буфера обмена
  • Обложки для проигрывателя windows media windows 10
  • Обложки для windows media player windows 7