Программирование для Windows NT© Александр Фролов, Григорий ФроловТом 27, часть 2, М.: Диалог-МИФИ, 1996, 272 стр. |
Когда вы создавали приложения для операционной системы Microsoft Windows версии 3.1, вы могли организовать передачу данных между параллельно работающими приложениями либо через общую область памяти, либо с использованием механизма динамического обмена данными DDE XE "DDE" , либо при помощи средств привязки и вставки объектов OLE XE "OLE" , либо через универсальный буфер обмена Clipboard XE "Clipboard" .
Что же касается Microsoft Windows NT, то в среде этой операционной системы вы по-прежнему можете пользоваться DDE, OLE и Clipboard, однако прямое создание глобальных областей памяти, доступных всем приложениям, невозможно. Причина этого лежит в том, что адресные пространства 32-разрядных приложений, работающих под управлением операционных систем Microsoft Windows NT и Microsoft Windows 95, полностью изолированы.
Что же предлагается взамен?
В программном интерфейсе Microsoft Windows NT предусмотрен достаточно широкий набор средств организации передачи данных между процессами, который вовсе не ограничивается относительно медленными механизмами DDE и OLE.
Прежде всего, вы можете организовать передачу данных между процессами, работающими в разных адресных пространствах, с использованием файлов, отображенных на память. О том, как работать с такими файлами, вы узнали из предыдущей главы.
Методика использования файлов, отображенных на память, для передачи данных между процессами заключается в следующем.
Один из процессов создает такой файл, задавая при этом имя отображения. Это имя является глобальным и доступно для всех процессов, запущенных в системе. Другие процессы могут воспользоваться именем отображения, открыв созданный ранее файл. В результате оба процесса могут получить указатели на область памяти, для которой выполнено отображение, и эти указатели будут ссылаться на одни и те же страницы виртуальной памяти. Обмениваясь данными через эту область, процессы должны обеспечить синхронизацию своей работы, например, с помощью критических секций, событий, объектов Mutex XE "Mutex" или семафоров (в зависимости от логики процесса обмена данными).
Если вы привыкли передавать данные вместе с сообщениями (что является общепринятой практикой в программировании для Microsoft Windows), то мы советуем вам обратить внимание на сообщение WM_COPYDATA. Это сообщение позволяет передавать данные между различными процессами за счет копирования передаваемых данных из адресного пространства одного процесса в адресное пространство другого процесса.
Еще один способ организации обмена данными между процессами заключается в организации специально предназначенных для этого каналов Pipe. Такие каналы напоминают файлы и достаточно удобны в работе. После их создания для обмена данными вы можете вызывать хорошо знакомые вам функции ReadFile XE "ReadFile" и WriteFile XE "WriteFile" , предназначенные для работы с файлами.
В том случае, когда требуется обеспечить передачу данных только в одном направлении, можно использовать так называемые каналы Mailslot.
Каналы Mailslot удобны тем, что их можно использовать для организации широковещательной передачи данных между процессами, запущенными на различных рабочих станциях сети. Что же касается каналов Pipe, то с их помощью можно организовать взаимодействие только двух процессов, но не широковещательную передачу данных.
Изучение средств обмена данными между процессами мы начнем с файлов, отображенных на память. Этот способ обладает высоким быстродействием, так как данные передаются между процессами непосредственно через виртуальную память.
Методика работы с файлами, отображаемыми на память, была описана в первой главе. Эта методика может быть использована без изменений для организации передачи данных между процессами, однако мы все же сделаем некоторые замечания.
Напомним, что отображение создается функцией CreateFileMapping.
Вот фрагмент кода из приложения Oem2Char, в котором создается отображение файла, а затем выполняется отображение этого файла в память:
hFileMapping = CreateFileMapping(hSrcFile, NULL, PAGE_READWRITE, 0, dwFileSize, NULL); if(hFileMapping == NULL) return; lpFileMap = MapViewOfFile(hFileMapping, FILE_MAP_WRITE, 0, 0, 0); if(lpFileMap == 0) return;
Здесь в качестве первого параметра для функции CreateFileMapping мы передаем идентификатор файла, открытого функцией CreateFile. Последний параметр указан как NULL, поэтому отображение не имеет имени.
Если отображение будет использоваться для передачи данных между процессами, удобно указать для него имя. Пользуясь этим именем, другие процессы смогут открыть отображение функцией OpenFileMapping.
Другое замечание касается идентификатора файла, передаваемого функции CreateFileMapping через первый параметр. Если вы создаете отображение только для того чтобы обеспечить передачу данных между процессами, вам не нужно создавать файл на диске компьютера. Указав в качестве идентификатора файла значение (HANDLE)0xFFFFFFFF, вы создадите отображение непосредственно в виртуальной памяти без использования дополнительного файла.
Ниже мы привели фрагмент кода, в котором создается отображение с именем $MyVerySpecialFileShareName$, причем это отображение создается в виртуальной памяти:
CHAR lpFileShareName[] = "$MyVerySpecialFileShareName$"; hFileMapping = CreateFileMapping((HANDLE)0xFFFFFFFF, NULL, PAGE_READWRITE, 0, 100, lpFileShareName);
После того как вы создали объект-отображение, следует выполнить отображение файла в память при помощи функции MapViewOfFile, как это было показано выше. В случае успеха эта функция вернет указатель на отображенную область памяти.
Итак, первый процесс создал отображение. Второй процесс, который будет выполнять обмен данными с первым процессом, должен открыть это отображение по имени при помощи функции OpenFileMapping, например, так:
hFileMapping = OpenFileMapping( FILE_MAP_READ | FILE_MAP_WRITE, FALSE, lpFileShareName);
Далее второе приложение выполняет отображение, вызывая функцию MapViewOfFile:
lpFileMap = MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);
Пользуясь значением, полученным от функции MapViewOfFile, второе приложение получает указатель на отображенную область памяти. Физически эта область находится в тех же страницах виртуальной памяти, что и область, созданная первым процессом. Таким образом, два процесса получили указатели на общие страницы памяти.
Перед завершением своей работы процессы должны отменить отображение файла и освободить идентификатор созданного объекта-отображения:
UnmapViewOfFile(lpFileMap); CloseHandle(hFileMapping);
Для иллюстрации методики обмена данными мжеду различными процессами с использованием файлов, отображаемых на память, мы подготовили исходные тексты двух консольных приложений: Fmap/Server и Fmap/Client. Эти приложения работают в паре (рис. 2.1).
Рис. 2.1. Взаимодействие консольных приложений Fmap/Server и Fmap/Client
Приложение Fmap/Server создает отображение и два объекта-события. Первый объект предназначен для работы с клавиатурой, второй - для обнаружения момента завершения приложения Fmap/Client. Объекты-события были описаны нами в предыдущем томе “Библиотеки системного программиста”, посвященном программированию для операционной системы Microsoft Windows NT.
Приложение Fmap/Client открывает созданное отображение и объекты-события, а затем в цикле вводит символы с клавиатуры, переключая один из объектов-событий в отмеченное состояние при вводе каждого символа. Коды введенных символов записываются в отображенную память.
По мере того как пользователь вводит символы в окне приложения Fmap/Client, приложение Fmap/Server отображает их в своем окне, получая коды введенных символов из отображенной памяти. Для синхронизации используется объект-событие, выделенное для работы с клавиатурой.
Если пользователь нажимает клавишу <Esc> в окне приложения Fmap/Client, это приложение отмечает оба события и завершает свою работу. Приложение Fmap/Server, обнаружив, что второй объект-событие оказался в отмеченном состоянии, также завершает свою работу. Таким образом, если завершить работу приложения Fmap/Client, то приложение Fmap/Server также будет завершено.
Исходный текст приложения Fmap/Server представлен в листинге 2.1.
Листинг 2.1. Файл fmap/server/server.c
#include <windows.h> #include <stdio.h> #include <conio.h> // Идентификаторы объектов-событий, которые используются // для синхронизации задач, принадлежащих разным процессам HANDLE hEventChar; HANDLE hEventTermination; HANDLE hEvents[2]; // Имя объекта-события для синхронизации ввода и отображения CHAR lpEventName[] = "$MyVerySpecialEventName$"; // Имя объекта-события для завершения процесса CHAR lpEventTerminationName[] = "$MyVerySpecialEventTerminationName$"; // Имя отображния файла на память CHAR lpFileShareName[] = "$MyVerySpecialFileShareName$"; // Идентификатор отображения файла на память HANDLE hFileMapping; // Указатель на отображенную область памяти LPVOID lpFileMap; int main() { DWORD dwRetCode; printf("Mapped and shared file, server process\n" "(C) A. Frolov, 1996, Email: frolov@glas.apc.org\n"); // Создаем объект-событие для синхронизации // ввода и отображения, выполняемого в разных процессах hEventChar = CreateEvent(NULL, FALSE, FALSE, lpEventName); // Если произошла ошибка, получаем и отображаем ее код, // а затем завершаем работу приложения if(hEventChar == NULL) { fprintf(stdout,"CreateEvent: Error %ld\n", GetLastError()); getch(); return 0; } // Если объект-событие с указанным именем существует, // считаем, что приложение EVENT уже было запущено if(GetLastError() == ERROR_ALREADY_EXISTS) { printf("\nApplication EVENT already started\n" "Press any key to exit..."); getch(); return 0; } // Создаем объект-событие для определения момента // завершения работы процесса ввода hEventTermination = CreateEvent(NULL, FALSE, FALSE, lpEventTerminationName); if(hEventTermination == NULL) { fprintf(stdout,"CreateEvent (Termination): Error %ld\n", GetLastError()); getch(); return 0; } // Создаем объект-отображение hFileMapping = CreateFileMapping((HANDLE)0xFFFFFFFF, NULL, PAGE_READWRITE, 0, 100, lpFileShareName); // Если создать не удалось, выводим код ошибки if(hFileMapping == NULL) { fprintf(stdout,"CreateFileMapping: Error %ld\n", GetLastError()); getch(); return 0; } // Выполняем отображение файла на память. // В переменную lpFileMap будет записан указатель на // отображаемую область памяти lpFileMap = MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0); // Если выполнить отображение не удалось, // выводим код ошибки if(lpFileMap == 0) { fprintf(stdout,"MapViewOfFile: Error %ld\n", GetLastError()); getch(); return 0; } // Готовим массив идентификаторов событий // для функции WaitForMultipleObjects hEvents[0] = hEventTermination; hEvents[1] = hEventChar; // Цикл отображения. Этот цикл завершает свою работу // при завершении процесса ввода while(TRUE) { // Выполняем ожидание одного из двух событий: // - завершение клиентского процесса; // - завершение ввода символа dwRetCode = WaitForMultipleObjects(2, hEvents, FALSE, INFINITE); // Если ожидание любого из двух событий было отменено, // если произошло первое событие (завершение клиентского // процесса) или если произошла ошибка, прерываем цикл if(dwRetCode == WAIT_ABANDONED_0 || dwRetCode == WAIT_ABANDONED_0 + 1 || dwRetCode == WAIT_OBJECT_0 || dwRetCode == WAIT_FAILED) break; // Читаем символ из первого байта отображенной // области памяти, записанный туда клиентским // процессом, и отображаем его в консольном окне putch(*((LPSTR)lpFileMap)); } // Закрываем идентификаторы объектов-событий CloseHandle(hEventChar); CloseHandle(hEventTermination); // Отменяем отображение файла UnmapViewOfFile(lpFileMap); // Освобождаем идентификатор созданного // объекта-отображения CloseHandle(hFileMapping); return 0; }
В глобальных переменных hEventChar и hEventTermination хранятся идентификаторы объектов-событий, предназначенных, соответственно, для работы с клавиатурой и для фиксации момента завершения работы приложения Fmap/Client. Эти же идентификаторы записываются в глобальный массив hEvents, который используется функцией WaitForMultipleObjects.
Глобальные имена объектов-событий хранятся в переменных lpEventName и lpEventTerminationName.
Имя отображения записывается в массив lpFileShareName, а идентификатор этого отображения - в глобальную переменную hFileMapping.
После выполнения отображения адрес отображенной области памяти, предназначенной для обмена данными с другим процессом, сохраняется в глобальной переменной lpFileMap.
Функция main приложения Fmap/Server создает два объекта-события, пользуясь для этого функцией CreateEvent. Описание этой функции вы найдете в предыдущем томе “Библиотеки системного программиста”.
Далее функция main создает объект-отображение и выполняет отображение, вызывая для этого, соответственно, функции CreateFileMapping и MapViewOfFile. Так как в качестве идентификатора файла функции CreateFileMapping передается значение (HANDLE)0xFFFFFFFF, отображение будет создано непосредственно в виртуальной памяти без использования файла, расположенного на диске.
После инициализации массива hEvents функция main запускает цикл, в котором выполняется ожидание событий и вывод символов, записанных приложением Fmap/Client в отображенную область виртуальной памяти.
Для ожидания двух событий используется функция WaitForMultipleObjects. Через третий параметр этой функции передается значение FALSE, поэтому ожидание прекращается в том случае, если любое из событий переходит в отмеченное состояние.
В том случае, когда в отмеченное состояние перешел объект-событие hEventTermination, функция WaitForMultipleObjects возвращает значение WAIT_OBJECT_0. Обнаружив это, функция main завершает свою работу, потому что событие hEventTermination отмечается при завершении работы клиентского приложения Fmap/Client.
Если же в отмеченное состояние переходит объект-событие hEventChar, функция WaitForMultipleObjects возвращает значение WAIT_OBJECT_0 + 1. В этом случае функция main читает первый байт из отображенной области памяти и выводит его в консольное окно при помощи хорошо знакомой вам из программирования для MS-DOS функции putch:
putch(*((LPSTR)lpFileMap));
Перед своим завершением функция main закрывает идентификаторы объектов-событий, отменяет отображение и освобождает идентификатор этого отображения.
Исходные тексты приложения Fmap/Client, предназначенного для совместной работы с приложением Fmap/Server, представлены в листинге 2.2.
Листинг 2.2. Файл fmap/client/client.c
#include <windows.h> #include <stdio.h> #include <conio.h> // Идентификаторы объектов-событий, которые используются // для синхронизации задач, принадлежащих разным процессам HANDLE hEvent; HANDLE hEventTermination; // Имя объекта-события для синхронизации ввода и отображения CHAR lpEventName[] = "$MyVerySpecialEventName$"; // Имя объекта-события для завершения процесса CHAR lpEventTerminationName[] = "$MyVerySpecialEventTerminationName$"; // Имя отображния файла на память CHAR lpFileShareName[] = "$MyVerySpecialFileShareName$"; // Идентификатор отображения файла на память HANDLE hFileMapping; // Указатель на отображенную область памяти LPVOID lpFileMap; int main() { CHAR chr; printf("Mapped and shared file, client process\n" "(C) A. Frolov, 1996, Email: frolov@glas.apc.org\n" "\n\nPress <ESC> to terminate...\n"); // Открываем объект-событие для синхронизации // ввода и отображения hEvent = OpenEvent(EVENT_ALL_ACCESS, FALSE, lpEventName); if(hEvent == NULL) { fprintf(stdout,"OpenEvent: Error %ld\n", GetLastError()); getch(); return 0; } // Открываем объект-событие для сигнализации о // завершении процесса ввода hEventTermination = OpenEvent(EVENT_ALL_ACCESS, FALSE, lpEventTerminationName); if(hEventTermination == NULL) { fprintf(stdout,"OpenEvent (Termination): Error %ld\n", GetLastError()); getch(); return 0; } // Открываем объект-отображение hFileMapping = OpenFileMapping( FILE_MAP_READ | FILE_MAP_WRITE, FALSE, lpFileShareName); // Если открыть не удалось, выводим код ошибки if(hFileMapping == NULL) { fprintf(stdout,"OpenFileMapping: Error %ld\n", GetLastError()); getch(); return 0; } // Выполняем отображение файла на память. // В переменную lpFileMap будет записан указатель на // отображаемую область памяти lpFileMap = MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0); // Если выполнить отображение не удалось, // выводим код ошибки if(lpFileMap == 0) { fprintf(stdout,"MapViewOfFile: Error %ld\n", GetLastError()); getch(); return 0; } // Цикл ввода. Этот цикл завершает свою работу, // когда пользователь нажимает клавишу <ESC>, // имеющую код 27 while(TRUE) { // Проверяем код введенной клавиши chr = getche(); // Если нажали клавишу <ESC>, прерываем цикл if(chr == 27) break; // Записываем символ в отображенную память, // доступную серверному процессу *((LPSTR)lpFileMap) = chr; // Устанавливаем объект-событие в отмеченное // состояние SetEvent(hEvent); } // После завершения цикла переключаем оба события // в отмеченное состояние для отмены ожидания в // процессе отображения и для завершения этого процесса SetEvent(hEvent); SetEvent(hEventTermination); // Закрываем идентификаторы объектов-событий CloseHandle(hEvent); CloseHandle(hEventTermination); // Отменяем отображение файла UnmapViewOfFile(lpFileMap); // Освобождаем идентификатор созданного // объекта-отображения CloseHandle(hFileMapping); return 0; }
После создания объектов-событий, предназначенных для синхронизации работы с приложением Fmap/Server, функция main приложения Fmap/Client открывает отображение при помощи функции OpenFileMapping, как это показано ниже:
hFileMapping = OpenFileMapping( FILE_MAP_READ | FILE_MAP_WRITE, FALSE, lpFileShareName);
В качестве имени отображения здесь указывается строка $MyVerySpecialFileShareName$ - точно такая же, что и в приложении Fmap/Server.
Далее в случае успеха выполняется отображение в память:
lpFileMap = MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);
Если отображение выполнено успешно, в глобальную переменную lpFileMap записывается указатель на отображенную область памяти, а затем запускается цикл ввода символов с клавиатуры.
Символы вводятся при помощи функции консольного ввода getche. Результат сохраняется в первом байте отображенной области памяти, откуда его будет брать для вывода приложение Fmap/Server:
chr = getche(); if(chr == 27) break; *((LPSTR)lpFileMap) = chr;
После выполнения записи функция main устанавливает в отмеченное состояние объект-событие, предназначенное для работы с клавиатурой.
Если пользователь нажимает в окне приложения Fmap/Client клавишу <Esc>, имеющую код 27, цикл прерывается. Оба объекта-события переводятся в отмеченное состояние, после чего идентификаторы этих объектов освобождаются.
Перед завершением работы функция main отменяет отображение файла и освобождает идентификатор объекта-отображения.
Метод передачи данных между процессами, основанный на использовании файлов, отображенных на виртуальную память, работает достаточно быстро, так как процессы имеют прямой доступ к общим страницам памяти. Тем не менее, при использовании этого метода необходимо заботиться о синхронизации процессов, для чего следует использовать объекты синхронизации.
Если скорость передачи данных не является критичной, можно воспользоваться удобным способом передачи данных, не требующим синхронизации. Этот метод основан на передаче сообщения WM_COPYDATA из одного приложения в другое при помощи функции SendMessage (функцию PostMessage для передачи этого сообщения использовать нельзя).
Сообщение WM_COPYDATA использует оба параметра - wParam и lParam. Через параметр wParam необходимо передать идентификатор окна, посылающего сообщение. Параметр lParam используется для передачи указателя на предварительно заполненную структуру COPYDATASTRUCT, в которой находится ссылка на передаваемые данные.
Если приложение обрабатывает сообщение WM_COPYDATA, то соответствующий обработчик должен вернуть значение TRUE, а если нет - FALSE.
Ниже мы привели формат структуры COPYDATASTRUCT:
typedef struct tagCOPYDATASTRUCT { DWORD dwData; // 32-разрядные данные DWORD cbData; // размер передаваемого буфера с данными PVOID lpData; // указатель на буфер с данными } COPYDATASTRUCT;
Перед посылкой сообщения WM_COPYDATA приложение должно заполнить структуру COPYDATASTRUCT.
В поле dwData можно записать произвольное 32-разрядное значение, которое будет передано вместе с сообщением.
В поле lpData вы дополнительно можете записать указатель на область данных, полученную, например, при помощи функции HeapAlloc. Размер этой области следует записать в поле cbData.
Когда функция окна принимающего приложения получит сообщение WM_COPYDATA, она должна скопировать в свой локальный буфер данные, которые были переданы вместе с сообщением. И это единственное, что можно с этими данными сделать. Их, например, нельзя изменять. Не следует также надеятся, что данные сохранятся до обработки другого сообщения, поэтому копирование следует выполнить во время обработки сообщения WM_COPYDATA.
Если вам достаточно передать из одного приложения в другое 32-разрядное значение, в поле lpData можно записать константу NULL.
Для иллюстрации методов работы с сообщением WM_COPYDATA мы подготовили два приложения, которые называются RCLOCK и STIME.
Приложение RCLOCK раз в секунду получает от приложения STIME сообщение WM_COPYDATA, вместе с которым передается строка текущего времени. Полученая строка отображается в небольшом окне, расположенном в левом нижнем углу рабочего стола (рис. 2.2).
Рис. 2.2. Окно приложения RCLOCK
Заметим, что сразу после запуска (если приложение STIME еще не активно) в окне приложения RCLOCK отображается строка <Unknown>, как это показано на рис. 2.3.
Рис. 2.3. Исхндное состояние окна приложения RCLOCK
Если при активном приложении RCLOCK завершить приложение STIME, последнее передаст строку <Terminated>, которая будет показана в окне приложения RCLOCK.
Для того чтобы завершить работу приложения RCLOCK вы должны сделать его окно текущим, щелкнув по нему левой клавишей мыши, а затем нажать комбинацию клавиш <Ctrl+F4>. При необходимости вы сможете реализовать более удобный способ самостоятельно. В 11 томе “Библиотеки системного программиста” мы привели исходные тексты приложения TMCLOCK, из которого вы можете взять некоторые идеи для улучшения пользовательского интерфейса. Например, вы можете организовать перемещение окна приложения мышью, обрабатывая сообщение WM_NCHITTEST.
Главный файл исходных текстов приложения RCLOCK представлен в листинге 2.3.
Листинг 2.3. Файл rclock/rclock.c
// ================================================== // Приложение RCLOCK (серверное) // Демонстрация использования сообщения WM_COPYDATA // для передачи данных между процессами // // (С) Фролов А.В., 1996 // Email: frolov@glas.apc.org // ================================================== #define STRICT #include <windows.h> #include <windowsx.h> #include <commctrl.h> #include "resource.h" #include "afxres.h" #include "rclock.h" HINSTANCE hInst; char szAppName[] = "RclockApp"; char szAppTitle[] = "Remote Clock"; // Метрики шрифта с фиксированной шириной символов LONG cxChar, cyChar; RECT rc; char szBuf[80]; // ----------------------------------------------------- // Функция WinMain // ----------------------------------------------------- int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WNDCLASSEX wc; HWND hWnd; MSG msg; // Сохраняем идентификатор приложения hInst = hInstance; // Преверяем, не было ли это приложение запущено ранее hWnd = FindWindow(szAppName, NULL); if(hWnd) { // Если было, выдвигаем окно приложения на // передний план if(IsIconic(hWnd)) ShowWindow(hWnd, SW_RESTORE); SetForegroundWindow(hWnd); return FALSE; } // Регистрируем класс окна memset(&wc, 0, sizeof(wc)); wc.cbSize = sizeof(WNDCLASSEX); wc.hIconSm = LoadImage(hInst, MAKEINTRESOURCE(IDI_APPICONSM), IMAGE_ICON, 16, 16, 0); wc.style = 0; wc.lpfnWndProc = (WNDPROC)WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInst; wc.hIcon = LoadImage(hInst, MAKEINTRESOURCE(IDI_APPICON), IMAGE_ICON, 32, 32, 0); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1); wc.lpszMenuName = MAKEINTRESOURCE(IDR_APPMENU); wc.lpszClassName = szAppName; if(!RegisterClassEx(&wc)) if(!RegisterClass((LPWNDCLASS)&wc.style)) return FALSE; // Создаем главное окно приложения hWnd = CreateWindow(szAppName, szAppTitle, WS_POPUPWINDOW | WS_THICKFRAME, 100, 100, 100, 100, NULL, NULL, hInst, NULL); if(!hWnd) return(FALSE); // Размещаем окно в нижней левой части рабочего стола GetWindowRect(GetDesktopWindow(), &rc); MoveWindow(hWnd, rc.right - cxChar * 25, rc.bottom - cyChar * 3, cxChar * 10, cyChar * 2, TRUE); // Отображаем окно и запускаем цикл // обработки сообщений ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); while(GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } // ----------------------------------------------------- // Функция WndProc // ----------------------------------------------------- LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { // Это сообщение посылается приложением STIME case WM_COPYDATA: { // Копируем данные, полученные от приложения STIME, // во внутренний буфер strcpy(szBuf, ((PCOPYDATASTRUCT)lParam)->lpData); // Перерисовываем содержимое окна, отображая в нем // полученную строку символов InvalidateRect(hWnd, NULL, TRUE); break; } HANDLE_MSG(hWnd, WM_CREATE, WndProc_OnCreate); HANDLE_MSG(hWnd, WM_DESTROY, WndProc_OnDestroy); HANDLE_MSG(hWnd, WM_PAINT, WndProc_OnPaint); default: return(DefWindowProc(hWnd, msg, wParam, lParam)); } } // ----------------------------------------------------- // Функция WndProc_OnCreate // ----------------------------------------------------- BOOL WndProc_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct) { HDC hdc; TEXTMETRIC tm; hdc = GetDC(hWnd); // Выбираем в контекст отображения шрифт с фиксированной // шириной букв SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT)); // Определяем и сохраняем метрики шрифта GetTextMetrics(hdc, &tm); cxChar = tm.tmMaxCharWidth; cyChar = tm.tmHeight + tm.tmExternalLeading; ReleaseDC(hWnd, hdc); // Выполняем инициализацию буфера szBuf, содержимое // которого отображается в окне приложения strcpy(szBuf, (LPSTR)"<Unknown>"); return TRUE; } // ----------------------------------------------------- // Функция WndProc_OnDestroy // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnDestroy(HWND hWnd) { PostQuitMessage(0); return 0L; } // ----------------------------------------------------- // Функция WndProc_OnPaint // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnPaint(HWND hWnd) { HDC hdc; PAINTSTRUCT ps; RECT rc; // Перерисовываем внутреннюю область окна hdc = BeginPaint(hWnd, &ps); GetClientRect(hWnd, &rc); // Рисуем в окне строку символов, полученную от // приложения STIME DrawText(hdc, szBuf, -1, &rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER); EndPaint(hWnd, &ps); return 0; }
В глобальном массиве szAppName хранится текстовая строка с названием приложения. Это название будет использовано приложением STIME для поиска главного окна приложения RCLOCK.
В глобальных переменных cxChar и cyChar хранятся метрики шрифта с фиксированной шириной символов, которые будут определены на этапе создания главного окна приложения при обработке сообщения WM_CREATE.
Структура rc типа RECT предназначена для хранения размеров окна рабочего стола.
Буфер szBuf используется для хранения данных, передаваемых из приложения STIME при помощи сообщения WM_COPYDATA.
Функция WinMain приложения RCLOCK сразу после запуска приложения выполняет поиск своей копии, используя для этого функцию FindWindow. Если такая копия найдена, главное окно этой копии выдвигается на передний план функцией SetForegroundWindow, после чего работа функции WinMain завершается. Такая техника уже использовалась нами ранее.
В том случае, когда запускается первая копия приложения RCLOCK, функция WinMain выполняет обычные действия. Она регистрирует класс окна и создает главное окно приложения. Для того чтобы это окно имело вид, показанный на рис. 2.2, для него указываются стили WS_POPUPWINDOW и WS_THICKFRAME:
hWnd = CreateWindow(szAppName, szAppTitle, WS_POPUPWINDOW | WS_THICKFRAME, 100, 100, 100, 100, NULL, NULL, hInst, NULL);
Для определения размеров и расположения главного окна приложения RCLOCK функция WinMain определяет размеры окна рабочего стола, сохраняя их в глобальной переменной rc:
GetWindowRect(GetDesktopWindow(), &rc);
Размещение главного окна приложения RCLOCK выполняется функцией MoveWindow, как это показано ниже:
MoveWindow(hWnd, rc.right - cxChar * 25, rc.bottom - cyChar * 3, cxChar * 10, cyChar * 2, TRUE);
Заметим, что метрики шрифта cxChar и cyChar определяются при обработке сообщения WM_CREATE, который получает управление при вызове функции CreateWindow. Поэтому после возвращения из функции CreateWindow содержимое глобальных переменных cxChar и cyChar будет отражать размеры рабочего стола.
После изменения размеров и расположения главного окна приложения RCLOCK выполняется отображение этого окна и запуск обычного цикла обработки сообщений.
В задачу функции WndProc входит обработка сообщений WM_CREATE, WM_DESTROY, WM_PAINT и WM_COPYDATA. Для обработки первых трех сообщений при помощи макрокоманды HANDLE_MSG вызываются функции WndProc_OnCreate, WndProc_OnDestroy и WndProc_OnPaint, соответственно.
Для сообщения WM_COPYDATA в файле windowsx.h, к сожалению, не предусмотрены специальные макрокоманды. Мы могли бы подготовить такую макрокоманду самостоятельно, однако, так как обработка сообщения WM_COPYDATA очень проста, мы использовали классический способ:
case WM_COPYDATA: { strcpy(szBuf, ((PCOPYDATASTRUCT)lParam)->lpData); InvalidateRect(hWnd, NULL, TRUE); break; }
Напомним, что вместе с параметром lParam сообщения WM_COPYDATA передается указатель на структуру COPYDATASTRUCT. Приложение, посылающее другому приложению сообщение WM_COPYDATA, подготавливает область данных, записывая ее адрес в поле lpData структуры типа COPYDATASTRUCT. Принимающее приложение должно скопировать эти данные в свой внутренний буфер.
В нашем случае в качестве данных передается строка символов, закрытая двоичным нулем, поэтому для копирования мы используем функцию strcpy.
После выполнения копирования обработчик сообщения WM_COPYDATA вызывает функцию InvalidateRect, что в результате приводит к перерисовке главного окна приложения. В этом окне обработчик сообщения WM_PAINT нарисует текстовую строку, полученную от другого приложения и скопированную только что в буфер szBuf.
Функция WndProc_OnCreate вызывается при создании главного окна приложения. Эта функция получает контекст отображения, выбирает в него шрифт с фиксированной шириной букв и определяет его метрики. Ширина и высота символов сохраняются, соответственно, в глобальных переменных cxChar и cyChar. Эти значения используются для вычисления размеров главного окна приложения.
В заверешении функция WndProc_OnCreate записывает в глобальный буфер szBuf строку <Unknown>:
strcpy(szBuf, (LPSTR)"<Unknown>");
Эта строка будет отображаться в главном окне приложения до тех пор, пока вы не запустите приложение STIME.
Эта функция завершает цикл обработки сообщений, вызывая для этого функцию PostQuitMessage.
При обработке сообщения WM_PAINT функция WndProc_OnPaint отображает в главном окне приложения RCLOCK текстовую строку, записанную в буфере szBuf. Для рисования строки используется функция DrawText, так как с ее помощью легко выполнить центровку строки в окне по вертикали и горизонтали.
В файле rclock.h (листинг 2.4) находятся прототипы функций, определенных в приложении RCLOCK.
Листинг 2.4. Файл rclock/rclock.h
// ----------------------------------------------------- // Описание функций // ----------------------------------------------------- LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); void WndProc_OnDestroy(HWND hWnd); BOOL WndProc_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct); void WndProc_OnPaint(HWND hWnd);
Файл resource.h (листинг 2.5) создается автоматически и содержит определения для файла описания ресурсов приложения RCLOCK.
Листинг 2.5. Файл rclock/resource.h
//{{NO_DEPENDENCIES}} // Microsoft Developer Studio generated include file. // Used by RCLOCK.RC // #define IDR_APPMENU 102 #define IDI_APPICON 103 #define IDI_APPICONSM 104 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 106 #define _APS_NEXT_COMMAND_VALUE 40006 #define _APS_NEXT_CONTROL_VALUE 1010 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif
В файле rclock.rc (листинг 2.6) определены ресурсы приложения RCLOCK.
Листинг 2.6. Файл rclock/rclock.rc
//Microsoft Developer Studio generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "afxres.h" ////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ////////////////////////////////////////////////////////////// // English (U.S.) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #endif //_WIN32 #ifdef APSTUDIO_INVOKED ////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE DISCARDABLE BEGIN "resource.h\0" END 2 TEXTINCLUDE DISCARDABLE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE DISCARDABLE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure // application icon // remains consistent on all systems. IDI_APPICON ICON DISCARDABLE "RCLOCK.ICO" IDI_APPICONSM ICON DISCARDABLE "RCLOCKSM.ICO" ////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO DISCARDABLE BEGIN IDD_DIALOG1, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 162 TOPMARGIN, 7 BOTTOMMARGIN, 52 END END #endif // APSTUDIO_INVOKED #endif // English (U.S.) resources ////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED
Приложение STIME работает в паре с приложением RCLOCK, периодически раз в секунду посылая последнему текстовые строки с помощью сообщения WM_COPYDATA. Главное окно приложения STIME не имеет никаких особенностей, поэтому мы его не приводим.
Главный файл исходных текстов приложения STIME представлен в листинге 2.7.
Листинг 2.7. Файл rclock/stime/stime.c
// ================================================== // Приложение STIME (работает вместе с приложением RTIME) // Демонстрация использования сообщения WM_COPYDATA // для передачи данных между процессами // // (С) Фролов А.В., 1996 // Email: frolov@glas.apc.org // ================================================== #define STRICT #include <windows.h> #include <windowsx.h> #include <commctrl.h> #include <time.h> #include "resource.h" #include "afxres.h" #include "stime.h" HINSTANCE hInst; char szAppName[] = "StimeApp"; char szAppTitle[] = "Time Sender"; // Имя приложения RTIME char szServerAppName[] = "RclockApp"; // Идентификатор главного окна приложения RTIME HWND hWndServer; // Структура для передачи данных между процессами // при помощи сообщения WM_COPYDATA COPYDATASTRUCT cd; // Буферы для передаваемых данных char szBuf[80]; char szTerminated[] = "<Terminated<"; // ----------------------------------------------------- // Функция WinMain // ----------------------------------------------------- int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WNDCLASSEX wc; HWND hWnd; MSG msg; // Сохраняем идентификатор приложения hInst = hInstance; // Преверяем, не было ли это приложение запущено ранее hWnd = FindWindow(szAppName, NULL); if(hWnd) { // Если было, выдвигаем окно приложения на // передний план if(IsIconic(hWnd)) ShowWindow(hWnd, SW_RESTORE); SetForegroundWindow(hWnd); return FALSE; } // Ищем окно серверного приложения RCLOCK и сохраняем // его идентификатор hWndServer = FindWindow(szServerAppName, NULL); if(hWndServer == NULL) { // Если окно серверного приложения не найдено, // выводим сообщение об ошибке и завершаем работу // приложения MessageBox(NULL, "Server RCLOCK not found", szAppName, MB_ICONEXCLAMATION | MB_OK); return FALSE; } // Регистрируем класс окна memset(&wc, 0, sizeof(wc)); wc.cbSize = sizeof(WNDCLASSEX); wc.hIconSm = LoadImage(hInst, MAKEINTRESOURCE(IDI_APPICONSM), IMAGE_ICON, 16, 16, 0); wc.style = 0; wc.lpfnWndProc = (WNDPROC)WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInst; wc.hIcon = LoadImage(hInst, MAKEINTRESOURCE(IDI_APPICON), IMAGE_ICON, 32, 32, 0); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1); wc.lpszMenuName = MAKEINTRESOURCE(IDR_APPMENU); wc.lpszClassName = szAppName; if(!RegisterClassEx(&wc)) if(!RegisterClass((LPWNDCLASS)&wc.style)) return FALSE; // Создаем главное окно приложения hWnd = CreateWindow(szAppName, szAppTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInst, NULL); if(!hWnd) return(FALSE); // Отображаем окно и запускаем цикл // обработки сообщений ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); while(GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } // ----------------------------------------------------- // Функция WndProc // ----------------------------------------------------- LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch(msg) { HANDLE_MSG(hWnd, WM_TIMER, WndProc_OnTimer); HANDLE_MSG(hWnd, WM_CREATE, WndProc_OnCreate); HANDLE_MSG(hWnd, WM_DESTROY, WndProc_OnDestroy); default: return(DefWindowProc(hWnd, msg, wParam, lParam)); } } // ----------------------------------------------------- // Функция WndProc_OnCreate // ----------------------------------------------------- BOOL WndProc_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct) { // Создаем таймер с периодом работы 1 сек SetTimer(hWnd, CLOCK_TIMER, 1000, NULL); return TRUE; } // ----------------------------------------------------- // Функция WndProc_OnTimer // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnTimer(HWND hWnd, UINT id) { time_t t; struct tm *ltime; // Определяем текущее время time(&t); ltime = localtime(&t); // Формируем текстовую строку времени wsprintf(szBuf, "%02d:%02d:%02d", ltime->tm_hour, ltime->tm_min,ltime->tm_sec); // Записываем адрес и размер строки в структуру // типа COPYDATASTRUCT cd.lpData = szBuf; cd.cbData = strlen(szBuf) + 1; // Посылаем сообщение серверному приложению RCLOCK SendMessage(hWndServer, WM_COPYDATA, (WPARAM)hWnd, (LPARAM)&cd); return 0; } // ----------------------------------------------------- // Функция WndProc_OnDestroy // ----------------------------------------------------- #pragma warning(disable: 4098) void WndProc_OnDestroy(HWND hWnd) { // Перед завершением работы приложения передаем // серверу строку <Terminated> cd.lpData = szTerminated; cd.cbData = strlen(szTerminated) + 1; SendMessage(hWndServer, WM_COPYDATA, (WPARAM)hWnd, (LPARAM)&cd); // Удаляем таймер KillTimer(hWnd, CLOCK_TIMER); PostQuitMessage(0); return 0L; }
Помимо всего прочего в области глобальных переменных определен массив szServerAppName, в котором хранится имя приложения RCLOCK. Это имя будет использовано в функции WinMain для поиска главного окна серверного приложения.
Идентификатор главного окна найденного приложения RCLOCK хранится в глобальной переменной hWndServer. Этот идентификатор используется для посылки сообщения WM_COPYDATA функцией SendMessage.
В области глобальных переменных определена структура cd типа COPYDATASTRUCT, которая совместно с глобальным буфером szBuf используется для передачи текстовой строки в приложение RCLOCK.
Кроме того, определен буфер szTerminated, в котором находится строка символов Terminated. Эта строка передается в приложение RCLOCK перед завершением работы приложения STIME.
После поиска своей собственной копии приложение STIME ищет окно серверного приложения RCLOCK:
hWndServer = FindWindow(szServerAppName, NULL);
Если это приложение не найдено, выдается сообщение об ошибке, вслед за чем работа приложения STIME завершается.
В случае успешного поиска идентификатор найденного окна приложения RCLOCK записывается в глобальную переменную hWndServer. Вслед за этим выполняется процедура регистрации класса главного окна приложения STIME, создается главное окно приложения и запускается обычный цикл обработки сообщений.
Функция WndProc обрабатывает сообщения WM_CREATE, WM_DESTROY и WM_TIMER, вызывая для этого функции WndProc_OnCreate, WndProc_OnDestroy и WndProc_OnTimer, соответственно.
При создании главного окна приложения STIME обработчик сообщения WM_CREATE создает таймер с периодом работы 1 сек, вызывая для этого функцию SetTimer:
SetTimer(hWnd, CLOCK_TIMER, 1000, NULL);
Созданный таймер будет иметь идентификатор CLOCK_TIMER.
Функция WndProc_OnTimer получает управление примерно один раз в секунду, обрабатывая сообщения WM_TIMER, поступающие от таймера с идентификатором CLOCK_TIMER.
Прежде всего эта функция формирует строку символов с текущим временем в формате “ЧЧ:ММ:СС”, вызывая библиотечные фукнции системы разработки time и localtime:
time(&t); ltime = localtime(&t); wsprintf(szBuf, "%02d:%02d:%02d", ltime->tm_hour, ltime->tm_min,ltime->tm_sec);
Затем выполняется инициализация полей структуры cd типа COPYDATASTRUCT. В процессе инициализации мы записываем адрес буфера, содержащего строку символов, в поле lpData, а размер этого буфера (с учетом двоичного нуля, закрывающего строку) - в поле cbData:
cd.lpData = szBuf; cd.cbData = strlen(szBuf) + 1;
Поле dwData не используется.
Заметим, что хотя серверное приложение RCLOCK при копировании полученных данных не использует поле cbData (так как мы передаем строку символов, закрытую двоичным нулем), при подготовке сообщения WM_COPYDATA к отправке необходимо заполнить оба поля: и lpData, и cbData.
Посылка сообщения WM_COPYDATA выполняется очень просто:
SendMessage(hWndServer, WM_COPYDATA, (WPARAM)hWnd, (LPARAM)&cd);
Сообщение посылается в окно с идентификатором hWndServer, который был определен в функции WinMain. В качестве параметра wParam вместе с этим сообщением необходимо передать идентификатор окна посылающего приложения, то есть идентификатор окна приложения STIME. Через параметр lParam передается адрес заполненной структуры cd типа COPYDATASTRUCT.
Перед завершением своей работы приложение STIME посылает приложению RCLOCK строку <Terminated>, которая будет отображена в окне приложения RCLOCK. Для посылки используется только что описанная нами методика:
cd.lpData = szTerminated; cd.cbData = strlen(szTerminated) + 1; SendMessage(hWndServer, WM_COPYDATA, (WPARAM)hWnd, (LPARAM)&cd);
Далее функция WndProc_OnDestroy удаляет таймер и завершает цикл обработки сообщений, вызывая для этого функцию PostQuitMessage.
В файле stime.h (листинг 2.8) определен идентификатор таймера CLOCK_TIMER, а также прототипы функций.
Листинг 2.8. Файл rclock/stime/stime.h
#define CLOCK_TIMER 100 LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); void WndProc_OnDestroy(HWND hWnd); BOOL WndProc_OnCreate(HWND hWnd, LPCREATESTRUCT lpCreateStruct); void WndProc_OnPaint(HWND hWnd); void WndProc_OnTimer(HWND hWnd, UINT id);
Файл resource.h (листинг 2.9) создается автоматически и содержит определения для файла описания ресурсов приложения STIME.
Листинг 2.9. Файл rclock/stime/resource.h
//{{NO_DEPENDENCIES}} // Microsoft Developer Studio generated include file. // Used by STIME.RC // #define IDR_APPMENU 102 #define IDI_APPICON 103 #define IDI_APPICONSM 104 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 106 #define _APS_NEXT_COMMAND_VALUE 40006 #define _APS_NEXT_CONTROL_VALUE 1010 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif
В файле stime.rc (листинг 2.10) определены ресурсы приложения STIME.
Листинг 2.10. Файл rclock/stime/stime.rc
//Microsoft Developer Studio generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "afxres.h" ////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ////////////////////////////////////////////////////////////// // English (U.S.) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) #ifdef _WIN32 LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US #pragma code_page(1252) #endif //_WIN32 #ifdef APSTUDIO_INVOKED ////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE DISCARDABLE BEGIN "resource.h\0" END 2 TEXTINCLUDE DISCARDABLE BEGIN "#include ""afxres.h""\r\n" "\0" END 3 TEXTINCLUDE DISCARDABLE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure // application icon // remains consistent on all systems. IDI_APPICON ICON DISCARDABLE "STIME.ICO" IDI_APPICONSM ICON DISCARDABLE "STIMESM.ICO" ////////////////////////////////////////////////////////////// // // DESIGNINFO // #ifdef APSTUDIO_INVOKED GUIDELINES DESIGNINFO DISCARDABLE BEGIN IDD_DIALOG1, DIALOG BEGIN LEFTMARGIN, 7 RIGHTMARGIN, 162 TOPMARGIN, 7 BOTTOMMARGIN, 52 END END #endif // APSTUDIO_INVOKED #endif // English (U.S.) resources ////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED
В среде операционной системы Microsoft Windows NT вам доступно такое удобное средство передачи данных между параллельно работающими процессами, как каналы типа Pipe. Это средство позволяет организовать передачу данных между локальными процессами, а также между процессами, запущенными на различных рабочих станциях в сети.
Каналы типа Pipe больше всего похожи на файлы, поэтому они достаточно просты в использовании.
Через канал можно передавать данные только между двумя процессами. Один из процессов создает канал, другой открывает его. После этого оба процесса могут передавать данные через канал в одну или обе стороны, используя для этого хорошо знакомые вам функции, предназначенные для работы с файлами, такие как ReadFile и WriteFile. Заметим, что приложения могут выполнять над каналами Pipe синхронные или асинхронные операции, аналогично тому, как это можно делать с файлами. В случае использования асинхронных операций необходимо отдельно побеспокоиться об организации синхронизации.
Существуют две разновидности каналов Pipe - именованные (Named Pipes) и анонимные (Anonymous Pipes).
Как видно из названия, именованным каналам при создании присваивается имя, которое доступно для других процессов. Зная имя какой-либо рабочей станции в сети, процесс может получить доступ к каналу, созданному на этой рабочей станции.
Анонимные каналы обычно используются для организации передачи данных между родительскими и дочерними процессами, запущенными на одной рабочей станции или на “отдельно стоящем” компьютере.
Имена каналов в общем случае имеют следующий вид:
\\ИмяСервера\pipe\ИмяКанала
Если процесс открывает канал, созданный на другой рабочей станции, он должен указать имя сервера. Если же процесс создает канал или открывает канал на своей рабочей станции, вместо имени указывается символ точки:
\\.\pipe\ИмяКанала
В любом случае процесс может создать канал только на той рабочей станции, где он запущен, поэтому при создании канала имя сервера никогда не указывается.
В простейшем случае один серверный процесс создает один канал (точнее говоря, одну реализацию канала) для работы с одним клиентским процессом.
Однако часто требуется организовать взаимодействие одного серверного процесса с несколькими клиентскими. Например, сервер базы данных может принимать от клиентов запросы и рассылать ответы на них.
В случае такой необходимости серверный процесс может создать несколько реализаций канала, по одной реализации для каждого клиентского процесса.
В этом разделе мы опишем наиболее важные функции программного интерфейса Microsoft Windows NT, предназначенные для работы с каналами Pipes. Более подробную информацию вы найдете в документации, которая поставляется в составе библиотеки разработчика Microsoft Development Library (MSDN).
Для создания именованных и анонимных каналов Pipes используются функции CreatePipe и CreateNamedPipe.
Анонимный канал создается функцией CreatePipe, имеющей следующий прототип:
BOOL CreatePipe( PHANDLE hReadPipe, // адрес переменной, в которую будет // записан идентификатор канала для // чтения данных PHANDLE hWritePipe, // адрес переменной, в которую будет // записан идентификатор канала для // записи данных LPSECURITY_ATTRIBUTES lpPipeAttributes, // адрес переменной // для атрибутов защиты DWORD nSize); // количество байт памяти, // зарезервированной для канала
Канал может использоваться как для записи в него данных, так и для чтения. Поэтому при создании канала функция CreatePipe возвращает два идентификатора, записывая их по адресу, заданному в параметрах hReadPipe и hWritePipe.
Идентификатор, записанный по адресу hReadPipe, можно передавать в качестве параметра функции ReadFile или ReadFileEx для выполнения операции чтения. Идентификатор, записанный по адресу hWritePipe, передается функции WriteFile или WriteFileEx для выполнения операции записи.
Через параметр lpPipeAttributes передается адрес переменной, содержащей атрибуты защиты для создаваемого канала. В наших приложениях мы будем указывать этот параметр как NULL. В результате канал будет иметь атрибуты защиты, принятые по умолчанию.
И, наконец, параметр nSize определяет размер буфера для создаваемого канала. Если этот размер указан как нуль, будет создан буфер с размером, принятым по умолчанию. Заметим, что при необходимости система может изменить указанный вами размер буфера.
В случае успеха функция CreatePipe возвращает значение TRUE, при ошибке - FALSE. В последнем случае для уточнения причины возникновения ошибки вы можете воспользоваться функцией GetLastError.
Для создания именованного канала Pipe вы должны использовать функцию CreateNamedPipe. Вот прототип этой функции:
HANDLE CreateNamedPipe( LPCTSTR lpName, // адрес строки имени канала DWORD dwOpenMode, // режим открытия канала DWORD dwPipeMode, // режим работы канала DWORD nMaxInstances, // максимальное количество // реализаций канала DWORD nOutBufferSize, // размер выходного буфера в байтах DWORD nInBufferSize, // размер входного буфера в байтах DWORD nDefaultTimeOut, // время ожидания в миллисекундах LPSECURITY_ATTRIBUTES lpSecurityAttributes); // адрес // переменной для атрибутов защиты
Через параметр lpName передается адрес строки имени канала в форме \\.\pipe\ИмяКанала (напомним, что при создании канала имя сервера не указывается, так как канал можно создать только на той рабочей станции, где запущен процесс, создающий канал).
Параметр dwOpenMode задает режим, в котором открывается канал. Остановимся на этом параметре подробнее.
Канал Pipe может быть ориентирован либо на передачу потока байт, либо на передачу сообщений. В первом случае данные через канал передаются по байтам, во втором - отдельными блоками заданной длины.
Режим работы канала (ориентированный на передачу байт или сообщений) задается, соответственно, константами PIPE_TYPE_BYTE или PIPE_TYPE_MESSAGE, которые указываются в параметре dwOpenMode. По умолчанию используется режим PIPE_TYPE_BYTE.
Помимо способа передачи данных через канал, с помощью параметра dwOpenMode можно указать, будет ли данный канал использован только для чтения данных, только для записи или одновременно для чтения и записи. Способ использования канала задается указанием одной из следующих констант:
Константа |
Использование канала |
PIPE_ACCESS_INBOUND |
Только для чтения |
PIPE_ACCESS_OUTBOUND |
Только для записи |
PIPE_ACCESS_DUPLEX |
Для чтения и записи |
Перечисленные выше параметры должны быть одинаковы для всех реализаций канала (о реализациях канала мы говорили выше). Далее мы перечислим параметры, которые могут отличаться для разных реализаций канала:
Константа |
Использование канала |
PIPE_READMODE_BYTE |
Канал открывается на чтение в режиме последовательной передачи отдельных байт |
PIPE_READMODE_MESSAGE |
Канал открывается на чтение в режиме передачи отдельных сообщений указанной длины |
PIPE_WAIT |
Канал будет работать в блокирующем режиме, когда процесс переводится в состояние ожидания до завершения операций в канале |
PIPE_NOWAIT |
Неблокирующий режим работы канала. Если операция не может быть выполнена немедленно, в неблокирующем режиме функция завершается с ошибкой |
FILE_FLAG_OVERLAPPED |
Использование асинхронных операций (ввод и вывод с перекрытием). Данный режим позволяет процессу выполнять полезную работу параллельно с проведением операций в канале |
FILE_FLAG_WRITE_THROUGH |
В этом режиме функции, работающие с каналом, не возвращают управление до тех пор, пока не будет полностью завершена операция на удаленном компьютере. Используется только с каналом, ориентированном на передачу отдельных байт и только в том случае, когда канал создан между процессами, запущенными на различных станциях сети |
Дополнительно к перечисленным выше флагам, через параметр dwOpenMode можно передавать следующие флаги защиты:
Флаг |
Описание |
WRITE_DAC |
Вызывающий процесс должен иметь права доступа на запись к произвольному управляющему списку доступа именованного канала access control list (ACL) |
WRITE_OWNER |
Вызывающий процесс должен иметь права доступа на запись к процессу, владеющему именованным каналом Pipe |
ACCESS_SYSTEM_SECURITY |
Вызывающий процесс должен иметь права доступа на запись к управляющему списку доступа именованного канала access control list (ACL) |
Подробное описание этих флагов выходит за рамки нашей книги. При необходимости обратитесь к документации, входящей в состав SDK.
Теперь перейдем к параметру dwPipeMode, определяющему режим работы канала. В этом параметре вы можете указать перечисленные выше константы PIPE_TYPE_BYTE, PIPE_TYPE_MESSAGE, PIPE_READMODE_BYTE, PIPE_READMODE_MESSAGE, PIPE_WAIT и PIPE_NOWAIT. Для всех реализаций канала необходимо указывать один и тот же набор констант.
Параметр nMaxInstances определяет максимальное количество реализаций, которые могут быть созданы для канала. Вы можете указывать здесь значения от 1 до PIPE_UNLIMITED_INSTANCES. В последнем случае максимальное количество реализаций ограничивается только наличием свободных системных ресурсов.
Заметим, что если один серверный процесс использует несколько реализаций канала для связи с несколькими клиенсткими, то общее количество реализаций может быть меньше, чем потенциальное максимальное количество клиентов. Это связано с тем, что клиенты могут использовать реализации по очереди, если только они не пожелают связаться с серверным процессом все одновременно.
Параметры nOutBufferSize и nInBufferSize определяют, соответственно, размер буферов, используемых для записи в канал и чтения из канала. При необходимости система может использовать буферы других, по сравнению с указанными, размеров.
Параметр nDefaultTimeOut определяет время ожидания для реализации канала. Для всех реализаций необходимо указывать одинаковое значение этого параметра.
Через параметр lpPipeAttributes передается адрес переменной, содержащей атрибуты защиты для создаваемого канала. В наших приложениях мы будем указывать этот параметр как NULL. В результате канал будет иметь атрибуты защиты, принятые по умолчанию.
В случае успеха функция CreateNamedPipe возвращает идентификатор созданной реализации канала, который можно использовать в операциях чтения и записи, выполняемых с помощью таких функций, как ReadFile и WriteFile.
При ошибке функция CreateNamedPipe возвращает значение INVALID_HANDLE_VALUE. Код ошибки вы можете уточнить, вызвав функцию GetLastError.
Функция CreateFile, предназначенная для работы с файлами, может также быть использована для создания новых каналов и открытия существующих. При этом вместо имени файла вы должны указать этой функции имя канала Pipe.
Приведем пример использования функции CreateNamedPipe для создания именованного канала Pipe с именем $MyPipe$, предназначенным для чтения и записи данных, работающем в блокирующем режиме и допускающем создание неограниченного количества реализаций:
HANDLE hNamedPipe; LPSTR lpszPipeName = "\\\\.\\pipe\\$MyPipe$"; hNamedPipe = CreateNamedPipe( lpszPipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 512, 512, 5000, NULL);
Через создаваемый канал передаются сообщения (так как указана константа PIPE_TYPE_MESSAGE). Данная реализация предназначена только для чтения (константа PIPE_READMODE_MESSAGE).
При создании канала мы указали размер буферов ввода и вывода, равный 512 байт. Время ожидания операций выбрано равным 5 секунд. Атрибуты защиты не указаны, поэтому используются значения, принятые по умолчанию.
После того как серверный процесс создал канал, он может перейти в режим соединения с клиентским процессом. Соединение со стороны сервера выполняется с помощью функции ConnectNamedPipe.
Прототип функции ConnectNamedPipe представлен ниже:
BOOL ConnectNamedPipe( HANDLE hNamedPipe, // идентификатор именованного канала LPOVERLAPPED lpOverlapped); // адрес структуры OVERLAPPED
Через первый параметр серверный процесс передает этой функции идентификатор канала, полученный от функции CreateNamedPipe.
Второй параметр используется только для огранизации асинхронного обмена данными через канал. Если вы используете только синхронные операции, в качестве значения для этого параметра можно указать NULL.
В случае успеха функция ConnectNamedPipe возвращает значение TRUE, а при ошибке - FALSE. Код ошибки можно получить с помощью функции GelLastError.
В зависимости от различных условий функция ConnectNamedPipe может вести себя по разному.
Если параметр lpOverlapped указан как NULL, функция выполняется в синхронном режиме. В противном случае используется асинхронный режим.
Для канала, созданного в синхронном блокирующем режиме (с использованием константы PIPE_WAIT), функция ConnectNamedPipe переходит в состояние ожидания соединения с клиентским процессом. Именно этот режим мы будем использовать в наших примерах программ, исходные тексты которых вы найдете ниже.
Если канал создан в синхронном неблокирующем режиме, функция ConnectNamedPipe немедленно возвращает управление с кодом TRUE, если только клиент был отключен от данной реализации канала и возможно подключение этого клиента. В противном случае возвращается значение FALSE. Дальнейший анализ необходимо выполнять с помощью функции GetLastError XE "GetLastError" . Эта функция может вернуть значение ERROR_PIPE_LISTENING (если к серверу еще не подключен ни один клиент), ERROR_PIPE_CONNECTED XE "ERROR_PIPE_CONNECTED" (если клиент уже подключен) или ERROR_NO_DATA (если предыдущий клиент отключился от сервера, но клиент еще не завершил соединение).
Ниже мы привели пример использования функции ConnectNamedPipe:
HANDLE hNamedPipe; LPSTR lpszPipeName = "\\\\.\\pipe\\$MyPipe$"; hNamedPipe = CreateNamedPipe( lpszPipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 512, 512, 5000, NULL); fConnected = ConnectNamedPipe(hNamedPipe, NULL);
В данном случае функция ConnectNamedPipe перейдет в состояние ожидания, так как канал был создан для работы в синхронном блокирующем режиме.
Для создания канала клиентский процесс может воспользоваться функцией CreateFile. Как вы помните, эта функция предназначена для работы с файлами, однако с ее помощью можно также открыть канал, указав его имя вместо имени файла. Забегая вперед, скажем, что функция CreateFile позволяет открывать не только файлы или каналы Pipe, но и другие системные ресурсы, например, устройства и каналы Mailslot.
Функция CreateFile была нами описана в предыдущем томе “Библиотеки системного программиста”, однако для удобства мы повторим прототип этой функции и ее краткое описание:
Итак, прототип функции CreateFile:
HANDLE CreateFile( LPCTSTR lpFileName, // адрес строки имени файла DWORD dwDesiredAccess, // режим доступа DWORD dwShareMode, // режим совместного использования файла LPSECURITY_ATTRIBUTES lpSecurityAttributes, // дескриптор // защиты DWORD dwCreationDistribution, // параметры создания DWORD dwFlagsAndAttributes, // атрибуты файла HANDLE hTemplateFile); // идентификатор файла с атрибутами
Раньше при работе с файлами через параметр lpFileName вы передавали этой функции адрес строки, содержащей имя файла, который вы собираетесь создать или открыть. Строка должна быть закрыта двоичным нулем. Если функция CreateFile работает с каналом Pipe, параметр lpFileName определяет имя канала.
Параметр dwDesiredAccess определяет тип доступа, который должен быть предоставлен к открываемому файлу. В нашем случае этот тип доступа будет относиться к каналу Pipe. Здесь вы можете использовать логическую комбинацию следующих констант:
Константа |
Описание |
0 |
Доступ запрещен, однако приложение может определять атрибуты файла, канала или устройства, открываемого при помощи функции CreateFile XE "CreateFile" |
GENERIC_READ |
Разрешен доступ на чтение из файла или канала Pipe |
GENERIC_WRITE |
Разрешен доступ на запись в файл или канал Pipe |
Тип доступа, указанный при помощи параметра dwDesiredAccess, не должен противоречить типу доступа для канала, заданного при его создании функцией CreateNamedPipe.
С помощью параметра dwShareMode задаются режимы совместного использования открываемого или создаваемого файла. Для этого параметра вы можете указать комбинацию следующих констант:
Константа |
Описание |
0 |
Совместное использование файла запрещено |
FILE_SHARE_READ |
Другие приложения могут открывать файл с помощью функции CreateFile для чтения |
FILE_SHARE_WRITE |
Аналогично предыдущему, но на запись |
Через параметр lpSecurityAttributes необходимо передать указатель на дескриптор защиты или значение NULL, если этот дескриптор не используется. В наших приложениях мы не работаем с дескриптором защиты.
Параметр dwCreationDistribution определяет действия, выполняемые функцией CreateFile, если приложение пытается создать файл, который уже существует. Для этого параметра вы можете указать одну из следующих констант:
Константа |
Описание |
CREATE_NEW |
Если создаваемый файл уже существует, функция CreateFile возвращает код ошибки |
CREATE_ALWAYS |
Существующий файл перезаписывается, при этом содержимое старого файла теряется |
OPEN_EXISTING |
Открывается существующий файл. Если файл с указанным именем не существует, функция CreateFile возвращает код ошибки |
OPEN_ALWAYS |
Если указанный файл существует, он открывается. Если файл не существует, он будет создан |
TRUNCATE_EXISTING |
Если файл существует, он открывается, после чего длина файла устанавливается равной нулю. Содержимое старого файла теряется. Если же файл не существует, функция CreateFile возвращает код ошибки |
Параметр dwFlagsAndAttributes задает атрибуты и флаги для файла.
При этом можно использовать любые логические комбинации следующих атрибутов (кроме атрибута FILE_ATTRIBUTE_NORMAL, который можно использовать только отдельно):
Атрибут |
Описание |
FILE_ATTRIBUTE_ARCHIVE |
Файл был архивирован (выгружен) |
FILE_ATTRIBUTE_COMPRESSED |
Файл, имеющий этот атрибут, динамически сжимается при записи и восстанавливается при чтении. Если этот атрибут имеет каталог, то для всех расположенных в нем файлов и каталогов также выполняется динамическое сжатие данных |
FILE_ATTRIBUTE_NORMAL |
Остальные перечисленные в этом списка атрибуты не установлены |
FILE_ATTRIBUTE_HIDDEN |
Скрытый файл |
FILE_ATTRIBUTE_READONLY |
Файл можно только читать |
FILE_ATTRIBUTE_SYSTEM |
Файл является частью операционной системы |
В дополнение к перечисленным выше атрибутам, через параметр dwFlagsAndAttributes вы можете передать любую логическую комбинацию флагов, перечисленных ниже:
Флаг |
Описание |
FILE_FLAG_WRITE_THROUGH |
Отмена промежуточного кэширования данных для уменьшения вероятности потери данных при аварии |
FILE_FLAG_NO_BUFFERING |
Отмена промежуточной буферизации или кэширования. При использовании этого флага необходимо выполнять чтение и запись порциями, кратными размеру сектора (обычно 512 байт) |
FILE_FLAG_OVERLAPPED |
Асинхронное выполнение чтения и записи. Во время асинхронного чтения или записи приложение может продолжать обработку данных |
FILE_FLAG_RANDOM_ACCESS |
Указывает, что к файлу будет выполняться произвольный доступ. Флаг предназначен для оптимизации кэширования |
FILE_FLAG_SEQUENTIAL_SCAN |
Указывает, что к файлу будет выполняться последовательный доступ от начала файла к его концу. Флаг предназначен для оптимизации кэширования |
FILE_FLAG_DELETE_ON_CLOSE |
Файл будет удален сразу после того как приложение закроет его идентификтор. Этот флаг удобно использовать для временных файлов |
FILE_FLAG_BACKUP_SEMANTICS |
Файл будет использован для выполнения операции выгрузки или восстановления. При этом выполняется проверка прав доступа |
FILE_FLAG_POSIX_SEMANTICS |
Доступ к файлу будет выполняться в соответствии со спецификацией POSIX |
И, наконец, последний параметр hTemplateFile предназначен для доступа к файлу шаблона с расширенными атрибутами создаваемого файла.
В случае успешного завершения функция CreateFile возвращает идентификатор созданного или открытого файла (или каталога), а при работе с каналом Pipe - идентификатор реализации канала.
При ошибке возвращается значение INVALID_HANDLE_VALUE (а не NULL, как можно было бы предположить). Код ошибки можно определить при помощи функции GetLastError.
В том случае, если файл уже существует и были указаны константы CREATE_ALWAYS или OPEN_ALWAYS, функция CreateFile не возвращает код ошибки. В то же время в этой ситуации функция GetLastError возвращает значение ERROR_ALREADY_EXISTS.
Приведем фрагмент исходного текста клиентского приложения, открывающего канал с именем $MyPipe$ при помощи функции CreateFile:
char szPipeName[256]; HANDLE hNamedPipe; strcpy(szPipeName, "\\\\.\\pipe\\$MyPipe$"); hNamedPipe = CreateFile( szPipeName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
Здесь канал открывается как для записи, так и для чтения.
Если сервер работает с несколькими клиентскими процессами, то он может использовать для этого несколько реализаций канала, причем одни и те же реализации могут применяться по очереди.
Установив канал с клиентским процессом при помощи функции ConnectNamedPipe, серверный процесс может затем разорвать канал, вызвав для этого функцию DisconnectNamedPipe. После этого реализация канала может быть вновь использована для соединения с другим клиентским процессом.
Прототип функции DisconnectNamedPipe мы привели ниже:
BOOL DisconnectNamedPipe(HANDLE hNamedPipe);
Через параметр hNamedPipe функции передается идентификатор реализации канала Pipe, полученный от функции CreateNamedPipe.
В случае успеха функция возвращает значение TRUE, а при ошибке - FALSE. Код ошибки можно получить от функции GetLastError.
Если канал больше не нужен, после отключения от клиентского процесса серверный и клиентский процессы должны закрыть его идентификатор функцией CloseHandle:
CloseHandle(hNamedPipe);
Запись данных в открытый канал выполняется с помощью функции WriteFile, аналогично записи в обычный файл:
HANDLE hNamedPipe; DWORD cbWritten; char szBuf[256]; WriteFile(hNamedPipe, szBuf, strlen(szBuf) + 1, &cbWritten, NULL);
Через первый параметр функции WriteFile передается идентификатор реализации канала. Через второй параметр передается адрес буфера, данные из которого будут записаны в канал. Размер этого буфера указывается при помощи третьего параметра. Предпоследний параметр используется для определения количества байт данных, действительно записанных в канал. И, наконец, последний параметр задан как NULL, поэтому запись будет выполняться в синхронном режиме.
Учтите, что если канал был создан для работы в блокирующем режиме, и функция WriteFile работает синхронно (без использования вывода с перекрытием), то эта функция не вернет управление до тех пор, пока данные не будут записаны в канал.
Как и следовало ожидать, для чтения данных из канала можно воспользоваться функцией ReadFile, например, так:
HANDLE hNamedPipe; DWORD cbRead; char szBuf[256]; ReadFile(hNamedPipe, szBuf, 512, &cbRead, NULL);
Данные, прочитанные из канала hNamedPipe, будут записаны в буфер szBuf, имеющий размер 512 байт. Количество действительно прочитанных байт данных будет сохранено функцией ReadFile в переменной cbRead. Так как последний параметр функции указан как NULL, используется синхронный режим работы без перекрытия.
Среди других функций, предназначенных для работы с каналами Pipe, мы рассмотрим функции CallNamedPipe, TransactNamedPipe, PeekNamedPipe, WaitNamedPipe, SetNamedPipeHandleState, GetNamedPipeInfo, GetNamedPipeHandleState.
Обычно сценарий взаимодействия клиентского процесса с серверным заключается в выполнении следующих операций:
· подключение к каналу с помощью функции CreateFile;
· выполнение операций чтения или записи такими функциями как ReadFile или WriteFile;
· отключение от канала функцией CloseHandle.
Функция CallNamedPipe позволяет выполнить эти операции за один прием, при условии что канал открыт в режиме передачи сообщений и что клиент посылает одно сообщение серверу и в ответ также получает от сервера одно сообщение.
Ниже мы привели прототип функции CallNamedPipe:
BOOL CallNamedPipe( LPCTSTR lpNamedPipeName, // адрес имени канала LPVOID lpOutBuffer, // адрес буфера для записи DWORD nOutBufferSize, // размер буфера для записи LPVOID lpInBuffer, // адрес буфера для чтения DWORD nInBufferSize, // размер буфера для чтения LPDWORD lpBytesRead, // адрес переменной для записи // количества прочитанных байт данных DWORD nTimeOut); // время ожидания в миллисекундах
Перед вызовом функции CallNamedPipe вы должны записать в параметр lpNamedPipeName указатель на текстовую строку, содержащую имя канала Pipe. При формировании этой строки вы должны руководствоваться теми же правилами, что и при использовании функции CreateFile.
Кроме имени канала, вы также должны подготовить буфер, содержащий передаваемые через канал данные. Адрес и размер этого буфера следует указать функции CallNamedPipe, соответственно, через параметры lpOutBuffer и nOutBufferSize.
Данные, полученные от сервера в ответ на посланное ему сообщение, будут записаны в буфер, который вы тоже должны подготовить заранее. Адрес этого буфера необходимо указать через параметр lpInBuffer, а размер буфера - через параметр nInBufferSize.
В переменную, адрес которой указан через параметр lpBytesRead, записывается количество байт, полученных через канал от сервера.
Параметр nTimeOut определяет, в течении какого времени функция CallNamedPipe будет ожидать доступности канала Pipe, прежде чем она вернет код ошибки. Помимо численного значения в миллисекундах, вы можете указать в этом параметре одну из следующих констант:
Константа |
Описание |
NMPWAIT_NOWAIT |
Ожидание канала Pipe не выполняется. Если канал не доступен, функция CallNamedPipe сразу возвращает код ошибки |
NMPWAIT_WAIT_FOREVER |
Ожидание выполняется бесконечно долго |
NMPWAIT_USE_DEFAULT_WAIT XE |
Ожидание выполняется в течении периода времени, указанного при вызове функции CreateNamedPipe |
В случае успешного завершения функция CallNamedPipe возвращает значение TRUE, а при ошибке - FALSE. Код ошибки можно получить, вызвав функцию GetLastError.
Заметим, что может возникнуть ситуация, при которой длина сообщения, полученного от сервера, превосходит размер буфера, предусмотренного процессом. В этом случае функция CallNamedPipe завершится с ошибкой, а функция GetLastError вернет значение ERROR_MORE_DATA. Не существует никакого способа получить через канал оставшуюся часть сообщения, так как перед возвращением управления функция CallNamedPipe закрывает канал с сервером.
Функция TransactNamedPipe, также как и функция CallNamedPipe, предназначена для выполнения передачи и приема данных от клиентского процесса серверному. Однако эта функция более гибкая в использовании, чем функция CallNamedPipe.
Прежде всего, перед использованием функции TransactNamedPipe клиентский процесс должен открыть канал с сервером, воспользовавашись для этого, например, функцией CreateFile.
Кроме того, клиентский процесс может выполнять обмен данными с сервером, вызывая функцию TransactNamedPipe много раз. При этом не будет происходить многократного открытия и закрытия канала.
Прототип функции TransactNamedPipe представлен ниже:
BOOL TransactNamedPipe( HANDLE hNamedPipe, // идентификатор канала Pipe LPVOID lpvWriteBuf, // адрес буфера для записи DWORD cbWriteBuf, // размер буфера для записи LPVOID lpvReadBuf, // адрес буфера для чтения DWORD cbReadBuf, // размер буфера для чтения LPDWORD lpcbRead, // адрес переменной, в которую будет // записано количество действительно прочитанных байт LPOVERLAPPED lpov); // адрес структуры типа OVERLAPPED
Через параметр hNamedPipe вы должны передать функции TransactNamedPipe идентификатор предварительно открытого канала. Канал следует открыть в режиме передачи сообщений.
Параметры lpvWriteBuf и cbWriteBuf задают, соответственно, адрес и размер буфера, содержимое которого будет передано через канал.
Ответное сообщение, полученное от сервера, будет записано в буфер с адресом lpvReadBuf. Размер этого буфера следует передать функции TransactNamedPipe через параметр cbReadBuf.
После того как функция TransactNamedPipe вернет управление, в переменную, адрес которой передавался через параметр lpcbRead, будет записан размер принятого от сервера сообщения в байтах.
Если операция передачи данных через канал должна выполняться с перекрытием (асинхронно), вы должны подготовить структуру типа OVERLAPPED и передать ее адрес через параметр lpov. В противном случае параметр lpov должен быть задан как NULL.
В случае успешного завершения функция TransactNamedPipe возвращает значение TRUE, а при ошибке - FALSE. Код ошибки можно получить, вызвав функцию GetLastError.
Если возникает ситуация, при которой длина сообщения, полученного от сервера, превосходит размер буфера, предусмотренного процессом, функция TransactNamedPipe завершится с ошибкой, а функция GetLastError вернет значение ERROR_MORE_DATA. Оставшуюся часть сообщения можно прочитать с помощью такой функции, как ReadFile.
Чтение данных из канала функцией ReadFile вызывает удаление прочитанных данных. В противоположность этому, функция PeekNamedPipe позволяет получить данные из именованного или анонимного канала без удаления, так что при последующих вызовах этой функции или функции ReadFile будут получены все те же данные, что и при первом вызове функции PeekNamedPipe.
Еще одно отличие заключается в том, что функция PeekNamedPipe никогда не переходит в состояние ожидания, сразу возвращая управление вне зависимости от того, есть данные в канале или нет.
Прототип функции PeekNamedPipe представлен ниже:
BOOL PeekNamedPipe( HANDLE hPipe, // идентификатор канала Pipe LPVOID lpvBuffer, // адрес буфера для прочитанных данных DWORD cbBuffer, // размер буфера прочитанных данных LPDWORD lpcbRead, // адрес переменной, в которую будет // записано количество действительно // прочитанных байт данных LPDWORD lpcbAvail, // адрес переменной, в которую будет // записано общее количество байт данных, // доступных в канале для чтения LPDWORD lpcbMessage); // адрес переменной, в которую будет // записано количество непрочитанных // байт в данном сообщении
Через параметр hPipe функции PeekNamedPipe нужно передать идентификатор открытого анонимного или именованного канала Pipe.
Данные, полученные из канала, будут записаны в буфер lpvBuffer, имеющий размер cbBuffer байт. При этом количество действительно прочитанных байт будет сохранено в переменной, адрес которой передается функции PeekNamedPipe через параметр lpcbRead.
В случае успешного завершения функция PeekNamedPipe возвращает значение TRUE, а при ошибке - FALSE. Код ошибки можно получить, вызвав функцию GetLastError.
С помощью функции WaitNamedPipe процесс может выполнять ожидание момента, когда канал Pipe будет доступен для соединения:
BOOL WaitNamedPipe( LPCTSTR lpszPipeName, // адрес имени канала Pipe DWORD dwTimeout); // время ожидания в миллисекундах
Через параметр lpszPipeName задается имя канала, для которого выполняется ожидание готовности к соединению. Время ожидания в миллисекундах задается через параметр dwTimeout.
Помимо численного значения в миллисекундах, вы можете указать в этом параметре одну из следующих констант:
Константа |
Описание |
NMPWAIT_WAIT_FOREVER |
Ожидание выполняется бесконечно долго |
NMPWAIT_USE_DEFAULT_WAIT |
Ожидание выполняется в течении периода времени, указанного при вызове функции CreateNamedPipe |
Если канал стал доступен до истечения периода времени, заданного параметром dwTimeout, функция WaitNamedPipe возвращает значение TRUE. В противном случае возвращается значение FALSE и вы можете воспользоваться функцией GetLastError.
При необходимости вы можете изменить режимы работы для уже созданного канала Pipe. Для этого предназначена функция SetNamedPipeHandleState, прототип которой мы привели ниже:
BOOL SetNamedPipeHandleState( HANDLE hNamedPipe, // идентификатор канала Pipe LPDWORD lpdwMode, // адрес переменной, в которой указан // новый режим канала LPDWORD lpcbMaxCollect, // адрес переменной, в которой // указывается максимальный размер // пакета, передаваемого в канал LPDWORD lpdwCollectDataTimeout); // адрес максимальной // задержки перед передачей данных
Параметр hNamedPipe задает идентификатор канала Pipe, режим работы которого будет изменен.
Новый режим работы записывается в переменную, адрес которой задан через параметр lpdwMode. Вы можете указать одну из следующих констант, определяющих режим работы канала:
Константа |
Использование канала |
PIPE_READMODE_BYTE |
Канал открывается на чтение в режиме последовательной передачи отдельных байт |
PIPE_READMODE_MESSAGE |
Канал открывается на чтение в режиме передачи отдельных сообщений указанной длины |
PIPE_WAIT |
Канал будет работать в блокирующем режиме, когда процесс переводится в состояние ожидания до завершения операций в канале |
PIPE_NOWAIT |
Неблокирующий режим работы канала. Если операция не может быть выполнена немедленно, в неблокирующем режиме функция завершается с ошибкой |
Константы PIPE_WAIT и PIPE_NOWAIT, задающие блокирующий и неблокирующий режим соответственно, можно комбинировать при помощи логической операции ИЛИ с константами PIPE_READMODE_BYTE и PIPE_READMODE_MESSAGE.
Если текущий режим работы канала изменять не нужно, для параметра lpdwMode следует указать значение NULL.
Теперь рассмотрим назначение параметра lpcbMaxCollect.
Если при открытии канала клиентским процессом функцией CreateFile не была указана константа FILE_FLAG_WRITE_THROUGH, то данные передаются пакетами, которые собираются из отдельных сообщений. Размер такого пакета как раз и определяет параметр lpcbMaxCollect.
В том случае, когда вы не собираетесь изменять размер пакета, укажите для параметра lpcbMaxCollect значение NULL.
Параметр lpdwCollectDataTimeout задает максимальный интервал между передачами данных по сети. Если функция SetNamedPipeHandleState изменяет параметры канала со стороны сервера, или если сервер и клиент работают на одном и том же компьютере, параметр lpdwCollectDataTimeout должен быть задан как NULL.
В случае успешного завершения функция SetNamedPipeHandleState возвращает значение TRUE, а при ошибке - FALSE. Код ошибки можно получить, вызвав функцию GetLastError.
С помощью функции GetNamedPipeHandleState процесс может определить состояние канала Pipe, зная его идентификатор.
Прототип функции GetNamedPipeHandleState мы привели ниже:
BOOL GetNamedPipeHandleState( HANDLE hNamedPipe, // идентификатор именованного канала LPDWORD lpState, // адрес флагов состояния канала LPDWORD lpCurInstances, // адрес количества реализаций LPDWORD lpMaxCollectionCount, // адрес размера пакета // передаваемых данных LPDWORD lpCollectDataTimeout, // адрес максимального // времени ожидания LPTSTR lpUserName, // адрес имени пользователя // клиентского процесса DWORD nMaxUserNameSize); // размер буфера для // имени пользователя клиентского процесса
Идентификатор канала, для которого нужно определить состояние, передается функции GetNamedPipeHandleState через параметр hNamedPipe.
Через параметр lpState нужно передать указатель на переменную типа DWORD, в которую будет записан один из флагов состояния канала:
Флаги состояния |
Описание |
PIPE_WAIT |
Канал будет работать в блокирующем режиме, когда процесс переводится в состояние ожидания до завершения операций в канале |
PIPE_NOWAIT |
Неблокирующий режим работы канала. Если операция не может быть выполнена немедленно, в неблокирующем режиме функция завершается с ошибкой |
Если информация о состоянии канала не требуется, в качестве значения для параметра lpState следует использовать константу NULL.
В переменную, адрес которой передается через параметр lpCurInstances, записывается текущее количество реализаций канала. Если эта информация вам не нужна, передайте через параметр lpCurInstances значение NULL.
Параметры lpMaxCollectionCount и lpCollectDataTimeout позволяют определить, соответственно, размер пакета передаваемых данных и максимальное время ожидания между передачами.
Через параметр lpUserName вы должны передать указатель на буфер, в который функция GetNamedPipeHandleState запишет имя пользователя клиентского процесса. Размер этого буфера задается в параметре nMaxUserNameSize.
При необходимости вы можете задать значения параметров lpMaxCollectionCount, lpCollectDataTimeout и lpUserName как NULL. В этом случае соответствующая информация не будет извлекаться.
В случае успешного завершения функция GetNamedPipeHandleState возвращает значение TRUE, а при ошибке - FALSE. Код ошибки можно получить, вызвав функцию GetLastError.
Еще одна функция, позволяющая получить информацию об именованном канале по его идентификатору, называется GetNamedPipeInfo:
BOOL GetNamedPipeInfo( HANDLE hNamedPipe, // идентификатор канала LPDWORD lpFlags, // адрес флагов типа канала LPDWORD lpOutBufferSize, // адрес размера выходного буфера LPDWORD lpInBufferSize, // адрес размера входного буфера LPDWORD lpMaxInstances); // адрес максимального количества // реализаций канала
Параметр hNamedPipe задает идентфикатор именованного канала Pipe, для которого требуется получить информацию.
Через параметр lpFlags функции GetNamedPipeInfo необходимо передать адрес переменной типа DWORD или NULL, если флаги определять не требуется. Ниже мы привели возможные значения флагов:
Флаг |
Описание |
PIPE_CLIENT_END |
Идентификатор ссылается на клиентскую часть канала |
PIPE_SERVER_END |
Идентификатор ссылается на серверную часть канала |
PIPE_TYPE_MESSAGE |
Канал работает в режиме передачи сообщений |
В переменные, адреса которых задаются через параметры lpOutBufferSize и lpInBufferSize, функция GetNamedPipeInfo заносит размеры входного и выходного буфера, соответственно. Если эта информация не нужна, передайте через параметры lpOutBufferSize и lpInBufferSize значение NULL.
И, наконец, через параметр lpMaxInstances передается адрес переменной, в которую будет записано максимальное значение реализаций, которое можно создать для данного канала. Если после вызова функции GetNamedPipeInfo в этой переменной записано значение PIPE_UNLIMITED_INSTANCES, количество реализаций ограничивается только свободными системными ресурсами.
В случае успешного завершения функция GetNamedPipeInfo возвращает значение TRUE, а при ошибке - FALSE. Код ошибки можно получить, вызвав функцию GetLastError.
В этом разделе мы приведем исходные тексты двух приложений, которые передают друг другу данные через именованный канал Pipes. Заметим, что наши приложения способны установить канал связи между различными рабочими станциями через сеть.
Первое из этих приложений называется PIPES. Оно выполняет роль сервера, который получает команды от клиентского приложения PIPEC и отображает их в консольном окне (рис. 2.4).
Рис. 2.4. Консольное окно серверного приложения PIPES
Сразу после запуска приложение PIPES переходит в состояние ожидания соединения с клиентским приложением. При этом в его окне отображается строка Waiting for connect.
Как только клиентское приложение PIPEC установит канал связи с приложением PIPES, в окно приложения PIPES выводятся строки Connected и Waiting for command. Команды, посылаемые серверу, выводятся в окне в режиме свертки. Никакой другой обработки команд не выполняется, однако принятые команды посылаются сервером обратно клиентскому приложению.
Консольное окно клиентского приложения PIPEC, запущенного в среде операционной системы Microsoft Windows 95, показано на рис. 2.5.
Рис. 2.5. Консольное окно клиентского приложения PIPEC
При запуске клиентского приложения PIPEC вы дополнительно можете указать параметр - имя рабочей станции, например:
pipec netlab
Если имя рабочей станции не указано, приложение PIPEC будет пытаться установить канал с серверным приложением PIPES, запущенным на том же компьютере, что и PIPEC.
После успешного создания канала клиентское приложение выводит сообщение Connected и предлагает вводить команды в приглашении cmd>. В качестве команд вы можете вводить произвольные последовательности символов, кроме exit. Команда exit используется для завершения работы приложений PIPEC и PIPES.
После того как команда посылается серверу, она возвращается обратно и отображается в окне клиентского приложения для контроля в строке Received back.
Исходные тексты приложения PIPES приведены в листинге 2.11.
Листинг 2.11. Файл pipe/pipes/pipes.c
// ================================================== // Приложение PIPES (серверное приложение) // Демонстрация использования каналов Pipe // для передачи данных между процессами // // (С) Фролов А.В., 1996 // Email: frolov@glas.apc.org // ================================================== #include <windows.h> #include <stdio.h> #include <conio.h> int main() { // Флаг успешного создания канала BOOL fConnected; // Идентификатор канала Pipe HANDLE hNamedPipe; // Имя создаваемого канала Pipe LPSTR lpszPipeName = "\\\\.\\pipe\\$MyPipe$"; // Буфер для передачи данных через канал char szBuf[512]; // Количество байт данных, принятых через канал DWORD cbRead; // Количество байт данных, переданных через канал DWORD cbWritten; printf("Named pipe server demo\n" "(C) A. Frolov, 1996, Email: frolov@glas.apc.org\n"); // Создаем канал Pipe, имеющий имя lpszPipeName hNamedPipe = CreateNamedPipe( lpszPipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 512, 512, 5000, NULL); // Если возникла ошибка, выводим ее код и зваершаем // работу приложения if(hNamedPipe == INVALID_HANDLE_VALUE) { fprintf(stdout,"CreateNamedPipe: Error %ld\n", GetLastError()); getch(); return 0; } // Выводим сообщение о начале процесса создания канала fprintf(stdout,"Waiting for connect...\n"); // Ожидаем соединения со стороны клиента fConnected = ConnectNamedPipe(hNamedPipe, NULL); // При возникновении ошибки выводим ее код if(!fConnected) { switch(GetLastError()) { case ERROR_NO_DATA: fprintf(stdout,"ConnectNamedPipe: ERROR_NO_DATA"); getch(); CloseHandle(hNamedPipe); return 0; break; case ERROR_PIPE_CONNECTED: fprintf(stdout, "ConnectNamedPipe: ERROR_PIPE_CONNECTED"); getch(); CloseHandle(hNamedPipe); return 0; break; case ERROR_PIPE_LISTENING: fprintf(stdout, "ConnectNamedPipe: ERROR_PIPE_LISTENING"); getch(); CloseHandle(hNamedPipe); return 0; break; case ERROR_CALL_NOT_IMPLEMENTED: fprintf(stdout, "ConnectNamedPipe: ERROR_CALL_NOT_IMPLEMENTED"); getch(); CloseHandle(hNamedPipe); return 0; break; default: fprintf(stdout,"ConnectNamedPipe: Error %ld\n", GetLastError()); getch(); CloseHandle(hNamedPipe); return 0; break; } CloseHandle(hNamedPipe); getch(); return 0; } // Выводим сообщение об успешном создании канала fprintf(stdout,"\nConnected. Waiting for command...\n"); // Цикл получения команд через канал while(1) { // Получаем очередную команду через канал Pipe if(ReadFile(hNamedPipe, szBuf, 512, &cbRead, NULL)) { // Посылаем эту команду обратно клиентскому // приложению if(!WriteFile(hNamedPipe, szBuf, strlen(szBuf) + 1, &cbWritten, NULL)) break; // Выводим принятую команду на консоль printf("Received: <%s>\n", szBuf); // Если пришла команда "exit", // завершаем работу приложения if(!strcmp(szBuf, "exit")) break; } else { fprintf(stdout,"ReadFile: Error %ld\n"#330033, GetLastError()); getch(); break; } } CloseHandle(hNamedPipe); return 0; }
В области локальных переменных функции main определена строка lpszPipeName, в которой хранится имя канала:
LPSTR lpszPipeName = "\\\\.\\pipe\\$MyPipe$";
Так как канал создается локально, в качестве имени компьютера указан символ точки. Канал называется $MyPipe$.
Буфер szBuf размером 512 байт нужен для хранения данных, передаваемых через канал.
В переменные cbRead и cbWritten при выполнении операций чтения и записи через канал записывается, соответственно, количество принятых и переданных байт данных.
После вывода “рекламной” строки приложение PIPES создает канал, вызывая для этого функцию CreateNamedPipe:
hNamedPipe = CreateNamedPipe( lpszPipeName, PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 512, 512, 5000, NULL);
В качестве первого параметра мы передаем этой функции имя канала. Во втором канале указана константа PIPE_ACCESS_DUPLEX, поэтому канал работает и на прием информации, и на передачу (в дуплексном режиме).
Константа PIPE_TYPE_MESSAGE определяет, что через канал будут передаваться сообщения заданной длины (а не просто последовательность байт данных).
Если в процессе создания канала произошла ошибка, ее код определяется с помощью функции GetLastError. Вслед за этим в консольное окно приложения выводится сообщение с кодом ошибки и приложение переводится в состояние ожидания до тех пор, пока пользователь не нажмет какую-нибудь клавишу. После этого работа приложения завершается.
После создания канала, который будет работать в блокирующем режиме, вызывается функция ConnectNamedPipe:
fConnected = ConnectNamedPipe(hNamedPipe, NULL);
Из-за блокирующего режима работы и из-за того, что канал работает без перекрытия в синхронном режиме, после вызова функции ConnectNamedPipe сервер перейдет в состояние ожидания. Он будет находиться в этом состоянии до тех пор, пока клиентское приложение PIPEC не установит с ним канал Pipe.
При возникновении ошибки наше приложение получает ее код и анализирует его, выводя в консольное окно соответствующее сообщение. Затем приложение закрывает идентификатор канала и завершает свою работу.
После успешного создания канала приложение PIPES входит в цикл получения команд от клиентского приложения PIPEC:
while(1) { if(ReadFile(hNamedPipe, szBuf, 512, &cbRead, NULL)) { if(!WriteFile(hNamedPipe, szBuf, strlen(szBuf) + 1, &cbWritten, NULL)) break; printf("Received: <%s>\n", szBuf); if(!strcmp(szBuf, "exit")) break; } else { fprintf(stdout,"ReadFile: Error %ld\n", GetLastError()); getch(); break; } }
В этом цикле серверное приложение читает команду из канала, пользуясь для этого обычной функцией ReadFile, которую вы знаете из предыдущей главы нашей книги. Так как канал работает в синхронном блокирующем режиме, функция ReadFile будет находиться в состоянии ожидания до тех пор, пока клиентское приложение не пришлет через канал Pipes сообщение с командой. Принятое сообщение записывается в буфер szBuf, а его размер - в переменную cbRead.
Если сообщение принято успешно, оно тут же посылается обратно, для чего серверное приложение посылает его обратно при помощи функции WriteFile. Так как сообщение представляет собой текстовую строку, закрытую двоичным нулем, при посылке размер сообщения вычисляется как длина этой строки плюс один байт.
Далее серверное приложение сравнивает принятую команду со строкой exit. Если от клиента пришла эта строка, цикл получения команд завершается.
Исходные тексты клиентского приложения PIPEC, предназначенного для совместной работы с приложением PIPES, представлены в листинге 2.12.
Листинг 2.12. Файл pipe/pipeс/pipeс.c
// ================================================== // Приложение PIPEC (клиент для приложения PIPES) // Демонстрация использования каналов Pipe // для передачи данных между процессами // // (С) Фролов А.В., 1996 // Email: frolov@glas.apc.org // ================================================== #include <windows.h> #include <stdio.h> #include <conio.h> DWORD main(int argc, char *argv[]) { // Идентификатор канала Pipe HANDLE hNamedPipe; // Количество байт, переданных через канал DWORD cbWritten; // Количество байт, принятых через канал DWORD cbRead; // Буфер для передачи данных char szBuf[256]; // Буфер для имени канала Pipe char szPipeName[256]; printf("Named pipe client demo\n" "(C) A. Frolov, 1996, Email: frolov@glas.apc.org\n\n"); printf("Syntax: pipec [servername]\n"); // Если при запуске PIPEC было указано имя срвера, // указываем его в имени канала Pipe if(argc > 1) sprintf(szPipeName, "\\\\%s\\pipe\\$MyPipe$", argv[1]); // Если имя сервера задано не было, создаем канал // с локальным процессом else strcpy(szPipeName, "\\\\.\\pipe\\$MyPipe$"); // Создаем канал с процессом PIPES hNamedPipe = CreateFile( szPipeName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); // Если возникла ошибка, выводим ее код и // завершаем работу приложения if(hNamedPipe == INVALID_HANDLE_VALUE) { fprintf(stdout,"CreateFile: Error %ld\n", GetLastError()); getch(); return 0; } // Выводим сообщение о создании канала fprintf(stdout,"\nConnected. Type 'exit' to terminate\n"); // Цикл обмена данными с серверным процессом while(1) { // Выводим приглашение для ввода команды printf("cmd>"); // Вводим текстовую строку gets(szBuf); // Передаем введенную строку серверному процессу // в качестве команды if(!WriteFile(hNamedPipe, szBuf, strlen(szBuf) + 1, &cbWritten, NULL)) break; // Получаем эту же команду обратно от сервера if(ReadFile(hNamedPipe, szBuf, 512, &cbRead, NULL)) printf("Received back: <%s>\n", szBuf); // Если произошла ошибка, выводим ее код и // завершаем работу приложения else { fprintf(stdout,"ReadFile: Error %ld\n", GetLastError()); getch(); break; } // В ответ на команду "exit" завершаем цикл // обмена данными с серверным процессом if(!strcmp(szBuf, "exit")) break; } // Закрываем идентификатор канала CloseHandle(hNamedPipe); return 0; }
Сразу после запуска приложение PIPEC проверяет количество аргументов командной строки. Если при запуске пользователь задал в качестве параметра имя рабочей станции, то это имя вставляется внутрь строки, задающей имя канала Pipe, как это показано ниже:
if(argc > 1) sprintf(szPipeName, "\\\\%s\\pipe\\$MyPipe$", argv[1]);
Если же при запуске имя рабочей станции не указано, клиентское приложение будет открывать локальный канал с локальным именем:
else strcpy(szPipeName, "\\\\.\\pipe\\$MyPipe$");
При использовании любого варианта для открытия канала используется функция CreateFile:
hNamedPipe = CreateFile( szPipeName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
Так как мы указали константы GENERIC_READ и GENERIC_WRITE, канал открывается и на чтение, и на запись.
После открытия канала запускается цикл ввода и передачи команд:
while(1) { printf("cmd>"); gets(szBuf); if(!WriteFile(hNamedPipe, szBuf, strlen(szBuf) + 1, &cbWritten, NULL)) break; if(ReadFile(hNamedPipe, szBuf, 512, &cbRead, NULL)) printf("Received back: <%s>\n", szBuf); else { fprintf(stdout,"ReadFile: Error %ld\n", GetLastError()); getch(); break; } if(!strcmp(szBuf, "exit")) break; }
Для ввода команд используется известная вам функция gets. Введенная команда посылается серверу функцией WriteFile. Сразу после этого наше приложение вызывает функцию ReadFile чтобы получить от сервера эту же команду обратно.
В случае успеха команда сравнивается со строкой exit. Если пользователь ввел эту строку, клиентское приложение завершает свою работу. Аналогично поступает и серверное приглашение PIPES.
В завершении этой главы мы рассмотрим еще один простой способ организации передачи данных между различными процессами, основанный на использовании датаграммных каналов Mailslot.
Каналы Mailslot позволяют выполнять одностороннюю передачу данных от одного или нескольких клиентов к одному или нескольким серверам. Главная особенность каналов Mailslot заключается в том, что они, в отличие от других средств, рассмотренных нами в этой главе, позволяют передавать данные в широковещательном режиме.
Это означает, что на компьютере или в сети могут работать несколько серверных процессов, способных получать сообщения через каналы Mailslot. При этом один клиентский процесс может посылать сообщения сразу всем этим серверным процессам.
С помощью каналов Pipe вы не сможете передавать данные в широковещательном режиме, так как только два процесса могут создать канал типа Pipe.
Канал Mailslot создается серверным процессом с помощью специально предназначенной для этого функции CreateMailslot, которую мы рассмотрим немного позже. После создания серверный процесс получает идентификатор канала Mailslot. Пользуясь этим идентификатором, сервер может читать сообщения, посылаемые в канал клиентскими процессами. Однако сервер не может выполнять над каналом Mailslot операцию записи, так как этот канал предназначен только для односторонней передачи данных - от клиента к серверу.
Приведем прототип функции CreateMailslot:
HANDLE CreateMailslot( LPCTSTR lpName, // адрес имени канала Mailslot DWORD nMaxMsgSize, // максимальный размер сообщения DWORD lReadTimeout, // время ожидания для чтения LPSECURITY_ATTRIBUTES lpSecurityAttributes); // адрес // структуры защиты
Через параметр lpName вы должны передать функции CreateMailslot адрес строки символов с именем канала Mailslot. Эта строка имеет следующий вид:
\\.\mailslot\[Путь]ИмяКанала
В этом имени путь является необязательной компонентой. Тем не менее, вы можете указать его аналогично тому, как это делается для файлов. Что же касается имени канала Mailslot, то оно задается аналогично имени канала Pipes.
Параметр nMaxMsgSize определяет максимальный размер сообщений, передаваемых через создаваемый канал Mailslot. Вы можете указать здесь нулевое значение, при этом размер сообщений не будет ограничен. Есть, однако, одно исключение - размер широковещательных сообщений, передаваемых всем рабочим станциям и серверам домена не должен превышать 400 байт.
С помощью параметра lReadTimeout серверное приложение может задать время ожидания для операции чтения в миллисекундах, по истечении которого функция чтения вернет код ошибки. Если вы укажите в этом параметре значение MAILSLOT_WAIT_FOREVER, ожидание будет бесконечным.
Параметр lpSecurityAttributes задает адрес структуры защиты, который мы в наших приложениях будем указывать как NULL.
При ошибке функцией CreateMailslot возвращается значение INVALID_HANDLE_VALUE. Код ошибки можно определить при помощи функции GetLastError.
Ниже мы привели пример использования функции CreateMailslot в серверном приложении:
LPSTR lpszMailslotName = "\\\\.\\mailslot\\$MailslotName$"; hMailslot = CreateMailslot(lpszMailslotName, 0, MAILSLOT_WAIT_FOREVER, NULL);
В этом примере мы задали максимальный размер сообщения, поэтому на эту величину нет ограничений (кроме ограничения в 400 байт для сообщений, передаваемых всем компьютерам домена в широковещательном режиме).
Время ожидания указано как MAILSLOT_WAIT_FOREVER, поэтому функции, работающие с данным каналом Mailslot, будут работать в блокирующем режиме.
Прежде чем приступить к работе с каналом Mailslot, клиентский процесс должен его открыть. Для выполнения этой операции следует использовать функцию CreateFile, например, так:
LPSTR lpszMailslotName = "\\\\.\\mailslot\\$MailslotName$"; hMailslot = CreateFile( lpszMailslotName, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
Здесь в качестве первого параметра функции CreateFile передается имя канала Mailslot. Заметим, что вы можете открыть канал Mailslot, созданный на другой рабочей станции в сети. Для этого строка имени канала, передаваемая функции CreateFile, должна иметь следующий вид:
\\ИмяРабочейСтанции\mailslot\[Путь]ИмяКанала
Можно открыть канал для передачи сообщений всем рабочим станциям заданного домена. Для этого необходимо задать имя по следующему образцу:
\\ИмяДомена\mailslot\[Путь]ИмяКанала
Для передачи сообщений одновременно всем рабочим станциям сети первичного домена имя задается следующим образом:
\\*\mailslot\[Путь]ИмяКанала
В качестве второго параметра функции CreateFile мы передаем константу GENERIC_WRITE. Эта константа определяет, что над открываемым каналом будет выполняться операция записи. Напомним, что клиентский процесс может только посылать сообщения в канал Mailslot, но не читать их оттуда. Чтение сообщений из канала Mailslot - задача для серверного процесса.
Третий параметр указан как FILE_SHARE_READ, и это тоже необходимо, так как сервер может читать сообщения, посылаемые одновременно несколькими клиентскими процессами.
Обратите также внимание на константу OPEN_EXISTING. Она используется потому, что функция CreateFile открывает существующий канал, а не создает новый.
Запись сообщений в канал Mailslot выполняет клиентский процесс, вызывая для этого функцию WriteFile. С этой функцией вы уже имели дело:
HANDLE hMailslot; char szBuf[512]; DWORD cbWritten; WriteFile(hMailslot, szBuf, strlen(szBuf) + 1, &cbWritten, NULL);
В качестве первого параметра этой функции необходимо передать идентификатор канала Mailslot, полученный от функции CreateFile.
Второй параметр определяет адрес буфера с сообщением, третий - размер сообщения. В нашем случае сообщения передаются в виде текстовой строки, закрытой двоичным нулем, поэтому для определения длины сообщения мы воспользовались функцией strlen.
Серверный процесс может читать сообщения из созданного им канала Mailslot при помощи функции ReadFile, как это показано ниже:
HANDLE hMailslot; char szBuf[512]; DWORD cbRead; ReadFile(hMailslot, szBuf, 512, &cbRead, NULL);
Через первый параметр функции ReadFile передается идентификатор созданного ранее канала Mailslot, полученный от функции CreateMailslot. Второй и третий параметры задают, соответственно, адрес буфера для сообщения и его размер.
Заметим, что перед выполнением операции чтения следует проверить состояние канала Mailslot. Если в нем нет сообщений, то функцию ReadFile вызывать не следует. Для проверки состояния канала вы должны воспользоваться функцией GetMailslotInfo, описанной ниже.
Серверный процесс может определить текущее состояние канала Mailslot по его идентификатору с помощью функции GetMailslotInfo. Прототип этой функции мы привели ниже:
BOOL GetMailslotInfo( HANDLE hMailslot, // идентификатор канала Mailslot LPDWORD lpMaxMessageSize, // адрес максимального размера // сообщения LPDWORD lpNextSize, // адрес размера следующего сообщения LPDWORD lpMessageCount, // адрес количества сообщений LPDWORD lpReadTimeout); // адрес времени ожидания
Через параметр hMailslot функции передается идентификатор канала Mailslot, состояние которого необходимо определить.
Остальные параметры задаются как указатели на переменные типа DWORD, в которые будут записаны параметры состояния канала Mailslot.
В переменную, адрес которой передается через параметр lpMaxMessageSize, после возвращения из функции GetMailslotInfo будет записан максимальный размер сообщения. Вы можете использовать это значение для динамического получения буфера памяти, в который это сообщение будет прочитано функцией ReadFile.
В переменную, адрес которой указан через параметр lpNextSize, записывается размер следующего сообщения, если оно есть в канале. Если же в канале больше нет сообщений, в эту переменную будет записана константа MAILSLOT_NO_MESSAGE.
С помощью параметра lpMessageCount вы можете определить количество сообщений, записанных в канал клиентскими процессами. Если это количество равно нулю, вам не следует вызывать функцию ReadFile для чтения несуществующего сообщения.
И, наконец, в переменную, адрес которой задается в параметре lpReadTimeout, записывается текущее время ожидания, установленное для канала (в миллисекундах).
Если вам не нужна вся информация, которую можно получить с помощью функции GetMailslotInfo, некоторые из ее параметров (кроме, разумеется, первого) можно указать как NULL.
В случае успешного завершения функция GetMailslotInfo возвращает значение TRUE, а при ошибке - FALSE. Код ошибки можно получить, вызвав функцию GetLastError.
Ниже мы привели пример использоания функции GetMailslotInfo:
BOOL fReturnCode; DWORD cbMessages; DWORD cbMsgNumber; fReturnCode = GetMailslotInfo(hMailslot, NULL, &cbMessages, &cbMsgNumber, NULL);
С помощью функции SetMailslotInfo серверный процесс может изменить время ожидания для канала Mailslot уеж после его создания.
Прототип функции SetMailslotInfo приведен ниже:
BOOL SetMailslotInfo( HANDLE hMailslot, // идентификатор канала Mailslot DWORD dwReadTimeout); // время ожидания
Через параметр hMailslot функции SetMailslotInfo передается идентификатор канала Mailslot, для которого нужно изменить время ожидания.
Новое значение времени ожидания в миллисекундах задается через параметр dwReadTimeout. Вы также можете указать здесь константы 0 или MAILSLOT_WAIT_FOREVER. В первом случае функции, работающие с каналом, вернут управление немедленно, во втором - будут находиться в состоянии ожидания до тех пор, пока не завершится выполняемая операция.
Для примера мы подготовили исходные тексты двух приложений - MSLOTS и MSLOTC, которые обмениваются информацией через канал Mailslot (рис. 2.6).
Рис. 2.6. Обмен сообщениями между приложениями MSLOTS и MSLOTC
Приложение MSLOTS выполняет роль сервера, создавая канал Mailslot. Периодически с интервалом 0,5 с это приложение проверяет, не появилось ли в канале сообщение. Если появилось, это сообщение отображается в консольном окне.
Клиентское приложение MSLOTC устанавливает связь с приложением MSLOTS. Если при запуске указать имя компьютера или домена, возможно подключение к серверу MSLOTS, запущенному на другой рабочей станции в сети.
Если вводить текстовые строки в приглашении cmd>, которое выводится в консольном окне клиентского приложения MSLOTC, они будут передаваться серверу через канал Mailslot и отображаться в его окне. Для завершения работы обоих приложений вы должны ввести в приглашении клиента команду exit.
Перейдем теперь к описанию исходных текстов наших приложений.
Исходный текст серверного приложения MSLOTS представлен в листинге 2.13.
Листинг 2.13. Файл mailslot/mslots/mslots.c
// ================================================== // Приложение MSLOTS (серверное приложение) // Демонстрация использования каналов Mailslot // для передачи данных между процессами // // (С) Фролов А.В., 1996 // Email: frolov@glas.apc.org // ================================================== #include <windows.h> #include <stdio.h> #include <conio.h> int main() { // Код возврата из функций BOOL fReturnCode; // Размер сообщения в байтах DWORD cbMessages; // Количество сообщений в канале Mailslot DWORD cbMsgNumber; // Идентификатор канала Mailslot HANDLE hMailslot; // Имя создаваемого канала Mailslot LPSTR lpszMailslotName = "\\\\.\\mailslot\\$Channel$"; // Буфер для передачи данных через канал char szBuf[512]; // Количество байт данных, принятых через канал DWORD cbRead; printf("Mailslot server demo\n" "(C) A. Frolov, 1996, Email: frolov@glas.apc.org\n"); // Создаем канал Mailslot, имеющий имя lpszMailslotName hMailslot = CreateMailslot( lpszMailslotName, 0, MAILSLOT_WAIT_FOREVER, NULL); // Если возникла ошибка, выводим ее код и зваершаем // работу приложения if(hMailslot == INVALID_HANDLE_VALUE) { fprintf(stdout,"CreateMailslot: Error %ld\n", GetLastError()); getch(); return 0; } // Выводим сообщение о создании канала fprintf(stdout,"Mailslot created\n"); // Цикл получения команд через канал while(1) { // Определяем состояние канала Mailslot fReturnCode = GetMailslotInfo( hMailslot, NULL, &cbMessages, &cbMsgNumber, NULL); if(!fReturnCode) { fprintf(stdout,"GetMailslotInfo: Error %ld\n", GetLastError()); getch(); break; } // Если в канале есть Mailslot сообщения, // читаем первое из них и выводим на экран if(cbMsgNumber != 0) { if(ReadFile(hMailslot, szBuf, 512, &cbRead, NULL)) { // Выводим принятую строку на консоль printf("Received: <%s>\n", szBuf); // Если пришла команда "exit", // завершаем работу приложения if(!strcmp(szBuf, "exit")) break; } else { fprintf(stdout,"ReadFile: Error %ld\n", GetLastError()); getch(); break; } } // Выполняем задержку на 500 миллисекунд Sleep(500); } // Перед завершением приложения закрываем // идентификатор канала Mailslot CloseHandle(hMailslot); return 0; }
Прежде всего, серверное приложение создает канал Mailslot, пользуясь для этого функцией CreateMailslot:
hMailslot = CreateMailslot(lpszMailslotName, 0, MAILSLOT_WAIT_FOREVER, NULL);
Далее запускается цикл, в котором после определения состояния канала выполняется чтение сообщений из него (при условии, что в канале есть сообщения). Для проверки состояния канала мы используем функцию GetMailslotInfo.
Сообщение читается функцией ReadFile:
ReadFile(hMailslot, szBuf, 512, &cbRead, NULL);
После чтения перед выполнением очередной проверки состояния приложение выполняет задержку, вызывая для этого функцию Sleep:
Sleep(500);
Задержка необходима для того, чтобы ожидание сообщения в цикле не отнимало слишком много системных ресурсов у других приложений.
Перед завершением работы приложения мы закрываем идентификатор канала Mailslotс помощью функции CloseHandle:
CloseHandle(hMailslot);
Исходный текст клиентского приложения MSLOTC представлен в листинге 2.14.
Листинг 2.14. Файл mailslot/mslotc/mslotc.c
// ================================================== // Приложение MSLOTC (клиентское приложение) // Демонстрация использования каналов Mailslot // для передачи данных между процессами // // (С) Фролов А.В., 1996 // Email: frolov@glas.apc.org // ================================================== #include <windows.h> #include <stdio.h> #include <conio.h> DWORD main(int argc, char *argv[]) { // Идентификатор канала Mailslot HANDLE hMailslot; // Буфер для имени канала Mailslot char szMailslotName[256]; // Буфер для передачи данных через канал char szBuf[512]; // Количество байт, переданных через канал DWORD cbWritten; printf("Mailslot client demo\n" "(C) A. Frolov, 1996, Email: frolov@glas.apc.org\n"); printf("Syntax: mslotc [servername]\n"); // Если при запуске было указано имя срвера, // указываем его в имени канала Mailslot if(argc > 1) sprintf(szMailslotName, "\\\\%s\\mailslot\\$Channel$", argv[1]); // Если имя сервера задано не было, создаем канал // с локальным процессом else strcpy(szMailslotName, "\\\\.\\mailslot\\$Channel$"); // Создаем канал с процессом MSLOTS hMailslot = CreateFile( szMailslotName, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); // Если возникла ошибка, выводим ее код и // завершаем работу приложения if(hMailslot == INVALID_HANDLE_VALUE) { fprintf(stdout,"CreateFile: Error %ld\n", GetLastError()); getch(); return 0; } // Выводим сообщение о создании канала fprintf(stdout,"\nConnected. Type 'exit' to terminate\n"); // Цикл посылки команд через канал while(1) { // Выводим приглашение для ввода команды printf("cmd>"); // Вводим текстовую строку gets(szBuf); // Передаем введенную строку серверному процессу // в качестве команды if(!WriteFile(hMailslot, szBuf, strlen(szBuf) + 1, &cbWritten, NULL)) break; // В ответ на команду "exit" завершаем цикл // обмена данными с серверным процессом if(!strcmp(szBuf, "exit")) break; } // Закрываем идентификатор канала CloseHandle(hMailslot); return 0; }
Сразу после запуска приложение проверяет параметры. Если вы указали имя компьютера или домена,оно будет вставлено в строку, передаваемую функции CreateFile, открывающей канал Mailslot.
Канал открывается следующим образом:
hMailslot = CreateFile( szMailslotName, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
В результате будет открыт канал, предназначенный для записи в синхронном режиме.
Ввод и передача текстовых строк через канал выполняется в цикле, не имеющем никаких особенностей. Для записи введенной строки в канал мы вызываем функцию WriteFile:
WriteFile(hMailslot, szBuf, strlen(szBuf) + 1, &cbWritten, NULL);
Перед завершением своей работы приложение MSLOTC закрывает канал, вызывая для этого функцию CloseHandle XE.