Цель: Изучение механизмов взаимодействия между процессами обмена сообщениями в операционной системе Windows.
Задачи:
1. Изучение теоретического материала по взаимодействию между процессами посредством почтовых ящиков и конвейеров.
2. Составление алгоритма программы.
3. Программная реализация.
Ход работы:
1. Получить у преподавателя индивидуальный вариант задания, который должен предусматривать разработку двух программ (сервер и клиент) и взаимодействие между ними посредством почтовых ящиков или конвейеров.
2. Разработать и отладить сервер и клиент в соответствии с полученным заданием.
3. Написать отчет и представить его к защите вместе с исполняемыми модулями программ и их исходными текстами.
Ход защиты:
1. Продемонстрировать преподавателю программы, использующие механизмы взаимодействия между процессами посредством почтовых ящиков и конвейеров.
2. Пояснить программный код разработанных приложений.
Почти во всех ОС семейства Windows реализован механизм называемый почтовыми ящиками (ПЯ). Они позволяют одному клиентскому процессу передавать сообщения одному или нескольким серверным на одном и том же компьютере или на разных. Разработка приложений, использующих ПЯ, не требует знания сетевых протоколов. Поскольку ПЯ основаны на широковещании, то они не гарантируют доставки данных.
|
|
ПЯ основаны на интерфейсе файловой системы. Клиенты и серверы используют стандартные функции Win 32 API для отправки и получения данных почтовым ящиком, а также правила именования файловой системы, а именно:
\\сервер\Mailslot\[path]name
где сервер – имя сервера, на котором создается ПЯ и выполняется серверное приложение, Mailslot – фиксированная обязательная строка, [ path ] name – имя почтового ящика. В качестве сервера может быть задана точка (.), снежинка (*), имя домена или сервера.
Для передачи сообщений по сети ПЯ обычно используют дейтаграммы – небольшие порции данных, передаваемые по сети без установления соединения. Для совместимости с большинством версий Windows нежелательно использовать ПЯ, чьи размеры превышают 424 байта, поскольку в некоторых из ОС сообщения больших размеров не пересылаются.
ПЯ используют архитектуру клиент-сервер, где данные передаются только в одном направлении. Сервер создает ПЯ и является единственным процессом, который может читать из него данные. Клиенты ПЯ – это процессы, открывающие экземпляры ПЯ и единственные, имеющие право записывать в них данные. Таким образом, для реализации ПЯ нужно написать базовое серверное приложение, в котором выполнить следующие действия:
1. Создать описатель ПЯ с помощью CreateMailSlot.
|
|
2. Получить данные от любого клиента путем вызова ReadFile с описателем ПЯ в качестве параметра.
3. Закрыть описатель ПЯ с помощью CloseHandle.
Первая из упомянутых функций определена так:
HANDLE CreateMailSlot(
LPCTSTR Name, // имя ПЯ
DWORD MaxMessSize,
// максимальный размер сообщения (0 – любой размер)
DWORD TimeOut, // Время ожидания сообщений, в мс.
LPSECURITY_ATTRIBUTES Attrib); //атрибуты безопасности
После создания ПЯ можно читать данные. Единственный процесс,
который может читать, это сервер. Для этого надо вызвать функцию
HANDLE ReadFile(
HANDLE File, // описатель файла
LPVOID Buffer, // буфер для сообщения
DWORD BytesToRead,
// размер буфера (обязательно больше размера сообщения)
LPDWORD BytesRead, // количество прочитанных байтов
LPOVERLAPPED Overlap); // асинхронное чтение данных из буфера
Сервер почтовых ящиков должен выглядеть следующим образом.
#include <windows.h>
#include <stdio.h>
void main(void)
{
HANDLE Mailslot;
char buffer[256];
DWORD NumberOfBytesRead;
// Создание почтового ящика и бесконечное ожидание сообщений
if ((Mailslot = CreateMailslot("\\\\.\\Mailslot\\slot1", 0,
MAILSLOT_WAIT_FOREVER, NULL)) == INVALID_HANDLE_VALUE)
{
printf("Ошибка при создании ПЯ %d\n", GetLastError());
return;
}
// Бесконечное чтение данных из ПЯ!
while(ReadFile(Mailslot, buffer, 256, &NumberOfBytesRead,
NULL)!= 0)
{
buffer[NumberOfBytesRead] = 0;
printf("%s\n", buffer);
}
CloseHandle (Mailslot);
}
Для реализации клиента нужно разработать приложение, ссылающееся на существующий ящик и записывающее в него данные. В таком приложении необходимо выполнить следующие шаги:
1. Открыть описатель ПЯ, в который нужно записать данные с помощью CreateFile.
2. Записать данные в ПЯ вызовом функции WriteFile.
3. Закрыть описатель ПЯ с помощью CloseHandle.
Открытие файла осуществляется при помощи функции
HANDLE CreateFile (
LPCTSTR Name, // имя ПЯ
DWORD DesAccess, // желаемый доступ
DWORD Sharing, // режим разделения
LPSECURITY_ATTRIBUTES Attrib, //атрибуты безопасности
DWORD CreatDispos, // создать или открыть
DWORD Flags, // флаги и атрибуты
HANDLE Template); // шаблон
Поскольку клиент может только записывать данные на сервер, то параметр DesAccess может быть только GENERIC_WRITE. Параметр Sharing должен быть только FILE_SHARE_READ, чтобы сервер мог открывать и читать из почтового ящика. Флаг CreateDispos обязан быть OPEN_EXISTING, а Flags – FILE_ATTRIBUTE_NORMAL.
Теперь можно записывать данные в ПЯ, для этого вызвать функцию
HANDLE WriteFile(
HANDLE File, // описатель файла
LPVOID Buffer, // буфер для сообщения
DWORD BytesToWrite,
// размер буфера (обязательно больше размера сообщения)
LPDWORD BytesWritten,// количество записанных байтов
LPOVERLAPPED Overlap); // асинхронное чтение данных из буфера
Пример клиента почтового ящика приведен ниже.
#include <windows.h>
#include <stdio.h>
void main(int argc, char *argv[])
{
HANDLE Mailslot;
DWORD BytesWritten;
CHAR ServerName[256];
// Вид командной строки для сервера,
// которому отправляется сообщение
if (argc < 2)
{
printf("Использование: client <server_name>\n");
return;
}
sprintf(ServerName, "\\\\%s\\Mailslot\\Slot1", argv[1]);
if ((Mailslot = CreateFile(ServerName, GENERIC_WRITE,
FILE_SHARE_READ, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL)) ==
INVALID_HANDLE_VALUE)
{
printf("Создание файла с ошибкой %d\n",
GetLastError());
return;
}
if (WriteFile(Mailslot, "Это тестовое сообщение",
sizeof ("Это тестовое сообщение"),
&BytesWritten, NULL) == 0)
{
printf("Запись в файл с ошибкой %d\n", GetLastError());
return;
}
printf("Записано %d байтов\n", BytesWritten);
CloseHandle(Mailslot);
}
Почти во всех ОС семейства Windows реализован механизм называемый именованными конвейерами (ИК). Они позволяют клиентскому процессу передавать сообщение серверному на одном и том же компьютере или на разных. Разработка приложений, использующих ИК, не требует знания сетевых протоколов. ИК представляют собой простую архитектуру клиент-сервер, обеспечивающую надежную передачу данных.
ИК основаны на интерфейсе файловой системы. Клиенты и серверы используют стандартные функции Win 32 API для отправки и получения данных именованным конвейером, а также правила именования файловой системы, а именно:
|
|
\\сервер\Pipe\[path]name
где сервер – имя сервера, на котором создается ИК, Pipe – фиксированная обязательная строка, [ path ] name – имя ИК. В качестве сервера может быть задана точка (.) и имя сервера. ИК нельзя создать на удаленном компьютере.
ИК используют два режима передачи данных: побайтовый и
сообщений. В первом случае сообщения передаются непрерывным потоком байтов между клиентом и сервером, при этом они не знают, сколько байтов считывается или записывается в ИК в определенный момент времени.
Значит, запись N байтов с одной стороны канала не означает чтения такого же количества байтов с другой стороны. Такой способ позволяет не заботиться о содержимом передаваемых данных. Во втором случае клиент обменивается сервером дискретными блоками данных, при этом сообщения читается целиком.
ИК используют архитектуру клиент-сервер, где данные передаются в одном или двух направлениях. Сервер создает ИК и принимает соединение с клиентом. Клиент ИК – это процесс, устанавливающий соединение с существующим сервером. После этого сервер и клиент могут читать и записывать данные в ИК с помощью функций ReadFile и WriteFile.
Процесс создания сервера заключается в последовательном вызове AP I-функций:
1. Создать экземпляр ИК с помощью CreateNamedPipe.
2. «Слушать» клиентские соединения с помощью ConnectNamedPipe.
3. Читать и записывать данные в ИК с помощью ReadFile и WriteFile.
4. Завершить соединение с помощью DisconnectNamedPipe.
5. Закрыть описатель экземпляра ИК с помощью CloseHandle.
Первая из упомянутых функций определена так.
HANDLE CreateNamedPipe(
LPCTSTR Name, // имя ПЯ
DWORD OpenMode, // направление передачи,
// управление вводом-выводом и безопасность
DWORD PipeMode, // режим операций чтения, записи, ожидания
DWORD MaxInst, // максимальное количество экземпляров канала
DWORD OutSize, // размер выходящего буфера
DWORD InSize, // размер входящего буфера
DWORD TimeOut, // Время ожидания соединения, в мс.
LPSECURITY_ATTRIBUTES Attrib); //атрибуты безопасности
|
|
Флаги режимов могут быть следующими: PIPE_ACCESS_DUPLEX
(передача данных в обоих направлениях), PIPE_ACCESS_OUTBOUND (данные передаются от сервера к клиенту) и PIPE_ACCESS_INBOUND (от клиента к серверу), FILE_FLAG_WRITE_THROUGH (функции записи не возвращают значение, пока данные передаются по сети или находятся в буфере), FILE_FLAG_OVERLAPPED (используется перекрытый ввод-вывод), WRITE_DAC (можно изменять список избирательного управления доступом), ACCESS_SYSTEM_SECURITY (можно изменять системный список управления доступом), WRITE_OWNER (можно изменять владельца ИК и групповой идентификатор безопасности).
Параметр PipeMode определяет режимы чтения, записи и ожидания. При создании нужно указать по одному флагу из каждой категории, объединив их операцией «|». Для записи данных флаг PIPE_TYPE_BYTE означает, что данные записываются в ИК потоком байтов, PIPE_TYPE_MESSAGE – потоком сообщений. Для чтения PIPE_READMODE_BYTE означает чтение данных потоком байтов, а PIPE_READMODE_MESSAGE – потоком сообщений. Для операции ожидания PIPE_WAIT включает режим блокировки, PIPE_NOWAIT – отключает режим блокировки.
Когда функция CreateNamedPipe вернет описатель ИК, сервер начинает ожидать соединения клиентов. Для установления соединения надо вызвать функцию
BOOL ConnectNamedPipe(
HANDLE Pipe, // описатель ИК
LPOVERLAPPED Overlap); // асинхронное чтение данных из буфера
Сервер именованных конвейеров может выглядеть следующим образом.
#include <windows.h>
#include <stdio.h>
void main(void)
{
HANDLE NPipe;
char buffer[256];
DWORD NumberOfBytesRead;
// Создание ИК
if ((NPipe = CreateNamedPipe("\\\\.\\Pipe\\pipe1",
PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE |
PIPE_READMODE_BYTE, 1, 0, 0, 2000, NULL)) ==
INVALID_HANDLE_VALUE)
{
printf("Ошибка при создании конвейера %d\n",
GetLastError());
return; }
printf (“Сервер запущен.\n”);
if (ConnectNamedPipe(NPipe, NULL) == 0)
{
printf("Ошибка при установлении соединения %d\n",
GetLastError());
CloseHandle (NPipe);
return;
}
if (ReadFile (NPipe, buffer, sizeof (buffer),
&NumberOfBytesRead, NULL) <= 0)
{
printf("Ошибка при чтении из конвейера %d\n",
GetLastError());
CloseHandle (NPipe);
return;
}
printf (“%s\n”, buffer);
if (DisconnectNamedPipe(NPipe) == 0)
{
printf("Ошибка при разрыве соединения %d\n",
GetLastError());
return;
}
CloseHandle (NPipe);
}
Для реализации клиента нужно разработать приложение, которое может подключаться к существующему ИК. В таком приложении необходимо выполнить следующие шаги:
1. Для проверки наличия свободного экземпляра ИК нужно вызвать функцию WaitNamedPipe.
2. Для установления соединения нужно использовать CreateFile.
3. Читать и записывать данные в ИК с помощью ReadFile и WriteFile.
4. Завершить соединение с помощью функции CloseHandle.
Перед установлением соединения клиент должен проверить наличие свободного ИК с помощью функции
BOOL WaitNamedPipe (
LPCTSTR Name, // имя ИК
DWORD TimeOut); // величина ожидания свободного ИК
Затем клиент может открыть этот экземпляр ИК с помощью CreateFile, указав в качестве имени название открываемого ИК. Поскольку клиент
может при желании читать и/или записывать данные на сервер, то параметр DesAccess может быть GENERIC_WRITE либо GENERIC_READ либо GENERIC_WRITE | GENERIC_READ. Параметр Sharing должен быть только 0, поскольку только один клиент получает доступ к ИК. Флаг CreateDispos обязан быть OPEN_EXISTING, а Flags должен обязательно включать FILE_ATTRIBUTE_NORMAL.
Пример клиента именованного конвейера приведен ниже.
#include <windows.h>
#include <stdio.h>
void main(void)
{
HANDLE NPipe;
DWORD BytesWritten;
if (WaitNamedPipe("\\\\.\\Pipe\\pipe1",
NMPWAIT_WAIT_FOREVER) == 0)
{
printf("Ожидание соединения с ошибкой %d\n",
GetLastError());
return;
}
// создание описателя ИК
if ((NPipe = CreateFile("\\\\.\\Pipe\\pipe1", GENERIC_READ |
GENERIC_WRITE, 0, (LPSECURITY_ATTRIBUTES)NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
(HANDLE)NULL)) == INVALID_HANDLE_VALUE)
{
printf("Создание файла с ошибкой %d\n",
GetLastError());
return;
}
if (WriteFile (NPipe, “Test string”,
strlen (“Test string”),
&BytesWritten, NULL) == 0)
{
printf("Запись в файл с ошибкой %d\n", GetLastError());
CloseHandle (NPipe);
return;
}
printf("Записано %d байтов\n", BytesWritten);
CloseHandle(NPipe);
}