Статьи Королевства Дельфи

       

Сервер удаленного доступа. Часть I


Раздел Подземелье Магов Автор Александр Галилов
дата публикации 05 ноября 1999г.

Введение В этой статье рассматривается проектирование сервера удаленного доступа под Windows 95/98, позволяющего осуществлять подключение клиентов к командному интерпретатору COMMAND.COM. Прошу читателей отнестись с пониманием к возможным ошибкам и неточностям, т.к. я сравнительно недавно занялся данной темой.
Приведенный в статье пример реализован на C++ Builder 1 и Delphi 3. Обратите внимание на то, что автор НЕ ТЕСТИРОВАЛ примеры Win NT. Имеются все основания предполагать некорректность их работы в этой операционной системе. Если хотите - проверьте.

Под WindowsNT прилагаемый проект не работает, проверено. Что, впрочем, автор и не обещает.
Лена Филиппова

Часть 1 Первая часть статьи посвящена вопросу построения внутрисистемного интерфейса сервера удаленного доступа. Здесь под термином "внутрисистемный интерфейс" подразумевается способ взаимодействия нитей (threads) сервера непосредственно с программой, производящей выполнение пользовательских запросов. В данном случае пользовательские запросы поступают во всем известный командный интерпретатор COMMAND.COM (кстати, в Windows95/98 этот файл имеет формат EXE). Для организации взаимодействия с командным интерпретатором я использовал механизм неименованных трубок (anonymous pipes). Данный механизм был выбран по причине отсутствия в Win95 таких средств, как именованные трубки (Named Pipes) в Win NT. Именованные трубки позволяют реализовать рассмотренный здесь пример со значительно меньшими усилиями. Практически отличия Win NT и Win95 таковы, что простой, в принципе, механизм приходится реализовывать весьма нетривиальным способом.
Трубка - это по сути канал передачи данных. Трубка имеет два файловых идентификатора - один для записи данных, другой - для чтения имеющейся в трубке информации. Порядок продвижения байтов в трубке - FIFO (первый поступивший байт первым оказывается на выходе). С помощью API функции CreateProcess мы запускаем командный процессор, но при этом стандартные ввод и вывод перенаправляем на наши трубки. После проделывания всех этих операций мы получаем пару файловых идентификаторов, при помощи которых можем общаться с "Сеансом MS-DOS", однако, имейте ввиду, что этот механизм НЕ ПОЗВОЛЯЕТ получать/принимать данные с не стандартного ввода (STDIN) и вывода (STDOUT), т.е. Вы не сможете работать через трубки с Norton Commander или Far manager, хотя без проблем можете их запустить из командного интерпретатора COMMAND. А вот использовать все команды DOS (даже format d:) - это запросто :). Ничего не мешает работать и с другими программами, имеющими стандартный ввод-вывод, например Турбо ассемблер (TASM).
Теперь немного уточню насчет использования трубок. Конечно, pipes - это не изобретение Микрософт. Когда-то их описание я обнаружил в руководстве системного программирования под Unix, но подозреваю, что и там они появились не впервые. Вот что написано про трубки в Win32 Developer's References:


A pipe is a communication conduit with two ends; a process with a handle to one end can communicate with a process having a handle to the other end.

Рассмотрим более подробно создание трубки. Функция CreatePipe создает трубку, предоставляемую программисту в виде "двух концов" - идентификаторов:



BOOL CreatePipe( PHANDLE hReadPipe, // address of variable for read handle
PHANDLE hWritePipe, // address of variable for write handle
LPSECURITY_ATTRIBUTES lpPipeAttributes, // pointer to security attributes
DWORD nSize // number of bytes reserved for pipe );

hReadPipe и hWritePipe - указатели на идентификаторы. Идентификаторы получают значение при выполнении этой функции.
lpPipeAttributes - если Вы в Win95/98 можете это опустить и указать просто NULL, если вы в Win NT - см. Win32 Developer's References.
nSize- предположительный размер буфера. Система опирается на это значение для вычисления реального размера буфера. Этот параметр может быть равным нулю. В этом случае система выберет размер буфера "по умолчанию", но какой именно - я не знаю.
Если трубка создана, функция возвратит ненулевое значение, в случае ошибки - вернет нуль.

Следует заметить, что для операций с трубками используются функции ReadFile и WriteFile. Причем операция чтения завершается только после того, как будет что-нибудь прочитано из трубки, а операция записи завершается после помещения данных в собственную очередь трубки. Если очередь пуста, ReadFile не завершиться, пока в трубку не поместит данные другой процесс или нить с помощью функции WriteFile. Если очередь заполнена то WriteFile не завершиться до тех пор, пока другой процесс или нить не прочитает данные из трубки с использованием ReadFile. Для общения с командным интерпретатором нам понадобится две трубки - одна для пересылки байтов "туда", другая - для получения информации из досовской сессии. Теперь обратим наше внимание на функцию CreateProcess - несомненно, очень важную и нужную (про функцию WinExec - не говорим).



BOOL CreateProcess( LPCTSTR lpApplicationName,

// pointer to name of executable module
LPTSTR lpCommandLine, // pointer to command line string
LPSECURITY_ATTRIBUTES lpProcessAttributes, // pointer to process security attributes
LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to thread security attributes
BOOL bInheritHandles, // handle inheritance flag
DWORD dwCreationFlags, // creation flags
LPVOID lpEnvironment, // pointer to new environment block
LPCTSTR lpCurrentDirectory, // pointer to current directory name
LPSTARTUPINFO lpStartupInfo, // pointer to STARTUPINFO
LPPROCESS_INFORMATION lpProcessInformation // pointer to PROCESS_INFORMATION );

lpApplicationName и lpCommandLine - указатели на "нуль-терминированные" (PChar) строки с именем запускаемого модуля (напр. "c:\command.com") и с командной строкой, которая будет передана запущенной программе в качестве аргумента. Если lpApplicationName равен нулю, то имя запускаемой программы должно быть первым в строке lpCommandLine и отделено пробелами от аргументов. Для Win NT - см. подробности в Win32 Developer's References.
lpProcessAttributes - в Win95/98 игнорируется, установите его в нуль. Для Win NT - см. подробности в Win32 Developer's References.
lpThreadAttributes - тоже, что и для lpProcessAttributes.
bInheritHandles - показывает как наследуются идентификаторы из вызывающего процесса. Если равно TRUE, то все идентификаторы, которые могут наследоваться, наследуются новым процессом. Унаследованные идентификаторы имеют то же самое значение и привелегии, что и оригинальные.
dwCreationFlags - определяет дополнительные флаги. Могут иметь следующие значения (список не полный, только для нашей задачи):
CREATE_DEFAULT_ERROR_MODE - новый процесс не наследует режим ошибок (error mode) от вызывающего процесса, вместо этого CreateProcess назначает новому процессу режи по умолчанию.
CREATE_NEW_CONSOLE - новый процесс создает новую консоль вместо родительской консоли. Этот флаг нельзя использовать вместе с флагом DETACHED_PROCESS.
DETACHED_PROCESS - для консольных процессов - новый процесс не имеет доступа к консоли родительского процесса.
HIGH_PRIORITY_CLASS - высокий приоритет. Используется в критичных ко времени выполнения процессах.
IDLE_PRIORITY_CLASS - нити процесса выполняются только при простое системы (idle).
NORMAL_PRIORITY_CLASS - приоритет для обыкновенных процессов без специальных задач.
REALTIME_PRIORITY_CLASS - наивысший приоритет. Системные сообщения могут теряться при выполнении потока с этим приоритетом.
lpEnvironment - указатель на среду окружения. Указывает на блок нуль-терминированных строк вида ИМЯ=ЗНАЧЕНИЕ. Сам блок завершается двумя нулевыми байтами для блока строк в формате ANSI и четырьмя нулевыми байтами для блока строк в формате UNICODE (см. подробности в Win32 Developer's References).
lpCurrentDirectory - указатель на нуль-терминированную строку, содержащую текущий каталог. Если указатель равен NULL, то текущий каталог тот же, что и у родительского процесса.
lpStartupInfo - указатель на структуру STARTUPINFO , которая определяет как должно появляться оконо для нового процесса.
lpProcessInformation - указатель на структуру PROCESS_INFORMATION , заполняемую функцией CreateProcess. Эта структура содержит информацию о запущенном процессе.

Теперь более подробно рассмотрим структуры STARTUPINFO и PROCESS_INFORMATION .
STARTUPINFO содержит следующие поля:
DWORD cb - размер структуры в байтах
LPTSTR lpReserved - не используется
LPTSTR lpDesktop - только в Win NT, подробности см. Win32 Developer's References
LPTSTR lpTitle - указатель на нуль-терминированную строку-заголовок консоли для консольных приложений
DWORD dwX - игнорируется, если не установлен флаг STARTF_USEPOSITION в dwFlags. Определяет координаты левого верхнего угла создаваемого окна в пикселях по горизонтали. Подробности см. Win32 Developer's References
DWORD dwY - игнорируется, если не установлен флаг STARTF_USEPOSITION в dwFlags. Определяет координаты левого верхнего угла создаваемого окна в пикселях по вертикали. Подробности см. Win32 Developer's References
DWORD dwXSize- игнорируется, если не установлен флаг STARTF_USESIZE в dwFlags. Определяет размер создаваемого окна в пикселях по горизонтали. Подробности см. Win32 Developer's References
DWORD dwYSize- игнорируется, если не установлен флаг STARTF_USESIZE в dwFlags. Определяет размер создаваемого окна в пикселях по вертикали. Подробности см. Win32 Developer's References
DWORD dwXCountChars- Игнорируется, если не установлен флаг STARTF_USECOUNTCHARS. Для консольных приложений, создавших новую консоль, определяет размер экранного буфера по горизонтали. Для GUI приложений всегда игнорируется
DWORD dwYCountChars- Игнорируется, если не установлен флаг STARTF_USECOUNTCHARS. Для консольных приложений, создавших новую консоль, определяет размер экранного буфера по вертикали. Для GUI приложений всегда игнорируется
DWORD dwFillAttribute- Игнорируется, если не установлен флаг STARTF_USEFILLATTRIBUTE. Определяет начальные атрибуты (цвет текста и фона) для консольных приложений. Игнорируется для GUI-приложений. Может принимать значения: FOREGROUND_BLUE, FOREGROUND_GREEN, FOREGROUND_RED, FOREGROUND_INTENSITY, BACKGROUND_BLUE, BACKGROUND_GREEN, BACKGROUND_RED и BACKGROUND_INTENSITY. Например FOREGROUND_RED | BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE даст красный текст на белом фоне
DWORD dwFlags- Битовое поле, показывающее какие поля структуры STARTUPINFO следует учитывать при создании окна. Могут использоваться любые комбинации значений (список не полный, подробности см. Win32 Developer's References): STARTF_USESHOWWINDOW- Если этот флаг не установлен, то wShowWindow игнорируется
STARTF_USEPOSITION- Если этот флаг не установлен, то dwX и dwY игнорируются
STARTF_USESIZE- Если этот флаг не установлен, то dwXSize и dwYSize игнорируются
STARTF_USECOUNTCHARS- Если этот флаг не установлен, то dwXCountChars и dwYCountChars игнорируются
STARTF_USEFILLATTRIBUTE- Если этот флаг не установлен, то dwFillAttribute игнорируется
STARTF_USESTDHANDLES- Если установлен этот флаг, присвойте идентификаторы стандартного ввода, стандартного вывода и стандартной ошибки полям hStdInput, hStdOutput, и hStdError соответственно. Чтобы это работало, параметр fInheritHandles при вызове CreateProcess должен быть равен TRUE.
WORD wShowWindow- Игнорируется, если не установлен флаг STARTF_USESHOWWINDOW. Если флаг STARTF_USESHOWWINDOW установлен, присвойте этому полю константу, определяющую способ отображения главного окна, например SW_MINIMIZE
WORD cbReserved2- Зарезервировано, должно равняться нулю
LPBYTE lpReserved2- Зарезервировано, должно равняться нулю
HANDLE hStdInput- Игнорируется, если не установлен STARTF_USESTDHANDLES. Если флаг STARTF_USESTDHANDLES установлен, см. пункт про STARTF_USESTDHANDLES
HANDLE hStdOutput- -""-
HANDLE hStdError- -""-
Структура PROCESS_INFORMATION содержит следующие поля: HANDLE hProcess- Дескриптор созданного процесса
HANDLE hThread- Дескриптор первичной нити процесса (primary thread)
DWORD dwProcessId- Идентификатор процесса
DWORD dwThreadId- Идентификатор первичной нити процесса
Заполнять поля структуры PROCESS_INFORMATION не нужно, они заполняются при вызове функции CreateProcess.

Рассмотрим пример использования трубок и функции CreateProcess для обмена данными с COMMAND.COM



var stinfo: TStartupInfo; prinfo: TProcessInformation; ReadPipe,WriteToCommand,ReadFromCommand,WritePipe: integer; // обнуляем поля структур для CreateProcess FillChar(stinfo,sizeof(TStartupInfo),0); FillChar(prinfo,sizeof(TProcessInformation),0); // пытаемся выполнить CreatePipe для первой и второй трубки if (not CreatePipe(ReadPipe,WriteToCommand,nil,PipeSize)) or (not CreatePipe(ReadFromCommand,WritePipe,nil,PipeSize)) then ErrorCode:=1 else begin stinfo.cb:= sizeof(stinfo); stinfo.lpReserved:= nil; stinfo.lpDesktop:= nil; stinfo.lpTitle:= nil; stinfo.dwFlags:= STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW; stinfo.cbReserved2:= 0; stinfo.lpReserved2:= nil; stinfo.hStdInput:= ReadPipe; stinfo.hStdOutput:= WritePipe; stinfo.hStdError:= WritePipe; stinfo.wShowWindow:= SW_HIDE; // запускаем COMMAND.COM CreateProcess(CommandPath,nil,nil,nil,true, CREATE_DEFAULT_ERROR_MODE or NORMAL_PRIORITY_CLASS, nil,CurrentDirectory, stinfo,prinfo) После выполнения этого фрагмента мы имеем в итоге запущенный командный интерпретатор и пару файловых идентификаторов для приема/передачи символьных сообщений между COMMAND.COM и нашей программой. Сейчас самое время решить, каким образом реализовать обмен данными с трубками.

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

Другой вариант - вместо отдельных очередей реализовать механизм Callback - процедур, например, по аналогии с оконными процедурами в Windows. В таком случае нити будут вызывать указанную им процедуру по мере надобности, т.е. если из досовской сессии поступает какая-либо информация, то вызывается процедура обработки входящих символов, а если есть возможность для передачи информации в сеанс MS-DOS - вызывается процедура передачи символов. Причем в последнем случае, возможно, не будет подлежащих передачи данных, тогда вызванная процедура должна сообщить об этом вызывающей нити специальным зарезервированным кодом. Таким образом, существует два основных способа обмена информацией с трубками - синхронный, когда используются Callback- процедуры и асинхронный - с использованием очередей ввода-вывода.



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

Это дает нам возможность из основной нити программы обращаться к нашим очередям в совершенно произвольные моменты времени. Однако, следует учесть некоторые детали, касающиеся разделения нитями ресурсов. Необходимо гарантировать, что при обращении к очереди из основной нити программы мы не прервем операции с тои же очередью, производимые нитями, работающими с трубками. То же самое касается и Callback-процедур. Для избежания этого конфликта используется механизм критических секций. Критическая секция - это объект, поддерживаемый системой и разделяемый между двумя или более нитями. Суть работы критической секции состоит в том, что перед выполнением некоторого участка кода, которое нельзя прерывать, нить вызывает метод Enter, а перед завершением выполнения критического участка - метод Leave. Если во время выполнения критического участка другая нить вызовет метод Enter той же самой критической секции, то выполнение этой нити будет остановлено внутри метода Enter до тех пор, пока "предыдущая" нить, захватившая критический участок не вызовет метод Leave Этот механизм предотвращает доступ нитей к критическому участку, если он уже выполняется. Все нити для конкретного критического участка должны использовать один и тот же разделяемый объект критической секции.

Далее в примере используются функции API ReadFile и WriteFile. Сначала я опишу их, опираясь на Win32 Developer's References.

BOOL ReadFile( HANDLE hFile,// handle of file to read LPVOID lpBuffer,// address of buffer that receives data DWORD nNumberOfBytesToRead,// number of bytes to read LPDWORD lpNumberOfBytesRead,// address of number of bytes read LPOVERLAPPED lpOverlapped// address of structure for data );
hFile- Идентификатор файла для чтения. Должен быть создан с режимом доступа к файлу GENERIC_READ
lpBuffer- Указатель на буфер, в который будут помещены загруженные прочитанные данные
nNumberOfBytesToRead- Задает число байт, которые нужно прочитать
lpNumberOfBytesRead- Указатель на переменную, которая получит значение количества прочитанных байт
lpOverlapped- Указатель на структуру OVERLAPPED. В примере не используется. Для более подробной информации см. Win32 Developer's References

BOOL WriteFile( HANDLE hFile,// handle to file to write to LPVOID lpBuffer,// pointer to data to write to file DWORD nNumberOfBytesToRead,// number of bytes to write LPDWORD lpNumberOfBytesRead,// pointer to number of bytes written LPOVERLAPPED lpOverlapped// pointer to structure needed for overlapped I/O );
hFile- Идентификатор файла для записи. Должен быть создан с режимом доступа к файлу GENERIC_WRITE
lpBuffer- Указатель на буфер, из которого будет записываться информация
nNumberOfBytesToRead- Задает число байт, которые нужно записать
lpNumberOfBytesRead- Указатель на переменную, которая получит значение количества записанных байт
lpOverlapped- Указатель на структуру OVERLAPPED. В примере не используется. Для более подробной информации см. Win32 Developer's References

Обе описанные функции возвращают ненулевое значение в случае успешного завершения



//============================================================================= // Получение символа из очереди. Если символа в очереди нет - возвращает -1, // если символ есть - возвращает код символа (не char, а int !!!) function TFlowFromCommand.Get: integer; begin // входим в критическую секцию cs.Enter; Result:=GetQueue(end_chain,start_chain,chain_data,CHAIN_SIZE_FROM_COMMAND); // покидаем критическую секцию cs.Leave; end; //============================================================================= // устанавливает символ в очередь. Если символ в очередь установлен, // функция возвращает 1, если в очереди нет места - возвращает 0 function TFlowFromCommand.Put(c: char):integer; begin // входим в критическую секцию cs.Enter; Result:=PutQueue(c,end_chain,start_chain,chain_data,CHAIN_SIZE_FROM_COMMAND); // покидаем критическую секцию cs.Leave; end; //============================================================================= // вызов Callback-процедуры для передачи символа в основную нить procedure TFlowFromCommand.VCLExec; begin CallBackReceive(c,self); end; //============================================================================= procedure TFlowFromCommand.Execute; var read: integer; begin // входим в цикл repeat // если попытка чтения символа из трубки вызвала ошибку c:=0; if (not ReadFile(Pipe,c,1,read,nil)) then begin // отдаем оставшуюся часть кванта времени системе Sleep(0); continue; end // иначе делаем попытки поставить символ в очередь пока это наконец не // удасться успешно выполнить или вызываем обработчик, если он установлен else if @CallBackReceive=nil then while(Put(chr(c))=0) do Sleep(0) else begin gcs2.Enter; Synchronize(VCLExec); gcs2.Leave; end; until (Terminated or Suspended); end; //=============================================================================

Теперь у нас есть две очереди и методы Get и Put для доступа к ним. Еще мы имеем возможность "подцепить" Callback-процедуры и работать без использования очередей. Мы можем в любой момент воспользоваться этими методами для осуществления обмена информацией между запущенным командным интерпретатором и основной нитью нашей программы. Также мы можем использовать методы доступа к очередям совершенно произвольно в других нитях процесса.

Пример реализации описанного в статье механизма (C++Builder 1, Delphi 3) Вы можете скачать (16 K)

Александр Галилов


Содержание раздела