Russian version
English version
ОБ АЛЬЯНСЕ | НАШИ УСЛУГИ | КАТАЛОГ РЕШЕНИЙ | ИНФОРМАЦИОННЫЙ ЦЕНТР | СТАНЬТЕ СПОНСОРАМИ SILICON TAIGA | ISDEF | КНИГИ И CD | ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ | УПРАВЛЕНИЕ КАЧЕСТВОМ | РОССИЙСКИЕ ТЕХНОЛОГИИ | НАНОТЕХНОЛОГИИ | ЮРИДИЧЕСКАЯ ПОДДЕРЖКА | АНАЛИТИКА | КАРТА САЙТА | КОНТАКТЫ
 
Программное обеспечение
 
Для зарегистрированных пользователей
 
РАССЫЛКИ НОВОСТЕЙ
IT-Новости
Новости компаний
Российские технологии
Новости ВПК
Нанотехнологии
 
Поиск по статьям
 
RSS-лента
Подписаться
Средства разработки

Что такое 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)


  Рекомендовать страницу   Обсудить материал  [1] Написать редактору  
  Распечатать страницу
 
  Дата публикации: 25.03.2006  

ОБ АЛЬЯНСЕ | НАШИ УСЛУГИ | КАТАЛОГ РЕШЕНИЙ | ИНФОРМАЦИОННЫЙ ЦЕНТР | СТАНЬТЕ СПОНСОРАМИ SILICON TAIGA | ISDEF | КНИГИ И CD | ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ | УПРАВЛЕНИЕ КАЧЕСТВОМ | РОССИЙСКИЕ ТЕХНОЛОГИИ | НАНОТЕХНОЛОГИИ | ЮРИДИЧЕСКАЯ ПОДДЕРЖКА | АНАЛИТИКА | КАРТА САЙТА | КОНТАКТЫ

Дизайн и поддержка: Silicon Taiga   Обратиться по техническим вопросам  
Rambler's Top100 Rambler's Top100