Что такое Named Pipes и как с ними бороться.
Именованные каналы (Named Pipes) - это объекты ядра, являющиеся средством межпроцессной коммуникации между сервером канала и одним или несколькими клиентами канала.
Сервером канала называется процесс, создающий именованный канал.
Клиентом канала называется процесс, подключающийся к созданному именованному каналу.
От других аналогичных объектов именованные каналы отличает гарантированная доставка сообщений, возможность асинхронного ввода/вывода, возможность коммуникации между процессами на разных компьютерах в локальной вычислительной сети и относительная простота использования.
По своему назначению они похожи на каналы операционной системы UNIX.
При создании именованного канала ему назначается уникальное имя, определяется максимальное количество одновременных соединений с клиентами канала и режим работы канала (должен ли канал быть односторонним или двусторонним (дуплексным), ведется ли передача пакетами или потоком байтов. При передаче пакетами данные одной операции записи отделяются в буфере канала от данных другой операции записи).
Базовым объектом для реализации именованных каналов служит объект "файл", поэтому для посылки и приема сообщений по именованным каналам используются те же самые функции Windows API, что и при работы с файлами (ReadFile, WriteFile).
Для каждого процесса-клиента канала создается свой экземпляр канала, с собственными буферами и дескрипторами (handles) и с собственным механизмом передачи данных, не влияющим на остальные экземпляры.
Экземпляры одного канала имеют общее имя, указанное при создании, сервер назначает имя канала в соответствии с универсальными правилами именования (Universal Naming Convention, UNC), которые обеспечивают независимый от протоколов способ идентификации каналов в Windows-сетях [1].
Именованные каналы, также как и файлы, наследуют стандартную защиту объектов Windows, что позволяет разграничить участников коммуникации и обеспечить запрет несанкционированного доступа к каналу.
Создание именованных каналов возможно только в NT-системах, подключение к созданному каналу возможно как в NT-системах, так и в Win9x. Кроме того, API работы с каналами в Win9x не поддерживает асинхронных операций ввода/вывода.
Именованные каналы широко используются внутри самой системы. Например, взаимодействие менеджера сервисов с самими сервисами осуществляется через несколько именованных каналов. Для связи с сервисами RunAs, с планировщиком событий и с сервером локальной аутентификации также используются именованные каналы.
Именованные каналы являются наиболее простым способом организации связи между сервисами и пользовательскими приложениями, нуждающимися в такой связи.
Одним из полезных (и довольно уникальных) свойств именованного канала является возможность сервера заменять права своей учетной записи правами учетной записи клиента, соединившегося с каналом. Эта возможность служит преимущественно для ограничения прав сервера при выполнении операций доступа к различным объектам системы.
Для работы с именованными каналами Windows API предоставляет следующие функции:
CreateNamedPipe |
Создание именованного канала или нового экземпляра канала. Функция доступна только серверу. |
ConnectNamedPipe или CreateFile |
Подключение к экземпляру именованного канала со стороны клиента. Функция доступна только клиенту. |
WaitNamedPipe |
Ожидание клиентом появления свободного экземпляра именованного канала для подключения к нему. |
ConnectNamedPipe |
Ожидание сервером подключения клиента к экземпляру именованного канала. |
ReadFile, ReadFileEx |
Чтение данных из именованного канала. Функция доступна как клиенту, так и серверу. |
WriteFile, WriteFileEx |
Запись данных в именованный канал. Функция доступна как клиенту, так и серверу. |
PeekNamedPipe |
Чтение данных из именованного канала без удаления прочитанных данных из буфера канала. Функция доступна как клиенту, так и серверу. |
TransactNamedPipe |
Запись и чтение из именованного канала одной операцией. Функция доступна как клиенту, так и серверу. |
DisconnectNamedPipe |
Отсоединение сервера от экземпляра именованного канала. |
GetNamedPipeInfo |
Получение информации об именованном канале. |
GetNamedPipeHandleState |
Получение текущего режима работы именованного канала и количества созданных экземпляров канала. |
SetNamedPipeHandleState |
Установка текущего режима работы именованного канала. |
CloseHandle |
Закрытие дескриптора экземпляра именованного канала, освобождение связанных с объектом ресурсов. |
FlushFileBuffers |
Сброс данных из кэша в буфер канала. |
Рассмотрим алгоритм работы сервера двустороннего именованного канала, обслуживающего одновременно нескольких клиентов с использованием асинхронного ввода-вывода. Работа данного сервера заключается в пересылке сообщений, полученных от каждого клиента всем клиентам, подключенным к каналу (некий аналог чата).
После инициализации, процесс сервера запускает поток, обслуживающий клиентское соединение. Поток создает именованный канал, используя функцию CreateNamedPipe, указывает максимальное количество экземпляров канала (равное максимально возможному количеству одновременно обслуживаемых клиентов) и режимы работы канала, одновременно с созданием канала создается первый его экземпляр.
Затем поток сервера, обслуживающий этот экземпляр именованного канала, вызывает функцию ConnectNamedPipe для ожидания клиентского подключения.
После того, как клиент подключился к экземпляру именованного канала, сервер инициализирует структуры для обмена данными с клиентом, читает из канала имя подсоединившегося клиента, регистрирует его и запускает копию этого же потока для обслуживания следующего клиентского подключения. Далее, поток обслуживания экземпляра канала входит в цикл чтения канала и рассылки прочитанных сообщений всем подключенным клиентам до тех пор, пока соединение с клиентом не разорвется.
После разрыва соединения поток отключается от экземпляра канала, чтобы освободить ресурсы, выделенные системой под обслуживание этого экземпляра, и завершает свою работу.
Последующие потоки при вызове функции CreateNamedPipe создают дополнительные экземпляры канала, при этом параметры, задающие режимы работы канала функцией игнорируются.
Алгоритм работы клиента следующий: После запуска клиент запрашивает у пользователя имя сервера и свое имя для идентификации, подключается к экземпляру канала вызовом функции CreateFile с указанием имени канала, регистрируется на сервере с помощью посылки в канал своего имени и запускает поток асинхронного чтения сообщений из канала. Основной поток принимает сообщения пользователя и передает их в канал серверу. Поток чтения работает до тех пор, пока не разорвется соединение с сервером, или пока пользователь не завершит клиентский процесс.
Рассмотрим реализацию потока сервера, обслуживающего экземпляр именованного канала: const
MAX_PIPE_INSTANCES = 100;
NAME_SIZE = 25;
LINE_LEN = 80;
{ Описание клиента, подключенного к каналу }
type
WRTHANDLE = packed record
hPipe: THANDLE;
hEvent: THANDLE;
overLap: OVERLAPPED;
Live: LongBool;
Name: array[0..NAME_SIZE] of WideChar;
end;
var
ClientCount: Integer = 0;
Clients: array[1..MAX_PIPE_INSTANCES] of WRTHANDLE;
Wnd: HWND;
procedure ServerProc (Param: Pointer); stdcall;
type
PHWND = ^HWND;
const
IN_BUF_SIZE = 1000;
OUT_BUF_SIZE = 1000;
TIME_OUT = 0;
MAX_READ = 1000*Sizeof(WideChar);
var
WindowHandle: HWND;
Dummy: ULONG;
hPipe: THANDLE;
inBuf: array[0..IN_BUF_SIZE] of WideChar; // Буфер чтения.
bytesRead: DWORD;
bytesTransRd: DWORD;
rc: Boolean;
ClientIndex: Integer;
LastError: DWORD;
ExitLoop: Boolean;
OverLapWrt: OVERLAPPED;
hEventWrt: THANDLE;
OverLapRd: OVERLAPPED;
hEventRd: THANDLE;
pSD: PSECURITY_DESCRIPTOR;
sa: SECURITY_ATTRIBUTES;
begin
WindowHandle := PHWND(Param)^;
inBuf[0] := #0;
ExitLoop := false;
lastError := 0;
// Создать пустой дескриптор безопасности, позволяющий всем писать в канал.
// Предупреждение: Указание nil в качестве последнего параметра функции
// CreateNamedPipe() означает, что все клиенты, подсоединившиеся к каналу
// будут иметь те же атрибуты безопасности, что и пользователь, чья учетная
// запись использовалась при создании серверной стороны канала.
pSD := PSECURITY_DESCRIPTOR(LocalAlloc(LPTR, SECURITY_DESCRIPTOR_MIN_LENGTH));
if not Assigned(pSD) then begin
MessageBoxW(WindowHandle, 'Error allocation memory for SD' ,
'Debug: ServerProc()' , MB_OK);
Exit;
end;
if not InitializeSecurityDescriptor (pSD,
SECURITY_DESCRIPTOR_REVISION) then begin
ShowLastErrorMessage(WindowHandle,
'Debug: ServerProc(): InitializeSecurityDescriptor' );
LocalFree(HLOCAL(pSD));
Exit;
end;
// Добавить NULL ACL к дескриптору безопасности
if not SetSecurityDescriptorDacl(pSD, true, nil, false) then begin
ShowLastErrorMessage(WindowHandle,
'Debug: ServerProc():SetSecurityDescriptorDacl' );
LocalFree(HLOCAL(pSD));
Exit;
end;
sa.nLength := sizeof(sa);
sa.lpSecurityDescriptor := pSD;
sa.bInheritHandle := true;
// Создать серверную часть канала на локальной машине
hPipe := CreateNamedPipeW ( '\\.\PIPE\test' , // Имя канала = 'test'.
PIPE_ACCESS_DUPLEX or // Двусторонний канал
FILE_FLAG_OVERLAPPED, // Асинхронный ввод-вывод
PIPE_WAIT or // Ожидать сообщений
PIPE_READMODE_MESSAGE or // Обмен в канале производится пакетами
PIPE_TYPE_MESSAGE,
MAX_PIPE_INSTANCES, // Максимальное числе экземпляров канала.
OUT_BUF_SIZE*SizeOf(WideChar), // Размеры буферов чтения/записи.
IN_BUF_SIZE*SizeOf(WideChar),
TIME_OUT, // Тайм-аут.
@sa); // Атрибуты безопасности.
if hPipe = INVALID_HANDLE_VALUE then begin
ShowLastErrorMessage(WindowHandle, 'Debug: ServerProc():CreateNamedPipeW' );
Exit;
end;
// Ожидаем подключения клиента.
ConnectNamedPipe(hPipe, nil);
// Создаем событие ожидания завершения записи в канал.
hEventWrt := CreateEventW (nil, true, false, nil);
FillChar(OverLapWrt, sizeof(OVERLAPPED), 0);
OverLapWrt.hEvent := hEventWrt;
// Создаем событие ожидания завершения чтения из канала.
hEventRd := CreateEventW (nil, true, false, nil);
FillChar(OverLapRd, sizeof(OVERLAPPED), 0);
OverLapRd.hEvent := hEventRd;
// Для подсоединившегося клиента заполним его описание
Inc(ClientCount);
ClientIndex := ClientCount;
Clients[ClientIndex].hPipe := hPipe;
Clients[ClientIndex].Live := true;
Clients[ClientIndex].OverLap := OverLapWrt;
Clients[ClientIndex].hEvent := hEventWrt;
// первым сообщением от клиента должно быть его имя
rc := ReadFile (hPipe, inBuf, MAX_READ, bytesRead, @OverLapRd);
if not rc then
lastError := GetLastError;
if lastError = ERROR_IO_PENDING then // Ожидаем завершения ввода-вывода
WaitForSingleObject (hEventRd, INFINITE);
// Запоминаем имя текущего клиента.
lstrcpyw (Clients[ClientIndex].Name, inBuf);
// Запускаем новый поток для ожидания нового клиента
CreateThread (nil, 0, @ServerProc, Param, 0, Dummy); //Поток выполняется сразу
// Посылка пустой строки вызовет обновление списка клиентов и его перерисовку
TellAll( '' );
// Читаем сообщения от этого клиента и передаем его всем подключенным клиентам
repeat
rc := ReadFile (hPipe, inBuf, MAX_READ, bytesRead, @OverLapRd);
// Проверяем три вида ошибки: IO_PENDING (ждем завершения операции)
// При BROKEN_PIPE (клиент или сервер умер), выход из цикла и завершение
// обслуживания клиента.
// При остальных ошибках выдаем сообщение, помечаем факт смерти клиента
// и завершаем его обслуживание, выходя из цикла чтения.
if not rc then begin
lastError := GetLastError;
case lastError of
ERROR_IO_PENDING: // Ожидаем завершения операции
WaitForSingleObject (hEventRd, INFINITE);
ERROR_BROKEN_PIPE: // Экземпляр канала сломался, завершаем обслуживание.
ExitLoop := true;
else
// Выдаем сообщение о нештатной ошибке и завершаем обслуживание клиента.
begin
ShowLastErrorMessage(WindowHandle, 'Debug: ServerProc():ReadFile' );
ExitLoop := true;
end;
end;
end;
if not ExitLoop then begin
GetOverlappedResult (hPipe, OverLapRd, bytesTransRd, false);
// Пересылаем сообщение всем клиентам
if bytesTransRd <> 0 then
TellAll(inBuf)
else
TellAll( '' );
end;
until ExitLoop;
Clients[ClientIndex].Live := false; // При выходе из цикла чтения клиент мертв
CloseHandle (hPipe);
CloseHandle (hEventRd);
CloseHandle (hEventWrt);
DisconnectNamedPipe (hPipe); // Разрушаем экземпляр канала
ExitThread(0); // Завершаем обслуживающий поток.
end;
и процедуры рассылки сообщения клиентам: procedure TellAll (const Message: PWideChar);
var
I: Integer;
BytesWritten: DWORD;
rc: Boolean;
lastError: DWORD;
MsgLength: DWORD;
begin
//передать сообщение всем живым клиентам в списке.
for I:=1 to ClientCount do
if Clients[I].Live then begin
MsgLength := lstrlenW(Message) * SizeOf(WideChar);
rc := WriteFile (Clients[I].hPipe, Message^, MsgLength, bytesWritten,
@Clients[I].overLap);
// Проверка на три вида ошибки: IO_PENDING, NO_DATA и остальные.
// Для случая IO_PENDING ожидать завершения асинхронного ввода-вывода
// на событии клиента, во всех остальных случаях, кроме NO_DATA
// считать клиента умершим и отметить факт его смерти в описании клиента.
if not rc then begin
lastError := GetLastError;
if lastError = ERROR_IO_PENDING then //Ждем завершения операции
WaitForSingleObject (Clients[i].hEvent, INFINITE)
else begin
if lastError <> ERROR_NO_DATA then //Клиент умер по причине lastError
//TODO: Указывать имя покойника
ShowLastErrorMessage (Wnd, 'TellAll:' , lastError);
//TODO: рассылать широковещательное сообщение об уходе?
Clients[i].Live := false;
end;
end;
end;
//Обновить окно с клиентами
InvalidateRect(Wnd, nil, true);
end;
Рассмотрим реализацию клиента, взаимодействующего с сервером именованного канала: function ClientDlgProc (WindowHandle: HWND; Message: UINT;
wParam, lParam: Cardinal): UINT; stdcall;
function TerminateDialog: UINT;
begin
CloseHandle (hPipe);
CloseHandle (hEventWrt);
EndDialog(WindowHandle, 1);
Result := 1;
end;
var
retCode: DWORD;
rc: Boolean;
errorBuf: array[0..LINE_LEN] of WideChar;
outBuf: array[0..OUT_BUF_SIZE] of WideChar;
sendBuf: array[0..OUT_BUF_SIZE] of WideChar;
bytesWritten: DWORD;
Dummy: DWORD;
fileName: array[0..LINE_LEN+NAME_SIZE+sizeof(WideChar)*2] of WideChar;
AFileName: WideString;
lastError: DWORD;
APipeName: string;
begin
hWndClient := WindowHandle;
errorBuf[0] := #0;
outBuf[0] := #0;
sendBuf[0] := #0;
case Message of
WM_COMMAND:
begin
case LOWORD(wParam) of
// После нажатия на кнопку Send получить текст для отправки серверу,
// префиксировать его именем клиента и записать в канал.
IDB_SEND:
begin
GetWindowTextW (GetDlgItem(WindowHandle, IDD_EDITWRITE), outBuf,
MAX_WRITE);
lstrcpyw(sendBuf, ClntName);
lstrcatw(sendBuf, ':' );
lstrcatw(sendBuf, outBuf);
// Записать сообщение в канал
rc := WriteFile (hPipe, sendBuf, MAX_WRITE, bytesWritten,
@OverLapWrt);
if not rc then begin
lastError := GetLastError;
// Если IO_PENDING, ждать завершения асинхронной операции записи
if lastError = ERROR_IO_PENDING then
WaitForSingleObject (hEventWrt, INFINITE);
end;
end;
end;
Result := 0;
end;
WM_INITCLIENT:
// При инициализации создать диалог для получения имен сервера и клиента
// Имя сервера, равное "." означает, что сервер находится на том же
// компьютере, что и клиент. Имя канала должно выглядеть как
// '\\.\PIPE\' для соединения с локальным сервером или
// '\\\PIPE\' для соединения с удаленным сервером
// После соединения с каналом, отослать серверу свое имя для идентификации
// и создать поток для чтения из канала.
begin
DialogBoxW (GetModuleHandle(nil), 'InitDialog' , WindowHandle,
@InitDlgProc);
// Записать имя клиента в заголовок окна
SetWindowTextW (WindowHandle, ClntName);
APipeName:= Format( '\\%s\PIPE\test' , [WideCharToString(ShrName)]);
AFileName:= StringToWideChar(APipeName, FileName, SizeOf(FileName));
// Соединиться с сервером
hPipe := CreateFileW (PWideChar(AFileName),
GENERIC_WRITE or // Доступ на чтение/запись
GENERIC_READ,
FILE_SHARE_READ or // Разделенный доступ
FILE_SHARE_WRITE,
nil,
OPEN_EXISTING, // Канал должен существовать
FILE_FLAG_OVERLAPPED, // Использовать асинхронный ввод/вывод
0);
if hPipe = INVALID_HANDLE_VALUE then begin
retCode := GetLastError;
// Проверить попытку подключения к несуществующему каналу
if (retCode = ERROR_SEEK_ON_DEVICE) or
(retCode = ERROR_FILE_NOT_FOUND) then
MessageBoxW (WindowHandle,
'CANNOT FIND PIPE: Assure Server32 is started, check share name.' ,
'' , MB_OK)
else begin
// Не удалось подключиться по другой причине
MessageBoxW(WindowHandle, StringToWideChar(SysErrorMessage(retCode),
errorBuf, SizeOf(errorBuf)),
'Debug Window:CreateFileW' , MB_OK or MB_ICONINFORMATION
or MB_APPLMODAL);
end;
EndDialog (WindowHandle, 0); // Умереть, если не удалось соединиться
Result := 0;
Exit;
end;
hEventWrt := CreateEvent (nil, true, false, nil);
OverLapWrt.hEvent := hEventWrt;
// Сообщить серверу свое имя
rc := WriteFile (hPipe, ClntName, MAX_WRITE, bytesWritten,
@OverLapWrt);
if not rc then // Если IO_PENDING, ожидать звершения операции
if GetLastError = ERROR_IO_PENDING then
WaitForSingleObject (hEventWrt, INFINITE);
// Создать поток чтения из канала.
CreateThread (nil, 0, @ReadPipe, @hPipe, 0, Dummy);
Result := 0;
end;
WM_INITDIALOG:
// Послать сообщение в очередь, чтобы успел создаться диалог
begin
PostMessageW (WindowHandle, WM_INITCLIENT, 0, 0);
Result := 0;
end;
WM_GO_AWAY: // Завершение работы клиентской части из-за разрыва соединения
// с сервером.
Result := TerminateDialog;
WM_SYSCOMMAND:
if (wParam and $FFF0) = SC_CLOSE then // Если диалог закрывается
// пользователем.
Result := TerminateDialog
else
Result := 0;
else
Result := 0;
end;
end;
И клиентского потока асинхронного чтения данных из канала: procedure ReadPipe (hPipe: PHANDLE); stdcall;
var
inBuf: array[0..IN_BUF_SIZE] of WideChar;
bytesRead: DWORD;
rc: Boolean;
lastError: DWORD;
hEventRd: THANDLE;
OverLapRd: OVERLAPPED;
bytesTrans: DWORD;
begin
inBuf[0] := #0;
hEventRd := CreateEventW (nil, true, false, nil);
FillChar (OverLapRd, sizeof(OVERLAPPED), 0);
OverLapRd.hEvent := hEventRd;
// Бесконечный цикл чтения из канала, до тех пор,пока не разорвется соединение
// Чтение происходит асинхронно, с ожиданием по событию. После того, как сооб-
// щение прочитано, оно помещается в элемент редактирования.
while true do begin
rc := ReadFile (hPipe^, inBuf, IN_BUF_SIZE*sizeof(WideChar), bytesRead,
@OverLapRd);
if not rc then begin
lastError := GetLastError;
// Проверка на три вида ошибки:
// IO_PENDING (ожидать завершения операции), BROKEN_PIPE (выйти из цикла)
// и остальные (выдать сообщение, выйти из цикла и умереть)
if lastError = ERROR_IO_PENDING then begin
WaitForSingleObject (hEventRd, INFINITE);
end else begin
if lastError = ERROR_BROKEN_PIPE then
MessageBoxW (hWndClient,
'The connection to this client has been broken.' , '' , MB_OK)
else
ShowLastErrorMessage(hWndClient,
PAnsiChar( 'Client: Debug():ReadFile' ));
Break;
end;
end;
GetOverlappedResult (hPipe^, OverLapRd, bytesTrans, false);
inBuf[bytesTrans div SizeOf(WideChar)] := #0; // Завершить полученную строку
SendMessageW (GetDlgItem (hWndClient, IDD_EDITREAD), EM_REPLACESEL,
0, LPARAM(@inBuf));
// Перевести курсор на следующую строку в элементе редактирования :)
SendMessageW (GetDlgItem (hWndClient, IDD_EDITREAD), EM_REPLACESEL,
0, LPARAM(PWideChar(CrLf)));
end;
// Если соединение с каналом разорвано, завершить программу
PostMessageW (hWndClient, WM_GO_AWAY, 0,0);
ExitThread(0);
end;
Полный текст примера сервера и клиента, использующих именованный канал для коммуникации можно найти в приложении к статье.
Как уже отмечалось ранее, базовым объектом для реализации именованных каналов является объект "Файл". Это позволяет перечислить созданные в системе именованные каналы программно средствами Native API: открыть корневой каталог файловой системы именованных каналов (\Device\NamedPipe) и перечислить его содержимое. Пример программы перечисления созданных именованных каналов можно найти на сайте http://www.sysinternals.com (PipeList) или в приложении к статье.
Автор выражает признательность Екатерине Субботиной, Дмитрию Заварзину и Александру Жигалину за конструктивную критику в процессе написания статьи.
Литература: 1. Д. Соломон, М. Руссинович: Внутреннее устройство Windows 2000. 2. MSDN Library (http://msdn.microsoft.com)
|