Книга: Сетевое программирование. От основ до приложений
Назад: Глава 16. Сокетный API в ОС Windows
Дальше: Глава 18. Управление сетью в ОС Windows

Глава 17. Альтернативы сокетам в ОС Windows

Интернет? Мы в этом не заинтересованы.

Билл Гейтс, 1993

Введение

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

Для этих способов существуют понятия сервера и клиента:

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

Клиент — использует для обмена уже созданные ящики или каналы.

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

Есть и другие варианты сетевой коммуникации — удаленные вызов процедур, или RPC, и очереди MSMQ. Их мы изучим в книге 3, когда будем говорить о способах взаимодействия.

С другой стороны, можно использовать сокетный интерфейс, но без задействования стека ядра. В ОС Windows для этого существует WSD, которого мы коснемся в главе 23.

Сейчас же мы приступим к рассмотрению почтовых ящиков и каналов. Изучим их API и разберем примеры.

Описание протоколов, которые использует описанный ниже API, .

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

Внимание! Удаленные почтовые ящики , так как они работают поверх протокола SMB1, который является небезопасным. В новых приложениях использовать не стоит.

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

К e-mail эти «почтовые ящики» не имеют никакого отношения.

Владелец почтового ящика, то есть процесс, который его создал, называется сервером почтового ящика.

Процесс, который записывает сообщение в почтовый ящик, называется клиентом почтового ящика.

Достаточно известная команда обмена сообщениями net send, которая в новых версиях ОС Windows заменена утилитой msg, также использовала для рассылки почтовые ящики с именем \\*\MAILSLOT\Messngr.

Как показано на рис. 17.1, почтовые ящики обеспечивают простой способ рассылки уведомлений процессам.

Рис. 17.1. Рассылка уведомлений почтовыми ящиками нескольким процессам

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

Домен — группа рабочих станций и серверов, которые имеют общее имя.

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

Речь идет о поступивших сообщениях: они ставятся в очередь. Но гарантии, что сообщения будут помещены в нее именно в порядке отправки, нет.

Внимание! Удаленные каналы поддерживают сообщения размером .

Технически сообщения каналов могут иметь любой размер, но желательно не больше 64 Кбайт. А при работе в сети MSDN устанавливает ограничения на размер сообщений в 424 байта.

Сообщения менее 425 байт отправляются при помощи UDP-дейтаграмм, а сообщения свыше 426 байт — путем соединения по протоколу SMB. Но эту возможность поддерживают только NT-версии ОС Windows, а старые, не NT, не поддерживают.

Редиректор Windows будет проверять, на какую версию ОС система отправляет сообщение, и если сообщение отправляется на старую версию Windows Me, просто обрежет его размер до 424 байт.

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

Когда сервер создает новый почтовый ящик, он получает его дескриптор. В системе почтовый ящик — это псевдофайл, который находится в памяти, поэтому для работы с ним используется файловый API.

Дескриптор нового почтового ящика используется для чтения сообщений. Чтобы отправить сообщение в почтовый ящик, процесс открывает почтовый ящик по имени.

Именование

Имя почтового ящика должно иметь следующий формат:

\\.\mailslot\[путь\]имя

Например:

\\.\mailslot\data\stream_speed

\\.\mailslot\data\stream_pressure

\\.\mailslot\control\valve_angle

Имена нечувствительны к регистру. Точка означает имя текущего узла. Если почтовый ящик создал процесс на другом узле, при открытии почтового ящика точка заменяется именем узла:

\\имя_узла\mailslot\[путь\]имя

Чтобы отправить сообщение в каждый почтовый ящик с заданным именем, в основном домене используется следующая форма:

\\*\mailslot\[путь\]имя

API почтовых ящиков

Алгоритм работы с ящиками следующий:

1. Создать почтовый ящик с помощью API-функции CreateMailslot().

2. Читать данные, используя функцию ReadFile().

3. Закрыть дескриптор почтового ящика с помощью CloseHandle().

Функция CreateMailslot():

#include <winbase.h>

 

HANDLE CreateMailslot(

    LPCTSTR lpName,

    DWORD nMaxMessageSize,

    DWORD lReadTimeout,

    LPSECURITY_ATTRIBUTES lpSecurityAttributes

);

Ее параметры:

lpName — путь к ящику.

• nMaxMessageSize — максимальный размер сообщения, которое может быть записано в почтовый ящик. Если сообщение может иметь произвольный размер, значение параметра должно быть равно нулю.

• lReadTimeout — время, в течение которого операция чтения будет ожидать новое сообщение, в миллисекундах.

lpSecurityAttributes — необязательный указатель на структуру SECURITY_ATTRIBUTES. Ее атрибут bInheritHandle определяет, может ли быть дескриптор унаследован процессами-потомками.

Параметр lReadTimeout имеет два специальных значения, определяющих поведение ReadFile(), если сообщений в ящике нет:

0 — возвратиться немедленно.

MAILSLOT_WAIT_FOREVER — заблокироваться до появления сообщения.

Созданный почтовый ящик полностью реализует семантику файла, и для его открытия клиент использует стандартную WinAPI-функцию CreateFile() с параметром dwDesiredAccess, равным GENERIC_WRITE, — открытие на запись — и параметром dwCreationDisposition, равным OPEN_EXISTING, то есть открывается существующий «файл»:

HANDLE hFile = CreateFile(mail_slot_name,

    GENERIC_WRITE,

    FILE_SHARE_READ,

    nullptr,

    OPEN_EXISTING,

    FILE_ATTRIBUTE_NORMAL,

    nullptr

);

Чтобы использовать почтовые ящики, обычно включают не специфичные файлы, в которых объявлены функции, а сразу заголовочный файл windows.h.

Помимо общего API для работы с хэндлами, такого как GetHandleInfo(), для почтовых ящиков существуют две специфичные функции:

#include <winbase.h>

 

// Получить информацию о ящике.

bool GetMailslotInfo(

    HANDLE hMailslot,

    LPDWORD lpMaxMessageSize,

    LPDWORD lpNextSize,

    LPDWORD lpMessageCount,

    LPDWORD lpReadTimeout

);

 

// Установить параметры ящика.

bool SetMailslotInfo(

    HANDLE hMailslot,

    DWORD lReadTimeout

);

Их параметры:

hMailSlot — хэндл почтового ящика.

• lpMaxMessageSize — максимальный размер сообщения в байтах, разрешенный для почтового ящика.

• lpNextSize — размер следующего сообщения в байтах. Если сообщения нет, в переменную будет записано MAILSLOT_NO_MESSAGE.

• lpMessageCount — общее количество сообщений, ожидающих чтения. Может быть nullptr.

lpReadTimeout — время ожидания функции ReadFile(). Может быть nullptr.

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

Через функцию SetHandleInformation() также можно переустановить флаг наследования. Однако общий файловый API — обширная тема, выходящая за рамки этой книги, поэтому мы ее затрагивать не будем.

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

Пример работы с ящиками

На рис. 17.2 показана схема записи сообщений в ящик, производимой через вызовы функции WriteFile(), и чтения полученных данных функцией ReadFile().

Внимание! Максимальный размер сообщения в почтовых слотах — 424 байта. Начиная с Windows XP SP2, можно отправлять 1536 байт. Однако размер сообщения по-прежнему ограничен размером дейтаграммы.

Рис. 17.2. Взаимодействие сервера и клиента в mailslots

Сервер:

int main()

{

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

    const LPCTSTR slot_name = TEXT("\\\\.\\mailslot\\test_mailslot");

 

    // Создать ящик. Работать в блокирующем режиме.

    HANDLE h_slot = CreateMailslot(slot_name, 0, MAILSLOT_WAIT_FOREVER,

                                   nullptr);

 

    if (INVALID_HANDLE_VALUE == h_slot)

    {

        std::cerr << "CreateMailslot failed" << std::endl;

        return EXIT_FAILURE;

    }

 

    std::cout << "Mailslot created successfully." << std::endl;

    std::array<char, 2048> buffer;

    DWORD bytes_read;

 

    // Чтение данных из слота.

    while (ReadFile(h_slot, &buffer[0], buffer.size(), &bytes_read, nullptr))

    {

        std::cout

            << "Received: "

            << std::string(buffer.begin(), buffer.begin() + bytes_read)

            << std::endl;

    }

    // Закрыть почтовый ящик.

    CloseHandle(h_slot);

 

    return EXIT_SUCCESS;

}

Клиент:

int main()

{

    const LPCTSTR slot_name = TEXT("\\\\.\\mailslot\\test_mailslot");

    // Подключиться к почтовому ящику.

    HANDLE h_slot = CreateFile(slot_name, GENERIC_WRITE, FILE_SHARE_READ,

                               nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,

                               nullptr);

 

    if (INVALID_HANDLE_VALUE == h_slot)

    {

        std::cerr << "CreateFile failed" << std::endl;

        return EXIT_FAILURE;

    }

 

    const std::string msg = "Test message...";

 

    DWORD bytes_written = 0;

 

    // Отправить сообщение, используя обычный файловый API.

    if (!WriteFile(h_slot, msg.c_str(), msg.size(), &bytes_written, nullptr))

    {

        std::cerr << "Failed" << std::endl;

        CloseHandle(h_slot);

        return EXIT_FAILURE;

    }

 

    std::cout << "Ok" << std::endl;

    CloseHandle(h_slot);

    return EXIT_SUCCESS;

}

Именованные каналы

Начиная с Windows 2000, для связи между локальными процессами ОС Windows предоставляет именованные каналы.

Именованный канал — это механизм одностороннего или двустороннего обмена данными между сервером и одним или несколькими клиентами.

Этот API является одним из вариантов замены для устаревшего API удаленных почтовых ящиков.

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

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

Локально работающие каналы будут использовать общую память — ОС сама выбирает оптимальный транспорт, если иное не было указано явно.

Именованные каналы доступны удаленно, если запущена служба сервера.

Если именованный канал используется только локально, необходимо запретить доступ к NT_AUTHORITY\NETWORK или переключиться на локальный RPC, для чего достаточно остановить службу Server.

При этом каналы не ограничены потоком данных, они могут передавать и поток сообщений, то есть бывают двух типов:

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

Ориентированные на сообщения, в которых границы порций данных известны.

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

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

Каналы могут быть не только однонаправленные, но и двунаправленные, как показано на рис. 17.3.

Рис. 17.3. Двунаправленный канал

Именование

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

\\имя_сервера\pipe\имя_канала

Имя сервера — имя удаленного узла или точка, если необходимо указать локальный узел:

\\.\pipe\имя_канала

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

Имя канала может включать любой символ, кроме обратного слеша, и содержит максимум 256 символов.

API именованных каналов

При вызове функции CreateNamedPipe() сервер каналов указывает имя канала для создания одного или нескольких его экземпляров:

#include <winbase.h>

 

HANDLE CreateNamedPipe(

    LPCTSTR lpName,

    DWORD dwOpenMode,

    DWORD dwPipeMode,

    DWORD nMaxInstances,

    DWORD nOutBufferSize,

    DWORD nInBufferSize,

    DWORD nDefaultTimeOut,

    LPSECURITY_ATTRIBUTES lpSecurityAttributes

);

Параметры функции CreateNamedPipe():

lpName — уникальное название канала, например \\.\pipe\my_pipename.

• dwOpenMode — флаги открытия канала:

PIPE_ACCESS_DUPLEX — канал двунаправленный. Как серверные, так и клиентские процессы могут читать и записывать в него.

PIPE_ACCESS_INBOUND — поток данных в канале идет только от клиента к серверу.

PIPE_ACCESS_OUTBOUND — поток данных в канале идет только от сервера к клиенту.

dwPipeMode — режим канала:

PIPE_TYPE_BYTE или PIPE_TYPE_MESSAGE — режим записи. Байтовый поток или сообщения.

PIPE_READMODE_BYTE или PIPE_READMODE_MESSAGE — режим чтения. Байтовый поток или сообщения.

PIPE_WAIT или PIPE_NOWAIT — блокирующий или неблокирующий режим.

• PIPE_ACCEPT_REMOTE_CLIENTS или PIPE_REJECT_REMOTE_CLIENTS — принимать подключения от удаленных клиентов или отбрасывать.

• nMaxInstances — максимальное количество экземпляров, которые можно создать для этого канала. Значение может быть указано в первом экземпляре канала и должно быть указано для всех других экземпляров. Допустимые значения находятся в диапазоне от 1 до PIPE_UNLIMITED_INSTANCES, то есть неограниченного количества экземпляров.

• nOutBufferSize — количество байтов в буфере отправки.

• nInBufferSize — количество байтов в буфере приема.

• nDefaultTimeout — время ожидания в миллисекундах. Должно быть одинаково у каждого экземпляра. По умолчанию — 50 мс.

lpSecurityAttributes — необязательный указатель на параметры безопасности — структуру SECURITY_ATTRIBUTES.

Параметр dwOpenMode может дополнительно включать следующие флаги:

FILE_FLAG_FIRST_PIPE_INSTANCE — если попытаться создать несколько экземпляров канала с этим флагом, создание первого экземпляра завершится успешно, а попытка создания других экземпляров будет завершаться с ошибкой ERROR_ACCESS_DENIED.

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

FILE_FLAG_OVERLAPPED — будет использован перекрывающийся ввод-вывод.

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

Функция возвращает дескриптор канала или INVALID_HANDLE.

Как и при использовании ящиков, клиентский процесс может подключиться к именованному каналу с помощью функции CreateFile():

#include <fileapi.h>

 

...

 

HANDLE h_pipe = CreateFile(TEXT(R"(\\.\pipe\test_pipe)"),

                           GENERIC_READ | GENERIC_WRITE,

                           0, nullptr, OPEN_EXISTING, 0, nullptr);

Логика ее вызова достаточно своеобразная:

1. Если функция возвращает корректный хэндл, значит, канал подключен.

2. Когда функция вернет INVALID_HANDLE_VALUE, необходимо проверить код ошибки посредством вызова GetLastError().

3. Код, равный ERROR_PIPE_BUSY, говорит о том, что канал занят и требуется вызвать функцию WaitNamedPipe(), описанную ниже.

4. Если функция вернет истину, можно повторить процесс подключения.

Функция на стороне сервера, ожидающая и принимающая входящие соединения, — ConnectNamedPipe().

Для разрыва соединения используется функция DisconnectNamedPipe():

#include <namedpipeapi.h>

 

// Подключиться к существующему каналу.

bool ConnectNamedPipe(HANDLE hNamedPipe, LPOVERLAPPED lpOverlapped);

// Разорвать соединение.

bool DisconnectNamedPipe(HANDLE hNamedPipe);

Ее параметры:

hNamedPipe — дескриптор канала.

lpOverlapped — необязательный указатель на структуру для выполнения перекрывающегося ввода-вывода.

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

bool WaitNamedPipe(LPCTSTR lpNamedPipeName, DWORD nTimeOut);

В случае успеха функции вернут истину, в противном случае установят код ошибки, например ERROR_SEM_TIMEOUT при истечении тайм-аута.

Возможно одновременно использовать несколько экземпляров канала и работать с ними несколькими способами:

Создать отдельный поток для каждого экземпляра канала. Пример — многопоточный конвейерный сервер.

• Использовать перекрывающиеся операции, указав структуру OVERLAPPED в функциях ReadFile(), WriteFile() и ConnectNamedPipe().

Использовать перекрывающиеся операции, вызывая функции ReadFileEx() и WriteFileEx(), которые определяют процедуру, выполняемую после завершения операции.

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

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

Процесс может получить тип канала, размер входного и выходного буферов и максимальное количество экземпляров канала, используя функцию GetNamedPipeInfo():

#include <namedpipeapi.h>

 

bool GetNamedPipeInfo(

    HANDLE hNamedPipe,

    LPDWORD lpFlags,

    LPDWORD lpOutBufferSize,

    LPDWORD lpInBufferSize,

    LPDWORD lpMaxInstances

);

Ее параметры:

hNamedPipe — хэндл канала.

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

• PIPE_CLIENT_END или PIPE_SERVER_END — канал подключен как клиент или как сервер.

• PIPE_TYPE_BYTE или PIPE_TYPE_MESSAGE — канал байтовый или ориентирован на сообщения.

• lpOutBufferSize — указатель на переменную, в которую будет записано количество байтов в буфере исходящих данных.

• lpInBufferSize — указатель на переменную, в которую будет записано количество байтов в буфере входящих данных.

lpMaxInstances — указатель на переменную, в которую будет записано максимальное количество экземпляров канала, которые можно создать. Значение PIPE_UNLIMITED_INSTANCES означает неограниченное количество.

Функции GetNamedPipeHandleState() и SetNamedPipeHandleState() позволяют узнать либо установить режим чтения и ожидания канала, а также дополнительную информацию:

#include <namedpipeapi.h>

 

bool GetNamedPipeHandleState(

    HANDLE hNamedPipe,

    LPDWORD lpState,

    LPDWORD lpCurInstances,

    LPDWORD lpMaxCollectionCount,

    LPDWORD lpCollectDataTimeout,

    LPWSTR lpUserName,

    DWORD nMaxUserNameSize

);

 

bool SetNamedPipeHandleState(

    HANDLE hNamedPipe,

    LPDWORD lpMode,

    LPDWORD lpMaxCollectionCount,

    LPDWORD lpCollectDataTimeout

);

Параметры указанных функций:

hNamedPipe — хэндл канала.

lpState — указатель на переменную, содержащую текущее состояние канала:

PIPE_NOWAIT — канал в неблокирующем режиме.

PIPE_READMODE_MESSAGE — канал находится в режиме чтения сообщений, а не байтов из потока.

lpMode — указатель на комбинацию флага режима чтения и флага режима ожидания:

PIPE_READMODE_BYTE или PIPE_READMODE_MESSAGE — данные читаются как поток байтов или поток сообщений. Последний может быть включен только для каналов обмена сообщениями.

PIPE_WAIT или PIPE_NOWAIT — включение блокирующего или неблокирующего режима.

lpCurInstances — указатель на переменную, получающую количество экземпляров канала.

• lpMaxCollectionCount — указатель на переменную, содержащую максимальное количество байтов, которые буферизуются клиентом перед отправкой на сервер.

• lpCollectDataTimeout — указатель на переменную, содержащую максимальное количество миллисекунд до передачи данных по сети.

• lpUserName — указатель на буфер с именем пользователя.

nMaxUserNameSize — размер буфера lpUserName.

Для обмена данными используются функции ReadFile() и WriteFile(), а также прочие функции из состава файлового API.

Эффективный обмен можно производить через вызов функции TransactNamedPipe(), которая отправляет данные, после чего сразу принимает ответ:

#include <namedpipeapi.h>

 

bool TransactNamedPipe(

    HANDLE hNamedPipe,

    void* lpInBuffer,

    DWORD nInBufferSize,

    void* lpOutBuffer,

    DWORD nOutBufferSize,

    LPDWORD lpBytesRead,

    LPOVERLAPPED lpOverlapped

);

Параметры функции TransactNamedPipe():

hNamedPipe — хэндл канала.

• lpInBuffer — буфер, содержащий данные, записываемые в канал.

• nInBufferSize — размер буфера записываемых данных.

• lpOutBuffer — буфер, принимающий данные ответа из канала.

• nOutBufferSize — размер буфера, принимающего данные.

• lpBytesRead — указатель на переменную, в которой будет сохранено количество байтов, прочитанных из канала. Актуален, только если не используется перекрывающийся ввод-вывод.

lpOverlapped — указатель на структуру OVERLAPPED. Требуется, если канал был открыт с флагом FILE_FLAG_OVERLAPPED.

Функция CallNamedPipe() еще сильнее упрощает работу с каналом:

#include <winbase.h>

 

bool CallNamedPipe(

    LPCTSTR lpNamedPipeName,

    LPVOID lpInBuffer,

    DWORD nInBufferSize,

    LPVOID lpOutBuffer,

    DWORD nOutBufferSize,

    LPDWORD lpBytesRead,

    DWORD nTimeOut

);

Параметры функции CallNamedPipe():

lpNamedPipeName — имя канала.

• lpInBuffer — данные для записи в канал.

• nInBufferSize — размер буфера записи.

• lpOutBuffer — указатель на буфер, который получает данные, считанные из канала.

• lpBytesRead — указатель на переменную, получающую число байтов, прочитанных из канала.

nTimeOut — ожидание доступности канала в миллисекундах. Специальные значения — NMPWAIT_USE_DEFAULT_WAIT для ожидания по умолчанию или NMPWAIT_WAIT_FOREVER для бесконечного ожидания.

Она создаст канал, отправит данные и примет ответ. Если операция выполнена до истечения тайм-аута, функция вернет истину.

Следующая функция позволяет читать данные в буфер, не удаляя их оттуда:

#include <namedpipeapi.h>

 

bool PeekNamedPipe(

    HANDLE hNamedPipe,

    void* lpBuffer,

    DWORD nBufferSize,

    LPDWORD lpBytesRead,

    LPDWORD lpTotalBytesAvail,

    LPDWORD lpBytesLeftThisMessage

);

Ее параметры:

hNamedPipe — хэндл канала.

• lpBuffer — буфер, в который будут прочитаны данные.

• nBufferSize — размер буфера.

• lpBytesRead — указатель на число прочитанных байтов.

• lpTotalBytesAvail — общее количество байтов, которые могут быть прочитаны из буфера.

lpBytesLeftThisMessage — указатель на переменную, получающую количество байтов, оставшихся в этом сообщении.

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

На рис. 17.4 показан процесс работы с каналом. Сначала он создается на сервере, который ожидает подключений. Когда подключился клиент, начинается обмен данными, который завершается отключением сторон. Серверный цикл ожидания подключений будет продолжаться, чтобы принимать новые клиенты.

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

Рис. 17.4. Процесс работы с каналом

Пример работы с именованными каналами на C++

Рассмотрим, как работать с каналом, на примере. Для начала реализуем небольшую функцию, чтобы получить размер элемента контейнера:

// Хелпер для получения размера элемента контейнера.

template <typename T>

constexpr auto ElementSize(const T&)

{

    return sizeof(T::value_type);

}

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

Сервер начинается с создания нового именованного канала:

int main(int argc, char **argv)

{

    HANDLE h_pipe;

    using tstring = std::basic_string<TCHAR,

        std::char_traits<TCHAR>, std::allocator<TCHAR>>;

    const tstring message = TEXT("I'm server: ");

 

    std::array<TCHAR, 512> read_buffer;

 

    while (true)

    {

        static const tstring pipe_name = TEXT(R"(\\.\pipe\test_pipe)");

        // Создать именованный канал.

        h_pipe = CreateNamedPipe(pipe_name.c_str(), PIPE_ACCESS_DUPLEX,

            PIPE_TYPE_MESSAGE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES,

            message.size() * ElementSize(message),

            read_buffer.size() * ElementSize(message),

            NMPWAIT_USE_DEFAULT_WAIT, nullptr);

        // Канал удалось создать.

        if (h_pipe != INVALID_HANDLE_VALUE) break;

 

        // Если код ошибки не ERROR_PIPE_BUSY, завершиться.

        if (GetLastError() != ERROR_PIPE_BUSY)

        {

            std::cerr << "Could not open pipe." << std::endl;

            return EXIT_FAILURE;

        }

    }

Канал предпочтительнее создавать в цикле, ведь если экземпляров канала создано достаточно много, функция GetLastError() вернет ошибку ERROR_PIPE_BUSY. В этом случае попытку создания необходимо повторить. Сказанное справедливо и при открытии канала на клиенте, но его мы намеренно упростили.

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

    while (true)

    {

        std::cout << "Waiting for connections..." << std::endl;

        // Ожидать соединений от клиента.

        if (!ConnectNamedPipe(h_pipe, nullptr))

        {

            std::cerr << "Could not connect pipe" << std::endl;

            CloseHandle(h_pipe);

            return EXIT_FAILURE;

        }

 

        std::cout << "Pipe connected, reading data..." << std::endl;

 

        // Канал подключен.

        // Изменить режим канала на режим чтения сообщений.

        DWORD mode = PIPE_READMODE_MESSAGE;

        if (!SetNamedPipeHandleState(h_pipe, &mode, nullptr, nullptr))

        {

            std::cerr << "SetNamedPipeHandleState() failed." << std::endl;

            CloseHandle(h_pipe);

            return EXIT_FAILURE;

        }

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

Вызов этой функции мы показали для примера.

Данные из канала прочитаем, используя функцию ReadFile() из «обычного» файлового API:

        // Количество байтов, прочитанных из канала.

        DWORD read_bytes = 0;

        // Прочитать данные из канала.

        if (!ReadFile(h_pipe, read_buffer.data(),

                      read_buffer.size() * ElementSize(read_buffer),

                      &read_bytes, nullptr)

            && (GetLastError() != ERROR_MORE_DATA))

        {

            // Выйти, если ошибка не ERROR_MORE_DATA.

            DisconnectNamedPipe(h_pipe);

            CloseHandle(h_pipe);

            return EXIT_FAILURE;

        }

        else

        {

            std::cout

                << "Data read: "

                << std::string(read_buffer.begin(),

                               read_buffer.begin() + read_bytes)

                << std::endl;

        }

 

        // Количество прочитанных байтов используется, чтобы выделить

        // порцию данных в буфере.

        const auto new_msg = message +

            tstring(read_buffer.begin(), read_buffer.begin() + read_bytes);

Отправка данных выполняется функцией WriteFile():

        std::cout << "Sending data..." << std::endl;

        // Количество записанных байтов.

        DWORD written_bytes = 0;

 

        // Отправить данные в канал.

        if (!WriteFile(h_pipe, new_msg.c_str(),

                       new_msg.size() * ElementSize(new_msg), &written_bytes,

                       nullptr))

        {

            std::cout << "Failed to send data." << std::endl;

            DisconnectNamedPipe(h_pipe);

            CloseHandle(h_pipe);

            return EXIT_FAILURE;

        }

 

        std::cout << "Bytes sent: " << written_bytes << std::endl;

После этого упрощенный сервер завершает общение с клиентом — отправляет неотправленные данные и разрывает соединение:

        // Отправить неотправленные данные.

        if (!FlushFileBuffers(h_pipe))

        {

            std::cerr << "FlushFileBuffers() failed." << std::endl;

            DisconnectNamedPipe(h_pipe);

            CloseHandle(h_pipe);

            return EXIT_FAILURE;

 

        }

 

        // Отключить соединение.

        if (!DisconnectNamedPipe(h_pipe))

        {

            std::cerr << "Could not disconnect pipe." << std::endl;

            CloseHandle(h_pipe);

            return EXIT_FAILURE;

        }

    }

 

    CloseHandle(h_pipe);

 

    return EXIT_SUCCESS;

}

Клиент открывает канал, предоставленный сервером, и устанавливает его режим:

int main(int argc, char** argv)

{

    using tstring = std::basic_string<TCHAR, std::char_traits<TCHAR>,

                                      std::allocator<TCHAR>>;

    tstring message = TEXT("I'm message from client");

 

    if (argc > 1)

    {

        message = argv[1];

    }

    const tstring pipe_name = TEXT(R"(\\.\pipe\test_pipe)");

 

    // Открыть именованный канал.

    const HANDLE h_pipe = CreateFile(pipe_name.c_str(),

       GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);

 

    if (INVALID_HANDLE_VALUE == h_pipe)

    {

        std::cerr << "Pipe creating error!" << std::endl;

        return EXIT_FAILURE;

    }

 

    DWORD mode = PIPE_READMODE_MESSAGE;

    // Установить канал в режим чтения сообщений.

    if (!SetNamedPipeHandleState(h_pipe, &mode, nullptr, nullptr))

    {

        std::cerr << "SetNamedPipeHandleState failed." << std::endl;

        CloseHandle(h_pipe);

        return EXIT_FAILURE;

    }

Обмен данными клиент будет осуществлять, используя функцию TransactNamedPipe():

    // Количество прочитанных из канала байтов.

    DWORD read_bytes;

 

    // Отправить в канал сообщение и прочитать ответ.

    if (!TransactNamedPipe(h_pipe,

            const_cast<TCHAR*>(message.c_str()),

            message.size() * ElementSize(message),

            read_buffer.data(),

            read_buffer.size() * ElementSize(read_buffer),

            &read_bytes, nullptr)

        && (GetLastError() != ERROR_MORE_DATA))

    {

        CloseHandle(h_pipe);

        std::cerr << "TransactNamedPipe failed." << std::endl;

        return EXIT_FAILURE;

    }

    else

    {

        std::cout

            << "Data: "

            << std::string(read_buffer.begin(),

                           read_buffer.begin() + read_bytes)

            << std::endl;

    }

 

    CloseHandle(h_pipe);

    return EXIT_SUCCESS;

}

Сначала запустим сервер, а затем клиент, отправляющий данные серверу, который вернет их клиенту. Поэтому на стороне клиента будут выведены данные, которые отправил клиент:

D:\build\bin> b01-ch17-named-pipe-client.exe

Data: I'm server: I'm message from client

 

D:\build\bin> b01-ch17-named-pipe-client.exe test

Data: I'm server: test

Сервер выведет данные, принятые от клиента:

D:\build\bin> b01-ch17-named-pipe-server.exe

Waiting for the connections...

Pipe connected, reading data...

Data read: I'm message from client

Sending data...

Bytes sent: 35

Waiting for the connections...

Pipe connected, reading data...

Data read: test

Sending data...

Bytes sent: 16

Waiting for the connections...

Реализовать клиент можно еще проще, парой десятков строчек, если использовать функцию CallNamedPipe(), которая откроет канал и выполнит обмен данными:

if (!CallNamedPipe(pipe_name.c_str(),

                   const_cast<TCHAR*>(message.c_str()),

                   message.size() * ElementSize(message),

                   read_buffer.data(),

                   read_buffer.size() * ElementSize(read_buffer),

                   &read_bytes, 0))

{

    std::cerr << "CallNamedPipe failed." << std::endl;

    return EXIT_FAILURE;

}

else

{

    std::cout

        << "Data: "

        << std::string(read_buffer.begin(), read_buffer.begin() + read_bytes)

        << std::endl;

}

Пример работы с именованными каналами на Python

Перепишем на Python реализованный на C++ сервер именованных каналов, используя pywin32.

Создадим канал:

import sys

from win32.win32api import GetLastError

from win32.win32file import ReadFile, WriteFile, CloseHandle, FlushFileBuffers

from win32.win32pipe import *

from winerror import ERROR_MORE_DATA, ERROR_PIPE_BUSY

 

h_pipe = None

message = "I'm server: "

pipe_name = r'\\.\pipe\test_pipe'

buf_size = 1024

 

# Попытаться открыть канал.

while True:

    # Создать именованный канал.

    h_pipe = CreateNamedPipe(pipe_name, PIPE_ACCESS_DUPLEX,

                             PIPE_TYPE_MESSAGE | PIPE_WAIT,

                             PIPE_UNLIMITED_INSTANCES,

                             len(message) * 2, buf_size,

                             NMPWAIT_USE_DEFAULT_WAIT, None)

    # Канал удалось создать.

    if h_pipe:

        break

 

    # Если код ошибки не ERROR_PIPE_BUSY, завершиться.

    if GetLastError() != win32api.ERROR_PIPE_BUSY:

        print('Could not open pipe.')

        sys.exit(1)

Будем ожидать подключений от клиентов:

try:

    while True:

        print('Waiting for connections...')

        try:

            # Ожидать соединений от клиента.

            ConnectNamedPipe(h_pipe, None)

        except OSError:

            print('Could not connect pipe')

            sys.exit(1)

 

        print('Pipe connected, reading data...')

 

        # Канал подключен. Изменить режим канала на режим чтения сообщений.

        try:

            SetNamedPipeHandleState(h_pipe, PIPE_READMODE_MESSAGE, None, None)

        except BaseException:

            print('SetNamedPipeHandleState() failed')

            sys.exit(1)

Затем, когда клиент подключился, прочитаем от него данные и отправим ему ответ, после чего отключим соединение:

        # Прочитать данные из канала.

        result, data = ReadFile(h_pipe, buf_size)

        # Выйти, если ошибка не ERROR_MORE_DATA.

        if result and GetLastError() != ERROR_MORE_DATA:

            DisconnectNamedPipe(h_pipe)

            sys.exit(1)

        else:

            print(f'Data read: {data}')

            new_msg = f'{message}{data.decode()}'.encode()

            print('Sending data...')

            # Отправить данные в канал.

            err_code, written_bytes = WriteFile(h_pipe, new_msg)

 

        if not err_code and written_bytes:

            print(f'Bytes sent: {written_bytes}')

        else:

            print('Failed to send data.')

            sys.exit(1)

        # Отправить неотправленные данные.

        if FlushFileBuffers(h_pipe):

            print('FlushFileBuffers() failed.')

            sys.exit(1)

        # Отключить соединение.

        if DisconnectNamedPipe(h_pipe):

            print('Could not disconnect pipe.')

            sys.exit(1)

finally:

    CloseHandle(h_pipe)

Видим, что код практически такой же, как и на C++. Запустим пример и подключимся к нему, используя клиент C++, реализованный ранее:

D:\network-programming-book-code> py src\book01\ch17\python

\pywin32_namedpipe_server.py

 

Waiting for connections...

Pipe connected, reading data...

Data read: b"I'm message from client"

Sending data...

Bytes sent: 35

Waiting for connections...

Pipe connected, reading data...

Data read: b"I'm message from client"

Sending data...

Bytes sent: 35

Waiting for connections...

Результат такой же, как и при использовании сервера C++.

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

Резюме

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

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

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

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

Именованный канал — это примитив, обеспечивающий байтовый поток, который принимает удаленные подключения.

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

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

Вопросы и задания

1. Что такое почтовые ящики и как они работают?

2. Каким образом формируются имена почтовых ящиков?

3. Как создать почтовый ящик на сервере?

4. Как обратиться к почтовому ящику с клиента?

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

6. Какую информацию о почтовом ящике можно получить? Какие функции для этого используются?

7. Чем ограничен размер сообщения, которое может быть отправлено в почтовый ящик?

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

9. Что такое именованные каналы и как они работают?

10. Каким образом формируются имена каналов?

11. Какие существуют типы именованных каналов?

12. Как создать именованный канал на сервере?

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

14. Могут ли клиенты одновременно использовать один и тот же именованный канал?

15. О чем говорит ошибка ERROR_PIPE_BUSY при вызове CreateFile() для открытия именованного канала? Что необходимо сделать в этом случае?

16. Как получить данные из буфера канала, не удаляя их оттуда?

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

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

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


Назад: Глава 16. Сокетный API в ОС Windows
Дальше: Глава 18. Управление сетью в ОС Windows