• Анонимные каналы
  • Пример: перенаправление ввода/вывода с использованием анонимного канала
  • Именованные каналы
  • Использование именованных каналов
  • Создание именованных каналов
  • Подключение клиентов именованных каналов
  • Функции состояния именованных каналов
  • Функции подключения именованных каналов
  • Подключение клиентов и серверов именованных каналов
  • Функции транзакций именованных каналов
  • Определение наличия сообщений в именованных каналах
  • Пример: клиент-серверный процессор командной строки
  • Комментарии по поводу клиент-серверного процессора командной строки
  • Почтовые ящики
  • Использование почтовых ящиков
  • Создание и открытие почтового ящика
  • Создание, подключение и именование каналов и почтовых ящиков
  • Пример: сервер, обнаруживаемый клиентами
  • Комментарии по поводу многопоточных моделей
  • Резюме
  • В следующих главах
  • Упражнения
  • ГЛАВА 11

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

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

    Ниже вы ознакомитесь с последовательным межпроцессным взаимодействием (Interprocess Communication, IPC)[30], в котором используются объекты, подобные файлам. Двумя основными механизмами Windows, реализующими IPC, являются анонимные и именованные каналы, доступ к которым осуществляется с помощью уже известных вам функций ReadFile и WriteFile. Простые анонимные каналы являются символьными и работают в полудуплексном режиме. Эти свойства делают их удобными для перенаправления выходных данных одной программы на вход другой, как это обычно делается в UNIX. В первом примере демонстрируется, как реализовать эту возможность.

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

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

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

    Анонимные каналы (anonymous channels) Windows обеспечивают однонаправленное (полудуплексное) посимвольное межпроцессное взаимодействие. Каждый канал имеет два дескриптора: дескриптор чтения (read handle) и дескриптор записи (write handle). Функция, с помощью которой создаются анонимные каналы, имеет следующий прототип:

    BOOL CreatePipe(PHANDLE phRead, PHANDLE phWrite, LPSECURITY_ATTRIBUTES lpsa, DWORD cbPipe)

    Дескрипторы каналов часто бывают наследуемыми; причины этого станут понятными из приведенного ниже примера. Значение параметра cbPipe, указывающее размер канала в байтах, носит рекомендательный характер, причем значению 0 соответствует размер канала по умолчанию.

    Чтобы канал можно было использовать для IPC, должен существовать еще один процесс, и для этого процесса требуется один из дескрипторов канала. Предположим, например, что родительскому процессу, вызвавшему функцию CreatePipe, необходимо вывести данные, которые нужны дочернему процессу. Тогда возникает вопрос о том, как передать дочернему процессу дескриптор чтения (phRead). Родительский процесс осуществляет это, устанавливая дескриптор стандартного ввода в структуре STARTUPINFO для дочерней процедуры равным *phRead.

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

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

    Пример: перенаправление ввода/вывода с использованием анонимного канала

    В программе 11.1 представлен родительский процесс, который создает два процесса из командной строки и соединяет их каналом. Родительский процесс устанавливает канал и осуществляет перенаправление стандартного ввода/вывода. Обратите внимание на то, каким образом задается свойство наследования дескрипторов анонимного канала и как организуется перенаправление стандартного ввода/вывода на два дочерних процесса; эти методики описаны в главе 6.

    Местоположение оператора WriteFile в блоке Program2 на рис. 11.1 справа предполагает, что программа считывает большой объем данных, обрабатывает их, и лишь после этого записывает результаты. Эту запись можно было бы осуществлять и внутри цикла, выводя результаты после каждого считывания.

    Рис. 11.1. Межпроцессное взаимодействие с использованием анонимного канала


    Дескрипторы каналов и потоков должны закрываться при первой же возможности. На рис. 11.1 закрытие дескрипторов не отражено, однако это делается в программе 11.1. Родительский процесс должен закрыть дескриптор устройства стандартного вывода сразу же после создания первого дочернего процесса, чтобы второй процесс мог распознать метку конца файла, когда завершится выполнение первого процесса. В случае существования открытого дескриптора первого процесса второй процесс не смог бы завершиться, поскольку система не обозначила бы конец файла.

    В программе 11.1 используется непривычный синтаксис: две команды, разделенные символом =, обозначающим канал. Использование для этой цели символа вертикальной черты (|) привело бы к возникновению конфликта с системным командным процессором. Рисунок 11.1 является схематическим представлением выполнения следующей команды:

    $ pipe Program1 аргументы = Program2 аргументы

    При использовании средств командного процессора UNIX или Windows соответствующая команда имела бы следующий вид:

    $ Program1 аргументы | Program2 аргументы

    Программа 11.1. pipe: межпроцессное взаимодействие с использованием анонимных каналов 

    #include "EvryThng.h"

    int _tmain(int argc, LPTSTR argv[])

    /* Соединение двух команд с помощью канала в командной строке: pipe команда1 = команда2 */

    {

     DWORD i = 0;

     HANDLE hReadPipe, hWritePipe;

     TCHAR Command1[MAX_PATH];

     SECURITY_ATTRIBUTES PipeSA = /* Для наследуемых дескрипторов. */

      {sizeof(SECURITY_ATTRIBUTES), NULL, TRUE};

     PROCESS_INFORMATION ProcInfo1, ProcInfo2;

     STARTUPINFO StartInfoCh1, StartInfoCh2;

     LPTSTR targv = SkipArg(GetCommandLine());

     GetStartupInfo(&StartInfoCh1);

     GetStartupInfo(&StartInfoCh2);

     /* Найти символ "=", разделяющий две команды. */

     while (*targv != '=' && *targv != '\0') {

      Command1[i] = *targv;

      targv++;

      i++;

     }

     Command1[i] = '\0';

     /* Пропуск до начала второй команды. */

     targv = SkipArg(targv);

     CreatePipe(&hReadPipe, &hWritePipe, &PipeSA, 0);

     /* Перенаправить стандартный вывод и создать первый процесс. */

     StartInfoCh1.hStdInput = GetStdHandle(STD_INPUT_HANDLE);

     StartInfoCh1.hStdError = GetStdHandle(STD_ERROR_HANDLE);

     StartInfoCh1.hStdOutput = hWritePipe;

     StartInfoCh1.dwFlags = STARTF_USESTDHANDLES;

     CreateProcess(NULL, (LPTSTR)Command1, NULL, NULL, TRUE /* Унаследовать дескрипторы. */, 0, NULL, NULL, &StartInfoCh1, &ProcInfo1);

     CloseHandle(ProcInfo1.hThread);

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

     CloseHandle(hWritePipe);

     /* Повторить операции (симметричным образом) для второго процесса. */

     StartInfoCh2.hStdInput = hReadPipe;

     StartInfoCh2.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);

     StartInfoCh2.hStdError = GetStdHandle(STD_ERROR_HANDLE);

     StartInfoCh2.dwFlags = STARTF_USESTDHANDLES;

     CreateProcess(NULL, (LPTSTR)targv, NULL, NULL, TRUE, 0, NULL, NULL, &StartInfoCh2, &ProcInfo2);

     CloseHandle(ProcInfo2.hThread);

     CloseHandle(hReadPipe);

     /* Ожидать завершения первого и второго процессов. */

     WaitForSingleObject(ProcInfo1.hProcess, INFINITE);

     CloseHandle(ProcInfo1.hProcess);

     WaitForSingleObject(ProcInfo2.hProcess, INFINITE);

     CloseHandle(ProcInfo2.hProcess);

     return 0;

    }

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

    Именованные каналы (named pipes) предлагают ряд возможностей, которые делают их полезными в качестве универсального механизма реализации приложений на основе IPC, включая приложения, требующие сетевого доступа к файлам, и клиент-серверные системы[31], хотя для реализации простых вариантов IPC, ориентированных на байтовые потоки, как в предыдущем примере, в котором взаимодействие процессов ограничивается рамками одной системы, анонимных каналов вам будет вполне достаточно. К числу упомянутых возможностей (часть которых обеспечивается дополнительно) относятся следующие:

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

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

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

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

    • Имеется несколько вспомогательных и связных функций, упрощающих обслуживание взаимодействия "запрос/ответ" и клиент-серверных соединений.

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

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

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

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

    На рис. 11.2 в иллюстративной форме представлены отношения "клиент/сервер", а также псевдокод, отражающий одну из возможных схем применения именованных каналов. Обратите внимание, что сервер создает множество экземпляров одного и того же канала, каждый из которых обеспечивает поддержку одного клиента. Кроме того, для каждого экземпляра именованного канала сервер создает поток, так что для каждого клиента существует выделенный поток и экземпляр именованного канала. Следовательно, рис. 11.2 показывает, как реализовать модель многопоточного сервера, впервые представленную на рис. 7.1.

    Рис. 11.2. Взаимодействие клиентов с сервером через именованные каналы

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

    Серверами именованных каналов могут быть только системы на основе Windows NT (как обычно, здесь имеются в виду версия 4.0 и последующие); системы на базе Windows 9x могут выступать только в роли клиентов.

    Прототип функции CreateNamedPipe представлен ниже.

    HANDLE CreateNamedPipe(LPCTSTR lpName, DWORD dwOpenMode, DWORD dwPipeMode, DWORD nMaxInstances, DWORD nOutBufferSize, DWORD nInBufferSize, DWORD nDefaultTimeOut, LPSECURITY ATTRIBUTES lpSecurityAttributes)
     

    Параметры

    lpName — указатель на имя канала, который должен иметь следующую форму:

    \\.\pipe\[path]pipename

    Точка (.) обозначает локальный компьютер; таким образом, создать канал на удаленном компьютере невозможно. 

    dwOpenMode — указывает один из следующих флагов:

    • PIPE_ACCESS_DUPLEX — этот флаг эквивалентен комбинации значений GENERIC_READ и GENERIC_WRITE.

    • PIPE_ACCESS_INBOUND — данные могут передаваться только в направлении от клиента к серверу; эквивалентно GENERIC_READ.

    • PIPE_ACCESS_OUTBOUND — этот флаг эквивалентен GENERIC_WRITE.

    При задании режима могут также указываться значения FILE_FLAG_WRITE_THROUGH (не используется с каналами сообщений) и FILE_FLAG_OVERLAPPED (перекрывающиеся операции рассматриваются в главе 14).

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

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

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

    • PIPE_WAIT и PIPE_NOWAIT — определяют, соответственно, будет или не будет блокироваться операция ReadFile. Следует использовать значение PIPE_WAIT, поскольку для обеспечения асинхронного ввода/вывода существуют лучшие способы.

    nMaxInstances — определяет количество экземпляров каналов, а следовательно, и количество одновременно поддерживаемых клиентов. Как показано на рис. 11.2, при каждом вызове функции CreateNamedPipe для данного канала должно использоваться одно и то же значение. Чтобы предоставить ОС возможность самостоятельно определить значение этого параметра на основании доступных системных ресурсов, следует указать значение PIPE_UNLIMITED_INSTANCES.

    nOutBufferSize и nInBufferSize — позволяют указать размеры (в байтах) выходного и входного буферов именованных каналов. Чтобы использовать размеры буферов по умолчанию, укажите значение 0.

    nDefaultTimeOut — длительность интервала ожидания по умолчанию (в миллисекундах) для функции WaitNamedPipe, которая обсуждается в следующем разделе. Эта ситуация, в которой функция, создающая объект, устанавливает интервал ожидания для родственной функции, является уникальной.

    В случае ошибки возвращается значение INVALID_HANDLE_VALUE, поскольку дескрипторы каналов аналогичны дескрипторам файлов. При попытке создания именованного канала под управлением Windows 9x, которая не может выступать в качестве сервера именованных каналов, возвращаемым значением будет NULL, что может стать причиной недоразумений.

    lpSecurityAttributes — имеет тот же смысл, что и в случае любой функции, создающей объект.

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

    Подключение клиентов именованных каналов

    Как показано на рис. 11.2, для подключения клиента к именованному каналу применяется функция CreateFile, при вызове которой указывается имя именованного канала. Часто клиент и сервер выполняются на одном компьютере, и в этом случае для указания имени канала используется следующая форма:

    \\.\pipe\[path]pipename

    Если сервер находится на другом компьютере, для указания имени канала используется следующая форма:

    \\servername\pipe\[path]pipename

    Использование точки (.) вместо имени локального компьютера в случае, когда сервер является локальным, позволяет значительно сократить время подключения.

    Функции состояния именованных каналов

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

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

    • SetNamedPipeHandleState — позволяет программе устанавливать атрибуты состояния. Параметр режима (NpMode) передается не по значению, а по адресу, что может стать причиной недоразумений. Применение этой функции демонстрируется в программе 11.2.

    • GetNamedPipeInfo — определяет, принадлежит ли дескриптор экземпляру клиента или сервера, размеры буферов и тому подобное.

    Функции подключения именованных каналов

    После создания именованного канала сервер может ожидать подключения клиента (осуществляемого с помощью функции CreateFile или функции CallNamedFile, описанной далее в этой главе), используя для этого функцию ConnectNamedPipe, которая является серверной функцией лишь в случае Windows NT: 

    Bool ConnectNamedPipe(HANDLE hNamedPipe, LPOVERLAPPED lpOverlapped)
     

    Если параметр lpOverlapped установлен равным NULL, то функция ConnectNamedPipe осуществляет возврат сразу же после установления соединения с клиентом. В случае успешного выполнения функции возвращаемым значением является TRUE. Если же подключение клиента происходит в течение промежутка времени между вызовами сервером функций CreateNamedPipe и ConnectNamed-Pipe, то возвращаемым значением будет FALSE. В этом случае функция GetLastError вернет значение ERROR_PIPE_CONNECTED.

    После возврата из функции ConnectNamedPipe сервер может выполнять чтение запросов с помощью функции ReadFile и запись ответов посредством функции WriteFile. Наконец, сервер должен вызвать функцию DisconnectNamedPipe, чтобы освободить дескриптор (экземпляра канала) для соединения с другим возможным клиентом.

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

    Подключение клиентов и серверов именованных каналов

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

    /* Последовательность операций при создании соединения с использованием именованного канала для сервера. */

    hNp = CreateNamedPipe("\\\\.\\pipe\\my_pipe", …);

    while (… /* Цикл продолжается вплоть до завершения работы сервера.*/) {

     ConnectNamedPipe(hNp, NULL);

     while (ReadFile(hNp, Request, …) {

      …

      WriteFile(hNp, Response, …);

     }

     DisconnectNamedPipe(hNp);

    }

    CloseHandle(hNp);

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

    /* Последовательность операций при создании соединения с использованием именованного канала для клиента. */

    WaitNamedPipe("\\\\ServerName\\pipe\\my_pipe", NMPWAIT_WAIT_FOREVER);

    hNp = CreateFile("\\\\ServerName\\pipe\\my_pipe", …);

    while (…/*Цикл выполняется до тех пор, пока не прекратятся запросы.*/ {

     WriteFile(hNp, Request, …);

     …

     ReadFile(hNp, Response);

    }

    CloseHandle (hNp); /* Разорвать соединение с сервером. */

    Обратите внимание, что клиент и сервер состязаются за ресурсы. Прежде всего, клиентский вызов функции WaitNamedPipe завершится ошибкой, если именованный канал к этому моменту еще не был создан сервером; для краткости тестирование успешности выполнения в нашем примере опущено, однако оно включено в примеры программ, доступные на Web-сайте. Далее, в редких случаях вызов CreateFile может быть выполнен еще до того, как сервер вызовет функцию ConnectNamedPipe. В этом случае функция ConnectNamedPipe вернет серверу значение FALSE, однако взаимодействия посредством именованного канала по-прежнему будет функционировать надлежащим образом. 

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

    Функции транзакций именованных каналов

    На рис. 11.2 показана типичная конфигурация клиента, в которой клиент выполняет следующие операции:

    • Открывает экземпляр канала, создавая долговременное соединение с сервером и занимая экземпляр канала.

    • Периодически посылает запросы и ожидает получения ответов.

    • Закрывает соединение.

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

    BOOL TransactNamedPipe(HANDLE hNamedPipe, LPVOID lpWriteBuf, DWORD cbWriteBuf, LPVOID lpReadBuf, DWORD cbReadBuf, LPDWORD lpcbRead, LPOVERLAPPED lpOverlapped)
     

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

    Функция TransactNamedPipe удобна в использовании, однако, как показывает рис. 11.2, она требует создания постоянного соединения, что ограничивает число возможных клиентов[32].

    Ниже приводится прототип второй клиентской вспомогательной функции.  

    BOOL CallNamedPipe(LPCTSTR lpPipeName, LPVOID lpWriteBuf, DWORD cbWriteBuf, LPVOID lpReadBuf, DWORD cbReadBuf, LPDWORD lpcbRead, DWORD dwTimeOut)
     

    Функция CallNamedPipe не требует образования постоянного соединения; вместо этого она создает временное соединение, объединяя в себе выполнение следующей последовательности операций:

    CreateFile

    WriteFile

    ReadFile

    CloseHandle

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

    Смысл параметров этой функции тот же, что и в случае функции TransactNamedPipe, если не считать того, что вместо дескриптора для указания канала используется его имя. Функция CallNamedPipe выполняется синхронном режиме (отсутствует структура OVERLAPPED). Указываемая при ее вызове длительность периода ожидания (dwTimeOut) (в миллисекундах) относится к соединению, а не транзакции. Параметр dwTimeOut имеет три специальных значения:

    • NMPWAIT_NOWAIT

    • NMPWAIT_WAIT_FOREVER

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

    Определение наличия сообщений в именованных каналах

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

    BOOL PeekNamedPipe(HANDLE hPipe, LPVOID lpBuffer, DWORD cbBuffer, LPDWORD lpcbRead, LPDWORD lpcbAvail, LPDWORD lpcbMessage) 

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

    Чтобы определить, имеются ли в канале данные, необходимо проверить значение *lpcbAvail; если данные в канале присутствуют, оно должно быть больше 0. В этом случае параметры lpBuffer и lpcbRead могут иметь значения NULL. Если же буфер определен параметрами lpBuffer и cbBuffer, то значение *lpcbMessage укажет вам, остается ли еще некоторое количество байтов сообщений, которые не умещаются в буфере, что позволяет распределять буфер большего размера, прежде чем осуществлять чтение из именованного канала. Для канала, работающего в режиме считывания байтов, это значение равно 0.

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

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

    • Каналы FIFO являются полудуплексными.

    • Каналы FIFO действуют только в пределах одного компьютера.

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

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

    Функция UNIX mkfifo является ограниченной версией функции CreateNamedFile.

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

    Пример: клиент-серверный процессор командной строки

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

    • С сервером могут взаимодействовать несколько клиентов.

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

    • Сервер является многопоточным, причем каждому именованному каналу назначается отдельный поток. Это означает, что существует пул потоков (thread pool), в который входят рабочие потоки, готовые к использованию подключающимися клиентами. Рабочие потоки предоставляются клиентам посредством экземпляра именованного канала, который система выделяет клиенту.

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

    В программе 11.2 представлен однопоточной клиент, а в программе 11.3 — его сервер. Сервер соответствует модели, представленной на рисунках 7.1 и 11.2. Запросом клиента является обычная командная строка. Ответом сервера является результирующий вывод, который посылается в виде нескольких сообщений. Кроме того, в программе используется находящийся на Web-сайте заголовочный файл ClntSrvr.h, в котором определены структуры данных запроса и ответа, а также имена каналов клиента и сервера.

    В программе 11.2 клиент вызывает функцию LocateServer, которая находит имя канала сервера. Функция LocateServer использует почтовый ящик (mailslot), описанный в одном из последующих разделов и представленный в программе 11.5.

    В объявлениях записей имеются поля длины, тип которых определен как DWORD32; это сделано для того, чтобы программы, получая возможность их последующего перенесения на платформу Win64, могли взаимодействовать с серверами и клиентами, выполняющимися под управлением любой системы Windows.

    Программа 11.2. clientNP: клиент, ориентированный на соединение посредством именованного канала 

    /* Глава 11. Клиент-серверная система. ВЕРСИЯ КЛИЕНТА.

       clientNP — клиент, ориентированный на установку соединения. */

    /* Выполнить командную строку (на сервере); отобразить ответ. */

    /* Клиент создает долговременное соединение с сервером (захватывая */

    /* экземпляр канала) и выводит приглашение пользователю для ввода команд.*/


    #include "EvryThng.h"

    #include "ClntSrvr.h" /* Определяет структуры записей запроса и ответа. */


    int _tmain(int argc, LPTSTR argv[]) {

     HANDLE hNamedPipe = INVALID_HANDLE_VALUE;

     TCHAR PromptMsg[] = _T("\nВведите команду: ");

     TCHAR QuitMsg[] = _T("$Quit");

     TCHAR ServerPipeName[MAX_PATH];

     REQUEST Request; /* См. файл ClntSrvr.h. */

     RESPONSE Response; /* См. файл ClntSrvr.h. */

     DWORD nRead, nWrite, NpMode = PIPE_READMODE_MESSAGE | PIPE_WAIT;

     LocateServer(ServerPipeName);

     /* Ожидать появления экземпляра именованного канала и "вступить в борьбу" за право его открытия. */

     while (INVALID_HANDLE_VALUE == hNamedPipe) {

      WaitNamedPipe(ServerPipeName, NMPWAIT_WAIT_FOREVER);

      hNamedPipe = CreateFile(ServerPipeName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

     }

     /* Задать блокирование дескриптора именованного канала; режим сообщений.*/

     SetNamedPipeHandleState(hNamedPipe, &NpMode, NULL, NULL);

     /* Вывести приглашение пользователю для ввода команд. Завершить выполнение по получении команды "$quit." */

     while (ConsolePrompt(PromptMsg, Request.Record, MAX_RQRS_LEN, TRUE) && (_tcscmp(Request.Record, QuitMsg) != 0)) {

      WriteFile(hNamedPipe, &Request, RQ_SIZE, &nWrite, NULL);

      /* Считать каждый ответ и направить его на стандартный вывод.

          Response.Status = 0 означает "конец ответного сообщения." */

      while (ReadFile(hNamedPipe, &Response, RS_SIZE, &nRead, NULL) && (Response.Status == 0)) _tprintf(_T("%s"), Response.Record);

     }

     _tprintf(_T("Получена команда завершения работы. Соединение разрывается."));

     CloseHandle(hNamedPipe);

     return 0;

    }

    Программа 11.3 — это серверная программа, включающая функцию потока сервера, которая обрабатывает запросы, генерируемые с помощью программы 11.2. Кроме того, сервер создает "широковещательный серверный поток" ("server broadcast thread") (см. программу 11.4), который используется для широковещательной рассылки имени своего канала всем клиентам, желающим подключиться, посредством почтового ящика. В программе 11.2 вызывается функция LocateServer, представленная в программе 11.5, которая считывает информацию, отправленную данным процессом. Почтовые ящики описываются далее в настоящей главе.

    Хотя соответствующий код и не включен в программу 11.4, в ней предусмотрена возможность защиты сервером (представлен на Web-сайте) своего именованного канала с целью предотвращения доступа к нему клиентов, не имеющих должных полномочий. Вопросы безопасности объектов рассматриваются в главе 15, где будет также показано, как использовать указанную возможность.

    Программа 11.3. serverNP: многопоточный сервер именованного канала 

    /* Глава 11. ServerNP. */

    /* Многопоточный сервер командной строки. Версия на основе именованных каналов. */


    #include "EvryThng.h"

    #include "ClntSrvr.h" /* Определения сообщений запроса и ответа. */


    typedef struct { /* Аргумент серверного потока. */

     HANDLE hNamedPipe; /* Экземпляр именованного канала. */

     DWORD ThreadNo;

     TCHAR TmpFileName[MAX_PATH]; /* Имя временного файла. */

    } THREAD_ARG;


    typedef THREAD_ARG *LPTHREAD_ARG;

    volatile static BOOL ShutDown = FALSE;

    static DWORD WINAPI Server(LPTHREAD_ARG);

    static DWORD WINAPI Connect(LPTHREAD_ARG);

    static DWORD WINAPI ServerBroadcast(LPLONG);

    static BOOL WINAPI Handler(DWORD);

    static TCHAR ShutRqst[] = _T("$ShutDownServer");


    _tmain(int argc, LPTSTR argv[]) {

     /* Определение MAX_CLIENTS содержится в файле ClntSrvr.h. */

     HANDLE hNp, hMonitor, hSrvrThread[MAXCLIENTS];

     DWORD iNp, MonitorId, ThreadId;

     LPSECURITY_ATTRIBUTES pNPSA = NULL;

     THREAD_ARG ThArgs[MAXCLIENTS];

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

     SetConsoleCtrlHandler(Handler, TRUE);

     /* Периодически создавать имя широковещательного канала потока. */

     hMonitor = (HANDLE)_beginthreadex(NULL, 0, ServerBroadcast, NULL, 0, &MonitorId);

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

     for (iNp = 0; iNp < MAX_CLIENTS; iNp++) {

      hNp = CreateNamedPipe(SERVER_PIPE, PIPE_ACCESS_DUPLEX, PIPE_READMODE_MESSAGE | PIPE_TYPE_MESSAGE | PIPE_WAIT, MAXCLIENTS, 0, 0, INFINITE, pNPSA);

      ThArgs[iNp].hNamedPipe = hNp;

      ThArgs[iNp].ThreadNo = iNp;

      GetTempFileName(_T("."), _T("CLP"), 0, ThArgs[iNp].TmpFileName);

      hSrvrThread[iNp] = (HANDLE)_beginthreadex(NULL, 0, Server, &ThArgs[iNp], 0, &ThreadId);

     }

     /* Ждать завершения выполнения всех потоков. */

     WaitForMultipleObjects(MAXCLIENTS, hSrvrThread, TRUE, INFINITE);

     WaitForSingleObject(hMonitor, INFINITE);

     CloseHandle(hMonitor);

     for (iNp = 0; iNp < MAXCLIENTS; iNp++) {

      /* Закрыть дескрипторы канала и удалить временные файлы. */

      CloseHandle(hSrvrThread[iNp]);

      DeleteFile(ThArgs[iNp].TmpFileName);

     }

     _tprintf(_T("Серверный процесс завершил выполнение.\n"));

     return 0;

    }


    static DWORD WINAPI Server(LPTHREAD_ARG pThArg)

    /* Функция потока сервера; по одной для каждого потенциального клиента. */

    {

     HANDLE hNamedPipe, hTmpFile = INVALID_HANDLE_VALUE, hConTh, hClient;

     DWORD nXfer, ConThId, ConThStatus;

     STARTUPINFO StartInfoCh;

     SECURITY_ATTRIBUTES TempSA = {sizeof(SECURITY_ATTRIBUTES), NULL, TRUE};

     PROCESS_INFORMATION ProcInfo;

     FILE *fp;

     REQUEST Request;

     RESPONSE Response;

     GetStartupInfo(&StartInfoCh);

     hNamedPipe = pThArg->hNamedPipe;

     hTmpFile = CreateFile(pThArg->TmpFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &TempSA, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL);

     while (!ShutDown) { /* Цикл соединений. */

      /* Создать поток соединения; ждать его завершения. */

      hConTh = (HANDLE)_beginthreadex(NULL, 0, Connect, pThArg, 0, &ConThId);

      /* Ожидание соединения с клиентом и проверка флага завершения работы.*/

      while (!ShutDown && WaitForSingleObject(hConTh, CS_TIMEOUT) == WAIT_TIMEOUT) { /* Пустое тело цикла. */ }; 

      CloseHandle(hConTh);

      if (ShutDown) continue; /*Флаг может быть установлен любым потоком.*/

      /* Соединение существует. */

      while (!ShutDown && ReadFile(hNamedPipe, &Request, RQ_SIZE, &nXfer, NULL)) {

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

       ShutDown = ShutDown || (_tcscmp(Request.Record, ShutRqst) == 0);

       if (ShutDown) continue; /* Проверяется на каждой итерации. */

       /* Создать процесс для выполнения команды. */

       StartInfoCh.hStdOutput = hTmpFile;

       StartInfoCh.hStdError = hTmpFile;

       StartInfoCh.hStdInput = GetStdHandle(STD_INPUT_HANDLE);

       StartInfoCh.dwFlags = STARTF_USESTDHANDLES;

       CreateProcess(NULL, Request.Record, NULL, NULL, TRUE, /* Унаследовать дескрипторы. */

        0, NULL, NULL, &StartInfoCh, &ProcInfo);

       /* Выполняется процесс сервера. */

       CloseHandle(ProcInfo.hThread);

       WaitForSingleObject(ProcInfo.hProcess, INFINITE);

       CloseHandle(ProcInfo.hProcess);

       /* Отвечать по одной строке за один раз. Здесь удобно использовать функции библиотеки С для работы со строками. */

       fp = _tfopen(pThArg->TmpFileName, _T("r"));

       Response.Status = 0;

       while(_fgetts(Response.Record, MAX_RQRS_LEN, fp) != NULL) WriteFile(hNamedPipe, &Response, RS_SIZE, &nXfer, NULL);

       FlushFileBuffers(hNamedPipe);

       fclose(fp);

       /* Уничтожить содержимое временного файла. */

       SetFilePointer(hTmpFile, 0, NULL, FILE_BEGIN);

       SetEndOfFile(hTmpFile);

       /* Отправить признак конца ответа. */

       Response.Status = 1;

       strcpy(Response.Record, "");

       WriteFile(hNamedPipe, &Response, RS_SIZE, &nXfer, NULL);

      }

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

      /* Принудительно завершить выполнение потока, если он все еще активен.*/

      GetExitCodeThread(hConTh, &ConThStatus);

      if (ConThStatus == STILL_ACTIVE) {

       hClient = CreateFile(SERVER_PIPE, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN EXISTING, FILE ATTRIBUTE NORMAL, NULL); 

       if (hClient != INVALID_HANDLE_VALUE) CloseHandle (hClient);

       WaitForSingleObject (hConTh, INFINITE);

      }

      /* Клиент отсоединился или имеется запрос останова. */

      FlushFileBuffers(hNamedPipe);

      DisconnectNamedPipe(hNamedPipe);

     }

     /* Конец командного цикла. Освободить ресурсы; выйти из потока. */

     if (hTmpFile != INVALID_HANDLE_VALUE) CloseHandle(hTmpFile);

     DeleteFile(pThArg->TmpFileName);

     _tprintf(_T("Выход из потока номер %d\n"), pThArg->ThreadNo);

     _endthreadex(0);

    }


    static DWORD WINAPI Connect(LPTHREAD_ARG pThArg) {

     /* Поток соединения разрешает серверу опрос флага ShutDown. */

     ConnectNamedPipe(pThArg->hNamedPipe, NULL);

     _endthreadex(0);

     return 0;

    }


    BOOL WINAPI Handler(DWORD CtrlEvent) {

     /* Завершить работу системы. */

     ShutDown = TRUE;

     return TRUE;

    }

    Комментарии по поводу клиент-серверного процессора командной строки

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

    • Соединяться с сервером и выполнять параллельные запросы могут сразу несколько серверов; каждому клиенту назначается серверный (или рабочий) поток, выделяемый из пула потоков.

    • Сервер и клиенты могут выполняться либо в ответ на отдельные подсказки командной строки, либо под управлением программы JobShell (программа 6.3).

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

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

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

    • Каждый рабочий поток сервера создает простой поток, осуществляющий соединение, который вызывает функцию ConnectNamedPipe и завершает выполнение сразу же после подключения клиента. Это позволяет организовать ожидание дескриптора потока соединения рабочим потоком с использованием конечного интервала ожидания и периодическое тестирование глобального флага завершения работы (ShutDown). Если бы рабочие потоки блокировались при выполнении функции ConnectNamedPipe, они не могли бы тестировать этот флаг, и сервер не мог бы завершить работу. По этой причине поток сервера осуществляет вызов CreateFile, используя дескриптор именованного канала, чтобы заставить поток соединения возобновиться и завершить выполнение. Альтернативным вариантом было бы использование асинхронного ввода/вывода (глава 14), что дало бы возможность связать событие с вызовом функции ConnectNamedPipe. Другие возможные варианты реализации и дополнительная информация предоставляются в комментариях к исходному тексту программы, размещенному на Web-сайте книги. Без этого решения потоки соединения могли бы никогда не завершить работу самостоятельно, что привело бы к утечке ресурсов в DLL. Этот вопрос обсуждается в главе 12.

    • Существует ряд благоприятных предпосылок для усовершенствования данной системы. Например, можно предусмотреть опцию выполнения внутрипроцессного сервера (in-process server), используя библиотеку DLL, которая реализует некоторые из команд. Это усовершенствование вводится в программу в главе 12.

    • Количество серверных потоков ограничивается при вызове функции WaitForMultipleObjects в основном потоке. Хотя это ограничение легко преодолимо, в данном случае система не обладает истинной масштабируемостью; как было показано в главе 10, чрезмерное увеличение количества потоков может оказать отрицательное влияние на производительность. В главе 14 для решения этой проблемы используются порты асинхронного ввода/вывода.

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

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

    • Почтовые ящики являются однонаправленными.

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

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

    • Почтовые ящики могут находиться в любом месте сети.

    • Размер сообщений ограничен.

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

    • Каждый сервер создает дескриптор почтового ящика с помощью функции CreateMailSlot.

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

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

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

    Существует еще одна возможность. В вызове функции CreateFile клиент может указать имя почтового ящика в следующем виде:

    \\*\mailslot\mailslotname

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

    Использование почтовых ящиков

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

    Сервер приложения (application server), действуя в качестве почтового клиента (mailslot client), периодически осуществляет широковещательную рассылку своего имени и имени именованного канала. Любой клиент приложения (application client), которому требуется найти сервер, может получить это имя, действуя в качестве сервера почтовых ящиков (mailslot server). Аналогичным образом сервер командной строки может периодически осуществлять широковещательную рассылку своего состояния, включая информацию о коэффициенте использования, клиентам. Это соответствует ситуации, в которой имеется одна записывающая программа (почтовый клиент) и несколько считывающих программ (почтовых серверов). Если бы почтовых клиентов (то есть серверов приложения) было несколько, то ситуация описывалась бы отношением типа "многие ко многим".

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

    Обмен ролями терминов клиент и сервер в данном контексте может несколько сбивать с толку, однако заметьте, что сервер именованного канала и почтовый сервер выполняют вызовы функций CreateNamedPipe (или CreateMailSlot), тогда как клиент (именованного канала или почтового ящика) создает соединение, используя функцию CreateFile. Кроме того, в обоих случаях первый вызов функции WriteFile выполняется клиентом, а первый вызов функции ReadFile выполняется сервером.

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

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

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

    Рис. 11.3. Использование клиентами почтового ящика для обнаружения сервера


    HANDLE CreateMailslot(LPCTSTR lpName, DWORD cbMaxMsg, DWORD dwReadTimeout, LPSECURITY_ATTRIBUTES lpsa) 

    Параметры

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

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

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

    cbMaxMsg — максимальный размер сообщения (в байтах), которые может записывать клиент. Значению 0 соответствует отсутствие ограничений.

    dwReadTimeOut — длительность интервала ожидания (в миллисекундах) для операции чтения. Значению 0 соответствует немедленный возврат, а значению MAILSLOT_WAIT_FOREVER — неопределенный период ожидания (который может длиться сколь угодно долго). 

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

    • \\ .\mailslot\ [путь]имя — определяет локальный почтовый ящик. Примечание. В Windows 95 длина имени ограничена 11 символами.

    • \\имя_компьютера\mailslot\[путь]имя — определяет почтовый ящик, расположенный на компьютере с заданным именем.

    • \\имя_домена\mailslot\[путь]имя — определяет все почтовые ящики с данным именем, расположенные на компьютерах, принадлежащих данному домену. В этом случае максимальный размер сообщения составляет 424 байта.

    • \\*\mailslot\[путь]имя — определяет все почтовые ящики с данным именем, расположенные на компьютерах, принадлежащих главному домену системы. В этом случае максимальный размер сообщения составляет 424 байта.

    Наконец, клиент должен указывать флаг FILE_SHARE_READ. Функции GetMailslotInfo и SetMailslotInfо похожи на свои аналоги, работающие с именованными каналами.

    Средства, сопоставимые с почтовыми ящиками, в UNIX отсутствуют. Однако для этой цели могут быть использованы широковещательные (broadcast) или групповые (multicast) дейтаграммы протокола TCP/IP.

    Создание, подключение и именование каналов и почтовых ящиков

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

    Аналогичная информация для почтовых ящиков приведена в табл. 11.2. Вспомните, что почтовый клиент (или сервер) не обязательно должен выполняться тем же процессом или даже на той же системе, что и клиент (или сервер) приложения.


    Таблица 11.1. Именованные каналы: создание, подключение и именование  

    Клиент приложения Сервер приложения
    Дескриптор именованного канала или соединение CreateFile CallNamedPipe TransactNamedPipe CreateNamedPipe
    Имя канала \\.\имя канала (канал является локальным) \\имя системы\имя канала (канал является локальным или удаленным) \\.\имя канала (канал создается локальным)

    Таблица 11.2. Почтовые ящики: создание, подключение и именование  

    Почтовый клиент Почтовый сервер
    Дескриптор почтового ящика CreateFile CreateMailslot
    Имя почтового ящика \\.\имя почтового ящика (почтовый ящик является локальным) \\имя системы\имя почтового ящика (почтовый ящик располагается на указанной удаленной системе) \\*\имя почтового ящика (все почтовые ящики, имеющие одно и то же указанное имя) \\.\имя почтового ящика (почтовый ящик создается локальным)

    Пример: сервер, обнаруживаемый клиентами

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

    Примечание

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

    Программа 11.4. SrvrBcst: функция потока почтового клиента 

    static DWORD WINAPI ServerBroadcast(LPLONG pNull) {

     MS_MESSAGE MsNotify;

     DWORD nXfer;

     HANDLE hMsFile;

     /*Открыть почтовый ящик для записывающей программы почтового "клиента"*/

     while (!ShutDown) { /* Цикл выполняется до тех пор, пока имеются серверные потоки. */

      /* Ждать, пока другой клиент не откроет почтовый ящик. */

      Sleep(CS_TIMEOUT);

      hMsFile = CreateFile(MS_CLTNAME, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN EXISTING, FILE ATTRIBUTE NORMAL, NULL); 

      if (hMsFile == INVALID_HANDLE_VALUE) continue;

      /* Отправить сообщение в почтовый ящик. */

      MsNotify.msStatus = 0;

      MsNotify.msUtilization = 0;

      _tcscpy(MsNotify.msName, SERVER_PIPE);

      if (WriteFile(hMsFile, &MsNotify, MSM_SIZE, &nXfer, NULL)) ReportError(_T("Ошибка записи почтового сервера."), 13, TRUE);

      CloseHandle(hMsFile);

     }

     _tprintf(_T("Закрытие контролирующего потока.\n"));

     _endthreadex(0);

     return 0;

    }
     

    В программе 11.5 представлена функция, которая вызывается клиентом (см. программу 11.2) для обнаружения сервера.

    Программа 11.5. LocSrvr: почтовый сервер 

    /* Глава 11. LocSrver.c */

    /* Найти сервер путем считывания информации из почтового ящика, используемого для широковещательной рассылки имен серверов. */


    #include "EvryThng.h"

    #include "ClntSrvr.h" /* Определяет имя почтового ящика. */


    BOOL LocateServer(LPTSTR pPipeName) {

     HANDLE MsFile;

     MS_MESSAGE ServerMsg;

     BOOL Found = FALSE;

     DWORD cbRead;

     MsFile = CreateMailslot(MS_SRVNAME, 0, CS_TIMEOUT, NULL);

     while (!Found) {

      _tprintf(_T("Поиск сервера.\n"));

      Found = ReadFile(MsFile, &ServerMsg, MSM_SIZE, &cbRead, NULL);

     }

     _tprintf(_T("Сервер найден.\n"));

     CloseHandle(MsFile);

     /* Имя канала сервера. */

     _tcscpy(pPipeName, ServerMsg.msName);

     return TRUE;

    }

    Комментарии по поводу многопоточных моделей

    Для описания методов проектирования многопоточных программ используются такие термины, как пул потоков (thread pool), симметричные потоки (symmetric threads) и асимметричная потоковая организация программ (asymmetric threading), а мы при создании примеров использовали модель "хозяин/рабочий", именованные каналы и другие классические модели организации многопоточного выполнения программ.

    В этом разделе дано краткое объяснение некоторых полезных описательных терминов, которые являются неотъемлемой частью объектно-ориентированной технологии, основанной на разработанной компанией Microsoft модели компонентных объектов (Component Object Model, СОМ; см. [3]): однопоточная модель (single threading), модель апартаментных потоков (apartment model) и модель свободных потоков (free threading). В СОМ эти модели реализуются за счет использования функций Windows, предназначенных для управления потоками и синхронизации их выполнения. Каждая из перечисленных моделей обладает отличными от других характеристиками производительности и предъявляет свои требования к синхронизации.

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

    • Потоковая модель является симметричной, если группа потоков выполняют одну и ту же задачу с использованием одной и той же функции потока. Симметричные потоки используются в программе grepMT (программа 7.1): все потоки выполняют один и тот же код поиска шаблона. Обратите внимание, что эти потоки не образуют пула; каждый из них создается для выполнения определенной задачи и завершается сразу же после того, как задача выполнена. Пул симметричных потоков создается в программе 11.3.

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

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

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

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

    Некоторые программы, например sortMT (программа 7.2), в рамки ни одной из перечисленных моделей точно не укладываются. Вспомните также, что нами уже использовались и другие модели, а именно, модель "хозяин/рабочий", именованные каналы и клиент-серверные модели, применение которых является общепринятым, однако не находит отражения в моделях Microsoft.

    Применение указанных моделей многопоточного программирования оказывается уместным и в главе 12, в которой вводятся внутрипроцессные серверы, и компания Microsoft использует соответствующие термины в некоторой части своей документации. Не забывайте о том, что эти термины определены применительно к СОМ-объектам; предыдущее обсуждение показало, как использовать их в более широком контексте. СОМ — это слишком большой и сложный предмет, чтобы мы могли полностью описать его в данной книге. В списке литературы, приведены некоторые ссылки, которыми вы можете воспользоваться для получения более подробных сведений по этому вопросу.

    Резюме

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

    В следующих главах

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

    Упражнения

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

    11.2. Воспользуйтесь программой JobShell из главы 6 для запуска сервера и нескольких клиентов, причем каждый из клиентов должен создаваться в режиме "отсоединения". Для завершения работы остановите сервер путем посылки управляющего сигнала консоли с помощью команды kill. Имеются ли у вас какие-либо соображения по поводу улучшения логики закрытия программы serverNP, чтобы подключенный серверный поток мог проверять флаг завершения работы, будучи блокированным в ожидании запроса клиента? Подсказка. Создайте считывающий поток, аналогичный потоку соединения.

    11.3. Усовершенствуйте сервер таким образом, чтобы имя его канала задавалось в виде аргумента в командной строке. Организуйте несколько серверных процессов с различными именами каналов, используя программу управления заданиями из главы 6. Убедитесь в том, что одновременно несколько клиентов получают доступ к этой многопроцессной серверной системе.

    11.4. Запустите клиент и сервер на различных системах, чтобы проверить их работу в условиях сети. Измените программу SrvrBcst (программа 11.4) таким образом, чтобы она включала имя компьютера сервера в имя канала. Кроме того, видоизмените имя почтового ящика, используемого в программе 11.4.

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

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

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

    11.8. Программа serverNP спроектирована таким образом, что она выполняется как сервер в течение неопределенного времени, давая клиентам возможность подключаться, получать услуги и разрывать соединение. Очень важно, чтобы при отключении клиента сервер освобождал все соответствующие ресурсы, например, память, дескрипторы файлов или потоков. Если это не делается, то в результате утечки ресурсов системные ресурсы, в конце концов, исчерпаются, что приведет аварийному завершению работы сервера с возможным предшествующим ухудшением показателей производительности. Тщательно проверьте код программы serverNP, чтобы убедиться в отсутствии утечки ресурсов, и в случае необходимости внесите необходимые исправления. (Просьба информировать автора обо всех обнаруженных ошибках, используя указанный в предисловии адрес электронной почты.) Примечание. Утечка ресурсов является весьма распространенным серьезным дефектом многих промышленных систем. Никакие попытки вывода изделия на уровень "промышленных стандартов" нельзя считать успешными, если указанной проблеме не было уделено должного внимания.

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


    Примечания:



    3

    Замечания, сделанные в адрес UNIX, в равной степени относятся также к Linux и некоторым другим системам, поддерживающим POSIX API.



    30

    Как показано в главе 10, в упражнении с семафором (упражнение 10.11), системные службы Windows предоставляют возможность организации взаимодействия между процессами также посредством отображаемых файлов. Дополнительные механизмы IPC включают файлы, сокеты, удаленные вызовы процедур, СОМ и отправку сообщений через почтовые ящики. Сокеты рассматриваются в главе 12.



    31

    Это утверждение нуждается в дополнительных разъяснениях. Для большинства сетевых приложений и высокоуровневых протоколов (http, ftp и так далее) более предпочтительным является интерфейс Windows Sockets API, особенно в тех случаях, когда требуется обеспечить межплатформенное взаимодействие с системами, отличными от Windows, на основе протокола TCP/IP. Многие разработчики предпочитают ограничивать использование именованных каналов лишь случаями IPC в пределах обособленной системы или в сетях Windows.



    32

    Заметьте, что функция TransactNamedPipe не только предлагает более удобный способ использования пары функций WriteFile и ReadFile, но и обеспечивает определенные преимущества в плане производительности. Один из экспериментов продемонстрировал повышение пропускной способности канала в интервале от 57% (небольшие сообщения) до 24% (крупные сообщения).







     


    Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх