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 с заданным именем, после чего любой процесс может открыть этот файл и передавать данные другому процессу, также открывшему файл с этим именем.
Именованные каналы используются для передачи данных между независимыми процессами или между процессами, работающими на разных компьютерах. Обычно, процесс сервера именованных каналов создает именованный канал с известным именем или с именем, которое будет передано клиентам. Процесс клиента именованных каналов, зная имя созданного канала, открывает его на своей стороне с учетом ограничений, указанных процессом сервера. После этого между сервером и клиентом создается соединение, по которому может производиться обмен данными в обоих направлениях. В дальнейшем наибольший интерес для нас будут представлять именованные каналы.
Рис. 2.22 Схема реализации канала
Очереди сообщений
Механизм очередей сообщений похож на механизм конвейеров с тем отличием, что он позволяет процессам и потока обмениваться структурированными сообщениями. Очереди сообщений являются глобальными средствами коммуникаций для процессов операционной системы, так как каждая очередь в пределах ОС имеет уникальное имя.
Разделяемая память
Разделяемая память представляет собой сегмент физической памяти, отображенной в виртуальное адресное пространство двух или более процессов.
а) б)
Рис. 2.23 а) файл, отображенный на память; б) разделяемая память
Одно из преимуществ файлов, отображаемых в память, заключается в том, что их легко использовать совместно. Присвоение имени объекту «отображение файла» делает возможным совместное использование файла несколькими процессами. В этом случае его содержимое отображено на совместно используемую физическую память.
Почтовые ящики
Почтовые ящики обеспечивают только однонаправленные соединения. Каждый процесс, который создает почтовый ящик, является «сервером почтовых ящиков». Другие процессы, называемые «клиентами почтовых ящиков», посылают сообщения серверу, записывая их в почтовый ящик. Входящие сообщения всегда дописываются в почтовый ящик и сохраняются до тех пор, пока сервер их не прочтет. Каждый процесс может одновременно быть и сервером и клиентом почтовых ящиков, создавая, таким образом, двунаправленные коммуникации между процессами.
Клиент может посылать сообщения на почтовый ящик, расположенный на том же компьютере, на компьютере в сети, или на все почтовые ящики с одним именем всем компьютерам выбранного домена. Почтовые ящики предлагают легкий путь для обмена короткими сообщениями, позволяя при этом вести передачу и по локальной сети, в том числе и по всему домену.
Почтовый ящик является псевдофайлом находящимся в памяти и необходимо использовать стандартные функции для работы с файлами, чтобы получить доступ к нему. Все почтовые ящики являются локальными по отношению к создавшему их процессу. Процесс не может создать удаленный почтовый ящик.
Сокеты
Рис. 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 тесту
- #
- #